@tea-agent/loop-agent 0.35.1-beta.3 → 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 -49
- 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
|
@@ -4,69 +4,10 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import {
|
|
7
|
+
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
|
-
export const frontendNormalizationActionSchema = z.enum([
|
|
12
|
-
"remove-trailing-commas",
|
|
13
|
-
"strip-comments",
|
|
14
|
-
"canonicalize-requirement-ids",
|
|
15
|
-
"canonicalize-verification-alias",
|
|
16
|
-
"inject-source-binding",
|
|
17
|
-
]);
|
|
18
|
-
export function sha256Hex(input) {
|
|
19
|
-
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
20
|
-
}
|
|
21
|
-
function sortKeysDeep(value) {
|
|
22
|
-
if (Array.isArray(value))
|
|
23
|
-
return value.map(sortKeysDeep);
|
|
24
|
-
if (value && typeof value === "object") {
|
|
25
|
-
const record = value;
|
|
26
|
-
const sorted = {};
|
|
27
|
-
for (const key of Object.keys(record).sort()) {
|
|
28
|
-
sorted[key] = sortKeysDeep(record[key]);
|
|
29
|
-
}
|
|
30
|
-
return sorted;
|
|
31
|
-
}
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
/** Deterministic JSON serialization: fixed key order, 2-space indent, no
|
|
35
|
-
* trailing newline. Shared by candidate/canonical/digest hashes so the same
|
|
36
|
-
* input always reproduces the same digest. */
|
|
37
|
-
export function serializeDeterministicJson(value) {
|
|
38
|
-
return JSON.stringify(sortKeysDeep(value), null, 2);
|
|
39
|
-
}
|
|
40
|
-
export function deterministicSha256(value) {
|
|
41
|
-
return sha256Hex(serializeDeterministicJson(value));
|
|
42
|
-
}
|
|
43
|
-
/** A contract materialization failure carries its classification plus the
|
|
44
|
-
* audit context collected up to the failure point, so the prewrite gate can
|
|
45
|
-
* persist a complete frontend-prewrite-result-v1 even when the canonical
|
|
46
|
-
* contract is never materialized. */
|
|
47
|
-
export class FrontendContractFailure extends Error {
|
|
48
|
-
kind;
|
|
49
|
-
candidateRawSha256;
|
|
50
|
-
candidateJsonSha256;
|
|
51
|
-
normalizationActions;
|
|
52
|
-
constructor(options) {
|
|
53
|
-
super(options.message);
|
|
54
|
-
this.name = "FrontendContractFailure";
|
|
55
|
-
this.kind = options.kind;
|
|
56
|
-
this.candidateRawSha256 = options.candidateRawSha256;
|
|
57
|
-
this.candidateJsonSha256 = options.candidateJsonSha256 ?? null;
|
|
58
|
-
this.normalizationActions = options.normalizationActions ?? [];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/** Write a JSON artifact with deterministic key order and return its sha256
|
|
62
|
-
* over the exact file bytes, so the canonical contract identity matches what
|
|
63
|
-
* downstream verify/repair/review re-read from disk. */
|
|
64
|
-
export async function writeDeterministicJsonArtifact(runDir, relativePath, value) {
|
|
65
|
-
const json = `${serializeDeterministicJson(value)}\n`;
|
|
66
|
-
const targetPath = path.join(runDir, relativePath);
|
|
67
|
-
await writeTextArtifactFile(targetPath, json);
|
|
68
|
-
return { path: targetPath, sha256: sha256Hex(json) };
|
|
69
|
-
}
|
|
70
11
|
/**
|
|
71
12
|
* Extract frozen command labels from the DAG run spec (run.json).
|
|
72
13
|
* The run spec is written before any node executes, so it is always available
|
|
@@ -191,32 +132,7 @@ const gap = z
|
|
|
191
132
|
blocking: z.boolean(),
|
|
192
133
|
})
|
|
193
134
|
.strict();
|
|
194
|
-
/**
|
|
195
|
-
* Normalize unstable model emissions before the strict schema sees them:
|
|
196
|
-
* recursively drop null values (object entries and array elements). A null for
|
|
197
|
-
* an optional field becomes "absent" (accepted); a null for a required field
|
|
198
|
-
* still fails, but with a clean Required issue instead of a confusing type
|
|
199
|
-
* mismatch. Unknown keys and misspellings remain strict rejections.
|
|
200
|
-
*/
|
|
201
|
-
function stripNullValuesDeep(value) {
|
|
202
|
-
if (Array.isArray(value)) {
|
|
203
|
-
return value
|
|
204
|
-
.filter((item) => item !== null)
|
|
205
|
-
.map((item) => stripNullValuesDeep(item));
|
|
206
|
-
}
|
|
207
|
-
if (value && typeof value === "object") {
|
|
208
|
-
const record = value;
|
|
209
|
-
const stripped = {};
|
|
210
|
-
for (const [key, item] of Object.entries(record)) {
|
|
211
|
-
if (item !== null)
|
|
212
|
-
stripped[key] = stripNullValuesDeep(item);
|
|
213
|
-
}
|
|
214
|
-
return stripped;
|
|
215
|
-
}
|
|
216
|
-
return value;
|
|
217
|
-
}
|
|
218
135
|
export const frontendImplementationContractSchema = z
|
|
219
|
-
.preprocess(stripNullValuesDeep, z
|
|
220
136
|
.object({
|
|
221
137
|
schemaVersion: z.literal(1),
|
|
222
138
|
sourceBinding: z
|
|
@@ -273,9 +189,7 @@ export const frontendImplementationContractSchema = z
|
|
|
273
189
|
"not-needed",
|
|
274
190
|
]),
|
|
275
191
|
productionDefaultOff: z.literal(true),
|
|
276
|
-
activation: z.preprocess((value) => value === "" || value === null
|
|
277
|
-
? "explicit activation boundary"
|
|
278
|
-
: value, z.string().min(1)),
|
|
192
|
+
activation: z.preprocess((value) => (value === "" || value === null ? "explicit activation boundary" : value), z.string().min(1)),
|
|
279
193
|
endpoints: z.array(z
|
|
280
194
|
.object({
|
|
281
195
|
method: z.enum([
|
|
@@ -425,7 +339,7 @@ export const frontendImplementationContractSchema = z
|
|
|
425
339
|
}
|
|
426
340
|
});
|
|
427
341
|
}
|
|
428
|
-
})
|
|
342
|
+
});
|
|
429
343
|
export async function assertFrontendSourceBindingFresh(input) {
|
|
430
344
|
for (const source of input.binding.sources) {
|
|
431
345
|
const absolute = resolveDagTaskSourcePath({
|
|
@@ -464,13 +378,8 @@ function secretIssues(value, at = "$", issues = []) {
|
|
|
464
378
|
}
|
|
465
379
|
return issues;
|
|
466
380
|
}
|
|
467
|
-
function
|
|
381
|
+
export function extractFrontendImplementationJson(text) {
|
|
468
382
|
const trimmed = text.trim();
|
|
469
|
-
const actions = [];
|
|
470
|
-
const recordAction = (action) => {
|
|
471
|
-
if (!actions.includes(action))
|
|
472
|
-
actions.push(action);
|
|
473
|
-
};
|
|
474
383
|
const parse = (source) => {
|
|
475
384
|
try {
|
|
476
385
|
return JSON.parse(source);
|
|
@@ -483,10 +392,6 @@ function extractFrontendImplementationJsonWithAudit(text) {
|
|
|
483
392
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
484
393
|
.replace(/^\s*\/\/.*$/gm, "");
|
|
485
394
|
const withoutTrailingCommas = withoutComments.replace(/,\s*([}\]])/g, "$1");
|
|
486
|
-
if (withoutComments !== source)
|
|
487
|
-
recordAction("strip-comments");
|
|
488
|
-
if (withoutTrailingCommas !== withoutComments)
|
|
489
|
-
recordAction("remove-trailing-commas");
|
|
490
395
|
try {
|
|
491
396
|
return JSON.parse(withoutTrailingCommas);
|
|
492
397
|
}
|
|
@@ -545,7 +450,7 @@ function extractFrontendImplementationJsonWithAudit(text) {
|
|
|
545
450
|
Array.isArray(record.verificationTargets));
|
|
546
451
|
};
|
|
547
452
|
if (trimmed.startsWith("{") && trimmed.endsWith("}"))
|
|
548
|
-
return
|
|
453
|
+
return parse(trimmed);
|
|
549
454
|
const balancedObjects = [];
|
|
550
455
|
for (let start = 0; start < trimmed.length; start += 1) {
|
|
551
456
|
if (trimmed[start] !== "{")
|
|
@@ -583,9 +488,9 @@ function extractFrontendImplementationJsonWithAudit(text) {
|
|
|
583
488
|
}
|
|
584
489
|
const balancedContracts = balancedObjects.filter(isContract);
|
|
585
490
|
if (balancedContracts.length === 1)
|
|
586
|
-
return
|
|
491
|
+
return balancedContracts[0];
|
|
587
492
|
if (balancedObjects.length === 1)
|
|
588
|
-
return
|
|
493
|
+
return balancedObjects[0];
|
|
589
494
|
const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
|
|
590
495
|
if (blocks.length === 0)
|
|
591
496
|
throw new Error("output must contain exactly one fenced json object");
|
|
@@ -603,16 +508,13 @@ function extractFrontendImplementationJsonWithAudit(text) {
|
|
|
603
508
|
}
|
|
604
509
|
const contractCandidates = candidates.filter(isContract);
|
|
605
510
|
if (contractCandidates.length === 1)
|
|
606
|
-
return
|
|
511
|
+
return contractCandidates[0];
|
|
607
512
|
if (candidates.length === 1)
|
|
608
|
-
return
|
|
513
|
+
return candidates[0];
|
|
609
514
|
if (candidates.length === 0)
|
|
610
515
|
throw new Error("output must contain exactly one valid fenced json object (found 0)");
|
|
611
516
|
throw new Error(`output must contain exactly one valid frontend contract json object (found ${contractCandidates.length || candidates.length})`);
|
|
612
517
|
}
|
|
613
|
-
export function extractFrontendImplementationJson(text) {
|
|
614
|
-
return extractFrontendImplementationJsonWithAudit(text).value;
|
|
615
|
-
}
|
|
616
518
|
/**
|
|
617
519
|
* Build the authoritative frontend-implementation-contract sourceBinding from
|
|
618
520
|
* the DAG-owned binding. Model JSON must not invent taskId/path/sha256; the
|
|
@@ -850,14 +752,7 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
850
752
|
const requirementId = canonicalizeRequirementId(asString(gap.requirementId));
|
|
851
753
|
// Model gaps are advisory. Blocking status is reconstructed below
|
|
852
754
|
// from the source binding and executable verification targets.
|
|
853
|
-
|
|
854
|
-
// accepts model emissions (null is not a valid string).
|
|
855
|
-
const { requirementId: _rawRequirementId, ...rest } = gap;
|
|
856
|
-
return {
|
|
857
|
-
...rest,
|
|
858
|
-
...(requirementId ? { requirementId } : {}),
|
|
859
|
-
blocking: false,
|
|
860
|
-
};
|
|
755
|
+
return { ...gap, blocking: false };
|
|
861
756
|
})
|
|
862
757
|
: [];
|
|
863
758
|
const derivedBlockingGaps = canonicalBinding.requirementIds
|
|
@@ -1333,87 +1228,40 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1333
1228
|
evidenceGaps,
|
|
1334
1229
|
};
|
|
1335
1230
|
}
|
|
1336
|
-
|
|
1337
|
-
* Deterministic contract validation shared by the prewrite gate and the plan
|
|
1338
|
-
* node output self-check. Extracts, security-checks, coerces, and schema-parses
|
|
1339
|
-
* a model-emitted frontend contract, so schema/typo/null violations surface as
|
|
1340
|
-
* invalid-output at the producing node (where they can retry) instead of only
|
|
1341
|
-
* failing later at the prewrite gate.
|
|
1342
|
-
*/
|
|
1343
|
-
export async function analyzeFrontendImplementationContract(input) {
|
|
1231
|
+
export async function materializeFrontendImplementationContract(input) {
|
|
1344
1232
|
if (!input.sourceBinding)
|
|
1345
1233
|
throw new Error("frontend implementation contract gate requires DAG sourceBinding");
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
}
|
|
1350
|
-
else if (input.fromNodeId !== undefined) {
|
|
1351
|
-
const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
|
|
1352
|
-
rawOutput = record.assistantText ?? record.stdout ?? "";
|
|
1353
|
-
}
|
|
1354
|
-
else {
|
|
1355
|
-
throw new Error("analyzeFrontendImplementationContract requires fromNodeId or rawContractText");
|
|
1356
|
-
}
|
|
1357
|
-
const rawContractText = rawOutput.trim();
|
|
1358
|
-
const candidateRawSha256 = sha256Hex(rawOutput);
|
|
1359
|
-
const normalizationActions = [];
|
|
1360
|
-
const pushAction = (action) => {
|
|
1361
|
-
if (!normalizationActions.includes(action))
|
|
1362
|
-
normalizationActions.push(action);
|
|
1363
|
-
};
|
|
1364
|
-
function fail(kind, message, candidateJsonSha256) {
|
|
1365
|
-
throw new FrontendContractFailure({
|
|
1366
|
-
kind,
|
|
1367
|
-
message,
|
|
1368
|
-
candidateRawSha256,
|
|
1369
|
-
candidateJsonSha256,
|
|
1370
|
-
normalizationActions,
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1234
|
+
const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
|
|
1235
|
+
let parsed;
|
|
1236
|
+
const rawContractText = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
1373
1237
|
if (rawContractText.includes('"files":["/'))
|
|
1374
|
-
|
|
1238
|
+
throw new Error("invalid-output: absolute frontend contract path is forbidden");
|
|
1375
1239
|
if (/(?:"(?:files|file|implementationTargets|fixture|consumer)"\s*:\s*(?:\[\s*)?)"\//.test(rawContractText))
|
|
1376
|
-
|
|
1240
|
+
throw new Error("invalid-output: absolute frontend contract path is forbidden");
|
|
1377
1241
|
if (/(?:"strategy"\s*:\s*")(?!native\b|browser-intercept\b|request-adapter\b|not-needed\b)[^"]+"/.test(rawContractText))
|
|
1378
|
-
|
|
1379
|
-
let parsed;
|
|
1242
|
+
throw new Error("invalid-output: unsupported mock strategy");
|
|
1380
1243
|
try {
|
|
1381
|
-
|
|
1382
|
-
parsed = extracted.value;
|
|
1383
|
-
for (const action of extracted.actions)
|
|
1384
|
-
pushAction(action);
|
|
1244
|
+
parsed = extractFrontendImplementationJson(rawContractText);
|
|
1385
1245
|
}
|
|
1386
1246
|
catch (error) {
|
|
1387
|
-
|
|
1247
|
+
throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
|
|
1388
1248
|
}
|
|
1389
|
-
const candidateJsonSha256 = deterministicSha256(parsed);
|
|
1390
1249
|
const secrets = secretIssues(parsed);
|
|
1391
1250
|
if (secrets.length)
|
|
1392
|
-
|
|
1251
|
+
throw new Error(`invalid-output: ${secrets.join("; ")}`);
|
|
1393
1252
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1394
|
-
|
|
1253
|
+
throw new Error("invalid-output: frontend contract must be a JSON object");
|
|
1395
1254
|
const parsedTargetObject = parsed.targets;
|
|
1396
1255
|
const parsedFiles = parsedTargetObject && typeof parsedTargetObject === "object"
|
|
1397
1256
|
? parsedTargetObject.files
|
|
1398
1257
|
: undefined;
|
|
1399
1258
|
if (Array.isArray(parsedFiles) && parsedFiles.some((file) => typeof file === "string" && file.startsWith("/")))
|
|
1400
|
-
|
|
1259
|
+
throw new Error("invalid-output: absolute frontend contract path is forbidden");
|
|
1401
1260
|
// Normalize model-emitted compact requirement IDs before deriving the
|
|
1402
1261
|
// canonical binding or invoking the strict zod schema.
|
|
1403
|
-
const beforeIds = JSON.stringify(parsed);
|
|
1404
1262
|
parsed = canonicalizeRequirementIdsInPayload(parsed);
|
|
1405
|
-
if (JSON.stringify(parsed) !== beforeIds)
|
|
1406
|
-
pushAction("canonicalize-requirement-ids");
|
|
1407
|
-
const beforeAliases = JSON.stringify(parsed);
|
|
1408
1263
|
parsed = canonicalizeVerificationTargetAliases(parsed);
|
|
1409
|
-
|
|
1410
|
-
pushAction("canonicalize-verification-alias");
|
|
1411
|
-
try {
|
|
1412
|
-
assertFrontendContractPathsSafe(parsed);
|
|
1413
|
-
}
|
|
1414
|
-
catch (error) {
|
|
1415
|
-
fail("blocked", error instanceof Error ? error.message : String(error), candidateJsonSha256);
|
|
1416
|
-
}
|
|
1264
|
+
assertFrontendContractPathsSafe(parsed);
|
|
1417
1265
|
// Validate verificationTarget commandLabels against frozen command set.
|
|
1418
1266
|
// The frozen set is derived from DAG verification shell task verifyEvidence.
|
|
1419
1267
|
const frozenLabels = await deriveFrozenCommandLabelsFromRun(input.runDir);
|
|
@@ -1424,7 +1272,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1424
1272
|
for (const vt of parsedVt) {
|
|
1425
1273
|
const label = asString(asRecord(vt)?.commandLabel);
|
|
1426
1274
|
if (label && !frozen.has(label)) {
|
|
1427
|
-
|
|
1275
|
+
throw new Error(`invalid-output: verificationTarget commandLabel "${label}" is not in the frozen command set [${[...frozen].join(", ")}]`);
|
|
1428
1276
|
}
|
|
1429
1277
|
}
|
|
1430
1278
|
}
|
|
@@ -1432,94 +1280,52 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1432
1280
|
const parsedTargets = asRecord(parsed)?.targets;
|
|
1433
1281
|
const parsedTargetFiles = asStringArray(asRecord(parsedTargets)?.files);
|
|
1434
1282
|
if (parsedTargetFiles.some((file) => file.startsWith("/") || file.includes("\\")))
|
|
1435
|
-
|
|
1283
|
+
throw new Error("invalid-output: frontend contract target paths must be relative POSIX paths");
|
|
1436
1284
|
const parsedStates = asRecord(parsed)?.uiStates;
|
|
1437
1285
|
if (Array.isArray(parsedStates) && parsedStates.some((item) => {
|
|
1438
1286
|
const state = asRecord(item);
|
|
1439
1287
|
return state?.applicable === true &&
|
|
1440
1288
|
(!asString(state.expectedBehavior) || asStringArray(state.implementationTargets).length === 0 || asStringArray(state.verificationTargetIds).length === 0);
|
|
1441
1289
|
}))
|
|
1442
|
-
|
|
1290
|
+
throw new Error("invalid-output: applicable UI state requires behavior, implementation, and verification");
|
|
1443
1291
|
const parsedMockApi = asRecord(parsed)?.mockApi;
|
|
1444
1292
|
if (asRecord(parsedMockApi) &&
|
|
1445
1293
|
typeof asRecord(parsedMockApi)?.strategy === "string" &&
|
|
1446
1294
|
!["native", "browser-intercept", "request-adapter", "not-needed"].includes(String(asRecord(parsedMockApi)?.strategy)))
|
|
1447
|
-
|
|
1295
|
+
throw new Error(`invalid-output: unsupported mock strategy ${String(asRecord(parsedMockApi)?.strategy)}`);
|
|
1448
1296
|
const baseCanonicalBinding = canonicalFrontendContractSourceBinding(input.sourceBinding);
|
|
1449
1297
|
const canonicalBinding = withDerivedRequirementIdsWhenUnscoped(baseCanonicalBinding, parsed);
|
|
1450
1298
|
// Always inject DAG-owned identity. Model-provided sourceBinding is advisory
|
|
1451
|
-
// only and must not fail a otherwise-valid contract
|
|
1452
|
-
//
|
|
1453
|
-
const
|
|
1454
|
-
if (serializeDeterministicJson(modelBinding ?? null) !== serializeDeterministicJson(canonicalBinding))
|
|
1455
|
-
pushAction("inject-source-binding");
|
|
1299
|
+
// only and must not fail a otherwise-valid contract (common live failure:
|
|
1300
|
+
// wrong requirementPath/sha, extra referencePaths, or omitted binding).
|
|
1301
|
+
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
1456
1302
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
1457
1303
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
1458
|
-
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
1459
1304
|
const candidate = deriveFrontendVerificationCoverage({
|
|
1460
1305
|
...(asRecord(normalizedContract) ?? parsed),
|
|
1461
1306
|
sourceBinding: canonicalBinding,
|
|
1462
1307
|
}, canonicalBinding);
|
|
1463
1308
|
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
1464
1309
|
if (!result.success)
|
|
1465
|
-
|
|
1310
|
+
throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
1466
1311
|
const blockingGaps = [
|
|
1467
1312
|
...result.data.evidenceGaps,
|
|
1468
1313
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
1469
1314
|
].filter((item) => item.blocking);
|
|
1470
1315
|
if (blockingGaps.length > 0)
|
|
1471
|
-
|
|
1316
|
+
throw new Error(`frontend contract has blocking evidence gap: ${blockingGaps
|
|
1472
1317
|
.map((item) => item.requirementId ?? item.description)
|
|
1473
|
-
.join(", ")}
|
|
1318
|
+
.join(", ")}`);
|
|
1474
1319
|
for (const requirementId of canonicalBinding.requirementIds)
|
|
1475
1320
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
1476
1321
|
!result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
canonical: result.data,
|
|
1480
|
-
candidateRawSha256,
|
|
1481
|
-
candidateJsonSha256,
|
|
1482
|
-
normalizationActions,
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
export async function writeFrontendImplementationContractArtifact(input) {
|
|
1486
|
-
const written = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(input.outputDir, input.artifactName), input.canonical);
|
|
1322
|
+
throw new Error(`frontend contract does not cover ${requirementId}`);
|
|
1323
|
+
const artifactPath = await writeDagRunJsonArtifact(input.runDir, path.posix.join(input.outputDir, input.artifactName), result.data);
|
|
1487
1324
|
return {
|
|
1488
|
-
path:
|
|
1489
|
-
sha256:
|
|
1325
|
+
path: artifactPath,
|
|
1326
|
+
sha256: createHash("sha256")
|
|
1327
|
+
.update(`${JSON.stringify(result.data, null, 2)}\n`)
|
|
1328
|
+
.digest("hex"),
|
|
1490
1329
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1491
1330
|
};
|
|
1492
1331
|
}
|
|
1493
|
-
/**
|
|
1494
|
-
* Node-output self-check for plan nodes that produce the implementation
|
|
1495
|
-
* contract. Mirrors the prewrite gate validation so schema/typo/null violations
|
|
1496
|
-
* are caught at the producing node and retried instead of failing the run later.
|
|
1497
|
-
* Skipped when no sourceBinding is available (the gate remains the authority).
|
|
1498
|
-
*/
|
|
1499
|
-
export async function validateFrontendContractNodeOutput(input) {
|
|
1500
|
-
if (!input.sourceBinding)
|
|
1501
|
-
return { ok: true };
|
|
1502
|
-
try {
|
|
1503
|
-
const analysis = await analyzeFrontendImplementationContract({
|
|
1504
|
-
runDir: input.runDir,
|
|
1505
|
-
rawContractText: input.text,
|
|
1506
|
-
sourceBinding: input.sourceBinding,
|
|
1507
|
-
});
|
|
1508
|
-
return { ok: true, contract: analysis.canonical };
|
|
1509
|
-
}
|
|
1510
|
-
catch (error) {
|
|
1511
|
-
return {
|
|
1512
|
-
ok: false,
|
|
1513
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
export async function materializeFrontendImplementationContract(input) {
|
|
1518
|
-
const analysis = await analyzeFrontendImplementationContract(input);
|
|
1519
|
-
return writeFrontendImplementationContractArtifact({
|
|
1520
|
-
runDir: input.runDir,
|
|
1521
|
-
outputDir: input.outputDir,
|
|
1522
|
-
artifactName: input.artifactName,
|
|
1523
|
-
canonical: analysis.canonical,
|
|
1524
|
-
});
|
|
1525
|
-
}
|