@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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/policy/evaluate-policy.d.ts +8 -0
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +9 -1
- package/dist/policy/run-policy.d.ts.map +1 -1
- package/dist/policy/run-policy.js +9 -1
- package/dist/scan/scan-imports.d.ts.map +1 -1
- package/dist/scan/scan-imports.js +10 -12
- package/dist/wiring/evaluate-wiring.d.ts +35 -8
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +0 -0
- package/dist/wiring/explain-wiring.d.ts +53 -0
- package/dist/wiring/explain-wiring.d.ts.map +1 -0
- package/dist/wiring/explain-wiring.js +81 -0
- package/dist/wiring/registration-graph.d.ts +92 -0
- package/dist/wiring/registration-graph.d.ts.map +1 -0
- package/dist/wiring/registration-graph.js +124 -0
- package/dist/wiring/registry-query.d.ts +40 -0
- package/dist/wiring/registry-query.d.ts.map +1 -0
- package/dist/wiring/registry-query.js +65 -0
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +18 -5
- package/dist/wiring/trace-literal.d.ts +60 -0
- package/dist/wiring/trace-literal.d.ts.map +1 -0
- package/dist/wiring/trace-literal.js +187 -0
- package/package.json +2 -2
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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,
|
|
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;
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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,
|
|
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,
|
|
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 =
|
|
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 {
|
|
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.
|
|
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.
|
|
46
|
+
"@shrkcrft/core": "^0.1.0-alpha.24"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|