@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
@@ -1,5 +1,39 @@
1
- import { isSilent } from './silent-exclusion.js';
2
- import { executeCheck, executeWarn, applyFilters } from './execute-rule.js';
1
+ import { assertionAdviceFor } from './assertion-advice.js';
2
+ import { countDeclaredGlobs, stampGlobs } from './glob-site.js';
3
+ import { TerminalBuilder, assertionLessViolation } from './terminal-builder.js';
4
+ import { assertsCardinality as conditionAssertsCardinality } from './cardinality.js';
5
+ /**
6
+ * A declared glob's own label in a dead-glob finding — the predicate/
7
+ * condition's description, disambiguated with the literal glob only when
8
+ * more than one site shares the description (a variadic predicate like
9
+ * `importFrom(...globs)` already spells every glob into its own
10
+ * description, so a substring test would collapse the one case this exists
11
+ * to separate — keyed on the site COUNT instead).
12
+ */
13
+ function describeOrigin(description, glob, siteCount) {
14
+ return siteCount > 1 ? `${description} ("${glob.glob}")` : description;
15
+ }
16
+ /**
17
+ * The actual walk `RuleBuilder.globs()` delegates to — a free function
18
+ * rather than a method body, to keep the class itself under this repo's own
19
+ * 300-line class-length gate (`arch.internal.rules.ts`).
20
+ */
21
+ function declaredGlobsOf(predicates, conditions) {
22
+ const trees = [];
23
+ for (const predicate of predicates) {
24
+ if (predicate.globs) {
25
+ const count = countDeclaredGlobs(predicate.globs);
26
+ trees.push(stampGlobs(predicate.globs, 'selector', (g) => describeOrigin(predicate.description, g, count)));
27
+ }
28
+ }
29
+ for (const condition of conditions) {
30
+ if (condition.globs) {
31
+ const count = countDeclaredGlobs(condition.globs);
32
+ trees.push(stampGlobs(condition.globs, 'condition', (g) => describeOrigin(condition.description, g, count)));
33
+ }
34
+ }
35
+ return trees;
36
+ }
3
37
  /**
4
38
  * Abstract base class for all rule builders.
5
39
  *
@@ -9,29 +43,46 @@ import { executeCheck, executeWarn, applyFilters } from './execute-rule.js';
9
43
  * 3. Add condition methods that call `addCondition()`
10
44
  *
11
45
  * The builder accumulates predicates and conditions. Nothing executes
12
- * until a terminal method (`.check()`, `.warn()`, `.severity()`) is called.
46
+ * until a terminal method (`.check()`, `.warn()`, `.severity()`) is called —
47
+ * inherited from {@link TerminalBuilder} (plan 0088 Phase 4), which also
48
+ * supplies `because`/`rule`/`excluding`/`violations`/`expectEmpty`/
49
+ * `expectNonEmpty` and the ADR-010 evidence gate. `RuleBuilder` keeps its own
50
+ * `<T, P>` two-param generic — `P` is the dialect's project type, kept
51
+ * separate from `TerminalBuilder` (which has no project concept, since
52
+ * `correspondence()`, `SmellBuilder`, and the graphql builders don't share a
53
+ * uniform notion of "one project") — every builder across all five dialects
54
+ * depends on this exact two-param signature, so it is not replaced by the
55
+ * fold, only extended.
13
56
  */
