@nielspeter/eess 0.2.2 → 0.4.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 (157) hide show
  1. package/README.md +3 -3
  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.d.ts +69 -4
  7. package/dist/baseline.d.ts.map +1 -1
  8. package/dist/baseline.js +119 -13
  9. package/dist/baseline.js.map +1 -1
  10. package/dist/cache-registry.d.ts +14 -0
  11. package/dist/cache-registry.d.ts.map +1 -0
  12. package/dist/cache-registry.js +37 -0
  13. package/dist/cache-registry.js.map +1 -0
  14. package/dist/cardinality.d.ts +35 -0
  15. package/dist/cardinality.d.ts.map +1 -0
  16. package/dist/cardinality.js +65 -0
  17. package/dist/cardinality.js.map +1 -0
  18. package/dist/check-options.d.ts +14 -0
  19. package/dist/check-options.d.ts.map +1 -1
  20. package/dist/comment-suppression.d.ts +60 -0
  21. package/dist/comment-suppression.d.ts.map +1 -0
  22. package/dist/comment-suppression.js +89 -0
  23. package/dist/comment-suppression.js.map +1 -0
  24. package/dist/condition.d.ts +6 -0
  25. package/dist/condition.d.ts.map +1 -1
  26. package/dist/correspondence-core.d.ts +35 -0
  27. package/dist/correspondence-core.d.ts.map +1 -0
  28. package/dist/correspondence-core.js +38 -0
  29. package/dist/correspondence-core.js.map +1 -0
  30. package/dist/correspondence.d.ts +27 -5
  31. package/dist/correspondence.d.ts.map +1 -1
  32. package/dist/correspondence.js +39 -5
  33. package/dist/correspondence.js.map +1 -1
  34. package/dist/dedupe-config-findings.d.ts +56 -0
  35. package/dist/dedupe-config-findings.d.ts.map +1 -0
  36. package/dist/dedupe-config-findings.js +74 -0
  37. package/dist/dedupe-config-findings.js.map +1 -0
  38. package/dist/define.d.ts +44 -2
  39. package/dist/define.d.ts.map +1 -1
  40. package/dist/define.js +45 -4
  41. package/dist/define.js.map +1 -1
  42. package/dist/diff-aware.d.ts +8 -0
  43. package/dist/diff-aware.d.ts.map +1 -1
  44. package/dist/diff-aware.js +11 -2
  45. package/dist/diff-aware.js.map +1 -1
  46. package/dist/diff-disclosure.d.ts +72 -0
  47. package/dist/diff-disclosure.d.ts.map +1 -0
  48. package/dist/diff-disclosure.js +110 -0
  49. package/dist/diff-disclosure.js.map +1 -0
  50. package/dist/disk-set.d.ts +27 -0
  51. package/dist/disk-set.d.ts.map +1 -0
  52. package/dist/disk-set.js +12 -0
  53. package/dist/disk-set.js.map +1 -0
  54. package/dist/edge-coverage.d.ts +96 -0
  55. package/dist/edge-coverage.d.ts.map +1 -0
  56. package/dist/edge-coverage.js +108 -0
  57. package/dist/edge-coverage.js.map +1 -0
  58. package/dist/errors.d.ts +25 -0
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +32 -0
  61. package/dist/errors.js.map +1 -1
  62. package/dist/execute-rule.d.ts +2 -1
  63. package/dist/execute-rule.d.ts.map +1 -1
  64. package/dist/execute-rule.js +38 -6
  65. package/dist/execute-rule.js.map +1 -1
  66. package/dist/format-github.d.ts.map +1 -1
  67. package/dist/format-github.js +37 -5
  68. package/dist/format-github.js.map +1 -1
  69. package/dist/format-json.d.ts +83 -1
  70. package/dist/format-json.d.ts.map +1 -1
  71. package/dist/format-json.js +70 -9
  72. package/dist/format-json.js.map +1 -1
  73. package/dist/format.d.ts +4 -2
  74. package/dist/format.d.ts.map +1 -1
  75. package/dist/format.js +41 -19
  76. package/dist/format.js.map +1 -1
  77. package/dist/glob-site.d.ts +232 -0
  78. package/dist/glob-site.d.ts.map +1 -0
  79. package/dist/glob-site.js +156 -0
  80. package/dist/glob-site.js.map +1 -0
  81. package/dist/identity-root.d.ts +55 -0
  82. package/dist/identity-root.d.ts.map +1 -0
  83. package/dist/identity-root.js +170 -0
  84. package/dist/identity-root.js.map +1 -0
  85. package/dist/index.d.ts +41 -3
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +33 -3
  88. package/dist/index.js.map +1 -1
  89. package/dist/owns-empty-discovery.d.ts +5 -0
  90. package/dist/owns-empty-discovery.d.ts.map +1 -0
  91. package/dist/owns-empty-discovery.js +77 -0
  92. package/dist/owns-empty-discovery.js.map +1 -0
  93. package/dist/path-universe.d.ts +44 -0
  94. package/dist/path-universe.d.ts.map +1 -0
  95. package/dist/path-universe.js +21 -0
  96. package/dist/path-universe.js.map +1 -0
  97. package/dist/predicate.d.ts +33 -0
  98. package/dist/predicate.d.ts.map +1 -1
  99. package/dist/preset-dispatch.d.ts +19 -0
  100. package/dist/preset-dispatch.d.ts.map +1 -1
  101. package/dist/preset-dispatch.js +43 -3
  102. package/dist/preset-dispatch.js.map +1 -1
  103. package/dist/project-relative.d.ts +25 -0
  104. package/dist/project-relative.d.ts.map +1 -0
  105. package/dist/project-relative.js +54 -0
  106. package/dist/project-relative.js.map +1 -0
  107. package/dist/report.d.ts +11 -5
  108. package/dist/report.d.ts.map +1 -1
  109. package/dist/report.js +17 -0
  110. package/dist/report.js.map +1 -1
  111. package/dist/rule-builder-like.d.ts +11 -0
  112. package/dist/rule-builder-like.d.ts.map +1 -0
  113. package/dist/rule-builder-like.js +2 -0
  114. package/dist/rule-builder-like.js.map +1 -0
  115. package/dist/rule-builder.d.ts +109 -84
  116. package/dist/rule-builder.d.ts.map +1 -1
  117. package/dist/rule-builder.js +202 -158
  118. package/dist/rule-builder.js.map +1 -1
  119. package/dist/rule-description.d.ts +33 -1
  120. package/dist/rule-description.d.ts.map +1 -1
  121. package/dist/rule-description.js +17 -1
  122. package/dist/rule-description.js.map +1 -1
  123. package/dist/rule-metadata.d.ts +20 -3
  124. package/dist/rule-metadata.d.ts.map +1 -1
  125. package/dist/selection-memo.d.ts +47 -0
  126. package/dist/selection-memo.d.ts.map +1 -0
  127. package/dist/selection-memo.js +68 -0
  128. package/dist/selection-memo.js.map +1 -0
  129. package/dist/shallow-clone.d.ts +20 -0
  130. package/dist/shallow-clone.d.ts.map +1 -0
  131. package/dist/shallow-clone.js +27 -0
  132. package/dist/shallow-clone.js.map +1 -0
  133. package/dist/stderr.d.ts +16 -0
  134. package/dist/stderr.d.ts.map +1 -0
  135. package/dist/stderr.js +73 -0
  136. package/dist/stderr.js.map +1 -0
  137. package/dist/terminal-builder.d.ts +209 -9
  138. package/dist/terminal-builder.d.ts.map +1 -1
  139. package/dist/terminal-builder.js +264 -20
  140. package/dist/terminal-builder.js.map +1 -1
  141. package/dist/type-guards.d.ts +38 -0
  142. package/dist/type-guards.d.ts.map +1 -0
  143. package/dist/type-guards.js +42 -0
  144. package/dist/type-guards.js.map +1 -0
  145. package/dist/unsuppressable.d.ts +54 -0
  146. package/dist/unsuppressable.d.ts.map +1 -0
  147. package/dist/unsuppressable.js +76 -0
  148. package/dist/unsuppressable.js.map +1 -0
  149. package/dist/vacuity-findings.d.ts +55 -0
  150. package/dist/vacuity-findings.d.ts.map +1 -0
  151. package/dist/vacuity-findings.js +124 -0
  152. package/dist/vacuity-findings.js.map +1 -0
  153. package/dist/violation.d.ts +152 -4
  154. package/dist/violation.d.ts.map +1 -1
  155. package/dist/violation.js +262 -1
  156. package/dist/violation.js.map +1 -1
  157. package/package.json +1 -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
 
