@tea-agent/loop-agent 0.42.0-next.9 → 0.43.0-next.1
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 +65 -59
- package/dist/application/dag/run-dag.js +3 -2
- 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 +1514 -175
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/shell-executor.js +131 -54
- 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/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-plan-protocol.js +104 -0
- 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 +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +142 -16
- 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 +2 -2
- package/dist/workflows/dag/init-hybrid.js +57 -28
- package/dist/workflows/dag/node-execution.js +201 -85
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -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 +221 -18
- 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 +220 -18
- 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",
|
|
@@ -27,6 +27,7 @@ export function deriveFrontendRecoveryFailureSource(result) {
|
|
|
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);
|
|
30
31
|
* - other admission failures → reset writer admission (+ descendants);
|
|
31
32
|
*
|
|
32
33
|
* Everything upstream of the failure source is imported as verified parent facts.
|
|
@@ -62,7 +63,9 @@ function computeFrontendRecoveryPlanForSource(input) {
|
|
|
62
63
|
// reset from implement: the recovery re-implements and re-verifies.
|
|
63
64
|
const resetRoot = input.failureSource === "frontend-verify-shell"
|
|
64
65
|
? "frontend-implement-pi"
|
|
65
|
-
: input.failureSource
|
|
66
|
+
: input.failureSource === "frontend-review-pi"
|
|
67
|
+
? "frontend-implement-pi"
|
|
68
|
+
: input.failureSource;
|
|
66
69
|
const resetNodeIds = computeResetClosure(input.spec, resetRoot);
|
|
67
70
|
const resetSet = new Set(resetNodeIds);
|
|
68
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,34 +275,52 @@ 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
|
|
279
281
|
// children otherwise copy the old spec and only rerun design review, which
|
|
280
282
|
// deterministically re-rejects the unchanged plan. Bind the parent's typed
|
|
281
283
|
// findings into the child spec as ordinary rerun feedback.
|
|
282
284
|
let childSpec = parentSpec;
|
|
283
|
-
if (recovery.failureSource === "frontend-plan-pi"
|
|
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;
|
|
284
291
|
try {
|
|
285
292
|
const { deriveDagRerunFeedback } = await import("./rerun-feedback.js");
|
|
286
293
|
const taskId = parentSpec.taskContractBinding?.taskId ??
|
|
287
294
|
parentSpec.sourceBinding?.taskId ??
|
|
288
295
|
parentRunId;
|
|
289
|
-
|
|
296
|
+
feedback = await deriveDagRerunFeedback({
|
|
290
297
|
parentRunId,
|
|
291
298
|
taskId,
|
|
292
299
|
runDir: parentRunDir,
|
|
293
300
|
spec: parentSpec,
|
|
294
301
|
state: parentState,
|
|
295
302
|
});
|
|
296
|
-
if (
|
|
297
|
-
|
|
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
|
+
}
|
|
298
310
|
}
|
|
299
311
|
catch (error) {
|
|
300
|
-
|
|
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
|
|
301
321
|
// findings. Fail closed instead of deterministically re-running the
|
|
302
322
|
// unchanged plan and producing another rejection.
|
|
303
|
-
throw new Error(`frontend recovery feedback unavailable: ${
|
|
323
|
+
throw new Error(`frontend recovery feedback unavailable: no typed ${reviewRecovery ? "review" : "design"} findings were found for recovery`);
|
|
304
324
|
}
|
|
305
325
|
}
|
|
306
326
|
await writeJsonVerified(stagingDir, "run.json", childSpec);
|
|
@@ -308,7 +328,10 @@ async function materializeChildStaging(input) {
|
|
|
308
328
|
// (frontend-implement-pi) instead of the candidate producer; everything
|
|
309
329
|
// upstream is imported as verified parent facts.
|
|
310
330
|
let plan;
|
|
311
|
-
if (
|
|
331
|
+
if (recoveryPlan) {
|
|
332
|
+
plan = recoveryPlan;
|
|
333
|
+
}
|
|
334
|
+
else if (recovery.failureSource) {
|
|
312
335
|
plan = computeFrontendRecoveryPlanForSource({
|
|
313
336
|
spec: parentSpec,
|
|
314
337
|
parentRunId,
|
|
@@ -373,6 +396,22 @@ async function materializeChildStaging(input) {
|
|
|
373
396
|
newRunDir: stagingDir,
|
|
374
397
|
relativePath: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
|
|
375
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
|
+
}
|
|
376
415
|
const childState = {
|
|
377
416
|
version: 1,
|
|
378
417
|
title: parentSpec.title,
|
|
@@ -382,6 +421,18 @@ async function materializeChildStaging(input) {
|
|
|
382
421
|
status: "pending",
|
|
383
422
|
ranks,
|
|
384
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
|
+
: {}),
|
|
385
436
|
frontendRecoveryState: {
|
|
386
437
|
schemaVersion: 1,
|
|
387
438
|
phase: "child-running",
|
|
@@ -390,8 +441,11 @@ async function materializeChildStaging(input) {
|
|
|
390
441
|
parentRunId,
|
|
391
442
|
childRunId,
|
|
392
443
|
attemptId: recovery.attemptId,
|
|
393
|
-
attemptIndex: 1,
|
|
394
|
-
continuationCount: 1,
|
|
444
|
+
attemptIndex: recovery.attemptIndex + 1,
|
|
445
|
+
continuationCount: recovery.continuationCount + 1,
|
|
446
|
+
...(recovery.failureSource
|
|
447
|
+
? { failureSource: recovery.failureSource }
|
|
448
|
+
: {}),
|
|
395
449
|
revision: 0,
|
|
396
450
|
},
|
|
397
451
|
...(parentState.evaluation
|
|
@@ -460,7 +514,41 @@ export async function stageRecoveryChild(options) {
|
|
|
460
514
|
const parentRunDir = getDagRunDir(cwd, "active", parentRunId);
|
|
461
515
|
const parentState = await deps.readParentState(parentRunDir);
|
|
462
516
|
const parentSpec = await readDagRunSpec(parentRunDir);
|
|
463
|
-
|
|
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
|
+
}
|
|
464
552
|
if (!recovery) {
|
|
465
553
|
throw new Error(`parent run ${parentRunId} has no frontend recovery intent`);
|
|
466
554
|
}
|
|
@@ -468,17 +556,49 @@ export async function stageRecoveryChild(options) {
|
|
|
468
556
|
if (!requestId) {
|
|
469
557
|
throw new Error(`parent run ${parentRunId} recovery intent has no requestId`);
|
|
470
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
|
+
}
|
|
471
578
|
// Idempotent short-circuit (D4.2): reserved childRunId + activated marker +
|
|
472
579
|
// existing child dir means the request was already fully committed.
|
|
473
580
|
if (recovery.childRunId) {
|
|
474
581
|
const existingMarker = await readFrontendRecoveryMarker(parentRunDir, requestId);
|
|
475
582
|
if (existingMarker) {
|
|
476
|
-
const
|
|
477
|
-
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
|
+
});
|
|
478
598
|
return {
|
|
479
599
|
ok: true,
|
|
480
600
|
childRunId: existingMarker.childRunId,
|
|
481
|
-
childRunDir:
|
|
601
|
+
childRunDir: existingChild.runDir,
|
|
482
602
|
marker: existingMarker,
|
|
483
603
|
idempotentReplay: true,
|
|
484
604
|
};
|
|
@@ -489,8 +609,13 @@ export async function stageRecoveryChild(options) {
|
|
|
489
609
|
// buildDagRunId is deterministic for the same task title on the same day. A
|
|
490
610
|
// recovery child must never reuse its still-active parent directory, or the
|
|
491
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.
|
|
492
615
|
const childRunId = recovery.childRunId ?? await (async () => {
|
|
493
|
-
const generatedChildRunId =
|
|
616
|
+
const generatedChildRunId = options.buildChildRunId
|
|
617
|
+
? await options.buildChildRunId(parentSpec, cwd)
|
|
618
|
+
: await buildDagRunId(parentSpec, { cwd });
|
|
494
619
|
return generatedChildRunId === parentRunId
|
|
495
620
|
? `${generatedChildRunId}-recovery-${requestId.slice(0, 8)}`
|
|
496
621
|
: generatedChildRunId;
|
|
@@ -528,6 +653,7 @@ export async function stageRecoveryChild(options) {
|
|
|
528
653
|
parentSpec,
|
|
529
654
|
parentState,
|
|
530
655
|
recovery,
|
|
656
|
+
recoveryPlan: options.recoveryPlan,
|
|
531
657
|
requestId,
|
|
532
658
|
childRunId,
|
|
533
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("; ")}`);
|
|
@@ -156,7 +156,7 @@ export function collectVerificationCommandFiles(commands) {
|
|
|
156
156
|
}
|
|
157
157
|
return [...files].sort();
|
|
158
158
|
}
|
|
159
|
-
/** Derive the concrete writeSet from implementation targets ∪
|
|
159
|
+
/** Derive the concrete writeSet from implementation targets ∪ behavior
|
|
160
160
|
* verification target files. Entries are de-duplicated and lexicographically
|
|
161
161
|
* sorted (frozen). */
|
|
162
162
|
export function deriveWriteSet(contract, options) {
|
|
@@ -164,7 +164,7 @@ export function deriveWriteSet(contract, options) {
|
|
|
164
164
|
const candidates = [
|
|
165
165
|
...contract.targets.files,
|
|
166
166
|
...contract.verificationTargets
|
|
167
|
-
.filter((target) => target.
|
|
167
|
+
.filter((target) => target.mode !== "static")
|
|
168
168
|
.map((target) => target.file),
|
|
169
169
|
];
|
|
170
170
|
// Expand glob entries (e.g. `src/**` inherited from the skeleton) against
|