@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.30
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 +267 -65
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- 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 +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +118 -14
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +8 -1
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +533 -29
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +181 -27
- 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 +22 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +478 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +244 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +12 -5
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +83 -10
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-CiGOV5eG.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-Ds9Gyfd8.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-C6V6uC98.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DezF7Ol9.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BCWk3gCT.js} +1 -1
- package/dist/worker/console/static/assets/channel-DMvu1nmi.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DbYr-Cxy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-XSlusW4G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-BtcKAj9i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BF57lhXj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-DJIlR25o.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-DklNc7dm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-fZNtEdQZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-BBgLqV4h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-Dl9e5n_J.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-B0JK51NT.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-Ch-ajv4O.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-xn_NxcfB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DRD0fyga.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-COtnKp4B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BQp_CAqG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-x0y-vNgq.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-N2ezwL31.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-CSaxRH6Q.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-6Ro7Kpbd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CrIIQEAI.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-DivKcq8g.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BXrfDo8y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-LTTQmMRJ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DP3o_5qd.js} +1 -1
- package/dist/worker/console/static/assets/index-C2jmYBfu.js +404 -0
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-wVfSY1WH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BRKfFL0I.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DEml3jLb.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-DTzBmOqN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-Cpy1d873.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-C1cSavOy.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-BEBSKvYM.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BrLs4Oac.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-B47AZcLa.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-s0dVeYR2.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-lW8aJ7Q6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-BcHJPf6Q.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DdegpUUf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-BxFWqNv8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-cOdUDPbR.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-DpxlYl8P.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DQOfurE0.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-CIp3tC6V.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-C-adIPKn.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CiYrar9M.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-WHrdTV0-.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- 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/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
- 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 +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +805 -113
- package/dist/workflows/dag/node-execution.js +194 -6
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +224 -27
- package/dist/workflows/dag/rerun-run.js +61 -6
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +113 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +157 -1
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- 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 -1
- package/package.json +12 -4
- 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/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +9 -6
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
|
@@ -8,11 +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 { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, openspecCitationsRel, parseOpenspecCitationsBlock, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
11
12
|
export const frontendNormalizationActionSchema = z.enum([
|
|
12
13
|
"remove-trailing-commas",
|
|
13
14
|
"strip-comments",
|
|
14
15
|
"canonicalize-requirement-ids",
|
|
15
16
|
"canonicalize-verification-alias",
|
|
17
|
+
"drop-unresolved-verification-symbols",
|
|
16
18
|
"inject-source-binding",
|
|
17
19
|
]);
|
|
18
20
|
export function sha256Hex(input) {
|
|
@@ -102,6 +104,21 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
102
104
|
return [...labels];
|
|
103
105
|
}
|
|
104
106
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
107
|
+
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
108
|
+
/**
|
|
109
|
+
* Build the deterministic, run-owned portion of the frontend contract. The
|
|
110
|
+
* planner is allowed to fill semantic fields with an RFC 7386 merge patch but
|
|
111
|
+
* cannot redefine task identity, risk, or the implementation write boundary.
|
|
112
|
+
*/
|
|
113
|
+
export function buildFrontendImplementationContractSkeleton(input) {
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: 1,
|
|
116
|
+
sourceBinding: canonicalFrontendContractSourceBinding(input.sourceBinding),
|
|
117
|
+
riskLevel: input.riskLevel,
|
|
118
|
+
targets: { files: [...input.targetFiles] },
|
|
119
|
+
mockApi: { productionDefaultOff: true },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
105
122
|
/**
|
|
106
123
|
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
107
124
|
* installed loop-agent package docs/templates/ path. Package-root discovery
|
|
@@ -191,6 +208,42 @@ const gap = z
|
|
|
191
208
|
blocking: z.boolean(),
|
|
192
209
|
})
|
|
193
210
|
.strict();
|
|
211
|
+
/**
|
|
212
|
+
* Structured component/token selection contract (ADR 0016). Every user-visible
|
|
213
|
+
* UI purpose the plan touches must be declared here so the prewrite gate can
|
|
214
|
+
* deterministically cross-check that the selection comes from the frontend
|
|
215
|
+
* specification (openspec/ai_workspace component + theme specs) instead of
|
|
216
|
+
* relying on design-review's soft model judgment.
|
|
217
|
+
*
|
|
218
|
+
* - `specified`: the frontend spec mandates this component for the purpose →
|
|
219
|
+
* `specReference` is required (path/section/line hit).
|
|
220
|
+
* - `reuse-existing`: reuse an existing repo component/convention not named by
|
|
221
|
+
* the spec → `specReference` may be null, rationale explains the basis.
|
|
222
|
+
* - `new`: neither spec nor existing code fits → `specReference` must be null,
|
|
223
|
+
* rationale must declare the deviation (design-review approves it).
|
|
224
|
+
*
|
|
225
|
+
* `specReference` is nullable + optional because `stripNullValuesDeep` strips
|
|
226
|
+
* null object values before validation, so a `new` choice's `specReference:
|
|
227
|
+
* null` becomes an absent field; the superRefine below treats absent and null
|
|
228
|
+
* identically.
|
|
229
|
+
*/
|
|
230
|
+
const uiComponentSpecReferenceSchema = z
|
|
231
|
+
.object({
|
|
232
|
+
path: safePath,
|
|
233
|
+
section: z.string(),
|
|
234
|
+
line: z.number().int().nullable().optional(),
|
|
235
|
+
})
|
|
236
|
+
.strict();
|
|
237
|
+
const uiComponentChoiceSchema = z
|
|
238
|
+
.object({
|
|
239
|
+
purpose: z.string().min(1),
|
|
240
|
+
component: z.string().min(1),
|
|
241
|
+
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
242
|
+
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
243
|
+
rationale: z.string().min(1),
|
|
244
|
+
})
|
|
245
|
+
.strict();
|
|
246
|
+
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
194
247
|
/**
|
|
195
248
|
* Normalize unstable model emissions before the strict schema sees them:
|
|
196
249
|
* recursively drop null values (object entries and array elements). A null for
|
|
@@ -316,6 +369,12 @@ export const frontendImplementationContractSchema = z
|
|
|
316
369
|
})
|
|
317
370
|
.strict()).min(1),
|
|
318
371
|
evidenceGaps: z.array(gap),
|
|
372
|
+
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
373
|
+
stylingStrategy: z.string().min(1).optional(),
|
|
374
|
+
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
375
|
+
dependencyPolicy: z.string().min(1).optional(),
|
|
376
|
+
residualRisks: z.array(z.string().min(1)).optional(),
|
|
377
|
+
realIntegrationGap: z.string().min(1).optional(),
|
|
319
378
|
})
|
|
320
379
|
.strict()
|
|
321
380
|
.superRefine((value, ctx) => {
|
|
@@ -391,6 +450,26 @@ export const frontendImplementationContractSchema = z
|
|
|
391
450
|
path: ["uiStates"],
|
|
392
451
|
});
|
|
393
452
|
}
|
|
453
|
+
for (const [index, choice] of (value.uiComponentChoices ?? []).entries()) {
|
|
454
|
+
if (choice.decision === "specified") {
|
|
455
|
+
if (!choice.specReference || !choice.specReference.path) {
|
|
456
|
+
ctx.addIssue({
|
|
457
|
+
code: "custom",
|
|
458
|
+
message: "specified component choice requires a non-empty specReference.path",
|
|
459
|
+
path: ["uiComponentChoices", index, "specReference"],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else if (choice.decision === "new") {
|
|
464
|
+
if (choice.specReference) {
|
|
465
|
+
ctx.addIssue({
|
|
466
|
+
code: "custom",
|
|
467
|
+
message: "new component choice must not carry a specReference",
|
|
468
|
+
path: ["uiComponentChoices", index, "specReference"],
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
394
473
|
if (value.mockApi.strategy !== "not-needed") {
|
|
395
474
|
if (value.mockApi.endpoints.length === 0) {
|
|
396
475
|
ctx.addIssue({
|
|
@@ -426,6 +505,47 @@ export const frontendImplementationContractSchema = z
|
|
|
426
505
|
});
|
|
427
506
|
}
|
|
428
507
|
}));
|
|
508
|
+
function isPlainObject(value) {
|
|
509
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Apply an RFC 7386 merge-patch on a target contract without mutating it.
|
|
513
|
+
* `null` removes the key; plain objects merge recursively when the target value
|
|
514
|
+
* is also a plain object; arrays and scalars replace the whole value. The
|
|
515
|
+
* result is re-validated by analyzeFrontendImplementationContract before it is
|
|
516
|
+
* ever materialized, so a patch that removes a required field still fails
|
|
517
|
+
* closed at the gate.
|
|
518
|
+
*/
|
|
519
|
+
export function applyFrontendContractMergePatch(target, patch) {
|
|
520
|
+
if (!isPlainObject(patch)) {
|
|
521
|
+
throw new Error("frontend contract patch must be a JSON object");
|
|
522
|
+
}
|
|
523
|
+
const merge = (base, delta) => {
|
|
524
|
+
if (delta === null)
|
|
525
|
+
return undefined;
|
|
526
|
+
if (!isPlainObject(delta))
|
|
527
|
+
return delta;
|
|
528
|
+
const result = isPlainObject(base)
|
|
529
|
+
? { ...base }
|
|
530
|
+
: {};
|
|
531
|
+
for (const [key, value] of Object.entries(delta)) {
|
|
532
|
+
if (value === null) {
|
|
533
|
+
delete result[key];
|
|
534
|
+
}
|
|
535
|
+
else if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
536
|
+
result[key] = merge(result[key], value);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
result[key] = value;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
};
|
|
544
|
+
return merge(target, patch);
|
|
545
|
+
}
|
|
546
|
+
export function applyFrontendImplementationContractPatch(target, patch) {
|
|
547
|
+
return applyFrontendContractMergePatch(target, patch);
|
|
548
|
+
}
|
|
429
549
|
export async function assertFrontendSourceBindingFresh(input) {
|
|
430
550
|
for (const source of input.binding.sources) {
|
|
431
551
|
const absolute = resolveDagTaskSourcePath({
|
|
@@ -648,6 +768,18 @@ function asStringArray(value) {
|
|
|
648
768
|
.map((item) => asString(item))
|
|
649
769
|
.filter((item) => item.length > 0);
|
|
650
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Pass structured component choices through the free-form compatibility
|
|
773
|
+
* branch. The strict-shape branch already spreads unknown fields, so only this
|
|
774
|
+
* explicit constructor needs to re-emit `uiComponentChoices`; a missing/empty
|
|
775
|
+
* array normalizes to undefined (absent) so the optional schema field stays
|
|
776
|
+
* compatible with legacy free-form contracts.
|
|
777
|
+
*/
|
|
778
|
+
function normalizeUiComponentChoices(value) {
|
|
779
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
780
|
+
return undefined;
|
|
781
|
+
return value;
|
|
782
|
+
}
|
|
651
783
|
function normalizeFrontendRoute(value) {
|
|
652
784
|
const route = asString(value).replaceAll("\\", "/").replace(/^\/+/, "");
|
|
653
785
|
if (!route || route === "." || route.split("/").includes(".."))
|
|
@@ -1331,6 +1463,12 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1331
1463
|
},
|
|
1332
1464
|
verificationTargets,
|
|
1333
1465
|
evidenceGaps,
|
|
1466
|
+
implementationSteps: asStringArray(record.implementationSteps),
|
|
1467
|
+
stylingStrategy: asString(record.stylingStrategy) || undefined,
|
|
1468
|
+
dependencyPolicy: asString(record.dependencyPolicy) || undefined,
|
|
1469
|
+
residualRisks: asStringArray(record.residualRisks),
|
|
1470
|
+
realIntegrationGap: asString(record.realIntegrationGap) || undefined,
|
|
1471
|
+
uiComponentChoices: normalizeUiComponentChoices(record.uiComponentChoices),
|
|
1334
1472
|
};
|
|
1335
1473
|
}
|
|
1336
1474
|
/**
|
|
@@ -1348,7 +1486,13 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1348
1486
|
rawOutput = input.rawContractText;
|
|
1349
1487
|
}
|
|
1350
1488
|
else if (input.fromNodeId !== undefined) {
|
|
1351
|
-
|
|
1489
|
+
let record;
|
|
1490
|
+
try {
|
|
1491
|
+
record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
|
|
1492
|
+
}
|
|
1493
|
+
catch (error) {
|
|
1494
|
+
throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1495
|
+
}
|
|
1352
1496
|
rawOutput = record.assistantText ?? record.stdout ?? "";
|
|
1353
1497
|
}
|
|
1354
1498
|
else {
|
|
@@ -1490,6 +1634,243 @@ export async function writeFrontendImplementationContractArtifact(input) {
|
|
|
1490
1634
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1491
1635
|
};
|
|
1492
1636
|
}
|
|
1637
|
+
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
1638
|
+
"/schemaVersion",
|
|
1639
|
+
"/sourceBinding",
|
|
1640
|
+
"/riskLevel",
|
|
1641
|
+
"/targets/files",
|
|
1642
|
+
"/mockApi/productionDefaultOff",
|
|
1643
|
+
];
|
|
1644
|
+
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1645
|
+
const record = asRecord(patch);
|
|
1646
|
+
if (!record)
|
|
1647
|
+
return ["/"];
|
|
1648
|
+
const violations = [];
|
|
1649
|
+
for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
|
|
1650
|
+
if (Object.hasOwn(record, key))
|
|
1651
|
+
violations.push(`/${key}`);
|
|
1652
|
+
}
|
|
1653
|
+
const targets = asRecord(record.targets);
|
|
1654
|
+
if (targets && Object.hasOwn(targets, "files"))
|
|
1655
|
+
violations.push("/targets/files");
|
|
1656
|
+
const mockApi = asRecord(record.mockApi);
|
|
1657
|
+
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
1658
|
+
violations.push("/mockApi/productionDefaultOff");
|
|
1659
|
+
return violations;
|
|
1660
|
+
}
|
|
1661
|
+
function workspaceRootFromDagRunDir(runDir) {
|
|
1662
|
+
const absolute = path.resolve(runDir);
|
|
1663
|
+
const segments = absolute.split(path.sep);
|
|
1664
|
+
const harnessIndex = segments.lastIndexOf(".harness");
|
|
1665
|
+
if (harnessIndex < 0 ||
|
|
1666
|
+
segments[harnessIndex + 1] !== "dag-runs" ||
|
|
1667
|
+
harnessIndex + 3 >= segments.length) {
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1671
|
+
return joined || path.parse(absolute).root;
|
|
1672
|
+
}
|
|
1673
|
+
function verificationSymbolMatchesFile(symbol, content) {
|
|
1674
|
+
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1675
|
+
return content.includes("describe(");
|
|
1676
|
+
const symbols = symbol.includes(" / ")
|
|
1677
|
+
? symbol
|
|
1678
|
+
.split(/[((]/, 1)[0]
|
|
1679
|
+
.split("/")
|
|
1680
|
+
.map((item) => item.trim())
|
|
1681
|
+
.filter(Boolean)
|
|
1682
|
+
: [symbol];
|
|
1683
|
+
return symbols.every((item) => {
|
|
1684
|
+
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1685
|
+
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1686
|
+
.filter((candidate) => Boolean(candidate))
|
|
1687
|
+
.some((candidate) => content.includes(candidate));
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
async function dropUnresolvedVerificationSymbols(input) {
|
|
1691
|
+
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1692
|
+
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1693
|
+
if (!workspaceRoot || !Array.isArray(targets))
|
|
1694
|
+
return false;
|
|
1695
|
+
let changed = false;
|
|
1696
|
+
for (const targetValue of targets) {
|
|
1697
|
+
const target = asRecord(targetValue);
|
|
1698
|
+
const file = asString(target?.file);
|
|
1699
|
+
const symbol = asString(target?.symbol);
|
|
1700
|
+
if (!target || !file || !symbol)
|
|
1701
|
+
continue;
|
|
1702
|
+
const absolute = path.resolve(workspaceRoot, file);
|
|
1703
|
+
const relative = path.relative(workspaceRoot, absolute);
|
|
1704
|
+
if (relative.startsWith("..") ||
|
|
1705
|
+
path.isAbsolute(relative) ||
|
|
1706
|
+
relative.includes("..")) {
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
let content;
|
|
1710
|
+
try {
|
|
1711
|
+
content = await readFile(absolute, "utf8");
|
|
1712
|
+
}
|
|
1713
|
+
catch {
|
|
1714
|
+
// Missing files remain a trace-time hard failure; do not hide them by
|
|
1715
|
+
// changing the optional symbol during candidate normalization.
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
if (verificationSymbolMatchesFile(symbol, content))
|
|
1719
|
+
continue;
|
|
1720
|
+
delete target.symbol;
|
|
1721
|
+
changed = true;
|
|
1722
|
+
}
|
|
1723
|
+
return changed;
|
|
1724
|
+
}
|
|
1725
|
+
function extractSingleOpenspecCitationsBlock(text) {
|
|
1726
|
+
// Line-anchored so a directly closed empty fence (no candidate rows) still
|
|
1727
|
+
// counts as the one required block instead of "found 0" retry loops.
|
|
1728
|
+
const blocks = [
|
|
1729
|
+
...text.matchAll(/^```openspec-citations[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim),
|
|
1730
|
+
].map((match) => match[0]);
|
|
1731
|
+
if (blocks.length !== 1) {
|
|
1732
|
+
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1733
|
+
}
|
|
1734
|
+
return blocks[0];
|
|
1735
|
+
}
|
|
1736
|
+
async function writeFrontendPlanCandidateRaw(input) {
|
|
1737
|
+
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1738
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
|
|
1739
|
+
return relativePath;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1743
|
+
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1744
|
+
* format/schema failures can be inspected and replayed without trusting model
|
|
1745
|
+
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
1746
|
+
* downstream review and prewrite nodes.
|
|
1747
|
+
*/
|
|
1748
|
+
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1749
|
+
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1750
|
+
const attempt = input.attempt ?? 1;
|
|
1751
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1752
|
+
const rawPath = await writeFrontendPlanCandidateRaw({
|
|
1753
|
+
runDir: input.runDir,
|
|
1754
|
+
nodeId,
|
|
1755
|
+
attempt,
|
|
1756
|
+
text: input.text,
|
|
1757
|
+
});
|
|
1758
|
+
const reportBase = {
|
|
1759
|
+
schemaVersion: 1,
|
|
1760
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
|
|
1761
|
+
nodeId,
|
|
1762
|
+
attempt,
|
|
1763
|
+
rawPath,
|
|
1764
|
+
protectedPaths: [...FRONTEND_PLAN_PATCH_PROTECTED_PATHS],
|
|
1765
|
+
candidateRawSha256: sha256Hex(input.text),
|
|
1766
|
+
};
|
|
1767
|
+
let extractedPatchPath;
|
|
1768
|
+
let extractedPatchSha256;
|
|
1769
|
+
const writeReport = async (value) => {
|
|
1770
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
1771
|
+
...reportBase,
|
|
1772
|
+
...(extractedPatchPath
|
|
1773
|
+
? { extractedPatchPath, extractedPatchSha256 }
|
|
1774
|
+
: {}),
|
|
1775
|
+
...value,
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
try {
|
|
1779
|
+
if (!input.sourceBinding)
|
|
1780
|
+
throw new Error("frontend plan patch validator requires DAG sourceBinding");
|
|
1781
|
+
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1782
|
+
if (!skeleton)
|
|
1783
|
+
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1784
|
+
const patch = extractFrontendImplementationJson(input.text);
|
|
1785
|
+
if (!isPlainObject(patch))
|
|
1786
|
+
throw new Error("frontend plan patch must extract to one JSON object");
|
|
1787
|
+
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1788
|
+
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1789
|
+
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1790
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1791
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1792
|
+
if (protectedViolations.length > 0)
|
|
1793
|
+
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1794
|
+
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1795
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
1796
|
+
runDir: input.runDir,
|
|
1797
|
+
contract: merged,
|
|
1798
|
+
});
|
|
1799
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
1800
|
+
runDir: input.runDir,
|
|
1801
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
1802
|
+
sourceBinding: input.sourceBinding,
|
|
1803
|
+
});
|
|
1804
|
+
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1805
|
+
await writeReport({
|
|
1806
|
+
classification: "accepted-normalized",
|
|
1807
|
+
normalizedContractPath: path.posix.relative(input.runDir.replaceAll(path.sep, "/"), normalizedArtifact.path.replaceAll(path.sep, "/")),
|
|
1808
|
+
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1809
|
+
normalizationActions: [
|
|
1810
|
+
"apply-runtime-skeleton",
|
|
1811
|
+
...(droppedUnresolvedSymbols
|
|
1812
|
+
? ["drop-unresolved-verification-symbols"]
|
|
1813
|
+
: []),
|
|
1814
|
+
...analysis.normalizationActions,
|
|
1815
|
+
],
|
|
1816
|
+
errors: [],
|
|
1817
|
+
});
|
|
1818
|
+
const canonicalRel = canonicalContractRel(nodeId);
|
|
1819
|
+
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
|
|
1820
|
+
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
1821
|
+
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
1822
|
+
citations: parsedCitations.citations,
|
|
1823
|
+
block: citationsBlock,
|
|
1824
|
+
});
|
|
1825
|
+
await persistRuntimeSkeleton({
|
|
1826
|
+
runDir: input.runDir,
|
|
1827
|
+
nodeId,
|
|
1828
|
+
skeleton,
|
|
1829
|
+
});
|
|
1830
|
+
const artifact = {
|
|
1831
|
+
path: canonicalArtifact.path,
|
|
1832
|
+
sha256: canonicalArtifact.sha256,
|
|
1833
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1834
|
+
};
|
|
1835
|
+
return {
|
|
1836
|
+
ok: true,
|
|
1837
|
+
contract: analysis.canonical,
|
|
1838
|
+
artifact,
|
|
1839
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
catch (error) {
|
|
1843
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1844
|
+
const classified = classifyStructuredContractFailure({
|
|
1845
|
+
reason,
|
|
1846
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
1847
|
+
});
|
|
1848
|
+
await writeReport({
|
|
1849
|
+
classification: classified.classification,
|
|
1850
|
+
normalizationActions: [],
|
|
1851
|
+
errors: classified.errors,
|
|
1852
|
+
});
|
|
1853
|
+
return {
|
|
1854
|
+
ok: false,
|
|
1855
|
+
reason,
|
|
1856
|
+
classification: classified.classification,
|
|
1857
|
+
errors: classified.errors,
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
export async function readFrontendCanonicalCandidate(runDir, nodeId) {
|
|
1862
|
+
const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
|
|
1863
|
+
try {
|
|
1864
|
+
const raw = await readFile(candidatePath, "utf8");
|
|
1865
|
+
const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
|
|
1866
|
+
return parsed.success ? parsed.data : undefined;
|
|
1867
|
+
}
|
|
1868
|
+
catch (error) {
|
|
1869
|
+
if (error.code === "ENOENT")
|
|
1870
|
+
return undefined;
|
|
1871
|
+
return undefined;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1493
1874
|
/**
|
|
1494
1875
|
* Node-output self-check for plan nodes that produce the implementation
|
|
1495
1876
|
* contract. Mirrors the prewrite gate validation so schema/typo/null violations
|
|
@@ -1508,12 +1889,122 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1508
1889
|
return { ok: true, contract: analysis.canonical };
|
|
1509
1890
|
}
|
|
1510
1891
|
catch (error) {
|
|
1892
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1893
|
+
const classified = classifyStructuredContractFailure({
|
|
1894
|
+
reason,
|
|
1895
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
1896
|
+
});
|
|
1511
1897
|
return {
|
|
1512
1898
|
ok: false,
|
|
1513
|
-
reason
|
|
1899
|
+
reason,
|
|
1900
|
+
classification: classified.classification,
|
|
1901
|
+
errors: classified.errors,
|
|
1514
1902
|
};
|
|
1515
1903
|
}
|
|
1516
1904
|
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Node-output self-check for the plan revision node. Format-level checks still
|
|
1907
|
+
* require one openspec-citations fence and one JSON object. When the original
|
|
1908
|
+
* canonical contract and sourceBinding are present, this node is the merge
|
|
1909
|
+
* authority: it applies the RFC 7386 patch, validates the full contract, and
|
|
1910
|
+
* writes the revision node's own hash-bound canonical artifact.
|
|
1911
|
+
*/
|
|
1912
|
+
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1913
|
+
const fail = (reason) => {
|
|
1914
|
+
const classified = classifyStructuredContractFailure({ reason });
|
|
1915
|
+
return {
|
|
1916
|
+
ok: false,
|
|
1917
|
+
reason,
|
|
1918
|
+
classification: classified.classification,
|
|
1919
|
+
errors: classified.errors,
|
|
1920
|
+
};
|
|
1921
|
+
};
|
|
1922
|
+
const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
|
|
1923
|
+
const attempt = input.attempt ?? 1;
|
|
1924
|
+
await writeFrontendPlanCandidateRaw({
|
|
1925
|
+
runDir: input.runDir,
|
|
1926
|
+
nodeId,
|
|
1927
|
+
attempt,
|
|
1928
|
+
text: input.text,
|
|
1929
|
+
});
|
|
1930
|
+
let citationsBlock;
|
|
1931
|
+
try {
|
|
1932
|
+
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1933
|
+
}
|
|
1934
|
+
catch (error) {
|
|
1935
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
1936
|
+
}
|
|
1937
|
+
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
1938
|
+
let patch;
|
|
1939
|
+
try {
|
|
1940
|
+
patch = extractFrontendImplementationJson(textWithoutCitations);
|
|
1941
|
+
}
|
|
1942
|
+
catch (error) {
|
|
1943
|
+
return fail(`revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`);
|
|
1944
|
+
}
|
|
1945
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
1946
|
+
return fail("revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)");
|
|
1947
|
+
}
|
|
1948
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1949
|
+
if (protectedViolations.length > 0) {
|
|
1950
|
+
return fail(`frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1951
|
+
}
|
|
1952
|
+
const original = (await readFrontendCanonicalCandidate(input.runDir, "frontend-plan-pi")) ??
|
|
1953
|
+
(await readFrontendCanonicalCandidate(input.runDir, nodeId));
|
|
1954
|
+
if (!input.sourceBinding) {
|
|
1955
|
+
return { ok: true, contract: patch };
|
|
1956
|
+
}
|
|
1957
|
+
if (!original) {
|
|
1958
|
+
return fail("frontend revision patch requires the original frontend-plan-pi canonical contract");
|
|
1959
|
+
}
|
|
1960
|
+
try {
|
|
1961
|
+
const merged = applyFrontendContractMergePatch(original, patch);
|
|
1962
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
1963
|
+
runDir: input.runDir,
|
|
1964
|
+
contract: merged,
|
|
1965
|
+
});
|
|
1966
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
1967
|
+
runDir: input.runDir,
|
|
1968
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
1969
|
+
sourceBinding: input.sourceBinding,
|
|
1970
|
+
});
|
|
1971
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1972
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`), patch);
|
|
1973
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1974
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
1975
|
+
classification: "accepted-normalized",
|
|
1976
|
+
normalizationActions: [
|
|
1977
|
+
"apply-original-canonical",
|
|
1978
|
+
...(droppedUnresolvedSymbols
|
|
1979
|
+
? ["drop-unresolved-verification-symbols"]
|
|
1980
|
+
: []),
|
|
1981
|
+
...analysis.normalizationActions,
|
|
1982
|
+
],
|
|
1983
|
+
errors: [],
|
|
1984
|
+
});
|
|
1985
|
+
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalContractRel(nodeId), analysis.canonical);
|
|
1986
|
+
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
1987
|
+
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
1988
|
+
citations: parsedCitations.citations,
|
|
1989
|
+
block: citationsBlock,
|
|
1990
|
+
});
|
|
1991
|
+
const artifact = {
|
|
1992
|
+
path: canonicalArtifact.path,
|
|
1993
|
+
sha256: canonicalArtifact.sha256,
|
|
1994
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1995
|
+
};
|
|
1996
|
+
return {
|
|
1997
|
+
ok: true,
|
|
1998
|
+
contract: analysis.canonical,
|
|
1999
|
+
artifact,
|
|
2000
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
catch (error) {
|
|
2004
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2005
|
+
return fail(reason);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
1517
2008
|
export async function materializeFrontendImplementationContract(input) {
|
|
1518
2009
|
const analysis = await analyzeFrontendImplementationContract(input);
|
|
1519
2010
|
return writeFrontendImplementationContractArtifact({
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Markdown renderer for a canonical frontend implementation
|
|
3
|
+
* contract. The prewrite gate materializes this as `contracts/frontend-plan.md`
|
|
4
|
+
* next to the contract JSON, so plan prose is never produced by the model — it
|
|
5
|
+
* is derived from the single machine contract at materialization time.
|
|
6
|
+
*
|
|
7
|
+
* The function is pure: the same contract always renders byte-identical
|
|
8
|
+
* Markdown, and absent optional fields render as placeholders without throwing.
|
|
9
|
+
*/
|
|
10
|
+
function bulletList(items) {
|
|
11
|
+
if (!items || items.length === 0)
|
|
12
|
+
return "";
|
|
13
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
14
|
+
}
|
|
15
|
+
function requirementsCoverage(contract) {
|
|
16
|
+
const lines = contract.requirements.map((requirement) => `- ${requirement.id}: ${requirement.expectedOutcome}`);
|
|
17
|
+
return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
|
|
18
|
+
}
|
|
19
|
+
function uiStateHandling(contract) {
|
|
20
|
+
const lines = contract.uiStates.map((state) => {
|
|
21
|
+
if (state.applicable) {
|
|
22
|
+
return `- ${state.name}: ${state.expectedBehavior ?? ""}`;
|
|
23
|
+
}
|
|
24
|
+
return `- ${state.name}: not applicable — ${state.notApplicableReason ?? ""}`;
|
|
25
|
+
});
|
|
26
|
+
return lines.length > 0 ? lines.join("\n") : "_(none)_";
|
|
27
|
+
}
|
|
28
|
+
function interactionNotes(contract) {
|
|
29
|
+
const lines = contract.interactions.map((interaction) => `- ${interaction.name}: ${interaction.trigger} → ${interaction.expectedBehavior}`);
|
|
30
|
+
return lines.length > 0 ? lines.join("\n") : "_(none)_";
|
|
31
|
+
}
|
|
32
|
+
function verificationPlan(contract) {
|
|
33
|
+
const lines = contract.verificationTargets.map((target) => `- ${target.id}: ${target.commandLabel} (${target.file})`);
|
|
34
|
+
return lines.length > 0 ? lines.join("\n") : "_(none)_";
|
|
35
|
+
}
|
|
36
|
+
function componentSelection(contract) {
|
|
37
|
+
const choices = contract.uiComponentChoices ?? [];
|
|
38
|
+
if (choices.length === 0)
|
|
39
|
+
return "_(not specified)_";
|
|
40
|
+
return choices
|
|
41
|
+
.map((choice) => {
|
|
42
|
+
const specReference = choice.specReference;
|
|
43
|
+
const refText = specReference
|
|
44
|
+
? ` ${specReference.path}${specReference.section ? `#${specReference.section}` : ""}${specReference.line != null ? `:${specReference.line}` : ""}`
|
|
45
|
+
: "";
|
|
46
|
+
return `- ${choice.purpose} → ${choice.component} (${choice.decision})${refText ? `:${refText}` : ""} — ${choice.rationale}`;
|
|
47
|
+
})
|
|
48
|
+
.join("\n");
|
|
49
|
+
}
|
|
50
|
+
export function renderFrontendPlanMarkdown(contract) {
|
|
51
|
+
return [
|
|
52
|
+
"# Frontend Implementation Plan",
|
|
53
|
+
"",
|
|
54
|
+
"## Requirement Coverage",
|
|
55
|
+
requirementsCoverage(contract),
|
|
56
|
+
"",
|
|
57
|
+
"## Implementation Steps",
|
|
58
|
+
bulletList(contract.implementationSteps) || "_(not specified)_",
|
|
59
|
+
"",
|
|
60
|
+
"## Target Files",
|
|
61
|
+
bulletList(contract.targets.files) || "_(none)_",
|
|
62
|
+
"",
|
|
63
|
+
"## UI State Handling",
|
|
64
|
+
uiStateHandling(contract),
|
|
65
|
+
"",
|
|
66
|
+
"## Styling & Component Strategy",
|
|
67
|
+
contract.stylingStrategy || "_(not specified)_",
|
|
68
|
+
"",
|
|
69
|
+
"## Component Selection (spec conformance)",
|
|
70
|
+
componentSelection(contract),
|
|
71
|
+
"",
|
|
72
|
+
"## Interaction Notes",
|
|
73
|
+
interactionNotes(contract),
|
|
74
|
+
"",
|
|
75
|
+
"## Mock & API Strategy",
|
|
76
|
+
contract.mockApi.strategy,
|
|
77
|
+
"",
|
|
78
|
+
"## Dependency Policy",
|
|
79
|
+
contract.dependencyPolicy || "_(not specified)_",
|
|
80
|
+
"",
|
|
81
|
+
"## Verification Plan",
|
|
82
|
+
verificationPlan(contract),
|
|
83
|
+
"",
|
|
84
|
+
"## Real Integration Gap",
|
|
85
|
+
contract.realIntegrationGap || "_(not specified)_",
|
|
86
|
+
"",
|
|
87
|
+
"## Residual Risks",
|
|
88
|
+
bulletList(contract.residualRisks) || "_(none)_",
|
|
89
|
+
].join("\n") + "\n";
|
|
90
|
+
}
|