@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.
- package/dist/commands/baseline.command.d.ts +25 -0
- package/dist/commands/baseline.command.d.ts.map +1 -1
- package/dist/commands/baseline.command.js +23 -7
- package/dist/commands/changelog-data.d.ts.map +1 -1
- package/dist/commands/changelog-data.js +26 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +19 -2
- package/dist/commands/code-intel.command.d.ts.map +1 -1
- package/dist/commands/code-intel.command.js +5 -1
- package/dist/commands/docs-references.command.d.ts +7 -0
- package/dist/commands/docs-references.command.d.ts.map +1 -0
- package/dist/commands/docs-references.command.js +323 -0
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +3 -2
- package/dist/commands/gates.command.d.ts +2 -0
- package/dist/commands/gates.command.d.ts.map +1 -1
- package/dist/commands/gates.command.js +640 -16
- package/dist/commands/generated.command.d.ts +32 -0
- package/dist/commands/generated.command.d.ts.map +1 -1
- package/dist/commands/generated.command.js +180 -49
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +5 -1
- package/dist/commands/wiring.command.d.ts.map +1 -1
- package/dist/commands/wiring.command.js +4 -3
- package/dist/gates/gate-envelope.d.ts +1 -1
- package/dist/gates/gate-envelope.d.ts.map +1 -1
- package/dist/gates/gate-rule-globs.d.ts +33 -0
- package/dist/gates/gate-rule-globs.d.ts.map +1 -0
- package/dist/gates/gate-rule-globs.js +101 -0
- package/dist/gates/gate-rule-view.d.ts +9 -3
- package/dist/gates/gate-rule-view.d.ts.map +1 -1
- package/dist/gates/gate-rule-view.js +13 -0
- package/dist/gates/rule-coverage.d.ts +39 -1
- package/dist/gates/rule-coverage.d.ts.map +1 -1
- package/dist/gates/rule-coverage.js +123 -8
- package/dist/gates/run-gate-planes.d.ts +44 -0
- package/dist/gates/run-gate-planes.d.ts.map +1 -0
- package/dist/gates/run-gate-planes.js +261 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -1
- 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":"
|
|
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
|
-
/**
|
|
131
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 =
|
|
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:
|
|
259
|
+
driftChecked: units.length > 0 && errors.length === 0,
|
|
260
|
+
handMaintained: scan.handMaintained,
|
|
261
|
+
writers: writers.length > 1 ? writers : [],
|
|
208
262
|
};
|
|
209
263
|
}
|
|
210
|
-
function
|
|
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:
|
|
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
|
-
|
|
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) =>
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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(` → ${
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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)`
|
|
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;
|
|
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
|
+
}
|