@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,89 @@
1
+ import { safeCompile } from "../util/safe-regex.js";
2
+ /** The head of a file, as the header contract sees it. */
3
+ function headOf(content, withinLines) {
4
+ const lines = content.split('\n');
5
+ return lines.slice(0, Math.max(1, withinLines)).join('\n');
6
+ }
7
+ /**
8
+ * Derive the globs to search for MISLABELED files when the rule doesn't name
9
+ * them: one `**\/*.<ext>` per distinct extension in `generatedGlob`. Bounded and
10
+ * deterministic — never a whole-tree read of every file type.
11
+ */
12
+ export function deriveOutsideGlobs(generatedGlob) {
13
+ const exts = new Set();
14
+ for (const g of generatedGlob) {
15
+ const m = /\.([A-Za-z0-9]+)$/.exec(g);
16
+ if (m)
17
+ exts.add(m[1]);
18
+ }
19
+ return [...exts].sort().map((e) => `**/*.${e}`);
20
+ }
21
+ /**
22
+ * Check the "this file is generated" header contract.
23
+ *
24
+ * Pure: the caller supplies the file contents, so this runs with no regen
25
+ * command, no temp dir, and no spawn — a header-only rule is fully useful (and
26
+ * safe to ship from a pack) on its own.
27
+ *
28
+ * `outside` should contain candidate files NOT in `generatedGlob`; pass an empty
29
+ * map when `forbidOutside` is off.
30
+ */
31
+ export function checkProvenanceHeaders(rule, generated, outside) {
32
+ const header = rule.provenanceHeader;
33
+ if (!header)
34
+ return { findings: [] };
35
+ const { re, error } = safeCompile(header.mustMatch, header.flags);
36
+ if (error || !re)
37
+ return { findings: [], error: `provenanceHeader ${error}` };
38
+ const severity = rule.severity ?? 'error';
39
+ const within = header.withinLines ?? 10;
40
+ const findings = [];
41
+ const test = (content) => {
42
+ re.lastIndex = 0;
43
+ return re.test(headOf(content, within));
44
+ };
45
+ for (const file of [...generated.keys()].sort()) {
46
+ const content = generated.get(file);
47
+ if (!test(content)) {
48
+ findings.push({
49
+ ruleId: rule.id,
50
+ file,
51
+ kind: 'missing-header',
52
+ severity,
53
+ message: `generated file carries no provenance header matching /${header.mustMatch}/`,
54
+ });
55
+ continue;
56
+ }
57
+ // Advisory only — a header that names its regen command saves the next
58
+ // editor a search, but its absence is not a correctness defect.
59
+ if (header.pointsToRegenCommand && rule.regen) {
60
+ const head = headOf(content, within);
61
+ const verb = rule.regen.trim().split(/\s+/)[0] ?? '';
62
+ if (verb !== '' && !head.includes(verb)) {
63
+ findings.push({
64
+ ruleId: rule.id,
65
+ file,
66
+ kind: 'no-regen-pointer',
67
+ severity: 'warning',
68
+ message: `provenance header does not name the regen command (\`${verb} …\`)`,
69
+ });
70
+ }
71
+ }
72
+ }
73
+ if (header.forbidOutside) {
74
+ for (const file of [...outside.keys()].sort()) {
75
+ if (generated.has(file))
76
+ continue;
77
+ if (!test(outside.get(file)))
78
+ continue;
79
+ findings.push({
80
+ ruleId: rule.id,
81
+ file,
82
+ kind: 'mislabeled',
83
+ severity,
84
+ message: 'hand-written file carries a generated-file header — either move it under the generated glob or drop the header',
85
+ });
86
+ }
87
+ }
88
+ return { findings };
89
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Compare a committed generated tree against a freshly regenerated one.
3
+ *
4
+ * The comparison is deliberately SYMMETRIC. A regen that writes a subset (a
5
+ * template dropped, an input renamed) leaves stale committed files behind, and
6
+ * a one-directional check would report that as clean — the same one-way blind
7
+ * spot that makes hand-rolled drift tests untrustworthy.
8
+ */
9
+ /** One file that differs between the committed tree and the regenerated one. */
10
+ export interface IGeneratedFileDiff {
11
+ readonly file: string;
12
+ /**
13
+ * `content` — present on both sides, bytes differ (the hand-edit signal).
14
+ * `only-committed` — regen no longer produces it (stale committed file).
15
+ * `only-regenerated` — regen produces it but it was never committed.
16
+ */
17
+ readonly kind: 'content' | 'only-committed' | 'only-regenerated';
18
+ }
19
+ export interface IGeneratedTreeDiff {
20
+ readonly differences: readonly IGeneratedFileDiff[];
21
+ readonly committedCount: number;
22
+ readonly regeneratedCount: number;
23
+ }
24
+ /** Compare two path→content maps. Pure — the caller does all the IO. */
25
+ export declare function compareGeneratedTrees(committed: ReadonlyMap<string, string>, regenerated: ReadonlyMap<string, string>, compare?: 'bytes' | 'normalized-whitespace'): IGeneratedTreeDiff;
26
+ //# sourceMappingURL=compare-trees.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-trees.d.ts","sourceRoot":"","sources":["../../src/generated/compare-trees.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gFAAgF;AAChF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;CAClE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACpD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAYD,wEAAwE;AACxE,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,OAAO,GAAE,OAAO,GAAG,uBAAiC,GACnD,kBAAkB,CAqBpB"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Compare a committed generated tree against a freshly regenerated one.
3
+ *
4
+ * The comparison is deliberately SYMMETRIC. A regen that writes a subset (a
5
+ * template dropped, an input renamed) leaves stale committed files behind, and
6
+ * a one-directional check would report that as clean — the same one-way blind
7
+ * spot that makes hand-rolled drift tests untrustworthy.
8
+ */
9
+ /** Normalize line endings + trailing whitespace, and drop a trailing newline. */
10
+ function normalizeWhitespace(text) {
11
+ return text
12
+ .replace(/\r\n/g, '\n')
13
+ .split('\n')
14
+ .map((l) => l.replace(/[ \t]+$/, ''))
15
+ .join('\n')
16
+ .replace(/\n+$/, '');
17
+ }
18
+ /** Compare two path→content maps. Pure — the caller does all the IO. */
19
+ export function compareGeneratedTrees(committed, regenerated, compare = 'bytes') {
20
+ const norm = (s) => (compare === 'bytes' ? s : normalizeWhitespace(s));
21
+ const differences = [];
22
+ for (const file of [...committed.keys()].sort()) {
23
+ const regen = regenerated.get(file);
24
+ if (regen === undefined) {
25
+ differences.push({ file, kind: 'only-committed' });
26
+ continue;
27
+ }
28
+ if (norm(committed.get(file)) !== norm(regen)) {
29
+ differences.push({ file, kind: 'content' });
30
+ }
31
+ }
32
+ for (const file of [...regenerated.keys()].sort()) {
33
+ if (!committed.has(file))
34
+ differences.push({ file, kind: 'only-regenerated' });
35
+ }
36
+ return {
37
+ differences,
38
+ committedCount: committed.size,
39
+ regeneratedCount: regenerated.size,
40
+ };
41
+ }
@@ -0,0 +1,17 @@
1
+ import type { IGeneratedArtifactRule } from '@shrkcrft/core';
2
+ /** The committed generated files plus the mislabel-candidate set. */
3
+ export interface IGeneratedScan {
4
+ /** Files matching `generatedGlob` (project-relative path → content). */
5
+ readonly generated: ReadonlyMap<string, string>;
6
+ /** Candidates for the `forbidOutside` mislabel check (empty when it is off). */
7
+ readonly outside: ReadonlyMap<string, string>;
8
+ /** Globs actually used for the outside scan (derived or configured). */
9
+ readonly outsideGlobs: readonly string[];
10
+ }
11
+ /**
12
+ * Read the committed side of a generated-artifact rule. Pure filesystem reads —
13
+ * the regen command (if any) is orchestrated by the caller, which is also where
14
+ * the local-config-only trust decision is enforced.
15
+ */
16
+ export declare function scanGeneratedFiles(projectRoot: string, rule: IGeneratedArtifactRule, excludeDirs?: readonly string[]): IGeneratedScan;
17
+ //# sourceMappingURL=scan-generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-generated.d.ts","sourceRoot":"","sources":["../../src/generated/scan-generated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAK7D,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,sBAAsB,EAC5B,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,cAAc,CAmBhB"}
@@ -0,0 +1,27 @@
1
+ import { matchesAny } from "../scan/glob.js";
2
+ import { readMatchingFiles } from "../util/walk-files.js";
3
+ import { deriveOutsideGlobs } from "./check-provenance.js";
4
+ /**
5
+ * Read the committed side of a generated-artifact rule. Pure filesystem reads —
6
+ * the regen command (if any) is orchestrated by the caller, which is also where
7
+ * the local-config-only trust decision is enforced.
8
+ */
9
+ export function scanGeneratedFiles(projectRoot, rule, excludeDirs = []) {
10
+ const exclude = new Set(excludeDirs);
11
+ const generated = readMatchingFiles(projectRoot, rule.generatedGlob, exclude);
12
+ const wantOutside = rule.provenanceHeader?.forbidOutside === true;
13
+ if (!wantOutside) {
14
+ return { generated, outside: new Map(), outsideGlobs: [] };
15
+ }
16
+ const outsideGlobs = rule.provenanceHeader?.outsideGlob && rule.provenanceHeader.outsideGlob.length > 0
17
+ ? [...rule.provenanceHeader.outsideGlob]
18
+ : deriveOutsideGlobs(rule.generatedGlob);
19
+ const all = readMatchingFiles(projectRoot, outsideGlobs, exclude);
20
+ const outside = new Map();
21
+ for (const [path, content] of all) {
22
+ if (matchesAny(path, rule.generatedGlob))
23
+ continue;
24
+ outside.set(path, content);
25
+ }
26
+ return { generated, outside, outsideGlobs };
27
+ }
package/dist/index.d.ts CHANGED
@@ -14,6 +14,17 @@ export * from './wiring/trace-literal.js';
14
14
  export * from './policy/extract-templates.js';
15
15
  export * from './policy/evaluate-policy.js';
16
16
  export * from './policy/run-policy.js';
17
+ export * from './extract/scan-literals.js';
18
+ export * from './extract/extract-tokens.js';
19
+ export * from './extract/code-zones.js';
20
+ export * from './extract/inspect-source.js';
21
+ export * from './baseline/canonicalize.js';
22
+ export * from './baseline/json-path-keys.js';
23
+ export * from './baseline/diff-baseline.js';
24
+ export * from './baseline/compute-baseline.js';
25
+ export * from './generated/check-provenance.js';
26
+ export * from './generated/compare-trees.js';
27
+ export * from './generated/scan-generated.js';
17
28
  export * from './util/safe-regex.js';
18
29
  export * from './util/walk-files.js';
19
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -14,5 +14,16 @@ export * from "./wiring/trace-literal.js";
14
14
  export * from "./policy/extract-templates.js";
15
15
  export * from "./policy/evaluate-policy.js";
16
16
  export * from "./policy/run-policy.js";
17
+ export * from "./extract/scan-literals.js";
18
+ export * from "./extract/extract-tokens.js";
19
+ export * from "./extract/code-zones.js";
20
+ export * from "./extract/inspect-source.js";
21
+ export * from "./baseline/canonicalize.js";
22
+ export * from "./baseline/json-path-keys.js";
23
+ export * from "./baseline/diff-baseline.js";
24
+ export * from "./baseline/compute-baseline.js";
25
+ export * from "./generated/check-provenance.js";
26
+ export * from "./generated/compare-trees.js";
27
+ export * from "./generated/scan-generated.js";
17
28
  export * from "./util/safe-regex.js";
18
29
  export * from "./util/walk-files.js";
@@ -11,6 +11,8 @@ export interface IPolicyUnit {
11
11
  readonly baseLine: number;
12
12
  /** Marks an inline-template unit (for clearer reporting). */
13
13
  readonly inlineTemplate?: boolean;
14
+ /** True when the file matched one of the rule's `exemptFiles` globs. */
15
+ readonly exemptFile?: boolean;
14
16
  }
15
17
  export interface IPolicyFinding {
16
18
  readonly ruleId: string;
@@ -24,24 +26,55 @@ export interface IPolicyFinding {
24
26
  readonly severity: 'error' | 'warning';
25
27
  readonly inlineTemplate?: boolean;
26
28
  }
29
+ /** A hit that WAS matched but dropped by an exemption — kept so it can be shown. */
30
+ export interface IPolicySuppression {
31
+ readonly ruleId: string;
32
+ readonly file: string;
33
+ readonly line: number;
34
+ readonly match: string;
35
+ /** Which exemption applied. */
36
+ readonly via: 'exemptFiles' | 'exemptLines' | 'scanZone';
37
+ }
38
+ /**
39
+ * Per-rule outcome. `skipped` is DISTINCT from `passed`: a rule that scanned no
40
+ * units checked nothing, and calling that a pass is exactly how a stale glob
41
+ * ships a real violation.
42
+ */
43
+ export type PolicyRuleStatus = 'passed' | 'failed' | 'skipped' | 'error';
27
44
  export interface IPolicyRuleResult {
28
45
  readonly ruleId: string;
29
46
  readonly surface: PolicySurface;
30
47
  readonly severity: 'error' | 'warning';
48
+ readonly status: PolicyRuleStatus;
31
49
  readonly findingCount: number;
50
+ /** Hits dropped by an exemption (still counted, never silent). */
51
+ readonly suppressedCount: number;
52
+ /** Content units actually scanned — the stale-glob signal. */
53
+ readonly unitsScanned: number;
32
54
  readonly error?: string;
33
55
  }
56
+ /** A rule that scanned nothing, reported loudly instead of as a green pass. */
57
+ export interface IPolicySkip {
58
+ readonly ruleId: string;
59
+ readonly reason: string;
60
+ /** True when the rule set `failOnEmpty` — the skip counts as a failure. */
61
+ readonly failed: boolean;
62
+ readonly severity: 'error' | 'warning';
63
+ }
34
64
  export interface IPolicyReport {
35
65
  readonly schema: typeof POLICY_LINT_SCHEMA;
36
66
  readonly rules: readonly IPolicyRuleResult[];
37
67
  readonly findings: readonly IPolicyFinding[];
38
68
  readonly diagnostics: readonly string[];
69
+ /** Hits an exemption dropped — surfaced by `policy-lint explain`. */
70
+ readonly suppressed: readonly IPolicySuppression[];
71
+ /** Rules that scanned nothing. */
72
+ readonly skipped: readonly IPolicySkip[];
39
73
  /**
40
74
  * Count of rules that actually scanned ≥1 unit. A rule whose globs matched 0
41
75
  * files (e.g. a `style` rule in a project with no stylesheets) is NOT
42
- * evaluated — a silent no-op the gate surfaces as `skipped` rather than a
43
- * green pass. Misconfigured rules count as evaluated so their error is not
44
- * swallowed by the gate's `evaluated === 0` skip path.
76
+ * evaluated — a loud skip rather than a green pass. Misconfigured rules count
77
+ * as evaluated so their error is not swallowed.
45
78
  */
46
79
  readonly evaluated: number;
47
80
  readonly verdict: 'pass' | 'errors' | 'warnings';
@@ -51,8 +84,11 @@ export type PolicyUnitResolver = (rule: IPolicyRule) => readonly IPolicyUnit[];
51
84
  /**
52
85
  * Pure policy evaluation. Each rule's regex is run over the units the resolver
53
86
  * supplies; matches become findings (capture group 1 is the reported token when
54
- * present). A misconfigured rule (uncompilable regex) degrades to a diagnostic
55
- * never throws, so one bad rule cannot crash the check.
87
+ * present). Exemptions (`exemptFiles` / `exemptLines`) and the lexical `scan`
88
+ * zone drop hits into {@link IPolicyReport.suppressed} rather than deleting
89
+ * them, so `explain` can show what was matched AND what was let through. A
90
+ * misconfigured rule (uncompilable regex) degrades to a diagnostic — never
91
+ * throws, so one bad rule cannot crash the check.
56
92
  */
57
93
  export declare function evaluatePolicy(rules: readonly IPolicyRule[], resolve: PolicyUnitResolver): IPolicyReport;
58
94
  //# sourceMappingURL=evaluate-policy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGjE,eAAO,MAAM,kBAAkB,EAAG,2BAAoC,CAAC;AAEvE;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,kBAAkB,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;CAClD;AAED,6FAA6F;AAC7F,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,WAAW,EAAE,CAAC;AAgB/E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,aAAa,CA2ExG"}
1
+ {"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { lexCodeZones, zoneAt } from "../extract/code-zones.js";
1
2
  import { safeCompile } from "../util/safe-regex.js";
2
3
  export const POLICY_LINT_SCHEMA = 'sharkcraft.policy-lint/v1';
3
4
  function lineWithin(content, index) {
@@ -13,16 +14,44 @@ function truncate(s, max = 120) {
13
14
  const oneLine = s.replace(/\s+/g, ' ').trim();
14
15
  return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
15
16
  }
17
+ /** Does the marker appear on `line` (1-based, within `lines`) or the one above it? */
18
+ function markerNear(lines, line, marker) {
19
+ const here = lines[line - 1];
20
+ if (here !== undefined && here.includes(marker))
21
+ return true;
22
+ const above = lines[line - 2];
23
+ return above !== undefined && above.includes(marker);
24
+ }
25
+ /**
26
+ * Whether the zone rule keeps a match at `index`. `all` keeps everything; the
27
+ * narrower zones require the match to START in the named zone. Not applied to
28
+ * inline-template units (their content is already a string body).
29
+ */
30
+ function zoneKeeps(zone, unit, zones, index) {
31
+ if (zone === 'all' || unit.inlineTemplate || zones === undefined)
32
+ return true;
33
+ const kind = zoneAt(zones, index);
34
+ if (zone === 'code')
35
+ return kind === 'code';
36
+ if (zone === 'strings')
37
+ return kind === 'string';
38
+ return kind === 'comment';
39
+ }
16
40
  /**
17
41
  * Pure policy evaluation. Each rule's regex is run over the units the resolver
18
42
  * supplies; matches become findings (capture group 1 is the reported token when
19
- * present). A misconfigured rule (uncompilable regex) degrades to a diagnostic
20
- * never throws, so one bad rule cannot crash the check.
43
+ * present). Exemptions (`exemptFiles` / `exemptLines`) and the lexical `scan`
44
+ * zone drop hits into {@link IPolicyReport.suppressed} rather than deleting
45
+ * them, so `explain` can show what was matched AND what was let through. A
46
+ * misconfigured rule (uncompilable regex) degrades to a diagnostic — never
47
+ * throws, so one bad rule cannot crash the check.
21
48
  */
22
49
  export function evaluatePolicy(rules, resolve) {
23
50
  const ruleResults = [];
24
51
  const findings = [];
52
+ const suppressed = [];
25
53
  const diagnostics = [];
54
+ const skipped = [];
26
55
  let evaluated = 0;
27
56
  let misconfigError = false;
28
57
  let misconfigWarn = false;
@@ -36,18 +65,29 @@ export function evaluatePolicy(rules, resolve) {
36
65
  misconfigError = true;
37
66
  else
38
67
  misconfigWarn = true;
39
- ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: 0, error: msg });
68
+ ruleResults.push({
69
+ ruleId: rule.id,
70
+ surface: rule.surface,
71
+ severity,
72
+ status: 'error',
73
+ findingCount: 0,
74
+ suppressedCount: 0,
75
+ unitsScanned: 0,
76
+ error: msg,
77
+ });
40
78
  // A misconfigured rule attempted to run — count it as evaluated so its
41
79
  // error isn't swallowed by the gate's `evaluated === 0` skip path.
42
80
  evaluated += 1;
43
81
  continue;
44
82
  }
45
83
  const units = resolve(rule);
46
- if (units.length > 0)
47
- evaluated += 1;
84
+ const zone = rule.scan ?? 'all';
48
85
  let count = 0;
86
+ let suppressedCount = 0;
49
87
  let zeroWidth = false;
50
88
  for (const unit of units) {
89
+ const lines = rule.exemptLines ? unit.content.split('\n') : undefined;
90
+ const zones = zone !== 'all' && !unit.inlineTemplate ? lexCodeZones(unit.content) : undefined;
51
91
  re.lastIndex = 0;
52
92
  let m;
53
93
  while ((m = re.exec(unit.content)) !== null) {
@@ -59,13 +99,30 @@ export function evaluatePolicy(rules, resolve) {
59
99
  continue;
60
100
  }
61
101
  const token = m[1] !== undefined ? m[1] : m[0];
62
- const line = unit.baseLine - 1 + lineWithin(unit.content, m.index);
102
+ const localLine = lineWithin(unit.content, m.index);
103
+ const line = unit.baseLine - 1 + localLine;
104
+ const match = truncate(token);
105
+ if (unit.exemptFile) {
106
+ suppressed.push({ ruleId: rule.id, file: unit.path, line, match, via: 'exemptFiles' });
107
+ suppressedCount += 1;
108
+ continue;
109
+ }
110
+ if (!zoneKeeps(zone, unit, zones, m.index)) {
111
+ suppressed.push({ ruleId: rule.id, file: unit.path, line, match, via: 'scanZone' });
112
+ suppressedCount += 1;
113
+ continue;
114
+ }
115
+ if (lines && markerNear(lines, localLine, rule.exemptLines)) {
116
+ suppressed.push({ ruleId: rule.id, file: unit.path, line, match, via: 'exemptLines' });
117
+ suppressedCount += 1;
118
+ continue;
119
+ }
63
120
  findings.push({
64
121
  ruleId: rule.id,
65
122
  surface: rule.surface,
66
123
  file: unit.path,
67
124
  line,
68
- match: truncate(token),
125
+ match,
69
126
  message: rule.message,
70
127
  ...(rule.suggest ? { suggest: rule.suggest } : {}),
71
128
  severity,
@@ -81,11 +138,54 @@ export function evaluatePolicy(rules, resolve) {
81
138
  misconfigError = true;
82
139
  else
83
140
  misconfigWarn = true;
84
- ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: count, error: msg });
141
+ ruleResults.push({
142
+ ruleId: rule.id,
143
+ surface: rule.surface,
144
+ severity,
145
+ status: 'error',
146
+ findingCount: count,
147
+ suppressedCount,
148
+ unitsScanned: units.length,
149
+ error: msg,
150
+ });
151
+ evaluated += 1;
152
+ continue;
85
153
  }
86
- else {
87
- ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: count });
154
+ if (units.length === 0) {
155
+ const failed = rule.failOnEmpty === true;
156
+ skipped.push({
157
+ ruleId: rule.id,
158
+ reason: '0 content units matched the rule globs',
159
+ failed,
160
+ severity,
161
+ });
162
+ if (failed) {
163
+ if (severity === 'error')
164
+ misconfigError = true;
165
+ else
166
+ misconfigWarn = true;
167
+ }
168
+ ruleResults.push({
169
+ ruleId: rule.id,
170
+ surface: rule.surface,
171
+ severity,
172
+ status: failed ? 'failed' : 'skipped',
173
+ findingCount: 0,
174
+ suppressedCount: 0,
175
+ unitsScanned: 0,
176
+ });
177
+ continue;
88
178
  }
179
+ evaluated += 1;
180
+ ruleResults.push({
181
+ ruleId: rule.id,
182
+ surface: rule.surface,
183
+ severity,
184
+ status: count > 0 ? 'failed' : 'passed',
185
+ findingCount: count,
186
+ suppressedCount,
187
+ unitsScanned: units.length,
188
+ });
89
189
  }
90
190
  const hasError = misconfigError || findings.some((f) => f.severity === 'error');
91
191
  const hasWarn = misconfigWarn || findings.some((f) => f.severity === 'warning');
@@ -94,6 +194,8 @@ export function evaluatePolicy(rules, resolve) {
94
194
  rules: ruleResults,
95
195
  findings,
96
196
  diagnostics,
197
+ suppressed,
198
+ skipped,
97
199
  evaluated,
98
200
  verdict: hasError ? 'errors' : hasWarn ? 'warnings' : 'pass',
99
201
  };
@@ -1 +1 @@
1
- {"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIjE,OAAO,EAAkB,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAY5F,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CA6Cf"}
1
+ {"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIjE,OAAO,EAAkB,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAY5F,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAkEf"}
@@ -35,7 +35,16 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
35
35
  selected = selected.filter((r) => changed.some((c) => matchesAny(c, globsFor(r))));
36
36
  }
37
37
  if (selected.length === 0) {
38
- return { schema: 'sharkcraft.policy-lint/v1', rules: [], findings: [], diagnostics: [], evaluated: 0, verdict: 'pass' };
38
+ return {
39
+ schema: 'sharkcraft.policy-lint/v1',
40
+ rules: [],
41
+ findings: [],
42
+ diagnostics: [],
43
+ suppressed: [],
44
+ skipped: [],
45
+ evaluated: 0,
46
+ verdict: 'pass',
47
+ };
39
48
  }
40
49
  // Under --changed-only, restrict the SCANNED files to the changed set too (not
41
50
  // just rule selection). Per-file regex findings have no cross-file dependency,
@@ -47,21 +56,33 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
47
56
  const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
48
57
  return evaluatePolicy(selected, (rule) => {
49
58
  const globs = globsFor(rule);
59
+ // `exemptFiles` never removes the file from the scan — it MARKS it, so the
60
+ // hits it would have produced are reported as suppressed rather than
61
+ // vanishing. A silently-dropped exemption is indistinguishable from a stale
62
+ // glob, which is the failure mode this whole plane exists to prevent.
63
+ const exempt = rule.exemptFiles && rule.exemptFiles.length > 0 ? rule.exemptFiles : undefined;
50
64
  const units = [];
51
65
  for (const [path, content] of cache) {
52
66
  if (changedSet && !changedSet.has(path))
53
67
  continue;
54
68
  if (!matchesAny(path, globs))
55
69
  continue;
70
+ const exemptFile = exempt !== undefined && matchesAny(path, exempt);
56
71
  const ext = nodePath.extname(path).toLowerCase();
57
72
  if (rule.surface === 'template' && SOURCE_EXT.has(ext)) {
58
73
  for (const tpl of extractInlineTemplates(content)) {
59
- units.push({ path, content: tpl.body, baseLine: tpl.startLine, inlineTemplate: true });
74
+ units.push({
75
+ path,
76
+ content: tpl.body,
77
+ baseLine: tpl.startLine,
78
+ inlineTemplate: true,
79
+ ...(exemptFile ? { exemptFile: true } : {}),
80
+ });
60
81
  }
61
82
  }
62
83
  else {
63
84
  // .html on the template surface, and all style/ts files: scan whole.
64
- units.push({ path, content, baseLine: 1 });
85
+ units.push({ path, content, baseLine: 1, ...(exemptFile ? { exemptFile: true } : {}) });
65
86
  }
66
87
  }
67
88
  return units;