@tea-agent/loop-agent 0.35.1-beta.2 → 0.35.1
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.
- package/AGENTS.md +0 -2
- package/CHANGELOG.md +25 -24
- package/bin/loop-agent.js +1 -37
- package/dist/application/dag/generate-task-dag.js +4 -1
- package/dist/application/task-lifecycle/advance.js +14 -0
- package/dist/cli/program.js +2 -2
- package/dist/commands/task-advance.js +1 -0
- package/dist/executors/dag-pi-executor.js +0 -44
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/config-types.js +2 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/source-prepare/build-draft.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +37 -10
- package/dist/task/task-demand-routing.js +10 -0
- package/dist/worker/console/operator-actions.js +72 -6
- package/dist/worker/console/prd-intake-bridge.js +10 -3
- package/dist/worker/console/prd-reference-discovery.js +124 -0
- package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
- package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
- package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +26 -41
- package/dist/workflows/dag/node-execution.js +0 -89
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/runner.js +11 -245
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +2 -130
- package/package.json +2 -2
- package/dist/build-stamp.json +0 -6
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
|
@@ -1,125 +1,9 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
3
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
-
import {
|
|
4
|
+
import { frontendImplementationContractSchema, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
|
|
6
5
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
7
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
8
|
-
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
9
|
-
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
10
|
-
/**
|
|
11
|
-
* Machine-readable failure classification for blocked/retryable-invalid
|
|
12
|
-
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
13
|
-
* downstream routing (repair prompting, dashboards, retry policies) can act on
|
|
14
|
-
* a stable code instead of parsing prose.
|
|
15
|
-
*/
|
|
16
|
-
export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
17
|
-
"plan-text-unreadable",
|
|
18
|
-
"review-text-unreadable",
|
|
19
|
-
"verdict-not-pass",
|
|
20
|
-
"missing-requirement-ids",
|
|
21
|
-
"source-binding-not-fresh",
|
|
22
|
-
"contract-invalid",
|
|
23
|
-
"contract-blocked",
|
|
24
|
-
"mock-strategy-outside-allowed",
|
|
25
|
-
"mock-strategy-no-verification-commands",
|
|
26
|
-
"target-outside-write-set",
|
|
27
|
-
"verification-target-outside-write-set",
|
|
28
|
-
]);
|
|
29
|
-
export const frontendPrewriteResultV1Schema = z
|
|
30
|
-
.object({
|
|
31
|
-
schemaVersion: z.literal(1),
|
|
32
|
-
schemaId: z.literal(FRONTEND_PREWRITE_RESULT_SCHEMA_ID),
|
|
33
|
-
classification: z.enum([
|
|
34
|
-
"accepted",
|
|
35
|
-
"accepted-normalized",
|
|
36
|
-
"retryable-invalid",
|
|
37
|
-
"blocked",
|
|
38
|
-
]),
|
|
39
|
-
failureCode: frontendPrewriteFailureCodeSchema.nullable(),
|
|
40
|
-
normalizationActions: z.array(frontendNormalizationActionSchema),
|
|
41
|
-
candidateRawSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
42
|
-
candidateJsonSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
43
|
-
canonicalSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
44
|
-
selectedPlanNodeId: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
45
|
-
sourceBindingSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
46
|
-
writeSetDigest: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
47
|
-
retryCount: z.number().int().min(0).max(1),
|
|
48
|
-
maxRetryCount: z.literal(1),
|
|
49
|
-
failureReason: z.string().nullable(),
|
|
50
|
-
digestAlgorithm: z.literal("sha256"),
|
|
51
|
-
})
|
|
52
|
-
.strict()
|
|
53
|
-
.superRefine((value, ctx) => {
|
|
54
|
-
const acceptedLike = value.classification === "accepted" ||
|
|
55
|
-
value.classification === "accepted-normalized";
|
|
56
|
-
const failed = value.classification === "retryable-invalid" ||
|
|
57
|
-
value.classification === "blocked";
|
|
58
|
-
if (acceptedLike) {
|
|
59
|
-
if (value.failureReason !== null)
|
|
60
|
-
ctx.addIssue({
|
|
61
|
-
code: "custom",
|
|
62
|
-
message: "failureReason must be null for accepted/accepted-normalized",
|
|
63
|
-
path: ["failureReason"],
|
|
64
|
-
});
|
|
65
|
-
if (value.failureCode !== null)
|
|
66
|
-
ctx.addIssue({
|
|
67
|
-
code: "custom",
|
|
68
|
-
message: "failureCode must be null for accepted/accepted-normalized",
|
|
69
|
-
path: ["failureCode"],
|
|
70
|
-
});
|
|
71
|
-
if (!value.candidateJsonSha256 || !value.canonicalSha256)
|
|
72
|
-
ctx.addIssue({
|
|
73
|
-
code: "custom",
|
|
74
|
-
message: "candidateJsonSha256 and canonicalSha256 are required for accepted/accepted-normalized",
|
|
75
|
-
path: ["candidateJsonSha256"],
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
if (value.classification === "accepted" && value.normalizationActions.length !== 0)
|
|
79
|
-
ctx.addIssue({
|
|
80
|
-
code: "custom",
|
|
81
|
-
message: "accepted must have empty normalizationActions",
|
|
82
|
-
path: ["normalizationActions"],
|
|
83
|
-
});
|
|
84
|
-
if (value.classification === "accepted-normalized" &&
|
|
85
|
-
value.normalizationActions.length === 0)
|
|
86
|
-
ctx.addIssue({
|
|
87
|
-
code: "custom",
|
|
88
|
-
message: "accepted-normalized must have non-empty normalizationActions",
|
|
89
|
-
path: ["normalizationActions"],
|
|
90
|
-
});
|
|
91
|
-
if (failed) {
|
|
92
|
-
if (value.normalizationActions.length !== 0)
|
|
93
|
-
ctx.addIssue({
|
|
94
|
-
code: "custom",
|
|
95
|
-
message: "retryable-invalid/blocked must have empty normalizationActions",
|
|
96
|
-
path: ["normalizationActions"],
|
|
97
|
-
});
|
|
98
|
-
if (!value.failureReason)
|
|
99
|
-
ctx.addIssue({
|
|
100
|
-
code: "custom",
|
|
101
|
-
message: "failureReason must be non-empty for retryable-invalid/blocked",
|
|
102
|
-
path: ["failureReason"],
|
|
103
|
-
});
|
|
104
|
-
if (value.failureCode === null)
|
|
105
|
-
ctx.addIssue({
|
|
106
|
-
code: "custom",
|
|
107
|
-
message: "failureCode must be non-null for retryable-invalid/blocked",
|
|
108
|
-
path: ["failureCode"],
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
export const frontendWriterAdmissionSchema = z
|
|
113
|
-
.object({
|
|
114
|
-
schemaVersion: z.literal(1),
|
|
115
|
-
writerNodeId: z.enum(["frontend-implement-pi", "frontend-repair-pi"]),
|
|
116
|
-
decision: z.enum(["authorized", "denied"]),
|
|
117
|
-
sourceArtifact: z.string().min(1),
|
|
118
|
-
artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
119
|
-
checkedAt: z.string().min(1),
|
|
120
|
-
reason: z.string().nullable(),
|
|
121
|
-
})
|
|
122
|
-
.strict();
|
|
123
7
|
async function selectNode(runDir, primary, fallbacks) {
|
|
124
8
|
const candidates = [primary, ...fallbacks.filter((id) => id !== primary)];
|
|
125
9
|
const rejections = [];
|
|
@@ -153,10 +37,6 @@ async function readNodeText(runDir, nodeId) {
|
|
|
153
37
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
154
38
|
return text;
|
|
155
39
|
}
|
|
156
|
-
async function readNodeRawText(runDir, nodeId) {
|
|
157
|
-
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
158
|
-
return record.assistantText ?? record.stdout ?? "";
|
|
159
|
-
}
|
|
160
40
|
function firstNonEmptyVerdictLine(text) {
|
|
161
41
|
const lines = text
|
|
162
42
|
.split(/\r?\n/)
|
|
@@ -252,141 +132,16 @@ async function checkOpenspecReadEvidence(input) {
|
|
|
252
132
|
}
|
|
253
133
|
return [...matched];
|
|
254
134
|
}
|
|
255
|
-
async function finalizePrewrite(input, pending) {
|
|
256
|
-
const sourceBindingSha256 = input.sourceBinding
|
|
257
|
-
? deterministicSha256(input.sourceBinding)
|
|
258
|
-
: null;
|
|
259
|
-
const writeSetDigest = input.config.implementationWriteSet
|
|
260
|
-
? deterministicSha256([...new Set(input.config.implementationWriteSet)].sort())
|
|
261
|
-
: null;
|
|
262
|
-
const prewriteResult = frontendPrewriteResultV1Schema.parse({
|
|
263
|
-
schemaVersion: 1,
|
|
264
|
-
schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
|
|
265
|
-
classification: pending.classification,
|
|
266
|
-
normalizationActions: pending.normalizationActions,
|
|
267
|
-
candidateRawSha256: pending.candidateRawSha256,
|
|
268
|
-
candidateJsonSha256: pending.candidateJsonSha256,
|
|
269
|
-
canonicalSha256: pending.canonicalSha256,
|
|
270
|
-
selectedPlanNodeId: pending.planNodeId,
|
|
271
|
-
sourceBindingSha256,
|
|
272
|
-
writeSetDigest,
|
|
273
|
-
retryCount: 0,
|
|
274
|
-
maxRetryCount: 1,
|
|
275
|
-
failureReason: pending.failureReason,
|
|
276
|
-
failureCode: pending.failureCode,
|
|
277
|
-
digestAlgorithm: "sha256",
|
|
278
|
-
});
|
|
279
|
-
const written = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(input.config.outputDir, FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME), prewriteResult);
|
|
280
|
-
return {
|
|
281
|
-
ok: true,
|
|
282
|
-
classification: pending.classification,
|
|
283
|
-
planNodeId: pending.planNodeId,
|
|
284
|
-
reviewNodeId: pending.reviewNodeId,
|
|
285
|
-
verdict: pending.verdict,
|
|
286
|
-
mockStrategy: pending.mockStrategy,
|
|
287
|
-
artifact: pending.artifact,
|
|
288
|
-
prewriteResult: {
|
|
289
|
-
path: written.path,
|
|
290
|
-
sha256: written.sha256,
|
|
291
|
-
schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
|
|
292
|
-
},
|
|
293
|
-
failureReason: pending.failureReason,
|
|
294
|
-
failureCode: pending.failureCode,
|
|
295
|
-
normalizationActions: pending.normalizationActions,
|
|
296
|
-
candidateRawSha256: pending.candidateRawSha256,
|
|
297
|
-
candidateJsonSha256: pending.candidateJsonSha256,
|
|
298
|
-
canonicalSha256: pending.canonicalSha256,
|
|
299
|
-
openspecReadPaths: pending.openspecReadPaths,
|
|
300
|
-
openspecCandidatePaths: pending.openspecCandidatePaths,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
135
|
export async function runFrontendPrewriteGate(input) {
|
|
304
136
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
305
|
-
// selectNode throws for a scheduling defect (no FINISHED plan/review). That
|
|
306
|
-
// remains an ERROR and is intentionally not classified into prewrite-result.
|
|
307
|
-
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
308
|
-
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
309
|
-
const candidateRawSha256 = sha256Hex(await readNodeRawText(input.runDir, planNodeId));
|
|
310
|
-
const basePending = {
|
|
311
|
-
planNodeId,
|
|
312
|
-
reviewNodeId,
|
|
313
|
-
verdict: "",
|
|
314
|
-
candidateRawSha256,
|
|
315
|
-
candidateJsonSha256: null,
|
|
316
|
-
canonicalSha256: null,
|
|
317
|
-
normalizationActions: [],
|
|
318
|
-
mockStrategy: undefined,
|
|
319
|
-
artifact: undefined,
|
|
320
|
-
failureReason: null,
|
|
321
|
-
failureCode: null,
|
|
322
|
-
classification: "accepted",
|
|
323
|
-
openspecReadPaths: [],
|
|
324
|
-
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
325
|
-
};
|
|
326
|
-
let planText;
|
|
327
|
-
try {
|
|
328
|
-
planText = await readNodeText(input.runDir, planNodeId);
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
return finalizePrewrite(input, {
|
|
332
|
-
...basePending,
|
|
333
|
-
classification: "retryable-invalid",
|
|
334
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
335
|
-
failureCode: "plan-text-unreadable",
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
let reviewText;
|
|
339
|
-
try {
|
|
340
|
-
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
return finalizePrewrite(input, {
|
|
344
|
-
...basePending,
|
|
345
|
-
classification: "blocked",
|
|
346
|
-
failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
347
|
-
failureCode: "review-text-unreadable",
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
const verdict = firstNonEmptyVerdictLine(reviewText);
|
|
351
|
-
if (verdict !== "VERDICT: pass") {
|
|
352
|
-
return finalizePrewrite(input, {
|
|
353
|
-
...basePending,
|
|
354
|
-
verdict,
|
|
355
|
-
classification: "blocked",
|
|
356
|
-
failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${verdict || "missing VERDICT"}`,
|
|
357
|
-
failureCode: "verdict-not-pass",
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
|
|
361
|
-
if (missingIds.length > 0) {
|
|
362
|
-
return finalizePrewrite(input, {
|
|
363
|
-
...basePending,
|
|
364
|
-
verdict,
|
|
365
|
-
classification: "blocked",
|
|
366
|
-
failureReason: `frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`,
|
|
367
|
-
failureCode: "missing-requirement-ids",
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
137
|
if (workspaceRoot) {
|
|
371
138
|
if (input.config.requireSourceFreshness) {
|
|
372
|
-
if (!input.sourceBinding)
|
|
139
|
+
if (!input.sourceBinding)
|
|
373
140
|
throw new Error("frontend prewrite gate requires sourceBinding for freshness check");
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
binding: input.sourceBinding,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
catch (error) {
|
|
382
|
-
return finalizePrewrite(input, {
|
|
383
|
-
...basePending,
|
|
384
|
-
verdict,
|
|
385
|
-
classification: "blocked",
|
|
386
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
387
|
-
failureCode: "source-binding-not-fresh",
|
|
388
|
-
});
|
|
389
|
-
}
|
|
141
|
+
await assertFrontendSourceBindingFresh({
|
|
142
|
+
workspaceRoot,
|
|
143
|
+
binding: input.sourceBinding,
|
|
144
|
+
});
|
|
390
145
|
}
|
|
391
146
|
let hasGitMetadata = true;
|
|
392
147
|
try {
|
|
@@ -404,34 +159,31 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
404
159
|
workspaceRoot,
|
|
405
160
|
});
|
|
406
161
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
sourceBinding: input.sourceBinding,
|
|
415
|
-
});
|
|
162
|
+
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
163
|
+
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
164
|
+
const planText = await readNodeText(input.runDir, planNodeId);
|
|
165
|
+
const reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
166
|
+
const verdict = firstNonEmptyVerdictLine(reviewText);
|
|
167
|
+
if (verdict !== "VERDICT: pass") {
|
|
168
|
+
throw new Error(`frontend prewrite gate blocked by ${reviewNodeId}: ${verdict || "missing VERDICT"}`);
|
|
416
169
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
...basePending,
|
|
421
|
-
verdict,
|
|
422
|
-
classification: error.kind,
|
|
423
|
-
failureReason: error.message,
|
|
424
|
-
failureCode: error.kind === "retryable-invalid"
|
|
425
|
-
? "contract-invalid"
|
|
426
|
-
: "contract-blocked",
|
|
427
|
-
candidateJsonSha256: error.candidateJsonSha256,
|
|
428
|
-
normalizationActions: [],
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
throw error;
|
|
170
|
+
const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
|
|
171
|
+
if (missingIds.length > 0) {
|
|
172
|
+
throw new Error(`frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`);
|
|
432
173
|
}
|
|
433
|
-
|
|
434
|
-
|
|
174
|
+
// Always rematerialize from the selected plan node. Reusing a previous
|
|
175
|
+
// contracts/*.json would accept stale contracts when a higher-priority
|
|
176
|
+
// FINISHED primary is present but invalid (see composite-shell invalid-primary).
|
|
177
|
+
const artifact = await materializeFrontendImplementationContract({
|
|
178
|
+
runDir: input.runDir,
|
|
179
|
+
fromNodeId: planNodeId,
|
|
180
|
+
artifactName: input.config.artifactName,
|
|
181
|
+
outputDir: input.config.outputDir,
|
|
182
|
+
sourceBinding: input.sourceBinding,
|
|
183
|
+
});
|
|
184
|
+
const raw = JSON.parse(await readFile(artifact.path, "utf8"));
|
|
185
|
+
const contract = frontendImplementationContractSchema.parse(raw);
|
|
186
|
+
if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
435
187
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
436
188
|
.map((strategy) => JSON.stringify(strategy))
|
|
437
189
|
.join(", ");
|
|
@@ -440,71 +192,30 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
440
192
|
const reason = forcedNotNeeded
|
|
441
193
|
? "the task is auto/not-required or lacks authorized deterministic Mock verification commands"
|
|
442
194
|
: `allowed strategies are [${quotedAllowed}]`;
|
|
443
|
-
|
|
444
|
-
...basePending,
|
|
445
|
-
verdict,
|
|
446
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
447
|
-
normalizationActions: [],
|
|
448
|
-
mockStrategy,
|
|
449
|
-
classification: "blocked",
|
|
450
|
-
failureReason: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${quotedAllowed}]; ${reason}. Regenerate the DAG or set frontendMock.policy=required with authorized Mock verification commands.`,
|
|
451
|
-
failureCode: "mock-strategy-outside-allowed",
|
|
452
|
-
});
|
|
195
|
+
throw new Error(`frontend contract mock strategy "${contract.mockApi.strategy}" is incompatible with the generated allowed strategies [${quotedAllowed}]; ${reason}. Regenerate the DAG or set frontendMock.policy=required with authorized Mock verification commands.`);
|
|
453
196
|
}
|
|
454
|
-
//
|
|
455
|
-
// by the DAG's frozen Mock verification
|
|
456
|
-
//
|
|
457
|
-
|
|
197
|
+
// Fail early instead of at the verification trace: a non-not-needed strategy
|
|
198
|
+
// can only be proven by executing the DAG's frozen Mock verification
|
|
199
|
+
// commands. If none were materialized at generation time the contract would
|
|
200
|
+
// fail "trace: selected Mock strategy requires successful Mock verification
|
|
201
|
+
// commands" after implementation, so block before any write is authorized.
|
|
202
|
+
if (contract.mockApi.strategy !== "not-needed" &&
|
|
458
203
|
(input.config.mockCommandLabels?.length ?? 0) === 0) {
|
|
459
|
-
|
|
460
|
-
...basePending,
|
|
461
|
-
verdict,
|
|
462
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
463
|
-
normalizationActions: [],
|
|
464
|
-
mockStrategy,
|
|
465
|
-
classification: "blocked",
|
|
466
|
-
failureReason: `frontend prewrite gate blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands; add frontendMock.verifyCommands or a project mock script, or select not-needed`,
|
|
467
|
-
failureCode: "mock-strategy-no-verification-commands",
|
|
468
|
-
});
|
|
204
|
+
throw new Error(`frontend prewrite gate blocked: contract selected Mock strategy ${contract.mockApi.strategy} but the DAG has no authorized Mock verification commands; add frontendMock.verifyCommands or a project mock script, or select not-needed`);
|
|
469
205
|
}
|
|
470
206
|
if (input.config.implementationWriteSet) {
|
|
471
207
|
const writeSet = new Set(input.config.implementationWriteSet);
|
|
472
|
-
const contractTargets = new Set(
|
|
208
|
+
const contractTargets = new Set(contract.targets.files);
|
|
473
209
|
const uncoveredTargets = [...contractTargets].filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
|
|
474
210
|
if (uncoveredTargets.length > 0) {
|
|
475
|
-
|
|
476
|
-
...basePending,
|
|
477
|
-
verdict,
|
|
478
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
479
|
-
normalizationActions: [],
|
|
480
|
-
mockStrategy,
|
|
481
|
-
classification: "blocked",
|
|
482
|
-
failureReason: `frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
|
|
483
|
-
failureCode: "target-outside-write-set",
|
|
484
|
-
});
|
|
211
|
+
throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
|
|
485
212
|
}
|
|
486
|
-
const nonStaticVTs =
|
|
213
|
+
const nonStaticVTs = contract.verificationTargets.filter((vt) => vt.type !== "static");
|
|
487
214
|
const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
488
215
|
if (uncoveredVTs.length > 0) {
|
|
489
|
-
|
|
490
|
-
...basePending,
|
|
491
|
-
verdict,
|
|
492
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
493
|
-
normalizationActions: [],
|
|
494
|
-
mockStrategy,
|
|
495
|
-
classification: "blocked",
|
|
496
|
-
failureReason: `frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
|
|
497
|
-
failureCode: "verification-target-outside-write-set",
|
|
498
|
-
});
|
|
216
|
+
throw new Error(`frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`);
|
|
499
217
|
}
|
|
500
218
|
}
|
|
501
|
-
// Materialize the canonical contract only after every governance check passed.
|
|
502
|
-
const artifact = await writeFrontendImplementationContractArtifact({
|
|
503
|
-
runDir: input.runDir,
|
|
504
|
-
outputDir: input.config.outputDir,
|
|
505
|
-
artifactName: input.config.artifactName,
|
|
506
|
-
canonical: analysis.canonical,
|
|
507
|
-
});
|
|
508
219
|
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
509
220
|
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
510
221
|
runDir: input.runDir,
|
|
@@ -513,46 +224,32 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
513
224
|
reviewNodeId,
|
|
514
225
|
repoRoot: workspaceRoot ?? process.cwd(),
|
|
515
226
|
});
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
227
|
+
// OpenSpec is advisory design evidence, not an authorization boundary.
|
|
228
|
+
// Model read behavior is nondeterministic, and a missed read must not block
|
|
229
|
+
// a contract/schema/write-set-authorized implementation. Preserve any
|
|
230
|
+
// successful evidence for review context, while allowing unavailable
|
|
231
|
+
// evidence to be reported downstream.
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
planNodeId,
|
|
235
|
+
reviewNodeId,
|
|
519
236
|
verdict,
|
|
520
|
-
|
|
521
|
-
failureReason: null,
|
|
522
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
523
|
-
canonicalSha256: artifact.sha256,
|
|
524
|
-
normalizationActions: analysis.normalizationActions,
|
|
525
|
-
mockStrategy,
|
|
237
|
+
mockStrategy: contract.mockApi.strategy,
|
|
526
238
|
artifact,
|
|
527
239
|
openspecReadPaths,
|
|
528
240
|
openspecCandidatePaths: candidatePaths,
|
|
529
|
-
}
|
|
241
|
+
};
|
|
530
242
|
}
|
|
531
243
|
export function formatFrontendPrewriteGateStdout(result) {
|
|
532
|
-
const lines = [
|
|
533
|
-
|
|
534
|
-
result.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
lines.push("Frontend prewrite gate: retryable-invalid");
|
|
542
|
-
}
|
|
543
|
-
lines.push(`Classification: ${result.classification}`);
|
|
544
|
-
lines.push(`Plan: ${result.planNodeId}`);
|
|
545
|
-
lines.push(`Review: ${result.reviewNodeId}`);
|
|
546
|
-
if (result.mockStrategy)
|
|
547
|
-
lines.push(`Mock strategy: ${result.mockStrategy}`);
|
|
548
|
-
if (result.artifact) {
|
|
549
|
-
lines.push(`Structured artifact: ${result.artifact.path}`);
|
|
550
|
-
lines.push(`Schema: ${result.artifact.schemaId}`);
|
|
551
|
-
lines.push(`SHA-256: ${result.artifact.sha256}`);
|
|
552
|
-
}
|
|
553
|
-
lines.push(`Prewrite result: ${result.prewriteResult.path}`);
|
|
554
|
-
if (result.failureReason)
|
|
555
|
-
lines.push(`Failure reason: ${result.failureReason}`);
|
|
244
|
+
const lines = [
|
|
245
|
+
"Frontend prewrite gate: pass",
|
|
246
|
+
`Plan: ${result.planNodeId}`,
|
|
247
|
+
`Review: ${result.reviewNodeId}`,
|
|
248
|
+
`Mock strategy: ${result.mockStrategy}`,
|
|
249
|
+
`Structured artifact: ${result.artifact.path}`,
|
|
250
|
+
`Schema: ${result.artifact.schemaId}`,
|
|
251
|
+
`SHA-256: ${result.artifact.sha256}`,
|
|
252
|
+
];
|
|
556
253
|
if (result.openspecReadPaths.length > 0) {
|
|
557
254
|
lines.push(`openspec read: ${result.openspecReadPaths.join(", ")}`);
|
|
558
255
|
}
|