@shrkcrft/boundaries 0.1.0-alpha.28 → 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 (51) hide show
  1. package/dist/baseline/compute-baseline.d.ts.map +1 -1
  2. package/dist/baseline/compute-baseline.js +2 -1
  3. package/dist/extract/extract-tokens.d.ts +19 -1
  4. package/dist/extract/extract-tokens.d.ts.map +1 -1
  5. package/dist/extract/extract-tokens.js +53 -1
  6. package/dist/extract/import-edges.d.ts +49 -0
  7. package/dist/extract/import-edges.d.ts.map +1 -0
  8. package/dist/extract/import-edges.js +157 -0
  9. package/dist/extract/inspect-source.d.ts +6 -0
  10. package/dist/extract/inspect-source.d.ts.map +1 -1
  11. package/dist/extract/inspect-source.js +6 -1
  12. package/dist/extract/parse-imports.d.ts +58 -0
  13. package/dist/extract/parse-imports.d.ts.map +1 -0
  14. package/dist/extract/parse-imports.js +155 -0
  15. package/dist/generated/check-provenance.d.ts +20 -2
  16. package/dist/generated/check-provenance.d.ts.map +1 -1
  17. package/dist/generated/check-provenance.js +24 -5
  18. package/dist/generated/scan-generated.d.ts +49 -3
  19. package/dist/generated/scan-generated.d.ts.map +1 -1
  20. package/dist/generated/scan-generated.js +90 -5
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -0
  24. package/dist/policy/evaluate-policy.d.ts +1 -1
  25. package/dist/policy/evaluate-policy.d.ts.map +1 -1
  26. package/dist/policy/evaluate-policy.js +2 -1
  27. package/dist/util/walk-files.d.ts +21 -2
  28. package/dist/util/walk-files.d.ts.map +1 -1
  29. package/dist/util/walk-files.js +85 -4
  30. package/dist/wiring/evaluate-wiring.d.ts +10 -3
  31. package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
  32. package/dist/wiring/evaluate-wiring.js +12 -7
  33. package/dist/wiring/explain-wiring.d.ts +7 -9
  34. package/dist/wiring/explain-wiring.d.ts.map +1 -1
  35. package/dist/wiring/explain-wiring.js +18 -0
  36. package/dist/wiring/plan-wiring-fix.d.ts +75 -0
  37. package/dist/wiring/plan-wiring-fix.d.ts.map +1 -0
  38. package/dist/wiring/plan-wiring-fix.js +302 -0
  39. package/dist/wiring/registration-graph.d.ts +1 -1
  40. package/dist/wiring/registration-graph.d.ts.map +1 -1
  41. package/dist/wiring/registration-graph.js +8 -3
  42. package/dist/wiring/registry-query.d.ts +1 -1
  43. package/dist/wiring/registry-query.d.ts.map +1 -1
  44. package/dist/wiring/registry-query.js +7 -3
  45. package/dist/wiring/scan-wiring-files.d.ts +1 -1
  46. package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
  47. package/dist/wiring/scan-wiring-files.js +5 -1
  48. package/dist/wiring/sink-imports.d.ts +84 -0
  49. package/dist/wiring/sink-imports.d.ts.map +1 -0
  50. package/dist/wiring/sink-imports.js +115 -0
  51. package/package.json +2 -2
@@ -6,7 +6,11 @@ export type ProvenanceFindingKind =
6
6
  /** A file OUTSIDE the glob carries the header — a hand-written file mislabeled. */
7
7
  | 'mislabeled'
8
8
  /** Advisory: the header does not name how to regenerate the file. */
