@shrkcrft/boundaries 0.1.0-alpha.29 → 0.1.0-alpha.31

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 (229) hide show
  1. package/dist/baseline/compute-baseline.d.ts +8 -0
  2. package/dist/baseline/compute-baseline.d.ts.map +1 -1
  3. package/dist/baseline/compute-baseline.js +10 -8
  4. package/dist/baseline/diff-baseline.d.ts +28 -0
  5. package/dist/baseline/diff-baseline.d.ts.map +1 -1
  6. package/dist/baseline/diff-baseline.js +29 -0
  7. package/dist/evaluate/boundary-unit-finding.d.ts +10 -0
  8. package/dist/evaluate/boundary-unit-finding.d.ts.map +1 -0
  9. package/dist/evaluate/boundary-unit-finding.js +21 -0
  10. package/dist/evaluate/boundary-unit-kind.d.ts +7 -0
  11. package/dist/evaluate/boundary-unit-kind.d.ts.map +1 -0
  12. package/dist/evaluate/boundary-unit-kind.js +14 -0
  13. package/dist/evaluate/evaluate-boundaries.d.ts +230 -3
  14. package/dist/evaluate/evaluate-boundaries.d.ts.map +1 -1
  15. package/dist/evaluate/evaluate-boundaries.js +481 -53
  16. package/dist/evaluate/i-boundary-rule-settle-input.d.ts +27 -0
  17. package/dist/evaluate/i-boundary-rule-settle-input.d.ts.map +1 -0
  18. package/dist/evaluate/i-boundary-rule-settle-input.js +1 -0
  19. package/dist/evaluate/i-boundary-rule-settlement.d.ts +23 -0
  20. package/dist/evaluate/i-boundary-rule-settlement.d.ts.map +1 -0
  21. package/dist/evaluate/i-boundary-rule-settlement.js +1 -0
  22. package/dist/evaluate/i-boundary-unit-finding.d.ts +22 -0
  23. package/dist/evaluate/i-boundary-unit-finding.d.ts.map +1 -0
  24. package/dist/evaluate/i-boundary-unit-finding.js +1 -0
  25. package/dist/evaluate/settle-boundary-rule.d.ts +23 -0
  26. package/dist/evaluate/settle-boundary-rule.d.ts.map +1 -0
  27. package/dist/evaluate/settle-boundary-rule.js +73 -0
  28. package/dist/evaluate/with-boundary-rule-settlement.d.ts +11 -0
  29. package/dist/evaluate/with-boundary-rule-settlement.d.ts.map +1 -0
  30. package/dist/evaluate/with-boundary-rule-settlement.js +44 -0
  31. package/dist/extract/code-zones.d.ts +95 -3
  32. package/dist/extract/code-zones.d.ts.map +1 -1
  33. package/dist/extract/code-zones.js +313 -4
  34. package/dist/extract/extract-tokens.d.ts +49 -1
  35. package/dist/extract/extract-tokens.d.ts.map +1 -1
  36. package/dist/extract/extract-tokens.js +283 -25
  37. package/dist/extract/i-labeled-source.d.ts +12 -0
  38. package/dist/extract/i-labeled-source.d.ts.map +1 -0
  39. package/dist/extract/i-labeled-source.js +1 -0
  40. package/dist/extract/import-edges.d.ts +50 -0
  41. package/dist/extract/import-edges.d.ts.map +1 -0
  42. package/dist/extract/import-edges.js +170 -0
  43. package/dist/extract/inspect-source.d.ts +38 -3
  44. package/dist/extract/inspect-source.d.ts.map +1 -1
  45. package/dist/extract/inspect-source.js +56 -8
  46. package/dist/extract/parse-imports.d.ts +80 -0
  47. package/dist/extract/parse-imports.d.ts.map +1 -0
  48. package/dist/extract/parse-imports.js +254 -0
  49. package/dist/extract/scan-literals.d.ts +12 -1
  50. package/dist/extract/scan-literals.d.ts.map +1 -1
  51. package/dist/extract/scan-literals.js +15 -1
  52. package/dist/extract/source-liveness-request.d.ts +24 -0
  53. package/dist/extract/source-liveness-request.d.ts.map +1 -0
  54. package/dist/extract/source-liveness-request.js +45 -0
  55. package/dist/generated/check-provenance.d.ts +20 -2
  56. package/dist/generated/check-provenance.d.ts.map +1 -1
  57. package/dist/generated/check-provenance.js +27 -6
  58. package/dist/generated/read-regen-tree.d.ts +16 -0
  59. package/dist/generated/read-regen-tree.d.ts.map +1 -0
  60. package/dist/generated/read-regen-tree.js +61 -0
  61. package/dist/generated/scan-generated.d.ts +57 -3
  62. package/dist/generated/scan-generated.d.ts.map +1 -1
  63. package/dist/generated/scan-generated.js +111 -11
  64. package/dist/index.d.ts +51 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +54 -0
  67. package/dist/model/boundary-intended-empty-line.d.ts +10 -0
  68. package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
  69. package/dist/model/boundary-intended-empty-line.js +11 -0
  70. package/dist/model/boundary-markable-list.d.ts +15 -0
  71. package/dist/model/boundary-markable-list.d.ts.map +1 -0
  72. package/dist/model/boundary-markable-list.js +15 -0
  73. package/dist/model/boundary-rule-input-keys.d.ts +11 -0
  74. package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
  75. package/dist/model/boundary-rule-input-keys.js +29 -0
  76. package/dist/model/boundary-rule-input.d.ts +23 -0
  77. package/dist/model/boundary-rule-input.d.ts.map +1 -0
  78. package/dist/model/boundary-rule-input.js +1 -0
  79. package/dist/model/boundary-rule-key-problems.d.ts +11 -0
  80. package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
  81. package/dist/model/boundary-rule-key-problems.js +36 -0
  82. package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
  83. package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
  84. package/dist/model/boundary-rule-marker-problems.js +94 -0
  85. package/dist/model/boundary-rule-scope.d.ts +88 -0
  86. package/dist/model/boundary-rule-scope.d.ts.map +1 -0
  87. package/dist/model/boundary-rule-scope.js +175 -0
  88. package/dist/model/boundary-rule.d.ts +120 -5
  89. package/dist/model/boundary-rule.d.ts.map +1 -1
  90. package/dist/model/boundary-rule.js +148 -1
  91. package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
  92. package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
  93. package/dist/model/boundary-unit-problem-issue.js +16 -0
  94. package/dist/model/normalize-boundary-rule.d.ts +21 -0
  95. package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
  96. package/dist/model/normalize-boundary-rule.js +62 -0
  97. package/dist/policy/evaluate-policy.d.ts +80 -3
  98. package/dist/policy/evaluate-policy.d.ts.map +1 -1
  99. package/dist/policy/evaluate-policy.js +152 -19
  100. package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
  101. package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
  102. package/dist/policy/i-policy-rule-liveness.js +1 -0
  103. package/dist/policy/run-policy.d.ts +1 -1
  104. package/dist/policy/run-policy.d.ts.map +1 -1
  105. package/dist/policy/run-policy.js +171 -21
  106. package/dist/registry/load-boundary-rules.d.ts +29 -1
  107. package/dist/registry/load-boundary-rules.d.ts.map +1 -1
  108. package/dist/registry/load-boundary-rules.js +38 -8
  109. package/dist/scan/glob.d.ts +90 -0
  110. package/dist/scan/glob.d.ts.map +1 -1
  111. package/dist/scan/glob.js +205 -0
  112. package/dist/scan/i-glob-unit-measure.d.ts +22 -0
  113. package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
  114. package/dist/scan/i-glob-unit-measure.js +1 -0
  115. package/dist/scan/import-pattern.d.ts +109 -0
  116. package/dist/scan/import-pattern.d.ts.map +1 -0
  117. package/dist/scan/import-pattern.js +191 -0
  118. package/dist/scan/node-builtin-package-names.d.ts +13 -0
  119. package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
  120. package/dist/scan/node-builtin-package-names.js +25 -0
  121. package/dist/scan/scan-imports.d.ts +36 -3
  122. package/dist/scan/scan-imports.d.ts.map +1 -1
  123. package/dist/scan/scan-imports.js +114 -45
  124. package/dist/util/blank-run-hazard-finding.d.ts +34 -0
  125. package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
  126. package/dist/util/blank-run-hazard-finding.js +1 -0
  127. package/dist/util/blank-run-hazard.d.ts +29 -0
  128. package/dist/util/blank-run-hazard.d.ts.map +1 -0
  129. package/dist/util/blank-run-hazard.js +450 -0
  130. package/dist/util/dead-glob-units.d.ts +46 -0
  131. package/dist/util/dead-glob-units.d.ts.map +1 -0
  132. package/dist/util/dead-glob-units.js +116 -0
  133. package/dist/util/glob-list-liveness-input.d.ts +31 -0
  134. package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
  135. package/dist/util/glob-list-liveness-input.js +65 -0
  136. package/dist/util/i-dead-glob-unit.d.ts +24 -0
  137. package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
  138. package/dist/util/i-dead-glob-unit.js +1 -0
  139. package/dist/util/i-glob-list-units.d.ts +25 -0
  140. package/dist/util/i-glob-list-units.d.ts.map +1 -0
  141. package/dist/util/i-glob-list-units.js +1 -0
  142. package/dist/util/i-glob-liveness-list.d.ts +23 -0
  143. package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
  144. package/dist/util/i-glob-liveness-list.js +1 -0
  145. package/dist/util/i-glob-liveness-request.d.ts +11 -0
  146. package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
  147. package/dist/util/i-glob-liveness-request.js +1 -0
  148. package/dist/util/i-glob-negation.d.ts +13 -0
  149. package/dist/util/i-glob-negation.d.ts.map +1 -0
  150. package/dist/util/i-glob-negation.js +1 -0
  151. package/dist/util/matched-files.d.ts +22 -0
  152. package/dist/util/matched-files.d.ts.map +1 -0
  153. package/dist/util/matched-files.js +1 -0
  154. package/dist/util/negation-cause.d.ts +15 -0
  155. package/dist/util/negation-cause.d.ts.map +1 -0
  156. package/dist/util/negation-cause.js +21 -0
  157. package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
  158. package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
  159. package/dist/util/plane-scan-exclude-dirs.js +22 -0
  160. package/dist/util/read-glob-list-liveness.d.ts +20 -0
  161. package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
  162. package/dist/util/read-glob-list-liveness.js +20 -0
  163. package/dist/util/read-scope-coverage.d.ts +90 -0
  164. package/dist/util/read-scope-coverage.d.ts.map +1 -0
  165. package/dist/util/read-scope-coverage.js +173 -0
  166. package/dist/util/read-scope.d.ts +14 -0
  167. package/dist/util/read-scope.d.ts.map +1 -0
  168. package/dist/util/read-scope.js +1 -0
  169. package/dist/util/read-selected-files.d.ts +16 -0
  170. package/dist/util/read-selected-files.d.ts.map +1 -0
  171. package/dist/util/read-selected-files.js +25 -0
  172. package/dist/util/settle-glob-lists.d.ts +12 -0
  173. package/dist/util/settle-glob-lists.d.ts.map +1 -0
  174. package/dist/util/settle-glob-lists.js +13 -0
  175. package/dist/util/unread-file-reason.d.ts +26 -0
  176. package/dist/util/unread-file-reason.d.ts.map +1 -0
  177. package/dist/util/unread-file-reason.js +26 -0
  178. package/dist/util/unread-file.d.ts +10 -0
  179. package/dist/util/unread-file.d.ts.map +1 -0
  180. package/dist/util/unread-file.js +1 -0
  181. package/dist/util/walk-files.d.ts +90 -7
  182. package/dist/util/walk-files.d.ts.map +1 -1
  183. package/dist/util/walk-files.js +206 -19
  184. package/dist/wiring/evaluate-wiring.d.ts +88 -7
  185. package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
  186. package/dist/wiring/evaluate-wiring.js +276 -16
  187. package/dist/wiring/explain-wiring.d.ts +56 -14
  188. package/dist/wiring/explain-wiring.d.ts.map +1 -1
  189. package/dist/wiring/explain-wiring.js +136 -10
  190. package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
  191. package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
  192. package/dist/wiring/i-idiom-role-coverage.js +1 -0
  193. package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
  194. package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
  195. package/dist/wiring/i-registration-query-verdict.js +1 -0
  196. package/dist/wiring/i-registration-roles.d.ts +71 -0
  197. package/dist/wiring/i-registration-roles.d.ts.map +1 -0
  198. package/dist/wiring/i-registration-roles.js +1 -0
  199. package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
  200. package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
  201. package/dist/wiring/measure-idiom-role-coverage.js +20 -0
  202. package/dist/wiring/measure-registration-roles.d.ts +25 -0
  203. package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
  204. package/dist/wiring/measure-registration-roles.js +124 -0
  205. package/dist/wiring/plan-wiring-fix.d.ts +16 -1
  206. package/dist/wiring/plan-wiring-fix.d.ts.map +1 -1
  207. package/dist/wiring/plan-wiring-fix.js +177 -4
  208. package/dist/wiring/registration-graph.d.ts +13 -1
  209. package/dist/wiring/registration-graph.d.ts.map +1 -1
  210. package/dist/wiring/registration-graph.js +22 -6
  211. package/dist/wiring/registration-query-verdict.d.ts +23 -0
  212. package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
  213. package/dist/wiring/registration-query-verdict.js +118 -0
  214. package/dist/wiring/registry-query.d.ts +9 -1
  215. package/dist/wiring/registry-query.d.ts.map +1 -1
  216. package/dist/wiring/registry-query.js +17 -6
  217. package/dist/wiring/scan-wiring-files.d.ts +5 -3
  218. package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
  219. package/dist/wiring/scan-wiring-files.js +55 -14
  220. package/dist/wiring/sink-imports.d.ts +95 -0
  221. package/dist/wiring/sink-imports.d.ts.map +1 -0
  222. package/dist/wiring/sink-imports.js +124 -0
  223. package/dist/wiring/trace-literal.d.ts +6 -0
  224. package/dist/wiring/trace-literal.d.ts.map +1 -1
  225. package/dist/wiring/trace-literal.js +5 -2
  226. package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
  227. package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
  228. package/dist/wiring/wiring-labeled-sources.js +20 -0
  229. package/package.json +2 -2
