Skip to content

Commit ed51ece

Browse files
committed
feat(context): add semantic handshake gate [Story 483.1]
1 parent 7bd2029 commit ed51ece

10 files changed

Lines changed: 1196 additions & 9 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict';
2+
3+
/**
4+
* G5 — Semantic Handshake Gate
5+
*
6+
* Agent: @dev
7+
* Type: Automated, Blocking when Semantic Handshake reports BLOCKER violations
8+
*
9+
* Purpose: Preserve intent integrity between planning and implementation by
10+
* evaluating proposed code against executable planning constraints.
11+
*/
12+
13+
const path = require('path');
14+
const { VerificationGate } = require(path.resolve(__dirname, '../verification-gate.js'));
15+
const {
16+
SemanticHandshakeEngine,
17+
} = require(path.resolve(__dirname, '../../synapse/context/semantic-handshake-engine.js'));
18+
19+
const G5_DEFAULT_TIMEOUT_MS = 2000;
20+
21+
class G5SemanticHandshakeGate extends VerificationGate {
22+
constructor(options = {}) {
23+
super({
24+
gateId: 'G5',
25+
agent: '@dev',
26+
blocking: true,
27+
timeoutMs: options.timeoutMs ?? G5_DEFAULT_TIMEOUT_MS,
28+
circuitBreakerOptions: options.circuitBreakerOptions,
29+
logger: options.logger,
30+
});
31+
32+
this._engine = options.engine || new SemanticHandshakeEngine({
33+
constraints: options.constraints || [],
34+
});
35+
}
36+
37+
async _doVerify(context = {}) {
38+
this._registerContextConstraints(context);
39+
40+
const constraints = this._engine.getConstraints();
41+
if (constraints.length === 0) {
42+
return {
43+
passed: true,
44+
warnings: ['No Semantic Handshake constraints registered'],
45+
opportunities: [],
46+
};
47+
}
48+
49+
if (!this._hasCodeContext(context)) {
50+
return {
51+
passed: true,
52+
warnings: ['Semantic Handshake constraints registered, but no proposed code was provided'],
53+
opportunities: constraints.map(constraint => ({
54+
entity: constraint.id,
55+
recommendation: constraint.description,
56+
severity: constraint.severity,
57+
})),
58+
};
59+
}
60+
61+
const result = await this._engine.validateExecutionIntent(context);
62+
const report = this._engine.generateComplianceReport(result);
63+
64+
return {
65+
passed: result.passed,
66+
warnings: [
67+
...result.warnings,
68+
...result.blockingViolations.map(violation => violation.message),
69+
],
70+
opportunities: [
71+
...result.verifiedConstraints.map(constraint => ({
72+
entity: constraint.id,
73+
recommendation: 'Constraint verified',
74+
severity: constraint.severity,
75+
})),
76+
...result.violations.map(violation => ({
77+
entity: violation.id,
78+
recommendation: violation.message,
79+
severity: violation.severity,
80+
matches: violation.matches,
81+
})),
82+
],
83+
override: result.passed ? null : {
84+
reason: 'Semantic Handshake blocker violation',
85+
correctionPrompt: result.correctionPrompt,
86+
report,
87+
},
88+
};
89+
}
90+
91+
_registerContextConstraints(context) {
92+
const planningText = [
93+
context.planningOutput,
94+
context.planningText,
95+
context.architectureText,
96+
context.storyText,
97+
].filter(Boolean).join('\n\n');
98+
99+
if (planningText.trim()) {
100+
this._engine.registerConstraints(planningText, {
101+
source: context.source || '@architect',
102+
metadata: {
103+
storyId: context.storyId || 'unknown',
104+
},
105+
});
106+
}
107+
108+
if (Array.isArray(context.constraints)) {
109+
for (const constraint of context.constraints) {
110+
this._engine.addConstraint(constraint);
111+
}
112+
}
113+
}
114+
115+
_hasCodeContext(context) {
116+
return Boolean(
117+
context.proposedCode ||
118+
(Array.isArray(context.files) && context.files.length > 0) ||
119+
(Array.isArray(context.diffs) && context.diffs.length > 0) ||
120+
(Array.isArray(context.codeFiles) && context.codeFiles.length > 0),
121+
);
122+
}
123+
}
124+
125+
module.exports = {
126+
G5SemanticHandshakeGate,
127+
G5_DEFAULT_TIMEOUT_MS,
128+
};

.aiox-core/core/ids/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ const { G1EpicCreationGate } = require('./gates/g1-epic-creation');
8484
const { G2StoryCreationGate } = require('./gates/g2-story-creation');
8585
const { G3StoryValidationGate } = require('./gates/g3-story-validation');
8686
const { G4DevContextGate, G4_DEFAULT_TIMEOUT_MS } = require('./gates/g4-dev-context');
87+
const {
88+
G5SemanticHandshakeGate,
89+
G5_DEFAULT_TIMEOUT_MS,
90+
} = require('./gates/g5-semantic-handshake');
8791

8892
// IDS-7: Framework Governor (aiox-master integration)
8993
const {
@@ -142,12 +146,14 @@ module.exports = {
142146
createGateResult,
143147
DEFAULT_TIMEOUT_MS,
144148

145-
// IDS-5a: Gates G1-G4
149+
// IDS-5a/5b: Gates G1-G5
146150
G1EpicCreationGate,
147151
G2StoryCreationGate,
148152
G3StoryValidationGate,
149153
G4DevContextGate,
150154
G4_DEFAULT_TIMEOUT_MS,
155+
G5SemanticHandshakeGate,
156+
G5_DEFAULT_TIMEOUT_MS,
151157

152158
// IDS-7: Framework Governor
153159
FrameworkGovernor,

.aiox-core/core/synapse/context/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,34 @@ The manager does not call OpenAI, Anthropic, Claude, Gemini, Kimi or OpenRouter
3333
directly. Inject a summarizer when an agent loop wants model-based compression.
3434
If that summarizer fails, the manager emits `swap:error` and falls back to a
3535
local extractive summary instead of crashing the loop.
36+
37+
## SemanticHandshakeEngine
38+
39+
`SemanticHandshakeEngine` turns planning constraints into executable checks that
40+
can run before implementation. It is deterministic by default and does not call
41+
an LLM.
42+
43+
```javascript
44+
const { SemanticHandshakeEngine } = require('./.aiox-core/core/synapse/context');
45+
46+
const handshake = new SemanticHandshakeEngine();
47+
handshake.registerConstraints('Use serverless functions with PostgreSQL.');
48+
49+
const result = await handshake.validateExecutionIntent({
50+
files: [
51+
{
52+
path: 'src/db.js',
53+
content: "const { Pool } = require('pg');",
54+
},
55+
],
56+
});
57+
58+
if (!result.passed) {
59+
throw new Error(handshake.generateComplianceReport(result));
60+
}
61+
```
62+
63+
The first supported extraction rules cover PostgreSQL, serverless local-state
64+
writes, absolute imports and `eval` bans. Callers can also add structured custom
65+
constraints with `addConstraint()`. Use `toContextMessage()` to inject the
66+
handshake result into an agent context as a system message.

.aiox-core/core/synapse/context/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
const contextTracker = require('./context-tracker');
1010
const contextBuilder = require('./context-builder');
1111
const hierarchicalContext = require('./hierarchical-context-manager');
12+
const semanticHandshake = require('./semantic-handshake-engine');
1213

1314
module.exports = {
1415
...contextTracker,
1516
...contextBuilder,
1617
...hierarchicalContext,
18+
...semanticHandshake,
1719
};

0 commit comments

Comments
 (0)