9
- | 'no-regen-pointer';
9
+ | 'no-regen-pointer'
10
+ /** In the tree, owned by no writer and not blessed as hand-maintained. */
11
+ | 'unclassified'
12
+ /** A `handMaintained` bless whose file no longer exists. */
13
+ | 'stale-hand-maintained';
10
14
  export interface IProvenanceFinding {
11
15
  readonly ruleId: string;
12
16
  readonly file: string;
@@ -20,6 +24,20 @@ export interface IProvenanceFinding {
20
24
  * deterministic — never a whole-tree read of every file type.
21
25
  */
22
26
  export declare function deriveOutsideGlobs(generatedGlob: readonly string[]): string[];
27
+ /**
28
+ * Tree-classification results a mixed-tree rule produces alongside its files.
29
+ *
30
+ * Passed in rather than recomputed so the classification a caller already did
31
+ * (see `scanGeneratedFiles`) is the same one reported — two implementations of
32
+ * "which files does this rule own?" is precisely the drift this plane exists
33
+ * to catch.
34
+ */
35
+ export interface IClassificationFindings {
36
+ /** Files under the tree owned by no writer and not blessed. */
37
+ readonly unclassified?: readonly string[];
38
+ /** `handMaintained` patterns currently matching no file. */
39
+ readonly staleHandMaintained?: readonly string[];
40
+ }
23
41
  /**
24
42
  * Check the "this file is generated" header contract.
25
43
  *
@@ -30,7 +48,7 @@ export declare function deriveOutsideGlobs(generatedGlob: readonly string[]): st
30
48
  * `outside` should contain candidate files NOT in `generatedGlob`; pass an empty
31
49
  * map when `forbidOutside` is off.
32
50
  */
33
- export declare function checkProvenanceHeaders(rule: IGeneratedArtifactRule, generated: ReadonlyMap<string, string>, outside: ReadonlyMap<string, string>): {
51
+ export declare function checkProvenanceHeaders(rule: IGeneratedArtifactRule, generated: ReadonlyMap<string, string>, outside: ReadonlyMap<string, string>, classification?: IClassificationFindings): {
34
52
  findings: readonly IProvenanceFinding[];
35
53
  error?: string;
36
54
  };
@@ -1 +1 @@
1
- {"version":3,"file":"check-provenance.d.ts","sourceRoot":"","sources":["../../src/generated/check-provenance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAG7D,0CAA0C;AAC1C,MAAM,MAAM,qBAAqB;AAC/B,qEAAqE;AACnE,gBAAgB;AAClB,mFAAmF;GACjF,YAAY;AACd,qEAAqE;GACnE,kBAAkB,CAAC;AAEvB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAO7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,sBAAsB,EAC5B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC;IAAE,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA2D7D"}
1
+ {"version":3,"file":"check-provenance.d.ts","sourceRoot":"","sources":["../../src/generated/check-provenance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAG7D,0CAA0C;AAC1C,MAAM,MAAM,qBAAqB;AAC/B,qEAAqE;AACnE,gBAAgB;AAClB,mFAAmF;GACjF,YAAY;AACd,qEAAqE;GACnE,kBAAkB;AACpB,0EAA0E;GACxE,cAAc;AAChB,4DAA4D;GAC1D,uBAAuB,CAAC;AAE5B,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAO7E;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,4DAA4D;IAC5D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClD;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,sBAAsB,EAC5B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,cAAc,GAAE,uBAA4B,GAC3C;IAAE,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAgF7D"}
@@ -28,16 +28,35 @@ export function deriveOutsideGlobs(generatedGlob) {
28
28
  * `outside` should contain candidate files NOT in `generatedGlob`; pass an empty
29
29
  * map when `forbidOutside` is off.
30
30
  */
31
- export function checkProvenanceHeaders(rule, generated, outside) {
31
+ export function checkProvenanceHeaders(rule, generated, outside, classification = {}) {
32
+ const severity = rule.severity ?? 'error';
33
+ // Classification findings do not depend on the header contract — a mixed tree
34
+ // must be fully accounted for whether or not the rule asserts headers, so
35
+ // these are produced before the early return below.
36
+ const classFindings = [
37
+ ...(classification.unclassified ?? []).map((file) => ({
38
+ ruleId: rule.id,
39
+ file,
40
+ kind: 'unclassified',
41
+ severity,
42
+ message: 'in the generated tree but owned by no `sources[]` glob and not listed in `handMaintained` — classify it',
43
+ })),
44
+ ...(classification.staleHandMaintained ?? []).map((pattern) => ({
45
+ ruleId: rule.id,
46
+ file: pattern,
47
+ kind: 'stale-hand-maintained',
48
+ severity: 'warning',
49
+ message: '`handMaintained` entry matches no file — the blessed file was renamed or deleted',
50
+ })),
51
+ ];
32
52
  const header = rule.provenanceHeader;
33
53
  if (!header)
34
- return { findings: [] };
54
+ return { findings: classFindings };
35
55
  const { re, error } = safeCompile(header.mustMatch, header.flags);
36
56
  if (error || !re)
37
- return { findings: [], error: `provenanceHeader ${error}` };
38
- const severity = rule.severity ?? 'error';
57
+ return { findings: classFindings, error: `provenanceHeader ${error}` };
39
58
  const within = header.withinLines ?? 10;
40
- const findings = [];
59
+ const findings = [...classFindings];
41
60
  const test = (content) => {
42
61
  re.lastIndex = 0;
43
62
  return re.test(headOf(content, within));
@@ -1,17 +1,63 @@
1
1
  import type { IGeneratedArtifactRule } from '@shrkcrft/core';
2
+ /** One writer's slice of a mixed generated tree. */
3
+ export interface IGeneratedSourceSlice {
4
+ /** Label for output — the writer's `id`, or `source[<index>]`. */
5
+ readonly label: string;
6
+ /** The regen command that owns this slice. */
7
+ readonly regen: string;
8
+ /** Globs selecting the committed files this writer owns. */
9
+ readonly glob: readonly string[];
10
+ /** Committed files matching this writer's glob (path → content). */
11
+ readonly files: ReadonlyMap<string, string>;
12
+ readonly timeoutMs?: number;
13
+ }
2
14
  /** The committed generated files plus the mislabel-candidate set. */
3
15
  export interface IGeneratedScan {
4
16
  /** Files matching `generatedGlob` (project-relative path → content). */
5
17
  readonly generated: ReadonlyMap<string, string>;
18
+ /**
19
+ * The subset of {@link generated} that is checkable — i.e. everything except
20
+ * the hand-maintained blesses. Header and byte checks run over this.
21
+ */
22
+ readonly checkable: ReadonlyMap<string, string>;
23
+ /** Files excluded by `handMaintained` or by the in-file marker, sorted. */
24
+ readonly handMaintained: readonly string[];
25
+ /** The subset of {@link handMaintained} that declared itself via the marker. */
26
+ readonly markedHandMaintained: readonly string[];
27
+ /**
28
+ * `handMaintained` patterns that currently match NO file. A bless whose file
29
+ * was renamed or deleted is a stale exemption: it silently widens nothing
30
+ * today, but it also documents a file that no longer exists, and the next
31
+ * file to take that name inherits an exemption nobody reviewed.
32
+ */
33
+ readonly staleHandMaintained: readonly string[];
34
+ /**
35
+ * Files under `generatedGlob` owned by NO writer and not blessed. Only
36
+ * populated for multi-writer rules, where the writers partition the tree —
37
+ * a single-writer rule owns everything its glob matches by definition.
38
+ */
39
+ readonly unclassified: readonly string[];
40
+ /** Per-writer slices for a multi-writer rule; empty for a single-writer one. */
41
+ readonly slices: readonly IGeneratedSourceSlice[];
6
42
  /** Candidates for the `forbidOutside` mislabel check (empty when it is off). */
7
43
  readonly outside: ReadonlyMap<string, string>;
8
44
  /** Globs actually used for the outside scan (derived or configured). */
9
45
  readonly outsideGlobs: readonly string[];
10
46
  }
11
47
  /**
12
- * Read the committed side of a generated-artifact rule. Pure filesystem reads —
13
- * the regen command (if any) is orchestrated by the caller, which is also where
14
- * the local-config-only trust decision is enforced.
48
+ * Read + CLASSIFY the committed side of a generated-artifact rule.
49
+ *
50
+ * Classification is the part that makes this usable on a real tree. A blanket
51
+ * "everything under this glob is generated by that one command" holds only in a
52
+ * demo; a real `generated/` dir mixes several writers' output with files that
53
+ * are still hand-maintained. Partitioning the tree first means each writer is
54
+ * compared against its OWN slice (no cross-writer `only-committed` noise), the
55
+ * hand-written files are exempted from the header contract instead of failing
56
+ * it, and anything belonging to neither is surfaced as `unclassified` rather
57
+ * than silently assumed.
58
+ *
59
+ * Pure filesystem reads — the regen commands (if any) are orchestrated by the
60
+ * caller, which is also where the local-config-only trust decision is enforced.
15
61
  */
16
62
  export declare function scanGeneratedFiles(projectRoot: string, rule: IGeneratedArtifactRule, excludeDirs?: readonly string[]): IGeneratedScan;
17
63
  //# sourceMappingURL=scan-generated.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scan-generated.d.ts","sourceRoot":"","sources":["../../src/generated/scan-generated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAK7D,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,sBAAsB,EAC5B,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,cAAc,CAmBhB"}
1
+ {"version":3,"file":"scan-generated.d.ts","sourceRoot":"","sources":["../../src/generated/scan-generated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAM7D,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAClD,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,sBAAsB,EAC5B,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,cAAc,CAiGhB"}
@@ -1,17 +1,92 @@
1
1
  import { matchesAny } from "../scan/glob.js";
2
+ import { safeCompile } from "../util/safe-regex.js";
2
3
  import { readMatchingFiles } from "../util/walk-files.js";
3
4
  import { deriveOutsideGlobs } from "./check-provenance.js";
4
5
  /**
5
- * Read the committed side of a generated-artifact rule. Pure filesystem reads —
6
- * the regen command (if any) is orchestrated by the caller, which is also where
7
- * the local-config-only trust decision is enforced.
6
+ * Read + CLASSIFY the committed side of a generated-artifact rule.
7
+ *
8
+ * Classification is the part that makes this usable on a real tree. A blanket
9
+ * "everything under this glob is generated by that one command" holds only in a
10
+ * demo; a real `generated/` dir mixes several writers' output with files that
11
+ * are still hand-maintained. Partitioning the tree first means each writer is
12
+ * compared against its OWN slice (no cross-writer `only-committed` noise), the
13
+ * hand-written files are exempted from the header contract instead of failing
14
+ * it, and anything belonging to neither is surfaced as `unclassified` rather
15
+ * than silently assumed.
16
+ *
17
+ * Pure filesystem reads — the regen commands (if any) are orchestrated by the
18
+ * caller, which is also where the local-config-only trust decision is enforced.
8
19
  */
9
20
  export function scanGeneratedFiles(projectRoot, rule, excludeDirs = []) {
10
21
  const exclude = new Set(excludeDirs);
11
22
  const generated = readMatchingFiles(projectRoot, rule.generatedGlob, exclude);
23
+ const blessPatterns = rule.handMaintained ?? [];
24
+ // The in-file marker is read from the same head window the header contract
25
+ // uses, so "the top of the file" means one thing for both classifications.
26
+ const markerWindow = rule.provenanceHeader?.withinLines ?? 10;
27
+ const marker = rule.handMaintainedMarker
28
+ ? safeCompile(rule.handMaintainedMarker, rule.handMaintainedMarkerFlags).re
29
+ : undefined;
30
+ const carriesMarker = (content) => {
31
+ if (!marker)
32
+ return false;
33
+ marker.lastIndex = 0;
34
+ return marker.test(content.split('\n').slice(0, Math.max(1, markerWindow)).join('\n'));
35
+ };
36
+ const handMaintained = [];
37
+ const markedHandMaintained = [];
38
+ const checkable = new Map();
39
+ for (const [path, content] of generated) {
40
+ if (blessPatterns.length > 0 && matchesAny(path, blessPatterns)) {
41
+ handMaintained.push(path);
42
+ continue;
43
+ }
44
+ if (carriesMarker(content)) {
45
+ handMaintained.push(path);
46
+ markedHandMaintained.push(path);
47
+ continue;
48
+ }
49
+ checkable.set(path, content);
50
+ }
51
+ handMaintained.sort();
52
+ markedHandMaintained.sort();
53
+ // A bless is pinned to a NAMED file (the schema forbids a wildcard basename),
54
+ // so one that matches nothing points at a file that has been renamed away.
55
+ const staleHandMaintained = blessPatterns
56
+ .filter((pattern) => ![...generated.keys()].some((p) => matchesAny(p, [pattern])))
57
+ .sort();
58
+ const slices = (rule.sources ?? []).map((src, i) => {
59
+ const files = new Map();
60
+ for (const [path, content] of checkable) {
61
+ if (matchesAny(path, src.glob))
62
+ files.set(path, content);
63
+ }
64
+ return {
65
+ label: src.id ?? `source[${i}]`,
66
+ regen: src.regen,
67
+ glob: src.glob,
68
+ files,
69
+ ...(src.timeoutMs !== undefined ? { timeoutMs: src.timeoutMs } : {}),
70
+ };
71
+ });
72
+ // Only a multi-writer rule PARTITIONS the tree; a single-writer rule owns
73
+ // everything its glob matched, so nothing there can be unclassified.
74
+ const unclassified = slices.length === 0
75
+ ? []
76
+ : [...checkable.keys()].filter((p) => !slices.some((s) => matchesAny(p, s.glob))).sort();
12
77
  const wantOutside = rule.provenanceHeader?.forbidOutside === true;
13
78
  if (!wantOutside) {
14
- return { generated, outside: new Map(), outsideGlobs: [] };
79
+ return {
80
+ generated,
81
+ checkable,
82
+ handMaintained,
83
+ markedHandMaintained,
84
+ staleHandMaintained,
85
+ unclassified,
86
+ slices,
87
+ outside: new Map(),
88
+ outsideGlobs: [],
89
+ };
15
90
  }
16
91
  const outsideGlobs = rule.provenanceHeader?.outsideGlob && rule.provenanceHeader.outsideGlob.length > 0
17
92
  ? [...rule.provenanceHeader.outsideGlob]
@@ -23,5 +98,15 @@ export function scanGeneratedFiles(projectRoot, rule, excludeDirs = []) {
23
98
  continue;
24
99
  outside.set(path, content);
25
100
  }
26
- return { generated, outside, outsideGlobs };
101
+ return {
102
+ generated,
103
+ checkable,
104
+ handMaintained,
105
+ markedHandMaintained,
106
+ staleHandMaintained,
107
+ unclassified,
108
+ slices,
109
+ outside,
110
+ outsideGlobs,
111
+ };
27
112
  }
package/dist/index.d.ts CHANGED
@@ -11,11 +11,15 @@ export * from './wiring/explain-wiring.js';
11
11
  export * from './wiring/registry-query.js';
12
12
  export * from './wiring/registration-graph.js';
13
13
  export * from './wiring/trace-literal.js';
14
+ export * from './wiring/plan-wiring-fix.js';
15
+ export * from './wiring/sink-imports.js';
14
16
  export * from './policy/extract-templates.js';
15
17
  export * from './policy/evaluate-policy.js';
16
18
  export * from './policy/run-policy.js';
17
19
  export * from './extract/scan-literals.js';
18
20
  export * from './extract/extract-tokens.js';
21
+ export * from './extract/parse-imports.js';
22
+ export * from './extract/import-edges.js';
19
23
  export * from './extract/code-zones.js';
20
24
  export * from './extract/inspect-source.js';
21
25
  export * from './baseline/canonicalize.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -11,11 +11,15 @@ export * from "./wiring/explain-wiring.js";
11
11
  export * from "./wiring/registry-query.js";
12
12
  export * from "./wiring/registration-graph.js";
13
13
  export * from "./wiring/trace-literal.js";
14
+ export * from "./wiring/plan-wiring-fix.js";
15
+ export * from "./wiring/sink-imports.js";
14
16
  export * from "./policy/extract-templates.js";
15
17
  export * from "./policy/evaluate-policy.js";
16
18
  export * from "./policy/run-policy.js";
17
19
  export * from "./extract/scan-literals.js";
18
20
  export * from "./extract/extract-tokens.js";
21
+ export * from "./extract/parse-imports.js";
22
+ export * from "./extract/import-edges.js";
19
23
  export * from "./extract/code-zones.js";
20
24
  export * from "./extract/inspect-source.js";
21
25
  export * from "./baseline/canonicalize.js";
@@ -1,4 +1,4 @@
1
- import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
1
+ import { type IPolicyRule, type PolicySurface } from '@shrkcrft/core';
2
2
  export declare const POLICY_LINT_SCHEMA: "sharkcraft.policy-lint/v1";
3
3
  /**
4
4
  * A chunk of content to scan for one rule. For whole files `baseLine` is 1; for
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAAuB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAI3G,eAAO,MAAM,kBAAkB,EAAG,2BAAoC,CAAC;AAEvE;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,oFAAoF;AACpF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,GAAG,UAAU,CAAC;CAC1D;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,kBAAkB,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,qEAAqE;IACrE,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;CAClD;AAED,6FAA6F;AAC7F,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,WAAW,EAAE,CAAC;AA0C/E;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,aAAa,CAyJxG"}
@@ -1,3 +1,4 @@
1
+ import { failsWhenEmpty } from '@shrkcrft/core';
1
2
  import { lexCodeZones, zoneAt } from "../extract/code-zones.js";
2
3
  import { safeCompile } from "../util/safe-regex.js";
3
4
  export const POLICY_LINT_SCHEMA = 'sharkcraft.policy-lint/v1';
@@ -152,7 +153,7 @@ export function evaluatePolicy(rules, resolve) {
152
153
  continue;
153
154
  }
154
155
  if (units.length === 0) {
155
- const failed = rule.failOnEmpty === true;
156
+ const failed = failsWhenEmpty(rule);
156
157
  skipped.push({
157
158
  ruleId: rule.id,
158
159
  reason: '0 content units matched the rule globs',
@@ -7,7 +7,26 @@ export declare const MAX_SCAN_FILE_BYTES = 1000000;
7
7
  * `excludeDirs` is a set of project-relative POSIX directory paths to prune
8
8
  * entirely (e.g. the SharkCraft asset/config dir).
9
9
  */
10
- export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): string[];
10
+ export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): string[];
11
+ /**
12
+ * Run `fn` with the read memo enabled, then clear it.
13
+ *
14
+ * ONLY wrap a scan that performs no writes and spawns no commands for its
15
+ * duration — within such a window the tree cannot change beneath the memo, so
16
+ * reuse is exact rather than merely probable.
17
+ */
18
+ export declare function withFileReadCache<T>(fn: () => T): T;
19
+ /** Drop every memoized read. For tests, and for a caller that has just written. */
20
+ export declare function clearFileReadCache(): void;
21
+ /**
22
+ * The dot-directory segments a glob set explicitly names.
23
+ *
24
+ * `.claude/skills/**` asks for `.claude`; `docs/**` asks for nothing. Deriving
25
+ * the allowlist from the globs themselves means a rule gets exactly the
26
+ * directories it named and no others — a blanket "scan dot-dirs" switch would
27
+ * wander into `.venv` and `.yarn` the moment someone wrote a recursive glob.
28
+ */
29
+ export declare function dotDirsNamedBy(globs: readonly string[]): Set<string>;
11
30
  /** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
12
- export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): Map<string, string>;
31
+ export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): Map<string, string>;
13
32
  //# sourceMappingURL=walk-files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"walk-files.d.ts","sourceRoot":"","sources":["../../src/util/walk-files.ts"],"names":[],"mappings":"AAIA,+CAA+C;AAC/C,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAUxC,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,UAAY,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,GAC3C,MAAM,EAAE,CA2BV;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,GAC3C,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrB"}
1
+ {"version":3,"file":"walk-files.d.ts","sourceRoot":"","sources":["../../src/util/walk-files.ts"],"names":[],"mappings":"AAIA,+CAA+C;AAC/C,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAUxC,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,UAAY,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,MAAM,EAAE,CAgCV;AAuBD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CASnD;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAGD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAUpE;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgCrB"}
@@ -20,7 +20,7 @@ export const MAX_SCAN_FILE_BYTES = 1_000_000;
20
20
  * `excludeDirs` is a set of project-relative POSIX directory paths to prune
21
21
  * entirely (e.g. the SharkCraft asset/config dir).
22
22
  */
23
- export function walkMatching(root, globs, excludeDirs = new Set()) {
23
+ export function walkMatching(root, globs, excludeDirs = new Set(), allowDotDirs = new Set()) {
24
24
  const out = [];
25
25
  const visit = (abs) => {
26
26
  let entries;
@@ -39,7 +39,13 @@ export function walkMatching(root, globs, excludeDirs = new Set()) {
39
39
  // so neither policy-lint nor wiring scans tooling/vendored sources.
40
40
  // (Dirent.isDirectory() is false for symlinks, so symlinked dirs are
41
41
  // never descended — no loop risk.)
42
- if (e.name.startsWith('.') || SKIP_DIRS.has(e.name) || excludeDirs.has(rel))
42
+ // Dot-directories are vendored tooling by default (`.venv`, `.yarn`,
43
+ // `.gradle`). But a rule may legitimately target one — an agent skill
44
+ // file lives in `.claude/skills` — so a caller can name the dot-dirs
45
+ // its OWN globs ask for. `SKIP_DIRS` stays absolute either way.
46
+ if (SKIP_DIRS.has(e.name) || excludeDirs.has(rel))
47
+ continue;
48
+ if (e.name.startsWith('.') && !allowDotDirs.has(e.name))
43
49
  continue;
44
50
  visit(childAbs);
45
51
  }
@@ -52,10 +58,83 @@ export function walkMatching(root, globs, excludeDirs = new Set()) {
52
58
  visit(root);
53
59
  return out;
54
60
  }
61
+ /**
62
+ * Memo of `readMatchingFiles`, keyed by (root, glob set, excludes).
63
+ *
64
+ * One coverage run resolves the SAME globs many times over: every rule on every
65
+ * plane walks the tree for its own sources, and a shared `$use` extractor is by
66
+ * construction read once per consumer. The walk + read is roughly half the cost
67
+ * of a wide extraction, so memoizing turns N walks into one.
68
+ *
69
+ * It is OFF by default and enabled only by {@link withFileReadCache}, around a
70
+ * scan that provably neither spawns nor writes. That restriction is not
71
+ * caution for its own sake — a global memo really does hand back a stale
72
+ * snapshot when the same process writes a file between two scans, and a trust
73
+ * tool answering "nothing drifted" from a stale read is worse than a slow one.
74
+ * Making the safe window explicit means the condition cannot be forgotten at a
75
+ * call site.
76
+ */
77
+ const READ_MEMO = new Map();
78
+ /** True only inside {@link withFileReadCache}. */
79
+ let memoEnabled = false;
80
+ /**
81
+ * Run `fn` with the read memo enabled, then clear it.
82
+ *
83
+ * ONLY wrap a scan that performs no writes and spawns no commands for its
84
+ * duration — within such a window the tree cannot change beneath the memo, so
85
+ * reuse is exact rather than merely probable.
86
+ */
87
+ export function withFileReadCache(fn) {
88
+ const previous = memoEnabled;
89
+ memoEnabled = true;
90
+ try {
91
+ return fn();
92
+ }
93
+ finally {
94
+ memoEnabled = previous;
95
+ if (!previous)
96
+ READ_MEMO.clear();
97
+ }
98
+ }
99
+ /** Drop every memoized read. For tests, and for a caller that has just written. */
100
+ export function clearFileReadCache() {
101
+ READ_MEMO.clear();
102
+ }
103
+ /**
104
+ * The dot-directory segments a glob set explicitly names.
105
+ *
106
+ * `.claude/skills/**` asks for `.claude`; `docs/**` asks for nothing. Deriving
107
+ * the allowlist from the globs themselves means a rule gets exactly the
108
+ * directories it named and no others — a blanket "scan dot-dirs" switch would
109
+ * wander into `.venv` and `.yarn` the moment someone wrote a recursive glob.
110
+ */
111
+ export function dotDirsNamedBy(globs) {
112
+ const out = new Set();
113
+ for (const glob of globs) {
114
+ for (const segment of glob.split('/')) {
115
+ if (segment.startsWith('.') && segment.length > 1 && !segment.includes('*') && !segment.includes('?')) {
116
+ out.add(segment);
117
+ }
118
+ }
119
+ }
120
+ return out;
121
+ }
55
122
  /** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
56
- export function readMatchingFiles(root, globs, excludeDirs = new Set()) {
123
+ export function readMatchingFiles(root, globs, excludeDirs = new Set(), allowDotDirs = new Set()) {
124
+ const key = memoEnabled
125
+ ? `${root}\u0000${[...globs].sort().join('\u0001')}\u0000${[...excludeDirs].sort().join('\u0001')}` +
126
+ `\u0000${[...allowDotDirs].sort().join('\u0001')}`
127
+ : undefined;
128
+ if (key !== undefined) {
129
+ const hit = READ_MEMO.get(key);
130
+ // Hand back a COPY: callers routinely mutate the map they get (the wiring
131
+ // scan filters it, the generated scan partitions it), and a shared instance
132
+ // would let one rule's bookkeeping corrupt the next rule's inputs.
133
+ if (hit)
134
+ return new Map(hit);
135
+ }
57
136
  const out = new Map();
58
- for (const rel of walkMatching(root, globs, excludeDirs)) {
137
+ for (const rel of walkMatching(root, globs, excludeDirs, allowDotDirs)) {
59
138
  const abs = nodePath.join(root, rel);
60
139
  let size = -1;
61
140
  try {
@@ -76,5 +155,7 @@ export function readMatchingFiles(root, globs, excludeDirs = new Set()) {
76
155
  // unreadable — skip
77
156
  }
78
157
  }
158
+ if (key !== undefined)
159
+ READ_MEMO.set(key, new Map(out));
79
160
  return out;
80
161
  }
@@ -1,5 +1,5 @@
1
1
  import { type IWiringRule, type IWiringSource } from '@shrkcrft/core';
2
- import { type IExtractFileEntry, type IExtractedSite } from '../extract/extract-tokens.js';
2
+ import { type IExtractContext, type IExtractFileEntry, type IExtractedSite } from '../extract/extract-tokens.js';
3
3
  export declare const WIRING_SCHEMA: "sharkcraft.wiring/v1";
4
4
  /** A file made available to the engine. */
5
5
  export type IWiringFileEntry = IExtractFileEntry;
@@ -68,6 +68,13 @@ export interface IWiringRuleResult {
68
68
  * silently reported as N unrelated violations.
69
69
  */
70
70
  readonly emptySink?: boolean;
71
+ /**
72
+ * A diagnosis for a side that extracted nothing for a knowable reason — e.g.
73
+ * an `import-edges` sink targeting `to.files` against barrel imports. Carried
74
+ * so the empty-sink message can explain itself instead of sending the reader
75
+ * to check a glob that is fine.
76
+ */
77
+ readonly sinkHint?: string;
71
78
  /** Per-hop breakdown for a `chain` rule. */
72
79
  readonly hops?: readonly IWiringHopResult[];
73
80
  }
@@ -107,7 +114,7 @@ export declare function wiringGlobsOf(rule: IWiringRule): string[];
107
114
  * extractor-backed baseline compute, so all three honour identical semantics. A
108
115
  * misconfigured source returns an `error` and no sites (never throws).
109
116
  */
110
- export declare function collectSourceSites(source: IWiringSource, files: readonly IWiringFileEntry[]): {
117
+ export declare function collectSourceSites(source: IWiringSource, files: readonly IWiringFileEntry[], context?: IExtractContext): {
111
118
  sites: readonly IWiringTokenSite[];
112
119
  error?: string;
113
120
  };
@@ -128,5 +135,5 @@ export declare function validateWiringRule(rule: IWiringRule): string | undefine
128
135
  * The `resolve` callback supplies the files for a given rule-side (injected so
129
136
  * the engine stays pure / testable — see `runWiring` for the fs-backed wiring).
130
137
  */
131
- export declare function evaluateWiring(rules: readonly IWiringRule[], resolve: WiringFileResolver): IWiringReport;
138
+ export declare function evaluateWiring(rules: readonly IWiringRule[], resolve: WiringFileResolver, context?: IExtractContext): IWiringReport;
132
139
  //# sourceMappingURL=evaluate-wiring.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate-wiring.d.ts","sourceRoot":"","sources":["../../src/wiring/evaluate-wiring.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"evaluate-wiring.d.ts","sourceRoot":"","sources":["../../src/wiring/evaluate-wiring.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { validateWiringSource, } from '@shrkcrft/core';
1
+ import { failsWhenEmpty, validateWiringSource, } from '@shrkcrft/core';
2
2
  import { extractTokens, } from "../extract/extract-tokens.js";
3
3
  export const WIRING_SCHEMA = 'sharkcraft.wiring/v1';
4
4
  /** Normalize the `registered` field (single source or union array) to an array. */
@@ -54,8 +54,8 @@ function hopPairs(rule) {
54
54
  * extractor-backed baseline compute, so all three honour identical semantics. A
55
55
  * misconfigured source returns an `error` and no sites (never throws).
56
56
  */
57
- export function collectSourceSites(source, files) {
58
- const res = extractTokens(source, files);
57
+ export function collectSourceSites(source, files, context = {}) {
58
+ const res = extractTokens(source, files, context);
59
59
  return res.error ? { sites: [], error: res.error } : { sites: res.sites };
60
60
  }
61
61
  /** Group key for a token site under `groupBy` (dir = dirname; package = first two segments). */
@@ -133,7 +133,7 @@ export function validateWiringRule(rule) {
133
133
  * The `resolve` callback supplies the files for a given rule-side (injected so
134
134
  * the engine stays pure / testable — see `runWiring` for the fs-backed wiring).
135
135
  */
136
- export function evaluateWiring(rules, resolve) {
136
+ export function evaluateWiring(rules, resolve, context = {}) {
137
137
  const ruleResults = [];
138
138
  const all = [];
139
139
  const diagnostics = [];
@@ -179,9 +179,10 @@ export function evaluateWiring(rules, resolve) {
179
179
  let sinkFiles = 0;
180
180
  let sinkCount = 0;
181
181
  let emptySink = false;
182
+ let sinkHint;
182
183
  for (const [hopIndex, pair] of pairs.entries()) {
183
184
  const fromFiles = resolve(pair.from);
184
- const fromSites = extractTokens(pair.from, fromFiles).sites;
185
+ const fromSites = extractTokens(pair.from, fromFiles, context).sites;
185
186
  const fromKeys = firstSites(fromSites, groupBy);
186
187
  // Each sink kept separate so `intersection` can require membership in ALL.
187
188
  const sinkKeySets = [];
@@ -189,7 +190,10 @@ export function evaluateWiring(rules, resolve) {
189
190
  for (const sink of pair.to) {
190
191
  const files = resolve(sink);
191
192
  hopSinkFiles += files.length;
192
- sinkKeySets.push(firstSites(extractTokens(sink, files).sites, groupBy));
193
+ const sinkRes = extractTokens(sink, files, context);
194
+ if (sinkRes.hint && sinkHint === undefined)
195
+ sinkHint = sinkRes.hint;
196
+ sinkKeySets.push(firstSites(sinkRes.sites, groupBy));
193
197
  }
194
198
  const unionKeys = new Map();
195
199
  for (const set of sinkKeySets) {
@@ -277,7 +281,7 @@ export function evaluateWiring(rules, resolve) {
277
281
  ? '0 ids extracted from the source side'
278
282
  : undefined;
279
283
  if (skipReason !== undefined) {
280
- const failed = rule.failOnEmpty === true;
284
+ const failed = failsWhenEmpty(rule);
281
285
  skipped.push({ ruleId: rule.id, reason: skipReason, failed, severity });
282
286
  if (failed) {
283
287
  if (severity === 'error')
@@ -310,6 +314,7 @@ export function evaluateWiring(rules, resolve) {
310
314
  registeredFiles: sinkFiles,
311
315
  violations,
312
316
  ...(emptySink ? { emptySink: true } : {}),
317
+ ...(emptySink && sinkHint !== undefined ? { sinkHint } : {}),
313
318
  ...(pairs.length > 1 ? { hops } : {}),
314
319
  });
315
320
  all.push(...violations);