@tea-agent/loop-agent 0.39.0-beta.4 → 0.39.0-beta.6
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 +5 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +167 -31
- package/dist/executors/pi-playwright-cli-tool.js +23 -16
- package/dist/executors/shell-executor.js +25 -8
- package/dist/shared/dag-failure-category.js +138 -0
- package/dist/task/config-types.js +6 -0
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/routes.js +5 -3
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-zsNyvGaH.js → abnfDiagram-N423BO3Z-CfDSkLtp.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BDjZ5kE1.js → arc-BlefM6I5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BkgaujnN.js → architectureDiagram-T3A2C74G-C4tHfzHn.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CqxOJi0j.js → blockDiagram-VBNYF7ZC-G-G77Rh1.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C2JHLdRi.js → c4Diagram-5PPSVZJV-DWgxmY72.js} +1 -1
- package/dist/worker/console/static/assets/channel-DsdX3ZUX.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DF_YSvOS.js → chunk-2GRJ4B5K-CJQcfbCY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D3Ort5Vo.js → chunk-2Q5K7J3B-vUPviH9y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-XfAfZeUC.js → chunk-5RXB4S5H-DzRXSTeh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-SLBibhf5.js → chunk-5VM5RSS4-CWfgqm07.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dz29Ye_f.js → chunk-6Q2QTUOP-BtTDauXA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DB2G4H8V.js → chunk-GF5L2VYU-Bc-NNt84.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CIoGkxdu.js → chunk-JWPE2WC7-B_f-emL4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CG78FGkn.js → chunk-KBJHAD2P-DV-jgJ66.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ZDHAvXPd.js → chunk-RYQCIY6F-DMqzn4RZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BKkkC4VA.js → chunk-XXDRQBXY-CVNtlAm4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CrvCyJ-8.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CrvCyJ-8.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-SposUZsO.js → cose-bilkent-JH36ORCC-BBZBhVTd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BSCtQpD7.js → cynefin-VYW2F7L2-DbMU2PZ2.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DgFHrw9l.js → cynefinDiagram-MW4NZA55-Dj0v8rjc.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BVBasYbj.js → dagre-VZM6K2ZE-BW8R4WvJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-6r_4rN_c.js → diagram-7IWD3JNH-DEgWz3ld.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-JmuaB41P.js → diagram-B4RE2ZJO-BAA01rlR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-xXvAY3sk.js → diagram-LBJQPF4R-Cau6qqpW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D2B3lY2o.js → diagram-Q27KOJAE-8HiPsLos.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-tK54tMEX.js → diagram-UB23O5K3-CmkXuTYL.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CNtELzV-.js → ebnfDiagram-BXEA7PRR-DCV_0seW.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-C1xKMKcW.js → erDiagram-JOGREHBK-gckfWESj.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZ7dlPMz.js → flowDiagram-UKHOOZJN-D2mT2BUu.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BqGb42Xe.js → ganttDiagram-PKOTCBZU-B_HR5qEw.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D_BlXEtt.js → gitGraphDiagram-DS77QQ5N-BUw4fpSa.js} +1 -1
- package/dist/worker/console/static/assets/{index-D5zGX6fG.js → index-DPbsruV5.js} +81 -79
- package/dist/worker/console/static/assets/index-DwOGauxU.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Vce2vmIe.js → infoDiagram-6WML65LV-BSRAqEj0.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DDwVBAvI.js → ishikawaDiagram-WSZJBQD7-CLxE8hUp.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-y_Ew-wv4.js → journeyDiagram-NVQOT4AX-juFzVXIj.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CjXJFp1K.js → kanban-definition-27J2QSJJ-CwLHVhOU.js} +1 -1
- package/dist/worker/console/static/assets/{linear-1itMst1Q.js → linear-9LfUyPAs.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-C-D3ZRUa.js → mermaid.core-TPPHfQbe.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BiYywmZc.js → mindmap-definition-FAOFIHXS-Dn_Z9RxF.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-l2fydNWA.js → pegDiagram-VL7TDLO6-CiEMKTgm.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Dz0hNbYW.js → pieDiagram-7S7Q4E2Y-JASaGxjZ.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BujI_mLZ.js → quadrantDiagram-CIZ2JOQS-ChmH0ply.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-pWe84beb.js → railroadDiagram-AXF67PYL-CtyM-HGQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-ByYu2frg.js → requirementDiagram-LRYGKXZP-BN9y_Bvs.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CoPnilFP.js → sankeyDiagram-W5VNT64P-IyS0GQ1-.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-1Md2LnS0.js → sequenceDiagram-SI44F4Z6-DjxGXurN.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqqqAoSH.js → sizeCapture-X5ZJPWSS-Cr3Z0S8y.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C4sGkf--.js → stateDiagram-OKZ733FA-BjJFC6pW.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bq44O9Nt.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ByxJrUzW.js → swimlanes-SLNWSIFB-DeMUJQiW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BT4tWyW3.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D8MLgM0O.js → timeline-definition-Z64GVDOM-Bc2Rv2Oo.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DnBis-wZ.js → vennDiagram-T6HMQDX7-CDhRUeqG.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-uuyYT7UO.js → wardleyDiagram-T6FBY63Y-DaN_z3Xy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BjgabhwK.js → xychartDiagram-ELKLHX3M-C1vTLBDG.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +13 -1
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +29 -9
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
- package/dist/workflows/dag/failure-category.js +7 -128
- package/dist/workflows/dag/frontend-design-policy.js +12 -7
- package/dist/workflows/dag/frontend-implementation-contract.js +10 -5
- package/dist/workflows/dag/frontend-plan-render.js +0 -3
- package/dist/workflows/dag/frontend-shadow-dual-write.js +51 -10
- package/dist/workflows/dag/frontend-test-case-checklist.js +4 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +6 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +6 -3
- package/dist/workflows/dag/frontend-test-environment-probe.js +10 -7
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +28 -14
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
- package/dist/workflows/dag/frontend-typed-event-store.js +13 -0
- package/dist/workflows/dag/init-hybrid.js +66 -19
- package/dist/workflows/dag/node-execution.js +21 -1
- package/dist/workflows/dag/retry-policy.js +32 -0
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +23 -19
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/package.json +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +4 -4
- package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +10 -8
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +0 -1
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +0 -8
|
@@ -58,7 +58,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
58
58
|
export function useChatStream(params) {
|
|
59
59
|
const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
|
|
60
60
|
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
|
|
61
|
-
const { setInput, setPendingImages } = composer;
|
|
61
|
+
const { setInput, setPendingImages, recordHistory } = composer;
|
|
62
62
|
const showNotice = useCallback((message) => {
|
|
63
63
|
if (!setNotice)
|
|
64
64
|
return;
|
|
@@ -393,7 +393,7 @@ export function useChatStream(params) {
|
|
|
393
393
|
setInput("");
|
|
394
394
|
setPendingImages([]);
|
|
395
395
|
setError(null);
|
|
396
|
-
showNotice(existing ? "
|
|
396
|
+
showNotice(existing ? "已更新排队消息" : "已排队,将在当前回合结束后自动发送");
|
|
397
397
|
};
|
|
398
398
|
const acceptDispatched = () => {
|
|
399
399
|
setDeferredTurn(null);
|
|
@@ -430,7 +430,7 @@ export function useChatStream(params) {
|
|
|
430
430
|
}
|
|
431
431
|
if (body.deferredTurn)
|
|
432
432
|
setDeferredTurn(body.deferredTurn);
|
|
433
|
-
if (body.error?.code !== "
|
|
433
|
+
if (body.error?.code !== "TURN_NOT_ACTIVE") {
|
|
434
434
|
setError(body.error?.message ??
|
|
435
435
|
`待发送消息未被服务端接受(HTTP ${response.status})`);
|
|
436
436
|
return { action: "handled" };
|
|
@@ -495,7 +495,9 @@ export function useChatStream(params) {
|
|
|
495
495
|
showNotice,
|
|
496
496
|
fetchSessionState,
|
|
497
497
|
]);
|
|
498
|
-
|
|
498
|
+
/** DELETE the queued turn; "restore" moves its text back into the composer,
|
|
499
|
+
* "discard" drops it entirely (2026-08-23 queue chip actions). */
|
|
500
|
+
const requestDeferredTurnDelete = useCallback(async (mode) => {
|
|
499
501
|
const activeSession = refs.sessionRef.current;
|
|
500
502
|
const deferred = thread.deferredTurn;
|
|
501
503
|
if (!activeSession || !deferred)
|
|
@@ -519,10 +521,15 @@ export function useChatStream(params) {
|
|
|
519
521
|
return;
|
|
520
522
|
}
|
|
521
523
|
setDeferredTurn(null);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
+
if (mode === "restore") {
|
|
525
|
+
setInput(body.deferredTurn?.text ?? "");
|
|
526
|
+
setPendingImages(body.deferredTurn?.images ?? []);
|
|
527
|
+
showNotice("待发送消息已移回输入框");
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
showNotice("已删除排队消息");
|
|
531
|
+
}
|
|
524
532
|
setError(null);
|
|
525
|
-
showNotice("待发送消息已移回输入框");
|
|
526
533
|
}
|
|
527
534
|
catch (error) {
|
|
528
535
|
setError(error instanceof Error ? error.message : String(error));
|
|
@@ -537,6 +544,8 @@ export function useChatStream(params) {
|
|
|
537
544
|
setError,
|
|
538
545
|
showNotice,
|
|
539
546
|
]);
|
|
547
|
+
const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
|
|
548
|
+
const discardDeferredTurn = useCallback(() => requestDeferredTurnDelete("discard"), [requestDeferredTurnDelete]);
|
|
540
549
|
const sendPrompt = useCallback(async (text, images) => {
|
|
541
550
|
const initialSession = refs.sessionRef.current;
|
|
542
551
|
if (!initialSession || (!text.trim() && images.length === 0))
|
|
@@ -548,10 +557,14 @@ export function useChatStream(params) {
|
|
|
548
557
|
});
|
|
549
558
|
const clientRequestId = requestId("chat-submit");
|
|
550
559
|
if (route.action === "hold") {
|
|
551
|
-
|
|
560
|
+
const phase = turnPhaseFromOwnership(ownership);
|
|
561
|
+
if (phase === "stopping")
|
|
552
562
|
showNotice("正在确认上一轮停止状态,当前消息已保留");
|
|
563
|
+
else if (phase === "running")
|
|
564
|
+
showNotice("正在确认当前回合状态,当前消息已保留");
|
|
553
565
|
return;
|
|
554
566
|
}
|
|
567
|
+
recordHistory?.(text);
|
|
555
568
|
if (route.action === "queue-deferred") {
|
|
556
569
|
const deferred = await queueDeferredTurn({
|
|
557
570
|
text,
|
|
@@ -721,6 +734,7 @@ export function useChatStream(params) {
|
|
|
721
734
|
setTurnPhase,
|
|
722
735
|
setStreamingAssistantId,
|
|
723
736
|
queueDeferredTurn,
|
|
737
|
+
recordHistory,
|
|
724
738
|
]);
|
|
725
739
|
const handleStop = useCallback(async () => {
|
|
726
740
|
// AC-R5-001: durable stopRequested is set synchronously before any await so
|
|
@@ -969,5 +983,11 @@ export function useChatStream(params) {
|
|
|
969
983
|
}
|
|
970
984
|
};
|
|
971
985
|
}, [session, recoveryVersion, refs, startEventsStream]);
|
|
972
|
-
return {
|
|
986
|
+
return {
|
|
987
|
+
sendPrompt,
|
|
988
|
+
handleStop,
|
|
989
|
+
cancelDeferredTurn,
|
|
990
|
+
discardDeferredTurn,
|
|
991
|
+
openEventsStream,
|
|
992
|
+
};
|
|
973
993
|
}
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } f
|
|
|
2
2
|
import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
|
|
3
3
|
import { confirmationToken } from "./format.js";
|
|
4
4
|
import { clampShortcutIndex } from "./slash-palette-nav.js";
|
|
5
|
+
import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
|
|
5
6
|
import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
|
|
6
7
|
/** Composer state: input text, @file palette, image attachments, /shortcuts,
|
|
7
8
|
* IME composition guards, auto-grow and server-side draft sync. */
|
|
@@ -31,6 +32,8 @@ export function useComposer(params) {
|
|
|
31
32
|
const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
|
|
32
33
|
const [commandsEpoch, setCommandsEpoch] = useState(0);
|
|
33
34
|
const textareaRef = useRef(null);
|
|
35
|
+
/** Arrow-key input history; unsent drafts survive history browsing. */
|
|
36
|
+
const historyRef = useRef(createInputHistoryState());
|
|
34
37
|
/** Active session generation for /commands; bumped immediately on switch (AC-2). */
|
|
35
38
|
const commandsSessionRef = useRef(sessionId);
|
|
36
39
|
const commandsGenerationRef = useRef(0);
|
|
@@ -38,6 +41,33 @@ export function useComposer(params) {
|
|
|
38
41
|
setInput(value);
|
|
39
42
|
setShortcutOpen(value.startsWith("/"));
|
|
40
43
|
setShortcutIndex(0);
|
|
44
|
+
// Manual typing edits a recalled entry in place → back to present draft.
|
|
45
|
+
historyRef.current = resetInputHistoryBrowsing(historyRef.current);
|
|
46
|
+
}, []);
|
|
47
|
+
/** Recall an earlier input via ↑/↓; returns true when the key was consumed. */
|
|
48
|
+
const navigateHistory = useCallback((direction) => {
|
|
49
|
+
const textarea = textareaRef.current;
|
|
50
|
+
const decision = navigateInputHistory(historyRef.current, direction, {
|
|
51
|
+
text: input,
|
|
52
|
+
caretStart: textarea?.selectionStart ?? input.length,
|
|
53
|
+
caretEnd: textarea?.selectionEnd ?? input.length,
|
|
54
|
+
});
|
|
55
|
+
if (!decision.handled)
|
|
56
|
+
return false;
|
|
57
|
+
historyRef.current = decision.state;
|
|
58
|
+
setInput(decision.text);
|
|
59
|
+
const caret = decision.text.length;
|
|
60
|
+
queueMicrotask(() => {
|
|
61
|
+
const el = textareaRef.current;
|
|
62
|
+
if (el && el.value === decision.text) {
|
|
63
|
+
el.setSelectionRange(caret, caret);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return true;
|
|
67
|
+
}, [input, setInput]);
|
|
68
|
+
/** Record a submitted text into history (consecutive duplicates collapse). */
|
|
69
|
+
const recordHistory = useCallback((text) => {
|
|
70
|
+
historyRef.current = recordInputHistoryEntry(historyRef.current, text);
|
|
41
71
|
}, []);
|
|
42
72
|
// Immediate invalidation on A→B even when B palette never opens (AC-2).
|
|
43
73
|
useLayoutEffect(() => {
|
|
@@ -234,6 +264,8 @@ export function useComposer(params) {
|
|
|
234
264
|
input,
|
|
235
265
|
setInput,
|
|
236
266
|
handleInputChange,
|
|
267
|
+
navigateHistory,
|
|
268
|
+
recordHistory,
|
|
237
269
|
fileOptions,
|
|
238
270
|
pendingImages,
|
|
239
271
|
setPendingImages,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision core for the TasksPage run-id resolver (2026-08-23).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the React effect so the full lifecycle — wizard first run,
|
|
5
|
+
* rerun dispatch, task switch, foreign shared selection, baseline-gated
|
|
6
|
+
* lookup — is unit-testable as state transitions without mounting React.
|
|
7
|
+
*/
|
|
8
|
+
export function initialRunResolutionState() {
|
|
9
|
+
return { baseline: null, ownedRunId: "", pendingSelection: "" };
|
|
10
|
+
}
|
|
11
|
+
export function observeRunResolution(state, input) {
|
|
12
|
+
const firstObservation = state.task === undefined;
|
|
13
|
+
const taskChanged = !firstObservation && state.task !== input.task;
|
|
14
|
+
const dispatchChanged = !firstObservation && state.dispatch !== input.dispatchId;
|
|
15
|
+
const next = {
|
|
16
|
+
...state,
|
|
17
|
+
task: input.task,
|
|
18
|
+
dispatch: input.dispatchId,
|
|
19
|
+
pendingSelection: "",
|
|
20
|
+
};
|
|
21
|
+
if (taskChanged) {
|
|
22
|
+
next.baseline = null;
|
|
23
|
+
next.ownedRunId = "";
|
|
24
|
+
return { state: next, observation: { action: "reset-selection" } };
|
|
25
|
+
}
|
|
26
|
+
if (dispatchChanged) {
|
|
27
|
+
next.ownedRunId = "";
|
|
28
|
+
if (input.selectedRunId) {
|
|
29
|
+
next.baseline = input.selectedRunId;
|
|
30
|
+
return { state: next, observation: { action: "invalidate-selection" } };
|
|
31
|
+
}
|
|
32
|
+
// Nothing shown yet: keep the existing baseline and keep probing.
|
|
33
|
+
}
|
|
34
|
+
const selected = input.selectedRunId;
|
|
35
|
+
if (selected && selected !== next.ownedRunId) {
|
|
36
|
+
next.pendingSelection = selected;
|
|
37
|
+
return { state: next, observation: { action: "verify-ownership" } };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
state: next,
|
|
41
|
+
observation: { action: selected ? "idle" : "probe" },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function markBaselineUnknown(state) {
|
|
45
|
+
return { ...state, baseline: null };
|
|
46
|
+
}
|
|
47
|
+
export function applyBaselineSnapshot(state, dagRunId) {
|
|
48
|
+
return { ...state, baseline: dagRunId ?? "" };
|
|
49
|
+
}
|
|
50
|
+
/** Accept an observeLink lookup: only a run of this task that differs from
|
|
51
|
+
* the pre-dispatch baseline may be adopted — never the previous run, and
|
|
52
|
+
* never anything while the baseline is still unknown. */
|
|
53
|
+
export function applyRunLookup(state, outcome) {
|
|
54
|
+
if (!outcome.ok || !outcome.dagRunId) {
|
|
55
|
+
return { state, acceptedRunId: null };
|
|
56
|
+
}
|
|
57
|
+
if (state.baseline === null || outcome.dagRunId === state.baseline) {
|
|
58
|
+
return { state, acceptedRunId: null };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
state: { ...state, ownedRunId: outcome.dagRunId },
|
|
62
|
+
acceptedRunId: outcome.dagRunId,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Apply an authoritative ownership answer for the pending selection.
|
|
66
|
+
* `clearSelection` tells the adapter to drop the shared selection and
|
|
67
|
+
* snapshot a fresh baseline; owning marks it as resolver-owned. */
|
|
68
|
+
export function applyOwnership(state, owns) {
|
|
69
|
+
if (owns) {
|
|
70
|
+
return {
|
|
71
|
+
state: { ...state, ownedRunId: state.pendingSelection, pendingSelection: "" },
|
|
72
|
+
clearSelection: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
state: { ...state, ownedRunId: "", pendingSelection: "", baseline: null },
|
|
77
|
+
clearSelection: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership re-check backoff for an unverified shared DAG-run selection
|
|
3
|
+
* (2026-08-23): a transient candidates-lookup failure must neither leave
|
|
4
|
+
* another task's run displayed indefinitely (waiting for a state change
|
|
5
|
+
* that may never come) nor hammer the API.
|
|
6
|
+
*/
|
|
7
|
+
const OWNERSHIP_RETRY_DELAYS_MS = [
|
|
8
|
+
2_000, 2_000, 4_000, 4_000, 8_000, 8_000, 15_000, 15_000, 30_000, 30_000,
|
|
9
|
+
];
|
|
10
|
+
/** `owns`: true = same task, false = foreign task, null = check unavailable. */
|
|
11
|
+
export function planOwnershipVerification(owns, attempt) {
|
|
12
|
+
if (owns === true)
|
|
13
|
+
return { kind: "own" };
|
|
14
|
+
if (owns === false)
|
|
15
|
+
return { kind: "clear" };
|
|
16
|
+
const delayMs = OWNERSHIP_RETRY_DELAYS_MS[attempt];
|
|
17
|
+
return delayMs === undefined
|
|
18
|
+
? { kind: "give-up" }
|
|
19
|
+
: { kind: "retry", delayMs };
|
|
20
|
+
}
|
|
21
|
+
/** Total bounded re-verification window before giving up (keeps the last
|
|
22
|
+
* selection; the next state change restarts verification). */
|
|
23
|
+
export const OWNERSHIP_RETRY_WINDOW_MS = OWNERSHIP_RETRY_DELAYS_MS.reduce((total, delay) => total + delay, 0);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure projection from a DAG run detail payload (GET /api/dag-runs/:id) into
|
|
3
|
+
* the structured run progress shown on the wizard RunPanel: node counts,
|
|
4
|
+
* current node + elapsed time, failure pointer, and stall signal. The panel
|
|
5
|
+
* used to show only a raw SSE log tail — node/phase information drowned in
|
|
6
|
+
* log noise during 20-40 minute runs.
|
|
7
|
+
*/
|
|
8
|
+
export function classifyRunProgressNodeStatus(raw) {
|
|
9
|
+
const status = (raw ?? "").trim().toLowerCase();
|
|
10
|
+
if (!status)
|
|
11
|
+
return "pending";
|
|
12
|
+
if (status.includes("running") || status === "in_progress" || status === "active") {
|
|
13
|
+
return "running";
|
|
14
|
+
}
|
|
15
|
+
if (status.includes("fail") || status.includes("error") || status === "blocked") {
|
|
16
|
+
return "failed";
|
|
17
|
+
}
|
|
18
|
+
if (status.includes("finish") ||
|
|
19
|
+
status.includes("success") ||
|
|
20
|
+
status === "done" ||
|
|
21
|
+
status === "ok") {
|
|
22
|
+
return "finished";
|
|
23
|
+
}
|
|
24
|
+
if (status.includes("skip"))
|
|
25
|
+
return "skipped";
|
|
26
|
+
return "pending";
|
|
27
|
+
}
|
|
28
|
+
function toProgressNode(node) {
|
|
29
|
+
const nodeId = String(node.nodeId ?? node.id ?? "").trim();
|
|
30
|
+
return {
|
|
31
|
+
nodeId,
|
|
32
|
+
label: String((node.label ?? node.name ?? nodeId) || "未命名节点"),
|
|
33
|
+
status: String(node.status ?? ""),
|
|
34
|
+
rank: typeof node.rank === "string" ? node.rank : undefined,
|
|
35
|
+
startedAt: typeof node.startedAt === "string" ? node.startedAt : undefined,
|
|
36
|
+
durationMs: typeof node.durationMs === "number" ? node.durationMs : undefined,
|
|
37
|
+
errorPreview: typeof node.errorPreview === "string" ? node.errorPreview : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function elapsedSince(iso, now) {
|
|
41
|
+
if (!iso)
|
|
42
|
+
return null;
|
|
43
|
+
const started = Date.parse(iso);
|
|
44
|
+
if (Number.isNaN(started))
|
|
45
|
+
return null;
|
|
46
|
+
const elapsed = now - started;
|
|
47
|
+
return elapsed >= 0 ? elapsed : null;
|
|
48
|
+
}
|
|
49
|
+
export function summarizeRunProgress(dag, now = Date.now()) {
|
|
50
|
+
if (!dag) {
|
|
51
|
+
return {
|
|
52
|
+
total: 0,
|
|
53
|
+
finished: 0,
|
|
54
|
+
running: null,
|
|
55
|
+
failed: null,
|
|
56
|
+
terminal: false,
|
|
57
|
+
effectiveStatus: null,
|
|
58
|
+
stalled: false,
|
|
59
|
+
currentNodeElapsedMs: null,
|
|
60
|
+
runElapsedMs: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const rawNodes = Array.isArray(dag.nodes) ? dag.nodes : [];
|
|
64
|
+
const nodes = rawNodes.map(toProgressNode);
|
|
65
|
+
const classes = nodes.map((node) => classifyRunProgressNodeStatus(node.status));
|
|
66
|
+
const finished = classes.filter((cls) => cls === "finished" || cls === "skipped").length;
|
|
67
|
+
const runningIndex = classes.indexOf("running");
|
|
68
|
+
const running = runningIndex >= 0 ? nodes[runningIndex] : null;
|
|
69
|
+
const failedIndex = classes.indexOf("failed");
|
|
70
|
+
const failed = failedIndex >= 0 ? nodes[failedIndex] : null;
|
|
71
|
+
const effectiveStatus = String(dag.effectiveStatus ??
|
|
72
|
+
dag.status ??
|
|
73
|
+
"").toLowerCase();
|
|
74
|
+
const terminal = TERMINAL_RUN_PROGRESS_STATUSES.has(effectiveStatus);
|
|
75
|
+
const runStartedAt = typeof dag.startedAt === "string" ? dag.startedAt : undefined;
|
|
76
|
+
const runDurationMs = typeof dag.durationMs === "number" ? dag.durationMs : undefined;
|
|
77
|
+
return {
|
|
78
|
+
total: nodes.length,
|
|
79
|
+
finished,
|
|
80
|
+
running,
|
|
81
|
+
failed,
|
|
82
|
+
terminal,
|
|
83
|
+
effectiveStatus: effectiveStatus || null,
|
|
84
|
+
stalled: effectiveStatus === "running-suspected-stall",
|
|
85
|
+
currentNodeElapsedMs: running?.durationMs ?? elapsedSince(running?.startedAt, now),
|
|
86
|
+
runElapsedMs: terminal
|
|
87
|
+
? (runDurationMs ?? elapsedSince(runStartedAt, now))
|
|
88
|
+
: elapsedSince(runStartedAt, now),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const TERMINAL_RUN_PROGRESS_STATUSES = new Set([
|
|
92
|
+
"finished",
|
|
93
|
+
"failed",
|
|
94
|
+
"partial_failed",
|
|
95
|
+
"superseded",
|
|
96
|
+
"abandoned",
|
|
97
|
+
]);
|
|
98
|
+
export function formatDurationMs(ms) {
|
|
99
|
+
if (ms == null || !Number.isFinite(ms) || ms < 0)
|
|
100
|
+
return "—";
|
|
101
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
102
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
103
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
104
|
+
const seconds = totalSeconds % 60;
|
|
105
|
+
if (hours > 0)
|
|
106
|
+
return `${hours} 时 ${minutes} 分`;
|
|
107
|
+
if (minutes > 0)
|
|
108
|
+
return `${minutes} 分 ${seconds} 秒`;
|
|
109
|
+
return `${seconds} 秒`;
|
|
110
|
+
}
|
|
@@ -129,11 +129,14 @@ export async function resolveRevisionDigestsForTaskSpec(input) {
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
/** Union TaskSpec allowed_paths with workflow-owned write roots. */
|
|
132
|
-
export function expandAllowedPathsForWorkflow(workflow, allowedPaths) {
|
|
132
|
+
export function expandAllowedPathsForWorkflow(workflow, allowedPaths, options) {
|
|
133
133
|
const extras = workflow === "backend-test"
|
|
134
134
|
? ["testcase/**", "docs/test-reports/**"]
|
|
135
135
|
: workflow === "frontend-test"
|
|
136
|
-
? [
|
|
136
|
+
? [
|
|
137
|
+
`${options?.frontendTestRoot ?? "testcase/frontend"}/**`,
|
|
138
|
+
"docs/test-reports/**",
|
|
139
|
+
]
|
|
137
140
|
: [];
|
|
138
141
|
return Array.from(new Set([...allowedPaths, ...extras]));
|
|
139
142
|
}
|
|
@@ -1483,6 +1483,9 @@ function mergeDagRun(existing, incoming) {
|
|
|
1483
1483
|
computeDurationMs(startedAt, finishedAt);
|
|
1484
1484
|
return {
|
|
1485
1485
|
dagRunId: incoming.dagRunId,
|
|
1486
|
+
...((incoming.taskId ?? existing.taskId)
|
|
1487
|
+
? { taskId: incoming.taskId ?? existing.taskId }
|
|
1488
|
+
: {}),
|
|
1486
1489
|
status: incoming.status ?? existing.status,
|
|
1487
1490
|
lifecycle: incoming.lifecycle ?? existing.lifecycle,
|
|
1488
1491
|
executionMode: incoming.executionMode ?? existing.executionMode,
|
|
@@ -1914,8 +1917,10 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1914
1917
|
}
|
|
1915
1918
|
: undefined;
|
|
1916
1919
|
const failureCategory = readString(parsed, "failureCategory");
|
|
1920
|
+
const boundTaskId = await readRunSpecTaskBinding(runSpecPath);
|
|
1917
1921
|
return {
|
|
1918
1922
|
dagRunId,
|
|
1923
|
+
...(boundTaskId ? { taskId: boundTaskId } : {}),
|
|
1919
1924
|
status,
|
|
1920
1925
|
...(lifecycle ? { lifecycle } : {}),
|
|
1921
1926
|
...(executionMode ? { executionMode } : {}),
|
|
@@ -2435,6 +2440,23 @@ async function safeReadJson(filePath) {
|
|
|
2435
2440
|
return undefined;
|
|
2436
2441
|
return parsed;
|
|
2437
2442
|
}
|
|
2443
|
+
/** Task binding for a DAG run from run.json — same precedence as
|
|
2444
|
+
* console/resolve-dag-run-for-task (taskContractBinding → sourceBinding →
|
|
2445
|
+
* Hybrid DAG title). */
|
|
2446
|
+
async function readRunSpecTaskBinding(runSpecPath) {
|
|
2447
|
+
const parsed = await safeReadJson(runSpecPath);
|
|
2448
|
+
if (!parsed)
|
|
2449
|
+
return undefined;
|
|
2450
|
+
const binding = parsed.taskContractBinding;
|
|
2451
|
+
const source = parsed.sourceBinding;
|
|
2452
|
+
const fromBinding = (typeof binding?.taskId === "string" && binding.taskId.trim()) ||
|
|
2453
|
+
(typeof source?.taskId === "string" && source.taskId.trim());
|
|
2454
|
+
if (fromBinding)
|
|
2455
|
+
return fromBinding;
|
|
2456
|
+
const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
|
|
2457
|
+
const hybrid = /^Hybrid DAG:\s*(.+)$/i.exec(title);
|
|
2458
|
+
return hybrid?.[1]?.trim() || undefined;
|
|
2459
|
+
}
|
|
2438
2460
|
function safeParseJson(raw) {
|
|
2439
2461
|
try {
|
|
2440
2462
|
const parsed = JSON.parse(raw);
|
|
@@ -76,7 +76,7 @@ export async function executeDynamicLoopUntil(input) {
|
|
|
76
76
|
bodyIdMap,
|
|
77
77
|
}));
|
|
78
78
|
for (const child of children) {
|
|
79
|
-
assertValidMaterializedDagTask(child);
|
|
79
|
+
assertValidMaterializedDagTask(child, input.spec);
|
|
80
80
|
if (!input.tasksById.has(child.id)) {
|
|
81
81
|
input.tasksById.set(child.id, child);
|
|
82
82
|
input.spec.tasks.push(child);
|
|
@@ -170,7 +170,7 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
170
170
|
nodeId,
|
|
171
171
|
}));
|
|
172
172
|
for (const child of children) {
|
|
173
|
-
assertValidMaterializedDagTask(child);
|
|
173
|
+
assertValidMaterializedDagTask(child, input.spec);
|
|
174
174
|
}
|
|
175
175
|
const writeSetConflicts = collectDynamicChildWriteSetConflicts(children);
|
|
176
176
|
if (writeSetConflicts.length > 0) {
|
|
@@ -1,130 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"human-required",
|
|
10
|
-
"human-rejected",
|
|
11
|
-
"shell-command",
|
|
12
|
-
"static-error",
|
|
13
|
-
"decision-envelope",
|
|
14
|
-
"skipped",
|
|
15
|
-
"unknown",
|
|
16
|
-
]);
|
|
2
|
+
import { DAG_NORMALIZED_FAILURE_CATEGORY_VALUES } from "../../shared/dag-failure-category.js";
|
|
3
|
+
// The pure classification core lives in src/shared/dag-failure-category.ts so
|
|
4
|
+
// the Operator Console projection can normalize raw CLI categories without
|
|
5
|
+
// importing the workflows kernel (console/workflows boundary test). This
|
|
6
|
+
// module keeps the schema surface for the DAG kernel's existing consumers.
|
|
7
|
+
export { isDagFailureCategory, normalizeDagFailureCategory, } from "../../shared/dag-failure-category.js";
|
|
8
|
+
export const dagNormalizedFailureCategorySchema = z.enum(DAG_NORMALIZED_FAILURE_CATEGORY_VALUES);
|
|
17
9
|
export const DAG_NORMALIZED_FAILURE_CATEGORIES = dagNormalizedFailureCategorySchema.options;
|
|
18
|
-
const RAW_TO_NORMALIZED = {
|
|
19
|
-
success: "success",
|
|
20
|
-
"write-guard": "write-guard",
|
|
21
|
-
timeout: "timeout",
|
|
22
|
-
// Clean implementer stall with zero writes — still a timeout class for report,
|
|
23
|
-
// but retryable under WRITER_TRANSPORT_RETRY_POLICY.
|
|
24
|
-
"writer-clean-timeout": "timeout",
|
|
25
|
-
"missing-api-key": "auth",
|
|
26
|
-
auth: "auth",
|
|
27
|
-
"human-rejected": "human-rejected",
|
|
28
|
-
"human-required": "human-required",
|
|
29
|
-
"decision-envelope-invalid": "decision-envelope",
|
|
30
|
-
"nonzero-exit": "shell-command",
|
|
31
|
-
"spawn-error": "shell-command",
|
|
32
|
-
"test-timeout": "timeout",
|
|
33
|
-
"static-error": "static-error",
|
|
34
|
-
"invalid-output": "validation",
|
|
35
|
-
"structured-output-truncated": "validation",
|
|
36
|
-
"output-too-large": "validation",
|
|
37
|
-
"structured-repair-exhausted": "validation",
|
|
38
|
-
"governance-blocked": "validation",
|
|
39
|
-
"path-violation": "validation",
|
|
40
|
-
"empty-output": "validation",
|
|
41
|
-
"verify-failure": "validation",
|
|
42
|
-
"test-failure": "validation",
|
|
43
|
-
"verification-plan-stale": "validation",
|
|
44
|
-
network: "executor",
|
|
45
|
-
quota: "executor",
|
|
46
|
-
"rate-limit": "executor",
|
|
47
|
-
unavailable: "executor",
|
|
48
|
-
// Deterministic capability/policy refusal: missing verified launcher JS
|
|
49
|
-
// entry, capability allowlist rejection, or missing bounded write tool
|
|
50
|
-
// profile. These are environment/config defects the operator repairs
|
|
51
|
-
// before rerunning; classify as executor so recovery recommends
|
|
52
|
-
// rerun-after-fix WITHOUT forcing a human-decision gate on
|
|
53
|
-
// standaloneTaskRerun (run-failed same-task rerun).
|
|
54
|
-
"tool-policy": "executor",
|
|
55
|
-
unknown: "unknown",
|
|
56
|
-
};
|
|
57
|
-
function isFinishedStatus(status) {
|
|
58
|
-
return status === "FINISHED" || status === "finished";
|
|
59
|
-
}
|
|
60
|
-
function isInProgressStatus(status) {
|
|
61
|
-
return (status === "PENDING" ||
|
|
62
|
-
status === "RUNNING" ||
|
|
63
|
-
status === "pending" ||
|
|
64
|
-
status === "running");
|
|
65
|
-
}
|
|
66
|
-
function isFailureStatus(status) {
|
|
67
|
-
return (status === "ERROR" ||
|
|
68
|
-
status === "failed" ||
|
|
69
|
-
status === "partial_failed");
|
|
70
|
-
}
|
|
71
|
-
function mapRawCategory(raw) {
|
|
72
|
-
const direct = RAW_TO_NORMALIZED[raw];
|
|
73
|
-
if (direct)
|
|
74
|
-
return direct;
|
|
75
|
-
const lower = raw.toLowerCase();
|
|
76
|
-
if (lower.includes("timeout"))
|
|
77
|
-
return "timeout";
|
|
78
|
-
if (lower.includes("validation") ||
|
|
79
|
-
lower.includes("invalid") ||
|
|
80
|
-
lower.includes("verification-plan-stale") ||
|
|
81
|
-
lower.includes("plan-stale"))
|
|
82
|
-
return "validation";
|
|
83
|
-
if (lower.includes("auth") || lower.includes("api-key"))
|
|
84
|
-
return "auth";
|
|
85
|
-
if (lower.includes("write-guard") || lower.includes("write_guard"))
|
|
86
|
-
return "write-guard";
|
|
87
|
-
if (lower.includes("decision-envelope") || lower.includes("decision_envelope"))
|
|
88
|
-
return "decision-envelope";
|
|
89
|
-
if (lower.includes("human-rejected"))
|
|
90
|
-
return "human-rejected";
|
|
91
|
-
if (lower.includes("human-required"))
|
|
92
|
-
return "human-required";
|
|
93
|
-
if (lower.includes("static-error"))
|
|
94
|
-
return "static-error";
|
|
95
|
-
if (lower.includes("nonzero-exit") ||
|
|
96
|
-
lower.includes("spawn-error") ||
|
|
97
|
-
lower.includes("shell"))
|
|
98
|
-
return "shell-command";
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
export function isDagFailureCategory(value) {
|
|
102
|
-
return DAG_NORMALIZED_FAILURE_CATEGORIES.includes(value);
|
|
103
|
-
}
|
|
104
|
-
export function normalizeDagFailureCategory(raw, status) {
|
|
105
|
-
if (status === "SKIPPED") {
|
|
106
|
-
return "skipped";
|
|
107
|
-
}
|
|
108
|
-
if (raw !== undefined) {
|
|
109
|
-
const mapped = mapRawCategory(raw);
|
|
110
|
-
if (mapped) {
|
|
111
|
-
if (mapped === "success") {
|
|
112
|
-
return isFinishedStatus(status) ? "success" : "unknown";
|
|
113
|
-
}
|
|
114
|
-
return mapped;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (raw === undefined && isFinishedStatus(status)) {
|
|
118
|
-
return "success";
|
|
119
|
-
}
|
|
120
|
-
if (isInProgressStatus(status)) {
|
|
121
|
-
return "unknown";
|
|
122
|
-
}
|
|
123
|
-
if (isFailureStatus(status)) {
|
|
124
|
-
return "unknown";
|
|
125
|
-
}
|
|
126
|
-
if (raw === "success" && isFinishedStatus(status)) {
|
|
127
|
-
return "success";
|
|
128
|
-
}
|
|
129
|
-
return "unknown";
|
|
130
|
-
}
|
|
@@ -26,6 +26,11 @@ export function checkUiDesignCoverage(contract) {
|
|
|
26
26
|
const findings = [];
|
|
27
27
|
const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
|
|
28
28
|
const hasStyling = Boolean(contract.stylingStrategy?.trim());
|
|
29
|
+
// Reuse of an existing component/pattern covers behavioural interactions
|
|
30
|
+
// without forcing one uiComponentChoices row per interaction: the model
|
|
31
|
+
// should not enumerate every click/hover on a reused Button. Only
|
|
32
|
+
// interactions with no reuse and no explicit purpose are flagged.
|
|
33
|
+
const hasReuseChoice = (contract.uiComponentChoices ?? []).some((choice) => choice.decision === "reuse-existing");
|
|
29
34
|
for (const state of contract.uiStates) {
|
|
30
35
|
if (!state.applicable)
|
|
31
36
|
continue;
|
|
@@ -39,13 +44,13 @@ export function checkUiDesignCoverage(contract) {
|
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
for (const interaction of contract.interactions) {
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
47
|
+
if (purposes.has(interaction.name) || hasReuseChoice)
|
|
48
|
+
continue;
|
|
49
|
+
findings.push({
|
|
50
|
+
code: "ui-design-coverage-missing",
|
|
51
|
+
message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose and no reuse-existing choice covering it`,
|
|
52
|
+
path: interaction.name,
|
|
53
|
+
});
|
|
49
54
|
}
|
|
50
55
|
return findings;
|
|
51
56
|
}
|