@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
@@ -1,3 +1,4 @@
1
+ import type { RuleMetadata } from './rule-metadata.js';
1
2
  /**
2
3
  * Structured description of a rule, returned by `.describe()`.
3
4
  * Used by the `explain` CLI subcommand to dump active rules as JSON.
@@ -8,7 +9,88 @@ export interface RuleDescription {
8
9
  because?: string;
9
10
  suggestion?: string;
10
11
  docs?: string;
11
- /** One-line imperative for AI-agent prompts (see {@link RuleMetadata.imperative}). */
12
+ /** Imperative "Do NOT … / MUST …" sentence for agent system prompts. */
12
13
  imperative?: string;
13
14
  }
15
+ /**
16
+ * A rule that can describe itself.
17
+ *
18
+ * `RuleBuilderLike` declares only `violations()` — deliberately, so a caller can
19
+ * pass a plain object — while every builder in `src/` implements `describeRule()`.
20
+ * So reading a rule's id or remedy needs a narrowing, and that narrowing existed
21
+ * in **three** places with two different implementations: `cli/commands/explain.ts`
22
+ * and two test files.
23
+ *
24
+ * One owner, because a duplicated predicate is not a style problem here:
25
+ * [ts-archunit bug 0044](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0044-an-inline-exclusion-comment-has-no-feedback-channel.md)
26
+ * was a measurement error caused by exactly that, and the fix was to delete the
27
+ * duplicate rather than to test both copies.
28
+ *
29
+ * `DiagnosableRule` and `orphanExclusions` are NOT copies of this: they declare
30
+ * `describeRule?` as an optional structural member of their own input type, which
31
+ * is a different mechanism and correct where it is used.
32
+ */
33
+ export interface Describable {
34
+ describeRule: () => RuleDescription;
35
+ }
36
+ /**
37
+ * Does this value describe itself?
38
+ *
39
+ * Cast-free, and that is the point: the version in `explain.ts` read
40
+ * `(value as Record<string, unknown>)['describeRule']`, an ADR-005 breach in
41
+ * shipped source with no `eslint-disable` and no interop boundary to justify it.
42
+ * It was also unnecessary — once `value` is narrowed to `object`, `'describeRule'
43
+ * in value` narrows enough for the property access to compile, which is what the
44
+ * two test copies already did.
45
+ */
46
+ export declare function isDescribable(value: unknown): value is Describable;
47
+ /**
48
+ * The metadata half of a rule's description — one projection, four callers.
49
+ *
50
+ * `id`, `because`, `suggestion` and `docs` are read off `RuleMetadata` the same
51
+ * way everywhere. `rule` and `imperative` are the two axes that genuinely vary,
52
+ * so they are parameters rather than something this function guesses:
53
+ *
54
+ * - `rule` — the base `TerminalBuilder` says `id ?? 'unnamed'` (a SENTINEL that
55
+ * `dedupe-config-findings.ts` keys on, not an identity), a `RuleBuilder` says
56
+ * the assembled `ruleDescriptionOf(...)` sentence, and several `eess-ts`
57
+ * subclasses override it again on top of `...super.describeRule()`.
58
+ * - `imperativeFallback` — what to say for an agent prompt when the author
59
+ * declared no `imperative`. `eess-ts`'s `RuleDeclaration` falls back to the
60
+ * reason and its `describeRuleOf` to a derived Do-NOT/MUST sentence; the
61
+ * kernel has no fallback, and that is not an oversight this extraction
62
+ * quietly fixed — `imperative` is read only by `eess-ts`'s
63
+ * `explain --format agent`, and `eess-mermaid`'s `explain` does not project
64
+ * the field at all. The kernel has no consumer to be wrong for.
65
+ *
66
+ * The four bodies were otherwise identical, which `no-copy-paste` reported as a
67
+ * 93% cluster.
68
+ */
69
+ export declare function ruleDescriptionFrom(opts: {
70
+ metadata: RuleMetadata | undefined;
71
+ reason: string | undefined;
72
+ rule: string;
73
+ imperativeFallback?: string;
74
+ }): RuleDescription;
75
+ /**
76
+ * A rule's English description, assembled from what it declared.
77
+ *
78
+ * `subject` is the noun the rule opens with when its builder has one — the
79
+ * GraphQL builders say `schema that ... should ...` and
80
+ * `resolvers that ... should ...`; the general builders open with `that`. That
81
+ * one word was the only difference between four copies of this function, which
82
+ * `no-copy-paste` reported as a cluster at 100%.
83
+ *
84
+ * An empty predicate or condition list contributes nothing rather than an empty
85
+ * clause, so a rule that never reached `.should()` reads `that X` and not
86
+ * `that X should ` — the missing half is what `assertionAdvice` then reports.
87
+ *
88
+ * `slice-rule-builder.ts` is deliberately NOT a caller: its description is a
89
+ * different sentence (`slices [a, b] should ...`), not this one with a prefix.
90
+ */
91
+ export declare function ruleDescriptionOf(predicates: readonly {
92
+ description: string;
93
+ }[], conditions: readonly {
94
+ description: string;
95
+ }[], subject?: string): string;
14
96
  //# sourceMappingURL=rule-description.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-description.d.ts","sourceRoot":"","sources":["../src/rule-description.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB"}
