sinapse-ai 1.6.1 → 1.8.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 (131) hide show
  1. package/.claude/CLAUDE.md +5 -11
  2. package/.claude/hooks/README.md +14 -1
  3. package/.claude/hooks/code-intel-pretool.cjs +115 -0
  4. package/.claude/hooks/enforce-delegation.cjs +31 -3
  5. package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
  6. package/.claude/hooks/enforce-permission-mode.cjs +249 -0
  7. package/.claude/hooks/secret-scanning.cjs +34 -43
  8. package/.claude/hooks/synapse-engine.cjs +23 -23
  9. package/.claude/hooks/telemetry-post-tool.cjs +128 -0
  10. package/.claude/hooks/telemetry-stop.cjs +132 -0
  11. package/.claude/hooks/verify-packages.cjs +9 -2
  12. package/.claude/rules/documentation-first.md +1 -1
  13. package/.claude/rules/hook-governance.md +2 -0
  14. package/.sinapse-ai/cli/commands/health/index.js +24 -0
  15. package/.sinapse-ai/core/README.md +11 -0
  16. package/.sinapse-ai/core/config/config-loader.js +19 -0
  17. package/.sinapse-ai/core/config/merge-utils.js +8 -0
  18. package/.sinapse-ai/core/errors/constants.js +147 -0
  19. package/.sinapse-ai/core/errors/error-registry.js +176 -0
  20. package/.sinapse-ai/core/errors/index.js +50 -0
  21. package/.sinapse-ai/core/errors/serializer.js +147 -0
  22. package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
  23. package/.sinapse-ai/core/errors/utils.js +187 -0
  24. package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
  25. package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
  26. package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
  27. package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
  28. package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
  29. package/.sinapse-ai/core/execution/wave-executor.js +4 -1
  30. package/.sinapse-ai/core/grounding/README.md +71 -11
  31. package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
  32. package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
  33. package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
  34. package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
  35. package/.sinapse-ai/core/health-check/healers/index.js +40 -3
  36. package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
  37. package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
  38. package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
  39. package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
  40. package/.sinapse-ai/core/ids/index.js +30 -0
  41. package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
  42. package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
  43. package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
  44. package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
  45. package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
  46. package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
  47. package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
  48. package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
  49. package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
  50. package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
  51. package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
  52. package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
  53. package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
  54. package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
  55. package/.sinapse-ai/core/registry/registry-loader.js +71 -5
  56. package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
  57. package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
  58. package/.sinapse-ai/core/synapse/context/index.js +19 -0
  59. package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
  60. package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
  61. package/.sinapse-ai/core/synapse/engine.js +43 -3
  62. package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
  63. package/.sinapse-ai/core/utils/output-formatter.js +8 -290
  64. package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
  65. package/.sinapse-ai/core-config.yaml +68 -1
  66. package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
  67. package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
  68. package/.sinapse-ai/development/agents/developer.md +2 -0
  69. package/.sinapse-ai/development/agents/devops.md +9 -0
  70. package/.sinapse-ai/development/external-executors/README.md +18 -0
  71. package/.sinapse-ai/development/external-executors/codex.md +56 -0
  72. package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
  73. package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
  74. package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
  75. package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
  76. package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
  77. package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
  78. package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
  79. package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
  80. package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
  81. package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
  82. package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
  83. package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
  84. package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
  85. package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
  86. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
  87. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
  88. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
  89. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
  90. package/.sinapse-ai/install-manifest.yaml +218 -114
  91. package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
  92. package/.sinapse-ai/scripts/pm.sh +18 -6
  93. package/bin/cli.js +17 -0
  94. package/bin/commands/agents.js +96 -0
  95. package/bin/commands/doctor.js +15 -0
  96. package/bin/commands/ideate.js +129 -0
  97. package/bin/commands/uninstall.js +40 -0
  98. package/bin/postinstall.js +50 -4
  99. package/bin/sinapse.js +146 -2
  100. package/bin/utils/secret-scanner-core.js +253 -0
  101. package/bin/utils/staged-secret-scan.js +106 -40
  102. package/docs/framework/collaboration-autonomy-plan.md +18 -18
  103. package/docs/guides/parallel-workflow.md +6 -6
  104. package/package.json +22 -5
  105. package/packages/installer/src/installer/git-hooks-installer.js +384 -0
  106. package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
  107. package/packages/installer/src/wizard/ide-config-generator.js +23 -0
  108. package/packages/installer/src/wizard/validators.js +38 -1
  109. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
  110. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
  111. package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
  112. package/scripts/eval-runner.js +422 -0
  113. package/scripts/generate-install-manifest.js +13 -9
  114. package/scripts/generate-synapse-runtime.js +51 -0
  115. package/scripts/regenerate-orqx-stubs.ps1 +6 -5
  116. package/scripts/validate-all.js +1 -0
  117. package/scripts/validate-evals.js +466 -0
  118. package/scripts/validate-schemas.js +539 -0
  119. package/scripts/validate-squad-orqx.js +9 -2
  120. package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
  121. package/squads/squad-brand/templates/client-delivery-template.md +1 -1
  122. package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
  123. package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
  124. package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
  125. package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
  126. package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
  127. package/docs/chrome-brain-upgrade-plan.md +0 -624
  128. package/docs/constitution-compliance.md +0 -87
  129. package/docs/mega-upgrade-orchestration-plan.md +0 -71
  130. package/docs/research-synthesis-for-upgrade.md +0 -511
  131. package/docs/security-audit-report.md +0 -306
