Skip to content

Commit c245e2f

Browse files
authored
refactor: migrate template simulations to TypeScript (#993)
## Summary - migrate consensus-gate simulation to strict TypeScript while preserving its CommonJS API and behavior - migrate two-stage validation simulation to a shared typed stage engine - isolate simulation runtime ports so the consensus path does not load hook/task executors ## Validation - `npm test` — 2,897 passing, 18 pending - `npm pack --dry-run --json` - focused consensus/two-stage tests — 17 passing - deep template validation — 27 templates valid - strict typecheck, lint, Prettier, Opcore introduced-change and clone checks
1 parent b60c178 commit c245e2f

16 files changed

Lines changed: 931 additions & 594 deletions

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,15 @@ src/providers/openai/index.js
184184
src/providers/opencode/index.js
185185
src/schemas/sub-cluster.js
186186
src/template-validation/report-formatter.js
187+
src/template-validation/consensus-gate-contracts.js
188+
src/template-validation/consensus-gate-stage.js
189+
src/template-validation/consensus-gate-scenarios.js
190+
src/template-validation/simulate-consensus-gates.js
191+
src/template-validation/simulate-two-stage-validation.js
192+
src/template-validation/simulation-runtime.js
193+
src/template-validation/two-stage-agent.js
194+
src/template-validation/two-stage-contracts.js
195+
src/template-validation/two-stage-inputs.js
196+
src/template-validation/two-stage-results.js
197+
src/template-validation/two-stage-runtime.js
198+
src/template-validation/two-stage-scenario.js

eslint.config.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,18 @@ export default [
433433
'src/providers/opencode/index.js',
434434
'src/schemas/sub-cluster.js',
435435
'src/template-validation/report-formatter.js',
436+
'src/template-validation/consensus-gate-contracts.js',
437+
'src/template-validation/consensus-gate-stage.js',
438+
'src/template-validation/consensus-gate-scenarios.js',
439+
'src/template-validation/simulate-consensus-gates.js',
440+
'src/template-validation/simulate-two-stage-validation.js',
441+
'src/template-validation/simulation-runtime.js',
442+
'src/template-validation/two-stage-agent.js',
443+
'src/template-validation/two-stage-contracts.js',
444+
'src/template-validation/two-stage-inputs.js',
445+
'src/template-validation/two-stage-results.js',
446+
'src/template-validation/two-stage-runtime.js',
447+
'src/template-validation/two-stage-scenario.js',
436448
],
437449
},
438450
prettierConfig,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
export type StageStartTopic = 'IMPLEMENTATION_READY' | 'QUICK_VALIDATION_PASSED';
2+
export type TopicProducers = Map<string, Set<string>>;
3+
export type ConsensusProducers = [string, string, ...string[]];
4+
5+
export interface RequiredQualityGate {
6+
id: string;
7+
scope?: string;
8+
}
9+
10+
interface CompletionHook {
11+
action?: string;
12+
config?: { topic?: unknown };
13+
}
14+
15+
export interface TemplateTrigger {
16+
topic?: string;
17+
action?: string;
18+
logic?: { script?: string };
19+
}
20+
21+
export interface TemplateAgent {
22+
id: string;
23+
role?: string;
24+
requiredQualityGates?: RequiredQualityGate[];
25+
triggers?: TemplateTrigger[];
26+
hooks?: { onComplete?: CompletionHook };
27+
}
28+
29+
export interface TemplateConfig {
30+
agents?: TemplateAgent[];
31+
}
32+
33+
export interface ConsensusSimulationOptions {
34+
allowExternalTopics?: string[];
35+
}
36+
37+
interface SimulationClusterAgent {
38+
id: string;
39+
role: string | undefined;
40+
requiredQualityGates: RequiredQualityGate[];
41+
}
42+
43+
export interface SimulationCluster {
44+
id: string;
45+
agents: SimulationClusterAgent[];
46+
}
47+
48+
export interface SimulationContext {
49+
agents: TemplateAgent[];
50+
producersByTopic: TopicProducers;
51+
allowExternalTopics: string[];
52+
cluster: SimulationCluster;
53+
}
54+
55+
export interface ScenarioContext {
56+
agentId: string;
57+
cluster: SimulationCluster;
58+
topic: string;
59+
script: string;
60+
requiredQualityGates: RequiredQualityGate[];
61+
producers: ConsensusProducers;
62+
producersByTopic: TopicProducers;
63+
requiredStageTopics: StageStartTopic[];
64+
allowExternalTopics: string[];
65+
}
66+
67+
export type ConsensusScenario =
68+
| { failure: string }
69+
| { failure: null }
70+
| { failure: null; context: ScenarioContext };
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import simulationRuntime = require('./simulation-runtime');
2+
import { publishStageStartMessages } from './consensus-gate-stage';
3+
import type {
4+
ConsensusProducers,
5+
RequiredQualityGate,
6+
ScenarioContext,
7+
} from './consensus-gate-contracts';
8+
9+
interface SimulationMessageBus {
10+
publish(message: unknown): unknown;
11+
}
12+
13+
interface EvaluateScenarioOptions extends ScenarioContext {
14+
publishMessages(
15+
messageBus: SimulationMessageBus,
16+
producers: ConsensusProducers,
17+
clusterId: string
18+
): void;
19+
}
20+
21+
interface PassingQualityGate {
22+
id: string;
23+
status: 'PASS';
24+
scope: string;
25+
completedAt: number;
26+
evidence: {
27+
command: string;
28+
exitCode: 0;
29+
output: string;
30+
};
31+
}
32+
33+
interface ApprovedResultData {
34+
approved: true;
35+
qualityGates?: PassingQualityGate[];
36+
}
37+
38+
function evaluateScenario({
39+
agentId,
40+
cluster,
41+
topic,
42+
script,
43+
requiredQualityGates,
44+
producers,
45+
producersByTopic,
46+
requiredStageTopics,
47+
allowExternalTopics,
48+
publishMessages,
49+
}: EvaluateScenarioOptions): boolean {
50+
const { ledger, messageBus, logicEngine } = simulationRuntime.createSimulationRuntime(cluster);
51+
52+
publishStageStartMessages({
53+
messageBus,
54+
clusterId: cluster.id,
55+
producersByTopic,
56+
requiredStageTopics,
57+
allowExternalTopics,
58+
});
59+
publishMessages(messageBus, producers, cluster.id);
60+
61+
const result = logicEngine.evaluate(
62+
script,
63+
{ id: agentId, cluster_id: cluster.id, requiredQualityGates },
64+
{ topic }
65+
);
66+
ledger.close();
67+
return result;
68+
}
69+
70+
function getPassingQualityGate(requiredGate: RequiredQualityGate): PassingQualityGate {
71+
const scope = requiredGate.scope || 'template-sim';
72+
return {
73+
id: requiredGate.id,
74+
status: 'PASS',
75+
scope,
76+
completedAt: Date.now(),
77+
evidence: {
78+
command: `quality-check --scope ${scope}`,
79+
exitCode: 0,
80+
output: 'template simulation quality pass',
81+
},
82+
};
83+
}
84+
85+
function getApprovedResultData(context: ScenarioContext): ApprovedResultData {
86+
const data: ApprovedResultData = { approved: true };
87+
const requiredQualityGates = Array.isArray(context.requiredQualityGates)
88+
? context.requiredQualityGates
89+
: [];
90+
if (
91+
context.agentId === 'git-pusher' &&
92+
context.topic === 'VALIDATION_RESULT' &&
93+
requiredQualityGates.length > 0
94+
) {
95+
data.qualityGates = requiredQualityGates.map(getPassingQualityGate);
96+
}
97+
return data;
98+
}
99+
100+
export function checkDuplicateProducerScenario(context: ScenarioContext): boolean {
101+
return evaluateScenario({
102+
...context,
103+
publishMessages(messageBus, producers, clusterId) {
104+
const message = {
105+
cluster_id: clusterId,
106+
topic: context.topic,
107+
sender: producers[0],
108+
content: { data: getApprovedResultData(context) },
109+
};
110+
messageBus.publish(message);
111+
messageBus.publish(message);
112+
},
113+
});
114+
}
115+
116+
export function checkDistinctProducerScenario(context: ScenarioContext): boolean {
117+
return evaluateScenario({
118+
...context,
119+
publishMessages(messageBus, producers, clusterId) {
120+
for (const producer of producers) {
121+
messageBus.publish({
122+
cluster_id: clusterId,
123+
topic: context.topic,
124+
sender: producer,
125+
content: { data: getApprovedResultData(context) },
126+
});
127+
}
128+
},
129+
});
130+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { StageStartTopic, TopicProducers } from './consensus-gate-contracts';
2+
3+
const STAGE_START_TOPICS = [
4+
'IMPLEMENTATION_READY',
5+
'QUICK_VALIDATION_PASSED',
6+
] as const satisfies readonly StageStartTopic[];
7+
8+
const EXTERNAL_STAGE_SENDERS: Readonly<Record<StageStartTopic, string>> = {
9+
IMPLEMENTATION_READY: 'worker',
10+
QUICK_VALIDATION_PASSED: 'consensus-coordinator',
11+
};
12+
13+
interface SimulationMessageBus {
14+
publish(message: unknown): unknown;
15+
}
16+
17+
interface PublishStageStartOptions {
18+
messageBus: SimulationMessageBus;
19+
clusterId: string;
20+
producersByTopic: TopicProducers;
21+
requiredStageTopics: StageStartTopic[];
22+
allowExternalTopics: string[];
23+
}
24+
25+
function scriptReferencesTopic(logicScript: string, topic: string): boolean {
26+
return logicScript.includes(`topic: '${topic}'`) || logicScript.includes(`topic: "${topic}"`);
27+
}
28+
29+
export function getRequiredStageTopics(logicScript: string): StageStartTopic[] {
30+
return STAGE_START_TOPICS.filter((topic) => scriptReferencesTopic(logicScript, topic));
31+
}
32+
33+
export function publishStageStartMessages({
34+
messageBus,
35+
clusterId,
36+
producersByTopic,
37+
requiredStageTopics,
38+
allowExternalTopics,
39+
}: PublishStageStartOptions): void {
40+
let timestamp = Date.now();
41+
42+
for (const topic of requiredStageTopics) {
43+
const producers = Array.from(producersByTopic.get(topic) || []);
44+
const sender =
45+
producers.at(0) ??
46+
(allowExternalTopics.includes(topic) ? EXTERNAL_STAGE_SENDERS[topic] : null);
47+
if (!sender) continue;
48+
49+
messageBus.publish({
50+
cluster_id: clusterId,
51+
topic,
52+
sender,
53+
timestamp: timestamp++,
54+
});
55+
}
56+
}

0 commit comments

Comments
 (0)