1
+ {"version":3,"file":"rule-description.d.ts","sourceRoot":"","sources":["../src/rule-description.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,eAAe,CAAA;CACpC;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAIlE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,QAAQ,EAAE,YAAY,GAAG,SAAS,CAAA;IAClC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,GAAG,eAAe,CASlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,SAAS;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,EAC9C,UAAU,EAAE,SAAS;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,EAC9C,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CAOR"}
@@ -1,2 +1,76 @@
1
- export {};
1
+ /**
2
+ * Does this value describe itself?
3
+ *
4
+ * Cast-free, and that is the point: the version in `explain.ts` read
5
+ * `(value as Record<string, unknown>)['describeRule']`, an ADR-005 breach in
6
+ * shipped source with no `eslint-disable` and no interop boundary to justify it.
7
+ * It was also unnecessary — once `value` is narrowed to `object`, `'describeRule'
8
+ * in value` narrows enough for the property access to compile, which is what the
9
+ * two test copies already did.
10
+ */
11
+ export function isDescribable(value) {
12
+ if (typeof value !== 'object' || value === null)
13
+ return false;
14
+ if (!('describeRule' in value))
15
+ return false;
16
+ return typeof value.describeRule === 'function';
17
+ }
18
+ /**
19
+ * The metadata half of a rule's description — one projection, four callers.
20
+ *
21
+ * `id`, `because`, `suggestion` and `docs` are read off `RuleMetadata` the same
22
+ * way everywhere. `rule` and `imperative` are the two axes that genuinely vary,
23
+ * so they are parameters rather than something this function guesses:
24
+ *
25
+ * - `rule` — the base `TerminalBuilder` says `id ?? 'unnamed'` (a SENTINEL that
26
+ * `dedupe-config-findings.ts` keys on, not an identity), a `RuleBuilder` says
27
+ * the assembled `ruleDescriptionOf(...)` sentence, and several `eess-ts`
28
+ * subclasses override it again on top of `...super.describeRule()`.
29
+ * - `imperativeFallback` — what to say for an agent prompt when the author
30
+ * declared no `imperative`. `eess-ts`'s `RuleDeclaration` falls back to the
31
+ * reason and its `describeRuleOf` to a derived Do-NOT/MUST sentence; the
32
+ * kernel has no fallback, and that is not an oversight this extraction
33
+ * quietly fixed — `imperative` is read only by `eess-ts`'s
34
+ * `explain --format agent`, and `eess-mermaid`'s `explain` does not project
35
+ * the field at all. The kernel has no consumer to be wrong for.
36
+ *
37
+ * The four bodies were otherwise identical, which `no-copy-paste` reported as a
38
+ * 93% cluster.
39
+ */
40
+ export function ruleDescriptionFrom(opts) {
41
+ return {
42
+ rule: opts.rule,
43
+ id: opts.metadata?.id,
44
+ because: opts.reason,
45
+ suggestion: opts.metadata?.suggestion,
46
+ docs: opts.metadata?.docs,
47
+ imperative: opts.metadata?.imperative ?? opts.imperativeFallback,
48
+ };
49
+ }
50
+ /**
51
+ * A rule's English description, assembled from what it declared.
52
+ *
53
+ * `subject` is the noun the rule opens with when its builder has one — the
54
+ * GraphQL builders say `schema that ... should ...` and
55
+ * `resolvers that ... should ...`; the general builders open with `that`. That
56
+ * one word was the only difference between four copies of this function, which
57
+ * `no-copy-paste` reported as a cluster at 100%.
58
+ *
59
+ * An empty predicate or condition list contributes nothing rather than an empty
60
+ * clause, so a rule that never reached `.should()` reads `that X` and not
61
+ * `that X should ` — the missing half is what `assertionAdvice` then reports.
62
+ *
63
+ * `slice-rule-builder.ts` is deliberately NOT a caller: its description is a
64
+ * different sentence (`slices [a, b] should ...`), not this one with a prefix.
65
+ */
66
+ export function ruleDescriptionOf(predicates, conditions, subject) {
67
+ const predicateDesc = predicates.map((p) => p.description).join(' and ');
68
+ const conditionDesc = conditions.map((c) => c.description).join(' and ');
69
+ const parts = subject === undefined ? [] : [subject];
70
+ if (predicateDesc)
71
+ parts.push(`that ${predicateDesc}`);
72
+ if (conditionDesc)
73
+ parts.push(`should ${conditionDesc}`);
74
+ return parts.join(' ');
75
+ }
2
76
  //# sourceMappingURL=rule-description.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-description.js","sourceRoot":"","sources":["../src/rule-description.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"rule-description.js","sourceRoot":"","sources":["../src/rule-description.ts"],"names":[],"mappings":"AAsCA;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC7D,IAAI,CAAC,CAAC,cAAc,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC5C,OAAO,OAAO,KAAK,CAAC,YAAY,KAAK,UAAU,CAAA;AACjD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAKnC;IACC,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE;QACrB,OAAO,EAAE,IAAI,CAAC,MAAM;QACpB,UAAU,EAAE,IAAI,CAAC,QAAQ,EAAE,UAAU;QACrC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI;QACzB,UAAU,EAAE,IAAI,CAAC,QAAQ,EAAE,UAAU,IAAI,IAAI,CAAC,kBAAkB;KACjE,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAA8C,EAC9C,UAA8C,EAC9C,OAAgB;IAEhB,MAAM,aAAa,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACxE,MAAM,aAAa,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACxE,MAAM,KAAK,GAAa,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;IAC9D,IAAI,aAAa;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,aAAa,EAAE,CAAC,CAAA;IACtD,IAAI,aAAa;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,aAAa,EAAE,CAAC,CAAA;IACxD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACxB,CAAC"}
@@ -14,10 +14,27 @@ export interface RuleMetadata {
14
14
  /** Link to documentation — ADR, wiki, style guide */
15
15
  docs?: string;
16
16
  /**
17
- * One-line imperative statement of the rule for an AI-agent system prompt,
18
- * e.g. `'Do NOT call eval()'`. Surfaced by `explain --format agent`; falls
19
- * back to the rule description when unset.
17
+ * Imperative one-liner for AI-agent system prompts, e.g.
18
+ * "Do NOT throw new Error() in repositories". Surfaced by
19
+ * `explain --format agent`. When unset, a heuristic is derived from the
20
+ * rule's predicate/condition description.
20
21
  */
21
22
  imperative?: string;
23
+ /**
24
+ * How the AUTHOR of this rule spells "I declare this empty" — plan 0099.
25
+ *
26
+ * Set by whoever constructs the rule, because only they know. A preset sets
27
+ * `expectEmpty: ['<id>']` because that is what its user can type; a builder
28
+ * author leaves it unset and gets `.expectEmpty()`.
29
+ *
30
+ * Core used to derive this from the rule id starting with `preset/`, which it
31
+ * cannot verify: that is false for a hand-written `.rule({ id: 'preset/...' })`,
32
+ * for a third-party preset that never extended `PresetBaseOptions`, and for a
33
+ * preset that forwards `overrides` but not `expectEmpty`. A prefix is a naming
34
+ * convention, not a capability, and the remedy has to name a call the reader
35
+ * can actually make ([ADR-009](../../../adr/009-agent-first-failure-surfaces.md)
36
+ * rule 2).
37
+ */
38
+ declarationSpelling?: string;
22
39
  }
23
40
  //# sourceMappingURL=rule-metadata.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-metadata.d.ts","sourceRoot":"","sources":["../src/rule-metadata.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,EAAE,CAAC,EAAE,MAAM,CAAA;IAEX,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB"}
1
+ {"version":3,"file":"rule-metadata.d.ts","sourceRoot":"","sources":["../src/rule-metadata.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,EAAE,CAAC,EAAE,MAAM,CAAA;IAEX,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Memoize a builder's materialized selection — plan 0096.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Plan 0096 makes every family expose the set its conditions receive, so that
7
+ * `diagnose()`'s preview and (in plan 0098) the floor read the **same
8
+ * computation** the gate reads rather than a parallel one. Two readers of one
9
+ * method means the walk would run twice per rule — and `doctor` calls
10
+ * `diagnose()` once per rule file, so on a 600-file project a rule file with ten
11
+ * smell rules paid seconds to compute ten integers.
12
+ *
13
+ * ## Why a WeakMap and not an instance field
14
+ *
15
+ * Because a field would be **wrong**, not merely different. These builders are
16
+ * copy-on-write: every chain method returns `this.copy()`, which is
17
+ * `shallowClone` — `Object.assign(clone, source)` over own enumerable
18
+ * properties. A memo stored on the instance is therefore copied onto a builder
19
+ * that has just been given a **different predicate**, and the clone answers with
20
+ * its parent's selection. That is a stale-evidence bug of exactly the kind this
21
+ * plan exists to prevent, and it would have been hard to see: the number is
22
+ * plausible, and only a test that narrows *after* materializing would catch it.
23
+ *
24
+ * A `WeakMap` keyed on the builder has no such hazard by construction. A clone
25
+ * is a different object, so a different key, so no entry. Nothing to clear, no
26
+ * `copy()` override to remember in five families, and the entry is collected
27
+ * with the builder.
28
+ *
29
+ * ## Why a factory rather than one shared map
30
+ *
31
+ * So the element type survives without an assertion. One `WeakMap<object,
32
+ * unknown[]>` shared by every family would need a cast on the way out, which
33
+ * [ADR-005](../../../adr/005-no-any-no-type-assertions.md) forbids — and the
34
+ * honest alternatives (a `filter` with an always-true type predicate) are worse:
35
+ * they compile, allocate, and lie.
36
+ *
37
+ * @example
38
+ * const selectionOf = selectionMemo<ArchFunction>()
39
+ *
40
+ * private selected(): ArchFunction[] {
41
+ * return selectionOf(this, () => matchingElements(this.getElements(), this._predicates))
42
+ * }
43
+ */
44
+ export declare function selectionMemo<T>(): (owner: object, compute: () => T[]) => T[];
45
+ //# sourceMappingURL=selection-memo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-memo.d.ts","sourceRoot":"","sources":["../src/selection-memo.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,aAAa,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,CAmB7E"}
@@ -0,0 +1,66 @@
1
+ import { registerCacheReset } from './cache-registry.js';
2
+ /**
3
+ * Memoize a builder's materialized selection — plan 0096.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Plan 0096 makes every family expose the set its conditions receive, so that
8
+ * `diagnose()`'s preview and (in plan 0098) the floor read the **same
9
+ * computation** the gate reads rather than a parallel one. Two readers of one
10
+ * method means the walk would run twice per rule — and `doctor` calls
11
+ * `diagnose()` once per rule file, so on a 600-file project a rule file with ten
12
+ * smell rules paid seconds to compute ten integers.
13
+ *
14
+ * ## Why a WeakMap and not an instance field
15
+ *
16
+ * Because a field would be **wrong**, not merely different. These builders are
17
+ * copy-on-write: every chain method returns `this.copy()`, which is
18
+ * `shallowClone` — `Object.assign(clone, source)` over own enumerable
19
+ * properties. A memo stored on the instance is therefore copied onto a builder
20
+ * that has just been given a **different predicate**, and the clone answers with
21
+ * its parent's selection. That is a stale-evidence bug of exactly the kind this
22
+ * plan exists to prevent, and it would have been hard to see: the number is
23
+ * plausible, and only a test that narrows *after* materializing would catch it.
24
+ *
25
+ * A `WeakMap` keyed on the builder has no such hazard by construction. A clone
26
+ * is a different object, so a different key, so no entry. Nothing to clear, no
27
+ * `copy()` override to remember in five families, and the entry is collected
28
+ * with the builder.
29
+ *
30
+ * ## Why a factory rather than one shared map
31
+ *
32
+ * So the element type survives without an assertion. One `WeakMap<object,
33
+ * unknown[]>` shared by every family would need a cast on the way out, which
34
+ * [ADR-005](../../../adr/005-no-any-no-type-assertions.md) forbids — and the
35
+ * honest alternatives (a `filter` with an always-true type predicate) are worse:
36
+ * they compile, allocate, and lie.
37
+ *
38
+ * @example
39
+ * const selectionOf = selectionMemo<ArchFunction>()
40
+ *
41
+ * private selected(): ArchFunction[] {
42
+ * return selectionOf(this, () => matchingElements(this.getElements(), this._predicates))
43
+ * }
44
+ */
45
+ export function selectionMemo() {
46
+ let cache = new WeakMap();
47
+ // Same staleness profile as `element-cache.ts`, so the same escape hatch — a
48
+ // consumer holding a builder across a mutation of the underlying ts-morph
49
+ // project gets the pre-mutation selection back, and identity has not changed
50
+ // so nothing else can notice. That is worse here than in the element cache:
51
+ // BOTH readers go through this memo, so a stale entry is a stale VERDICT and
52
+ // a stale count that agree with each other. Reasoning about the clone hazard
53
+ // and stopping one short of this one would read as "considered and safe".
54
+ registerCacheReset(() => {
55
+ cache = new WeakMap();
56
+ });
57
+ return (owner, compute) => {
58
+ const cached = cache.get(owner);
59
+ if (cached !== undefined)
60
+ return cached;
61
+ const computed = compute();
62
+ cache.set(owner, computed);
63
+ return computed;
64
+ };
65
+ }
66
+ //# sourceMappingURL=selection-memo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-memo.js","sourceRoot":"","sources":["../src/selection-memo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,KAAK,GAAG,IAAI,OAAO,EAAe,CAAA;IACtC,6EAA6E;IAC7E,0EAA0E;IAC1E,6EAA6E;IAC7E,4EAA4E;IAC5E,6EAA6E;IAC7E,6EAA6E;IAC7E,0EAA0E;IAC1E,kBAAkB,CAAC,GAAG,EAAE;QACtB,KAAK,GAAG,IAAI,OAAO,EAAe,CAAA;IACpC,CAAC,CAAC,CAAA;IACF,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QACxB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAC/B,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAA;QACvC,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAA;QAC1B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;QAC1B,OAAO,QAAQ,CAAA;IACjB,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A same-prototype shallow copy of a builder.
3
+ *
4
+ * The one place in the codebase that reaches for `Object.create` /
5
+ * `Object.getPrototypeOf`, which are the JS-interop boundary ADR-005's
6
+ * `eslint-disable` carve-out exists for. Every copy-on-write builder goes
7
+ * through here so that carve-out is written once and reviewed once.
8
+ *
9
+ * Shallow is deliberate. It gives the clone its own *slots*, which is what
10
+ * makes `next._x = ...` safe; a field holding a mutable container still needs
11
+ * the owning class to replace it, which is what the `copy()` overrides do.
12
+ * A deep clone would instead copy ts-morph `Project` graphs — the opposite of
13
+ * cheap, and it would break identity comparisons on `SourceFile`.
14
+ *
15
+ * Class fields declared with a constructor parameter property (`private
16
+ * readonly project`) are ordinary instance properties at runtime, so they come
17
+ * across too: a clone is built against the same project, without re-loading it.
18
+ */
19
+ export declare function shallowClone<T extends object>(source: T): T;
20
+ //# sourceMappingURL=shallow-clone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shallow-clone.d.ts","sourceRoot":"","sources":["../src/shallow-clone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAO3D"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A same-prototype shallow copy of a builder.
3
+ *
4
+ * The one place in the codebase that reaches for `Object.create` /
5
+ * `Object.getPrototypeOf`, which are the JS-interop boundary ADR-005's
6
+ * `eslint-disable` carve-out exists for. Every copy-on-write builder goes
7
+ * through here so that carve-out is written once and reviewed once.
8
+ *
9
+ * Shallow is deliberate. It gives the clone its own *slots*, which is what
10
+ * makes `next._x = ...` safe; a field holding a mutable container still needs
11
+ * the owning class to replace it, which is what the `copy()` overrides do.
12
+ * A deep clone would instead copy ts-morph `Project` graphs — the opposite of
13
+ * cheap, and it would break identity comparisons on `SourceFile`.
14
+ *
15
+ * Class fields declared with a constructor parameter property (`private
16
+ * readonly project`) are ordinary instance properties at runtime, so they come
17
+ * across too: a clone is built against the same project, without re-loading it.
18
+ */
19
+ export function shallowClone(source) {
20
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Object.getPrototypeOf returns `any`
21
+ const proto = Object.getPrototypeOf(source);
22
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Object.create returns `any`
23
+ const clone = Object.create(proto);
24
+ Object.assign(clone, source);
25
+ return clone;
26
+ }
27
+ //# sourceMappingURL=shallow-clone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shallow-clone.js","sourceRoot":"","sources":["../src/shallow-clone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,YAAY,CAAmB,MAAS;IACtD,0GAA0G;IAC1G,MAAM,KAAK,GAAW,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;IACnD,kGAAkG;IAClG,MAAM,KAAK,GAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACrC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAC5B,OAAO,KAAK,CAAA;AACd,CAAC"}
@@ -34,5 +34,17 @@ export declare function silent(pattern: string | RegExp): SilentExclusion;
34
34
  * Check whether a value is a SilentExclusion.
35
35
  */
36
36
  export declare function isSilent(value: unknown): value is SilentExclusion;
37
+ /**
38
+ * Record `patterns` into a builder's exclusion state, tracking which are silent.
39
+ *
40
+ * A free function over the two collections rather than a method, because the two
41
+ * builders that need it sit in incompatible class hierarchies — `eess-ts` has
42
+ * `RuleDeclaration -> TerminalBuilder -> RuleBuilder` where the kernel has two
43
+ * levels — so inheritance cannot share it. It mutates only what it is handed,
44
+ * so no private state crosses a package boundary.
45
+ *
46
+ * The two copies were byte-identical (`no-copy-paste` reported 100%).
47
+ */
48
+ export declare function recordExclusions(patterns: readonly (string | RegExp | SilentExclusion)[], exclusions: (string | RegExp)[], silentIndices: Set<number>): void;
37
49
  export {};
38
50
  //# sourceMappingURL=silent-exclusion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"silent-exclusion.d.ts","sourceRoot":"","sources":["../src/silent-exclusion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,QAAA,MAAM,MAAM,EAAE,OAAO,MAAmC,CAAA;AAExD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;IACjC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,CAAA;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,eAAe,CAEhE;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAEjE"}
1
+ {"version":3,"file":"silent-exclusion.d.ts","sourceRoot":"","sources":["../src/silent-exclusion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,QAAA,MAAM,MAAM,EAAE,OAAO,MAAmC,CAAA;AAExD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;IACjC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,CAAA;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,eAAe,CAEhE;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAEjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,GAAG,eAAe,CAAC,EAAE,EACxD,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAC/B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GACzB,IAAI,CASN"}
@@ -27,4 +27,26 @@ export function silent(pattern) {
27
27
  export function isSilent(value) {
28
28
  return typeof value === 'object' && value !== null && SILENT in value;
29
29
  }
30
+ /**
31
+ * Record `patterns` into a builder's exclusion state, tracking which are silent.
32
+ *
33
+ * A free function over the two collections rather than a method, because the two
34
+ * builders that need it sit in incompatible class hierarchies — `eess-ts` has
35
+ * `RuleDeclaration -> TerminalBuilder -> RuleBuilder` where the kernel has two
36
+ * levels — so inheritance cannot share it. It mutates only what it is handed,
37
+ * so no private state crosses a package boundary.
38
+ *
39
+ * The two copies were byte-identical (`no-copy-paste` reported 100%).
40
+ */
41
+ export function recordExclusions(patterns, exclusions, silentIndices) {
42
+ for (const p of patterns) {
43
+ if (isSilent(p)) {
44
+ exclusions.push(p.pattern);
45
+ silentIndices.add(exclusions.length - 1);
46
+ }
47
+ else {
48
+ exclusions.push(p);
49
+ }
50
+ }
51
+ }
30
52
  //# sourceMappingURL=silent-exclusion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"silent-exclusion.js","sourceRoot":"","sources":["../src/silent-exclusion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,GAAkB,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAcxD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,MAAM,CAAC,OAAwB;IAC7C,OAAO,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAA;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,CAAA;AACvE,CAAC"}
1
+ {"version":3,"file":"silent-exclusion.js","sourceRoot":"","sources":["../src/silent-exclusion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,GAAkB,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAcxD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,MAAM,CAAC,OAAwB;IAC7C,OAAO,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAA;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,CAAA;AACvE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAwD,EACxD,UAA+B,EAC/B,aAA0B;IAE1B,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YAChB,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;YAC1B,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAC1C,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACpB,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Write one message to stderr so it survives a test runner.
3
+ *
4
+ * A trailing newline is added when absent, because the call sites this replaced
5
+ * used `console.warn`, which appends one — omitting it at ten call sites is a
6
+ * mistake waiting to happen, and a message run onto the next is the defect this
7
+ * channel exists to avoid.
8
+ */
9
+ export declare function writeStderr(message: string): void;
10
+ /**
11
+ * Reset the lazily-attached listener. **Tests only** — a suite that asserts on
12
+ * the attachment needs to observe it happening rather than inherit it from
13
+ * whichever earlier test wrote first.
14
+ */
15
+ export declare function resetStderrGuardForTests(): void;
16
+ //# sourceMappingURL=stderr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stderr.d.ts","sourceRoot":"","sources":["../src/stderr.ts"],"names":[],"mappings":"AA8CA;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAQjD;AAED;;;;GAIG;AAEH,wBAAgB,wBAAwB,IAAI,IAAI,CAG/C"}
package/dist/stderr.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The library's single stderr channel.
3
+ *
4
+ * [ts-archunit Bug 0024](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0024-warn-terminal-is-invisible-inside-a-test-runner.md):
5
+ * every library-originated message went through `console.warn`, and vitest's
6
+ * default reporter **intercepts** console output and replays it only for
7
+ * **failing** tests. `.warn()` never fails a test, so its output was always
8
+ * dropped. Measured on a real child `vitest run` with the default reporter: a
9
+ * passing test whose rule has 4 real violations printed **nothing** — zero
10
+ * occurrences of the violation text.
11
+ *
12
+ * That silence was not confined to `.warn()`. The stale-exclusion warning, the
13
+ * unused-exclusion warning, the exclusion-comment parse warnings, the
14
+ * invalid-baseline warning, the diff-aware fallback warning and `expression()`'s
15
+ * escape-hatch warning were all invisible in a passing test — five files' worth.
16
+ * A finding nobody reads has not been reported (ADR-009 rule 1).
17
+ *
18
+ * **The EPIPE guard is not optional, and it is why this is a function rather
19
+ * than a bare write.** Node's `Console` is constructed with `ignoreErrors: true`
20
+ * and swallows write errors; `process.stderr.write` does not. With a closed
21
+ * downstream pipe — `eess-ts check 2>&1 | head` — the error arrives
22
+ * **asynchronously**, so neither `try`/`catch` nor the write callback can see
23
+ * it, and the process dies with an uncaught EPIPE. Measured over 20 000 lines:
24
+ *
25
+ * bare process.stderr.write node exits 1
26
+ * with an 'error' listener attached node exits 0
27
+ * console.warn node exits 0
28
+ *
29
+ * `writeReport` already wrote to `process.stderr` unguarded, so that exit-1 was
30
+ * a live defect before this existed: a piped `check` could fail for EPIPE rather
31
+ * than for findings, and the two are indistinguishable from the exit code.
32
+ *
33
+ * A persistent listener rather than `once`: `once` removes itself after the
34
+ * first error, leaving a second EPIPE uncaught, and re-adding per write leaks
35
+ * listeners. One listener, attached lazily, never removed — the same trade
36
+ * `ignoreErrors: true` makes, and the reason it is documented here rather than
37
+ * hidden.
38
+ *
39
+ * **The accepted cost:** vitest annotates intercepted console output with the
40
+ * test that produced it (`stderr | file > test name`), and a direct write loses
41
+ * that. For a violation report the rule's own identity is in the message, so the
42
+ * loss is real but small — and being attributed to a test that never printed is
43
+ * worse than being unattributed.
44
+ */
45
+ let listenerAttached = false;
46
+ /**
47
+ * Write one message to stderr so it survives a test runner.
48
+ *
49
+ * A trailing newline is added when absent, because the call sites this replaced
50
+ * used `console.warn`, which appends one — omitting it at ten call sites is a
51
+ * mistake waiting to happen, and a message run onto the next is the defect this
52
+ * channel exists to avoid.
53
+ */
54
+ export function writeStderr(message) {
55
+ if (!listenerAttached) {
56
+ // See the module docstring: the EPIPE is asynchronous, so this listener is
57
+ // the only thing that can catch it.
58
+ process.stderr.on('error', () => { });
59
+ listenerAttached = true;
60
+ }
61
+ process.stderr.write(message.endsWith('\n') ? message : `${message}\n`);
62
+ }
63
+ /**
64
+ * Reset the lazily-attached listener. **Tests only** — a suite that asserts on
65
+ * the attachment needs to observe it happening rather than inherit it from
66
+ * whichever earlier test wrote first.
67
+ */
68
+ // eess-exclude eess/no-unused-exports: re-exported from `src/index.ts`; this gate does not count a barrel `export … from` re-export as usage — see work/bugs/0168
69
+ export function resetStderrGuardForTests() {
70
+ process.stderr.removeAllListeners('error');
71
+ listenerAttached = false;
72
+ }
73
+ //# sourceMappingURL=stderr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stderr.js","sourceRoot":"","sources":["../src/stderr.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,IAAI,gBAAgB,GAAG,KAAK,CAAA;AAE5B;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,2EAA2E;QAC3E,oCAAoC;QACpC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACpC,gBAAgB,GAAG,IAAI,CAAA;IACzB,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,CAAA;AACzE,CAAC;AAED;;;;GAIG;AACH,kKAAkK;AAClK,MAAM,UAAU,wBAAwB;IACtC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC1C,gBAAgB,GAAG,KAAK,CAAA;AAC1B,CAAC"}