@shrkcrft/cli 0.1.0-alpha.29 → 0.1.0-alpha.30

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 (41) hide show
  1. package/dist/commands/baseline.command.d.ts +25 -0
  2. package/dist/commands/baseline.command.d.ts.map +1 -1
  3. package/dist/commands/baseline.command.js +23 -7
  4. package/dist/commands/changelog-data.d.ts.map +1 -1
  5. package/dist/commands/changelog-data.js +26 -0
  6. package/dist/commands/check.command.d.ts.map +1 -1
  7. package/dist/commands/check.command.js +19 -2
  8. package/dist/commands/code-intel.command.d.ts.map +1 -1
  9. package/dist/commands/code-intel.command.js +5 -1
  10. package/dist/commands/docs-references.command.d.ts +7 -0
  11. package/dist/commands/docs-references.command.d.ts.map +1 -0
  12. package/dist/commands/docs-references.command.js +323 -0
  13. package/dist/commands/doctor.command.d.ts.map +1 -1
  14. package/dist/commands/doctor.command.js +3 -2
  15. package/dist/commands/gates.command.d.ts +2 -0
  16. package/dist/commands/gates.command.d.ts.map +1 -1
  17. package/dist/commands/gates.command.js +640 -16
  18. package/dist/commands/generated.command.d.ts +32 -0
  19. package/dist/commands/generated.command.d.ts.map +1 -1
  20. package/dist/commands/generated.command.js +180 -49
  21. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  22. package/dist/commands/graph-code-subverbs.js +5 -1
  23. package/dist/commands/wiring.command.d.ts.map +1 -1
  24. package/dist/commands/wiring.command.js +4 -3
  25. package/dist/gates/gate-envelope.d.ts +1 -1
  26. package/dist/gates/gate-envelope.d.ts.map +1 -1
  27. package/dist/gates/gate-rule-globs.d.ts +33 -0
  28. package/dist/gates/gate-rule-globs.d.ts.map +1 -0
  29. package/dist/gates/gate-rule-globs.js +101 -0
  30. package/dist/gates/gate-rule-view.d.ts +9 -3
  31. package/dist/gates/gate-rule-view.d.ts.map +1 -1
  32. package/dist/gates/gate-rule-view.js +13 -0
  33. package/dist/gates/rule-coverage.d.ts +39 -1
  34. package/dist/gates/rule-coverage.d.ts.map +1 -1
  35. package/dist/gates/rule-coverage.js +123 -8
  36. package/dist/gates/run-gate-planes.d.ts +44 -0
  37. package/dist/gates/run-gate-planes.d.ts.map +1 -0
  38. package/dist/gates/run-gate-planes.js +261 -0
  39. package/dist/main.d.ts.map +1 -1
  40. package/dist/main.js +5 -1
  41. package/package.json +33 -33
@@ -1,6 +1,38 @@
1
+ import { type IGeneratedArtifactRule } from '@shrkcrft/core';
2
+ import { type IGeneratedTreeDiff, type IProvenanceFinding } from '@shrkcrft/boundaries';
1
3
  import { type ICommandHandler } from '../command-registry.js';
4
+ /**
5
+ * One rule's outcome. Exported so `shrk gates check` aggregates the SAME
6
+ * evaluation the per-plane verb runs, rather than a second implementation that
7
+ * could drift from it.
8
+ */
9
+ export interface IGeneratedOutcome {
10
+ readonly rule: IGeneratedArtifactRule;
11
+ readonly status: 'passed' | 'failed' | 'skipped' | 'error';
12
+ readonly committedCount: number;
13
+ readonly treeDiff?: IGeneratedTreeDiff;
14
+ readonly provenance: readonly IProvenanceFinding[];
15
+ readonly error?: string;
16
+ readonly skipReason?: string;
17
+ /** True when the drift half was deliberately not run (`--headers-only` / no `regen`). */
18
+ readonly driftChecked: boolean;
19
+ /** Files excluded from every check by `handMaintained`. */
20
+ readonly handMaintained: readonly string[];
21
+ /** Per-writer drift results, for a multi-writer rule. */
22
+ readonly writers: readonly IWriterOutcome[];
23
+ }
24
+ /** One writer's slice result inside a multi-writer rule. */
25
+ interface IWriterOutcome {
26
+ readonly label: string;
27
+ readonly committedCount: number;
28
+ readonly differences: number;
29
+ readonly error?: string;
30
+ }
31
+ export declare function evaluateGeneratedRule(cwd: string, rule: IGeneratedArtifactRule, excludeDirs: readonly string[], headersOnly: boolean): IGeneratedOutcome;
32
+ export declare function generatedHintFor(rule: IGeneratedArtifactRule): string;
2
33
  export declare const generatedListCommand: ICommandHandler;
