@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,35 @@
1
+ /**
2
+ * Canonicalization for baseline comparison.
3
+ *
4
+ * The point is NOT prettiness — it is that a reordering, a re-indent, or a
5
+ * key shuffle must not read as drift. A noisy diff trains reviewers to bless
6
+ * without reading, which is how a real deletion ships inside a "formatting"
7
+ * update.
8
+ */
9
+ /** The canonical forms a baseline rule may request. */
10
+ export type CanonicalForm = 'auto' | 'json-sorted-keys' | 'lines-sorted' | 'lines' | 'raw';
11
+ /** The form actually applied, plus the canonical text. */
12
+ export interface ICanonicalResult {
13
+ readonly text: string;
14
+ /** The concrete form used (`auto` is resolved to one of the others). */
15
+ readonly form: Exclude<CanonicalForm, 'auto'>;
16
+ }
17
+ /** Parse `text` as JSON, or `undefined` when it is not JSON. */
18
+ export declare function tryParseJson(text: string): unknown | undefined;
19
+ /** Recursively sort object keys so key ORDER is never mistaken for a change. */
20
+ export declare function sortJsonKeys(value: unknown): unknown;
21
+ /**
22
+ * Bring both sides of a comparison into the same form.
23
+ *
24
+ * `auto` resolves to `json-sorted-keys` only when BOTH sides parse as JSON —
25
+ * a pure function of the two inputs, so the same pair always canonicalizes the
26
+ * same way (a per-side sniff would make the result depend on which file was
27
+ * malformed).
28
+ */
29
+ export declare function canonicalizePair(expected: string, actual: string, form?: CanonicalForm): {
30
+ expected: ICanonicalResult;
31
+ actual: ICanonicalResult;
32
+ };
33
+ /** Apply one concrete canonical form. Falls back to `lines` when JSON won't parse. */
34
+ export declare function canonicalizeOne(text: string, form: Exclude<CanonicalForm, 'auto'>): string;
35
+ //# sourceMappingURL=canonicalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../../src/baseline/canonicalize.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uDAAuD;AACvD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,kBAAkB,GAAG,cAAc,GAAG,OAAO,GAAG,KAAK,CAAC;AAE3F,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;CAC/C;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAU9D;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAUpD;AAWD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,aAAsB,GAC3B;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAa1D;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG,MAAM,CAgB1F"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Canonicalization for baseline comparison.
3
+ *
4
+ * The point is NOT prettiness — it is that a reordering, a re-indent, or a
5
+ * key shuffle must not read as drift. A noisy diff trains reviewers to bless
6
+ * without reading, which is how a real deletion ships inside a "formatting"
7
+ * update.
8
+ */
9
+ /** Parse `text` as JSON, or `undefined` when it is not JSON. */
10
+ export function tryParseJson(text) {
11
+ const trimmed = text.trim();
12
+ if (trimmed === '')
13
+ return undefined;
14
+ const first = trimmed[0];
15
+ if (first !== '{' && first !== '[' && first !== '"')
16
+ return undefined;
17
+ try {
18
+ return JSON.parse(trimmed);
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ /** Recursively sort object keys so key ORDER is never mistaken for a change. */
25
+ export function sortJsonKeys(value) {
26
+ if (Array.isArray(value))
27
+ return value.map(sortJsonKeys);
28
+ if (value !== null && typeof value === 'object') {
29
+ const out = {};
30
+ for (const k of Object.keys(value).sort()) {
31
+ out[k] = sortJsonKeys(value[k]);
32
+ }
33
+ return out;
34
+ }
35
+ return value;
36
+ }
37
+ function normalizeLines(text, sorted) {
38
+ const lines = text
39
+ .split(/\r?\n/)
40
+ .map((l) => l.replace(/[ \t]+$/, ''))
41
+ .filter((l) => l.trim() !== '');
42
+ if (sorted)
43
+ lines.sort();
44
+ return lines.join('\n');
45
+ }
46
+ /**
47
+ * Bring both sides of a comparison into the same form.
48
+ *
49
+ * `auto` resolves to `json-sorted-keys` only when BOTH sides parse as JSON —
50
+ * a pure function of the two inputs, so the same pair always canonicalizes the
51
+ * same way (a per-side sniff would make the result depend on which file was
52
+ * malformed).
53
+ */
54
+ export function canonicalizePair(expected, actual, form = 'auto') {
55
+ let resolved;
56
+ if (form === 'auto') {
57
+ const bothJson = tryParseJson(expected) !== undefined && tryParseJson(actual) !== undefined;
58
+ resolved = bothJson ? 'json-sorted-keys' : 'lines';
59
+ }
60
+ else {
61
+ resolved = form;
62
+ }
63
+ const one = (text) => ({
64
+ text: canonicalizeOne(text, resolved),
65
+ form: resolved,
66
+ });
67
+ return { expected: one(expected), actual: one(actual) };
68
+ }
69
+ /** Apply one concrete canonical form. Falls back to `lines` when JSON won't parse. */
70
+ export function canonicalizeOne(text, form) {
71
+ switch (form) {
72
+ case 'json-sorted-keys': {
73
+ const parsed = tryParseJson(text);
74
+ if (parsed === undefined)
75
+ return normalizeLines(text, false);
76
+ return JSON.stringify(sortJsonKeys(parsed), null, 2);
77
+ }
78
+ case 'lines-sorted':
79
+ return normalizeLines(text, true);
80
+ case 'lines':
81
+ return normalizeLines(text, false);
82
+ case 'raw':
83
+ return text.replace(/\r\n/g, '\n').replace(/\n+$/, '');
84
+ default:
85
+ return text;
86
+ }
87
+ }
@@ -0,0 +1,23 @@
1
+ import type { IWiringSource } from '@shrkcrft/core';
2
+ /** The value an extractor-backed baseline computes, ready to diff or commit. */
3
+ export interface IExtractorCompute {
4
+ /** Canonical serialization: a pretty-printed JSON array of sorted, unique ids. */
5
+ readonly text: string;
6
+ readonly ids: readonly string[];
7
+ readonly filesScanned: number;
8
+ /** Set when the source is misconfigured (never throws). */
9
+ readonly error?: string;
10
+ }
11
+ /**
12
+ * Compute a baseline from the extraction DSL — the pure alternative to shelling
13
+ * out.
14
+ *
15
+ * Emitted as a pretty-printed JSON array so the committed artifact is valid
16
+ * JSON *and* diffs one id per line: a review can see exactly which entry was
17
+ * gained or lost without a tool.
18
+ *
19
+ * No spawn, no network, no model — so an extractor baseline is safe to ship
20
+ * from a pack, unlike a `command` one.
21
+ */
22
+ export declare function computeBaselineFromExtractor(projectRoot: string, source: IWiringSource, excludeDirs?: readonly string[]): IExtractorCompute;
23
+ //# sourceMappingURL=compute-baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compute-baseline.d.ts","sourceRoot":"","sources":["../../src/baseline/compute-baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAKpD,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,iBAAiB,CAenB"}
@@ -0,0 +1,30 @@
1
+ import { matchesAny } from "../scan/glob.js";
2
+ import { readMatchingFiles } from "../util/walk-files.js";
3
+ import { extractTokens } from "../extract/extract-tokens.js";
4
+ /**
5
+ * Compute a baseline from the extraction DSL — the pure alternative to shelling
6
+ * out.
7
+ *
8
+ * Emitted as a pretty-printed JSON array so the committed artifact is valid
9
+ * JSON *and* diffs one id per line: a review can see exactly which entry was
10
+ * gained or lost without a tool.
11
+ *
12
+ * No spawn, no network, no model — so an extractor baseline is safe to ship
13
+ * from a pack, unlike a `command` one.
14
+ */
15
+ export function computeBaselineFromExtractor(projectRoot, source, excludeDirs = []) {
16
+ const cache = readMatchingFiles(projectRoot, source.files ?? [], new Set(excludeDirs));
17
+ const files = [...cache.entries()]
18
+ .filter(([path]) => matchesAny(path, source.files ?? []))
19
+ .map(([path, content]) => ({ path, content }));
20
+ const res = extractTokens(source, files);
21
+ if (res.error) {
22
+ return { text: '[]', ids: [], filesScanned: files.length, error: res.error };
23
+ }
24
+ const ids = [...new Set(res.sites.map((s) => s.token))].sort();
25
+ return {
26
+ text: JSON.stringify(ids, null, 2) + '\n',
27
+ ids,
28
+ filesScanned: files.length,
29
+ };
30
+ }
@@ -0,0 +1,68 @@
1
+ import type { IBaselineRule } from '@shrkcrft/core';
2
+ export declare const BASELINE_SCHEMA: "sharkcraft.baseline/v1";
3
+ /** How the two sides were compared. */
4
+ export type BaselineCompareMode = 'keyed-set' | 'element-set' | 'canonical-text';
5
+ /**
6
+ * Per-rule outcome. `skipped` is DISTINCT from `passed`: a recompute that
7
+ * produced nothing compared nothing, and a silently-empty compute would
8
+ * "match" an empty baseline forever.
9
+ */
10
+ export type BaselineStatus = 'passed' | 'failed' | 'skipped' | 'error';
11
+ export interface IBaselineDiff {
12
+ /** Entries present now but not in the committed baseline. */
13
+ readonly added: readonly string[];
14
+ /** Entries in the committed baseline but gone now. */
15
+ readonly removed: readonly string[];
16
+ /** How the comparison was performed. */
17
+ readonly mode: BaselineCompareMode;
18
+ /** The canonical form applied to both sides. */
19
+ readonly canonical: string;
20
+ /** True when the canonical texts are byte-identical. */
21
+ readonly identical: boolean;
22
+ }
23
+ export interface IBaselineResult {
24
+ readonly ruleId: string;
25
+ readonly description?: string;
26
+ readonly baselinePath: string;
27
+ readonly severity: 'error' | 'warning';
28
+ readonly status: BaselineStatus;
29
+ readonly direction: 'two-way' | 'additions-only' | 'no-shrink';
30
+ readonly diff?: IBaselineDiff;
31
+ /** Entry count on each side (keyed mode) or line count (text mode). */
32
+ readonly baselineCount: number;
33
+ readonly currentCount: number;
34
+ /** Set when the rule could not run (missing baseline file, failed compute, bad config). */
35
+ readonly error?: string;
36
+ /** Why the rule compared nothing. */
37
+ readonly skipReason?: string;
38
+ /** The remediation line for this rule. */
39
+ readonly hint: string;
40
+ }
41
+ export interface IBaselineReport {
42
+ readonly schema: typeof BASELINE_SCHEMA;
43
+ readonly results: readonly IBaselineResult[];
44
+ readonly evaluated: number;
45
+ readonly verdict: 'pass' | 'errors' | 'warnings';
46
+ }
47
+ /**
48
+ * Compare a committed baseline against a freshly computed value.
49
+ *
50
+ * With `keyBy` the two sides are compared as KEYED SETS (per-key added/removed);
51
+ * otherwise both are canonicalized and compared as text, with the line-level set
52
+ * difference reported so the human diff names what actually moved.
53
+ *
54
+ * Pure: the caller supplies both strings, so the same engine serves a
55
+ * shell-computed ledger, an extractor-computed inventory, and a unit test.
56
+ */
57
+ export declare function diffBaseline(rule: IBaselineRule, committed: string, current: string): IBaselineDiff;
58
+ /**
59
+ * Does this diff fail under the rule's direction?
60
+ *
61
+ * `two-way` (the default) is deliberate: the historical failure of hand-rolled
62
+ * ledgers is being one-directional, so a silent DELETION passes. Narrowing to
63
+ * one direction must be an explicit, visible choice.
64
+ */
65
+ export declare function baselineFails(rule: IBaselineRule, diff: IBaselineDiff): boolean;
66
+ /** Entry count for a side, matching the comparison mode. */
67
+ export declare function baselineCount(rule: IBaselineRule, text: string): number;
68
+ //# sourceMappingURL=diff-baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-baseline.d.ts","sourceRoot":"","sources":["../../src/baseline/diff-baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIpD,eAAO,MAAM,eAAe,EAAG,wBAAiC,CAAC;AAEjE,uCAAuC;AACvC,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAEjF;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvE,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,gBAAgB,GAAG,WAAW,CAAC;IAC/D,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,eAAe,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;CAClD;AAwCD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,aAAa,CA4Bf;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAU/E;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAOvE"}
@@ -0,0 +1,110 @@
1
+ import { extractJsonPath } from "./json-path-keys.js";
2
+ import { canonicalizePair, tryParseJson } from "./canonicalize.js";
3
+ export const BASELINE_SCHEMA = 'sharkcraft.baseline/v1';
4
+ /** Split canonical text into comparable entries (a set of non-empty lines). */
5
+ function textEntries(text) {
6
+ return text.split('\n').filter((l) => l.trim() !== '');
7
+ }
8
+ /**
9
+ * The scalar elements of a top-level JSON array, or `undefined` for any other
10
+ * shape.
11
+ *
12
+ * A committed inventory is most often exactly this — a JSON list of ids — and
13
+ * diffing it by LINE reports ` "beta"` with its quote and comma, plus the
14
+ * bracket lines, as changes. Naming the entry is the whole point of a ledger
15
+ * diff, so the array case is compared element-wise.
16
+ */
17
+ function jsonArrayEntries(text) {
18
+ const parsed = tryParseJson(text);
19
+ if (!Array.isArray(parsed))
20
+ return undefined;
21
+ const out = [];
22
+ for (const el of parsed) {
23
+ if (el === null || typeof el === 'object')
24
+ return undefined;
25
+ out.push(String(el));
26
+ }
27
+ return out;
28
+ }
29
+ /** Set difference preserving the left side's order, de-duplicated. */
30
+ function minus(left, right) {
31
+ const rightSet = new Set(right);
32
+ const seen = new Set();
33
+ const out = [];
34
+ for (const v of left) {
35
+ if (rightSet.has(v) || seen.has(v))
36
+ continue;
37
+ seen.add(v);
38
+ out.push(v);
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * Compare a committed baseline against a freshly computed value.
44
+ *
45
+ * With `keyBy` the two sides are compared as KEYED SETS (per-key added/removed);
46
+ * otherwise both are canonicalized and compared as text, with the line-level set
47
+ * difference reported so the human diff names what actually moved.
48
+ *
49
+ * Pure: the caller supplies both strings, so the same engine serves a
50
+ * shell-computed ledger, an extractor-computed inventory, and a unit test.
51
+ */
52
+ export function diffBaseline(rule, committed, current) {
53
+ const form = (rule.compute.canonical ?? 'auto');
54
+ const pair = canonicalizePair(committed, current, form);
55
+ if (rule.keyBy) {
56
+ const before = extractJsonPath(committed, rule.keyBy);
57
+ const after = extractJsonPath(current, rule.keyBy);
58
+ return {
59
+ added: minus(after, before),
60
+ removed: minus(before, after),
61
+ mode: 'keyed-set',
62
+ canonical: pair.expected.form,
63
+ identical: pair.expected.text === pair.actual.text,
64
+ };
65
+ }
66
+ const beforeArray = jsonArrayEntries(committed);
67
+ const afterArray = jsonArrayEntries(current);
68
+ const elementSet = beforeArray !== undefined && afterArray !== undefined;
69
+ const before = elementSet ? beforeArray : textEntries(pair.expected.text);
70
+ const after = elementSet ? afterArray : textEntries(pair.actual.text);
71
+ return {
72
+ added: minus(after, before),
73
+ removed: minus(before, after),
74
+ mode: elementSet ? 'element-set' : 'canonical-text',
75
+ canonical: pair.expected.form,
76
+ identical: pair.expected.text === pair.actual.text,
77
+ };
78
+ }
79
+ /**
80
+ * Does this diff fail under the rule's direction?
81
+ *
82
+ * `two-way` (the default) is deliberate: the historical failure of hand-rolled
83
+ * ledgers is being one-directional, so a silent DELETION passes. Narrowing to
84
+ * one direction must be an explicit, visible choice.
85
+ */
86
+ export function baselineFails(rule, diff) {
87
+ const direction = rule.direction ?? 'two-way';
88
+ const gained = diff.added.length > 0;
89
+ const lost = diff.removed.length > 0;
90
+ // A pure-reordering change is not drift under keyed-set semantics, but under
91
+ // canonical-text semantics an identical canonical form means no change at all.
92
+ if (!gained && !lost && diff.identical)
93
+ return false;
94
+ if (direction === 'additions-only')
95
+ return gained;
96
+ if (direction === 'no-shrink')
97
+ return lost;
98
+ return gained || lost || !diff.identical;
99
+ }
100
+ /** Entry count for a side, matching the comparison mode. */
101
+ export function baselineCount(rule, text) {
102
+ if (rule.keyBy)
103
+ return extractJsonPath(text, rule.keyBy).length;
104
+ const asArray = jsonArrayEntries(text);
105
+ if (asArray !== undefined)
106
+ return asArray.length;
107
+ const form = (rule.compute.canonical ?? 'auto');
108
+ const pair = canonicalizePair(text, text, form);
109
+ return textEntries(pair.expected.text).length;
110
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Extract every id `jsonPath` selects from `text` (JSON only — a hand-rolled
3
+ * YAML reader would be a silent-wrong-answer risk). Non-JSON text yields no
4
+ * keys, which the caller reports as an empty side rather than a false match.
5
+ *
6
+ * Delegates to the shared extraction DSL so `keyBy` and a wiring rule's
7
+ * `json-path` source resolve paths identically.
8
+ */
9
+ export declare function extractJsonPath(text: string, jsonPath: string): string[];
10
+ //# sourceMappingURL=json-path-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-path-keys.d.ts","sourceRoot":"","sources":["../../src/baseline/json-path-keys.ts"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAGxE"}
@@ -0,0 +1,13 @@
1
+ import { extractTokens } from "../extract/extract-tokens.js";
2
+ /**
3
+ * Extract every id `jsonPath` selects from `text` (JSON only — a hand-rolled
4
+ * YAML reader would be a silent-wrong-answer risk). Non-JSON text yields no
5
+ * keys, which the caller reports as an empty side rather than a false match.
6
+ *
7
+ * Delegates to the shared extraction DSL so `keyBy` and a wiring rule's
8
+ * `json-path` source resolve paths identically.
9
+ */
10
+ export function extractJsonPath(text, jsonPath) {
11
+ const source = { files: ['<inline>'], extract: 'json-path', jsonPath };
12
+ return extractTokens(source, [{ path: '<inline>', content: text }]).sites.map((s) => s.token);
13
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Which lexical zone of a file a character belongs to.
3
+ *
4
+ * The dominant false positive of a text-based policy rule is a hit inside a
5
+ * comment ("we used to call Date.now() here") or inside an unrelated string.
6
+ * The dominant false NEGATIVE of a language-scoped linter is the opposite: it
7
+ * cannot see inside an inline `template:` string at all. Classifying zones
8
+ * lexically lets one rule say which of the three it means — without a parser,
9
+ * and identically across `.ts`, `.kt`, `.scss` and friends.
10
+ */
11
+ export type CodeZoneKind = 'code' | 'string' | 'comment';
12
+ /** A half-open `[start, end)` span of one zone kind. */
13
+ export interface ICodeZone {
14
+ readonly kind: CodeZoneKind;
15
+ readonly start: number;
16
+ readonly end: number;
17
+ }
18
+ /**
19
+ * Split `content` into contiguous code / string / comment zones.
20
+ *
21
+ * A lexer, not a parser: it recognises the C/JS-family string quotes
22
+ * (`'`, `"`, `` ` ``) and comment forms (`//`, `/* … *\/`). Documented limits —
23
+ * a regex literal containing `//` reads as a comment, and `#`-comment languages
24
+ * are reported as code.
25
+ */
26
+ export declare function lexCodeZones(content: string): readonly ICodeZone[];
27
+ /**
28
+ * Zone kind at a character offset. `code` when the offset falls outside every
29
+ * recorded zone (e.g. past end-of-content).
30
+ */
31
+ export declare function zoneAt(zones: readonly ICodeZone[], index: number): CodeZoneKind;
32
+ //# sourceMappingURL=code-zones.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-zones.d.ts","sourceRoot":"","sources":["../../src/extract/code-zones.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CA4BlE;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAY/E"}
@@ -0,0 +1,59 @@
1
+ import { skipComment, skipString } from "./scan-literals.js";
2
+ /**
3
+ * Split `content` into contiguous code / string / comment zones.
4
+ *
5
+ * A lexer, not a parser: it recognises the C/JS-family string quotes
6
+ * (`'`, `"`, `` ` ``) and comment forms (`//`, `/* … *\/`). Documented limits —
7
+ * a regex literal containing `//` reads as a comment, and `#`-comment languages
8
+ * are reported as code.
9
+ */
10
+ export function lexCodeZones(content) {
11
+ const zones = [];
12
+ let codeStart = 0;
13
+ const flushCode = (upTo) => {
14
+ if (upTo > codeStart)
15
+ zones.push({ kind: 'code', start: codeStart, end: upTo });
16
+ };
17
+ for (let i = 0; i < content.length; i += 1) {
18
+ const c = content[i];
19
+ if (c === '"' || c === "'" || c === '`') {
20
+ flushCode(i);
21
+ const end = skipString(content, i);
22
+ zones.push({ kind: 'string', start: i, end: end + 1 });
23
+ i = end;
24
+ codeStart = i + 1;
25
+ continue;
26
+ }
27
+ if (c === '/') {
28
+ const end = skipComment(content, i);
29
+ if (end >= 0) {
30
+ flushCode(i);
31
+ zones.push({ kind: 'comment', start: i, end: end + 1 });
32
+ i = end;
33
+ codeStart = i + 1;
34
+ }
35
+ }
36
+ }
37
+ flushCode(content.length);
38
+ return zones;
39
+ }
40
+ /**
41
+ * Zone kind at a character offset. `code` when the offset falls outside every
42
+ * recorded zone (e.g. past end-of-content).
43
+ */
44
+ export function zoneAt(zones, index) {
45
+ // Zones are contiguous and ascending — binary search.
46
+ let lo = 0;
47
+ let hi = zones.length - 1;
48
+ while (lo <= hi) {
49
+ const mid = (lo + hi) >> 1;
50
+ const z = zones[mid];
51
+ if (index < z.start)
52
+ hi = mid - 1;
53
+ else if (index >= z.end)
54
+ lo = mid + 1;
55
+ else
56
+ return z.kind;
57
+ }
58
+ return 'code';
59
+ }
@@ -0,0 +1,30 @@
1
+ import { type IWiringSource } from '@shrkcrft/core';
2
+ /** A file made available to an extractor. */
3
+ export interface IExtractFileEntry {
4
+ /** Project-relative POSIX path. */
5
+ readonly path: string;
6
+ readonly content: string;
7
+ }
8
+ /** A captured id + where it was captured. */
9
+ export interface IExtractedSite {
10
+ readonly token: string;
11
+ readonly file: string;
12
+ readonly line: number;
13
+ }
14
+ /** Outcome of running one source: the sites, or a configuration error. */
15
+ export interface IExtractResult {
16
+ readonly sites: readonly IExtractedSite[];
17
+ /** Set when the SOURCE is misconfigured (never thrown — rules degrade). */
18
+ readonly error?: string;
19
+ }
20
+ /**
21
+ * Run one source's extractor over the given files.
22
+ *
23
+ * This is the reusable core of the completeness plane: wiring rules, registry
24
+ * inventories, and extractor-backed baselines all harvest their ids through
25
+ * here, so every one of them honours the exact same semantics (and gains every
26
+ * new extractor kind for free). Never throws — a misconfigured source returns
27
+ * an `error` and no sites.
28
+ */
29
+ export declare function extractTokens(source: IWiringSource, files: readonly IExtractFileEntry[]): IExtractResult;
30
+ //# sourceMappingURL=extract-tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-tokens.d.ts","sourceRoot":"","sources":["../../src/extract/extract-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAUxB,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,iBAAiB,EAAE,GAClC,cAAc,CA4DhB"}