Skip to content

Commit be17cc6

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

10 files changed

Lines changed: 1309 additions & 12 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
const engine = this._createScopedEngine(context);
39+
40+
const constraints = 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 engine.validateExecutionIntent(context);
62+
const report = 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+
_createScopedEngine(context) {
92+
const engine = typeof this._engine.clone === 'function'
93+
? this._engine.clone()
94+
: new SemanticHandshakeEngine();
95+
96+
this._registerContextConstraints(engine, context);
97+
return engine;
98+
}
99+
100+
_registerContextConstraints(engine, context) {
101+
const planningText = [
102+
context.planningOutput,
103+
context.planningText,
104+
context.architectureText,
105+
context.storyText,
106+
].filter(Boolean).join('\n\n');
107+
108+
if (planningText.trim()) {
109+
engine.registerConstraints(planningText, {
110+
source: context.source || '@architect',
111+
metadata: {
112+
storyId: context.storyId || 'unknown',
113+
},
114+
});
115+
}
116+
117+
if (Array.isArray(context.constraints)) {
118+
for (const constraint of context.constraints) {
119+
engine.addConstraint(constraint);
120+
}
121+
}
122+
}
123+
124+
_hasCodeContext(context) {
125+
return Boolean(
126+
context.proposedCode ||
127+
(Array.isArray(context.files) && context.files.length > 0) ||
128+
(Array.isArray(context.diffs) && context.diffs.length > 0) ||
129+
(Array.isArray(context.codeFiles) && context.codeFiles.length > 0),
130+
);
131+
}
132+
}
133+
134+
module.exports = {
135+
G5SemanticHandshakeGate,
136+
G5_DEFAULT_TIMEOUT_MS,
137+
};

.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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66

77
'use strict';
88

9-
const contextTracker = require('./context-tracker');
10-
const contextBuilder = require('./context-builder');
11-
const hierarchicalContext = require('./hierarchical-context-manager');
9+
const path = require('path');
10+
11+
const contextTracker = require(path.resolve(__dirname, './context-tracker.js'));
12+
const contextBuilder = require(path.resolve(__dirname, './context-builder.js'));
13+
const hierarchicalContext = require(path.resolve(__dirname, './hierarchical-context-manager.js'));
14+
const semanticHandshake = require(path.resolve(__dirname, './semantic-handshake-engine.js'));
1215

1316
module.exports = {
1417
...contextTracker,
1518
...contextBuilder,
1619
...hierarchicalContext,
20+
...semanticHandshake,
1721
};

0 commit comments

Comments
 (0)