@tea-agent/loop-agent 0.39.0-next.2 → 0.39.0-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/build-stamp.json +2 -2
- package/dist/commands/init-upgrade.js +97 -19
- package/dist/executors/dag-pi-executor.js +42 -4
- package/dist/executors/shell-executor.js +269 -15
- package/dist/shared/operator/capabilities.js +149 -27
- package/dist/task/config-types.js +22 -0
- 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 +477 -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 +128 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +8 -1
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/security.js +20 -1
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
- package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.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/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/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
- 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 +249 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- 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/frontend-implementation-contract.js +386 -0
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +243 -8
- package/dist/workflows/dag/init-hybrid.js +793 -104
- package/dist/workflows/dag/node-execution.js +61 -2
- package/dist/workflows/dag/prompt.js +45 -0
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +93 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +22 -0
- package/dist/workflows/dag/types.js +125 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/harness.json +3 -1
- package/package.json +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/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
|
@@ -2,12 +2,17 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
+
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
5
6
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
6
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
7
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
|
+
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
7
9
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
8
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
11
|
+
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
9
12
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
10
13
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
14
|
+
/** Canonical contract serialization must stay at or below this byte size. */
|
|
15
|
+
export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
11
16
|
/**
|
|
12
17
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
13
18
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -31,6 +36,11 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
31
36
|
"openspec-citation-block-unparseable",
|
|
32
37
|
"openspec-not-cited",
|
|
33
38
|
"openspec-citation-not-read",
|
|
39
|
+
"component-spec-reference-invalid",
|
|
40
|
+
"component-spec-not-cited",
|
|
41
|
+
"component-choices-missing",
|
|
42
|
+
"contract-too-large",
|
|
43
|
+
"revision-patch-unmergeable",
|
|
34
44
|
]);
|
|
35
45
|
export const frontendPrewriteResultV1Schema = z
|
|
36
46
|
.object({
|
|
@@ -163,16 +173,53 @@ async function readNodeRawText(runDir, nodeId) {
|
|
|
163
173
|
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
164
174
|
return record.assistantText ?? record.stdout ?? "";
|
|
165
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Same shape predicate as the internal `isContract` inside
|
|
178
|
+
* `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
|
|
179
|
+
* schemaVersion 1 + a plain `targets` object + `requirements` and
|
|
180
|
+
* `verificationTargets` arrays. A merge-patch delta does not carry these
|
|
181
|
+
* fields, so this is the deterministic boundary between a full contract and a
|
|
182
|
+
* patch. Keep the two predicates in sync.
|
|
183
|
+
*/
|
|
184
|
+
function isFullContractShape(value) {
|
|
185
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const record = value;
|
|
189
|
+
const targets = record.targets;
|
|
190
|
+
return (record.schemaVersion === 1 &&
|
|
191
|
+
targets !== null &&
|
|
192
|
+
typeof targets === "object" &&
|
|
193
|
+
!Array.isArray(targets) &&
|
|
194
|
+
Array.isArray(record.requirements) &&
|
|
195
|
+
Array.isArray(record.verificationTargets));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Shared merge pipeline for revisionPatch mode and content-driven
|
|
199
|
+
* compatibility: read the original contract node, extract the original
|
|
200
|
+
* contract and the effective node's patch, apply the RFC 7386 merge-patch, and
|
|
201
|
+
* serialize deterministically. Throws on any failure; callers map the error to
|
|
202
|
+
* their own classification (retryable-invalid for explicit revisionPatch mode,
|
|
203
|
+
* blocked for content-driven fallback).
|
|
204
|
+
*/
|
|
205
|
+
async function mergeRevisionPatch(runDir, planText, originalNodeId) {
|
|
206
|
+
const originalText = await readNodeText(runDir, originalNodeId);
|
|
207
|
+
const originalValue = extractFrontendImplementationJson(originalText);
|
|
208
|
+
const patchValue = extractFrontendImplementationJson(planText);
|
|
209
|
+
const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
|
|
210
|
+
return serializeDeterministicJson(merged);
|
|
211
|
+
}
|
|
166
212
|
function firstNonEmptyVerdictLine(text) {
|
|
167
213
|
const lines = text
|
|
168
214
|
.split(/\r?\n/)
|
|
169
215
|
.map((line) => line.trim())
|
|
170
216
|
.filter(Boolean);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
217
|
+
// Emphasis stripping (backtick + asterisk) is shared with the runIf
|
|
218
|
+
// selector and node-level output-protocol validation via
|
|
219
|
+
// normalizeVerdictCandidateLine. The gate keeps its own loose
|
|
220
|
+
// pass/request-revision normalization below (prefix match + trailing
|
|
221
|
+
// prose tolerance) for deterministic fail-closed behavior.
|
|
222
|
+
const normalizedLines = lines.map(normalizeVerdictCandidateLine);
|
|
176
223
|
const first = normalizedLines[0] ?? "";
|
|
177
224
|
// The prompt requires VERDICT to be the first non-empty line, but model
|
|
178
225
|
// output can still prepend a summary. Keep the protocol strict in the
|
|
@@ -442,7 +489,78 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
442
489
|
failureCode: "verdict-not-pass",
|
|
443
490
|
});
|
|
444
491
|
}
|
|
445
|
-
|
|
492
|
+
// revisionPatch mode: the effective plan node emits an RFC 7386 merge-patch
|
|
493
|
+
// delta against the original contract node (planFallbackFromNodeIds[0]).
|
|
494
|
+
// Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
|
|
495
|
+
const revisionPatchMode = input.config.revisionPatch === true &&
|
|
496
|
+
planNodeId === input.config.planFromNodeId &&
|
|
497
|
+
input.config.planFallbackFromNodeIds.length > 0;
|
|
498
|
+
let effectiveContractText = null;
|
|
499
|
+
if (revisionPatchMode) {
|
|
500
|
+
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
501
|
+
try {
|
|
502
|
+
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
return finalizePrewrite(input, {
|
|
506
|
+
...basePending,
|
|
507
|
+
verdict,
|
|
508
|
+
classification: "retryable-invalid",
|
|
509
|
+
failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
510
|
+
failureCode: "contract-invalid",
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// Content-driven compatibility (Bug 1): legacy DAGs without
|
|
516
|
+
// revisionPatch can still receive a merge-patch delta from the revision
|
|
517
|
+
// node. Detect the output shape instead of trusting the DAG flag alone.
|
|
518
|
+
// - Full contract shape → keep legacy full-contract semantics unchanged.
|
|
519
|
+
// - Patch shape with a usable original contract node → auto-merge and
|
|
520
|
+
// run the existing analysis/materialization pipeline.
|
|
521
|
+
// - Patch shape but unmergeable → blocked + revision-patch-unmergeable
|
|
522
|
+
// (no retryable loop).
|
|
523
|
+
let planJsonValue = null;
|
|
524
|
+
try {
|
|
525
|
+
planJsonValue = extractFrontendImplementationJson(planText);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// Not machine-readable JSON: fall back to the legacy full-contract
|
|
529
|
+
// path where analyzeFrontendImplementationContract surfaces the
|
|
530
|
+
// original diagnostic (fail-closed without a new failure mode).
|
|
531
|
+
}
|
|
532
|
+
if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
|
|
533
|
+
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
534
|
+
if (!originalNodeId || originalNodeId === planNodeId) {
|
|
535
|
+
return finalizePrewrite(input, {
|
|
536
|
+
...basePending,
|
|
537
|
+
verdict,
|
|
538
|
+
classification: "blocked",
|
|
539
|
+
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.",
|
|
540
|
+
failureCode: "revision-patch-unmergeable",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
const originalText = await readNodeText(input.runDir, originalNodeId);
|
|
545
|
+
const originalValue = extractFrontendImplementationJson(originalText);
|
|
546
|
+
if (!isFullContractShape(originalValue)) {
|
|
547
|
+
throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
|
|
548
|
+
}
|
|
549
|
+
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
return finalizePrewrite(input, {
|
|
553
|
+
...basePending,
|
|
554
|
+
verdict,
|
|
555
|
+
classification: "blocked",
|
|
556
|
+
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.`,
|
|
557
|
+
failureCode: "revision-patch-unmergeable",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
563
|
+
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
446
564
|
if (missingIds.length > 0) {
|
|
447
565
|
return finalizePrewrite(input, {
|
|
448
566
|
...basePending,
|
|
@@ -495,7 +613,8 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
495
613
|
try {
|
|
496
614
|
analysis = await analyzeFrontendImplementationContract({
|
|
497
615
|
runDir: input.runDir,
|
|
498
|
-
fromNodeId: planNodeId,
|
|
616
|
+
fromNodeId: effectiveContractText ? undefined : planNodeId,
|
|
617
|
+
rawContractText: effectiveContractText ?? undefined,
|
|
499
618
|
sourceBinding: input.sourceBinding,
|
|
500
619
|
});
|
|
501
620
|
}
|
|
@@ -515,6 +634,21 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
515
634
|
}
|
|
516
635
|
throw error;
|
|
517
636
|
}
|
|
637
|
+
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
638
|
+
// (compress the contract) rather than a writer authorization failure.
|
|
639
|
+
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
640
|
+
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
641
|
+
return finalizePrewrite(input, {
|
|
642
|
+
...basePending,
|
|
643
|
+
verdict,
|
|
644
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
645
|
+
normalizationActions: [],
|
|
646
|
+
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
647
|
+
classification: "retryable-invalid",
|
|
648
|
+
failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
|
|
649
|
+
failureCode: "contract-too-large",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
518
652
|
const mockStrategy = analysis.canonical.mockApi.strategy;
|
|
519
653
|
if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
|
|
520
654
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
@@ -711,6 +845,105 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
711
845
|
}
|
|
712
846
|
}
|
|
713
847
|
}
|
|
848
|
+
// Component selection spec conformance (ADR 0016). Deterministic cross-check
|
|
849
|
+
// that declared component/token selections trace to the frontend spec
|
|
850
|
+
// (openspec/ai_workspace component + theme) instead of a model's ad-hoc
|
|
851
|
+
// "better" choice. Runs after the openspec read gate so citation + read
|
|
852
|
+
// evidence are already resolved.
|
|
853
|
+
const componentChoices = analysis.canonical.uiComponentChoices ?? [];
|
|
854
|
+
const componentSpecCandidates = input.config.componentSpecCandidatePaths ?? [];
|
|
855
|
+
const hasUiVisibleWork = analysis.canonical.uiStates.some((state) => state.applicable) ||
|
|
856
|
+
analysis.canonical.interactions.length > 0;
|
|
857
|
+
if (componentSpecCandidates.length > 0 &&
|
|
858
|
+
hasUiVisibleWork &&
|
|
859
|
+
componentChoices.length === 0) {
|
|
860
|
+
return finalizePrewrite(input, {
|
|
861
|
+
...basePending,
|
|
862
|
+
verdict,
|
|
863
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
864
|
+
normalizationActions: [],
|
|
865
|
+
mockStrategy,
|
|
866
|
+
classification: "retryable-invalid",
|
|
867
|
+
failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new),specified 必须给 specReference.path/section/line,new 必须给 rationale 声明偏差。",
|
|
868
|
+
failureCode: "component-choices-missing",
|
|
869
|
+
openspecReadPaths,
|
|
870
|
+
openspecCandidatePaths: candidatePaths,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
const componentSpecReferences = componentChoices
|
|
874
|
+
.map((choice) => ({
|
|
875
|
+
raw: choice.specReference?.path ?? "",
|
|
876
|
+
normalized: choice.specReference?.path
|
|
877
|
+
? normalizeCitationPath(choice.specReference.path, candidateRoot)
|
|
878
|
+
: null,
|
|
879
|
+
}))
|
|
880
|
+
.filter((ref) => ref.raw.length > 0);
|
|
881
|
+
if (componentSpecReferences.length > 0) {
|
|
882
|
+
const invalidRefs = [
|
|
883
|
+
...new Set(componentSpecReferences
|
|
884
|
+
.filter((ref) => !ref.normalized ||
|
|
885
|
+
!isOpenspecSpecFilePath(ref.normalized))
|
|
886
|
+
.map((ref) => ref.raw)),
|
|
887
|
+
];
|
|
888
|
+
if (invalidRefs.length > 0) {
|
|
889
|
+
return finalizePrewrite(input, {
|
|
890
|
+
...basePending,
|
|
891
|
+
verdict,
|
|
892
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
893
|
+
normalizationActions: [],
|
|
894
|
+
mockStrategy,
|
|
895
|
+
classification: "retryable-invalid",
|
|
896
|
+
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are not supported openspec/ai_workspace spec files: ${invalidRefs.join(", ")}`,
|
|
897
|
+
failureCode: "component-spec-reference-invalid",
|
|
898
|
+
openspecReadPaths,
|
|
899
|
+
openspecCandidatePaths: candidatePaths,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (openspecPolicy === "cited") {
|
|
903
|
+
const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
|
|
904
|
+
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
905
|
+
const readSet = new Set(openspecReadPaths);
|
|
906
|
+
const uncorroborated = [
|
|
907
|
+
...new Set(componentSpecReferences
|
|
908
|
+
.filter((ref) => !ref.normalized ||
|
|
909
|
+
!citedPaths.has(ref.normalized) ||
|
|
910
|
+
!readSet.has(ref.normalized))
|
|
911
|
+
.map((ref) => ref.raw)),
|
|
912
|
+
];
|
|
913
|
+
if (uncorroborated.length > 0) {
|
|
914
|
+
return finalizePrewrite(input, {
|
|
915
|
+
...basePending,
|
|
916
|
+
verdict,
|
|
917
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
918
|
+
normalizationActions: [],
|
|
919
|
+
mockStrategy,
|
|
920
|
+
classification: "retryable-invalid",
|
|
921
|
+
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths were not cited in the plan openspec-citations block or lack a successful read event: ${uncorroborated.join(", ")}`,
|
|
922
|
+
failureCode: "component-spec-not-cited",
|
|
923
|
+
openspecReadPaths,
|
|
924
|
+
openspecCandidatePaths: candidatePaths,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// Render the deterministic plan.md from the canonical contract (fail-closed
|
|
930
|
+
// before any artifact is written: no contract, no writer authorization).
|
|
931
|
+
let planMarkdown;
|
|
932
|
+
try {
|
|
933
|
+
planMarkdown = renderFrontendPlanMarkdown(analysis.canonical);
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
return finalizePrewrite(input, {
|
|
937
|
+
...basePending,
|
|
938
|
+
verdict,
|
|
939
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
940
|
+
normalizationActions: [],
|
|
941
|
+
mockStrategy,
|
|
942
|
+
classification: "retryable-invalid",
|
|
943
|
+
failureReason: `frontend prewrite gate: failed to render ${input.config.planMdArtifactName ?? "frontend-plan.md"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
944
|
+
failureCode: "contract-invalid",
|
|
945
|
+
});
|
|
946
|
+
}
|
|
714
947
|
// Materialize the canonical contract only after every governance check passed.
|
|
715
948
|
const artifact = await writeFrontendImplementationContractArtifact({
|
|
716
949
|
runDir: input.runDir,
|
|
@@ -718,6 +951,8 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
718
951
|
artifactName: input.config.artifactName,
|
|
719
952
|
canonical: analysis.canonical,
|
|
720
953
|
});
|
|
954
|
+
// Materialize the rendered plan.md in the same outputDir as the contract.
|
|
955
|
+
await writeTextArtifactFile(path.join(input.runDir, path.posix.join(input.config.outputDir, input.config.planMdArtifactName ?? "frontend-plan.md")), planMarkdown);
|
|
721
956
|
const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
|
|
722
957
|
return finalizePrewrite(input, {
|
|
723
958
|
...basePending,
|