@tea-agent/loop-agent 0.42.0-next.14 → 0.42.0-next.16
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 +19 -3
- package/dist/application/task-lifecycle/advance.js +9 -4
- package/dist/build-stamp.json +3 -3
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +1437 -154
- package/dist/executors/shell-executor.js +107 -35
- 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/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +8 -0
- 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/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BGMbAdAG.js → abnfDiagram-N423BO3Z-CgXb0EVO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CJu1LND8.js → arc-DN59MZqN.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BmJh9gMQ.js → architectureDiagram-T3A2C74G-BUk3sWpn.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BWjaX3ZF.js → blockDiagram-VBNYF7ZC-IH-cPBFE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CLEOAxLB.js → c4Diagram-5PPSVZJV-BJWf1nGo.js} +1 -1
- package/dist/worker/console/static/assets/channel-i1DjpDIw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CydnyQUX.js → chunk-2GRJ4B5K-BCPb5H0y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-tjJ1IoZ1.js → chunk-2Q5K7J3B-Cfs3jeW2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DR358Phd.js → chunk-5RXB4S5H-BhQI1B_T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-B-LuWFZD.js → chunk-5VM5RSS4-Cc_m4gyV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-FG53fA6v.js → chunk-6Q2QTUOP-JPqDS3IV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bkt-w8SZ.js → chunk-GF5L2VYU-AeGX6EAg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-WxHo0z2I.js → chunk-JWPE2WC7-pEyTjekV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CqHrBJhp.js → chunk-KBJHAD2P-Bi5UopKd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-D4MVae_f.js → chunk-RYQCIY6F-FkGFJxgQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CamBh0Ll.js → chunk-XXDRQBXY-D-7LGrV2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C3qheCuW.js → cose-bilkent-JH36ORCC-CCEDNDaX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DQlWfkS7.js → cynefin-VYW2F7L2-DDd-t7fm.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BAxa9rFb.js → cynefinDiagram-MW4NZA55-LWe4Ikzr.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-YM09nLJw.js → dagre-VZM6K2ZE-BBCZScc-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-I4W9EJj2.js → diagram-7IWD3JNH-B6liRVig.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DH8eJ8Ff.js → diagram-B4RE2ZJO-DO_hIrpW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D0D0ntDW.js → diagram-LBJQPF4R-dmCy93uR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BIISKq1s.js → diagram-Q27KOJAE-D6mFTIxP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3--xyAWj0w.js → diagram-UB23O5K3-DCteVUfY.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B3sYXLJ4.js → ebnfDiagram-BXEA7PRR-DTv530VK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-zRSYWhJP.js → erDiagram-JOGREHBK-DCXDMxCr.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BUYlie03.js → flowDiagram-UKHOOZJN-BwEBLOJh.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DP4kzzrM.js → ganttDiagram-PKOTCBZU-Ck-Vymjm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CAPoUxTF.js → gitGraphDiagram-DS77QQ5N-Hqs6X_3L.js} +1 -1
- package/dist/worker/console/static/assets/{index-CWhTyxvc.js → index-BmMi-Bve.js} +71 -71
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BXPfjekV.js → infoDiagram-6WML65LV-Cjc6M9eg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-aOI3pV6t.js → ishikawaDiagram-WSZJBQD7-CPchYZMl.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DAPZuvi4.js → journeyDiagram-NVQOT4AX-BABdJNNC.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-atwDzL3C.js → kanban-definition-27J2QSJJ-1oXhbM4j.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D5E09qji.js → linear-PTmQ9LkV.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CE4idY-s.js → mermaid.core-B6Lxil0V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CbZxtPca.js → mindmap-definition-FAOFIHXS-BUqjNGEw.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-De-FYEte.js → pegDiagram-VL7TDLO6-8D1N-orJ.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Bs0USobz.js → pieDiagram-7S7Q4E2Y-BlPMN9d9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bq3ZrGCJ.js → quadrantDiagram-CIZ2JOQS-BIL5YDes.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bk8ojyIj.js → railroadDiagram-AXF67PYL-Dqmil3ie.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CGhoyws1.js → requirementDiagram-LRYGKXZP-C_-Dp4mH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DmVSQs3b.js → sankeyDiagram-W5VNT64P-C0s5-bQS.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C80h1u0Q.js → sequenceDiagram-SI44F4Z6-DiVCHPfi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CcbksQVZ.js → sizeCapture-X5ZJPWSS-CxQ9WVvx.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CFTTTQKj.js → stateDiagram-OKZ733FA-D9mFHA-Y.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqXQxfFC.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Cw7Jr7Tg.js → swimlanes-SLNWSIFB-DDnC5l-M.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CXwWNRJl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CVcdTVa5.js → timeline-definition-Z64GVDOM-C8QySPC-.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BpLbmWp9.js → vennDiagram-T6HMQDX7-BeYdDiz6.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-6PXfbwj-.js → wardleyDiagram-T6FBY63Y-D8hjFWLH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Bjkcz4ih.js → xychartDiagram-ELKLHX3M-ClqG_JGK.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- 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 +100 -15
- package/dist/workflows/dag/frontend-implementation-contract.js +56 -8
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- 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 +32 -0
- package/dist/workflows/dag/init-hybrid.js +16 -13
- package/dist/workflows/dag/node-execution.js +178 -88
- 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 +7 -0
- package/dist/workflows/dag/retry-policy.js +16 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +67 -7
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- 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/frontend-implementation-contract.schema.json +34 -2
- package/package.json +2 -3
- package/skills/frontend-plan/SKILL.md +14 -1
- package/skills/frontend-plan/references/decision-contract.md +84 -10
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/dist/worker/console/static/assets/channel-DOsaUk1k.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D4-D49vY.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D4-D49vY.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C4YCesmi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-lN2U_DLl.js +0 -8
|
@@ -28,31 +28,37 @@ function sortFindings(findings) {
|
|
|
28
28
|
* visual UI-state dimension (it cannot cover behavioural interactions). */
|
|
29
29
|
export function checkUiDesignCoverage(contract) {
|
|
30
30
|
const findings = [];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const
|
|
31
|
+
// Explicit per-id coverage (dogfood r-plan-quality): one choice covers an
|
|
32
|
+
// interaction/state when its purpose equals the name OR the choice lists
|
|
33
|
+
// the name in covers. The old wildcards are gone — a single reuse-existing
|
|
34
|
+
// row no longer passes every interaction, and a non-empty stylingStrategy
|
|
35
|
+
// no longer passes every state.
|
|
36
|
+
const covered = new Set();
|
|
37
|
+
for (const choice of contract.uiComponentChoices ?? []) {
|
|
38
|
+
if (choice.purpose.trim())
|
|
39
|
+
covered.add(choice.purpose);
|
|
40
|
+
for (const id of choice.covers ?? []) {
|
|
41
|
+
if (id.trim())
|
|
42
|
+
covered.add(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
38
45
|
for (const state of contract.uiStates) {
|
|
39
46
|
if (!state.applicable)
|
|
40
47
|
continue;
|
|
41
|
-
|
|
42
|
-
if (!covered) {
|
|
48
|
+
if (!covered.has(state.name)) {
|
|
43
49
|
findings.push({
|
|
44
50
|
code: "ui-design-coverage-missing",
|
|
45
|
-
message: `applicable UI state "${state.name}"
|
|
51
|
+
message: `applicable UI state "${state.name}" is not covered by any uiComponentChoices purpose or covers entry`,
|
|
46
52
|
path: state.name,
|
|
47
53
|
});
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
for (const interaction of contract.interactions) {
|
|
51
|
-
if (
|
|
57
|
+
if (covered.has(interaction.name))
|
|
52
58
|
continue;
|
|
53
59
|
findings.push({
|
|
54
60
|
code: "ui-design-coverage-missing",
|
|
55
|
-
message: `interaction "${interaction.name}"
|
|
61
|
+
message: `interaction "${interaction.name}" is not covered by any uiComponentChoices purpose or covers entry`,
|
|
56
62
|
path: interaction.name,
|
|
57
63
|
});
|
|
58
64
|
}
|
|
@@ -158,7 +164,7 @@ export function checkMockStrategy(contract, allowedMockStrategies) {
|
|
|
158
164
|
* A component specReference must still not point into clearly non-component
|
|
159
165
|
* spec areas — templates and the ai_workspace governance subtree cannot be the
|
|
160
166
|
* authoritative component definition. */
|
|
161
|
-
export function checkComponentChoice(contract, componentSpecCandidatePaths) {
|
|
167
|
+
export function checkComponentChoice(contract, componentSpecCandidatePaths, options) {
|
|
162
168
|
const findings = [];
|
|
163
169
|
const candidates = new Set(componentSpecCandidatePaths.map(normalizeRelativePath));
|
|
164
170
|
const isNonComponentSpecArea = (path) => {
|
|
@@ -200,6 +206,23 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
|
|
|
200
206
|
});
|
|
201
207
|
}
|
|
202
208
|
}
|
|
209
|
+
else if (choice.decision === "reuse-existing") {
|
|
210
|
+
if (!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
|
|
211
|
+
findings.push({
|
|
212
|
+
code: "component-reuse-evidence-missing",
|
|
213
|
+
message: `reuse-existing component choice #${index} is missing evidencePath naming the existing repo file that proves the reuse; a greenfield path must use decision=new`,
|
|
214
|
+
path: choice.purpose,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else if (options?.hasFreshPathEvidence &&
|
|
218
|
+
!options.hasFreshPathEvidence(choice.evidencePath)) {
|
|
219
|
+
findings.push({
|
|
220
|
+
code: "component-reuse-path-not-fresh",
|
|
221
|
+
message: `reuse-existing component choice #${index} evidencePath "${choice.evidencePath}" has no fresh existence evidence (path not found); reuse requires an existing file — use decision=new for greenfield paths`,
|
|
222
|
+
path: choice.evidencePath,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
203
226
|
else if (choice.decision === "new") {
|
|
204
227
|
if (!choice.specReference || !choice.specReference.path) {
|
|
205
228
|
findings.push({
|
|
@@ -236,7 +259,7 @@ export function checkTargetPaths(contract, writeSetPatterns) {
|
|
|
236
259
|
if (contract.targets.files.length > MAX_WRITE_SET_FILES) {
|
|
237
260
|
findings.push({
|
|
238
261
|
code: "write-set-too-large",
|
|
239
|
-
message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task
|
|
262
|
+
message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task at the Contract boundary so no single implement node exceeds the bound; preserve all required files`,
|
|
240
263
|
});
|
|
241
264
|
}
|
|
242
265
|
for (const file of contract.targets.files) {
|
|
@@ -371,6 +394,64 @@ export function checkDesignApproval(designApproval) {
|
|
|
371
394
|
}
|
|
372
395
|
return [];
|
|
373
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* UX vocabulary collisions (item ③, dogfood dag-1788504923861-0f7b17a9):
|
|
399
|
+
* prioritize-planner-task / prioritizePlannerTask / prioritize-planner-tasks
|
|
400
|
+
* are one behavior renamed three ways. Case/separator-insensitive duplicates
|
|
401
|
+
* inside interactions or inside uiStates are blocking.
|
|
402
|
+
*/
|
|
403
|
+
export function checkUxNameCollisions(contract) {
|
|
404
|
+
const findings = [];
|
|
405
|
+
const normalize = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
406
|
+
const collisions = (names, kind) => {
|
|
407
|
+
const byNormalized = new Map();
|
|
408
|
+
const found = [];
|
|
409
|
+
for (const name of names) {
|
|
410
|
+
const key = normalize(name);
|
|
411
|
+
if (!key)
|
|
412
|
+
continue;
|
|
413
|
+
const existing = byNormalized.get(key);
|
|
414
|
+
if (existing !== undefined && existing !== name) {
|
|
415
|
+
found.push({
|
|
416
|
+
code: "ux-name-collision",
|
|
417
|
+
message: `"${name}" and "${existing}" are the same ${kind} name up to case/separator — one behavior must keep one stable name`,
|
|
418
|
+
path: name,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
byNormalized.set(key, name);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return found;
|
|
426
|
+
};
|
|
427
|
+
findings.push(...collisions(contract.interactions.map((interaction) => interaction.name), "interaction"));
|
|
428
|
+
findings.push(...collisions(contract.uiStates.map((state) => state.name), "UI state"));
|
|
429
|
+
return findings;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Deliverable completeness (item ⑦): every file explicitly declared by Contract
|
|
433
|
+
* from frozen source fragments (projected into targets.requiredDeliverables)
|
|
434
|
+
* must be planned — bound by some requirement's implementationTargets or a
|
|
435
|
+
* verification target's file, or listed in targets.files. allowedPaths is a
|
|
436
|
+
* write boundary and never implies coverage.
|
|
437
|
+
*/
|
|
438
|
+
export function checkRequiredDeliverables(contract) {
|
|
439
|
+
const required = contract.targets.requiredDeliverables ?? [];
|
|
440
|
+
if (required.length === 0)
|
|
441
|
+
return [];
|
|
442
|
+
const planned = new Set([
|
|
443
|
+
...contract.targets.files,
|
|
444
|
+
...contract.requirements.flatMap((requirement) => requirement.implementationTargets),
|
|
445
|
+
...contract.verificationTargets.map((target) => target.file),
|
|
446
|
+
]);
|
|
447
|
+
return required
|
|
448
|
+
.filter((path) => !planned.has(path))
|
|
449
|
+
.map((path) => ({
|
|
450
|
+
code: "required-deliverable-unplanned",
|
|
451
|
+
message: `Contract declares required deliverable "${path}" but no requirement implementationTarget, verification target, or targets.files entry plans it`,
|
|
452
|
+
path,
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
374
455
|
/** Aggregate the deterministic design-policy checks into a single
|
|
375
456
|
* deterministic result. Any finding blocks; an empty finding list approves. */
|
|
376
457
|
export function evaluateFrontendDesignPolicy(input) {
|
|
@@ -379,10 +460,14 @@ export function evaluateFrontendDesignPolicy(input) {
|
|
|
379
460
|
...checkUiDesignCoverage(input.contract),
|
|
380
461
|
...checkSourceFreshness(input.contract, input.sourceFreshness),
|
|
381
462
|
...checkMockStrategy(input.contract, input.allowedMockStrategies),
|
|
382
|
-
...checkComponentChoice(input.contract, input.componentSpecCandidatePaths
|
|
463
|
+
...checkComponentChoice(input.contract, input.componentSpecCandidatePaths, input.hasFreshPathEvidence
|
|
464
|
+
? { hasFreshPathEvidence: input.hasFreshPathEvidence }
|
|
465
|
+
: undefined),
|
|
383
466
|
...checkTargetPaths(input.contract, input.writeSetPatterns),
|
|
384
467
|
...checkDependencies(input.contract, input.allowedDependencies),
|
|
385
468
|
...checkUiStateAttribution(input.contract),
|
|
469
|
+
...checkUxNameCollisions(input.contract),
|
|
470
|
+
...checkRequiredDeliverables(input.contract),
|
|
386
471
|
]);
|
|
387
472
|
const classification = findings.length > 0 ? "blocked" : "approved";
|
|
388
473
|
const result = {
|
|
@@ -9,10 +9,12 @@ import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
11
|
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
12
|
+
import { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
13
|
+
export { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
12
14
|
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
15
|
// only at call time, so the contract module and the policy module can depend
|
|
14
16
|
// on each other without module-init ordering issues.
|
|
15
|
-
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
17
|
+
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, checkRequiredDeliverables, checkUxNameCollisions, } from "./frontend-design-policy.js";
|
|
16
18
|
import { isDagSourceBindingV2 } from "./types.js";
|
|
17
19
|
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
18
20
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
@@ -600,12 +602,30 @@ const uiComponentChoiceSchema = z
|
|
|
600
602
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
601
603
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
602
604
|
// Optional: the decision enum already carries the semantic (specified →
|
|
603
|
-
// specReference, new → task-source traceability, reuse-existing →
|
|
604
|
-
//
|
|
605
|
-
// Omit for reuse-existing to keep plan output small.
|
|
605
|
+
// specReference, new → task-source traceability, reuse-existing →
|
|
606
|
+
// evidencePath). Omit for reuse-existing to keep plan output small.
|
|
606
607
|
rationale: z.string().min(1).optional(),
|
|
608
|
+
// Multi-id coverage (dogfood r-plan-quality): ONE choice may cover many
|
|
609
|
+
// UI states/interactions explicitly instead of the old implicit
|
|
610
|
+
// "reuse-existing covers everything" wildcard.
|
|
611
|
+
covers: z.array(z.string().min(1)).optional(),
|
|
612
|
+
// reuse-existing evidence (dogfood dag-1788504923861-0f7b17a9: 18
|
|
613
|
+
// reuse-existing rows cited conventions in a not-yet-written
|
|
614
|
+
// src/planner.ts): a repo-relative path to the existing file that
|
|
615
|
+
// proves the reuse. Enforced by superRefine + fresh-existence checks.
|
|
616
|
+
evidencePath: safePath.optional(),
|
|
607
617
|
})
|
|
608
|
-
.strict()
|
|
618
|
+
.strict()
|
|
619
|
+
.superRefine((choice, ctx) => {
|
|
620
|
+
if (choice.decision === "reuse-existing" &&
|
|
621
|
+
!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
|
|
622
|
+
ctx.addIssue({
|
|
623
|
+
code: z.ZodIssueCode.custom,
|
|
624
|
+
path: ["evidencePath"],
|
|
625
|
+
message: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; use decision=new for greenfield paths",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
609
629
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
610
630
|
/**
|
|
611
631
|
* Normalize unstable model emissions before the strict schema sees them:
|
|
@@ -680,6 +700,11 @@ export const frontendImplementationContractSchema = z
|
|
|
680
700
|
files: z.array(safePath).min(1),
|
|
681
701
|
routes: z.array(z.string().startsWith("/")).optional(),
|
|
682
702
|
publicApiChanges: z.array(z.string().min(1)).optional(),
|
|
703
|
+
// Runtime-owned deliverable contract (dogfood r-plan-quality):
|
|
704
|
+
// explicit, source-bound file inventory committed by Contract.
|
|
705
|
+
// The design-policy completeness gate checks ONLY these —
|
|
706
|
+
// allowedPaths is a write boundary, not a coverage obligation.
|
|
707
|
+
requiredDeliverables: z.array(safePath).optional(),
|
|
683
708
|
})
|
|
684
709
|
.strict(),
|
|
685
710
|
requirements: z.array(z
|
|
@@ -1634,7 +1659,12 @@ export function compactFrontendImplementationContract(value) {
|
|
|
1634
1659
|
const targets = asRecord(record.targets);
|
|
1635
1660
|
if (targets) {
|
|
1636
1661
|
const compactTargets = { ...targets };
|
|
1637
|
-
for (const field of [
|
|
1662
|
+
for (const field of [
|
|
1663
|
+
"files",
|
|
1664
|
+
"routes",
|
|
1665
|
+
"publicApiChanges",
|
|
1666
|
+
"requiredDeliverables",
|
|
1667
|
+
]) {
|
|
1638
1668
|
if (Object.hasOwn(compactTargets, field)) {
|
|
1639
1669
|
compactTargets[field] = dedupeJsonArray(compactTargets[field]);
|
|
1640
1670
|
}
|
|
@@ -2425,6 +2455,16 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2425
2455
|
files: referencedConcretePaths,
|
|
2426
2456
|
};
|
|
2427
2457
|
}
|
|
2458
|
+
// Both Plan precheck and policy materialization consume this exact inventory.
|
|
2459
|
+
// Never trust a Plan patch or infer delivery obligations from prose.
|
|
2460
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.contractNodeId ?? "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
2461
|
+
const requiredDeliverables = extractRequiredDeliverablePaths(contractRecords
|
|
2462
|
+
.filter((record) => record.phase === "committed")
|
|
2463
|
+
.map((record) => record.fact));
|
|
2464
|
+
delete result.data.targets.requiredDeliverables;
|
|
2465
|
+
if (requiredDeliverables.length > 0) {
|
|
2466
|
+
result.data.targets.requiredDeliverables = requiredDeliverables;
|
|
2467
|
+
}
|
|
2428
2468
|
const blockingGaps = [
|
|
2429
2469
|
...(result.data.evidenceGaps ?? []),
|
|
2430
2470
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
@@ -2632,6 +2672,7 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
2632
2672
|
"/sourceBinding",
|
|
2633
2673
|
"/riskLevel",
|
|
2634
2674
|
"/targets/files",
|
|
2675
|
+
"/targets/requiredDeliverables",
|
|
2635
2676
|
"/mockApi/productionDefaultOff",
|
|
2636
2677
|
];
|
|
2637
2678
|
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
@@ -2680,6 +2721,8 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
2680
2721
|
const targets = asRecord(record.targets);
|
|
2681
2722
|
if (targets && Object.hasOwn(targets, "files"))
|
|
2682
2723
|
violations.push("/targets/files");
|
|
2724
|
+
if (targets && Object.hasOwn(targets, "requiredDeliverables"))
|
|
2725
|
+
violations.push("/targets/requiredDeliverables");
|
|
2683
2726
|
const mockApi = asRecord(record.mockApi);
|
|
2684
2727
|
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
2685
2728
|
violations.push("/mockApi/productionDefaultOff");
|
|
@@ -2864,6 +2907,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
|
2864
2907
|
const policyPreFindings = [
|
|
2865
2908
|
...checkUiDesignCoverage(analysis.canonical),
|
|
2866
2909
|
...checkUiStateAttribution(analysis.canonical),
|
|
2910
|
+
...checkRequiredDeliverables(analysis.canonical),
|
|
2911
|
+
...checkUxNameCollisions(analysis.canonical),
|
|
2867
2912
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2868
2913
|
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
2869
2914
|
];
|
|
@@ -3082,9 +3127,11 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
3082
3127
|
}
|
|
3083
3128
|
// b. Committed contract terminal fact (contract-finalized).
|
|
3084
3129
|
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
3085
|
-
const
|
|
3130
|
+
const committedContractFacts = contractRecords
|
|
3086
3131
|
.filter((record) => record.phase === "committed")
|
|
3087
|
-
.map((record) => record.fact
|
|
3132
|
+
.map((record) => record.fact);
|
|
3133
|
+
const contractKinds = new Set(committedContractFacts
|
|
3134
|
+
.map((fact) => fact?.kind)
|
|
3088
3135
|
.filter((kind) => typeof kind === "string"));
|
|
3089
3136
|
if (!contractKinds.has("contract-finalized")) {
|
|
3090
3137
|
return {
|
|
@@ -3139,6 +3186,7 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
3139
3186
|
runDir: input.runDir,
|
|
3140
3187
|
rawContractText: serializeDeterministicJson(merged),
|
|
3141
3188
|
sourceBinding: input.sourceBinding,
|
|
3189
|
+
contractNodeId,
|
|
3142
3190
|
});
|
|
3143
3191
|
return {
|
|
3144
3192
|
ok: true,
|
|
@@ -12,8 +12,19 @@ function bulletList(items) {
|
|
|
12
12
|
return "";
|
|
13
13
|
return items.map((item) => `- ${item}`).join("\n");
|
|
14
14
|
}
|
|
15
|
+
/** The runtime's deterministic fallback for an outcome the plan did not
|
|
16
|
+
* author. It is bookkeeping for the schema's non-empty constraint, not user
|
|
17
|
+
* prose — the renderer must never present it as requirement content. */
|
|
18
|
+
const IMPLIED_OUTCOME_PREFIX = "implied by contract:";
|
|
15
19
|
function requirementsCoverage(contract) {
|
|
16
|
-
const lines = contract.requirements.map((requirement) =>
|
|
20
|
+
const lines = contract.requirements.map((requirement) => {
|
|
21
|
+
const targets = requirement.implementationTargets.join(", ");
|
|
22
|
+
const verificationTargets = requirement.verificationTargetIds.join(", ");
|
|
23
|
+
const authoredOutcome = requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
|
|
24
|
+
? ""
|
|
25
|
+
: ` — ${requirement.expectedOutcome}`;
|
|
26
|
+
return `- ${requirement.id} → ${targets || "(no targets)"} [${verificationTargets || "no VT"}]${authoredOutcome}`;
|
|
27
|
+
});
|
|
17
28
|
return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
|
|
18
29
|
}
|
|
19
30
|
function uiStateHandling(contract) {
|
|
@@ -98,7 +109,7 @@ export function renderFrontendPlanMarkdown(contract) {
|
|
|
98
109
|
interactionNotes(contract),
|
|
99
110
|
"",
|
|
100
111
|
"## Mock & API Strategy",
|
|
101
|
-
contract.mockApi
|
|
112
|
+
contract.mockApi?.strategy || "_(not specified)_",
|
|
102
113
|
"",
|
|
103
114
|
"## Dependency Policy",
|
|
104
115
|
contract.dependencyPolicy || "_(not specified)_",
|
|
@@ -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);
|
|
@@ -302,6 +324,15 @@ export async function evaluateVerificationTargets(input) {
|
|
|
302
324
|
target,
|
|
303
325
|
});
|
|
304
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
|
+
}
|
|
305
336
|
const status = issues.length ? "failed" : "ok";
|
|
306
337
|
if (issues.length)
|
|
307
338
|
hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
|
|
@@ -473,6 +504,7 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
473
504
|
contract,
|
|
474
505
|
labelOwners,
|
|
475
506
|
workspaceRoot: input.workspaceRoot,
|
|
507
|
+
executedTests: input.evidence?.behavior.executedTests,
|
|
476
508
|
});
|
|
477
509
|
if (hardIssues.length) {
|
|
478
510
|
throw new Error(`trace: ${hardIssues.join("; ")}`);
|