@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.19
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 +1 -1
- package/CHANGELOG.md +68 -2309
- package/README.md +7 -0
- package/dist/application/dag/args.js +8 -0
- package/dist/application/task-lifecycle/advance.js +38 -7
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +2 -0
- package/dist/cli/program.js +3 -1
- package/dist/cli/update/init-surface-notifier.js +2 -0
- package/dist/cli/update/policy.js +2 -0
- package/dist/commands/cursor-prompt.js +2 -0
- package/dist/commands/dag-approve.js +2 -0
- package/dist/commands/dag-final-verification.js +2 -0
- package/dist/commands/dag-init-hybrid.js +2 -0
- package/dist/commands/dag-reconcile-tasks.js +2 -0
- package/dist/commands/dag-reject.js +2 -0
- package/dist/commands/dag-report.js +2 -0
- package/dist/commands/dag-rerun.js +2 -0
- package/dist/commands/dag-resume.js +2 -0
- package/dist/commands/dag-workflow-compile.js +2 -0
- package/dist/commands/dag-workflow-plan.js +2 -0
- package/dist/commands/dag-workflow-validate.js +2 -0
- package/dist/commands/delegate.js +2 -0
- package/dist/commands/examples.js +2 -0
- package/dist/commands/import-prd.js +2 -0
- package/dist/commands/init-model-catalog.js +2 -0
- package/dist/commands/init.js +19 -13
- package/dist/commands/instructions.js +2 -0
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/loop-benchmark.js +2 -0
- package/dist/commands/pi-prompt.js +2 -0
- package/dist/commands/pi-reuse-benchmark.js +2 -0
- package/dist/commands/promote-run.js +2 -0
- package/dist/commands/stats.js +1 -1
- package/dist/commands/study-init.js +2 -0
- package/dist/commands/task-advance.js +34 -0
- package/dist/commands/task-source-prepare.js +2 -0
- package/dist/commands/worktree-create.js +2 -0
- package/dist/commands/worktree-remove.js +2 -0
- package/dist/executors/dag-pi-executor.js +1640 -194
- package/dist/executors/pi-executor.js +85 -3
- package/dist/executors/pi-sdk-executor.js +18 -0
- package/dist/executors/shell-executor.js +92 -7
- package/dist/executors/shell-verification.js +3 -0
- package/dist/governance/exec-plans.js +6 -3
- package/dist/infrastructure/console/app-data.js +6 -0
- package/dist/shared/artifacts-core.js +2 -0
- package/dist/shared/backend-dogfood-preflight.js +47 -0
- package/dist/shared/dag-failure-category.js +3 -0
- package/dist/shared/git-progress.js +2 -0
- package/dist/shared/one-shot-prompt-args.js +2 -0
- package/dist/shared/operator/capabilities.js +180 -0
- package/dist/shared/package-metadata.js +6 -4
- package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
- package/dist/shared/reference-context.js +2 -0
- package/dist/task/config-types.js +29 -0
- package/dist/task/contract/project.js +9 -0
- package/dist/task/contract/recover.js +6 -0
- package/dist/task/contract/schema.js +17 -0
- package/dist/task/source-prepare/artifact-meta.js +9 -2
- package/dist/task/source-prepare/build-draft.js +60 -0
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/task/source-prepare/parse-intent.js +36 -11
- package/dist/task/task-demand-routing.js +5 -2
- package/dist/worker/console/chat/browser-automation.js +468 -308
- package/dist/worker/console/chat/browser-routes.js +175 -57
- package/dist/worker/console/chat/explore-tools.js +5 -2
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +115 -20
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +24 -16
- package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
- package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
- package/dist/worker/console/chat/pi-runtime.js +95 -28
- package/dist/worker/console/chat/routes.js +20 -1
- package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
- package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
- package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
- package/dist/worker/console/chat/session-mode.js +8 -4
- package/dist/worker/console/chat/session-store.js +42 -9
- package/dist/worker/console/chat/tool-preview.js +115 -0
- package/dist/worker/console/chat/tools.js +7 -1
- package/dist/worker/console/chat/turn-order.js +13 -0
- package/dist/worker/console/chat/turn-process.js +32 -30
- package/dist/worker/console/operator-actions.js +50 -0
- package/dist/worker/console/prd-intake-bridge.js +54 -1
- package/dist/worker/console/scheduled-goal-host.js +98 -0
- package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
- package/dist/worker/console/server.js +15 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-eoQiMvuk.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
- package/dist/worker/console/static/assets/{index-wXlQohNs.js → index-Cdkvw_H6.js} +97 -97
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-BDZ9riWi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-7pKqYtpZ.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
- package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
- package/dist/worker/console/static-src/prd-file-import.js +3 -1
- package/dist/worker/console/workspace-context.js +17 -1
- package/dist/worker/delivery/git-transaction.js +2 -0
- package/dist/worker/feature/fullstack-validate.js +2 -0
- package/dist/worker/loop-agent/loop-agent-client.js +8 -2
- package/dist/worker/materialize/harness-task-materializer.js +4 -1
- package/dist/worker/metrics/projector.js +1 -1
- package/dist/worker/observability/event-store.js +4 -1
- package/dist/worker/observability/read-model.js +4 -1
- package/dist/worker/observe/node-transparency.js +23 -1
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/operator-chrome.js +3 -1
- package/dist/worker/outcomes/declared-artifacts.js +2 -0
- package/dist/worker/preflight.js +3 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
- package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
- package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
- package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
- package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
- package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
- package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
- package/dist/worker/task-spec/validate.js +4 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
- package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
- package/dist/workflows/dag/dag-retry-schema.js +11 -0
- package/dist/workflows/dag/decision-envelope.js +2 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
- package/dist/workflows/dag/frontend-closeout.js +4 -2
- package/dist/workflows/dag/frontend-committed-facts.js +461 -0
- package/dist/workflows/dag/frontend-durable-tools.js +15 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +365 -8
- package/dist/workflows/dag/frontend-plan-canary.js +53 -0
- package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
- package/dist/workflows/dag/frontend-plan-render.js +0 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
- package/dist/workflows/dag/frontend-recovery-run.js +57 -0
- package/dist/workflows/dag/frontend-review-context.js +43 -70
- package/dist/workflows/dag/frontend-risk.js +92 -10
- package/dist/workflows/dag/frontend-session-budget.js +117 -3
- package/dist/workflows/dag/frontend-shape.js +11 -55
- package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
- package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
- package/dist/workflows/dag/frontend-verification-trace.js +25 -12
- package/dist/workflows/dag/frontend-writer-admission.js +3 -47
- package/dist/workflows/dag/frontend-writer-status.js +0 -23
- package/dist/workflows/dag/init-hybrid.js +202 -111
- package/dist/workflows/dag/interrupt-request.js +2 -0
- package/dist/workflows/dag/lifecycle.js +11 -2
- package/dist/workflows/dag/node-execution.js +50 -13
- package/dist/workflows/dag/reconcile-run.js +2 -0
- package/dist/workflows/dag/repair-artifact.js +3 -1
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/rerun-plan.js +10 -0
- package/dist/workflows/dag/rerun-task.js +79 -1
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/scheduler.js +2 -4
- package/dist/workflows/dag/types.js +44 -6
- package/dist/workflows/dag/validate.js +4 -0
- package/docs/README.md +1 -0
- package/docs/governance/README.md +1 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +14 -10
- package/docs/templates/frontend-implementation-contract.schema.json +0 -7
- package/docs/templates/frontend-implementation-dag.json +8 -9
- package/package.json +8 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -4
- package/skills/frontend-design-review/SKILL.md +7 -12
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +8 -18
- package/skills/frontend-plan/references/decision-contract.md +19 -26
- package/skills/frontend-review/SKILL.md +21 -25
- package/skills/frontend-review/references/review-findings.md +48 -24
- package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
|
@@ -443,6 +443,306 @@ async function deriveWriteSetFromRun(runDir) {
|
|
|
443
443
|
}
|
|
444
444
|
return [...writeSet];
|
|
445
445
|
}
|
|
446
|
+
/** Read the frozen task source paths from run.json sourceBinding so the plan
|
|
447
|
+
* precheck can require new/specified component choices to cite a traceable
|
|
448
|
+
* task source. Smoke r37/r38: specReference.path pointed at a to-be-implemented
|
|
449
|
+
* file or an AC id — a demand that only lived in review prose could never be
|
|
450
|
+
* closed by the plan retry ladder. The allowed set is enumerable here. */
|
|
451
|
+
async function deriveFrontendSourceBindingPathsFromRun(runDir) {
|
|
452
|
+
const specPath = path.join(runDir, "run.json");
|
|
453
|
+
let raw;
|
|
454
|
+
try {
|
|
455
|
+
raw = await readFile(specPath, "utf8");
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
let spec;
|
|
461
|
+
try {
|
|
462
|
+
spec = JSON.parse(raw);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
const paths = [];
|
|
468
|
+
for (const source of spec.sourceBinding?.sources ?? []) {
|
|
469
|
+
if (source &&
|
|
470
|
+
typeof source === "object" &&
|
|
471
|
+
typeof source.path === "string" &&
|
|
472
|
+
(source.path).trim()) {
|
|
473
|
+
paths.push((source.path).trim());
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return paths;
|
|
477
|
+
}
|
|
478
|
+
function stripSourcePathFragment(value) {
|
|
479
|
+
const hash = value.indexOf("#");
|
|
480
|
+
return (hash >= 0 ? value.slice(0, hash) : value).trim().replace(/\/$/, "");
|
|
481
|
+
}
|
|
482
|
+
/** decision=new/specified component choices must cite a frozen task source
|
|
483
|
+
* path (requirement/constraint), never a to-be-implemented file or a bare
|
|
484
|
+
* acceptance-criterion id. Absent sourceBinding paths (legacy fixtures) the
|
|
485
|
+
* check is skipped — generation always freezes them. */
|
|
486
|
+
function checkComponentChoiceSourceReferences(canonical, sourcePaths) {
|
|
487
|
+
const findings = [];
|
|
488
|
+
if (sourcePaths.length === 0)
|
|
489
|
+
return findings;
|
|
490
|
+
for (const choice of canonical.uiComponentChoices ?? []) {
|
|
491
|
+
if (choice.decision !== "new" && choice.decision !== "specified") {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const rawPath = choice.specReference?.path ?? "";
|
|
495
|
+
const normalized = stripSourcePathFragment(rawPath);
|
|
496
|
+
const traceable = normalized.length > 0 &&
|
|
497
|
+
sourcePaths.some((source) => stripSourcePathFragment(source) === normalized);
|
|
498
|
+
if (!traceable) {
|
|
499
|
+
findings.push({
|
|
500
|
+
code: "component-choice-source-reference-invalid",
|
|
501
|
+
message: `component choice "${choice.component}" (decision=${choice.decision}) must cite a frozen task source path as specReference.path; allowed: [${sourcePaths.join(", ")}]${rawPath ? ` — got "${rawPath}"` : " — missing"}`,
|
|
502
|
+
path: "uiComponentChoices",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return findings;
|
|
507
|
+
}
|
|
508
|
+
const CAPABILITY_BOUNDARY_MARKER = "测试能力边界(冻结)";
|
|
509
|
+
export async function deriveFrontendInteractionIdsFromRun(runDir) {
|
|
510
|
+
const specPath = path.join(runDir, "run.json");
|
|
511
|
+
let raw;
|
|
512
|
+
try {
|
|
513
|
+
raw = await readFile(specPath, "utf8");
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
let spec;
|
|
519
|
+
try {
|
|
520
|
+
spec = JSON.parse(raw);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
for (const task of spec.tasks ?? []) {
|
|
526
|
+
const ids = task.shell?.frontendDesignPolicy?.interactionIds;
|
|
527
|
+
if (ids && ids.length > 0)
|
|
528
|
+
return ids.filter((id) => typeof id === "string" && id.trim());
|
|
529
|
+
}
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
function checkInteractionIdVocabulary(canonical, declaredIds) {
|
|
533
|
+
const findings = [];
|
|
534
|
+
if (declaredIds.length === 0)
|
|
535
|
+
return findings;
|
|
536
|
+
for (const interaction of canonical.interactions ?? []) {
|
|
537
|
+
if (!declaredIds.includes(interaction.name)) {
|
|
538
|
+
findings.push({
|
|
539
|
+
code: "interaction-name-not-declared",
|
|
540
|
+
message: `interaction "${interaction.name}" is not in the frozen interaction id vocabulary; allowed: [${declaredIds.join(", ")}] — retract the wrong record via record_data_flow {"interaction": "${interaction.name}", "retracted": true} or retract_data_flow, then record the declared id`,
|
|
541
|
+
path: "interactions",
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return findings;
|
|
546
|
+
}
|
|
547
|
+
export async function deriveFrontendAllowedMockStrategiesFromRun(runDir) {
|
|
548
|
+
const specPath = path.join(runDir, "run.json");
|
|
549
|
+
let raw;
|
|
550
|
+
try {
|
|
551
|
+
raw = await readFile(specPath, "utf8");
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
let spec;
|
|
557
|
+
try {
|
|
558
|
+
spec = JSON.parse(raw);
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
for (const task of spec.tasks ?? []) {
|
|
564
|
+
const strategies = task.shell?.frontendDesignPolicy?.allowedMockStrategies;
|
|
565
|
+
if (strategies &&
|
|
566
|
+
strategies.length > 0 &&
|
|
567
|
+
strategies.every((s) => typeof s === "string" && s.trim())) {
|
|
568
|
+
return strategies;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
function checkMockApiStrategyAllowed(canonical, allowedStrategies) {
|
|
574
|
+
const findings = [];
|
|
575
|
+
if (allowedStrategies.length === 0)
|
|
576
|
+
return findings;
|
|
577
|
+
const strategy = canonical.mockApi?.strategy;
|
|
578
|
+
if (strategy && !allowedStrategies.includes(strategy)) {
|
|
579
|
+
const onlyNotNeeded = allowedStrategies.length === 1 && allowedStrategies[0] === "not-needed";
|
|
580
|
+
findings.push({
|
|
581
|
+
code: "mock-strategy-not-allowed",
|
|
582
|
+
message: `mockApi.strategy "${strategy}" is not in the frozen allowed strategies; allowed: [${allowedStrategies.join(", ")}] — re-record the api mock decision with an allowed strategy (relationship path: record_mock_api {"mockApi": {...}, "replace": true}; decision path: record_api_mock_boundary with the same boundary and a mode that maps to an allowed strategy)${onlyNotNeeded ? "; the run has no deterministic Mock verification capability, so keep strategy \"not-needed\" and preserve the real integration gap instead of inventing Mock evidence" : ""}`,
|
|
583
|
+
path: "mockApi",
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return findings;
|
|
587
|
+
}
|
|
588
|
+
async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
|
|
589
|
+
const specPath = path.join(runDir, "run.json");
|
|
590
|
+
let raw;
|
|
591
|
+
try {
|
|
592
|
+
raw = await readFile(specPath, "utf8");
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
let spec;
|
|
598
|
+
try {
|
|
599
|
+
spec = JSON.parse(raw);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
for (const task of spec.tasks ?? []) {
|
|
605
|
+
const boundary = task.shell?.frontendDesignPolicy?.capabilityBoundary;
|
|
606
|
+
if (boundary &&
|
|
607
|
+
((boundary.allowed?.length ?? 0) > 0 ||
|
|
608
|
+
(boundary.forbidden?.length ?? 0) > 0)) {
|
|
609
|
+
return boundary;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
/** Runtime composes the frozen capability boundary into dependencyPolicy —
|
|
615
|
+
* the planner never maintains it (smoke r42: the boundary lived in PRD prose,
|
|
616
|
+
* the plan omitted it twice, and recovery could not close a prose gap). */
|
|
617
|
+
export function composeDependencyPolicyWithBoundary(policy, boundary) {
|
|
618
|
+
if (!boundary)
|
|
619
|
+
return policy;
|
|
620
|
+
const allowed = (boundary.allowed ?? []).filter((item) => item.trim());
|
|
621
|
+
const forbidden = (boundary.forbidden ?? []).filter((item) => item.trim());
|
|
622
|
+
if (allowed.length === 0 && forbidden.length === 0)
|
|
623
|
+
return policy;
|
|
624
|
+
if (policy?.includes(CAPABILITY_BOUNDARY_MARKER))
|
|
625
|
+
return policy;
|
|
626
|
+
const sentence = `${CAPABILITY_BOUNDARY_MARKER}:` +
|
|
627
|
+
(allowed.length > 0 ? `仅可使用 ${allowed.join("、")}` : "无允许清单") +
|
|
628
|
+
(forbidden.length > 0 ? `;禁止 ${forbidden.join("、")}` : "") +
|
|
629
|
+
"。";
|
|
630
|
+
return policy?.trim() ? `${policy.trim()};${sentence}` : sentence;
|
|
631
|
+
}
|
|
632
|
+
/** Read the generation-frozen requirement→file ownership from run.json
|
|
633
|
+
* (frontendDesignPolicy spec). Absent (legacy fixtures) → no ownership gate. */
|
|
634
|
+
export async function deriveRequirementOwnershipFromRun(runDir) {
|
|
635
|
+
const specPath = path.join(runDir, "run.json");
|
|
636
|
+
let raw;
|
|
637
|
+
try {
|
|
638
|
+
raw = await readFile(specPath, "utf8");
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
let spec;
|
|
644
|
+
try {
|
|
645
|
+
spec = JSON.parse(raw);
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
const merged = new Map();
|
|
651
|
+
for (const task of spec.tasks ?? []) {
|
|
652
|
+
for (const entry of task.shell?.frontendDesignPolicy?.requirementOwnership ?? []) {
|
|
653
|
+
const key = [...(entry.requirementIds ?? [])].sort().join("|");
|
|
654
|
+
if (!merged.has(key))
|
|
655
|
+
merged.set(key, entry);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return [...merged.values()];
|
|
659
|
+
}
|
|
660
|
+
/** Deterministic requirement→file ownership check. A requirement's
|
|
661
|
+
* implementation targets must live inside its declared implementationFiles,
|
|
662
|
+
* and every verification target bound to it must file under its declared
|
|
663
|
+
* verificationFiles. Unknown requirement ids are findings too: a typo in the
|
|
664
|
+
* declaration must fail the declaration, not deadlock the planner. */
|
|
665
|
+
function checkRequirementOwnership(canonical, ownership) {
|
|
666
|
+
const findings = [];
|
|
667
|
+
if (ownership.length === 0)
|
|
668
|
+
return findings;
|
|
669
|
+
const requirementById = new Map((canonical.requirements ?? []).map((requirement) => [requirement.id, requirement]));
|
|
670
|
+
const targetsByRequirement = new Map();
|
|
671
|
+
for (const target of canonical.verificationTargets ?? []) {
|
|
672
|
+
for (const id of target.requirementIds ?? []) {
|
|
673
|
+
if (!targetsByRequirement.has(id))
|
|
674
|
+
targetsByRequirement.set(id, new Set());
|
|
675
|
+
targetsByRequirement.get(id).add(target.file);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const declaredRequirementIds = new Set();
|
|
679
|
+
for (const entry of ownership) {
|
|
680
|
+
for (const id of entry.requirementIds)
|
|
681
|
+
declaredRequirementIds.add(id);
|
|
682
|
+
}
|
|
683
|
+
for (const entry of ownership) {
|
|
684
|
+
for (const id of entry.requirementIds) {
|
|
685
|
+
if (!requirementById.has(id)) {
|
|
686
|
+
findings.push({
|
|
687
|
+
code: "requirement-ownership-unknown-requirement",
|
|
688
|
+
message: `requirementOwnership declares unknown requirement id "${id}"; frozen requirement ids: [${[...requirementById.keys()].join(", ")}]`,
|
|
689
|
+
path: "requirements",
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
for (const entry of ownership) {
|
|
695
|
+
const implementationFiles = entry.implementationFiles ?? [];
|
|
696
|
+
const verificationFiles = entry.verificationFiles ?? [];
|
|
697
|
+
for (const id of entry.requirementIds) {
|
|
698
|
+
const requirement = requirementById.get(id);
|
|
699
|
+
if (!requirement)
|
|
700
|
+
continue;
|
|
701
|
+
if (implementationFiles.length > 0) {
|
|
702
|
+
const allTargets = requirement.implementationTargets ?? [];
|
|
703
|
+
// Test-shaped placements belong to the verification lane (a shared
|
|
704
|
+
// behavior group's placement carries the test file for the
|
|
705
|
+
// test-writing requirement); they are governed by allowedPaths and
|
|
706
|
+
// the entry's verificationFiles, not by implementationFiles.
|
|
707
|
+
const implementationFilesAreTestShaped = implementationFiles.some((file) => isFrontendTestFilePath(file));
|
|
708
|
+
const targets = implementationFilesAreTestShaped
|
|
709
|
+
? allTargets
|
|
710
|
+
: allTargets.filter((file) => !isFrontendTestFilePath(file));
|
|
711
|
+
// Contract deliverables are existence obligations, not ownership
|
|
712
|
+
// claims: a contract may require a mounting file to exist for a
|
|
713
|
+
// requirement implemented elsewhere (smoke r55 — a deliverable
|
|
714
|
+
// otherwise made plan finalize permanently unfixable).
|
|
715
|
+
const deliverables = new Set(requirement.deliverableTargets ?? []);
|
|
716
|
+
const ownedTargets = targets.filter((file) => !deliverables.has(file));
|
|
717
|
+
// Only files OUTSIDE the declared set violate. An empty owned set
|
|
718
|
+
// means every mapped file is a deliverable/test-shaped obligation —
|
|
719
|
+
// unplaced requirements are rejected upstream, not here (r56:
|
|
720
|
+
// "maps [X] expects [X]" still violated when deliverables covered
|
|
721
|
+
// the whole mapping).
|
|
722
|
+
const outside = ownedTargets.filter((file) => !implementationFiles.includes(file));
|
|
723
|
+
if (outside.length > 0) {
|
|
724
|
+
findings.push({
|
|
725
|
+
code: "requirement-ownership-violated",
|
|
726
|
+
message: `requirement ${id} expects implementation within [${implementationFiles.join(", ")}] but plan maps [${targets.join(", ") || "none"}] — re-map the owning behavior group's module placement`,
|
|
727
|
+
path: "requirements",
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (verificationFiles.length > 0) {
|
|
732
|
+
const files = [...(targetsByRequirement.get(id) ?? [])];
|
|
733
|
+
const outside = files.filter((file) => !verificationFiles.includes(file));
|
|
734
|
+
if (outside.length > 0) {
|
|
735
|
+
findings.push({
|
|
736
|
+
code: "requirement-ownership-violated",
|
|
737
|
+
message: `requirement ${id} expects verification evidence within [${verificationFiles.join(", ")}] but verification targets bind [${files.join(", ") || "none"}]`,
|
|
738
|
+
path: "verificationTargets",
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return findings;
|
|
745
|
+
}
|
|
446
746
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
447
747
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
448
748
|
export const FRONTEND_CONTRACT_REVIEW_INDEX_SCHEMA_ID = "frontend-contract-review-index-v1";
|
|
@@ -463,7 +763,7 @@ const FRONTEND_CONTRACT_REVIEW_GROUPS = [
|
|
|
463
763
|
},
|
|
464
764
|
{
|
|
465
765
|
id: "verification",
|
|
466
|
-
fields: ["verificationTargets", "evidenceGaps", "
|
|
766
|
+
fields: ["verificationTargets", "evidenceGaps", "residualRisks"],
|
|
467
767
|
},
|
|
468
768
|
];
|
|
469
769
|
const FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO = 0.6;
|
|
@@ -714,6 +1014,11 @@ export const frontendImplementationContractSchema = z
|
|
|
714
1014
|
expectedOutcome: z.string().min(1),
|
|
715
1015
|
execution: frontendExecutionSchema.optional(),
|
|
716
1016
|
implementationTargets: z.array(safePath),
|
|
1017
|
+
/** Contract-bound "files that must exist" (from the plan
|
|
1018
|
+
* relationship patch). Ownership-exempt: a deliverable is an
|
|
1019
|
+
* existence obligation, not an implementation-ownership claim
|
|
1020
|
+
* (smoke r55 regression). */
|
|
1021
|
+
deliverableTargets: z.array(safePath).optional(),
|
|
717
1022
|
verificationTargetIds: z.array(z.string().min(1)),
|
|
718
1023
|
evidenceGap: gap.optional(),
|
|
719
1024
|
// Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
|
|
@@ -799,7 +1104,6 @@ export const frontendImplementationContractSchema = z
|
|
|
799
1104
|
})
|
|
800
1105
|
.strict()).min(1),
|
|
801
1106
|
evidenceGaps: z.array(gap).optional(),
|
|
802
|
-
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
803
1107
|
stylingStrategy: z.string().min(1).optional(),
|
|
804
1108
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
805
1109
|
dependencyPolicy: z.string().min(1).optional(),
|
|
@@ -1681,6 +1985,7 @@ export function compactFrontendImplementationContract(value) {
|
|
|
1681
1985
|
}
|
|
1682
1986
|
compact.requirements = compactRecordArrayField(record.requirements, [
|
|
1683
1987
|
"implementationTargets",
|
|
1988
|
+
"deliverableTargets",
|
|
1684
1989
|
"verificationTargetIds",
|
|
1685
1990
|
"sourceFragmentIds",
|
|
1686
1991
|
"sourceRefs",
|
|
@@ -1716,7 +2021,7 @@ export function compactFrontendImplementationContract(value) {
|
|
|
1716
2021
|
conflicts: dedupeJsonArray(designEvidence.conflicts),
|
|
1717
2022
|
};
|
|
1718
2023
|
}
|
|
1719
|
-
for (const field of ["evidenceGaps", "
|
|
2024
|
+
for (const field of ["evidenceGaps", "residualRisks"]) {
|
|
1720
2025
|
if (Object.hasOwn(compact, field))
|
|
1721
2026
|
compact[field] = dedupeJsonArray(compact[field]);
|
|
1722
2027
|
}
|
|
@@ -2071,6 +2376,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
2071
2376
|
? { sourceFragmentIds: req.sourceFragmentIds }
|
|
2072
2377
|
: {}),
|
|
2073
2378
|
...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
|
|
2379
|
+
...(Array.isArray(req.deliverableTargets)
|
|
2380
|
+
? { deliverableTargets: req.deliverableTargets }
|
|
2381
|
+
: {}),
|
|
2074
2382
|
...(req.execution !== undefined ? { execution: req.execution } : {}),
|
|
2075
2383
|
};
|
|
2076
2384
|
// Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
|
|
@@ -2250,7 +2558,6 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
2250
2558
|
},
|
|
2251
2559
|
verificationTargets,
|
|
2252
2560
|
evidenceGaps,
|
|
2253
|
-
implementationSteps: asStringArray(record.implementationSteps),
|
|
2254
2561
|
stylingStrategy: asString(record.stylingStrategy) || undefined,
|
|
2255
2562
|
dependencyPolicy: asString(record.dependencyPolicy) || undefined,
|
|
2256
2563
|
residualRisks: asStringArray(record.residualRisks),
|
|
@@ -2486,6 +2793,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2486
2793
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
2487
2794
|
!(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
|
|
2488
2795
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
2796
|
+
result.data.dependencyPolicy = composeDependencyPolicyWithBoundary(result.data.dependencyPolicy, await deriveFrontendCapabilityBoundaryFromRun(input.runDir));
|
|
2489
2797
|
return {
|
|
2490
2798
|
canonical: result.data,
|
|
2491
2799
|
candidateRawSha256,
|
|
@@ -2506,11 +2814,12 @@ function splitFrontendContractReviewGroup(canonical, fields) {
|
|
|
2506
2814
|
slices = [];
|
|
2507
2815
|
};
|
|
2508
2816
|
for (const field of fields) {
|
|
2509
|
-
if (!Object.hasOwn(canonical, field))
|
|
2510
|
-
continue;
|
|
2511
2817
|
const fieldValue = canonical[field];
|
|
2512
|
-
//
|
|
2513
|
-
//
|
|
2818
|
+
// Normalization keeps optional fields as present keys with an undefined
|
|
2819
|
+
// value; JSON serialization drops them, so a slice emitted here would
|
|
2820
|
+
// bind the section to a field the on-disk canonical never contains and
|
|
2821
|
+
// every reader (which re-parses the canonical from JSON) would fail to
|
|
2822
|
+
// resolve it.
|
|
2514
2823
|
if (fieldValue === undefined)
|
|
2515
2824
|
continue;
|
|
2516
2825
|
if (!Array.isArray(fieldValue)) {
|
|
@@ -2881,6 +3190,27 @@ function assertVerificationSymbolShapes(contract) {
|
|
|
2881
3190
|
* (finalize_plan receipt) can synthesize fix suggestions instead of making
|
|
2882
3191
|
* the model re-derive them from prose.
|
|
2883
3192
|
*/
|
|
3193
|
+
/** Normalized-content sha256 of a parent run's canonical contract, used by the
|
|
3194
|
+
* recovery identical-plan guard. Both sides hash serializeDeterministicJson of
|
|
3195
|
+
* the parsed canonical, so on-disk formatting never changes the verdict.
|
|
3196
|
+
* Unreadable/missing parent contract → undefined (guard skipped). */
|
|
3197
|
+
export async function readParentCanonical(parentRunDir) {
|
|
3198
|
+
try {
|
|
3199
|
+
const raw = await readFile(path.join(parentRunDir, "contracts", "frontend-implementation-contract.json"), "utf8");
|
|
3200
|
+
return JSON.parse(raw);
|
|
3201
|
+
}
|
|
3202
|
+
catch {
|
|
3203
|
+
return undefined;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
export async function parentCanonicalContentSha256(parentRunDir) {
|
|
3207
|
+
const canonical = await readParentCanonical(parentRunDir);
|
|
3208
|
+
if (!canonical)
|
|
3209
|
+
return undefined;
|
|
3210
|
+
return createHash("sha256")
|
|
3211
|
+
.update(serializeDeterministicJson(canonical))
|
|
3212
|
+
.digest("hex");
|
|
3213
|
+
}
|
|
2884
3214
|
export class PlanPolicyPrecheckFailure extends Error {
|
|
2885
3215
|
findings;
|
|
2886
3216
|
constructor(message, findings) {
|
|
@@ -2924,7 +3254,34 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
|
2924
3254
|
...checkUxNameCollisions(analysis.canonical),
|
|
2925
3255
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2926
3256
|
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
3257
|
+
...checkComponentChoiceSourceReferences(analysis.canonical, await deriveFrontendSourceBindingPathsFromRun(input.runDir)),
|
|
3258
|
+
...checkRequirementOwnership(analysis.canonical, await deriveRequirementOwnershipFromRun(input.runDir)),
|
|
3259
|
+
...checkInteractionIdVocabulary(analysis.canonical, await deriveFrontendInteractionIdsFromRun(input.runDir)),
|
|
3260
|
+
...checkMockApiStrategyAllowed(analysis.canonical, await deriveFrontendAllowedMockStrategiesFromRun(input.runDir)),
|
|
2927
3261
|
];
|
|
3262
|
+
// Front-load the static verification coverage gap (smoke r16-r20): when
|
|
3263
|
+
// the frozen command directory offers static entrypoints and no
|
|
3264
|
+
// verification target binds one, type-level/build constraints cannot be
|
|
3265
|
+
// proven. Surface the offered commands here so the plan retry ladder
|
|
3266
|
+
// records a static focus instead of the review flagging it post-hoc.
|
|
3267
|
+
// Every frozen static entry is a required check (smoke r27-r31): binding
|
|
3268
|
+
// only `verify-build` used to satisfy an "at least one" rule, after which
|
|
3269
|
+
// design review rejected the plan for the uncovered `verify-typecheck`
|
|
3270
|
+
// entry — a demand that reached the plan as prose and could never be
|
|
3271
|
+
// machine-checked. Coverage is enumerable, so it is enforced here.
|
|
3272
|
+
const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
|
|
3273
|
+
const staticCommands = verifyDirectory.filter((entry) => entry.mode === "static");
|
|
3274
|
+
if (staticCommands.length > 0) {
|
|
3275
|
+
const boundCommandIds = new Set(analysis.canonical.verificationTargets.map((target) => target.commandId));
|
|
3276
|
+
const uncovered = staticCommands.filter((entry) => !boundCommandIds.has(entry.commandId));
|
|
3277
|
+
if (uncovered.length > 0) {
|
|
3278
|
+
policyPreFindings.push({
|
|
3279
|
+
code: "static-verification-coverage-missing",
|
|
3280
|
+
message: `verification targets miss frozen static command(s): [${uncovered.map((entry) => `${entry.commandId} (${entry.label})`).join(", ")}]; the frozen directory offers: [${staticCommands.map((entry) => `${entry.commandId} (${entry.label})`).join(", ")}] — record record_verification_focus for every static commandId`,
|
|
3281
|
+
path: "verificationTargets",
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
2928
3285
|
if (policyPreFindings.length > 0) {
|
|
2929
3286
|
const message = `frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
|
|
2930
3287
|
.map((finding) => `${finding.code}: ${finding.message}`)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFrontendPlanCanaryRunMetrics, } from "./frontend-session-budget.js";
|
|
2
|
+
export const FRONTEND_PLAN_CANARY_SCENARIOS = [
|
|
3
|
+
"missing-idle-loading-empty",
|
|
4
|
+
"loading-not-mounted",
|
|
5
|
+
"test-layer-does-not-prove-wiring",
|
|
6
|
+
"mock-claimed-as-real-integration",
|
|
7
|
+
];
|
|
8
|
+
const reduction = (before, after) => before !== null && after !== null && before > 0 ? ((before - after) / before) * 100 : null;
|
|
9
|
+
/** Compare two same-input runs. Missing observations fail the canary rather
|
|
10
|
+
* than being interpreted as a zero-cost candidate. */
|
|
11
|
+
export function evaluateFrontendPlanCanary(input) {
|
|
12
|
+
const required = { planSessionsPct: 30, planWallClockPct: 30, protocolRetryPct: 50, ...input.requiredReductions };
|
|
13
|
+
const reductions = {
|
|
14
|
+
planSessionsPct: reduction(input.baseline.planSessions, input.candidate.planSessions),
|
|
15
|
+
planWallClockPct: reduction(input.baseline.planWallClockMs, input.candidate.planWallClockMs),
|
|
16
|
+
protocolRetryPct: reduction(input.baseline.retries, input.candidate.retries),
|
|
17
|
+
};
|
|
18
|
+
const failedChecks = [];
|
|
19
|
+
if (reductions.planSessionsPct === null || reductions.planSessionsPct < required.planSessionsPct)
|
|
20
|
+
failedChecks.push("plan-session-reduction");
|
|
21
|
+
if (reductions.planWallClockPct === null || reductions.planWallClockPct < required.planWallClockPct)
|
|
22
|
+
failedChecks.push("plan-wall-clock-reduction");
|
|
23
|
+
// A zero retry baseline is already optimal: a zero->zero candidate is not a
|
|
24
|
+
// regression and must not block, while zero->positive or missing evidence
|
|
25
|
+
// still fails closed.
|
|
26
|
+
const retriesAlreadyOptimal = input.baseline.retries === 0 && input.candidate.retries === 0;
|
|
27
|
+
if (!retriesAlreadyOptimal &&
|
|
28
|
+
(reductions.protocolRetryPct === null || reductions.protocolRetryPct < required.protocolRetryPct)) {
|
|
29
|
+
failedChecks.push("protocol-retry-reduction");
|
|
30
|
+
}
|
|
31
|
+
for (const scenario of FRONTEND_PLAN_CANARY_SCENARIOS) {
|
|
32
|
+
const result = input.scenarios[scenario];
|
|
33
|
+
if (result !== "blocked" && result !== "reported")
|
|
34
|
+
failedChecks.push(`regression:${scenario}`);
|
|
35
|
+
}
|
|
36
|
+
return { pass: failedChecks.length === 0, reductions, failedChecks };
|
|
37
|
+
}
|
|
38
|
+
/** Read two frozen run directories and evaluate the same-input canary. Plan
|
|
39
|
+
* cost metrics come from persisted session receipts; supplemental DAG/review/
|
|
40
|
+
* recovery values come from each run's `frontend-plan-canary-metrics.json`.
|
|
41
|
+
* Missing observations surface as `null` and fail the evaluation, never as a
|
|
42
|
+
* manufactured zero-cost improvement. */
|
|
43
|
+
export async function compareFrontendPlanCanaryRuns(input) {
|
|
44
|
+
const baseline = await readFrontendPlanCanaryRunMetrics(input.baselineRunDir);
|
|
45
|
+
const candidate = await readFrontendPlanCanaryRunMetrics(input.candidateRunDir);
|
|
46
|
+
const evaluation = evaluateFrontendPlanCanary({
|
|
47
|
+
baseline,
|
|
48
|
+
candidate,
|
|
49
|
+
scenarios: input.scenarios,
|
|
50
|
+
requiredReductions: input.requiredReductions,
|
|
51
|
+
});
|
|
52
|
+
return { ...evaluation, baseline, candidate };
|
|
53
|
+
}
|