@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.4
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 +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1691 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1152 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +60 -17
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- 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 +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.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/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +366 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -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 +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -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 +425 -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-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -8,6 +8,13 @@ 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, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
16
|
+
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
17
|
+
import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
|
|
11
18
|
export const frontendNormalizationActionSchema = z.enum([
|
|
12
19
|
"remove-trailing-commas",
|
|
13
20
|
"strip-comments",
|
|
@@ -102,6 +109,39 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
102
109
|
}
|
|
103
110
|
return [...labels];
|
|
104
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Extract the design-policy allowedDependencies set from the DAG run spec
|
|
114
|
+
* (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
|
|
115
|
+
* node's front-loaded policy pre-check so dependency declarations can be
|
|
116
|
+
* corrected through the retry ladder before the policy shell (the authority)
|
|
117
|
+
* re-runs the same check on committed facts.
|
|
118
|
+
*/
|
|
119
|
+
async function deriveAllowedDependenciesFromRun(runDir) {
|
|
120
|
+
const specPath = path.join(runDir, "run.json");
|
|
121
|
+
let raw;
|
|
122
|
+
try {
|
|
123
|
+
raw = await readFile(specPath, "utf8");
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
let spec;
|
|
129
|
+
try {
|
|
130
|
+
spec = JSON.parse(raw);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const deps = new Set();
|
|
136
|
+
for (const task of spec.tasks ?? []) {
|
|
137
|
+
const allowed = task.shell?.frontendDesignPolicy?.allowedDependencies;
|
|
138
|
+
if (allowed) {
|
|
139
|
+
for (const dep of allowed)
|
|
140
|
+
deps.add(dep);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [...deps];
|
|
144
|
+
}
|
|
105
145
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
106
146
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
107
147
|
/**
|
|
@@ -1079,6 +1119,40 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
1079
1119
|
asRecord(record.mockApi) !== null &&
|
|
1080
1120
|
asRecord(record.designEvidence) !== null);
|
|
1081
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Single shared completion layer for required-with-fallback contract fields.
|
|
1124
|
+
*
|
|
1125
|
+
* Structural invariant: the coerce normalizer has strict-shape and free-form
|
|
1126
|
+
* branches that reshape differently, but every required key that carries a
|
|
1127
|
+
* deterministic fallback (currently `designEvidence`) must be completed in
|
|
1128
|
+
* exactly ONE place — here — so the branches can never drift again (the
|
|
1129
|
+
* designEvidence Required and invented-uiStates failures both came from
|
|
1130
|
+
* branch-local rules diverging). This layer never invents values that later
|
|
1131
|
+
* refinements reject: it only fills documented fallbacks for keys already
|
|
1132
|
+
* present-but-partial, and is idempotent for already-complete inputs.
|
|
1133
|
+
*/
|
|
1134
|
+
export function completeCanonicalRequiredFields(value) {
|
|
1135
|
+
const record = asRecord(value);
|
|
1136
|
+
if (!record)
|
|
1137
|
+
return value;
|
|
1138
|
+
const designEvidence = asRecord(record.designEvidence);
|
|
1139
|
+
if (!designEvidence)
|
|
1140
|
+
return value;
|
|
1141
|
+
const source = asString(designEvidence.source);
|
|
1142
|
+
const paths = Array.isArray(designEvidence.paths);
|
|
1143
|
+
const conflicts = Array.isArray(designEvidence.conflicts);
|
|
1144
|
+
if (source && paths && conflicts)
|
|
1145
|
+
return value;
|
|
1146
|
+
return {
|
|
1147
|
+
...record,
|
|
1148
|
+
designEvidence: {
|
|
1149
|
+
...designEvidence,
|
|
1150
|
+
...(source ? {} : { source: "repository-fallback+task-source" }),
|
|
1151
|
+
...(paths ? {} : { paths: [] }),
|
|
1152
|
+
...(conflicts ? {} : { conflicts: [] }),
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1082
1156
|
/**
|
|
1083
1157
|
* Coerce common free-form plan JSON into frontend-implementation-contract-v1.
|
|
1084
1158
|
* Near-schema payloads are left untouched so unknown-key fail-closed still holds.
|
|
@@ -1183,6 +1257,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1183
1257
|
...strictRecord,
|
|
1184
1258
|
requirements,
|
|
1185
1259
|
...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
|
|
1260
|
+
// Required-with-fallback keys (designEvidence etc.) are completed
|
|
1261
|
+
// by the single shared `completeCanonicalRequiredFields` layer in
|
|
1262
|
+
// the analyze pipeline — never per-branch.
|
|
1186
1263
|
};
|
|
1187
1264
|
}
|
|
1188
1265
|
}
|
|
@@ -1231,9 +1308,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1231
1308
|
: typeRaw.includes("type")
|
|
1232
1309
|
? "static"
|
|
1233
1310
|
: "unit";
|
|
1234
|
-
const commandLabel = asString(vt.commandLabel) ||
|
|
1235
|
-
asString(vt.command) ||
|
|
1236
|
-
(type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
|
|
1311
|
+
const commandLabel = asString(vt.commandLabel) || asString(vt.command);
|
|
1237
1312
|
const file = asString(vt.file) ||
|
|
1238
1313
|
(Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
|
|
1239
1314
|
targetFiles.find((p) => p.includes("__tests__")) ||
|
|
@@ -1249,7 +1324,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1249
1324
|
requirementIds: requirementIds.length > 0
|
|
1250
1325
|
? requirementIds
|
|
1251
1326
|
: [...canonicalBinding.requirementIds],
|
|
1252
|
-
|
|
1327
|
+
// Never invent UI state references: "success" is not a canonical
|
|
1328
|
+
// state name, so a defaulted ["success","error"] was always rejected
|
|
1329
|
+
// by the schema's unknown-uiState refinement — a verification
|
|
1330
|
+
// target that declares no UI binding stays unbound ([]).
|
|
1331
|
+
uiStates: uiStateNames,
|
|
1253
1332
|
});
|
|
1254
1333
|
}
|
|
1255
1334
|
if (verificationTargets.length === 0) {
|
|
@@ -1485,7 +1564,13 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1485
1564
|
rawOutput = input.rawContractText;
|
|
1486
1565
|
}
|
|
1487
1566
|
else if (input.fromNodeId !== undefined) {
|
|
1488
|
-
|
|
1567
|
+
let record;
|
|
1568
|
+
try {
|
|
1569
|
+
record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
|
|
1570
|
+
}
|
|
1571
|
+
catch (error) {
|
|
1572
|
+
throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1573
|
+
}
|
|
1489
1574
|
rawOutput = record.assistantText ?? record.stdout ?? "";
|
|
1490
1575
|
}
|
|
1491
1576
|
else {
|
|
@@ -1593,8 +1678,9 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1593
1678
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
1594
1679
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
1595
1680
|
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
1681
|
+
const completedContract = completeCanonicalRequiredFields(normalizedContract);
|
|
1596
1682
|
const candidate = deriveFrontendVerificationCoverage({
|
|
1597
|
-
...(asRecord(
|
|
1683
|
+
...(asRecord(completedContract) ?? parsed),
|
|
1598
1684
|
sourceBinding: canonicalBinding,
|
|
1599
1685
|
}, canonicalBinding);
|
|
1600
1686
|
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
@@ -1634,6 +1720,37 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
1634
1720
|
"/targets/files",
|
|
1635
1721
|
"/mockApi/productionDefaultOff",
|
|
1636
1722
|
];
|
|
1723
|
+
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
1724
|
+
"target-surface",
|
|
1725
|
+
"component-choice",
|
|
1726
|
+
"state-flow",
|
|
1727
|
+
"data-flow",
|
|
1728
|
+
"mock-api",
|
|
1729
|
+
"design-deviation",
|
|
1730
|
+
"dependency",
|
|
1731
|
+
];
|
|
1732
|
+
/**
|
|
1733
|
+
* Restore the editable RFC 7386 plan patch from a committed plan ledger.
|
|
1734
|
+
* This is the single compile authority for the frontend implementation
|
|
1735
|
+
* contract: the tool payload (`patch` on any committed plan fact) wins over
|
|
1736
|
+
* any narrative JSON in the plan node text.
|
|
1737
|
+
*/
|
|
1738
|
+
export function restorePlanPatchFromCommittedFacts(records) {
|
|
1739
|
+
const planFacts = records
|
|
1740
|
+
.filter((record) => record.phase === "committed")
|
|
1741
|
+
.map((record) => record.fact)
|
|
1742
|
+
.filter((fact) => fact.origin === "plan" &&
|
|
1743
|
+
typeof fact.kind === "string" &&
|
|
1744
|
+
PLAN_LEDGER_FACT_KINDS_FOR_COMPILE.includes(fact.kind));
|
|
1745
|
+
if (planFacts.length === 0)
|
|
1746
|
+
return undefined;
|
|
1747
|
+
for (const fact of planFacts) {
|
|
1748
|
+
const patch = fact.patch;
|
|
1749
|
+
if (isPlainObject(patch))
|
|
1750
|
+
return patch;
|
|
1751
|
+
}
|
|
1752
|
+
return undefined;
|
|
1753
|
+
}
|
|
1637
1754
|
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1638
1755
|
const record = asRecord(patch);
|
|
1639
1756
|
if (!record)
|
|
@@ -1663,34 +1780,34 @@ function workspaceRootFromDagRunDir(runDir) {
|
|
|
1663
1780
|
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1664
1781
|
return joined || path.parse(absolute).root;
|
|
1665
1782
|
}
|
|
1666
|
-
function verificationSymbolMatchesFile(symbol, content) {
|
|
1667
|
-
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1668
|
-
return content.includes("describe(");
|
|
1669
|
-
const symbols = symbol.includes(" / ")
|
|
1670
|
-
? symbol
|
|
1671
|
-
.split(/[((]/, 1)[0]
|
|
1672
|
-
.split("/")
|
|
1673
|
-
.map((item) => item.trim())
|
|
1674
|
-
.filter(Boolean)
|
|
1675
|
-
: [symbol];
|
|
1676
|
-
return symbols.every((item) => {
|
|
1677
|
-
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1678
|
-
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1679
|
-
.filter((candidate) => Boolean(candidate))
|
|
1680
|
-
.some((candidate) => content.includes(candidate));
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
1783
|
async function dropUnresolvedVerificationSymbols(input) {
|
|
1684
1784
|
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1685
1785
|
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1686
1786
|
if (!workspaceRoot || !Array.isArray(targets))
|
|
1687
|
-
return
|
|
1688
|
-
|
|
1787
|
+
return [];
|
|
1788
|
+
const actions = [];
|
|
1689
1789
|
for (const targetValue of targets) {
|
|
1690
1790
|
const target = asRecord(targetValue);
|
|
1691
1791
|
const file = asString(target?.file);
|
|
1692
1792
|
const symbol = asString(target?.symbol);
|
|
1693
|
-
if (!target || !file
|
|
1793
|
+
if (!target || !file)
|
|
1794
|
+
continue;
|
|
1795
|
+
// Static targets (project-wide commands such as typecheck/build) verify
|
|
1796
|
+
// by command exit code, never by a per-file symbol. Strip any symbol
|
|
1797
|
+
// the planner attached — including fabricated placeholders like "null"
|
|
1798
|
+
// — so the plan shape check and the verify trace gate never evaluate
|
|
1799
|
+
// it. This is the root fix for the recurring fabricated-symbol error
|
|
1800
|
+
// class: the field was allowed for static, the prompt did not forbid
|
|
1801
|
+
// it, and models habitually fill it; the compiler removes it instead.
|
|
1802
|
+
if (target.type === "static") {
|
|
1803
|
+
if (symbol) {
|
|
1804
|
+
delete target.symbol;
|
|
1805
|
+
if (!actions.includes("strip-static-verification-symbols"))
|
|
1806
|
+
actions.push("strip-static-verification-symbols");
|
|
1807
|
+
}
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (!symbol)
|
|
1694
1811
|
continue;
|
|
1695
1812
|
const absolute = path.resolve(workspaceRoot, file);
|
|
1696
1813
|
const relative = path.relative(workspaceRoot, absolute);
|
|
@@ -1699,6 +1816,10 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1699
1816
|
relative.includes("..")) {
|
|
1700
1817
|
continue;
|
|
1701
1818
|
}
|
|
1819
|
+
// Configuration files (tsconfig.json, vite.config.*, ...) do not expose
|
|
1820
|
+
// source symbols; the trace gate skips them, so the drop must too.
|
|
1821
|
+
if (isConfigurationVerificationFile(file))
|
|
1822
|
+
continue;
|
|
1702
1823
|
let content;
|
|
1703
1824
|
try {
|
|
1704
1825
|
content = await readFile(absolute, "utf8");
|
|
@@ -1708,65 +1829,78 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1708
1829
|
// changing the optional symbol during candidate normalization.
|
|
1709
1830
|
continue;
|
|
1710
1831
|
}
|
|
1711
|
-
if (
|
|
1832
|
+
if (verificationSymbolMatchesContent(symbol, content))
|
|
1712
1833
|
continue;
|
|
1713
1834
|
delete target.symbol;
|
|
1714
|
-
|
|
1835
|
+
if (!actions.includes("drop-unresolved-verification-symbols"))
|
|
1836
|
+
actions.push("drop-unresolved-verification-symbols");
|
|
1715
1837
|
}
|
|
1716
|
-
return
|
|
1717
|
-
}
|
|
1718
|
-
function extractSingleOpenspecCitationsBlock(text, citationsRequired = true) {
|
|
1719
|
-
const blocks = [...text.matchAll(/^```openspec-citations[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim)].map((match) => match[0]);
|
|
1720
|
-
if (!citationsRequired) {
|
|
1721
|
-
if (blocks.length === 0)
|
|
1722
|
-
return "";
|
|
1723
|
-
throw new Error("frontend plan output must not include an openspec-citations block when the generation-frozen OpenSpec candidate set is empty");
|
|
1724
|
-
}
|
|
1725
|
-
if (blocks.length !== 1) {
|
|
1726
|
-
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1727
|
-
}
|
|
1728
|
-
return blocks[0];
|
|
1729
|
-
}
|
|
1730
|
-
/**
|
|
1731
|
-
* Frontend plan/revision output has two JSON-bearing regions: the primary
|
|
1732
|
-
* payload and line-oriented OpenSpec citations. Keep their parsing domains
|
|
1733
|
-
* separate so a citation row can never become a competing contract candidate.
|
|
1734
|
-
* A bare JSON payload is retained only for historical node-output compatibility.
|
|
1735
|
-
*/
|
|
1736
|
-
export function extractFrontendPlanPayload(text, options) {
|
|
1737
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(text, options?.citationsRequired !== false);
|
|
1738
|
-
return {
|
|
1739
|
-
payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
|
|
1740
|
-
citationsBlock,
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
function extractFrontendPlanPrimaryPayload(text) {
|
|
1744
|
-
const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
|
|
1745
|
-
if (fences.length > 1) {
|
|
1746
|
-
throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
|
|
1747
|
-
}
|
|
1748
|
-
if (fences.length === 1) {
|
|
1749
|
-
try {
|
|
1750
|
-
return JSON.parse(fences[0][1]);
|
|
1751
|
-
}
|
|
1752
|
-
catch (error) {
|
|
1753
|
-
throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
return extractFrontendImplementationJson(text);
|
|
1838
|
+
return actions;
|
|
1757
1839
|
}
|
|
1758
1840
|
async function writeFrontendPlanCandidateRaw(input) {
|
|
1759
1841
|
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1760
|
-
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
|
|
1842
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
|
|
1761
1843
|
return relativePath;
|
|
1762
1844
|
}
|
|
1763
1845
|
/**
|
|
1764
1846
|
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1765
1847
|
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1766
1848
|
* format/schema failures can be inspected and replayed without trusting model
|
|
1767
|
-
* prose. A successful result returns
|
|
1849
|
+
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
1768
1850
|
* downstream review and prewrite nodes.
|
|
1769
1851
|
*/
|
|
1852
|
+
/**
|
|
1853
|
+
* Placeholder tokens that are never valid verification symbols ("null" is the
|
|
1854
|
+
* classic model artifact: it serializes a JS null into the symbol string).
|
|
1855
|
+
*/
|
|
1856
|
+
const VERIFICATION_SYMBOL_PLACEHOLDERS = new Set([
|
|
1857
|
+
"null",
|
|
1858
|
+
"undefined",
|
|
1859
|
+
"none",
|
|
1860
|
+
"n/a",
|
|
1861
|
+
"na",
|
|
1862
|
+
"tbd",
|
|
1863
|
+
"todo",
|
|
1864
|
+
"...",
|
|
1865
|
+
"?",
|
|
1866
|
+
"-",
|
|
1867
|
+
]);
|
|
1868
|
+
/** An over-long symbol can never resolve to a real declaration. */
|
|
1869
|
+
const VERIFICATION_SYMBOL_MAX_CHARS = 60;
|
|
1870
|
+
/**
|
|
1871
|
+
* Deterministic shape check for a verification target symbol. Catches obvious
|
|
1872
|
+
* model fabrication (placeholder tokens, prose fragments) at plan time so the
|
|
1873
|
+
* plan retry ladder can fix the facts in-node instead of the verify trace gate
|
|
1874
|
+
* failing at the very end of the run.
|
|
1875
|
+
*
|
|
1876
|
+
* Deliberately conservative: bare test titles without punctuation ("renders
|
|
1877
|
+
* loading") and `describe(...)` / `it(...)` forms stay valid — a real symbol
|
|
1878
|
+
* may be a function name, a dotted path, or a describe/it title.
|
|
1879
|
+
*/
|
|
1880
|
+
function isSuspiciousVerificationSymbol(symbol) {
|
|
1881
|
+
const trimmed = symbol.trim();
|
|
1882
|
+
if (!trimmed)
|
|
1883
|
+
return false;
|
|
1884
|
+
if (VERIFICATION_SYMBOL_PLACEHOLDERS.has(trimmed.toLowerCase()))
|
|
1885
|
+
return true;
|
|
1886
|
+
if (trimmed.length > VERIFICATION_SYMBOL_MAX_CHARS)
|
|
1887
|
+
return true;
|
|
1888
|
+
// Deliberately conservative: bare test titles with punctuation (e.g.
|
|
1889
|
+
// "adds +1 button") are valid describe/it-style symbols and must pass;
|
|
1890
|
+
// the length + placeholder rules above already catch obvious fabrication
|
|
1891
|
+
// ("null", over-long prose fragments). Everything else is left to the
|
|
1892
|
+
// deterministic drop/trace resolvability check.
|
|
1893
|
+
return false;
|
|
1894
|
+
}
|
|
1895
|
+
function assertVerificationSymbolShapes(contract) {
|
|
1896
|
+
for (const target of contract.verificationTargets) {
|
|
1897
|
+
if (!target.symbol)
|
|
1898
|
+
continue;
|
|
1899
|
+
if (isSuspiciousVerificationSymbol(target.symbol)) {
|
|
1900
|
+
throw new Error(`frontend plan verification target "${target.id}" declares a fabricated symbol "${target.symbol}" for ${target.file}; fix it to a real symbol present in that file or leave the symbol empty (the deterministic trace gate verifies symbol resolvability after verification)`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1770
1904
|
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1771
1905
|
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1772
1906
|
const attempt = input.attempt ?? 1;
|
|
@@ -1803,22 +1937,18 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1803
1937
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1804
1938
|
if (!skeleton)
|
|
1805
1939
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
const patch = extractFrontendPlanPrimaryPayload(input.text);
|
|
1940
|
+
const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
|
|
1941
|
+
const patch = restorePlanPatchFromCommittedFacts(committed);
|
|
1809
1942
|
if (!isPlainObject(patch))
|
|
1810
|
-
throw new Error("frontend plan
|
|
1943
|
+
throw new Error("frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority");
|
|
1811
1944
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1812
1945
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1813
1946
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1814
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text, input.structuredContractOutput?.citationsRequired !== false);
|
|
1815
|
-
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1816
|
-
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1817
1947
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1818
1948
|
if (protectedViolations.length > 0)
|
|
1819
1949
|
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1820
1950
|
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1821
|
-
const
|
|
1951
|
+
const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
|
|
1822
1952
|
runDir: input.runDir,
|
|
1823
1953
|
contract: merged,
|
|
1824
1954
|
});
|
|
@@ -1827,6 +1957,23 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1827
1957
|
rawContractText: serializeDeterministicJson(merged),
|
|
1828
1958
|
sourceBinding: input.sourceBinding,
|
|
1829
1959
|
});
|
|
1960
|
+
// Front-load the verification-symbol shape check so fabricated symbols
|
|
1961
|
+
// are fixed by the plan retry ladder in-node instead of failing the
|
|
1962
|
+
// verify trace gate at the end of the run.
|
|
1963
|
+
assertVerificationSymbolShapes(analysis.canonical);
|
|
1964
|
+
// Front-load the plan-attributable design-policy checks so the §5.1
|
|
1965
|
+
// retry ladder can fix these facts in-node with diagnostics instead of
|
|
1966
|
+
// the run terminating at the policy shell. The policy shell remains the
|
|
1967
|
+
// final authority and re-runs the identical checks on committed facts.
|
|
1968
|
+
const policyPreFindings = [
|
|
1969
|
+
...checkUiDesignCoverage(analysis.canonical),
|
|
1970
|
+
...checkUiStateAttribution(analysis.canonical),
|
|
1971
|
+
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
1972
|
+
];
|
|
1973
|
+
if (policyPreFindings.length > 0)
|
|
1974
|
+
throw new Error(`frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
|
|
1975
|
+
.map((finding) => `${finding.code}: ${finding.message}`)
|
|
1976
|
+
.join("; ")}`);
|
|
1830
1977
|
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1831
1978
|
await writeReport({
|
|
1832
1979
|
classification: "accepted-normalized",
|
|
@@ -1834,47 +1981,60 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1834
1981
|
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1835
1982
|
normalizationActions: [
|
|
1836
1983
|
"apply-runtime-skeleton",
|
|
1837
|
-
...
|
|
1838
|
-
? ["drop-unresolved-verification-symbols"]
|
|
1839
|
-
: []),
|
|
1984
|
+
...symbolNormalizationActions,
|
|
1840
1985
|
...analysis.normalizationActions,
|
|
1841
1986
|
],
|
|
1842
1987
|
errors: [],
|
|
1843
1988
|
});
|
|
1989
|
+
const canonicalRel = canonicalContractRel(nodeId);
|
|
1990
|
+
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
|
|
1991
|
+
await persistRuntimeSkeleton({
|
|
1992
|
+
runDir: input.runDir,
|
|
1993
|
+
nodeId,
|
|
1994
|
+
skeleton,
|
|
1995
|
+
});
|
|
1996
|
+
const artifact = {
|
|
1997
|
+
path: canonicalArtifact.path,
|
|
1998
|
+
sha256: canonicalArtifact.sha256,
|
|
1999
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2000
|
+
};
|
|
1844
2001
|
return {
|
|
1845
2002
|
ok: true,
|
|
1846
2003
|
contract: analysis.canonical,
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
sha256: normalizedArtifact.sha256,
|
|
1850
|
-
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1851
|
-
},
|
|
1852
|
-
frontendPlanOutput: {
|
|
1853
|
-
schemaVersion: 1,
|
|
1854
|
-
payloadKind: "canonical-contract",
|
|
1855
|
-
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1856
|
-
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1857
|
-
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1858
|
-
rawCandidateSha256: sha256Hex(input.text),
|
|
1859
|
-
},
|
|
1860
|
-
normalizedText: [
|
|
1861
|
-
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1862
|
-
"",
|
|
1863
|
-
"```json",
|
|
1864
|
-
serializeDeterministicJson(analysis.canonical),
|
|
1865
|
-
"```",
|
|
1866
|
-
citationsBlock,
|
|
1867
|
-
].join("\n"),
|
|
2004
|
+
artifact,
|
|
2005
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
1868
2006
|
};
|
|
1869
2007
|
}
|
|
1870
2008
|
catch (error) {
|
|
1871
2009
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2010
|
+
const classified = classifyStructuredContractFailure({
|
|
2011
|
+
reason,
|
|
2012
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
2013
|
+
});
|
|
1872
2014
|
await writeReport({
|
|
1873
|
-
classification:
|
|
2015
|
+
classification: classified.classification,
|
|
1874
2016
|
normalizationActions: [],
|
|
1875
|
-
errors:
|
|
2017
|
+
errors: classified.errors,
|
|
1876
2018
|
});
|
|
1877
|
-
return {
|
|
2019
|
+
return {
|
|
2020
|
+
ok: false,
|
|
2021
|
+
reason,
|
|
2022
|
+
classification: classified.classification,
|
|
2023
|
+
errors: classified.errors,
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
export async function readFrontendCanonicalCandidate(runDir, nodeId) {
|
|
2028
|
+
const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
|
|
2029
|
+
try {
|
|
2030
|
+
const raw = await readFile(candidatePath, "utf8");
|
|
2031
|
+
const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
|
|
2032
|
+
return parsed.success ? parsed.data : undefined;
|
|
2033
|
+
}
|
|
2034
|
+
catch (error) {
|
|
2035
|
+
if (error.code === "ENOENT")
|
|
2036
|
+
return undefined;
|
|
2037
|
+
return undefined;
|
|
1878
2038
|
}
|
|
1879
2039
|
}
|
|
1880
2040
|
/**
|
|
@@ -1895,82 +2055,140 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1895
2055
|
return { ok: true, contract: analysis.canonical };
|
|
1896
2056
|
}
|
|
1897
2057
|
catch (error) {
|
|
2058
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2059
|
+
const classified = classifyStructuredContractFailure({
|
|
2060
|
+
reason,
|
|
2061
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
2062
|
+
});
|
|
1898
2063
|
return {
|
|
1899
2064
|
ok: false,
|
|
1900
|
-
reason
|
|
2065
|
+
reason,
|
|
2066
|
+
classification: classified.classification,
|
|
2067
|
+
errors: classified.errors,
|
|
1901
2068
|
};
|
|
1902
2069
|
}
|
|
1903
2070
|
}
|
|
1904
2071
|
/**
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
2072
|
+
* Deterministically assemble the canonical frontend implementation contract
|
|
2073
|
+
* from committed typed facts, never from plan/contract node text.
|
|
2074
|
+
*
|
|
2075
|
+
* Inputs (all read-only, all fail-closed):
|
|
2076
|
+
* - `<planNodeId>/plan-typed-facts.jsonl` (phase=committed) restores the
|
|
2077
|
+
* editable RFC 7386 patch;
|
|
2078
|
+
* - `<contractNodeId>/contract-typed-facts.jsonl` (phase=committed) must carry
|
|
2079
|
+
* a `contract-finalized` terminal fact;
|
|
2080
|
+
* - `<contractNodeId>/frontend-task-contract-vNext.json` must exist and parse.
|
|
2081
|
+
*
|
|
2082
|
+
* The patch is applied onto the runtime skeleton and re-validated through the
|
|
2083
|
+
* same `analyzeFrontendImplementationContract` pipeline used by the legacy
|
|
2084
|
+
* text path, so the downstream artifact semantics are unchanged. The function
|
|
2085
|
+
* is pure: it never writes candidate/audit artifacts and always produces the
|
|
2086
|
+
* same canonical serialization for the same committed facts. It never falls
|
|
2087
|
+
* back to parsing Plan/Contract node text.
|
|
1914
2088
|
*/
|
|
1915
|
-
export async function
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2089
|
+
export async function buildContractFromCommittedFacts(input) {
|
|
2090
|
+
const contractNodeId = input.contractNodeId ?? "frontend-contract-pi";
|
|
2091
|
+
// Skeleton guard: the runtime skeleton must carry the protected identity
|
|
2092
|
+
// fields so the merged contract can be validated against the full schema.
|
|
2093
|
+
const skeletonRecord = asRecord(input.skeleton);
|
|
2094
|
+
const skeletonTargets = asRecord(skeletonRecord?.targets);
|
|
2095
|
+
const skeletonRiskLevel = asString(skeletonRecord?.riskLevel);
|
|
2096
|
+
if (!skeletonRecord ||
|
|
2097
|
+
!["small", "standard", "high-risk"].includes(skeletonRiskLevel) ||
|
|
2098
|
+
!Array.isArray(skeletonTargets?.files) ||
|
|
2099
|
+
skeletonTargets.files.length === 0) {
|
|
2100
|
+
return {
|
|
2101
|
+
ok: false,
|
|
2102
|
+
reason: "frontend contract skeleton missing required runtime fields (riskLevel/targets.files)",
|
|
2103
|
+
failureCode: "missing-skeleton",
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
// a. Committed plan ledger restores the editable patch.
|
|
2107
|
+
const planRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.planNodeId, "plan-typed-facts.jsonl"));
|
|
2108
|
+
const patch = restorePlanPatchFromCommittedFacts(planRecords);
|
|
2109
|
+
if (!isPlainObject(patch)) {
|
|
2110
|
+
return {
|
|
2111
|
+
ok: false,
|
|
2112
|
+
reason: "frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority",
|
|
2113
|
+
failureCode: "missing-plan-ledger",
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
// b. Committed contract terminal fact (contract-finalized).
|
|
2117
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
2118
|
+
const contractKinds = new Set(contractRecords
|
|
2119
|
+
.filter((record) => record.phase === "committed")
|
|
2120
|
+
.map((record) => record.fact.kind)
|
|
2121
|
+
.filter((kind) => typeof kind === "string"));
|
|
2122
|
+
if (!contractKinds.has("contract-finalized")) {
|
|
2123
|
+
return {
|
|
2124
|
+
ok: false,
|
|
2125
|
+
reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
|
|
2126
|
+
failureCode: "missing-contract-finalize",
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
// c. frontend-task-contract-vNext.json must exist and parse (input integrity).
|
|
2130
|
+
let rawVNext;
|
|
1927
2131
|
try {
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2132
|
+
rawVNext = await readFile(path.join(input.runDir, contractNodeId, "frontend-task-contract-vNext.json"), "utf8");
|
|
2133
|
+
}
|
|
2134
|
+
catch {
|
|
2135
|
+
return {
|
|
2136
|
+
ok: false,
|
|
2137
|
+
reason: `frontend task contract vNext missing at ${contractNodeId}/frontend-task-contract-vNext.json`,
|
|
2138
|
+
failureCode: "contract-vnext-missing",
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
JSON.parse(rawVNext);
|
|
1933
2143
|
}
|
|
1934
2144
|
catch (error) {
|
|
1935
2145
|
return {
|
|
1936
2146
|
ok: false,
|
|
1937
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
2147
|
+
reason: `frontend task contract vNext is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
2148
|
+
failureCode: "contract-vnext-invalid",
|
|
1938
2149
|
};
|
|
1939
2150
|
}
|
|
1940
|
-
|
|
2151
|
+
// d. Merge the editable patch onto the protected skeleton and validate.
|
|
2152
|
+
let merged;
|
|
2153
|
+
try {
|
|
2154
|
+
merged = applyFrontendContractMergePatch(input.skeleton, patch);
|
|
2155
|
+
}
|
|
2156
|
+
catch (error) {
|
|
1941
2157
|
return {
|
|
1942
2158
|
ok: false,
|
|
1943
|
-
reason:
|
|
2159
|
+
reason: `frontend contract patch could not be applied to the runtime skeleton: ${error instanceof Error ? error.message : String(error)}`,
|
|
2160
|
+
failureCode: "schema-drift",
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
// The design-policy shell is the authoritative materializer of the
|
|
2164
|
+
// canonical contract; keep it on the same unresolved-symbol drop as the
|
|
2165
|
+
// plan validator so the verify trace gate never sees fabricated symbols.
|
|
2166
|
+
await dropUnresolvedVerificationSymbols({
|
|
2167
|
+
runDir: input.runDir,
|
|
2168
|
+
contract: merged,
|
|
2169
|
+
});
|
|
2170
|
+
try {
|
|
2171
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
2172
|
+
runDir: input.runDir,
|
|
2173
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
2174
|
+
sourceBinding: input.sourceBinding,
|
|
2175
|
+
});
|
|
2176
|
+
return {
|
|
2177
|
+
ok: true,
|
|
2178
|
+
canonical: analysis.canonical,
|
|
2179
|
+
normalizationActions: analysis.normalizationActions,
|
|
2180
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
1944
2181
|
};
|
|
1945
2182
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
2183
|
+
catch (error) {
|
|
2184
|
+
const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
|
|
1948
2185
|
return {
|
|
1949
2186
|
ok: false,
|
|
1950
|
-
reason: `frontend
|
|
2187
|
+
reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
|
|
2188
|
+
failureCode: "schema-drift",
|
|
2189
|
+
...(failureKind ? { failureKind } : {}),
|
|
1951
2190
|
};
|
|
1952
2191
|
}
|
|
1953
|
-
const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
|
|
1954
|
-
const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
|
|
1955
|
-
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1956
|
-
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1957
|
-
return {
|
|
1958
|
-
ok: true,
|
|
1959
|
-
contract: patch,
|
|
1960
|
-
structuredArtifact: {
|
|
1961
|
-
path: payloadArtifact.path,
|
|
1962
|
-
sha256: payloadArtifact.sha256,
|
|
1963
|
-
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
1964
|
-
},
|
|
1965
|
-
frontendPlanOutput: {
|
|
1966
|
-
schemaVersion: 1,
|
|
1967
|
-
payloadKind: "merge-patch",
|
|
1968
|
-
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1969
|
-
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1970
|
-
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1971
|
-
rawCandidateSha256: sha256Hex(input.text),
|
|
1972
|
-
},
|
|
1973
|
-
};
|
|
1974
2192
|
}
|
|
1975
2193
|
export async function materializeFrontendImplementationContract(input) {
|
|
1976
2194
|
const analysis = await analyzeFrontendImplementationContract(input);
|