@shrkcrft/boundaries 0.1.0-alpha.21 → 0.1.0-alpha.23

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
@@ -5,4 +5,12 @@ export * from './scan/glob.js';
5
5
  export * from './scan/scan-imports.js';
6
6
  export * from './evaluate/evaluate-boundaries.js';
7
7
  export * from './scan/tsconfig-aliases.js';
8
+ export * from './wiring/evaluate-wiring.js';
9
+ export * from './wiring/scan-wiring-files.js';
10
+ export * from './wiring/registry-query.js';
11
+ export * from './policy/extract-templates.js';
12
+ export * from './policy/evaluate-policy.js';
13
+ export * from './policy/run-policy.js';
14
+ export * from './util/safe-regex.js';
15
+ export * from './util/walk-files.js';
8
16
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
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,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -5,3 +5,11 @@ export * from "./scan/glob.js";
5
5
  export * from "./scan/scan-imports.js";
6
6
  export * from "./evaluate/evaluate-boundaries.js";
7
7
  export * from "./scan/tsconfig-aliases.js";
8
+ export * from "./wiring/evaluate-wiring.js";
9
+ export * from "./wiring/scan-wiring-files.js";
10
+ export * from "./wiring/registry-query.js";
11
+ export * from "./policy/extract-templates.js";
12
+ export * from "./policy/evaluate-policy.js";
13
+ export * from "./policy/run-policy.js";
14
+ export * from "./util/safe-regex.js";
15
+ export * from "./util/walk-files.js";
@@ -0,0 +1,58 @@
1
+ import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
2
+ export declare const POLICY_LINT_SCHEMA: "sharkcraft.policy-lint/v1";
3
+ /**
4
+ * A chunk of content to scan for one rule. For whole files `baseLine` is 1; for
5
+ * an inline template it is the source line where the template body begins, so
6
+ * findings map back to the real file line.
7
+ */
8
+ export interface IPolicyUnit {
9
+ readonly path: string;
10
+ readonly content: string;
11
+ readonly baseLine: number;
12
+ /** Marks an inline-template unit (for clearer reporting). */
13
+ readonly inlineTemplate?: boolean;
14
+ }
15
+ export interface IPolicyFinding {
16
+ readonly ruleId: string;
17
+ readonly surface: PolicySurface;
18
+ readonly file: string;
19
+ readonly line: number;
20
+ /** The matched token (capture group 1 if present, else the whole match, truncated). */
21
+ readonly match: string;
22
+ readonly message: string;
23
+ readonly suggest?: string;
24
+ readonly severity: 'error' | 'warning';
25
+ readonly inlineTemplate?: boolean;
26
+ }
27
+ export interface IPolicyRuleResult {
28
+ readonly ruleId: string;
29
+ readonly surface: PolicySurface;
30
+ readonly severity: 'error' | 'warning';
31
+ readonly findingCount: number;
32
+ readonly error?: string;
33
+ }
34
+ export interface IPolicyReport {
35
+ readonly schema: typeof POLICY_LINT_SCHEMA;
36
+ readonly rules: readonly IPolicyRuleResult[];
37
+ readonly findings: readonly IPolicyFinding[];
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;
47
+ readonly verdict: 'pass' | 'errors' | 'warnings';
48
+ }
49
+ /** Resolves the content units to scan for a given rule (injected for purity/testability). */
50
+ export type PolicyUnitResolver = (rule: IPolicyRule) => readonly IPolicyUnit[];
51
+ /**
52
+ * Pure policy evaluation. Each rule's regex is run over the units the resolver
53
+ * supplies; matches become findings (capture group 1 is the reported token when
54
+ * present). A misconfigured rule (uncompilable regex) degrades to a diagnostic
55
+ * — never throws, so one bad rule cannot crash the check.
56
+ */
57
+ export declare function evaluatePolicy(rules: readonly IPolicyRule[], resolve: PolicyUnitResolver): IPolicyReport;
58
+ //# sourceMappingURL=evaluate-policy.d.ts.map
@@ -0,0 +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;;;;;;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"}
@@ -0,0 +1,100 @@
1
+ import { safeCompile } from "../util/safe-regex.js";
2
+ export const POLICY_LINT_SCHEMA = 'sharkcraft.policy-lint/v1';
3
+ function lineWithin(content, index) {
4
+ let line = 1;
5
+ const end = Math.min(index, content.length);
6
+ for (let i = 0; i < end; i += 1) {
7
+ if (content[i] === '\n')
8
+ line += 1;
9
+ }
10
+ return line;
11
+ }
12
+ function truncate(s, max = 120) {
13
+ const oneLine = s.replace(/\s+/g, ' ').trim();
14
+ return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
15
+ }
16
+ /**
17
+ * Pure policy evaluation. Each rule's regex is run over the units the resolver
18
+ * supplies; matches become findings (capture group 1 is the reported token when
19
+ * present). A misconfigured rule (uncompilable regex) degrades to a diagnostic
20
+ * — never throws, so one bad rule cannot crash the check.
21
+ */
22
+ export function evaluatePolicy(rules, resolve) {
23
+ const ruleResults = [];
24
+ const findings = [];
25
+ const diagnostics = [];
26
+ let evaluated = 0;
27
+ let misconfigError = false;
28
+ let misconfigWarn = false;
29
+ for (const rule of rules) {
30
+ const severity = rule.severity ?? 'error';
31
+ const { re, error } = safeCompile(rule.pattern, rule.flags);
32
+ if (error || !re) {
33
+ const msg = `rule "${rule.id}": ${error}`;
34
+ diagnostics.push(msg);
35
+ if (severity === 'error')
36
+ misconfigError = true;
37
+ else
38
+ misconfigWarn = true;
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;
43
+ continue;
44
+ }
45
+ const units = resolve(rule);
46
+ if (units.length > 0)
47
+ evaluated += 1;
48
+ let count = 0;
49
+ let zeroWidth = false;
50
+ for (const unit of units) {
51
+ re.lastIndex = 0;
52
+ let m;
53
+ while ((m = re.exec(unit.content)) !== null) {
54
+ if (m[0] === '') {
55
+ // A zero-width pattern (`a?`, a lookahead, …) would otherwise emit one
56
+ // empty finding per position. Advance and refuse to record; flag it.
57
+ zeroWidth = true;
58
+ re.lastIndex += 1;
59
+ continue;
60
+ }
61
+ const token = m[1] !== undefined ? m[1] : m[0];
62
+ const line = unit.baseLine - 1 + lineWithin(unit.content, m.index);
63
+ findings.push({
64
+ ruleId: rule.id,
65
+ surface: rule.surface,
66
+ file: unit.path,
67
+ line,
68
+ match: truncate(token),
69
+ message: rule.message,
70
+ ...(rule.suggest ? { suggest: rule.suggest } : {}),
71
+ severity,
72
+ ...(unit.inlineTemplate ? { inlineTemplate: true } : {}),
73
+ });
74
+ count += 1;
75
+ }
76
+ }
77
+ if (zeroWidth) {
78
+ const msg = `rule "${rule.id}": pattern matches the empty string (zero-width) — likely a misconfiguration`;
79
+ diagnostics.push(msg);
80
+ if (severity === 'error')
81
+ misconfigError = true;
82
+ else
83
+ misconfigWarn = true;
84
+ ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: count, error: msg });
85
+ }
86
+ else {
87
+ ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: count });
88
+ }
89
+ }
90
+ const hasError = misconfigError || findings.some((f) => f.severity === 'error');
91
+ const hasWarn = misconfigWarn || findings.some((f) => f.severity === 'warning');
92
+ return {
93
+ schema: POLICY_LINT_SCHEMA,
94
+ rules: ruleResults,
95
+ findings,
96
+ diagnostics,
97
+ evaluated,
98
+ verdict: hasError ? 'errors' : hasWarn ? 'warnings' : 'pass',
99
+ };
100
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Extract inline `template:` string literals from source files so the template
3
+ * policy surface can see markup that AOT/tsc treat as an opaque string. This is
4
+ * a deterministic, framework-agnostic regex extraction (Angular/Vue/Lit-style
5
+ * `template:` properties) — not a full TS parse. `templateUrl` is intentionally
6
+ * NOT followed (the referenced `.html` file is scanned directly).
7
+ */
8
+ export interface IExtractedTemplate {
9
+ /** The template body (literal contents, quotes/backticks stripped). */
10
+ readonly body: string;
11
+ /** 1-based line in the source file where the body begins. */
12
+ readonly startLine: number;
13
+ }
14
+ export declare function extractInlineTemplates(content: string): IExtractedTemplate[];
15
+ //# sourceMappingURL=extract-templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-templates.d.ts","sourceRoot":"","sources":["../../src/policy/extract-templates.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAmBD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAe5E"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Extract inline `template:` string literals from source files so the template
3
+ * policy surface can see markup that AOT/tsc treat as an opaque string. This is
4
+ * a deterministic, framework-agnostic regex extraction (Angular/Vue/Lit-style
5
+ * `template:` properties) — not a full TS parse. `templateUrl` is intentionally
6
+ * NOT followed (the referenced `.html` file is scanned directly).
7
+ */
8
+ // `template:` followed by a single-quoted, double-quoted, or backtick literal.
9
+ // Backtick allows newlines (multi-line templates). The `(?:\\.|[^delim\\])*`
10
+ // form is escape-aware, so an escaped same-delimiter quote (`\"` inside `"…"`)
11
+ // or an escaped backtick does NOT truncate the captured literal (which would
12
+ // silently drop violations after it). A nested backtick inside a `${…}`
13
+ // interpolation is still not handled (rare).
14
+ const TEMPLATE_RE = /\btemplate\s*:\s*(`(?:\\.|[^`\\])*`|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")/g;
15
+ function lineOf(content, index) {
16
+ let line = 1;
17
+ const end = Math.min(index, content.length);
18
+ for (let i = 0; i < end; i += 1) {
19
+ if (content[i] === '\n')
20
+ line += 1;
21
+ }
22
+ return line;
23
+ }
24
+ export function extractInlineTemplates(content) {
25
+ const out = [];
26
+ TEMPLATE_RE.lastIndex = 0;
27
+ let m;
28
+ while ((m = TEMPLATE_RE.exec(content)) !== null) {
29
+ if (m.index === TEMPLATE_RE.lastIndex)
30
+ TEMPLATE_RE.lastIndex += 1; // zero-width guard
31
+ const literal = m[1];
32
+ if (!literal)
33
+ continue;
34
+ const body = literal.slice(1, -1);
35
+ if (body.length === 0)
36
+ continue;
37
+ // Index of the opening delimiter, then +1 for the first body char.
38
+ const literalStart = m.index + m[0].length - literal.length;
39
+ out.push({ body, startLine: lineOf(content, literalStart + 1) });
40
+ }
41
+ return out;
42
+ }
@@ -0,0 +1,21 @@
1
+ import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
2
+ import { type IPolicyReport } from './evaluate-policy.js';
3
+ export interface IRunPolicyOptions {
4
+ /** Restrict to rules on these surfaces. */
5
+ readonly surfaces?: readonly PolicySurface[];
6
+ /** Run only these rule ids. */
7
+ readonly only?: readonly string[];
8
+ /** When true, only run rules whose globs match a changed file. */
9
+ readonly changedOnly?: boolean;
10
+ readonly changedFiles?: readonly string[];
11
+ /** Project-relative directories to prune from the walk (e.g. the SharkCraft asset dir). */
12
+ readonly excludeDirs?: readonly string[];
13
+ }
14
+ /**
15
+ * Filesystem-backed policy-lint. Walks the project once; on the `template`
16
+ * surface, source files contribute their inline `template:` bodies (with real
17
+ * source line numbers) while `.html` files are scanned whole. Pure-engine
18
+ * output; the only IO is the read-only walk + reads.
19
+ */
20
+ export declare function runPolicyLint(projectRoot: string, rules: readonly IPolicyRule[], options?: IRunPolicyOptions): IPolicyReport;
21
+ //# sourceMappingURL=run-policy.d.ts.map
@@ -0,0 +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,CA6Cf"}
@@ -0,0 +1,69 @@
1
+ import * as nodePath from 'node:path';
2
+ import { matchesAny } from "../scan/glob.js";
3
+ import { readMatchingFiles } from "../util/walk-files.js";
4
+ import { extractInlineTemplates } from "./extract-templates.js";
5
+ import { evaluatePolicy } from "./evaluate-policy.js";
6
+ /** Per-surface default globs when a rule omits `files`. */
7
+ const SURFACE_DEFAULT_GLOBS = {
8
+ // markup files (scanned whole) + source files (inline `template:` extracted).
9
+ template: ['**/*.html', '**/*.htm', '**/*.ts', '**/*.tsx'],
10
+ style: ['**/*.css', '**/*.scss', '**/*.sass', '**/*.less', '**/*.styl'],
11
+ ts: ['**/*.ts', '**/*.tsx'],
12
+ };
13
+ const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
14
+ function globsFor(rule) {
15
+ return rule.files && rule.files.length > 0 ? rule.files : SURFACE_DEFAULT_GLOBS[rule.surface];
16
+ }
17
+ /**
18
+ * Filesystem-backed policy-lint. Walks the project once; on the `template`
19
+ * surface, source files contribute their inline `template:` bodies (with real
20
+ * source line numbers) while `.html` files are scanned whole. Pure-engine
21
+ * output; the only IO is the read-only walk + reads.
22
+ */
23
+ export function runPolicyLint(projectRoot, rules, options = {}) {
24
+ let selected = rules;
25
+ if (options.surfaces && options.surfaces.length > 0) {
26
+ const s = new Set(options.surfaces);
27
+ selected = selected.filter((r) => s.has(r.surface));
28
+ }
29
+ if (options.only && options.only.length > 0) {
30
+ const ids = new Set(options.only);
31
+ selected = selected.filter((r) => ids.has(r.id));
32
+ }
33
+ if (options.changedOnly) {
34
+ const changed = options.changedFiles ?? [];
35
+ selected = selected.filter((r) => changed.some((c) => matchesAny(c, globsFor(r))));
36
+ }
37
+ if (selected.length === 0) {
38
+ return { schema: 'sharkcraft.policy-lint/v1', rules: [], findings: [], diagnostics: [], evaluated: 0, verdict: 'pass' };
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;
46
+ const allGlobs = [...new Set(selected.flatMap((r) => [...globsFor(r)]))];
47
+ const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
48
+ return evaluatePolicy(selected, (rule) => {
49
+ const globs = globsFor(rule);
50
+ const units = [];
51
+ for (const [path, content] of cache) {
52
+ if (changedSet && !changedSet.has(path))
53
+ continue;
54
+ if (!matchesAny(path, globs))
55
+ continue;
56
+ const ext = nodePath.extname(path).toLowerCase();
57
+ if (rule.surface === 'template' && SOURCE_EXT.has(ext)) {
58
+ for (const tpl of extractInlineTemplates(content)) {
59
+ units.push({ path, content: tpl.body, baseLine: tpl.startLine, inlineTemplate: true });
60
+ }
61
+ }
62
+ else {
63
+ // .html on the template surface, and all style/ts files: scan whole.
64
+ units.push({ path, content, baseLine: 1 });
65
+ }
66
+ }
67
+ return units;
68
+ });
69
+ }
@@ -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()) {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Compile a user/pack-supplied regex without ever throwing. The `g` flag is
3
+ * always applied (engines scan with `exec` loops); extra flags are merged and
4
+ * de-duped. A bad pattern / bad flags returns a clear `error` string so callers
5
+ * degrade a misconfigured rule to a diagnostic instead of crashing.
6
+ */
7
+ export declare function safeCompile(pattern: string, flags?: string): {
8
+ re?: RegExp;
9
+ error?: string;
10
+ };
11
+ /** Number of capture groups in a compiled regex (probe via an always-empty variant). */
12
+ export declare function countCaptureGroups(re: RegExp): number;
13
+ //# sourceMappingURL=safe-regex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-regex.d.ts","sourceRoot":"","sources":["../../src/util/safe-regex.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAO5F;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMrD"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Compile a user/pack-supplied regex without ever throwing. The `g` flag is
3
+ * always applied (engines scan with `exec` loops); extra flags are merged and
4
+ * de-duped. A bad pattern / bad flags returns a clear `error` string so callers
5
+ * degrade a misconfigured rule to a diagnostic instead of crashing.
6
+ */
7
+ export function safeCompile(pattern, flags) {
8
+ try {
9
+ const f = [...new Set(['g', ...(flags ?? '').split('')].filter(Boolean))].join('');
10
+ return { re: new RegExp(pattern, f) };
11
+ }
12
+ catch (e) {
13
+ return { error: `invalid regex /${pattern}/${flags ?? ''}: ${e.message}` };
14
+ }
15
+ }
16
+ /** Number of capture groups in a compiled regex (probe via an always-empty variant). */
17
+ export function countCaptureGroups(re) {
18
+ try {
19
+ return (new RegExp(re.source + '|').exec('')?.length ?? 1) - 1;
20
+ }
21
+ catch {
22
+ return 1; // can't determine → assume valid
23
+ }
24
+ }
@@ -0,0 +1,13 @@
1
+ /** Vendor / build / VCS dirs never scanned. */
2
+ export declare const SKIP_DIRS: ReadonlySet<string>;
3
+ /** Files larger than this are skipped (regex token extraction over multi-MB blobs is pointless). */
4
+ export declare const MAX_SCAN_FILE_BYTES = 1000000;
5
+ /**
6
+ * Walk `root`, returning project-relative POSIX paths that match any glob.
7
+ * `excludeDirs` is a set of project-relative POSIX directory paths to prune
8
+ * entirely (e.g. the SharkCraft asset/config dir).
9
+ */
10
+ export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): string[];
11
+ /** 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>;
13
+ //# sourceMappingURL=walk-files.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,80 @@
1
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { matchesAny } from "../scan/glob.js";
4
+ /** Vendor / build / VCS dirs never scanned. */
5
+ export const SKIP_DIRS = new Set([
6
+ 'node_modules',
7
+ '.git',
8
+ 'dist',
9
+ 'build',
10
+ 'coverage',
11
+ '.sharkcraft',
12
+ '.next',
13
+ '.turbo',
14
+ '.cache',
15
+ ]);
16
+ /** Files larger than this are skipped (regex token extraction over multi-MB blobs is pointless). */
17
+ export const MAX_SCAN_FILE_BYTES = 1_000_000;
18
+ /**
19
+ * Walk `root`, returning project-relative POSIX paths that match any glob.
20
+ * `excludeDirs` is a set of project-relative POSIX directory paths to prune
21
+ * entirely (e.g. the SharkCraft asset/config dir).
22
+ */
23
+ export function walkMatching(root, globs, excludeDirs = new Set()) {
24
+ const out = [];
25
+ const visit = (abs) => {
26
+ let entries;
27
+ try {
28
+ entries = readdirSync(abs, { withFileTypes: true });
29
+ }
30
+ catch {
31
+ return;
32
+ }
33
+ for (const e of entries) {
34
+ const childAbs = nodePath.join(abs, e.name);
35
+ const rel = nodePath.relative(root, childAbs).split(nodePath.sep).join('/');
36
+ if (e.isDirectory()) {
37
+ // Skip vendor/build/VCS dirs AND every dot-directory (`.yarn`, `.pnp`,
38
+ // `.venv`, `.gradle`, …) — matching the established scan-imports walker,
39
+ // so neither policy-lint nor wiring scans tooling/vendored sources.
40
+ // (Dirent.isDirectory() is false for symlinks, so symlinked dirs are
41
+ // never descended — no loop risk.)
42
+ if (e.name.startsWith('.') || SKIP_DIRS.has(e.name) || excludeDirs.has(rel))
43
+ continue;
44
+ visit(childAbs);
45
+ }
46
+ else if (e.isFile()) {
47
+ if (matchesAny(rel, globs))
48
+ out.push(rel);
49
+ }
50
+ }
51
+ };
52
+ visit(root);
53
+ return out;
54
+ }
55
+ /** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
56
+ export function readMatchingFiles(root, globs, excludeDirs = new Set()) {
57
+ const out = new Map();
58
+ for (const rel of walkMatching(root, globs, excludeDirs)) {
59
+ const abs = nodePath.join(root, rel);
60
+ let size = -1;
61
+ try {
62
+ const st = statSync(abs);
63
+ if (!st.isFile())
64
+ continue;
65
+ size = st.size;
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ if (size > MAX_SCAN_FILE_BYTES)
71
+ continue;
72
+ try {
73
+ out.set(rel, readFileSync(abs, 'utf8'));
74
+ }
75
+ catch {
76
+ // unreadable — skip
77
+ }
78
+ }
79
+ return out;
80
+ }
@@ -0,0 +1,79 @@
1
+ import type { IWiringRule, IWiringSource } from '@shrkcrft/core';
2
+ export declare const WIRING_SCHEMA: "sharkcraft.wiring/v1";
3
+ /** A file made available to the engine. */
4
+ export interface IWiringFileEntry {
5
+ /** Project-relative POSIX path. */
6
+ readonly path: string;
7
+ readonly content: string;
8
+ }
9
+ /** A captured token + where it was captured. */
10
+ export interface IWiringTokenSite {
11
+ readonly token: string;
12
+ readonly file: string;
13
+ readonly line: number;
14
+ }
15
+ export interface IWiringViolation {
16
+ readonly ruleId: string;
17
+ /** The token that is missing from the other side. */
18
+ readonly token: string;
19
+ /** Declaring (or registering) file (project-relative) + 1-based line. */
20
+ readonly file: string;
21
+ readonly line: number;
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';
29
+ readonly hint?: string;
30
+ }
31
+ export interface IWiringRuleResult {
32
+ readonly ruleId: string;
33
+ readonly description?: string;
34
+ readonly severity: 'error' | 'warning';
35
+ readonly declaredCount: number;
36
+ readonly registeredCount: number;
37
+ readonly violations: readonly IWiringViolation[];
38
+ /** Set when the rule is misconfigured (bad regex / no capture group / bad source). */
39
+ readonly error?: string;
40
+ }
41
+ export interface IWiringReport {
42
+ readonly schema: typeof WIRING_SCHEMA;
43
+ readonly rules: readonly IWiringRuleResult[];
44
+ readonly violations: readonly IWiringViolation[];
45
+ /** Rule-level misconfiguration messages (never throws — degrades gracefully). */
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;
54
+ readonly verdict: 'pass' | 'errors' | 'warnings';
55
+ }
56
+ /** Resolves a rule-side's globs to the concrete files (path + content) to scan. */
57
+ export type WiringFileResolver = (source: IWiringSource) => readonly IWiringFileEntry[];
58
+ /**
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).
77
+ */
78
+ export declare function evaluateWiring(rules: readonly IWiringRule[], resolve: WiringFileResolver): IWiringReport;
79
+ //# sourceMappingURL=evaluate-wiring.d.ts.map
@@ -0,0 +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,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,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
+ }
@@ -0,0 +1,19 @@
1
+ import type { IWiringRule } from '@shrkcrft/core';
2
+ import { type IWiringReport } from './evaluate-wiring.js';
3
+ export interface IRunWiringOptions {
4
+ /** Only run rules touched by these (project-relative) changed files. */
5
+ readonly changedFiles?: readonly string[];
6
+ /** When true with no changed files, run nothing (matches `--changed-only` with a clean tree). */
7
+ readonly changedOnly?: boolean;
8
+ /** Run only these rule ids. */
9
+ readonly only?: readonly string[];
10
+ /** Project-relative directories to prune from the walk (e.g. the SharkCraft asset dir). */
11
+ readonly excludeDirs?: readonly string[];
12
+ }
13
+ /**
14
+ * Filesystem-backed wiring run: walks the project once, reads only the files a
15
+ * rule references (skipping oversized files), and evaluates every (selected)
16
+ * rule. Pure-engine output; the only IO is the read-only tree walk + reads.
17
+ */
18
+ export declare function runWiring(projectRoot: string, rules: readonly IWiringRule[], options?: IRunWiringOptions): IWiringReport;
19
+ //# sourceMappingURL=scan-wiring-files.d.ts.map
@@ -0,0 +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,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"}
@@ -0,0 +1,45 @@
1
+ import { matchesAny } from "../scan/glob.js";
2
+ import { readMatchingFiles } from "../util/walk-files.js";
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
+ }
12
+ /**
13
+ * Filesystem-backed wiring run: walks the project once, reads only the files a
14
+ * rule references (skipping oversized files), and evaluates every (selected)
15
+ * rule. Pure-engine output; the only IO is the read-only tree walk + reads.
16
+ */
17
+ export function runWiring(projectRoot, rules, options = {}) {
18
+ let selected = rules;
19
+ if (options.only && options.only.length > 0) {
20
+ const ids = new Set(options.only);
21
+ selected = selected.filter((r) => ids.has(r.id));
22
+ }
23
+ if (options.changedOnly) {
24
+ const changed = options.changedFiles ?? [];
25
+ selected = selected.filter((r) => {
26
+ const globs = ruleGlobs(r);
27
+ return changed.some((c) => matchesAny(c, globs));
28
+ });
29
+ }
30
+ if (selected.length === 0) {
31
+ return {
32
+ schema: 'sharkcraft.wiring/v1',
33
+ rules: [],
34
+ violations: [],
35
+ diagnostics: [],
36
+ evaluated: 0,
37
+ verdict: 'pass',
38
+ };
39
+ }
40
+ // Union of all globs across selected rules → one tree walk, cached reads.
41
+ const allGlobs = [...new Set(selected.flatMap(ruleGlobs))];
42
+ const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
43
+ const entries = [...cache.entries()].map(([path, content]) => ({ path, content }));
44
+ return evaluateWiring(selected, (source) => entries.filter((f) => matchesAny(f.path, source.files)));
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/boundaries",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.23",
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.21"
46
+ "@shrkcrft/core": "^0.1.0-alpha.23"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public"