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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/dist/baseline/compute-baseline.d.ts +8 -0
  2. package/dist/baseline/compute-baseline.d.ts.map +1 -1
  3. package/dist/baseline/compute-baseline.js +8 -7
  4. package/dist/baseline/diff-baseline.d.ts +28 -0
  5. package/dist/baseline/diff-baseline.d.ts.map +1 -1
  6. package/dist/baseline/diff-baseline.js +29 -0
  7. package/dist/evaluate/boundary-unit-finding.d.ts +10 -0
  8. package/dist/evaluate/boundary-unit-finding.d.ts.map +1 -0
  9. package/dist/evaluate/boundary-unit-finding.js +21 -0
  10. package/dist/evaluate/boundary-unit-kind.d.ts +7 -0
  11. package/dist/evaluate/boundary-unit-kind.d.ts.map +1 -0
  12. package/dist/evaluate/boundary-unit-kind.js +14 -0
  13. package/dist/evaluate/evaluate-boundaries.d.ts +230 -3
  14. package/dist/evaluate/evaluate-boundaries.d.ts.map +1 -1
  15. package/dist/evaluate/evaluate-boundaries.js +481 -53
  16. package/dist/evaluate/i-boundary-rule-settle-input.d.ts +27 -0
  17. package/dist/evaluate/i-boundary-rule-settle-input.d.ts.map +1 -0
  18. package/dist/evaluate/i-boundary-rule-settle-input.js +1 -0
  19. package/dist/evaluate/i-boundary-rule-settlement.d.ts +23 -0
  20. package/dist/evaluate/i-boundary-rule-settlement.d.ts.map +1 -0
  21. package/dist/evaluate/i-boundary-rule-settlement.js +1 -0
  22. package/dist/evaluate/i-boundary-unit-finding.d.ts +22 -0
  23. package/dist/evaluate/i-boundary-unit-finding.d.ts.map +1 -0
  24. package/dist/evaluate/i-boundary-unit-finding.js +1 -0
  25. package/dist/evaluate/settle-boundary-rule.d.ts +23 -0
  26. package/dist/evaluate/settle-boundary-rule.d.ts.map +1 -0
  27. package/dist/evaluate/settle-boundary-rule.js +73 -0
  28. package/dist/evaluate/with-boundary-rule-settlement.d.ts +11 -0
  29. package/dist/evaluate/with-boundary-rule-settlement.d.ts.map +1 -0
  30. package/dist/evaluate/with-boundary-rule-settlement.js +44 -0
  31. package/dist/extract/code-zones.d.ts +95 -3
  32. package/dist/extract/code-zones.d.ts.map +1 -1
  33. package/dist/extract/code-zones.js +313 -4
  34. package/dist/extract/extract-tokens.d.ts +30 -0
  35. package/dist/extract/extract-tokens.d.ts.map +1 -1
  36. package/dist/extract/extract-tokens.js +233 -27
  37. package/dist/extract/i-labeled-source.d.ts +12 -0
  38. package/dist/extract/i-labeled-source.d.ts.map +1 -0
  39. package/dist/extract/i-labeled-source.js +1 -0
  40. package/dist/extract/import-edges.d.ts +1 -0
  41. package/dist/extract/import-edges.d.ts.map +1 -1
  42. package/dist/extract/import-edges.js +20 -7
  43. package/dist/extract/inspect-source.d.ts +32 -3
  44. package/dist/extract/inspect-source.d.ts.map +1 -1
  45. package/dist/extract/inspect-source.js +50 -7
  46. package/dist/extract/parse-imports.d.ts +34 -12
  47. package/dist/extract/parse-imports.d.ts.map +1 -1
  48. package/dist/extract/parse-imports.js +119 -20
  49. package/dist/extract/scan-literals.d.ts +12 -1
  50. package/dist/extract/scan-literals.d.ts.map +1 -1
  51. package/dist/extract/scan-literals.js +15 -1
  52. package/dist/extract/source-liveness-request.d.ts +24 -0
  53. package/dist/extract/source-liveness-request.d.ts.map +1 -0
  54. package/dist/extract/source-liveness-request.js +45 -0
  55. package/dist/generated/check-provenance.d.ts.map +1 -1
  56. package/dist/generated/check-provenance.js +3 -1
  57. package/dist/generated/read-regen-tree.d.ts +16 -0
  58. package/dist/generated/read-regen-tree.d.ts.map +1 -0
  59. package/dist/generated/read-regen-tree.js +61 -0
  60. package/dist/generated/scan-generated.d.ts +8 -0
  61. package/dist/generated/scan-generated.d.ts.map +1 -1
  62. package/dist/generated/scan-generated.js +25 -10
  63. package/dist/index.d.ts +48 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +51 -0
  66. package/dist/model/boundary-intended-empty-line.d.ts +10 -0
  67. package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
  68. package/dist/model/boundary-intended-empty-line.js +11 -0
  69. package/dist/model/boundary-markable-list.d.ts +15 -0
  70. package/dist/model/boundary-markable-list.d.ts.map +1 -0
  71. package/dist/model/boundary-markable-list.js +15 -0
  72. package/dist/model/boundary-rule-input-keys.d.ts +11 -0
  73. package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
  74. package/dist/model/boundary-rule-input-keys.js +29 -0
  75. package/dist/model/boundary-rule-input.d.ts +23 -0
  76. package/dist/model/boundary-rule-input.d.ts.map +1 -0
  77. package/dist/model/boundary-rule-input.js +1 -0
  78. package/dist/model/boundary-rule-key-problems.d.ts +11 -0
  79. package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
  80. package/dist/model/boundary-rule-key-problems.js +36 -0
  81. package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
  82. package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
  83. package/dist/model/boundary-rule-marker-problems.js +94 -0
  84. package/dist/model/boundary-rule-scope.d.ts +88 -0
  85. package/dist/model/boundary-rule-scope.d.ts.map +1 -0
  86. package/dist/model/boundary-rule-scope.js +175 -0
  87. package/dist/model/boundary-rule.d.ts +120 -5
  88. package/dist/model/boundary-rule.d.ts.map +1 -1
  89. package/dist/model/boundary-rule.js +148 -1
  90. package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
  91. package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
  92. package/dist/model/boundary-unit-problem-issue.js +16 -0
  93. package/dist/model/normalize-boundary-rule.d.ts +21 -0
  94. package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
  95. package/dist/model/normalize-boundary-rule.js +62 -0
  96. package/dist/policy/evaluate-policy.d.ts +80 -3
  97. package/dist/policy/evaluate-policy.d.ts.map +1 -1
  98. package/dist/policy/evaluate-policy.js +152 -19
  99. package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
  100. package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
  101. package/dist/policy/i-policy-rule-liveness.js +1 -0
  102. package/dist/policy/run-policy.d.ts +1 -1
  103. package/dist/policy/run-policy.d.ts.map +1 -1
  104. package/dist/policy/run-policy.js +171 -21
  105. package/dist/registry/load-boundary-rules.d.ts +29 -1
  106. package/dist/registry/load-boundary-rules.d.ts.map +1 -1
  107. package/dist/registry/load-boundary-rules.js +38 -8
  108. package/dist/scan/glob.d.ts +90 -0
  109. package/dist/scan/glob.d.ts.map +1 -1
  110. package/dist/scan/glob.js +205 -0
  111. package/dist/scan/i-glob-unit-measure.d.ts +22 -0
  112. package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
  113. package/dist/scan/i-glob-unit-measure.js +1 -0
  114. package/dist/scan/import-pattern.d.ts +109 -0
  115. package/dist/scan/import-pattern.d.ts.map +1 -0
  116. package/dist/scan/import-pattern.js +191 -0
  117. package/dist/scan/node-builtin-package-names.d.ts +13 -0
  118. package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
  119. package/dist/scan/node-builtin-package-names.js +25 -0
  120. package/dist/scan/scan-imports.d.ts +36 -3
  121. package/dist/scan/scan-imports.d.ts.map +1 -1
  122. package/dist/scan/scan-imports.js +114 -45
  123. package/dist/util/blank-run-hazard-finding.d.ts +34 -0
  124. package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
  125. package/dist/util/blank-run-hazard-finding.js +1 -0
  126. package/dist/util/blank-run-hazard.d.ts +29 -0
  127. package/dist/util/blank-run-hazard.d.ts.map +1 -0
  128. package/dist/util/blank-run-hazard.js +450 -0
  129. package/dist/util/dead-glob-units.d.ts +46 -0
  130. package/dist/util/dead-glob-units.d.ts.map +1 -0
  131. package/dist/util/dead-glob-units.js +116 -0
  132. package/dist/util/glob-list-liveness-input.d.ts +31 -0
  133. package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
  134. package/dist/util/glob-list-liveness-input.js +65 -0
  135. package/dist/util/i-dead-glob-unit.d.ts +24 -0
  136. package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
  137. package/dist/util/i-dead-glob-unit.js +1 -0
  138. package/dist/util/i-glob-list-units.d.ts +25 -0
  139. package/dist/util/i-glob-list-units.d.ts.map +1 -0
  140. package/dist/util/i-glob-list-units.js +1 -0
  141. package/dist/util/i-glob-liveness-list.d.ts +23 -0
  142. package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
  143. package/dist/util/i-glob-liveness-list.js +1 -0
  144. package/dist/util/i-glob-liveness-request.d.ts +11 -0
  145. package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
  146. package/dist/util/i-glob-liveness-request.js +1 -0
  147. package/dist/util/i-glob-negation.d.ts +13 -0
  148. package/dist/util/i-glob-negation.d.ts.map +1 -0
  149. package/dist/util/i-glob-negation.js +1 -0
  150. package/dist/util/matched-files.d.ts +22 -0
  151. package/dist/util/matched-files.d.ts.map +1 -0
  152. package/dist/util/matched-files.js +1 -0
  153. package/dist/util/negation-cause.d.ts +15 -0
  154. package/dist/util/negation-cause.d.ts.map +1 -0
  155. package/dist/util/negation-cause.js +21 -0
  156. package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
  157. package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
  158. package/dist/util/plane-scan-exclude-dirs.js +22 -0
  159. package/dist/util/read-glob-list-liveness.d.ts +20 -0
  160. package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
  161. package/dist/util/read-glob-list-liveness.js +20 -0
  162. package/dist/util/read-scope-coverage.d.ts +90 -0
  163. package/dist/util/read-scope-coverage.d.ts.map +1 -0
  164. package/dist/util/read-scope-coverage.js +173 -0
  165. package/dist/util/read-scope.d.ts +14 -0
  166. package/dist/util/read-scope.d.ts.map +1 -0
  167. package/dist/util/read-scope.js +1 -0
  168. package/dist/util/read-selected-files.d.ts +16 -0
  169. package/dist/util/read-selected-files.d.ts.map +1 -0
  170. package/dist/util/read-selected-files.js +25 -0
  171. package/dist/util/settle-glob-lists.d.ts +12 -0
  172. package/dist/util/settle-glob-lists.d.ts.map +1 -0
  173. package/dist/util/settle-glob-lists.js +13 -0
  174. package/dist/util/unread-file-reason.d.ts +26 -0
  175. package/dist/util/unread-file-reason.d.ts.map +1 -0
  176. package/dist/util/unread-file-reason.js +26 -0
  177. package/dist/util/unread-file.d.ts +10 -0
  178. package/dist/util/unread-file.d.ts.map +1 -0
  179. package/dist/util/unread-file.js +1 -0
  180. package/dist/util/walk-files.d.ts +74 -10
  181. package/dist/util/walk-files.d.ts.map +1 -1
  182. package/dist/util/walk-files.js +137 -31
  183. package/dist/wiring/evaluate-wiring.d.ts +79 -5
  184. package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
  185. package/dist/wiring/evaluate-wiring.js +267 -12
  186. package/dist/wiring/explain-wiring.d.ts +49 -5
  187. package/dist/wiring/explain-wiring.d.ts.map +1 -1
  188. package/dist/wiring/explain-wiring.js +118 -10
  189. package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
  190. package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
  191. package/dist/wiring/i-idiom-role-coverage.js +1 -0
  192. package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
  193. package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
  194. package/dist/wiring/i-registration-query-verdict.js +1 -0
  195. package/dist/wiring/i-registration-roles.d.ts +71 -0
  196. package/dist/wiring/i-registration-roles.d.ts.map +1 -0
  197. package/dist/wiring/i-registration-roles.js +1 -0
  198. package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
  199. package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
  200. package/dist/wiring/measure-idiom-role-coverage.js +20 -0
  201. package/dist/wiring/measure-registration-roles.d.ts +25 -0
  202. package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
  203. package/dist/wiring/measure-registration-roles.js +124 -0
  204. package/dist/wiring/plan-wiring-fix.js +2 -2
  205. package/dist/wiring/registration-graph.d.ts +12 -0
  206. package/dist/wiring/registration-graph.d.ts.map +1 -1
  207. package/dist/wiring/registration-graph.js +16 -5
  208. package/dist/wiring/registration-query-verdict.d.ts +23 -0
  209. package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
  210. package/dist/wiring/registration-query-verdict.js +118 -0
  211. package/dist/wiring/registry-query.d.ts +8 -0
  212. package/dist/wiring/registry-query.d.ts.map +1 -1
  213. package/dist/wiring/registry-query.js +12 -5
  214. package/dist/wiring/scan-wiring-files.d.ts +4 -2
  215. package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
  216. package/dist/wiring/scan-wiring-files.js +52 -15
  217. package/dist/wiring/sink-imports.d.ts +11 -0
  218. package/dist/wiring/sink-imports.d.ts.map +1 -1
  219. package/dist/wiring/sink-imports.js +11 -2
  220. package/dist/wiring/trace-literal.d.ts +6 -0
  221. package/dist/wiring/trace-literal.d.ts.map +1 -1
  222. package/dist/wiring/trace-literal.js +5 -2
  223. package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
  224. package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
  225. package/dist/wiring/wiring-labeled-sources.js +20 -0
  226. package/package.json +2 -2