14
- export class RuleBuilder {
57
+ export class RuleBuilder extends TerminalBuilder {
15
58
  project;
16
59
  _predicates = [];
17
60
  _conditions = [];
18
- _reason;
19
- _metadata;
20
- _exclusions = [];
21
- _silentIndices = new Set();
22
61
  _phase = 'predicate';
62
+ /** Whether `.should()` was ever reached — bug 0155 state 3/4 vs state 1. */
63
+ _reachedShould = false;
64
+ /**
65
+ * Descriptions of predicate-only methods used AFTER `.should()` — bug 0155
66
+ * state 2. A dual-use method dispatches to a condition in that phase and
67
+ * never lands here, so anything recorded is genuinely a filter written where
68
+ * an assertion was meant.
69
+ */
70
+ _misplaced = [];
23
71
  constructor(project) {
72
+ super();
24
73
  this.project = project;
25
74
  }
26
75
  // --- Chain methods (grammar transitions) ---
27
76
  /**
28
- * Begin the predicate phase. Returns `this` for chaining.
77
+ * Begin the predicate phase. Returns a COPY — a held selection is never
78
+ * edited by narrowing it (see `TerminalBuilder.copy()`).
29
79
  * Purely a readability marker — `.that().haveNameMatching(...)` reads like English.
30
80
  * Explicitly resets phase to 'predicate' — defensive against `.should().that()` misuse.
31
81
  */
32
82
  that() {
33
- this._phase = 'predicate';
34
- return this;
83
+ const next = this.copy();
84
+ next._phase = 'predicate';
85
+ return next;
35
86
  }
36
87
  /**
37
88
  * Add another predicate (AND). Returns `this` for chaining.
@@ -48,6 +99,7 @@ export class RuleBuilder {
48
99
  should() {
49
100
  const fork = this.fork();
50
101
  fork._phase = 'condition';
102
+ fork._reachedShould = true;
51
103
  return fork;
52
104
  }
53
105
  /**
@@ -72,70 +124,11 @@ export class RuleBuilder {
72
124
  }
73
125
  return this.addCondition(custom);
74
126
  }
75
- /**
76
- * Attach a human-readable rationale to the rule.
77
- * Included in violation messages when `.check()` throws.
78
- */
79
- because(reason) {
80
- this._reason = reason;
81
- return this;
82
- }
83
- /**
84
- * Attach rich metadata to the rule.
85
- * Provides educational context in violation output: why, how to fix, docs link.
86
- *
87
- * If `metadata.because` is set, it also sets the reason (same as `.because()`).
88
- */
89
- rule(metadata) {
90
- this._metadata = metadata;
91
- if (metadata.because) {
92
- this._reason = metadata.because;
93
- }
94
- return this;
95
- }
96
- /**
97
- * Exclude specific violations from reporting by matching against
98
- * the violation's `element`, `file`, or `message` fields.
99
- *
100
- * Matched violations are silently suppressed. Use for permanent,
101
- * intentional exceptions — not for temporary violations (use baseline for those).
102
- *
103
- * Patterns are matched against all three fields. Prefer anchored regexes
104
- * or full string matches over short substrings, especially for `message`
105
- * matching, to avoid accidentally suppressing unrelated violations whose
106
- * messages happen to contain the same text.
107
- *
108
- * Emits a warning if an exclusion matches zero violations — so renamed
109
- * or deleted exceptions don't silently stay in the rule.
110
- *
111
- * For narrowing a rule's scope at the predicate phase (so the rule never
112
- * evaluates the excluded element), use `satisfy(not(<predicate>))` instead.
113
- * See the "Excluding a file from a rule's scope" recipe in docs/recipes.md.
114
- *
115
- * @example
116
- * // Exclude by element name (fully qualified)
117
- * .excluding('Asset.getImageUrl')
118
- *
119
- * @example
120
- * // Exclude by file path (regex anchored to suffix)
121
- * .excluding(/repositories\/index\.ts$/)
122
- *
123
- * @example
124
- * // Multiple exclusions, mixed forms
125
- * .excluding('Asset.getImageUrl', /\/legacy\//, /generated/)
126
- */
127
- excluding(...patterns) {
128
- for (const p of patterns) {
129
- if (isSilent(p)) {
130
- this._exclusions.push(p.pattern);
131
- this._silentIndices.add(this._exclusions.length - 1);
132
- }
133
- else {
134
- this._exclusions.push(p);
135
- }
136
- }
137
- return this;
138
- }
127
+ // `because`, `rule`, `excluding`, `violations`, `check`, `warn`, `severity`,
128
+ // `expectEmpty`, `expectNonEmpty` are inherited from `TerminalBuilder`
129
+ // (plan 0088 Phase 4) — this class used to duplicate all of them, and the
130
+ // two copies had already drifted (the exact hazard `ts-archunit`'s own
131
+ // `RuleBuilder`/`TerminalBuilder` merge commit documents).
139
132
  // --- Terminal methods ---
140
133
  /**
141
134
  * Return the predicate-filtered elements as a labelled `Selection<T>` for
@@ -164,96 +157,133 @@ export class RuleBuilder {
164
157
  };
165
158
  }
166
159
  /**
167
- * Execute the rule and return violations after exclusion filtering.
168
- * Does not throw — use for programmatic access (presets, aggregation).
169
- */
170
- violations() {
171
- const raw = this.evaluate();
172
- return applyFilters(raw, {
173
- reason: this._reason,
174
- metadata: this._metadata,
175
- exclusions: this._exclusions,
176
- silentIndices: this._silentIndices,
177
- });
178
- }
179
- /**
180
- * Execute the rule and throw `ArchRuleError` if any violations are found.
181
- * This is the primary terminal method — use in test assertions.
160
+ * ADR-010: is this rule's assertion satisfied by an empty selection, as a
161
+ * property of what its conditions assert (`.notExist()`) rather than a
162
+ * caller's `.expectEmpty()` declaration?
163
+ *
164
+ * `.every()`, not `.some()` — `.andShould()` ANDs, so a rule reading
165
+ * `.should().notExist().andShould().beExported()` still asserts something
166
+ * about subjects that exist, and exempting it on the strength of the one
167
+ * cardinality condition would silence the other. An empty condition list is
168
+ * NOT exempt: `[].every()` is vacuously `true`, and a rule with zero
169
+ * conditions is the assertion-less case `collectViolations()` now reports as
170
+ * a configuration finding (bug 0155; it used to be an unreachable stderr
171
+ * warning, which this comment named until that fix landed) — this override
172
+ * must not paper over it by also declaring it cardinality-satisfied.
182
173
  *
183
- * @param options - Optional baseline and diff filtering
174
+ * **Load-bearing:** for a DEAD selector this early return is the only thing
175
+ * between an assertion-less rule and a silent pass (the gate runs after the
176
+ * zero-examined branch). Pinned in `assertion-less-rules.test.ts`.
184
177
  */
185
- check(options) {
186
- const violations = this.evaluate();
187
- executeCheck(violations, {
188
- reason: this._reason,
189
- metadata: this._metadata,
190
- exclusions: this._exclusions,
191
- silentIndices: this._silentIndices,
192
- }, options);
193
- }
194
178
  /**
195
- * Execute the rule and log violations to stderr. Does not throw.
196
- * Use for rules that should warn but not fail CI.
179
+ * Does this rule assert anything at all? — bug 0155.
197
180
  *
198
- * @param options - Optional baseline and diff filtering
181
+ * Conditions alone are not enough: a predicate-only method written *after*
182
+ * `.should()` silently shrank the set the conditions run over, possibly to
183
+ * empty, in which case they hold vacuously. That rule asserts nothing
184
+ * meaningful even though `_conditions` is non-empty.
199
185
  */
200
- warn(options) {
201
- const violations = this.evaluate();
202
- executeWarn(violations, {
203
- reason: this._reason,
204
- metadata: this._metadata,
205
- exclusions: this._exclusions,
206
- silentIndices: this._silentIndices,
207
- }, options);
186
+ assertsSomething() {
187
+ return this._conditions.length > 0 && this._misplaced.length === 0;
188
+ }
189
+ assertionAdvice() {
190
+ return assertionAdviceFor({
191
+ reachedShould: this._reachedShould,
192
+ misplaced: this._misplaced,
193
+ conditionCount: this._conditions.length,
194
+ });
195
+ }
196
+ assertsCardinality() {
197
+ if (this._conditions.length === 0)
198
+ return false;
199
+ return this._conditions.every((condition) => conditionAssertsCardinality(condition));
208
200
  }
209
201
  /**
210
- * Execute the rule with the given severity.
211
- * `.severity('error')` is equivalent to `.check()`.
212
- * `.severity('warn')` is equivalent to `.warn()`.
202
+ * ADR-010 part 3: did this rule's own underlying source (the project, a
203
+ * diagram, an SDL string — whatever `P` loads) produce nothing at all,
204
+ * before `getElements()`'s own domain-specific extraction or any
205
+ * predicate ran? Default `false` — the kernel has no way to know, since
206
+ * `P` is opaque here. A dialect's concrete builder, which knows `P`'s real
207
+ * shape (e.g. `ArchProject`), overrides this to check its own project's
208
+ * file count directly — never `getElements().length`, which conflates "the
209
+ * source is empty" with "this domain legitimately has nothing in a
210
+ * healthy project" (e.g. zero JSX elements in a backend-only codebase).
213
211
  */
214
- severity(level) {
215
- if (level === 'error') {
216
- this.check();
217
- }
218
- else {
219
- this.warn();
220
- }
212
+ sourceEmpty() {
213
+ return false;
214
+ }
215
+ /** Declared globs, position derived from `_phase`. Pure — no `P` needed. */
216
+ globs() {
217
+ return declaredGlobsOf(this._predicates, this._conditions);
218
+ }
219
+ /** This rule's own project, for `diagnose()`/`doctor` outside `.check()`. */
220
+ getProject() {
221
+ return this.project;
222
+ }
223
+ /** A `globs()` tree diagnosably dead, when zero examined — same shape as `sourceEmpty()`. */
224
+ deadGlobDiagnosis() {
225
+ return undefined;
221
226
  }
222
227
  // --- Protected: for subclasses ---
223
228
  /**
224
229
  * Register a predicate. Called by concrete builder methods like
225
- * `.haveNameMatching()`, `.extend()`, etc.
230
+ * `.haveNameMatching()`, `.extend()`, etc. Returns a COPY — see `copy()`.
226
231
  */
227
232
  addPredicate(predicate) {
228
- this._predicates.push(predicate);
229
- return this;
233
+ const next = this.copy();
234
+ next._predicates.push(predicate);
235
+ // Bug 0155 state 2: a predicate-only method used after `.should()`. Dual-use
236
+ // methods dispatch to conditions in that phase and never reach here, so this
237
+ // is a filter written where an assertion was meant — the one state whose fix
238
+ // is "move it before .should()", not "add a condition".
239
+ if (next._phase === 'condition')
240
+ next._misplaced.push(predicate.description);
241
+ return next;
230
242
  }
231
243
  /**
232
244
  * Register a condition. Called by concrete builder methods like
233
- * `.notContain()`, `.notExist()`, etc.
245
+ * `.notContain()`, `.notExist()`, etc. Returns a COPY — see `copy()`.
234
246
  */
235
247
  addCondition(condition) {
236
- this._conditions.push(condition);
237
- return this;
248
+ const next = this.copy();
249
+ next._conditions.push(condition);
250
+ return next;
238
251
  }
239
252
  /**
240
- * Create a fork of this builder with the same predicates but empty conditions.
241
- * Used by `.should()` to support named selections without mutation.
253
+ * An independent copy, carrying **both** predicates and conditions.
254
+ * Extends `TerminalBuilder.copy()` with the fields this class adds.
242
255
  *
243
- * Subclasses with additional constructor args MUST override this method.
256
+ * Subclasses with additional mutable-reference fields of their own (arrays,
257
+ * sets, maps a chain method pushes onto) MUST override this and call
258
+ * `super.copy()` first, the same reason `TerminalBuilder.copy()`'s own
259
+ * docstring gives — `Object.assign` shallow-copies a field, it does not
260
+ * give it independent identity.
261
+ */
262
+ copy() {
263
+ const clone = super.copy();
264
+ clone._predicates = [...this._predicates];
265
+ clone._conditions = [...this._conditions];
266
+ clone._misplaced = [...this._misplaced];
267
+ return clone;
268
+ }
269
+ /**
270
+ * A fork of this builder, carrying BOTH lists. Used by `.should()` to support
271
+ * named selections without mutation.
272
+ *
273
+ * **Nothing here clears the conditions** (bug 0156). It used to, and a second
274
+ * `.should()` therefore silently discarded the first assertion — a rule that
275
+ * asserted something turned into one that asserted less, by a chain method.
276
+ * `.should().X().should().Y()` now accumulates exactly as `.andShould()` does.
277
+ *
278
+ * Ported from `packages/ts/src/core/rule-builder.ts`, where the engine copy
279
+ * landed the fix and left the kernel behind. Found by the architect review of
280
+ * PR #72: `eess-md`, `eess-mermaid` and `eess-gherkin` all extend THIS class,
281
+ * so they carried the defect — and `check:corpus`, `check:ledger` and
282
+ * `check:diagram` are md/mermaid gates, meaning this repo's own corpus
283
+ * enforcement ran on the broken copy.
244
284
  */
245
285
  fork() {
246
- // Object.create/getPrototypeOf return untyped — casts unavoidable at JS interop boundary
247
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
248
- const proto = Object.getPrototypeOf(this);
249
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
250
- const fork = Object.create(proto);
251
- Object.assign(fork, this);
252
- fork._predicates = [...this._predicates];
253
- fork._conditions = [];
254
- fork._exclusions = [...this._exclusions];
255
- fork._silentIndices = new Set(this._silentIndices);
256
- fork._metadata = this._metadata ? { ...this._metadata } : undefined;
286
+ const fork = this.copy();
257
287
  fork._reason = fork._metadata?.because ?? this._reason;
258
288
  return fork;
259
289
  }
@@ -272,28 +302,42 @@ export class RuleBuilder {
272
302
  return parts.join(' ');
273
303
  }
274
304
  /**
275
- * Execute the full pipeline: filter elements with predicates,
276
- * evaluate conditions, return violations.
305
+ * Execute the full pipeline: filter elements with predicates, evaluate
306
+ * conditions, return violations plus the ADR-010 evidence — the
307
+ * post-predicate subject count, whatever it is. `TerminalBuilder.check()` /
308
+ * `.warn()` / `.violations()` decide what a zero count means (a
309
+ * configuration finding, unless `.expectEmpty()` was declared); this method
310
+ * only reports the count, never interprets it.
311
+ *
312
+ * `sourceEmpty` (ADR-010 part 3) is deliberately not `allElements.length
313
+ * === 0` — a domain's own `getElements()` legitimately returning nothing in
314
+ * a healthy, fully-loaded project (e.g. zero JSX elements in a
315
+ * backend-only codebase) is not brokenness, and conflating the two broke
316
+ * `.notExist()`'s cardinality exemption for exactly that case. Subclasses
317
+ * that can answer "did my underlying source load anything at all" override
318
+ * `sourceEmpty()`; the default assumes it did.
277
319
  */
278
- evaluate() {
279
- // Step 1: Get all elements from the concrete builder
320
+ collectViolations() {
280
321
  const allElements = this.getElements();
281
- // Step 2: Filter with predicates (AND — all predicates must match)
282
322
  const filtered = allElements.filter((element) => this._predicates.every((predicate) => predicate.test(element)));
283
- // Step 3: If no elements match predicates, no violations
323
+ const examined = filtered.length;
284
324
  if (filtered.length === 0) {
285
- return [];
325
+ const sourceEmpty = this.sourceEmpty();
326
+ // Skip the diagnosis (a real filesystem walk, in a dialect's
327
+ // override) when the project itself is already known empty — nothing
328
+ // useful to say about one glob's fate against zero loaded files, and
329
+ // `sourceEmpty` outranks it in `evidencedViolations()` regardless.
330
+ const deadGlob = sourceEmpty ? undefined : this.deadGlobDiagnosis();
331
+ return { violations: [], examined, sourceEmpty, deadGlob };
286
332
  }
287
- // Step 3b: Warn if no conditions were added and phase is still 'predicate'
288
- // — likely a predicate-only method was called after .should().
289
- // Phase-aware methods dispatch correctly, so this only fires for predicate-only methods.
290
- if (this._conditions.length === 0 && this._phase === 'predicate') {
333
+ // Bug 0155 — gate-first, before the conditions run. See
334
+ // `assertionLessViolation` for why this is a finding and not a warning,
335
+ // and why there is no `_phase` term.
336
+ // `_expectEmpty === undefined`: a declared emptiness expectation is an
337
+ // assertion too — see `assertionLessViolation`.
338
+ if (!this.assertsSomething() && this._expectEmpty === undefined) {
291
339
  const ruleId = this._metadata?.id ?? (this.buildRuleDescription() || 'unnamed');
292
- console.warn(`[eess] Rule '${ruleId}' has predicates but no conditions. ` +
293
- `Did you use a predicate-only method after .should()? ` +
294
- `Predicate-only methods (e.g. areExported, areAsync) filter elements; ` +
295
- `use a condition method or .satisfy() after .should().`);
296
- return [];
340
+ return { violations: [assertionLessViolation(ruleId, this.assertionAdvice())], examined };
297
341
  }
298
342
  // Step 4: Build context for conditions
299
343
  const context = this.buildConditionContext();
@@ -302,7 +346,7 @@ export class RuleBuilder {
302
346
  for (const condition of this._conditions) {
303
347
  violations.push(...condition.evaluate(filtered, context));
304
348
  }
305
- return violations;
349
+ return { violations, examined };
306
350
  }
307
351
  /**
308
352
  * Build the `ConditionContext` passed to each condition.
@@ -1 +1 @@
1
- {"version":3,"file":"rule-builder.js","sourceRoot":"","sources":["../src/rule-builder.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAG3E;;;;;;;;;;GAUG;AACH,MAAM,OAAgB,WAAW;IASA;IARrB,WAAW,GAAmB,EAAE,CAAA;IAChC,WAAW,GAAmB,EAAE,CAAA;IAChC,OAAO,CAAS;IAChB,SAAS,CAAe;IACxB,WAAW,GAAwB,EAAE,CAAA;IACrC,cAAc,GAAgB,IAAI,GAAG,EAAE,CAAA;IACvC,MAAM,GAA8B,WAAW,CAAA;IAEzD,YAA+B,OAAU;QAAV,YAAO,GAAP,OAAO,CAAG;IAAG,CAAC;IAE7C,8CAA8C;IAE9C;;;;OAIG;IACH,IAAI;QACF,IAAI,CAAC,MAAM,GAAG,WAAW,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,GAAG;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;OAIG;IACH,MAAM;QACJ,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;QACxB,IAAI,CAAC,MAAM,GAAG,WAAW,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,SAAS;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAmC;QACzC,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;QAClC,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,MAAc;QACpB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,QAAsB;QACzB,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAA;QACzB,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAA;QACjC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,SAAS,CAAC,GAAG,QAA+C;QAC1D,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;gBAChC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;YACtD,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YAC1B,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,2BAA2B;IAE3B;;;;;;;OAOG;IACH,MAAM,CAAC,IAA8D;QACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CACrD,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAC/D,CAAA;QACD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAA;IAC3E,CAAC;IAED;;;OAGG;IACH,YAAY;QACV,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,oBAAoB,EAAE;YACjC,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE;YACtB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU;YACtC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI;YAC1B,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU;SACvC,CAAA;IACH,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;QAC3B,OAAO,YAAY,CAAC,GAAG,EAAE;YACvB,MAAM,EAAE,IAAI,CAAC,OAAO;YACpB,QAAQ,EAAE,IAAI,CAAC,SAAS;YACxB,UAAU,EAAE,IAAI,CAAC,WAAW;YAC5B,aAAa,EAAE,IAAI,CAAC,cAAc;SACnC,CAAC,CAAA;IACJ,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAsB;QAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;QAClC,YAAY,CACV,UAAU,EACV;YACE,MAAM,EAAE,IAAI,CAAC,OAAO;YACpB,QAAQ,EAAE,IAAI,CAAC,SAAS;YACxB,UAAU,EAAE,IAAI,CAAC,WAAW;YAC5B,aAAa,EAAE,IAAI,CAAC,cAAc;SACnC,EACD,OAAO,CACR,CAAA;IACH,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,OAAsB;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;QAClC,WAAW,CACT,UAAU,EACV;YACE,MAAM,EAAE,IAAI,CAAC,OAAO;YACpB,QAAQ,EAAE,IAAI,CAAC,SAAS;YACxB,UAAU,EAAE,IAAI,CAAC,WAAW;YAC5B,aAAa,EAAE,IAAI,CAAC,cAAc;SACnC,EACD,OAAO,CACR,CAAA;IACH,CAAC;IAED;;;;OAIG;IACH,QAAQ,CAAC,KAAuB;QAC9B,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;YACtB,IAAI,CAAC,KAAK,EAAE,CAAA;QACd,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,EAAE,CAAA;QACb,CAAC;IACH,CAAC;IAED,oCAAoC;IAEpC;;;OAGG;IACO,YAAY,CAAC,SAAuB;QAC5C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAChC,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACO,YAAY,CAAC,SAAuB;QAC5C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAChC,OAAO,IAAI,CAAA;IACb,CAAC;IAQD;;;;;OAKG;IACO,IAAI;QACZ,yFAAyF;QACzF,mEAAmE;QACnE,MAAM,KAAK,GAAW,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;QACjD,mEAAmE;QACnE,MAAM,IAAI,GAAS,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACzB,IAAI,CAAC,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAA;QACxC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAA;QACrB,IAAI,CAAC,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAA;QACxC,IAAI,CAAC,cAAc,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAClD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QACnE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,IAAI,IAAI,CAAC,OAAO,CAAA;QACtD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,oCAAoC;IAEpC;;OAEG;IACK,oBAAoB;QAC1B,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC9E,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC9E,MAAM,KAAK,GAAa,EAAE,CAAA;QAC1B,IAAI,aAAa;YAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,aAAa,EAAE,CAAC,CAAA;QACtD,IAAI,aAAa;YAAE,KAAK,CAAC,IAAI,CAAC,UAAU,aAAa,EAAE,CAAC,CAAA;QACxD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACxB,CAAC;IAED;;;OAGG;IACK,QAAQ;QACd,qDAAqD;QACrD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;QAEtC,mEAAmE;QACnE,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAC9C,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAC/D,CAAA;QAED,yDAAyD;QACzD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,CAAA;QACX,CAAC;QAED,2EAA2E;QAC3E,+DAA+D;QAC/D,yFAAyF;QACzF,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YACjE,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,IAAI,SAAS,CAAC,CAAA;YAC/E,OAAO,CAAC,IAAI,CACV,gBAAgB,MAAM,sCAAsC;gBAC1D,uDAAuD;gBACvD,uEAAuE;gBACvE,uDAAuD,CAC1D,CAAA;YACD,OAAO,EAAE,CAAA;QACX,CAAC;QAED,uCAAuC;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAA;QAE5C,wDAAwD;QACxD,MAAM,UAAU,GAAoB,EAAE,CAAA;QACtC,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACzC,UAAU,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAA;QAC3D,CAAC;QAED,OAAO,UAAU,CAAA;IACnB,CAAC;IAED;;;;;;OAMG;IACO,qBAAqB;QAC7B,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,oBAAoB,EAAE;YACjC,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE;YAC1B,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU;YACtC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI;SAC3B,CAAA;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"rule-builder.js","sourceRoot":"","sources":["../src/rule-builder.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,39 @@ export interface RuleDescription {
8
8
  because?: string;
9
9
  suggestion?: string;
10
10
  docs?: string;
11
- /** One-line imperative for AI-agent prompts (see {@link RuleMetadata.imperative}). */
11
+ /** Imperative "Do NOT … / MUST …" sentence for agent system prompts. */
12
12
  imperative?: string;
13
13
  }
14
+ /**
15
+ * A rule that can describe itself.
16
+ *
17
+ * `RuleBuilderLike` declares only `violations()` — deliberately, so a caller can
18
+ * pass a plain object — while every builder in `src/` implements `describeRule()`.
19
+ * So reading a rule's id or remedy needs a narrowing, and that narrowing existed
20
+ * in **three** places with two different implementations: `cli/commands/explain.ts`
21
+ * and two test files.
22
+ *
23
+ * One owner, because a duplicated predicate is not a style problem here:
24
+ * [ts-archunit bug 0044](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0044-an-inline-exclusion-comment-has-no-feedback-channel.md)
25
+ * was a measurement error caused by exactly that, and the fix was to delete the
26
+ * duplicate rather than to test both copies.
27
+ *
28
+ * `DiagnosableRule` and `orphanExclusions` are NOT copies of this: they declare
29
+ * `describeRule?` as an optional structural member of their own input type, which
30
+ * is a different mechanism and correct where it is used.
31
+ */
32
+ export interface Describable {
33
+ describeRule: () => RuleDescription;
34
+ }
35
+ /**
36
+ * Does this value describe itself?
37
+ *
38
+ * Cast-free, and that is the point: the version in `explain.ts` read
39
+ * `(value as Record<string, unknown>)['describeRule']`, an ADR-005 breach in
40
+ * shipped source with no `eslint-disable` and no interop boundary to justify it.
41
+ * It was also unnecessary — once `value` is narrowed to `object`, `'describeRule'
42
+ * in value` narrows enough for the property access to compile, which is what the
43
+ * two test copies already did.
44
+ */
45
+ export declare function isDescribable(value: unknown): value is Describable;
14
46
  //# 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;;;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"}
@@ -1,2 +1,18 @@
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
+ }
2
18
  //# 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":"AAoCA;;;;;;;;;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"}
@@ -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"}