@tekyzinc/gsd-t 4.9.14 → 4.10.10
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 +15 -0
- package/README.md +1 -1
- package/bin/gsd-t-competition-judge.cjs +7 -1
- package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
- package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
- package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
- package/bin/gsd-t-file-disjointness.cjs +319 -7
- package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
- package/bin/gsd-t-graph-edge-extract.cjs +612 -0
- package/bin/gsd-t-graph-freshness.cjs +506 -0
- package/bin/gsd-t-graph-index.cjs +540 -0
- package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
- package/bin/gsd-t-graph-query-cli.cjs +1182 -0
- package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
- package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
- package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
- package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
- package/bin/gsd-t-scip-reader.cjs +167 -0
- package/bin/gsd-t.js +166 -48
- package/commands/gsd-t-debug.md +10 -0
- package/commands/gsd-t-design-build.md +10 -0
- package/commands/gsd-t-execute.md +15 -0
- package/commands/gsd-t-feature.md +15 -7
- package/commands/gsd-t-gap-analysis.md +17 -7
- package/commands/gsd-t-impact.md +25 -0
- package/commands/gsd-t-integrate.md +25 -0
- package/commands/gsd-t-partition.md +18 -0
- package/commands/gsd-t-plan.md +16 -0
- package/commands/gsd-t-populate.md +16 -5
- package/commands/gsd-t-prd.md +18 -0
- package/commands/gsd-t-project.md +19 -0
- package/commands/gsd-t-promote-debt.md +16 -5
- package/commands/gsd-t-qa.md +20 -8
- package/commands/gsd-t-quick.md +10 -0
- package/commands/gsd-t-scan.md +21 -3
- package/commands/gsd-t-test-sync.md +10 -0
- package/commands/gsd-t-verify.md +25 -0
- package/commands/gsd-t-wave.md +8 -0
- package/package.json +10 -2
- package/templates/workflows/gsd-t-debug.workflow.js +81 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
- package/templates/workflows/gsd-t-phase.workflow.js +84 -0
- package/templates/workflows/gsd-t-quick.workflow.js +64 -0
- package/templates/workflows/gsd-t-scan.workflow.js +200 -9
- package/templates/workflows/gsd-t-verify.workflow.js +50 -1
- package/bin/graph-cgc.js +0 -510
- package/bin/graph-indexer.js +0 -147
- package/bin/graph-overlay.js +0 -195
- package/bin/graph-parsers.js +0 -327
- package/bin/graph-query.js +0 -453
- package/bin/graph-store.js +0 -154
|
@@ -19,6 +19,29 @@
|
|
|
19
19
|
* - D5 checks WRITE targets only; reads never conflict
|
|
20
20
|
* - Mode-agnostic
|
|
21
21
|
* - Git-history heuristic bounded to 100 commits
|
|
22
|
+
*
|
|
23
|
+
* M94-D11 additions (graph-aware disjointness — SAFETY-CRITICAL):
|
|
24
|
+
* The graph-aware layer consults `bin/gsd-t-graph-query-cli.cjs` (blast-radius /
|
|
25
|
+
* who-imports) to detect TRANSITIVE dependency overlap between two domain
|
|
26
|
+
* touch-lists. Two domains whose touched files share a transitive dependency (one
|
|
27
|
+
* imports a module the other also imports) are NOT disjoint even if their declared
|
|
28
|
+
* Touches lists have no literal overlap.
|
|
29
|
+
*
|
|
30
|
+
* FAIL-LOUD invariant: on `graph-unavailable`, the graph-aware check either HALTS
|
|
31
|
+
* (returns {ok:false, reason:"GRAPH_UNAVAILABLE"}) or falls back to the literal-
|
|
32
|
+
* Touches-overlap check ONLY when `opts.disjointnessFallback === "touches-only"` is
|
|
33
|
+
* explicitly set by the operator (the bootstrap escape hatch). A grep-reconstructed
|
|
34
|
+
* disjointness guess is NEVER taken. [RULE] execute-disjointness-fail-loud-halts-never-grep-guess
|
|
35
|
+
*
|
|
36
|
+
* Bootstrap escape hatch: a fresh repo with no graph yet, or a parser regression,
|
|
37
|
+
* must NOT permanently brick parallel execution. The operator may pass
|
|
38
|
+
* `opts.disjointnessFallback = "touches-only"` to degrade to the Touches-only check
|
|
39
|
+
* with a loud ANNOUNCED WARNING. This escape applies ONLY to `graph-unavailable` —
|
|
40
|
+
* it NEVER applies to `graph-says-non-disjoint` (a real non-disjoint verdict stays
|
|
41
|
+
* absolute and un-escapable). [RULE] disjointness-bootstrap-escape-not-a-no-recourse-brick
|
|
42
|
+
*
|
|
43
|
+
* The existing Touches-overlap check is PRESERVED (additive — Destructive Action Guard).
|
|
44
|
+
* [RULE] execute-disjointness-output-flips-on-graph-edge
|
|
22
45
|
*/
|
|
23
46
|
|
|
24
47
|
const fs = require("node:fs");
|
|
@@ -163,21 +186,275 @@ function groupByOverlap(items) {
|
|
|
163
186
|
return Array.from(groups.values());
|
|
164
187
|
}
|
|
165
188
|
|
|
189
|
+
// ─── Graph-aware disjointness (M94-D11 — SAFETY-CRITICAL) ───────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Query the graph CLI for the blast-radius of a file (transitive downstream set).
|
|
193
|
+
*
|
|
194
|
+
* Returns { blastRadius: string[], graphAvailable: boolean, cliPresent: boolean }.
|
|
195
|
+
*
|
|
196
|
+
* `cliPresent: false` means no local `bin/gsd-t-graph-query-cli.cjs` was found —
|
|
197
|
+
* the project has no graph infrastructure at all. Callers should skip the graph
|
|
198
|
+
* check entirely in that case (not FAIL LOUD — this is "no graph", not "broken graph").
|
|
199
|
+
*
|
|
200
|
+
* `cliPresent: true, graphAvailable: false` means the CLI exists but the graph index
|
|
201
|
+
* is missing or broken → FAIL LOUD (callers should halt fan-out).
|
|
202
|
+
*
|
|
203
|
+
* Only the LOCAL `bin/gsd-t-graph-query-cli.cjs` is used — no global `gsd-t` fallback.
|
|
204
|
+
* The global `gsd-t` binary serves other purposes; only the local CLI is the graph
|
|
205
|
+
* query interface. This ensures projects without graph infrastructure do not accidentally
|
|
206
|
+
* query a global binary (which would have no graph data for this project anyway).
|
|
207
|
+
*
|
|
208
|
+
* Never throws.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} projectDir
|
|
211
|
+
* @param {string} filePath — repo-relative path to query
|
|
212
|
+
*/
|
|
213
|
+
function queryBlastRadius(projectDir, filePath) {
|
|
214
|
+
const queryCliPath = path.join(projectDir, "bin", "gsd-t-graph-query-cli.cjs");
|
|
215
|
+
const cliPresent = fs.existsSync(queryCliPath);
|
|
216
|
+
|
|
217
|
+
// No local CLI → no graph infrastructure. Skip the graph check; do NOT FAIL LOUD.
|
|
218
|
+
if (!cliPresent) {
|
|
219
|
+
return { blastRadius: [], graphAvailable: false, cliPresent: false };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let raw;
|
|
223
|
+
try {
|
|
224
|
+
raw = execSync(`node "${queryCliPath}" blast-radius "${filePath}"`, {
|
|
225
|
+
cwd: projectDir,
|
|
226
|
+
encoding: "utf8",
|
|
227
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
228
|
+
timeout: 10000,
|
|
229
|
+
});
|
|
230
|
+
} catch {
|
|
231
|
+
// CLI present but query failed (graph index broken / parse error).
|
|
232
|
+
return { blastRadius: [], graphAvailable: false, cliPresent: true };
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const envelope = JSON.parse((raw || "").trim());
|
|
236
|
+
if (!envelope.ok && envelope.reason === "graph-unavailable") {
|
|
237
|
+
// CLI present but graph index not built yet.
|
|
238
|
+
return { blastRadius: [], graphAvailable: false, cliPresent: true };
|
|
239
|
+
}
|
|
240
|
+
if (envelope.ok && Array.isArray(envelope.results)) {
|
|
241
|
+
const files = envelope.results.map((r) =>
|
|
242
|
+
typeof r === "string" ? r : (r && (r.file || r.path || ""))
|
|
243
|
+
).filter(Boolean);
|
|
244
|
+
return { blastRadius: files, graphAvailable: true, cliPresent: true };
|
|
245
|
+
}
|
|
246
|
+
return { blastRadius: [], graphAvailable: false, cliPresent: true };
|
|
247
|
+
} catch {
|
|
248
|
+
return { blastRadius: [], graphAvailable: false, cliPresent: true };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* M94-D11: Graph-aware disjointness check between two touch-lists.
|
|
254
|
+
*
|
|
255
|
+
* Returns one of:
|
|
256
|
+
* { verdict: "disjoint" }
|
|
257
|
+
* { verdict: "non-disjoint", reason: "graph-overlap"|"literal-touches-overlap", overlap: [...] }
|
|
258
|
+
* { verdict: "graph-unavailable", reason: "GRAPH_UNAVAILABLE" }
|
|
259
|
+
* — CLI present, graph index broken/missing → FAIL LOUD
|
|
260
|
+
* { verdict: "no-cli" }
|
|
261
|
+
* — no local graph CLI binary → skip graph check, Touches-only path
|
|
262
|
+
*
|
|
263
|
+
* The literal-Touches-overlap check runs FIRST (fast path). The graph check runs ONLY
|
|
264
|
+
* when the literal check returns disjoint — to discover transitive overlaps.
|
|
265
|
+
*
|
|
266
|
+
* [RULE] execute-disjointness-graph-aware-dependency-overlap
|
|
267
|
+
* [RULE] execute-disjointness-output-flips-on-graph-edge
|
|
268
|
+
*/
|
|
269
|
+
function graphAwareDisjointCheck(projectDir, touchesA, touchesB) {
|
|
270
|
+
// Fast path: literal Touches overlap — already know they're non-disjoint.
|
|
271
|
+
if (haveOverlap(touchesA, touchesB)) {
|
|
272
|
+
return { verdict: "non-disjoint", reason: "literal-touches-overlap", overlap: [] };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Slow path: query the graph for transitive dependency overlap.
|
|
276
|
+
let graphAvailableConfirmed = false;
|
|
277
|
+
|
|
278
|
+
for (const fileA of touchesA.slice(0, 5)) {
|
|
279
|
+
const { blastRadius, graphAvailable, cliPresent } = queryBlastRadius(projectDir, fileA);
|
|
280
|
+
if (!cliPresent) {
|
|
281
|
+
// No local graph CLI in this project — skip graph check entirely.
|
|
282
|
+
return { verdict: "no-cli" };
|
|
283
|
+
}
|
|
284
|
+
if (!graphAvailable) {
|
|
285
|
+
// CLI present but index missing/broken → FAIL LOUD.
|
|
286
|
+
return { verdict: "graph-unavailable", reason: "GRAPH_UNAVAILABLE" };
|
|
287
|
+
}
|
|
288
|
+
graphAvailableConfirmed = true;
|
|
289
|
+
const setBtouch = new Set(touchesB);
|
|
290
|
+
const overlap = blastRadius.filter((f) => setBtouch.has(f));
|
|
291
|
+
if (overlap.length > 0) {
|
|
292
|
+
return {
|
|
293
|
+
verdict: "non-disjoint",
|
|
294
|
+
reason: "graph-overlap",
|
|
295
|
+
overlap,
|
|
296
|
+
detail: `"${fileA}" has transitive dependents in domain B: ${overlap.slice(0, 3).join(", ")}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const fileB of touchesB.slice(0, 5)) {
|
|
302
|
+
const { blastRadius, graphAvailable, cliPresent } = queryBlastRadius(projectDir, fileB);
|
|
303
|
+
if (!cliPresent) {
|
|
304
|
+
if (!graphAvailableConfirmed) {
|
|
305
|
+
return { verdict: "no-cli" };
|
|
306
|
+
}
|
|
307
|
+
continue; // Transient: A's queries confirmed the CLI; skip B
|
|
308
|
+
}
|
|
309
|
+
if (!graphAvailable) {
|
|
310
|
+
if (!graphAvailableConfirmed) {
|
|
311
|
+
// CLI present but index missing/broken → FAIL LOUD.
|
|
312
|
+
return { verdict: "graph-unavailable", reason: "GRAPH_UNAVAILABLE" };
|
|
313
|
+
}
|
|
314
|
+
continue; // Transient error on B after A confirmed available — skip
|
|
315
|
+
}
|
|
316
|
+
graphAvailableConfirmed = true;
|
|
317
|
+
const setAtouch = new Set(touchesA);
|
|
318
|
+
const overlap = blastRadius.filter((f) => setAtouch.has(f));
|
|
319
|
+
if (overlap.length > 0) {
|
|
320
|
+
return {
|
|
321
|
+
verdict: "non-disjoint",
|
|
322
|
+
reason: "graph-overlap",
|
|
323
|
+
overlap,
|
|
324
|
+
detail: `"${fileB}" has transitive dependents in domain A: ${overlap.slice(0, 3).join(", ")}`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { verdict: "disjoint" };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* M94-D11: Graph-aware group partitioner.
|
|
334
|
+
*
|
|
335
|
+
* On graph-unavailable:
|
|
336
|
+
* - fallbackToTouchesOnly=true → ANNOUNCED WARNING + literal-Touches fallback
|
|
337
|
+
* (bootstrap escape hatch for fresh repo / parser regression).
|
|
338
|
+
* - fallbackToTouchesOnly=false → returns { graphUnavailable:true } immediately
|
|
339
|
+
* so the caller can FAIL LOUD and HALT.
|
|
340
|
+
*
|
|
341
|
+
* The escape hatch NEVER applies to a graph-says-non-disjoint verdict — real blocks
|
|
342
|
+
* stay absolute.
|
|
343
|
+
*
|
|
344
|
+
* [RULE] execute-disjointness-fail-loud-halts-never-grep-guess
|
|
345
|
+
* [RULE] disjointness-bootstrap-escape-not-a-no-recourse-brick
|
|
346
|
+
*/
|
|
347
|
+
function groupByOverlapGraphAware(items, projectDir, fallbackToTouchesOnly) {
|
|
348
|
+
const n = items.length;
|
|
349
|
+
if (n === 0) return { parallel: [], sequential: [], graphUnavailable: false };
|
|
350
|
+
|
|
351
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
352
|
+
const find = (i) => {
|
|
353
|
+
while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }
|
|
354
|
+
return i;
|
|
355
|
+
};
|
|
356
|
+
const union = (i, j) => { const a = find(i), b = find(j); if (a !== b) parent[a] = b; };
|
|
357
|
+
|
|
358
|
+
let graphUnavailable = false;
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < n; i++) {
|
|
361
|
+
for (let j = i + 1; j < n; j++) {
|
|
362
|
+
const result = graphAwareDisjointCheck(projectDir, items[i].touches, items[j].touches);
|
|
363
|
+
if (result.verdict === "no-cli") {
|
|
364
|
+
// No local graph CLI in this project → skip graph check for all pairs.
|
|
365
|
+
// Fall through to Touches-only for this pair (NOT FAIL LOUD — this is "no graph").
|
|
366
|
+
if (haveOverlap(items[i].touches, items[j].touches)) {
|
|
367
|
+
union(i, j);
|
|
368
|
+
}
|
|
369
|
+
} else if (result.verdict === "graph-unavailable") {
|
|
370
|
+
graphUnavailable = true;
|
|
371
|
+
if (fallbackToTouchesOnly) {
|
|
372
|
+
// Bootstrap escape hatch: ANNOUNCED WARNING, degrade to literal-Touches.
|
|
373
|
+
// NEVER silent. NEVER applied to graph-says-non-disjoint verdicts.
|
|
374
|
+
process.stderr.write(
|
|
375
|
+
`[gsd-t disjointness] WARNING: graph unavailable — falling back to ` +
|
|
376
|
+
`literal-Touches-only check (--disjointness-fallback=touches-only). ` +
|
|
377
|
+
`Transitive dependency overlaps will NOT be detected. ` +
|
|
378
|
+
`Fix the graph index (gsd-t graph build) before the next parallel execute.\n`
|
|
379
|
+
);
|
|
380
|
+
if (haveOverlap(items[i].touches, items[j].touches)) {
|
|
381
|
+
union(i, j);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
// No escape hatch: FAIL LOUD — return the unavailable signal.
|
|
385
|
+
// The caller (proveDisjointness) surfaces this and HALTS fan-out.
|
|
386
|
+
return { parallel: [], sequential: [], graphUnavailable: true };
|
|
387
|
+
}
|
|
388
|
+
} else if (result.verdict === "non-disjoint") {
|
|
389
|
+
// Real non-disjoint (literal OR graph-says-non-disjoint) — ABSOLUTE, not escapable.
|
|
390
|
+
union(i, j);
|
|
391
|
+
}
|
|
392
|
+
// verdict === "disjoint" → no union (safe to parallel)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const groups = new Map();
|
|
397
|
+
for (let i = 0; i < n; i++) {
|
|
398
|
+
const root = find(i);
|
|
399
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
400
|
+
groups.get(root).push(items[i].task);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const parallel = [];
|
|
404
|
+
const sequential = [];
|
|
405
|
+
for (const group of groups.values()) {
|
|
406
|
+
if (group.length === 1) parallel.push(group);
|
|
407
|
+
else sequential.push(group);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { parallel, sequential, graphUnavailable: false };
|
|
411
|
+
}
|
|
412
|
+
|
|
166
413
|
// ─── Public API ──────────────────────────────────────────────────────────
|
|
167
414
|
|
|
168
415
|
/**
|
|
169
416
|
* Prove pairwise file-disjointness across a candidate parallel set.
|
|
170
417
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
418
|
+
* M94-D11: Graph-aware when `bin/gsd-t-graph-query-cli.cjs` is present in projectDir.
|
|
419
|
+
* - No local CLI → auto-detects via `no-cli` verdict → Touches-only (backward-compat).
|
|
420
|
+
* - Local CLI present but graph index missing/broken → FAIL LOUD (graphUnavailable=true).
|
|
421
|
+
* - Local CLI present and graph available → transitive-overlap check via blast-radius.
|
|
173
422
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
423
|
+
* @param {{
|
|
424
|
+
* tasks: object[],
|
|
425
|
+
* projectDir: string,
|
|
426
|
+
* disjointnessFallback?: "touches-only", // bootstrap escape hatch: graph-unavailable → Touches-only
|
|
427
|
+
* skipGraphCheck?: boolean, // test-only: isolate Touches-only verdict (consumption-proof delta)
|
|
428
|
+
* }} opts
|
|
429
|
+
*
|
|
430
|
+
* @returns {{
|
|
431
|
+
* parallel: object[][],
|
|
432
|
+
* sequential: object[][],
|
|
433
|
+
* unprovable: object[],
|
|
434
|
+
* graphUnavailable?: boolean, // true when CLI present but graph broken + no escape hatch
|
|
435
|
+
* haltReason?: string, // "GRAPH_UNAVAILABLE"
|
|
436
|
+
* haltMessage?: string, // human-readable remediation message
|
|
437
|
+
* }}
|
|
438
|
+
*
|
|
439
|
+
* FAIL-LOUD invariant: when graphUnavailable=true, the CALLER must surface:
|
|
440
|
+
* "graph unavailable — fix it (gsd-t graph status)" and HALT fan-out.
|
|
441
|
+
* It MUST NOT proceed on a grep-reconstructed guess.
|
|
442
|
+
* [RULE] execute-disjointness-fail-loud-halts-never-grep-guess
|
|
443
|
+
*
|
|
444
|
+
* Bootstrap escape hatch (opts.disjointnessFallback === "touches-only"):
|
|
445
|
+
* On CLI-present-but-graph-unavailable, falls back to literal-Touches with ANNOUNCED WARNING.
|
|
446
|
+
* Applies ONLY to graph-unavailable — graph-says-non-disjoint is absolute and un-escapable.
|
|
447
|
+
* [RULE] disjointness-bootstrap-escape-not-a-no-recourse-brick
|
|
448
|
+
*
|
|
449
|
+
* Never throws.
|
|
177
450
|
*/
|
|
178
451
|
function proveDisjointness(opts) {
|
|
179
452
|
const tasks = (opts && Array.isArray(opts.tasks)) ? opts.tasks : [];
|
|
180
453
|
const projectDir = (opts && opts.projectDir) || process.cwd();
|
|
454
|
+
const fallbackToTouchesOnly = (opts && opts.disjointnessFallback) === "touches-only";
|
|
455
|
+
// skipGraphCheck=true forces Touches-only even if a local CLI is present.
|
|
456
|
+
// Used by the consumption-proof test to isolate the Touches-only verdict for comparison.
|
|
457
|
+
const skipGraphCheck = !!(opts && opts.skipGraphCheck);
|
|
181
458
|
|
|
182
459
|
const parallel = [];
|
|
183
460
|
const sequential = [];
|
|
@@ -193,7 +470,6 @@ function proveDisjointness(opts) {
|
|
|
193
470
|
const { touches, source } = resolveTouches(t, projectDir);
|
|
194
471
|
if (source === "none") {
|
|
195
472
|
unprovable.push(t);
|
|
196
|
-
// Unprovable → always sequential (singleton group). Safe-default.
|
|
197
473
|
sequential.push([t]);
|
|
198
474
|
appendFallbackEvent(projectDir, t.id, "unprovable");
|
|
199
475
|
} else {
|
|
@@ -201,7 +477,40 @@ function proveDisjointness(opts) {
|
|
|
201
477
|
}
|
|
202
478
|
}
|
|
203
479
|
|
|
204
|
-
|
|
480
|
+
if (provable.length === 0) {
|
|
481
|
+
return { parallel, sequential, unprovable };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// M94-D11: Graph-aware path — auto-activated when the local graph CLI is present.
|
|
485
|
+
// When no local CLI is found, graphAwareDisjointCheck returns verdict="no-cli" and
|
|
486
|
+
// groupByOverlapGraphAware degrades to literal-Touches seamlessly (no FAIL LOUD).
|
|
487
|
+
if (!skipGraphCheck) {
|
|
488
|
+
const graphResult = groupByOverlapGraphAware(provable, projectDir, fallbackToTouchesOnly);
|
|
489
|
+
if (graphResult.graphUnavailable) {
|
|
490
|
+
// FAIL LOUD: local CLI is present but graph index is broken/missing.
|
|
491
|
+
// Caller must surface this and HALT fan-out.
|
|
492
|
+
return {
|
|
493
|
+
parallel: [],
|
|
494
|
+
sequential: [],
|
|
495
|
+
unprovable,
|
|
496
|
+
graphUnavailable: true,
|
|
497
|
+
haltReason: "GRAPH_UNAVAILABLE",
|
|
498
|
+
haltMessage: "graph unavailable — fix it (gsd-t graph status). " +
|
|
499
|
+
"Parallel execution requires the graph index for dependency-overlap detection. " +
|
|
500
|
+
"To bypass temporarily (fresh-repo bootstrap only), pass --disjointness-fallback=touches-only " +
|
|
501
|
+
"(ANNOUNCED WARNING — transitive overlaps will NOT be detected).",
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
// Graph check succeeded (or no-cli → Touches-only used silently).
|
|
505
|
+
for (const group of graphResult.parallel) { parallel.push(group); }
|
|
506
|
+
for (const group of graphResult.sequential) {
|
|
507
|
+
sequential.push(group);
|
|
508
|
+
for (const t of group) { appendFallbackEvent(projectDir, t.id, "write-target-overlap"); }
|
|
509
|
+
}
|
|
510
|
+
return { parallel, sequential, unprovable };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// skipGraphCheck=true: Touches-only path (for consumption-proof delta test).
|
|
205
514
|
const groups = groupByOverlap(provable);
|
|
206
515
|
for (const group of groups) {
|
|
207
516
|
if (group.length === 1) {
|
|
@@ -222,6 +531,9 @@ module.exports = {
|
|
|
222
531
|
// Internals exposed for unit tests:
|
|
223
532
|
_haveOverlap: haveOverlap,
|
|
224
533
|
_groupByOverlap: groupByOverlap,
|
|
534
|
+
_groupByOverlapGraphAware: groupByOverlapGraphAware,
|
|
535
|
+
_graphAwareDisjointCheck: graphAwareDisjointCheck,
|
|
536
|
+
_queryBlastRadius: queryBlastRadius,
|
|
225
537
|
_resolveTouches: resolveTouches,
|
|
226
538
|
_gitHistoryTouches: gitHistoryTouches,
|
|
227
539
|
};
|