@@ -1,6 +1,8 @@
1
- import { matchesAny } from "../scan/glob.js";
2
- import { escapeRegex, scanBalanced } from "../extract/scan-literals.js";
1
+ import { globListSelects } from "../scan/glob.js";
2
+ import { elementToken, escapeRegex, scanBalanced } from "../extract/scan-literals.js";
3
+ import { extractTokens } from "../extract/extract-tokens.js";
3
4
  import { registeredSources } from "./evaluate-wiring.js";
5
+ import { collectSinkBindings, deriveImportTemplate, insertImportStatement, renderImport, renderSpecifier, } from "./sink-imports.js";
4
6
  import { resolveExtractorAnchor, resolveExtractorKind } from '@shrkcrft/core';
5
7
  export const WIRING_FIX_SCHEMA = 'sharkcraft.wiring-fix/v1';
6
8
  /** Indentation of the line containing `index`. */
@@ -17,6 +19,134 @@ function lineOf(content, index) {
17
19
  }
18
20
  return line;
19
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
+ }
20
150
  /**
21
151
  * Plan the mechanically-unambiguous fixes for `declared-but-not-registered`
22
152
  * violations.
@@ -57,7 +187,7 @@ export function planWiringFix(rule, violations, files) {
57
187
  const anchor = resolveExtractorAnchor(sink);
58
188
  if (anchor === undefined)
59
189
  return refuseAll('sink-not-an-array', 'sink declares no anchor');
60
- const sinkFiles = files.filter((f) => matchesAny(f.path, sink.files ?? []));
190
+ const sinkFiles = files.filter((f) => globListSelects(f.path, sink.files ?? []));
61
191
  if (sinkFiles.length !== 1) {
62
192
  return refuseAll('ambiguous-sink-file', `sink glob matched ${sinkFiles.length} files — the insertion point must be unique`);
63
193
  }
@@ -99,6 +229,25 @@ export function planWiringFix(rule, violations, files) {
99
229
  }
100
230
  const open = mm.index + mm[0].length - 1;
101
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
+ }
102
251
  insertAt = scan.end;
103
252
  const last = scan.elements[scan.elements.length - 1];
104
253
  const indent = last ? indentOf(content, last.index) : ' ';
@@ -116,13 +265,37 @@ export function planWiringFix(rule, violations, files) {
116
265
  ? `${needsComma ? ',' : ''}\n${indent}${rendered},`
117
266
  : `${needsComma ? ', ' : ''}${rendered}`;
118
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
+ }
119
291
  edits.push({
120
292
  ruleId: rule.id,
121
293
  token: v.token,
122
294
  file: target.path,
123
- line: lineOf(content, cut + insert.length),
295
+ line: arrayLine,
124
296
  insert: insert.trim(),
125
297
  nextContent: content,
298
+ ...(importInsert !== undefined ? { importInsert, importLine: importLine } : {}),
126
299
  });
127
300
  }
128
301
  return { schema: WIRING_FIX_SCHEMA, edits, skipped };
@@ -1,4 +1,5 @@
1
- import type { IRegistrationIdiom } from '@shrkcrft/core';
1
+ import { type IRegistrationIdiom } from '@shrkcrft/core';
2
+ import type { IReadScope } from '../util/read-scope.js';
2
3
  import { type IWiringFileEntry } from './evaluate-wiring.js';
3
4
  export declare const REGISTRATION_GRAPH_SCHEMA: "sharkcraft.registration-graph/v1";
4
5
  /** A token role-site: which idiom + file:line it was found at. */
