cclaw-cli 6.14.3 → 6.14.4
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.
|
@@ -1339,11 +1339,19 @@ function pickEventTs(rows) {
|
|
|
1339
1339
|
return undefined;
|
|
1340
1340
|
}
|
|
1341
1341
|
/**
|
|
1342
|
-
* v6.14.2 — slices whose terminal
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1342
|
+
* v6.14.2 — slices whose terminal closure event recorded a `completedTs`
|
|
1343
|
+
* that PRECEDES the latest `leasedUntil` for the same slice. The lease
|
|
1344
|
+
* was never reclaimed but the wave closed in time; the missing audit
|
|
1345
|
+
* row is advisory bookkeeping, not a correctness failure.
|
|
1346
|
+
*
|
|
1347
|
+
* v6.14.4 — also recognize stream-mode (per-slice checkpoint) closure:
|
|
1348
|
+
* a `phase=green status=completed` row carrying `refactorOutcome`
|
|
1349
|
+
* (`inline` or `deferred`) IS the slice closure. Without this, every
|
|
1350
|
+
* stream-mode slice incorrectly fired `tdd_lease_expired_unreclaimed`
|
|
1351
|
+
* once its lease expired, even though the slice was already closed
|
|
1352
|
+
* (this mirrors the same predicate already used in
|
|
1353
|
+
* `src/internal/wave-status.ts` for `closedSlices` tracking — same
|
|
1354
|
+
* decision, just now applied to lease-closure detection).
|
|
1347
1355
|
*/
|
|
1348
1356
|
function computeClosedBeforeLeaseExpiry(events) {
|
|
1349
1357
|
const terminalPhases = new Set([
|
|
@@ -1353,6 +1361,15 @@ function computeClosedBeforeLeaseExpiry(events) {
|
|
|
1353
1361
|
]);
|
|
1354
1362
|
const lastLease = new Map();
|
|
1355
1363
|
const earliestTerminal = new Map();
|
|
1364
|
+
const recordTerminal = (sliceId, completedTs) => {
|
|
1365
|
+
const ts = Date.parse(completedTs);
|
|
1366
|
+
if (!Number.isFinite(ts))
|
|
1367
|
+
return;
|
|
1368
|
+
const prev = earliestTerminal.get(sliceId);
|
|
1369
|
+
if (prev === undefined || ts < prev) {
|
|
1370
|
+
earliestTerminal.set(sliceId, ts);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1356
1373
|
for (const ev of events) {
|
|
1357
1374
|
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
1358
1375
|
continue;
|
|
@@ -1367,16 +1384,21 @@ function computeClosedBeforeLeaseExpiry(events) {
|
|
|
1367
1384
|
}
|
|
1368
1385
|
}
|
|
1369
1386
|
}
|
|
1370
|
-
if (ev.status
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1387
|
+
if (ev.status !== "completed" || typeof ev.completedTs !== "string") {
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
if (typeof ev.phase !== "string")
|
|
1391
|
+
continue;
|
|
1392
|
+
if (terminalPhases.has(ev.phase)) {
|
|
1393
|
+
recordTerminal(ev.sliceId, ev.completedTs);
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
// v6.14.4 — stream-mode closure: GREEN-only with refactorOutcome
|
|
1397
|
+
// folded inline IS the slice's terminal row.
|
|
1398
|
+
if (ev.phase === "green" && ev.refactorOutcome) {
|
|
1399
|
+
const mode = ev.refactorOutcome.mode;
|
|
1400
|
+
if (mode === "inline" || mode === "deferred") {
|
|
1401
|
+
recordTerminal(ev.sliceId, ev.completedTs);
|
|
1380
1402
|
}
|
|
1381
1403
|
}
|
|
1382
1404
|
}
|
|
@@ -57,8 +57,48 @@ export declare function extractParallelExecutionManagedBody(planMarkdown: string
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function extractMembersListFromLine(trimmedLine: string): string | null;
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* v6.14.4 — extract a `(sliceId, unitId)` pair from a markdown table data
|
|
61
|
+
* row whose first column is an `S-NN` token. Used by the wave parser to
|
|
62
|
+
* recognize the table-format Parallel Execution Plan that hox-shape
|
|
63
|
+
* projects emit alongside (or instead of) the legacy `**Members:**`
|
|
64
|
+
* bullet line.
|
|
65
|
+
*
|
|
66
|
+
* Rules:
|
|
67
|
+
* - The line must start with `|` (after trimming).
|
|
68
|
+
* - Column 1 (after stripping markdown noise) must match `^S-(\d+)$` —
|
|
69
|
+
* header rows (`| sliceId | …`) and separator rows (`|---|---|…`) are
|
|
70
|
+
* silently skipped.
|
|
71
|
+
* - Column 2, when present and non-empty, becomes the `unitId`
|
|
72
|
+
* verbatim (after stripping whitespace + backticks/quotes/brackets).
|
|
73
|
+
* This preserves the hox convention of recording task ids
|
|
74
|
+
* (`T-010`, `T-008a`, …) in the `unit` column without forcing a
|
|
75
|
+
* `U-NN` derivation.
|
|
76
|
+
* - When column 2 is absent or empty, fall back to the legacy
|
|
77
|
+
* `S-NN → U-NN` derivation so the existing `**Members:**` parser path
|
|
78
|
+
* stays bit-identical for non-table plans.
|
|
79
|
+
*/
|
|
80
|
+
export declare function parseTableRowMember(trimmedLine: string): ParsedParallelWaveMember | null;
|
|
81
|
+
/**
|
|
82
|
+
* Parse `## Parallel Execution Plan` managed block for wave headings and
|
|
83
|
+
* member declarations. Recognizes BOTH the legacy `**Members:**` /
|
|
84
|
+
* `Members:` line shape AND the markdown-table shape
|
|
85
|
+
* (`| sliceId | unit | dependsOn | …`) used by hox-shape projects and by
|
|
86
|
+
* any plan written by `cclaw-cli sync` after v6.13.x.
|
|
87
|
+
*
|
|
88
|
+
* Wave headings accepted (case-insensitive, trailing text allowed):
|
|
89
|
+
* - `### Wave 04`
|
|
90
|
+
* - `### Wave W-04`
|
|
91
|
+
* - `### Wave W-04 — после успешного fan-in W-03 (5 lanes …)`
|
|
92
|
+
*
|
|
93
|
+
* Within a single wave the parser dedupes by `sliceId`: if the same
|
|
94
|
+
* slice appears in both `**Members:**` and a table row, the first
|
|
95
|
+
* occurrence wins (line-order). Cross-wave duplicates still throw
|
|
96
|
+
* `WavePlanDuplicateSliceError`.
|
|
97
|
+
*
|
|
98
|
+
* Malformed member tokens are skipped. Empty waves (heading present
|
|
99
|
+
* but neither a Members line nor any matching `| S-NN |` row found
|
|
100
|
+
* before the next heading) are RETURNED with `members: []` so callers
|
|
101
|
+
* can surface the boundary; classification is up to the caller.
|
|
62
102
|
*/
|
|
63
103
|
export declare function parseParallelExecutionPlanWaves(planMarkdown: string): ParsedParallelWave[];
|
|
64
104
|
/**
|
|
@@ -61,8 +61,73 @@ export function extractMembersListFromLine(trimmedLine) {
|
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* v6.14.4 — extract a `(sliceId, unitId)` pair from a markdown table data
|
|
65
|
+
* row whose first column is an `S-NN` token. Used by the wave parser to
|
|
66
|
+
* recognize the table-format Parallel Execution Plan that hox-shape
|
|
67
|
+
* projects emit alongside (or instead of) the legacy `**Members:**`
|
|
68
|
+
* bullet line.
|
|
69
|
+
*
|
|
70
|
+
* Rules:
|
|
71
|
+
* - The line must start with `|` (after trimming).
|
|
72
|
+
* - Column 1 (after stripping markdown noise) must match `^S-(\d+)$` —
|
|
73
|
+
* header rows (`| sliceId | …`) and separator rows (`|---|---|…`) are
|
|
74
|
+
* silently skipped.
|
|
75
|
+
* - Column 2, when present and non-empty, becomes the `unitId`
|
|
76
|
+
* verbatim (after stripping whitespace + backticks/quotes/brackets).
|
|
77
|
+
* This preserves the hox convention of recording task ids
|
|
78
|
+
* (`T-010`, `T-008a`, …) in the `unit` column without forcing a
|
|
79
|
+
* `U-NN` derivation.
|
|
80
|
+
* - When column 2 is absent or empty, fall back to the legacy
|
|
81
|
+
* `S-NN → U-NN` derivation so the existing `**Members:**` parser path
|
|
82
|
+
* stays bit-identical for non-table plans.
|
|
83
|
+
*/
|
|
84
|
+
export function parseTableRowMember(trimmedLine) {
|
|
85
|
+
if (!trimmedLine.startsWith("|"))
|
|
86
|
+
return null;
|
|
87
|
+
const inner = trimmedLine.replace(/^\|/u, "").replace(/\|\s*$/u, "");
|
|
88
|
+
if (inner.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
const cells = inner.split("|").map((cell) => cell.trim());
|
|
91
|
+
if (cells.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const stripDecorations = (raw) => raw.replace(/^[`"'[\]()]+|[`"'[\]()]+$/gu, "").trim();
|
|
94
|
+
const col1 = stripDecorations(cells[0]);
|
|
95
|
+
const sliceMatch = /^S-(\d+)$/u.exec(col1);
|
|
96
|
+
if (!sliceMatch)
|
|
97
|
+
return null;
|
|
98
|
+
const sliceNum = sliceMatch[1];
|
|
99
|
+
const sliceId = `S-${sliceNum}`;
|
|
100
|
+
let unitId = `U-${sliceNum}`;
|
|
101
|
+
if (cells.length >= 2) {
|
|
102
|
+
const col2 = stripDecorations(cells[1]);
|
|
103
|
+
if (col2.length > 0) {
|
|
104
|
+
const normalized = tokenToSliceAndUnit(col2);
|
|
105
|
+
unitId = normalized ? normalized.unitId : col2;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { sliceId, unitId };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse `## Parallel Execution Plan` managed block for wave headings and
|
|
112
|
+
* member declarations. Recognizes BOTH the legacy `**Members:**` /
|
|
113
|
+
* `Members:` line shape AND the markdown-table shape
|
|
114
|
+
* (`| sliceId | unit | dependsOn | …`) used by hox-shape projects and by
|
|
115
|
+
* any plan written by `cclaw-cli sync` after v6.13.x.
|
|
116
|
+
*
|
|
117
|
+
* Wave headings accepted (case-insensitive, trailing text allowed):
|
|
118
|
+
* - `### Wave 04`
|
|
119
|
+
* - `### Wave W-04`
|
|
120
|
+
* - `### Wave W-04 — после успешного fan-in W-03 (5 lanes …)`
|
|
121
|
+
*
|
|
122
|
+
* Within a single wave the parser dedupes by `sliceId`: if the same
|
|
123
|
+
* slice appears in both `**Members:**` and a table row, the first
|
|
124
|
+
* occurrence wins (line-order). Cross-wave duplicates still throw
|
|
125
|
+
* `WavePlanDuplicateSliceError`.
|
|
126
|
+
*
|
|
127
|
+
* Malformed member tokens are skipped. Empty waves (heading present
|
|
128
|
+
* but neither a Members line nor any matching `| S-NN |` row found
|
|
129
|
+
* before the next heading) are RETURNED with `members: []` so callers
|
|
130
|
+
* can surface the boundary; classification is up to the caller.
|
|
66
131
|
*/
|
|
67
132
|
export function parseParallelExecutionPlanWaves(planMarkdown) {
|
|
68
133
|
const body = extractParallelExecutionManagedBody(planMarkdown);
|
|
@@ -72,22 +137,60 @@ export function parseParallelExecutionPlanWaves(planMarkdown) {
|
|
|
72
137
|
const waves = [];
|
|
73
138
|
let current = null;
|
|
74
139
|
const seenSlices = new Set();
|
|
140
|
+
let inWaveSlicesSeen = new Set();
|
|
75
141
|
const flushCurrent = () => {
|
|
76
|
-
if (current
|
|
142
|
+
if (current) {
|
|
77
143
|
waves.push(current);
|
|
78
144
|
}
|
|
79
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* Strict add: throw on duplicates within the same wave OR across waves.
|
|
148
|
+
* Used for the `**Members:**` path so v6.13.1's duplicate-detection
|
|
149
|
+
* contract is preserved bit-identically.
|
|
150
|
+
*/
|
|
151
|
+
const addMemberStrict = (member) => {
|
|
152
|
+
if (!current)
|
|
153
|
+
return;
|
|
154
|
+
if (inWaveSlicesSeen.has(member.sliceId) ||
|
|
155
|
+
seenSlices.has(member.sliceId)) {
|
|
156
|
+
throw new WavePlanDuplicateSliceError(`duplicate slice ${member.sliceId} in Parallel Execution Plan managed block`);
|
|
157
|
+
}
|
|
158
|
+
seenSlices.add(member.sliceId);
|
|
159
|
+
inWaveSlicesSeen.add(member.sliceId);
|
|
160
|
+
current.members.push(member);
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Lenient add: silently dedupe duplicates within the same wave (so the
|
|
164
|
+
* documented "Members + table both present" case keeps the Members
|
|
165
|
+
* declaration as authoritative); still throw on cross-wave duplicates
|
|
166
|
+
* to surface real plan-authoring bugs.
|
|
167
|
+
*/
|
|
168
|
+
const addMemberDedupInWave = (member) => {
|
|
169
|
+
if (!current)
|
|
170
|
+
return;
|
|
171
|
+
if (inWaveSlicesSeen.has(member.sliceId))
|
|
172
|
+
return;
|
|
173
|
+
if (seenSlices.has(member.sliceId)) {
|
|
174
|
+
throw new WavePlanDuplicateSliceError(`duplicate slice ${member.sliceId} in Parallel Execution Plan managed block`);
|
|
175
|
+
}
|
|
176
|
+
seenSlices.add(member.sliceId);
|
|
177
|
+
inWaveSlicesSeen.add(member.sliceId);
|
|
178
|
+
current.members.push(member);
|
|
179
|
+
};
|
|
80
180
|
for (const rawLine of lines) {
|
|
81
181
|
const trimmed = rawLine.trim();
|
|
82
|
-
const waveMatch = /^###\s+Wave\s+(\d+)\
|
|
182
|
+
const waveMatch = /^###\s+Wave\s+(?:W-)?(\d+)\b/iu.exec(trimmed);
|
|
83
183
|
if (waveMatch) {
|
|
84
184
|
flushCurrent();
|
|
85
185
|
const n = waveMatch[1];
|
|
86
186
|
current = { waveId: `W-${n.padStart(2, "0")}`, members: [] };
|
|
187
|
+
inWaveSlicesSeen = new Set();
|
|
87
188
|
continue;
|
|
88
189
|
}
|
|
190
|
+
if (!current)
|
|
191
|
+
continue;
|
|
89
192
|
const membersCsv = extractMembersListFromLine(trimmed);
|
|
90
|
-
if (membersCsv !== null
|
|
193
|
+
if (membersCsv !== null) {
|
|
91
194
|
const parts = membersCsv
|
|
92
195
|
.split(/,/u)
|
|
93
196
|
.map((p) => p.trim())
|
|
@@ -96,12 +199,13 @@ export function parseParallelExecutionPlanWaves(planMarkdown) {
|
|
|
96
199
|
const ids = tokenToSliceAndUnit(part);
|
|
97
200
|
if (!ids)
|
|
98
201
|
continue;
|
|
99
|
-
|
|
100
|
-
throw new WavePlanDuplicateSliceError(`duplicate slice ${ids.sliceId} in Parallel Execution Plan managed block`);
|
|
101
|
-
}
|
|
102
|
-
seenSlices.add(ids.sliceId);
|
|
103
|
-
current.members.push(ids);
|
|
202
|
+
addMemberStrict(ids);
|
|
104
203
|
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const tableMember = parseTableRowMember(trimmed);
|
|
207
|
+
if (tableMember) {
|
|
208
|
+
addMemberDedupInWave(tableMember);
|
|
105
209
|
}
|
|
106
210
|
}
|
|
107
211
|
flushCurrent();
|