@@ -0,0 +1,555 @@
1
+ /**
2
+ * Semantic Handshake Engine
3
+ *
4
+ * Converts planning constraints into executable assertions that can be
5
+ * validated before development execution. The engine is deterministic and
6
+ * offline-friendly: callers may add structured constraints directly, or ask
7
+ * the engine to extract a small set of known hard rules from planning text.
8
+ *
9
+ * @module core/synapse/context/semantic-handshake-engine
10
+ * @created Story 483.1 - Semantic Handshake Contract and Pre-Execution Gate
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const DEFAULT_SOURCE = '@architect';
16
+
17
+ const ConstraintType = Object.freeze({
18
+ TECH_STACK: 'TECH_STACK',
19
+ PATTERN: 'PATTERN',
20
+ SECURITY: 'SECURITY',
21
+ PERFORMANCE: 'PERFORMANCE',
22
+ IMPORTS: 'IMPORTS',
23
+ CUSTOM: 'CUSTOM',
24
+ });
25
+
26
+ const ConstraintSeverity = Object.freeze({
27
+ BLOCKER: 'BLOCKER',
28
+ WARNING: 'WARNING',
29
+ });
30
+
31
+ function escapeRegExp(value) {
32
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
+ }
34
+
35
+ function normalizeSeverity(value) {
36
+ return value === ConstraintSeverity.WARNING
37
+ ? ConstraintSeverity.WARNING
38
+ : ConstraintSeverity.BLOCKER;
39
+ }
40
+
41
+ function normalizeType(value) {
42
+ return Object.values(ConstraintType).includes(value) ? value : ConstraintType.CUSTOM;
43
+ }
44
+
45
+ function cloneValue(value) {
46
+ if (value === undefined) return undefined;
47
+ if (value === null) return null;
48
+
49
+ try {
50
+ return JSON.parse(JSON.stringify(value));
51
+ } catch {
52
+ return value;
53
+ }
54
+ }
55
+
56
+ function normalizePattern(pattern) {
57
+ if (!pattern) return null;
58
+ if (pattern instanceof RegExp) return pattern;
59
+
60
+ if (typeof pattern === 'string') {
61
+ return new RegExp(escapeRegExp(pattern), 'i');
62
+ }
63
+
64
+ if (pattern.pattern) {
65
+ return new RegExp(pattern.pattern, pattern.flags || 'i');
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ function normalizePatterns(patterns) {
72
+ if (!patterns) return [];
73
+ const input = Array.isArray(patterns) ? patterns : [patterns];
74
+ return input.map(normalizePattern).filter(Boolean);
75
+ }
76
+
77
+ function patternMatches(pattern, text) {
78
+ pattern.lastIndex = 0;
79
+ return pattern.test(text);
80
+ }
81
+
82
+ function findPatternMatches(patterns, text) {
83
+ const matches = [];
84
+
85
+ for (const pattern of patterns) {
86
+ if (patternMatches(pattern, text)) {
87
+ matches.push(pattern.toString());
88
+ }
89
+ }
90
+
91
+ return matches;
92
+ }
93
+
94
+ function normalizeContent(value) {
95
+ if (value === undefined || value === null) return '';
96
+ return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
97
+ }
98
+
99
+ function normalizeFiles(input = {}) {
100
+ if (typeof input === 'string') {
101
+ return [{ path: '<inline>', content: input }];
102
+ }
103
+
104
+ if (input.proposedCode) {
105
+ return [{ path: input.path || '<inline>', content: input.proposedCode }];
106
+ }
107
+
108
+ const candidates = input.files || input.diffs || input.codeFiles || [];
109
+ if (!Array.isArray(candidates)) {
110
+ return [];
111
+ }
112
+
113
+ return candidates
114
+ .map((file, index) => {
115
+ if (typeof file === 'string') {
116
+ return { path: `<inline:${index + 1}>`, content: file };
117
+ }
118
+
119
+ if (!file || typeof file !== 'object') {
120
+ return null;
121
+ }
122
+
123
+ return {
124
+ path: file.path || file.filePath || `<inline:${index + 1}>`,
125
+ content: normalizeContent(file.content ?? file.after ?? file.diff ?? file.code),
126
+ };
127
+ })
128
+ .filter(Boolean);
129
+ }
130
+
131
+ function buildCodeContext(files) {
132
+ return files.map(file => `// File: ${file.path}\n${file.content}`).join('\n\n');
133
+ }
134
+
135
+ const DEFAULT_EXTRACTORS = [
136
+ {
137
+ id: 'TECH-POSTGRESQL',
138
+ detects: [/\bpostgres(?:ql)?\b/i],
139
+ constraint: {
140
+ source: DEFAULT_SOURCE,
141
+ type: ConstraintType.TECH_STACK,
142
+ severity: ConstraintSeverity.BLOCKER,
143
+ description: 'Must use PostgreSQL adapter, not SQLite or another local database.',
144
+ appliesWhen: [
145
+ /\b(sqlite|better-sqlite3|mysql|mongodb|database|db|datasource|prisma|typeorm|sequelize|pg|postgres(?:ql)?)\b/i,
146
+ ],
147
+ requiredPatterns: [
148
+ /\bpg\b/i,
149
+ /\bpostgres(?:ql)?\b/i,
150
+ /from\s+['"]pg['"]/i,
151
+ /require\(\s*['"]pg['"]\s*\)/i,
152
+ ],
153
+ forbiddenPatterns: [
154
+ /\bsqlite\b/i,
155
+ /\bbetter-sqlite3\b/i,
156
+ ],
157
+ },
158
+ },
159
+ {
160
+ id: 'ARCH-SERVERLESS-STATE',
161
+ detects: [/\bserverless\b/i],
162
+ constraint: {
163
+ source: DEFAULT_SOURCE,
164
+ type: ConstraintType.PATTERN,
165
+ severity: ConstraintSeverity.BLOCKER,
166
+ description: 'Serverless architecture must not write runtime state to the local filesystem.',
167
+ forbiddenPatterns: [
168
+ /\bfs\.writeFile(?:Sync)?\s*\(/,
169
+ /\bfs\.appendFile(?:Sync)?\s*\(/,
170
+ /\bfs\.createWriteStream\s*\(/,
171
+ /require\(\s*['"]fs['"]\s*\)\.writeFile(?:Sync)?\s*\(/,
172
+ ],
173
+ },
174
+ },
175
+ {
176
+ id: 'IMPORT-ABSOLUTE',
177
+ detects: [/\babsolute imports?\b/i, /\bimports? absolutos?\b/i],
178
+ constraint: {
179
+ source: DEFAULT_SOURCE,
180
+ type: ConstraintType.IMPORTS,
181
+ severity: ConstraintSeverity.BLOCKER,
182
+ description: 'Use absolute imports; parent-directory relative imports are not allowed.',
183
+ forbiddenPatterns: [
184
+ /from\s+['"]\.\.\//,
185
+ /require\(\s*['"]\.\.\//,
186
+ ],
187
+ },
188
+ },
189
+ {
190
+ id: 'SEC-NO-EVAL',
191
+ detects: [/\bno eval\b/i, /\bnever use eval\b/i, /\bsem eval\b/i],
192
+ constraint: {
193
+ source: '@security',
194
+ type: ConstraintType.SECURITY,
195
+ severity: ConstraintSeverity.BLOCKER,
196
+ description: 'Do not execute dynamic code with eval or new Function.',
197
+ forbiddenPatterns: [
198
+ /\beval\s*\(/,
199
+ /\bnew\s+Function\s*\(/,
200
+ ],
201
+ },
202
+ },
203
+ ];
204
+
205
+ class SemanticHandshakeEngine {
206
+ constructor(options = {}) {
207
+ this._constraints = new Map();
208
+ this._extractors = options.extractors || DEFAULT_EXTRACTORS;
209
+
210
+ if (Array.isArray(options.constraints)) {
211
+ for (const constraint of options.constraints) {
212
+ this.addConstraint(constraint);
213
+ }
214
+ }
215
+ }
216
+
217
+ clone() {
218
+ return new SemanticHandshakeEngine({
219
+ extractors: this._extractors,
220
+ constraints: [...this._constraints.values()].map(constraint => ({
221
+ id: constraint.id,
222
+ source: constraint.source,
223
+ type: constraint.type,
224
+ severity: constraint.severity,
225
+ description: constraint.description,
226
+ appliesWhen: constraint.appliesWhen,
227
+ requiredPatterns: constraint.requiredPatterns,
228
+ forbiddenPatterns: constraint.forbiddenPatterns,
229
+ validator: constraint.validator,
230
+ metadata: cloneValue(constraint.metadata || {}),
231
+ })),
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Extract known hard constraints from planning text.
237
+ *
238
+ * @param {string} planningOutput
239
+ * @param {object} [options]
240
+ * @returns {object[]} Registered constraints.
241
+ */
242
+ registerConstraints(planningOutput, options = {}) {
243
+ const text = normalizeContent(planningOutput);
244
+ const registered = [];
245
+
246
+ for (const extractor of this._extractors) {
247
+ const detects = normalizePatterns(extractor.detects);
248
+ if (!detects.some(pattern => patternMatches(pattern, text))) {
249
+ continue;
250
+ }
251
+
252
+ const constraint = this.addConstraint({
253
+ id: extractor.id,
254
+ ...extractor.constraint,
255
+ source: options.source || extractor.constraint.source || DEFAULT_SOURCE,
256
+ metadata: {
257
+ ...(extractor.constraint.metadata || {}),
258
+ ...(options.metadata || {}),
259
+ extractedFrom: options.extractedFrom || 'planning-output',
260
+ },
261
+ });
262
+
263
+ registered.push(constraint);
264
+ }
265
+
266
+ return registered;
267
+ }
268
+
269
+ addConstraint(input) {
270
+ if (!input || typeof input !== 'object') {
271
+ throw new TypeError('constraint must be an object');
272
+ }
273
+
274
+ if (!input.id && !input.description) {
275
+ throw new Error('constraint requires id or description');
276
+ }
277
+
278
+ const id = String(input.id || input.description)
279
+ .trim()
280
+ .toUpperCase()
281
+ .replace(/[^A-Z0-9]+/g, '-')
282
+ .replace(/^-|-$/g, '');
283
+
284
+ if (!id) {
285
+ throw new Error('constraint id normalizes to an empty value');
286
+ }
287
+
288
+ const constraint = {
289
+ id,
290
+ source: input.source || DEFAULT_SOURCE,
291
+ type: normalizeType(input.type),
292
+ severity: normalizeSeverity(input.severity),
293
+ description: input.description || id,
294
+ appliesWhen: normalizePatterns(input.appliesWhen),
295
+ requiredPatterns: normalizePatterns(input.requiredPatterns),
296
+ forbiddenPatterns: normalizePatterns(input.forbiddenPatterns),
297
+ validator: typeof input.validator === 'function' ? input.validator : null,
298
+ metadata: cloneValue(input.metadata || {}),
299
+ };
300
+
301
+ this._constraints.set(id, constraint);
302
+ return this._publicConstraint(constraint);
303
+ }
304
+
305
+ getConstraints() {
306
+ return [...this._constraints.values()].map(constraint => this._publicConstraint(constraint));
307
+ }
308
+
309
+ clear() {
310
+ this._constraints.clear();
311
+ }
312
+
313
+ /**
314
+ * Validate proposed code or file list against registered constraints.
315
+ *
316
+ * @param {string|object} executionIntent
317
+ * @returns {Promise<object>}
318
+ */
319
+ async validateExecutionIntent(executionIntent = {}) {
320
+ const files = normalizeFiles(executionIntent);
321
+ const codeContext = buildCodeContext(files);
322
+ const violations = [];
323
+ const blockingViolations = [];
324
+ const warnings = [];
325
+ const verifiedConstraints = [];
326
+ const skippedConstraints = [];
327
+
328
+ for (const constraint of this._constraints.values()) {
329
+ const result = await this._evaluateConstraint(constraint, codeContext, files, executionIntent);
330
+
331
+ if (result.skipped) {
332
+ skippedConstraints.push(this._publicConstraint(constraint));
333
+ continue;
334
+ }
335
+
336
+ if (result.passed) {
337
+ verifiedConstraints.push(this._publicConstraint(constraint));
338
+ continue;
339
+ }
340
+
341
+ violations.push(result.violation);
342
+ if (constraint.severity === ConstraintSeverity.BLOCKER) {
343
+ blockingViolations.push(result.violation);
344
+ } else {
345
+ warnings.push(result.violation.message);
346
+ }
347
+ }
348
+
349
+ const passed = blockingViolations.length === 0;
350
+ const response = {
351
+ passed,
352
+ evaluatedAt: new Date().toISOString(),
353
+ constraintCount: this._constraints.size,
354
+ files: files.map(file => file.path),
355
+ verifiedConstraints,
356
+ skippedConstraints,
357
+ violations,
358
+ blockingViolations,
359
+ warnings,
360
+ };
361
+
362
+ response.correctionPrompt = this.buildCorrectionPrompt(response);
363
+ return response;
364
+ }
365
+
366
+ generateComplianceReport(result) {
367
+ const lines = ['Semantic Handshake Report'];
368
+ lines.push(`Status: ${result.passed ? 'PASSED' : 'FAILED'}`);
369
+ lines.push(`Constraints evaluated: ${result.constraintCount || 0}`);
370
+
371
+ if (result.verifiedConstraints?.length) {
372
+ lines.push('');
373
+ lines.push('Verified constraints:');
374
+ for (const constraint of result.verifiedConstraints) {
375
+ lines.push(`- ${constraint.id}: ${constraint.description}`);
376
+ }
377
+ }
378
+
379
+ if (result.skippedConstraints?.length) {
380
+ lines.push('');
381
+ lines.push('Skipped constraints:');
382
+ for (const constraint of result.skippedConstraints) {
383
+ lines.push(`- ${constraint.id}: not applicable to current code context`);
384
+ }
385
+ }
386
+
387
+ if (result.violations?.length) {
388
+ lines.push('');
389
+ lines.push('Violations:');
390
+ for (const violation of result.violations) {
391
+ lines.push(`- [${violation.severity}] ${violation.id}: ${violation.message}`);
392
+ }
393
+ }
394
+
395
+ if (!result.passed) {
396
+ lines.push('');
397
+ lines.push('Action: update the implementation or request an explicit architecture override.');
398
+ }
399
+
400
+ return lines.join('\n');
401
+ }
402
+
403
+ buildCorrectionPrompt(result) {
404
+ if (!result || result.passed) {
405
+ return '';
406
+ }
407
+
408
+ const lines = [
409
+ 'Semantic Handshake failed. Revise the implementation before continuing.',
410
+ 'Blocking constraints:',
411
+ ];
412
+
413
+ for (const violation of result.blockingViolations || []) {
414
+ lines.push(`- ${violation.id}: ${violation.message}`);
415
+ }
416
+
417
+ return lines.join('\n');
418
+ }
419
+
420
+ toContextMessage(result, options = {}) {
421
+ const content = [
422
+ options.prefix || 'Pre-execution Semantic Handshake result:',
423
+ this.generateComplianceReport(result),
424
+ result?.correctionPrompt || '',
425
+ ].filter(Boolean).join('\n\n');
426
+
427
+ return {
428
+ role: 'system',
429
+ content,
430
+ metadata: {
431
+ snps: {
432
+ type: 'semantic_handshake_report',
433
+ passed: Boolean(result?.passed),
434
+ blockingViolationCount: result?.blockingViolations?.length || 0,
435
+ violationCount: result?.violations?.length || 0,
436
+ verifiedConstraintCount: result?.verifiedConstraints?.length || 0,
437
+ },
438
+ },
439
+ };
440
+ }
441
+
442
+ async _evaluateConstraint(constraint, codeContext, files, executionIntent) {
443
+ if (!this._constraintApplies(constraint, codeContext)) {
444
+ return { skipped: true };
445
+ }
446
+
447
+ if (constraint.validator) {
448
+ let validation;
449
+ try {
450
+ validation = await constraint.validator({
451
+ codeContext,
452
+ files,
453
+ executionIntent,
454
+ constraint: this._publicConstraint(constraint),
455
+ });
456
+ } catch (error) {
457
+ const errorMessage = error instanceof Error ? error.message : String(error);
458
+ validation = {
459
+ passed: false,
460
+ message: `Constraint validator failed: ${errorMessage}`,
461
+ matches: ['validator-error'],
462
+ metadata: {
463
+ validatorError: {
464
+ message: errorMessage,
465
+ stack: error instanceof Error ? error.stack : null,
466
+ },
467
+ },
468
+ };
469
+ }
470
+ return this._normalizeValidatorResult(validation, constraint);
471
+ }
472
+
473
+ const forbiddenMatches = findPatternMatches(constraint.forbiddenPatterns, codeContext);
474
+ if (forbiddenMatches.length > 0) {
475
+ return {
476
+ passed: false,
477
+ violation: this._buildViolation(constraint, {
478
+ message: constraint.description,
479
+ matches: forbiddenMatches,
480
+ }),
481
+ };
482
+ }
483
+
484
+ if (
485
+ constraint.requiredPatterns.length > 0 &&
486
+ findPatternMatches(constraint.requiredPatterns, codeContext).length === 0
487
+ ) {
488
+ return {
489
+ passed: false,
490
+ violation: this._buildViolation(constraint, {
491
+ message: `Required implementation evidence missing: ${constraint.description}`,
492
+ matches: [],
493
+ }),
494
+ };
495
+ }
496
+
497
+ return { passed: true };
498
+ }
499
+
500
+ _constraintApplies(constraint, codeContext) {
501
+ if (!codeContext.trim()) return false;
502
+ if (constraint.appliesWhen.length === 0) return true;
503
+ return constraint.appliesWhen.some(pattern => patternMatches(pattern, codeContext));
504
+ }
505
+
506
+ _normalizeValidatorResult(validation, constraint) {
507
+ if (validation === true || validation?.passed === true) {
508
+ return { passed: true };
509
+ }
510
+
511
+ const message = validation?.message || constraint.description;
512
+ return {
513
+ passed: false,
514
+ violation: this._buildViolation(constraint, {
515
+ message,
516
+ matches: validation?.matches || [],
517
+ metadata: validation?.metadata || {},
518
+ }),
519
+ };
520
+ }
521
+
522
+ _buildViolation(constraint, fields = {}) {
523
+ return {
524
+ id: constraint.id,
525
+ source: constraint.source,
526
+ type: constraint.type,
527
+ severity: constraint.severity,
528
+ description: constraint.description,
529
+ message: fields.message || constraint.description,
530
+ matches: fields.matches || [],
531
+ metadata: {
532
+ ...cloneValue(constraint.metadata || {}),
533
+ ...cloneValue(fields.metadata || {}),
534
+ },
535
+ };
536
+ }
537
+
538
+ _publicConstraint(constraint) {
539
+ return {
540
+ id: constraint.id,
541
+ source: constraint.source,
542
+ type: constraint.type,
543
+ severity: constraint.severity,
544
+ description: constraint.description,
545
+ metadata: cloneValue(constraint.metadata || {}),
546
+ };
547
+ }
548
+ }
549
+
550
+ module.exports = {
551
+ SemanticHandshakeEngine,
552
+ ConstraintType,
553
+ ConstraintSeverity,
554
+ DEFAULT_EXTRACTORS,
555
+ };
@@ -34,10 +34,12 @@ const LAYER_NAMES = {
34
34
  * @param {number} promptCount - Current prompt count from session
35
35
  * @param {string|null} activeAgentId - Active agent ID (for L2 match check)
36
36
  * @param {object} manifest - Parsed manifest object
37
+ * @param {object} [options={}] - Context-estimate overrides (maxContext, avgTokensPerPrompt).
38
+ * Defaults to the active model's config (model-aware), matching the engine's own bracket calc.
37
39
  * @returns {{ bracket: string, contextPercent: number, layers: Array<{ layer: string, expected: string, status: string }> }}
38
40
  */
39
- function collectPipelineSimulation(promptCount, activeAgentId, manifest) {
40
- const contextPercent = estimateContextPercent(promptCount || 0);
41
+ function collectPipelineSimulation(promptCount, activeAgentId, manifest, options = {}) {
42
+ const contextPercent = estimateContextPercent(promptCount || 0, options);
41
43
  const bracket = calculateBracket(contextPercent);
42
44
  const layerConfig = getActiveLayers(bracket);
43
45
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+ const { normalizeError, serializeError } = require('../errors');
15
16
 
16
17
  const {
17
18
  estimateContextPercent,
@@ -131,10 +132,20 @@ class PipelineMetrics {
131
132
  existing.end = endTime;
132
133
  existing.duration = Number(endTime - existing.start) / 1e6;
133
134
  }
135
+ const normalizedError = normalizeError(error, {
136
+ code: 'SNPS_SYNAPSE_LAYER_FAILED',
137
+ metadata: {
138
+ synapse: {
139
+ layer: name,
140
+ },
141
+ },
142
+ });
143
+
134
144
  this.layers[name] = {
135
145
  ...existing,
136
146
  status: 'error',
137
- error: error && error.message ? error.message : String(error),
147
+ error: normalizedError.message,
148
+ errorDetails: serializeError(normalizedError),
138
149
  };
139
150
  }
140
151
 
@@ -180,6 +191,24 @@ const PIPELINE_TIMEOUT_MS = 100;
180
191
  const DEFAULT_ACTIVE_LAYERS = [0, 1, 2];
181
192
  const LEGACY_MODE = process.env.SYNAPSE_LEGACY_MODE === 'true';
182
193
 
194
+ /**
195
+ * Safely read the last processing error exposed by a layer.
196
+ *
197
+ * @param {object} layer - Synapse layer instance.
198
+ * @returns {Error|null} Last layer error, or the accessor failure as an Error.
199
+ */
200
+ function getLayerError(layer) {
201
+ if (!layer) return null;
202
+ if (typeof layer.getLastError === 'function') {
203
+ try {
204
+ return layer.getLastError();
205
+ } catch (error) {
206
+ return error instanceof Error ? error : new Error(String(error));
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+
183
212
  /**
184
213
  * Orchestrates the 8-layer SYNAPSE context injection pipeline.
185
214
  *
@@ -298,14 +327,25 @@ class SynapseEngine {
298
327
  previousLayers,
299
328
  });
300
329
 
301
- const result = layer._safeProcess(context);
330
+ let result;
331
+ try {
332
+ result = layer._safeProcess(context);
333
+ } catch (error) {
334
+ metrics.errorLayer(layer.name, error);
335
+ continue;
336
+ }
302
337
 
303
338
  if (result && Array.isArray(result.rules)) {
304
339
  metrics.endLayer(layer.name, result.rules.length);
305
340
  results.push(result);
306
341
  previousLayers.push(result);
307
342
  } else if (result === null || result === undefined) {
308
- metrics.skipLayer(layer.name, 'Returned null');
343
+ const layerError = getLayerError(layer);
344
+ if (layerError) {
345
+ metrics.errorLayer(layer.name, layerError);
346
+ } else {
347
+ metrics.skipLayer(layer.name, 'Returned null');
348
+ }
309
349
  } else {
310
350
  metrics.skipLayer(layer.name, 'Invalid result format');
311
351
  }