* fix: ContextScout falls back to global core/ when local core/ missing Users who install OAC globally have core standards at ~/.config/opencode/context/core/ but no local core/ directory. ContextScout now does a one-time check on startup: if local core/navigation.md doesn't exist, it checks the global path from paths.json. This resolves the ENOENT errors when agents try to read core/standards/ files that only exist globally. - paths.json: enabled global path (~/.config/opencode/context) - contextscout.md: added global_fallback rule with 2-glob cap - Fallback is ONLY for core/ files, never project-intelligence * docs: add context resolution explanation to README Explains how ContextScout resolves context files: local-first, global fallback only for core/ files, project-intelligence always local. Includes flow diagram, key rules, and common setup table. * fix: resolve installer failures and add comprehensive validation ## Summary Fixes critical installer failures where ContextScout and other components couldn't find their configuration files after installation. Adds comprehensive validation system to prevent future issues. ## Problems Fixed ### 1. Missing Critical Files (13 dead registry references) Files were referenced in registry but didn't exist on disk: - workflows-delegation (split into 3 files) - design-iteration (split into 8 files) - animation-patterns (split into 6 files) - adding-agent (split into 2 files) - adding-skill (split into 3 files) - navigation-design (split into 2 files) - external-libraries (split into 3 files) - claude-code-docs/* (directory removed) ### 2. Missing Infrastructure Files ContextScout failed after installation because 2 critical files weren't in registry: - paths.json - Context root configuration - navigation.md - Root navigation for discovery ### 3. Profile Inconsistencies Profiles referenced components that don't exist: - context:adding-skill → context:adding-skill-basics - context:animation-patterns → animation-basics/components/advanced - context:design-assets (removed - doesn't exist) - context:to-be-consumed/* (removed - directory doesn't exist) - skill:context-manager → subagent:context-manager ## Changes Made ### Registry (registry.json) - Removed 13 dead references - Added 27 new split-file entries - Added 2 critical infrastructure entries - Fixed all 5 profile configurations - Updated ContextScout dependencies - Updated agent-metadata.json dependencies ### New Validation Scripts - scripts/registry/check-dependencies.ts - Comprehensive dependency checker - scripts/tests/test-installer-files.sh - Installer simulation test - scripts/registry/fix-registry.py - Automated registry fixer - scripts/hooks/pre-commit - Git pre-commit hook ### Documentation - docs/maintenance/REGISTRY_FIXES_SUMMARY.md - docs/maintenance/CONTEXTSCOUT_FIX.md - docs/maintenance/REGISTRY_VALIDATION.md ## Validation Results Before: - 13 missing files in registry - 78 orphaned files on disk - 3 missing dependencies - Installer would fail After: - 244 total paths, 0 missing - 262 dependency checks, 0 failures - 0 orphaned dependencies - All profiles consistent - Installer works correctly ## Testing ```bash # Registry validation bun run scripts/registry/validate-registry.ts # ✓ All 244 paths valid # Dependency checking bun run scripts/registry/check-dependencies.ts # ✓ All 262 checks passed # Installer test ./scripts/tests/test-installer-files.sh --local --profile=essential # ✓ All files accessible ``` ## Usage ### Run validation before commits: ```bash bun run scripts/registry/check-dependencies.ts ``` ### Install pre-commit hook: ```bash cp scripts/hooks/pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit ``` ### Test installer: ```bash ./scripts/tests/test-installer-files.sh --local --profile=essential ``` ## Impact This fix ensures: - ✓ Installer downloads all required files - ✓ ContextScout works immediately after install - ✓ No broken dependencies - ✓ No missing profile components - ✓ Future issues caught by validation ## Breaking Changes None - all changes are additive or fix broken references. ## Related - Fixes: ContextScout failing after installation - Prevents: Missing file errors during install - Adds: Comprehensive validation system * fix: resolve shellcheck warnings in test scripts - Remove unused REPO_URL variable from test-installer-files.sh - Add shellcheck disable comments for cleanup functions used via trap - Fixes CI failures in ShellCheck Analysis workflow * fix: resolve shellcheck errors across all scripts Fixed arithmetic expansion issues and code quality warnings: - Changed ((var++)) to POSIX-compliant 1 syntax - Fixed unquoted variables and grep pipe issues (SC2002, SC2143) - Added shellcheck disable comments for intentional patterns - Fixed variable initialization and return statement placement Scripts fixed: - scripts/tests/*.sh (5 files) - scripts/registry/*.sh (4 files) - scripts/validation/*.sh (2 files) - scripts/*.sh (2 files) - evals/framework/scripts/*.sh (2 files) All scripts now pass shellcheck --severity=warning * chore: trigger CI shellcheck validation
420 lines
13 KiB
TypeScript
420 lines
13 KiB
TypeScript
#!/usr/bin/env bun
|
||
/**
|
||
* Comprehensive Registry Dependency Checker
|
||
*
|
||
* This script validates that:
|
||
* 1. All component dependencies exist in the registry
|
||
* 2. All profile components exist in the registry
|
||
* 3. Critical infrastructure files are included
|
||
* 4. No orphaned references exist
|
||
*
|
||
* Exit codes:
|
||
* 0 = All checks passed
|
||
* 1 = Missing dependencies found
|
||
* 2 = Critical files missing
|
||
* 3 = Configuration errors
|
||
*/
|
||
|
||
import { existsSync, readFileSync } from 'fs';
|
||
import { join, dirname } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
// Configuration
|
||
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const REGISTRY_FILE = join(REPO_ROOT, 'registry.json');
|
||
const PROFILES_DIR = join(REPO_ROOT, '.opencode/profiles');
|
||
|
||
// Colors
|
||
const colors = {
|
||
red: '\x1b[0;31m',
|
||
green: '\x1b[0;32m',
|
||
yellow: '\x1b[1;33m',
|
||
blue: '\x1b[0;34m',
|
||
cyan: '\x1b[0;36m',
|
||
bold: '\x1b[1m',
|
||
reset: '\x1b[0m',
|
||
};
|
||
|
||
// Counters
|
||
let TOTAL_CHECKS = 0;
|
||
let PASSED_CHECKS = 0;
|
||
let FAILED_CHECKS = 0;
|
||
|
||
// Issues
|
||
const MISSING_PROFILE_COMPONENTS: string[] = [];
|
||
const ORPHANED_DEPENDENCIES: string[] = [];
|
||
const CRITICAL_FILES_MISSING: string[] = [];
|
||
|
||
// CLI flags
|
||
let VERBOSE = false;
|
||
|
||
// Types
|
||
interface Component {
|
||
id: string;
|
||
name: string;
|
||
type: string;
|
||
path: string;
|
||
dependencies?: string[];
|
||
}
|
||
|
||
interface Registry {
|
||
version: string;
|
||
schema_version: string;
|
||
repository: string;
|
||
categories: Record<string, string>;
|
||
components: {
|
||
agents?: Component[];
|
||
subagents?: Component[];
|
||
commands?: Component[];
|
||
tools?: Component[];
|
||
plugins?: Component[];
|
||
contexts?: Component[];
|
||
config?: Component[];
|
||
skills?: Component[];
|
||
};
|
||
profiles?: Record<string, {
|
||
name: string;
|
||
description: string;
|
||
components: string[];
|
||
}>;
|
||
}
|
||
|
||
// Critical files that must be in registry
|
||
const CRITICAL_FILES = [
|
||
{ id: 'root-navigation', path: '.opencode/context/navigation.md', description: 'Root navigation file - ContextScout starts here' },
|
||
{ id: 'context-paths-config', path: '.opencode/context/core/config/paths.json', description: 'Context paths configuration - loaded via @ reference' },
|
||
{ id: 'context-system', path: '.opencode/context/core/context-system.md', description: 'Context system guide' },
|
||
];
|
||
|
||
// Print functions
|
||
function printHeader(): void {
|
||
console.log(`${colors.cyan}${colors.bold}`);
|
||
console.log('╔════════════════════════════════════════════════════════════════╗');
|
||
console.log('║ ║');
|
||
console.log('║ Comprehensive Dependency Checker v1.0.0 ║');
|
||
console.log('║ ║');
|
||
console.log('╚════════════════════════════════════════════════════════════════╝');
|
||
console.log(`${colors.reset}`);
|
||
}
|
||
|
||
function printSuccess(msg: string): void {
|
||
console.log(`${colors.green}✓${colors.reset} ${msg}`);
|
||
}
|
||
|
||
function printError(msg: string): void {
|
||
console.log(`${colors.red}✗${colors.reset} ${msg}`);
|
||
}
|
||
|
||
function printWarning(msg: string): void {
|
||
console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
|
||
}
|
||
|
||
function printInfo(msg: string): void {
|
||
console.log(`${colors.blue}ℹ${colors.reset} ${msg}`);
|
||
}
|
||
|
||
function printStep(msg: string): void {
|
||
console.log(`\n${colors.bold}${msg}${colors.reset}`);
|
||
}
|
||
|
||
function usage(): void {
|
||
console.log('Usage: bun run scripts/registry/check-dependencies.ts [OPTIONS]');
|
||
console.log('');
|
||
console.log('Options:');
|
||
console.log(' -v, --verbose Show detailed validation output');
|
||
console.log(' -h, --help Show this help message');
|
||
console.log('');
|
||
console.log('Exit codes:');
|
||
console.log(' 0 = All checks passed');
|
||
console.log(' 1 = Missing dependencies found');
|
||
console.log(' 2 = Critical files missing');
|
||
process.exit(0);
|
||
}
|
||
|
||
// Load registry
|
||
function loadRegistry(): Registry {
|
||
if (!existsSync(REGISTRY_FILE)) {
|
||
printError(`Registry file not found: ${REGISTRY_FILE}`);
|
||
process.exit(3);
|
||
}
|
||
|
||
try {
|
||
const content = readFileSync(REGISTRY_FILE, 'utf-8');
|
||
const registry = JSON.parse(content) as Registry;
|
||
printSuccess('Registry loaded successfully');
|
||
return registry;
|
||
} catch (error) {
|
||
printError('Failed to parse registry.json');
|
||
process.exit(3);
|
||
}
|
||
}
|
||
|
||
// Load profiles
|
||
function loadProfiles(): Record<string, any> {
|
||
const profiles: Record<string, any> = {};
|
||
|
||
const profileFiles = ['essential', 'developer', 'business', 'full', 'advanced'];
|
||
|
||
for (const profileName of profileFiles) {
|
||
const profilePath = join(PROFILES_DIR, profileName, 'profile.json');
|
||
if (existsSync(profilePath)) {
|
||
try {
|
||
const content = readFileSync(profilePath, 'utf-8');
|
||
profiles[profileName] = JSON.parse(content);
|
||
} catch (error) {
|
||
printWarning(`Failed to load profile: ${profileName}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
const registry = loadRegistry();
|
||
if (registry.profiles) {
|
||
Object.assign(profiles, registry.profiles);
|
||
}
|
||
|
||
printSuccess(`Loaded ${Object.keys(profiles).length} profiles`);
|
||
return profiles;
|
||
}
|
||
|
||
// Build component lookup map
|
||
function buildComponentMap(registry: Registry): Map<string, Component> {
|
||
const map = new Map<string, Component>();
|
||
|
||
const categories = ['agents', 'subagents', 'commands', 'tools', 'plugins', 'contexts', 'config', 'skills'];
|
||
|
||
for (const category of categories) {
|
||
const components = registry.components[category as keyof Registry['components']];
|
||
if (components) {
|
||
for (const component of components) {
|
||
const key = `${category.replace(/s$/, '')}:${component.id}`;
|
||
map.set(key, component);
|
||
map.set(component.id, component);
|
||
}
|
||
}
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
// Check if component exists
|
||
function componentExists(dep: string, componentMap: Map<string, Component>): boolean {
|
||
if (dep.includes('*')) return true; // Skip wildcards
|
||
|
||
const match = dep.match(/^([^:]+):(.+)$/);
|
||
if (!match) return false;
|
||
|
||
const [, type, id] = match;
|
||
const fullKey = `${type}:${id}`;
|
||
return componentMap.has(fullKey) || componentMap.has(id);
|
||
}
|
||
|
||
// Check critical files
|
||
function checkCriticalFiles(registry: Registry): void {
|
||
printStep('Checking Critical Infrastructure Files...');
|
||
|
||
const allPaths = new Set<string>();
|
||
const allIds = new Set<string>();
|
||
|
||
for (const category of Object.keys(registry.components)) {
|
||
const components = registry.components[category as keyof Registry['components']];
|
||
if (components) {
|
||
for (const c of components) {
|
||
allPaths.add(c.path);
|
||
allIds.add(c.id);
|
||
}
|
||
}
|
||
}
|
||
|
||
for (const critical of CRITICAL_FILES) {
|
||
TOTAL_CHECKS++;
|
||
|
||
const hasPath = allPaths.has(critical.path);
|
||
const hasId = allIds.has(critical.id);
|
||
|
||
if (hasPath && hasId) {
|
||
PASSED_CHECKS++;
|
||
printSuccess(`${critical.description}`);
|
||
} else {
|
||
FAILED_CHECKS++;
|
||
CRITICAL_FILES_MISSING.push(critical.id);
|
||
printError(`MISSING CRITICAL FILE: ${critical.description}`);
|
||
printInfo(` Expected ID: ${critical.id}`);
|
||
printInfo(` Expected path: ${critical.path}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Validate profiles
|
||
function validateProfiles(profiles: Record<string, any>, componentMap: Map<string, Component>): void {
|
||
printStep('Validating Profile Components...');
|
||
|
||
for (const [profileName, profile] of Object.entries(profiles)) {
|
||
if (VERBOSE) {
|
||
printInfo(`Checking profile: ${profileName}`);
|
||
}
|
||
|
||
const components = profile.components || [];
|
||
|
||
for (const componentRef of components) {
|
||
TOTAL_CHECKS++;
|
||
|
||
if (componentRef.includes('*')) {
|
||
PASSED_CHECKS++;
|
||
continue;
|
||
}
|
||
|
||
if (componentExists(componentRef, componentMap)) {
|
||
PASSED_CHECKS++;
|
||
if (VERBOSE) {
|
||
printSuccess(` ${componentRef}`);
|
||
}
|
||
} else {
|
||
FAILED_CHECKS++;
|
||
MISSING_PROFILE_COMPONENTS.push(`${profileName}|${componentRef}`);
|
||
printError(`Profile "${profileName}" references missing: ${componentRef}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Validate component dependencies
|
||
function validateComponentDependencies(registry: Registry, componentMap: Map<string, Component>): void {
|
||
printStep('Validating Component Dependencies...');
|
||
|
||
const categories = Object.keys(registry.components) as Array<keyof Registry['components']>;
|
||
|
||
for (const category of categories) {
|
||
const components = registry.components[category];
|
||
if (!components) continue;
|
||
|
||
for (const component of components) {
|
||
if (!component.dependencies || component.dependencies.length === 0) continue;
|
||
|
||
for (const dep of component.dependencies) {
|
||
if (!dep) continue;
|
||
|
||
TOTAL_CHECKS++;
|
||
|
||
if (componentExists(dep, componentMap)) {
|
||
PASSED_CHECKS++;
|
||
if (VERBOSE) {
|
||
printSuccess(`${component.name} → ${dep}`);
|
||
}
|
||
} else {
|
||
FAILED_CHECKS++;
|
||
ORPHANED_DEPENDENCIES.push(`${category}|${component.id}|${dep}`);
|
||
printError(`${component.name} (${category}) → missing: ${dep}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Print summary
|
||
function printSummary(): number {
|
||
console.log('');
|
||
console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
|
||
console.log(`${colors.bold}Dependency Check Summary${colors.reset}`);
|
||
console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
|
||
console.log('');
|
||
console.log(`Total checks: ${colors.cyan}${TOTAL_CHECKS}${colors.reset}`);
|
||
console.log(`Passed: ${colors.green}${PASSED_CHECKS}${colors.reset}`);
|
||
console.log(`Failed: ${colors.red}${FAILED_CHECKS}${colors.reset}`);
|
||
console.log('');
|
||
|
||
let exitCode = 0;
|
||
|
||
if (CRITICAL_FILES_MISSING.length > 0) {
|
||
exitCode = 2;
|
||
printError(`CRITICAL: ${CRITICAL_FILES_MISSING.length} infrastructure file(s) missing!`);
|
||
console.log('');
|
||
console.log('Missing critical files:');
|
||
for (const id of CRITICAL_FILES_MISSING) {
|
||
const critical = CRITICAL_FILES.find(c => c.id === id);
|
||
console.log(` - ${id}: ${critical?.description}`);
|
||
console.log(` Path: ${critical?.path}`);
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
if (MISSING_PROFILE_COMPONENTS.length > 0) {
|
||
exitCode = exitCode || 1;
|
||
printError(`Found ${MISSING_PROFILE_COMPONENTS.length} missing profile component(s)`);
|
||
console.log('');
|
||
for (const entry of MISSING_PROFILE_COMPONENTS) {
|
||
const [profile, component] = entry.split('|');
|
||
console.log(` - Profile "${profile}" → ${component}`);
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
if (ORPHANED_DEPENDENCIES.length > 0) {
|
||
exitCode = exitCode || 1;
|
||
printError(`Found ${ORPHANED_DEPENDENCIES.length} orphaned dependency(ies)`);
|
||
console.log('');
|
||
for (const entry of ORPHANED_DEPENDENCIES) {
|
||
const [category, id, dep] = entry.split('|');
|
||
console.log(` - ${id} (${category}) → ${dep}`);
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
if (exitCode === 0) {
|
||
printSuccess('All dependency checks passed!');
|
||
console.log('');
|
||
printInfo('No issues found. Registry is consistent.');
|
||
} else {
|
||
console.log(`${colors.yellow}Action required:${colors.reset}`);
|
||
console.log(' 1. Add missing components to registry.json');
|
||
console.log(' 2. Update profile references to use valid component IDs');
|
||
console.log(' 3. Ensure all component dependencies exist');
|
||
console.log('');
|
||
console.log(`${colors.yellow}Prevention:${colors.reset}`);
|
||
console.log(' Run this check before committing changes:');
|
||
console.log(' bun run scripts/registry/check-dependencies.ts');
|
||
}
|
||
|
||
return exitCode;
|
||
}
|
||
|
||
// Main
|
||
function main(): void {
|
||
printHeader();
|
||
|
||
// Parse arguments
|
||
const args = process.argv.slice(2);
|
||
for (const arg of args) {
|
||
switch (arg) {
|
||
case '-v':
|
||
case '--verbose':
|
||
VERBOSE = true;
|
||
break;
|
||
case '-h':
|
||
case '--help':
|
||
usage();
|
||
break;
|
||
default:
|
||
console.log(`Unknown option: ${arg}`);
|
||
usage();
|
||
}
|
||
}
|
||
|
||
// Load data
|
||
const registry = loadRegistry();
|
||
const profiles = loadProfiles();
|
||
const componentMap = buildComponentMap(registry);
|
||
|
||
printInfo(`Loaded ${componentMap.size} components`);
|
||
console.log('');
|
||
|
||
// Run checks
|
||
checkCriticalFiles(registry);
|
||
validateProfiles(profiles, componentMap);
|
||
validateComponentDependencies(registry, componentMap);
|
||
|
||
// Print summary and exit
|
||
const exitCode = printSummary();
|
||
process.exit(exitCode);
|
||
}
|
||
|
||
main();
|