3
34
  export declare const generatedCheckCommand: ICommandHandler;
4
35
  export declare const generatedUpdateCommand: ICommandHandler;
5
36
  export declare const generatedExplainCommand: ICommandHandler;
37
+ export {};
6
38
  //# sourceMappingURL=generated.command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generated.command.d.ts","sourceRoot":"","sources":["../../src/commands/generated.command.ts"],"names":[],"mappings":"AA+BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAyShC,eAAO,MAAM,oBAAoB,EAAE,eA2ClC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAuHnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAmDpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAiErC,CAAC"}
1
+ {"version":3,"file":"generated.command.d.ts","sourceRoot":"","sources":["../../src/commands/generated.command.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAkB,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAML,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA0LhC;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;IAC3D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAED,4DAA4D;AAC5D,UAAU,cAAc;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,sBAAsB,EAC5B,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,WAAW,EAAE,OAAO,GACnB,iBAAiB,CAqFnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CAOrE;AAwED,eAAO,MAAM,oBAAoB,EAAE,eA4DlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAgInC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAgEpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAyGrC,CAAC"}
@@ -127,16 +127,23 @@ function alignToCommitted(temp, committed) {
127
127
  }
128
128
  return out;
129
129
  }
130
- /** Run `regen` into a fresh temp dir and read the result back. Always cleans up. */
131
- function runRegen(cwd, rule, committed) {
130
+ /**
131
+ * Run ONE regen command into a fresh temp dir and read the result back, keyed
132
+ * onto the committed paths it corresponds to. Always cleans up.
133
+ *
134
+ * `committed` is the slice this command OWNS, not the whole tree — that is what
135
+ * keeps a multi-writer tree honest: each command's output is aligned against
136
+ * (and later diffed against) only its own files, so writer A's output can never
137
+ * be reported as writer B's stale committed file.
138
+ */
139
+ function runOneRegen(cwd, command, committed, timeoutMs) {
132
140
  const tmp = mkdtempSync(nodePath.join(nodeOs.tmpdir(), 'shrk-generated-'));
133
141
  try {
134
- const command = rule.regen.split('{TMP}').join(tmp);
135
- const child = spawnSync(command, {
142
+ const child = spawnSync(command.split('{TMP}').join(tmp), {
136
143
  cwd,
137
144
  shell: true,
138
145
  encoding: 'utf8',
139
- timeout: rule.timeoutMs ?? DEFAULT_TIMEOUT_MS,
146
+ timeout: timeoutMs,
140
147
  maxBuffer: 16 * 1024 * 1024,
141
148
  });
142
149
  if (child.error)
@@ -155,7 +162,27 @@ function runRegen(cwd, rule, committed) {
155
162
  rmSync(tmp, { recursive: true, force: true });
156
163
  }
157
164
  }
158
- function evaluateRule(cwd, rule, excludeDirs, headersOnly) {
165
+ /**
166
+ * The regen units for a rule: one per `sources[]` writer, or a single unit for
167
+ * the classic `regen` form. A rule declaring neither yields none (header-only).
168
+ */
169
+ function regenUnitsOf(rule, scan) {
170
+ const fallback = rule.timeoutMs ?? DEFAULT_TIMEOUT_MS;
171
+ if (scan.slices.length > 0) {
172
+ return scan.slices.map((slice) => ({
173
+ label: slice.label,
174
+ command: slice.regen,
175
+ committed: slice.files,
176
+ timeoutMs: slice.timeoutMs ?? fallback,
177
+ }));
178
+ }
179
+ if (rule.regen === undefined)
180
+ return [];
181
+ // Single-writer: the checkable set, so a hand-maintained bless inside the
182
+ // glob is not compared against output no generator produces.
183
+ return [{ label: rule.id, command: rule.regen, committed: scan.checkable, timeoutMs: fallback }];
184
+ }
185
+ export function evaluateGeneratedRule(cwd, rule, excludeDirs, headersOnly) {
159
186
  const scan = scanGeneratedFiles(cwd, rule, excludeDirs);
160
187
  const severity = rule.severity ?? 'error';
161
188
  if (scan.generated.size === 0) {
@@ -166,10 +193,17 @@ function evaluateRule(cwd, rule, excludeDirs, headersOnly) {
166
193
  committedCount: 0,
167
194
  provenance: [],
168
195
  driftChecked: false,
196
+ handMaintained: [],
197
+ writers: [],
169
198
  skipReason: `0 files matched generatedGlob (${rule.generatedGlob.join(', ')})`,
170
199
  };
171
200
  }
172
- const headers = checkProvenanceHeaders(rule, scan.generated, scan.outside);
201
+ // Headers + byte checks run over the CHECKABLE set — a hand-maintained bless
202
+ // is excluded from both, which is the whole point of declaring it.
203
+ const headers = checkProvenanceHeaders(rule, scan.checkable, scan.outside, {
204
+ unclassified: scan.unclassified,
205
+ staleHandMaintained: scan.staleHandMaintained,
206
+ });
173
207
  if (headers.error) {
174
208
  return {
175
209
  rule,
@@ -177,21 +211,39 @@ function evaluateRule(cwd, rule, excludeDirs, headersOnly) {
177
211
  committedCount: scan.generated.size,
178
212
  provenance: [],
179
213
  driftChecked: false,
214
+ handMaintained: scan.handMaintained,
215
+ writers: [],
180
216
  error: headers.error,
181
217
  };
182
218
  }
183
- let treeDiff;
184
- let error;
185
- const wantDrift = !headersOnly && rule.regen !== undefined;
186
- if (wantDrift) {
187
- const regen = runRegen(cwd, rule, scan.generated);
188
- if (regen.error)
189
- error = regen.error;
190
- else
191
- treeDiff = compareGeneratedTrees(scan.generated, regen.files, rule.compare ?? 'bytes');
219
+ const units = headersOnly ? [] : regenUnitsOf(rule, scan);
220
+ const differences = [];
221
+ const writers = [];
222
+ const errors = [];
223
+ let committedCompared = 0;
224
+ let regeneratedCount = 0;
225
+ for (const unit of units) {
226
+ const regen = runOneRegen(cwd, unit.command, unit.committed, unit.timeoutMs);
227
+ if (regen.error) {
228
+ errors.push(units.length > 1 ? `${unit.label}: ${regen.error}` : regen.error);
229
+ writers.push({ label: unit.label, committedCount: unit.committed.size, differences: 0, error: regen.error });
230
+ continue;
231
+ }
232
+ const diff = compareGeneratedTrees(unit.committed, regen.files, rule.compare ?? 'bytes');
233
+ differences.push(...diff.differences);
234
+ committedCompared += diff.committedCount;
235
+ regeneratedCount += diff.regeneratedCount;
236
+ writers.push({ label: unit.label, committedCount: unit.committed.size, differences: diff.differences.length });
192
237
  }
238
+ const ranAnyWriter = units.length > 0 && errors.length < units.length;
239
+ const treeDiff = ranAnyWriter
240
+ ? { differences, committedCount: committedCompared, regeneratedCount }
241
+ : undefined;
242
+ const error = errors.length > 0 ? errors.join(' · ') : undefined;
193
243
  const hardFindings = headers.findings.filter((f) => f.severity === 'error');
194
- const drifted = (treeDiff?.differences.length ?? 0) > 0;
244
+ const drifted = differences.length > 0;
245
+ // A writer that failed to RUN proved nothing, so the rule is in error even if
246
+ // its siblings were clean — a partial regen must never read as a full pass.
195
247
  const status = error !== undefined
196
248
  ? 'error'
197
249
  : drifted || (hardFindings.length > 0 && severity === 'error')
@@ -204,12 +256,14 @@ function evaluateRule(cwd, rule, excludeDirs, headersOnly) {
204
256
  ...(treeDiff ? { treeDiff } : {}),
205
257
  provenance: headers.findings,
206
258
  ...(error ? { error } : {}),
207
- driftChecked: wantDrift && error === undefined,
259
+ driftChecked: units.length > 0 && errors.length === 0,
260
+ handMaintained: scan.handMaintained,
261
+ writers: writers.length > 1 ? writers : [],
208
262
  };
209
263
  }
210
- function hintFor(rule) {
264
+ export function generatedHintFor(rule) {
211
265
  return (rule.hint ??
212
- (rule.regen
266
+ (rule.regen || rule.sources
213
267
  ? `regenerate with \`shrk generated update --id ${rule.id}\` and commit the result`
214
268
  : 'add the provenance header to the generated file, or move it out of the generated glob'));
215
269
  }
@@ -221,11 +275,13 @@ function outcomeJson(o) {
221
275
  severity: o.rule.severity ?? 'error',
222
276
  committedCount: o.committedCount,
223
277
  driftChecked: o.driftChecked,
278
+ handMaintained: o.handMaintained,
279
+ ...(o.writers.length > 0 ? { writers: o.writers } : {}),
224
280
  ...(o.treeDiff ? { differences: o.treeDiff.differences, regeneratedCount: o.treeDiff.regeneratedCount } : {}),
225
281
  provenance: o.provenance,
226
282
  ...(o.error ? { error: o.error } : {}),
227
283
  ...(o.skipReason ? { skipReason: o.skipReason } : {}),
228
- hint: hintFor(o.rule),
284
+ hint: generatedHintFor(o.rule),
229
285
  };
230
286
  }
231
287
  async function prepare(args) {
@@ -291,6 +347,9 @@ export const generatedListCommand = {
291
347
  description: r.description ?? null,
292
348
  generatedGlob: r.generatedGlob,
293
349
  regen: r.regen ?? null,
350
+ sources: r.sources ?? null,
351
+ handMaintained: r.handMaintained ?? null,
352
+ handMaintainedMarker: r.handMaintainedMarker ?? null,
294
353
  compare: r.compare ?? 'bytes',
295
354
  provenanceHeader: r.provenanceHeader ?? null,
296
355
  failOnEmpty: r.failOnEmpty === true,
@@ -303,7 +362,22 @@ export const generatedListCommand = {
303
362
  for (const r of prep.all) {
304
363
  process.stdout.write(` • ${r.id}\n`);
305
364
  process.stdout.write(` glob ${r.generatedGlob.join(', ')}\n`);
306
- process.stdout.write(` regen ${r.regen ?? '(header-only never spawns)'}\n`);
365
+ if (r.sources && r.sources.length > 0) {
366
+ process.stdout.write(` regen ${r.sources.length} writer(s):\n`);
367
+ r.sources.forEach((src, i) => {
368
+ process.stdout.write(` [${src.id ?? i}] ${src.regen}\n`);
369
+ process.stdout.write(` owns ${src.glob.join(', ')}\n`);
370
+ });
371
+ }
372
+ else {
373
+ process.stdout.write(` regen ${r.regen ?? '(header-only — never spawns)'}\n`);
374
+ }
375
+ if (r.handMaintained && r.handMaintained.length > 0) {
376
+ process.stdout.write(` hand ${r.handMaintained.join(', ')} (exempt from header + byte checks)\n`);
377
+ }
378
+ if (r.handMaintainedMarker) {
379
+ process.stdout.write(` marker /${r.handMaintainedMarker}/ (in-file bless)\n`);
380
+ }
307
381
  if (r.provenanceHeader) {
308
382
  process.stdout.write(` header /${r.provenanceHeader.mustMatch}/${r.provenanceHeader.forbidOutside ? ' + mislabel check' : ''}\n`);
309
383
  }
@@ -328,7 +402,7 @@ export const generatedCheckCommand = {
328
402
  const headersOnly = flagBool(args, 'headers-only');
329
403
  if (prep.rules.length === 0)
330
404
  return writeNoRules(json);
331
- const outcomes = prep.rules.map((r) => evaluateRule(prep.cwd, r, prep.excludeDirs, headersOnly));
405
+ const outcomes = prep.rules.map((r) => evaluateGeneratedRule(prep.cwd, r, prep.excludeDirs, headersOnly));
332
406
  const failed = outcomes.filter((o) => o.status === 'failed' || (o.status === 'error' && (o.rule.severity ?? 'error') === 'error'));
333
407
  const evaluated = outcomes.filter((o) => o.status !== 'skipped').length;
334
408
  const skippedCount = outcomes.length - evaluated;
@@ -361,13 +435,13 @@ export const generatedCheckCommand = {
361
435
  id: d.file,
362
436
  file: d.file,
363
437
  message: d.kind,
364
- hint: hintFor(o.rule),
438
+ hint: generatedHintFor(o.rule),
365
439
  })),
366
440
  ...o.provenance.map((f) => ({
367
441
  id: f.file,
368
442
  file: f.file,
369
443
  message: f.message,
370
- hint: hintFor(o.rule),
444
+ hint: generatedHintFor(o.rule),
371
445
  })),
372
446
  ],
373
447
  ...(o.skipReason ? { skipReason: o.skipReason } : {}),
@@ -391,11 +465,18 @@ export const generatedCheckCommand = {
391
465
  }
392
466
  const diffs = o.treeDiff?.differences ?? [];
393
467
  if (o.status === 'passed') {
394
- process.stdout.write(` ✓ ${o.rule.id} (${o.committedCount} files` +
468
+ const exempt = o.handMaintained.length > 0 ? `, ${o.handMaintained.length} hand-maintained` : '';
469
+ process.stdout.write(` ✓ ${o.rule.id} (${o.committedCount} files${exempt}` +
395
470
  `${o.driftChecked ? ', byte-identical to a fresh regen' : ', headers only'})\n`);
471
+ for (const w of o.writers) {
472
+ process.stdout.write(` · ${w.label}: ${w.committedCount} file(s) verified\n`);
473
+ }
396
474
  }
397
475
  else {
398
476
  process.stdout.write(` ✗ ${o.rule.id} ${diffs.length} file(s) differ from a fresh regen\n`);
477
+ for (const w of o.writers) {
478
+ process.stdout.write(` · ${w.label}: ${w.error ? `ERROR — ${w.error}` : `${w.differences} of ${w.committedCount} differ`}\n`);
479
+ }
399
480
  for (const d of diffs.slice(0, 25)) {
400
481
  const label = d.kind === 'content'
401
482
  ? 'hand-edited (or source changed)'
@@ -414,7 +495,7 @@ export const generatedCheckCommand = {
414
495
  process.stdout.write(` … (${o.provenance.length - 25} more header finding(s))\n`);
415
496
  }
416
497
  if (o.status === 'failed')
417
- process.stdout.write(` → ${hintFor(o.rule)}\n`);
498
+ process.stdout.write(` → ${generatedHintFor(o.rule)}\n`);
418
499
  }
419
500
  for (const d of prep.planeDiagnostics)
420
501
  process.stdout.write(` ! ${d}\n`);
@@ -441,29 +522,41 @@ export const generatedUpdateCommand = {
441
522
  return writeNoRules(json);
442
523
  const results = [];
443
524
  for (const rule of prep.rules) {
444
- if (!rule.regen) {
525
+ // A multi-writer rule blesses by running EVERY writer — regenerating one
526
+ // slice and calling the artifact updated is how a mixed tree drifts.
527
+ const commands = rule.sources && rule.sources.length > 0
528
+ ? rule.sources.map((src, i) => ({
529
+ label: `${rule.id}/${src.id ?? i}`,
530
+ command: src.regen,
531
+ timeoutMs: src.timeoutMs ?? rule.timeoutMs ?? DEFAULT_TIMEOUT_MS,
532
+ }))
533
+ : rule.regen
534
+ ? [{ label: rule.id, command: rule.regen, timeoutMs: rule.timeoutMs ?? DEFAULT_TIMEOUT_MS }]
535
+ : [];
536
+ if (commands.length === 0) {
445
537
  results.push({ id: rule.id, ran: false, error: 'header-only rule — nothing to regenerate' });
446
538
  continue;
447
539
  }
448
- // `{TMP}` is the CHECK contract; `update` writes in place, so it is
449
- // substituted with the project root and the regen writes its real output.
450
- const command = rule.regen.split('{TMP}').join(prep.cwd);
451
- const child = spawnSync(command, {
452
- cwd: prep.cwd,
453
- shell: true,
454
- encoding: 'utf8',
455
- timeout: rule.timeoutMs ?? DEFAULT_TIMEOUT_MS,
456
- maxBuffer: 16 * 1024 * 1024,
457
- stdio: json ? 'pipe' : 'inherit',
458
- });
459
- if (child.error) {
460
- results.push({ id: rule.id, ran: false, error: child.error.message });
461
- }
462
- else if (child.status !== 0) {
463
- results.push({ id: rule.id, ran: true, error: `exited ${child.status ?? 'null'}` });
464
- }
465
- else {
466
- results.push({ id: rule.id, ran: true });
540
+ for (const { label, command, timeoutMs } of commands) {
541
+ // `{TMP}` is the CHECK contract; `update` writes in place, so it is
542
+ // substituted with the project root and the regen writes its real output.
543
+ const child = spawnSync(command.split('{TMP}').join(prep.cwd), {
544
+ cwd: prep.cwd,
545
+ shell: true,
546
+ encoding: 'utf8',
547
+ timeout: timeoutMs,
548
+ maxBuffer: 16 * 1024 * 1024,
549
+ stdio: json ? 'pipe' : 'inherit',
550
+ });
551
+ if (child.error) {
552
+ results.push({ id: label, ran: false, error: child.error.message });
553
+ }
554
+ else if (child.status !== 0) {
555
+ results.push({ id: label, ran: true, error: `exited ${child.status ?? 'null'}` });
556
+ }
557
+ else {
558
+ results.push({ id: label, ran: true });
559
+ }
467
560
  }
468
561
  }
469
562
  const failed = results.filter((r) => r.error !== undefined);
@@ -502,12 +595,23 @@ export const generatedExplainCommand = {
502
595
  }
503
596
  // explain never spawns — the point is to show what the rule SEES.
504
597
  const scan = scanGeneratedFiles(prep.cwd, rule, prep.excludeDirs);
505
- const outcome = evaluateRule(prep.cwd, rule, prep.excludeDirs, true);
598
+ const outcome = evaluateGeneratedRule(prep.cwd, rule, prep.excludeDirs, true);
506
599
  if (flagBool(args, 'json')) {
507
600
  process.stdout.write(asJson({
508
601
  schema: 'sharkcraft.generated-explain/v1',
509
602
  ...outcomeJson(outcome),
510
603
  files: [...scan.generated.keys()],
604
+ checkable: [...scan.checkable.keys()],
605
+ handMaintained: scan.handMaintained,
606
+ markedHandMaintained: scan.markedHandMaintained,
607
+ staleHandMaintained: scan.staleHandMaintained,
608
+ unclassified: scan.unclassified,
609
+ slices: scan.slices.map((sl) => ({
610
+ label: sl.label,
611
+ regen: sl.regen,
612
+ glob: sl.glob,
613
+ files: [...sl.files.keys()],
614
+ })),
511
615
  outsideScanned: scan.outside.size,
512
616
  outsideGlobs: scan.outsideGlobs,
513
617
  regen: rule.regen ?? null,
@@ -519,7 +623,34 @@ export const generatedExplainCommand = {
519
623
  process.stdout.write(` ${rule.description}\n`);
520
624
  process.stdout.write(kv('glob', rule.generatedGlob.join(', ')) + '\n');
521
625
  process.stdout.write(kv('files matched', String(scan.generated.size)) + '\n');
522
- process.stdout.write(kv('regen', rule.regen ?? '(header-only)') + '\n');
626
+ if (scan.slices.length > 0) {
627
+ process.stdout.write(kv('writers', String(scan.slices.length)) + '\n');
628
+ for (const sl of scan.slices) {
629
+ process.stdout.write(` [${sl.label}] ${sl.files.size} file(s) — ${sl.regen}\n`);
630
+ process.stdout.write(` owns ${sl.glob.join(', ')}\n`);
631
+ }
632
+ }
633
+ else {
634
+ process.stdout.write(kv('regen', rule.regen ?? '(header-only)') + '\n');
635
+ }
636
+ if (scan.handMaintained.length > 0) {
637
+ process.stdout.write(kv('hand-maintained', `${scan.handMaintained.length} file(s) — exempt from header + byte checks`) + '\n');
638
+ const marked = new Set(scan.markedHandMaintained);
639
+ // Say WHICH mechanism blessed each file: a config entry and an in-file
640
+ // marker are reviewed in different places, so "why is this exempt?" has
641
+ // two different answers and the reader needs to know which one applies.
642
+ for (const f of scan.handMaintained.slice(0, 20)) {
643
+ process.stdout.write(` ${f}${marked.has(f) ? ' (in-file marker)' : ' (handMaintained[])'}\n`);
644
+ }
645
+ }
646
+ if (scan.unclassified.length > 0) {
647
+ process.stdout.write(kv('unclassified', `${scan.unclassified.length} file(s) — owned by no writer, not blessed`) + '\n');
648
+ for (const f of scan.unclassified.slice(0, 20))
649
+ process.stdout.write(` ${f}\n`);
650
+ }
651
+ for (const pattern of scan.staleHandMaintained) {
652
+ process.stdout.write(` ! handMaintained "${pattern}" matches no file — stale bless\n`);
653
+ }
523
654
  process.stdout.write(kv('compare', rule.compare ?? 'bytes') + '\n');
524
655
  if (rule.provenanceHeader) {
525
656
  process.stdout.write(kv('header', `/${rule.provenanceHeader.mustMatch}/`) + '\n');
@@ -1 +1 @@
1
- {"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAiE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAgLxH,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkFtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2F1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+EpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+FtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+HtE;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAiGvE;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHpE"}
1
+ {"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAiE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAgLxH,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkFtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2F1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+EpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmGtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+HtE;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAiGvE;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHpE"}
@@ -594,7 +594,11 @@ export async function runGraphStatus(args) {
594
594
  }
595
595
  process.stdout.write(header('Graph status'));
596
596
  process.stdout.write(kv('schema', payload.schema) + '\n');
597
- process.stdout.write(kv('files', String(payload.fileCount)) + '\n');
597
+ // "files indexed", not "files": this is the size of the STORED snapshot, and
598
+ // it deliberately does not move when a new file appears on disk — that shows
599
+ // up on the `drift` line below. Reading it as a live count is how an added
600
+ // file looks invisible to the index.
601
+ process.stdout.write(kv('files indexed', String(payload.fileCount)) + '\n');
598
602
  process.stdout.write(kv('nodes', String(payload.nodeCount)) + '\n');
599
603
  process.stdout.write(kv('edges', String(payload.edgeCount)) + '\n');
600
604
  process.stdout.write(kv('packages', String(payload.workspacePackages.length)) + '\n');
@@ -1 +1 @@
1
- {"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAMhC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAoFrF;AAyZD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
1
+ {"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAMhC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAwFrF;AAyZD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
@@ -28,9 +28,10 @@ export function renderWiringExplain(report, wantJson) {
28
28
  process.stdout.write(kv('registeredMode', 'intersection (must be in EVERY sink)') + '\n');
29
29
  }
30
30
  process.stdout.write(kv('status', report.status) + '\n');
31
- process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)`) +
32
- '\n');
33
- process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)`) + '\n');
31
+ process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)` +
32
+ (report.declared.viaExtractor ? ` (via $use:${report.declared.viaExtractor})` : '')) + '\n');
33
+ process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)` +
34
+ (report.registered.viaExtractor ? ` (via $use:${report.registered.viaExtractor})` : '')) + '\n');
34
35
  if (report.declared.error)
35
36
  process.stdout.write(` ! declared side: ${report.declared.error}\n`);
36
37
  if (report.registered.error) {
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export declare const GATE_ENVELOPE_SCHEMA: "sharkcraft.gate/v1";
15
15
  /** Which data-defined plane produced a rule result. */
16
- export type GateRuleType = 'wiring' | 'policy' | 'registry' | 'registration' | 'baseline' | 'generated';
16
+ export type GateRuleType = 'wiring' | 'policy' | 'registry' | 'registration' | 'baseline' | 'generated' | 'doc-reference';
17
17
  /**
18
18
  * Per-rule outcome, uniform across planes. `skipped` is deliberately a
19
19
  * first-class status, not folded into `passed` — a rule that matched nothing
@@ -1 +1 @@
1
- {"version":3,"file":"gate-envelope.d.ts","sourceRoot":"","sources":["../../src/gates/gate-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,EAAG,oBAA6B,CAAC;AAElE,uDAAuD;AACvD,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,cAAc,GACd,UAAU,GACV,WAAW,CAAC;AAEhB;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvE,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,oBAAoB,CAAC;IAC7C,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,aAAa,CAUf"}
1
+ {"version":3,"file":"gate-envelope.d.ts","sourceRoot":"","sources":["../../src/gates/gate-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,EAAG,oBAA6B,CAAC;AAElE,uDAAuD;AACvD,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,cAAc,GACd,UAAU,GACV,WAAW,GACX,eAAe,CAAC;AAEpB;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvE,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,oBAAoB,CAAC;IAC7C,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,aAAa,CAUf"}
@@ -0,0 +1,33 @@
1
+ import { type IWiringSource } from '@shrkcrft/core';
2
+ import type { IGateRuleView } from './gate-rule-view.js';
3
+ /**
4
+ * The FOOTPRINT of a gate rule: every project-relative glob it reads.
5
+ *
6
+ * `--changed-only` is only trustworthy if the footprint is complete. A rule
7
+ * scoped by its declared side alone would go unevaluated when the change edits
8
+ * the REGISTERED side — a registration deleted in file B silently skipping the
9
+ * rule declared over file A is the precise bug this plane exists to catch, so
10
+ * every side, every hop, and every watched input counts.
11
+ */
12
+ export declare function gateRuleGlobs(view: IGateRuleView): readonly string[];
13
+ /**
14
+ * EVERY extraction source a rule reads — both wiring sides, every union sink,
15
+ * every chain hop, a registry's consumer, all three registration roles.
16
+ *
17
+ * Collecting all of them (not just the rule's primary side) is what lets the
18
+ * coverage view name every consumer of a shared extractor. A rule that `$use`s
19
+ * one on its *registered* side is just as bound to that definition as one that
20
+ * uses it on `declared`, and listing only half of them would understate exactly
21
+ * the guarantee the shared extractor exists to provide.
22
+ */
23
+ export declare function gateRuleSources(view: IGateRuleView): readonly IWiringSource[];
24
+ /**
25
+ * Whether a rule's footprint intersects the changed set.
26
+ *
27
+ * A rule with NO resolvable footprint (a command baseline with no
28
+ * `watchFiles`) cannot be proven out of scope, so it stays IN scope. Guessing
29
+ * "probably unaffected" is how a `--changed-only` run silently stops checking
30
+ * the one thing that broke.
31
+ */
32
+ export declare function ruleTouchedBy(view: IGateRuleView, changed: readonly string[]): boolean;
33
+ //# sourceMappingURL=gate-rule-globs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-rule-globs.d.ts","sourceRoot":"","sources":["../../src/gates/gate-rule-globs.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,MAAM,EAAE,CAqBpE;AAcD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,CA+B7E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAItF"}
@@ -0,0 +1,101 @@
1
+ import { resolveSourceGlobs, } from '@shrkcrft/core';
2
+ import { matchesAny } from '@shrkcrft/boundaries';
3
+ /**
4
+ * The FOOTPRINT of a gate rule: every project-relative glob it reads.
5
+ *
6
+ * `--changed-only` is only trustworthy if the footprint is complete. A rule
7
+ * scoped by its declared side alone would go unevaluated when the change edits
8
+ * the REGISTERED side — a registration deleted in file B silently skipping the
9
+ * rule declared over file A is the precise bug this plane exists to catch, so
10
+ * every side, every hop, and every watched input counts.
11
+ */
12
+ export function gateRuleGlobs(view) {
13
+ if (view.plane === 'policy')
14
+ return view.raw.files ?? [];
15
+ if (view.plane === 'baseline') {
16
+ const rule = view.raw;
17
+ // `watchFiles` is the ONLY footprint a command compute has — without it a
18
+ // command baseline can never be scoped, so it always runs (see below).
19
+ return [
20
+ ...gateRuleSources(view).flatMap(sourceGlobs),
21
+ ...(rule.watchFiles ?? []),
22
+ rule.baseline,
23
+ ];
24
+ }
25
+ if (view.plane === 'generated') {
26
+ const rule = view.raw;
27
+ return [
28
+ ...rule.generatedGlob,
29
+ ...(rule.sources ?? []).flatMap((s) => s.glob),
30
+ ...(rule.handMaintained ?? []),
31
+ ];
32
+ }
33
+ return gateRuleSources(view).flatMap(sourceGlobs);
34
+ }
35
+ /**
36
+ * Every glob one source reads: the files it scans, plus an `import-edges`
37
+ * source's TARGET globs.
38
+ *
39
+ * A fence whose footprint covered only the consumer side would go unevaluated
40
+ * when the target subtree moved — and "cannot be proven out of scope" must
41
+ * always resolve to staying IN scope, never to a quiet skip.
42
+ */
43
+ function sourceGlobs(src) {
44
+ return [...resolveSourceGlobs(src), ...(src.to?.files ?? [])];
45
+ }
46
+ /**
47
+ * EVERY extraction source a rule reads — both wiring sides, every union sink,
48
+ * every chain hop, a registry's consumer, all three registration roles.
49
+ *
50
+ * Collecting all of them (not just the rule's primary side) is what lets the
51
+ * coverage view name every consumer of a shared extractor. A rule that `$use`s
52
+ * one on its *registered* side is just as bound to that definition as one that
53
+ * uses it on `declared`, and listing only half of them would understate exactly
54
+ * the guarantee the shared extractor exists to provide.
55
+ */
56
+ export function gateRuleSources(view) {
57
+ switch (view.plane) {
58
+ case 'wiring': {
59
+ const rule = view.raw;
60
+ return [
61
+ rule.declared,
62
+ ...(Array.isArray(rule.registered)
63
+ ? rule.registered
64
+ : rule.registered
65
+ ? [rule.registered]
66
+ : []),
67
+ ...(rule.chain ?? []),
68
+ ].filter((s) => s !== undefined);
69
+ }
70
+ case 'registry': {
71
+ const decl = view.raw;
72
+ return decl.consumer ? [decl.source, decl.consumer] : [decl.source];
73
+ }
74
+ case 'registration': {
75
+ const idiom = view.raw;
76
+ return [idiom.declared, idiom.provided, idiom.consumed];
77
+ }
78
+ case 'baseline': {
79
+ const source = view.raw.compute.source;
80
+ return source ? [source] : [];
81
+ }
82
+ default:
83
+ // `policy` and `generated` select files by glob, not by an extraction
84
+ // source, so neither can reference a shared extractor.
85
+ return [];
86
+ }
87
+ }
88
+ /**
89
+ * Whether a rule's footprint intersects the changed set.
90
+ *
91
+ * A rule with NO resolvable footprint (a command baseline with no
92
+ * `watchFiles`) cannot be proven out of scope, so it stays IN scope. Guessing
93
+ * "probably unaffected" is how a `--changed-only` run silently stops checking
94
+ * the one thing that broke.
95
+ */
96
+ export function ruleTouchedBy(view, changed) {
97
+ const globs = gateRuleGlobs(view).filter((g) => g.length > 0);
98
+ if (globs.length === 0)
99
+ return true;
100
+ return changed.some((file) => matchesAny(file, globs));
101
+ }