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

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 (59) hide show
  1. package/dist/baseline/canonicalize.d.ts +35 -0
  2. package/dist/baseline/canonicalize.d.ts.map +1 -0
  3. package/dist/baseline/canonicalize.js +87 -0
  4. package/dist/baseline/compute-baseline.d.ts +23 -0
  5. package/dist/baseline/compute-baseline.d.ts.map +1 -0
  6. package/dist/baseline/compute-baseline.js +30 -0
  7. package/dist/baseline/diff-baseline.d.ts +68 -0
  8. package/dist/baseline/diff-baseline.d.ts.map +1 -0
  9. package/dist/baseline/diff-baseline.js +110 -0
  10. package/dist/baseline/json-path-keys.d.ts +10 -0
  11. package/dist/baseline/json-path-keys.d.ts.map +1 -0
  12. package/dist/baseline/json-path-keys.js +13 -0
  13. package/dist/extract/code-zones.d.ts +32 -0
  14. package/dist/extract/code-zones.d.ts.map +1 -0
  15. package/dist/extract/code-zones.js +59 -0
  16. package/dist/extract/extract-tokens.d.ts +30 -0
  17. package/dist/extract/extract-tokens.d.ts.map +1 -0
  18. package/dist/extract/extract-tokens.js +328 -0
  19. package/dist/extract/inspect-source.d.ts +24 -0
  20. package/dist/extract/inspect-source.d.ts.map +1 -0
  21. package/dist/extract/inspect-source.js +27 -0
  22. package/dist/extract/scan-literals.d.ts +54 -0
  23. package/dist/extract/scan-literals.d.ts.map +1 -0
  24. package/dist/extract/scan-literals.js +177 -0
  25. package/dist/generated/check-provenance.d.ts +37 -0
  26. package/dist/generated/check-provenance.d.ts.map +1 -0
  27. package/dist/generated/check-provenance.js +89 -0
  28. package/dist/generated/compare-trees.d.ts +26 -0
  29. package/dist/generated/compare-trees.d.ts.map +1 -0
  30. package/dist/generated/compare-trees.js +41 -0
  31. package/dist/generated/scan-generated.d.ts +17 -0
  32. package/dist/generated/scan-generated.d.ts.map +1 -0
  33. package/dist/generated/scan-generated.js +27 -0
  34. package/dist/index.d.ts +11 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +11 -0
  37. package/dist/policy/evaluate-policy.d.ts +41 -5
  38. package/dist/policy/evaluate-policy.d.ts.map +1 -1
  39. package/dist/policy/evaluate-policy.js +112 -10
  40. package/dist/policy/run-policy.d.ts.map +1 -1
  41. package/dist/policy/run-policy.js +24 -3
  42. package/dist/wiring/evaluate-wiring.d.ts +81 -28
  43. package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
  44. package/dist/wiring/evaluate-wiring.js +0 -0
  45. package/dist/wiring/explain-wiring.d.ts +15 -7
  46. package/dist/wiring/explain-wiring.d.ts.map +1 -1
  47. package/dist/wiring/explain-wiring.js +28 -27
  48. package/dist/wiring/registration-graph.d.ts +27 -2
  49. package/dist/wiring/registration-graph.d.ts.map +1 -1
  50. package/dist/wiring/registration-graph.js +51 -2
  51. package/dist/wiring/registry-query.d.ts +9 -0
  52. package/dist/wiring/registry-query.d.ts.map +1 -1
  53. package/dist/wiring/registry-query.js +11 -0
  54. package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
  55. package/dist/wiring/scan-wiring-files.js +4 -7
  56. package/dist/wiring/trace-literal.d.ts +6 -0
  57. package/dist/wiring/trace-literal.d.ts.map +1 -1
  58. package/dist/wiring/trace-literal.js +22 -0
  59. package/package.json +2 -2
