diff --git a/.aiox-core/core/ids/gates/g5-semantic-handshake.js b/.aiox-core/core/ids/gates/g5-semantic-handshake.js new file mode 100644 index 0000000000..f98d0d8b30 --- /dev/null +++ b/.aiox-core/core/ids/gates/g5-semantic-handshake.js @@ -0,0 +1,137 @@ +'use strict'; + +/** + * G5 — Semantic Handshake Gate + * + * Agent: @dev + * Type: Automated, Blocking when Semantic Handshake reports BLOCKER violations + * + * Purpose: Preserve intent integrity between planning and implementation by + * evaluating proposed code against executable planning constraints. + */ + +const path = require('path'); +const { VerificationGate } = require(path.resolve(__dirname, '../verification-gate.js')); +const { + SemanticHandshakeEngine, +} = require(path.resolve(__dirname, '../../synapse/context/semantic-handshake-engine.js')); + +const G5_DEFAULT_TIMEOUT_MS = 2000; + +class G5SemanticHandshakeGate extends VerificationGate { + constructor(options = {}) { + super({ + gateId: 'G5', + agent: '@dev', + blocking: true, + timeoutMs: options.timeoutMs ?? G5_DEFAULT_TIMEOUT_MS, + circuitBreakerOptions: options.circuitBreakerOptions, + logger: options.logger, + }); + + this._engine = options.engine || new SemanticHandshakeEngine({ + constraints: options.constraints || [], + }); + } + + async _doVerify(context = {}) { + const engine = this._createScopedEngine(context); + + const constraints = engine.getConstraints(); + if (constraints.length === 0) { + return { + passed: true, + warnings: ['No Semantic Handshake constraints registered'], + opportunities: [], + }; + } + + if (!this._hasCodeContext(context)) { + return { + passed: true, + warnings: ['Semantic Handshake constraints registered, but no proposed code was provided'], + opportunities: constraints.map(constraint => ({ + entity: constraint.id, + recommendation: constraint.description, + severity: constraint.severity, + })), + }; + } + + const result = await engine.validateExecutionIntent(context); + const report = engine.generateComplianceReport(result); + + return { + passed: result.passed, + warnings: [ + ...result.warnings, + ...result.blockingViolations.map(violation => violation.message), + ], + opportunities: [ + ...result.verifiedConstraints.map(constraint => ({ + entity: constraint.id, + recommendation: 'Constraint verified', + severity: constraint.severity, + })), + ...result.violations.map(violation => ({ + entity: violation.id, + recommendation: violation.message, + severity: violation.severity, + matches: violation.matches, + })), + ], + override: result.passed ? null : { + reason: 'Semantic Handshake blocker violation', + correctionPrompt: result.correctionPrompt, + report, + }, + }; + } + + _createScopedEngine(context) { + const engine = typeof this._engine.clone === 'function' + ? this._engine.clone() + : new SemanticHandshakeEngine(); + + this._registerContextConstraints(engine, context); + return engine; + } + + _registerContextConstraints(engine, context) { + const planningText = [ + context.planningOutput, + context.planningText, + context.architectureText, + context.storyText, + ].filter(Boolean).join('\n\n'); + + if (planningText.trim()) { + engine.registerConstraints(planningText, { + source: context.source || '@architect', + metadata: { + storyId: context.storyId || 'unknown', + }, + }); + } + + if (Array.isArray(context.constraints)) { + for (const constraint of context.constraints) { + engine.addConstraint(constraint); + } + } + } + + _hasCodeContext(context) { + return Boolean( + context.proposedCode || + (Array.isArray(context.files) && context.files.length > 0) || + (Array.isArray(context.diffs) && context.diffs.length > 0) || + (Array.isArray(context.codeFiles) && context.codeFiles.length > 0), + ); + } +} + +module.exports = { + G5SemanticHandshakeGate, + G5_DEFAULT_TIMEOUT_MS, +}; diff --git a/.aiox-core/core/ids/index.js b/.aiox-core/core/ids/index.js index 17d1a7bd23..39d987253b 100644 --- a/.aiox-core/core/ids/index.js +++ b/.aiox-core/core/ids/index.js @@ -84,6 +84,10 @@ const { G1EpicCreationGate } = require('./gates/g1-epic-creation'); const { G2StoryCreationGate } = require('./gates/g2-story-creation'); const { G3StoryValidationGate } = require('./gates/g3-story-validation'); const { G4DevContextGate, G4_DEFAULT_TIMEOUT_MS } = require('./gates/g4-dev-context'); +const { + G5SemanticHandshakeGate, + G5_DEFAULT_TIMEOUT_MS, +} = require('./gates/g5-semantic-handshake'); // IDS-7: Framework Governor (aiox-master integration) const { @@ -142,12 +146,14 @@ module.exports = { createGateResult, DEFAULT_TIMEOUT_MS, - // IDS-5a: Gates G1-G4 + // IDS-5a/5b: Gates G1-G5 G1EpicCreationGate, G2StoryCreationGate, G3StoryValidationGate, G4DevContextGate, G4_DEFAULT_TIMEOUT_MS, + G5SemanticHandshakeGate, + G5_DEFAULT_TIMEOUT_MS, // IDS-7: Framework Governor FrameworkGovernor, diff --git a/.aiox-core/core/synapse/context/README.md b/.aiox-core/core/synapse/context/README.md index 4211a41582..72d88cb434 100644 --- a/.aiox-core/core/synapse/context/README.md +++ b/.aiox-core/core/synapse/context/README.md @@ -33,3 +33,34 @@ The manager does not call OpenAI, Anthropic, Claude, Gemini, Kimi or OpenRouter directly. Inject a summarizer when an agent loop wants model-based compression. If that summarizer fails, the manager emits `swap:error` and falls back to a local extractive summary instead of crashing the loop. + +## SemanticHandshakeEngine + +`SemanticHandshakeEngine` turns planning constraints into executable checks that +can run before implementation. It is deterministic by default and does not call +an LLM. + +```javascript +const { SemanticHandshakeEngine } = require('./.aiox-core/core/synapse/context'); + +const handshake = new SemanticHandshakeEngine(); +handshake.registerConstraints('Use serverless functions with PostgreSQL.'); + +const result = await handshake.validateExecutionIntent({ + files: [ + { + path: 'src/db.js', + content: "const { Pool } = require('pg');", + }, + ], +}); + +if (!result.passed) { + throw new Error(handshake.generateComplianceReport(result)); +} +``` + +The first supported extraction rules cover PostgreSQL, serverless local-state +writes, absolute imports and `eval` bans. Callers can also add structured custom +constraints with `addConstraint()`. Use `toContextMessage()` to inject the +handshake result into an agent context as a system message. diff --git a/.aiox-core/core/synapse/context/index.js b/.aiox-core/core/synapse/context/index.js index d0cb73e43c..8ed967c352 100644 --- a/.aiox-core/core/synapse/context/index.js +++ b/.aiox-core/core/synapse/context/index.js @@ -6,12 +6,16 @@ 'use strict'; -const contextTracker = require('./context-tracker'); -const contextBuilder = require('./context-builder'); -const hierarchicalContext = require('./hierarchical-context-manager'); +const path = require('path'); + +const contextTracker = require(path.resolve(__dirname, './context-tracker.js')); +const contextBuilder = require(path.resolve(__dirname, './context-builder.js')); +const hierarchicalContext = require(path.resolve(__dirname, './hierarchical-context-manager.js')); +const semanticHandshake = require(path.resolve(__dirname, './semantic-handshake-engine.js')); module.exports = { ...contextTracker, ...contextBuilder, ...hierarchicalContext, + ...semanticHandshake, }; diff --git a/.aiox-core/core/synapse/context/semantic-handshake-engine.js b/.aiox-core/core/synapse/context/semantic-handshake-engine.js new file mode 100644 index 0000000000..e8aed96e32 --- /dev/null +++ b/.aiox-core/core/synapse/context/semantic-handshake-engine.js @@ -0,0 +1,555 @@ +/** + * Semantic Handshake Engine + * + * Converts planning constraints into executable assertions that can be + * validated before development execution. The engine is deterministic and + * offline-friendly: callers may add structured constraints directly, or ask + * the engine to extract a small set of known hard rules from planning text. + * + * @module core/synapse/context/semantic-handshake-engine + * @created Story 483.1 - Semantic Handshake Contract and Pre-Execution Gate + */ + +'use strict'; + +const DEFAULT_SOURCE = '@architect'; + +const ConstraintType = Object.freeze({ + TECH_STACK: 'TECH_STACK', + PATTERN: 'PATTERN', + SECURITY: 'SECURITY', + PERFORMANCE: 'PERFORMANCE', + IMPORTS: 'IMPORTS', + CUSTOM: 'CUSTOM', +}); + +const ConstraintSeverity = Object.freeze({ + BLOCKER: 'BLOCKER', + WARNING: 'WARNING', +}); + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function normalizeSeverity(value) { + return value === ConstraintSeverity.WARNING + ? ConstraintSeverity.WARNING + : ConstraintSeverity.BLOCKER; +} + +function normalizeType(value) { + return Object.values(ConstraintType).includes(value) ? value : ConstraintType.CUSTOM; +} + +function cloneValue(value) { + if (value === undefined) return undefined; + if (value === null) return null; + + try { + return JSON.parse(JSON.stringify(value)); + } catch { + return value; + } +} + +function normalizePattern(pattern) { + if (!pattern) return null; + if (pattern instanceof RegExp) return pattern; + + if (typeof pattern === 'string') { + return new RegExp(escapeRegExp(pattern), 'i'); + } + + if (pattern.pattern) { + return new RegExp(pattern.pattern, pattern.flags || 'i'); + } + + return null; +} + +function normalizePatterns(patterns) { + if (!patterns) return []; + const input = Array.isArray(patterns) ? patterns : [patterns]; + return input.map(normalizePattern).filter(Boolean); +} + +function patternMatches(pattern, text) { + pattern.lastIndex = 0; + return pattern.test(text); +} + +function findPatternMatches(patterns, text) { + const matches = []; + + for (const pattern of patterns) { + if (patternMatches(pattern, text)) { + matches.push(pattern.toString()); + } + } + + return matches; +} + +function normalizeContent(value) { + if (value === undefined || value === null) return ''; + return typeof value === 'string' ? value : JSON.stringify(value, null, 2); +} + +function normalizeFiles(input = {}) { + if (typeof input === 'string') { + return [{ path: '', content: input }]; + } + + if (input.proposedCode) { + return [{ path: input.path || '', content: input.proposedCode }]; + } + + const candidates = input.files || input.diffs || input.codeFiles || []; + if (!Array.isArray(candidates)) { + return []; + } + + return candidates + .map((file, index) => { + if (typeof file === 'string') { + return { path: ``, content: file }; + } + + if (!file || typeof file !== 'object') { + return null; + } + + return { + path: file.path || file.filePath || ``, + content: normalizeContent(file.content ?? file.after ?? file.diff ?? file.code), + }; + }) + .filter(Boolean); +} + +function buildCodeContext(files) { + return files.map(file => `// File: ${file.path}\n${file.content}`).join('\n\n'); +} + +const DEFAULT_EXTRACTORS = [ + { + id: 'TECH-POSTGRESQL', + detects: [/\bpostgres(?:ql)?\b/i], + constraint: { + source: DEFAULT_SOURCE, + type: ConstraintType.TECH_STACK, + severity: ConstraintSeverity.BLOCKER, + description: 'Must use PostgreSQL adapter, not SQLite or another local database.', + appliesWhen: [ + /\b(sqlite|better-sqlite3|mysql|mongodb|database|db|datasource|prisma|typeorm|sequelize|pg|postgres(?:ql)?)\b/i, + ], + requiredPatterns: [ + /\bpg\b/i, + /\bpostgres(?:ql)?\b/i, + /from\s+['"]pg['"]/i, + /require\(\s*['"]pg['"]\s*\)/i, + ], + forbiddenPatterns: [ + /\bsqlite\b/i, + /\bbetter-sqlite3\b/i, + ], + }, + }, + { + id: 'ARCH-SERVERLESS-STATE', + detects: [/\bserverless\b/i], + constraint: { + source: DEFAULT_SOURCE, + type: ConstraintType.PATTERN, + severity: ConstraintSeverity.BLOCKER, + description: 'Serverless architecture must not write runtime state to the local filesystem.', + forbiddenPatterns: [ + /\bfs\.writeFile(?:Sync)?\s*\(/, + /\bfs\.appendFile(?:Sync)?\s*\(/, + /\bfs\.createWriteStream\s*\(/, + /require\(\s*['"]fs['"]\s*\)\.writeFile(?:Sync)?\s*\(/, + ], + }, + }, + { + id: 'IMPORT-ABSOLUTE', + detects: [/\babsolute imports?\b/i, /\bimports? absolutos?\b/i], + constraint: { + source: DEFAULT_SOURCE, + type: ConstraintType.IMPORTS, + severity: ConstraintSeverity.BLOCKER, + description: 'Use absolute imports; parent-directory relative imports are not allowed.', + forbiddenPatterns: [ + /from\s+['"]\.\.\//, + /require\(\s*['"]\.\.\//, + ], + }, + }, + { + id: 'SEC-NO-EVAL', + detects: [/\bno eval\b/i, /\bnever use eval\b/i, /\bsem eval\b/i], + constraint: { + source: '@security', + type: ConstraintType.SECURITY, + severity: ConstraintSeverity.BLOCKER, + description: 'Do not execute dynamic code with eval or new Function.', + forbiddenPatterns: [ + /\beval\s*\(/, + /\bnew\s+Function\s*\(/, + ], + }, + }, +]; + +class SemanticHandshakeEngine { + constructor(options = {}) { + this._constraints = new Map(); + this._extractors = options.extractors || DEFAULT_EXTRACTORS; + + if (Array.isArray(options.constraints)) { + for (const constraint of options.constraints) { + this.addConstraint(constraint); + } + } + } + + clone() { + return new SemanticHandshakeEngine({ + extractors: this._extractors, + constraints: [...this._constraints.values()].map(constraint => ({ + id: constraint.id, + source: constraint.source, + type: constraint.type, + severity: constraint.severity, + description: constraint.description, + appliesWhen: constraint.appliesWhen, + requiredPatterns: constraint.requiredPatterns, + forbiddenPatterns: constraint.forbiddenPatterns, + validator: constraint.validator, + metadata: cloneValue(constraint.metadata || {}), + })), + }); + } + + /** + * Extract known hard constraints from planning text. + * + * @param {string} planningOutput + * @param {object} [options] + * @returns {object[]} Registered constraints. + */ + registerConstraints(planningOutput, options = {}) { + const text = normalizeContent(planningOutput); + const registered = []; + + for (const extractor of this._extractors) { + const detects = normalizePatterns(extractor.detects); + if (!detects.some(pattern => patternMatches(pattern, text))) { + continue; + } + + const constraint = this.addConstraint({ + id: extractor.id, + ...extractor.constraint, + source: options.source || extractor.constraint.source || DEFAULT_SOURCE, + metadata: { + ...(extractor.constraint.metadata || {}), + ...(options.metadata || {}), + extractedFrom: options.extractedFrom || 'planning-output', + }, + }); + + registered.push(constraint); + } + + return registered; + } + + addConstraint(input) { + if (!input || typeof input !== 'object') { + throw new TypeError('constraint must be an object'); + } + + if (!input.id && !input.description) { + throw new Error('constraint requires id or description'); + } + + const id = String(input.id || input.description) + .trim() + .toUpperCase() + .replace(/[^A-Z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + + if (!id) { + throw new Error('constraint id normalizes to an empty value'); + } + + const constraint = { + id, + source: input.source || DEFAULT_SOURCE, + type: normalizeType(input.type), + severity: normalizeSeverity(input.severity), + description: input.description || id, + appliesWhen: normalizePatterns(input.appliesWhen), + requiredPatterns: normalizePatterns(input.requiredPatterns), + forbiddenPatterns: normalizePatterns(input.forbiddenPatterns), + validator: typeof input.validator === 'function' ? input.validator : null, + metadata: cloneValue(input.metadata || {}), + }; + + this._constraints.set(id, constraint); + return this._publicConstraint(constraint); + } + + getConstraints() { + return [...this._constraints.values()].map(constraint => this._publicConstraint(constraint)); + } + + clear() { + this._constraints.clear(); + } + + /** + * Validate proposed code or file list against registered constraints. + * + * @param {string|object} executionIntent + * @returns {Promise} + */ + async validateExecutionIntent(executionIntent = {}) { + const files = normalizeFiles(executionIntent); + const codeContext = buildCodeContext(files); + const violations = []; + const blockingViolations = []; + const warnings = []; + const verifiedConstraints = []; + const skippedConstraints = []; + + for (const constraint of this._constraints.values()) { + const result = await this._evaluateConstraint(constraint, codeContext, files, executionIntent); + + if (result.skipped) { + skippedConstraints.push(this._publicConstraint(constraint)); + continue; + } + + if (result.passed) { + verifiedConstraints.push(this._publicConstraint(constraint)); + continue; + } + + violations.push(result.violation); + if (constraint.severity === ConstraintSeverity.BLOCKER) { + blockingViolations.push(result.violation); + } else { + warnings.push(result.violation.message); + } + } + + const passed = blockingViolations.length === 0; + const response = { + passed, + evaluatedAt: new Date().toISOString(), + constraintCount: this._constraints.size, + files: files.map(file => file.path), + verifiedConstraints, + skippedConstraints, + violations, + blockingViolations, + warnings, + }; + + response.correctionPrompt = this.buildCorrectionPrompt(response); + return response; + } + + generateComplianceReport(result) { + const lines = ['Semantic Handshake Report']; + lines.push(`Status: ${result.passed ? 'PASSED' : 'FAILED'}`); + lines.push(`Constraints evaluated: ${result.constraintCount || 0}`); + + if (result.verifiedConstraints?.length) { + lines.push(''); + lines.push('Verified constraints:'); + for (const constraint of result.verifiedConstraints) { + lines.push(`- ${constraint.id}: ${constraint.description}`); + } + } + + if (result.skippedConstraints?.length) { + lines.push(''); + lines.push('Skipped constraints:'); + for (const constraint of result.skippedConstraints) { + lines.push(`- ${constraint.id}: not applicable to current code context`); + } + } + + if (result.violations?.length) { + lines.push(''); + lines.push('Violations:'); + for (const violation of result.violations) { + lines.push(`- [${violation.severity}] ${violation.id}: ${violation.message}`); + } + } + + if (!result.passed) { + lines.push(''); + lines.push('Action: update the implementation or request an explicit architecture override.'); + } + + return lines.join('\n'); + } + + buildCorrectionPrompt(result) { + if (!result || result.passed) { + return ''; + } + + const lines = [ + 'Semantic Handshake failed. Revise the implementation before continuing.', + 'Blocking constraints:', + ]; + + for (const violation of result.blockingViolations || []) { + lines.push(`- ${violation.id}: ${violation.message}`); + } + + return lines.join('\n'); + } + + toContextMessage(result, options = {}) { + const content = [ + options.prefix || 'Pre-execution Semantic Handshake result:', + this.generateComplianceReport(result), + result?.correctionPrompt || '', + ].filter(Boolean).join('\n\n'); + + return { + role: 'system', + content, + metadata: { + aiox: { + type: 'semantic_handshake_report', + passed: Boolean(result?.passed), + blockingViolationCount: result?.blockingViolations?.length || 0, + violationCount: result?.violations?.length || 0, + verifiedConstraintCount: result?.verifiedConstraints?.length || 0, + }, + }, + }; + } + + async _evaluateConstraint(constraint, codeContext, files, executionIntent) { + if (!this._constraintApplies(constraint, codeContext)) { + return { skipped: true }; + } + + if (constraint.validator) { + let validation; + try { + validation = await constraint.validator({ + codeContext, + files, + executionIntent, + constraint: this._publicConstraint(constraint), + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + validation = { + passed: false, + message: `Constraint validator failed: ${errorMessage}`, + matches: ['validator-error'], + metadata: { + validatorError: { + message: errorMessage, + stack: error instanceof Error ? error.stack : null, + }, + }, + }; + } + return this._normalizeValidatorResult(validation, constraint); + } + + const forbiddenMatches = findPatternMatches(constraint.forbiddenPatterns, codeContext); + if (forbiddenMatches.length > 0) { + return { + passed: false, + violation: this._buildViolation(constraint, { + message: constraint.description, + matches: forbiddenMatches, + }), + }; + } + + if ( + constraint.requiredPatterns.length > 0 && + findPatternMatches(constraint.requiredPatterns, codeContext).length === 0 + ) { + return { + passed: false, + violation: this._buildViolation(constraint, { + message: `Required implementation evidence missing: ${constraint.description}`, + matches: [], + }), + }; + } + + return { passed: true }; + } + + _constraintApplies(constraint, codeContext) { + if (!codeContext.trim()) return false; + if (constraint.appliesWhen.length === 0) return true; + return constraint.appliesWhen.some(pattern => patternMatches(pattern, codeContext)); + } + + _normalizeValidatorResult(validation, constraint) { + if (validation === true || validation?.passed === true) { + return { passed: true }; + } + + const message = validation?.message || constraint.description; + return { + passed: false, + violation: this._buildViolation(constraint, { + message, + matches: validation?.matches || [], + metadata: validation?.metadata || {}, + }), + }; + } + + _buildViolation(constraint, fields = {}) { + return { + id: constraint.id, + source: constraint.source, + type: constraint.type, + severity: constraint.severity, + description: constraint.description, + message: fields.message || constraint.description, + matches: fields.matches || [], + metadata: { + ...cloneValue(constraint.metadata || {}), + ...cloneValue(fields.metadata || {}), + }, + }; + } + + _publicConstraint(constraint) { + return { + id: constraint.id, + source: constraint.source, + type: constraint.type, + severity: constraint.severity, + description: constraint.description, + metadata: cloneValue(constraint.metadata || {}), + }; + } +} + +module.exports = { + SemanticHandshakeEngine, + ConstraintType, + ConstraintSeverity, + DEFAULT_EXTRACTORS, +}; diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 86b9836142..121b392238 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,9 +8,9 @@ # - File types for categorization # version: 5.1.17 -generated_at: "2026-05-09T01:47:43.261Z" +generated_at: "2026-05-09T07:21:15.059Z" generator: scripts/generate-install-manifest.js -file_count: 1123 +file_count: 1125 files: - path: cli/commands/config/index.js hash: sha256:25c4b9bf4e0241abf7754b55153f49f1a214f1fb5fe904a576675634cb7b3da9 @@ -752,14 +752,18 @@ files: hash: sha256:a0fdd59eb0c3a8a59862397b1af5af84971ce051929ae9d32361b7ca99a444fb type: core size: 4399 + - path: core/ids/gates/g5-semantic-handshake.js + hash: sha256:a008c60a4afc98f03dc41219989f7306884254d802b0958282a29462c9fd0f72 + type: core + size: 3907 - path: core/ids/incremental-decision-engine.js hash: sha256:257b1f67f6df8eb91fe0a95405563611b8bf2f836cbca2398a0a394e40d6c219 type: core size: 21404 - path: core/ids/index.js - hash: sha256:0f3ac48411d378d184f01152eaefe1b9d87af5ef39fa51371cb07605c2f6a645 + hash: sha256:363ef37a0fcc18e9e2646650f74750a77c21a95e16de976f64f52825cc50a6a1 type: core - size: 3624 + size: 3785 - path: core/ids/layer-classifier.js hash: sha256:2a240b70ac3507e50a64b96d580c4d933bf2116125fb52c8237db2ed9ebf27b7 type: core @@ -1101,13 +1105,17 @@ files: type: core size: 16313 - path: core/synapse/context/index.js - hash: sha256:a67e843cf61e823e0c1b73efdb2d67d268a798fc0c25505a199c1da130d4fe36 + hash: sha256:6475a5730451d8754cc6c7ab4c1ef4decca98e093c69045661eae17da5897e07 type: core - size: 363 + size: 596 - path: core/synapse/context/README.md - hash: sha256:0376afd13b40d31338ecfdc327f872b2290e2f1736483b527fb1ae469f0c33dc + hash: sha256:3c9d4ac5a77f0a8a7d8e20c0bb30385036cc9a56f817be2d2d415a8c9813d08e + type: core + size: 2354 + - path: core/synapse/context/semantic-handshake-engine.js + hash: sha256:be5f2f7300902a2916f6758b46636b96f083c11e1b831f5835bf97859a4b80a6 type: core - size: 1380 + size: 15725 - path: core/synapse/diagnostics/collectors/consistency-collector.js hash: sha256:65f4255f87c9900400649dc8b9aedaac4851b5939d93e127778bd93cee99db12 type: core diff --git a/docs/stories/epic-483-semantic-handshake/EPIC-483-SEMANTIC-HANDSHAKE.md b/docs/stories/epic-483-semantic-handshake/EPIC-483-SEMANTIC-HANDSHAKE.md new file mode 100644 index 0000000000..70ff3057e3 --- /dev/null +++ b/docs/stories/epic-483-semantic-handshake/EPIC-483-SEMANTIC-HANDSHAKE.md @@ -0,0 +1,44 @@ +# Epic 483: Semantic Handshake + +## Metadata + +| Campo | Valor | +|-------|-------| +| Epic ID | 483 | +| Source Issue | #483 | +| Issue URL | https://github.com/SynkraAI/aiox-core/issues/483 | +| Status | In Progress | +| Priority | P2 | +| Repository | SynkraAI/aiox-core | + +## Problem + +O issue #483 descreve o gap entre planejamento e execução: decisões de arquitetura e restrições duras podem ser comprimidas, perdidas ou ignoradas quando a story passa de `@architect` para `@dev`. + +## Current Reality + +- Issue #447 foi resolvido pela Story 447.1 e entrega compactação hierárquica de contexto, mas não valida se o código proposto cumpre restrições de planejamento. +- PRs #564 e #565 citam Decision Memory e Agent Reflection, mas estão fechados sem merge e não existem na `main` atual. +- O runtime atual tem `G4DevContextGate`, mas ele é informativo e não bloqueia violações arquiteturais. + +## Goal + +Criar um protocolo determinístico de Semantic Handshake que transforme restrições de planejamento em contratos executáveis e permita bloquear a execução quando o código proposto violar `BLOCKER`s. + +## Stories + +| Story | Title | Status | Priority | Points | +|-------|-------|--------|----------|--------| +| 483.1 | Semantic Handshake Contract and Pre-Execution Gate | Ready for Review | P2 | 8 | + +## Non-Goals + +- Não importar o protótipo TypeScript do anexo como código de produção. +- Não depender de LLM para extrair restrições no primeiro slice. +- Não reabrir PRs antigos fechados; usar apenas evidência e padrões úteis. + +## Completion Criteria + +- Pelo menos uma story entrega engine, gate e testes determinísticos. +- A implementação é aditiva e exportada por superfícies existentes. +- Issue #483 pode ser atualizado com evidência objetiva do que foi resolvido e do que fica para slices futuros. diff --git a/docs/stories/epic-483-semantic-handshake/STORY-483.1-SEMANTIC-HANDSHAKE-CONTRACT-GATE.md b/docs/stories/epic-483-semantic-handshake/STORY-483.1-SEMANTIC-HANDSHAKE-CONTRACT-GATE.md new file mode 100644 index 0000000000..a978a00a96 --- /dev/null +++ b/docs/stories/epic-483-semantic-handshake/STORY-483.1-SEMANTIC-HANDSHAKE-CONTRACT-GATE.md @@ -0,0 +1,214 @@ +# Story 483.1: Semantic Handshake Contract and Pre-Execution Gate + +## Metadata + +| Campo | Valor | +|-------|-------| +| Story ID | 483.1 | +| Epic | [483 - Semantic Handshake](./EPIC-483-SEMANTIC-HANDSHAKE.md) | +| Status | Ready for Review | +| Executor | @dev | +| Quality Gate | @architect | +| quality_gate_tools | npm test focused, npm run lint, npm run typecheck, npm run validate:manifest | +| Points | 8 | +| Priority | P2 | +| Story Order | 1 | +| Source Issue | #483 | +| Issue URL | https://github.com/SynkraAI/aiox-core/issues/483 | +| Implementation Repository | SynkraAI/aiox-core | +| Start Gate | Confirmar que #447 cobre contexto, mas não cobre restrições executáveis. | + +## Status + +- [x] Draft +- [x] Ready for implementation +- [x] Ready for Review +- [ ] Done + +## Executor Assignment + +```yaml +executor: "@dev" +quality_gate: "@architect" +quality_gate_tools: + - npm test focused + - npm run lint + - npm run typecheck + - npm run validate:manifest +accountable: "pedro-valerio" +domain: "synapse-context" +deploy_type: "none" +``` + +## Community Origin + +**Discussion URL**: https://github.com/SynkraAI/aiox-core/issues/483 +**Author**: GitHub issue author +**Approved Date**: 2026-05-09 +**Approved By**: AIOX triage via live issue queue + +## Story + +**As a** framework maintainer, +**I want** a Semantic Handshake engine and pre-execution gate, +**so that** hard planning constraints survive context compression and block code that contradicts the approved architecture. + +## Contexto + +Issue #483 requests a middleware layer that treats architectural decisions as executable contracts. Current `main` has the Hierarchical Context Manager from #447, which keeps context available, but it does not convert constraints into validators. + +Evidence checked before implementation: + +- PR #706 / Story 447.1 is merged and covers hierarchical context compaction only. +- PRs #564 and #565 are closed without merge, so Decision Memory and Agent Reflection are not available on `main`. +- `G4DevContextGate` is intentionally non-blocking and only recommends reuse/adapt opportunities. +- The issue attachment is a TypeScript prototype; implementation must be repository-native CommonJS and deterministic. + +## Acceptance Criteria + +- [x] AC1: A canonical `SemanticHandshakeEngine` exists under the SYNAPSE context runtime and is exported from `.aiox-core/core/synapse/context`. +- [x] AC2: The engine supports `registerConstraints()`, `addConstraint()`, `validateExecutionIntent()`, `generateComplianceReport()` and `toContextMessage()` without live LLM calls. +- [x] AC3: Constraint extraction deterministically recognizes at least PostgreSQL, serverless local-state, absolute-import and no-eval planning rules. +- [x] AC4: Validation distinguishes `BLOCKER` from `WARNING`; blockers make the result fail, warnings appear in the report without blocking. +- [x] AC5: Validation accepts proposed code as a string or file list and returns verified constraints, violations, blocking violations and a correction prompt. +- [x] AC6: A `G5SemanticHandshakeGate` exists in IDS, uses the engine, and marks the gate blocking only when blocker violations are present. +- [x] AC7: The engine can emit an LLM-ready system context message with AIOX metadata so future dev loops can inject the handshake result. +- [x] AC8: Focused tests cover extraction, violation detection, warning behavior, compliance report generation, context message output and gate blocking semantics. +- [x] AC9: Documentation/story File List and Dev Agent Record are updated with implemented files and validation evidence. + +## CodeRabbit Integration + +### Story Type Analysis + +**Primary Type**: Core runtime governance feature +**Secondary Type(s)**: Context management, IDS gate, architecture compliance +**Complexity**: High + +### Specialized Agent Assignment + +**Primary Agents**: +- @dev +- @architect + +**Supporting Agents**: +- @qa +- @po + +### Quality Gate Tasks + +- [x] Pre-Commit (@dev): Run focused Semantic Handshake engine and gate tests. +- [x] Pre-PR (@devops): Confirm additive exports and manifest update. +- [ ] Architecture Review (@architect): Confirm this complements #447 instead of duplicating context management. + +## Tasks / Subtasks + +- [x] T1: Implement `SemanticHandshakeEngine` with deterministic extraction and structured constraint validation. +- [x] T2: Export the engine from SYNAPSE context runtime. +- [x] T3: Implement `G5SemanticHandshakeGate` as an IDS pre-execution gate. +- [x] T4: Export the gate from IDS runtime. +- [x] T5: Add focused tests for engine behavior and gate behavior. +- [x] T6: Update manifest if package files change. +- [x] T7: Run focused tests, lint/typecheck where feasible, and manifest validation. + +## Dev Notes + +- Keep the implementation additive. +- Do not add an LLM dependency. +- Do not treat every architectural mismatch as a blocker by default; only constraints marked `BLOCKER` should block. +- Prefer plain CommonJS modules matching the current runtime style. + +## Testing + +Required focused validation: + +```bash +npm test -- --runTestsByPath tests/synapse/semantic-handshake-engine.test.js tests/core/ids/semantic-handshake-gate.test.js +npm run validate:manifest +npm run lint +npm run typecheck +``` + +## Dependencies + +- **Depends on:** Story 447.1 context runtime export surface. +- **Blocks:** future dev-loop automatic injection of Semantic Handshake reports. +- **Related:** Issue #482, because persistence/evolution can later consume handshake violations as learning events. + +## Definition of Ready + +- [x] Current code reality checked. +- [x] Source issue and attachment reviewed. +- [x] Acceptance criteria are testable without external services. + +## Definition of Done + +- [x] All ACs complete. +- [x] Focused tests pass. +- [x] Manifest validation passes. +- [x] Story File List and Dev Agent Record are updated. + +## Dev Agent Record + +### Debug Log + +- Draft created from issue #483 and current repo inspection on 2026-05-09. +- Confirmed #447 is complete but does not enforce executable constraints. +- Confirmed PRs #564/#565 are closed without merge and are not available in current `main`. +- Reviewed attached `Semantic.Handshake.js` prototype and mapped it to repo-native CommonJS surfaces. +- Implemented `SemanticHandshakeEngine` with deterministic extraction for PostgreSQL, serverless local-state, absolute imports and no-eval constraints. +- Implemented `G5SemanticHandshakeGate` as a blocking IDS gate for `BLOCKER` violations. +- Updated SYNAPSE context and IDS barrel exports. +- Documented Semantic Handshake usage in the SYNAPSE context runtime README. +- Regenerated install manifest to include the new runtime files. +- Validation completed: + - `npm test -- --runTestsByPath tests/synapse/semantic-handshake-engine.test.js tests/core/ids/semantic-handshake-gate.test.js` + - `npm test -- --runTestsByPath tests/synapse/hierarchical-context-manager.test.js tests/core/ids/verification-gates.test.js tests/synapse/semantic-handshake-engine.test.js tests/core/ids/semantic-handshake-gate.test.js` + - `npx eslint .aiox-core/core/synapse/context/semantic-handshake-engine.js .aiox-core/core/ids/gates/g5-semantic-handshake.js tests/synapse/semantic-handshake-engine.test.js tests/core/ids/semantic-handshake-gate.test.js` + - `npm run validate:manifest` + - `npm run lint` + - `npm run typecheck` +- CodeRabbit review fixes completed: + - `G5SemanticHandshakeGate` now verifies against a scoped engine clone so context constraints do not leak between gate runs. + - `SemanticHandshakeEngine.addConstraint()` now rejects constraints whose normalized id is empty. + - Custom validator exceptions now become deterministic blocker violations with validator-error metadata. + - SYNAPSE context exports now use `path.resolve()` for the Semantic Handshake export surface. +- Post-review validation completed: + - `npm test -- --runTestsByPath tests/synapse/semantic-handshake-engine.test.js tests/core/ids/semantic-handshake-gate.test.js` + - `npx eslint .aiox-core/core/synapse/context/semantic-handshake-engine.js .aiox-core/core/ids/gates/g5-semantic-handshake.js .aiox-core/core/synapse/context/index.js tests/synapse/semantic-handshake-engine.test.js tests/core/ids/semantic-handshake-gate.test.js` + - `npm run generate:manifest && npm run validate:manifest` + - `npm run typecheck` + - `npm run lint` + +### Agent Model Used + +- GPT-5 Codex + +### Completion Notes + +- Semantic Handshake now has a deterministic contract engine and a blocking pre-execution IDS gate. +- The engine is additive, offline-safe and returns both compliance reports and LLM-ready system context messages for future dev-loop injection. +- Gate behavior is conservative: missing proposed code warns and proceeds, `WARNING` violations report without blocking, and only `BLOCKER` violations make the gate blocking. +- Post-review hardening prevents cross-run constraint leakage, empty normalized IDs and thrown custom validators from creating nondeterministic gate behavior. + +### File List + +| File | Action | +|------|--------| +| `docs/stories/epic-483-semantic-handshake/EPIC-483-SEMANTIC-HANDSHAKE.md` | Created | +| `docs/stories/epic-483-semantic-handshake/STORY-483.1-SEMANTIC-HANDSHAKE-CONTRACT-GATE.md` | Created | +| `.aiox-core/core/synapse/context/semantic-handshake-engine.js` | Created | +| `.aiox-core/core/synapse/context/index.js` | Updated | +| `.aiox-core/core/synapse/context/README.md` | Updated | +| `.aiox-core/core/ids/gates/g5-semantic-handshake.js` | Created | +| `.aiox-core/core/ids/index.js` | Updated | +| `tests/synapse/semantic-handshake-engine.test.js` | Created | +| `tests/core/ids/semantic-handshake-gate.test.js` | Created | +| `.aiox-core/install-manifest.yaml` | Updated | + +## Change Log + +| Data | Agente | Mudança | +|------|--------|---------| +| 2026-05-09 | @sm / Codex | Epic e story criados a partir do issue #483, do issue attachment e da leitura da implementação atual. | +| 2026-05-09 | @dev (Dex) | Implementados engine, gate, exports, testes focados e manifesto para o Semantic Handshake. | +| 2026-05-09 | @dev (Dex) | Aplicados ajustes de CodeRabbit para isolamento do gate, validação de IDs e erro determinístico de validators. | diff --git a/tests/core/ids/semantic-handshake-gate.test.js b/tests/core/ids/semantic-handshake-gate.test.js new file mode 100644 index 0000000000..0114b78696 --- /dev/null +++ b/tests/core/ids/semantic-handshake-gate.test.js @@ -0,0 +1,114 @@ +'use strict'; + +const { + G5SemanticHandshakeGate, + G5_DEFAULT_TIMEOUT_MS, +} = require('../../../.aiox-core/core/ids/gates/g5-semantic-handshake'); +const { SemanticHandshakeEngine } = require('../../../.aiox-core/core/synapse/context'); + +function createLogger() { + return { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + log: jest.fn(), + }; +} + +describe('G5SemanticHandshakeGate', () => { + test('is exported with a default timeout', () => { + expect(G5SemanticHandshakeGate).toBeDefined(); + expect(G5_DEFAULT_TIMEOUT_MS).toBe(2000); + }); + + test('blocks @dev execution when blocker constraints fail', async () => { + const gate = new G5SemanticHandshakeGate({ logger: createLogger() }); + + const result = await gate.verify({ + storyId: '483.1', + planningText: 'Use serverless architecture with PostgreSQL.', + files: [ + { + path: 'src/handler.js', + content: ` + const sqlite = require('sqlite'); + const fs = require('fs'); + fs.writeFileSync('state.json', '{}'); + `, + }, + ], + }); + + expect(result.result.passed).toBe(false); + expect(result.result.blocking).toBe(true); + expect(result.result.warnings).toEqual(expect.arrayContaining([ + 'Must use PostgreSQL adapter, not SQLite or another local database.', + 'Serverless architecture must not write runtime state to the local filesystem.', + ])); + expect(result.override.reason).toBe('Semantic Handshake blocker violation'); + expect(result.override.correctionPrompt).toContain('Blocking constraints'); + }); + + test('passes when constraints are verified or not applicable', async () => { + const gate = new G5SemanticHandshakeGate({ logger: createLogger() }); + + const result = await gate.verify({ + storyId: '483.1', + architectureText: 'Use PostgreSQL and serverless.', + proposedCode: ` + const { Pool } = require('pg'); + module.exports = new Pool({ connectionString: process.env.DATABASE_URL }); + `, + }); + + expect(result.result.passed).toBe(true); + expect(result.result.blocking).toBe(false); + expect(result.result.opportunities.some(item => item.entity === 'TECH-POSTGRESQL')).toBe(true); + }); + + test('warns and proceeds when no proposed code is available yet', async () => { + const gate = new G5SemanticHandshakeGate({ logger: createLogger() }); + + const result = await gate.verify({ + storyId: '483.1', + planningText: 'Use absolute imports only.', + }); + + expect(result.result.passed).toBe(true); + expect(result.result.blocking).toBe(false); + expect(result.result.warnings).toContain( + 'Semantic Handshake constraints registered, but no proposed code was provided', + ); + + const nextResult = await gate.verify({ + proposedCode: "const helper = require('../relative-helper');", + }); + + expect(nextResult.result.passed).toBe(true); + expect(nextResult.result.warnings).toContain('No Semantic Handshake constraints registered'); + }); + + test('accepts an injected engine and structured constraints', async () => { + const engine = new SemanticHandshakeEngine(); + const gate = new G5SemanticHandshakeGate({ engine, logger: createLogger() }); + + const result = await gate.verify({ + constraints: [ + { + id: 'SEC-NO-DYNAMIC-FUNCTION', + severity: 'BLOCKER', + description: 'Do not use dynamic Function constructors.', + forbiddenPatterns: [/new\s+Function\s*\(/], + }, + ], + proposedCode: 'const fn = new Function("return true");', + }); + + expect(result.result.passed).toBe(false); + expect(result.result.blocking).toBe(true); + expect(result.result.opportunities[0]).toMatchObject({ + entity: 'SEC-NO-DYNAMIC-FUNCTION', + severity: 'BLOCKER', + }); + }); +}); diff --git a/tests/synapse/semantic-handshake-engine.test.js b/tests/synapse/semantic-handshake-engine.test.js new file mode 100644 index 0000000000..cacd06a21d --- /dev/null +++ b/tests/synapse/semantic-handshake-engine.test.js @@ -0,0 +1,184 @@ +const { + SemanticHandshakeEngine, + ConstraintSeverity, + ConstraintType, +} = require('aiox-core/core/synapse/context'); + +describe('SemanticHandshakeEngine', () => { + test('exports the engine from the SYNAPSE context surface', () => { + expect(SemanticHandshakeEngine).toBeDefined(); + expect(typeof SemanticHandshakeEngine).toBe('function'); + expect(ConstraintSeverity.BLOCKER).toBe('BLOCKER'); + expect(ConstraintType.TECH_STACK).toBe('TECH_STACK'); + }); + + test('extracts PostgreSQL and serverless constraints from planning text', () => { + const engine = new SemanticHandshakeEngine(); + const constraints = engine.registerConstraints( + 'Architecture: use a serverless runtime with PostgreSQL for persistence.', + ); + + expect(constraints.map(constraint => constraint.id)).toEqual([ + 'TECH-POSTGRESQL', + 'ARCH-SERVERLESS-STATE', + ]); + expect(engine.getConstraints()).toHaveLength(2); + }); + + test('blocks code that violates extracted hard constraints', async () => { + const engine = new SemanticHandshakeEngine(); + engine.registerConstraints('Use serverless functions and PostgreSQL.'); + + const result = await engine.validateExecutionIntent({ + files: [ + { + path: 'src/db.js', + content: ` + const sqlite = require('sqlite'); + const fs = require('fs'); + fs.writeFileSync('state.json', JSON.stringify({ ok: true })); + `, + }, + ], + }); + + expect(result.passed).toBe(false); + expect(result.blockingViolations).toHaveLength(2); + expect(result.blockingViolations.map(violation => violation.id)).toEqual([ + 'TECH-POSTGRESQL', + 'ARCH-SERVERLESS-STATE', + ]); + expect(result.correctionPrompt).toContain('Semantic Handshake failed'); + }); + + test('verifies compliant code and constraints without local-state violations', async () => { + const engine = new SemanticHandshakeEngine(); + engine.registerConstraints('Use PostgreSQL and serverless architecture.'); + + const result = await engine.validateExecutionIntent({ + files: [ + { + path: 'src/db.js', + content: ` + const { Pool } = require('pg'); + module.exports = new Pool({ connectionString: process.env.DATABASE_URL }); + `, + }, + ], + }); + + expect(result.passed).toBe(true); + expect(result.verifiedConstraints.map(constraint => constraint.id)).toContain('TECH-POSTGRESQL'); + expect(result.verifiedConstraints.map(constraint => constraint.id)).toContain('ARCH-SERVERLESS-STATE'); + }); + + test('warning constraints report violations without blocking execution', async () => { + const engine = new SemanticHandshakeEngine({ + constraints: [ + { + id: 'OBS-CONSOLE', + source: '@architect', + type: 'PATTERN', + severity: 'WARNING', + description: 'Avoid console logging in production paths.', + forbiddenPatterns: [/console\.log\s*\(/], + }, + ], + }); + + const result = await engine.validateExecutionIntent('console.log("debug");'); + + expect(result.passed).toBe(true); + expect(result.violations).toHaveLength(1); + expect(result.blockingViolations).toHaveLength(0); + expect(result.warnings[0]).toContain('Avoid console logging'); + }); + + test('detects absolute import and no-eval violations', async () => { + const engine = new SemanticHandshakeEngine(); + engine.registerConstraints('Coding standards: absolute imports only. Security: no eval.'); + + const result = await engine.validateExecutionIntent({ + path: 'src/example.js', + proposedCode: ` + const helper = require('../utils/helper'); + eval('2 + 2'); + module.exports = helper; + `, + }); + + expect(result.passed).toBe(false); + expect(result.blockingViolations.map(violation => violation.id)).toEqual([ + 'IMPORT-ABSOLUTE', + 'SEC-NO-EVAL', + ]); + }); + + test('supports custom validators and LLM-ready context messages', async () => { + const engine = new SemanticHandshakeEngine(); + engine.addConstraint({ + id: 'CUSTOM-NO-TODO', + severity: 'BLOCKER', + description: 'Implementation must not leave TODO markers.', + validator: ({ codeContext }) => ({ + passed: !codeContext.includes('TODO'), + message: 'Remove TODO markers before execution.', + }), + }); + + const result = await engine.validateExecutionIntent('// TODO: finish later'); + const report = engine.generateComplianceReport(result); + const message = engine.toContextMessage(result); + + expect(result.passed).toBe(false); + expect(report).toContain('Status: FAILED'); + expect(message).toMatchObject({ + role: 'system', + metadata: { + aiox: { + type: 'semantic_handshake_report', + passed: false, + blockingViolationCount: 1, + }, + }, + }); + expect(message.content).toContain('Remove TODO markers'); + }); + + test('rejects constraints whose id normalizes to an empty value', () => { + const engine = new SemanticHandshakeEngine(); + + expect(() => engine.addConstraint({ + id: '---', + description: '---', + })).toThrow('constraint id normalizes to an empty value'); + }); + + test('turns thrown validator errors into deterministic violations', async () => { + const engine = new SemanticHandshakeEngine(); + engine.addConstraint({ + id: 'CUSTOM-VALIDATOR', + severity: 'BLOCKER', + description: 'Validator must be deterministic.', + validator: () => { + throw new Error('boom'); + }, + }); + + const result = await engine.validateExecutionIntent('const ok = true;'); + + expect(result.passed).toBe(false); + expect(result.blockingViolations).toHaveLength(1); + expect(result.blockingViolations[0]).toMatchObject({ + id: 'CUSTOM-VALIDATOR', + message: 'Constraint validator failed: boom', + matches: ['validator-error'], + metadata: { + validatorError: { + message: 'boom', + }, + }, + }); + expect(result.correctionPrompt).toContain('CUSTOM-VALIDATOR'); + }); +});