@tea-agent/loop-agent 0.42.0-next.3 → 0.42.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 +7 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +6 -2
- package/dist/worker/console/chat/pi-runtime.js +26 -0
- package/dist/worker/console/chat/routes.js +60 -5
- package/dist/worker/console/chat/sdd-data-alignment.js +219 -0
- package/dist/worker/console/chat/session-catalog.js +2 -0
- package/dist/worker/console/chat/session-store.js +26 -0
- package/dist/worker/console/chat/shortcuts.js +18 -2
- package/dist/worker/console/server.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CeznJ4Iw.js → abnfDiagram-N423BO3Z-BJpsGgAv.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4u9u7SX.js → arc-DjMFPsEd.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-oghpT81p.js → architectureDiagram-T3A2C74G-DqdZlr1O.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BPJ5Kvcj.js → blockDiagram-VBNYF7ZC-BmZnfFEO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CoIu69pd.js → c4Diagram-5PPSVZJV-3cKberpL.js} +1 -1
- package/dist/worker/console/static/assets/channel-DWYwciiv.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DtgiAsSc.js → chunk-2GRJ4B5K-q-0yjQ1h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-YSu_EVSD.js → chunk-2Q5K7J3B-DnONBaDW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DaRO-NB6.js → chunk-5RXB4S5H-v3a6PCOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Dh0ZKiGA.js → chunk-5VM5RSS4-D1kLrlU_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DOrvbR-H.js → chunk-6Q2QTUOP-BPelzihi.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3b22LN2.js → chunk-GF5L2VYU-DauNE69H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CXlqO8l6.js → chunk-JWPE2WC7-RIg5_bFJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BblTBrDu.js → chunk-KBJHAD2P-BuILuoxs.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DswqPALe.js → chunk-RYQCIY6F-VHORV5oW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CfTwVOlc.js → chunk-XXDRQBXY-BJYr4xHC.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-bLD8jVFo.js → cose-bilkent-JH36ORCC-BS6nuqQn.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BvoG1fh5.js → cynefin-VYW2F7L2-BBhF0NAT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DEPAdJu3.js → cynefinDiagram-MW4NZA55-D1135Ifg.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DmrNvBLy.js → dagre-VZM6K2ZE-vAOYPUWJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdwzIwvs.js → diagram-7IWD3JNH-DuaSPkfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CJ3hj-md.js → diagram-B4RE2ZJO-mMvXvGU6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C6YJT2a4.js → diagram-LBJQPF4R-CDIEJtlL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C_LPfjhX.js → diagram-Q27KOJAE-Bi_6JvCW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-xvsSb5Ie.js → diagram-UB23O5K3-DAqj57yN.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Rxp2tKcD.js → ebnfDiagram-BXEA7PRR-DfWaxNEo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DRyLYmZh.js → erDiagram-JOGREHBK-Bt7QqkNG.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-TLSU_D2B.js → flowDiagram-UKHOOZJN-CRRhQwT2.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGqeTvHC.js → ganttDiagram-PKOTCBZU-DAV1O5B6.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BZxyH2wh.js → gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js} +1 -1
- package/dist/worker/console/static/assets/index-CVdddQsA.js +451 -0
- package/dist/worker/console/static/assets/index-DDQc5a50.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BsYtzx01.js → infoDiagram-6WML65LV-C5W5eILC.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js → ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DYbB3ibo.js → journeyDiagram-NVQOT4AX-CKDRcaKf.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BHJHewZO.js → kanban-definition-27J2QSJJ-Cc7_TuSb.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CmPIPmCG.js → linear-BcQYYmeT.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-F--C1uV0.js → mermaid.core-CwLZPtQo.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-YTXfL1st.js → mindmap-definition-FAOFIHXS-D29M025z.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DdDMYXP9.js → pegDiagram-VL7TDLO6-CV-ZAWYe.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C3nH4KVd.js → pieDiagram-7S7Q4E2Y-C6iPS_o5.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js → quadrantDiagram-CIZ2JOQS-BFCD1sTg.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-KNzbl-7w.js → railroadDiagram-AXF67PYL-CztNj4O9.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CSwI6dG0.js → requirementDiagram-LRYGKXZP-BWIELSNd.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bl1AyuXy.js → sankeyDiagram-W5VNT64P-CPtxrjJQ.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-2y4EmXQa.js → sequenceDiagram-SI44F4Z6-BEWVSFsu.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-E8v8YrMg.js → sizeCapture-X5ZJPWSS-DyhzoX_J.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eGGlR856.js → stateDiagram-OKZ733FA-xLox0nu7.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-B7g7xc07.js → swimlanes-SLNWSIFB-CLC_ow9A.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Dvpxn-bf.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DR3VCHBG.js → timeline-definition-Z64GVDOM-DmJZ4uyY.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LUvapZIy.js → vennDiagram-T6HMQDX7-DD7GGi9Z.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C7GDcaXq.js → wardleyDiagram-T6FBY63Y-Bi8PQoGt.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CbNAqoGK.js → xychartDiagram-ELKLHX3M-uKdAY8Dp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +225 -21
- package/dist/worker/console/static-src/operator-chat/useComposer.js +5 -1
- package/dist/worker/console/workspace-context.js +4 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +10 -0
- package/dist/workflows/dag/init-hybrid.js +22 -15
- package/docs/templates/backend-test-dag.json +8 -5
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +0 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +0 -451
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +0 -8
|
@@ -6,6 +6,94 @@ import { confirmationToken } from "./format.js";
|
|
|
6
6
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
7
7
|
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
8
8
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
9
|
+
/** Pure list mutations keep optimistic UI deterministic and easy to verify. */
|
|
10
|
+
export function insertOptimisticSession(list, row) {
|
|
11
|
+
return [row, ...list.filter((item) => item.sessionId !== row.sessionId)];
|
|
12
|
+
}
|
|
13
|
+
export function replaceOptimisticSession(list, temporaryId, durable) {
|
|
14
|
+
return [
|
|
15
|
+
durable,
|
|
16
|
+
...list.filter((item) => item.sessionId !== durable.sessionId &&
|
|
17
|
+
(!temporaryId || item.sessionId !== temporaryId)),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
export function removeSessionById(list, sessionId) {
|
|
21
|
+
const index = list.findIndex((item) => item.sessionId === sessionId);
|
|
22
|
+
if (index < 0)
|
|
23
|
+
return { next: list, index: -1 };
|
|
24
|
+
return {
|
|
25
|
+
next: list.filter((item) => item.sessionId !== sessionId),
|
|
26
|
+
removed: list[index],
|
|
27
|
+
index,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function restoreSessionAt(list, row, index) {
|
|
31
|
+
if (list.some((item) => item.sessionId === row.sessionId))
|
|
32
|
+
return list;
|
|
33
|
+
const next = [...list];
|
|
34
|
+
next.splice(Math.max(0, Math.min(index, next.length)), 0, row);
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
function sameQueueSummary(a, b) {
|
|
38
|
+
const left = a.deferredTurns ?? (a.deferredTurn ? [a.deferredTurn] : []);
|
|
39
|
+
const right = b.deferredTurns ?? (b.deferredTurn ? [b.deferredTurn] : []);
|
|
40
|
+
if (left.length !== right.length)
|
|
41
|
+
return false;
|
|
42
|
+
return left.every((item, index) => {
|
|
43
|
+
const other = right[index];
|
|
44
|
+
return Boolean(other &&
|
|
45
|
+
item.clientRequestId === other.clientRequestId &&
|
|
46
|
+
item.revision === other.revision &&
|
|
47
|
+
item.state === other.state &&
|
|
48
|
+
item.updatedAt === other.updatedAt &&
|
|
49
|
+
item.textPreview === other.textPreview &&
|
|
50
|
+
item.imageCount === other.imageCount &&
|
|
51
|
+
item.acceptedTurnId === other.acceptedTurnId &&
|
|
52
|
+
item.lastError?.code === other.lastError?.code &&
|
|
53
|
+
item.lastError?.message === other.lastError?.message);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function sameSessionSummary(a, b) {
|
|
57
|
+
return (a.sessionId === b.sessionId &&
|
|
58
|
+
a.workspaceId === b.workspaceId &&
|
|
59
|
+
a.state === b.state &&
|
|
60
|
+
a.title === b.title &&
|
|
61
|
+
a.displayTitle === b.displayTitle &&
|
|
62
|
+
a.updatedAt === b.updatedAt &&
|
|
63
|
+
a.activeTurnId === b.activeTurnId &&
|
|
64
|
+
a.turnPhase === b.turnPhase &&
|
|
65
|
+
a.model?.provider === b.model?.provider &&
|
|
66
|
+
a.model?.modelId === b.model?.modelId &&
|
|
67
|
+
a.thinkingLevel === b.thinkingLevel &&
|
|
68
|
+
a.isBlank === b.isBlank &&
|
|
69
|
+
a.optimisticState === b.optimisticState &&
|
|
70
|
+
sameQueueSummary(a, b));
|
|
71
|
+
}
|
|
72
|
+
/** Reuse unchanged row and list references across catalog refreshes. */
|
|
73
|
+
export function stabilizeSessionList(previous, next) {
|
|
74
|
+
const previousById = new Map(previous.map((item) => [item.sessionId, item]));
|
|
75
|
+
let changed = previous.length !== next.length;
|
|
76
|
+
const stabilized = next.map((item, index) => {
|
|
77
|
+
const existing = previousById.get(item.sessionId);
|
|
78
|
+
const value = existing && sameSessionSummary(existing, item) ? existing : item;
|
|
79
|
+
if (value !== previous[index])
|
|
80
|
+
changed = true;
|
|
81
|
+
return value;
|
|
82
|
+
});
|
|
83
|
+
return changed ? stabilized : previous;
|
|
84
|
+
}
|
|
85
|
+
/** Select a server-confirmed blank session owned by the requested workspace. */
|
|
86
|
+
export function findReusableBlankSession(list, workspaceId) {
|
|
87
|
+
const scope = workspaceId.trim();
|
|
88
|
+
if (!scope)
|
|
89
|
+
return undefined;
|
|
90
|
+
return list.find((item) => item.workspaceId === scope &&
|
|
91
|
+
item.state === "active" &&
|
|
92
|
+
item.isBlank === true &&
|
|
93
|
+
item.turnPhase === "idle" &&
|
|
94
|
+
!item.activeTurnId &&
|
|
95
|
+
!item.optimisticState);
|
|
96
|
+
}
|
|
9
97
|
export async function createSessionAndPrimeTitleWatcher(input) {
|
|
10
98
|
if (input.creatingRef?.current) {
|
|
11
99
|
throw new Error("CREATE_SESSION_IN_FLIGHT");
|
|
@@ -23,7 +111,10 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
23
111
|
"x-loop-console-confirmation": confirmationToken(),
|
|
24
112
|
},
|
|
25
113
|
credentials: "include",
|
|
26
|
-
body: JSON.stringify(
|
|
114
|
+
body: JSON.stringify((() => {
|
|
115
|
+
const { optimisticSessionId: _optimisticSessionId, ...body } = input.options ?? {};
|
|
116
|
+
return body;
|
|
117
|
+
})()),
|
|
27
118
|
});
|
|
28
119
|
if (!res.ok) {
|
|
29
120
|
const body = (await res.json().catch(() => ({})));
|
|
@@ -35,7 +126,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
35
126
|
const body = (await res.json());
|
|
36
127
|
if (!body.session)
|
|
37
128
|
throw new Error("创建会话响应缺少 durable session");
|
|
38
|
-
const durable = body.session;
|
|
129
|
+
const durable = { ...body.session, isBlank: true };
|
|
39
130
|
const created = {
|
|
40
131
|
sessionId: durable.sessionId,
|
|
41
132
|
...(durable.workspaceId || input.options?.workspaceId
|
|
@@ -45,16 +136,12 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
45
136
|
: {}),
|
|
46
137
|
...runtimeSelectionFromRecord(durable),
|
|
47
138
|
};
|
|
48
|
-
const nextList =
|
|
49
|
-
durable,
|
|
50
|
-
...input.sessionListRef.current.filter((item) => item.sessionId !== durable.sessionId),
|
|
51
|
-
];
|
|
139
|
+
const nextList = replaceOptimisticSession(input.sessionListRef.current, input.options?.optimisticSessionId, durable);
|
|
52
140
|
// Commit ref → state → eligibility → deferred refresh. The ref assignment
|
|
53
141
|
// makes the durable record observable before React's state commit.
|
|
54
142
|
input.sessionListRef.current = nextList;
|
|
55
143
|
input.setSessionList(nextList);
|
|
56
144
|
input.titleWatcher?.markEligible(durable.sessionId);
|
|
57
|
-
void input.refreshSessionList();
|
|
58
145
|
return created;
|
|
59
146
|
}
|
|
60
147
|
finally {
|
|
@@ -90,6 +177,11 @@ export function useChatSessions(params) {
|
|
|
90
177
|
}, [refs]);
|
|
91
178
|
const [sessionList, setSessionList] = useState([]);
|
|
92
179
|
const sessionListRef = useRef([]);
|
|
180
|
+
const pendingDeletedIdsRef = useRef(new Set());
|
|
181
|
+
const pendingCreatedRowsRef = useRef(new Map());
|
|
182
|
+
const sessionMutationVersionRef = useRef(0);
|
|
183
|
+
const deleteRequestVersionsRef = useRef(new Map());
|
|
184
|
+
const activateSessionRef = useRef(null);
|
|
93
185
|
sessionListRef.current = sessionList;
|
|
94
186
|
const titleWatcherRef = useRef(null);
|
|
95
187
|
// Shared busy gate for sidebar + Landing create entries (one render feedback).
|
|
@@ -159,9 +251,13 @@ export function useChatSessions(params) {
|
|
|
159
251
|
return;
|
|
160
252
|
}
|
|
161
253
|
const sessions = body.sessions ?? [];
|
|
254
|
+
const durable = sessions.filter((item) => !pendingDeletedIdsRef.current.has(item.sessionId));
|
|
255
|
+
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => !durable.some((session) => session.sessionId === item.sessionId));
|
|
162
256
|
// Preserve every workspace's summaries. ChatWorkspaceSidebar applies the
|
|
163
257
|
// visible recent-item limit independently inside each workspace group.
|
|
164
|
-
|
|
258
|
+
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
259
|
+
sessionListRef.current = next;
|
|
260
|
+
setSessionList(next);
|
|
165
261
|
if (body.workspaceErrors?.length) {
|
|
166
262
|
setError(`部分工作区的会话加载失败(${body.workspaceErrors.length} 个)`);
|
|
167
263
|
}
|
|
@@ -171,6 +267,60 @@ export function useChatSessions(params) {
|
|
|
171
267
|
}
|
|
172
268
|
}, [origin, blocked, setError]);
|
|
173
269
|
const deleteSession = useCallback(async (sessionId) => {
|
|
270
|
+
if (pendingDeletedIdsRef.current.has(sessionId))
|
|
271
|
+
return false;
|
|
272
|
+
const requestId = ++sessionMutationVersionRef.current;
|
|
273
|
+
const currentList = sessionListRef.current;
|
|
274
|
+
const { next: removedList, removed, index: removedIndex } = removeSessionById(currentList, sessionId);
|
|
275
|
+
if (!removed || removedIndex < 0)
|
|
276
|
+
return false;
|
|
277
|
+
pendingDeletedIdsRef.current.add(sessionId);
|
|
278
|
+
deleteRequestVersionsRef.current.set(sessionId, requestId);
|
|
279
|
+
const activeSession = refs.sessionRef.current?.sessionId === sessionId
|
|
280
|
+
? refs.sessionRef.current
|
|
281
|
+
: null;
|
|
282
|
+
const activeWorkspaceId = activeSession?.workspaceId ?? workspaceId;
|
|
283
|
+
const deletingRow = {
|
|
284
|
+
...removed,
|
|
285
|
+
optimisticState: "deleting",
|
|
286
|
+
};
|
|
287
|
+
const deletingList = restoreSessionAt(removedList, deletingRow, removedIndex);
|
|
288
|
+
sessionListRef.current = deletingList;
|
|
289
|
+
setSessionList(deletingList);
|
|
290
|
+
window.setTimeout(() => {
|
|
291
|
+
if (!pendingDeletedIdsRef.current.has(sessionId) ||
|
|
292
|
+
deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
293
|
+
return;
|
|
294
|
+
const next = sessionListRef.current.filter((item) => item.sessionId !== sessionId);
|
|
295
|
+
sessionListRef.current = next;
|
|
296
|
+
setSessionList(next);
|
|
297
|
+
}, 150);
|
|
298
|
+
const restore = () => {
|
|
299
|
+
if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
300
|
+
return;
|
|
301
|
+
pendingDeletedIdsRef.current.delete(sessionId);
|
|
302
|
+
deleteRequestVersionsRef.current.delete(sessionId);
|
|
303
|
+
const restored = restoreSessionAt(sessionListRef.current, removed, removedIndex);
|
|
304
|
+
if (restored === sessionListRef.current)
|
|
305
|
+
return;
|
|
306
|
+
sessionListRef.current = restored;
|
|
307
|
+
setSessionList(restored);
|
|
308
|
+
};
|
|
309
|
+
let deletionSelectionVersion = null;
|
|
310
|
+
if (activeSession) {
|
|
311
|
+
deletionSelectionVersion = ++sessionSelectionVersionRef.current;
|
|
312
|
+
refs.sessionRef.current = null;
|
|
313
|
+
resetActiveProjection();
|
|
314
|
+
setSession(null);
|
|
315
|
+
forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
|
|
316
|
+
}
|
|
317
|
+
const restoreActiveSession = async () => {
|
|
318
|
+
if (activeSession &&
|
|
319
|
+
deletionSelectionVersion === sessionSelectionVersionRef.current &&
|
|
320
|
+
!refs.sessionRef.current) {
|
|
321
|
+
await activateSessionRef.current?.(activeSession.sessionId, activeSession.workspaceId);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
174
324
|
try {
|
|
175
325
|
// AC-002: delete follows the session's durable owner, not the
|
|
176
326
|
// global active workspace.
|
|
@@ -184,22 +334,21 @@ export function useChatSessions(params) {
|
|
|
184
334
|
},
|
|
185
335
|
});
|
|
186
336
|
if (!res.ok) {
|
|
337
|
+
restore();
|
|
338
|
+
await restoreActiveSession();
|
|
187
339
|
setError("删除会话失败");
|
|
188
340
|
return false;
|
|
189
341
|
}
|
|
190
342
|
titleWatcherRef.current?.cancel(sessionId);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
resetActiveProjection();
|
|
196
|
-
setSession(null);
|
|
197
|
-
forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
|
|
198
|
-
}
|
|
199
|
-
await refreshSessionList();
|
|
343
|
+
void refreshSessionList().finally(() => {
|
|
344
|
+
pendingDeletedIdsRef.current.delete(sessionId);
|
|
345
|
+
deleteRequestVersionsRef.current.delete(sessionId);
|
|
346
|
+
});
|
|
200
347
|
return true;
|
|
201
348
|
}
|
|
202
349
|
catch (error) {
|
|
350
|
+
restore();
|
|
351
|
+
await restoreActiveSession();
|
|
203
352
|
setError(error instanceof Error ? error.message : String(error));
|
|
204
353
|
return false;
|
|
205
354
|
}
|
|
@@ -225,7 +374,7 @@ export function useChatSessions(params) {
|
|
|
225
374
|
if (!res.ok || !body.session)
|
|
226
375
|
throw new Error(body.error?.message ?? "会话不可用");
|
|
227
376
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
228
|
-
return;
|
|
377
|
+
return false;
|
|
229
378
|
const next = {
|
|
230
379
|
sessionId: body.session.sessionId,
|
|
231
380
|
...(body.session.workspaceId ? { workspaceId: body.session.workspaceId } : {}),
|
|
@@ -276,11 +425,13 @@ export function useChatSessions(params) {
|
|
|
276
425
|
});
|
|
277
426
|
rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
|
|
278
427
|
setActivePreviewPath(null);
|
|
428
|
+
return true;
|
|
279
429
|
}
|
|
280
430
|
catch (error) {
|
|
281
431
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
282
|
-
return;
|
|
432
|
+
return false;
|
|
283
433
|
setError(error instanceof Error ? error.message : String(error));
|
|
434
|
+
return false;
|
|
284
435
|
}
|
|
285
436
|
}, [
|
|
286
437
|
origin,
|
|
@@ -296,6 +447,7 @@ export function useChatSessions(params) {
|
|
|
296
447
|
sessionOwnerQuery,
|
|
297
448
|
workspaceId,
|
|
298
449
|
]);
|
|
450
|
+
activateSessionRef.current = activateSession;
|
|
299
451
|
useEffect(() => {
|
|
300
452
|
void refreshSessionList();
|
|
301
453
|
}, [refreshSessionList]);
|
|
@@ -437,15 +589,38 @@ export function useChatSessions(params) {
|
|
|
437
589
|
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
438
590
|
return Promise.reject(new Error("WORKSPACE_REQUIRED"));
|
|
439
591
|
}
|
|
592
|
+
const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
593
|
+
const now = new Date().toISOString();
|
|
594
|
+
const optimisticRow = {
|
|
595
|
+
sessionId: optimisticSessionId,
|
|
596
|
+
workspaceId: scope,
|
|
597
|
+
state: "active",
|
|
598
|
+
updatedAt: now,
|
|
599
|
+
turnPhase: "idle",
|
|
600
|
+
optimisticState: "creating",
|
|
601
|
+
};
|
|
602
|
+
pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
|
|
603
|
+
const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
|
|
604
|
+
sessionListRef.current = optimisticList;
|
|
605
|
+
setSessionList(optimisticList);
|
|
440
606
|
return createSessionAndPrimeTitleWatcher({
|
|
441
607
|
origin,
|
|
442
608
|
sessionListRef,
|
|
443
609
|
setSessionList,
|
|
444
610
|
titleWatcher: titleWatcherRef.current,
|
|
445
611
|
refreshSessionList,
|
|
446
|
-
options: { ...options, workspaceId: scope },
|
|
612
|
+
options: { ...options, workspaceId: scope, optimisticSessionId },
|
|
447
613
|
setCreatingSession,
|
|
448
614
|
creatingRef: creatingSessionRef,
|
|
615
|
+
}).then((created) => {
|
|
616
|
+
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
617
|
+
return created;
|
|
618
|
+
}).catch((error) => {
|
|
619
|
+
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
620
|
+
const cleaned = sessionListRef.current.filter((item) => item.sessionId !== optimisticSessionId);
|
|
621
|
+
sessionListRef.current = cleaned;
|
|
622
|
+
setSessionList(cleaned);
|
|
623
|
+
throw error;
|
|
449
624
|
});
|
|
450
625
|
}, [origin, refreshSessionList, workspaceId, setError]);
|
|
451
626
|
const createSession = useCallback(async (options) => {
|
|
@@ -456,11 +631,36 @@ export function useChatSessions(params) {
|
|
|
456
631
|
return null; // AC-R5-002: unknown workspace — no POST
|
|
457
632
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
458
633
|
setError(null);
|
|
634
|
+
const previousSession = refs.sessionRef.current;
|
|
635
|
+
const scope = options?.workspaceId?.trim() ?? workspaceId?.trim();
|
|
636
|
+
if (!scope) {
|
|
637
|
+
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
// A caller-provided snapshot is an explicit new-session boundary; reusing
|
|
641
|
+
// an existing shell would silently drop that context suffix.
|
|
642
|
+
const reusable = options?.snapshot
|
|
643
|
+
? undefined
|
|
644
|
+
: findReusableBlankSession(sessionListRef.current, scope);
|
|
645
|
+
if (reusable) {
|
|
646
|
+
if (previousSession?.sessionId === reusable.sessionId)
|
|
647
|
+
return previousSession;
|
|
648
|
+
const activated = await activateSession(reusable.sessionId, reusable.workspaceId);
|
|
649
|
+
if (!activated)
|
|
650
|
+
return null;
|
|
651
|
+
return {
|
|
652
|
+
sessionId: reusable.sessionId,
|
|
653
|
+
...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
|
|
654
|
+
...runtimeSelectionFromRecord(reusable),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
459
657
|
try {
|
|
658
|
+
resetActiveProjection({ keepStreams: true });
|
|
659
|
+
refs.sessionRef.current = null;
|
|
660
|
+
setSession(null);
|
|
460
661
|
const created = await createSessionAndPrimeTitleWatcherForHook(options);
|
|
461
662
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
462
663
|
return null;
|
|
463
|
-
resetActiveProjection({ keepStreams: true });
|
|
464
664
|
// Set ref synchronously BEFORE setState so a chained
|
|
465
665
|
// createSession().then(sendPrompt) reads the new session even
|
|
466
666
|
// though sendPrompt's closure captured the stale (null) value.
|
|
@@ -472,6 +672,9 @@ export function useChatSessions(params) {
|
|
|
472
672
|
catch (e) {
|
|
473
673
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
474
674
|
return null;
|
|
675
|
+
if (previousSession) {
|
|
676
|
+
await activateSession(previousSession.sessionId, previousSession.workspaceId);
|
|
677
|
+
}
|
|
475
678
|
const message = e instanceof Error ? e.message : String(e);
|
|
476
679
|
if (message !== "CREATE_SESSION_IN_FLIGHT") {
|
|
477
680
|
setError(message);
|
|
@@ -484,6 +687,7 @@ export function useChatSessions(params) {
|
|
|
484
687
|
setError,
|
|
485
688
|
resetActiveProjection,
|
|
486
689
|
createSessionAndPrimeTitleWatcherForHook,
|
|
690
|
+
activateSession,
|
|
487
691
|
blocked,
|
|
488
692
|
workspaceId,
|
|
489
693
|
]);
|
|
@@ -28,6 +28,7 @@ export function useComposer(params) {
|
|
|
28
28
|
}, []);
|
|
29
29
|
const [shortcutIndex, setShortcutIndex] = useState(0);
|
|
30
30
|
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
31
|
+
const [commandCapabilities, setCommandCapabilities] = useState({ sddBingo: false });
|
|
31
32
|
const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
|
|
32
33
|
const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
|
|
33
34
|
const [commandsEpoch, setCommandsEpoch] = useState(0);
|
|
@@ -74,6 +75,7 @@ export function useComposer(params) {
|
|
|
74
75
|
commandsSessionRef.current = sessionId;
|
|
75
76
|
commandsGenerationRef.current += 1;
|
|
76
77
|
setDynamicCommands([]);
|
|
78
|
+
setCommandCapabilities({ sddBingo: false });
|
|
77
79
|
setDynamicCommandsError(null);
|
|
78
80
|
setDynamicCommandsLoading(false);
|
|
79
81
|
}, [sessionId]);
|
|
@@ -122,6 +124,7 @@ export function useComposer(params) {
|
|
|
122
124
|
if (!isCurrent())
|
|
123
125
|
return;
|
|
124
126
|
setDynamicCommands(items);
|
|
127
|
+
setCommandCapabilities({ sddBingo: body?.capabilities?.sddBingo === true });
|
|
125
128
|
// Compact server fail-soft / partial-source warning; local Operator/Web still merge.
|
|
126
129
|
const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
|
|
127
130
|
setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
|
|
@@ -131,6 +134,7 @@ export function useComposer(params) {
|
|
|
131
134
|
return;
|
|
132
135
|
setDynamicCommandsError("动态命令加载失败");
|
|
133
136
|
setDynamicCommands([]);
|
|
137
|
+
setCommandCapabilities({ sddBingo: false });
|
|
134
138
|
}
|
|
135
139
|
finally {
|
|
136
140
|
if (isCurrent()) {
|
|
@@ -227,7 +231,7 @@ export function useComposer(params) {
|
|
|
227
231
|
}, 400);
|
|
228
232
|
return () => window.clearTimeout(timer);
|
|
229
233
|
}, [origin, sessionId, input]);
|
|
230
|
-
const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands), [dynamicCommands]);
|
|
234
|
+
const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands, commandCapabilities), [dynamicCommands, commandCapabilities]);
|
|
231
235
|
const filteredShortcuts = useMemo(() => input.startsWith("/")
|
|
232
236
|
? filterSlashCommands(mergedCommands, input)
|
|
233
237
|
: [], [input, mergedCommands]);
|
|
@@ -32,6 +32,7 @@ import { ChatSessionStore } from "./chat/session-store.js";
|
|
|
32
32
|
import { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
|
|
33
33
|
import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.js";
|
|
34
34
|
import { ConsolePiRuntime } from "./chat/pi-runtime.js";
|
|
35
|
+
import { AimaxAuthReader, SddDataAlignmentService } from "./chat/sdd-data-alignment.js";
|
|
35
36
|
import { UserQuestionRegistry } from "./chat/user-questions.js";
|
|
36
37
|
import { TodoListRegistry } from "./chat/todos.js";
|
|
37
38
|
import { GoalRegistry } from "./chat/goals.js";
|
|
@@ -253,6 +254,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
253
254
|
}
|
|
254
255
|
},
|
|
255
256
|
});
|
|
257
|
+
const sddDataAlignment = new SddDataAlignmentService(chatStore, new AimaxAuthReader(), deps.fetchImpl);
|
|
256
258
|
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
257
259
|
const chatEvents = createChatEventStore({
|
|
258
260
|
appData,
|
|
@@ -291,6 +293,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
291
293
|
questionRegistry,
|
|
292
294
|
todoRegistry,
|
|
293
295
|
goalRegistry,
|
|
296
|
+
sddDataAlignment,
|
|
294
297
|
});
|
|
295
298
|
let chatRouteDeps;
|
|
296
299
|
const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
|
|
@@ -302,6 +305,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
302
305
|
questionRegistry,
|
|
303
306
|
todoRegistry,
|
|
304
307
|
goalRegistry,
|
|
308
|
+
sddDataAlignment,
|
|
305
309
|
goalRoundDriver,
|
|
306
310
|
operationLinker: chatOperationLinker,
|
|
307
311
|
appData,
|
|
@@ -904,6 +904,16 @@ export async function writeBackendTestWriterProgressArtifacts(input) {
|
|
|
904
904
|
await writeFile(reportPath, body, "utf8");
|
|
905
905
|
return { factsPath, reportPath };
|
|
906
906
|
}
|
|
907
|
+
export function isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate(task) {
|
|
908
|
+
return (task.id === "repair-backend-pytest-collection-pi" &&
|
|
909
|
+
task.executor === "pi" &&
|
|
910
|
+
task.role === "implementer" &&
|
|
911
|
+
task.toolProfile === "write" &&
|
|
912
|
+
task.writePolicy === "exclusive" &&
|
|
913
|
+
(task.writeSet?.length ?? 0) > 0 &&
|
|
914
|
+
task.writerOutcomePolicy?.type === "implementation-outcome-v1" &&
|
|
915
|
+
task.writerOutcomePolicy.requireChangedFiles === true);
|
|
916
|
+
}
|
|
907
917
|
export function isBackendTestCompletenessRetryCandidate(task) {
|
|
908
918
|
if (task.executor !== "pi" || task.role !== "implementer")
|
|
909
919
|
return false;
|
|
@@ -4256,21 +4256,26 @@ const headerIndex=lines.findIndex(line=>{const row=cells(line);return expectedHe
|
|
|
4256
4256
|
if(headerIndex<0){process.stderr.write('invalid-module-index-header: require exact business ownership and path columns\\n');process.exit(2);}
|
|
4257
4257
|
const dataRows=lines.slice(headerIndex+2).map(cells).filter(row=>row.length>=8&&row[0]&&row[0]!=='Module Stem');
|
|
4258
4258
|
const allowedSplit=new Set(['explicit-user-layout','primary-business-resource','independent-business-resource','output-budget']);
|
|
4259
|
-
const
|
|
4260
|
-
const operationOwners=new Map();const declaredModules=[];let planRepairApplied=false;
|
|
4259
|
+
const operationOwners=new Map();const canonicalSeen=new Set();const declaredModules=[];let planRepairApplied=false;
|
|
4261
4260
|
for(const row of dataRows){
|
|
4262
|
-
const
|
|
4263
|
-
|
|
4261
|
+
const rawStem=String(row[0]||'').trim(),resource=String(row[1]||'').trim(),operations=String(row[2]||'').split(';').map(value=>value.trim()).filter(Boolean),split=String(row[5]||'').trim();
|
|
4262
|
+
const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];
|
|
4263
|
+
if(mdMatches.length!==1){process.stderr.write('module-markdown-path-mismatch: '+(rawStem||'unknown')+'; require exactly one canonical Markdown Path\\n');process.exit(2);}
|
|
4264
|
+
let stem=norm(mdMatches[0][1]),markdownPath=mdMatches[0][0];
|
|
4265
|
+
if(strictLayout&&!strictLayout.modules.some(item=>item.markdownPath===markdownPath)){planRepairApplied=true;continue;}
|
|
4264
4266
|
if(!resource){process.stderr.write('missing-business-resource: '+stem+'\\n');process.exit(2);}
|
|
4265
4267
|
if(/^(?:response|resp|regression|positive|negative|boundary|error|combo|filter)(?:[_ -]|$)/i.test(resource)){process.stderr.write('test-purpose-business-resource: '+stem+' -> '+resource+'\\n');process.exit(2);}
|
|
4266
4268
|
if(!allowedSplit.has(split)){process.stderr.write('invalid-module-split-reason: '+stem+' -> '+split+'\\n');process.exit(2);}
|
|
4267
4269
|
if(operations.length===0){process.stderr.write('missing-owned-operation: '+stem+'\\n');process.exit(2);}
|
|
4268
|
-
const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];let markdownPath=mdMatches[0]&&mdMatches[0][0];
|
|
4269
4270
|
let pytestPath=String(row[7]||'').replace(/^\\.\\//,'').replace(/\\\\/g,'/');
|
|
4270
|
-
if(mdMatches.length!==1||norm(mdMatches[0][1])!==stem){process.stderr.write('module-markdown-path-mismatch: '+stem+'\\n');process.exit(2);}
|
|
4271
4271
|
if(!/^[A-Za-z0-9_./-]+\\.py$/.test(pytestPath)||pytestPath.includes('..')){process.stderr.write('invalid-module-pytest-path: '+stem+' -> '+pytestPath+'\\n');process.exit(2);}
|
|
4272
|
-
const
|
|
4272
|
+
const requiredCandidates=strictLayout?strictLayout.modules.filter(item=>item.markdownPath===markdownPath):[];
|
|
4273
|
+
if(strictLayout&&requiredCandidates.length!==1){process.stderr.write('MODULE_LAYOUT_CONFLICT: no unique required module for Markdown Path '+markdownPath+'\\n');process.exit(2);}
|
|
4274
|
+
const required=requiredCandidates[0];
|
|
4275
|
+
if(!required&&norm(rawStem)!==stem)planRepairApplied=true;
|
|
4273
4276
|
if(required){
|
|
4277
|
+
stem=required.stem;
|
|
4278
|
+
if(norm(rawStem)!==stem)planRepairApplied=true;
|
|
4274
4279
|
if(!required.markdownPath.startsWith(mdDir+'/')||!required.pytestPath.startsWith(scriptDir+'/')||required.markdownPath.includes('..')||required.pytestPath.includes('..')){process.stderr.write('MODULE_LAYOUT_CONFLICT: strict paths outside frozen roots for '+stem+'\\n');process.exit(2);}
|
|
4275
4280
|
if(markdownPath!==required.markdownPath||pytestPath!==required.pytestPath||resource!==required.businessResource||JSON.stringify(operations)!==JSON.stringify(required.ownedOperations)||split!==required.splitReason){planRepairApplied=true;}
|
|
4276
4281
|
markdownPath=required.markdownPath;pytestPath=required.pytestPath;
|
|
@@ -4280,17 +4285,19 @@ for(const row of dataRows){
|
|
|
4280
4285
|
}
|
|
4281
4286
|
}
|
|
4282
4287
|
const item={stem,markdownPath,pytestPath,businessResource:required?required.businessResource:resource,ownedOperations:required?required.ownedOperations:operations,ownedRuleKeys:String(row[3]||'').split(';').map(value=>value.trim()).filter(Boolean),caseIds:String(row[4]||'').split(';').map(value=>value.trim()).filter(Boolean),splitReason:required?required.splitReason:split,...(required&&required.budgetProof?{budgetProof:required.budgetProof}:{})};
|
|
4288
|
+
if(canonicalSeen.has(item.stem)){process.stderr.write('duplicate-canonical-module-stem: '+item.stem+'; module identity must be unique\\n');process.exit(2);}
|
|
4289
|
+
canonicalSeen.add(item.stem);
|
|
4283
4290
|
declaredModules.push(item);
|
|
4284
4291
|
for(const operation of item.ownedOperations){const owners=operationOwners.get(operation)||[];owners.push({stem,split:item.splitReason});operationOwners.set(operation,owners);}
|
|
4285
4292
|
}
|
|
4286
4293
|
if(strictLayout){
|
|
4287
4294
|
const missing=strictLayout.modules.filter(item=>!declaredModules.some(actual=>actual.stem===item.stem));
|
|
4288
4295
|
if(missing.length){process.stderr.write('MODULE_LAYOUT_CONFLICT: plan missing required modules '+missing.map(item=>item.stem).join(',')+'; deterministic Plan-only repair cannot invent Rule/Case ownership\\n');process.exit(2);}
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4296
|
+
}
|
|
4297
|
+
if(planRepairApplied){
|
|
4298
|
+
const table=['| '+expectedHeader.join(' | ')+' |','|'+expectedHeader.map(()=>'---').join('|')+'|',...declaredModules.map(item=>'| '+[item.stem,item.businessResource,item.ownedOperations.join('; '),item.ownedRuleKeys.join('; '),item.caseIds.join('; '),item.splitReason,'['+item.stem+'](./'+item.stem+'.md) '+item.markdownPath,item.pytestPath].join(' | ')+' |')].join('\\n');
|
|
4299
|
+
readme=[...allLines.slice(0,headings[0]+1),table,...allLines.slice(end)].join('\\n');
|
|
4300
|
+
fs.writeFileSync(planPath,readme,'utf8');
|
|
4294
4301
|
}
|
|
4295
4302
|
for(const [operation,owners] of operationOwners){if(owners.length>1&&!owners.every(owner=>owner.split==='explicit-user-layout'||owner.split==='output-budget')){process.stderr.write('overlapping-operation-modules: '+operation+' -> '+owners.map(owner=>owner.stem).join(',')+'; merge by business resource or use an authoritative explicit-user-layout\\n');process.exit(2);}}
|
|
4296
4303
|
if(!strictLayout){
|
|
@@ -4302,8 +4309,8 @@ if(!strictLayout){
|
|
|
4302
4309
|
}
|
|
4303
4310
|
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
4304
4311
|
if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
|
|
4305
|
-
const
|
|
4306
|
-
for(const item of declaredModules){if(valid(item.stem)
|
|
4312
|
+
const modules=[];
|
|
4313
|
+
for(const item of declaredModules){if(valid(item.stem))modules.push(item);}
|
|
4307
4314
|
if(modules.length===0){process.stderr.write('empty-module-index: require at least one stable business module\\n');process.exit(2);}
|
|
4308
4315
|
if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
|
|
4309
4316
|
const planReadPath=path.relative(process.cwd(),planPath).split(path.sep).join('/');
|
|
@@ -4899,7 +4906,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4899
4906
|
"Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
|
|
4900
4907
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
4901
4908
|
"For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
|
|
4902
|
-
"Mandatory module layout contract: first inspect the PRIMARY requirement for an explicit list of required Markdown/Python output path pairs. When explicit paths are present, they are authoritative `explicit-user-layout`: reproduce their exact filenames, count and one-to-one pairs in Module Index; do not rename, merge, split, omit or add a module from reference/example scripts. Only when the primary requirement has no explicit file layout may you derive the smallest `business-resource-layout`. Existing examples, historical regression functions and shared setup may add evidence/assertions to an existing required module, but never create an extra physical module by themselves. A reference-only `resp_regression`, positive/negative/boundary/error/response module is forbidden.",
|
|
4909
|
+
"Mandatory module layout contract: first inspect the PRIMARY requirement for an explicit list of required Markdown/Python output path pairs. When explicit paths are present, they are authoritative `explicit-user-layout`: reproduce their exact filenames, count and one-to-one pairs in Module Index; do not rename, merge, split, omit or add a module from reference/example scripts. Only when the primary requirement has no explicit file layout may you derive the smallest `business-resource-layout`. Existing examples, historical regression functions and shared setup may add evidence/assertions to an existing required module, but never create an extra physical module by themselves. A reference-only `resp_regression`, positive/negative/boundary/error/response module is forbidden. The Module Stem cell must contain only the plain filename stem; never put Markdown link syntax or a path in that cell.",
|
|
4903
4910
|
...(taskConfig.backendTest?.moduleLayout
|
|
4904
4911
|
? [
|
|
4905
4912
|
"A strict task-contract `backendTest.moduleLayout` is bound and is authoritative over model-derived layout. Reproduce every stem, businessResource, ownedOperations, splitReason, markdownPath and pytestPath exactly; do not add, omit, rename or reorder physical modules. The downstream preflight compares exact path sets and may perform at most one deterministic Plan-only pruning/path normalization; it cannot invent missing Rule/Case ownership.",
|