@shrkcrft/boundaries 0.1.0-alpha.28 → 0.1.0-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/baseline/compute-baseline.d.ts.map +1 -1
  2. package/dist/baseline/compute-baseline.js +2 -1
  3. package/dist/extract/extract-tokens.d.ts +19 -1
  4. package/dist/extract/extract-tokens.d.ts.map +1 -1
  5. package/dist/extract/extract-tokens.js +53 -1
  6. package/dist/extract/import-edges.d.ts +49 -0
  7. package/dist/extract/import-edges.d.ts.map +1 -0
  8. package/dist/extract/import-edges.js +157 -0
  9. package/dist/extract/inspect-source.d.ts +6 -0
  10. package/dist/extract/inspect-source.d.ts.map +1 -1
  11. package/dist/extract/inspect-source.js +6 -1
  12. package/dist/extract/parse-imports.d.ts +58 -0
  13. package/dist/extract/parse-imports.d.ts.map +1 -0
  14. package/dist/extract/parse-imports.js +155 -0
  15. package/dist/generated/check-provenance.d.ts +20 -2
  16. package/dist/generated/check-provenance.d.ts.map +1 -1
  17. package/dist/generated/check-provenance.js +24 -5
  18. package/dist/generated/scan-generated.d.ts +49 -3
  19. package/dist/generated/scan-generated.d.ts.map +1 -1
  20. package/dist/generated/scan-generated.js +90 -5
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -0
  24. package/dist/policy/evaluate-policy.d.ts +1 -1
  25. package/dist/policy/evaluate-policy.d.ts.map +1 -1
  26. package/dist/policy/evaluate-policy.js +2 -1
  27. package/dist/util/walk-files.d.ts +21 -2
  28. package/dist/util/walk-files.d.ts.map +1 -1
  29. package/dist/util/walk-files.js +85 -4
  30. package/dist/wiring/evaluate-wiring.d.ts +10 -3
  31. package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
  32. package/dist/wiring/evaluate-wiring.js +12 -7
  33. package/dist/wiring/explain-wiring.d.ts +7 -9
  34. package/dist/wiring/explain-wiring.d.ts.map +1 -1
  35. package/dist/wiring/explain-wiring.js +18 -0
  36. package/dist/wiring/plan-wiring-fix.d.ts +75 -0
  37. package/dist/wiring/plan-wiring-fix.d.ts.map +1 -0
  38. package/dist/wiring/plan-wiring-fix.js +302 -0
  39. package/dist/wiring/registration-graph.d.ts +1 -1
  40. package/dist/wiring/registration-graph.d.ts.map +1 -1
  41. package/dist/wiring/registration-graph.js +8 -3
  42. package/dist/wiring/registry-query.d.ts +1 -1
  43. package/dist/wiring/registry-query.d.ts.map +1 -1
  44. package/dist/wiring/registry-query.js +7 -3
  45. package/dist/wiring/scan-wiring-files.d.ts +1 -1
  46. package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
  47. package/dist/wiring/scan-wiring-files.js +5 -1
  48. package/dist/wiring/sink-imports.d.ts +84 -0
  49. package/dist/wiring/sink-imports.d.ts.map +1 -0
  50. package/dist/wiring/sink-imports.js +115 -0
  51. package/package.json +2 -2
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Import analysis for a wiring SINK file.
3
+ *
4
+ * `check wiring --fix` appends a token to a registry array. That edit is only
5
+ * safe when the token actually RESOLVES in the sink file. The common registry
6
+ * shape imports every member:
7
+ *
8
+ * ```ts
9
+ * import { ALPHA_HANDLER } from './ALPHA_HANDLER';
10
+ * export const HANDLERS = [ALPHA_HANDLER];
11
+ * ```
12
+ *
13
+ * Appending `NEW_HANDLER` to that array turns the wiring gate GREEN while
14
+ * leaving the file referencing an unbound symbol — it no longer compiles. A
15
+ * gate that goes green by breaking the build is the exact inverse of the
16
+ * point, so the planner has to know what the sink file binds before it writes.
17
+ *
18
+ * This is a lexer, not a compiler: it answers "is this name bound in this
19
+ * file?" and "do the existing members follow one derivable import pattern?" —
20
+ * both of which are decidable from the text, and neither of which is guessed.
21
+ */
22
+ /** How a name entered the file's scope. */
23
+ export type SinkBindingKind = 'named-import' | 'default-import' | 'namespace-import' | 'local';
24
+ /** One value binding visible in the sink file. */
25
+ export interface ISinkBinding {
26
+ /** The name as used in the file (the local name, after any `as` alias). */
27
+ readonly local: string;
28
+ readonly kind: SinkBindingKind;
29
+ /** The exported name, for a named import (differs from `local` when aliased). */
30
+ readonly imported?: string;
31
+ /** Module specifier, for an import binding. */
32
+ readonly specifier?: string;
33
+ }
34
+ /**
35
+ * Every value name bound in the file: imports plus its own top-level
36
+ * declarations.
37
+ *
38
+ * Local declarations count because a registry that declares its members inline
39
+ * needs no import at all — refusing to fix those would be a false refusal.
40
+ */
41
+ export declare function collectSinkBindings(content: string): ISinkBinding[];
42
+ /** Whether `name` already resolves in the sink file. */
43
+ export declare function isBoundInSink(content: string, name: string): boolean;
44
+ /** A uniform import pattern the existing members all follow. */
45
+ export interface IImportTemplate {
46
+ /** The specifier with every occurrence of the member name replaced by `{}`. */
47
+ readonly specifierTemplate: string;
48
+ /** Quote character used by the existing imports. */
49
+ readonly quote: string;
50
+ /** Whether existing import statements end in a semicolon. */
51
+ readonly semicolon: boolean;
52
+ /** How many members the template was derived from. */
53
+ readonly derivedFrom: number;
54
+ }
55
+ /**
56
+ * Derive the import pattern the sink's existing members follow, if there is
57
+ * exactly one.
58
+ *
59
+ * Derivation succeeds only when the specifier is a pure FUNCTION OF THE MEMBER
60
+ * NAME — every member `M` imported as `import { M } from '<something>M<something>'`
61
+ * with the same surrounding text. A barrel (`from './handlers'`), mixed paths,
62
+ * or an aliased import make the next specifier unknowable, and the planner must
63
+ * never invent a path: those return `undefined` so the caller refuses instead.
64
+ */
65
+ export declare function deriveImportTemplate(content: string, members: readonly string[]): IImportTemplate | undefined;
66
+ /** Render the import statement this template produces for `token`. */
67
+ export declare function renderImport(template: IImportTemplate, token: string): string;
68
+ /** The specifier a template yields for `token`, without the surrounding syntax. */
69
+ export declare function renderSpecifier(template: IImportTemplate, token: string): string;
70
+ /** Where a new import line goes, and the resulting content. */
71
+ export interface IImportInsertion {
72
+ readonly nextContent: string;
73
+ /** 1-based line the inserted statement lands on. */
74
+ readonly line: number;
75
+ }
76
+ /**
77
+ * Insert an import statement after the LAST existing import.
78
+ *
79
+ * Appending to the import block (rather than sorting into it) keeps the edit a
80
+ * pure insertion: no existing line moves, so the diff shows exactly one added
81
+ * line and a reviewer can see the whole change at a glance.
82
+ */
83
+ export declare function insertImportStatement(content: string, statement: string): IImportInsertion | undefined;
84
+ //# sourceMappingURL=sink-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sink-imports.d.ts","sourceRoot":"","sources":["../../src/wiring/sink-imports.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,OAAO,CAAC;AAE/F,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAMD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CAsBnE;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,eAAe,GAAG,SAAS,CA6B7B;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG7E;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,gBAAgB,GAAG,SAAS,CAe9B"}
@@ -0,0 +1,115 @@
1
+ import { lineOf } from "../extract/scan-literals.js";
2
+ import { parseImportStatements } from "../extract/parse-imports.js";
3
+ /** Top-level value/type declarations — the names a file binds itself. */
4
+ const LOCAL_DECL = /(?:^|\n)\s*(?:export\s+)?(?:declare\s+)?(?:default\s+)?(?:const|let|var|function\s*\*?|class|enum|interface|type)\s+([A-Za-z_$][\w$]*)/g;
5
+ /**
6
+ * Every value name bound in the file: imports plus its own top-level
7
+ * declarations.
8
+ *
9
+ * Local declarations count because a registry that declares its members inline
10
+ * needs no import at all — refusing to fix those would be a false refusal.
11
+ */
12
+ export function collectSinkBindings(content) {
13
+ const bindings = [];
14
+ for (const statement of parseImportStatements(content)) {
15
+ // A type-only import binds no VALUE, so it can never make an array element
16
+ // resolve. Counting it would let the planner write a reference that erases
17
+ // at compile time.
18
+ if (statement.typeOnly)
19
+ continue;
20
+ for (const b of statement.bindings) {
21
+ bindings.push({
22
+ local: b.local,
23
+ kind: `${b.kind}-import`,
24
+ ...(b.imported !== undefined ? { imported: b.imported } : {}),
25
+ specifier: statement.specifier,
26
+ });
27
+ }
28
+ }
29
+ LOCAL_DECL.lastIndex = 0;
30
+ let m;
31
+ while ((m = LOCAL_DECL.exec(content)) !== null) {
32
+ bindings.push({ local: m[1], kind: 'local' });
33
+ }
34
+ return bindings;
35
+ }
36
+ /** Whether `name` already resolves in the sink file. */
37
+ export function isBoundInSink(content, name) {
38
+ return collectSinkBindings(content).some((b) => b.local === name);
39
+ }
40
+ /**
41
+ * Derive the import pattern the sink's existing members follow, if there is
42
+ * exactly one.
43
+ *
44
+ * Derivation succeeds only when the specifier is a pure FUNCTION OF THE MEMBER
45
+ * NAME — every member `M` imported as `import { M } from '<something>M<something>'`
46
+ * with the same surrounding text. A barrel (`from './handlers'`), mixed paths,
47
+ * or an aliased import make the next specifier unknowable, and the planner must
48
+ * never invent a path: those return `undefined` so the caller refuses instead.
49
+ */
50
+ export function deriveImportTemplate(content, members) {
51
+ const bindings = collectSinkBindings(content);
52
+ const byLocal = new Map(bindings.map((b) => [b.local, b]));
53
+ let template;
54
+ let derivedFrom = 0;
55
+ for (const member of members) {
56
+ const binding = byLocal.get(member);
57
+ if (!binding || binding.kind === 'local')
58
+ continue;
59
+ // An alias means the local name is NOT the exported name, so the next
60
+ // token's exported name cannot be derived from the name we would write.
61
+ if (binding.kind !== 'named-import' || binding.imported !== binding.local)
62
+ return undefined;
63
+ if (!binding.specifier || !binding.specifier.includes(member))
64
+ return undefined;
65
+ const candidate = binding.specifier.split(member).join('{}');
66
+ if (template === undefined)
67
+ template = candidate;
68
+ else if (template !== candidate)
69
+ return undefined;
70
+ derivedFrom += 1;
71
+ }
72
+ if (template === undefined || derivedFrom === 0)
73
+ return undefined;
74
+ // Mirror the file's existing punctuation so the inserted line does not fight
75
+ // the formatter that will run over it next.
76
+ const raw = parseImportStatements(content).find((st) => st.kind === 'import')?.raw ?? '';
77
+ return {
78
+ specifierTemplate: template,
79
+ quote: raw.includes('"') ? '"' : "'",
80
+ semicolon: raw.trimEnd().endsWith(';'),
81
+ derivedFrom,
82
+ };
83
+ }
84
+ /** Render the import statement this template produces for `token`. */
85
+ export function renderImport(template, token) {
86
+ const specifier = template.specifierTemplate.split('{}').join(token);
87
+ return `import { ${token} } from ${template.quote}${specifier}${template.quote}${template.semicolon ? ';' : ''}`;
88
+ }
89
+ /** The specifier a template yields for `token`, without the surrounding syntax. */
90
+ export function renderSpecifier(template, token) {
91
+ return template.specifierTemplate.split('{}').join(token);
92
+ }
93
+ /**
94
+ * Insert an import statement after the LAST existing import.
95
+ *
96
+ * Appending to the import block (rather than sorting into it) keeps the edit a
97
+ * pure insertion: no existing line moves, so the diff shows exactly one added
98
+ * line and a reviewer can see the whole change at a glance.
99
+ */
100
+ export function insertImportStatement(content, statement) {
101
+ // Only the STATIC import block is a valid insertion point: landing after a
102
+ // dynamic `import()` deep in the file would put a top-level import in the
103
+ // middle of a function body.
104
+ const statements = parseImportStatements(content).filter((st) => st.kind === 'import');
105
+ const last = statements[statements.length - 1];
106
+ if (!last)
107
+ return undefined;
108
+ const endOfLast = last.index + last.raw.length;
109
+ // Land immediately after the statement's own newline so the insertion never
110
+ // splits a line, whatever trails the specifier.
111
+ const newline = content.indexOf('\n', endOfLast);
112
+ const at = newline === -1 ? content.length : newline + 1;
113
+ const nextContent = content.slice(0, at) + statement + '\n' + content.slice(at);
114
+ return { nextContent, line: lineOf(nextContent, at) };
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/boundaries",
3
- "version": "0.1.0-alpha.28",
3
+ "version": "0.1.0-alpha.30",
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.28"
46
+ "@shrkcrft/core": "^0.1.0-alpha.30"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public"