@@ -31,6 +32,13 @@ export interface IRegistrationGraph {
31
32
  readonly tokens: readonly IRegistrationNode[];
32
33
  /** Misconfiguration messages (bad regex / no capture group / bad source). */
33
34
  readonly diagnostics: readonly string[];
35
+ /**
36
+ * Set only when a file an idiom's globs matched was NOT read (over the read
37
+ * cap, or unreadable): the files read, and the unread ones. A token declared,
38
+ * provided or consumed only there is missing from `tokens`, so an
39
+ * "unprovided" or "orphan" answer over them proves nothing.
40
+ */
41
+ readonly readScope?: IReadScope;
34
42
  }
35
43
  export interface IBuildRegistrationGraphOptions {
36
44
  /** Project-relative directories to prune from the walk. */
@@ -53,6 +61,10 @@ export declare function buildRegistrationGraph(projectRoot: string, idioms: read
53
61
  * persisted registration-graph cache — a source edit shifts the signature even
54
62
  * when no reindex has run, so a stale wiring verdict is impossible. Walk-and-stat
55
63
  * only (no file reads), so it stays far cheaper than a full build. Never throws.
64
+ *
65
+ * It signs the POSITIVE union walk, not each role's selected set: a file a
66
+ * role's `!` excludes still shifts the signature. That over-inclusion can only
67
+ * cause a spurious cache miss — the safe direction — never a stale verdict.
56
68
  */
57
69
  export declare function registrationGraphSignature(projectRoot: string, idioms: readonly IRegistrationIdiom[], options?: IBuildRegistrationGraphOptions): string;
58
70
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"registration-graph.d.ts","sourceRoot":"","sources":["../../src/wiring/registration-graph.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registration-graph.d.ts","sourceRoot":"","sources":["../../src/wiring/registration-graph.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { statSync } from 'node:fs';
3
3
  import * as nodePath from 'node:path';
4
- import { matchesAny } from "../scan/glob.js";
4
+ import { resolveSourceGlobs } from '@shrkcrft/core';
5
+ import { globListSelects } from "../scan/glob.js";
5
6
  import { readMatchingFiles, walkMatching } from "../util/walk-files.js";
7
+ import { readScopeOfLists } from "../util/read-scope-coverage.js";
6
8
  import { collectSourceSites } from "./evaluate-wiring.js";
7
9
  export const REGISTRATION_GRAPH_SCHEMA = 'sharkcraft.registration-graph/v1';
8
10
  function sortSites(sites) {
@@ -11,7 +13,11 @@ function sortSites(sites) {
11
13
  /** Union (deduped) of every glob any role of any idiom references. */
12
14
  function registrationGlobs(idioms) {
13
15
  return [
14
- ...new Set(idioms.flatMap((i) => [...i.declared.files, ...i.provided.files, ...i.consumed.files])),
16
+ ...new Set(idioms.flatMap((i) => [
17
+ ...resolveSourceGlobs(i.declared),
18
+ ...resolveSourceGlobs(i.provided),
19
+ ...resolveSourceGlobs(i.consumed),
20
+ ])),
15
21
  ];
16
22
  }
17
23
  /**
@@ -23,12 +29,14 @@ function registrationGlobs(idioms) {
23
29
  * diagnostic.
24
30
  */
25
31
  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]) => ({
32
+ const matched = readMatchingFiles(projectRoot, registrationGlobs(idioms), new Set(options.excludeDirs ?? []));
33
+ const entries = [...matched.files.entries()].map(([path, content]) => ({
28
34
  path,
29
35
  content,
30
36
  }));
31
- const filesFor = (source) => entries.filter((f) => matchesAny(f.path, source.files));
37
+ // The walk is the POSITIVE union of every role of every idiom; each role
38
+ // selects its own files, so one role's `!x` never hides another role's file.
39
+ const filesFor = (source) => entries.filter((f) => globListSelects(f.path, resolveSourceGlobs(source)));
32
40
  const declared = new Map();
33
41
  const provided = new Map();
34
42
  const consumed = new Map();
@@ -55,11 +63,15 @@ export function buildRegistrationGraph(projectRoot, idioms, options = {}) {
55
63
  provided: sortSites(provided.get(token) ?? []),
56
64
  consumed: sortSites(consumed.get(token) ?? []),
57
65
  }));
66
+ // Read scope PER ROLE LIST (one file counted once): an unread file every
67
+ // role that could reach it EXCLUDES is out of scope, not a gap.
68
+ const scope = readScopeOfLists(matched, idioms.flatMap((i) => [i.declared, i.provided, i.consumed].map((s) => resolveSourceGlobs(s))));
58
69
  return {
59
70
  schema: REGISTRATION_GRAPH_SCHEMA,
60
71
  idioms: idioms.map((i) => i.name),
61
72
  tokens,
62
73
  diagnostics,
74
+ ...(scope.unread.length > 0 ? { readScope: scope } : {}),
63
75
  };
64
76
  }
65
77
  /**
@@ -70,6 +82,10 @@ export function buildRegistrationGraph(projectRoot, idioms, options = {}) {
70
82
  * persisted registration-graph cache — a source edit shifts the signature even
71
83
  * when no reindex has run, so a stale wiring verdict is impossible. Walk-and-stat
72
84
  * only (no file reads), so it stays far cheaper than a full build. Never throws.
85
+ *
86
+ * It signs the POSITIVE union walk, not each role's selected set: a file a
87
+ * role's `!` excludes still shifts the signature. That over-inclusion can only
88
+ * cause a spurious cache miss — the safe direction — never a stale verdict.
73
89
  */
74
90
  export function registrationGraphSignature(projectRoot, idioms, options = {}) {
75
91
  const files = walkMatching(projectRoot, registrationGlobs(idioms), new Set(options.excludeDirs ?? [])).sort();
@@ -96,7 +112,7 @@ export function registrationGraphSignature(projectRoot, idioms, options = {}) {
96
112
  export function providedTokensFromEntries(idioms, entries) {
97
113
  const tokens = new Set();
98
114
  for (const idiom of idioms) {
99
- const files = entries.filter((f) => matchesAny(f.path, idiom.provided.files));
115
+ const files = entries.filter((f) => globListSelects(f.path, resolveSourceGlobs(idiom.provided)));
100
116
  for (const s of collectSourceSites(idiom.provided, files).sites)
101
117
  tokens.add(s.token);
102
118
  }
@@ -0,0 +1,23 @@
1
+ import { type IRegistrationIdiom, type IVerdictCoverage } from '@shrkcrft/core';
2
+ import type { IIdiomRoleCoverage } from './i-idiom-role-coverage.js';
3
+ import type { IRegistrationQueryVerdict } from './i-registration-query-verdict.js';
4
+ import { type IOrphanRegistration, type IRegistrationGraph, type IUnprovidedToken } from './registration-graph.js';
5
+ /**
6
+ * THE registration graph's read-scope record: the idiom files the reader could
7
+ * not read, named (`examined 3 of 4 files, 1 over the 1MB read cap: …`), or
8
+ * `undefined` when it read every file its idioms matched. `wiring unprovided |
9
+ * orphans`, finish's unprovided sub-gate and MCP `get_wiring_graph` all read
10
+ * it here, so the verb and its siblings cannot disagree about what was read.
11
+ */
12
+ export declare function registrationGraphCoverage(graph: IRegistrationGraph): IVerdictCoverage | undefined;
13
+ /**
14
+ * `wiring unprovided` settled against what the graph read AND what every
15
+ * idiom's roles examined (`roles`: `measureIdiomRoleCoverage`, THE role
16
+ * authority — required, so no surface can forget the fold). `extra` adds
17
+ * candidates found another way (finish's base-ref provider regressions); they
18
+ * are merged by token and sorted, as finish always did.
19
+ */
20
+ export declare function registrationUnprovidedVerdict(graph: IRegistrationGraph, idioms: readonly IRegistrationIdiom[], roles: readonly IIdiomRoleCoverage[], changedFiles?: readonly string[], extra?: readonly IUnprovidedToken[]): IRegistrationQueryVerdict<IUnprovidedToken>;
21
+ /** `wiring orphans` settled against what the graph read and what every idiom's roles examined. */
22
+ export declare function registrationOrphansVerdict(graph: IRegistrationGraph, idioms: readonly IRegistrationIdiom[], roles: readonly IIdiomRoleCoverage[], changedFiles?: readonly string[]): IRegistrationQueryVerdict<IOrphanRegistration>;
23
+ //# sourceMappingURL=registration-query-verdict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registration-query-verdict.d.ts","sourceRoot":"","sources":["../../src/wiring/registration-query-verdict.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAKjC;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,SAAS,CASjG;AAwFD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,EAChC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,GAClC,yBAAyB,CAAC,gBAAgB,CAAC,CAQ7C;AAED,kGAAkG;AAClG,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAC/B,yBAAyB,CAAC,mBAAmB,CAAC,CAUhD"}
@@ -0,0 +1,118 @@
1
+ import { coverageShortfall, resolveSourceGlobs, ruleVerdictRecords, } from '@shrkcrft/core';
2
+ import { describeUnread, readScopeCoverage, unreadMatching } from "../util/read-scope-coverage.js";
3
+ import { registrationOrphans, registrationUnprovided, } from "./registration-graph.js";
4
+ /** At most this many demoted tokens ride on one coverage record. */
5
+ const TOKEN_LABEL_CAP = 20;
6
+ /**
7
+ * THE registration graph's read-scope record: the idiom files the reader could
8
+ * not read, named (`examined 3 of 4 files, 1 over the 1MB read cap: …`), or
9
+ * `undefined` when it read every file its idioms matched. `wiring unprovided |
10
+ * orphans`, finish's unprovided sub-gate and MCP `get_wiring_graph` all read
11
+ * it here, so the verb and its siblings cannot disagree about what was read.
12
+ */
13
+ export function registrationGraphCoverage(graph) {
14
+ if (!graph.readScope || graph.readScope.unread.length === 0)
15
+ return undefined;
16
+ return {
17
+ ...readScopeCoverage({ unit: 'registration idioms', expected: graph.idioms.length, examined: graph.idioms.length }, graph.readScope),
18
+ subject: 'registration graph',
19
+ };
20
+ }
21
+ /**
22
+ * Every idiom's role records, as an absence query settles them (round 13, P4):
23
+ * the role authority's coverage (`examined 2 of 3 roles … declared (0 files)`)
24
+ * plus its expectEmpty acceptance, folded by core's `ruleVerdictRecords` and
25
+ * subject-prefixed with the idiom. A role record that is only the idiom's read
26
+ * gap is dropped while the graph's own read-scope record is carried — that
27
+ * record already names every unread idiom file (a union over idioms); its
28
+ * acceptance is kept, so a record B is never dropped silently.
29
+ */
30
+ function roleRecords(graph, roles) {
31
+ const graphNamesUnread = registrationGraphCoverage(graph) !== undefined;
32
+ return roles.flatMap((r) => {
33
+ const records = graphNamesUnread && r.readGap
34
+ ? r.unitAcceptance
35
+ ? [r.unitAcceptance]
36
+ : []
37
+ : ruleVerdictRecords(r.coverage, r.unitAcceptance);
38
+ return records.map((c) => ({ ...c, subject: c.subject ?? r.idiom }));
39
+ });
40
+ }
41
+ /**
42
+ * THE rule for an absence claim over an incomplete read. The graph merges
43
+ * tokens across idioms, so a site of the missing role in ANY idiom satisfies a
44
+ * token. When an unread file matches any idiom's `missingRole` globs, every
45
+ * candidate could be refuted there: all are demoted to `unproven`, named in a
46
+ * second coverage record. Otherwise the candidates are findings.
47
+ *
48
+ * Round 13 (P4): every idiom's ROLE coverage is carried too, always — the
49
+ * graph's read scope said nothing about a role whose globs matched no file, so
50
+ * "every declared token has a provider" printed a ✓ over a declared role that
51
+ * examined nothing. A dead role never demotes a candidate (a finding stays a
52
+ * finding, as on `gates check`); it keeps a clean answer from being a pass.
53
+ */
54
+ function absenceVerdict(graph, idioms, roles, missingRole, candidates, unit, subject) {
55
+ const unread = graph.readScope?.unread ?? [];
56
+ // Per idiom's role LIST, never a flattened union: a negation subtracts from
57
+ // its own list only, so one idiom's `!x` cannot hide a refuting file from
58
+ // another idiom that reads it. Deduped by path, sorted.
59
+ const refutingByPath = new Map();
60
+ for (const i of unread.length > 0 ? idioms : []) {
61
+ for (const u of unreadMatching(unread, resolveSourceGlobs(i[missingRole]))) {
62
+ if (!refutingByPath.has(u.path))
63
+ refutingByPath.set(u.path, u);
64
+ }
65
+ }
66
+ const refuting = [...refutingByPath.values()].sort((a, b) => a.path.localeCompare(b.path));
67
+ const unproven = refuting.length > 0 ? candidates : [];
68
+ const fileRecord = registrationGraphCoverage(graph);
69
+ const coverage = [
70
+ ...(fileRecord ? [fileRecord] : []),
71
+ ...roleRecords(graph, roles),
72
+ ...(unproven.length > 0
73
+ ? [
74
+ {
75
+ unit,
76
+ expected: unproven.length,
77
+ examined: 0,
78
+ unexamined: unproven.slice(0, TOKEN_LABEL_CAP).map((t) => t.token),
79
+ unexaminedTotal: unproven.length,
80
+ reason: `may be ${missingRole} in a file the reader did not read — ${describeUnread(refuting)}`,
81
+ subject,
82
+ },
83
+ ]
84
+ : []),
85
+ ];
86
+ return {
87
+ findings: refuting.length > 0 ? [] : candidates,
88
+ unproven,
89
+ coverage,
90
+ shortfalls: coverage.flatMap((c) => {
91
+ const s = coverageShortfall(c);
92
+ return s === undefined ? [] : [s];
93
+ }),
94
+ unread,
95
+ };
96
+ }
97
+ /**
98
+ * `wiring unprovided` settled against what the graph read AND what every
99
+ * idiom's roles examined (`roles`: `measureIdiomRoleCoverage`, THE role
100
+ * authority — required, so no surface can forget the fold). `extra` adds
101
+ * candidates found another way (finish's base-ref provider regressions); they
102
+ * are merged by token and sorted, as finish always did.
103
+ */
104
+ export function registrationUnprovidedVerdict(graph, idioms, roles, changedFiles, extra) {
105
+ let candidates = registrationUnprovided(graph, changedFiles);
106
+ if (extra !== undefined) {
107
+ const byToken = new Map();
108
+ for (const u of [...candidates, ...extra])
109
+ if (!byToken.has(u.token))
110
+ byToken.set(u.token, u);
111
+ candidates = [...byToken.values()].sort((a, b) => a.token.localeCompare(b.token));
112
+ }
113
+ return absenceVerdict(graph, idioms, roles, 'provided', candidates, 'unprovided tokens', 'unprovided');
114
+ }
115
+ /** `wiring orphans` settled against what the graph read and what every idiom's roles examined. */
116
+ export function registrationOrphansVerdict(graph, idioms, roles, changedFiles) {
117
+ return absenceVerdict(graph, idioms, roles, 'consumed', registrationOrphans(graph, changedFiles), 'orphan registrations', 'orphans');
118
+ }
@@ -1,4 +1,5 @@
1
- import type { IRegistryDeclaration } from '@shrkcrft/core';
1
+ import { type IRegistryDeclaration } from '@shrkcrft/core';
2
+ import type { IReadScope } from '../util/read-scope.js';
2
3
  export declare const REGISTRY_SCHEMA: "sharkcraft.registry-inventory/v1";
3
4
  /** Where an id was found (project-relative path + 1-based line). */
4
5
  export interface IRegistrySite {
@@ -22,6 +23,13 @@ export interface IRegistryInventory {
22
23
  readonly entries: readonly IRegistryEntry[];
23
24
  /** Misconfiguration messages (bad regex / no capture group / bad source). */
24
25
  readonly diagnostics: readonly string[];
26
+ /**
27
+ * The SOURCE side's read scope: declaration files read, and matched ones the
28
+ * reader did not read (over the read cap, or unreadable). With any unread
29
+ * the inventory is incomplete: an absent id, "no duplicates" or an empty
30
+ * inventory proves nothing.
31
+ */
32
+ readonly readScope: IReadScope;
25
33
  }
26
34
  export interface IScanRegistryOptions {
27
35
  /** Project-relative directories to prune from the walk. */
@@ -1 +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;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,kBAAkB,GAAG,SAAS,cAAc,EAAE,CAE3F"}
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;AAG/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,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;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;CAChC;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,CAwDpB;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,5 +1,7 @@
1
- import { matchesAny } from "../scan/glob.js";
1
+ import { resolveSourceGlobs } from '@shrkcrft/core';
2
+ import { globListSelects } from "../scan/glob.js";
2
3
  import { readMatchingFiles } from "../util/walk-files.js";
4
+ import { unreadMatching } from "../util/read-scope-coverage.js";
3
5
  import { collectSourceSites } from "./evaluate-wiring.js";
4
6
  export const REGISTRY_SCHEMA = 'sharkcraft.registry-inventory/v1';
5
7
  function sortSites(sites) {
@@ -12,18 +14,23 @@ function sortSites(sites) {
12
14
  */
13
15
  export function scanRegistry(projectRoot, decl, options = {}) {
14
16
  const globs = [
15
- ...new Set([...decl.source.files, ...(decl.consumer ? decl.consumer.files : [])]),
17
+ ...new Set([
18
+ ...resolveSourceGlobs(decl.source),
19
+ ...(decl.consumer ? resolveSourceGlobs(decl.consumer) : []),
20
+ ]),
16
21
  ];
17
- const cache = readMatchingFiles(projectRoot, globs, new Set(options.excludeDirs ?? []));
18
- const entries = [...cache.entries()].map(([path, content]) => ({ path, content }));
22
+ const matched = readMatchingFiles(projectRoot, globs, new Set(options.excludeDirs ?? []));
23
+ const entries = [...matched.files.entries()].map(([path, content]) => ({ path, content }));
19
24
  const diagnostics = [];
20
- const sourceFiles = entries.filter((f) => matchesAny(f.path, decl.source.files));
25
+ // The walk above is the POSITIVE union of both sides; each side selects its
26
+ // own files, so the source's `!` never hides a consumer file (or vice versa).
27
+ const sourceFiles = entries.filter((f) => globListSelects(f.path, resolveSourceGlobs(decl.source)));
21
28
  const declared = collectSourceSites(decl.source, sourceFiles);
22
29
  if (declared.error)
23
30
  diagnostics.push(`registry "${decl.name}" source: ${declared.error}`);
24
31
  const consumerByToken = new Map();
25
32
  if (decl.consumer) {
26
- const consumerFiles = entries.filter((f) => matchesAny(f.path, decl.consumer.files));
33
+ const consumerFiles = entries.filter((f) => globListSelects(f.path, resolveSourceGlobs(decl.consumer)));
27
34
  const consumed = collectSourceSites(decl.consumer, consumerFiles);
28
35
  if (consumed.error)
29
36
  diagnostics.push(`registry "${decl.name}" consumer: ${consumed.error}`);
@@ -53,6 +60,10 @@ export function scanRegistry(projectRoot, decl, options = {}) {
53
60
  ...(decl.description ? { description: decl.description } : {}),
54
61
  entries: entriesOut,
55
62
  diagnostics,
63
+ readScope: {
64
+ read: sourceFiles.length,
65
+ unread: unreadMatching(matched.unread, resolveSourceGlobs(decl.source)),
66
+ },
56
67
  };
57
68
  }
58
69
  /** True if `id` is declared in the registry (alias-blind exact membership). */
@@ -1,4 +1,4 @@
1
- import type { IWiringRule } from '@shrkcrft/core';
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. */
@@ -12,8 +12,10 @@ export interface IRunWiringOptions {
12
12
  }
13
13
  /**
14
14
  * Filesystem-backed wiring run: walks the project once, reads only the files a
15
- * rule references (skipping oversized files), and evaluates every (selected)
16
- * rule. Pure-engine output; the only IO is the read-only tree walk + reads.
15
+ * rule references, and evaluates every (selected) rule. A matched file the
16
+ * reader could not read (over the read cap) is handed to the engine as
17
+ * unread, so the rule's coverage names it instead of passing over it.
18
+ * Pure-engine output; the only IO is the read-only tree walk + reads.
17
19
  */
18
20
  export declare function runWiring(projectRoot: string, rules: readonly IWiringRule[], options?: IRunWiringOptions): IWiringReport;
19
21
  //# sourceMappingURL=scan-wiring-files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scan-wiring-files.d.ts","sourceRoot":"","sources":["../../src/wiring/scan-wiring-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,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,CA+Bf"}
1
+ {"version":3,"file":"scan-wiring-files.d.ts","sourceRoot":"","sources":["../../src/wiring/scan-wiring-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2C,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAQ3F,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAQ9B,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;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAsEf"}