@shrkcrft/boundaries 0.1.0-alpha.28 → 0.1.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/baseline/compute-baseline.d.ts.map +1 -1
- package/dist/baseline/compute-baseline.js +2 -1
- package/dist/extract/extract-tokens.d.ts +19 -1
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +53 -1
- package/dist/extract/import-edges.d.ts +49 -0
- package/dist/extract/import-edges.d.ts.map +1 -0
- package/dist/extract/import-edges.js +157 -0
- package/dist/extract/inspect-source.d.ts +6 -0
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +6 -1
- package/dist/extract/parse-imports.d.ts +58 -0
- package/dist/extract/parse-imports.d.ts.map +1 -0
- package/dist/extract/parse-imports.js +155 -0
- package/dist/generated/check-provenance.d.ts +20 -2
- package/dist/generated/check-provenance.d.ts.map +1 -1
- package/dist/generated/check-provenance.js +24 -5
- package/dist/generated/scan-generated.d.ts +49 -3
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +90 -5
- 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 +1 -1
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +2 -1
- package/dist/util/walk-files.d.ts +21 -2
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +85 -4
- package/dist/wiring/evaluate-wiring.d.ts +10 -3
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +12 -7
- package/dist/wiring/explain-wiring.d.ts +7 -9
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +18 -0
- package/dist/wiring/plan-wiring-fix.d.ts +75 -0
- package/dist/wiring/plan-wiring-fix.d.ts.map +1 -0
- package/dist/wiring/plan-wiring-fix.js +302 -0
- package/dist/wiring/registration-graph.d.ts +1 -1
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +8 -3
- package/dist/wiring/registry-query.d.ts +1 -1
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +7 -3
- package/dist/wiring/scan-wiring-files.d.ts +1 -1
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +5 -1
- package/dist/wiring/sink-imports.d.ts +84 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -0
- package/dist/wiring/sink-imports.js +115 -0
- package/package.json +2 -2
|
@@ -11,6 +11,13 @@ export interface IWiringSideExplain {
|
|
|
11
11
|
readonly filesScanned: number;
|
|
12
12
|
/** Misconfiguration (bad regex / no capture group / bad source), if any. */
|
|
13
13
|
readonly error?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Named extractor this side resolved from, when it used one. Surfaced so an
|
|
16
|
+
* author reading an explain can tell a SHARED selector from a local copy of
|
|
17
|
+
* it — the two look identical in the extracted ids, and only one of them is
|
|
18
|
+
* guaranteed to stay in step with the other planes.
|
|
19
|
+
*/
|
|
20
|
+
readonly viaExtractor?: string;
|
|
14
21
|
}
|
|
15
22
|
/**
|
|
16
23
|
* The full intermediate output of evaluating ONE wiring rule against the live
|
|
@@ -48,14 +55,5 @@ export interface IExplainWiringOptions {
|
|
|
48
55
|
/** Project-relative directories to prune from the walk. */
|
|
49
56
|
readonly excludeDirs?: readonly string[];
|
|
50
57
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Dry-run a single wiring rule against the live tree and return what each side
|
|
53
|
-
* extracted (declared set, registered set, the set-difference, the verdict) —
|
|
54
|
-
* WITHOUT writing config. Powers `wiring explain <ruleId>`, `wiring test
|
|
55
|
-
* <candidate>`, `gates explain <id>`, and `check wiring --explain <ruleId>`: the
|
|
56
|
-
* author can SEE the cross-file set-difference the gate computes before
|
|
57
|
-
* committing a rule. The diff/verdict reuse {@link evaluateWiring} so they match
|
|
58
|
-
* the gate exactly (incl. `groupBy` membership). Never throws.
|
|
59
|
-
*/
|
|
60
58
|
export declare function explainWiring(projectRoot: string, rule: IWiringRule, options?: IExplainWiringOptions): IWiringExplain;
|
|
61
59
|
//# sourceMappingURL=explain-wiring.d.ts.map
|
|
@@ -1 +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,EAOL,KAAK,gBAAgB,EACrB,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;
|
|
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,EAOL,KAAK,gBAAgB,EACrB,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;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;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,GAAG,UAAU,CAAC;IAChD,QAAQ,CAAC,cAAc,EAAE,OAAO,GAAG,cAAc,CAAC;IAClD,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,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;IAC3D,yDAAyD;IACzD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,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;AAiCD,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE,qBAA0B,GAClC,cAAc,CA2EhB"}
|
|
@@ -14,6 +14,22 @@ function sortSites(sites) {
|
|
|
14
14
|
* committing a rule. The diff/verdict reuse {@link evaluateWiring} so they match
|
|
15
15
|
* the gate exactly (incl. `groupBy` membership). Never throws.
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* The extractor a rule's registered side resolved from — only when EVERY sink
|
|
19
|
+
* shares it. A union of sinks where one is shared and another is inline has no
|
|
20
|
+
* single answer, and naming just one would be a claim the config does not make.
|
|
21
|
+
*/
|
|
22
|
+
function registeredExtractorRef(rule) {
|
|
23
|
+
const sinks = Array.isArray(rule.registered)
|
|
24
|
+
? rule.registered
|
|
25
|
+
: rule.registered
|
|
26
|
+
? [rule.registered]
|
|
27
|
+
: [];
|
|
28
|
+
if (sinks.length === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
const first = sinks[0].$use;
|
|
31
|
+
return first !== undefined && sinks.every((s) => s.$use === first) ? first : undefined;
|
|
32
|
+
}
|
|
17
33
|
export function explainWiring(projectRoot, rule, options = {}) {
|
|
18
34
|
const sourceSide = wiringSourceSide(rule);
|
|
19
35
|
// For a chain, the reported "registered" side is the LAST hop (the far end).
|
|
@@ -63,12 +79,14 @@ export function explainWiring(projectRoot, rule, options = {}) {
|
|
|
63
79
|
distinctCount: ruleResult?.declaredCount ?? 0,
|
|
64
80
|
filesScanned: declaredFiles.length,
|
|
65
81
|
...(declaredRes.error ? { error: declaredRes.error } : {}),
|
|
82
|
+
...(rule.declared?.$use ? { viaExtractor: rule.declared.$use } : {}),
|
|
66
83
|
},
|
|
67
84
|
registered: {
|
|
68
85
|
sites: sortSites(registeredSites),
|
|
69
86
|
distinctCount: ruleResult?.registeredCount ?? 0,
|
|
70
87
|
filesScanned: registeredFiles.size,
|
|
71
88
|
...(registeredError ? { error: registeredError } : {}),
|
|
89
|
+
...(registeredExtractorRef(rule) ? { viaExtractor: registeredExtractorRef(rule) } : {}),
|
|
72
90
|
},
|
|
73
91
|
declaredNotRegistered: byDirection('declared-missing'),
|
|
74
92
|
registeredNotDeclared: byDirection('registered-missing'),
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { IWiringRule } from '@shrkcrft/core';
|
|
2
|
+
import { type IWiringViolation } from './evaluate-wiring.js';
|
|
3
|
+
export declare const WIRING_FIX_SCHEMA: "sharkcraft.wiring-fix/v1";
|
|
4
|
+
/** Why a violation could not be fixed mechanically. */
|
|
5
|
+
export type WiringFixRefusal =
|
|
6
|
+
/** More than one registered sink — which array should it go in? */
|
|
7
|
+
'ambiguous-sink'
|
|
8
|
+
/** The sink is not an array literal the engine can append to. */
|
|
9
|
+
| 'sink-not-an-array'
|
|
10
|
+
/** The sink glob resolves to several files, or none. */
|
|
11
|
+
| 'ambiguous-sink-file'
|
|
12
|
+
/** The array literal could not be located in the sink file. */
|
|
13
|
+
| 'array-not-found'
|
|
14
|
+
/** The token is a quoted string in the sink; the declared side is a symbol (or vice versa). */
|
|
15
|
+
| 'token-shape-mismatch'
|
|
16
|
+
/**
|
|
17
|
+
* The sink IMPORTS its array members, so appending to the array alone would
|
|
18
|
+
* leave an unbound reference — a green gate over a file that does not
|
|
19
|
+
* compile. Raised whenever the needed import cannot be derived with
|
|
20
|
+
* certainty.
|
|
21
|
+
*/
|
|
22
|
+
| 'needs-import';
|
|
23
|
+
/** One planned edit: append `token` into the anchor array of `file`. */
|
|
24
|
+
export interface IWiringFixEdit {
|
|
25
|
+
readonly ruleId: string;
|
|
26
|
+
readonly token: string;
|
|
27
|
+
/** Project-relative sink file the edit lands in. */
|
|
28
|
+
readonly file: string;
|
|
29
|
+
/** 1-based line of the array's closing bracket, where the token is inserted before. */
|
|
30
|
+
readonly line: number;
|
|
31
|
+
/** The exact text inserted, including indentation and trailing comma. */
|
|
32
|
+
readonly insert: string;
|
|
33
|
+
/** The full new file contents, ready to write. */
|
|
34
|
+
readonly nextContent: string;
|
|
35
|
+
/**
|
|
36
|
+
* The import statement added alongside the array append, when the sink
|
|
37
|
+
* imports its members and the specifier was derivable. Absent when the token
|
|
38
|
+
* was already bound in the sink file.
|
|
39
|
+
*/
|
|
40
|
+
readonly importInsert?: string;
|
|
41
|
+
/** 1-based line the import statement lands on. */
|
|
42
|
+
readonly importLine?: number;
|
|
43
|
+
}
|
|
44
|
+
/** A violation the planner deliberately did not touch. */
|
|
45
|
+
export interface IWiringFixSkip {
|
|
46
|
+
readonly ruleId: string;
|
|
47
|
+
readonly token: string;
|
|
48
|
+
readonly reason: WiringFixRefusal;
|
|
49
|
+
readonly detail: string;
|
|
50
|
+
}
|
|
51
|
+
export interface IWiringFixPlan {
|
|
52
|
+
readonly schema: typeof WIRING_FIX_SCHEMA;
|
|
53
|
+
readonly edits: readonly IWiringFixEdit[];
|
|
54
|
+
readonly skipped: readonly IWiringFixSkip[];
|
|
55
|
+
}
|
|
56
|
+
/** A file the planner may read/rewrite. */
|
|
57
|
+
export interface IWiringFixFile {
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly content: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Plan the mechanically-unambiguous fixes for `declared-but-not-registered`
|
|
63
|
+
* violations.
|
|
64
|
+
*
|
|
65
|
+
* The bar is deliberately high, because a wrong autofix in a gate is worse than
|
|
66
|
+
* no autofix: the tool would be writing the very thing it is supposed to
|
|
67
|
+
* verify. An edit is planned ONLY when there is exactly one registered sink,
|
|
68
|
+
* that sink is an `array-members` source, its glob resolves to exactly one
|
|
69
|
+
* file, and the anchor array is found exactly once in it. Every other shape is
|
|
70
|
+
* reported as skipped WITH the reason — never guessed at.
|
|
71
|
+
*
|
|
72
|
+
* Pure: the caller supplies file contents and performs any writes.
|
|
73
|
+
*/
|
|
74
|
+
export declare function planWiringFix(rule: IWiringRule, violations: readonly IWiringViolation[], files: readonly IWiringFixFile[]): IWiringFixPlan;
|
|
75
|
+
//# sourceMappingURL=plan-wiring-fix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-wiring-fix.d.ts","sourceRoot":"","sources":["../../src/wiring/plan-wiring-fix.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,gBAAgB,CAAC;AAIjE,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAWhF,eAAO,MAAM,iBAAiB,EAAG,0BAAmC,CAAC;AAErE,uDAAuD;AACvD,MAAM,MAAM,gBAAgB;AAC1B,mEAAmE;AACjE,gBAAgB;AAClB,iEAAiE;GAC/D,mBAAmB;AACrB,wDAAwD;GACtD,qBAAqB;AACvB,+DAA+D;GAC7D,iBAAiB;AACnB,+FAA+F;GAC7F,sBAAsB;AACxB;;;;;GAKG;GACD,cAAc,CAAC;AAEnB,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,kDAAkD;IAClD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,iBAAiB,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAuKD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,KAAK,EAAE,SAAS,cAAc,EAAE,GAC/B,cAAc,CA2KhB"}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { matchesAny } from "../scan/glob.js";
|
|
2
|
+
import { elementToken, escapeRegex, scanBalanced } from "../extract/scan-literals.js";
|
|
3
|
+
import { extractTokens } from "../extract/extract-tokens.js";
|
|
4
|
+
import { registeredSources } from "./evaluate-wiring.js";
|
|
5
|
+
import { collectSinkBindings, deriveImportTemplate, insertImportStatement, renderImport, renderSpecifier, } from "./sink-imports.js";
|
|
6
|
+
import { resolveExtractorAnchor, resolveExtractorKind } from '@shrkcrft/core';
|
|
7
|
+
export const WIRING_FIX_SCHEMA = 'sharkcraft.wiring-fix/v1';
|
|
8
|
+
/** Indentation of the line containing `index`. */
|
|
9
|
+
function indentOf(content, index) {
|
|
10
|
+
const lineStart = content.lastIndexOf('\n', index - 1) + 1;
|
|
11
|
+
const m = /^[ \t]*/.exec(content.slice(lineStart, index));
|
|
12
|
+
return m ? m[0] : ' ';
|
|
13
|
+
}
|
|
14
|
+
function lineOf(content, index) {
|
|
15
|
+
let line = 1;
|
|
16
|
+
for (let i = 0; i < index && i < content.length; i += 1) {
|
|
17
|
+
if (content[i] === '\n')
|
|
18
|
+
line += 1;
|
|
19
|
+
}
|
|
20
|
+
return line;
|
|
21
|
+
}
|
|
22
|
+
/** Extensions a derived specifier may resolve through, in probe order. */
|
|
23
|
+
const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a relative specifier against the sink's directory, POSIX-style.
|
|
26
|
+
*
|
|
27
|
+
* Only relative specifiers are resolvable this way. A bare specifier is a
|
|
28
|
+
* package (or a tsconfig alias) whose target this engine cannot know without a
|
|
29
|
+
* resolver, so those are reported unresolved and the caller refuses.
|
|
30
|
+
*/
|
|
31
|
+
function resolveRelative(sinkPath, specifier) {
|
|
32
|
+
if (!specifier.startsWith('.'))
|
|
33
|
+
return undefined;
|
|
34
|
+
const dir = sinkPath.includes('/') ? sinkPath.slice(0, sinkPath.lastIndexOf('/')) : '';
|
|
35
|
+
const parts = [...dir.split('/').filter(Boolean), ...specifier.split('/')];
|
|
36
|
+
const stack = [];
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (part === '' || part === '.')
|
|
39
|
+
continue;
|
|
40
|
+
if (part === '..') {
|
|
41
|
+
if (stack.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
stack.pop();
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
stack.push(part);
|
|
47
|
+
}
|
|
48
|
+
return stack.join('/');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a derived specifier really points at the file that DECLARES the
|
|
52
|
+
* token.
|
|
53
|
+
*
|
|
54
|
+
* This is the safety net that makes the derived-import path trustworthy rather
|
|
55
|
+
* than merely plausible. The template says what the specifier *should* be; this
|
|
56
|
+
* confirms it names the file the gate found the token in. A template that
|
|
57
|
+
* happens to fit the existing members but misses for the new one is caught
|
|
58
|
+
* here instead of being written to disk.
|
|
59
|
+
*/
|
|
60
|
+
function specifierPointsAtDeclaration(sinkPath, specifier, declaringFile) {
|
|
61
|
+
const base = resolveRelative(sinkPath, specifier);
|
|
62
|
+
if (base === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
if (base === declaringFile)
|
|
65
|
+
return true;
|
|
66
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
67
|
+
if (base + ext === declaringFile)
|
|
68
|
+
return true;
|
|
69
|
+
if (`${base}/index${ext}` === declaringFile)
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether `file` exports `token`, per the same extractor the declared side of a
|
|
76
|
+
* rule most often uses.
|
|
77
|
+
*
|
|
78
|
+
* Reuses `extractTokens` rather than a second regex so "what counts as an
|
|
79
|
+
* export" has exactly one definition — a private answer here that drifted from
|
|
80
|
+
* the extractor would either block valid fixes or wave through invalid ones.
|
|
81
|
+
*/
|
|
82
|
+
function exportsToken(content, token) {
|
|
83
|
+
const res = extractTokens({ files: ['*'], extract: 'export-names' }, [{ path: 'x', content }]);
|
|
84
|
+
return res.sites.some((site) => site.token === token);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decide whether appending `token` to the sink needs an import, and whether
|
|
88
|
+
* that import can be derived.
|
|
89
|
+
*
|
|
90
|
+
* The condition is an INCONSISTENCY check, not a resolvability check. An import
|
|
91
|
+
* is needed exactly when the sink's existing members are import-bound but the
|
|
92
|
+
* new token would not be — that is the case where the append introduces a
|
|
93
|
+
* reference the file cannot resolve, and where the gate would go green over a
|
|
94
|
+
* file that no longer compiles.
|
|
95
|
+
*
|
|
96
|
+
* Deliberately NOT "the token must be bound". If the existing members are
|
|
97
|
+
* themselves unbound (ambient globals, a `/// <reference>`, a fixture), then
|
|
98
|
+
* whatever makes them resolve applies equally to the new one, and the append is
|
|
99
|
+
* consistent with the file's own convention. Refusing there would be a false
|
|
100
|
+
* refusal on a file the edit does not make any worse.
|
|
101
|
+
*/
|
|
102
|
+
function decideImport(sinkPath, sinkContent, members, token, declaringFile, declaringContent) {
|
|
103
|
+
const bindings = collectSinkBindings(sinkContent);
|
|
104
|
+
// Already resolvable here — imported, or declared inline in the sink.
|
|
105
|
+
if (bindings.some((b) => b.local === token))
|
|
106
|
+
return { kind: 'append-only' };
|
|
107
|
+
// Only members brought in by an IMPORT establish a binding convention the new
|
|
108
|
+
// token has to follow. A locally-declared member says nothing about where an
|
|
109
|
+
// external one would come from.
|
|
110
|
+
const importedMembers = members.filter((m) => bindings.some((b) => b.local === m && b.kind !== 'local'));
|
|
111
|
+
// No member arrives by import, so there is no convention the new token is
|
|
112
|
+
// breaking — the append leaves the file exactly as consistent as it was.
|
|
113
|
+
if (importedMembers.length === 0)
|
|
114
|
+
return { kind: 'append-only' };
|
|
115
|
+
const template = deriveImportTemplate(sinkContent, members);
|
|
116
|
+
if (!template) {
|
|
117
|
+
return {
|
|
118
|
+
kind: 'refuse',
|
|
119
|
+
detail: `sink ${sinkPath} imports its members, but their import paths are not a function of the member name ` +
|
|
120
|
+
'(a barrel, mixed paths, or an aliased import) — the specifier for this token cannot be derived',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (declaringFile === undefined) {
|
|
124
|
+
return {
|
|
125
|
+
kind: 'refuse',
|
|
126
|
+
detail: `sink ${sinkPath} imports its members and the declaring file of "${token}" is unknown, so the import cannot be verified`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// A named import only works if the target actually EXPORTS the token. The
|
|
130
|
+
// gate may have found it with any extractor (`regex-capture` over `const (\w+)`
|
|
131
|
+
// matches un-exported locals too), so being declared there does not imply
|
|
132
|
+
// being exported from there.
|
|
133
|
+
if (declaringContent !== undefined && !exportsToken(declaringContent, token)) {
|
|
134
|
+
return {
|
|
135
|
+
kind: 'refuse',
|
|
136
|
+
detail: `"${token}" is declared in ${declaringFile} but not exported from it — ` +
|
|
137
|
+
'a named import would not resolve, so the array append is refused too',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const specifier = renderSpecifier(template, token);
|
|
141
|
+
if (!specifierPointsAtDeclaration(sinkPath, specifier, declaringFile)) {
|
|
142
|
+
return {
|
|
143
|
+
kind: 'refuse',
|
|
144
|
+
detail: `sink ${sinkPath} imports its members; the derived specifier "${specifier}" does not resolve to ` +
|
|
145
|
+
`${declaringFile}, where "${token}" is declared — refusing rather than writing a guess`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { kind: 'derived', template, statement: renderImport(template, token) };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Plan the mechanically-unambiguous fixes for `declared-but-not-registered`
|
|
152
|
+
* violations.
|
|
153
|
+
*
|
|
154
|
+
* The bar is deliberately high, because a wrong autofix in a gate is worse than
|
|
155
|
+
* no autofix: the tool would be writing the very thing it is supposed to
|
|
156
|
+
* verify. An edit is planned ONLY when there is exactly one registered sink,
|
|
157
|
+
* that sink is an `array-members` source, its glob resolves to exactly one
|
|
158
|
+
* file, and the anchor array is found exactly once in it. Every other shape is
|
|
159
|
+
* reported as skipped WITH the reason — never guessed at.
|
|
160
|
+
*
|
|
161
|
+
* Pure: the caller supplies file contents and performs any writes.
|
|
162
|
+
*/
|
|
163
|
+
export function planWiringFix(rule, violations, files) {
|
|
164
|
+
const edits = [];
|
|
165
|
+
const skipped = [];
|
|
166
|
+
const declaredMissing = violations.filter((v) => v.ruleId === rule.id && v.direction === 'declared-missing');
|
|
167
|
+
if (declaredMissing.length === 0)
|
|
168
|
+
return { schema: WIRING_FIX_SCHEMA, edits, skipped };
|
|
169
|
+
const refuseAll = (reason, detail) => ({
|
|
170
|
+
schema: WIRING_FIX_SCHEMA,
|
|
171
|
+
edits: [],
|
|
172
|
+
skipped: declaredMissing.map((v) => ({ ruleId: rule.id, token: v.token, reason, detail })),
|
|
173
|
+
});
|
|
174
|
+
// A chain rule has no single "the sink"; refuse rather than pick one.
|
|
175
|
+
const sinks = rule.chain
|
|
176
|
+
? []
|
|
177
|
+
: registeredSources(rule.registered);
|
|
178
|
+
if (sinks.length !== 1) {
|
|
179
|
+
return refuseAll('ambiguous-sink', sinks.length === 0
|
|
180
|
+
? 'rule has no single registered sink (chain rules are never auto-fixed)'
|
|
181
|
+
: `rule has ${sinks.length} registered sinks — which one should the token join?`);
|
|
182
|
+
}
|
|
183
|
+
const sink = sinks[0];
|
|
184
|
+
if (resolveExtractorKind(sink) !== 'array-members') {
|
|
185
|
+
return refuseAll('sink-not-an-array', `sink uses extract "${resolveExtractorKind(sink) ?? 'unknown'}" — only an array literal can be appended to`);
|
|
186
|
+
}
|
|
187
|
+
const anchor = resolveExtractorAnchor(sink);
|
|
188
|
+
if (anchor === undefined)
|
|
189
|
+
return refuseAll('sink-not-an-array', 'sink declares no anchor');
|
|
190
|
+
const sinkFiles = files.filter((f) => matchesAny(f.path, sink.files ?? []));
|
|
191
|
+
if (sinkFiles.length !== 1) {
|
|
192
|
+
return refuseAll('ambiguous-sink-file', `sink glob matched ${sinkFiles.length} files — the insertion point must be unique`);
|
|
193
|
+
}
|
|
194
|
+
const target = sinkFiles[0];
|
|
195
|
+
// Locate the anchor array. More than one occurrence is ambiguous.
|
|
196
|
+
const head = new RegExp(`(?<![\\w$])${escapeRegex(anchor)}\\s*(?::[^=\\n]*)?[:=]\\s*(?:[A-Za-z_$][\\w$.]*\\s*\\(\\s*)?\\[`, 'g');
|
|
197
|
+
const opens = [];
|
|
198
|
+
let m;
|
|
199
|
+
while ((m = head.exec(target.content)) !== null)
|
|
200
|
+
opens.push(m.index + m[0].length - 1);
|
|
201
|
+
if (opens.length !== 1) {
|
|
202
|
+
return refuseAll('array-not-found', opens.length === 0
|
|
203
|
+
? `array \`${anchor}\` not found in ${target.path}`
|
|
204
|
+
: `array \`${anchor}\` appears ${opens.length} times in ${target.path}`);
|
|
205
|
+
}
|
|
206
|
+
const openIndex = opens[0];
|
|
207
|
+
const { elements, end } = scanBalanced(target.content, openIndex);
|
|
208
|
+
// Match the shape of what is already in the array: bare identifiers vs
|
|
209
|
+
// quoted strings. Inserting the wrong shape would compile-fail or silently
|
|
210
|
+
// not match on the next run.
|
|
211
|
+
const firstEl = elements[0]?.text ?? '';
|
|
212
|
+
const quoted = firstEl.startsWith("'") || firstEl.startsWith('"');
|
|
213
|
+
const quoteChar = firstEl.startsWith('"') ? '"' : "'";
|
|
214
|
+
// Build all edits against one evolving buffer so multiple tokens accumulate.
|
|
215
|
+
let content = target.content;
|
|
216
|
+
let insertAt = end;
|
|
217
|
+
for (const v of declaredMissing) {
|
|
218
|
+
// Re-locate the closing bracket after each insertion.
|
|
219
|
+
const reHead = new RegExp(head.source, 'g');
|
|
220
|
+
const mm = reHead.exec(content);
|
|
221
|
+
if (!mm) {
|
|
222
|
+
skipped.push({
|
|
223
|
+
ruleId: rule.id,
|
|
224
|
+
token: v.token,
|
|
225
|
+
reason: 'array-not-found',
|
|
226
|
+
detail: `array \`${anchor}\` vanished mid-plan in ${target.path}`,
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const open = mm.index + mm[0].length - 1;
|
|
231
|
+
const scan = scanBalanced(content, open);
|
|
232
|
+
// Decide the IMPORT before touching the array. An append that leaves the
|
|
233
|
+
// token unbound compiles-fails, and a gate that goes green by breaking the
|
|
234
|
+
// build is worse than one that refuses — so a token we cannot bind is
|
|
235
|
+
// skipped with the reason, never half-written.
|
|
236
|
+
const currentMembers = scan.elements
|
|
237
|
+
.map((el) => elementToken(el.text))
|
|
238
|
+
.filter((t) => t !== undefined);
|
|
239
|
+
const decision = quoted
|
|
240
|
+
? { kind: 'append-only' } // string members bind nothing
|
|
241
|
+
: decideImport(target.path, content, currentMembers, v.token, v.file, v.file ? files.find((f) => f.path === v.file)?.content : undefined);
|
|
242
|
+
if (decision.kind === 'refuse') {
|
|
243
|
+
skipped.push({
|
|
244
|
+
ruleId: rule.id,
|
|
245
|
+
token: v.token,
|
|
246
|
+
reason: 'needs-import',
|
|
247
|
+
detail: decision.detail,
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
insertAt = scan.end;
|
|
252
|
+
const last = scan.elements[scan.elements.length - 1];
|
|
253
|
+
const indent = last ? indentOf(content, last.index) : ' ';
|
|
254
|
+
const multiline = content.slice(open, insertAt).includes('\n');
|
|
255
|
+
const rendered = quoted ? `${quoteChar}${v.token}${quoteChar}` : v.token;
|
|
256
|
+
// Insert after the last non-whitespace character, and KEEP the whitespace
|
|
257
|
+
// run that precedes the closing bracket — trimming it would drag `]` up
|
|
258
|
+
// onto the last element's line and reformat code the user did not ask us
|
|
259
|
+
// to touch.
|
|
260
|
+
const beforeInsert = content.slice(0, insertAt).replace(/\s+$/, '');
|
|
261
|
+
const cut = beforeInsert.length;
|
|
262
|
+
const tail = content.slice(cut, insertAt);
|
|
263
|
+
const needsComma = scan.elements.length > 0 && !beforeInsert.endsWith(',');
|
|
264
|
+
const insert = multiline
|
|
265
|
+
? `${needsComma ? ',' : ''}\n${indent}${rendered},`
|
|
266
|
+
: `${needsComma ? ', ' : ''}${rendered}`;
|
|
267
|
+
content = content.slice(0, cut) + insert + tail + content.slice(insertAt);
|
|
268
|
+
const arrayLine = lineOf(content, cut + insert.length);
|
|
269
|
+
// Add the derived import LAST, so the array line number reported above is
|
|
270
|
+
// computed before the insertion shifts it — the two edits are reported
|
|
271
|
+
// against the file the reviewer will actually see.
|
|
272
|
+
let importInsert;
|
|
273
|
+
let importLine;
|
|
274
|
+
if (decision.kind === 'derived') {
|
|
275
|
+
const inserted = insertImportStatement(content, decision.statement);
|
|
276
|
+
if (!inserted) {
|
|
277
|
+
// Unreachable in practice (a template implies an import exists), but a
|
|
278
|
+
// planner that silently dropped the import would reintroduce the bug.
|
|
279
|
+
skipped.push({
|
|
280
|
+
ruleId: rule.id,
|
|
281
|
+
token: v.token,
|
|
282
|
+
reason: 'needs-import',
|
|
283
|
+
detail: `could not locate an import block in ${target.path} to extend`,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
content = inserted.nextContent;
|
|
288
|
+
importInsert = decision.statement;
|
|
289
|
+
importLine = inserted.line;
|
|
290
|
+
}
|
|
291
|
+
edits.push({
|
|
292
|
+
ruleId: rule.id,
|
|
293
|
+
token: v.token,
|
|
294
|
+
file: target.path,
|
|
295
|
+
line: arrayLine,
|
|
296
|
+
insert: insert.trim(),
|
|
297
|
+
nextContent: content,
|
|
298
|
+
...(importInsert !== undefined ? { importInsert, importLine: importLine } : {}),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return { schema: WIRING_FIX_SCHEMA, edits, skipped };
|
|
302
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IRegistrationIdiom } from '@shrkcrft/core';
|
|
2
2
|
import { type IWiringFileEntry } from './evaluate-wiring.js';
|
|
3
3
|
export declare const REGISTRATION_GRAPH_SCHEMA: "sharkcraft.registration-graph/v1";
|
|
4
4
|
/** A token role-site: which idiom + file:line it was found at. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registration-graph.d.ts","sourceRoot":"","sources":["../../src/wiring/registration-graph.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"registration-graph.d.ts","sourceRoot":"","sources":["../../src/wiring/registration-graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import * as nodePath from 'node:path';
|
|
4
|
+
import { resolveSourceGlobs } from '@shrkcrft/core';
|
|
4
5
|
import { matchesAny } from "../scan/glob.js";
|
|
5
6
|
import { readMatchingFiles, walkMatching } from "../util/walk-files.js";
|
|
6
7
|
import { collectSourceSites } from "./evaluate-wiring.js";
|
|
@@ -11,7 +12,11 @@ function sortSites(sites) {
|
|
|
11
12
|
/** Union (deduped) of every glob any role of any idiom references. */
|
|
12
13
|
function registrationGlobs(idioms) {
|
|
13
14
|
return [
|
|
14
|
-
...new Set(idioms.flatMap((i) => [
|
|
15
|
+
...new Set(idioms.flatMap((i) => [
|
|
16
|
+
...resolveSourceGlobs(i.declared),
|
|
17
|
+
...resolveSourceGlobs(i.provided),
|
|
18
|
+
...resolveSourceGlobs(i.consumed),
|
|
19
|
+
])),
|
|
15
20
|
];
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
@@ -28,7 +33,7 @@ export function buildRegistrationGraph(projectRoot, idioms, options = {}) {
|
|
|
28
33
|
path,
|
|
29
34
|
content,
|
|
30
35
|
}));
|
|
31
|
-
const filesFor = (source) => entries.filter((f) => matchesAny(f.path, source
|
|
36
|
+
const filesFor = (source) => entries.filter((f) => matchesAny(f.path, resolveSourceGlobs(source)));
|
|
32
37
|
const declared = new Map();
|
|
33
38
|
const provided = new Map();
|
|
34
39
|
const consumed = new Map();
|
|
@@ -96,7 +101,7 @@ export function registrationGraphSignature(projectRoot, idioms, options = {}) {
|
|
|
96
101
|
export function providedTokensFromEntries(idioms, entries) {
|
|
97
102
|
const tokens = new Set();
|
|
98
103
|
for (const idiom of idioms) {
|
|
99
|
-
const files = entries.filter((f) => matchesAny(f.path, idiom.provided
|
|
104
|
+
const files = entries.filter((f) => matchesAny(f.path, resolveSourceGlobs(idiom.provided)));
|
|
100
105
|
for (const s of collectSourceSites(idiom.provided, files).sites)
|
|
101
106
|
tokens.add(s.token);
|
|
102
107
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IRegistryDeclaration } from '@shrkcrft/core';
|
|
2
2
|
export declare const REGISTRY_SCHEMA: "sharkcraft.registry-inventory/v1";
|
|
3
3
|
/** Where an id was found (project-relative path + 1-based line). */
|
|
4
4
|
export interface IRegistrySite {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry-query.d.ts","sourceRoot":"","sources":["../../src/wiring/registry-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"registry-query.d.ts","sourceRoot":"","sources":["../../src/wiring/registry-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAK/E,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,CAkDpB;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;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,kBAAkB,GAAG,SAAS,cAAc,EAAE,CAE3F"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveSourceGlobs } from '@shrkcrft/core';
|
|
1
2
|
import { matchesAny } from "../scan/glob.js";
|
|
2
3
|
import { readMatchingFiles } from "../util/walk-files.js";
|
|
3
4
|
import { collectSourceSites } from "./evaluate-wiring.js";
|
|
@@ -12,18 +13,21 @@ function sortSites(sites) {
|
|
|
12
13
|
*/
|
|
13
14
|
export function scanRegistry(projectRoot, decl, options = {}) {
|
|
14
15
|
const globs = [
|
|
15
|
-
...new Set([
|
|
16
|
+
...new Set([
|
|
17
|
+
...resolveSourceGlobs(decl.source),
|
|
18
|
+
...(decl.consumer ? resolveSourceGlobs(decl.consumer) : []),
|
|
19
|
+
]),
|
|
16
20
|
];
|
|
17
21
|
const cache = readMatchingFiles(projectRoot, globs, new Set(options.excludeDirs ?? []));
|
|
18
22
|
const entries = [...cache.entries()].map(([path, content]) => ({ path, content }));
|
|
19
23
|
const diagnostics = [];
|
|
20
|
-
const sourceFiles = entries.filter((f) => matchesAny(f.path, decl.source
|
|
24
|
+
const sourceFiles = entries.filter((f) => matchesAny(f.path, resolveSourceGlobs(decl.source)));
|
|
21
25
|
const declared = collectSourceSites(decl.source, sourceFiles);
|
|
22
26
|
if (declared.error)
|
|
23
27
|
diagnostics.push(`registry "${decl.name}" source: ${declared.error}`);
|
|
24
28
|
const consumerByToken = new Map();
|
|
25
29
|
if (decl.consumer) {
|
|
26
|
-
const consumerFiles = entries.filter((f) => matchesAny(f.path, decl.consumer
|
|
30
|
+
const consumerFiles = entries.filter((f) => matchesAny(f.path, resolveSourceGlobs(decl.consumer)));
|
|
27
31
|
const consumed = collectSourceSites(decl.consumer, consumerFiles);
|
|
28
32
|
if (consumed.error)
|
|
29
33
|
diagnostics.push(`registry "${decl.name}" consumer: ${consumed.error}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IWiringRule } from '@shrkcrft/core';
|
|
2
2
|
import { type IWiringReport } from './evaluate-wiring.js';
|
|
3
3
|
export interface IRunWiringOptions {
|
|
4
4
|
/** Only run rules touched by these (project-relative) changed files. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan-wiring-files.d.ts","sourceRoot":"","sources":["../../src/wiring/scan-wiring-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"scan-wiring-files.d.ts","sourceRoot":"","sources":["../../src/wiring/scan-wiring-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAItE,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAO9B,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,CAqCf"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { resolveSourceGlobs } from '@shrkcrft/core';
|
|
1
2
|
import { matchesAny } from "../scan/glob.js";
|
|
2
3
|
import { readMatchingFiles } from "../util/walk-files.js";
|
|
4
|
+
import { loadTsconfigPaths } from "../scan/tsconfig-aliases.js";
|
|
3
5
|
import { evaluateWiring, wiringGlobsOf, } from "./evaluate-wiring.js";
|
|
4
6
|
/** All file globs a rule references: every side / every hop. */
|
|
5
7
|
function ruleGlobs(rule) {
|
|
@@ -38,5 +40,7 @@ export function runWiring(projectRoot, rules, options = {}) {
|
|
|
38
40
|
const allGlobs = [...new Set(selected.flatMap(ruleGlobs))];
|
|
39
41
|
const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
|
|
40
42
|
const entries = [...cache.entries()].map(([path, content]) => ({ path, content }));
|
|
41
|
-
|
|
43
|
+
// The tsconfig paths let an `import-edges` source resolve alias specifiers the
|
|
44
|
+
// way the compiler does — the same map `check boundaries` resolves with.
|
|
45
|
+
return evaluateWiring(selected, (source) => entries.filter((f) => matchesAny(f.path, resolveSourceGlobs(source))), { tsconfigPaths: loadTsconfigPaths(projectRoot) });
|
|
42
46
|
}
|