@@ -0,0 +1,328 @@
1
+ import { resolveExtractorAnchor, resolveExtractorKind, validateWiringSource, } from '@shrkcrft/core';
2
+ import { safeCompile } from "../util/safe-regex.js";
3
+ import { elementToken, elementValue, escapeRegex, lineOf, scanBalanced, } from "./scan-literals.js";
4
+ /**
5
+ * Run one source's extractor over the given files.
6
+ *
7
+ * This is the reusable core of the completeness plane: wiring rules, registry
8
+ * inventories, and extractor-backed baselines all harvest their ids through
9
+ * here, so every one of them honours the exact same semantics (and gains every
10
+ * new extractor kind for free). Never throws — a misconfigured source returns
11
+ * an `error` and no sites.
12
+ */
13
+ export function extractTokens(source, files) {
14
+ const error = validateWiringSource(source);
15
+ if (error)
16
+ return { sites: [], error };
17
+ const kind = resolveExtractorKind(source);
18
+ const anchor = resolveExtractorAnchor(source);
19
+ let sites;
20
+ switch (kind) {
21
+ case 'regex-capture':
22
+ sites = byRegex(source, files);
23
+ break;
24
+ case 'array-members':
25
+ sites = byBracketLiteral(anchor, '[', files, (el) => elementToken(el));
26
+ break;
27
+ case 'object-keys':
28
+ sites = byBracketLiteral(anchor, '{', files, (el) => source.capture === 'value' ? elementValue(el) : elementToken(el));
29
+ break;
30
+ case 'enum-members':
31
+ sites = byEnumMembers(anchor, source.capture === 'value', files);
32
+ break;
33
+ case 'export-names':
34
+ sites = byExportNames(files);
35
+ break;
36
+ case 'call-args':
37
+ sites = byCallArgs(anchor, source.argIndex ?? 0, false, files);
38
+ break;
39
+ case 'decorator-args':
40
+ sites = byCallArgs(anchor, source.argIndex ?? 0, true, files);
41
+ break;
42
+ case 'string-union-members':
43
+ sites = byStringUnion(anchor, files);
44
+ break;
45
+ case 'json-path':
46
+ sites = byJsonPath(source.jsonPath, files);
47
+ break;
48
+ default:
49
+ return { sites: [], error: `unknown extract kind "${String(kind)}"` };
50
+ }
51
+ if (source.match !== undefined) {
52
+ const { re } = safeCompile(source.match, source.matchFlags);
53
+ if (re) {
54
+ sites = sites.filter((s) => {
55
+ re.lastIndex = 0;
56
+ return re.test(s.token);
57
+ });
58
+ }
59
+ }
60
+ if (source.exclude !== undefined) {
61
+ const { re } = safeCompile(source.exclude, source.excludeFlags);
62
+ if (re) {
63
+ sites = sites.filter((s) => {
64
+ re.lastIndex = 0;
65
+ return !re.test(s.token);
66
+ });
67
+ }
68
+ }
69
+ return { sites };
70
+ }
71
+ /** Capture-group-1 of a pattern, per file. */
72
+ function byRegex(source, files) {
73
+ const { re } = safeCompile(source.pattern, source.flags);
74
+ if (!re)
75
+ return [];
76
+ const sites = [];
77
+ for (const f of files) {
78
+ re.lastIndex = 0;
79
+ let m;
80
+ while ((m = re.exec(f.content)) !== null) {
81
+ // Guard against a zero-width match looping forever.
82
+ if (m.index === re.lastIndex)
83
+ re.lastIndex += 1;
84
+ const token = m[1];
85
+ if (token === undefined || token === '')
86
+ continue;
87
+ sites.push({ token, file: f.path, line: lineOf(f.content, m.index) });
88
+ }
89
+ }
90
+ return sites;
91
+ }
92
+ /**
93
+ * Elements of every `<anchor> = <open> … ` / `<anchor>: <open> … ` literal.
94
+ *
95
+ * Covers `export const ARR = [ … ]`, an inline `arrayProperty: [ … ]`, a typed
96
+ * `const ARR: readonly T[] = [ … ]`, and the very common freeze/wrapper form
97
+ * `export const ARR = Object.freeze([ … ])` — a registry array is nearly always
98
+ * wrapped, and missing that would silently extract nothing.
99
+ */
100
+ function byBracketLiteral(anchor, open, files, tokenOf) {
101
+ const sites = [];
102
+ const head = new RegExp(`(?<![\\w$])${escapeRegex(anchor)}\\s*(?::[^=\\n]*)?[:=]\\s*(?:[A-Za-z_$][\\w$.]*\\s*\\(\\s*)?\\${open}`, 'g');
103
+ for (const f of files) {
104
+ head.lastIndex = 0;
105
+ let m;
106
+ while ((m = head.exec(f.content)) !== null) {
107
+ if (m.index === head.lastIndex)
108
+ head.lastIndex += 1;
109
+ const openIndex = m.index + m[0].length - 1;
110
+ const { elements, end } = scanBalanced(f.content, openIndex);
111
+ for (const el of elements) {
112
+ const token = tokenOf(el.text);
113
+ if (token === undefined || token === '')
114
+ continue;
115
+ sites.push({ token, file: f.path, line: lineOf(f.content, el.index) });
116
+ }
117
+ head.lastIndex = Math.max(end + 1, head.lastIndex);
118
+ }
119
+ }
120
+ return sites;
121
+ }
122
+ /** Members of `enum <anchor> { … }` — the member name, or its assigned value. */
123
+ function byEnumMembers(anchor, wantValue, files) {
124
+ const sites = [];
125
+ const head = new RegExp(`\\benum\\s+${escapeRegex(anchor)}\\s*\\{`, 'g');
126
+ for (const f of files) {
127
+ head.lastIndex = 0;
128
+ let m;
129
+ while ((m = head.exec(f.content)) !== null) {
130
+ if (m.index === head.lastIndex)
131
+ head.lastIndex += 1;
132
+ const openIndex = m.index + m[0].length - 1;
133
+ const { elements, end } = scanBalanced(f.content, openIndex);
134
+ for (const el of elements) {
135
+ const token = wantValue ? elementValue(el.text) : elementToken(el.text);
136
+ if (token === undefined || token === '')
137
+ continue;
138
+ sites.push({ token, file: f.path, line: lineOf(f.content, el.index) });
139
+ }
140
+ head.lastIndex = Math.max(end + 1, head.lastIndex);
141
+ }
142
+ }
143
+ return sites;
144
+ }
145
+ /** Every exported binding name, including re-export aliases (`export { A as B }` → `B`). */
146
+ const EXPORT_DECL = /\bexport\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function\*?|class|interface|type|enum|namespace)\s+([A-Za-z_$][\w$]*)/g;
147
+ const EXPORT_CLAUSE = /\bexport\s*(?:type\s+)?\{([^}]*)\}/g;
148
+ function byExportNames(files) {
149
+ const sites = [];
150
+ for (const f of files) {
151
+ EXPORT_DECL.lastIndex = 0;
152
+ let m;
153
+ while ((m = EXPORT_DECL.exec(f.content)) !== null) {
154
+ sites.push({ token: m[1], file: f.path, line: lineOf(f.content, m.index) });
155
+ }
156
+ EXPORT_CLAUSE.lastIndex = 0;
157
+ while ((m = EXPORT_CLAUSE.exec(f.content)) !== null) {
158
+ const line = lineOf(f.content, m.index);
159
+ for (const raw of m[1].split(',')) {
160
+ const spec = raw.trim().replace(/^type\s+/, '');
161
+ if (spec === '' || spec === 'default')
162
+ continue;
163
+ // `A as B` exports B; a bare `A` exports A.
164
+ const parts = spec.split(/\s+as\s+/);
165
+ const name = (parts[parts.length - 1] ?? '').trim();
166
+ if (/^[A-Za-z_$][\w$]*$/.test(name))
167
+ sites.push({ token: name, file: f.path, line });
168
+ }
169
+ }
170
+ }
171
+ return sites;
172
+ }
173
+ /** Argument `argIndex` of every `anchor(…)` call — or `@anchor(…)` decorator. */
174
+ function byCallArgs(anchor, argIndex, decorator, files) {
175
+ const sites = [];
176
+ const head = decorator
177
+ ? new RegExp(`@${escapeRegex(anchor)}\\s*\\(`, 'g')
178
+ : new RegExp(`(?<![\\w$.@])${escapeRegex(anchor)}\\s*\\(`, 'g');
179
+ for (const f of files) {
180
+ head.lastIndex = 0;
181
+ let m;
182
+ while ((m = head.exec(f.content)) !== null) {
183
+ if (m.index === head.lastIndex)
184
+ head.lastIndex += 1;
185
+ const openIndex = m.index + m[0].length - 1;
186
+ const { elements, end } = scanBalanced(f.content, openIndex);
187
+ const el = elements[argIndex];
188
+ if (el) {
189
+ const token = elementToken(el.text);
190
+ if (token !== undefined && token !== '') {
191
+ sites.push({ token, file: f.path, line: lineOf(f.content, el.index) });
192
+ }
193
+ }
194
+ head.lastIndex = Math.max(end + 1, head.lastIndex);
195
+ }
196
+ }
197
+ return sites;
198
+ }
199
+ /** The string literals of `type <anchor> = 'a' | 'b' | 'c'`. */
200
+ function byStringUnion(anchor, files) {
201
+ const sites = [];
202
+ const head = new RegExp(`\\btype\\s+${escapeRegex(anchor)}\\s*(?:<[^>]*>)?\\s*=`, 'g');
203
+ const member = /['"`]([^'"`]+)['"`]/g;
204
+ for (const f of files) {
205
+ head.lastIndex = 0;
206
+ let m;
207
+ while ((m = head.exec(f.content)) !== null) {
208
+ if (m.index === head.lastIndex)
209
+ head.lastIndex += 1;
210
+ const start = m.index + m[0].length;
211
+ // The alias body runs to the first `;` or blank line at depth 0.
212
+ const semi = f.content.indexOf(';', start);
213
+ const blank = f.content.indexOf('\n\n', start);
214
+ const ends = [semi, blank].filter((i) => i >= 0);
215
+ const end = ends.length > 0 ? Math.min(...ends) : f.content.length;
216
+ const body = f.content.slice(start, end);
217
+ member.lastIndex = 0;
218
+ let mm;
219
+ while ((mm = member.exec(body)) !== null) {
220
+ sites.push({
221
+ token: mm[1],
222
+ file: f.path,
223
+ line: lineOf(f.content, start + mm.index),
224
+ });
225
+ }
226
+ head.lastIndex = Math.max(end, head.lastIndex);
227
+ }
228
+ }
229
+ return sites;
230
+ }
231
+ /**
232
+ * Leaves selected by a JSON path. Supports `.key`, `[n]`, `[*]`; a leading `$.`
233
+ * is accepted and ignored. Scalars become ids; a selected object contributes
234
+ * its KEYS (so `compilerOptions.paths` yields the alias names). Line numbers are
235
+ * resolved by locating the token's first quoted occurrence in the raw text —
236
+ * exact when the id is unique, and honestly approximate when it is not.
237
+ */
238
+ function byJsonPath(jsonPath, files) {
239
+ const segments = parseJsonPath(jsonPath);
240
+ const sites = [];
241
+ for (const f of files) {
242
+ let doc;
243
+ try {
244
+ doc = JSON.parse(f.content);
245
+ }
246
+ catch {
247
+ continue; // not JSON — a `json-path` source over mixed globs skips it
248
+ }
249
+ for (const leaf of selectJsonPath(doc, segments)) {
250
+ const token = String(leaf);
251
+ if (token === '')
252
+ continue;
253
+ const at = f.content.indexOf(`"${token}"`);
254
+ sites.push({
255
+ token,
256
+ file: f.path,
257
+ line: at >= 0 ? lineOf(f.content, at) : 1,
258
+ });
259
+ }
260
+ }
261
+ return sites;
262
+ }
263
+ function parseJsonPath(path) {
264
+ const out = [];
265
+ const cleaned = path.replace(/^\$\.?/, '');
266
+ for (const part of cleaned.split('.')) {
267
+ if (part === '')
268
+ continue;
269
+ const head = part.replace(/\[.*$/, '');
270
+ if (head !== '')
271
+ out.push({ key: head });
272
+ for (const b of part.matchAll(/\[([^\]]*)\]/g)) {
273
+ const inner = (b[1] ?? '').trim();
274
+ if (inner === '*')
275
+ out.push({ all: true });
276
+ else if (/^\d+$/.test(inner))
277
+ out.push({ index: Number(inner) });
278
+ else
279
+ out.push({ key: inner.replace(/^['"]|['"]$/g, '') });
280
+ }
281
+ }
282
+ return out;
283
+ }
284
+ function selectJsonPath(doc, segments) {
285
+ let current = [doc];
286
+ for (const seg of segments) {
287
+ const next = [];
288
+ for (const node of current) {
289
+ if (node === null || node === undefined)
290
+ continue;
291
+ if ('all' in seg) {
292
+ if (Array.isArray(node))
293
+ next.push(...node);
294
+ else if (typeof node === 'object')
295
+ next.push(...Object.values(node));
296
+ }
297
+ else if ('index' in seg) {
298
+ if (Array.isArray(node))
299
+ next.push(node[seg.index]);
300
+ }
301
+ else if (typeof node === 'object' && !Array.isArray(node)) {
302
+ next.push(node[seg.key]);
303
+ }
304
+ }
305
+ current = next;
306
+ }
307
+ const out = [];
308
+ for (const leaf of current) {
309
+ if (leaf === null || leaf === undefined)
310
+ continue;
311
+ if (typeof leaf === 'object') {
312
+ // A selected object contributes its keys; an array, its scalar elements.
313
+ if (Array.isArray(leaf)) {
314
+ for (const el of leaf)
315
+ if (typeof el === 'string' || typeof el === 'number')
316
+ out.push(String(el));
317
+ }
318
+ else {
319
+ out.push(...Object.keys(leaf));
320
+ }
321
+ continue;
322
+ }
323
+ if (typeof leaf === 'boolean')
324
+ continue;
325
+ out.push(String(leaf));
326
+ }
327
+ return out;
328
+ }
@@ -0,0 +1,24 @@
1
+ import type { IWiringSource } from '@shrkcrft/core';
2
+ import { type IExtractedSite } from './extract-tokens.js';
3
+ /** What one source actually resolved to against the live tree. */
4
+ export interface ISourceInspection {
5
+ /** Files the globs matched (after the shared walk's skip rules). */
6
+ readonly filesScanned: number;
7
+ /** Distinct ids extracted, sorted. */
8
+ readonly ids: readonly string[];
9
+ /** Every capture site, in stable (file, line) order. */
10
+ readonly sites: readonly IExtractedSite[];
11
+ /** Set when the source is misconfigured. */
12
+ readonly error?: string;
13
+ }
14
+ /**
15
+ * Resolve ONE source against the tree and report what it matched.
16
+ *
17
+ * This is the primitive behind the rule-authoring trust layer: every plane
18
+ * (wiring, registry, registration idioms, extractor baselines) is ultimately a
19
+ * set of these, so "how many files did this rule see, and which ids did it
20
+ * extract?" has one answer computed one way. A rule that matched 0 is then a
21
+ * fact the tooling can report, not something an author has to notice.
22
+ */
23
+ export declare function inspectSource(projectRoot: string, source: IWiringSource, excludeDirs?: readonly string[]): ISourceInspection;
24
+ //# sourceMappingURL=inspect-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect-source.d.ts","sourceRoot":"","sources":["../../src/extract/inspect-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEzE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sCAAsC;IACtC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,iBAAiB,CAgBnB"}
@@ -0,0 +1,27 @@
1
+ import { matchesAny } from "../scan/glob.js";
2
+ import { readMatchingFiles } from "../util/walk-files.js";
3
+ import { extractTokens } from "./extract-tokens.js";
4
+ /**
5
+ * Resolve ONE source against the tree and report what it matched.
6
+ *
7
+ * This is the primitive behind the rule-authoring trust layer: every plane
8
+ * (wiring, registry, registration idioms, extractor baselines) is ultimately a
9
+ * set of these, so "how many files did this rule see, and which ids did it
10
+ * extract?" has one answer computed one way. A rule that matched 0 is then a
11
+ * fact the tooling can report, not something an author has to notice.
12
+ */
13
+ export function inspectSource(projectRoot, source, excludeDirs = []) {
14
+ const globs = source.files ?? [];
15
+ const cache = readMatchingFiles(projectRoot, globs, new Set(excludeDirs));
16
+ const files = [...cache.entries()]
17
+ .filter(([path]) => matchesAny(path, globs))
18
+ .map(([path, content]) => ({ path, content }));
19
+ const res = extractTokens(source, files);
20
+ const sites = [...res.sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.token.localeCompare(b.token));
21
+ return {
22
+ filesScanned: files.length,
23
+ ids: [...new Set(sites.map((s) => s.token))].sort(),
24
+ sites,
25
+ ...(res.error ? { error: res.error } : {}),
26
+ };
27
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Lexical primitives shared by every extractor and by the policy zone
3
+ * classifier: skip a string literal, skip a comment, find the line of an
4
+ * offset, split a balanced bracket block into its top-level elements.
5
+ *
6
+ * These are deliberately a LEXER, not a parser. The engine must read `.ts`,
7
+ * `.kt`, `.swift`, `.scss`, `.json` and inline template strings with one code
8
+ * path and no per-language toolchain; a lexer is the largest thing that stays
9
+ * honest across all of them. The documented limits: a regex literal whose body
10
+ * contains `//` or `/*` is read as a comment, and a language whose string or
11
+ * comment syntax differs from the C/JS family (e.g. `#` comments) is scanned as
12
+ * plain code.
13
+ */
14
+ /** Index of the closing quote of the string starting at `start` (handles escapes). */
15
+ export declare function skipString(content: string, start: number): number;
16
+ /**
17
+ * If a comment starts at `start`, the index of its LAST character; otherwise
18
+ * `-1`. Handles `// … \n` and `/* … *\/`.
19
+ */
20
+ export declare function skipComment(content: string, start: number): number;
21
+ /** 1-based line number of a character offset. */
22
+ export declare function lineOf(content: string, index: number): number;
23
+ /** Escape a literal string for embedding in a RegExp source. */
24
+ export declare function escapeRegex(s: string): string;
25
+ /** One top-level element of a balanced bracket block, with its source offset. */
26
+ export interface IBracketElement {
27
+ readonly text: string;
28
+ readonly index: number;
29
+ }
30
+ /** Result of {@link scanBalanced}: the elements and the closing bracket's offset. */
31
+ export interface IBracketScan {
32
+ readonly elements: readonly IBracketElement[];
33
+ readonly end: number;
34
+ }
35
+ /**
36
+ * Scan a `[ … ]` / `( … )` / `{ … }` block from its OPENING bracket, splitting
37
+ * the top-level (depth-1) comma-separated elements. String- and comment-aware,
38
+ * so commas and brackets inside nested literals, strings, or comments never
39
+ * mis-split. An unterminated block yields everything to end-of-content.
40
+ */
41
+ export declare function scanBalanced(content: string, openIndex: number): IBracketScan;
42
+ /**
43
+ * The id carried by one element: the contents of a leading quoted string, or
44
+ * the leading identifier. `undefined` when the element starts with neither
45
+ * (a spread, a number, a nested literal).
46
+ */
47
+ export declare function elementToken(text: string): string | undefined;
48
+ /**
49
+ * The VALUE side of a `key: value` / `member = value` element: the contents of
50
+ * a quoted string, or a bare numeric/identifier token. `undefined` when the
51
+ * element carries no assignment or the value is a nested construct.
52
+ */
53
+ export declare function elementValue(text: string): string | undefined;
54
+ //# sourceMappingURL=scan-literals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-literals.d.ts","sourceRoot":"","sources":["../../src/extract/scan-literals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAUjE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYlE;AAED,iDAAiD;AACjD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAO7D;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,qFAAqF;AACrF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AA0CD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAgC7E;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ7D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsB7D"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Lexical primitives shared by every extractor and by the policy zone
3
+ * classifier: skip a string literal, skip a comment, find the line of an
4
+ * offset, split a balanced bracket block into its top-level elements.
5
+ *
6
+ * These are deliberately a LEXER, not a parser. The engine must read `.ts`,
7
+ * `.kt`, `.swift`, `.scss`, `.json` and inline template strings with one code
8
+ * path and no per-language toolchain; a lexer is the largest thing that stays
9
+ * honest across all of them. The documented limits: a regex literal whose body
10
+ * contains `//` or `/*` is read as a comment, and a language whose string or
11
+ * comment syntax differs from the C/JS family (e.g. `#` comments) is scanned as
12
+ * plain code.
13
+ */
14
+ /** Index of the closing quote of the string starting at `start` (handles escapes). */
15
+ export function skipString(content, start) {
16
+ const quote = content[start];
17
+ for (let i = start + 1; i < content.length; i += 1) {
18
+ if (content[i] === '\\') {
19
+ i += 1;
20
+ continue;
21
+ }
22
+ if (content[i] === quote)
23
+ return i;
24
+ }
25
+ return content.length - 1;
26
+ }
27
+ /**
28
+ * If a comment starts at `start`, the index of its LAST character; otherwise
29
+ * `-1`. Handles `// … \n` and `/* … *\/`.
30
+ */
31
+ export function skipComment(content, start) {
32
+ if (content[start] !== '/')
33
+ return -1;
34
+ const next = content[start + 1];
35
+ if (next === '/') {
36
+ const nl = content.indexOf('\n', start + 2);
37
+ return nl === -1 ? content.length - 1 : nl - 1;
38
+ }
39
+ if (next === '*') {
40
+ const end = content.indexOf('*/', start + 2);
41
+ return end === -1 ? content.length - 1 : end + 1;
42
+ }
43
+ return -1;
44
+ }
45
+ /** 1-based line number of a character offset. */
46
+ export function lineOf(content, index) {
47
+ let line = 1;
48
+ const end = Math.min(index, content.length);
49
+ for (let i = 0; i < end; i += 1) {
50
+ if (content[i] === '\n')
51
+ line += 1;
52
+ }
53
+ return line;
54
+ }
55
+ /** Escape a literal string for embedding in a RegExp source. */
56
+ export function escapeRegex(s) {
57
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ }
59
+ /**
60
+ * Advance past whitespace AND leading comments, returning the offset of the
61
+ * element's first real character.
62
+ *
63
+ * A registry array is exactly where authors leave section comments
64
+ * (`// spec read-only tools.`), and treating one as the element's text drops
65
+ * the entry that follows it — a false "not registered" that is worse than no
66
+ * rule at all. Skipping the trivia also puts the reported line on the token
67
+ * rather than on its comment.
68
+ */
69
+ function skipLeadingTrivia(content, start, end) {
70
+ let i = start;
71
+ while (i < end) {
72
+ const c = content[i];
73
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
74
+ i += 1;
75
+ continue;
76
+ }
77
+ const commentEnd = skipComment(content, i);
78
+ if (commentEnd >= 0) {
79
+ i = commentEnd + 1;
80
+ continue;
81
+ }
82
+ break;
83
+ }
84
+ return i;
85
+ }
86
+ function pushElement(elements, content, start, end) {
87
+ const from = skipLeadingTrivia(content, start, end);
88
+ const text = content.slice(from, end).trim();
89
+ if (text === '')
90
+ return;
91
+ elements.push({ text, index: from });
92
+ }
93
+ /**
94
+ * Scan a `[ … ]` / `( … )` / `{ … }` block from its OPENING bracket, splitting
95
+ * the top-level (depth-1) comma-separated elements. String- and comment-aware,
96
+ * so commas and brackets inside nested literals, strings, or comments never
97
+ * mis-split. An unterminated block yields everything to end-of-content.
98
+ */
99
+ export function scanBalanced(content, openIndex) {
100
+ const elements = [];
101
+ let depth = 0;
102
+ let elemStart = openIndex + 1;
103
+ for (let i = openIndex; i < content.length; i += 1) {
104
+ const c = content[i];
105
+ if (c === '"' || c === "'" || c === '`') {
106
+ i = skipString(content, i);
107
+ continue;
108
+ }
109
+ if (c === '/') {
110
+ const commentEnd = skipComment(content, i);
111
+ if (commentEnd >= 0) {
112
+ i = commentEnd;
113
+ continue;
114
+ }
115
+ }
116
+ if (c === '[' || c === '(' || c === '{') {
117
+ depth += 1;
118
+ }
119
+ else if (c === ']' || c === ')' || c === '}') {
120
+ depth -= 1;
121
+ if (depth === 0) {
122
+ pushElement(elements, content, elemStart, i);
123
+ return { elements, end: i };
124
+ }
125
+ }
126
+ else if (c === ',' && depth === 1) {
127
+ pushElement(elements, content, elemStart, i);
128
+ elemStart = i + 1;
129
+ }
130
+ }
131
+ pushElement(elements, content, elemStart, content.length);
132
+ return { elements, end: content.length - 1 };
133
+ }
134
+ /**
135
+ * The id carried by one element: the contents of a leading quoted string, or
136
+ * the leading identifier. `undefined` when the element starts with neither
137
+ * (a spread, a number, a nested literal).
138
+ */
139
+ export function elementToken(text) {
140
+ const first = text[0];
141
+ if (first === '"' || first === "'" || first === '`') {
142
+ const close = text.indexOf(first, 1);
143
+ return close > 0 ? text.slice(1, close) : undefined;
144
+ }
145
+ const m = /^[A-Za-z_$][\w$]*/.exec(text);
146
+ return m ? m[0] : undefined;
147
+ }
148
+ /**
149
+ * The VALUE side of a `key: value` / `member = value` element: the contents of
150
+ * a quoted string, or a bare numeric/identifier token. `undefined` when the
151
+ * element carries no assignment or the value is a nested construct.
152
+ */
153
+ export function elementValue(text) {
154
+ // Find the first top-level `:` or `=` (skipping strings and `=>`).
155
+ for (let i = 0; i < text.length; i += 1) {
156
+ const c = text[i];
157
+ if (c === '"' || c === "'" || c === '`') {
158
+ i = skipString(text, i);
159
+ continue;
160
+ }
161
+ if (c === '=' && text[i + 1] === '>')
162
+ return undefined;
163
+ if (c === ':' || c === '=') {
164
+ const rhs = text.slice(i + 1).trim();
165
+ if (rhs === '')
166
+ return undefined;
167
+ const q = rhs[0];
168
+ if (q === '"' || q === "'" || q === '`') {
169
+ const close = rhs.indexOf(q, 1);
170
+ return close > 0 ? rhs.slice(1, close) : undefined;
171
+ }
172
+ const m = /^[\w$.-]+/.exec(rhs);
173
+ return m ? m[0] : undefined;
174
+ }
175
+ }
176
+ return undefined;
177
+ }
@@ -0,0 +1,37 @@
1
+ import type { IGeneratedArtifactRule } from '@shrkcrft/core';
2
+ /** What a provenance finding is about. */
3
+ export type ProvenanceFindingKind =
4
+ /** A file inside `generatedGlob` carries no "do not edit" header. */
5
+ 'missing-header'
6
+ /** A file OUTSIDE the glob carries the header — a hand-written file mislabeled. */
7
+ | 'mislabeled'
8
+ /** Advisory: the header does not name how to regenerate the file. */
9
+ | 'no-regen-pointer';
10
+ export interface IProvenanceFinding {
11
+ readonly ruleId: string;
12
+ readonly file: string;
13
+ readonly kind: ProvenanceFindingKind;
14
+ readonly severity: 'error' | 'warning';
15
+ readonly message: string;
16
+ }
17
+ /**
18
+ * Derive the globs to search for MISLABELED files when the rule doesn't name
19
+ * them: one `**\/*.<ext>` per distinct extension in `generatedGlob`. Bounded and
20
+ * deterministic — never a whole-tree read of every file type.
21
+ */
22
+ export declare function deriveOutsideGlobs(generatedGlob: readonly string[]): string[];
23
+ /**
24
+ * Check the "this file is generated" header contract.
25
+ *
26
+ * Pure: the caller supplies the file contents, so this runs with no regen
27
+ * command, no temp dir, and no spawn — a header-only rule is fully useful (and
28
+ * safe to ship from a pack) on its own.
29
+ *
30
+ * `outside` should contain candidate files NOT in `generatedGlob`; pass an empty
31
+ * map when `forbidOutside` is off.
32
+ */
33
+ export declare function checkProvenanceHeaders(rule: IGeneratedArtifactRule, generated: ReadonlyMap<string, string>, outside: ReadonlyMap<string, string>): {
34
+ findings: readonly IProvenanceFinding[];
35
+ error?: string;
36
+ };
37
+ //# sourceMappingURL=check-provenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-provenance.d.ts","sourceRoot":"","sources":["../../src/generated/check-provenance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAG7D,0CAA0C;AAC1C,MAAM,MAAM,qBAAqB;AAC/B,qEAAqE;AACnE,gBAAgB;AAClB,mFAAmF;GACjF,YAAY;AACd,qEAAqE;GACnE,kBAAkB,CAAC;AAEvB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAO7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,sBAAsB,EAC5B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC;IAAE,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA2D7D"}