@tea-agent/loop-agent 0.38.2 → 0.38.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 +171 -96
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- 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 +76 -10
- 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 +51 -12
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +265 -15
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- 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/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/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/operator-actions.js +116 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +36 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
- package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
- package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- 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 +76 -6
- package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
- package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- 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-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +196 -117
- package/dist/workflows/dag/node-execution.js +139 -10
- package/dist/workflows/dag/prompt.js +15 -1
- package/dist/workflows/dag/rerun-plan.js +134 -3
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +91 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +32 -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 +2 -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 +2 -2
- 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/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/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/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
|
@@ -8,6 +8,7 @@ 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",
|
|
@@ -1485,7 +1486,13 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1485
1486
|
rawOutput = input.rawContractText;
|
|
1486
1487
|
}
|
|
1487
1488
|
else if (input.fromNodeId !== undefined) {
|
|
1488
|
-
|
|
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
|
+
}
|
|
1489
1496
|
rawOutput = record.assistantText ?? record.stdout ?? "";
|
|
1490
1497
|
}
|
|
1491
1498
|
else {
|
|
@@ -1716,7 +1723,11 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1716
1723
|
return changed;
|
|
1717
1724
|
}
|
|
1718
1725
|
function extractSingleOpenspecCitationsBlock(text) {
|
|
1719
|
-
|
|
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]);
|
|
1720
1731
|
if (blocks.length !== 1) {
|
|
1721
1732
|
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1722
1733
|
}
|
|
@@ -1731,7 +1742,7 @@ async function writeFrontendPlanCandidateRaw(input) {
|
|
|
1731
1742
|
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1732
1743
|
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1733
1744
|
* format/schema failures can be inspected and replayed without trusting model
|
|
1734
|
-
* prose. A successful result returns
|
|
1745
|
+
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
1735
1746
|
* downstream review and prewrite nodes.
|
|
1736
1747
|
*/
|
|
1737
1748
|
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
@@ -1804,27 +1815,60 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1804
1815
|
],
|
|
1805
1816
|
errors: [],
|
|
1806
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
|
+
};
|
|
1807
1835
|
return {
|
|
1808
1836
|
ok: true,
|
|
1809
1837
|
contract: analysis.canonical,
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
"",
|
|
1813
|
-
"```json",
|
|
1814
|
-
serializeDeterministicJson(analysis.canonical),
|
|
1815
|
-
"```",
|
|
1816
|
-
citationsBlock,
|
|
1817
|
-
].join("\n"),
|
|
1838
|
+
artifact,
|
|
1839
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
1818
1840
|
};
|
|
1819
1841
|
}
|
|
1820
1842
|
catch (error) {
|
|
1821
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
|
+
});
|
|
1822
1848
|
await writeReport({
|
|
1823
|
-
classification:
|
|
1849
|
+
classification: classified.classification,
|
|
1824
1850
|
normalizationActions: [],
|
|
1825
|
-
errors:
|
|
1851
|
+
errors: classified.errors,
|
|
1826
1852
|
});
|
|
1827
|
-
return {
|
|
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;
|
|
1828
1872
|
}
|
|
1829
1873
|
}
|
|
1830
1874
|
/**
|
|
@@ -1845,60 +1889,121 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1845
1889
|
return { ok: true, contract: analysis.canonical };
|
|
1846
1890
|
}
|
|
1847
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
|
+
});
|
|
1848
1897
|
return {
|
|
1849
1898
|
ok: false,
|
|
1850
|
-
reason
|
|
1899
|
+
reason,
|
|
1900
|
+
classification: classified.classification,
|
|
1901
|
+
errors: classified.errors,
|
|
1851
1902
|
};
|
|
1852
1903
|
}
|
|
1853
1904
|
}
|
|
1854
1905
|
/**
|
|
1855
|
-
* Node-output self-check for the plan revision node
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1858
|
-
*
|
|
1859
|
-
*
|
|
1860
|
-
* schemaVersion/targets, so no full-contract schema/semantic checks run here;
|
|
1861
|
-
* content parsing, patch application, and merged-contract validation stay with
|
|
1862
|
-
* the prewrite gate, which remains the only authority.
|
|
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.
|
|
1863
1911
|
*/
|
|
1864
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
|
+
});
|
|
1865
1930
|
let citationsBlock;
|
|
1866
1931
|
try {
|
|
1867
1932
|
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1868
1933
|
}
|
|
1869
1934
|
catch (error) {
|
|
1870
|
-
return
|
|
1871
|
-
ok: false,
|
|
1872
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1873
|
-
};
|
|
1935
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
1874
1936
|
}
|
|
1875
|
-
// Strip the citation blocks before extraction: their per-row JSON objects
|
|
1876
|
-
// would otherwise count as competing balanced objects in the extractor.
|
|
1877
1937
|
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
1878
1938
|
let patch;
|
|
1879
1939
|
try {
|
|
1880
1940
|
patch = extractFrontendImplementationJson(textWithoutCitations);
|
|
1881
1941
|
}
|
|
1882
1942
|
catch (error) {
|
|
1883
|
-
return {
|
|
1884
|
-
ok: false,
|
|
1885
|
-
reason: `revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`,
|
|
1886
|
-
};
|
|
1943
|
+
return fail(`revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`);
|
|
1887
1944
|
}
|
|
1888
1945
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
1889
|
-
return
|
|
1890
|
-
ok: false,
|
|
1891
|
-
reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
|
|
1892
|
-
};
|
|
1946
|
+
return fail("revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)");
|
|
1893
1947
|
}
|
|
1894
1948
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1895
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
|
+
};
|
|
1896
1996
|
return {
|
|
1897
|
-
ok:
|
|
1898
|
-
|
|
1997
|
+
ok: true,
|
|
1998
|
+
contract: analysis.canonical,
|
|
1999
|
+
artifact,
|
|
2000
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
1899
2001
|
};
|
|
1900
2002
|
}
|
|
1901
|
-
|
|
2003
|
+
catch (error) {
|
|
2004
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2005
|
+
return fail(reason);
|
|
2006
|
+
}
|
|
1902
2007
|
}
|
|
1903
2008
|
export async function materializeFrontendImplementationContract(input) {
|
|
1904
2009
|
const analysis = await analyzeFrontendImplementationContract(input);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
6
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
6
7
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
7
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson,
|
|
8
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
9
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
9
10
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
10
11
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
@@ -157,60 +158,68 @@ async function selectNode(runDir, primary, fallbacks) {
|
|
|
157
158
|
}
|
|
158
159
|
throw new Error(`frontend prewrite gate no FINISHED candidate: ${rejections.join("; ")}`);
|
|
159
160
|
}
|
|
160
|
-
async function
|
|
161
|
-
|
|
161
|
+
async function readFinishedNodeRecord(runDir, nodeId) {
|
|
162
|
+
let record;
|
|
163
|
+
try {
|
|
164
|
+
record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw new Error(`frontend prewrite gate cannot parse node record ${nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
}
|
|
162
169
|
if (record.status !== "FINISHED") {
|
|
163
170
|
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
164
171
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// canonical plan appear as two complete JSON contracts to the prewrite gate.
|
|
172
|
+
return record;
|
|
173
|
+
}
|
|
174
|
+
async function readNodeText(runDir, nodeId) {
|
|
175
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
170
176
|
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
171
177
|
if (!text)
|
|
172
178
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
173
179
|
return text;
|
|
174
180
|
}
|
|
175
|
-
async function
|
|
176
|
-
const record =
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
181
|
+
async function readCanonicalContractFromNode(runDir, nodeId) {
|
|
182
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
183
|
+
if (!record.structuredArtifactPath) {
|
|
184
|
+
throw new Error(`frontend prewrite gate missing structured artifact for ${nodeId}`);
|
|
185
|
+
}
|
|
186
|
+
const artifactPath = path.isAbsolute(record.structuredArtifactPath)
|
|
187
|
+
? record.structuredArtifactPath
|
|
188
|
+
: path.join(runDir, record.structuredArtifactPath);
|
|
189
|
+
const resolvedRun = path.resolve(runDir);
|
|
190
|
+
const resolvedArtifact = path.resolve(artifactPath);
|
|
191
|
+
const relative = path.relative(resolvedRun, resolvedArtifact);
|
|
192
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
193
|
+
throw new Error(`frontend prewrite gate structured artifact escapes runDir: ${record.structuredArtifactPath}`);
|
|
194
|
+
}
|
|
195
|
+
const bytes = await readFile(resolvedArtifact);
|
|
196
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
197
|
+
if (record.structuredArtifactSha256 &&
|
|
198
|
+
record.structuredArtifactSha256 !== digest) {
|
|
199
|
+
throw new Error(`frontend prewrite gate structured artifact hash mismatch for ${nodeId}`);
|
|
200
|
+
}
|
|
201
|
+
if (record.structuredArtifactSchemaId &&
|
|
202
|
+
record.structuredArtifactSchemaId !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
|
|
203
|
+
throw new Error(`frontend prewrite gate structured artifact schema mismatch for ${nodeId}: ${record.structuredArtifactSchemaId}`);
|
|
204
|
+
}
|
|
205
|
+
const contractText = bytes.toString("utf8");
|
|
206
|
+
let citationsText = "";
|
|
207
|
+
try {
|
|
208
|
+
const citationsRaw = await readFile(path.join(path.dirname(resolvedArtifact), "openspec-citations.json"), "utf8");
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(citationsRaw);
|
|
211
|
+
citationsText =
|
|
212
|
+
typeof parsed.block === "string" ? parsed.block : citationsRaw;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
citationsText = citationsRaw;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error.code !== "ENOENT")
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return { contractText, citationsText, rawSha256: digest };
|
|
214
223
|
}
|
|
215
224
|
function firstNonEmptyVerdictLine(text) {
|
|
216
225
|
const lines = text
|
|
@@ -439,7 +448,30 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
439
448
|
// remains an ERROR and is intentionally not classified into prewrite-result.
|
|
440
449
|
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
441
450
|
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
442
|
-
|
|
451
|
+
let planArtifact;
|
|
452
|
+
try {
|
|
453
|
+
planArtifact = await readCanonicalContractFromNode(input.runDir, planNodeId);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return finalizePrewrite(input, {
|
|
457
|
+
planNodeId,
|
|
458
|
+
reviewNodeId,
|
|
459
|
+
verdict: "",
|
|
460
|
+
candidateRawSha256: sha256Hex(""),
|
|
461
|
+
candidateJsonSha256: null,
|
|
462
|
+
canonicalSha256: null,
|
|
463
|
+
normalizationActions: [],
|
|
464
|
+
mockStrategy: undefined,
|
|
465
|
+
artifact: undefined,
|
|
466
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
467
|
+
failureCode: "plan-text-unreadable",
|
|
468
|
+
classification: "retryable-invalid",
|
|
469
|
+
openspecReadPaths: [],
|
|
470
|
+
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
471
|
+
openspecPolicy: input.config.openspecPolicy ?? "scan-strict",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const candidateRawSha256 = planArtifact.rawSha256;
|
|
443
475
|
const openspecPolicy = input.config.openspecPolicy ?? "scan-strict";
|
|
444
476
|
const basePending = {
|
|
445
477
|
planNodeId,
|
|
@@ -458,18 +490,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
458
490
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
459
491
|
openspecPolicy,
|
|
460
492
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
catch (error) {
|
|
466
|
-
return finalizePrewrite(input, {
|
|
467
|
-
...basePending,
|
|
468
|
-
classification: "retryable-invalid",
|
|
469
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
470
|
-
failureCode: "plan-text-unreadable",
|
|
471
|
-
});
|
|
472
|
-
}
|
|
493
|
+
const planText = [planArtifact.contractText, planArtifact.citationsText]
|
|
494
|
+
.filter(Boolean)
|
|
495
|
+
.join("\n");
|
|
473
496
|
let reviewText;
|
|
474
497
|
try {
|
|
475
498
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -492,77 +515,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
492
515
|
failureCode: "verdict-not-pass",
|
|
493
516
|
});
|
|
494
517
|
}
|
|
495
|
-
|
|
496
|
-
// delta against the original contract node (planFallbackFromNodeIds[0]).
|
|
497
|
-
// Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
|
|
498
|
-
const revisionPatchMode = input.config.revisionPatch === true &&
|
|
499
|
-
planNodeId === input.config.planFromNodeId &&
|
|
500
|
-
input.config.planFallbackFromNodeIds.length > 0;
|
|
501
|
-
let effectiveContractText = null;
|
|
502
|
-
if (revisionPatchMode) {
|
|
503
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
504
|
-
try {
|
|
505
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
return finalizePrewrite(input, {
|
|
509
|
-
...basePending,
|
|
510
|
-
verdict,
|
|
511
|
-
classification: "retryable-invalid",
|
|
512
|
-
failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
513
|
-
failureCode: "contract-invalid",
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
// Content-driven compatibility (Bug 1): legacy DAGs without
|
|
519
|
-
// revisionPatch can still receive a merge-patch delta from the revision
|
|
520
|
-
// node. Detect the output shape instead of trusting the DAG flag alone.
|
|
521
|
-
// - Full contract shape → keep legacy full-contract semantics unchanged.
|
|
522
|
-
// - Patch shape with a usable original contract node → auto-merge and
|
|
523
|
-
// run the existing analysis/materialization pipeline.
|
|
524
|
-
// - Patch shape but unmergeable → blocked + revision-patch-unmergeable
|
|
525
|
-
// (no retryable loop).
|
|
526
|
-
let planJsonValue = null;
|
|
527
|
-
try {
|
|
528
|
-
planJsonValue = extractFrontendImplementationJson(planText);
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
// Not machine-readable JSON: fall back to the legacy full-contract
|
|
532
|
-
// path where analyzeFrontendImplementationContract surfaces the
|
|
533
|
-
// original diagnostic (fail-closed without a new failure mode).
|
|
534
|
-
}
|
|
535
|
-
if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
|
|
536
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
537
|
-
if (!originalNodeId || originalNodeId === planNodeId) {
|
|
538
|
-
return finalizePrewrite(input, {
|
|
539
|
-
...basePending,
|
|
540
|
-
verdict,
|
|
541
|
-
classification: "blocked",
|
|
542
|
-
failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
|
|
543
|
-
failureCode: "revision-patch-unmergeable",
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
try {
|
|
547
|
-
const originalText = await readNodeText(input.runDir, originalNodeId);
|
|
548
|
-
const originalValue = extractFrontendImplementationJson(originalText);
|
|
549
|
-
if (!isFullContractShape(originalValue)) {
|
|
550
|
-
throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
|
|
551
|
-
}
|
|
552
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
553
|
-
}
|
|
554
|
-
catch (error) {
|
|
555
|
-
return finalizePrewrite(input, {
|
|
556
|
-
...basePending,
|
|
557
|
-
verdict,
|
|
558
|
-
classification: "blocked",
|
|
559
|
-
failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
|
|
560
|
-
failureCode: "revision-patch-unmergeable",
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
518
|
+
const requirementIdSearchText = planArtifact.contractText;
|
|
566
519
|
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
567
520
|
if (missingIds.length > 0) {
|
|
568
521
|
return finalizePrewrite(input, {
|
|
@@ -610,14 +563,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
610
563
|
workspaceRoot,
|
|
611
564
|
});
|
|
612
565
|
}
|
|
613
|
-
// Candidate -> canonical conversion. The gate consumes the
|
|
614
|
-
//
|
|
566
|
+
// Candidate -> canonical conversion. The gate consumes the producing node's
|
|
567
|
+
// hash-bound canonical artifact and never re-merges plan/revision stdout.
|
|
615
568
|
let analysis;
|
|
616
569
|
try {
|
|
617
570
|
analysis = await analyzeFrontendImplementationContract({
|
|
618
571
|
runDir: input.runDir,
|
|
619
|
-
|
|
620
|
-
rawContractText: effectiveContractText ?? undefined,
|
|
572
|
+
rawContractText: planArtifact.contractText,
|
|
621
573
|
sourceBinding: input.sourceBinding,
|
|
622
574
|
});
|
|
623
575
|
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
5
5
|
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
6
|
+
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT, FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE, } from "./frontend-verification-trace.js";
|
|
6
7
|
export const FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID = "frontend-repair-assessment-v1";
|
|
7
8
|
export const frontendRepairFailureClassSchema = z.enum([
|
|
8
9
|
"typecheck",
|
|
@@ -82,6 +83,12 @@ export function classifyFrontendFailure(input) {
|
|
|
82
83
|
if (input.failureCategory === "write-guard") {
|
|
83
84
|
return "path";
|
|
84
85
|
}
|
|
86
|
+
if (`${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
|
|
87
|
+
// Verification unavailability is a generation-time contract gap: no
|
|
88
|
+
// authorized verification command exists. Bounded repair cannot fix it;
|
|
89
|
+
// the operator must declare commands and regenerate the DAG.
|
|
90
|
+
return "contract";
|
|
91
|
+
}
|
|
85
92
|
if (input.traceTarget || input.commandCategory === "trace") {
|
|
86
93
|
return "trace";
|
|
87
94
|
}
|
|
@@ -424,6 +431,13 @@ export async function runFrontendFailureAssessGate(input) {
|
|
|
424
431
|
: !baseEligible
|
|
425
432
|
? `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`
|
|
426
433
|
: `missing structured evidence for failureClass=${failureClass} on ${primary.nodeId}`;
|
|
434
|
+
if (!eligible &&
|
|
435
|
+
`${primary.record.stderr ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
|
|
436
|
+
// Surface the actionable unavailable-verification reason in the
|
|
437
|
+
// assessment (and the non-repairable gate error) instead of a
|
|
438
|
+
// generic class label.
|
|
439
|
+
reason = FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE;
|
|
440
|
+
}
|
|
427
441
|
}
|
|
428
442
|
// fixScope and changedPaths must stay inside the implement writeSet.
|
|
429
443
|
if (evidence.fixScope?.length) {
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Deterministic frontend-implementation risk classifier (M4).
|
|
3
3
|
* Pure function: no I/O, no model, no side effects.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Dependency-installation language in task text. Mentioning package.json or
|
|
7
|
+
* allowing its path is NOT dependency introduction by itself; the manifest
|
|
8
|
+
* path check in classifyFrontendRisk requires this language to escalate.
|
|
9
|
+
*/
|
|
10
|
+
const DEPENDENCY_INSTALL_LANGUAGE = /new\s+dependency|add\s+dependencies?|install\s+dependencies?|npm\s+(?:install|i)\b|yarn\s+(?:add|install)\b|pnpm\s+(?:add|install)\b|bun\s+(?:add|install)\b|新增依赖|添加依赖|安装依赖|引入依赖/i;
|
|
5
11
|
const HIGH_RISK_PATTERNS = [
|
|
6
12
|
{
|
|
7
13
|
id: "new-route",
|
|
@@ -25,7 +31,7 @@ const HIGH_RISK_PATTERNS = [
|
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
id: "new-dependency",
|
|
28
|
-
re:
|
|
34
|
+
re: DEPENDENCY_INSTALL_LANGUAGE,
|
|
29
35
|
},
|
|
30
36
|
{
|
|
31
37
|
id: "schema-api-change",
|
|
@@ -88,7 +94,14 @@ export function classifyFrontendRisk(input) {
|
|
|
88
94
|
const paths = input.allowedPaths ?? [];
|
|
89
95
|
const spread = pathSpreadScore(paths);
|
|
90
96
|
if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
|
|
91
|
-
|
|
97
|
+
if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
|
|
98
|
+
signals.push("new-dependency");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Editing manifest scripts/config is not dependency introduction;
|
|
102
|
+
// record the rejected escalation instead of forcing full gates.
|
|
103
|
+
rejectedSignals.push("new-dependency:manifest-path-without-install-language");
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
// Explicit multi-domain language only — path count alone is not high-risk
|
|
94
107
|
if (spread >= 3 &&
|