@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
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
findDagForRun,
|
|
77
77
|
} from "../dag-model.js";
|
|
78
78
|
import { describeDagNodePurpose } from "../dag-node-purpose.js";
|
|
79
|
+
import { inspectorValueLabel } from "../dag-inspector-humanize.js";
|
|
79
80
|
import { layoutDag } from "../dag-layout.js";
|
|
80
81
|
import {
|
|
81
82
|
showView,
|
|
@@ -197,7 +198,9 @@ export function buildDagNodeTable(dagRunId, nodes) {
|
|
|
197
198
|
index + 1,
|
|
198
199
|
node.nodeId,
|
|
199
200
|
node.rank ?? "—",
|
|
200
|
-
node.executor
|
|
201
|
+
node.executor
|
|
202
|
+
? inspectorValueLabel("executor", node.executor)
|
|
203
|
+
: "—",
|
|
201
204
|
node.model ?? "—",
|
|
202
205
|
node.status ? badge(node.status) : "—",
|
|
203
206
|
formatMs(node.durationMs),
|
|
@@ -211,10 +214,10 @@ export function buildDagNodeTable(dagRunId, nodes) {
|
|
|
211
214
|
const nodeTable = buildTable(
|
|
212
215
|
[
|
|
213
216
|
"序号",
|
|
214
|
-
"
|
|
215
|
-
"
|
|
217
|
+
"步骤编号",
|
|
218
|
+
"执行层级",
|
|
216
219
|
"执行方式",
|
|
217
|
-
"
|
|
220
|
+
"实际模型",
|
|
218
221
|
"状态",
|
|
219
222
|
"耗时",
|
|
220
223
|
"执行历史",
|
|
@@ -261,7 +264,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
261
264
|
const metaEl = document.getElementById("dag-meta");
|
|
262
265
|
const ranksEl = document.getElementById("dag-ranks");
|
|
263
266
|
const nodesEl = document.getElementById("dag-nodes");
|
|
264
|
-
if (initial && metaEl) mountViewState(metaEl, "loading", "
|
|
267
|
+
if (initial && metaEl) mountViewState(metaEl, "loading", "正在加载运行流程…");
|
|
265
268
|
uiState.dagGraphViewportState = captureDagGraphViewportState(
|
|
266
269
|
uiState.dagGraphViewportState,
|
|
267
270
|
dagRunId,
|
|
@@ -281,7 +284,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
281
284
|
}
|
|
282
285
|
if (!dag) {
|
|
283
286
|
clearNode(metaEl);
|
|
284
|
-
mountViewState(metaEl, "error", "
|
|
287
|
+
mountViewState(metaEl, "error", "找不到这次运行。运行记录可能已被清理。");
|
|
285
288
|
mountRelationBar("dag-relations", {}, snapshot);
|
|
286
289
|
closeDagInspector();
|
|
287
290
|
return false;
|
|
@@ -354,7 +357,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
354
357
|
LIVENESS_LABELS[dag.liveness] ?? dag.liveness ?? LIVENESS_LABELS.unknown;
|
|
355
358
|
const primaryEntries = [
|
|
356
359
|
["标题", dag.title || dag.dagRunId],
|
|
357
|
-
["
|
|
360
|
+
["运行编号", dag.dagRunId],
|
|
358
361
|
["状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
359
362
|
];
|
|
360
363
|
if (visibility.showExecutorStatus)
|
|
@@ -383,7 +386,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
383
386
|
["总耗时", formatDuration(dag.durationMs)],
|
|
384
387
|
["进度", `${progress.finished}/${progress.total}`],
|
|
385
388
|
["成功/失败/跳过", `${succeededCount}/${failedCount}/${skippedCount}`],
|
|
386
|
-
["
|
|
389
|
+
["记录保存位置", dag.dagPath ?? "—"],
|
|
387
390
|
);
|
|
388
391
|
const lintSummary = frontendLintSummary(dag.frontendLint);
|
|
389
392
|
if (lintSummary) {
|
|
@@ -429,7 +432,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
429
432
|
);
|
|
430
433
|
if (nodes.length === 0) {
|
|
431
434
|
clearNode(ranksEl);
|
|
432
|
-
ranksEl.appendChild(el("h3", null, "
|
|
435
|
+
ranksEl.appendChild(el("h3", null, "执行步骤关系"));
|
|
433
436
|
ranksEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
|
|
434
437
|
} else if (reuseShell && existingGraphWrap?.parentNode === ranksEl) {
|
|
435
438
|
// reuse-dag-graph-wrap + viewport identity: keep wrap/viewport alive.
|
|
@@ -440,16 +443,16 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
440
443
|
}
|
|
441
444
|
} else {
|
|
442
445
|
clearNode(ranksEl);
|
|
443
|
-
ranksEl.appendChild(el("h3", null, "
|
|
446
|
+
ranksEl.appendChild(el("h3", null, "执行步骤关系"));
|
|
444
447
|
ranksEl.appendChild(renderDagGraph(dag, dagRunId, null));
|
|
445
448
|
}
|
|
446
449
|
|
|
447
450
|
const nodesScrollLeft = nodesEl.scrollLeft;
|
|
448
451
|
clearNode(nodesEl);
|
|
449
452
|
const nodeHeading = el("div", "dag-section-heading");
|
|
450
|
-
nodeHeading.appendChild(el("h3", null, "
|
|
453
|
+
nodeHeading.appendChild(el("h3", null, "执行步骤"));
|
|
451
454
|
nodeHeading.appendChild(
|
|
452
|
-
el("span", "dag-section-meta", `共 ${nodes.length}
|
|
455
|
+
el("span", "dag-section-meta", `共 ${nodes.length} 个步骤`),
|
|
453
456
|
);
|
|
454
457
|
nodesEl.appendChild(nodeHeading);
|
|
455
458
|
|
|
@@ -479,6 +482,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
479
482
|
dagRunId,
|
|
480
483
|
nodes.find((node) => node.nodeId === uiState.selectedDagNodeId),
|
|
481
484
|
initial,
|
|
485
|
+
dag,
|
|
482
486
|
);
|
|
483
487
|
return isDagRunActive(dag);
|
|
484
488
|
}
|
|
@@ -1116,9 +1116,11 @@ function rerenderSessionTimeline(content, nodeId, options) {
|
|
|
1116
1116
|
const scrollLeft = content.scrollLeft;
|
|
1117
1117
|
const autoScrollEnabled = isTimelineAutoScrollEnabled();
|
|
1118
1118
|
const preserved = [...content.children].filter((child) =>
|
|
1119
|
-
|
|
1120
|
-
.
|
|
1121
|
-
|
|
1119
|
+
["dag-inspector-attempt-bar", "inspector-page-lead"].some((className) =>
|
|
1120
|
+
String(child.className ?? "")
|
|
1121
|
+
.split(/\s+/)
|
|
1122
|
+
.includes(className),
|
|
1123
|
+
),
|
|
1122
1124
|
);
|
|
1123
1125
|
while (content.firstChild) content.removeChild(content.firstChild);
|
|
1124
1126
|
for (const child of preserved) content.appendChild(child);
|
|
@@ -101,6 +101,110 @@ export function lookupRequiredVariantSlotsForMarkdownPath(inventory, markdownPat
|
|
|
101
101
|
.replace(/\.md$/i, "");
|
|
102
102
|
return inventory.modules.find((item) => item.stem === stem)?.requiredVariantSlots ?? [];
|
|
103
103
|
}
|
|
104
|
+
const MODULE_INDEX_HEADER = [
|
|
105
|
+
"Module Stem",
|
|
106
|
+
"Business Resource",
|
|
107
|
+
"Owned Operations",
|
|
108
|
+
"Owned Rule Keys",
|
|
109
|
+
"Case IDs",
|
|
110
|
+
"Split Reason",
|
|
111
|
+
"Markdown Path",
|
|
112
|
+
"Pytest Path",
|
|
113
|
+
];
|
|
114
|
+
function markdownTableCells(line) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (!trimmed.startsWith("|") || !trimmed.endsWith("|"))
|
|
117
|
+
return undefined;
|
|
118
|
+
return trimmed
|
|
119
|
+
.slice(1, -1)
|
|
120
|
+
.split("|")
|
|
121
|
+
.map((cell) => cell.trim().replace(/^`|`$/g, ""));
|
|
122
|
+
}
|
|
123
|
+
function isMarkdownSeparatorRow(cells) {
|
|
124
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
125
|
+
}
|
|
126
|
+
/** Validate the minimum complete run-owned Markdown protocol before N2 succeeds. */
|
|
127
|
+
export function assessBackendTestPlanProtocol(markdown) {
|
|
128
|
+
const canonical = canonicalizeBackendTestPlanProtocolHeadings(markdown).markdown;
|
|
129
|
+
const lines = canonical.split("\n");
|
|
130
|
+
const issues = [];
|
|
131
|
+
if (lines.some((line) => /^(?:```|~~~)/.test(line.trim()))) {
|
|
132
|
+
issues.push({
|
|
133
|
+
code: "fenced-plan-output",
|
|
134
|
+
detail: "return the final Markdown plan directly, without a code fence",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const headingIndexes = new Map();
|
|
138
|
+
for (const heading of [
|
|
139
|
+
"Coverage Scope",
|
|
140
|
+
"Coverage Matrix",
|
|
141
|
+
"Scenario Partitions",
|
|
142
|
+
"Module Index",
|
|
143
|
+
]) {
|
|
144
|
+
headingIndexes.set(heading, lines.flatMap((line, index) => line.trim() === `## ${heading}` ? [index] : []));
|
|
145
|
+
}
|
|
146
|
+
const requireUnique = (heading, missingCode, duplicateCode) => {
|
|
147
|
+
const count = headingIndexes.get(heading)?.length ?? 0;
|
|
148
|
+
if (count === 0) {
|
|
149
|
+
issues.push({
|
|
150
|
+
code: missingCode,
|
|
151
|
+
detail: `require exactly one exact ## ${heading} section`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (count > 1) {
|
|
155
|
+
issues.push({
|
|
156
|
+
code: duplicateCode,
|
|
157
|
+
detail: `found ${count}; require exactly one exact ## ${heading} section`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
// Module Index is first so retry diagnostics lead with the shard authority.
|
|
162
|
+
requireUnique("Module Index", "missing-module-index", "duplicate-module-index");
|
|
163
|
+
requireUnique("Coverage Scope", "missing-coverage-scope", "duplicate-coverage-scope");
|
|
164
|
+
requireUnique("Coverage Matrix", "missing-coverage-matrix", "duplicate-coverage-matrix");
|
|
165
|
+
const scenarioCount = headingIndexes.get("Scenario Partitions")?.length ?? 0;
|
|
166
|
+
if (scenarioCount > 1) {
|
|
167
|
+
issues.push({
|
|
168
|
+
code: "duplicate-scenario-partitions",
|
|
169
|
+
detail: `found ${scenarioCount}; require at most one exact ## Scenario Partitions section`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const moduleIndexes = headingIndexes.get("Module Index") ?? [];
|
|
173
|
+
if (moduleIndexes.length === 1) {
|
|
174
|
+
const start = moduleIndexes[0] + 1;
|
|
175
|
+
const nextHeading = lines
|
|
176
|
+
.slice(start)
|
|
177
|
+
.findIndex((line) => /^##\s+\S/.test(line.trim()));
|
|
178
|
+
const end = nextHeading < 0 ? lines.length : start + nextHeading;
|
|
179
|
+
const rows = lines
|
|
180
|
+
.slice(start, end)
|
|
181
|
+
.map(markdownTableCells)
|
|
182
|
+
.filter((cells) => cells !== undefined);
|
|
183
|
+
const headerIndex = rows.findIndex((cells) => cells.length === MODULE_INDEX_HEADER.length &&
|
|
184
|
+
MODULE_INDEX_HEADER.every((name, index) => cells[index] === name));
|
|
185
|
+
if (headerIndex < 0) {
|
|
186
|
+
issues.push({
|
|
187
|
+
code: "invalid-module-index-header",
|
|
188
|
+
detail: `require exact Module Index header: ${MODULE_INDEX_HEADER.join(" | ")}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const dataRows = rows
|
|
193
|
+
.slice(headerIndex + 1)
|
|
194
|
+
.filter((cells) => !isMarkdownSeparatorRow(cells));
|
|
195
|
+
if (!dataRows.some((cells) => cells.length === MODULE_INDEX_HEADER.length &&
|
|
196
|
+
Boolean(cells[0]?.trim()) &&
|
|
197
|
+
Boolean(cells[6]?.trim()) &&
|
|
198
|
+
Boolean(cells[7]?.trim()))) {
|
|
199
|
+
issues.push({
|
|
200
|
+
code: "empty-module-index",
|
|
201
|
+
detail: "Module Index must contain at least one complete 8-cell module row",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { ok: issues.length === 0, issues };
|
|
207
|
+
}
|
|
104
208
|
export function isCanonicalPlanHeadingLine(line) {
|
|
105
209
|
return CANONICAL_HEADING_LINE.test(line.trim());
|
|
106
210
|
}
|
|
@@ -32,7 +32,10 @@ export const DEFAULT_DAG_RETRY_CATEGORIES = [
|
|
|
32
32
|
"rate-limit",
|
|
33
33
|
"unavailable",
|
|
34
34
|
"empty-output",
|
|
35
|
+
"output-limit",
|
|
35
36
|
];
|
|
37
|
+
/** Provider stopReason=length: output/context capacity ended the turn early. */
|
|
38
|
+
export const OUTPUT_LIMIT_RETRY_CATEGORY = "output-limit";
|
|
36
39
|
export const STRUCTURED_OUTPUT_RETRY_CATEGORY = "output-too-large";
|
|
37
40
|
export const PROTOCOL_INVALID_RETRY_CATEGORY = "protocol-invalid";
|
|
38
41
|
/** Recoverable model artifact/schema formatting failure on read-only structured nodes. */
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Shared projections of committed frontend facts. Callers filter commit phase. */
|
|
3
|
+
function object(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Join requirement-owned identity with separately committed evidence by id.
|
|
9
|
+
* Later evidence declarations replace only the lanes they explicitly name. */
|
|
10
|
+
export function resolveFrontendContractRequirements(facts) {
|
|
11
|
+
const contractFacts = facts.map(object).filter((fact) => fact?.origin === "contract");
|
|
12
|
+
const evidenceById = new Map();
|
|
13
|
+
for (const fact of contractFacts) {
|
|
14
|
+
if (fact.kind !== "evidence-expectation" ||
|
|
15
|
+
typeof fact.requirementId !== "string")
|
|
16
|
+
continue;
|
|
17
|
+
evidenceById.set(fact.requirementId, {
|
|
18
|
+
...evidenceById.get(fact.requirementId),
|
|
19
|
+
...object(fact.evidence),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return contractFacts.flatMap((fact) => fact.kind === "requirement" && typeof fact.id === "string"
|
|
23
|
+
? [{
|
|
24
|
+
...fact,
|
|
25
|
+
id: fact.id,
|
|
26
|
+
evidence: { ...object(fact.evidence), ...evidenceById.get(fact.id) },
|
|
27
|
+
}]
|
|
28
|
+
: []);
|
|
29
|
+
}
|
|
30
|
+
function names(value) {
|
|
31
|
+
return Array.isArray(value)
|
|
32
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
33
|
+
: [];
|
|
34
|
+
}
|
|
35
|
+
const evidenceStatus = z.enum(["required", "optional", "not-applicable"]);
|
|
36
|
+
export const frontendEvidenceExpectationSchema = z.object({
|
|
37
|
+
requirementId: z.string().min(1),
|
|
38
|
+
evidence: z.object({
|
|
39
|
+
static: evidenceStatus.optional(),
|
|
40
|
+
behavior: evidenceStatus.optional(),
|
|
41
|
+
mock: evidenceStatus.optional(),
|
|
42
|
+
"real-integration": evidenceStatus.optional(),
|
|
43
|
+
}).strict().refine((evidence) => Object.keys(evidence).length > 0, "declare at least one evidence lane"),
|
|
44
|
+
}).strict();
|
|
45
|
+
const concretePath = z.string().min(1).refine((value) => value === value.trim() &&
|
|
46
|
+
!/^[A-Za-z]:/.test(value) &&
|
|
47
|
+
!/[\\*?\[\]{}\0]/.test(value) &&
|
|
48
|
+
value.split("/").every((part) => part !== "" && part !== "." && part !== ".."), "must be a concrete relative POSIX file path without traversal");
|
|
49
|
+
export const frontendRequiredDeliverablesSchema = z.object({
|
|
50
|
+
items: z.array(z.object({
|
|
51
|
+
path: concretePath,
|
|
52
|
+
requirementId: z.string().min(1),
|
|
53
|
+
sourceFragmentId: z.string().min(1),
|
|
54
|
+
}).strict()),
|
|
55
|
+
}).strict();
|
|
56
|
+
/** Validate structure and provenance; the Contract model owns the distinction
|
|
57
|
+
* between delivery obligations, permissions, prohibitions and references. */
|
|
58
|
+
export function validateFrontendRequiredDeliverables(value, requirements) {
|
|
59
|
+
const declaration = frontendRequiredDeliverablesSchema.parse(value);
|
|
60
|
+
for (const item of declaration.items) {
|
|
61
|
+
const requirement = requirements.get(item.requirementId);
|
|
62
|
+
if (!requirement?.sourceFragmentIds.includes(item.sourceFragmentId)) {
|
|
63
|
+
throw new Error(`required deliverable ${item.path} has an unknown requirement/fragment binding`);
|
|
64
|
+
}
|
|
65
|
+
const text = requirement.sourceFragments?.get(item.sourceFragmentId);
|
|
66
|
+
const escaped = item.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67
|
+
// A sentence-ending period is punctuation; a dot followed by filename
|
|
68
|
+
// characters is still part of the token (app.ts must not match app.ts.map).
|
|
69
|
+
const exactPath = new RegExp(`(^|[^\\p{L}\\p{N}_./-])${escaped}(?=$|[^\\p{L}\\p{N}_./-]|\\.(?=$|[^\\p{L}\\p{N}_./-]))`, "u");
|
|
70
|
+
if (!text || !exactPath.test(text)) {
|
|
71
|
+
throw new Error(`required deliverable ${item.path} is not an exact path in frozen source fragment ${item.sourceFragmentId}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return declaration;
|
|
75
|
+
}
|
|
76
|
+
/** Read the latest explicit inventory. Never infer obligations from prose. */
|
|
77
|
+
export function extractRequiredDeliverablePaths(facts) {
|
|
78
|
+
const declaration = facts.map(object).filter((fact) => fact?.origin === "contract" && fact.kind === "required-deliverables").at(-1);
|
|
79
|
+
if (!declaration)
|
|
80
|
+
return [];
|
|
81
|
+
const { items } = frontendRequiredDeliverablesSchema.parse({ items: declaration.items });
|
|
82
|
+
return [...new Set(items.map((item) => item.path))].sort();
|
|
83
|
+
}
|
|
84
|
+
/** Apply state-flow replacement/removal semantics exactly as the plan compiler does. */
|
|
85
|
+
export function collectCanonicalStateFlowNames(committedFacts, includeRegistry = false) {
|
|
86
|
+
const uiStateNames = new Set();
|
|
87
|
+
const interactionNames = new Set();
|
|
88
|
+
for (const value of committedFacts) {
|
|
89
|
+
const record = object(value);
|
|
90
|
+
if (!record || (record.phase !== undefined && record.phase !== "committed"))
|
|
91
|
+
continue;
|
|
92
|
+
const fact = object(record.fact) ?? record;
|
|
93
|
+
if (fact.origin !== "plan")
|
|
94
|
+
continue;
|
|
95
|
+
if (includeRegistry && fact.kind === "state-registry") {
|
|
96
|
+
uiStateNames.clear();
|
|
97
|
+
interactionNames.clear();
|
|
98
|
+
for (const name of names(fact.uiStateNames))
|
|
99
|
+
uiStateNames.add(name);
|
|
100
|
+
for (const name of names(fact.interactionNames))
|
|
101
|
+
interactionNames.add(name);
|
|
102
|
+
}
|
|
103
|
+
if (fact.kind !== "state-flow")
|
|
104
|
+
continue;
|
|
105
|
+
for (const name of names(fact.removeUiStateNames)) {
|
|
106
|
+
uiStateNames.delete(name);
|
|
107
|
+
}
|
|
108
|
+
for (const name of names(fact.removeInteractionNames)) {
|
|
109
|
+
interactionNames.delete(name);
|
|
110
|
+
}
|
|
111
|
+
for (const state of Array.isArray(fact.uiStates) ? fact.uiStates : []) {
|
|
112
|
+
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
113
|
+
continue;
|
|
114
|
+
const name = state.name;
|
|
115
|
+
if (typeof name === "string" && name.trim())
|
|
116
|
+
uiStateNames.add(name);
|
|
117
|
+
}
|
|
118
|
+
for (const interaction of Array.isArray(fact.interactions)
|
|
119
|
+
? fact.interactions
|
|
120
|
+
: []) {
|
|
121
|
+
if (!interaction || typeof interaction !== "object" || Array.isArray(interaction)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const name = interaction.name;
|
|
125
|
+
if (typeof name === "string" && name.trim())
|
|
126
|
+
interactionNames.add(name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { uiStateNames, interactionNames };
|
|
130
|
+
}
|
|
@@ -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) {
|
|
@@ -249,7 +272,7 @@ export function checkTargetPaths(contract, writeSetPatterns) {
|
|
|
249
272
|
}
|
|
250
273
|
}
|
|
251
274
|
for (const target of contract.verificationTargets) {
|
|
252
|
-
if (target.
|
|
275
|
+
if (target.mode === "static")
|
|
253
276
|
continue;
|
|
254
277
|
if (!matches(target.file)) {
|
|
255
278
|
findings.push({
|
|
@@ -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 = {
|