@@ -1,4 +1,9 @@
1
- import { type IPolicyRule, type PolicySurface } from '@shrkcrft/core';
1
+ import { type IPolicyRule, type IUnitLiveness, type IUnitStateLists, type IVerdictCoverage, type PolicySurface } from '@shrkcrft/core';
2
+ import type { IPolicyRuleLiveness } from './i-policy-rule-liveness.js';
3
+ import type { IReadScope } from '../util/read-scope.js';
4
+ import type { IUnreadFile } from '../util/unread-file.js';
5
+ import type { IDeadGlobUnit } from '../util/i-dead-glob-unit.js';
6
+ import type { IGlobNegation } from '../util/i-glob-negation.js';
2
7
  export declare const POLICY_LINT_SCHEMA: "sharkcraft.policy-lint/v1";
3
8
  /**
4
9
  * A chunk of content to scan for one rule. For whole files `baseLine` is 1; for
@@ -52,6 +57,49 @@ export interface IPolicyRuleResult {
52
57
  /** Content units actually scanned — the stale-glob signal. */
53
58
  readonly unitsScanned: number;
54
59
  readonly error?: string;
60
+ /**
61
+ * What the rule examined against what it was asked to (`IVerdictCoverage`,
62
+ * unit `content units`) — the ONE per-rule record `policy-lint`, `gates
63
+ * check`, `quality`, `shrk gate` and `finish` all settle on. A rule that
64
+ * scanned nothing, or is misconfigured, examined nothing (`expected: 0` — a
65
+ * shortfall). The engine fills it in; it never decides a verdict from it.
66
+ */
67
+ readonly coverage: IVerdictCoverage;
68
+ /**
69
+ * Glob-matched files in the rule's scope that the one reader did not read
70
+ * (over the read cap, or unreadable). Set only when non-empty. `coverage`
71
+ * names them too: such a rule is never a pass.
72
+ */
73
+ readonly unread?: readonly IUnreadFile[];
74
+ /**
75
+ * Globs of the rule's own `files[]` that do nothing — an inclusion glob
76
+ * that selects no file in the walk, or a negation that excludes none — a
77
+ * dead unit inside a rule whose other globs keep it scanning. Set only when
78
+ * non-empty, and only for author-written `files` (a surface's default globs
79
+ * are not the author's selector). Advisory; `gates coverage` reports it.
80
+ */
81
+ readonly deadGlobs?: readonly string[];
82
+ /** The units behind {@link deadGlobs}, same order, each with its reason (`globListUnits`). */
83
+ readonly deadGlobUnits?: readonly IDeadGlobUnit[];
84
+ /** Live negations of the rule's own `files[]`, each with the files it excludes. Set only when non-empty. */
85
+ readonly negations?: readonly IGlobNegation[];
86
+ /**
87
+ * Set only when the rule scanned nothing BECAUSE of its own `files[]`
88
+ * negations: its inclusion globs matched files and these negations excluded
89
+ * every one (`globListUnits`' `allExcluded`). That is not a stale selector,
90
+ * so its skip reason and coverage reason say what happened instead.
91
+ */
92
+ readonly excludedByNegations?: readonly IGlobNegation[];
93
+ /**
94
+ * The acceptance of the rule's `expectEmpty` units — settle record B of its
95
+ * `files` (round 13). Folded into the envelope's ONE settle beside
96
+ * {@link coverage}; when the rule is intended-empty it IS {@link coverage}.
97
+ */
98
+ readonly unitAcceptance?: IVerdictCoverage;
99
+ /** The rule's dead / intended-empty / went-live `files` units as printed lines (`unitStateLists`). */
100
+ readonly units?: IUnitStateLists;
101
+ /** The rule's non-live `files` units (dead, intended-empty, went-live, unproven), for `--fail-on-dead-units` (`selectorUnitFails`). */
102
+ readonly unitLiveness?: readonly IUnitLiveness[];
55
103
  }
