@tea-agent/loop-agent 0.38.1 → 0.38.2
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/CHANGELOG.md +109 -13
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +2 -14
- package/dist/executors/dag-pi-executor.js +41 -4
- package/dist/executors/shell-executor.js +269 -15
- package/dist/governance/manifest-types.js +0 -2
- package/dist/shared/operator/capabilities.js +87 -22
- package/dist/task/config-types.js +22 -0
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/pi-runtime.js +10 -5
- package/dist/worker/console/chat/routes.js +458 -47
- package/dist/worker/console/chat/semantic-activity.js +13 -13
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/harness-pi-model-matrix.js +2 -11
- package/dist/worker/console/operator-actions.js +5 -4
- package/dist/worker/console/pi-readiness.js +1 -0
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
- package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-SAD7McOS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
- package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-D9Py31Ld.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-D7Occzk_.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +0 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +38 -1
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +236 -8
- package/dist/workflows/dag/frontend-prewrite-gate.js +95 -35
- package/dist/workflows/dag/init-hybrid.js +652 -87
- package/dist/workflows/dag/node-execution.js +45 -18
- package/dist/workflows/dag/prompt.js +45 -0
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +93 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/types.js +98 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/harness.schema.json +0 -8
- package/harness.json +1 -8
- package/package.json +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
- package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
|
@@ -21,6 +21,17 @@ const REVIEW_VERDICT_NODE_ID = "review-verdict-recovery-pi";
|
|
|
21
21
|
const INITIAL_REVIEW_NODE_ID = "initial-review-pi";
|
|
22
22
|
const INITIAL_REVIEW_GATE_NODE_ID = "initial-review-gate-shell";
|
|
23
23
|
const INITIAL_REVIEW_PASS_NODE_ID = "initial-review-pass-shell";
|
|
24
|
+
/**
|
|
25
|
+
* Standard-dag governance verify chain ids. When the convergence chain
|
|
26
|
+
* observes the governance standard gate, a legitimate verify-pi
|
|
27
|
+
* `request-revision` re-enters a bounded implement-repair loop instead of
|
|
28
|
+
* terminating the run at the gate, mirroring the review-chain re-entry
|
|
29
|
+
* semantics (AC3/AC4). Protocol/provider/safety failures on verify-pi keep
|
|
30
|
+
* failing closed; only a safe `request-revision` drives code repair here.
|
|
31
|
+
*/
|
|
32
|
+
const GOVERNANCE_VERIFY_GATE_NODE_ID = "governance-standard-gate-shell";
|
|
33
|
+
const GOVERNANCE_VERIFY_NODE_ID = "verify-pi";
|
|
34
|
+
const GOVERNANCE_VERIFY_WRITER_NODE_ID = "implement-pi";
|
|
24
35
|
// Automatic code repair is authorized only for an ordinary command/test
|
|
25
36
|
// failure. Unknown, protocol, validation, provider, safety, governance, and
|
|
26
37
|
// human-decision categories all fail closed instead of reaching a writer.
|
|
@@ -57,6 +68,15 @@ export async function runConvergencePassController(input) {
|
|
|
57
68
|
}
|
|
58
69
|
const chain = getConvergenceChain(input.spec);
|
|
59
70
|
const observesReview = chain.includes(REVIEW_GATE_NODE_ID);
|
|
71
|
+
const observesGovernanceVerify = chain.includes(GOVERNANCE_VERIFY_GATE_NODE_ID);
|
|
72
|
+
// Standard-dag governance verify convergence: verify-pi drives a bounded
|
|
73
|
+
// implement-repair loop on `request-revision`. This shape has no
|
|
74
|
+
// hard-verify/review nodes; dispatch before the legacy hard-verify logic so
|
|
75
|
+
// the standard governance DAG never falls into `unsupported-dag-shape`/
|
|
76
|
+
// `non-retry-failure` dead ends.
|
|
77
|
+
if (observesGovernanceVerify && !observesReview) {
|
|
78
|
+
return handleGovernanceVerifyConvergence(input);
|
|
79
|
+
}
|
|
60
80
|
if (input.tasksById.has(INITIAL_REVIEW_NODE_ID)) {
|
|
61
81
|
const initialReview = input.state.nodes[INITIAL_REVIEW_NODE_ID];
|
|
62
82
|
const initialGate = input.state.nodes[INITIAL_REVIEW_GATE_NODE_ID];
|
|
@@ -397,6 +417,127 @@ async function handleReviewRequestRevision(input, reviewGate) {
|
|
|
397
417
|
function getConvergenceChain(spec) {
|
|
398
418
|
return spec.convergence?.chainNodeIds ?? DEFAULT_CONVERGENCE_CHAIN_NODE_IDS;
|
|
399
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Legitimate governance verify revision: verify-pi finished and emitted
|
|
422
|
+
* `request-revision`. No secondary verdict-recovery node exists on the
|
|
423
|
+
* standard dag, so the single verify-pi verdict plus a non-transport failure
|
|
424
|
+
* category is the legitimacy bar (protocol-invalid/invalid-output/timeout on
|
|
425
|
+
* verify-pi fail closed instead of driving repair).
|
|
426
|
+
*/
|
|
427
|
+
function isLegitimateGovernanceVerifyRevision(state) {
|
|
428
|
+
const verifyNode = state.nodes[GOVERNANCE_VERIFY_NODE_ID];
|
|
429
|
+
return (verifyNode?.status === "FINISHED" &&
|
|
430
|
+
parseProcessVerdict(verifyNode) === "request-revision");
|
|
431
|
+
}
|
|
432
|
+
function isLegitimateGovernanceVerifyPass(state) {
|
|
433
|
+
const verifyNode = state.nodes[GOVERNANCE_VERIFY_NODE_ID];
|
|
434
|
+
return (verifyNode?.status === "FINISHED" &&
|
|
435
|
+
parseProcessVerdict(verifyNode) === "pass");
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Standard-dag governance verify convergence loop. Terminal pass when
|
|
439
|
+
* verify-pi passes; bounded re-entry (reset implement-pi + verify chain) when
|
|
440
|
+
* verify-pi legitimately requests revision. The reset lets the next
|
|
441
|
+
* implement-pi pass consume the prior verify findings through the structured
|
|
442
|
+
* convergence feedback block (node-execution injects it for implementer
|
|
443
|
+
* roles), so `already-satisfied` no-ops are explicitly contradicted by the
|
|
444
|
+
* preserved verify-pi evidence refs.
|
|
445
|
+
*/
|
|
446
|
+
async function handleGovernanceVerifyConvergence(input) {
|
|
447
|
+
const convergence = input.state.convergence;
|
|
448
|
+
const currentPass = convergence.currentPass || 1;
|
|
449
|
+
const verifyNode = input.state.nodes[GOVERNANCE_VERIFY_NODE_ID];
|
|
450
|
+
const verifyShell = input.state.nodes["verify-shell"];
|
|
451
|
+
// Hard verify commands failed: not a governance verdict issue. Without a
|
|
452
|
+
// supervised repair chain, this stays terminal so the failure-aware
|
|
453
|
+
// closeout reports it truthfully.
|
|
454
|
+
if (verifyShell?.status === "ERROR") {
|
|
455
|
+
const passRecord = await buildConvergencePassRecord({
|
|
456
|
+
pass: currentPass,
|
|
457
|
+
status: "terminal",
|
|
458
|
+
reason: "hard-verify-failed",
|
|
459
|
+
state: input.state,
|
|
460
|
+
runDir: input.runDir,
|
|
461
|
+
spec: input.spec,
|
|
462
|
+
});
|
|
463
|
+
convergence.passHistory.push(passRecord);
|
|
464
|
+
convergence.terminalReason = "hard-verify-failed";
|
|
465
|
+
await input.persistState();
|
|
466
|
+
return { retry: false };
|
|
467
|
+
}
|
|
468
|
+
if (isLegitimateGovernanceVerifyPass(input.state)) {
|
|
469
|
+
convergence.terminalReason = "hard-verify-pass";
|
|
470
|
+
await appendConvergenceKnowledgePattern({
|
|
471
|
+
cwd: input.cwd,
|
|
472
|
+
state: input.state,
|
|
473
|
+
});
|
|
474
|
+
return { retry: false };
|
|
475
|
+
}
|
|
476
|
+
const verifyFailureCategory = verifyNode?.failureCategory;
|
|
477
|
+
const legitimateRevision = isLegitimateGovernanceVerifyRevision(input.state);
|
|
478
|
+
if (!legitimateRevision ||
|
|
479
|
+
(verifyFailureCategory &&
|
|
480
|
+
REVIEW_SOURCE_NON_RETRY_FAILURES.has(verifyFailureCategory))) {
|
|
481
|
+
const passRecord = await buildConvergencePassRecord({
|
|
482
|
+
pass: currentPass,
|
|
483
|
+
status: "terminal",
|
|
484
|
+
reason: "non-retry-failure",
|
|
485
|
+
state: input.state,
|
|
486
|
+
runDir: input.runDir,
|
|
487
|
+
spec: input.spec,
|
|
488
|
+
});
|
|
489
|
+
convergence.passHistory.push(passRecord);
|
|
490
|
+
convergence.terminalReason = "non-retry-failure";
|
|
491
|
+
await input.persistState();
|
|
492
|
+
return { retry: false };
|
|
493
|
+
}
|
|
494
|
+
const passRecord = await buildConvergencePassRecord({
|
|
495
|
+
pass: currentPass,
|
|
496
|
+
status: "retrying",
|
|
497
|
+
reason: "verify-request-revision",
|
|
498
|
+
state: input.state,
|
|
499
|
+
runDir: input.runDir,
|
|
500
|
+
spec: input.spec,
|
|
501
|
+
});
|
|
502
|
+
if (currentPass >= convergence.maxPasses) {
|
|
503
|
+
passRecord.status = "terminal";
|
|
504
|
+
passRecord.reason = "max-passes";
|
|
505
|
+
convergence.passHistory.push(passRecord);
|
|
506
|
+
convergence.terminalReason = "max-passes";
|
|
507
|
+
await input.persistState();
|
|
508
|
+
return { retry: false };
|
|
509
|
+
}
|
|
510
|
+
convergence.passHistory.push(passRecord);
|
|
511
|
+
convergence.currentPass = currentPass + 1;
|
|
512
|
+
// Reset the writer plus the governance verify chain: the next implement-pi
|
|
513
|
+
// pass must consume the preserved verify-pi findings (injected as
|
|
514
|
+
// convergence feedback) instead of returning already-satisfied.
|
|
515
|
+
for (const id of collectGovernanceVerifyResetIds(input.spec)) {
|
|
516
|
+
const task = input.tasksById.get(id);
|
|
517
|
+
if (!task)
|
|
518
|
+
continue;
|
|
519
|
+
input.state.nodes[id] = freshNodeRecord(task);
|
|
520
|
+
}
|
|
521
|
+
await input.persistState();
|
|
522
|
+
return { retry: true };
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Reset set for the governance verify loop: the exclusive writer and the
|
|
526
|
+
* verify chain closure (verify-shell, verify-pi, governance gate, and every
|
|
527
|
+
* transitive descendant so closeout re-adjudicates from fresh evidence).
|
|
528
|
+
*/
|
|
529
|
+
function collectGovernanceVerifyResetIds(spec) {
|
|
530
|
+
const resetIds = new Set([
|
|
531
|
+
GOVERNANCE_VERIFY_WRITER_NODE_ID,
|
|
532
|
+
"verify-shell",
|
|
533
|
+
GOVERNANCE_VERIFY_NODE_ID,
|
|
534
|
+
GOVERNANCE_VERIFY_GATE_NODE_ID,
|
|
535
|
+
]);
|
|
536
|
+
for (const id of collectTransitiveDescendantTaskIds(spec, GOVERNANCE_VERIFY_GATE_NODE_ID)) {
|
|
537
|
+
resetIds.add(id);
|
|
538
|
+
}
|
|
539
|
+
return resetIds;
|
|
540
|
+
}
|
|
400
541
|
function hasConvergenceChain(tasksById, spec) {
|
|
401
542
|
const chain = getConvergenceChain(spec);
|
|
402
543
|
return chain.every((id) => tasksById.has(id));
|
|
@@ -367,19 +367,26 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
const budgetBlockedIds = new Set(blockedChildren.map((entry) => entry.nodeId));
|
|
370
|
+
const failOnTokenBudgetExhaustion = input.expansion.failOnTokenBudgetExhaustion === true;
|
|
370
371
|
const failedChildren = tolerateChildFailures
|
|
371
372
|
? []
|
|
372
373
|
: childNodeIds.filter((nodeId) => {
|
|
373
374
|
const record = input.state.nodes[nodeId];
|
|
374
375
|
if (!record || record.status === "FINISHED")
|
|
375
376
|
return false;
|
|
376
|
-
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record))
|
|
377
|
-
return
|
|
377
|
+
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record)) {
|
|
378
|
+
return failOnTokenBudgetExhaustion;
|
|
379
|
+
}
|
|
378
380
|
return true;
|
|
379
381
|
});
|
|
382
|
+
const finishedItemCount = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status === "FINISHED").length;
|
|
383
|
+
const blockedItemCount = budgetBlockedIds.size;
|
|
380
384
|
const aggregate = {
|
|
381
385
|
workflowNodeId: input.expansion.workflowNodeId,
|
|
382
386
|
itemCount: items.length,
|
|
387
|
+
expectedItemCount: items.length,
|
|
388
|
+
finishedItemCount,
|
|
389
|
+
blockedItemCount,
|
|
383
390
|
children: childNodeIds.map((nodeId, index) => ({
|
|
384
391
|
nodeId,
|
|
385
392
|
item: items[index],
|
|
@@ -399,21 +406,30 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
399
406
|
?.reason,
|
|
400
407
|
})),
|
|
401
408
|
tokensUsed: totalTokensUsed,
|
|
409
|
+
maxTotalTokens: tokenBudget?.maxTotalTokens,
|
|
402
410
|
caseOutcomeNotes: caseOutcomeNotes.length > 0 ? caseOutcomeNotes : undefined,
|
|
403
411
|
};
|
|
412
|
+
const tokenBudgetFailure = failOnTokenBudgetExhaustion && blockedItemCount > 0;
|
|
404
413
|
return {
|
|
405
|
-
// With tolerateChildFailures: barrier
|
|
406
|
-
// case evidence
|
|
407
|
-
|
|
414
|
+
// With tolerateChildFailures: barrier succeeds and product outcomes live in
|
|
415
|
+
// case evidence. Backend-test opts into token-budget fail-closed because a
|
|
416
|
+
// partial generated module set is never a valid downstream input.
|
|
417
|
+
ok: failedChildren.length === 0 && !tokenBudgetFailure,
|
|
408
418
|
stdout: JSON.stringify(aggregate),
|
|
409
419
|
stderr: caseOutcomeNotes.length > 0
|
|
410
420
|
? `map children recorded as case outcomes: ${caseOutcomeNotes
|
|
411
421
|
.map((entry) => `${entry.nodeId}=${entry.status}:${entry.reason}`)
|
|
412
422
|
.join(", ")}`
|
|
423
|
+
: tokenBudgetFailure
|
|
424
|
+
? `dynamic map token budget exhausted: expected=${items.length} finished=${finishedItemCount} blocked=${blockedItemCount} tokensUsed=${totalTokensUsed} maxTotalTokens=${tokenBudget?.maxTotalTokens ?? "unbounded"}`
|
|
425
|
+
: failedChildren.length > 0
|
|
426
|
+
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
427
|
+
: "",
|
|
428
|
+
failureCategory: tokenBudgetFailure
|
|
429
|
+
? "dynamic-expansion-token-budget-exhausted"
|
|
413
430
|
: failedChildren.length > 0
|
|
414
|
-
?
|
|
415
|
-
: "",
|
|
416
|
-
failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
|
|
431
|
+
? "dynamic-expansion-child-failed"
|
|
432
|
+
: "success",
|
|
417
433
|
durationMs: Date.now() - started,
|
|
418
434
|
};
|
|
419
435
|
}
|
|
@@ -30,7 +30,7 @@ export function getNodeOutputAsJson(state, nodeId) {
|
|
|
30
30
|
/** Normalize a line the same way verdict gates do (strip whole-line Markdown emphasis). */
|
|
31
31
|
export function normalizeVerdictCandidateLine(value) {
|
|
32
32
|
const trimmed = String(value).trim();
|
|
33
|
-
const emphasized = trimmed.match(
|
|
33
|
+
const emphasized = trimmed.match(/^(?:`{1,3}|\*{1,3})\s*(VERDICT:[^`*]+?)\s*(?:`{1,3}|\*{1,3})$/);
|
|
34
34
|
const line = (emphasized ? emphasized[1] : trimmed).trim();
|
|
35
35
|
// Canonicalize the colon separator and trailing sentence punctuation so
|
|
36
36
|
// "VERDICT:pass", "VERDICT: pass", or "VERDICT: pass." match the declared
|
|
@@ -13,6 +13,7 @@ export const frontendNormalizationActionSchema = z.enum([
|
|
|
13
13
|
"strip-comments",
|
|
14
14
|
"canonicalize-requirement-ids",
|
|
15
15
|
"canonicalize-verification-alias",
|
|
16
|
+
"drop-unresolved-verification-symbols",
|
|
16
17
|
"inject-source-binding",
|
|
17
18
|
]);
|
|
18
19
|
export function sha256Hex(input) {
|
|
@@ -102,6 +103,21 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
102
103
|
return [...labels];
|
|
103
104
|
}
|
|
104
105
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
106
|
+
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
107
|
+
/**
|
|
108
|
+
* Build the deterministic, run-owned portion of the frontend contract. The
|
|
109
|
+
* planner is allowed to fill semantic fields with an RFC 7386 merge patch but
|
|
110
|
+
* cannot redefine task identity, risk, or the implementation write boundary.
|
|
111
|
+
*/
|
|
112
|
+
export function buildFrontendImplementationContractSkeleton(input) {
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
sourceBinding: canonicalFrontendContractSourceBinding(input.sourceBinding),
|
|
116
|
+
riskLevel: input.riskLevel,
|
|
117
|
+
targets: { files: [...input.targetFiles] },
|
|
118
|
+
mockApi: { productionDefaultOff: true },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
105
121
|
/**
|
|
106
122
|
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
107
123
|
* installed loop-agent package docs/templates/ path. Package-root discovery
|
|
@@ -499,9 +515,9 @@ function isPlainObject(value) {
|
|
|
499
515
|
* ever materialized, so a patch that removes a required field still fails
|
|
500
516
|
* closed at the gate.
|
|
501
517
|
*/
|
|
502
|
-
export function
|
|
518
|
+
export function applyFrontendContractMergePatch(target, patch) {
|
|
503
519
|
if (!isPlainObject(patch)) {
|
|
504
|
-
throw new Error("frontend
|
|
520
|
+
throw new Error("frontend contract patch must be a JSON object");
|
|
505
521
|
}
|
|
506
522
|
const merge = (base, delta) => {
|
|
507
523
|
if (delta === null)
|
|
@@ -526,6 +542,9 @@ export function applyFrontendImplementationContractPatch(target, patch) {
|
|
|
526
542
|
};
|
|
527
543
|
return merge(target, patch);
|
|
528
544
|
}
|
|
545
|
+
export function applyFrontendImplementationContractPatch(target, patch) {
|
|
546
|
+
return applyFrontendContractMergePatch(target, patch);
|
|
547
|
+
}
|
|
529
548
|
export async function assertFrontendSourceBindingFresh(input) {
|
|
530
549
|
for (const source of input.binding.sources) {
|
|
531
550
|
const absolute = resolveDagTaskSourcePath({
|
|
@@ -1608,6 +1627,206 @@ export async function writeFrontendImplementationContractArtifact(input) {
|
|
|
1608
1627
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1609
1628
|
};
|
|
1610
1629
|
}
|
|
1630
|
+
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
1631
|
+
"/schemaVersion",
|
|
1632
|
+
"/sourceBinding",
|
|
1633
|
+
"/riskLevel",
|
|
1634
|
+
"/targets/files",
|
|
1635
|
+
"/mockApi/productionDefaultOff",
|
|
1636
|
+
];
|
|
1637
|
+
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1638
|
+
const record = asRecord(patch);
|
|
1639
|
+
if (!record)
|
|
1640
|
+
return ["/"];
|
|
1641
|
+
const violations = [];
|
|
1642
|
+
for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
|
|
1643
|
+
if (Object.hasOwn(record, key))
|
|
1644
|
+
violations.push(`/${key}`);
|
|
1645
|
+
}
|
|
1646
|
+
const targets = asRecord(record.targets);
|
|
1647
|
+
if (targets && Object.hasOwn(targets, "files"))
|
|
1648
|
+
violations.push("/targets/files");
|
|
1649
|
+
const mockApi = asRecord(record.mockApi);
|
|
1650
|
+
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
1651
|
+
violations.push("/mockApi/productionDefaultOff");
|
|
1652
|
+
return violations;
|
|
1653
|
+
}
|
|
1654
|
+
function workspaceRootFromDagRunDir(runDir) {
|
|
1655
|
+
const absolute = path.resolve(runDir);
|
|
1656
|
+
const segments = absolute.split(path.sep);
|
|
1657
|
+
const harnessIndex = segments.lastIndexOf(".harness");
|
|
1658
|
+
if (harnessIndex < 0 ||
|
|
1659
|
+
segments[harnessIndex + 1] !== "dag-runs" ||
|
|
1660
|
+
harnessIndex + 3 >= segments.length) {
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1664
|
+
return joined || path.parse(absolute).root;
|
|
1665
|
+
}
|
|
1666
|
+
function verificationSymbolMatchesFile(symbol, content) {
|
|
1667
|
+
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1668
|
+
return content.includes("describe(");
|
|
1669
|
+
const symbols = symbol.includes(" / ")
|
|
1670
|
+
? symbol
|
|
1671
|
+
.split(/[((]/, 1)[0]
|
|
1672
|
+
.split("/")
|
|
1673
|
+
.map((item) => item.trim())
|
|
1674
|
+
.filter(Boolean)
|
|
1675
|
+
: [symbol];
|
|
1676
|
+
return symbols.every((item) => {
|
|
1677
|
+
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1678
|
+
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1679
|
+
.filter((candidate) => Boolean(candidate))
|
|
1680
|
+
.some((candidate) => content.includes(candidate));
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
async function dropUnresolvedVerificationSymbols(input) {
|
|
1684
|
+
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1685
|
+
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1686
|
+
if (!workspaceRoot || !Array.isArray(targets))
|
|
1687
|
+
return false;
|
|
1688
|
+
let changed = false;
|
|
1689
|
+
for (const targetValue of targets) {
|
|
1690
|
+
const target = asRecord(targetValue);
|
|
1691
|
+
const file = asString(target?.file);
|
|
1692
|
+
const symbol = asString(target?.symbol);
|
|
1693
|
+
if (!target || !file || !symbol)
|
|
1694
|
+
continue;
|
|
1695
|
+
const absolute = path.resolve(workspaceRoot, file);
|
|
1696
|
+
const relative = path.relative(workspaceRoot, absolute);
|
|
1697
|
+
if (relative.startsWith("..") ||
|
|
1698
|
+
path.isAbsolute(relative) ||
|
|
1699
|
+
relative.includes("..")) {
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
let content;
|
|
1703
|
+
try {
|
|
1704
|
+
content = await readFile(absolute, "utf8");
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
// Missing files remain a trace-time hard failure; do not hide them by
|
|
1708
|
+
// changing the optional symbol during candidate normalization.
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
if (verificationSymbolMatchesFile(symbol, content))
|
|
1712
|
+
continue;
|
|
1713
|
+
delete target.symbol;
|
|
1714
|
+
changed = true;
|
|
1715
|
+
}
|
|
1716
|
+
return changed;
|
|
1717
|
+
}
|
|
1718
|
+
function extractSingleOpenspecCitationsBlock(text) {
|
|
1719
|
+
const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
|
|
1720
|
+
if (blocks.length !== 1) {
|
|
1721
|
+
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1722
|
+
}
|
|
1723
|
+
return blocks[0];
|
|
1724
|
+
}
|
|
1725
|
+
async function writeFrontendPlanCandidateRaw(input) {
|
|
1726
|
+
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1727
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
|
|
1728
|
+
return relativePath;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1732
|
+
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1733
|
+
* format/schema failures can be inspected and replayed without trusting model
|
|
1734
|
+
* prose. A successful result returns canonical full-contract text for all
|
|
1735
|
+
* downstream review and prewrite nodes.
|
|
1736
|
+
*/
|
|
1737
|
+
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1738
|
+
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1739
|
+
const attempt = input.attempt ?? 1;
|
|
1740
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1741
|
+
const rawPath = await writeFrontendPlanCandidateRaw({
|
|
1742
|
+
runDir: input.runDir,
|
|
1743
|
+
nodeId,
|
|
1744
|
+
attempt,
|
|
1745
|
+
text: input.text,
|
|
1746
|
+
});
|
|
1747
|
+
const reportBase = {
|
|
1748
|
+
schemaVersion: 1,
|
|
1749
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
|
|
1750
|
+
nodeId,
|
|
1751
|
+
attempt,
|
|
1752
|
+
rawPath,
|
|
1753
|
+
protectedPaths: [...FRONTEND_PLAN_PATCH_PROTECTED_PATHS],
|
|
1754
|
+
candidateRawSha256: sha256Hex(input.text),
|
|
1755
|
+
};
|
|
1756
|
+
let extractedPatchPath;
|
|
1757
|
+
let extractedPatchSha256;
|
|
1758
|
+
const writeReport = async (value) => {
|
|
1759
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
1760
|
+
...reportBase,
|
|
1761
|
+
...(extractedPatchPath
|
|
1762
|
+
? { extractedPatchPath, extractedPatchSha256 }
|
|
1763
|
+
: {}),
|
|
1764
|
+
...value,
|
|
1765
|
+
});
|
|
1766
|
+
};
|
|
1767
|
+
try {
|
|
1768
|
+
if (!input.sourceBinding)
|
|
1769
|
+
throw new Error("frontend plan patch validator requires DAG sourceBinding");
|
|
1770
|
+
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1771
|
+
if (!skeleton)
|
|
1772
|
+
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1773
|
+
const patch = extractFrontendImplementationJson(input.text);
|
|
1774
|
+
if (!isPlainObject(patch))
|
|
1775
|
+
throw new Error("frontend plan patch must extract to one JSON object");
|
|
1776
|
+
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1777
|
+
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1778
|
+
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1779
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1780
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1781
|
+
if (protectedViolations.length > 0)
|
|
1782
|
+
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1783
|
+
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1784
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
1785
|
+
runDir: input.runDir,
|
|
1786
|
+
contract: merged,
|
|
1787
|
+
});
|
|
1788
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
1789
|
+
runDir: input.runDir,
|
|
1790
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
1791
|
+
sourceBinding: input.sourceBinding,
|
|
1792
|
+
});
|
|
1793
|
+
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1794
|
+
await writeReport({
|
|
1795
|
+
classification: "accepted-normalized",
|
|
1796
|
+
normalizedContractPath: path.posix.relative(input.runDir.replaceAll(path.sep, "/"), normalizedArtifact.path.replaceAll(path.sep, "/")),
|
|
1797
|
+
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1798
|
+
normalizationActions: [
|
|
1799
|
+
"apply-runtime-skeleton",
|
|
1800
|
+
...(droppedUnresolvedSymbols
|
|
1801
|
+
? ["drop-unresolved-verification-symbols"]
|
|
1802
|
+
: []),
|
|
1803
|
+
...analysis.normalizationActions,
|
|
1804
|
+
],
|
|
1805
|
+
errors: [],
|
|
1806
|
+
});
|
|
1807
|
+
return {
|
|
1808
|
+
ok: true,
|
|
1809
|
+
contract: analysis.canonical,
|
|
1810
|
+
normalizedText: [
|
|
1811
|
+
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1812
|
+
"",
|
|
1813
|
+
"```json",
|
|
1814
|
+
serializeDeterministicJson(analysis.canonical),
|
|
1815
|
+
"```",
|
|
1816
|
+
citationsBlock,
|
|
1817
|
+
].join("\n"),
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
catch (error) {
|
|
1821
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1822
|
+
await writeReport({
|
|
1823
|
+
classification: "retryable-invalid",
|
|
1824
|
+
normalizationActions: [],
|
|
1825
|
+
errors: [{ path: "$", code: "invalid-output", message: reason }],
|
|
1826
|
+
});
|
|
1827
|
+
return { ok: false, reason };
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1611
1830
|
/**
|
|
1612
1831
|
* Node-output self-check for plan nodes that produce the implementation
|
|
1613
1832
|
* contract. Mirrors the prewrite gate validation so schema/typo/null violations
|
|
@@ -1635,7 +1854,7 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1635
1854
|
/**
|
|
1636
1855
|
* Node-output self-check for the plan revision node, which emits an RFC 7386
|
|
1637
1856
|
* merge-patch delta against the original contract instead of a full contract.
|
|
1638
|
-
* Deliberately loose and format-level: the output must contain
|
|
1857
|
+
* Deliberately loose and format-level: the output must contain exactly one
|
|
1639
1858
|
* ```openspec-citations fenced block and must extract (after the citations
|
|
1640
1859
|
* blocks are stripped) to exactly one JSON object. The delta carries no
|
|
1641
1860
|
* schemaVersion/targets, so no full-contract schema/semantic checks run here;
|
|
@@ -1643,17 +1862,19 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1643
1862
|
* the prewrite gate, which remains the only authority.
|
|
1644
1863
|
*/
|
|
1645
1864
|
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1865
|
+
let citationsBlock;
|
|
1866
|
+
try {
|
|
1867
|
+
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1868
|
+
}
|
|
1869
|
+
catch (error) {
|
|
1649
1870
|
return {
|
|
1650
1871
|
ok: false,
|
|
1651
|
-
reason:
|
|
1872
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1652
1873
|
};
|
|
1653
1874
|
}
|
|
1654
1875
|
// Strip the citation blocks before extraction: their per-row JSON objects
|
|
1655
1876
|
// would otherwise count as competing balanced objects in the extractor.
|
|
1656
|
-
const textWithoutCitations = input.text.replace(
|
|
1877
|
+
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
1657
1878
|
let patch;
|
|
1658
1879
|
try {
|
|
1659
1880
|
patch = extractFrontendImplementationJson(textWithoutCitations);
|
|
@@ -1670,6 +1891,13 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
|
1670
1891
|
reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
|
|
1671
1892
|
};
|
|
1672
1893
|
}
|
|
1894
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1895
|
+
if (protectedViolations.length > 0) {
|
|
1896
|
+
return {
|
|
1897
|
+
ok: false,
|
|
1898
|
+
reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1673
1901
|
return { ok: true, contract: patch };
|
|
1674
1902
|
}
|
|
1675
1903
|
export async function materializeFrontendImplementationContract(input) {
|