@shrkcrft/boundaries 0.1.0-alpha.22 → 0.1.0-alpha.24

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/index.d.ts CHANGED
@@ -7,6 +7,10 @@ export * from './evaluate/evaluate-boundaries.js';
7
7
  export * from './scan/tsconfig-aliases.js';
8
8
  export * from './wiring/evaluate-wiring.js';
9
9
  export * from './wiring/scan-wiring-files.js';
10
+ export * from './wiring/explain-wiring.js';
11
+ export * from './wiring/registry-query.js';
12
+ export * from './wiring/registration-graph.js';
13
+ export * from './wiring/trace-literal.js';
10
14
  export * from './policy/extract-templates.js';
11
15
  export * from './policy/evaluate-policy.js';
12
16
  export * from './policy/run-policy.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,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,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,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@ export * from "./evaluate/evaluate-boundaries.js";
7
7
  export * from "./scan/tsconfig-aliases.js";
8
8
  export * from "./wiring/evaluate-wiring.js";
9
9
  export * from "./wiring/scan-wiring-files.js";
10
+ export * from "./wiring/explain-wiring.js";
11
+ export * from "./wiring/registry-query.js";
12
+ export * from "./wiring/registration-graph.js";
13
+ export * from "./wiring/trace-literal.js";
10
14
  export * from "./policy/extract-templates.js";
11
15
  export * from "./policy/evaluate-policy.js";
12
16
  export * from "./policy/run-policy.js";
@@ -36,6 +36,14 @@ export interface IPolicyReport {
36
36
  readonly rules: readonly IPolicyRuleResult[];
37
37
  readonly findings: readonly IPolicyFinding[];
38
38
  readonly diagnostics: readonly string[];
39
+ /**
40
+ * Count of rules that actually scanned ≥1 unit. A rule whose globs matched 0
41
+ * files (e.g. a `style` rule in a project with no stylesheets) is NOT
42
+ * evaluated — a silent no-op the gate surfaces as `skipped` rather than a
43
+ * green pass. Misconfigured rules count as evaluated so their error is not
44
+ * swallowed by the gate's `evaluated === 0` skip path.
45
+ */
46
+ readonly evaluated: number;
39
47
  readonly verdict: 'pass' | 'errors' | 'warnings';
40
48
  }
41
49
  /** Resolves the content units to scan for a given rule (injected for purity/testability). */
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGjE,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;CACnC;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,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,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;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,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;AAgB/E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,aAAa,CAoExG"}
1
+ {"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGjE,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;CACnC;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,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,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;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;;;;;;OAMG;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;AAgB/E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,aAAa,CA2ExG"}
@@ -23,6 +23,7 @@ export function evaluatePolicy(rules, resolve) {
23
23
  const ruleResults = [];
24
24
  const findings = [];
25
25
  const diagnostics = [];
26
+ let evaluated = 0;
26
27
  let misconfigError = false;
27
28
  let misconfigWarn = false;
28
29
  for (const rule of rules) {
@@ -36,11 +37,17 @@ export function evaluatePolicy(rules, resolve) {
36
37
  else
37
38
  misconfigWarn = true;
38
39
  ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: 0, error: msg });
40
+ // A misconfigured rule attempted to run — count it as evaluated so its
41
+ // error isn't swallowed by the gate's `evaluated === 0` skip path.
42
+ evaluated += 1;
39
43
  continue;
40
44
  }
45
+ const units = resolve(rule);
46
+ if (units.length > 0)
47
+ evaluated += 1;
41
48
  let count = 0;
42
49
  let zeroWidth = false;
