@tea-agent/loop-agent 0.44.0-next.7 → 0.44.0-next.9
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 +100 -0
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/guards.js +4 -0
- package/dist/executors/dag-pi/model-config.js +30 -0
- package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +25 -41
- package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
- package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +166 -0
- package/dist/executors/dag-pi/tools/plan-decision-tools.js +519 -0
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +166 -0
- package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
- package/dist/executors/dag-pi-executor.js +198 -118
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +76 -11
- package/dist/executors/shell-executor.js +88 -12
- package/dist/infrastructure/console/operation-store.js +2 -0
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/task/config-types.js +22 -9
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -8
- package/dist/task/frontend-preflight.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +18 -3
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/operator-actions.js +0 -1
- package/dist/worker/console/operator-user-error.js +1 -1
- package/dist/worker/console/prd-intake-bridge.js +5 -15
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DTuku_-7.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CQbCsSqb.js → arc-dT5T4sVL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C-xUntTR.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DhNlwGAl.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B7j3nx8o.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BCrcxEh2.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CsaePDb3.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H--SRQc1rN.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C5BFY2Po.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Caso6uN7.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-AY2ZSAx2.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DiMqZQ_8.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-gpvvriOp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUdyoJhI.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bp1xW3T4.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-D-JIcOIT.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C-NKFflx.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CSOcvOAA.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BQSBltMy.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0LVC45G.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cypg3nqy.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BBDU88XJ.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BZusLqSb.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Cfolotcr.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CjfyUy0m.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DJdz7txE.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CQqgPO6y.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BOpGfS2m.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-C6UCu0qx.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
- package/dist/worker/console/static/assets/{index-DWKejNVJ.js → index-CTMZsDmh.js} +98 -98
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWeaVEUB.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C8gmlBbk.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DF3bm8Bp.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D2fG2Vxi.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BGKgMc2T.js → linear-DGRlieMH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DF6mMPJ9.js → mermaid.core-4P9yPC9c.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-AMhePheC.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DN1Cl0W0.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cx9ngUeN.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BcYPTQJb.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BqHUZWnH.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CmqWZ0-B.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P--4YetH1y.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DGD4VMfD.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-QVw9q7rh.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CuemV9pe.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C7oKzZKs.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C2Of60nT.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DHtpQHyL.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-COi9OhSy.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DPHQfRrt.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +2 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -67
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/backend-test-dag.json +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +0 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +0 -8
|
@@ -1,19 +1,15 @@
|
|
|
1
|
+
import { FRONTEND_PLAN_CANARY_SCENARIOS, readFrontendCanaryEvidence, compareFrontendPairIdentity, readFrontendRootObservation } from "./frontend-root-observation.js";
|
|
1
2
|
import { readFrontendPlanCanaryRunMetrics, } from "./frontend-session-budget.js";
|
|
2
|
-
export
|
|
3
|
-
"missing-idle-loading-empty",
|
|
4
|
-
"loading-not-mounted",
|
|
5
|
-
"test-layer-does-not-prove-wiring",
|
|
6
|
-
"mock-claimed-as-real-integration",
|
|
7
|
-
];
|
|
3
|
+
export { FRONTEND_PLAN_CANARY_SCENARIOS } from "./frontend-root-observation.js";
|
|
8
4
|
const reduction = (before, after) => before !== null && after !== null && before > 0 ? ((before - after) / before) * 100 : null;
|
|
9
5
|
/** Compare two same-input runs. Missing observations fail the canary rather
|
|
10
6
|
* than being interpreted as a zero-cost candidate. */
|
|
11
7
|
export function evaluateFrontendPlanCanary(input) {
|
|
12
|
-
const required = { planSessionsPct: 30, planWallClockPct: 30,
|
|
8
|
+
const required = { planSessionsPct: 30, planWallClockPct: 30, factReplacementPct: 50, ...input.requiredReductions };
|
|
13
9
|
const reductions = {
|
|
14
10
|
planSessionsPct: reduction(input.baseline.planSessions, input.candidate.planSessions),
|
|
15
11
|
planWallClockPct: reduction(input.baseline.planWallClockMs, input.candidate.planWallClockMs),
|
|
16
|
-
|
|
12
|
+
factReplacementPct: reduction(input.baseline.factReplacements, input.candidate.factReplacements),
|
|
17
13
|
};
|
|
18
14
|
const failedChecks = [];
|
|
19
15
|
if (reductions.planSessionsPct === null || reductions.planSessionsPct < required.planSessionsPct)
|
|
@@ -23,31 +19,85 @@ export function evaluateFrontendPlanCanary(input) {
|
|
|
23
19
|
// A zero retry baseline is already optimal: a zero->zero candidate is not a
|
|
24
20
|
// regression and must not block, while zero->positive or missing evidence
|
|
25
21
|
// still fails closed.
|
|
26
|
-
const retriesAlreadyOptimal = input.baseline.
|
|
22
|
+
const retriesAlreadyOptimal = input.baseline.factReplacements === 0 && input.candidate.factReplacements === 0;
|
|
27
23
|
if (!retriesAlreadyOptimal &&
|
|
28
|
-
(reductions.
|
|
29
|
-
failedChecks.push("
|
|
24
|
+
(reductions.factReplacementPct === null || reductions.factReplacementPct < required.factReplacementPct)) {
|
|
25
|
+
failedChecks.push("fact-replacement-reduction");
|
|
30
26
|
}
|
|
31
27
|
for (const scenario of FRONTEND_PLAN_CANARY_SCENARIOS) {
|
|
32
28
|
const result = input.scenarios[scenario];
|
|
33
29
|
if (result !== "blocked" && result !== "reported")
|
|
34
30
|
failedChecks.push(`regression:${scenario}`);
|
|
35
31
|
}
|
|
36
|
-
|
|
32
|
+
for (const metric of ["planModelRequests", "planTokens", "toolRejections", "transportRetries", "correctionSessions", "finalizeRejections"]) {
|
|
33
|
+
const before = input.baseline[metric], after = input.candidate[metric];
|
|
34
|
+
if (before == null || after == null || !Number.isFinite(before) || !Number.isFinite(after) || after < 0 || after > before)
|
|
35
|
+
failedChecks.push(`plan-cost:${metric}`);
|
|
36
|
+
}
|
|
37
|
+
const planPass = failedChecks.length === 0;
|
|
38
|
+
for (const metric of ["rootTokens", "rootReservedRequests"]) {
|
|
39
|
+
const before = input.baseline[metric], after = input.candidate[metric];
|
|
40
|
+
const allowance = metric === "rootTokens" ? 1.1 : 1;
|
|
41
|
+
if (before == null || after == null || !Number.isFinite(before) || !Number.isFinite(after) || before < 0 || after < 0 || after > before * allowance)
|
|
42
|
+
failedChecks.push(`root-cost:${metric}`);
|
|
43
|
+
}
|
|
44
|
+
for (const metric of ["dagWallClockMs", "designReviewRejectRate", "finalReviewDefectRate", "recoveryCount"]) {
|
|
45
|
+
const before = input.baseline[metric], after = input.candidate[metric];
|
|
46
|
+
if (before == null || after == null || !Number.isFinite(before) || !Number.isFinite(after) || after < 0 || after > before)
|
|
47
|
+
failedChecks.push(`quality:${metric}`);
|
|
48
|
+
}
|
|
49
|
+
return { pass: failedChecks.length === 0, planPass, reductions, failedChecks };
|
|
37
50
|
}
|
|
38
51
|
/** Read two frozen run directories and evaluate the same-input canary. Plan
|
|
39
|
-
* cost metrics come from persisted session receipts
|
|
40
|
-
*
|
|
52
|
+
* cost metrics come from persisted session receipts and optionally explicit root
|
|
53
|
+
* lineage. Free numeric supplements cannot establish quality evidence.
|
|
41
54
|
* Missing observations surface as `null` and fail the evaluation, never as a
|
|
42
55
|
* manufactured zero-cost improvement. */
|
|
43
56
|
export async function compareFrontendPlanCanaryRuns(input) {
|
|
44
57
|
const baseline = await readFrontendPlanCanaryRunMetrics(input.baselineRunDir);
|
|
45
58
|
const candidate = await readFrontendPlanCanaryRunMetrics(input.candidateRunDir);
|
|
59
|
+
const lineageChecks = [];
|
|
60
|
+
const regressionEvidence = [];
|
|
61
|
+
const identities = {};
|
|
62
|
+
if (input.rootRuns) {
|
|
63
|
+
for (const [name, metrics, runId] of [["baseline", baseline, input.rootRuns.baselineRunId], ["candidate", candidate, input.rootRuns.candidateRunId]]) {
|
|
64
|
+
const root = await readFrontendRootObservation(input.rootRuns.workspaceRoot, runId);
|
|
65
|
+
const consumed = root.cumulativeBudget?.consumed;
|
|
66
|
+
Object.assign(metrics, root.plan, { dagWallClockMs: root.dagWallClockMs, recoveryCount: root.recoveryCount,
|
|
67
|
+
rootTokens: root.complete && consumed?.missingTokenNodeIds?.length === 0 ? consumed.tokens : null,
|
|
68
|
+
rootReservedRequests: root.complete ? consumed?.providerRequests ?? null : null });
|
|
69
|
+
if (!root.complete)
|
|
70
|
+
lineageChecks.push(`lineage:${name}`);
|
|
71
|
+
if (root.complete && input.evidence?.[name]) {
|
|
72
|
+
try {
|
|
73
|
+
const evidence = await readFrontendCanaryEvidence({ workspaceRoot: input.rootRuns.workspaceRoot, runDir: name === "baseline" ? input.baselineRunDir : input.candidateRunDir, runIds: root.runIds, manifest: input.evidence[name] });
|
|
74
|
+
identities[name] = evidence.identity;
|
|
75
|
+
regressionEvidence.push(evidence.regressions);
|
|
76
|
+
Object.assign(metrics, { designReviewRejectRate: evidence.designReviewRejectRate, finalReviewDefectRate: evidence.finalReviewDefectRate });
|
|
77
|
+
if (!evidence.acceptancePassed)
|
|
78
|
+
lineageChecks.push(`acceptance:${name}`);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
lineageChecks.push(`evidence:${name}:${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
lineageChecks.push(`missing-evidence:${name}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
lineageChecks.push("missing-root-lineage");
|
|
90
|
+
const scenarios = {};
|
|
91
|
+
for (const scenario of FRONTEND_PLAN_CANARY_SCENARIOS) {
|
|
92
|
+
scenarios[scenario] = regressionEvidence.length === 2 && regressionEvidence.every(evidence => evidence[scenario] === "blocked" || evidence[scenario] === "reported") ? "reported" : "not-observed";
|
|
93
|
+
}
|
|
46
94
|
const evaluation = evaluateFrontendPlanCanary({
|
|
47
95
|
baseline,
|
|
48
96
|
candidate,
|
|
49
|
-
scenarios
|
|
97
|
+
scenarios,
|
|
50
98
|
requiredReductions: input.requiredReductions,
|
|
51
99
|
});
|
|
52
|
-
|
|
100
|
+
const identityChecks = compareFrontendPairIdentity(identities.baseline ?? {}, identities.candidate ?? {});
|
|
101
|
+
const failedChecks = [...evaluation.failedChecks, ...identityChecks, ...lineageChecks];
|
|
102
|
+
return { ...evaluation, pass: failedChecks.length === 0, failedChecks, baseline, candidate };
|
|
53
103
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { repairAssertionSchema } from "./frontend-repair-assertions.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
3
4
|
import { deterministicSha256 } from "./frontend-implementation-contract.js";
|
|
@@ -94,7 +95,7 @@ export function projectFrontendSafetyFacts(input) {
|
|
|
94
95
|
const ids = sorted(group?.requirementIds ?? []);
|
|
95
96
|
for (const id of ids)
|
|
96
97
|
verificationByRequirement[id]?.push(focus.id);
|
|
97
|
-
return { id: focus.id, file: focus.file, commandId: focus.commandId, requirementIds: ids };
|
|
98
|
+
return { id: focus.id, file: focus.file, commandId: focus.commandId, requirementIds: ids, evidenceLevel: focus.evidenceLevel, ...(focus.boundary ? { boundary: focus.boundary } : {}) };
|
|
98
99
|
});
|
|
99
100
|
for (const id of requirementIds)
|
|
100
101
|
verificationByRequirement[id] = sorted(verificationByRequirement[id]);
|
|
@@ -443,6 +444,8 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
443
444
|
: declaredStateNamesByGroup.get(focus.behaviorGroupId) ?? [];
|
|
444
445
|
return {
|
|
445
446
|
id: focus.id,
|
|
447
|
+
evidenceLevel: focus.evidenceLevel,
|
|
448
|
+
...(focus.boundary ? { boundary: focus.boundary } : {}),
|
|
446
449
|
commandId: focus.commandId,
|
|
447
450
|
file: focus.file,
|
|
448
451
|
requirementIds,
|
|
@@ -450,7 +453,7 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
450
453
|
};
|
|
451
454
|
});
|
|
452
455
|
const verificationTargetIds = new Set(verificationTargets.map(target => target.id));
|
|
453
|
-
// ---- requirements:
|
|
456
|
+
// ---- requirements: preserve group placement separately from contract deliverable
|
|
454
457
|
// obligations. Every requirement must keep a placement and at least one
|
|
455
458
|
// verification target (both are guaranteed post-finalize because the
|
|
456
459
|
// decision safety projection blocks unplaced / unverified requirements).
|
|
@@ -458,7 +461,7 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
458
461
|
const groupId = groupByRequirement.get(requirement.id);
|
|
459
462
|
const placed = sorted(placementPathsById.get(groupId ?? "") ?? []);
|
|
460
463
|
const deliverable = sorted(requiredByRequirement.get(requirement.id) ?? []);
|
|
461
|
-
const implementationTargets =
|
|
464
|
+
const implementationTargets = placed;
|
|
462
465
|
const derivedVerificationTargetIds = sorted([...(vtIdsByGroup.get(groupId ?? "") ?? [])].filter(id => verificationTargetIds.has(id)));
|
|
463
466
|
return {
|
|
464
467
|
id: requirement.id,
|
|
@@ -613,12 +616,6 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
613
616
|
...(stylingStrategy ? { stylingStrategy } : {}),
|
|
614
617
|
};
|
|
615
618
|
}
|
|
616
|
-
const repairAssertionSchema = z.discriminatedUnion("check", [
|
|
617
|
-
z.object({ check: z.literal("file-absent"), token: z.string().min(1), source: z.string() }).strict(),
|
|
618
|
-
z.object({ check: z.literal("state-present"), token: z.string().min(1), source: z.string() }).strict(),
|
|
619
|
-
z.object({ check: z.literal("vt-changed"), token: z.string().min(1), source: z.string(),
|
|
620
|
-
expectedRequirementIds: z.array(z.string().min(1)).min(1) }).strict(),
|
|
621
|
-
]);
|
|
622
619
|
/** No heuristic fallback: absent structured expectations mean that semantic
|
|
623
620
|
* closure must be decided by the subsequent design review. Invalid explicit
|
|
624
621
|
* assertions throw rather than silently disabling a requested check. */
|
|
@@ -742,26 +739,21 @@ export function collapseDecisionEntriesByKind(kind, collected) {
|
|
|
742
739
|
};
|
|
743
740
|
const byKey = new Map();
|
|
744
741
|
const keyless = [];
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
const retractedKeys = new Set();
|
|
748
|
-
for (const entry of collected) {
|
|
749
|
-
if (entry.retracted === true && typeof entry.interaction === "string") {
|
|
750
|
-
retractedKeys.add(entry.interaction);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
742
|
+
// Chronological last-wins semantics also permit a later explicit correction
|
|
743
|
+
// after a tombstone. Never return the original tombstones as active rows.
|
|
753
744
|
for (const entry of collected) {
|
|
754
|
-
if (entry.retracted === true)
|
|
755
|
-
continue;
|
|
756
745
|
const key = keyOf(entry);
|
|
757
|
-
if (
|
|
746
|
+
if (entry.retracted === true) {
|
|
747
|
+
if (key)
|
|
748
|
+
byKey.delete(key);
|
|
758
749
|
continue;
|
|
750
|
+
}
|
|
759
751
|
if (key)
|
|
760
752
|
byKey.set(key, entry);
|
|
761
753
|
else
|
|
762
754
|
keyless.push(entry);
|
|
763
755
|
}
|
|
764
|
-
return
|
|
756
|
+
return [...byKey.values(), ...keyless];
|
|
765
757
|
}
|
|
766
758
|
/** Compile committed decision facts into a FrontendPlanDecisionContract. Each
|
|
767
759
|
* fact's `entry` is already schema-shaped (the tool validates it); this collects
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { assemblePlanPatchFromCommittedFacts } from "./frontend-committed-facts.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4
|
+
const identity = (params) => {
|
|
5
|
+
const p = object(params), entry = object(p.entry ?? p.choice);
|
|
6
|
+
return String(p.id ?? p.scopeId ?? p.requirementId ?? entry.id ?? entry.name ?? entry.purpose ?? entry.state ?? entry.interaction ?? entry.boundary ?? "singleton");
|
|
7
|
+
};
|
|
8
|
+
/** Counts unresolved diagnostic identities, not tool activity. A changed
|
|
9
|
+
* rationale/read/heartbeat cannot resolve the same compiler diagnostic. */
|
|
10
|
+
export function createFrontendPlanProgressGuard() {
|
|
11
|
+
let stop;
|
|
12
|
+
let terminal = false;
|
|
13
|
+
let failures = [];
|
|
14
|
+
const states = [];
|
|
15
|
+
const warned = new Set();
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
return {
|
|
18
|
+
readStop: () => stop,
|
|
19
|
+
acceptTerminal: () => { if (!stop) {
|
|
20
|
+
terminal = true;
|
|
21
|
+
failures = [];
|
|
22
|
+
} },
|
|
23
|
+
subscribe(listener) { listeners.add(listener); if (stop)
|
|
24
|
+
listener(); return () => { listeners.delete(listener); }; },
|
|
25
|
+
observe(tool, params, details, committed, effectiveDigest) {
|
|
26
|
+
if (stop)
|
|
27
|
+
return stop;
|
|
28
|
+
if (terminal)
|
|
29
|
+
return undefined;
|
|
30
|
+
const key = `${tool}:${identity(params)}`;
|
|
31
|
+
if (details?.ok === true) {
|
|
32
|
+
// Only a successful durable correction of the rejected identity (or
|
|
33
|
+
// a successful compile) resolves it. Unrelated writes do not reset it.
|
|
34
|
+
if (tool.startsWith("finalize_") && committed) {
|
|
35
|
+
terminal = true;
|
|
36
|
+
failures = [];
|
|
37
|
+
}
|
|
38
|
+
else if (committed)
|
|
39
|
+
failures = failures.filter(f => f.key !== key);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const stage = typeof details?.validationStage === "string" ? details.validationStage : undefined;
|
|
43
|
+
const completed = Array.isArray(details?.completedValidationStages) ? details.completedValidationStages : [];
|
|
44
|
+
failures = failures.filter(f => !f.stage || !completed.includes(f.stage));
|
|
45
|
+
const error = String(details?.error ?? details?.code ?? "unknown tool failure").replace(/\s+/g, " ").trim();
|
|
46
|
+
const fingerprint = createHash("sha256").update(JSON.stringify([key, details?.code ?? null, error])).digest("hex");
|
|
47
|
+
failures.push({ fingerprint, key, stage });
|
|
48
|
+
const state = `${stage ?? key}:${fingerprint}:${effectiveDigest ?? "unknown"}`;
|
|
49
|
+
states.push(state);
|
|
50
|
+
if (states.length > 8)
|
|
51
|
+
states.shift();
|
|
52
|
+
failures = failures.slice(-12);
|
|
53
|
+
const tail = failures.slice(-6);
|
|
54
|
+
const repeated = failures.filter(f => f.fingerprint === fingerprint).length;
|
|
55
|
+
if (repeated >= 6 || (tail.length === 6 && new Set(tail.map(f => f.fingerprint)).size <= 2) || (states.length === 8 && new Set(states).size <= 2)) {
|
|
56
|
+
stop = { code: "FRONTEND_PLAN_NO_PROGRESS", fingerprint, error: `Unresolved repeated Plan diagnostic: ${error}` };
|
|
57
|
+
for (const listener of listeners)
|
|
58
|
+
listener();
|
|
59
|
+
return stop;
|
|
60
|
+
}
|
|
61
|
+
if (repeated >= 3 && !warned.has(fingerprint)) {
|
|
62
|
+
warned.add(fingerprint);
|
|
63
|
+
return { code: "FRONTEND_PLAN_REPAIR_HINT", fingerprint, error: `The same diagnostic remains unresolved. Correct the named fact before repeating this call: ${error}` };
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Effective facts for the existing node retry ladder. Replacement history and
|
|
70
|
+
* explanation-only edits must not look like a new implementation decision. */
|
|
71
|
+
export function frontendPlanSemanticDigest(records) {
|
|
72
|
+
const current = new Map();
|
|
73
|
+
const strip = (value) => Array.isArray(value) ? value.map(strip) : value && typeof value === "object"
|
|
74
|
+
? Object.fromEntries(Object.entries(value).filter(([key]) => !["rationale", "replaces"].includes(key)).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, strip(item)])) : value;
|
|
75
|
+
const ordinary = records.some(r => ["plan-requirement", "plan-verification-target", "state-registry", "component-choice", "state-flow", "mock-api"].includes(String(object(r.fact).kind)));
|
|
76
|
+
if (ordinary) {
|
|
77
|
+
try {
|
|
78
|
+
const patch = assemblePlanPatchFromCommittedFacts(records.map(r => ({ ...r, fact: { ...object(r.fact), origin: "plan" } })));
|
|
79
|
+
if (patch)
|
|
80
|
+
return createHash("sha256").update(JSON.stringify(strip(patch))).digest("hex");
|
|
81
|
+
}
|
|
82
|
+
catch { /* An incomplete ledger still needs a stable diagnostic state. */ }
|
|
83
|
+
}
|
|
84
|
+
for (const record of records) {
|
|
85
|
+
if (record.phase !== "committed")
|
|
86
|
+
continue;
|
|
87
|
+
const fact = object(record.fact);
|
|
88
|
+
const key = `${fact.kind}:${identity(fact)}`;
|
|
89
|
+
current.set(key, strip(fact));
|
|
90
|
+
}
|
|
91
|
+
return createHash("sha256").update(JSON.stringify([...current].sort(([a], [b]) => a.localeCompare(b)))).digest("hex");
|
|
92
|
+
}
|
|
@@ -14,8 +14,7 @@ export const FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH = ".runtime/import-manif
|
|
|
14
14
|
/** Staging directory prefix under `active/` for in-flight child materialization. */
|
|
15
15
|
export const FRONTEND_RECOVERY_STAGING_PREFIX = ".frontend-recovery-staging-";
|
|
16
16
|
export function deriveFrontendRecoveryFailureSource(result) {
|
|
17
|
-
if (result.failureSource === "frontend-
|
|
18
|
-
result.failureSource === "frontend-plan-pi" ||
|
|
17
|
+
if (result.failureSource === "frontend-plan-pi" ||
|
|
19
18
|
result.failureSource === "frontend-design-policy-shell" ||
|
|
20
19
|
result.failureSource === "frontend-implement-pi" ||
|
|
21
20
|
result.failureSource === "frontend-verify-shell") {
|
|
@@ -62,14 +61,14 @@ function computeFrontendRecoveryPlanForSource(input) {
|
|
|
62
61
|
// verification commands / trace. Resetting only verify + its descendants
|
|
63
62
|
// would re-run the same failing checks against the same implementation, so
|
|
64
63
|
// reset from implement: the recovery re-implements and re-verifies.
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
const ownerRoots = { contract: "frontend-contract-pi", scout: "frontend-scout-pi", plan: "frontend-plan-pi", implementation: "frontend-implement-pi", "verification-config": "frontend-contract-pi" };
|
|
65
|
+
if (input.failureOwner && !ownerRoots[input.failureOwner])
|
|
66
|
+
throw new Error(`FRONTEND_RECOVERY_OWNER_UNSUPPORTED: ${input.failureOwner}; resolve the owning input before retrying`);
|
|
67
|
+
const resetRoot = input.failureOwner ? ownerRoots[input.failureOwner] :
|
|
68
|
+
input.failureSource === "frontend-verify-shell" || input.failureSource === "frontend-review-pi" ? "frontend-implement-pi" :
|
|
69
|
+
input.failureSource === "frontend-design-review-pi" ? "frontend-plan-pi" : input.failureSource;
|
|
70
|
+
if (!input.spec.tasks.some(task => task.id === resetRoot))
|
|
71
|
+
throw new Error(`FRONTEND_RECOVERY_PRODUCER_UNAVAILABLE: ${resetRoot}; this topology has no authorized producer, reproject the task contract instead of importing the same candidate`);
|
|
73
72
|
const resetNodeIds = computeResetClosure(input.spec, resetRoot);
|
|
74
73
|
const resetSet = new Set(resetNodeIds);
|
|
75
74
|
const importedNodeIds = input.spec.tasks
|
|
@@ -81,6 +80,8 @@ function computeFrontendRecoveryPlanForSource(input) {
|
|
|
81
80
|
parentRunId: input.parentRunId,
|
|
82
81
|
requestId: input.requestId,
|
|
83
82
|
failureSource: input.failureSource,
|
|
83
|
+
...(input.failureOwner ? { failureOwner: input.failureOwner } : {}),
|
|
84
|
+
resetRootNodeId: resetRoot,
|
|
84
85
|
resetNodeIds,
|
|
85
86
|
importedNodeIds,
|
|
86
87
|
};
|
|
@@ -298,6 +298,34 @@ async function casUpdateFrontendRecoveryState(parentRunDir, expectedRevision, de
|
|
|
298
298
|
}
|
|
299
299
|
async function materializeChildStaging(input) {
|
|
300
300
|
const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, recoveryPlan, requestId, childRunId, stagingDir, createdAt, } = input;
|
|
301
|
+
// Phase 5: writer transient partial-write recovery resets the writer subtree
|
|
302
|
+
// (frontend-implement-pi) instead of the candidate producer; everything
|
|
303
|
+
// upstream is imported as verified parent facts.
|
|
304
|
+
let plan;
|
|
305
|
+
if (recoveryPlan) {
|
|
306
|
+
plan = recoveryPlan;
|
|
307
|
+
}
|
|
308
|
+
else if (recovery.failureSource) {
|
|
309
|
+
plan = computeFrontendRecoveryPlanForSource({
|
|
310
|
+
spec: parentSpec,
|
|
311
|
+
parentRunId,
|
|
312
|
+
requestId,
|
|
313
|
+
failureSource: recovery.failureSource,
|
|
314
|
+
failureOwner: recovery.failureOwner,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const admissionRead = await readFrontendWriterAdmissionResult(parentRunDir);
|
|
319
|
+
if (!admissionRead.ok) {
|
|
320
|
+
throw new Error(admissionRead.reason);
|
|
321
|
+
}
|
|
322
|
+
plan = computeFrontendRecoveryPlan({
|
|
323
|
+
spec: parentSpec,
|
|
324
|
+
parentRunId,
|
|
325
|
+
requestId,
|
|
326
|
+
result: admissionRead.result,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
301
329
|
await prepareRunDir(stagingDir);
|
|
302
330
|
// Design-review rejection must be visible to the reset planner. Recovery
|
|
303
331
|
// children otherwise copy the old spec and only rerun design review, which
|
|
@@ -305,6 +333,8 @@ async function materializeChildStaging(input) {
|
|
|
305
333
|
// findings into the child spec as ordinary rerun feedback.
|
|
306
334
|
let childSpec = parentSpec;
|
|
307
335
|
if (recovery.failureSource === "frontend-plan-pi" ||
|
|
336
|
+
recovery.failureSource === "frontend-design-review-pi" ||
|
|
337
|
+
plan.resetRootNodeId === "frontend-contract-pi" ||
|
|
308
338
|
recovery.failureSource === "frontend-review-pi") {
|
|
309
339
|
const designRecovery = parentState.terminalReason === "design-request-changes";
|
|
310
340
|
const reviewRecovery = recovery.failureSource === "frontend-review-pi" ||
|
|
@@ -331,12 +361,13 @@ async function materializeChildStaging(input) {
|
|
|
331
361
|
}
|
|
332
362
|
}
|
|
333
363
|
catch (error) {
|
|
334
|
-
|
|
335
|
-
throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
336
|
-
}
|
|
364
|
+
throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
337
365
|
}
|
|
338
366
|
if (feedback) {
|
|
339
367
|
childSpec = { ...parentSpec, rerunFeedback: feedback };
|
|
368
|
+
if (plan.resetRootNodeId === "frontend-contract-pi") {
|
|
369
|
+
childSpec = { ...childSpec, tasks: childSpec.tasks.map(task => task.id !== plan.resetRootNodeId ? task : { ...task, subtask_prompt: `${task.subtask_prompt}\n\n<contract_recovery_diagnostic>\nParent run: ${parentRunId}\nSource node: ${feedback.sourceNodeId}\nRe-extract only from the unchanged frozen task source. These parent findings are diagnostic hypotheses, not new requirements or authorization. Do not import the parent's Plan as Contract facts. Report unresolved source ambiguity rather than inventing a decision.\n${feedback.feedbackText}\n</contract_recovery_diagnostic>` }) };
|
|
370
|
+
}
|
|
340
371
|
}
|
|
341
372
|
else if (designRecovery || reviewRecovery) {
|
|
342
373
|
// A design/review rejection cannot be safely retried without its typed
|
|
@@ -346,33 +377,6 @@ async function materializeChildStaging(input) {
|
|
|
346
377
|
}
|
|
347
378
|
}
|
|
348
379
|
await writeJsonVerified(stagingDir, "run.json", childSpec);
|
|
349
|
-
// Phase 5: writer transient partial-write recovery resets the writer subtree
|
|
350
|
-
// (frontend-implement-pi) instead of the candidate producer; everything
|
|
351
|
-
// upstream is imported as verified parent facts.
|
|
352
|
-
let plan;
|
|
353
|
-
if (recoveryPlan) {
|
|
354
|
-
plan = recoveryPlan;
|
|
355
|
-
}
|
|
356
|
-
else if (recovery.failureSource) {
|
|
357
|
-
plan = computeFrontendRecoveryPlanForSource({
|
|
358
|
-
spec: parentSpec,
|
|
359
|
-
parentRunId,
|
|
360
|
-
requestId,
|
|
361
|
-
failureSource: recovery.failureSource,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
const admissionRead = await readFrontendWriterAdmissionResult(parentRunDir);
|
|
366
|
-
if (!admissionRead.ok) {
|
|
367
|
-
throw new Error(admissionRead.reason);
|
|
368
|
-
}
|
|
369
|
-
plan = computeFrontendRecoveryPlan({
|
|
370
|
-
spec: parentSpec,
|
|
371
|
-
parentRunId,
|
|
372
|
-
requestId,
|
|
373
|
-
result: admissionRead.result,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
380
|
const { ranks } = topoSortToRanks(childSpec);
|
|
377
381
|
const resetSet = new Set(plan.resetNodeIds);
|
|
378
382
|
const importedSet = new Set(plan.importedNodeIds);
|
|
@@ -408,16 +412,16 @@ async function materializeChildStaging(input) {
|
|
|
408
412
|
}
|
|
409
413
|
// Canonical contract + writer-admission identity facts are copied alongside
|
|
410
414
|
// the per-node facts so reset nodes can re-read them (AC-4).
|
|
411
|
-
const canonicalContract = await copyArtifactVerified({
|
|
415
|
+
const canonicalContract = plan.importedNodeIds.some(id => ["frontend-design-policy-shell", "frontend-contract-import-shell"].includes(id)) ? await copyArtifactVerified({
|
|
412
416
|
parentRunDir,
|
|
413
417
|
newRunDir: stagingDir,
|
|
414
418
|
relativePath: resolveCanonicalContractRelPath(childSpec),
|
|
415
|
-
});
|
|
416
|
-
const writerAdmissionResult = await copyArtifactVerified({
|
|
419
|
+
}) : undefined;
|
|
420
|
+
const writerAdmissionResult = plan.importedNodeIds.includes("frontend-writer-admission-shell") ? await copyArtifactVerified({
|
|
417
421
|
parentRunDir,
|
|
418
422
|
newRunDir: stagingDir,
|
|
419
423
|
relativePath: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
|
|
420
|
-
});
|
|
424
|
+
}) : undefined;
|
|
421
425
|
// Imported nodes keep their parent FINISHED record, so the run-owned files
|
|
422
426
|
// their bodies wrote outside the node directory travel with them (smoke r31:
|
|
423
427
|
// a review-triggered recovery reached review-context with no worktree
|
|
@@ -470,6 +474,7 @@ async function materializeChildStaging(input) {
|
|
|
470
474
|
}
|
|
471
475
|
}
|
|
472
476
|
const childState = {
|
|
477
|
+
...(parentSpec.sourceProviderBudget ? { sourceProviderBudget: structuredClone(parentSpec.sourceProviderBudget) } : {}),
|
|
473
478
|
...(parentState.budget ? { budget: structuredClone(parentState.budget) } : {}),
|
|
474
479
|
...(parentState.budgetLedger ? { budgetLedger: structuredClone(parentState.budgetLedger) } : {}),
|
|
475
480
|
version: 1,
|
|
@@ -506,6 +511,8 @@ async function materializeChildStaging(input) {
|
|
|
506
511
|
...(recovery.failureSource
|
|
507
512
|
? { failureSource: recovery.failureSource }
|
|
508
513
|
: {}),
|
|
514
|
+
...(plan.failureOwner ? { failureOwner: plan.failureOwner } : {}),
|
|
515
|
+
...(plan.resetRootNodeId ? { resetRootNodeId: plan.resetRootNodeId } : {}),
|
|
509
516
|
revision: 0,
|
|
510
517
|
},
|
|
511
518
|
...(parentState.evaluation
|
|
@@ -518,14 +525,8 @@ async function materializeChildStaging(input) {
|
|
|
518
525
|
parentRunId,
|
|
519
526
|
requestId,
|
|
520
527
|
createdAt,
|
|
521
|
-
canonicalContract: {
|
|
522
|
-
|
|
523
|
-
sha256: canonicalContract.sha256,
|
|
524
|
-
},
|
|
525
|
-
writerAdmissionResult: {
|
|
526
|
-
relativePath: writerAdmissionResult.destinationRelativePath,
|
|
527
|
-
sha256: writerAdmissionResult.sha256,
|
|
528
|
-
},
|
|
528
|
+
...(canonicalContract ? { canonicalContract: { relativePath: canonicalContract.destinationRelativePath, sha256: canonicalContract.sha256 } } : {}),
|
|
529
|
+
...(writerAdmissionResult ? { writerAdmissionResult: { relativePath: writerAdmissionResult.destinationRelativePath, sha256: writerAdmissionResult.sha256 } } : {}),
|
|
529
530
|
importedRunArtifacts,
|
|
530
531
|
importedFacts,
|
|
531
532
|
};
|
|
@@ -590,7 +591,8 @@ export async function stageRecoveryChild(options) {
|
|
|
590
591
|
recovery.recoveryRootRunId !== intent.recoveryRootRunId ||
|
|
591
592
|
recovery.attemptIndex !== intent.attemptIndex ||
|
|
592
593
|
recovery.continuationCount !== intent.continuationCount ||
|
|
593
|
-
recovery.failureSource !== intent.failureSource
|
|
594
|
+
recovery.failureSource !== intent.failureSource ||
|
|
595
|
+
recovery.failureOwner !== intent.failureOwner || recovery.resetRootNodeId !== intent.resetRootNodeId) {
|
|
594
596
|
throw new Error("frontend recovery intent replay binding mismatch");
|
|
595
597
|
}
|
|
596
598
|
}
|
|
@@ -620,7 +622,8 @@ export async function stageRecoveryChild(options) {
|
|
|
620
622
|
if (options.recoveryPlan) {
|
|
621
623
|
if (options.recoveryPlan.parentRunId !== parentRunId ||
|
|
622
624
|
options.recoveryPlan.requestId !== requestId ||
|
|
623
|
-
options.recoveryPlan.failureSource !== recovery.failureSource
|
|
625
|
+
options.recoveryPlan.failureSource !== recovery.failureSource ||
|
|
626
|
+
options.recoveryPlan.failureOwner !== recovery.failureOwner || options.recoveryPlan.resetRootNodeId !== recovery.resetRootNodeId) {
|
|
624
627
|
throw new Error("frontend recovery plan binding mismatch");
|
|
625
628
|
}
|
|
626
629
|
const expectedPlan = computeFrontendRecoveryPlanForSource({
|
|
@@ -628,9 +631,11 @@ export async function stageRecoveryChild(options) {
|
|
|
628
631
|
parentRunId,
|
|
629
632
|
requestId,
|
|
630
633
|
failureSource: recovery.failureSource,
|
|
634
|
+
failureOwner: recovery.failureOwner,
|
|
631
635
|
});
|
|
632
|
-
if (
|
|
633
|
-
JSON.stringify(
|
|
636
|
+
if (expectedPlan.resetRootNodeId !== options.recoveryPlan.resetRootNodeId ||
|
|
637
|
+
JSON.stringify(expectedPlan.resetNodeIds) !==
|
|
638
|
+
JSON.stringify(options.recoveryPlan.resetNodeIds) ||
|
|
634
639
|
JSON.stringify(expectedPlan.importedNodeIds) !==
|
|
635
640
|
JSON.stringify(options.recoveryPlan.importedNodeIds)) {
|
|
636
641
|
throw new Error("frontend recovery plan drifted from parent DAG");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const repairAssertionSchema = z.discriminatedUnion("check", [
|
|
3
|
+
z.object({ check: z.literal("file-absent"), token: z.string().min(1).max(512), source: z.string().max(4096) }).strict(),
|
|
4
|
+
z.object({ check: z.literal("state-present"), token: z.string().min(1).max(512), source: z.string().max(4096) }).strict(),
|
|
5
|
+
z.object({ check: z.literal("vt-changed"), token: z.string().min(1).max(512), source: z.string().max(4096), expectedRequirementIds: z.array(z.string().min(1)).min(1).max(128) }).strict(),
|
|
6
|
+
]);
|
|
7
|
+
/** Invalid optional assertions never erase a blocking finding or its verdict. */
|
|
8
|
+
export function sanitizeRepairAssertions(value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return { assertions: [], diagnostics: [] };
|
|
11
|
+
if (!Array.isArray(value))
|
|
12
|
+
return { assertions: [], diagnostics: ["repairAssertions must be an array; semantic review remains required"] };
|
|
13
|
+
const assertions = [], diagnostics = [];
|
|
14
|
+
for (const [index, raw] of value.slice(0, 32).entries()) {
|
|
15
|
+
const parsed = repairAssertionSchema.safeParse(raw);
|
|
16
|
+
if (parsed.success)
|
|
17
|
+
assertions.push(parsed.data);
|
|
18
|
+
else
|
|
19
|
+
diagnostics.push(`repairAssertions[${index}] invalid; semantic review remains required`);
|
|
20
|
+
}
|
|
21
|
+
if (value.length > 32)
|
|
22
|
+
diagnostics.push("repairAssertions exceeds 32 items; excess items require semantic review");
|
|
23
|
+
return { assertions, diagnostics };
|
|
24
|
+
}
|
|
25
|
+
/** Review annotations cannot authorize deletion or invent requirements/states. */
|
|
26
|
+
export function authorizeRepairAssertions(assertions, input) {
|
|
27
|
+
const files = new Set(input.parent.targets?.files ?? []);
|
|
28
|
+
const required = new Set([...(input.parent.targets?.requiredDeliverables ?? []), ...(input.requiredDeliverables ?? [])]);
|
|
29
|
+
const states = new Set(input.stateIds);
|
|
30
|
+
const targets = new Set(input.parent.verificationTargets?.map(target => String(target.id)) ?? []);
|
|
31
|
+
return assertions.flatMap(assertion => {
|
|
32
|
+
const permitted = assertion.check === "file-absent" ? files.has(assertion.token) && !required.has(assertion.token) :
|
|
33
|
+
assertion.check === "state-present" ? states.has(assertion.token) : targets.has(assertion.token) && assertion.expectedRequirementIds.every(id => input.requirementIds.includes(id));
|
|
34
|
+
return permitted ? [] : [`${assertion.check}: ${assertion.token} is outside frozen repair authority or contradicts a required delivery`];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
3
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
5
6
|
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
|
|
@@ -244,6 +245,31 @@ function assertReviewEvidence(input) {
|
|
|
244
245
|
throw new FrontendReviewContextFailure("review-context-verification-unbound", "frontend review context verification trace is not bound to a passing contract");
|
|
245
246
|
}
|
|
246
247
|
}
|
|
248
|
+
/** Snapshot only exact behavior-test paths already named by the frozen contract. */
|
|
249
|
+
export async function captureFrontendReviewTestSources(input) {
|
|
250
|
+
const sources = [];
|
|
251
|
+
for (const [index, file] of [...new Set(input.files)].entries()) {
|
|
252
|
+
const targetPath = path.resolve(input.workspaceRoot, file);
|
|
253
|
+
const exists = await lstat(targetPath).catch((error) => { if (error.code === "ENOENT")
|
|
254
|
+
return undefined; throw error; });
|
|
255
|
+
if (!exists) {
|
|
256
|
+
sources.push({ file, status: "unavailable", scopes: [] });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: input.workspaceRoot, targetPath, label: "frontend review test source" });
|
|
260
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
261
|
+
const text = bytes.toString("utf8");
|
|
262
|
+
const scopes = [];
|
|
263
|
+
// Small text slices bound each model input; all slices are retained.
|
|
264
|
+
for (let offset = 0; offset < Math.max(1, text.length); offset += 2000) {
|
|
265
|
+
const relative = `contracts/test-source-${index}-${offset / 2000}.json`;
|
|
266
|
+
await writeDagRunJsonArtifact(input.runDir, relative, { schemaVersion: 1, file, sourceSha256: sha256, offset, totalCharacters: text.length, text: text.slice(offset, offset + 2000) });
|
|
267
|
+
scopes.push({ path: relative, sha256: createHash("sha256").update(await readFile(path.join(input.runDir, relative))).digest("hex") });
|
|
268
|
+
}
|
|
269
|
+
sources.push({ file, status: "captured", sha256, scopes });
|
|
270
|
+
}
|
|
271
|
+
return sources;
|
|
272
|
+
}
|
|
247
273
|
export async function runFrontendReviewContextGate(input) {
|
|
248
274
|
const contractRelativePath = "contracts/frontend-implementation-contract.json";
|
|
249
275
|
let contractRaw;
|
|
@@ -284,6 +310,7 @@ export async function runFrontendReviewContextGate(input) {
|
|
|
284
310
|
const verifyFailure = await readOptionalVerifyFailure(input.runDir);
|
|
285
311
|
const lintAssessment = await readOptionalLintAssessment(input.runDir);
|
|
286
312
|
const verificationEvidence = await readVerificationEvidence(input.runDir);
|
|
313
|
+
const testSources = await captureFrontendReviewTestSources({ ...input, files: parsedContract.data.verificationTargets.filter(target => target.mode === "behavior").map(target => target.file) });
|
|
287
314
|
const cumulativeDiff = await captureCumulativeDiffContext({
|
|
288
315
|
runDir: input.runDir,
|
|
289
316
|
workspaceRoot: input.workspaceRoot,
|
|
@@ -320,6 +347,7 @@ export async function runFrontendReviewContextGate(input) {
|
|
|
320
347
|
},
|
|
321
348
|
verificationTrace,
|
|
322
349
|
verificationEvidence,
|
|
350
|
+
testSources,
|
|
323
351
|
// Protocol-legal lint vocabulary; `lintConfigured: false` tells the
|
|
324
352
|
// reviewer that this task declares no lint surface at all, so
|
|
325
353
|
// `unavailable` reads as "not applicable", not "missing evidence".
|