@@ -16,8 +16,8 @@ This package is the engine every eess dialect runs on. It knows nothing about Ty
16
16
 
17
17
  You normally don't install this directly — you install a dialect:
18
18
 
19
- - [`@nielspeter/eess-ts`](../ts) — TypeScript
20
- - [`@nielspeter/eess-mermaid`](../mermaid) — Mermaid class diagrams
19
+ - [`@nielspeter/eess-ts`](https://www.npmjs.com/package/@nielspeter/eess-ts) — TypeScript
20
+ - [`@nielspeter/eess-mermaid`](https://www.npmjs.com/package/@nielspeter/eess-mermaid) — Mermaid class diagrams
21
21
 
22
22
  Install `@nielspeter/eess` directly only when authoring a new dialect or a cross-dialect tool on top of the kernel.
23
23
 
@@ -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"}
@@ -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/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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-registry.js","sourceRoot":"","sources":["../src/cache-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,GAAmB,EAAE,CAAA;AAEjC,4DAA4D;AAC5D,MAAM,UAAU,kBAAkB,CAAC,KAAiB;IAClD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACpB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB;IACnC,KAAK,MAAM,KAAK,IAAI,MAAM;QAAE,KAAK,EAAE,CAAA;AACrC,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Declare that this condition is satisfied by an empty selection.
3
+ *
4
+ * **A registry, because a symbol was forgeable.** The first version keyed a
5
+ * module-private `unique symbol` onto the condition, reasoning that a consumer
6
+ * could not import it to name the key. They do not need to import it — four
7
+ * shipped conditions carry it as an own property, and `notExist` is publicly
8
+ * exported, so:
9
+ *
10
+ * ```ts
11
+ * const stolen = Object.getOwnPropertySymbols(notExist())[0]
12
+ * const mine = { description: 'x', evaluate: () => [], [stolen]: true }
13
+ * ```
14
+ *
15
+ * Measured: an honest condition on an empty selection produces **1** configuration
16
+ * finding and that forgery produces **0**. One line, through documented exports,
17
+ * to exempt any rule from the empty-selection gate — the gate this library is
18
+ * named around, and precisely the `.allowEmpty()` hazard the symbol was chosen to
19
+ * prevent.
20
+ *
21
+ * Found by review, immediately after the identical hole was closed in
22
+ * [ts-archunit plan 0081](https://github.com/nielspeter/ts-archunit/blob/main/plans/completed/0081-a-condition-declares-discovery-ownership.md)'s
23
+ * symbol — while this one was being cited in that module's docstring as the safe
24
+ * precedent. The lesson is worth more than the fix: **"module-private" describes
25
+ * the binding, not the value.** A symbol keyed onto a public object is unlisted,
26
+ * not unreachable.
27
+ *
28
+ * `WeakSet` membership cannot be read off an object, copied, or forged: a caller
29
+ * would need this module's binding, and it is not exported. Weak so a condition is
30
+ * not retained after its rule is discarded.
31
+ */
32
+ export declare function marksAssertsCardinality<T extends object>(condition: T): T;
33
+ /** Is this condition satisfied by an empty selection? */
34
+ export declare function assertsCardinality(condition: object): boolean;
35
+ //# sourceMappingURL=cardinality.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardinality.d.ts","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAGzE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE7D"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The cardinality exemption, keyed so that only this library can set it.
3
+ *
4
+ * Plan 0074 (R3b), and specifically constraint 2 of 0069's appendix:
5
+ *
6
+ * > `Condition` is a public export and `defineCondition` is its sanctioned
7
+ * > constructor. If the flag is expressible there, it is a one-line silent
8
+ * > opt-out on any user condition — the hazard the Decisions section used to
9
+ * > reject `.allowEmpty()`, relocated onto the condition object. A
10
+ * > module-private `unique symbol`, with no `defineCondition` parameter, closes
11
+ * > it by construction (ADR-009 rule 3's corollary).
12
+ *
13
+ * A first cut of this shipped as a plain `assertsCardinality?: true` property,
14
+ * which is exactly the rejected shape: `Condition` is exported from
15
+ * `src/index.ts`, so any user object literal could set it and silence the
16
+ * empty-selection gate on any rule, permanently and invisibly. That is
17
+ * `.allowEmpty()` wearing a different hat.
18
+ *
19
+ * This symbol is **not** re-exported from `src/index.ts`, and `package.json`'s
20
+ * `exports` map has no wildcard subpath, so a consumer cannot import it to
21
+ * name the key. The escape hatch users *do* get is `.expectEmpty()`, which is
22
+ * an assertion that fails when the selection stops being empty — not a
23
+ * silencer.
24
+ */
25
+ const CARDINALITY_ASSERTERS = new WeakSet();
26
+ /**
27
+ * Declare that this condition is satisfied by an empty selection.
28
+ *
29
+ * **A registry, because a symbol was forgeable.** The first version keyed a
30
+ * module-private `unique symbol` onto the condition, reasoning that a consumer
31
+ * could not import it to name the key. They do not need to import it — four
32
+ * shipped conditions carry it as an own property, and `notExist` is publicly
33
+ * exported, so:
34
+ *
35
+ * ```ts
36
+ * const stolen = Object.getOwnPropertySymbols(notExist())[0]
37
+ * const mine = { description: 'x', evaluate: () => [], [stolen]: true }
38
+ * ```
39
+ *
40
+ * Measured: an honest condition on an empty selection produces **1** configuration
41
+ * finding and that forgery produces **0**. One line, through documented exports,
42
+ * to exempt any rule from the empty-selection gate — the gate this library is
43
+ * named around, and precisely the `.allowEmpty()` hazard the symbol was chosen to
44
+ * prevent.
45
+ *
46
+ * Found by review, immediately after the identical hole was closed in
47
+ * [ts-archunit plan 0081](https://github.com/nielspeter/ts-archunit/blob/main/plans/completed/0081-a-condition-declares-discovery-ownership.md)'s
48
+ * symbol — while this one was being cited in that module's docstring as the safe
49
+ * precedent. The lesson is worth more than the fix: **"module-private" describes
50
+ * the binding, not the value.** A symbol keyed onto a public object is unlisted,
51
+ * not unreachable.
52
+ *
53
+ * `WeakSet` membership cannot be read off an object, copied, or forged: a caller
54
+ * would need this module's binding, and it is not exported. Weak so a condition is
55
+ * not retained after its rule is discarded.
56
+ */
57
+ export function marksAssertsCardinality(condition) {
58
+ CARDINALITY_ASSERTERS.add(condition);
59
+ return condition;
60
+ }
61
+ /** Is this condition satisfied by an empty selection? */
62
+ export function assertsCardinality(condition) {
63
+ return CARDINALITY_ASSERTERS.has(condition);
64
+ }
65
+ //# sourceMappingURL=cardinality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardinality.js","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,qBAAqB,GAAG,IAAI,OAAO,EAAU,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,uBAAuB,CAAmB,SAAY;IACpE,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACpC,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,OAAO,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AAC7C,CAAC"}
@@ -22,6 +22,20 @@ export interface BaselineFilter {
22
22
  export interface DiffFilterLike {
23
23
  /** Filter violations to only those in changed files. */
24
24
  filterToChanged(violations: ArchViolation[]): ArchViolation[];
25
+ /**
26
+ * How many files this filter considers changed, or a negative number when it
27
+ * could not tell (`DiffFilter` returns -1 when git was unavailable).
28
+ *
29
+ * **Optional, and the disclosure degrades rather than guesses.** This is a
30
+ * structural interface, so a consumer may pass any object with
31
+ * `filterToChanged`; when `size` is absent the suppression notice omits the
32
+ * file count instead of inventing one. What it never omits is the suppressed
33
+ * count itself, because the caller derives that by subtraction rather than
34
+ * asking the filter (see `core/diff-disclosure.ts`).
35
+ */
36
+ readonly size?: number;
37
+ /** The branch diffed against, when the filter knows it. Optional, as `size`. */
38
+ readonly baseBranch?: string;
25
39
  }
26
40
  /**
27
41
  * Options passed to .check() and .warn() to filter violations.
@@ -1 +1 @@
1
- {"version":3,"file":"check-options.d.ts","sourceRoot":"","sources":["../src/check-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEzD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;CACxD;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;CAC9D;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,iDAAiD;IACjD,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,wDAAwD;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB"}
1
+ {"version":3,"file":"check-options.d.ts","sourceRoot":"","sources":["../src/check-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEzD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;CACxD;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;IAC7D;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,iDAAiD;IACjD,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,wDAAwD;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB"}