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.
Files changed (111) hide show
  1. package/.aios-core/cli/commands/migrate/analyze.js +6 -6
  2. package/.aios-core/cli/commands/migrate/backup.js +2 -2
  3. package/.aios-core/cli/commands/migrate/execute.js +4 -4
  4. package/.aios-core/cli/commands/migrate/index.js +5 -5
  5. package/.aios-core/cli/commands/migrate/rollback.js +6 -6
  6. package/.aios-core/cli/commands/migrate/update-imports.js +2 -2
  7. package/.aios-core/cli/commands/migrate/validate.js +2 -2
  8. package/.aios-core/cli/commands/pro/index.js +52 -0
  9. package/.aios-core/cli/index.js +1 -1
  10. package/.aios-core/core/ids/registry-updater.js +29 -3
  11. package/.aios-core/core/migration/migration-config.yaml +2 -2
  12. package/.aios-core/core/migration/module-mapping.yaml +2 -2
  13. package/.aios-core/core/registry/README.md +2 -2
  14. package/.aios-core/core/synapse/context/context-builder.js +34 -0
  15. package/.aios-core/core/synapse/diagnostics/collectors/consistency-collector.js +168 -0
  16. package/.aios-core/core/synapse/diagnostics/collectors/hook-collector.js +129 -0
  17. package/.aios-core/core/synapse/diagnostics/collectors/manifest-collector.js +82 -0
  18. package/.aios-core/core/synapse/diagnostics/collectors/output-analyzer.js +134 -0
  19. package/.aios-core/core/synapse/diagnostics/collectors/pipeline-collector.js +75 -0
  20. package/.aios-core/core/synapse/diagnostics/collectors/quality-collector.js +252 -0
  21. package/.aios-core/core/synapse/diagnostics/collectors/relevance-matrix.js +174 -0
  22. package/.aios-core/core/synapse/diagnostics/collectors/safe-read-json.js +31 -0
  23. package/.aios-core/core/synapse/diagnostics/collectors/session-collector.js +102 -0
  24. package/.aios-core/core/synapse/diagnostics/collectors/timing-collector.js +126 -0
  25. package/.aios-core/core/synapse/diagnostics/collectors/uap-collector.js +83 -0
  26. package/.aios-core/core/synapse/diagnostics/report-formatter.js +484 -0
  27. package/.aios-core/core/synapse/diagnostics/synapse-diagnostics.js +95 -0
  28. package/.aios-core/core/synapse/engine.js +73 -20
  29. package/.aios-core/core/synapse/runtime/hook-runtime.js +60 -0
  30. package/.aios-core/core-config.yaml +6 -0
  31. package/.aios-core/data/agent-config-requirements.yaml +2 -2
  32. package/.aios-core/data/aios-kb.md +4 -4
  33. package/.aios-core/data/entity-registry.yaml +5 -5
  34. package/.aios-core/development/agents/architect.md +10 -10
  35. package/.aios-core/development/agents/devops.md +93 -50
  36. package/.aios-core/development/agents/qa.md +94 -40
  37. package/.aios-core/development/agents/ux-design-expert.md +25 -25
  38. package/.aios-core/development/scripts/activation-runtime.js +63 -0
  39. package/.aios-core/development/scripts/generate-greeting.js +9 -8
  40. package/.aios-core/development/scripts/unified-activation-pipeline.js +102 -2
  41. package/.aios-core/development/tasks/{db-expansion-pack-integration.md → db-squad-integration.md} +5 -5
  42. package/.aios-core/development/tasks/{integrate-expansion-pack.md → integrate-squad.md} +2 -2
  43. package/.aios-core/development/tasks/next.md +3 -3
  44. package/.aios-core/development/tasks/pr-automation.md +2 -2
  45. package/.aios-core/development/tasks/publish-npm.md +257 -0
  46. package/.aios-core/development/tasks/release-management.md +4 -4
  47. package/.aios-core/development/tasks/setup-github.md +1 -1
  48. package/.aios-core/development/tasks/squad-creator-migrate.md +1 -1
  49. package/.aios-core/development/tasks/squad-creator-sync-ide-command.md +14 -14
  50. package/.aios-core/development/tasks/update-aios.md +1 -1
  51. package/.aios-core/docs/standards/AIOS-COLOR-PALETTE-QUICK-REFERENCE.md +1 -1
  52. package/.aios-core/docs/standards/AIOS-COLOR-PALETTE-V2.1.md +5 -5
  53. package/.aios-core/docs/standards/AIOS-LIVRO-DE-OURO-V2.1-COMPLETE.md +21 -21
  54. package/.aios-core/docs/standards/AIOS-LIVRO-DE-OURO-V2.2-SUMMARY.md +25 -25
  55. package/.aios-core/docs/standards/OPEN-SOURCE-VS-SERVICE-DIFFERENCES.md +4 -4
  56. package/.aios-core/docs/standards/QUALITY-GATES-SPECIFICATION.md +3 -3
  57. package/.aios-core/docs/standards/STANDARDS-INDEX.md +13 -13
  58. package/.aios-core/docs/standards/STORY-TEMPLATE-V2-SPECIFICATION.md +1 -1
  59. package/.aios-core/framework-config.yaml +4 -0
  60. package/.aios-core/infrastructure/scripts/codex-skills-sync/index.js +182 -0
  61. package/.aios-core/infrastructure/scripts/codex-skills-sync/validate.js +172 -0
  62. package/.aios-core/infrastructure/scripts/ide-sync/README.md +14 -0
  63. package/.aios-core/infrastructure/scripts/ide-sync/index.js +6 -0
  64. package/.aios-core/infrastructure/scripts/tool-resolver.js +4 -4
  65. package/.aios-core/infrastructure/scripts/validate-paths.js +142 -0
  66. package/.aios-core/infrastructure/templates/aios-sync.yaml.template +11 -11
  67. package/.aios-core/infrastructure/templates/github-workflows/README.md +1 -1
  68. package/.aios-core/install-manifest.yaml +190 -106
  69. package/.aios-core/local-config.yaml.template +2 -0
  70. package/.aios-core/product/README.md +2 -2
  71. package/.aios-core/product/data/integration-patterns.md +1 -1
  72. package/.aios-core/product/templates/ide-rules/cline-rules.md +1 -1
  73. package/.aios-core/product/templates/ide-rules/codex-rules.md +65 -0
  74. package/.aios-core/product/templates/ide-rules/copilot-rules.md +1 -1
  75. package/.aios-core/product/templates/ide-rules/roo-rules.md +1 -1
  76. package/.aios-core/user-guide.md +15 -14
  77. package/.aios-core/workflow-intelligence/engine/output-formatter.js +1 -1
  78. package/.claude/hooks/enforce-architecture-first.py +196 -0
  79. package/.claude/hooks/install-hooks.sh +41 -0
  80. package/.claude/hooks/mind-clone-governance.py +192 -0
  81. package/.claude/hooks/pre-commit-mmos-guard.sh +99 -0
  82. package/.claude/hooks/pre-commit-version-check.sh +156 -0
  83. package/.claude/hooks/read-protection.py +151 -0
  84. package/.claude/hooks/slug-validation.py +176 -0
  85. package/.claude/hooks/sql-governance.py +182 -0
  86. package/.claude/hooks/synapse-engine.js +9 -20
  87. package/.claude/hooks/write-path-validation.py +194 -0
  88. package/README.md +44 -14
  89. package/bin/aios-init.js +255 -184
  90. package/bin/aios-minimal.js +2 -2
  91. package/bin/aios.js +19 -19
  92. package/package.json +7 -4
  93. package/packages/aios-pro-cli/bin/aios-pro.js +75 -2
  94. package/packages/aios-pro-cli/package.json +5 -1
  95. package/packages/aios-pro-cli/src/recover.js +100 -0
  96. package/packages/installer/src/__tests__/performance-benchmark.js +382 -0
  97. package/packages/installer/src/config/ide-configs.js +12 -1
  98. package/packages/installer/src/config/templates/core-config-template.js +2 -2
  99. package/packages/installer/src/installer/aios-core-installer.js +2 -2
  100. package/packages/installer/src/installer/file-hasher.js +97 -0
  101. package/packages/installer/src/installer/post-install-validator.js +41 -1
  102. package/packages/installer/src/pro/pro-scaffolder.js +335 -0
  103. package/packages/installer/src/utils/aios-colors.js +2 -2
  104. package/packages/installer/src/wizard/feedback.js +1 -1
  105. package/packages/installer/src/wizard/ide-config-generator.js +2 -2
  106. package/packages/installer/src/wizard/index.js +58 -19
  107. package/packages/installer/src/wizard/pro-setup.js +547 -0
  108. package/packages/installer/src/wizard/questions.js +20 -14
  109. package/packages/installer/src/wizard/validators.js +1 -1
  110. package/scripts/package-synapse.js +323 -0
  111. 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
+ };