@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/bin/gsd-t-competition-judge.cjs +7 -1
  4. package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
  5. package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
  6. package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
  7. package/bin/gsd-t-file-disjointness.cjs +319 -7
  8. package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
  9. package/bin/gsd-t-graph-edge-extract.cjs +612 -0
  10. package/bin/gsd-t-graph-freshness.cjs +506 -0
  11. package/bin/gsd-t-graph-index.cjs +540 -0
  12. package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
  13. package/bin/gsd-t-graph-query-cli.cjs +1182 -0
  14. package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
  15. package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
  16. package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
  17. package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
  18. package/bin/gsd-t-scip-reader.cjs +167 -0
  19. package/bin/gsd-t.js +166 -48
  20. package/commands/gsd-t-debug.md +10 -0
  21. package/commands/gsd-t-design-build.md +10 -0
  22. package/commands/gsd-t-execute.md +15 -0
  23. package/commands/gsd-t-feature.md +15 -7
  24. package/commands/gsd-t-gap-analysis.md +17 -7
  25. package/commands/gsd-t-impact.md +25 -0
  26. package/commands/gsd-t-integrate.md +25 -0
  27. package/commands/gsd-t-partition.md +18 -0
  28. package/commands/gsd-t-plan.md +16 -0
  29. package/commands/gsd-t-populate.md +16 -5
  30. package/commands/gsd-t-prd.md +18 -0
  31. package/commands/gsd-t-project.md +19 -0
  32. package/commands/gsd-t-promote-debt.md +16 -5
  33. package/commands/gsd-t-qa.md +20 -8
  34. package/commands/gsd-t-quick.md +10 -0
  35. package/commands/gsd-t-scan.md +21 -3
  36. package/commands/gsd-t-test-sync.md +10 -0
  37. package/commands/gsd-t-verify.md +25 -0
  38. package/commands/gsd-t-wave.md +8 -0
  39. package/package.json +10 -2
  40. package/templates/workflows/gsd-t-debug.workflow.js +81 -0
  41. package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
  42. package/templates/workflows/gsd-t-phase.workflow.js +84 -0
  43. package/templates/workflows/gsd-t-quick.workflow.js +64 -0
  44. package/templates/workflows/gsd-t-scan.workflow.js +200 -9
  45. package/templates/workflows/gsd-t-verify.workflow.js +50 -1
  46. package/bin/graph-cgc.js +0 -510
  47. package/bin/graph-indexer.js +0 -147
  48. package/bin/graph-overlay.js +0 -195
  49. package/bin/graph-parsers.js +0 -327
  50. package/bin/graph-query.js +0 -453
  51. 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
- * @param {{tasks: object[], projectDir: string}} opts
172
- * @returns {{parallel: object[][], sequential: object[][], unprovable: object[]}}
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
- * Never throws. Appends a `disjointness_fallback` event to
175
- * `.gsd-t/events/YYYY-MM-DD.jsonl` for every task routed sequential
176
- * (including unprovable tasks).
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
- // Group provable tasks by overlap. Singletons parallel. Multi → sequential.
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
  };