56
104
  /** A rule that scanned nothing, reported loudly instead of as a green pass. */
57
105
  export interface IPolicySkip {
@@ -74,9 +122,21 @@ export interface IPolicyReport {
74
122
  * Count of rules that actually scanned ≥1 unit. A rule whose globs matched 0
75
123
  * files (e.g. a `style` rule in a project with no stylesheets) is NOT
76
124
  * evaluated — a loud skip rather than a green pass. Misconfigured rules count
77
- * as evaluated so their error is not swallowed.
125
+ * as evaluated so their error is not swallowed, and so does a rule whose
126
+ * globs matched a file the reader could not read (it ran, and is PARTIAL:
127
+ * its coverage names the unread file). A rule ACCEPTED as intended-empty
128
+ * (round 13 — every inclusion glob of its `files` marked `expectEmpty`, 0
129
+ * files matched) is counted too, so the verdict path's `evaluated === 0`
130
+ * NOT-VERIFIED guard never reads an accepted plan as "nothing ran"; the
131
+ * printed "N evaluated" excludes it: N = `evaluated − acceptedEmpty`.
78
132
  */
79
133
  readonly evaluated: number;
134
+ /**
135
+ * Rules accepted as intended-empty (round 13, K6): they examined 0 files, so
136
+ * a renderer prints them apart — `N evaluated, M accepted as
137
+ * intended-empty` — never inside the evaluated count.
138
+ */
139
+ readonly acceptedEmpty: number;
80
140
  readonly verdict: 'pass' | 'errors' | 'warnings';
81
141
  }
82
142
  /** Resolves the content units to scan for a given rule (injected for purity/testability). */
@@ -89,6 +149,23 @@ export type PolicyUnitResolver = (rule: IPolicyRule) => readonly IPolicyUnit[];
89
149
  * them, so `explain` can show what was matched AND what was let through. A
90
150
  * misconfigured rule (uncompilable regex) degrades to a diagnostic — never
91
151
  * throws, so one bad rule cannot crash the check.
152
+ *
153
+ * `scopeOf` reports each rule's read scope: the glob-matched files the one
154
+ * reader read and the ones it did not. A rule with an unread file in scope
155
+ * examined less than its globs matched, so its coverage is the file record
156
+ * (`readScopeCoverage`) and it is never a pass. When the unread file is why it
157
+ * scanned no unit at all, it is PARTIAL, not a `failOnEmpty` failure: it
158
+ * matched something it could not read, which is not "matched nothing".
159
+ */
160
+ export declare function evaluatePolicy(rules: readonly IPolicyRule[], resolve: PolicyUnitResolver, scopeOf?: (rule: IPolicyRule) => IReadScope,
161
+ /**
162
+ * The rule's own `files` judged per unit and settled with its `expectEmpty`
163
+ * markers (round 13) — `runPolicyLint` supplies it from the walk it already
164
+ * did. A rule that scanned nothing is decided from it by THE rule-emptiness
165
+ * settle (`settleRuleEmptiness`): every inclusion glob intended-empty and no
166
+ * file matched → accepted (`passed`, the acceptance as its coverage), never a
167
+ * failOnEmpty failure. Absent (hand-driven, or no own `files`): no unit is
168
+ * marked, so an empty rule is a loud skip exactly as before.
92
169
  */
93
- export declare function evaluatePolicy(rules: readonly IPolicyRule[], resolve: PolicyUnitResolver): IPolicyReport;
170
+ livenessOf?: (rule: IPolicyRule) => IPolicyRuleLiveness | undefined): IPolicyReport;
94
171
  //# 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":"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"}
1
+ {"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,8 @@
1
- import { failsWhenEmpty } from '@shrkcrft/core';
2
- import { lexCodeZones, zoneAt } from "../extract/code-zones.js";
1
+ import { failsWhenEmpty, normalizeRuleList, RuleEmptiness, settleRuleEmptiness, } from '@shrkcrft/core';
2
+ import { lexCodeZones, zoneKeepsAt } from "../extract/code-zones.js";
3
+ import { allExcludedCause } from "../util/negation-cause.js";
4
+ import { settleGlobLists } from "../util/settle-glob-lists.js";
5
+ import { readScopeCoverage, readScopeHasUnread } from "../util/read-scope-coverage.js";
3
6
  import { safeCompile } from "../util/safe-regex.js";
4
7
  export const POLICY_LINT_SCHEMA = 'sharkcraft.policy-lint/v1';
5
8
  function lineWithin(content, index) {
@@ -24,19 +27,34 @@ function markerNear(lines, line, marker) {
24
27
  return above !== undefined && above.includes(marker);
25
28
  }
26
29
  /**
27
- * Whether the zone rule keeps a match at `index`. `all` keeps everything; the
28
- * narrower zones require the match to START in the named zone. Not applied to
29
- * inline-template units (their content is already a string body).
30
+ * Whether the zone rule keeps a match at `index`.
31
+ *
32
+ * Delegates the zone semantics to {@link zoneKeepsAt} the one authority the
33
+ * extraction DSL also reads — and adds only the policy-specific exemption:
34
+ * inline-template units are NOT zoned, because their content is already a
35
+ * string body lifted out of a source file.
30
36
  */
31
37
  function zoneKeeps(zone, unit, zones, index) {
32
38
  if (zone === 'all' || unit.inlineTemplate || zones === undefined)
33
39
  return true;
34
- const kind = zoneAt(zones, index);
35
- if (zone === 'code')
36
- return kind === 'code';
37
- if (zone === 'strings')
38
- return kind === 'string';
39
- return kind === 'comment';
40
+ return zoneKeepsAt(zone, zones, index);
41
+ }
42
+ /** Why a rule that scanned no content unit examined nothing. */
43
+ const NO_UNITS_REASON = '0 content units matched the rule globs';
44
+ /**
45
+ * What one policy rule examined: every content unit its globs matched (whole
46
+ * files, or inline-template bodies). A rule that scanned nothing, or is
47
+ * misconfigured, examined nothing — `expected: 0`, which core's
48
+ * `coverageShortfall` reports as a gap. The one derivation; every surface that
49
+ * settles a policy verdict reads the record this puts on the rule result.
50
+ */
51
+ function policyCoverage(outcome) {
52
+ const unit = 'content units';
53
+ if (outcome.kind === 'error')
54
+ return { unit, expected: 0, examined: 0, reason: 'the rule is misconfigured' };
55
+ if (outcome.kind === 'skipped')
56
+ return { unit, expected: 0, examined: 0, reason: outcome.reason };
57
+ return { unit, expected: outcome.units, examined: outcome.units };
40
58
  }
41
59
  /**
42
60
  * Pure policy evaluation. Each rule's regex is run over the units the resolver
@@ -46,18 +64,66 @@ function zoneKeeps(zone, unit, zones, index) {
46
64
  * them, so `explain` can show what was matched AND what was let through. A
47
65
  * misconfigured rule (uncompilable regex) degrades to a diagnostic — never
48
66
  * throws, so one bad rule cannot crash the check.
67
+ *
68
+ * `scopeOf` reports each rule's read scope: the glob-matched files the one
69
+ * reader read and the ones it did not. A rule with an unread file in scope
70
+ * examined less than its globs matched, so its coverage is the file record
71
+ * (`readScopeCoverage`) and it is never a pass. When the unread file is why it
72
+ * scanned no unit at all, it is PARTIAL, not a `failOnEmpty` failure: it
73
+ * matched something it could not read, which is not "matched nothing".
74
+ */
75
+ export function evaluatePolicy(rules, resolve, scopeOf,
76
+ /**
77
+ * The rule's own `files` judged per unit and settled with its `expectEmpty`
78
+ * markers (round 13) — `runPolicyLint` supplies it from the walk it already
79
+ * did. A rule that scanned nothing is decided from it by THE rule-emptiness
80
+ * settle (`settleRuleEmptiness`): every inclusion glob intended-empty and no
81
+ * file matched → accepted (`passed`, the acceptance as its coverage), never a
82
+ * failOnEmpty failure. Absent (hand-driven, or no own `files`): no unit is
83
+ * marked, so an empty rule is a loud skip exactly as before.
49
84
  */
50
- export function evaluatePolicy(rules, resolve) {
85
+ livenessOf) {
51
86
  const ruleResults = [];
52
87
  const findings = [];
53
88
  const suppressed = [];
54
89
  const diagnostics = [];
55
90
  const skipped = [];
56
91
  let evaluated = 0;
92
+ let acceptedEmpty = 0;
57
93
  let misconfigError = false;
58
94
  let misconfigWarn = false;
59
- for (const rule of rules) {
60
- const severity = rule.severity ?? 'error';
95
+ for (const authored of rules) {
96
+ const severity = authored.severity ?? 'error';
97
+ // The engine entry normalises idempotently (round 13): a loaded rule comes
98
+ // back as the SAME object; a hand-built `{ pattern, expectEmpty }` entry is
99
+ // a unit plus a marker; a malformed entry is a misconfigured rule — never
100
+ // an object handed to a glob reader, never a crash.
101
+ const normalized = normalizeRuleList(authored, 'files');
102
+ if (!normalized.ok) {
103
+ const msg = `rule "${authored.id}": ${normalized.error.message}`;
104
+ diagnostics.push(msg);
105
+ if (severity === 'error')
106
+ misconfigError = true;
107
+ else
108
+ misconfigWarn = true;
109
+ ruleResults.push({
110
+ ruleId: authored.id,
111
+ surface: authored.surface,
112
+ severity,
113
+ status: 'error',
114
+ findingCount: 0,
115
+ suppressedCount: 0,
116
+ unitsScanned: 0,
117
+ error: msg,
118
+ coverage: policyCoverage({ kind: 'error' }),
119
+ });
120
+ evaluated += 1;
121
+ continue;
122
+ }
123
+ const rule = normalized.value;
124
+ const lv = livenessOf?.(rule);
125
+ const acceptance = lv?.liveness.acceptance;
126
+ const acceptanceField = acceptance !== undefined ? { unitAcceptance: acceptance } : {};
61
127
  const { re, error } = safeCompile(rule.pattern, rule.flags);
62
128
  if (error || !re) {
63
129
  const msg = `rule "${rule.id}": ${error}`;
@@ -75,6 +141,7 @@ export function evaluatePolicy(rules, resolve) {
75
141
  suppressedCount: 0,
76
142
  unitsScanned: 0,
77
143
  error: msg,
144
+ coverage: policyCoverage({ kind: 'error' }),
78
145
  });
79
146
  // A misconfigured rule attempted to run — count it as evaluated so its
80
147
  // error isn't swallowed by the gate's `evaluated === 0` skip path.
@@ -148,18 +215,77 @@ export function evaluatePolicy(rules, resolve) {
148
215
  suppressedCount,
149
216
  unitsScanned: units.length,
150
217
  error: msg,
218
+ coverage: policyCoverage({ kind: 'error' }),
151
219
  });
152
220
  evaluated += 1;
153
221
  continue;
154
222
  }
155
- if (units.length === 0) {
156
- const failed = failsWhenEmpty(rule);
157
- skipped.push({
223
+ const scope = scopeOf?.(rule);
224
+ const unreadField = scope !== undefined && scope.unread.length > 0 ? { unread: scope.unread } : {};
225
+ if (units.length === 0 && readScopeHasUnread(scope)) {
226
+ // Its globs matched files, but the reader could not read them. That is
227
+ // not a stale glob: it ran over everything readable (nothing) and is
228
+ // PARTIAL, naming the unread files. Never failOnEmpty's 1, never a pass.
229
+ evaluated += 1;
230
+ ruleResults.push({
158
231
  ruleId: rule.id,
159
- reason: '0 content units matched the rule globs',
160
- failed,
232
+ surface: rule.surface,
161
233
  severity,
234
+ status: 'passed',
235
+ findingCount: 0,
236
+ suppressedCount: 0,
237
+ unitsScanned: 0,
238
+ ...unreadField,
239
+ ...acceptanceField,
240
+ coverage: readScopeCoverage(policyCoverage({ kind: 'skipped', reason: NO_UNITS_REASON }), scope),
162
241
  });
242
+ continue;
243
+ }
244
+ if (units.length === 0) {
245
+ // THE rule-emptiness settle (round 13, `settleRuleEmptiness`): every
246
+ // inclusion glob of the rule's own `files` marked `expectEmpty` and no
247
+ // file matched → the empty result is the INTENDED one, accepted and
248
+ // printed; anything else — a dead or unmarked glob, files that yielded no
249
+ // content unit — is the loud skip it always was (failOnEmpty's 1, else 2).
250
+ const emptiedBy = lv?.units.allExcluded === true ? lv.units.negations : undefined;
251
+ const emptiness = settleRuleEmptiness({
252
+ subject: rule.id,
253
+ unitLabel: 'content units',
254
+ filesMatched: scope?.read ?? 0,
255
+ unitsMatched: 0,
256
+ unread: false,
257
+ emptiedByNegations: emptiedBy !== undefined,
258
+ ...(emptiedBy !== undefined ? { emptiedReason: `0 content units: ${allExcludedCause(emptiedBy)}` } : {}),
259
+ liveness: lv?.liveness ?? settleGlobLists({ subject: rule.id, lists: [], marks: [] }),
260
+ primaryLists: ['files'],
261
+ failOnEmpty: failsWhenEmpty(rule),
262
+ noFilesReason: NO_UNITS_REASON,
263
+ noUnitsReason: NO_UNITS_REASON,
264
+ });
265
+ if (emptiness.state === RuleEmptiness.IntendedEmpty && emptiness.coverage !== undefined) {
266
+ // Counted in `evaluated` (the verdict path's "nothing ran" guard must
267
+ // never read an accepted plan as a skip) AND in `acceptedEmpty`, which
268
+ // a renderer subtracts: `N evaluated, M accepted as intended-empty`.
269
+ evaluated += 1;
270
+ acceptedEmpty += 1;
271
+ ruleResults.push({
272
+ ruleId: rule.id,
273
+ surface: rule.surface,
274
+ severity,
275
+ status: 'passed',
276
+ findingCount: 0,
277
+ suppressedCount: 0,
278
+ unitsScanned: 0,
279
+ ...unreadField,
280
+ // The acceptance IS the rule's coverage (the same record, folded once).
281
+ coverage: emptiness.coverage,
282
+ unitAcceptance: emptiness.coverage,
283
+ });
284
+ continue;
285
+ }
286
+ const failed = emptiness.fails;
287
+ const reason = emptiness.skipReason ?? NO_UNITS_REASON;
288
+ skipped.push({ ruleId: rule.id, reason, failed, severity });
163
289
  if (failed) {
164
290
  if (severity === 'error')
165
291
  misconfigError = true;
@@ -174,6 +300,9 @@ export function evaluatePolicy(rules, resolve) {
174
300
  findingCount: 0,
175
301
  suppressedCount: 0,
176
302
  unitsScanned: 0,
303
+ ...unreadField,
304
+ ...acceptanceField,
305
+ coverage: readScopeCoverage(policyCoverage({ kind: 'skipped', reason: emptiedBy !== undefined ? allExcludedCause(emptiedBy) : reason }), scope),
177
306
  });
178
307
  continue;
179
308
  }
@@ -186,6 +315,9 @@ export function evaluatePolicy(rules, resolve) {
186
315
  findingCount: count,
187
316
  suppressedCount,
188
317
  unitsScanned: units.length,
318
+ ...unreadField,
319
+ ...acceptanceField,
320
+ coverage: readScopeCoverage(policyCoverage({ kind: 'ran', units: units.length }), scope),
189
321
  });
190
322
  }
191
323
  const hasError = misconfigError || findings.some((f) => f.severity === 'error');
@@ -198,6 +330,7 @@ export function evaluatePolicy(rules, resolve) {
198
330
  suppressed,
199
331
  skipped,
200
332
  evaluated,
333
+ acceptedEmpty,
201
334
  verdict: hasError ? 'errors' : hasWarn ? 'warnings' : 'pass',
202
335
  };
203
336
  }
@@ -0,0 +1,15 @@
1
+ import type { ISettledUnitLiveness } from '@shrkcrft/core';
2
+ import type { IGlobListUnits } from '../util/i-glob-list-units.js';
3
+ /**
4
+ * One policy rule's own `files` judged per unit and settled with its
5
+ * `expectEmpty` markers (round 13) — what `evaluatePolicy` decides a rule that
6
+ * scanned nothing from (`settleRuleEmptiness`), and what `runPolicyLint`
7
+ * reports as its dead / intended-empty / went-live units.
8
+ */
9
+ export interface IPolicyRuleLiveness {
10
+ /** The list's units, by the one dead-unit decision (`globListUnits`). */
11
+ readonly units: IGlobListUnits;
12
+ /** The settle (`settleUnitLiveness` over `globListLivenessInput`). */
13
+ readonly liveness: ISettledUnitLiveness;
14
+ }
15
+ //# sourceMappingURL=i-policy-rule-liveness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i-policy-rule-liveness.d.ts","sourceRoot":"","sources":["../../src/policy/i-policy-rule-liveness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;CACzC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
1
+ import { type IPolicyRule, type PolicySurface } from '@shrkcrft/core';
2
2
  import { type IPolicyReport } from './evaluate-policy.js';
3
3
  export interface IRunPolicyOptions {
4
4
  /** Restrict to rules on these surfaces. */
@@ -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,CAkEf"}
1
+ {"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AASxB,OAAO,EAAkB,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAa5F,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;AA2BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAgMf"}
@@ -1,6 +1,11 @@
1
1
  import * as nodePath from 'node:path';
2
- import { matchesAny } from "../scan/glob.js";
2
+ import { normalizeRuleList, settleUnitLiveness, unitStateLists, UnitLivenessState, } from '@shrkcrft/core';
3
+ import { globListSelects } from "../scan/glob.js";
4
+ import { readScopeHasUnread, readScopeOf, unreadEntryWhollyMatches } from "../util/read-scope-coverage.js";
3
5
  import { readMatchingFiles } from "../util/walk-files.js";
6
+ import { globListUnits } from "../util/dead-glob-units.js";
7
+ import { globListLivenessInput } from "../util/glob-list-liveness-input.js";
8
+ import { allExcludedCause } from "../util/negation-cause.js";
4
9
  import { extractInlineTemplates } from "./extract-templates.js";
5
10
  import { evaluatePolicy } from "./evaluate-policy.js";
6
11
  /** Per-surface default globs when a rule omits `files`. */
@@ -14,6 +19,25 @@ const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs'
14
19
  function globsFor(rule) {
15
20
  return rule.files && rule.files.length > 0 ? rule.files : SURFACE_DEFAULT_GLOBS[rule.surface];
16
21
  }
22
+ // Why a rule scanned nothing when its OWN negations emptied its `files[]` is
23
+ // THE shared cause (`allExcludedCause`, util/negation-cause.ts — the wiring
24
+ // engine words its source side from the same module). The coverage reason
25
+ // carries it bare (the shortfall line already says "0 content units"); the
26
+ // skip reason leads with the count. `evaluatePolicy` words both.
27
+ /** The report of a run with no rule in scope. */
28
+ function emptyReport() {
29
+ return {
30
+ schema: 'sharkcraft.policy-lint/v1',
31
+ rules: [],
32
+ findings: [],
33
+ diagnostics: [],
34
+ suppressed: [],
35
+ skipped: [],
36
+ evaluated: 0,
37
+ acceptedEmpty: 0,
38
+ verdict: 'pass',
39
+ };
40
+ }
17
41
  /**
18
42
  * Filesystem-backed policy-lint. Walks the project once; on the `template`
19
43
  * surface, source files contribute their inline `template:` bodies (with real
@@ -21,7 +45,19 @@ function globsFor(rule) {
21
45
  * output; the only IO is the read-only walk + reads.
22
46
  */
23
47
  export function runPolicyLint(projectRoot, rules, options = {}) {
24
- let selected = rules;
48
+ // The engine entry normalises idempotently (round 13): a loaded rule comes
49
+ // back as the SAME object (so every per-rule memo below hits), a hand-built
50
+ // `{ pattern, expectEmpty }` entry becomes a unit plus a marker. A MALFORMED
51
+ // entry is left for `evaluatePolicy` to report as a misconfigured rule, and
52
+ // no glob reader below is ever handed it.
53
+ const malformed = new Set();
54
+ let selected = rules.map((r) => {
55
+ const n = normalizeRuleList(r, 'files');
56
+ if (n.ok)
57
+ return n.value;
58
+ malformed.add(r);
59
+ return r;
60
+ });
25
61
  if (options.surfaces && options.surfaces.length > 0) {
26
62
  const s = new Set(options.surfaces);
27
63
  selected = selected.filter((r) => s.has(r.surface));
@@ -32,29 +68,142 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
32
68
  }
33
69
  if (options.changedOnly) {
34
70
  const changed = options.changedFiles ?? [];
35
- selected = selected.filter((r) => changed.some((c) => matchesAny(c, globsFor(r))));
36
- }
37
- if (selected.length === 0) {
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
- };
71
+ // A changed file the rule's own `files` EXCLUDE (a `!` entry) cannot put a
72
+ // finding in its scope, so it does not select the rule. A malformed rule
73
+ // has no footprint to prove it out of scope, so it stays in (and errors).
74
+ selected = selected.filter((r) => malformed.has(r) || changed.some((c) => globListSelects(c, globsFor(r))));
48
75
  }
76
+ if (selected.length === 0)
77
+ return emptyReport();
49
78
  // Under --changed-only, restrict the SCANNED files to the changed set too (not
50
79
  // just rule selection). Per-file regex findings have no cross-file dependency,
51
80
  // so a pre-existing violation in a file the diff never touched is out of scope
52
81
  // — this mirrors how `check boundaries`/wiring restrict to the changeset and
53
82
  // stops the gate failing RED on untouched legacy debt.
54
83
  const changedSet = options.changedOnly ? new Set(options.changedFiles ?? []) : undefined;
55
- const allGlobs = [...new Set(selected.flatMap((r) => [...globsFor(r)]))];
56
- const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
57
- return evaluatePolicy(selected, (rule) => {
84
+ const excludeDirs = new Set(options.excludeDirs ?? []);
85
+ // One POSITIVE walk over the union; each rule selects its own units from it
86
+ // (`globListSelects`), so rule A's `!x` never removes x from rule B.
87
+ const allGlobs = [...new Set(selected.filter((r) => !malformed.has(r)).flatMap((r) => [...globsFor(r)]))];
88
+ const matched = readMatchingFiles(projectRoot, allGlobs, excludeDirs);
89
+ const cache = matched.files;
90
+ // The walk holds every file any selected rule's inclusion globs matched —
91
+ // the positive set — so a glob absent from it matched nothing.
92
+ const walked = [...cache.keys()];
93
+ const unitsByRule = new Map();
94
+ const unitsFor = (rule) => {
95
+ const memo = unitsByRule.get(rule);
96
+ if (memo !== undefined)
97
+ return memo;
98
+ const units = resolveUnits(rule);
99
+ unitsByRule.set(rule, units);
100
+ return units;
101
+ };
102
+ // What each rule's walk covered: the matched files in its scope (the
103
+ // changeset, under --changed-only) that were read, and the ones the reader
104
+ // reported UNREAD (over the read cap, or unreadable). The reader's own list
105
+ // is the one answer to "was this matched path left unexamined?". Nothing
106
+ // here re-derives the walk's skip rules or re-stats a path: a path the walk
107
+ // never reaches is neither read nor unread, which is exactly narrowing.
108
+ const scopeByRule = new Map();
109
+ const scopeFor = (rule) => {
110
+ const memo = scopeByRule.get(rule);
111
+ if (memo !== undefined)
112
+ return memo;
113
+ const raw = readScopeOf(matched, globsFor(rule), changedSet);
114
+ // An `exemptFiles` file's hits can only ever be suppressions, so leaving
115
+ // it unread cannot change the verdict: an explicit exemption is narrowing,
116
+ // not a gap (the keystone contract). Without this, a large exempted
117
+ // vendored or generated file made the rule NOT VERIFIED (2) for good, with
118
+ // no valve (`--allow-empty` never accepts a partial scope).
119
+ const exempt = rule.exemptFiles && rule.exemptFiles.length > 0 ? rule.exemptFiles : undefined;
120
+ const scope = exempt === undefined
121
+ ? raw
122
+ : { read: raw.read, unread: raw.unread.filter((u) => !unreadEntryWhollyMatches(u, exempt)) };
123
+ scopeByRule.set(rule, scope);
124
+ return scope;
125
+ };
126
+ // Per-glob units of the author's OWN `files[]`, off the walk this run already
127
+ // did, settled with the rule's `expectEmpty` markers (round 13): core's one
128
+ // authority, `settleUnitLiveness`, over THE gate-plane glob predicate
129
+ // (`globListLivenessInput`) — the same input `gates coverage` builds, so the
130
+ // two cannot disagree about one glob. The one dead-unit decision
131
+ // (`globListUnits`) judges a negation by what it EXCLUDES, and a glob that
132
+ // reaches an UNREAD file (or beneath an unlistable directory) the list keeps
133
+ // in scope is never dead. A surface's default globs are not a selector
134
+ // anyone wrote, so they are not judged.
135
+ const livenessByRule = new Map();
136
+ const livenessFor = (rule) => {
137
+ if (livenessByRule.has(rule))
138
+ return livenessByRule.get(rule);
139
+ const own = rule.files;
140
+ let value;
141
+ if (own !== undefined && own.length > 0 && !malformed.has(rule)) {
142
+ const units = globListUnits(walked, matched.unread, own);
143
+ const liveness = settleUnitLiveness(globListLivenessInput({
144
+ subject: rule.id,
145
+ lists: [{ list: 'files', globs: own, units }],
146
+ marks: rule.expectEmptyUnits ?? [],
147
+ }));
148
+ value = { units, liveness };
149
+ }
150
+ livenessByRule.set(rule, value);
151
+ return value;
152
+ };
153
+ // --changed-only SELECTS a rule because a changed PATH matches its globs,
154
+ // but the change may still put nothing in the rule's scope: the path was
155
+ // deleted, or it was read and holds nothing on the rule's surface (a `.ts`
156
+ // with no inline `template:` under a template rule), or it lies where no
157
+ // policy walk goes. That is narrowing — the change cannot have introduced a
158
+ // finding there — so the rule is narrowed out exactly like an unselected
159
+ // one, never reported as a rule that "matched nothing": `failOnEmpty` turns
160
+ // that into a FAILURE, so a pure deletion failed `policy-lint --changed-only`
161
+ // and, once `finish` / `shrk gate` stopped masking a selected rule that
162
+ // examined nothing, would have failed them too. A matched path left UNREAD
163
+ // keeps the rule in scope: PARTIAL, naming the file, never narrowed away.
164
+ const inScope = changedSet
165
+ ? selected.filter((rule) => malformed.has(rule) || unitsFor(rule).length > 0 || readScopeHasUnread(scopeFor(rule)))
166
+ : selected;
167
+ if (inScope.length === 0)
168
+ return emptyReport();
169
+ const report = evaluatePolicy(inScope, unitsFor, scopeFor, livenessFor);
170
+ // Each rule's units, reported: `evaluatePolicy` emits one result per rule, in
171
+ // order, so the index is the rule. Dead units are the UNMARKED dead ones
172
+ // only — a marked glob is intended-empty (accepted, on `unitAcceptance`) or
173
+ // went-live — and every non-live unit rides along for `--fail-on-dead-units`.
174
+ const skippedIds = new Set(report.skipped.map((s) => s.ruleId));
175
+ const ruleResults = report.rules.map((r, i) => {
176
+ const rule = inScope[i];
177
+ const lv = rule !== undefined ? livenessFor(rule) : undefined;
178
+ if (lv === undefined)
179
+ return r;
180
+ const settled = lv.liveness;
181
+ const deadSet = new Set(settled.dead.map((u) => u.unit));
182
+ const nonLive = settled.units.filter((u) => u.state !== UnitLivenessState.Live);
183
+ const listed = settled.dead.length + settled.intendedEmpty.length + settled.wentLive.length > 0;
184
+ // A rule its OWN negations emptied is not a stale selector: every file its
185
+ // inclusion globs select was excluded as written.
186
+ const emptied = lv.units.allExcluded && skippedIds.has(r.ruleId);
187
+ return {
188
+ ...r,
189
+ ...(deadSet.size > 0
190
+ ? {
191
+ deadGlobs: lv.units.dead.filter((d) => deadSet.has(d.glob)).map((d) => d.glob),
192
+ deadGlobUnits: lv.units.dead.filter((d) => deadSet.has(d.glob)),
193
+ }
194
+ : {}),
195
+ ...(lv.units.negations.length > 0 ? { negations: lv.units.negations } : {}),
196
+ ...(listed ? { units: unitStateLists(settled) } : {}),
197
+ ...(nonLive.length > 0 ? { unitLiveness: nonLive } : {}),
198
+ ...(emptied
199
+ ? { excludedByNegations: lv.units.negations, coverage: { ...r.coverage, reason: allExcludedCause(lv.units.negations) } }
200
+ : {}),
201
+ };
202
+ });
203
+ return { ...report, rules: ruleResults };
204
+ function resolveUnits(rule) {
205
+ if (malformed.has(rule))
206
+ return [];
58
207
  const globs = globsFor(rule);
59
208
  // `exemptFiles` never removes the file from the scan — it MARKS it, so the
60
209
  // hits it would have produced are reported as suppressed rather than
@@ -65,9 +214,10 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
65
214
  for (const [path, content] of cache) {
66
215
  if (changedSet && !changedSet.has(path))
67
216
  continue;
68
- if (!matchesAny(path, globs))
217
+ // `files` `!` EXCLUDES (the file is out of scope); `exemptFiles` MARKS.
218
+ if (!globListSelects(path, globs))
69
219
  continue;
70
- const exemptFile = exempt !== undefined && matchesAny(path, exempt);
220
+ const exemptFile = exempt !== undefined && globListSelects(path, exempt);
71
221
  const ext = nodePath.extname(path).toLowerCase();
72
222
  if (rule.surface === 'template' && SOURCE_EXT.has(ext)) {
73
223
  for (const tpl of extractInlineTemplates(content)) {
@@ -86,5 +236,5 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
86
236
  }
87
237
  }
88
238
  return units;
89
- });
239
+ }
90
240
  }