@nielspeter/eess 0.2.2 → 0.5.0

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 (209) hide show
  1. package/README.md +97 -4
  2. package/dist/assertion-advice.d.ts +26 -0
  3. package/dist/assertion-advice.d.ts.map +1 -0
  4. package/dist/assertion-advice.js +29 -0
  5. package/dist/assertion-advice.js.map +1 -0
  6. package/dist/baseline-generator.d.ts +9 -0
  7. package/dist/baseline-generator.d.ts.map +1 -1
  8. package/dist/baseline-generator.js +9 -0
  9. package/dist/baseline-generator.js.map +1 -1
  10. package/dist/baseline.d.ts +69 -4
  11. package/dist/baseline.d.ts.map +1 -1
  12. package/dist/baseline.js +119 -13
  13. package/dist/baseline.js.map +1 -1
  14. package/dist/cache-registry.d.ts +14 -0
  15. package/dist/cache-registry.d.ts.map +1 -0
  16. package/dist/cache-registry.js +37 -0
  17. package/dist/cache-registry.js.map +1 -0
  18. package/dist/cardinality.d.ts +35 -0
  19. package/dist/cardinality.d.ts.map +1 -0
  20. package/dist/cardinality.js +80 -0
  21. package/dist/cardinality.js.map +1 -0
  22. package/dist/check-options.d.ts +14 -0
  23. package/dist/check-options.d.ts.map +1 -1
  24. package/dist/cli-config.d.ts +59 -0
  25. package/dist/cli-config.d.ts.map +1 -0
  26. package/dist/cli-config.js +84 -0
  27. package/dist/cli-config.js.map +1 -0
  28. package/dist/collect-result.d.ts +94 -0
  29. package/dist/collect-result.d.ts.map +1 -0
  30. package/dist/collect-result.js +142 -0
  31. package/dist/collect-result.js.map +1 -0
  32. package/dist/combinators.d.ts +14 -0
  33. package/dist/combinators.d.ts.map +1 -1
  34. package/dist/combinators.js +53 -16
  35. package/dist/combinators.js.map +1 -1
  36. package/dist/comment-suppression.d.ts +60 -0
  37. package/dist/comment-suppression.d.ts.map +1 -0
  38. package/dist/comment-suppression.js +89 -0
  39. package/dist/comment-suppression.js.map +1 -0
  40. package/dist/condition.d.ts +43 -0
  41. package/dist/condition.d.ts.map +1 -1
  42. package/dist/condition.js +48 -1
  43. package/dist/condition.js.map +1 -1
  44. package/dist/correspondence-core.d.ts +35 -0
  45. package/dist/correspondence-core.d.ts.map +1 -0
  46. package/dist/correspondence-core.js +38 -0
  47. package/dist/correspondence-core.js.map +1 -0
  48. package/dist/correspondence.d.ts +57 -5
  49. package/dist/correspondence.d.ts.map +1 -1
  50. package/dist/correspondence.js +74 -5
  51. package/dist/correspondence.js.map +1 -1
  52. package/dist/dedupe-config-findings.d.ts +56 -0
  53. package/dist/dedupe-config-findings.d.ts.map +1 -0
  54. package/dist/dedupe-config-findings.js +101 -0
  55. package/dist/dedupe-config-findings.js.map +1 -0
  56. package/dist/define.d.ts +44 -2
  57. package/dist/define.d.ts.map +1 -1
  58. package/dist/define.js +45 -4
  59. package/dist/define.js.map +1 -1
  60. package/dist/diff-aware.d.ts +8 -1
  61. package/dist/diff-aware.d.ts.map +1 -1
  62. package/dist/diff-aware.js +39 -9
  63. package/dist/diff-aware.js.map +1 -1
  64. package/dist/diff-disclosure.d.ts +72 -0
  65. package/dist/diff-disclosure.d.ts.map +1 -0
  66. package/dist/diff-disclosure.js +110 -0
  67. package/dist/diff-disclosure.js.map +1 -0
  68. package/dist/disk-set.d.ts +27 -0
  69. package/dist/disk-set.d.ts.map +1 -0
  70. package/dist/disk-set.js +12 -0
  71. package/dist/disk-set.js.map +1 -0
  72. package/dist/edge-coverage.d.ts +96 -0
  73. package/dist/edge-coverage.d.ts.map +1 -0
  74. package/dist/edge-coverage.js +108 -0
  75. package/dist/edge-coverage.js.map +1 -0
  76. package/dist/emitter-findings.d.ts +167 -0
  77. package/dist/emitter-findings.d.ts.map +1 -0
  78. package/dist/emitter-findings.js +237 -0
  79. package/dist/emitter-findings.js.map +1 -0
  80. package/dist/errors.d.ts +73 -0
  81. package/dist/errors.d.ts.map +1 -1
  82. package/dist/errors.js +93 -0
  83. package/dist/errors.js.map +1 -1
  84. package/dist/exclusion-comments.d.ts +36 -1
  85. package/dist/exclusion-comments.d.ts.map +1 -1
  86. package/dist/exclusion-comments.js +185 -37
  87. package/dist/exclusion-comments.js.map +1 -1
  88. package/dist/execute-rule.d.ts +11 -1
  89. package/dist/execute-rule.d.ts.map +1 -1
  90. package/dist/execute-rule.js +220 -11
  91. package/dist/execute-rule.js.map +1 -1
  92. package/dist/file-walk.d.ts +22 -0
  93. package/dist/file-walk.d.ts.map +1 -0
  94. package/dist/file-walk.js +51 -0
  95. package/dist/file-walk.js.map +1 -0
  96. package/dist/format-github.d.ts.map +1 -1
  97. package/dist/format-github.js +37 -5
  98. package/dist/format-github.js.map +1 -1
  99. package/dist/format-json.d.ts +96 -1
  100. package/dist/format-json.d.ts.map +1 -1
  101. package/dist/format-json.js +94 -9
  102. package/dist/format-json.js.map +1 -1
  103. package/dist/format.d.ts +4 -2
  104. package/dist/format.d.ts.map +1 -1
  105. package/dist/format.js +41 -19
  106. package/dist/format.js.map +1 -1
  107. package/dist/glob-site.d.ts +232 -0
  108. package/dist/glob-site.d.ts.map +1 -0
  109. package/dist/glob-site.js +156 -0
  110. package/dist/glob-site.js.map +1 -0
  111. package/dist/identity-root.d.ts +55 -0
  112. package/dist/identity-root.d.ts.map +1 -0
  113. package/dist/identity-root.js +170 -0
  114. package/dist/identity-root.js.map +1 -0
  115. package/dist/index.d.ts +18 -12
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +12 -10
  118. package/dist/index.js.map +1 -1
  119. package/dist/internal.d.ts +55 -0
  120. package/dist/internal.d.ts.map +1 -0
  121. package/dist/internal.js +92 -0
  122. package/dist/internal.js.map +1 -0
  123. package/dist/mask-non-comment.d.ts +49 -0
  124. package/dist/mask-non-comment.d.ts.map +1 -0
  125. package/dist/mask-non-comment.js +261 -0
  126. package/dist/mask-non-comment.js.map +1 -0
  127. package/dist/owns-empty-discovery.d.ts +5 -0
  128. package/dist/owns-empty-discovery.d.ts.map +1 -0
  129. package/dist/owns-empty-discovery.js +85 -0
  130. package/dist/owns-empty-discovery.js.map +1 -0
  131. package/dist/path-suffix.d.ts +57 -0
  132. package/dist/path-suffix.d.ts.map +1 -0
  133. package/dist/path-suffix.js +73 -0
  134. package/dist/path-suffix.js.map +1 -0
  135. package/dist/path-universe.d.ts +44 -0
  136. package/dist/path-universe.d.ts.map +1 -0
  137. package/dist/path-universe.js +21 -0
  138. package/dist/path-universe.js.map +1 -0
  139. package/dist/predicate.d.ts +45 -0
  140. package/dist/predicate.d.ts.map +1 -1
  141. package/dist/predicate.js +20 -1
  142. package/dist/predicate.js.map +1 -1
  143. package/dist/preset-dispatch.d.ts +6 -15
  144. package/dist/preset-dispatch.d.ts.map +1 -1
  145. package/dist/preset-dispatch.js +35 -17
  146. package/dist/preset-dispatch.js.map +1 -1
  147. package/dist/project-relative.d.ts +25 -0
  148. package/dist/project-relative.d.ts.map +1 -0
  149. package/dist/project-relative.js +54 -0
  150. package/dist/project-relative.js.map +1 -0
  151. package/dist/report.d.ts +42 -12
  152. package/dist/report.d.ts.map +1 -1
  153. package/dist/report.js +190 -14
  154. package/dist/report.js.map +1 -1
  155. package/dist/rule-builder-like.d.ts +17 -0
  156. package/dist/rule-builder-like.d.ts.map +1 -0
  157. package/dist/rule-builder-like.js +2 -0
  158. package/dist/rule-builder-like.js.map +1 -0
  159. package/dist/rule-builder.d.ts +124 -84
  160. package/dist/rule-builder.d.ts.map +1 -1
  161. package/dist/rule-builder.js +230 -181
  162. package/dist/rule-builder.js.map +1 -1
  163. package/dist/rule-description.d.ts +83 -1
  164. package/dist/rule-description.d.ts.map +1 -1
  165. package/dist/rule-description.js +75 -1
  166. package/dist/rule-description.js.map +1 -1
  167. package/dist/rule-metadata.d.ts +20 -3
  168. package/dist/rule-metadata.d.ts.map +1 -1
  169. package/dist/selection-memo.d.ts +45 -0
  170. package/dist/selection-memo.d.ts.map +1 -0
  171. package/dist/selection-memo.js +66 -0
  172. package/dist/selection-memo.js.map +1 -0
  173. package/dist/shallow-clone.d.ts +20 -0
  174. package/dist/shallow-clone.d.ts.map +1 -0
  175. package/dist/shallow-clone.js +27 -0
  176. package/dist/shallow-clone.js.map +1 -0
  177. package/dist/silent-exclusion.d.ts +12 -0
  178. package/dist/silent-exclusion.d.ts.map +1 -1
  179. package/dist/silent-exclusion.js +22 -0
  180. package/dist/silent-exclusion.js.map +1 -1
  181. package/dist/stderr.d.ts +16 -0
  182. package/dist/stderr.d.ts.map +1 -0
  183. package/dist/stderr.js +73 -0
  184. package/dist/stderr.js.map +1 -0
  185. package/dist/terminal-builder.d.ts +224 -10
  186. package/dist/terminal-builder.d.ts.map +1 -1
  187. package/dist/terminal-builder.js +336 -46
  188. package/dist/terminal-builder.js.map +1 -1
  189. package/dist/type-guards.d.ts +38 -0
  190. package/dist/type-guards.d.ts.map +1 -0
  191. package/dist/type-guards.js +42 -0
  192. package/dist/type-guards.js.map +1 -0
  193. package/dist/unsuppressable.d.ts +54 -0
  194. package/dist/unsuppressable.d.ts.map +1 -0
  195. package/dist/unsuppressable.js +76 -0
  196. package/dist/unsuppressable.js.map +1 -0
  197. package/dist/vacuity-findings.d.ts +49 -0
  198. package/dist/vacuity-findings.d.ts.map +1 -0
  199. package/dist/vacuity-findings.js +94 -0
  200. package/dist/vacuity-findings.js.map +1 -0
  201. package/dist/violation.d.ts +173 -4
  202. package/dist/violation.d.ts.map +1 -1
  203. package/dist/violation.js +262 -1
  204. package/dist/violation.js.map +1 -1
  205. package/dist/watch.d.ts +71 -0
  206. package/dist/watch.d.ts.map +1 -0
  207. package/dist/watch.js +129 -0
  208. package/dist/watch.js.map +1 -0
  209. package/package.json +6 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @nielspeter/eess