43
- for (const unit of resolve(rule)) {
50
+ for (const unit of units) {
44
51
  re.lastIndex = 0;
45
52
  let m;
46
53
  while ((m = re.exec(unit.content)) !== null) {
@@ -87,6 +94,7 @@ export function evaluatePolicy(rules, resolve) {
87
94
  rules: ruleResults,
88
95
  findings,
89
96
  diagnostics,
97
+ evaluated,
90
98
  verdict: hasError ? 'errors' : hasWarn ? 'warnings' : 'pass',
91
99
  };
92
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIjE,OAAO,EAAkB,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAY5F,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAsCf"}
1
+ {"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIjE,OAAO,EAAkB,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAY5F,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CA6Cf"}
@@ -35,14 +35,22 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
35
35
  selected = selected.filter((r) => changed.some((c) => matchesAny(c, globsFor(r))));
36
36
  }
37
37
  if (selected.length === 0) {
38
- return { schema: 'sharkcraft.policy-lint/v1', rules: [], findings: [], diagnostics: [], verdict: 'pass' };
38
+ return { schema: 'sharkcraft.policy-lint/v1', rules: [], findings: [], diagnostics: [], evaluated: 0, verdict: 'pass' };
39
39
  }
40
+ // Under --changed-only, restrict the SCANNED files to the changed set too (not
41
+ // just rule selection). Per-file regex findings have no cross-file dependency,
42
+ // so a pre-existing violation in a file the diff never touched is out of scope
43
+ // — this mirrors how `check boundaries`/wiring restrict to the changeset and
44
+ // stops the gate failing RED on untouched legacy debt.
45
+ const changedSet = options.changedOnly ? new Set(options.changedFiles ?? []) : undefined;
40
46
  const allGlobs = [...new Set(selected.flatMap((r) => [...globsFor(r)]))];
41
47
  const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
42
48
  return evaluatePolicy(selected, (rule) => {
43
49
  const globs = globsFor(rule);
44
50
  const units = [];
45
51
  for (const [path, content] of cache) {
52
+ if (changedSet && !changedSet.has(path))
53
+ continue;
46
54
  if (!matchesAny(path, globs))
47
55
  continue;
48
56
  const ext = nodePath.extname(path).toLowerCase();
@@ -1 +1 @@
1
- {"version":3,"file":"scan-imports.d.ts","sourceRoot":"","sources":["../../src/scan/scan-imports.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,iEAAiE;IACjE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAoFD;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,iBAAiB,CAyB3E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,SAAS;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,CAmB7E"}
1
+ {"version":3,"file":"scan-imports.d.ts","sourceRoot":"","sources":["../../src/scan/scan-imports.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,iEAAiE;IACjE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAkFD;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,iBAAiB,CAyB3E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,SAAS;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,CAmB7E"}
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
3
  const SUPPORTED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
4
4
  const DEFAULT_IGNORE = new Set([
@@ -46,18 +46,16 @@ function* walk(root, current, extraIgnore) {
46
46
  const name = String(entry.name);
47
47
  if (isIgnored(name, extraIgnore))
48
48
  continue;
49
+ // Do NOT descend into (or yield through) symlinks. Following a symlinked
50
+ // directory can inject phantom files into the graph — a self-referential
51
+ // loop (`src/loop -> src` => `src/loop/loop/loop/...`) only self-terminates
52
+ // by accident at PATH_MAX, and a link to a large external tree gets fully
53
+ // scanned. Mirror detect-workspace.ts's isTraversableDir pruning.
54
+ if (entry.isSymbolicLink())
55
+ continue;
49
56
  const full = nodePath.join(current, name);
50
- try {
51
- const isDir = entry.isDirectory() || entry.isSymbolicLink();
52
- if (isDir) {
53
- const stat = statSync(full);
54
- if (stat.isDirectory()) {
55
- yield* walk(root, full, extraIgnore);
56
- }
57
- continue;
58
- }
59
- }
60
- catch {
57
+ if (entry.isDirectory()) {
58
+ yield* walk(root, full, extraIgnore);
61
59
  continue;
62
60
  }
63
61
  if (entry.isFile()) {
@@ -6,7 +6,7 @@ export interface IWiringFileEntry {
6
6
  readonly path: string;
7
7
  readonly content: string;
8
8
  }
9
- /** A captured token + where it was captured (for declared tokens). */
9
+ /** A captured token + where it was captured. */
10
10
  export interface IWiringTokenSite {
11
11
  readonly token: string;
12
12
  readonly file: string;
@@ -14,12 +14,18 @@ export interface IWiringTokenSite {
14
14
  }
15
15
  export interface IWiringViolation {
16
16
  readonly ruleId: string;
17
- /** The declared token that is not present in the registered set. */
17
+ /** The token that is missing from the other side. */
18
18
  readonly token: string;
19
- /** Declaring file (project-relative) + 1-based line. */
19
+ /** Declaring (or registering) file (project-relative) + 1-based line. */
20
20
  readonly file: string;
21
21
  readonly line: number;
22
22
  readonly severity: 'error' | 'warning';
23
+ /**
24
+ * Which side the token is missing from. `declared-missing` (the subset case):
25
+ * declared but not registered. `registered-missing` (parity only): registered
26
+ * but never declared.
27
+ */
28
+ readonly direction?: 'declared-missing' | 'registered-missing';
23
29
  readonly hint?: string;
24
30
  }
25
31
  export interface IWiringRuleResult {
@@ -29,7 +35,7 @@ export interface IWiringRuleResult {
29
35
  readonly declaredCount: number;
30
36
  readonly registeredCount: number;
31
37
  readonly violations: readonly IWiringViolation[];
32
- /** Set when the rule is misconfigured (bad regex / no capture group). */
38
+ /** Set when the rule is misconfigured (bad regex / no capture group / bad source). */
33
39
  readonly error?: string;
34
40
  }
35
41
  export interface IWiringReport {
@@ -38,15 +44,36 @@ export interface IWiringReport {
38
44
  readonly violations: readonly IWiringViolation[];
39
45
  /** Rule-level misconfiguration messages (never throws — degrades gracefully). */
40
46
  readonly diagnostics: readonly string[];
47
+ /**
48
+ * Count of rules that actually ran a comparison. A rule whose declared+
49
+ * registered globs matched 0 files is NOT evaluated (a silent no-op the gate
50
+ * surfaces as `skipped`). Misconfigured rules count as evaluated so their
51
+ * error is not swallowed.
52
+ */
53
+ readonly evaluated: number;
41
54
  readonly verdict: 'pass' | 'errors' | 'warnings';
42
55
  }
43
56
  /** Resolves a rule-side's globs to the concrete files (path + content) to scan. */
44
57
  export type WiringFileResolver = (source: IWiringSource) => readonly IWiringFileEntry[];
45
58
  /**
46
- * Pure wiring evaluation. For each rule: every DECLARED token that is not in the
47
- * REGISTERED set is a violation, reported at its first declaring site. The
48
- * `resolve` callback supplies the files for a given rule-side (injected so the
49
- * engine stays pure / testable see `runWiring` for the fs-backed wiring).
59
+ * Public: extract every token site from ONE wiring source (regex capture group
60
+ * 1 or `arrayProperty` elements) over the given files. Shared by the wiring
61
+ * evaluator and the registry-inventory engine so both honour the exact same
62
+ * extraction semantics. A misconfigured source returns an `error` and no sites
63
+ * (never throws).
64
+ */
65
+ export declare function collectSourceSites(source: IWiringSource, files: readonly IWiringFileEntry[]): {
66
+ sites: readonly IWiringTokenSite[];
67
+ error?: string;
68
+ };
69
+ /**
70
+ * Pure wiring evaluation. For each rule: the DECLARED token set is checked
71
+ * against the UNION of the REGISTERED sources. Every declared token absent from
72
+ * the registered set is a `declared-missing` violation; in `parity` mode every
73
+ * registered token absent from the declared set is also a `registered-missing`
74
+ * violation. With `groupBy`, membership is checked within the same dir/package.
75
+ * The `resolve` callback supplies the files for a given rule-side (injected so
76
+ * the engine stays pure / testable — see `runWiring` for the fs-backed wiring).
50
77
  */
51
78
  export declare function evaluateWiring(rules: readonly IWiringRule[], resolve: WiringFileResolver): IWiringReport;
52
79
  //# 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":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGjE,eAAO,MAAM,aAAa,EAAG,sBAA+B,CAAC;AAE7D,2CAA2C;AAC3C,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,yEAAyE;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;CAClD;AAED,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,aAAa,KAAK,SAAS,gBAAgB,EAAE,CAAC;AAiDxF;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAkFf"}
1
+ {"version":3,"file":"evaluate-wiring.d.ts","sourceRoot":"","sources":["../../src/wiring/evaluate-wiring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGjE,eAAO,MAAM,aAAa,EAAG,sBAA+B,CAAC;AAE7D,2CAA2C;AAC3C,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,kBAAkB,GAAG,oBAAoB,CAAC;IAC/D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;CAClD;AAED,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,aAAa,KAAK,SAAS,gBAAgB,EAAE,CAAC;AAsCxF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,gBAAgB,EAAE,GACjC;IAAE,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAOxD;AAuJD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAsHf"}
Binary file
@@ -0,0 +1,53 @@
1
+ import type { IWiringRule } from '@shrkcrft/core';
2
+ import { type IWiringTokenSite } from './evaluate-wiring.js';
3
+ export declare const WIRING_EXPLAIN_SCHEMA: "sharkcraft.wiring-explain/v1";
4
+ /** One side (declared or registered) of a wiring rule, as extracted from the tree. */
5
+ export interface IWiringSideExplain {
6
+ /** Every capture site (token + file:line), stable-sorted by (file, line). */
7
+ readonly sites: readonly IWiringTokenSite[];
8
+ /** Distinct membership-key count (mirrors the gate's `declared/registered N`). */
9
+ readonly distinctCount: number;
10
+ /** Files scanned for this side (after glob resolution). */
11
+ readonly filesScanned: number;
12
+ /** Misconfiguration (bad regex / no capture group / bad source), if any. */
13
+ readonly error?: string;
14
+ }
15
+ /**
16
+ * The full intermediate output of evaluating ONE wiring rule against the live
17
+ * tree: the declared set and the registered set each source extracted (with
18
+ * file:line), plus the set-difference and verdict — the thing {@link
19
+ * evaluateWiring} computes internally but only emits as counts + violations.
20
+ */
21
+ export interface IWiringExplain {
22
+ readonly schema: typeof WIRING_EXPLAIN_SCHEMA;
23
+ readonly ruleId: string;
24
+ readonly description?: string;
25
+ readonly mode: 'subset' | 'parity';
26
+ readonly groupBy?: 'dir' | 'package';
27
+ readonly severity: 'error' | 'warning';
28
+ readonly declared: IWiringSideExplain;
29
+ readonly registered: IWiringSideExplain;
30
+ /** Declared tokens absent from the registered set (the `declared-missing` diff). */
31
+ readonly declaredNotRegistered: readonly IWiringTokenSite[];
32
+ /** Registered tokens absent from the declared set (parity-only `registered-missing`). */
33
+ readonly registeredNotDeclared: readonly IWiringTokenSite[];
34
+ readonly verdict: 'pass' | 'errors' | 'warnings';
35
+ /** Rule-level misconfiguration messages (engine degrades gracefully). */
36
+ readonly diagnostics: readonly string[];
37
+ }
38
+ export interface IExplainWiringOptions {
39
+ /** Project-relative directories to prune from the walk. */
40
+ readonly excludeDirs?: readonly string[];
41
+ }
42
+ /**
43
+ * Dry-run a single wiring rule against the live tree and return what each side
44
+ * extracted (declared set, registered set, the set-difference, the verdict) —
45
+ * WITHOUT writing config. Powers `wiring explain <ruleId>`, `wiring test
46
+ * <candidate>`, and `check wiring --explain <ruleId>`: the author can SEE the
47
+ * alias-resolved cross-file set-difference the gate computes before committing
48
+ * a rule. The diff/verdict reuse {@link evaluateWiring} so they match the gate
49
+ * exactly (incl. `groupBy` membership); the full per-site lists are extracted
50
+ * with the shared {@link collectSourceSites}. Never throws.
51
+ */
52
+ export declare function explainWiring(projectRoot: string, rule: IWiringRule, options?: IExplainWiringOptions): IWiringExplain;
53
+ //# sourceMappingURL=explain-wiring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explain-wiring.d.ts","sourceRoot":"","sources":["../../src/wiring/explain-wiring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,gBAAgB,CAAC;AAGjE,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,qBAAqB,EAAG,8BAAuC,CAAC;AAE7E,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5C,kFAAkF;IAClF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;IACxC,oFAAoF;IACpF,QAAQ,CAAC,qBAAqB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5D,yFAAyF;IACzF,QAAQ,CAAC,qBAAqB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;IACjD,yEAAyE;IACzE,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACpC,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAYD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAmEhB"}
@@ -0,0 +1,81 @@
1
+ import { matchesAny } from "../scan/glob.js";
2
+ import { readMatchingFiles } from "../util/walk-files.js";
3
+ import { collectSourceSites, evaluateWiring, } from "./evaluate-wiring.js";
4
+ export const WIRING_EXPLAIN_SCHEMA = 'sharkcraft.wiring-explain/v1';
5
+ function registeredSources(reg) {
6
+ return Array.isArray(reg) ? reg : [reg];
7
+ }
8
+ function sortSites(sites) {
9
+ return [...sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.token.localeCompare(b.token));
10
+ }
11
+ /**
12
+ * Dry-run a single wiring rule against the live tree and return what each side
13
+ * extracted (declared set, registered set, the set-difference, the verdict) —
14
+ * WITHOUT writing config. Powers `wiring explain <ruleId>`, `wiring test
15
+ * <candidate>`, and `check wiring --explain <ruleId>`: the author can SEE the
16
+ * alias-resolved cross-file set-difference the gate computes before committing
17
+ * a rule. The diff/verdict reuse {@link evaluateWiring} so they match the gate
18
+ * exactly (incl. `groupBy` membership); the full per-site lists are extracted
19
+ * with the shared {@link collectSourceSites}. Never throws.
20
+ */
21
+ export function explainWiring(projectRoot, rule, options = {}) {
22
+ const regSources = registeredSources(rule.registered);
23
+ const allGlobs = [
24
+ ...new Set([...rule.declared.files, ...regSources.flatMap((s) => [...s.files])]),
25
+ ];
26
+ const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
27
+ const entries = [...cache.entries()].map(([path, content]) => ({
28
+ path,
29
+ content,
30
+ }));
31
+ const filesFor = (source) => entries.filter((f) => matchesAny(f.path, source.files));
32
+ // Declared side — full sites.
33
+ const declaredFiles = filesFor(rule.declared);
34
+ const declaredRes = collectSourceSites(rule.declared, declaredFiles);
35
+ // Registered side — union of every source, full sites.
36
+ const registeredFiles = new Set();
37
+ const registeredSites = [];
38
+ let registeredError;
39
+ for (const source of regSources) {
40
+ const files = filesFor(source);
41
+ for (const f of files)
42
+ registeredFiles.add(f.path);
43
+ const res = collectSourceSites(source, files);
44
+ if (res.error && !registeredError)
45
+ registeredError = res.error;
46
+ registeredSites.push(...res.sites);
47
+ }
48
+ // Canonical diff + counts + verdict from the gate engine (same groupBy logic).
49
+ const report = evaluateWiring([rule], filesFor);
50
+ const ruleResult = report.rules[0];
51
+ const declaredNotRegistered = sortSites((ruleResult?.violations ?? [])
52
+ .filter((v) => v.direction === 'declared-missing')
53
+ .map((v) => ({ token: v.token, file: v.file, line: v.line })));
54
+ const registeredNotDeclared = sortSites((ruleResult?.violations ?? [])
55
+ .filter((v) => v.direction === 'registered-missing')
56
+ .map((v) => ({ token: v.token, file: v.file, line: v.line })));
57
+ return {
58
+ schema: WIRING_EXPLAIN_SCHEMA,
59
+ ruleId: rule.id,
60
+ ...(rule.description ? { description: rule.description } : {}),
61
+ mode: rule.mode === 'parity' ? 'parity' : 'subset',
62
+ ...(rule.groupBy ? { groupBy: rule.groupBy } : {}),
63
+ severity: rule.severity ?? 'error',
64
+ declared: {
65
+ sites: sortSites(declaredRes.sites),
66
+ distinctCount: ruleResult?.declaredCount ?? 0,
67
+ filesScanned: declaredFiles.length,
68
+ ...(declaredRes.error ? { error: declaredRes.error } : {}),
69
+ },
70
+ registered: {
71
+ sites: sortSites(registeredSites),
72
+ distinctCount: ruleResult?.registeredCount ?? 0,
73
+ filesScanned: registeredFiles.size,
74
+ ...(registeredError ? { error: registeredError } : {}),
75
+ },
76
+ declaredNotRegistered,
77
+ registeredNotDeclared,
78
+ verdict: report.verdict,
79
+ diagnostics: report.diagnostics,
80
+ };
81
+ }
@@ -0,0 +1,92 @@
1
+ import type { IRegistrationIdiom } from '@shrkcrft/core';
2
+ export declare const REGISTRATION_GRAPH_SCHEMA: "sharkcraft.registration-graph/v1";
3
+ /** A token role-site: which idiom + file:line it was found at. */
4
+ export interface IRegistrationSite {
5
+ readonly idiom: string;
6
+ readonly file: string;
7
+ readonly line: number;
8
+ }
9
+ /** One token in the registration graph and the three roles it plays. */
10
+ export interface IRegistrationNode {
11
+ readonly token: string;
12
+ /** Sites where the token is DECLARED (token/provider definition). */
13
+ readonly declared: readonly IRegistrationSite[];
14
+ /** Sites where the token is PROVIDED / REGISTERED into a composition. */
15
+ readonly provided: readonly IRegistrationSite[];
16
+ /** Sites where the token is CONSUMED / INJECTED. */
17
+ readonly consumed: readonly IRegistrationSite[];
18
+ }
19
+ /**
20
+ * The registration / DI graph: a peer to the import graph extracted from the
21
+ * declared idiom shapes, keyed by token. Distinct edge kinds (declared /
22
+ * provided / consumed) so the runtime-wiring questions imports can't answer
23
+ * become deterministic queries.
24
+ */
25
+ export interface IRegistrationGraph {
26
+ readonly schema: typeof REGISTRATION_GRAPH_SCHEMA;
27
+ /** Idiom names that contributed to this graph. */
28
+ readonly idioms: readonly string[];
29
+ /** Every distinct token, sorted, with its role-sites. */
30
+ readonly tokens: readonly IRegistrationNode[];
31
+ /** Misconfiguration messages (bad regex / no capture group / bad source). */
32
+ readonly diagnostics: readonly string[];
33
+ }
34
+ export interface IBuildRegistrationGraphOptions {
35
+ /** Project-relative directories to prune from the walk. */
36
+ readonly excludeDirs?: readonly string[];
37
+ }
38
+ /**
39
+ * Build the registration/DI graph from the declared idioms. For each idiom the
40
+ * three roles (declared / provided / consumed) are extracted with the shared
41
+ * alias-aware {@link collectSourceSites}, then bucketed by token across every
42
+ * idiom. Pure-engine output; the only IO is a single read-only tree walk over
43
+ * the union of all idiom globs. Never throws — a misconfigured source becomes a
44
+ * diagnostic.
45
+ */
46
+ export declare function buildRegistrationGraph(projectRoot: string, idioms: readonly IRegistrationIdiom[], options?: IBuildRegistrationGraphOptions): IRegistrationGraph;
47
+ /**
48
+ * A cheap content-signature of the exact files {@link buildRegistrationGraph}
49
+ * would read for these idioms: the sorted `relpath:mtimeMs:size` of every matched
50
+ * file, hashed. Because it reflects the graph's ACTUAL data source (a live tree
51
+ * walk) rather than the unrelated code-graph index, it is the correct key for any
52
+ * persisted registration-graph cache — a source edit shifts the signature even
53
+ * when no reindex has run, so a stale wiring verdict is impossible. Walk-and-stat
54
+ * only (no file reads), so it stays far cheaper than a full build. Never throws.
55
+ */
56
+ export declare function registrationGraphSignature(projectRoot: string, idioms: readonly IRegistrationIdiom[], options?: IBuildRegistrationGraphOptions): string;
57
+ /** A token's full registration chain, with role presence flags. */
58
+ export interface IRegistrationChain extends IRegistrationNode {
59
+ readonly isDeclared: boolean;
60
+ readonly isProvided: boolean;
61
+ readonly isConsumed: boolean;
62
+ }
63
+ /**
64
+ * `wiring chain <token>` — the declared → provided → consumed hops of one token
65
+ * with file:line at each, or `undefined` if the token is unknown to the graph.
66
+ */
67
+ export declare function registrationChain(graph: IRegistrationGraph, token: string): IRegistrationChain | undefined;
68
+ /** A token declared or injected but never provided — silently absent at runtime. */
69
+ export interface IUnprovidedToken {
70
+ readonly token: string;
71
+ readonly declared: readonly IRegistrationSite[];
72
+ readonly consumed: readonly IRegistrationSite[];
73
+ }
74
+ /**
75
+ * `wiring unprovided` — tokens that are DECLARED or CONSUMED but have ZERO
76
+ * provided sites. This is the silent-at-runtime class: typecheck/AOT-green, but
77
+ * the provider is never registered (or the injected token has no provider
78
+ * anywhere), so it resolves to undefined at runtime. The thing imports can't see.
79
+ */
80
+ export declare function registrationUnprovided(graph: IRegistrationGraph): readonly IUnprovidedToken[];
81
+ /** A token provided/registered that nothing consumes — a dead registration. */
82
+ export interface IOrphanRegistration {
83
+ readonly token: string;
84
+ readonly provided: readonly IRegistrationSite[];
85
+ }
86
+ /**
87
+ * `wiring orphans` — tokens that ARE provided/registered but have ZERO consumed
88
+ * sites: a provider/registration nothing injects (a build-clean no-op, or a
89
+ * sign the consumer was renamed/removed).
90
+ */
91
+ export declare function registrationOrphans(graph: IRegistrationGraph): readonly IOrphanRegistration[];
92
+ //# sourceMappingURL=registration-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registration-graph.d.ts","sourceRoot":"","sources":["../../src/wiring/registration-graph.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,gBAAgB,CAAC;AAKxE,eAAO,MAAM,yBAAyB,EAAG,kCAA2C,CAAC;AAErF,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACjD;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,yBAAyB,CAAC;IAClD,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,8BAA8B;IAC7C,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAiBD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,OAAO,GAAE,8BAAmC,GAC3C,kBAAkB,CAqDpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,OAAO,GAAE,8BAAmC,GAC3C,MAAM,CAiBR;AAED,mEAAmE;AACnE,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,EACzB,KAAK,EAAE,MAAM,GACZ,kBAAkB,GAAG,SAAS,CAShC;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACjD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,gBAAgB,EAAE,CAI7F;AAED,+EAA+E;AAC/E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACjD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,mBAAmB,EAAE,CAI7F"}
@@ -0,0 +1,124 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { statSync } from 'node:fs';
3
+ import * as nodePath from 'node:path';
4
+ import { matchesAny } from "../scan/glob.js";
5
+ import { readMatchingFiles, walkMatching } from "../util/walk-files.js";
6
+ import { collectSourceSites } from "./evaluate-wiring.js";
7
+ export const REGISTRATION_GRAPH_SCHEMA = 'sharkcraft.registration-graph/v1';
8
+ function sortSites(sites) {
9
+ return [...sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.idiom.localeCompare(b.idiom));
10
+ }
11
+ /** Union (deduped) of every glob any role of any idiom references. */
12
+ function registrationGlobs(idioms) {
13
+ return [
14
+ ...new Set(idioms.flatMap((i) => [...i.declared.files, ...i.provided.files, ...i.consumed.files])),
15
+ ];
16
+ }
17
+ /**
18
+ * Build the registration/DI graph from the declared idioms. For each idiom the
19
+ * three roles (declared / provided / consumed) are extracted with the shared
20
+ * alias-aware {@link collectSourceSites}, then bucketed by token across every
21
+ * idiom. Pure-engine output; the only IO is a single read-only tree walk over
22
+ * the union of all idiom globs. Never throws — a misconfigured source becomes a
23
+ * diagnostic.
24
+ */
25
+ export function buildRegistrationGraph(projectRoot, idioms, options = {}) {
26
+ const cache = readMatchingFiles(projectRoot, registrationGlobs(idioms), new Set(options.excludeDirs ?? []));
27
+ const entries = [...cache.entries()].map(([path, content]) => ({
28
+ path,
29
+ content,
30
+ }));
31
+ const filesFor = (source) => entries.filter((f) => matchesAny(f.path, source.files));
32
+ const declared = new Map();
33
+ const provided = new Map();
34
+ const consumed = new Map();
35
+ const diagnostics = [];
36
+ const harvest = (idiom, role, source, into) => {
37
+ const res = collectSourceSites(source, filesFor(source));
38
+ if (res.error)
39
+ diagnostics.push(`idiom "${idiom}" ${role}: ${res.error}`);
40
+ for (const s of res.sites) {
41
+ const list = into.get(s.token) ?? [];
42
+ list.push({ idiom, file: s.file, line: s.line });
43
+ into.set(s.token, list);
44
+ }
45
+ };
46
+ for (const idiom of idioms) {
47
+ harvest(idiom.name, 'declared', idiom.declared, declared);
48
+ harvest(idiom.name, 'provided', idiom.provided, provided);
49
+ harvest(idiom.name, 'consumed', idiom.consumed, consumed);
50
+ }
51
+ const tokenSet = new Set([...declared.keys(), ...provided.keys(), ...consumed.keys()]);
52
+ const tokens = [...tokenSet].sort().map((token) => ({
53
+ token,
54
+ declared: sortSites(declared.get(token) ?? []),
55
+ provided: sortSites(provided.get(token) ?? []),
56
+ consumed: sortSites(consumed.get(token) ?? []),
57
+ }));
58
+ return {
59
+ schema: REGISTRATION_GRAPH_SCHEMA,
60
+ idioms: idioms.map((i) => i.name),
61
+ tokens,
62
+ diagnostics,
63
+ };
64
+ }
65
+ /**
66
+ * A cheap content-signature of the exact files {@link buildRegistrationGraph}
67
+ * would read for these idioms: the sorted `relpath:mtimeMs:size` of every matched
68
+ * file, hashed. Because it reflects the graph's ACTUAL data source (a live tree
69
+ * walk) rather than the unrelated code-graph index, it is the correct key for any
70
+ * persisted registration-graph cache — a source edit shifts the signature even
71
+ * when no reindex has run, so a stale wiring verdict is impossible. Walk-and-stat
72
+ * only (no file reads), so it stays far cheaper than a full build. Never throws.
73
+ */
74
+ export function registrationGraphSignature(projectRoot, idioms, options = {}) {
75
+ const files = walkMatching(projectRoot, registrationGlobs(idioms), new Set(options.excludeDirs ?? [])).sort();
76
+ const parts = [];
77
+ for (const rel of files) {
78
+ try {
79
+ const st = statSync(nodePath.join(projectRoot, rel));
80
+ parts.push(`${rel}:${st.mtimeMs}:${st.size}`);
81
+ }
82
+ catch {
83
+ // Unreadable / racing delete — omit; a real content change still shifts
84
+ // the hash via the surviving entries (and via this file's disappearance).
85
+ }
86
+ }
87
+ return createHash('sha1').update(parts.join('\n')).digest('hex').slice(0, 16);
88
+ }
89
+ /**
90
+ * `wiring chain <token>` — the declared → provided → consumed hops of one token
91
+ * with file:line at each, or `undefined` if the token is unknown to the graph.
92
+ */
93
+ export function registrationChain(graph, token) {
94
+ const node = graph.tokens.find((t) => t.token === token);
95
+ if (!node)
96
+ return undefined;
97
+ return {
98
+ ...node,
99
+ isDeclared: node.declared.length > 0,
100
+ isProvided: node.provided.length > 0,
101
+ isConsumed: node.consumed.length > 0,
102
+ };
103
+ }
104
+ /**
105
+ * `wiring unprovided` — tokens that are DECLARED or CONSUMED but have ZERO
106
+ * provided sites. This is the silent-at-runtime class: typecheck/AOT-green, but
107
+ * the provider is never registered (or the injected token has no provider
108
+ * anywhere), so it resolves to undefined at runtime. The thing imports can't see.
109
+ */
110
+ export function registrationUnprovided(graph) {
111
+ return graph.tokens
112
+ .filter((t) => t.provided.length === 0 && (t.declared.length > 0 || t.consumed.length > 0))
113
+ .map((t) => ({ token: t.token, declared: t.declared, consumed: t.consumed }));
114
+ }
115
+ /**
116
+ * `wiring orphans` — tokens that ARE provided/registered but have ZERO consumed
117
+ * sites: a provider/registration nothing injects (a build-clean no-op, or a
118
+ * sign the consumer was renamed/removed).
119
+ */
120
+ export function registrationOrphans(graph) {
121
+ return graph.tokens
122
+ .filter((t) => t.provided.length > 0 && t.consumed.length === 0)
123
+ .map((t) => ({ token: t.token, provided: t.provided }));
124
+ }
@@ -0,0 +1,40 @@
1
+ import type { IRegistryDeclaration } from '@shrkcrft/core';
2
+ export declare const REGISTRY_SCHEMA: "sharkcraft.registry-inventory/v1";
3
+ /** Where an id was found (project-relative path + 1-based line). */
4
+ export interface IRegistrySite {
5
+ readonly file: string;
6
+ readonly line: number;
7
+ }
8
+ /** One id in a registry: its declaration sites + (optional) consumer/binding sites. */
9
+ export interface IRegistryEntry {
10
+ readonly id: string;
11
+ /** Declaration sites (from `source`), sorted by file then line. */
12
+ readonly sites: readonly IRegistrySite[];
13
+ /** Consumer/binding sites (from `consumer`), when the declaration sets one. */
14
+ readonly consumerSites?: readonly IRegistrySite[];
15
+ }
16
+ /** A scanned registry inventory: every distinct id and where it lives. */
17
+ export interface IRegistryInventory {
18
+ readonly schema: typeof REGISTRY_SCHEMA;
19
+ readonly name: string;
20
+ readonly description?: string;
21
+ /** Distinct ids, sorted, each with its sites. */
22
+ readonly entries: readonly IRegistryEntry[];
23
+ /** Misconfiguration messages (bad regex / no capture group / bad source). */
24
+ readonly diagnostics: readonly string[];
25
+ }
26
+ export interface IScanRegistryOptions {
27
+ /** Project-relative directories to prune from the walk. */
28
+ readonly excludeDirs?: readonly string[];
29
+ }
30
+ /**
31
+ * Scan a declared registry, returning every distinct id and its declaration
32
+ * (and optional consumer) sites. Pure-engine output; the only IO is a single
33
+ * read-only tree walk over the union of the source/consumer globs.
34
+ */
35
+ export declare function scanRegistry(projectRoot: string, decl: IRegistryDeclaration, options?: IScanRegistryOptions): IRegistryInventory;
36
+ /** True if `id` is declared in the registry (alias-blind exact membership). */
37
+ export declare function registryExists(inventory: IRegistryInventory, id: string): boolean;
38
+ /** The entry for `id`, or undefined if not declared. */
39
+ export declare function registryWhere(inventory: IRegistryInventory, id: string): IRegistryEntry | undefined;
40
+ //# sourceMappingURL=registry-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-query.d.ts","sourceRoot":"","sources":["../../src/wiring/registry-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAK3D,eAAO,MAAM,eAAe,EAAG,kCAA2C,CAAC;AAE3E,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACnD;AAED,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,eAAe,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAMD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,oBAAoB,EAC1B,OAAO,GAAE,oBAAyB,GACjC,kBAAkB,CA+CpB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,SAAS,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEjF;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,SAAS,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEnG"}
@@ -0,0 +1,65 @@
1
+ import { matchesAny } from "../scan/glob.js";
2
+ import { readMatchingFiles } from "../util/walk-files.js";
3
+ import { collectSourceSites } from "./evaluate-wiring.js";
4
+ export const REGISTRY_SCHEMA = 'sharkcraft.registry-inventory/v1';
5
+ function sortSites(sites) {
6
+ return [...sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
7
+ }
8
+ /**
9
+ * Scan a declared registry, returning every distinct id and its declaration
10
+ * (and optional consumer) sites. Pure-engine output; the only IO is a single
11
+ * read-only tree walk over the union of the source/consumer globs.
12
+ */
13
+ export function scanRegistry(projectRoot, decl, options = {}) {
14
+ const globs = [
15
+ ...new Set([...decl.source.files, ...(decl.consumer ? decl.consumer.files : [])]),
16
+ ];
17
+ const cache = readMatchingFiles(projectRoot, globs, new Set(options.excludeDirs ?? []));
18
+ const entries = [...cache.entries()].map(([path, content]) => ({ path, content }));
19
+ const diagnostics = [];
20
+ const sourceFiles = entries.filter((f) => matchesAny(f.path, decl.source.files));
21
+ const declared = collectSourceSites(decl.source, sourceFiles);
22
+ if (declared.error)
23
+ diagnostics.push(`registry "${decl.name}" source: ${declared.error}`);
24
+ const consumerByToken = new Map();
25
+ if (decl.consumer) {
26
+ const consumerFiles = entries.filter((f) => matchesAny(f.path, decl.consumer.files));
27
+ const consumed = collectSourceSites(decl.consumer, consumerFiles);
28
+ if (consumed.error)
29
+ diagnostics.push(`registry "${decl.name}" consumer: ${consumed.error}`);
30
+ for (const s of consumed.sites) {
31
+ const list = consumerByToken.get(s.token) ?? [];
32
+ list.push({ file: s.file, line: s.line });
33
+ consumerByToken.set(s.token, list);
34
+ }
35
+ }
36
+ const declaredByToken = new Map();
37
+ for (const s of declared.sites) {
38
+ const list = declaredByToken.get(s.token) ?? [];
39
+ list.push({ file: s.file, line: s.line });
40
+ declaredByToken.set(s.token, list);
41
+ }
42
+ const entriesOut = [...declaredByToken.keys()].sort().map((id) => {
43
+ const consumerSites = consumerByToken.get(id);
44
+ return {
45
+ id,
46
+ sites: sortSites(declaredByToken.get(id) ?? []),
47
+ ...(consumerSites && consumerSites.length > 0 ? { consumerSites: sortSites(consumerSites) } : {}),
48
+ };
49
+ });
50
+ return {
51
+ schema: REGISTRY_SCHEMA,
52
+ name: decl.name,
53
+ ...(decl.description ? { description: decl.description } : {}),
54
+ entries: entriesOut,
55
+ diagnostics,
56
+ };
57
+ }
58
+ /** True if `id` is declared in the registry (alias-blind exact membership). */
59
+ export function registryExists(inventory, id) {
60
+ return inventory.entries.some((e) => e.id === id);
61
+ }
62
+ /** The entry for `id`, or undefined if not declared. */
63
+ export function registryWhere(inventory, id) {
64
+ return inventory.entries.find((e) => e.id === id);
65
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"scan-wiring-files.d.ts","sourceRoot":"","sources":["../../src/wiring/scan-wiring-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,iGAAiG;IACjG,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAyBf"}
1
+ {"version":3,"file":"scan-wiring-files.d.ts","sourceRoot":"","sources":["../../src/wiring/scan-wiring-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,gBAAgB,CAAC;AAGjE,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAW9B,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,iGAAiG;IACjG,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CA8Bf"}
@@ -1,6 +1,14 @@
1
1
  import { matchesAny } from "../scan/glob.js";
2
2
  import { readMatchingFiles } from "../util/walk-files.js";
3
3
  import { evaluateWiring, } from "./evaluate-wiring.js";
4
+ /** All file globs a rule references: its declared side + every registered source (union). */
5
+ function ruleGlobs(rule) {
6
+ const reg = rule.registered;
7
+ const sources = Array.isArray(reg)
8
+ ? reg
9
+ : [reg];
10
+ return [...rule.declared.files, ...sources.flatMap((s) => [...s.files])];
11
+ }
4
12
  /**
5
13
  * Filesystem-backed wiring run: walks the project once, reads only the files a
6
14
  * rule references (skipping oversized files), and evaluates every (selected)
@@ -15,17 +23,22 @@ export function runWiring(projectRoot, rules, options = {}) {
15
23
  if (options.changedOnly) {
16
24
  const changed = options.changedFiles ?? [];
17
25
  selected = selected.filter((r) => {
18
- const globs = [...r.declared.files, ...r.registered.files];
26
+ const globs = ruleGlobs(r);
19
27
  return changed.some((c) => matchesAny(c, globs));
20
28
  });
21
29
  }
22
30
  if (selected.length === 0) {
23
- return { schema: 'sharkcraft.wiring/v1', rules: [], violations: [], diagnostics: [], verdict: 'pass' };
31
+ return {
32
+ schema: 'sharkcraft.wiring/v1',
33
+ rules: [],
34
+ violations: [],
35
+ diagnostics: [],
36
+ evaluated: 0,
37
+ verdict: 'pass',
38
+ };
24
39
  }
25
40
  // Union of all globs across selected rules → one tree walk, cached reads.
26
- const allGlobs = [
27
- ...new Set(selected.flatMap((r) => [...r.declared.files, ...r.registered.files])),
28
- ];
41
+ const allGlobs = [...new Set(selected.flatMap(ruleGlobs))];
29
42
  const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
30
43
  const entries = [...cache.entries()].map(([path, content]) => ({ path, content }));
31
44
  return evaluateWiring(selected, (source) => entries.filter((f) => matchesAny(f.path, source.files)));
@@ -0,0 +1,60 @@
1
+ export declare const TRACE_SCHEMA: "sharkcraft.trace/v1";
2
+ /** Default source globs scanned when `trace` is given no `--glob`. */
3
+ export declare const TRACE_DEFAULT_GLOBS: readonly string[];
4
+ /**
5
+ * How a literal occurrence relates to the cross-fence contract — the direction
6
+ * grep can't give you:
7
+ * - `declare` — a canonical definition (`const X = 'lit'`, `kind: 'lit'`, an enum value).
8
+ * - `register` — added to a collection / mapping (`register('lit')`, an array element, `{ 'lit': … }`).
9
+ * - `consume` — compared / switched / handled (`=== 'lit'`, `case 'lit':`).
10
+ * - `reference`— an occurrence we can't confidently classify (still reported, with context).
11
+ */
12
+ export declare enum TraceRole {
13
+ Declare = "declare",
14
+ Register = "register",
15
+ Consume = "consume",
16
+ Reference = "reference"
17
+ }
18
+ /** One occurrence of the traced literal (or a const aliased to it). */
19
+ export interface ITraceSite {
20
+ readonly file: string;
21
+ readonly line: number;
22
+ readonly role: TraceRole;
23
+ /** The trimmed source line, for context. */
24
+ readonly text: string;
25
+ /** Set when this site reached the literal through a `const NAME = 'literal'` alias. */
26
+ readonly viaAlias?: string;
27
+ }
28
+ export interface ITraceReport {
29
+ readonly schema: typeof TRACE_SCHEMA;
30
+ readonly literal: string;
31
+ readonly total: number;
32
+ /** Distinct files the literal (or an alias) was found in. */
33
+ readonly files: number;
34
+ /** Sites grouped by role, each group sorted by (file, line). */
35
+ readonly byRole: Readonly<Record<TraceRole, readonly ITraceSite[]>>;
36
+ /** Const names found bound to the literal (`const NAME = 'literal'`), if any. */
37
+ readonly aliases: readonly string[];
38
+ }
39
+ export interface ITraceOptions {
40
+ /** Override the default source globs. */
41
+ readonly globs?: readonly string[];
42
+ /** Project-relative directories to prune from the walk. */
43
+ readonly excludeDirs?: readonly string[];
44
+ /**
45
+ * Resolve `const NAME = 'literal'` bindings and also classify uses of NAME
46
+ * (flagged `viaAlias`). On by default — it is the cross-fence value over grep.
47
+ */
48
+ readonly resolveAliases?: boolean;
49
+ }
50
+ /**
51
+ * Trace every declare → register → consume site of an EXACT string literal
52
+ * across the tree, classifying each occurrence with a direction. Generalizes
53
+ * `registry where` to any cross-fence string contract (a kind slug, permission
54
+ * id, route key, data key) WITHOUT a pre-declared registry — point it at any
55
+ * literal and get the chain grep can't: direction + role + layer-spanning
56
+ * grouping, plus (by default) the const-alias bindings of the literal and their
57
+ * use-sites. Pure-engine; the only IO is one read-only tree walk. Never throws.
58
+ */
59
+ export declare function traceLiteral(projectRoot: string, literal: string, options?: ITraceOptions): ITraceReport;
60
+ //# sourceMappingURL=trace-literal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace-literal.d.ts","sourceRoot":"","sources":["../../src/wiring/trace-literal.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,EAAG,qBAA8B,CAAC;AAE3D,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAShD,CAAC;AAEF;;;;;;;GAOG;AACH,oBAAY,SAAS;IACnB,OAAO,YAAY;IACnB,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,SAAS,cAAc;CACxB;AAED,uEAAuE;AACvE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,4CAA4C;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,YAAY,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,UAAU,EAAE,CAAC,CAAC,CAAC;IACpE,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC;AAmED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,aAAkB,GAC1B,YAAY,CAqFd"}
@@ -0,0 +1,187 @@
1
+ import { readMatchingFiles } from "../util/walk-files.js";
2
+ export const TRACE_SCHEMA = 'sharkcraft.trace/v1';
3
+ /** Default source globs scanned when `trace` is given no `--glob`. */
4
+ export const TRACE_DEFAULT_GLOBS = [
5
+ '**/*.ts',
6
+ '**/*.tsx',
7
+ '**/*.mts',
8
+ '**/*.cts',
9
+ '**/*.js',
10
+ '**/*.jsx',
11
+ '**/*.mjs',
12
+ '**/*.cjs',
13
+ ];
14
+ /**
15
+ * How a literal occurrence relates to the cross-fence contract — the direction
16
+ * grep can't give you:
17
+ * - `declare` — a canonical definition (`const X = 'lit'`, `kind: 'lit'`, an enum value).
18
+ * - `register` — added to a collection / mapping (`register('lit')`, an array element, `{ 'lit': … }`).
19
+ * - `consume` — compared / switched / handled (`=== 'lit'`, `case 'lit':`).
20
+ * - `reference`— an occurrence we can't confidently classify (still reported, with context).
21
+ */
22
+ export var TraceRole;
23
+ (function (TraceRole) {
24
+ TraceRole["Declare"] = "declare";
25
+ TraceRole["Register"] = "register";
26
+ TraceRole["Consume"] = "consume";
27
+ TraceRole["Reference"] = "reference";
28
+ })(TraceRole || (TraceRole = {}));
29
+ function escapeRegex(s) {
30
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ }
32
+ function lineStarts(content) {
33
+ const starts = [0];
34
+ for (let i = 0; i < content.length; i += 1) {
35
+ if (content[i] === '\n')
36
+ starts.push(i + 1);
37
+ }
38
+ return starts;
39
+ }
40
+ /** 1-based line for a character offset, given precomputed line-start offsets. */
41
+ function lineAt(starts, offset) {
42
+ // Binary search for the greatest start <= offset.
43
+ let lo = 0;
44
+ let hi = starts.length - 1;
45
+ while (lo < hi) {
46
+ const mid = (lo + hi + 1) >> 1;
47
+ if (starts[mid] <= offset)
48
+ lo = mid;
49
+ else
50
+ hi = mid - 1;
51
+ }
52
+ return lo + 1;
53
+ }
54
+ function lineText(content, starts, line) {
55
+ const start = starts[line - 1] ?? 0;
56
+ const nl = content.indexOf('\n', start);
57
+ return content.slice(start, nl === -1 ? content.length : nl);
58
+ }
59
+ /**
60
+ * Heuristic declare-vs-register-vs-consume classifier from the text immediately
61
+ * around the occurrence. High-precision rules first; anything unmatched stays
62
+ * `reference` (reported, never silently dropped). Deterministic.
63
+ */
64
+ function classifyRole(before, after) {
65
+ const b = before.replace(/\s+$/, '');
66
+ const a = after.replace(/^\s+/, '');
67
+ // switch/case handler.
68
+ if (/\bcase$/.test(b) && a.startsWith(':'))
69
+ return TraceRole.Consume;
70
+ // equality / inequality comparison on either side.
71
+ if (/(===|!==|==|!=)$/.test(b) || /^(===|!==|==|!=)/.test(a))
72
+ return TraceRole.Consume;
73
+ // object / map key mapping: `{ 'lit': … }` or `, 'lit': …`.
74
+ if (a.startsWith(':') && /[{,]$/.test(b))
75
+ return TraceRole.Register;
76
+ // registration-ish call: register('lit'), provide('lit'), on('lit'), .set('lit', …).
77
+ if (/\b(register|provide|add|use|on|handle|bind|emit|dispatch|listen|subscribe|define)\w*\($/i.test(b)) {
78
+ return TraceRole.Register;
79
+ }
80
+ if (/\.\s*set\($/.test(b))
81
+ return TraceRole.Register;
82
+ // array element: the literal directly follows `[` or `,`.
83
+ if (/[[,]$/.test(b))
84
+ return TraceRole.Register;
85
+ // canonical binding: const/let/var X = 'lit'.
86
+ if (/\b(?:const|let|var)\s+[\w$]+\s*=$/.test(b))
87
+ return TraceRole.Declare;
88
+ // definition-ish object property: `kind: 'lit'`, `id = 'lit'`, etc.
89
+ if (/\b(kind|type|id|name|slug|key|tag|code|token|permission|route|event|action|channel|topic|status|provide|providerToken)\s*[:=]$/i.test(b)) {
90
+ return TraceRole.Declare;
91
+ }
92
+ return TraceRole.Reference;
93
+ }
94
+ /** A const name is alias-resolvable only if Pascal/SCREAMING-cased (low collision risk). */
95
+ const ALIAS_NAME = /^[A-Z][\w$]*$/;
96
+ /**
97
+ * Trace every declare → register → consume site of an EXACT string literal
98
+ * across the tree, classifying each occurrence with a direction. Generalizes
99
+ * `registry where` to any cross-fence string contract (a kind slug, permission
100
+ * id, route key, data key) WITHOUT a pre-declared registry — point it at any
101
+ * literal and get the chain grep can't: direction + role + layer-spanning
102
+ * grouping, plus (by default) the const-alias bindings of the literal and their
103
+ * use-sites. Pure-engine; the only IO is one read-only tree walk. Never throws.
104
+ */
105
+ export function traceLiteral(projectRoot, literal, options = {}) {
106
+ const globs = options.globs && options.globs.length > 0 ? options.globs : TRACE_DEFAULT_GLOBS;
107
+ const exclude = new Set(options.excludeDirs ?? []);
108
+ const cache = readMatchingFiles(projectRoot, globs, exclude);
109
+ const sites = [];
110
+ const aliasNames = new Set();
111
+ // Exact quoted-literal match: the closing quote immediately follows, so the
112
+ // quoted CONTENT equals the literal (not a substring of a longer string).
113
+ const litRe = new RegExp(`(['"\`])${escapeRegex(literal)}\\1`, 'g');
114
+ for (const [file, content] of cache) {
115
+ const starts = lineStarts(content);
116
+ litRe.lastIndex = 0;
117
+ let m;
118
+ while ((m = litRe.exec(content)) !== null) {
119
+ if (m.index === litRe.lastIndex)
120
+ litRe.lastIndex += 1;
121
+ const line = lineAt(starts, m.index);
122
+ const text = lineText(content, starts, line);
123
+ const col = m.index - (starts[line - 1] ?? 0);
124
+ const before = text.slice(0, col);
125
+ const after = text.slice(col + m[0].length);
126
+ const role = classifyRole(before, after);
127
+ sites.push({ file, line, role, text: text.trim() });
128
+ // Capture a const alias bound to this literal for the second pass.
129
+ const bind = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=$/.exec(before.replace(/\s+$/, ''));
130
+ if (bind && ALIAS_NAME.test(bind[1]))
131
+ aliasNames.add(bind[1]);
132
+ }
133
+ }
134
+ // Second pass: classify uses of any const aliased to the literal.
135
+ const resolveAliases = options.resolveAliases !== false;
136
+ if (resolveAliases && aliasNames.size > 0) {
137
+ for (const name of aliasNames) {
138
+ // The lookbehind excludes `.` as well as word chars/`$`: a genuine use of
139
+ // the top-level const alias is never dot-prefixed, whereas `Enum.NAME` /
140
+ // `obj.NAME` is a member/enum accessor that merely shares the alias's name
141
+ // and must NOT be counted as a use of the traced literal (over-match).
142
+ const useRe = new RegExp(`(?<![\\w$.])${escapeRegex(name)}(?![\\w$])`, 'g');
143
+ for (const [file, content] of cache) {
144
+ const starts = lineStarts(content);
145
+ useRe.lastIndex = 0;
146
+ let m;
147
+ while ((m = useRe.exec(content)) !== null) {
148
+ const line = lineAt(starts, m.index);
149
+ const text = lineText(content, starts, line);
150
+ const col = m.index - (starts[line - 1] ?? 0);
151
+ const before = text.slice(0, col);
152
+ const after = text.slice(col + name.length);
153
+ // Skip ONLY the binding occurrence itself (`const NAME = …`) — it's
154
+ // already counted as a literal Declare site. Keying on the exact
155
+ // `const NAME` position (not the whole line) means a genuine alias use
156
+ // that merely SHARES a line with some literal is still reported.
157
+ if (/\b(?:const|let|var)\s+$/.test(before))
158
+ continue;
159
+ const role = classifyRole(before, after);
160
+ sites.push({ file, line, role, text: text.trim(), viaAlias: name });
161
+ }
162
+ }
163
+ }
164
+ }
165
+ const sortSites = (xs) => [...xs].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
166
+ const byRole = {
167
+ [TraceRole.Declare]: [],
168
+ [TraceRole.Register]: [],
169
+ [TraceRole.Consume]: [],
170
+ [TraceRole.Reference]: [],
171
+ };
172
+ for (const s of sites)
173
+ byRole[s.role].push(s);
174
+ return {
175
+ schema: TRACE_SCHEMA,
176
+ literal,
177
+ total: sites.length,
178
+ files: new Set(sites.map((s) => s.file)).size,
179
+ byRole: {
180
+ [TraceRole.Declare]: sortSites(byRole[TraceRole.Declare]),
181
+ [TraceRole.Register]: sortSites(byRole[TraceRole.Register]),
182
+ [TraceRole.Consume]: sortSites(byRole[TraceRole.Consume]),
183
+ [TraceRole.Reference]: sortSites(byRole[TraceRole.Reference]),
184
+ },
185
+ aliases: [...aliasNames].sort(),
186
+ };
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/boundaries",
3
- "version": "0.1.0-alpha.22",
3
+ "version": "0.1.0-alpha.24",
4
4
  "description": "SharkCraft boundary rules: detect when a repository violates its own architecture (forbidden imports across folder/package/layer boundaries).",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -43,7 +43,7 @@
43
43
  "typecheck": "tsc --noEmit -p tsconfig.json"
44
44
  },
45
45
  "dependencies": {
46
- "@shrkcrft/core": "^0.1.0-alpha.22"
46
+ "@shrkcrft/core": "^0.1.0-alpha.24"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public"