@tea-agent/loop-agent 0.41.0 → 0.41.1-beta.0
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 +267 -33
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +945 -217
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +34 -7
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/pi-runtime.js +2 -3
- package/dist/worker/console/chat/routes.js +3 -12
- package/dist/worker/console/chat/semantic-activity.js +11 -12
- package/dist/worker/console/chat/session-stats.js +0 -96
- package/dist/worker/console/chat/turn-process.js +4 -28
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/pi-plugins.js +1 -3
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-Bho4we4c.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-TBDkTeK0.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-BF_fZrn2.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-CCnPgUe2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-BCYZJLAT.js} +1 -1
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-D8K6adhU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-DkT1Q78b.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-DuhvpIr7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-BnK6dC49.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-DPz__z7r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-0kkppM2j.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-BB41iK5t.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-CiVrxKHU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-C2h9Xje1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-3iOSHdbM.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-BN3A7tkk.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-CTuBy0Or.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-wb6tUlMN.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-GP1Ivx8B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-ejrcK6L0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-YFkCCvUU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-B4rrvIaL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-C-YIZcUf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-BPF5wcL1.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-CU5SzXjO.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-CpL414DE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-MrhLzNau.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-Bg4qFuUo.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-ColxSXLR.js} +1 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +1 -0
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +437 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-Czvtm6gc.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-DWYZcvph.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-KTLZRUjx.js} +1 -1
- package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-_Exn7bfl.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-Cxg8kX8V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-oKrlo2Bt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-XAeteiNd.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BTf1K-lp.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BVbKtZhW.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-C5e4JFcr.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-Cz30hc29.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-DS33n6d0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-46w7v-9T.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-pbcD5SsZ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-DuivYBSj.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-Cb9VZVIl.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-D2-mfOgR.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-DeS2aC76.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-WGsrFjxH.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +0 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +3 -28
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +0 -106
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observability/read-model.js +0 -4
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/worker/observe/static/views/dag-inspector.js +7 -2
- package/dist/worker/observe/static/views/session-timeline.js +15 -46
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +932 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-read-budgets.js +12 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +685 -494
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +732 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +10 -2
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +214 -104
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
- package/dist/worker/console/static/assets/index-Bih9w9h9.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +0 -22
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -8,8 +8,16 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
|
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
|
+
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
12
|
+
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
|
+
// only at call time, so the contract module and the policy module can depend
|
|
14
|
+
// on each other without module-init ordering issues.
|
|
15
|
+
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
11
16
|
import { isDagSourceBindingV2 } from "./types.js";
|
|
12
|
-
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer,
|
|
17
|
+
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
18
|
+
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
19
|
+
import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
|
|
20
|
+
import { FRONTEND_READ_BUDGETS } from "./frontend-read-budgets.js";
|
|
13
21
|
export const frontendNormalizationActionSchema = z.enum([
|
|
14
22
|
"remove-trailing-commas",
|
|
15
23
|
"strip-comments",
|
|
@@ -17,6 +25,7 @@ export const frontendNormalizationActionSchema = z.enum([
|
|
|
17
25
|
"canonicalize-verification-alias",
|
|
18
26
|
"drop-unresolved-verification-symbols",
|
|
19
27
|
"inject-source-binding",
|
|
28
|
+
"compact-canonical-contract",
|
|
20
29
|
]);
|
|
21
30
|
export function sha256Hex(input) {
|
|
22
31
|
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
@@ -40,6 +49,15 @@ function sortKeysDeep(value) {
|
|
|
40
49
|
export function serializeDeterministicJson(value) {
|
|
41
50
|
return JSON.stringify(sortKeysDeep(value), null, 2);
|
|
42
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Deterministic compact JSON for the canonical contract artifact. The pretty
|
|
54
|
+
* serializer remains the default for human-facing audit artifacts, while the
|
|
55
|
+
* writer contract uses this representation to reduce artifact size and
|
|
56
|
+
* downstream read pressure without changing contract semantics.
|
|
57
|
+
*/
|
|
58
|
+
export function serializeCompactDeterministicJson(value) {
|
|
59
|
+
return JSON.stringify(sortKeysDeep(value));
|
|
60
|
+
}
|
|
43
61
|
export function deterministicSha256(value) {
|
|
44
62
|
return sha256Hex(serializeDeterministicJson(value));
|
|
45
63
|
}
|
|
@@ -70,6 +88,13 @@ export async function writeDeterministicJsonArtifact(runDir, relativePath, value
|
|
|
70
88
|
await writeTextArtifactFile(targetPath, json);
|
|
71
89
|
return { path: targetPath, sha256: sha256Hex(json) };
|
|
72
90
|
}
|
|
91
|
+
/** Write a canonical contract artifact with compact deterministic JSON. */
|
|
92
|
+
export async function writeCompactDeterministicJsonArtifact(runDir, relativePath, value) {
|
|
93
|
+
const json = `${serializeCompactDeterministicJson(value)}\n`;
|
|
94
|
+
const targetPath = path.join(runDir, relativePath);
|
|
95
|
+
await writeTextArtifactFile(targetPath, json);
|
|
96
|
+
return { path: targetPath, sha256: sha256Hex(json) };
|
|
97
|
+
}
|
|
73
98
|
/**
|
|
74
99
|
* Extract frozen command labels from the DAG run spec (run.json).
|
|
75
100
|
* The run spec is written before any node executes, so it is always available
|
|
@@ -104,8 +129,93 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
104
129
|
}
|
|
105
130
|
return [...labels];
|
|
106
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Extract the design-policy allowedDependencies set from the DAG run spec
|
|
134
|
+
* (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
|
|
135
|
+
* node's front-loaded policy pre-check so dependency declarations can be
|
|
136
|
+
* corrected through the retry ladder before the policy shell (the authority)
|
|
137
|
+
* re-runs the same check on committed facts.
|
|
138
|
+
*/
|
|
139
|
+
async function deriveAllowedDependenciesFromRun(runDir) {
|
|
140
|
+
const specPath = path.join(runDir, "run.json");
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = await readFile(specPath, "utf8");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
let spec;
|
|
149
|
+
try {
|
|
150
|
+
spec = JSON.parse(raw);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const deps = new Set();
|
|
156
|
+
for (const task of spec.tasks ?? []) {
|
|
157
|
+
const allowed = task.shell?.frontendDesignPolicy?.allowedDependencies;
|
|
158
|
+
if (allowed) {
|
|
159
|
+
for (const dep of allowed)
|
|
160
|
+
deps.add(dep);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return [...deps];
|
|
164
|
+
}
|
|
165
|
+
/** Read the frozen implementation writeSet patterns from run.json so the plan
|
|
166
|
+
* validator can front-load checkTargetPaths (write-set-too-large) before the
|
|
167
|
+
* design-policy shell terminates the run. */
|
|
168
|
+
async function deriveWriteSetFromRun(runDir) {
|
|
169
|
+
const specPath = path.join(runDir, "run.json");
|
|
170
|
+
let raw;
|
|
171
|
+
try {
|
|
172
|
+
raw = await readFile(specPath, "utf8");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
let spec;
|
|
178
|
+
try {
|
|
179
|
+
spec = JSON.parse(raw);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const writeSet = new Set();
|
|
185
|
+
for (const task of spec.tasks ?? []) {
|
|
186
|
+
const patterns = task.shell?.frontendDesignPolicy?.implementationWriteSet;
|
|
187
|
+
if (patterns) {
|
|
188
|
+
for (const pattern of patterns)
|
|
189
|
+
writeSet.add(pattern);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return [...writeSet];
|
|
193
|
+
}
|
|
107
194
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
108
195
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
196
|
+
export const FRONTEND_CONTRACT_REVIEW_INDEX_SCHEMA_ID = "frontend-contract-review-index-v1";
|
|
197
|
+
export const FRONTEND_CONTRACT_SECTION_SCHEMA_ID = "frontend-contract-section-v1";
|
|
198
|
+
export const FRONTEND_CONTRACT_CAPACITY_SCHEMA_ID = "frontend-contract-capacity-v1";
|
|
199
|
+
const FRONTEND_CONTRACT_REVIEW_GROUPS = [
|
|
200
|
+
{
|
|
201
|
+
id: "core",
|
|
202
|
+
fields: ["schemaVersion", "sourceBinding", "riskLevel", "targets", "requirements"],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "ux",
|
|
206
|
+
fields: ["uiStates", "interactions", "uiComponentChoices", "stylingStrategy"],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "data-design",
|
|
210
|
+
fields: ["mockApi", "designEvidence", "dependencyPolicy", "realIntegrationGap"],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "verification",
|
|
214
|
+
fields: ["verificationTargets", "evidenceGaps", "implementationSteps", "residualRisks"],
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
const FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO = 0.6;
|
|
218
|
+
const FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES = 40 * 1024;
|
|
109
219
|
/**
|
|
110
220
|
* Build the deterministic, run-owned portion of the frontend contract. The
|
|
111
221
|
* planner is allowed to fill semantic fields with an RFC 7386 merge patch but
|
|
@@ -171,7 +281,6 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
|
|
|
171
281
|
"mockApi",
|
|
172
282
|
"designEvidence",
|
|
173
283
|
"verificationTargets",
|
|
174
|
-
"evidenceGaps",
|
|
175
284
|
];
|
|
176
285
|
const actualRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
177
286
|
const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
|
|
@@ -220,13 +329,13 @@ const gap = z
|
|
|
220
329
|
* `specReference` is required (path/section/line hit).
|
|
221
330
|
* - `reuse-existing`: reuse an existing repo component/convention not named by
|
|
222
331
|
* the spec → `specReference` may be null, rationale explains the basis.
|
|
223
|
-
* - `new`:
|
|
224
|
-
*
|
|
332
|
+
* - `new`: the task source mandates a component absent from the repository →
|
|
333
|
+
* `specReference` is required and points at that task-source declaration;
|
|
334
|
+
* rationale explains the bounded addition.
|
|
225
335
|
*
|
|
226
|
-
* `specReference` is nullable + optional
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* identically.
|
|
336
|
+
* `specReference` is nullable + optional at the wire boundary because
|
|
337
|
+
* `stripNullValuesDeep` removes model-emitted nulls before validation. The
|
|
338
|
+
* decision-specific requirements are enforced by the superRefine below.
|
|
230
339
|
*/
|
|
231
340
|
const uiComponentSpecReferenceSchema = z
|
|
232
341
|
.object({
|
|
@@ -241,7 +350,11 @@ const uiComponentChoiceSchema = z
|
|
|
241
350
|
component: z.string().min(1),
|
|
242
351
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
243
352
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
244
|
-
|
|
353
|
+
// Optional: the decision enum already carries the semantic (specified →
|
|
354
|
+
// specReference, new → task-source traceability, reuse-existing → no
|
|
355
|
+
// rationale needed).
|
|
356
|
+
// Omit for reuse-existing to keep plan output small.
|
|
357
|
+
rationale: z.string().min(1).optional(),
|
|
245
358
|
})
|
|
246
359
|
.strict();
|
|
247
360
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
@@ -270,7 +383,12 @@ function stripNullValuesDeep(value) {
|
|
|
270
383
|
return value;
|
|
271
384
|
}
|
|
272
385
|
export const frontendImplementationContractSchema = z
|
|
273
|
-
.preprocess(
|
|
386
|
+
.preprocess((value) => {
|
|
387
|
+
const record = asRecord(value);
|
|
388
|
+
if (!record)
|
|
389
|
+
return stripNullValuesDeep(value);
|
|
390
|
+
return stripNullValuesDeep(record);
|
|
391
|
+
}, z
|
|
274
392
|
.object({
|
|
275
393
|
schemaVersion: z.literal(1),
|
|
276
394
|
sourceBinding: z
|
|
@@ -398,7 +516,7 @@ export const frontendImplementationContractSchema = z
|
|
|
398
516
|
uiStates: z.array(z.string().min(1)),
|
|
399
517
|
})
|
|
400
518
|
.strict()).min(1),
|
|
401
|
-
evidenceGaps: z.array(gap),
|
|
519
|
+
evidenceGaps: z.array(gap).optional(),
|
|
402
520
|
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
403
521
|
stylingStrategy: z.string().min(1).optional(),
|
|
404
522
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
@@ -408,6 +526,37 @@ export const frontendImplementationContractSchema = z
|
|
|
408
526
|
})
|
|
409
527
|
.strict()
|
|
410
528
|
.superRefine((value, ctx) => {
|
|
529
|
+
const stableIdentities = [
|
|
530
|
+
{
|
|
531
|
+
path: "requirements",
|
|
532
|
+
label: "requirement id",
|
|
533
|
+
values: value.requirements.map((item) => item.id),
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
path: "uiStates",
|
|
537
|
+
label: "UI state name",
|
|
538
|
+
values: value.uiStates.map((item) => item.name),
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
path: "interactions",
|
|
542
|
+
label: "interaction name",
|
|
543
|
+
values: value.interactions.map((item) => item.name),
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
path: "uiComponentChoices",
|
|
547
|
+
label: "component choice purpose",
|
|
548
|
+
values: (value.uiComponentChoices ?? []).map((item) => item.purpose),
|
|
549
|
+
},
|
|
550
|
+
];
|
|
551
|
+
for (const identity of stableIdentities) {
|
|
552
|
+
if (new Set(identity.values).size !== identity.values.length) {
|
|
553
|
+
ctx.addIssue({
|
|
554
|
+
code: "custom",
|
|
555
|
+
message: `duplicate ${identity.label}`,
|
|
556
|
+
path: [identity.path],
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
411
560
|
const verificationIds = value.verificationTargets.map((target) => target.id);
|
|
412
561
|
if (new Set(verificationIds).size !== verificationIds.length)
|
|
413
562
|
ctx.addIssue({
|
|
@@ -462,18 +611,32 @@ export const frontendImplementationContractSchema = z
|
|
|
462
611
|
message: `unknown verification target ${targetId}`,
|
|
463
612
|
path: ["requirements"],
|
|
464
613
|
});
|
|
614
|
+
// Interactions bind to VTs too: an interaction whose behavior
|
|
615
|
+
// verification points at an unmaterialized VT is untraceable —
|
|
616
|
+
// r20 review finding (dangling *-BEHAVIOR references sailed
|
|
617
|
+
// through plan/design/implement to the final review).
|
|
618
|
+
for (const interaction of value.interactions)
|
|
619
|
+
for (const targetId of interaction.verificationTargetIds)
|
|
620
|
+
if (!verificationIds.includes(targetId))
|
|
621
|
+
ctx.addIssue({
|
|
622
|
+
code: "custom",
|
|
623
|
+
message: `interaction "${interaction.name}" references unknown verification target ${targetId}`,
|
|
624
|
+
path: ["interactions"],
|
|
625
|
+
});
|
|
465
626
|
// Source fidelity ledger binding (AC-005/AC-006): 绑定携带
|
|
466
627
|
// requirementToFragments(ledger v2)时,每个 requirement 必须携带
|
|
467
|
-
//
|
|
628
|
+
// 非空 sourceFragmentIds,否则 fail closed(防引用伪造/缺失)。
|
|
629
|
+
// sourceRefs(fragment→path 的派生展示)不设硬门槛:模型从 binding
|
|
630
|
+
// 只能拿到 fragment id 拿不到权威 path,强制只会诱导编造;防伪造的
|
|
631
|
+
// 真实性校验由 fidelity gate 按 sourceFragmentIds 执行(存在性 +
|
|
632
|
+
// 与 ledger boundFragments 一致)。
|
|
468
633
|
const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
|
|
469
634
|
if (value.sourceBinding.ledgerPath &&
|
|
470
635
|
(!requirement.sourceFragmentIds ||
|
|
471
|
-
requirement.sourceFragmentIds.length === 0
|
|
472
|
-
!requirement.sourceRefs ||
|
|
473
|
-
requirement.sourceRefs.length === 0)) {
|
|
636
|
+
requirement.sourceFragmentIds.length === 0)) {
|
|
474
637
|
ctx.addIssue({
|
|
475
638
|
code: "custom",
|
|
476
|
-
message: `requirement ${requirement.id} must carry sourceFragmentIds
|
|
639
|
+
message: `requirement ${requirement.id} must carry sourceFragmentIds when a source fidelity ledger is bound`,
|
|
477
640
|
path: ["requirements", requirement.id, "sourceFragmentIds"],
|
|
478
641
|
});
|
|
479
642
|
}
|
|
@@ -491,12 +654,23 @@ export const frontendImplementationContractSchema = z
|
|
|
491
654
|
if (state.applicable &&
|
|
492
655
|
(!state.expectedBehavior ||
|
|
493
656
|
!state.implementationTargets?.length ||
|
|
494
|
-
!state.verificationTargetIds?.length))
|
|
657
|
+
!state.verificationTargetIds?.length)) {
|
|
658
|
+
// Name the state and the exact missing fields: the fixer is a
|
|
659
|
+
// model iterating on finalize receipts — it cannot fix a
|
|
660
|
+
// defect it cannot locate (r18: 39 blind finalize retries).
|
|
661
|
+
const missing = [];
|
|
662
|
+
if (!state.expectedBehavior)
|
|
663
|
+
missing.push("expectedBehavior");
|
|
664
|
+
if (!state.implementationTargets?.length)
|
|
665
|
+
missing.push("implementationTargets");
|
|
666
|
+
if (!state.verificationTargetIds?.length)
|
|
667
|
+
missing.push("verificationTargetIds");
|
|
495
668
|
ctx.addIssue({
|
|
496
669
|
code: "custom",
|
|
497
|
-
message:
|
|
670
|
+
message: `applicable UI state "${state.name}" is missing: ${missing.join(", ")} — record_state_flow it again with those fields filled`,
|
|
498
671
|
path: ["uiStates"],
|
|
499
672
|
});
|
|
673
|
+
}
|
|
500
674
|
if (!state.applicable && !state.notApplicableReason)
|
|
501
675
|
ctx.addIssue({
|
|
502
676
|
code: "custom",
|
|
@@ -515,10 +689,10 @@ export const frontendImplementationContractSchema = z
|
|
|
515
689
|
}
|
|
516
690
|
}
|
|
517
691
|
else if (choice.decision === "new") {
|
|
518
|
-
if (choice.specReference) {
|
|
692
|
+
if (!choice.specReference || !choice.specReference.path) {
|
|
519
693
|
ctx.addIssue({
|
|
520
694
|
code: "custom",
|
|
521
|
-
message: "new component choice
|
|
695
|
+
message: "new component choice requires a task-source specReference.path",
|
|
522
696
|
path: ["uiComponentChoices", index, "specReference"],
|
|
523
697
|
});
|
|
524
698
|
}
|
|
@@ -1045,11 +1219,40 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1045
1219
|
])];
|
|
1046
1220
|
const evidenceGap = asRecord(requirement.evidenceGap);
|
|
1047
1221
|
const hasProof = provenRequirementIds.has(requirementId);
|
|
1222
|
+
// A committed evidenceGap with a blank description means "no gap":
|
|
1223
|
+
// small-output models emit the slot defensively with description
|
|
1224
|
+
// "" and the strict gap schema (description min 1 char) would
|
|
1225
|
+
// fail the whole compile. Strip the incoming slot first and only
|
|
1226
|
+
// re-add it when usable — the requirement either has proof (no
|
|
1227
|
+
// gap needed) or receives the derived blocking gap below.
|
|
1228
|
+
const evidenceGapDescription = asString(evidenceGap?.description).trim();
|
|
1229
|
+
const hasUsableEvidenceGap = evidenceGap !== undefined && evidenceGapDescription !== "";
|
|
1230
|
+
// Source fidelity bindings are deterministic ledger data, not
|
|
1231
|
+
// model-authored content: when the plan requirement carries no
|
|
1232
|
+
// binding (r6 — the contract input block degraded, the model
|
|
1233
|
+
// correctly refused to invent ids), inject the ledger binding
|
|
1234
|
+
// for the requirement id. The strict schema fails a ledger-bound
|
|
1235
|
+
// contract on any requirement without sourceFragmentIds.
|
|
1236
|
+
const declaredSourceFragmentIds = Array.isArray(requirement.sourceFragmentIds)
|
|
1237
|
+
? requirement.sourceFragmentIds
|
|
1238
|
+
: undefined;
|
|
1239
|
+
const ledgerBoundFragmentIds = Array.isArray(canonicalBinding.requirementToFragments?.[requirementId])
|
|
1240
|
+
? canonicalBinding.requirementToFragments[requirementId]
|
|
1241
|
+
: undefined;
|
|
1242
|
+
const sourceFragmentIds = declaredSourceFragmentIds && declaredSourceFragmentIds.length > 0
|
|
1243
|
+
? declaredSourceFragmentIds
|
|
1244
|
+
: ledgerBoundFragmentIds;
|
|
1245
|
+
const { evidenceGap: _incomingGap, sourceFragmentIds: _incomingFragmentIds, ...requirementWithoutGap } = requirement;
|
|
1246
|
+
void _incomingGap;
|
|
1247
|
+
void _incomingFragmentIds;
|
|
1048
1248
|
return {
|
|
1049
|
-
...
|
|
1249
|
+
...requirementWithoutGap,
|
|
1250
|
+
...(sourceFragmentIds ? { sourceFragmentIds } : {}),
|
|
1050
1251
|
...(verificationTargetIds.length > 0 ? { verificationTargetIds } : {}),
|
|
1051
1252
|
...(hasProof
|
|
1052
|
-
? (
|
|
1253
|
+
? (hasUsableEvidenceGap
|
|
1254
|
+
? { evidenceGap: { ...evidenceGap, blocking: false } }
|
|
1255
|
+
: {})
|
|
1053
1256
|
: {
|
|
1054
1257
|
evidenceGap: {
|
|
1055
1258
|
requirementId,
|
|
@@ -1061,10 +1264,16 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1061
1264
|
})
|
|
1062
1265
|
: record.requirements;
|
|
1063
1266
|
const modelEvidenceGaps = Array.isArray(record.evidenceGaps)
|
|
1064
|
-
? record.evidenceGaps
|
|
1267
|
+
? record.evidenceGaps
|
|
1268
|
+
.map((item) => {
|
|
1065
1269
|
const gap = asRecord(item);
|
|
1066
1270
|
if (!gap)
|
|
1067
1271
|
return item;
|
|
1272
|
+
// Blank-description gaps are meaningless statements that would
|
|
1273
|
+
// fail the strict gap schema; drop them like the embedded
|
|
1274
|
+
// per-requirement empty slots.
|
|
1275
|
+
if (asString(gap.description).trim() === "")
|
|
1276
|
+
return undefined;
|
|
1068
1277
|
const requirementId = canonicalizeRequirementId(asString(gap.requirementId));
|
|
1069
1278
|
// Model gaps are advisory. Blocking status is reconstructed below
|
|
1070
1279
|
// from the source binding and executable verification targets.
|
|
@@ -1077,6 +1286,7 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1077
1286
|
blocking: false,
|
|
1078
1287
|
};
|
|
1079
1288
|
})
|
|
1289
|
+
.filter((item) => item !== undefined)
|
|
1080
1290
|
: [];
|
|
1081
1291
|
const derivedBlockingGaps = canonicalBinding.requirementIds
|
|
1082
1292
|
.filter((requirementId) => !provenRequirementIds.has(requirementId))
|
|
@@ -1091,6 +1301,114 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1091
1301
|
evidenceGaps: [...modelEvidenceGaps, ...derivedBlockingGaps],
|
|
1092
1302
|
};
|
|
1093
1303
|
}
|
|
1304
|
+
function dedupeJsonArray(value) {
|
|
1305
|
+
if (!Array.isArray(value))
|
|
1306
|
+
return value;
|
|
1307
|
+
const seen = new Set();
|
|
1308
|
+
return value.filter((item) => {
|
|
1309
|
+
const key = serializeCompactDeterministicJson(item);
|
|
1310
|
+
if (seen.has(key))
|
|
1311
|
+
return false;
|
|
1312
|
+
seen.add(key);
|
|
1313
|
+
return true;
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
function compactRecordArrayField(value, fields) {
|
|
1317
|
+
if (!Array.isArray(value))
|
|
1318
|
+
return value;
|
|
1319
|
+
return dedupeJsonArray(value.map((item) => {
|
|
1320
|
+
const record = asRecord(item);
|
|
1321
|
+
if (!record)
|
|
1322
|
+
return item;
|
|
1323
|
+
const compact = { ...record };
|
|
1324
|
+
for (const field of fields) {
|
|
1325
|
+
if (Object.hasOwn(compact, field)) {
|
|
1326
|
+
compact[field] = dedupeJsonArray(compact[field]);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return compact;
|
|
1330
|
+
}));
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Compact the canonical contract without weakening its schema or coverage
|
|
1334
|
+
* semantics. This is deliberately a closed list of transformations:
|
|
1335
|
+
* byte-identical records and repeated set members are deduplicated, and
|
|
1336
|
+
* mutually exclusive UI-state fields are normalized. Conflicting stable
|
|
1337
|
+
* identities remain visible so strict schema validation can reject them.
|
|
1338
|
+
*/
|
|
1339
|
+
export function compactFrontendImplementationContract(value) {
|
|
1340
|
+
const record = asRecord(value);
|
|
1341
|
+
if (!record)
|
|
1342
|
+
return value;
|
|
1343
|
+
const compact = { ...record };
|
|
1344
|
+
const sourceBinding = asRecord(record.sourceBinding);
|
|
1345
|
+
if (sourceBinding) {
|
|
1346
|
+
const compactBinding = { ...sourceBinding };
|
|
1347
|
+
for (const field of ["referencePaths", "requirementIds"]) {
|
|
1348
|
+
if (Object.hasOwn(compactBinding, field)) {
|
|
1349
|
+
compactBinding[field] = dedupeJsonArray(compactBinding[field]);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const fragments = asRecord(sourceBinding.requirementToFragments);
|
|
1353
|
+
if (fragments) {
|
|
1354
|
+
compactBinding.requirementToFragments = Object.fromEntries(Object.entries(fragments).map(([id, ids]) => [id, dedupeJsonArray(ids)]));
|
|
1355
|
+
}
|
|
1356
|
+
compact.sourceBinding = compactBinding;
|
|
1357
|
+
}
|
|
1358
|
+
const targets = asRecord(record.targets);
|
|
1359
|
+
if (targets) {
|
|
1360
|
+
const compactTargets = { ...targets };
|
|
1361
|
+
for (const field of ["files", "routes", "publicApiChanges"]) {
|
|
1362
|
+
if (Object.hasOwn(compactTargets, field)) {
|
|
1363
|
+
compactTargets[field] = dedupeJsonArray(compactTargets[field]);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
compact.targets = compactTargets;
|
|
1367
|
+
}
|
|
1368
|
+
compact.requirements = compactRecordArrayField(record.requirements, [
|
|
1369
|
+
"implementationTargets",
|
|
1370
|
+
"verificationTargetIds",
|
|
1371
|
+
"sourceFragmentIds",
|
|
1372
|
+
"sourceRefs",
|
|
1373
|
+
]);
|
|
1374
|
+
compact.uiStates = compactRecordArrayField(record.uiStates, ["implementationTargets", "verificationTargetIds"]);
|
|
1375
|
+
if (Array.isArray(compact.uiStates)) {
|
|
1376
|
+
compact.uiStates = dedupeJsonArray(compact.uiStates.map((item) => {
|
|
1377
|
+
const state = asRecord(item);
|
|
1378
|
+
if (!state)
|
|
1379
|
+
return item;
|
|
1380
|
+
const normalized = { ...state };
|
|
1381
|
+
if (normalized.applicable === true)
|
|
1382
|
+
delete normalized.notApplicableReason;
|
|
1383
|
+
if (normalized.applicable === false)
|
|
1384
|
+
delete normalized.expectedBehavior;
|
|
1385
|
+
return normalized;
|
|
1386
|
+
}));
|
|
1387
|
+
}
|
|
1388
|
+
compact.interactions = compactRecordArrayField(record.interactions, ["implementationTargets", "verificationTargetIds"]);
|
|
1389
|
+
compact.verificationTargets = compactRecordArrayField(record.verificationTargets, ["requirementIds", "uiStates"]);
|
|
1390
|
+
const mockApi = asRecord(record.mockApi);
|
|
1391
|
+
if (mockApi) {
|
|
1392
|
+
compact.mockApi = {
|
|
1393
|
+
...mockApi,
|
|
1394
|
+
endpoints: dedupeJsonArray(mockApi.endpoints),
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
const designEvidence = asRecord(record.designEvidence);
|
|
1398
|
+
if (designEvidence) {
|
|
1399
|
+
compact.designEvidence = {
|
|
1400
|
+
...designEvidence,
|
|
1401
|
+
paths: dedupeJsonArray(designEvidence.paths),
|
|
1402
|
+
conflicts: dedupeJsonArray(designEvidence.conflicts),
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
for (const field of ["evidenceGaps", "implementationSteps", "residualRisks"]) {
|
|
1406
|
+
if (Object.hasOwn(compact, field))
|
|
1407
|
+
compact[field] = dedupeJsonArray(compact[field]);
|
|
1408
|
+
}
|
|
1409
|
+
compact.uiComponentChoices = dedupeJsonArray(record.uiComponentChoices);
|
|
1410
|
+
return compact;
|
|
1411
|
+
}
|
|
1094
1412
|
function assertFrontendContractPathsSafe(value) {
|
|
1095
1413
|
const record = asRecord(value);
|
|
1096
1414
|
if (!record)
|
|
@@ -1166,6 +1484,40 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
1166
1484
|
asRecord(record.mockApi) !== null &&
|
|
1167
1485
|
asRecord(record.designEvidence) !== null);
|
|
1168
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Single shared completion layer for required-with-fallback contract fields.
|
|
1489
|
+
*
|
|
1490
|
+
* Structural invariant: the coerce normalizer has strict-shape and free-form
|
|
1491
|
+
* branches that reshape differently, but every required key that carries a
|
|
1492
|
+
* deterministic fallback (currently `designEvidence`) must be completed in
|
|
1493
|
+
* exactly ONE place — here — so the branches can never drift again (the
|
|
1494
|
+
* designEvidence Required and invented-uiStates failures both came from
|
|
1495
|
+
* branch-local rules diverging). This layer never invents values that later
|
|
1496
|
+
* refinements reject: it only fills documented fallbacks for keys already
|
|
1497
|
+
* present-but-partial, and is idempotent for already-complete inputs.
|
|
1498
|
+
*/
|
|
1499
|
+
export function completeCanonicalRequiredFields(value) {
|
|
1500
|
+
const record = asRecord(value);
|
|
1501
|
+
if (!record)
|
|
1502
|
+
return value;
|
|
1503
|
+
const designEvidence = asRecord(record.designEvidence);
|
|
1504
|
+
if (!designEvidence)
|
|
1505
|
+
return value;
|
|
1506
|
+
const source = asString(designEvidence.source);
|
|
1507
|
+
const paths = Array.isArray(designEvidence.paths);
|
|
1508
|
+
const conflicts = Array.isArray(designEvidence.conflicts);
|
|
1509
|
+
if (source && paths && conflicts)
|
|
1510
|
+
return value;
|
|
1511
|
+
return {
|
|
1512
|
+
...record,
|
|
1513
|
+
designEvidence: {
|
|
1514
|
+
...designEvidence,
|
|
1515
|
+
...(source ? {} : { source: "repository-fallback+task-source" }),
|
|
1516
|
+
...(paths ? {} : { paths: [] }),
|
|
1517
|
+
...(conflicts ? {} : { conflicts: [] }),
|
|
1518
|
+
},
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1169
1521
|
/**
|
|
1170
1522
|
* Coerce common free-form plan JSON into frontend-implementation-contract-v1.
|
|
1171
1523
|
* Near-schema payloads are left untouched so unknown-key fail-closed still holds.
|
|
@@ -1270,6 +1622,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1270
1622
|
...strictRecord,
|
|
1271
1623
|
requirements,
|
|
1272
1624
|
...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
|
|
1625
|
+
// Required-with-fallback keys (designEvidence etc.) are completed
|
|
1626
|
+
// by the single shared `completeCanonicalRequiredFields` layer in
|
|
1627
|
+
// the analyze pipeline — never per-branch.
|
|
1273
1628
|
};
|
|
1274
1629
|
}
|
|
1275
1630
|
}
|
|
@@ -1318,9 +1673,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1318
1673
|
: typeRaw.includes("type")
|
|
1319
1674
|
? "static"
|
|
1320
1675
|
: "unit";
|
|
1321
|
-
const commandLabel = asString(vt.commandLabel) ||
|
|
1322
|
-
asString(vt.command) ||
|
|
1323
|
-
(type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
|
|
1676
|
+
const commandLabel = asString(vt.commandLabel) || asString(vt.command);
|
|
1324
1677
|
const file = asString(vt.file) ||
|
|
1325
1678
|
(Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
|
|
1326
1679
|
targetFiles.find((p) => p.includes("__tests__")) ||
|
|
@@ -1336,7 +1689,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1336
1689
|
requirementIds: requirementIds.length > 0
|
|
1337
1690
|
? requirementIds
|
|
1338
1691
|
: [...canonicalBinding.requirementIds],
|
|
1339
|
-
|
|
1692
|
+
// Never invent UI state references: "success" is not a canonical
|
|
1693
|
+
// state name, so a defaulted ["success","error"] was always rejected
|
|
1694
|
+
// by the schema's unknown-uiState refinement — a verification
|
|
1695
|
+
// target that declares no UI binding stays unbound ([]).
|
|
1696
|
+
uiStates: uiStateNames,
|
|
1340
1697
|
});
|
|
1341
1698
|
}
|
|
1342
1699
|
if (verificationTargets.length === 0) {
|
|
@@ -1371,6 +1728,14 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1371
1728
|
verificationTargetIds: verificationTargetIds.length > 0
|
|
1372
1729
|
? verificationTargetIds
|
|
1373
1730
|
: defaultVerificationIds,
|
|
1731
|
+
// Source fidelity provenance 透传:兼容分支重建 requirement 时必须
|
|
1732
|
+
// 保留已声明的 sourceFragmentIds/sourceRefs(strict 分支通过
|
|
1733
|
+
// `...requirement` 保留,两边必须一致),否则 ledger-bound 契约在
|
|
1734
|
+
// designEvidence 缺失等情况下走兼容分支时丢失防伪造证据。
|
|
1735
|
+
...(Array.isArray(req.sourceFragmentIds)
|
|
1736
|
+
? { sourceFragmentIds: req.sourceFragmentIds }
|
|
1737
|
+
: {}),
|
|
1738
|
+
...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
|
|
1374
1739
|
};
|
|
1375
1740
|
// Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
|
|
1376
1741
|
if (gapText) {
|
|
@@ -1664,12 +2029,25 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1664
2029
|
if (parsedTargetFiles.some((file) => file.startsWith("/") || file.includes("\\")))
|
|
1665
2030
|
fail("blocked", "invalid-output: frontend contract target paths must be relative POSIX paths", candidateJsonSha256);
|
|
1666
2031
|
const parsedStates = asRecord(parsed)?.uiStates;
|
|
1667
|
-
if (Array.isArray(parsedStates)
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2032
|
+
if (Array.isArray(parsedStates)) {
|
|
2033
|
+
const incompleteStates = [];
|
|
2034
|
+
for (const item of parsedStates) {
|
|
2035
|
+
const state = asRecord(item);
|
|
2036
|
+
if (!state || state.applicable !== true)
|
|
2037
|
+
continue;
|
|
2038
|
+
const missing = [];
|
|
2039
|
+
if (!asString(state.expectedBehavior))
|
|
2040
|
+
missing.push("expectedBehavior");
|
|
2041
|
+
if (asStringArray(state.implementationTargets).length === 0)
|
|
2042
|
+
missing.push("implementationTargets");
|
|
2043
|
+
if (asStringArray(state.verificationTargetIds).length === 0)
|
|
2044
|
+
missing.push("verificationTargetIds");
|
|
2045
|
+
if (missing.length > 0)
|
|
2046
|
+
incompleteStates.push(`"${asString(state.name)}": missing ${missing.join(", ")}`);
|
|
2047
|
+
}
|
|
2048
|
+
if (incompleteStates.length > 0)
|
|
2049
|
+
fail("retryable-invalid", `invalid-output: applicable UI states incomplete — re-record each with record_state_flow filling the named fields: ${incompleteStates.join("; ")}`, candidateJsonSha256);
|
|
2050
|
+
}
|
|
1673
2051
|
const parsedMockApi = asRecord(parsed)?.mockApi;
|
|
1674
2052
|
if (asRecord(parsedMockApi) &&
|
|
1675
2053
|
typeof asRecord(parsedMockApi)?.strategy === "string" &&
|
|
@@ -1686,15 +2064,49 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1686
2064
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
1687
2065
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
1688
2066
|
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
2067
|
+
const completedContract = completeCanonicalRequiredFields(normalizedContract);
|
|
1689
2068
|
const candidate = deriveFrontendVerificationCoverage({
|
|
1690
|
-
...(asRecord(
|
|
2069
|
+
...(asRecord(completedContract) ?? parsed),
|
|
1691
2070
|
sourceBinding: canonicalBinding,
|
|
1692
2071
|
}, canonicalBinding);
|
|
1693
|
-
const
|
|
2072
|
+
const compactedCandidate = compactFrontendImplementationContract(candidate);
|
|
2073
|
+
if (serializeCompactDeterministicJson(compactedCandidate) !==
|
|
2074
|
+
serializeCompactDeterministicJson(candidate))
|
|
2075
|
+
pushAction("compact-canonical-contract");
|
|
2076
|
+
const result = frontendImplementationContractSchema.safeParse(compactedCandidate);
|
|
1694
2077
|
if (!result.success)
|
|
1695
2078
|
fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
|
|
2079
|
+
// targets.files is runtime-owned (the skeleton derives it from the task
|
|
2080
|
+
// writeSet, a glob the model must not edit). Narrow it AFTER the full
|
|
2081
|
+
// schema validation — never before, so model-authored unsafe/absolute
|
|
2082
|
+
// declarations are still rejected — to every CONCRETE path the validated
|
|
2083
|
+
// contract references (requirement implementationTargets,
|
|
2084
|
+
// verification-target files, Mock endpoint fixture/consumer paths). The
|
|
2085
|
+
// prewrite containment semantics keep covering everything the contract
|
|
2086
|
+
// references because every narrowed entry already matched the declared
|
|
2087
|
+
// patterns during validation. Satisfies frozen requirements like "deliver
|
|
2088
|
+
// exactly these files" without asking the model to edit a protected
|
|
2089
|
+
// field (r10/r11 design-review findings).
|
|
2090
|
+
const referencedConcretePaths = [
|
|
2091
|
+
...new Set([
|
|
2092
|
+
...result.data.requirements.flatMap((requirement) => requirement.implementationTargets),
|
|
2093
|
+
// Mock endpoint paths stay in the set: the zod refine validated
|
|
2094
|
+
// them against the declared targets.files patterns, so the
|
|
2095
|
+
// narrowing must keep covering them. Verification-target files
|
|
2096
|
+
// deliberately do NOT join: they are test artifacts, not
|
|
2097
|
+
// deliverables, and pulling them in would change which gate
|
|
2098
|
+
// fires for an out-of-writeSet VT (AC-3a semantics).
|
|
2099
|
+
...(result.data.mockApi?.endpoints ?? []).flatMap((endpoint) => [endpoint.fixture, endpoint.consumer].filter((value) => typeof value === "string" && value.length > 0)),
|
|
2100
|
+
].filter((value) => value.length > 0)),
|
|
2101
|
+
].sort();
|
|
2102
|
+
if (referencedConcretePaths.length > 0) {
|
|
2103
|
+
result.data.targets = {
|
|
2104
|
+
...result.data.targets,
|
|
2105
|
+
files: referencedConcretePaths,
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
1696
2108
|
const blockingGaps = [
|
|
1697
|
-
...result.data.evidenceGaps,
|
|
2109
|
+
...(result.data.evidenceGaps ?? []),
|
|
1698
2110
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
1699
2111
|
].filter((item) => item.blocking);
|
|
1700
2112
|
if (blockingGaps.length > 0)
|
|
@@ -1703,7 +2115,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1703
2115
|
.join(", ")}`, candidateJsonSha256);
|
|
1704
2116
|
for (const requirementId of canonicalBinding.requirementIds)
|
|
1705
2117
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
1706
|
-
!result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
|
|
2118
|
+
!(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
|
|
1707
2119
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
1708
2120
|
return {
|
|
1709
2121
|
canonical: result.data,
|
|
@@ -1712,12 +2124,211 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1712
2124
|
normalizationActions,
|
|
1713
2125
|
};
|
|
1714
2126
|
}
|
|
2127
|
+
function frontendCapacityEntry(consumer, maxFiles, maxBytes, contractBytes, sectionCount) {
|
|
2128
|
+
const utilizationRatio = contractBytes / maxBytes;
|
|
2129
|
+
const indexedFiles = sectionCount + 2;
|
|
2130
|
+
const byteLevel = utilizationRatio >= 1
|
|
2131
|
+
? "over-budget"
|
|
2132
|
+
: utilizationRatio >= FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO
|
|
2133
|
+
? "warning"
|
|
2134
|
+
: "ok";
|
|
2135
|
+
return {
|
|
2136
|
+
consumer,
|
|
2137
|
+
maxFiles,
|
|
2138
|
+
maxBytes,
|
|
2139
|
+
contractBytes,
|
|
2140
|
+
indexedFiles,
|
|
2141
|
+
headroomBytes: Math.max(0, maxBytes - contractBytes),
|
|
2142
|
+
utilizationRatio: Number(utilizationRatio.toFixed(4)),
|
|
2143
|
+
level: byteLevel !== "ok" && indexedFiles > maxFiles
|
|
2144
|
+
? "over-budget"
|
|
2145
|
+
: byteLevel,
|
|
2146
|
+
recommendedReadMode: byteLevel === "ok"
|
|
2147
|
+
? "full-contract"
|
|
2148
|
+
: "indexed-sections",
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function splitFrontendContractReviewGroup(canonical, fields) {
|
|
2152
|
+
const parts = [];
|
|
2153
|
+
let data = {};
|
|
2154
|
+
let slices = [];
|
|
2155
|
+
const dataBytes = (candidate) => Buffer.byteLength(serializeCompactDeterministicJson(candidate), "utf8");
|
|
2156
|
+
const flush = () => {
|
|
2157
|
+
if (Object.keys(data).length === 0)
|
|
2158
|
+
return;
|
|
2159
|
+
parts.push({ data, slices });
|
|
2160
|
+
data = {};
|
|
2161
|
+
slices = [];
|
|
2162
|
+
};
|
|
2163
|
+
for (const field of fields) {
|
|
2164
|
+
if (!Object.hasOwn(canonical, field))
|
|
2165
|
+
continue;
|
|
2166
|
+
const fieldValue = canonical[field];
|
|
2167
|
+
if (!Array.isArray(fieldValue)) {
|
|
2168
|
+
const candidate = { ...data, [field]: fieldValue };
|
|
2169
|
+
if (Object.keys(data).length > 0 &&
|
|
2170
|
+
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2171
|
+
flush();
|
|
2172
|
+
}
|
|
2173
|
+
data[field] = fieldValue;
|
|
2174
|
+
slices.push({ field });
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (fieldValue.length === 0) {
|
|
2178
|
+
const candidate = { ...data, [field]: [] };
|
|
2179
|
+
if (Object.keys(data).length > 0 &&
|
|
2180
|
+
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2181
|
+
flush();
|
|
2182
|
+
}
|
|
2183
|
+
data[field] = [];
|
|
2184
|
+
slices.push({ field, start: 0, end: 0, total: 0 });
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
let start = 0;
|
|
2188
|
+
while (start < fieldValue.length) {
|
|
2189
|
+
let end = start;
|
|
2190
|
+
let accepted = [];
|
|
2191
|
+
while (end < fieldValue.length) {
|
|
2192
|
+
const next = [...accepted, fieldValue[end]];
|
|
2193
|
+
const candidate = { ...data, [field]: next };
|
|
2194
|
+
if (accepted.length > 0 &&
|
|
2195
|
+
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
if (accepted.length === 0 &&
|
|
2199
|
+
Object.keys(data).length > 0 &&
|
|
2200
|
+
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2201
|
+
flush();
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
accepted = next;
|
|
2205
|
+
end += 1;
|
|
2206
|
+
}
|
|
2207
|
+
data[field] = accepted;
|
|
2208
|
+
slices.push({ field, start, end, total: fieldValue.length });
|
|
2209
|
+
start = end;
|
|
2210
|
+
if (start < fieldValue.length)
|
|
2211
|
+
flush();
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
flush();
|
|
2215
|
+
return parts;
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Materialize a hash-bound field index and bounded contract sections.
|
|
2219
|
+
* These artifacts are an alternate read surface only: the canonical contract
|
|
2220
|
+
* remains authoritative and capacity diagnostics never affect admission.
|
|
2221
|
+
*/
|
|
2222
|
+
export async function writeFrontendContractReviewArtifacts(input) {
|
|
2223
|
+
const contractRelativePath = path.posix.join(input.outputDir, input.artifactName);
|
|
2224
|
+
const baseName = input.artifactName.endsWith(".json")
|
|
2225
|
+
? input.artifactName.slice(0, -".json".length)
|
|
2226
|
+
: input.artifactName;
|
|
2227
|
+
const sectionDir = path.posix.join(input.outputDir, `${baseName}.sections`);
|
|
2228
|
+
const indexRelativePath = path.posix.join(input.outputDir, `${baseName}.index.json`);
|
|
2229
|
+
const capacityRelativePath = path.posix.join(input.outputDir, `${baseName}.capacity.json`);
|
|
2230
|
+
const compactContract = `${serializeCompactDeterministicJson(input.canonical)}\n`;
|
|
2231
|
+
const canonicalArtifact = input.canonicalArtifact ?? {
|
|
2232
|
+
path: path.join(input.runDir, contractRelativePath),
|
|
2233
|
+
sha256: sha256Hex(compactContract),
|
|
2234
|
+
bytes: Buffer.byteLength(compactContract, "utf8"),
|
|
2235
|
+
};
|
|
2236
|
+
const sections = [];
|
|
2237
|
+
for (const [index, group] of FRONTEND_CONTRACT_REVIEW_GROUPS.entries()) {
|
|
2238
|
+
const parts = splitFrontendContractReviewGroup(input.canonical, group.fields);
|
|
2239
|
+
for (const [partIndex, part] of parts.entries()) {
|
|
2240
|
+
const relativePath = path.posix.join(sectionDir, `${String(index + 1).padStart(2, "0")}-${group.id}-part-${String(partIndex + 1).padStart(2, "0")}.json`);
|
|
2241
|
+
const sectionPayload = {
|
|
2242
|
+
schemaVersion: 1,
|
|
2243
|
+
schemaId: FRONTEND_CONTRACT_SECTION_SCHEMA_ID,
|
|
2244
|
+
contractSchemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2245
|
+
contractSha256: canonicalArtifact.sha256,
|
|
2246
|
+
group: group.id,
|
|
2247
|
+
part: partIndex + 1,
|
|
2248
|
+
partCount: parts.length,
|
|
2249
|
+
fields: Object.keys(part.data),
|
|
2250
|
+
slices: part.slices,
|
|
2251
|
+
data: part.data,
|
|
2252
|
+
};
|
|
2253
|
+
const written = await writeCompactDeterministicJsonArtifact(input.runDir, relativePath, sectionPayload);
|
|
2254
|
+
sections.push({
|
|
2255
|
+
id: group.id,
|
|
2256
|
+
fields: Object.keys(part.data),
|
|
2257
|
+
part: partIndex + 1,
|
|
2258
|
+
partCount: parts.length,
|
|
2259
|
+
slices: part.slices,
|
|
2260
|
+
path: relativePath,
|
|
2261
|
+
sha256: written.sha256,
|
|
2262
|
+
bytes: Buffer.byteLength(`${serializeCompactDeterministicJson(sectionPayload)}\n`, "utf8"),
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const indexPayload = {
|
|
2267
|
+
schemaVersion: 1,
|
|
2268
|
+
schemaId: FRONTEND_CONTRACT_REVIEW_INDEX_SCHEMA_ID,
|
|
2269
|
+
contract: {
|
|
2270
|
+
path: contractRelativePath,
|
|
2271
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2272
|
+
sha256: canonicalArtifact.sha256,
|
|
2273
|
+
bytes: canonicalArtifact.bytes,
|
|
2274
|
+
},
|
|
2275
|
+
sections,
|
|
2276
|
+
};
|
|
2277
|
+
const writtenIndex = await writeCompactDeterministicJsonArtifact(input.runDir, indexRelativePath, indexPayload);
|
|
2278
|
+
const consumers = [
|
|
2279
|
+
frontendCapacityEntry("design-review", FRONTEND_READ_BUDGETS.designReview.maxFiles, FRONTEND_READ_BUDGETS.designReview.maxBytes, canonicalArtifact.bytes, sections.length),
|
|
2280
|
+
frontendCapacityEntry("final-review", FRONTEND_READ_BUDGETS.finalReview.maxFiles, FRONTEND_READ_BUDGETS.finalReview.maxBytes, canonicalArtifact.bytes, sections.length),
|
|
2281
|
+
];
|
|
2282
|
+
const capacityPayload = {
|
|
2283
|
+
schemaVersion: 1,
|
|
2284
|
+
schemaId: FRONTEND_CONTRACT_CAPACITY_SCHEMA_ID,
|
|
2285
|
+
nonBlocking: true,
|
|
2286
|
+
warningThresholdRatio: FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO,
|
|
2287
|
+
contract: indexPayload.contract,
|
|
2288
|
+
index: {
|
|
2289
|
+
path: indexRelativePath,
|
|
2290
|
+
sha256: writtenIndex.sha256,
|
|
2291
|
+
sectionCount: sections.length,
|
|
2292
|
+
sectionBytes: sections.reduce((total, section) => total + section.bytes, 0),
|
|
2293
|
+
maxSectionBytes: Math.max(...sections.map((section) => section.bytes)),
|
|
2294
|
+
oversizedSectionCount: sections.filter((section) => section.bytes >= 48 * 1024).length,
|
|
2295
|
+
},
|
|
2296
|
+
consumers,
|
|
2297
|
+
overallLevel: consumers.some((entry) => entry.level === "over-budget")
|
|
2298
|
+
? "over-budget"
|
|
2299
|
+
: consumers.some((entry) => entry.level === "warning")
|
|
2300
|
+
? "warning"
|
|
2301
|
+
: "ok",
|
|
2302
|
+
};
|
|
2303
|
+
const writtenCapacity = await writeCompactDeterministicJsonArtifact(input.runDir, capacityRelativePath, capacityPayload);
|
|
2304
|
+
return {
|
|
2305
|
+
index: {
|
|
2306
|
+
path: writtenIndex.path,
|
|
2307
|
+
relativePath: indexRelativePath,
|
|
2308
|
+
artifactSha256: writtenIndex.sha256,
|
|
2309
|
+
...indexPayload,
|
|
2310
|
+
},
|
|
2311
|
+
capacity: {
|
|
2312
|
+
path: writtenCapacity.path,
|
|
2313
|
+
relativePath: capacityRelativePath,
|
|
2314
|
+
artifactSha256: writtenCapacity.sha256,
|
|
2315
|
+
...capacityPayload,
|
|
2316
|
+
},
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
1715
2319
|
export async function writeFrontendImplementationContractArtifact(input) {
|
|
1716
|
-
const written = await
|
|
2320
|
+
const written = await writeCompactDeterministicJsonArtifact(input.runDir, path.posix.join(input.outputDir, input.artifactName), input.canonical);
|
|
2321
|
+
const bytes = Buffer.byteLength(`${serializeCompactDeterministicJson(input.canonical)}\n`, "utf8");
|
|
2322
|
+
const reviewArtifacts = await writeFrontendContractReviewArtifacts({
|
|
2323
|
+
...input,
|
|
2324
|
+
canonicalArtifact: { ...written, bytes },
|
|
2325
|
+
});
|
|
1717
2326
|
return {
|
|
1718
2327
|
path: written.path,
|
|
1719
2328
|
sha256: written.sha256,
|
|
1720
2329
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2330
|
+
reviewIndexPath: reviewArtifacts.index.path,
|
|
2331
|
+
capacityDiagnosticPath: reviewArtifacts.capacity.path,
|
|
1721
2332
|
};
|
|
1722
2333
|
}
|
|
1723
2334
|
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
@@ -1727,6 +2338,40 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
1727
2338
|
"/targets/files",
|
|
1728
2339
|
"/mockApi/productionDefaultOff",
|
|
1729
2340
|
];
|
|
2341
|
+
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
2342
|
+
"target-surface",
|
|
2343
|
+
"component-choice",
|
|
2344
|
+
"state-flow",
|
|
2345
|
+
"data-flow",
|
|
2346
|
+
"mock-api",
|
|
2347
|
+
"design-deviation",
|
|
2348
|
+
"dependency",
|
|
2349
|
+
"plan-requirement",
|
|
2350
|
+
"plan-verification-target",
|
|
2351
|
+
"plan-evidence-gap",
|
|
2352
|
+
];
|
|
2353
|
+
/**
|
|
2354
|
+
* Restore the editable RFC 7386 plan patch from a committed plan ledger.
|
|
2355
|
+
* This is the single compile authority for the frontend implementation
|
|
2356
|
+
* contract: the tool payload (`patch` on any committed plan fact) wins over
|
|
2357
|
+
* any narrative JSON in the plan node text.
|
|
2358
|
+
*/
|
|
2359
|
+
export function restorePlanPatchFromCommittedFacts(records) {
|
|
2360
|
+
const planFacts = records
|
|
2361
|
+
.filter((record) => record.phase === "committed")
|
|
2362
|
+
.map((record) => record.fact)
|
|
2363
|
+
.filter((fact) => fact.origin === "plan" &&
|
|
2364
|
+
typeof fact.kind === "string" &&
|
|
2365
|
+
PLAN_LEDGER_FACT_KINDS_FOR_COMPILE.includes(fact.kind));
|
|
2366
|
+
if (planFacts.length === 0)
|
|
2367
|
+
return undefined;
|
|
2368
|
+
for (const fact of planFacts) {
|
|
2369
|
+
const patch = fact.patch;
|
|
2370
|
+
if (isPlainObject(patch))
|
|
2371
|
+
return patch;
|
|
2372
|
+
}
|
|
2373
|
+
return undefined;
|
|
2374
|
+
}
|
|
1730
2375
|
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1731
2376
|
const record = asRecord(patch);
|
|
1732
2377
|
if (!record)
|
|
@@ -1756,34 +2401,34 @@ function workspaceRootFromDagRunDir(runDir) {
|
|
|
1756
2401
|
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1757
2402
|
return joined || path.parse(absolute).root;
|
|
1758
2403
|
}
|
|
1759
|
-
function verificationSymbolMatchesFile(symbol, content) {
|
|
1760
|
-
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1761
|
-
return content.includes("describe(");
|
|
1762
|
-
const symbols = symbol.includes(" / ")
|
|
1763
|
-
? symbol
|
|
1764
|
-
.split(/[((]/, 1)[0]
|
|
1765
|
-
.split("/")
|
|
1766
|
-
.map((item) => item.trim())
|
|
1767
|
-
.filter(Boolean)
|
|
1768
|
-
: [symbol];
|
|
1769
|
-
return symbols.every((item) => {
|
|
1770
|
-
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1771
|
-
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1772
|
-
.filter((candidate) => Boolean(candidate))
|
|
1773
|
-
.some((candidate) => content.includes(candidate));
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
2404
|
async function dropUnresolvedVerificationSymbols(input) {
|
|
1777
2405
|
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1778
2406
|
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1779
2407
|
if (!workspaceRoot || !Array.isArray(targets))
|
|
1780
|
-
return
|
|
1781
|
-
|
|
2408
|
+
return [];
|
|
2409
|
+
const actions = [];
|
|
1782
2410
|
for (const targetValue of targets) {
|
|
1783
2411
|
const target = asRecord(targetValue);
|
|
1784
2412
|
const file = asString(target?.file);
|
|
1785
2413
|
const symbol = asString(target?.symbol);
|
|
1786
|
-
if (!target || !file
|
|
2414
|
+
if (!target || !file)
|
|
2415
|
+
continue;
|
|
2416
|
+
// Static targets (project-wide commands such as typecheck/build) verify
|
|
2417
|
+
// by command exit code, never by a per-file symbol. Strip any symbol
|
|
2418
|
+
// the planner attached — including fabricated placeholders like "null"
|
|
2419
|
+
// — so the plan shape check and the verify trace gate never evaluate
|
|
2420
|
+
// it. This is the root fix for the recurring fabricated-symbol error
|
|
2421
|
+
// class: the field was allowed for static, the prompt did not forbid
|
|
2422
|
+
// it, and models habitually fill it; the compiler removes it instead.
|
|
2423
|
+
if (target.type === "static") {
|
|
2424
|
+
if (symbol) {
|
|
2425
|
+
delete target.symbol;
|
|
2426
|
+
if (!actions.includes("strip-static-verification-symbols"))
|
|
2427
|
+
actions.push("strip-static-verification-symbols");
|
|
2428
|
+
}
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
if (!symbol)
|
|
1787
2432
|
continue;
|
|
1788
2433
|
const absolute = path.resolve(workspaceRoot, file);
|
|
1789
2434
|
const relative = path.relative(workspaceRoot, absolute);
|
|
@@ -1792,6 +2437,11 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1792
2437
|
relative.includes("..")) {
|
|
1793
2438
|
continue;
|
|
1794
2439
|
}
|
|
2440
|
+
// Configuration files (tsconfig.json, vite.config.*, ...) do not expose
|
|
2441
|
+
// source symbols; legacy symbol normalization leaves them alone. Runtime
|
|
2442
|
+
// trace binding handles static targets through file + command only.
|
|
2443
|
+
if (isConfigurationVerificationFile(file))
|
|
2444
|
+
continue;
|
|
1795
2445
|
let content;
|
|
1796
2446
|
try {
|
|
1797
2447
|
content = await readFile(absolute, "utf8");
|
|
@@ -1801,23 +2451,13 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1801
2451
|
// changing the optional symbol during candidate normalization.
|
|
1802
2452
|
continue;
|
|
1803
2453
|
}
|
|
1804
|
-
if (
|
|
2454
|
+
if (verificationSymbolMatchesContent(symbol, content))
|
|
1805
2455
|
continue;
|
|
1806
2456
|
delete target.symbol;
|
|
1807
|
-
|
|
2457
|
+
if (!actions.includes("drop-unresolved-verification-symbols"))
|
|
2458
|
+
actions.push("drop-unresolved-verification-symbols");
|
|
1808
2459
|
}
|
|
1809
|
-
return
|
|
1810
|
-
}
|
|
1811
|
-
function extractSingleOpenspecCitationsBlock(text) {
|
|
1812
|
-
// Line-anchored so a directly closed empty fence (no candidate rows) still
|
|
1813
|
-
// counts as the one required block instead of "found 0" retry loops.
|
|
1814
|
-
const blocks = [
|
|
1815
|
-
...text.matchAll(/^```openspec-citations[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim),
|
|
1816
|
-
].map((match) => match[0]);
|
|
1817
|
-
if (blocks.length !== 1) {
|
|
1818
|
-
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1819
|
-
}
|
|
1820
|
-
return blocks[0];
|
|
2460
|
+
return actions;
|
|
1821
2461
|
}
|
|
1822
2462
|
async function writeFrontendPlanCandidateRaw(input) {
|
|
1823
2463
|
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
@@ -1831,6 +2471,118 @@ async function writeFrontendPlanCandidateRaw(input) {
|
|
|
1831
2471
|
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
1832
2472
|
* downstream review and prewrite nodes.
|
|
1833
2473
|
*/
|
|
2474
|
+
/**
|
|
2475
|
+
* Placeholder tokens that are never valid verification symbols ("null" is the
|
|
2476
|
+
* classic model artifact: it serializes a JS null into the symbol string).
|
|
2477
|
+
*/
|
|
2478
|
+
const VERIFICATION_SYMBOL_PLACEHOLDERS = new Set([
|
|
2479
|
+
"null",
|
|
2480
|
+
"undefined",
|
|
2481
|
+
"none",
|
|
2482
|
+
"n/a",
|
|
2483
|
+
"na",
|
|
2484
|
+
"tbd",
|
|
2485
|
+
"todo",
|
|
2486
|
+
"...",
|
|
2487
|
+
"?",
|
|
2488
|
+
"-",
|
|
2489
|
+
]);
|
|
2490
|
+
/** An over-long symbol can never resolve to a real declaration. */
|
|
2491
|
+
const VERIFICATION_SYMBOL_MAX_CHARS = 60;
|
|
2492
|
+
/**
|
|
2493
|
+
* Deterministic shape check for a verification target symbol. Catches obvious
|
|
2494
|
+
* model fabrication (placeholder tokens, prose fragments) when reading a
|
|
2495
|
+
* legacy contract. New plan tool calls do not expose this field; runtime trace
|
|
2496
|
+
* binding uses the stable verification-target id instead.
|
|
2497
|
+
*
|
|
2498
|
+
* Deliberately conservative: bare test titles without punctuation ("renders
|
|
2499
|
+
* loading") and `describe(...)` / `it(...)` forms stay valid — a real symbol
|
|
2500
|
+
* may be a function name, a dotted path, or a describe/it title.
|
|
2501
|
+
*/
|
|
2502
|
+
export function isSuspiciousVerificationSymbol(symbol) {
|
|
2503
|
+
const trimmed = symbol.trim();
|
|
2504
|
+
if (!trimmed)
|
|
2505
|
+
return false;
|
|
2506
|
+
if (VERIFICATION_SYMBOL_PLACEHOLDERS.has(trimmed.toLowerCase()))
|
|
2507
|
+
return true;
|
|
2508
|
+
if (trimmed.length > VERIFICATION_SYMBOL_MAX_CHARS)
|
|
2509
|
+
return true;
|
|
2510
|
+
// Deliberately conservative: bare test titles with punctuation (e.g.
|
|
2511
|
+
// "adds +1 button") are valid describe/it-style symbols and must pass;
|
|
2512
|
+
// the length + placeholder rules above already catch obvious fabrication
|
|
2513
|
+
// ("null", over-long prose fragments). Everything else is left to the
|
|
2514
|
+
// deterministic drop/trace resolvability check.
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
function assertVerificationSymbolShapes(contract) {
|
|
2518
|
+
for (const target of contract.verificationTargets) {
|
|
2519
|
+
if (!target.symbol)
|
|
2520
|
+
continue;
|
|
2521
|
+
if (isSuspiciousVerificationSymbol(target.symbol)) {
|
|
2522
|
+
throw new Error(`frontend plan verification target "${target.id}" declares a fabricated legacy symbol "${target.symbol}" for ${target.file}; remove the symbol (deterministic trace binding uses the stable target id)`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Thrown by analyzeFrontendPlanPatchCandidate when the design-policy
|
|
2528
|
+
* pre-checks report findings. Carries the structured findings so callers
|
|
2529
|
+
* (finalize_plan receipt) can synthesize fix suggestions instead of making
|
|
2530
|
+
* the model re-derive them from prose.
|
|
2531
|
+
*/
|
|
2532
|
+
export class PlanPolicyPrecheckFailure extends Error {
|
|
2533
|
+
findings;
|
|
2534
|
+
constructor(message, findings) {
|
|
2535
|
+
super(message);
|
|
2536
|
+
this.name = "PlanPolicyPrecheckFailure";
|
|
2537
|
+
this.findings = findings;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Shared tail of the plan patch validation: analyze the merged contract and
|
|
2542
|
+
* front-load the plan-attributable design-policy checks. Throws the same
|
|
2543
|
+
* errors the node self-check throws (FrontendContractFailure for schema
|
|
2544
|
+
* failures, Error for policy findings) so every caller — the node validator
|
|
2545
|
+
* and the finalize_plan receipt — surfaces identical diagnostics.
|
|
2546
|
+
*/
|
|
2547
|
+
export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
2548
|
+
const analysis = await analyzeFrontendImplementationContract(input);
|
|
2549
|
+
// A committed VT with a fabricated symbol is an immutable ledger fact —
|
|
2550
|
+
// the record boundary rejects duplicate ids, so the model cannot overwrite
|
|
2551
|
+
// it and throwing here deadlocks the receipt loop (r19: VT-AC006-BEHAVIOR).
|
|
2552
|
+
// Drop suspicious symbols deterministically instead: the VT stays valid and
|
|
2553
|
+
// the deterministic trace gate verifies file+command (and resolvability)
|
|
2554
|
+
// after verification. Mirrors the shell materialization's drop semantics.
|
|
2555
|
+
for (const target of analysis.canonical.verificationTargets) {
|
|
2556
|
+
if (target.symbol && isSuspiciousVerificationSymbol(target.symbol)) {
|
|
2557
|
+
target.symbol = undefined;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
// Front-load the verification-symbol shape check so fabricated symbols
|
|
2561
|
+
// are fixed by the plan retry ladder in-node instead of failing the
|
|
2562
|
+
// verify trace gate at the end of the run.
|
|
2563
|
+
assertVerificationSymbolShapes(analysis.canonical);
|
|
2564
|
+
// Front-load the plan-attributable design-policy checks so the §5.1
|
|
2565
|
+
// retry ladder can fix these facts in-node with diagnostics instead of
|
|
2566
|
+
// the run terminating at the policy shell. The policy shell remains the
|
|
2567
|
+
// final authority and re-runs the identical checks on committed facts.
|
|
2568
|
+
const policyPreFindings = [
|
|
2569
|
+
...checkUiDesignCoverage(analysis.canonical),
|
|
2570
|
+
...checkUiStateAttribution(analysis.canonical),
|
|
2571
|
+
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2572
|
+
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
2573
|
+
];
|
|
2574
|
+
if (policyPreFindings.length > 0) {
|
|
2575
|
+
const message = `frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
|
|
2576
|
+
.map((finding) => `${finding.code}: ${finding.message}`)
|
|
2577
|
+
.join("; ")}`;
|
|
2578
|
+
throw new PlanPolicyPrecheckFailure(message, policyPreFindings.map((finding) => ({
|
|
2579
|
+
code: finding.code,
|
|
2580
|
+
message: finding.message,
|
|
2581
|
+
...(finding.path ? { path: finding.path } : {}),
|
|
2582
|
+
})));
|
|
2583
|
+
}
|
|
2584
|
+
return analysis;
|
|
2585
|
+
}
|
|
1834
2586
|
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1835
2587
|
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1836
2588
|
const attempt = input.attempt ?? 1;
|
|
@@ -1867,22 +2619,22 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1867
2619
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1868
2620
|
if (!skeleton)
|
|
1869
2621
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1870
|
-
const
|
|
2622
|
+
const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
|
|
2623
|
+
const patch = restorePlanPatchFromCommittedFacts(committed);
|
|
1871
2624
|
if (!isPlainObject(patch))
|
|
1872
|
-
throw new Error("frontend plan
|
|
2625
|
+
throw new Error("frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority");
|
|
1873
2626
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1874
2627
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1875
2628
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1876
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1877
2629
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1878
2630
|
if (protectedViolations.length > 0)
|
|
1879
2631
|
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1880
2632
|
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1881
|
-
const
|
|
2633
|
+
const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
|
|
1882
2634
|
runDir: input.runDir,
|
|
1883
2635
|
contract: merged,
|
|
1884
2636
|
});
|
|
1885
|
-
const analysis = await
|
|
2637
|
+
const analysis = await analyzeFrontendPlanPatchCandidate({
|
|
1886
2638
|
runDir: input.runDir,
|
|
1887
2639
|
rawContractText: serializeDeterministicJson(merged),
|
|
1888
2640
|
sourceBinding: input.sourceBinding,
|
|
@@ -1894,20 +2646,13 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1894
2646
|
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1895
2647
|
normalizationActions: [
|
|
1896
2648
|
"apply-runtime-skeleton",
|
|
1897
|
-
...
|
|
1898
|
-
? ["drop-unresolved-verification-symbols"]
|
|
1899
|
-
: []),
|
|
2649
|
+
...symbolNormalizationActions,
|
|
1900
2650
|
...analysis.normalizationActions,
|
|
1901
2651
|
],
|
|
1902
2652
|
errors: [],
|
|
1903
2653
|
});
|
|
1904
2654
|
const canonicalRel = canonicalContractRel(nodeId);
|
|
1905
2655
|
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
|
|
1906
|
-
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
1907
|
-
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
1908
|
-
citations: parsedCitations.citations,
|
|
1909
|
-
block: citationsBlock,
|
|
1910
|
-
});
|
|
1911
2656
|
await persistRuntimeSkeleton({
|
|
1912
2657
|
runDir: input.runDir,
|
|
1913
2658
|
nodeId,
|
|
@@ -1918,11 +2663,17 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1918
2663
|
sha256: canonicalArtifact.sha256,
|
|
1919
2664
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1920
2665
|
};
|
|
2666
|
+
// Human-readable plan summary appended after the artifact pointer. The
|
|
2667
|
+
// pointer remains the machine authority (path/schema/sha256); the
|
|
2668
|
+
// rendered summary is a deterministic view of the canonical contract so
|
|
2669
|
+
// Console/reviewers can read the plan without opening the JSON. It never
|
|
2670
|
+
// participates in downstream validation.
|
|
2671
|
+
const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
|
|
1921
2672
|
return {
|
|
1922
2673
|
ok: true,
|
|
1923
2674
|
contract: analysis.canonical,
|
|
1924
2675
|
artifact,
|
|
1925
|
-
normalizedText: formatStructuredArtifactPointer(artifact)
|
|
2676
|
+
normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
|
|
1926
2677
|
};
|
|
1927
2678
|
}
|
|
1928
2679
|
catch (error) {
|
|
@@ -1989,106 +2740,125 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1989
2740
|
}
|
|
1990
2741
|
}
|
|
1991
2742
|
/**
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
2743
|
+
* Deterministically assemble the canonical frontend implementation contract
|
|
2744
|
+
* from committed typed facts, never from plan/contract node text.
|
|
2745
|
+
*
|
|
2746
|
+
* Inputs (all read-only, all fail-closed):
|
|
2747
|
+
* - `<planNodeId>/plan-typed-facts.jsonl` (phase=committed) restores the
|
|
2748
|
+
* editable RFC 7386 patch;
|
|
2749
|
+
* - `<contractNodeId>/contract-typed-facts.jsonl` (phase=committed) must carry
|
|
2750
|
+
* a `contract-finalized` terminal fact;
|
|
2751
|
+
* - `<contractNodeId>/frontend-task-contract-vNext.json` must exist and parse.
|
|
2752
|
+
*
|
|
2753
|
+
* The patch is applied onto the runtime skeleton and re-validated through the
|
|
2754
|
+
* same `analyzeFrontendImplementationContract` pipeline used by the legacy
|
|
2755
|
+
* text path, so the downstream artifact semantics are unchanged. The function
|
|
2756
|
+
* is pure: it never writes candidate/audit artifacts and always produces the
|
|
2757
|
+
* same canonical serialization for the same committed facts. It never falls
|
|
2758
|
+
* back to parsing Plan/Contract node text.
|
|
1997
2759
|
*/
|
|
1998
|
-
export async function
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2760
|
+
export async function buildContractFromCommittedFacts(input) {
|
|
2761
|
+
const contractNodeId = input.contractNodeId ?? "frontend-contract-pi";
|
|
2762
|
+
// Skeleton guard: the runtime skeleton must carry the protected identity
|
|
2763
|
+
// fields so the merged contract can be validated against the full schema.
|
|
2764
|
+
const skeletonRecord = asRecord(input.skeleton);
|
|
2765
|
+
const skeletonTargets = asRecord(skeletonRecord?.targets);
|
|
2766
|
+
const skeletonRiskLevel = asString(skeletonRecord?.riskLevel);
|
|
2767
|
+
if (!skeletonRecord ||
|
|
2768
|
+
!["small", "standard", "high-risk"].includes(skeletonRiskLevel) ||
|
|
2769
|
+
!Array.isArray(skeletonTargets?.files) ||
|
|
2770
|
+
skeletonTargets.files.length === 0) {
|
|
2001
2771
|
return {
|
|
2002
2772
|
ok: false,
|
|
2003
|
-
reason,
|
|
2004
|
-
|
|
2005
|
-
errors: classified.errors,
|
|
2773
|
+
reason: "frontend contract skeleton missing required runtime fields (riskLevel/targets.files)",
|
|
2774
|
+
failureCode: "missing-skeleton",
|
|
2006
2775
|
};
|
|
2007
|
-
};
|
|
2008
|
-
const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
|
|
2009
|
-
const attempt = input.attempt ?? 1;
|
|
2010
|
-
await writeFrontendPlanCandidateRaw({
|
|
2011
|
-
runDir: input.runDir,
|
|
2012
|
-
nodeId,
|
|
2013
|
-
attempt,
|
|
2014
|
-
text: input.text,
|
|
2015
|
-
});
|
|
2016
|
-
let citationsBlock;
|
|
2017
|
-
try {
|
|
2018
|
-
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
2019
2776
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2777
|
+
// a. Committed plan ledger restores the editable patch.
|
|
2778
|
+
const planRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.planNodeId, "plan-typed-facts.jsonl"));
|
|
2779
|
+
const patch = restorePlanPatchFromCommittedFacts(planRecords);
|
|
2780
|
+
if (!isPlainObject(patch)) {
|
|
2781
|
+
return {
|
|
2782
|
+
ok: false,
|
|
2783
|
+
reason: "frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority",
|
|
2784
|
+
failureCode: "missing-plan-ledger",
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
// b. Committed contract terminal fact (contract-finalized).
|
|
2788
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
2789
|
+
const contractKinds = new Set(contractRecords
|
|
2790
|
+
.filter((record) => record.phase === "committed")
|
|
2791
|
+
.map((record) => record.fact.kind)
|
|
2792
|
+
.filter((kind) => typeof kind === "string"));
|
|
2793
|
+
if (!contractKinds.has("contract-finalized")) {
|
|
2794
|
+
return {
|
|
2795
|
+
ok: false,
|
|
2796
|
+
reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
|
|
2797
|
+
failureCode: "missing-contract-finalize",
|
|
2798
|
+
};
|
|
2022
2799
|
}
|
|
2023
|
-
|
|
2024
|
-
let
|
|
2800
|
+
// c. frontend-task-contract-vNext.json must exist and parse (input integrity).
|
|
2801
|
+
let rawVNext;
|
|
2025
2802
|
try {
|
|
2026
|
-
|
|
2803
|
+
rawVNext = await readFile(path.join(input.runDir, contractNodeId, "frontend-task-contract-vNext.json"), "utf8");
|
|
2027
2804
|
}
|
|
2028
|
-
catch
|
|
2029
|
-
return
|
|
2805
|
+
catch {
|
|
2806
|
+
return {
|
|
2807
|
+
ok: false,
|
|
2808
|
+
reason: `frontend task contract vNext missing at ${contractNodeId}/frontend-task-contract-vNext.json`,
|
|
2809
|
+
failureCode: "contract-vnext-missing",
|
|
2810
|
+
};
|
|
2030
2811
|
}
|
|
2031
|
-
|
|
2032
|
-
|
|
2812
|
+
try {
|
|
2813
|
+
JSON.parse(rawVNext);
|
|
2033
2814
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2815
|
+
catch (error) {
|
|
2816
|
+
return {
|
|
2817
|
+
ok: false,
|
|
2818
|
+
reason: `frontend task contract vNext is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
2819
|
+
failureCode: "contract-vnext-invalid",
|
|
2820
|
+
};
|
|
2037
2821
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2822
|
+
// d. Merge the editable patch onto the protected skeleton and validate.
|
|
2823
|
+
let merged;
|
|
2824
|
+
try {
|
|
2825
|
+
merged = applyFrontendContractMergePatch(input.skeleton, patch);
|
|
2042
2826
|
}
|
|
2043
|
-
|
|
2044
|
-
return
|
|
2827
|
+
catch (error) {
|
|
2828
|
+
return {
|
|
2829
|
+
ok: false,
|
|
2830
|
+
reason: `frontend contract patch could not be applied to the runtime skeleton: ${error instanceof Error ? error.message : String(error)}`,
|
|
2831
|
+
failureCode: "schema-drift",
|
|
2832
|
+
};
|
|
2045
2833
|
}
|
|
2834
|
+
// The design-policy shell is the authoritative materializer of the
|
|
2835
|
+
// canonical contract; keep legacy symbol normalization identical to the
|
|
2836
|
+
// plan validator even though trace binding now uses stable target ids.
|
|
2837
|
+
await dropUnresolvedVerificationSymbols({
|
|
2838
|
+
runDir: input.runDir,
|
|
2839
|
+
contract: merged,
|
|
2840
|
+
});
|
|
2046
2841
|
try {
|
|
2047
|
-
const merged = applyFrontendContractMergePatch(original, patch);
|
|
2048
|
-
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
2049
|
-
runDir: input.runDir,
|
|
2050
|
-
contract: merged,
|
|
2051
|
-
});
|
|
2052
2842
|
const analysis = await analyzeFrontendImplementationContract({
|
|
2053
2843
|
runDir: input.runDir,
|
|
2054
2844
|
rawContractText: serializeDeterministicJson(merged),
|
|
2055
2845
|
sourceBinding: input.sourceBinding,
|
|
2056
2846
|
});
|
|
2057
|
-
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
2058
|
-
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`), patch);
|
|
2059
|
-
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
2060
|
-
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
2061
|
-
classification: "accepted-normalized",
|
|
2062
|
-
normalizationActions: [
|
|
2063
|
-
"apply-original-canonical",
|
|
2064
|
-
...(droppedUnresolvedSymbols
|
|
2065
|
-
? ["drop-unresolved-verification-symbols"]
|
|
2066
|
-
: []),
|
|
2067
|
-
...analysis.normalizationActions,
|
|
2068
|
-
],
|
|
2069
|
-
errors: [],
|
|
2070
|
-
});
|
|
2071
|
-
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalContractRel(nodeId), analysis.canonical);
|
|
2072
|
-
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
2073
|
-
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
2074
|
-
citations: parsedCitations.citations,
|
|
2075
|
-
block: citationsBlock,
|
|
2076
|
-
});
|
|
2077
|
-
const artifact = {
|
|
2078
|
-
path: canonicalArtifact.path,
|
|
2079
|
-
sha256: canonicalArtifact.sha256,
|
|
2080
|
-
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2081
|
-
};
|
|
2082
2847
|
return {
|
|
2083
2848
|
ok: true,
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2849
|
+
canonical: analysis.canonical,
|
|
2850
|
+
normalizationActions: analysis.normalizationActions,
|
|
2851
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
2087
2852
|
};
|
|
2088
2853
|
}
|
|
2089
2854
|
catch (error) {
|
|
2090
|
-
const
|
|
2091
|
-
return
|
|
2855
|
+
const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
|
|
2856
|
+
return {
|
|
2857
|
+
ok: false,
|
|
2858
|
+
reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
|
|
2859
|
+
failureCode: "schema-drift",
|
|
2860
|
+
...(failureKind ? { failureKind } : {}),
|
|
2861
|
+
};
|
|
2092
2862
|
}
|
|
2093
2863
|
}
|
|
2094
2864
|
export async function materializeFrontendImplementationContract(input) {
|