2
2
 
3
- The dialect-independent **kernel** of the [eess](../../README.md) family.
3
+ The dialect-independent **kernel** of the [eess](https://github.com/nielspeter/eess/blob/main/README.md) family.
4
4
 
5
5
  This package is the engine every eess dialect runs on. It knows nothing about TypeScript, Mermaid, or any specific artifact format; it is generic over the element type being validated.
6
6
 
@@ -12,14 +12,107 @@ This package is the engine every eess dialect runs on. It knows nothing about Ty
12
12
  - Baseline mode, diff-aware filtering, exclusions, and the terminal/GitHub/JSON formatters
13
13
  - `definePredicate` / `defineCondition` — extension points for advanced users
14
14
 
15
+ ## Two entry points
16
+
17
+ `@nielspeter/eess` publishes two:
18
+
19
+ | Specifier | What it is |
20
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `@nielspeter/eess` | The public API. Everything below is documented here, and a change to it is a versioned change. |
22
+ | `@nielspeter/eess/internal` | Family plumbing the sibling dialects share — cache registries, stderr guards, suppression counters, identity hashing, glob-tree internals. Not API. |
23
+
24
+ You want the root. `/internal` exists so the dialects can share one engine without
25
+ each name they touch becoming a public commitment; it is published because npm has
26
+ no way to ship a package-private module, not because it is for you. Nothing there is
27
+ documented, and no dialect re-exports it _as its own API_. Two names — `DiskSet`
28
+ and `OnDisk` — are also on `@nielspeter/eess-ts`'s root, where they are eess-ts's
29
+ own types that happen to share a name; that is the whole of the overlap.
30
+
31
+ Breaking something here is still a versioned change and still needs a changeset
32
+ (ADR-011 clause 4). What you are not owed is migration guidance, because it is not
33
+ API. See
34
+ [ADR-011](https://github.com/nielspeter/eess/blob/main/adr/011-the-kernels-public-api-is-explicit.md).
35
+
36
+ If a symbol you used disappeared from the root, it moved here — the import path is
37
+ the only change.
38
+
39
+ ## Kernel API reference
40
+
41
+ The fluent surface (`RuleBuilder`, `Predicate`, `Condition`, the combinators, the
42
+ formatters) is covered in the [docs site](https://nielspeter.github.io/eess/). This
43
+ section covers the supporting types you will meet in signatures.
44
+
45
+ ### Reporting and severity
46
+
47
+ A preset never emits on its own — the caller owns emission (ADR-008).
48
+
49
+ - **`ReportMode`** — `'throw' | 'return' | 'warn'`. What a finisher does with findings: raise an `ArchRuleError`, hand them back, or print them as advisory and continue.
50
+ - **`ReportOptions`** — the emission controls: `format` (terminal/json/github) and `reason`, the rationale threaded into the output.
51
+ - **`RuleSeverity`** — `'error' | 'warn' | 'off'`, the value of a per-rule override.
52
+ - **`PresetBaseOptions`** — what every preset accepts: an `overrides` map of rule id → `RuleSeverity`, plus the reporting controls. Extend it when you write a preset so `{ report: 'return' }` works without your own plumbing.
53
+ - **`RuleBuilderLike`** — the one-method shape (`violations()`) a preset needs from anything it dispatches, so a preset is not coupled to a dialect's builder class.
54
+ - **`Dispatchable`** — what `dispatchRule` accepts: a builder that can produce violations _and_ name itself, so the dispatcher can apply a per-rule override to it. Strictly more than `RuleBuilderLike`; that one is the narrower shape used where only the findings matter.
55
+ - **`isArchRuleError(err)`** — use this instead of `instanceof ArchRuleError` when catching. `instanceof` compares class identity, so it returns `false` whenever two copies of the package are on disk — a nested `node_modules`, or a rule file loaded through a separate module registry. The check is structural (the stamped `name` plus the `violations` array), so it survives that. The thrown shape itself — `violations: ArchViolation[]` — is public either way.
56
+ - **`ArchConfigError`** / **`isArchConfigError(err)`** — thrown when a _rule_ is misconfigured (bad arguments to a condition, a malformed rule file), as opposed to `ArchRuleError`, which means the _architecture_ is wrong. Two different faults with two different audiences: one tells a rule author they mistyped something, the other tells a team their code drifted. Carries `subject` — the condition, predicate or file at fault — so a caller can route the failure without parsing the message. `isArchConfigError` is structural for the same two-copies-on-disk reason as `isArchRuleError`.
57
+ - **`ArchFix`** — a machine-applicable edit on a violation: `file`, a `[start, end)` character span, and the `replacement` text. This is what `--fix` applies.
58
+
59
+ ### The JSON report
60
+
61
+ `--format json` emits an **`ArchJsonReport`**: a `summary` (`total`, `errors`, `warnings`, `reason`), then **`ArchJsonViolation`** entries, plus **`ArchJsonSuppression`** for findings silenced by an `eess-exclude` comment and **`ArchJsonUntestedAllowlist`** for allowlists nothing exercised. Import these to type a tool that consumes eess output rather than re-declaring the shape.
62
+
63
+ ### Filtering a run
64
+
65
+ - **`BaselineFilter`** — decides whether an already-accepted finding is suppressed by the baseline file.
66
+ - **`DiffFilterLike`** — decides whether a finding falls inside the changed region, for `--changed` runs.
67
+
68
+ Both are interfaces so a host can supply its own; the kernel ships implementations.
69
+
70
+ ### Correspondence
71
+
72
+ `correspondence()` binds two `Selection`s and checks they agree in both directions.
73
+
74
+ - **`CorrespondenceOptions`** — its argument: the `left` and `right` selections, a join strategy, and optional per-side `suggest` callbacks for the two-sided message.
75
+ - **`KeyBy`** — the fast join: a key extracted per element, giving an O(n+m) match. Prefer it. The fallback `matchBy` predicate is O(n×m).
76
+ - **`RelationSpec`** — what `preserveRelations()` takes: how to read edges from each side, so the check covers relationships and not just membership.
77
+ - **`ElementInfo`** — the `name` / `file` / `line` a dialect reports for one element, which is what the message and the violation's location are built from.
78
+
79
+ ### Declaring globs (for custom predicates)
80
+
81
+ A predicate written with `definePredicate` should declare the globs it matches on.
82
+ Without that it is opaque, so a vacuity diagnosis cannot tell "matched nothing
83
+ because the glob is dead" from "matched nothing because nothing was there" — and
84
+ any `or()` containing it becomes undiagnosable too.
85
+
86
+ - **`globNode(leaf)`** — wrap a single declared glob into a tree. The common case.
87
+ - **`globAnyOf(globs, kind, base?)`** — declare several alternatives at once.
88
+ - **`DeclaredGlob`** — one glob with its `kind` and `base`. **`GlobKind`** is what the glob matches against (`'file-path' | 'parent-dir' | 'import-target' | 'specifier' | 'literal'`) and **`GlobBase`** is what it is resolved against (`'absolute' | 'tsconfig-relative' | 'normalized'`).
89
+ - **`GlobTree<L>`** — the `and`/`any` tree the declarations form; **`DeclaredGlobs`** is that tree over `DeclaredGlob`, and **`GlobLeaf<L>`** is one position in it.
90
+ - **`OpaqueGlob`** — the explicit "this branch cannot be described" marker. Honest, and it is what keeps an undeclared predicate from silently reading as a dead one.
91
+ - **`GlobSite`** — a declared glob plus where it was written; **`GlobPosition`** is that where (`'selector' | 'discovery' | 'condition' | 'exclusion'`), which is what lets a diagnosis point at the glob you actually got wrong.
92
+ - **`GlobNode`** — the tree over `GlobSite`, i.e. a fully-stamped declaration ready to diagnose.
93
+
94
+ ### Coverage evidence
95
+
96
+ - **`EdgeCoverage`** — one rule's evidence: how many `subjects` it saw, how many `edges` it actually tested, and a `reason` when that number is zero.
97
+ - **`UntestedReason`** — why zero: `'no-edges'` (nothing to test, usually correct), `'all-filtered'` (edges existed and every one was filtered out), `'none-matched'` (edges existed and none matched the allowlist). The distinction is the point — a rule that tested nothing should say which of the three it was, per ADR-010.
98
+
15
99
  ## Who uses it
16
100
 
17
101
  You normally don't install this directly — you install a dialect:
18
102
 
19
- - [`@nielspeter/eess-ts`](../ts) — TypeScript
20
- - [`@nielspeter/eess-mermaid`](../mermaid) — Mermaid class diagrams
103
+ - [`@nielspeter/eess-ts`](https://www.npmjs.com/package/@nielspeter/eess-ts) — TypeScript
104
+ - [`@nielspeter/eess-mermaid`](https://www.npmjs.com/package/@nielspeter/eess-mermaid) — Mermaid class diagrams
105
+
106
+ Install `@nielspeter/eess` directly only when authoring a new dialect or a cross-dialect
107
+ tool on top of the kernel.
21
108
 
22
- Install `@nielspeter/eess` directly only when authoring a new dialect or a cross-dialect tool on top of the kernel.
109
+ **If that is you, `/internal` is your surface, not a wall.** The two sentences above look
110
+ like they contradict each other — "not for you", then "install it if you are writing a
111
+ dialect" — so to be explicit: a dialect author is exactly who `/internal` exists for, and
112
+ the five in this repo import from it. What the root alone gives you is the rule-authoring
113
+ vocabulary, not the engine. Nothing there is documented and it changes without migration
114
+ notes, so treat it as a contract you re-check on each kernel bump; if you are building a
115
+ dialect you intend to publish, open an issue and we will work out what belongs on the root.
23
116
 
24
117
  ## License
25
118
 
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The assertion-less remedy, by state — bug 0155.
3
+ *
4
+ * A pure function of three facts about a rule, deliberately outside
5
+ * `RuleBuilder`: the class is gated at 300 lines and 20 methods by this repo's
6
+ * own `arch.internal.rules.ts`, and this capability pushed it over both. It is
7
+ * also genuinely independent of the builder — three inputs, one string — so a
8
+ * dialect builder with its own hierarchy can reuse it rather than reinvent the
9
+ * wording.
10
+ *
11
+ * **Branches on `reachedShould`/`misplaced`, never on the builder's phase:**
12
+ * `.should().that()` lands back in the predicate phase having reached
13
+ * `.should()`, and any phase-derived message would tell that author a
14
+ * verifiable falsehood.
15
+ */
16
+ interface AssertionState {
17
+ /** Whether `.should()` was ever reached. */
18
+ readonly reachedShould: boolean;
19
+ /** Descriptions of predicate-only methods written after `.should()`. */
20
+ readonly misplaced: readonly string[];
21
+ /** How many conditions the rule actually carries. */
22
+ readonly conditionCount: number;
23
+ }
24
+ export declare function assertionAdviceFor(state: AssertionState): string;
25
+ export {};
26
+ //# sourceMappingURL=assertion-advice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertion-advice.d.ts","sourceRoot":"","sources":["../src/assertion-advice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,UAAU,cAAc;IACtB,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,qDAAqD;IACrD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAmChE"}
@@ -0,0 +1,29 @@
1
+ export function assertionAdviceFor(state) {
2
+ if (!state.reachedShould) {
3
+ return ('This rule never reached .should(), so it asserts nothing and can never fail. ' +
4
+ 'Add .should() and a condition, or delete the rule.');
5
+ }
6
+ if (state.misplaced.length > 0) {
7
+ const names = state.misplaced.map((d) => `"${d}"`).join(', ');
8
+ const one = state.misplaced.length === 1;
9
+ const verb = one ? 'is a predicate, which filters' : 'are predicates, which filter';
10
+ const it = one ? 'it' : 'them';
11
+ // Two faults, two remedies. With conditions present the rule is not
12
+ // "asserting nothing" in the reader's sense — it asserts something over a
13
+ // set the misplaced predicate silently shrank. Telling this author to
14
+ // "add a condition" would name a fix that leaves the rule as broken.
15
+ if (state.conditionCount > 0) {
16
+ return (`This rule's ${names} ${verb} subjects rather than asserting anything about them, ` +
17
+ `and ${one ? 'it comes' : 'they come'} after .should() — so ${it} narrowed the ` +
18
+ "selection this rule's conditions are evaluated over, and if that narrowed it to " +
19
+ `nothing the conditions hold vacuously. Move ${it} before .should(), where the ` +
20
+ 'filtering is explicit.');
21
+ }
22
+ return (`This rule asserts nothing: ${names} ${verb} subjects rather than asserting ` +
23
+ `anything about them. Move ${it} before .should(), then add a condition.`);
24
+ }
25
+ return ('This rule reached .should() but no condition follows, so it asserts nothing and can ' +
26
+ 'never fail. Add a condition after .should() — or, if this rule is generated from ' +
27
+ 'configuration, skip generating it when there is nothing to assert.');
28
+ }
29
+ //# sourceMappingURL=assertion-advice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertion-advice.js","sourceRoot":"","sources":["../src/assertion-advice.ts"],"names":[],"mappings":"AAwBA,MAAM,UAAU,kBAAkB,CAAC,KAAqB;IACtD,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;QACzB,OAAO,CACL,+EAA+E;YAC/E,oDAAoD,CACrD,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAA;QACxC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,8BAA8B,CAAA;QACnF,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;QAC9B,oEAAoE;QACpE,0EAA0E;QAC1E,sEAAsE;QACtE,qEAAqE;QACrE,IAAI,KAAK,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;YAC7B,OAAO,CACL,eAAe,KAAK,IAAI,IAAI,uDAAuD;gBACnF,OAAO,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,yBAAyB,EAAE,gBAAgB;gBAChF,kFAAkF;gBAClF,+CAA+C,EAAE,+BAA+B;gBAChF,wBAAwB,CACzB,CAAA;QACH,CAAC;QACD,OAAO,CACL,8BAA8B,KAAK,IAAI,IAAI,kCAAkC;YAC7E,6BAA6B,EAAE,0CAA0C,CAC1E,CAAA;IACH,CAAC;IACD,OAAO,CACL,sFAAsF;QACtF,mFAAmF;QACnF,oEAAoE,CACrE,CAAA;AACH,CAAC"}
@@ -5,6 +5,15 @@ import type { ArchViolation } from './violation.js';
5
5
  * Used to generate a baseline: run all rules, collect violations,
6
6
  * write them to a baseline file.
7
7
  *
8
+ * **This bypasses the evidence gate, deliberately and knowingly** — see ADR-014
9
+ * and proposal 011 ask C. Prefer a dialect's `baseline` command, which gates per
10
+ * rule file and refuses a builder that certified nothing.
11
+ *
12
+ * **Not the same function as `eess-ts`'s `collectViolations`.** This one takes
13
+ * builders with a throwing `check()` and harvests the `ArchRuleError`; the
14
+ * dialect's takes builders with a non-throwing `violations()`. Same name, same
15
+ * `@example`, different contract.
16
+ *
8
17
  * @example
9
18
  * const violations = collectViolations(
10
19
  * classes(p).that().extend('Base').should().notContain(call('parseInt')),
@@ -1 +1 @@
1
- {"version":3,"file":"baseline-generator.d.ts","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,GAAG,aAAa,EAAE,CAc5F"}
1
+ {"version":3,"file":"baseline-generator.d.ts","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,GAAG,aAAa,EAAE,CAc5F"}
@@ -5,6 +5,15 @@ import { ArchRuleError } from './errors.js';
5
5
  * Used to generate a baseline: run all rules, collect violations,
6
6
  * write them to a baseline file.
7
7
  *
8
+ * **This bypasses the evidence gate, deliberately and knowingly** — see ADR-014
9
+ * and proposal 011 ask C. Prefer a dialect's `baseline` command, which gates per
10
+ * rule file and refuses a builder that certified nothing.
11
+ *
12
+ * **Not the same function as `eess-ts`'s `collectViolations`.** This one takes
13
+ * builders with a throwing `check()` and harvests the `ArchRuleError`; the
14
+ * dialect's takes builders with a non-throwing `violations()`. Same name, same
15
+ * `@example`, different contract.
16
+ *
8
17
  * @example
9
18
  * const violations = collectViolations(
10
19
  * classes(p).that().extend('Base').should().notContain(call('parseInt')),
@@ -1 +1 @@
1
- {"version":3,"file":"baseline-generator.js","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAG,QAAsC;IACzE,MAAM,aAAa,GAAoB,EAAE,CAAA;IAEzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,OAAO,CAAC,KAAK,EAAE,CAAA;QACjB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;gBACnC,aAAa,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAA;YACzC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,aAAa,CAAA;AACtB,CAAC"}
1
+ {"version":3,"file":"baseline-generator.js","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAG,QAAsC;IACzE,MAAM,aAAa,GAAoB,EAAE,CAAA;IAEzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,OAAO,CAAC,KAAK,EAAE,CAAA;QACjB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;gBACnC,aAAa,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAA;YACzC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,aAAa,CAAA;AACtB,CAAC"}
@@ -16,6 +16,33 @@ export interface BaselineEntry {
16
16
  line: number;
17
17
  /** Stable identity hash: sha256(rule + element + message) */
18
18
  hash: string;
19
+ /**
20
+ * The measurement this entry accepted, for a metric finding
21
+ * (`ArchViolation.measured`). Absent for every non-metric entry, so a
22
+ * baseline with no metric findings is byte-identical to one without this
23
+ * field at all.
24
+ */
25
+ measured?: number;
26
+ /**
27
+ * What {@link measured} counts — `code-lines`, `methods`, `complexity`.
28
+ *
29
+ * Written since [bug 0171](../../../work/bugs/fixed/0171-a-metric-unit-change-silently-loosens-every-baselined-ratchet.md).
30
+ * An accepted ceiling is a number IN A UNIT, and comparing across a change of
31
+ * unit does not measure a regression — it moves the bar. Absent on entries
32
+ * written before stamping, which {@link Baseline.isKnown} treats as "unknown
33
+ * unit": comparable only for metrics whose meaning has never changed.
34
+ *
35
+ * No dialect on the kernel emits `measured` today; this is here so the hole
36
+ * cannot open silently in the one that does, and so the two copies of this
37
+ * file do not disagree about what a baseline entry means.
38
+ */
39
+ measuredUnit?: string;
40
+ }
41
+ /** A measurement an entry accepted, and what that number counts (bug 0171). */
42
+ interface AcceptedMeasurement {
43
+ value: number;
44
+ /** `undefined` on entries written before unit stamping. */
45
+ unit?: string;
19
46
  }
20
47
  /**
21
48
  * The baseline file structure.
@@ -31,19 +58,32 @@ export interface BaselineFile {
31
58
  /**
32
59
  * Compute a stable hash for a violation.
33
60
  *
34
- * Uses rule + element + message as identity. This survives:
61
+ * Uses `violation.identity` when the condition set one (a canonical,
62
+ * position-independent key — see `ArchViolation.identity`'s own doc comment);
63
+ * otherwise falls back to rule + element + message. The fallback survives:
35
64
  * - Line number changes (code moved)
36
65
  * - Unrelated code changes in the same file
37
66
  *
38
67
  * Does NOT survive:
39
68
  * - Rule description changes (rewording .because())
40
69
  * - Element renames (class renamed)
41
- * - Message text changes (condition wording updated)
70
+ * - Message text changes (condition wording updated) — including a message
71
+ * that embeds a line number, which changes whenever code shifts above the
72
+ * match. A condition that reports such a message sets `identity` instead
73
+ * of relying on this fallback.
42
74
  *
43
75
  * This is intentional — if the rule or element changes,
44
76
  * the violation should be re-evaluated.
77
+ *
78
+ * `root`, when supplied, is scrubbed out of both fields via
79
+ * `normalizeIdentityText` before hashing — an `identity` string that
80
+ * interpolates a file path (e.g. `identifyMatches`'s per-declaration
81
+ * identities) otherwise embeds the absolute checkout path, and the same
82
+ * baseline generated on two machines (or locally vs. CI) would never match.
83
+ * Omitted, hashing is unchanged — this stays opt-in so a caller with no
84
+ * portability need (or no discoverable root) pays nothing for it.
45
85
  */
46
- export declare function hashViolation(violation: ArchViolation): string;
86
+ export declare function hashViolation(violation: ArchViolation, root?: string): string;
47
87
  /**
48
88
  * Load a baseline from a JSON file.
49
89
  *
@@ -67,17 +107,42 @@ export declare function generateBaseline(violations: ArchViolation[], outputPath
67
107
  export declare class Baseline {
68
108
  private readonly knownHashes;
69
109
  private readonly baselineDir;
70
- constructor(knownHashes: Set<string>, baselineDir: string);
110
+ /** Computed once at load time — must match `generateBaseline()`'s root for entries to line up. */
111
+ private readonly root?;
112
+ /** hash -> accepted measurement, for metric findings. */
113
+ private readonly accepted;
114
+ constructor(knownHashes: Set<string>, baselineDir: string,
115
+ /** Computed once at load time — must match `generateBaseline()`'s root for entries to line up. */
116
+ root?: string | undefined,
117
+ /** hash -> accepted measurement, for metric findings. */
118
+ accepted?: Map<string, AcceptedMeasurement>);
71
119
  /**
72
120
  * Check if a violation is known (exists in the baseline).
73
121
  * Known violations are filtered out — they don't cause failures.
122
+ *
123
+ * For a metric finding (`violation.measured` set), "known" is a **ratchet**,
124
+ * not hash equality: the finding is known if the baseline has an accepted
125
+ * measurement for this identity AND the current measurement is no worse
126
+ * than what was accepted. Identity alone (the hash) cannot express this —
127
+ * it deliberately excludes the count, so an improved measurement keeps the
128
+ * same hash as a regressed one, and hash-only matching would treat both as
129
+ * "known" alike. Comparing the two numbers is what tells them apart: an
130
+ * improvement stays known (still <= accepted), a regression does not (now >
131
+ * accepted), even though neither changed which entry it matches.
74
132
  */
75
133
  isKnown(violation: ArchViolation): boolean;
76
134
  /**
77
135
  * Filter out known violations, returning only new ones.
136
+ *
137
+ * A `bypassFilters` configuration finding is never treated as known, even
138
+ * if an older or hand-edited baseline file happens to carry a matching
139
+ * hash — `generateBaseline()` no longer writes these, but a baseline
140
+ * generated before that fix (or hand-edited) must not be able to
141
+ * resurrect the suppression this defends against.
78
142
  */
79
143
  filterNew(violations: ArchViolation[]): ArchViolation[];
80
144
  /** Number of known violations in the baseline */
81
145
  get size(): number;
82
146
  }
147
+ export {};
83
148
  //# sourceMappingURL=baseline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAA;IACnB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,UAAU,EAAE,aAAa,EAAE,CAAA;CAC5B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAG9D;AAUD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CA6B3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAmBtF;AAED;;GAEG;AACH,qBAAa,QAAQ;IAEjB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW;gBADX,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,MAAM;IAGtC;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO;IAI1C;;OAEG;IACH,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE;IAIvD,iDAAiD;IACjD,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAInD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,+EAA+E;AAC/E,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AA6BD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAA;IACnB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,UAAU,EAAE,aAAa,EAAE,CAAA;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAa7E;AAUD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAwC3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAgCtF;AAED;;GAEG;AACH,qBAAa,QAAQ;IAEjB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,kGAAkG;IAClG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;IACtB,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBALR,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,MAAM;IACpC,kGAAkG;IACjF,IAAI,CAAC,EAAE,MAAM,YAAA;IAC9B,yDAAyD;IACxC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAa;IAGzE;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO;IAU1C;;;;;;;;OAQG;IACH,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE;IAIvD,iDAAiD;IACjD,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
package/dist/baseline.js CHANGED
@@ -1,23 +1,73 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
+ import { discoverIdentityRoot, normalizeIdentityText } from './identity-root.js';
5
+ import { writeStderr } from './stderr.js';
6
+ /**
7
+ * Units whose meaning has never changed, so an entry predating unit stamping is
8
+ * still safely comparable against one.
9
+ *
10
+ * `code-lines` is deliberately absent: an unstamped `lines` entry was written
11
+ * when the metric counted a SPAN, so its number is denominated in something no
12
+ * longer produced. Comparing across that raises the ceiling silently.
13
+ */
14
+ const MEANING_NEVER_CHANGED = new Set([
15
+ 'methods',
16
+ 'parameters',
17
+ 'properties',
18
+ 'named-exports',
19
+ 'complexity',
20
+ ]);
21
+ /**
22
+ * Whether a stored measurement may be compared against a current one. Fails
23
+ * CLOSED — anything but a demonstrable match is incomparable, because assuming
24
+ * agreement is how a ratchet loosens without anyone being told (ADR-010).
25
+ */
26
+ function measurementComparable(stored, current) {
27
+ if (stored === current)
28
+ return true;
29
+ if (stored === undefined)
30
+ return current !== undefined && MEANING_NEVER_CHANGED.has(current);
31
+ return false;
32
+ }
4
33
  /**
5
34
  * Compute a stable hash for a violation.
6
35
  *
7
- * Uses rule + element + message as identity. This survives:
36
+ * Uses `violation.identity` when the condition set one (a canonical,
37
+ * position-independent key — see `ArchViolation.identity`'s own doc comment);
38
+ * otherwise falls back to rule + element + message. The fallback survives:
8
39
  * - Line number changes (code moved)
9
40
  * - Unrelated code changes in the same file
10
41
  *
11
42
  * Does NOT survive:
12
43
  * - Rule description changes (rewording .because())
13
44
  * - Element renames (class renamed)
14
- * - Message text changes (condition wording updated)
45
+ * - Message text changes (condition wording updated) — including a message
46
+ * that embeds a line number, which changes whenever code shifts above the
47
+ * match. A condition that reports such a message sets `identity` instead
48
+ * of relying on this fallback.
15
49
  *
16
50
  * This is intentional — if the rule or element changes,
17
51
  * the violation should be re-evaluated.
52
+ *
53
+ * `root`, when supplied, is scrubbed out of both fields via
54
+ * `normalizeIdentityText` before hashing — an `identity` string that
55
+ * interpolates a file path (e.g. `identifyMatches`'s per-declaration
56
+ * identities) otherwise embeds the absolute checkout path, and the same
57
+ * baseline generated on two machines (or locally vs. CI) would never match.
58
+ * Omitted, hashing is unchanged — this stays opt-in so a caller with no
59
+ * portability need (or no discoverable root) pays nothing for it.
18
60
  */
19
- export function hashViolation(violation) {
20
- const content = `${violation.rule}::${violation.element}::${violation.message}`;
61
+ export function hashViolation(violation, root) {
62
+ const scrub = (text) => root === undefined ? text : normalizeIdentityText(text, root);
63
+ // `rule` is always part of the hash, even when `identity` is set — two
64
+ // different rules that happened to produce the same identity string (e.g.
65
+ // two metric conditions on the same class) must not collide in one
66
+ // baseline entry.
67
+ const subject = violation.identity !== undefined
68
+ ? violation.identity
69
+ : `${violation.element}::${violation.message}`;
70
+ const content = `${scrub(violation.rule)}::${scrub(subject)}`;
21
71
  return createHash('sha256').update(content).digest('hex').slice(0, 16);
22
72
  }
23
73
  /**
@@ -36,9 +86,10 @@ function toRelativePath(absolutePath, baselineDir) {
36
86
  export function withBaseline(baselinePath) {
37
87
  const resolved = path.resolve(baselinePath);
38
88
  const baselineDir = path.dirname(resolved);
89
+ const root = discoverIdentityRoot(baselineDir);
39
90
  if (!fs.existsSync(resolved)) {
40
91
  // No baseline file = no known violations = all violations are new
41
- return new Baseline(new Set(), baselineDir);
92
+ return new Baseline(new Set(), baselineDir, root);
42
93
  }
43
94
  const raw = fs.readFileSync(resolved, 'utf-8');
44
95
  const parsed = JSON.parse(raw);
@@ -46,17 +97,26 @@ export function withBaseline(baselinePath) {
46
97
  typeof parsed !== 'object' ||
47
98
  !('violations' in parsed) ||
48
99
  !Array.isArray(parsed.violations)) {
49
- console.warn(`[eess] Invalid baseline file format at ${resolved} — treating as empty`);
50
- return new Baseline(new Set(), baselineDir);
100
+ writeStderr(`[eess] Invalid baseline file format at ${resolved} — treating as empty`);
101
+ return new Baseline(new Set(), baselineDir, root);
51
102
  }
52
103
  const violations = parsed.violations;
53
104
  const hashes = new Set();
105
+ const accepted = new Map();
54
106
  for (const entry of violations) {
55
107
  if (entry && typeof entry === 'object' && 'hash' in entry && typeof entry.hash === 'string') {
56
108
  hashes.add(entry.hash);
109
+ if ('measured' in entry && typeof entry.measured === 'number') {
110
+ accepted.set(entry.hash, {
111
+ value: entry.measured,
112
+ unit: 'measuredUnit' in entry && typeof entry.measuredUnit === 'string'
113
+ ? entry.measuredUnit
114
+ : undefined,
115
+ });
116
+ }
57
117
  }
58
118
  }
59
- return new Baseline(hashes, baselineDir);
119
+ return new Baseline(hashes, baselineDir, root, accepted);
60
120
  }
61
121
  /**
62
122
  * Generate a baseline file from a list of violations.
@@ -70,11 +130,24 @@ export function withBaseline(baselinePath) {
70
130
  export function generateBaseline(violations, outputPath) {
71
131
  const resolved = path.resolve(outputPath);
72
132
  const baselineDir = path.dirname(resolved);
73
- const entries = violations.map((v) => ({
133
+ const root = discoverIdentityRoot(baselineDir);
134
+ // A bypassFilters configuration finding (ADR-010) can never legitimately
135
+ // become "known, pre-existing debt" — it means the rule's own instrument
136
+ // is broken right now. Recording it would let a future run of `check
137
+ // --baseline` silently treat a dead selector as already-accepted.
138
+ const entries = violations
139
+ .filter((v) => v.bypassFilters !== true)
140
+ .map((v) => ({
74
141
  rule: v.rule,
75
142
  file: toRelativePath(v.file, baselineDir),
76
143
  line: v.line,
77
- hash: hashViolation(v),
144
+ hash: hashViolation(v, root),
145
+ // Written only for metric findings, so a baseline with none is
146
+ // byte-identical to one from before this field existed.
147
+ ...(v.measured === undefined ? {} : { measured: v.measured }),
148
+ // Bug 0171: what that number counts, so a later change to the metric
149
+ // cannot silently re-denominate an accepted ceiling.
150
+ ...(v.measuredUnit === undefined ? {} : { measuredUnit: v.measuredUnit }),
78
151
  }));
79
152
  const baseline = {
80
153
  generatedAt: new Date().toISOString(),
@@ -90,22 +163,55 @@ export function generateBaseline(violations, outputPath) {
90
163
  export class Baseline {
91
164
  knownHashes;
92
165
  baselineDir;
93
- constructor(knownHashes, baselineDir) {
166
+ root;
167
+ accepted;
168
+ constructor(knownHashes, baselineDir,
169
+ /** Computed once at load time — must match `generateBaseline()`'s root for entries to line up. */
170
+ root,
171
+ /** hash -> accepted measurement, for metric findings. */
172
+ accepted = new Map()) {
94
173
  this.knownHashes = knownHashes;
95
174
  this.baselineDir = baselineDir;
175
+ this.root = root;
176
+ this.accepted = accepted;
96
177
  }
97
178
  /**
98
179
  * Check if a violation is known (exists in the baseline).
99
180
  * Known violations are filtered out — they don't cause failures.
181
+ *
182
+ * For a metric finding (`violation.measured` set), "known" is a **ratchet**,
183
+ * not hash equality: the finding is known if the baseline has an accepted
184
+ * measurement for this identity AND the current measurement is no worse
185
+ * than what was accepted. Identity alone (the hash) cannot express this —
186
+ * it deliberately excludes the count, so an improved measurement keeps the
187
+ * same hash as a regressed one, and hash-only matching would treat both as
188
+ * "known" alike. Comparing the two numbers is what tells them apart: an
189
+ * improvement stays known (still <= accepted), a regression does not (now >
190
+ * accepted), even though neither changed which entry it matches.
100
191
  */
101
192
  isKnown(violation) {
102
- return this.knownHashes.has(hashViolation(violation));
193
+ const hash = hashViolation(violation, this.root);
194
+ if (violation.measured === undefined)
195
+ return this.knownHashes.has(hash);
196
+ const acceptedMeasurement = this.accepted.get(hash);
197
+ if (acceptedMeasurement === undefined)
198
+ return false;
199
+ // Bug 0171: `<=` means nothing until both numbers count the same thing.
200
+ if (!measurementComparable(acceptedMeasurement.unit, violation.measuredUnit))
201
+ return false;
202
+ return violation.measured <= acceptedMeasurement.value;
103
203
  }
104
204
  /**
105
205
  * Filter out known violations, returning only new ones.
206
+ *
207
+ * A `bypassFilters` configuration finding is never treated as known, even
208
+ * if an older or hand-edited baseline file happens to carry a matching
209
+ * hash — `generateBaseline()` no longer writes these, but a baseline
210
+ * generated before that fix (or hand-edited) must not be able to
211
+ * resurrect the suppression this defends against.
106
212
  */
107
213
  filterNew(violations) {
108
- return violations.filter((v) => !this.isKnown(v));
214
+ return violations.filter((v) => v.bypassFilters === true || !this.isKnown(v));
109
215
  }
110
216
  /** Number of known violations in the baseline */
111
217
  get size() {
@@ -1 +1 @@
1
- {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /** Register a cache's reset. Call once, at module scope. */
2
+ export declare function registerCacheReset(reset: () => void): void;
3
+ /**
4
+ * Drop every registered cache.
5
+ *
6
+ * Called by `resetProjectCache()`. The caches are keyed on object identity, so
7
+ * a caller who obtains projects through `project()` / `workspace()` is already
8
+ * covered — those functions hand back a new object. This is the escape hatch
9
+ * for a consumer holding an `ArchProject` **they** built across a mutation of
10
+ * the underlying ts-morph project, where identity does not change and the
11
+ * cached population would otherwise be frozen.
12
+ */
13
+ export declare function clearRegisteredCaches(): void;
14
+ //# sourceMappingURL=cache-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-registry.d.ts","sourceRoot":"","sources":["../src/cache-registry.ts"],"names":[],"mappings":"AAmBA,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,IAAI,GAAG,IAAI,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Where invalidatable caches register themselves.
3
+ *
4
+ * This module exists to break an import cycle, and the cycle was found by this
5
+ * repository's own `beFreeOfCycles` rule rather than by review. Wiring
6
+ * `resetProjectCache()` directly to `clearElementCaches()` made
7
+ * `project.ts → element-cache.ts → project.ts` — the back edge being
8
+ * `import type { ArchProject }`, which counts, because dependency conditions
9
+ * have seen type-expression edges since v0.28.0.
10
+ *
11
+ * So the registry depends on nothing. `element-cache.ts` and `module-edges.ts`
12
+ * register a reset closure here; `project.ts` calls {@link clearRegisteredCaches}
13
+ * and never learns what is in it.
14
+ *
15
+ * A `WeakMap` cannot be enumerated, so "clear" means "replace the map". Each
16
+ * cache contributes the closure that replaces its own.
17
+ */
18
+ const resets = [];
19
+ /** Register a cache's reset. Call once, at module scope. */
20
+ export function registerCacheReset(reset) {
21
+ resets.push(reset);
22
+ }
23
+ /**
24
+ * Drop every registered cache.
25
+ *
26
+ * Called by `resetProjectCache()`. The caches are keyed on object identity, so
27
+ * a caller who obtains projects through `project()` / `workspace()` is already
28
+ * covered — those functions hand back a new object. This is the escape hatch
29
+ * for a consumer holding an `ArchProject` **they** built across a mutation of
30
+ * the underlying ts-morph project, where identity does not change and the
31
+ * cached population would otherwise be frozen.
32
+ */
33
+ export function clearRegisteredCaches() {
34
+ for (const reset of resets)
35
+ reset();
36
+ }
37
+ //# sourceMappingURL=cache-registry.js.map