@tea-agent/loop-agent 0.42.0-next.8 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -50
- package/dist/application/dag/run-dag.js +41 -0
- package/dist/application/task-lifecycle/advance.js +9 -4
- package/dist/build-stamp.json +3 -3
- package/dist/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +1809 -213
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/shell-executor.js +178 -73
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +28 -12
- package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
- package/dist/worker/console/chat/session-store.js +5 -1
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/tool-preview.js +10 -0
- package/dist/worker/console/chat/tools.js +11 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-DCPjC19G.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +449 -0
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-7U2ue5IE.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-BUuGHmWO.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +133 -5
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +5 -5
- package/dist/worker/observe/static/styles.css +171 -0
- package/dist/worker/observe/static/views/dag-graph.js +1 -1
- package/dist/worker/observe/static/views/dag-inspector.js +329 -174
- package/dist/worker/observe/static/views/dag.js +16 -12
- package/dist/worker/observe/static/views/session-timeline.js +5 -3
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +18 -0
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +30 -1
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +101 -16
- package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
- package/dist/workflows/dag/frontend-recovery-plan.js +6 -2
- package/dist/workflows/dag/frontend-recovery-run.js +169 -15
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
- package/dist/workflows/dag/frontend-shape.js +16 -6
- package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +46 -12
- package/dist/workflows/dag/init-hybrid.js +59 -29
- package/dist/workflows/dag/node-execution.js +277 -85
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +60 -0
- package/dist/workflows/dag/rerun-plan.js +90 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +224 -15
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +238 -29
- package/dist/workflows/dag/scheduler.js +21 -6
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +4 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/harness.json +1 -1
- package/package.json +4 -5
- package/skills/frontend-plan/SKILL.md +14 -1
- package/skills/frontend-plan/references/decision-contract.md +101 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1
2
|
import { isTerminalOperationState, } from "../../infrastructure/console/operation-store.js";
|
|
2
3
|
import { runOperation, } from "../../worker/console/operation-runner.js";
|
|
3
4
|
import { buildFailureCapsule, classifyFailureOwner, projectProtocolFailureReason, } from "./frontend-recovery-capsule.js";
|
|
@@ -78,7 +79,13 @@ export async function runRecoveryOperationController(input) {
|
|
|
78
79
|
const readChangedFiles = deps.readChangedFiles ?? (async () => []);
|
|
79
80
|
const fingerprints = [...(deps.previousFingerprints ?? [])];
|
|
80
81
|
let budgetAggregate;
|
|
81
|
-
|
|
82
|
+
const pollIntervalMs = Math.max(10, deps.operationPollIntervalMs ?? 1_000);
|
|
83
|
+
const waitTimeoutMs = Math.max(pollIntervalMs, deps.operationWaitTimeoutMs ??
|
|
84
|
+
deps.operationRunnerDeps.defaultTimeoutMs ??
|
|
85
|
+
30 * 60_000);
|
|
86
|
+
const waitStartedAt = Date.now();
|
|
87
|
+
let attempt = 1;
|
|
88
|
+
while (attempt <= maxAttempts) {
|
|
82
89
|
const operation = await deps.store.get(operationId);
|
|
83
90
|
if (!operation) {
|
|
84
91
|
return {
|
|
@@ -190,7 +197,24 @@ export async function runRecoveryOperationController(input) {
|
|
|
190
197
|
detail: `interrupt projection: ${projected}`,
|
|
191
198
|
};
|
|
192
199
|
}
|
|
200
|
+
const operationAlreadyInFlight = operation.state === "starting" || operation.state === "running";
|
|
201
|
+
const operationOwnedElsewhere = input.observeOnly &&
|
|
202
|
+
(operation.state === "queued" || operation.state === "needs-reconcile");
|
|
203
|
+
if (operationAlreadyInFlight || operationOwnedElsewhere) {
|
|
204
|
+
if (Date.now() - waitStartedAt >= waitTimeoutMs) {
|
|
205
|
+
return {
|
|
206
|
+
terminalReason: "needs-attention",
|
|
207
|
+
attempts: attempt,
|
|
208
|
+
budgetAggregate,
|
|
209
|
+
fingerprints,
|
|
210
|
+
detail: `recovery operation did not settle within ${waitTimeoutMs}ms`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
await delay(pollIntervalMs);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
193
216
|
await runOperation(operationId, deps.operationRunnerDeps);
|
|
217
|
+
attempt += 1;
|
|
194
218
|
}
|
|
195
219
|
return {
|
|
196
220
|
terminalReason: "max-attempts",
|
|
@@ -26,7 +26,9 @@ export function deriveFrontendRecoveryFailureSource(result) {
|
|
|
26
26
|
* Compute the frontend candidate-continuation reset partition:
|
|
27
27
|
* - `frontend-plan-pi` failure → reset plan + design-review + policy/admission (+ descendants);
|
|
28
28
|
* - `frontend-design-policy-shell` failure → reset policy/admission (+ descendants);
|
|
29
|
-
* -
|
|
29
|
+
* - design-review rejection → reset plan + design review + admission (+ descendants);
|
|
30
|
+
* - final-review rejection → reset implementation + verification/review (+ descendants);
|
|
31
|
+
* - other admission failures → reset writer admission (+ descendants);
|
|
30
32
|
*
|
|
31
33
|
* Everything upstream of the failure source is imported as verified parent facts.
|
|
32
34
|
*/
|
|
@@ -61,7 +63,9 @@ function computeFrontendRecoveryPlanForSource(input) {
|
|
|
61
63
|
// reset from implement: the recovery re-implements and re-verifies.
|
|
62
64
|
const resetRoot = input.failureSource === "frontend-verify-shell"
|
|
63
65
|
? "frontend-implement-pi"
|
|
64
|
-
: input.failureSource
|
|
66
|
+
: input.failureSource === "frontend-review-pi"
|
|
67
|
+
? "frontend-implement-pi"
|
|
68
|
+
: input.failureSource;
|
|
65
69
|
const resetNodeIds = computeResetClosure(input.spec, resetRoot);
|
|
66
70
|
const resetSet = new Set(resetNodeIds);
|
|
67
71
|
const importedNodeIds = input.spec.tasks
|
|
@@ -3,8 +3,10 @@ import path from "node:path";
|
|
|
3
3
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
4
4
|
import { sha256Hex } from "./frontend-implementation-contract.js";
|
|
5
5
|
import { FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH, FRONTEND_RECOVERY_INTENT_REL_DIR, FRONTEND_RECOVERY_STAGING_PREFIX, computeFrontendRecoveryPlan, computeFrontendRecoveryPlanForSource, } from "./frontend-recovery-plan.js";
|
|
6
|
-
import { dagRunDirExists, getDagRunDir, readDagRunSpec, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
|
|
6
|
+
import { dagRunDirExists, getDagRunDir, locateDagRun, readDagRunSpec, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
|
|
7
7
|
import { buildDagRunId } from "./runner.js";
|
|
8
|
+
import { buildFrontendTopologyBinding } from "./frontend-shape.js";
|
|
9
|
+
import { FRONTEND_SHAPE_BOUNDARIES, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
|
|
8
10
|
import { prepareRunDir, writeNodeRecord } from "./run-store.js";
|
|
9
11
|
import { FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, readFrontendWriterAdmissionResult, } from "./scheduler.js";
|
|
10
12
|
import { topoSortToRanks } from "./topo.js";
|
|
@@ -273,14 +275,63 @@ async function casUpdateFrontendRecoveryState(parentRunDir, expectedRevision, de
|
|
|
273
275
|
await deps.writeParentState(parentRunDir, state);
|
|
274
276
|
}
|
|
275
277
|
async function materializeChildStaging(input) {
|
|
276
|
-
const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, requestId, childRunId, stagingDir, createdAt, } = input;
|
|
278
|
+
const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, recoveryPlan, requestId, childRunId, stagingDir, createdAt, } = input;
|
|
277
279
|
await prepareRunDir(stagingDir);
|
|
278
|
-
|
|
280
|
+
// Design-review rejection must be visible to the reset planner. Recovery
|
|
281
|
+
// children otherwise copy the old spec and only rerun design review, which
|
|
282
|
+
// deterministically re-rejects the unchanged plan. Bind the parent's typed
|
|
283
|
+
// findings into the child spec as ordinary rerun feedback.
|
|
284
|
+
let childSpec = parentSpec;
|
|
285
|
+
if (recovery.failureSource === "frontend-plan-pi" ||
|
|
286
|
+
recovery.failureSource === "frontend-review-pi") {
|
|
287
|
+
const designRecovery = parentState.terminalReason === "design-request-changes";
|
|
288
|
+
const reviewRecovery = recovery.failureSource === "frontend-review-pi" ||
|
|
289
|
+
parentState.terminalReason === "review-request-changes";
|
|
290
|
+
let feedback;
|
|
291
|
+
try {
|
|
292
|
+
const { deriveDagRerunFeedback } = await import("./rerun-feedback.js");
|
|
293
|
+
const taskId = parentSpec.taskContractBinding?.taskId ??
|
|
294
|
+
parentSpec.sourceBinding?.taskId ??
|
|
295
|
+
parentRunId;
|
|
296
|
+
feedback = await deriveDagRerunFeedback({
|
|
297
|
+
parentRunId,
|
|
298
|
+
taskId,
|
|
299
|
+
runDir: parentRunDir,
|
|
300
|
+
spec: parentSpec,
|
|
301
|
+
state: parentState,
|
|
302
|
+
});
|
|
303
|
+
if (reviewRecovery &&
|
|
304
|
+
feedback &&
|
|
305
|
+
feedback.sourceNodeId !== "frontend-review-pi") {
|
|
306
|
+
// Do not accidentally carry an older design/verify finding when a
|
|
307
|
+
// review-triggered continuation has lost its typed review fact.
|
|
308
|
+
feedback = undefined;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (designRecovery || reviewRecovery) {
|
|
313
|
+
throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (feedback) {
|
|
317
|
+
childSpec = { ...parentSpec, rerunFeedback: feedback };
|
|
318
|
+
}
|
|
319
|
+
else if (designRecovery || reviewRecovery) {
|
|
320
|
+
// A design/review rejection cannot be safely retried without its typed
|
|
321
|
+
// findings. Fail closed instead of deterministically re-running the
|
|
322
|
+
// unchanged plan and producing another rejection.
|
|
323
|
+
throw new Error(`frontend recovery feedback unavailable: no typed ${reviewRecovery ? "review" : "design"} findings were found for recovery`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
await writeJsonVerified(stagingDir, "run.json", childSpec);
|
|
279
327
|
// Phase 5: writer transient partial-write recovery resets the writer subtree
|
|
280
328
|
// (frontend-implement-pi) instead of the candidate producer; everything
|
|
281
329
|
// upstream is imported as verified parent facts.
|
|
282
330
|
let plan;
|
|
283
|
-
if (
|
|
331
|
+
if (recoveryPlan) {
|
|
332
|
+
plan = recoveryPlan;
|
|
333
|
+
}
|
|
334
|
+
else if (recovery.failureSource) {
|
|
284
335
|
plan = computeFrontendRecoveryPlanForSource({
|
|
285
336
|
spec: parentSpec,
|
|
286
337
|
parentRunId,
|
|
@@ -300,10 +351,10 @@ async function materializeChildStaging(input) {
|
|
|
300
351
|
result: admissionRead.result,
|
|
301
352
|
});
|
|
302
353
|
}
|
|
303
|
-
const { ranks } = topoSortToRanks(
|
|
354
|
+
const { ranks } = topoSortToRanks(childSpec);
|
|
304
355
|
const resetSet = new Set(plan.resetNodeIds);
|
|
305
356
|
const importedSet = new Set(plan.importedNodeIds);
|
|
306
|
-
const tasksById = new Map(
|
|
357
|
+
const tasksById = new Map(childSpec.tasks.map((task) => [task.id, task]));
|
|
307
358
|
const nodes = {};
|
|
308
359
|
const importedFacts = [];
|
|
309
360
|
for (const nodeId of plan.importedNodeIds) {
|
|
@@ -328,7 +379,7 @@ async function materializeChildStaging(input) {
|
|
|
328
379
|
}
|
|
329
380
|
nodes[nodeId] = buildPendingNodeRecord(task);
|
|
330
381
|
}
|
|
331
|
-
for (const task of
|
|
382
|
+
for (const task of childSpec.tasks) {
|
|
332
383
|
if (!resetSet.has(task.id) && !importedSet.has(task.id)) {
|
|
333
384
|
throw new Error(`node ${task.id} missing from frontend recovery plan partition`);
|
|
334
385
|
}
|
|
@@ -338,13 +389,29 @@ async function materializeChildStaging(input) {
|
|
|
338
389
|
const canonicalContract = await copyArtifactVerified({
|
|
339
390
|
parentRunDir,
|
|
340
391
|
newRunDir: stagingDir,
|
|
341
|
-
relativePath: resolveCanonicalContractRelPath(
|
|
392
|
+
relativePath: resolveCanonicalContractRelPath(childSpec),
|
|
342
393
|
});
|
|
343
394
|
const writerAdmissionResult = await copyArtifactVerified({
|
|
344
395
|
parentRunDir,
|
|
345
396
|
newRunDir: stagingDir,
|
|
346
397
|
relativePath: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
|
|
347
398
|
});
|
|
399
|
+
// The scheduler gates every shape boundary by its committed fact once the
|
|
400
|
+
// boundary's source node is FINISHED. Imported nodes stay FINISHED in the
|
|
401
|
+
// child, so their boundaries must inherit the parent's committed facts or
|
|
402
|
+
// the child hard-blocks at the first gated rank. Reset boundaries re-commit
|
|
403
|
+
// their own facts when their source nodes re-run.
|
|
404
|
+
const inheritedCommittedFacts = {};
|
|
405
|
+
if (isCanonicalFrontendShapeDag(new Map(childSpec.tasks.map((task) => [task.id, task])))) {
|
|
406
|
+
for (const entry of FRONTEND_SHAPE_BOUNDARIES) {
|
|
407
|
+
if (!importedSet.has(entry.sourceNodeId))
|
|
408
|
+
continue;
|
|
409
|
+
const parentFact = parentState.frontendShapeState?.committedFacts[entry.boundary];
|
|
410
|
+
if (parentFact) {
|
|
411
|
+
inheritedCommittedFacts[entry.boundary] = structuredClone(parentFact);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
348
415
|
const childState = {
|
|
349
416
|
version: 1,
|
|
350
417
|
title: parentSpec.title,
|
|
@@ -354,6 +421,18 @@ async function materializeChildStaging(input) {
|
|
|
354
421
|
status: "pending",
|
|
355
422
|
ranks,
|
|
356
423
|
nodes,
|
|
424
|
+
// Mirror createInitialRunState: a canonical frontend child must carry its
|
|
425
|
+
// own frozen topology binding or executeDag's binding assert rejects the
|
|
426
|
+
// staged child before its first node runs.
|
|
427
|
+
...(isCanonicalFrontendShapeDag(new Map(childSpec.tasks.map((task) => [task.id, task])))
|
|
428
|
+
? {
|
|
429
|
+
frontendShapeState: {
|
|
430
|
+
frozenTopology: buildFrontendTopologyBinding(childSpec.tasks.map((task) => task.id)),
|
|
431
|
+
revision: 0,
|
|
432
|
+
committedFacts: inheritedCommittedFacts,
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
: {}),
|
|
357
436
|
frontendRecoveryState: {
|
|
358
437
|
schemaVersion: 1,
|
|
359
438
|
phase: "child-running",
|
|
@@ -362,8 +441,11 @@ async function materializeChildStaging(input) {
|
|
|
362
441
|
parentRunId,
|
|
363
442
|
childRunId,
|
|
364
443
|
attemptId: recovery.attemptId,
|
|
365
|
-
attemptIndex: 1,
|
|
366
|
-
continuationCount: 1,
|
|
444
|
+
attemptIndex: recovery.attemptIndex + 1,
|
|
445
|
+
continuationCount: recovery.continuationCount + 1,
|
|
446
|
+
...(recovery.failureSource
|
|
447
|
+
? { failureSource: recovery.failureSource }
|
|
448
|
+
: {}),
|
|
367
449
|
revision: 0,
|
|
368
450
|
},
|
|
369
451
|
...(parentState.evaluation
|
|
@@ -432,7 +514,41 @@ export async function stageRecoveryChild(options) {
|
|
|
432
514
|
const parentRunDir = getDagRunDir(cwd, "active", parentRunId);
|
|
433
515
|
const parentState = await deps.readParentState(parentRunDir);
|
|
434
516
|
const parentSpec = await readDagRunSpec(parentRunDir);
|
|
435
|
-
|
|
517
|
+
let recovery = parentState.frontendRecoveryState;
|
|
518
|
+
if (options.recoveryIntent) {
|
|
519
|
+
const intent = options.recoveryIntent;
|
|
520
|
+
if (intent.parentRunId !== parentRunId ||
|
|
521
|
+
intent.requestId.length === 0 ||
|
|
522
|
+
intent.phase !== "child-staging" ||
|
|
523
|
+
intent.childRunId !== undefined ||
|
|
524
|
+
intent.revision !== 0) {
|
|
525
|
+
throw new Error("frontend recovery intent binding mismatch");
|
|
526
|
+
}
|
|
527
|
+
if (recovery?.requestId === intent.requestId) {
|
|
528
|
+
if (recovery.parentRunId !== intent.parentRunId ||
|
|
529
|
+
recovery.recoveryRootRunId !== intent.recoveryRootRunId ||
|
|
530
|
+
recovery.attemptIndex !== intent.attemptIndex ||
|
|
531
|
+
recovery.continuationCount !== intent.continuationCount ||
|
|
532
|
+
recovery.failureSource !== intent.failureSource) {
|
|
533
|
+
throw new Error("frontend recovery intent replay binding mismatch");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const previousAttemptIndex = recovery?.attemptIndex ?? 0;
|
|
538
|
+
const previousContinuationCount = recovery?.continuationCount ?? 0;
|
|
539
|
+
const expectedRoot = recovery?.recoveryRootRunId ?? parentRunId;
|
|
540
|
+
if (intent.recoveryRootRunId !== expectedRoot ||
|
|
541
|
+
intent.attemptIndex !== previousAttemptIndex ||
|
|
542
|
+
intent.continuationCount !== previousContinuationCount) {
|
|
543
|
+
throw new Error("frontend recovery intent lineage mismatch");
|
|
544
|
+
}
|
|
545
|
+
recovery = structuredClone(intent);
|
|
546
|
+
await deps.writeParentState(parentRunDir, {
|
|
547
|
+
...parentState,
|
|
548
|
+
frontendRecoveryState: recovery,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
436
552
|
if (!recovery) {
|
|
437
553
|
throw new Error(`parent run ${parentRunId} has no frontend recovery intent`);
|
|
438
554
|
}
|
|
@@ -440,17 +556,49 @@ export async function stageRecoveryChild(options) {
|
|
|
440
556
|
if (!requestId) {
|
|
441
557
|
throw new Error(`parent run ${parentRunId} recovery intent has no requestId`);
|
|
442
558
|
}
|
|
559
|
+
if (options.recoveryPlan) {
|
|
560
|
+
if (options.recoveryPlan.parentRunId !== parentRunId ||
|
|
561
|
+
options.recoveryPlan.requestId !== requestId ||
|
|
562
|
+
options.recoveryPlan.failureSource !== recovery.failureSource) {
|
|
563
|
+
throw new Error("frontend recovery plan binding mismatch");
|
|
564
|
+
}
|
|
565
|
+
const expectedPlan = computeFrontendRecoveryPlanForSource({
|
|
566
|
+
spec: parentSpec,
|
|
567
|
+
parentRunId,
|
|
568
|
+
requestId,
|
|
569
|
+
failureSource: recovery.failureSource,
|
|
570
|
+
});
|
|
571
|
+
if (JSON.stringify(expectedPlan.resetNodeIds) !==
|
|
572
|
+
JSON.stringify(options.recoveryPlan.resetNodeIds) ||
|
|
573
|
+
JSON.stringify(expectedPlan.importedNodeIds) !==
|
|
574
|
+
JSON.stringify(options.recoveryPlan.importedNodeIds)) {
|
|
575
|
+
throw new Error("frontend recovery plan drifted from parent DAG");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
443
578
|
// Idempotent short-circuit (D4.2): reserved childRunId + activated marker +
|
|
444
579
|
// existing child dir means the request was already fully committed.
|
|
445
580
|
if (recovery.childRunId) {
|
|
446
581
|
const existingMarker = await readFrontendRecoveryMarker(parentRunDir, requestId);
|
|
447
582
|
if (existingMarker) {
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
583
|
+
const existingChild = await locateDagRun(cwd, existingMarker.childRunId);
|
|
584
|
+
if (existingChild &&
|
|
585
|
+
(existingChild.lifecycle === "active" ||
|
|
586
|
+
existingChild.lifecycle === "paused" ||
|
|
587
|
+
existingChild.lifecycle === "completed")) {
|
|
588
|
+
await verifyThreeWayConsistency({
|
|
589
|
+
parentRunDir,
|
|
590
|
+
activeChildDir: existingChild.runDir,
|
|
591
|
+
marker: existingMarker,
|
|
592
|
+
parentRunId,
|
|
593
|
+
requestId,
|
|
594
|
+
recoveryRootRunId: recovery.recoveryRootRunId,
|
|
595
|
+
childRunId: existingMarker.childRunId,
|
|
596
|
+
deps,
|
|
597
|
+
});
|
|
450
598
|
return {
|
|
451
599
|
ok: true,
|
|
452
600
|
childRunId: existingMarker.childRunId,
|
|
453
|
-
childRunDir:
|
|
601
|
+
childRunDir: existingChild.runDir,
|
|
454
602
|
marker: existingMarker,
|
|
455
603
|
idempotentReplay: true,
|
|
456
604
|
};
|
|
@@ -461,8 +609,13 @@ export async function stageRecoveryChild(options) {
|
|
|
461
609
|
// buildDagRunId is deterministic for the same task title on the same day. A
|
|
462
610
|
// recovery child must never reuse its still-active parent directory, or the
|
|
463
611
|
// staging path aliases the parent and the required import manifest is absent.
|
|
612
|
+
// The default builder must receive `{ cwd }` (its options object), not the
|
|
613
|
+
// bare string: only the options form enables the collision-safe `-rN`
|
|
614
|
+
// suffix across active/completed/paused runs.
|
|
464
615
|
const childRunId = recovery.childRunId ?? await (async () => {
|
|
465
|
-
const generatedChildRunId =
|
|
616
|
+
const generatedChildRunId = options.buildChildRunId
|
|
617
|
+
? await options.buildChildRunId(parentSpec, cwd)
|
|
618
|
+
: await buildDagRunId(parentSpec, { cwd });
|
|
466
619
|
return generatedChildRunId === parentRunId
|
|
467
620
|
? `${generatedChildRunId}-recovery-${requestId.slice(0, 8)}`
|
|
468
621
|
: generatedChildRunId;
|
|
@@ -500,6 +653,7 @@ export async function stageRecoveryChild(options) {
|
|
|
500
653
|
parentSpec,
|
|
501
654
|
parentState,
|
|
502
655
|
recovery,
|
|
656
|
+
recoveryPlan: options.recoveryPlan,
|
|
503
657
|
requestId,
|
|
504
658
|
childRunId,
|
|
505
659
|
stagingDir,
|
|
@@ -70,8 +70,10 @@ function blobOf(input) {
|
|
|
70
70
|
].join("\n");
|
|
71
71
|
}
|
|
72
72
|
function pathSpreadScore(paths) {
|
|
73
|
+
const auxiliaryRoot = /^(?:test|tests|__tests__|e2e|cypress|spec|specs|docs|documentation)$/i;
|
|
73
74
|
const tops = new Set(paths
|
|
74
75
|
.map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
|
|
76
|
+
.filter((root) => !auxiliaryRoot.test(root))
|
|
75
77
|
.filter(Boolean));
|
|
76
78
|
return tops.size;
|
|
77
79
|
}
|
|
@@ -226,6 +226,7 @@ export function parseTargetSurfaceFacts(text) {
|
|
|
226
226
|
}
|
|
227
227
|
export const PLAN_LEDGER_FACT_KINDS = [
|
|
228
228
|
"target-surface",
|
|
229
|
+
"state-registry",
|
|
229
230
|
"component-choice",
|
|
230
231
|
"state-flow",
|
|
231
232
|
"data-flow",
|
|
@@ -484,7 +485,22 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
484
485
|
}
|
|
485
486
|
const verificationTargets = verificationTargetOrder
|
|
486
487
|
.map((id) => verificationTargetById.get(id))
|
|
487
|
-
.filter((entry) => Boolean(entry))
|
|
488
|
+
.filter((entry) => Boolean(entry))
|
|
489
|
+
.map((entry) => {
|
|
490
|
+
// Coverage precedes UX. A later state-flow fact explicitly naming
|
|
491
|
+
// this VT establishes the reverse binding without another model call.
|
|
492
|
+
// No path/name heuristics: unbound or ambiguous decisions stay subject
|
|
493
|
+
// to canonical validation. Recompute from live states after removals.
|
|
494
|
+
const boundStates = [...stateByName.values()]
|
|
495
|
+
.filter((state) => state.applicable === true &&
|
|
496
|
+
asStringArray(state.verificationTargetIds).includes(asString(entry.id)))
|
|
497
|
+
.map((state) => asString(state.name));
|
|
498
|
+
if (boundStates.length === 0)
|
|
499
|
+
return entry;
|
|
500
|
+
return { ...entry, uiStates: [...new Set([
|
|
501
|
+
...asStringArray(entry.uiStates), ...boundStates,
|
|
502
|
+
])].sort() };
|
|
503
|
+
});
|
|
488
504
|
if (verificationTargets.length > 0) {
|
|
489
505
|
patch.verificationTargets = verificationTargets;
|
|
490
506
|
}
|
|
@@ -46,8 +46,15 @@ const EXECUTABLE_SHAPE_RANK = {
|
|
|
46
46
|
high: 3,
|
|
47
47
|
};
|
|
48
48
|
function pathSpreadScore(paths) {
|
|
49
|
+
// Verification-only roots are routinely authorized together with the
|
|
50
|
+
// implementation root (for example `src/**` + `test/**`). They do not make
|
|
51
|
+
// a UI change cross-domain, so exclude them from the topology spread signal.
|
|
52
|
+
// Keep product packages and source roots intact: `apps/**` + `packages/**`
|
|
53
|
+
// must still count as a real spread and prevent an unsafe small topology.
|
|
54
|
+
const auxiliaryRoot = /^(?:test|tests|__tests__|e2e|cypress|spec|specs|docs|documentation)$/i;
|
|
49
55
|
const tops = new Set(paths
|
|
50
56
|
.map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
|
|
57
|
+
.filter((root) => !auxiliaryRoot.test(root))
|
|
51
58
|
.filter(Boolean));
|
|
52
59
|
return tops.size;
|
|
53
60
|
}
|
|
@@ -65,6 +72,13 @@ function collectEscalationSignals(blob, supervised) {
|
|
|
65
72
|
}
|
|
66
73
|
return [...new Set(signals)];
|
|
67
74
|
}
|
|
75
|
+
/** Negation applies to the API mention itself, never to unrelated Mock policy. */
|
|
76
|
+
function hasFrontendRemoteSignal(blob) {
|
|
77
|
+
const remaining = blob
|
|
78
|
+
.replace(/\bno\s+api\b/gi, "")
|
|
79
|
+
.replace(/无\s*(?:api\b|接口)/gi, "");
|
|
80
|
+
return /\b(api|fetch|axios|graphql|endpoint)\b|远程|接口/i.test(remaining);
|
|
81
|
+
}
|
|
68
82
|
function resolveBaselineFrontendTaskShape(input) {
|
|
69
83
|
const blob = [
|
|
70
84
|
input.requirementMarkdown ?? "",
|
|
@@ -84,9 +98,7 @@ function resolveBaselineFrontendTaskShape(input) {
|
|
|
84
98
|
const paths = input.allowedPaths ?? [];
|
|
85
99
|
const spread = pathSpreadScore(paths);
|
|
86
100
|
const concentrated = spread <= 1 && paths.length <= 4;
|
|
87
|
-
const hasApiSignal =
|
|
88
|
-
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
89
|
-
/无\s*api|无\s*接口/i.test(blob));
|
|
101
|
+
const hasApiSignal = hasFrontendRemoteSignal(blob);
|
|
90
102
|
const complexity = (input.complexity ?? "").toLowerCase();
|
|
91
103
|
// split-required wins over everything: it is a termination/escalation
|
|
92
104
|
// signal, not an executable topology.
|
|
@@ -266,9 +278,7 @@ export function deriveFrontendRuntimeShapeFacts(input) {
|
|
|
266
278
|
const supervised = /\bsupervised\b/i.test(blob);
|
|
267
279
|
const escalationSignals = collectEscalationSignals(blob, supervised);
|
|
268
280
|
const paths = input.allowedPaths ?? [];
|
|
269
|
-
const hasApiSignal =
|
|
270
|
-
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
271
|
-
/无\s*api|无\s*接口/i.test(blob));
|
|
281
|
+
const hasApiSignal = hasFrontendRemoteSignal(blob);
|
|
272
282
|
const requiresFull = escalationSignals.length > 0 || pathSpreadScore(paths) > 1 || hasApiSignal;
|
|
273
283
|
return {
|
|
274
284
|
splitRequired: input.splitRequired,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/** Observe supported frozen Vitest commands without changing their selection. */
|
|
5
|
+
export async function withFrontendTestReporter(input) {
|
|
6
|
+
// Compound commands/wrappers need their own adapter: never append flags to
|
|
7
|
+
// a different shell command, change selection, or override a user reporter.
|
|
8
|
+
if (/[;&|<>`\n\r$]/.test(input.command) || /--(?:reporter|outputFile)/.test(input.command))
|
|
9
|
+
return undefined;
|
|
10
|
+
const direct = /^(?:(?:npx|pnpm exec|yarn exec)\s+)?vitest\s+run(?:\s|$)/.test(input.command.trim());
|
|
11
|
+
const npm = /^npm\s+(?:run\s+([\w:-]+)|(test))(?=\s|$)(.*)$/.exec(input.command.trim());
|
|
12
|
+
let separator = "";
|
|
13
|
+
if (!direct) {
|
|
14
|
+
if (!npm)
|
|
15
|
+
return undefined;
|
|
16
|
+
let manifest;
|
|
17
|
+
try {
|
|
18
|
+
manifest = JSON.parse(await readFile(path.join(input.cwd, "package.json"), "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const script = manifest.scripts?.[npm[1] ?? "test"] ?? "";
|
|
24
|
+
if (!/^vitest\s+run(?:\s|$)/.test(script) || /[;&|<>`\n\r$]|--(?:reporter|outputFile)/.test(script))
|
|
25
|
+
return undefined;
|
|
26
|
+
if (npm[3]?.trim() && !/^--(?:\s|$)/.test(npm[3].trim()))
|
|
27
|
+
return undefined;
|
|
28
|
+
separator = npm[3]?.trim() ? "" : " --";
|
|
29
|
+
}
|
|
30
|
+
const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
31
|
+
return `${input.command}${separator} --reporter=json --outputFile=${quote(input.reportPath)}`;
|
|
32
|
+
}
|
|
33
|
+
const resultSchema = z.object({
|
|
34
|
+
testResults: z.array(z.object({
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
assertionResults: z.array(z.object({
|
|
37
|
+
title: z.string(), fullName: z.string().optional(),
|
|
38
|
+
ancestorTitles: z.array(z.string()).optional(), status: z.string(),
|
|
39
|
+
})),
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
export function parseFrontendTestExecutionReport(raw, cwd) {
|
|
43
|
+
return resultSchema.parse(raw).testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
|
|
44
|
+
file: path.relative(cwd, path.resolve(cwd, suite.name)).replace(/\\/g, "/"),
|
|
45
|
+
title: test.fullName ?? [...(test.ancestorTitles ?? []), test.title].join(" "),
|
|
46
|
+
status: test.status,
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
@@ -95,6 +95,8 @@ export const CONTRACT_FACT_KINDS = [
|
|
|
95
95
|
"open-question",
|
|
96
96
|
"split-proposal",
|
|
97
97
|
"openspec-selection",
|
|
98
|
+
"ui-state-declaration",
|
|
99
|
+
"required-deliverables",
|
|
98
100
|
"contract-finalized",
|
|
99
101
|
];
|
|
100
102
|
export const CONTRACT_TERMINAL_FACT_KINDS = ["contract-finalized"];
|
|
@@ -124,6 +126,7 @@ export const PLAN_RECORD_FACT_KINDS = [
|
|
|
124
126
|
"plan-requirement",
|
|
125
127
|
"plan-verification-target",
|
|
126
128
|
"plan-evidence-gap",
|
|
129
|
+
"state-registry",
|
|
127
130
|
];
|
|
128
131
|
/**
|
|
129
132
|
* A+B (AC-009): typed issue category shared by review and design change
|
|
@@ -152,6 +152,7 @@ export function extractFrontendTestTitles(content) {
|
|
|
152
152
|
const tokens = lexFrontendTestSource(content);
|
|
153
153
|
const titles = [];
|
|
154
154
|
const configurationModifiers = new Set(["each", "runIf", "skipIf"]);
|
|
155
|
+
const nonExecutingModifiers = new Set(["skip", "todo", "fails"]);
|
|
155
156
|
function closingParenIndex(openIndex) {
|
|
156
157
|
let depth = 0;
|
|
157
158
|
for (let cursor = openIndex; cursor < tokens.length; cursor += 1) {
|
|
@@ -177,12 +178,21 @@ export function extractFrontendTestTitles(content) {
|
|
|
177
178
|
}
|
|
178
179
|
let cursor = index + 1;
|
|
179
180
|
let hasConfigurationModifier = false;
|
|
181
|
+
let hasNonExecutingModifier = false;
|
|
182
|
+
let conditionalModifier;
|
|
180
183
|
while (tokens[cursor]?.type === "punctuation" &&
|
|
181
184
|
tokens[cursor]?.value === "." &&
|
|
182
185
|
tokens[cursor + 1]?.type === "identifier") {
|
|
183
186
|
if (tokens[cursor + 1]?.type === "identifier" &&
|
|
184
187
|
configurationModifiers.has(tokens[cursor + 1].value)) {
|
|
185
188
|
hasConfigurationModifier = true;
|
|
189
|
+
if (["runIf", "skipIf"].includes(tokens[cursor + 1].value)) {
|
|
190
|
+
conditionalModifier = tokens[cursor + 1].value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (tokens[cursor + 1]?.type === "identifier" &&
|
|
194
|
+
nonExecutingModifiers.has(tokens[cursor + 1].value)) {
|
|
195
|
+
hasNonExecutingModifier = true;
|
|
186
196
|
}
|
|
187
197
|
cursor += 2;
|
|
188
198
|
}
|
|
@@ -191,6 +201,13 @@ export function extractFrontendTestTitles(content) {
|
|
|
191
201
|
continue;
|
|
192
202
|
}
|
|
193
203
|
if (hasConfigurationModifier) {
|
|
204
|
+
if (conditionalModifier && tokens[cursor + 1]?.type === "identifier" &&
|
|
205
|
+
tokens[cursor + 2]?.value === ")") {
|
|
206
|
+
const condition = tokens[cursor + 1].value;
|
|
207
|
+
if ((conditionalModifier === "runIf" && condition === "false") ||
|
|
208
|
+
(conditionalModifier === "skipIf" && condition === "true"))
|
|
209
|
+
hasNonExecutingModifier = true;
|
|
210
|
+
}
|
|
194
211
|
const close = closingParenIndex(cursor);
|
|
195
212
|
if (close === null ||
|
|
196
213
|
tokens[close + 1]?.type !== "punctuation" ||
|
|
@@ -199,6 +216,11 @@ export function extractFrontendTestTitles(content) {
|
|
|
199
216
|
}
|
|
200
217
|
cursor = close + 1;
|
|
201
218
|
}
|
|
219
|
+
if (hasNonExecutingModifier) {
|
|
220
|
+
// A skipped suite also skips every nested test title in its body.
|
|
221
|
+
index = closingParenIndex(cursor) ?? tokens.length;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
202
224
|
if (tokens[cursor + 1]?.type !== "string")
|
|
203
225
|
continue;
|
|
204
226
|
titles.push(tokens[cursor + 1].value);
|
|
@@ -226,7 +248,7 @@ async function evaluateTargetFileTrace(input) {
|
|
|
226
248
|
workspaceRoot: input.workspaceRoot,
|
|
227
249
|
file: input.target.file,
|
|
228
250
|
});
|
|
229
|
-
if (fileIssues.length || input.target.
|
|
251
|
+
if (fileIssues.length || input.target.mode === "static") {
|
|
230
252
|
return { issues: fileIssues };
|
|
231
253
|
}
|
|
232
254
|
const absolute = path.resolve(input.workspaceRoot, input.target.file);
|
|
@@ -261,10 +283,11 @@ export async function evaluateFrontendWriterTracePreflight(input) {
|
|
|
261
283
|
}
|
|
262
284
|
targets.push({
|
|
263
285
|
id: target.id,
|
|
286
|
+
commandId: target.commandId,
|
|
264
287
|
commandLabel: target.commandLabel,
|
|
265
288
|
file: target.file,
|
|
266
289
|
...(target.symbol ? { symbol: target.symbol } : {}),
|
|
267
|
-
...(target.
|
|
290
|
+
...(target.mode !== "static" ? { traceKey: target.id } : {}),
|
|
268
291
|
status: fileTrace.issues.length ? "failed" : "ok",
|
|
269
292
|
matchedNodeIds: [],
|
|
270
293
|
issues: fileTrace.issues,
|
|
@@ -301,15 +324,25 @@ export async function evaluateVerificationTargets(input) {
|
|
|
301
324
|
target,
|
|
302
325
|
});
|
|
303
326
|
issues.push(...fileTrace.issues);
|
|
327
|
+
const executed = input.executedTests?.get(target.commandLabel);
|
|
328
|
+
if (target.mode !== "static" && executed !== undefined) {
|
|
329
|
+
// safePath permits "./" prefixes; reporter files are cwd-relative.
|
|
330
|
+
const targetFile = path.posix.normalize(target.file.replace(/\\/g, "/"));
|
|
331
|
+
const matching = executed.filter((test) => test.file === targetFile && traceKeyMatchesTitle(target.id, test.title));
|
|
332
|
+
if (matching.length === 0 || matching.some((test) => test.status !== "passed")) {
|
|
333
|
+
issues.push(`behavior target was not fully executed and passed: ${target.id} in ${target.file}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
304
336
|
const status = issues.length ? "failed" : "ok";
|
|
305
337
|
if (issues.length)
|
|
306
338
|
hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
|
|
307
339
|
targets.push({
|
|
308
340
|
id: target.id,
|
|
341
|
+
commandId: target.commandId,
|
|
309
342
|
commandLabel: target.commandLabel,
|
|
310
343
|
file: target.file,
|
|
311
344
|
symbol: target.symbol,
|
|
312
|
-
...(target.
|
|
345
|
+
...(target.mode !== "static" ? { traceKey: target.id } : {}),
|
|
313
346
|
status,
|
|
314
347
|
matchedNodeIds: owners ?? [],
|
|
315
348
|
issues,
|
|
@@ -383,7 +416,7 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
383
416
|
.join("; ")}`);
|
|
384
417
|
}
|
|
385
418
|
const contract = parsed.data;
|
|
386
|
-
const requiresBehaviorVerification = contract.verificationTargets.some((target) => target.
|
|
419
|
+
const requiresBehaviorVerification = contract.verificationTargets.some((target) => target.mode !== "static");
|
|
387
420
|
// Prefer post-repair reverify nodes when present (M3); else initial verify nodes (M2).
|
|
388
421
|
const staticCandidates = [
|
|
389
422
|
"frontend-static-reverify-shell",
|
|
@@ -471,6 +504,7 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
471
504
|
contract,
|
|
472
505
|
labelOwners,
|
|
473
506
|
workspaceRoot: input.workspaceRoot,
|
|
507
|
+
executedTests: input.evidence?.behavior.executedTests,
|
|
474
508
|
});
|
|
475
509
|
if (hardIssues.length) {
|
|
476
510
|
throw new Error(`trace: ${hardIssues.join("; ")}`);
|