@tea-agent/loop-agent 0.44.0-next.3 → 0.44.0-next.5
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 +8 -2
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +2 -2
- package/dist/executors/shell-write-guard.js +26 -3
- package/dist/shared/file-identity.js +23 -0
- package/dist/shared/git-workspace-status.js +480 -0
- package/dist/shared/path-safety.js +2 -2
- package/dist/task/config-types.js +2 -1
- package/dist/task/contract/schema.js +1 -0
- package/dist/worker/console/chat/chat-event-store.js +32 -0
- package/dist/worker/console/chat/context-insights.js +723 -0
- package/dist/worker/console/chat/conversation-nodes.js +383 -0
- package/dist/worker/console/chat/pi-runtime.js +128 -12
- package/dist/worker/console/chat/provider-error.js +0 -12
- package/dist/worker/console/chat/repo-browser.js +3 -4
- package/dist/worker/console/chat/routes.js +209 -4
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/turn-process.js +128 -7
- package/dist/worker/console/prd-intake-bridge.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-COQlLTI7.js → abnfDiagram-N423BO3Z-BHuhGM1e.js} +1 -1
- package/dist/worker/console/static/assets/{arc-zN3QkNaV.js → arc-CKh5E_sr.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Pyoyj5f2.js → architectureDiagram-T3A2C74G-DizhiBEv.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BNppA5WW.js → blockDiagram-VBNYF7ZC-3Neea-Z-.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-lyxL8nTx.js → c4Diagram-5PPSVZJV-BCP_Qrgi.js} +1 -1
- package/dist/worker/console/static/assets/channel-DxgzA1GF.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-0hgjcdK4.js → chunk-2GRJ4B5K-C_wydftK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D63N9d4i.js → chunk-2Q5K7J3B-jRPXZbzx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CUw2AyB5.js → chunk-5RXB4S5H-BLQf2OYO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Q_kwZT30.js → chunk-5VM5RSS4-0qRq64rI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C7vMiKcw.js → chunk-6Q2QTUOP-ClMg8xmV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-T9GbV5Hz.js → chunk-GF5L2VYU-Coop2FlL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D8nUSUJI.js → chunk-JWPE2WC7-CDdO-jPJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BCH2ewdp.js → chunk-KBJHAD2P-D8Gkppjl.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BTdDuDBN.js → chunk-RYQCIY6F-DA9HIySf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Cqm2RcKF.js → chunk-XXDRQBXY-DZNDRX1k.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-pjwfpVu4.js → cose-bilkent-JH36ORCC-CZ1zGyjq.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BvalptKA.js → cynefin-VYW2F7L2-B8u1_FtM.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BU4qJ6d7.js → cynefinDiagram-MW4NZA55-CPBB6lfo.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DVTpq8J9.js → dagre-VZM6K2ZE-CG3Eov3E.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B8s1W6Fc.js → diagram-7IWD3JNH-ycI-dOjh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DPrFk9yH.js → diagram-B4RE2ZJO-Bx_GyQ7Y.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CnQIQsHC.js → diagram-LBJQPF4R-m0G6mP4d.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-op0DEpCv.js → diagram-Q27KOJAE-DTDJkqcJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DXQamm4T.js → diagram-UB23O5K3-BCfFrG-p.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-b1ad2C0r.js → ebnfDiagram-BXEA7PRR-BIkTNPpp.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B4cD-ZFJ.js → erDiagram-JOGREHBK-DTcMWrd2.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CrgGjlQ1.js → flowDiagram-UKHOOZJN-BqzDaNQ7.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DovmXQNV.js → ganttDiagram-PKOTCBZU-BSl_vGFe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DpFyu2Jn.js → gitGraphDiagram-DS77QQ5N-DCA5HXM6.js} +1 -1
- package/dist/worker/console/static/assets/index-BAEDjTbD.css +1 -0
- package/dist/worker/console/static/assets/index-wKEJUaP5.js +482 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-D3MoQZIl.js → infoDiagram-6WML65LV-DvqT18ge.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Ci6C1yjS.js → ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWMo2lIP.js → journeyDiagram-NVQOT4AX-BpLQPI9C.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdthIAg3.js → kanban-definition-27J2QSJJ-CzIz_-Lt.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DuFAlSOj.js → linear-CMQ8Jpp-.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Dqz3Slj9.js → mermaid.core-BdAMrTf7.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DNVTrD09.js → mindmap-definition-FAOFIHXS-hSwF6sOq.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CgfZPfBh.js → pegDiagram-VL7TDLO6-DZdv509B.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ClM9P7yo.js → pieDiagram-7S7Q4E2Y-CmD4-b0K.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DwnEak2J.js → quadrantDiagram-CIZ2JOQS-DtKxNxlS.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-rK4hxt9P.js → railroadDiagram-AXF67PYL-D48iIoiS.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Cjqa6vo3.js → requirementDiagram-LRYGKXZP-C3N0K-2p.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D3Fa250Z.js → sankeyDiagram-W5VNT64P-CACWR-Xs.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-2PdrNajj.js → sequenceDiagram-SI44F4Z6-g_QgugvZ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BBcuGeZn.js → sizeCapture-X5ZJPWSS-DqLoRPGK.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CSJX9X6s.js → stateDiagram-OKZ733FA-ES59VgMG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-pOUU1bic.js → swimlanes-SLNWSIFB-fjyLVNWo.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CAeqfQn0.js → timeline-definition-Z64GVDOM-Cd5-biKv.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DYPLOMVO.js → vennDiagram-T6HMQDX7-CZTu6mPf.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Dy2V0Cha.js → wardleyDiagram-T6FBY63Y-DR01jjbe.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Dj9o7jrn.js → xychartDiagram-ELKLHX3M-iZcuGVGa.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +97 -3
- package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +11 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +9 -0
- package/dist/worker/observe/node-context-insights.js +479 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +3 -1
- package/dist/workflows/dag/frontend-plan-completeness.js +13 -3
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -6
- package/dist/workflows/dag/frontend-typed-event-store.js +5 -0
- package/dist/workflows/dag/frontend-writer-admission.js +1 -0
- package/dist/workflows/dag/output-protocol.js +1 -0
- package/dist/workflows/dag/rerun-task.js +1 -0
- package/docs/README.md +1 -1
- package/docs/governance/harness-methodology-verification.md +11 -0
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-CgLPkTw2.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuSD1Mhb.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuSD1Mhb.js +0 -1
- package/dist/worker/console/static/assets/index-DHBWEwz_.css +0 -1
- package/dist/worker/console/static/assets/index-JRfTUgP7.js +0 -478
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cqskj0zt.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CM0ZIhNP.js +0 -8
|
@@ -9,11 +9,15 @@ import { isSubagentTool } from "../../chat/tool-preview.js";
|
|
|
9
9
|
* @returns the three bucket counts.
|
|
10
10
|
*/
|
|
11
11
|
export function countTurnProcessItems(items) {
|
|
12
|
+
return countEntries(items.map((item) => ({ source: "process", item })));
|
|
13
|
+
}
|
|
14
|
+
function countEntries(entries) {
|
|
12
15
|
let toolCalls = 0;
|
|
13
16
|
let messages = 0;
|
|
14
17
|
let subagents = 0;
|
|
15
|
-
for (const
|
|
16
|
-
if (item.kind === "tool") {
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.source === "process" && entry.item.kind === "tool") {
|
|
20
|
+
const item = entry.item;
|
|
17
21
|
if (isSubagentTool(item.tool.toolName))
|
|
18
22
|
subagents += 1;
|
|
19
23
|
else
|
|
@@ -24,6 +28,11 @@ export function countTurnProcessItems(items) {
|
|
|
24
28
|
}
|
|
25
29
|
return { toolCalls, messages, subagents };
|
|
26
30
|
}
|
|
31
|
+
/** Count only the ordered process window; prelude/post-answer nodes are kept
|
|
32
|
+
* visible but do not inflate the DSH compact disclosure summary. */
|
|
33
|
+
export function countTurnFlowItems(entries) {
|
|
34
|
+
return countEntries(entries);
|
|
35
|
+
}
|
|
27
36
|
/**
|
|
28
37
|
* Summary label for a completed turn's process disclosure.
|
|
29
38
|
*
|
|
@@ -9,5 +9,6 @@ export function sameTurnGroupInputs(previous, next) {
|
|
|
9
9
|
previous.user === next.user &&
|
|
10
10
|
previous.live === next.live &&
|
|
11
11
|
sameReferences(previous.assistants, next.assistants) &&
|
|
12
|
-
sameReferences(previous.tools, next.tools)
|
|
12
|
+
sameReferences(previous.tools, next.tools) &&
|
|
13
|
+
sameReferences(previous.conversationItems ?? [], next.conversationItems ?? []));
|
|
13
14
|
}
|
|
@@ -501,6 +501,7 @@ export function useChatSessions(params) {
|
|
|
501
501
|
applyActivatedSession({
|
|
502
502
|
messages: hydrated.messages,
|
|
503
503
|
toolCalls: hydrated.toolCalls,
|
|
504
|
+
conversationEvents: body.conversationEvents,
|
|
504
505
|
sessionStats: body.sessionStats,
|
|
505
506
|
usage: body.usage,
|
|
506
507
|
turnTelemetry: body.turnTelemetry,
|
|
@@ -636,6 +637,7 @@ export function useChatSessions(params) {
|
|
|
636
637
|
applyActivatedSession({
|
|
637
638
|
messages: hydrated.messages,
|
|
638
639
|
toolCalls: hydrated.toolCalls,
|
|
640
|
+
conversationEvents: body.conversationEvents,
|
|
639
641
|
sessionStats: body.sessionStats,
|
|
640
642
|
usage: body.usage,
|
|
641
643
|
turnTelemetry: body.turnTelemetry,
|
|
@@ -8,6 +8,7 @@ import { mergePendingSubmissionOnHydration } from "./pending-user-message.js";
|
|
|
8
8
|
import { taskContextUnchanged } from "./format.js";
|
|
9
9
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
10
10
|
import { sameTurnGroupInputs } from "./turn-group-equality.js";
|
|
11
|
+
import { conversationNodesFromEvents, } from "../../chat/conversation-nodes.js";
|
|
11
12
|
const EMPTY_USAGE = {
|
|
12
13
|
inputTokens: 0,
|
|
13
14
|
outputTokens: 0,
|
|
@@ -141,6 +142,7 @@ export function useChatThread(params) {
|
|
|
141
142
|
const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
|
|
142
143
|
const [turnTelemetry, setTurnTelemetry] = useState([]);
|
|
143
144
|
const [operatorActions, setOperatorActions] = useState([]);
|
|
145
|
+
const [conversationNodes, setConversationNodes] = useState([]);
|
|
144
146
|
const [sddSessionIdentity, setSddSessionIdentity] = useState(null);
|
|
145
147
|
const [streaming, setStreaming] = useState(false);
|
|
146
148
|
const [turnPhase, setTurnPhase] = useState("idle");
|
|
@@ -192,6 +194,7 @@ export function useChatThread(params) {
|
|
|
192
194
|
setTodos,
|
|
193
195
|
setGoal,
|
|
194
196
|
setOperatorActions,
|
|
197
|
+
setConversationNodes,
|
|
195
198
|
setSddSessionIdentity,
|
|
196
199
|
setStreamingAssistantId,
|
|
197
200
|
setUnread,
|
|
@@ -330,6 +333,7 @@ export function useChatThread(params) {
|
|
|
330
333
|
toolCalls,
|
|
331
334
|
streaming,
|
|
332
335
|
activeAssistantId: streamingAssistantId,
|
|
336
|
+
conversationNodes,
|
|
333
337
|
});
|
|
334
338
|
// Preserve completed group objects across streaming tokens. Stable
|
|
335
339
|
// historical groups keep their references so
|
|
@@ -348,6 +352,7 @@ export function useChatThread(params) {
|
|
|
348
352
|
toolCalls,
|
|
349
353
|
streaming,
|
|
350
354
|
streamingAssistantId,
|
|
355
|
+
conversationNodes,
|
|
351
356
|
historyLimit,
|
|
352
357
|
]);
|
|
353
358
|
const loadEarlierMessages = useCallback(async (beforePrepend) => {
|
|
@@ -427,6 +432,7 @@ export function useChatThread(params) {
|
|
|
427
432
|
setSessionStats(EMPTY_SESSION_STATS);
|
|
428
433
|
setTurnTelemetry([]);
|
|
429
434
|
setOperatorActions([]);
|
|
435
|
+
setConversationNodes([]);
|
|
430
436
|
setSddSessionIdentity(null);
|
|
431
437
|
setStreaming(false);
|
|
432
438
|
setTurnPhase("idle");
|
|
@@ -476,6 +482,7 @@ export function useChatThread(params) {
|
|
|
476
482
|
}
|
|
477
483
|
: undefined));
|
|
478
484
|
setToolCalls(next.toolCalls);
|
|
485
|
+
setConversationNodes(conversationNodesFromEvents(next.conversationEvents ?? []));
|
|
479
486
|
setOperationCards([]);
|
|
480
487
|
setHumanGateCards([]);
|
|
481
488
|
setArtifactCards([]);
|
|
@@ -546,6 +553,7 @@ export function useChatThread(params) {
|
|
|
546
553
|
refs.turnAssistantIdsRef.current = new Map();
|
|
547
554
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
548
555
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
556
|
+
setConversationNodes(conversationNodesFromEvents(body.conversationEvents ?? []));
|
|
549
557
|
// Recovery is addressed to targetSessionId, so an optimistic
|
|
550
558
|
// submission from another session must never be projected into
|
|
551
559
|
// this snapshot (the activation path enforces the same owner gate).
|
|
@@ -618,6 +626,7 @@ export function useChatThread(params) {
|
|
|
618
626
|
sessionStats,
|
|
619
627
|
turnTelemetry,
|
|
620
628
|
operatorActions,
|
|
629
|
+
conversationNodes,
|
|
621
630
|
sddSessionIdentity,
|
|
622
631
|
setSddSessionIdentity,
|
|
623
632
|
streaming,
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only context insights projection for a DAG node attempt — the
|
|
3
|
+
* loop-agent counterpart of the dsh-context dashboard, built exclusively from
|
|
4
|
+
* recorded run facts:
|
|
5
|
+
*
|
|
6
|
+
* - context receipt (`<node>/context-receipts/attempt-N.json`) → composition
|
|
7
|
+
* - session events (`<node>/session-events.jsonl`) → per-request usage
|
|
8
|
+
* trend, tool activity, file footprint, timing
|
|
9
|
+
* - node record (`<node>.json`) → skill injection footprint, headline totals
|
|
10
|
+
*
|
|
11
|
+
* Everything is derived deterministically from persisted facts; nothing is
|
|
12
|
+
* estimated beyond what the recorded bytes already contain. All projections are
|
|
13
|
+
* read-only, bounded, and redaction-preserving (paths already pass through
|
|
14
|
+
* redactSecrets in node-transparency before reaching here for receipt data;
|
|
15
|
+
* file-activity paths are redacted in this module).
|
|
16
|
+
*/
|
|
17
|
+
import { readFile } from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { redactSecrets } from "../../shared/preview.js";
|
|
20
|
+
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
21
|
+
import { resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
22
|
+
export const NODE_CONTEXT_INSIGHTS_SCHEMA_VERSION = 1;
|
|
23
|
+
/** Upper bound on session-event lines scanned per attempt (defensive). */
|
|
24
|
+
const SESSION_EVENT_LINE_LIMIT = 50_000;
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
function asInt(value) {
|
|
29
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
30
|
+
? value
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
function asString(value) {
|
|
34
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
35
|
+
}
|
|
36
|
+
/** Minimal per-line JSON parse (session events are JSONL). */
|
|
37
|
+
function parseSessionEventLine(line) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(line);
|
|
40
|
+
return isRecord(parsed) ? parsed : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function messageOf(event) {
|
|
47
|
+
const message = event.message;
|
|
48
|
+
return isRecord(message) ? message : null;
|
|
49
|
+
}
|
|
50
|
+
function contentBlocksOf(message) {
|
|
51
|
+
const content = message?.content;
|
|
52
|
+
return Array.isArray(content) ? content : [];
|
|
53
|
+
}
|
|
54
|
+
function usageOf(message) {
|
|
55
|
+
const usage = message?.usage;
|
|
56
|
+
return isRecord(usage) ? usage : null;
|
|
57
|
+
}
|
|
58
|
+
function toolNameOfArgs(args) {
|
|
59
|
+
for (const key of ["path", "file", "file_path", "filePath"]) {
|
|
60
|
+
const value = args[key];
|
|
61
|
+
if (typeof value === "string" && value.length > 0)
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function searchTargetsOfArgs(args) {
|
|
67
|
+
const targets = [];
|
|
68
|
+
const push = (value) => {
|
|
69
|
+
if (typeof value === "string" && value.length > 0)
|
|
70
|
+
targets.push(value);
|
|
71
|
+
};
|
|
72
|
+
push(args.path);
|
|
73
|
+
push(args.directory);
|
|
74
|
+
push(args.dir);
|
|
75
|
+
const pattern = asString(args.pattern) ?? asString(args.query);
|
|
76
|
+
if (pattern)
|
|
77
|
+
targets.push(`pattern:${pattern.slice(0, 120)}`);
|
|
78
|
+
return targets;
|
|
79
|
+
}
|
|
80
|
+
/** Extract referenced file paths from a bash command string (best effort). */
|
|
81
|
+
function filesFromCommand(command) {
|
|
82
|
+
const files = [];
|
|
83
|
+
const patterns = [
|
|
84
|
+
/(?:^|\s)(?:cat|head|tail|less|more)\s+([^\s;&|]+)/g,
|
|
85
|
+
/(?:^|\s)(?:rg|grep|find|fd|ls)\s+.*?([^\s;&|]*\.[A-Za-z0-9]{1,8})/g,
|
|
86
|
+
/(?:^|\s)(?:sed|awk|sort|uniq|wc)\s+.*?([^\s;&|]*\.[A-Za-z0-9]{1,8})/g,
|
|
87
|
+
];
|
|
88
|
+
for (const pattern of patterns) {
|
|
89
|
+
let match;
|
|
90
|
+
while ((match = pattern.exec(command)) !== null) {
|
|
91
|
+
const candidate = match[1];
|
|
92
|
+
if (candidate && candidate.length > 1)
|
|
93
|
+
files.push(candidate);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
function classifyFileOp(toolName) {
|
|
99
|
+
const name = toolName.toLowerCase();
|
|
100
|
+
if (["read", "read_file", "readfile", "view"].includes(name))
|
|
101
|
+
return "read";
|
|
102
|
+
if (["write", "write_file", "writefile", "create"].includes(name))
|
|
103
|
+
return "write";
|
|
104
|
+
if (["edit", "str_replace", "str_replace_editor", "apply_patch"].includes(name))
|
|
105
|
+
return "edit";
|
|
106
|
+
if (["grep", "rg", "find", "fd", "search", "glob", "list", "ls"].includes(name))
|
|
107
|
+
return "search";
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function normalizeRepoPath(raw, repoRoot) {
|
|
111
|
+
let value = raw;
|
|
112
|
+
const resolvedRoot = path.resolve(repoRoot);
|
|
113
|
+
const rootVariants = [
|
|
114
|
+
resolvedRoot,
|
|
115
|
+
resolvedRoot.replace(/\\/g, "/"),
|
|
116
|
+
resolvedRoot.replace(/\\/g, "/").toLowerCase(),
|
|
117
|
+
];
|
|
118
|
+
for (const variant of rootVariants) {
|
|
119
|
+
if (value.toLowerCase().startsWith(`${variant.toLowerCase()}\\`) ||
|
|
120
|
+
value.toLowerCase().startsWith(`${variant.toLowerCase()}/`)) {
|
|
121
|
+
value = value.slice(variant.length + 1);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (value.toLowerCase() === variant.toLowerCase()) {
|
|
125
|
+
value = ".";
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return value.replace(/\\/g, "/");
|
|
130
|
+
}
|
|
131
|
+
function accumulateFileActivity(fileActivity, key, op) {
|
|
132
|
+
const existing = fileActivity.get(key) ?? {
|
|
133
|
+
path: key,
|
|
134
|
+
reads: 0,
|
|
135
|
+
writes: 0,
|
|
136
|
+
edits: 0,
|
|
137
|
+
searches: 0,
|
|
138
|
+
};
|
|
139
|
+
if (op === "read")
|
|
140
|
+
existing.reads += 1;
|
|
141
|
+
else if (op === "write")
|
|
142
|
+
existing.writes += 1;
|
|
143
|
+
else if (op === "edit")
|
|
144
|
+
existing.edits += 1;
|
|
145
|
+
else
|
|
146
|
+
existing.searches += 1;
|
|
147
|
+
fileActivity.set(key, existing);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Pure projection over parsed session events. Exported for unit tests.
|
|
151
|
+
*/
|
|
152
|
+
export function projectSessionInsights(events, repoRoot) {
|
|
153
|
+
let requestCount = 0;
|
|
154
|
+
let toolCalls = 0;
|
|
155
|
+
let toolErrors = 0;
|
|
156
|
+
let humanInputs = 0;
|
|
157
|
+
let thinkingSteps = 0;
|
|
158
|
+
let inputTokens = 0;
|
|
159
|
+
let outputTokens = 0;
|
|
160
|
+
let cacheReadTokens = 0;
|
|
161
|
+
let cacheWriteTokens = 0;
|
|
162
|
+
let cacheSamples = 0;
|
|
163
|
+
let cacheHits = 0;
|
|
164
|
+
const requests = [];
|
|
165
|
+
const toolCounters = new Map();
|
|
166
|
+
const fileActivity = new Map();
|
|
167
|
+
// Tool calls issued in the current in-flight model request (reset per
|
|
168
|
+
// assistant message_end) so each request row attributes its own calls.
|
|
169
|
+
let pendingToolCalls = 0;
|
|
170
|
+
let pendingThinking = false;
|
|
171
|
+
for (const event of events) {
|
|
172
|
+
if (!event)
|
|
173
|
+
continue;
|
|
174
|
+
const type = asString(event.type);
|
|
175
|
+
if (type === "tool_execution_start") {
|
|
176
|
+
toolCalls += 1;
|
|
177
|
+
pendingToolCalls += 1;
|
|
178
|
+
const toolName = asString(event.toolName) ?? "unknown";
|
|
179
|
+
const row = toolCounters.get(toolName) ?? {
|
|
180
|
+
toolName,
|
|
181
|
+
calls: 0,
|
|
182
|
+
errors: 0,
|
|
183
|
+
};
|
|
184
|
+
row.calls += 1;
|
|
185
|
+
toolCounters.set(toolName, row);
|
|
186
|
+
const args = isRecord(event.args) ? event.args : {};
|
|
187
|
+
const op = classifyFileOp(toolName);
|
|
188
|
+
if (op === "read" || op === "write" || op === "edit") {
|
|
189
|
+
const target = toolNameOfArgs(args);
|
|
190
|
+
if (target) {
|
|
191
|
+
accumulateFileActivity(fileActivity, normalizeRepoPath(redactSecrets(target), repoRoot), op);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (op === "search") {
|
|
195
|
+
for (const target of searchTargetsOfArgs(args)) {
|
|
196
|
+
accumulateFileActivity(fileActivity, normalizeRepoPath(redactSecrets(target), repoRoot), "search");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (toolName.toLowerCase() === "bash") {
|
|
200
|
+
const command = asString(args.command) ?? "";
|
|
201
|
+
for (const file of filesFromCommand(command)) {
|
|
202
|
+
accumulateFileActivity(fileActivity, normalizeRepoPath(redactSecrets(file), repoRoot), "search");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (type === "tool_execution_end") {
|
|
208
|
+
if (event.isError === true)
|
|
209
|
+
toolErrors += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const message = messageOf(event);
|
|
213
|
+
if (!message)
|
|
214
|
+
continue;
|
|
215
|
+
const role = asString(message.role);
|
|
216
|
+
if (type === "message_start" && role === "assistant") {
|
|
217
|
+
// A new model request begins.
|
|
218
|
+
requestCount += 1;
|
|
219
|
+
pendingToolCalls = 0;
|
|
220
|
+
pendingThinking = false;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (type === "message_end" && role === "assistant") {
|
|
224
|
+
for (const block of contentBlocksOf(message)) {
|
|
225
|
+
if (isRecord(block) && asString(block.type) === "thinking") {
|
|
226
|
+
thinkingSteps += 1;
|
|
227
|
+
pendingThinking = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const usage = usageOf(message);
|
|
231
|
+
const input = asInt(usage?.input);
|
|
232
|
+
const output = asInt(usage?.output);
|
|
233
|
+
const cacheRead = asInt(usage?.cacheRead);
|
|
234
|
+
const cacheWrite = asInt(usage?.cacheWrite);
|
|
235
|
+
if (usage && (input ?? 0) + (cacheRead ?? 0) > 0) {
|
|
236
|
+
cacheSamples += 1;
|
|
237
|
+
if ((cacheRead ?? 0) > 0)
|
|
238
|
+
cacheHits += 1;
|
|
239
|
+
}
|
|
240
|
+
if (input != null)
|
|
241
|
+
inputTokens += input;
|
|
242
|
+
if (output != null)
|
|
243
|
+
outputTokens += output;
|
|
244
|
+
if (cacheRead != null)
|
|
245
|
+
cacheReadTokens += cacheRead;
|
|
246
|
+
if (cacheWrite != null)
|
|
247
|
+
cacheWriteTokens += cacheWrite;
|
|
248
|
+
requests.push({
|
|
249
|
+
request: requestCount,
|
|
250
|
+
recordedAt: asString(event.recordedAt),
|
|
251
|
+
model: asString(message.model),
|
|
252
|
+
provider: asString(message.provider),
|
|
253
|
+
usage: usage == null
|
|
254
|
+
? null
|
|
255
|
+
: {
|
|
256
|
+
input,
|
|
257
|
+
output,
|
|
258
|
+
cacheRead,
|
|
259
|
+
cacheWrite,
|
|
260
|
+
totalTokens: asInt(usage.totalTokens),
|
|
261
|
+
},
|
|
262
|
+
stopReason: asString(message.stopReason),
|
|
263
|
+
toolCalls: pendingToolCalls,
|
|
264
|
+
thinking: pendingThinking,
|
|
265
|
+
});
|
|
266
|
+
pendingToolCalls = 0;
|
|
267
|
+
pendingThinking = false;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (type === "message_end" && role === "user") {
|
|
271
|
+
humanInputs += 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const toolActivity = [...toolCounters.values()].sort((a, b) => b.calls - a.calls || a.toolName.localeCompare(b.toolName));
|
|
275
|
+
const fileActivityRows = [...fileActivity.values()].sort((a, b) => b.reads + b.writes + b.edits + b.searches -
|
|
276
|
+
(a.reads + a.writes + a.edits + a.searches) ||
|
|
277
|
+
a.path.localeCompare(b.path));
|
|
278
|
+
return {
|
|
279
|
+
requests,
|
|
280
|
+
toolActivity,
|
|
281
|
+
fileActivity: fileActivityRows,
|
|
282
|
+
stats: {
|
|
283
|
+
requests: requestCount,
|
|
284
|
+
toolCalls,
|
|
285
|
+
toolErrors,
|
|
286
|
+
humanInputs,
|
|
287
|
+
thinkingSteps,
|
|
288
|
+
cacheHitRate: cacheSamples > 0 ? cacheHits / cacheSamples : null,
|
|
289
|
+
inputTokens,
|
|
290
|
+
outputTokens,
|
|
291
|
+
cacheReadTokens,
|
|
292
|
+
cacheWriteTokens,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export async function loadDagNodeContextInsights(repoRoot, dagRunId, nodeId, attempt) {
|
|
297
|
+
if (!isSafeObservabilityIdentifier(dagRunId)) {
|
|
298
|
+
return {
|
|
299
|
+
status: 400,
|
|
300
|
+
body: { error: "Invalid DAG run identifier", reason: "invalid-id" },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (!isSafeObservabilityIdentifier(nodeId)) {
|
|
304
|
+
return {
|
|
305
|
+
status: 400,
|
|
306
|
+
body: { error: "Invalid node identifier", reason: "invalid-id" },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const resolvedRun = resolveDagRunJson(repoRoot, dagRunId);
|
|
310
|
+
if (!resolvedRun.ok) {
|
|
311
|
+
return {
|
|
312
|
+
status: resolvedRun.reason === "invalid-id" ? 400 : 404,
|
|
313
|
+
body: { error: "DAG run not found", reason: resolvedRun.reason },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// Node record (<nodeId>.json) carries attempt metadata + headline totals.
|
|
317
|
+
const recordResolved = resolveDagRunArtifact(repoRoot, dagRunId, [
|
|
318
|
+
`${nodeId}.json`,
|
|
319
|
+
]);
|
|
320
|
+
let nodeRecord = null;
|
|
321
|
+
if (recordResolved.ok) {
|
|
322
|
+
try {
|
|
323
|
+
const raw = JSON.parse(await readFile(recordResolved.result.absolutePath, "utf8"));
|
|
324
|
+
if (isRecord(raw))
|
|
325
|
+
nodeRecord = raw;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
nodeRecord = null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const selectedAttempt = normalizeAttemptNumber(attempt, nodeRecord);
|
|
332
|
+
// Context receipt composition.
|
|
333
|
+
const receiptResolved = resolveDagRunArtifact(repoRoot, dagRunId, [
|
|
334
|
+
nodeId,
|
|
335
|
+
"context-receipts",
|
|
336
|
+
`attempt-${selectedAttempt}.json`,
|
|
337
|
+
]);
|
|
338
|
+
let composition = null;
|
|
339
|
+
if (receiptResolved.ok) {
|
|
340
|
+
try {
|
|
341
|
+
const raw = JSON.parse(await readFile(receiptResolved.result.absolutePath, "utf8"));
|
|
342
|
+
if (isRecord(raw) && Array.isArray(raw.orderedComponents)) {
|
|
343
|
+
const byKind = new Map();
|
|
344
|
+
for (const entry of raw.orderedComponents) {
|
|
345
|
+
if (!isRecord(entry))
|
|
346
|
+
continue;
|
|
347
|
+
const kind = asString(entry.kind);
|
|
348
|
+
const bytes = asInt(entry.bytes) ?? 0;
|
|
349
|
+
if (!kind)
|
|
350
|
+
continue;
|
|
351
|
+
const slice = byKind.get(kind) ?? { kind, items: 0, bytes: 0 };
|
|
352
|
+
slice.items += 1;
|
|
353
|
+
slice.bytes += bytes;
|
|
354
|
+
byKind.set(kind, slice);
|
|
355
|
+
}
|
|
356
|
+
composition = [...byKind.values()].sort((a, b) => b.bytes - a.bytes);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
composition = null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Session events for the node (single file across attempts in v1 format).
|
|
364
|
+
const sessionResolved = resolveDagRunArtifact(repoRoot, dagRunId, [
|
|
365
|
+
nodeId,
|
|
366
|
+
"session-events.jsonl",
|
|
367
|
+
]);
|
|
368
|
+
let sessionProjection = null;
|
|
369
|
+
if (sessionResolved.ok) {
|
|
370
|
+
try {
|
|
371
|
+
const raw = await readFile(sessionResolved.result.absolutePath, "utf8");
|
|
372
|
+
const lines = raw.split("\n").slice(0, SESSION_EVENT_LINE_LIMIT);
|
|
373
|
+
const events = lines
|
|
374
|
+
.map((line) => line.trim())
|
|
375
|
+
.filter(Boolean)
|
|
376
|
+
.map(parseSessionEventLine);
|
|
377
|
+
sessionProjection = projectSessionInsights(events, repoRoot);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
sessionProjection = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const attemptRecord = findAttemptRecord(nodeRecord, selectedAttempt);
|
|
384
|
+
const timing = timingFromRecords(nodeRecord, attemptRecord);
|
|
385
|
+
const skills = skillInjectionsOf(nodeRecord);
|
|
386
|
+
const body = {
|
|
387
|
+
schemaVersion: NODE_CONTEXT_INSIGHTS_SCHEMA_VERSION,
|
|
388
|
+
dagRunId,
|
|
389
|
+
nodeId,
|
|
390
|
+
attempt: selectedAttempt,
|
|
391
|
+
generatedFrom: {
|
|
392
|
+
sessionEvents: sessionProjection != null,
|
|
393
|
+
contextReceipt: composition != null,
|
|
394
|
+
nodeRecord: nodeRecord != null,
|
|
395
|
+
},
|
|
396
|
+
stats: {
|
|
397
|
+
requests: sessionProjection?.stats.requests ?? 0,
|
|
398
|
+
toolCalls: sessionProjection?.stats.toolCalls ?? 0,
|
|
399
|
+
toolErrors: sessionProjection?.stats.toolErrors ?? 0,
|
|
400
|
+
humanInputs: sessionProjection?.stats.humanInputs ?? 0,
|
|
401
|
+
thinkingSteps: sessionProjection?.stats.thinkingSteps ?? 0,
|
|
402
|
+
tokensUsed: asInt(attemptRecord?.tokensUsed) ?? asInt(nodeRecord?.tokensUsed),
|
|
403
|
+
parsedEvents: asInt(attemptRecord?.parsedEvents) ?? asInt(nodeRecord?.parsedEvents),
|
|
404
|
+
writeToolCallCount: asInt(attemptRecord?.writeToolCallCount) ??
|
|
405
|
+
asInt(nodeRecord?.writeToolCallCount),
|
|
406
|
+
cacheHitRate: sessionProjection?.stats.cacheHitRate ?? null,
|
|
407
|
+
inputTokens: sessionProjection?.stats.inputTokens ?? null,
|
|
408
|
+
outputTokens: sessionProjection?.stats.outputTokens ?? null,
|
|
409
|
+
cacheReadTokens: sessionProjection?.stats.cacheReadTokens ?? null,
|
|
410
|
+
cacheWriteTokens: sessionProjection?.stats.cacheWriteTokens ?? null,
|
|
411
|
+
},
|
|
412
|
+
timing,
|
|
413
|
+
composition,
|
|
414
|
+
skills,
|
|
415
|
+
toolActivity: sessionProjection?.toolActivity ?? [],
|
|
416
|
+
fileActivity: sessionProjection?.fileActivity ?? [],
|
|
417
|
+
usageTrend: sessionProjection?.requests ?? [],
|
|
418
|
+
};
|
|
419
|
+
return { status: 200, body };
|
|
420
|
+
}
|
|
421
|
+
function normalizeAttemptNumber(rawAttempt, nodeRecord) {
|
|
422
|
+
if (Number.isSafeInteger(rawAttempt) && (rawAttempt ?? 0) > 0) {
|
|
423
|
+
return rawAttempt;
|
|
424
|
+
}
|
|
425
|
+
if (typeof nodeRecord?.currentAttempt === "number" &&
|
|
426
|
+
Number.isSafeInteger(nodeRecord.currentAttempt) &&
|
|
427
|
+
nodeRecord.currentAttempt > 0) {
|
|
428
|
+
return nodeRecord.currentAttempt;
|
|
429
|
+
}
|
|
430
|
+
return 1;
|
|
431
|
+
}
|
|
432
|
+
function findAttemptRecord(nodeRecord, attempt) {
|
|
433
|
+
if (!nodeRecord)
|
|
434
|
+
return null;
|
|
435
|
+
const attempts = nodeRecord.attempts;
|
|
436
|
+
if (!Array.isArray(attempts))
|
|
437
|
+
return null;
|
|
438
|
+
for (const entry of attempts) {
|
|
439
|
+
if (isRecord(entry) && entry.attempt === attempt)
|
|
440
|
+
return entry;
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
function timingFromRecords(nodeRecord, attemptRecord) {
|
|
445
|
+
const attemptTiming = isRecord(attemptRecord?.timing)
|
|
446
|
+
? attemptRecord.timing
|
|
447
|
+
: null;
|
|
448
|
+
const nodeTiming = isRecord(nodeRecord?.timing)
|
|
449
|
+
? nodeRecord.timing
|
|
450
|
+
: null;
|
|
451
|
+
return {
|
|
452
|
+
startedAt: asString(attemptRecord?.startedAt) ?? asString(nodeRecord?.startedAt),
|
|
453
|
+
finishedAt: asString(attemptRecord?.finishedAt) ?? asString(nodeRecord?.finishedAt),
|
|
454
|
+
durationMs: asInt(attemptRecord?.durationMs) ?? asInt(nodeRecord?.durationMs),
|
|
455
|
+
executorDurationMs: asInt(attemptTiming?.executorDurationMs) ??
|
|
456
|
+
asInt(nodeTiming?.executorDurationMs),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function skillInjectionsOf(nodeRecord) {
|
|
460
|
+
const raw = nodeRecord?.resolvedSkills;
|
|
461
|
+
if (!Array.isArray(raw))
|
|
462
|
+
return [];
|
|
463
|
+
const skills = [];
|
|
464
|
+
for (const entry of raw) {
|
|
465
|
+
if (!isRecord(entry))
|
|
466
|
+
continue;
|
|
467
|
+
const name = asString(entry.name);
|
|
468
|
+
if (!name)
|
|
469
|
+
continue;
|
|
470
|
+
skills.push({
|
|
471
|
+
name,
|
|
472
|
+
resolution: asString(entry.resolution) ?? "unknown",
|
|
473
|
+
bytes: asInt(entry.bytes),
|
|
474
|
+
truncated: entry.truncated === true,
|
|
475
|
+
injectionMode: asString(entry.injectionMode),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return skills;
|
|
479
|
+
}
|
|
@@ -619,13 +619,15 @@ export function composeDependencyPolicyWithBoundary(policy, boundary) {
|
|
|
619
619
|
return policy;
|
|
620
620
|
const allowed = (boundary.allowed ?? []).filter((item) => item.trim());
|
|
621
621
|
const forbidden = (boundary.forbidden ?? []).filter((item) => item.trim());
|
|
622
|
-
|
|
622
|
+
const requiredClauses = (boundary.requiredClauses ?? []).filter((item) => item.trim());
|
|
623
|
+
if (allowed.length === 0 && forbidden.length === 0 && requiredClauses.length === 0)
|
|
623
624
|
return policy;
|
|
624
625
|
if (policy?.includes(CAPABILITY_BOUNDARY_MARKER))
|
|
625
626
|
return policy;
|
|
626
627
|
const sentence = `${CAPABILITY_BOUNDARY_MARKER}:` +
|
|
627
628
|
(allowed.length > 0 ? `仅可使用 ${allowed.join("、")}` : "无允许清单") +
|
|
628
629
|
(forbidden.length > 0 ? `;禁止 ${forbidden.join("、")}` : "") +
|
|
630
|
+
(requiredClauses.length > 0 ? `;必须满足 ${requiredClauses.join(";")}` : "") +
|
|
629
631
|
"。";
|
|
630
632
|
return policy?.trim() ? `${policy.trim()};${sentence}` : sentence;
|
|
631
633
|
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { collectCanonicalStateFlowNames } from "./frontend-contract-facts.js";
|
|
2
|
+
export function enrichFrontendPlanMissingFacts(missing) {
|
|
3
|
+
return missing.map((item) => ({
|
|
4
|
+
...item,
|
|
5
|
+
owner: "Plan",
|
|
6
|
+
repairScope: item.kind === "state-registry" || item.kind === "mock-api"
|
|
7
|
+
? "global-phase"
|
|
8
|
+
: item.requirementIds.length > 1 ? "requirement-slice" : "fact",
|
|
9
|
+
nextAction: "replace-fact-and-finalize",
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
2
12
|
export function committedFactFromPlanRecord(value) {
|
|
3
13
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4
14
|
return undefined;
|
|
@@ -94,7 +104,7 @@ export function collectFrontendPlanMissingFacts(input) {
|
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
|
-
return missing;
|
|
107
|
+
return enrichFrontendPlanMissingFacts(missing);
|
|
98
108
|
}
|
|
99
109
|
/** Completeness checks for phases whose facts are committed incrementally. */
|
|
100
110
|
export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
@@ -155,7 +165,7 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
|
155
165
|
});
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
|
-
return missing;
|
|
168
|
+
return enrichFrontendPlanMissingFacts(missing);
|
|
159
169
|
}
|
|
160
170
|
const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
|
|
161
171
|
const allInteractions = collectCanonicalStateFlowNames(facts).interactionNames;
|
|
@@ -182,5 +192,5 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
|
182
192
|
reason: `interaction ${interaction} has no committed data-flow fact`,
|
|
183
193
|
});
|
|
184
194
|
}
|
|
185
|
-
return missing;
|
|
195
|
+
return enrichFrontendPlanMissingFacts(missing);
|
|
186
196
|
}
|