aios-core 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aios-core/cli/commands/migrate/analyze.js +6 -6
- package/.aios-core/cli/commands/migrate/backup.js +2 -2
- package/.aios-core/cli/commands/migrate/execute.js +4 -4
- package/.aios-core/cli/commands/migrate/index.js +5 -5
- package/.aios-core/cli/commands/migrate/rollback.js +6 -6
- package/.aios-core/cli/commands/migrate/update-imports.js +2 -2
- package/.aios-core/cli/commands/migrate/validate.js +2 -2
- package/.aios-core/cli/commands/pro/index.js +52 -0
- package/.aios-core/cli/index.js +1 -1
- package/.aios-core/core/ids/registry-updater.js +29 -3
- package/.aios-core/core/migration/migration-config.yaml +2 -2
- package/.aios-core/core/migration/module-mapping.yaml +2 -2
- package/.aios-core/core/registry/README.md +2 -2
- package/.aios-core/core/synapse/context/context-builder.js +34 -0
- package/.aios-core/core/synapse/diagnostics/collectors/consistency-collector.js +168 -0
- package/.aios-core/core/synapse/diagnostics/collectors/hook-collector.js +129 -0
- package/.aios-core/core/synapse/diagnostics/collectors/manifest-collector.js +82 -0
- package/.aios-core/core/synapse/diagnostics/collectors/output-analyzer.js +134 -0
- package/.aios-core/core/synapse/diagnostics/collectors/pipeline-collector.js +75 -0
- package/.aios-core/core/synapse/diagnostics/collectors/quality-collector.js +252 -0
- package/.aios-core/core/synapse/diagnostics/collectors/relevance-matrix.js +174 -0
- package/.aios-core/core/synapse/diagnostics/collectors/safe-read-json.js +31 -0
- package/.aios-core/core/synapse/diagnostics/collectors/session-collector.js +102 -0
- package/.aios-core/core/synapse/diagnostics/collectors/timing-collector.js +126 -0
- package/.aios-core/core/synapse/diagnostics/collectors/uap-collector.js +83 -0
- package/.aios-core/core/synapse/diagnostics/report-formatter.js +484 -0
- package/.aios-core/core/synapse/diagnostics/synapse-diagnostics.js +95 -0
- package/.aios-core/core/synapse/engine.js +73 -20
- package/.aios-core/core/synapse/runtime/hook-runtime.js +60 -0
- package/.aios-core/core-config.yaml +6 -0
- package/.aios-core/data/agent-config-requirements.yaml +2 -2
- package/.aios-core/data/aios-kb.md +4 -4
- package/.aios-core/data/entity-registry.yaml +5 -5
- package/.aios-core/development/agents/architect.md +10 -10
- package/.aios-core/development/agents/devops.md +93 -50
- package/.aios-core/development/agents/qa.md +94 -40
- package/.aios-core/development/agents/ux-design-expert.md +25 -25
- package/.aios-core/development/scripts/activation-runtime.js +63 -0
- package/.aios-core/development/scripts/generate-greeting.js +9 -8
- package/.aios-core/development/scripts/unified-activation-pipeline.js +102 -2
- package/.aios-core/development/tasks/{db-expansion-pack-integration.md → db-squad-integration.md} +5 -5
- package/.aios-core/development/tasks/{integrate-expansion-pack.md → integrate-squad.md} +2 -2
- package/.aios-core/development/tasks/next.md +3 -3
- package/.aios-core/development/tasks/pr-automation.md +2 -2
- package/.aios-core/development/tasks/publish-npm.md +257 -0
- package/.aios-core/development/tasks/release-management.md +4 -4
- package/.aios-core/development/tasks/setup-github.md +1 -1
- package/.aios-core/development/tasks/squad-creator-migrate.md +1 -1
- package/.aios-core/development/tasks/squad-creator-sync-ide-command.md +14 -14
- package/.aios-core/development/tasks/update-aios.md +1 -1
- package/.aios-core/docs/standards/AIOS-COLOR-PALETTE-QUICK-REFERENCE.md +1 -1
- package/.aios-core/docs/standards/AIOS-COLOR-PALETTE-V2.1.md +5 -5
- package/.aios-core/docs/standards/AIOS-LIVRO-DE-OURO-V2.1-COMPLETE.md +21 -21
- package/.aios-core/docs/standards/AIOS-LIVRO-DE-OURO-V2.2-SUMMARY.md +25 -25
- package/.aios-core/docs/standards/OPEN-SOURCE-VS-SERVICE-DIFFERENCES.md +4 -4
- package/.aios-core/docs/standards/QUALITY-GATES-SPECIFICATION.md +3 -3
- package/.aios-core/docs/standards/STANDARDS-INDEX.md +13 -13
- package/.aios-core/docs/standards/STORY-TEMPLATE-V2-SPECIFICATION.md +1 -1
- package/.aios-core/framework-config.yaml +4 -0
- package/.aios-core/infrastructure/scripts/codex-skills-sync/index.js +182 -0
- package/.aios-core/infrastructure/scripts/codex-skills-sync/validate.js +172 -0
- package/.aios-core/infrastructure/scripts/ide-sync/README.md +14 -0
- package/.aios-core/infrastructure/scripts/ide-sync/index.js +6 -0
- package/.aios-core/infrastructure/scripts/tool-resolver.js +4 -4
- package/.aios-core/infrastructure/scripts/validate-paths.js +142 -0
- package/.aios-core/infrastructure/templates/aios-sync.yaml.template +11 -11
- package/.aios-core/infrastructure/templates/github-workflows/README.md +1 -1
- package/.aios-core/install-manifest.yaml +190 -106
- package/.aios-core/local-config.yaml.template +2 -0
- package/.aios-core/product/README.md +2 -2
- package/.aios-core/product/data/integration-patterns.md +1 -1
- package/.aios-core/product/templates/ide-rules/cline-rules.md +1 -1
- package/.aios-core/product/templates/ide-rules/codex-rules.md +65 -0
- package/.aios-core/product/templates/ide-rules/copilot-rules.md +1 -1
- package/.aios-core/product/templates/ide-rules/roo-rules.md +1 -1
- package/.aios-core/user-guide.md +15 -14
- package/.aios-core/workflow-intelligence/engine/output-formatter.js +1 -1
- package/.claude/hooks/enforce-architecture-first.py +196 -0
- package/.claude/hooks/install-hooks.sh +41 -0
- package/.claude/hooks/mind-clone-governance.py +192 -0
- package/.claude/hooks/pre-commit-mmos-guard.sh +99 -0
- package/.claude/hooks/pre-commit-version-check.sh +156 -0
- package/.claude/hooks/read-protection.py +151 -0
- package/.claude/hooks/slug-validation.py +176 -0
- package/.claude/hooks/sql-governance.py +182 -0
- package/.claude/hooks/synapse-engine.js +9 -20
- package/.claude/hooks/write-path-validation.py +194 -0
- package/README.md +44 -14
- package/bin/aios-init.js +255 -184
- package/bin/aios-minimal.js +2 -2
- package/bin/aios.js +19 -19
- package/package.json +7 -4
- package/packages/aios-pro-cli/bin/aios-pro.js +75 -2
- package/packages/aios-pro-cli/package.json +5 -1
- package/packages/aios-pro-cli/src/recover.js +100 -0
- package/packages/installer/src/__tests__/performance-benchmark.js +382 -0
- package/packages/installer/src/config/ide-configs.js +12 -1
- package/packages/installer/src/config/templates/core-config-template.js +2 -2
- package/packages/installer/src/installer/aios-core-installer.js +2 -2
- package/packages/installer/src/installer/file-hasher.js +97 -0
- package/packages/installer/src/installer/post-install-validator.js +41 -1
- package/packages/installer/src/pro/pro-scaffolder.js +335 -0
- package/packages/installer/src/utils/aios-colors.js +2 -2
- package/packages/installer/src/wizard/feedback.js +1 -1
- package/packages/installer/src/wizard/ide-config-generator.js +2 -2
- package/packages/installer/src/wizard/index.js +58 -19
- package/packages/installer/src/wizard/pro-setup.js +547 -0
- package/packages/installer/src/wizard/questions.js +20 -14
- package/packages/installer/src/wizard/validators.js +1 -1
- package/scripts/package-synapse.js +323 -0
- package/scripts/validate-package-completeness.js +317 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Collector — Verifies SYNAPSE hook registration and file integrity.
|
|
3
|
+
*
|
|
4
|
+
* Checks:
|
|
5
|
+
* - settings.local.json has UserPromptSubmit hook entry
|
|
6
|
+
* - Hook file exists at expected path
|
|
7
|
+
* - Hook file is valid Node.js (can be required)
|
|
8
|
+
*
|
|
9
|
+
* @module core/synapse/diagnostics/collectors/hook-collector
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
* @created Story SYN-13
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collect hook registration and integrity data.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
23
|
+
* @returns {{ checks: Array<{ name: string, status: string, detail: string }> }}
|
|
24
|
+
*/
|
|
25
|
+
function collectHookStatus(projectRoot) {
|
|
26
|
+
const checks = [];
|
|
27
|
+
|
|
28
|
+
// Check 1: settings.local.json has hook entry
|
|
29
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.local.json');
|
|
30
|
+
let hasHookRegistered = false;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (fs.existsSync(settingsPath)) {
|
|
34
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
35
|
+
const hooks = settings.hooks || {};
|
|
36
|
+
const promptHooks = hooks.UserPromptSubmit || hooks.userPromptSubmit || [];
|
|
37
|
+
|
|
38
|
+
hasHookRegistered = promptHooks.some((entry) => {
|
|
39
|
+
// Flat format: { command: "node ..." } or string
|
|
40
|
+
const flatCmd = typeof entry === 'string' ? entry : (entry.command || '');
|
|
41
|
+
if (flatCmd.includes('synapse-engine')) return true;
|
|
42
|
+
// Nested format (Claude Code actual): { hooks: [{ type, command }] }
|
|
43
|
+
if (Array.isArray(entry.hooks)) {
|
|
44
|
+
return entry.hooks.some((h) => {
|
|
45
|
+
const cmd = typeof h === 'string' ? h : (h.command || '');
|
|
46
|
+
return cmd.includes('synapse-engine');
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
checks.push({
|
|
53
|
+
name: 'Hook registered',
|
|
54
|
+
status: hasHookRegistered ? 'PASS' : 'FAIL',
|
|
55
|
+
detail: hasHookRegistered
|
|
56
|
+
? 'settings.local.json has UserPromptSubmit entry for synapse-engine'
|
|
57
|
+
: 'No synapse-engine hook found in settings.local.json',
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
checks.push({
|
|
61
|
+
name: 'Hook registered',
|
|
62
|
+
status: 'FAIL',
|
|
63
|
+
detail: 'settings.local.json not found',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
checks.push({
|
|
68
|
+
name: 'Hook registered',
|
|
69
|
+
status: 'ERROR',
|
|
70
|
+
detail: `Failed to read settings: ${error.message}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check 2: Hook file exists
|
|
75
|
+
const hookPath = path.join(projectRoot, '.claude', 'hooks', 'synapse-engine.js');
|
|
76
|
+
const hookExists = fs.existsSync(hookPath);
|
|
77
|
+
|
|
78
|
+
if (hookExists) {
|
|
79
|
+
try {
|
|
80
|
+
const stat = fs.statSync(hookPath);
|
|
81
|
+
const lineCount = fs.readFileSync(hookPath, 'utf8').split('\n').length;
|
|
82
|
+
checks.push({
|
|
83
|
+
name: 'Hook file exists',
|
|
84
|
+
status: 'PASS',
|
|
85
|
+
detail: `.claude/hooks/synapse-engine.js (${lineCount} lines, ${stat.size} bytes)`,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
checks.push({
|
|
89
|
+
name: 'Hook file exists',
|
|
90
|
+
status: 'ERROR',
|
|
91
|
+
detail: `File exists but cannot be read: ${error.message}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
checks.push({
|
|
96
|
+
name: 'Hook file exists',
|
|
97
|
+
status: 'FAIL',
|
|
98
|
+
detail: '.claude/hooks/synapse-engine.js not found',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check 3: Hook file is valid Node.js
|
|
103
|
+
if (hookExists) {
|
|
104
|
+
try {
|
|
105
|
+
require.resolve(hookPath);
|
|
106
|
+
checks.push({
|
|
107
|
+
name: 'Hook executable',
|
|
108
|
+
status: 'PASS',
|
|
109
|
+
detail: 'node can resolve the hook file',
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
checks.push({
|
|
113
|
+
name: 'Hook executable',
|
|
114
|
+
status: 'FAIL',
|
|
115
|
+
detail: `Cannot resolve: ${error.message}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
checks.push({
|
|
120
|
+
name: 'Hook executable',
|
|
121
|
+
status: 'SKIP',
|
|
122
|
+
detail: 'Hook file does not exist',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { checks };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { collectHookStatus };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest Collector — Validates manifest integrity vs. domain files.
|
|
3
|
+
*
|
|
4
|
+
* Checks:
|
|
5
|
+
* - Every domain in manifest has a corresponding file
|
|
6
|
+
* - Every domain file in .synapse/ has a manifest entry
|
|
7
|
+
*
|
|
8
|
+
* @module core/synapse/diagnostics/collectors/manifest-collector
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
* @created Story SYN-13
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { parseManifest } = require('../../domain/domain-loader');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collect manifest integrity data.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
23
|
+
* @returns {{ entries: Array<{ domain: string, inManifest: string, fileExists: boolean, status: string }>, orphanedFiles: string[] }}
|
|
24
|
+
*/
|
|
25
|
+
function collectManifestIntegrity(projectRoot) {
|
|
26
|
+
const synapsePath = path.join(projectRoot, '.synapse');
|
|
27
|
+
const manifestPath = path.join(synapsePath, 'manifest');
|
|
28
|
+
const entries = [];
|
|
29
|
+
const orphanedFiles = [];
|
|
30
|
+
|
|
31
|
+
// Parse manifest
|
|
32
|
+
const manifest = parseManifest(manifestPath);
|
|
33
|
+
|
|
34
|
+
// Check each domain in manifest has a file
|
|
35
|
+
for (const [_domainName, domainConfig] of Object.entries(manifest.domains)) {
|
|
36
|
+
const fileName = domainConfig.file;
|
|
37
|
+
const domainFilePath = path.join(synapsePath, fileName);
|
|
38
|
+
const fileExists = fs.existsSync(domainFilePath);
|
|
39
|
+
|
|
40
|
+
const stateInfo = domainConfig.state || 'unknown';
|
|
41
|
+
const triggers = [];
|
|
42
|
+
if (domainConfig.agentTrigger) triggers.push(`trigger=${domainConfig.agentTrigger}`);
|
|
43
|
+
if (domainConfig.workflowTrigger) triggers.push(`trigger=${domainConfig.workflowTrigger}`);
|
|
44
|
+
if (domainConfig.alwaysOn) triggers.push('ALWAYS_ON');
|
|
45
|
+
|
|
46
|
+
entries.push({
|
|
47
|
+
domain: fileName,
|
|
48
|
+
inManifest: `${stateInfo}${triggers.length ? ', ' + triggers.join(', ') : ''}`,
|
|
49
|
+
fileExists,
|
|
50
|
+
status: fileExists ? 'PASS' : 'FAIL',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for orphaned domain files (files not in manifest)
|
|
55
|
+
const manifestFileNames = new Set(
|
|
56
|
+
Object.values(manifest.domains).map((d) => d.file),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const allFiles = fs.readdirSync(synapsePath);
|
|
61
|
+
const domainFiles = allFiles.filter((f) => {
|
|
62
|
+
// Skip known non-domain files
|
|
63
|
+
if (f === 'manifest' || f === '.gitignore' || f.startsWith('.')) return false;
|
|
64
|
+
// Skip directories
|
|
65
|
+
const stat = fs.statSync(path.join(synapsePath, f));
|
|
66
|
+
if (stat.isDirectory()) return false;
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
for (const file of domainFiles) {
|
|
71
|
+
if (!manifestFileNames.has(file)) {
|
|
72
|
+
orphanedFiles.push(file);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// .synapse/ not readable
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { entries, orphanedFiles };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { collectManifestIntegrity };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Analyzer — Per-component quality checks for UAP loaders and Hook layers.
|
|
3
|
+
*
|
|
4
|
+
* Examines the actual output quality beyond binary pass/fail by checking:
|
|
5
|
+
* - UAP loaders: Did the loader produce meaningful data?
|
|
6
|
+
* - Hook layers: Did the layer produce rules? How many?
|
|
7
|
+
*
|
|
8
|
+
* @module core/synapse/diagnostics/collectors/output-analyzer
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
* @created Story SYN-14
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { safeReadJson } = require('./safe-read-json');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Expected UAP loader output characteristics.
|
|
20
|
+
* @type {Object.<string, { minFields: number, description: string }>}
|
|
21
|
+
*/
|
|
22
|
+
const UAP_OUTPUT_EXPECTATIONS = {
|
|
23
|
+
agentConfig: { minFields: 3, description: 'Should have name, id, title at minimum' },
|
|
24
|
+
permissionMode: { minFields: 1, description: 'Should have mode value' },
|
|
25
|
+
gitConfig: { minFields: 1, description: 'Should have branch name' },
|
|
26
|
+
sessionContext: { minFields: 1, description: 'Should have session type' },
|
|
27
|
+
projectStatus: { minFields: 1, description: 'Should have status data' },
|
|
28
|
+
memories: { minFields: 0, description: 'Optional — Pro feature' },
|
|
29
|
+
synapseSession: { minFields: 1, description: 'Should have bridge write confirmation' },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Analyze output quality from persisted metrics.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
36
|
+
* @returns {{
|
|
37
|
+
* available: boolean,
|
|
38
|
+
* uapAnalysis: Array<{ name: string, status: string, quality: string, detail: string }>,
|
|
39
|
+
* hookAnalysis: Array<{ name: string, status: string, rules: number, quality: string, detail: string }>,
|
|
40
|
+
* summary: { uapHealthy: number, uapTotal: number, hookHealthy: number, hookTotal: number }
|
|
41
|
+
* }}
|
|
42
|
+
*/
|
|
43
|
+
function collectOutputAnalysis(projectRoot) {
|
|
44
|
+
const metricsDir = path.join(projectRoot, '.synapse', 'metrics');
|
|
45
|
+
|
|
46
|
+
const uapData = safeReadJson(path.join(metricsDir, 'uap-metrics.json'));
|
|
47
|
+
const hookData = safeReadJson(path.join(metricsDir, 'hook-metrics.json'));
|
|
48
|
+
|
|
49
|
+
if (!uapData && !hookData) {
|
|
50
|
+
return {
|
|
51
|
+
available: false,
|
|
52
|
+
uapAnalysis: [],
|
|
53
|
+
hookAnalysis: [],
|
|
54
|
+
summary: { uapHealthy: 0, uapTotal: 0, hookHealthy: 0, hookTotal: 0 },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const uapAnalysis = _analyzeUapOutput(uapData);
|
|
59
|
+
const hookAnalysis = _analyzeHookOutput(hookData);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
available: true,
|
|
63
|
+
uapAnalysis,
|
|
64
|
+
hookAnalysis,
|
|
65
|
+
summary: {
|
|
66
|
+
uapHealthy: uapAnalysis.filter(a => a.quality === 'good').length,
|
|
67
|
+
uapTotal: uapAnalysis.length,
|
|
68
|
+
hookHealthy: hookAnalysis.filter(a => a.quality === 'good').length,
|
|
69
|
+
hookTotal: hookAnalysis.length,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Analyze UAP loader outputs.
|
|
76
|
+
* @param {Object|null} data
|
|
77
|
+
* @returns {Array<{ name: string, status: string, quality: string, detail: string }>}
|
|
78
|
+
*/
|
|
79
|
+
function _analyzeUapOutput(data) {
|
|
80
|
+
if (!data || !data.loaders) return [];
|
|
81
|
+
|
|
82
|
+
return Object.entries(UAP_OUTPUT_EXPECTATIONS).map(([name, _expectation]) => {
|
|
83
|
+
const loader = data.loaders[name];
|
|
84
|
+
if (!loader) {
|
|
85
|
+
const desc = _expectation.description || '';
|
|
86
|
+
const detail = desc.includes('Optional') ? desc : 'Loader not present in metrics';
|
|
87
|
+
return { name, status: 'missing', quality: 'none', detail };
|
|
88
|
+
}
|
|
89
|
+
if (loader.status === 'error') {
|
|
90
|
+
return { name, status: 'error', quality: 'bad', detail: `Error: ${loader.error || 'unknown'}` };
|
|
91
|
+
}
|
|
92
|
+
if (loader.status === 'timeout') {
|
|
93
|
+
return { name, status: 'timeout', quality: 'bad', detail: `Timeout after ${loader.duration || 0}ms` };
|
|
94
|
+
}
|
|
95
|
+
if (loader.status === 'skipped') {
|
|
96
|
+
return { name, status: 'skipped', quality: 'none', detail: 'Loader was skipped' };
|
|
97
|
+
}
|
|
98
|
+
// Status 'ok' — check duration for anomalies
|
|
99
|
+
if (loader.duration > 200) {
|
|
100
|
+
return { name, status: 'ok', quality: 'degraded', detail: `Slow: ${loader.duration}ms (>200ms)` };
|
|
101
|
+
}
|
|
102
|
+
return { name, status: 'ok', quality: 'good', detail: `${loader.duration}ms` };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Analyze Hook layer outputs.
|
|
108
|
+
* @param {Object|null} data
|
|
109
|
+
* @returns {Array<{ name: string, status: string, rules: number, quality: string, detail: string }>}
|
|
110
|
+
*/
|
|
111
|
+
function _analyzeHookOutput(data) {
|
|
112
|
+
if (!data || !data.perLayer) return [];
|
|
113
|
+
|
|
114
|
+
return Object.entries(data.perLayer).map(([name, info]) => {
|
|
115
|
+
const rules = info.rules || 0;
|
|
116
|
+
const status = info.status || 'unknown';
|
|
117
|
+
|
|
118
|
+
if (status === 'error') {
|
|
119
|
+
return { name, status, rules, quality: 'bad', detail: 'Error in layer' };
|
|
120
|
+
}
|
|
121
|
+
if (status === 'skipped') {
|
|
122
|
+
return { name, status, rules, quality: 'none', detail: info.reason || 'Skipped' };
|
|
123
|
+
}
|
|
124
|
+
if (status === 'ok' && rules === 0) {
|
|
125
|
+
return { name, status, rules, quality: 'empty', detail: 'Loaded but produced 0 rules' };
|
|
126
|
+
}
|
|
127
|
+
if (status === 'ok' && rules > 0) {
|
|
128
|
+
return { name, status, rules, quality: 'good', detail: `${rules} rules in ${info.duration || 0}ms` };
|
|
129
|
+
}
|
|
130
|
+
return { name, status, rules, quality: 'unknown', detail: `Status: ${status}` };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { collectOutputAnalysis, UAP_OUTPUT_EXPECTATIONS };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Collector — Simulates engine pipeline for expected vs. actual comparison.
|
|
3
|
+
*
|
|
4
|
+
* Calculates which layers should be active for the current bracket
|
|
5
|
+
* and compares against what was actually loaded.
|
|
6
|
+
*
|
|
7
|
+
* @module core/synapse/diagnostics/collectors/pipeline-collector
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @created Story SYN-13
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
estimateContextPercent,
|
|
16
|
+
calculateBracket,
|
|
17
|
+
getActiveLayers,
|
|
18
|
+
} = require('../../context/context-tracker');
|
|
19
|
+
|
|
20
|
+
const LAYER_NAMES = {
|
|
21
|
+
0: 'L0 Constitution',
|
|
22
|
+
1: 'L1 Global',
|
|
23
|
+
2: 'L2 Agent',
|
|
24
|
+
3: 'L3 Workflow',
|
|
25
|
+
4: 'L4 Task',
|
|
26
|
+
5: 'L5 Squad',
|
|
27
|
+
6: 'L6 Keyword',
|
|
28
|
+
7: 'L7 Star-Command',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Collect pipeline simulation data.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} promptCount - Current prompt count from session
|
|
35
|
+
* @param {string|null} activeAgentId - Active agent ID (for L2 match check)
|
|
36
|
+
* @param {object} manifest - Parsed manifest object
|
|
37
|
+
* @returns {{ bracket: string, contextPercent: number, layers: Array<{ layer: string, expected: string, status: string }> }}
|
|
38
|
+
*/
|
|
39
|
+
function collectPipelineSimulation(promptCount, activeAgentId, manifest) {
|
|
40
|
+
const contextPercent = estimateContextPercent(promptCount || 0);
|
|
41
|
+
const bracket = calculateBracket(contextPercent);
|
|
42
|
+
const layerConfig = getActiveLayers(bracket);
|
|
43
|
+
|
|
44
|
+
const activeLayers = layerConfig ? layerConfig.layers : [];
|
|
45
|
+
const layers = [];
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i <= 7; i++) {
|
|
48
|
+
const layerName = LAYER_NAMES[i] || `L${i}`;
|
|
49
|
+
const isActive = activeLayers.includes(i);
|
|
50
|
+
|
|
51
|
+
let expected = isActive ? 'ACTIVE' : `SKIP (${bracket})`;
|
|
52
|
+
let status = 'PASS';
|
|
53
|
+
|
|
54
|
+
// For L2, check if active agent has a matching domain
|
|
55
|
+
if (i === 2 && isActive && activeAgentId) {
|
|
56
|
+
const domains = manifest?.domains || {};
|
|
57
|
+
const hasMatchingDomain = Object.values(domains).some(
|
|
58
|
+
(d) => d.agentTrigger === activeAgentId,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (hasMatchingDomain) {
|
|
62
|
+
expected = `ACTIVE (agent: ${activeAgentId})`;
|
|
63
|
+
} else {
|
|
64
|
+
expected = `ACTIVE (no domain for ${activeAgentId})`;
|
|
65
|
+
status = 'WARN';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
layers.push({ layer: layerName, expected, status });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { bracket, contextPercent, layers };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { collectPipelineSimulation };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Collector — Scores context relevance for UAP loaders and SYNAPSE layers.
|
|
3
|
+
*
|
|
4
|
+
* Uses weighted rubrics to produce a 0-100 score for each pipeline,
|
|
5
|
+
* then combines them into an overall grade.
|
|
6
|
+
*
|
|
7
|
+
* @module core/synapse/diagnostics/collectors/quality-collector
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @created Story SYN-12
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { safeReadJson } = require('./safe-read-json');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* UAP loader scoring rubric.
|
|
19
|
+
* Weight = max points for this loader. Criticality = human label.
|
|
20
|
+
* @type {Array<{ name: string, weight: number, criticality: string, impact: string }>}
|
|
21
|
+
*/
|
|
22
|
+
const UAP_RUBRIC = [
|
|
23
|
+
{ name: 'agentConfig', weight: 25, criticality: 'CRITICAL', impact: 'Agent identity and commands' },
|
|
24
|
+
{ name: 'memories', weight: 20, criticality: 'HIGH', impact: 'Context from progressive retrieval' },
|
|
25
|
+
{ name: 'sessionContext', weight: 15, criticality: 'MEDIUM', impact: 'Session continuity' },
|
|
26
|
+
{ name: 'projectStatus', weight: 12, criticality: 'MEDIUM', impact: 'Project status context' },
|
|
27
|
+
{ name: 'gitConfig', weight: 8, criticality: 'LOW', impact: 'Branch name context' },
|
|
28
|
+
{ name: 'permissionMode', weight: 5, criticality: 'LOW', impact: 'Permission badge visual' },
|
|
29
|
+
{ name: 'synapseSession', weight: 5, criticality: 'LOW', impact: 'Bridge write for SYNAPSE' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hook layer scoring rubric.
|
|
34
|
+
* @type {Array<{ name: string, weight: number, criticality: string, impact: string }>}
|
|
35
|
+
*/
|
|
36
|
+
const HOOK_RUBRIC = [
|
|
37
|
+
{ name: 'constitution', weight: 25, criticality: 'CRITICAL', impact: 'Framework principles' },
|
|
38
|
+
{ name: 'agent', weight: 25, criticality: 'CRITICAL', impact: 'Agent-specific instructions' },
|
|
39
|
+
{ name: 'global', weight: 20, criticality: 'CRITICAL', impact: 'Project-wide rules' },
|
|
40
|
+
{ name: 'workflow', weight: 10, criticality: 'MEDIUM', impact: 'Workflow context' },
|
|
41
|
+
{ name: 'task', weight: 10, criticality: 'MEDIUM', impact: 'Task instructions' },
|
|
42
|
+
{ name: 'squad', weight: 5, criticality: 'LOW', impact: 'Squad rules' },
|
|
43
|
+
{ name: 'keyword', weight: 3, criticality: 'LOW', impact: 'Keyword triggers' },
|
|
44
|
+
{ name: 'star-command', weight: 2, criticality: 'LOW', impact: 'Command-specific rules' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Layers expected to be active per bracket.
|
|
49
|
+
* Used to adjust max possible score for hook layers.
|
|
50
|
+
* @type {Object.<string, string[]>}
|
|
51
|
+
*/
|
|
52
|
+
/** Maximum staleness threshold in ms (30 minutes).
|
|
53
|
+
* UAP metrics are written once at agent activation, so staleness > 5 min is normal.
|
|
54
|
+
* After this threshold, scores are degraded (50% penalty) rather than zeroed.
|
|
55
|
+
*/
|
|
56
|
+
const MAX_STALENESS_MS = 30 * 60 * 1000;
|
|
57
|
+
|
|
58
|
+
/** Degradation factor applied to stale metrics (50% penalty). */
|
|
59
|
+
const STALE_DEGRADATION_FACTOR = 0.5;
|
|
60
|
+
|
|
61
|
+
const BRACKET_ACTIVE_LAYERS = {
|
|
62
|
+
FRESH: ['constitution', 'global', 'agent', 'star-command'],
|
|
63
|
+
MODERATE: ['constitution', 'global', 'agent', 'workflow', 'task', 'squad', 'keyword', 'star-command'],
|
|
64
|
+
DEPLETED: ['constitution', 'global', 'agent', 'workflow', 'task', 'squad', 'keyword', 'star-command'],
|
|
65
|
+
CRITICAL: ['constitution', 'agent'],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Grade thresholds.
|
|
70
|
+
* @param {number} score - Score 0-100
|
|
71
|
+
* @returns {string} Grade letter
|
|
72
|
+
*/
|
|
73
|
+
function _getGrade(score) {
|
|
74
|
+
if (score >= 90) return 'A';
|
|
75
|
+
if (score >= 75) return 'B';
|
|
76
|
+
if (score >= 60) return 'C';
|
|
77
|
+
if (score >= 45) return 'D';
|
|
78
|
+
return 'F';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Grade label.
|
|
83
|
+
* @param {string} grade
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function _getGradeLabel(grade) {
|
|
87
|
+
const labels = { A: 'EXCELLENT', B: 'GOOD', C: 'ADEQUATE', D: 'POOR', F: 'FAILING' };
|
|
88
|
+
return labels[grade] || 'UNKNOWN';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Collect context quality analysis from persisted metrics files.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
95
|
+
* @returns {{
|
|
96
|
+
* uap: { available: boolean, score: number, maxPossible: number, loaders: Array },
|
|
97
|
+
* hook: { available: boolean, score: number, maxPossible: number, bracket: string, layers: Array },
|
|
98
|
+
* overall: { score: number, grade: string, label: string }
|
|
99
|
+
* }}
|
|
100
|
+
*/
|
|
101
|
+
function collectQualityMetrics(projectRoot) {
|
|
102
|
+
const metricsDir = path.join(projectRoot, '.synapse', 'metrics');
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
|
|
105
|
+
const uapData = safeReadJson(path.join(metricsDir, 'uap-metrics.json'));
|
|
106
|
+
const hookData = safeReadJson(path.join(metricsDir, 'hook-metrics.json'));
|
|
107
|
+
|
|
108
|
+
// SYN-14: Staleness detection — stale data scores 0
|
|
109
|
+
const uapAge = uapData && uapData.timestamp ? now - new Date(uapData.timestamp).getTime() : 0;
|
|
110
|
+
const hookAge = hookData && hookData.timestamp ? now - new Date(hookData.timestamp).getTime() : 0;
|
|
111
|
+
const uapStale = uapAge > MAX_STALENESS_MS;
|
|
112
|
+
const hookStale = hookAge > MAX_STALENESS_MS;
|
|
113
|
+
|
|
114
|
+
// SYN-14 fix: Stale data is degraded (50% penalty) instead of zeroed.
|
|
115
|
+
// UAP writes once at activation; being "stale" after 5 min is normal behavior.
|
|
116
|
+
const uap = _scoreUap(uapData);
|
|
117
|
+
if (uapStale) uap.stale = true;
|
|
118
|
+
const hook = _scoreHook(hookData);
|
|
119
|
+
if (hookStale) hook.stale = true;
|
|
120
|
+
|
|
121
|
+
let uapNormalized = uap.maxPossible > 0 ? (uap.score / uap.maxPossible) * 100 : 0;
|
|
122
|
+
let hookNormalized = hook.maxPossible > 0 ? (hook.score / hook.maxPossible) * 100 : 0;
|
|
123
|
+
if (uapStale) uapNormalized *= STALE_DEGRADATION_FACTOR;
|
|
124
|
+
if (hookStale) hookNormalized *= STALE_DEGRADATION_FACTOR;
|
|
125
|
+
|
|
126
|
+
let overallScore;
|
|
127
|
+
if (uap.available && hook.available) {
|
|
128
|
+
overallScore = Math.round(uapNormalized * 0.4 + hookNormalized * 0.6);
|
|
129
|
+
} else if (uap.available) {
|
|
130
|
+
overallScore = Math.round(uapNormalized);
|
|
131
|
+
} else if (hook.available) {
|
|
132
|
+
overallScore = Math.round(hookNormalized);
|
|
133
|
+
} else {
|
|
134
|
+
overallScore = 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const grade = _getGrade(overallScore);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
uap: {
|
|
141
|
+
available: uap.available,
|
|
142
|
+
score: Math.round(uapNormalized),
|
|
143
|
+
maxPossible: uap.maxPossible,
|
|
144
|
+
loaders: uap.loaders,
|
|
145
|
+
stale: uapStale,
|
|
146
|
+
},
|
|
147
|
+
hook: {
|
|
148
|
+
available: hook.available,
|
|
149
|
+
score: Math.round(hookNormalized),
|
|
150
|
+
maxPossible: hook.maxPossible,
|
|
151
|
+
bracket: hook.bracket,
|
|
152
|
+
layers: hook.layers,
|
|
153
|
+
stale: hookStale,
|
|
154
|
+
},
|
|
155
|
+
overall: {
|
|
156
|
+
score: overallScore,
|
|
157
|
+
grade,
|
|
158
|
+
label: _getGradeLabel(grade),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Score UAP loaders based on rubric.
|
|
165
|
+
* @param {Object|null} data - Parsed uap-metrics.json
|
|
166
|
+
* @returns {{ available: boolean, score: number, maxPossible: number, loaders: Array }}
|
|
167
|
+
*/
|
|
168
|
+
function _scoreUap(data) {
|
|
169
|
+
if (!data || !data.loaders) {
|
|
170
|
+
return { available: false, score: 0, maxPossible: 0, loaders: [] };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const maxPossible = UAP_RUBRIC.reduce((sum, r) => sum + r.weight, 0);
|
|
174
|
+
let totalScore = 0;
|
|
175
|
+
|
|
176
|
+
const loaders = UAP_RUBRIC.map((rubric) => {
|
|
177
|
+
const loader = data.loaders[rubric.name];
|
|
178
|
+
const isOk = loader && loader.status === 'ok';
|
|
179
|
+
const score = isOk ? rubric.weight : 0;
|
|
180
|
+
totalScore += score;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
name: rubric.name,
|
|
184
|
+
score,
|
|
185
|
+
maxScore: rubric.weight,
|
|
186
|
+
criticality: rubric.criticality,
|
|
187
|
+
impact: rubric.impact,
|
|
188
|
+
status: loader ? loader.status : 'missing',
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { available: true, score: totalScore, maxPossible, loaders };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Score Hook layers based on rubric, adjusted by bracket.
|
|
197
|
+
* @param {Object|null} data - Parsed hook-metrics.json
|
|
198
|
+
* @returns {{ available: boolean, score: number, maxPossible: number, bracket: string, layers: Array }}
|
|
199
|
+
*/
|
|
200
|
+
function _scoreHook(data) {
|
|
201
|
+
if (!data || !data.perLayer) {
|
|
202
|
+
return { available: false, score: 0, maxPossible: 0, bracket: 'unknown', layers: [] };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const bracket = data.bracket || 'MODERATE';
|
|
206
|
+
const activeLayers = BRACKET_ACTIVE_LAYERS[bracket] || BRACKET_ACTIVE_LAYERS.MODERATE;
|
|
207
|
+
|
|
208
|
+
let totalScore = 0;
|
|
209
|
+
let maxPossible = 0;
|
|
210
|
+
|
|
211
|
+
const layers = HOOK_RUBRIC.map((rubric) => {
|
|
212
|
+
const isExpected = activeLayers.includes(rubric.name);
|
|
213
|
+
if (!isExpected) {
|
|
214
|
+
return {
|
|
215
|
+
name: rubric.name,
|
|
216
|
+
score: 0,
|
|
217
|
+
maxScore: 0,
|
|
218
|
+
criticality: rubric.criticality,
|
|
219
|
+
impact: rubric.impact,
|
|
220
|
+
status: 'not-expected',
|
|
221
|
+
rules: 0,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
maxPossible += rubric.weight;
|
|
226
|
+
const layer = data.perLayer[rubric.name];
|
|
227
|
+
const isOk = layer && layer.status === 'ok';
|
|
228
|
+
const score = isOk ? rubric.weight : 0;
|
|
229
|
+
totalScore += score;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name: rubric.name,
|
|
233
|
+
score,
|
|
234
|
+
maxScore: rubric.weight,
|
|
235
|
+
criticality: rubric.criticality,
|
|
236
|
+
impact: rubric.impact,
|
|
237
|
+
status: layer ? layer.status : 'missing',
|
|
238
|
+
rules: layer ? (layer.rules || 0) : 0,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return { available: true, score: totalScore, maxPossible, bracket, layers };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
collectQualityMetrics,
|
|
247
|
+
UAP_RUBRIC,
|
|
248
|
+
HOOK_RUBRIC,
|
|
249
|
+
BRACKET_ACTIVE_LAYERS,
|
|
250
|
+
MAX_STALENESS_MS,
|
|
251
|
+
STALE_DEGRADATION_FACTOR,
|
|
252
|
+
};
|