@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,28 +1,113 @@
1
+ import { hasEvidence } from './collect-result.js';
2
+ import { commentSuppressions } from './comment-suppression.js';
3
+ export function formatViolationsJson(violations, reason,
1
4
  /**
2
- * Format violations as a JSON string.
5
+ * Allowlist rules that tested no edges (bug 0015).
3
6
  *
4
- * Useful for CI pipelines, custom dashboards, or piping to other tools.
5
- *
6
- * @example
7
- * const violations = collectViolations(rule1, rule2)
8
- * console.log(formatViolationsJson(violations))
7
+ * **Passed in, not read from module state.** The first cut called
8
+ * `untestedRules()` here, which made a documented pure formatter impure: a
9
+ * per-rule JSON document in a vitest suite named rules from every earlier
10
+ * rule in the process, because nothing resets between them. Same input,
11
+ * different output depending on history.
9
12
  */
10
- export function formatViolationsJson(violations, reason) {
13
+ untested = []) {
14
+ const errors = violations.filter((v) => (v.severity ?? 'error') === 'error').length;
11
15
  const output = {
12
16
  summary: {
13
17
  total: violations.length,
18
+ errors,
19
+ warnings: violations.length - errors,
14
20
  reason: reason ?? null,
21
+ /**
22
+ * The denominator, carried by hand — plan 0235.
23
+ *
24
+ * `JSON.stringify` DROPS an array's own properties: an `ArchViolation[]`
25
+ * carrying `examined` serialises as a bare `[…]` and the evidence is
26
+ * simply gone. Measured: `JSON.stringify(Object.assign([1,2],{examined:7}))`
27
+ * is `[1,2]`.
28
+ *
29
+ * Without this line `--format json` would carry violations and no
30
+ * denominator, which forecloses the machine-readable half of
31
+ * [bug 0174](../../../work/bugs/0174-eess-ts-reports-a-clean-gate-with-no-denominator.md)
32
+ * — a green with no way for a consumer to tell a clean run from one that
33
+ * examined nothing. `null` when the caller handed over a bare array, which
34
+ * is itself the honest answer: no evidence was supplied.
35
+ */
36
+ examined: hasEvidence(violations) ? violations.examined : null,
15
37
  },
38
+ // Bug 0015: an allowlist constrains edges, so a subject with none passes
39
+ // whatever the allowlist says. Reported rather than failed — for the `only*`
40
+ // family zero edges is maximal compliance, and only the reader can tell a
41
+ // dependency-free module from a rule that certified nothing. Always present
42
+ // (an empty array, not omitted) so a consumer can distinguish "none" from
43
+ // "this version does not report it".
44
+ untestedAllowlists: untested.map((c) => ({
45
+ rule: c.rule,
46
+ subjects: c.subjects,
47
+ edges: c.edges,
48
+ })),
49
+ // What inline `// eess-exclude` comments removed from this report.
50
+ // Structural rather than prose, for the same reason `untestedAllowlists` is:
51
+ // the agent reading this document needs identities it can act on, not a
52
+ // sentence to grep. Always present — an empty array, not omitted — so a
53
+ // consumer can tell "nothing was suppressed" from "this version does not
54
+ // report it".
55
+ //
56
+ // Every other filter in the pipeline discloses itself; this one did not,
57
+ // which after bug 0041 made it the widest silent filter we ship.
58
+ commentSuppressed: commentSuppressions().map((c) => ({
59
+ ruleId: c.ruleId,
60
+ file: c.file,
61
+ })),
16
62
  violations: violations.map((v) => ({
17
63
  rule: v.rule,
18
64
  ruleId: v.ruleId ?? null,
65
+ severity: v.severity ?? 'error',
19
66
  element: v.element,
20
- file: v.file,
21
- line: v.line,
67
+ // `null`, not `''`/`1`, when there is no source location. A configuration
68
+ // finding reports that a RULE enforces nothing; it has no line, and saying
69
+ // `"file": "", "line": 1` states one that looks real (bug 0047). A human
70
+ // skims past it; an agent may open it or anchor an edit to it.
71
+ //
72
+ // `null` rather than omission, and consistent with the rest of this
73
+ // document — `ruleId`, `because`, `suggestion`, `docs` and `measured` all
74
+ // null when absent, so a consumer already handles the idiom here.
75
+ file: v.file === '' ? null : v.file,
76
+ line: v.file === '' ? null : v.line,
77
+ // What kind of finding this is, which drives what the reader should DO:
78
+ // `'violation'` — the code is wrong, edit the named file;
79
+ // `'configuration'` — the RULE enforces nothing, so editing the code
80
+ // cannot clear it. This distinction was absent from the payload entirely,
81
+ // so an agent could not tell them apart by any field.
82
+ //
83
+ // A string rather than a boolean, decided deliberately. There are twelve
84
+ // configuration-finding producers with materially different causes — an
85
+ // empty selector, a dead glob, a rule with no condition, a preset that
86
+ // discovered nothing — and someone will want to distinguish them. A
87
+ // boolean leaves nowhere to put that; adding a second overlapping field
88
+ // later is worse than one field with room in it. This is the agent
89
+ // contract, so the cost of the wrong shape is permanent.
90
+ //
91
+ // Not named after `bypassFilters`: the consumer cares what the finding
92
+ // IS, not which of our filters it happens to survive.
93
+ kind: v.bypassFilters === true ? 'configuration' : 'violation',
22
94
  message: v.message,
23
95
  because: v.because ?? null,
24
96
  suggestion: v.suggestion ?? null,
25
97
  docs: v.docs ?? null,
98
+ codeFrame: v.codeFrame ?? null,
99
+ // Bug 0012's measurement, on the wire. Without it the ratchet is
100
+ // machine-readable only inside the baseline file, and a dashboard wanting
101
+ // the number has to regex it back out of the message — which is the
102
+ // fragility bug 0012 was filed about.
103
+ measured: v.measured ?? null,
104
+ // Bug 0239. A finding about a relationship concerns several files and is
105
+ // reported at one of them; a consumer that filters by `file` needs the
106
+ // rest or it reproduces the very bug this field was added to fix. This is
107
+ // the documented agent-actionable stream, so omitting it would make the
108
+ // changeset's own advice — "read `relatedFiles` too" — impossible to
109
+ // follow through the interface CLAUDE.md points agents at.
110
+ relatedFiles: v.relatedFiles ?? null,
26
111
  })),
27
112
  };
28
113
  return JSON.stringify(output, null, 2);
@@ -1 +1 @@
1
- {"version":3,"file":"format-json.js","sourceRoot":"","sources":["../src/format-json.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAA2B,EAAE,MAAe;IAC/E,MAAM,MAAM,GAAG;QACb,OAAO,EAAE;YACP,KAAK,EAAE,UAAU,CAAC,MAAM;YACxB,MAAM,EAAE,MAAM,IAAI,IAAI;SACvB;QACD,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACjC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI;YACxB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI;YAC1B,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,IAAI;YAChC,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI;SACrB,CAAC,CAAC;KACJ,CAAA;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;AACxC,CAAC"}
1
+ {"version":3,"file":"format-json.js","sourceRoot":"","sources":["../src/format-json.ts"],"names":[],"mappings":"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"}
package/dist/format.d.ts CHANGED
@@ -18,8 +18,10 @@ export declare function formatViolations(violations: ArchViolation[], reason?: s
18
18
  /**
19
19
  * Format violations into a plain-text string (no ANSI codes).
20
20
  *
21
- * Used by ArchRuleError.message — error messages should be plain text
22
- * since they may be captured by test runners, serialized, or logged to files.
21
+ * Public export, for callers that aggregate violations themselves and need
22
+ * output free of ANSI codes — serialized, logged to a file, or embedded in
23
+ * another tool's report. `ArchRuleError.message` is a one-line summary and does
24
+ * not use this; the run's detail is written by `writeReport`.
23
25
  */
24
26
  export declare function formatViolationsPlain(violations: ArchViolation[], reason?: string): string;
25
27
  //# sourceMappingURL=format.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAInD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AA0CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,aAAa,EAAE,EAC3B,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,CAYR;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB1F"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAKnD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAoDD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,aAAa,EAAE,EAC3B,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,CAYR;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B1F"}
package/dist/format.js CHANGED
@@ -1,28 +1,39 @@
1
+ import { remedyRepeatsMessage } from './violation.js';
1
2
  import { bold, red, cyan, dim } from './ansi.js';
2
3
  import path from 'node:path';
3
4
  /** Format a single violation into a rich terminal section. */
4
5
  function formatSingleViolation(v, index, total, cwd, showCodeFrames, reason) {
5
6
  const counter = bold(red(`Architecture Violation [${String(index + 1)} of ${String(total)}]`));
6
7
  const ruleLine = ` ${dim('Rule:')} ${v.rule}`;
7
- const relativePath = path.relative(cwd, v.file);
8
- const locationRef = cyan(relativePath + ':' + String(v.line));
9
- const location = ` ${locationRef} ${dim('—')} ${v.element}`;
8
+ // `message` is printed for BOTH shapes, and it used to be printed for neither
9
+ // — a located violation rendered only `file:line — element`, so the one
10
+ // sentence saying what is actually wrong was dropped from the default surface
11
+ // for every ordinary violation. Nothing failed when this was fixed, because
12
+ // nothing pinned it either way; `formatViolationsPlain` had always printed it,
13
+ // so the two formatters disagreed about what a violation IS.
14
+ //
15
+ // A config-level finding has no source location: `path.relative(cwd, '')`
16
+ // renders as the cwd and ':0' is noise, so it gets the message alone.
17
+ const location = v.file
18
+ ? ` ${v.message}\n ${cyan(path.relative(cwd, v.file) + ':' + String(v.line))} ${dim('—')} ${v.element}`
19
+ : ` ${v.message}`;
10
20
  const codeLine = showCodeFrames && v.codeFrame ? `\n${v.codeFrame}` : '';
11
21
  const whyText = v.because ?? reason;
12
22
  const whyLine = whyText ? ` ${dim('Why:')} ${whyText}` : '';
13
- const fixLine = v.suggestion ? ` ${dim('Fix:')} ${v.suggestion}` : '';
23
+ // Suppressed whenever the `location` slot above already printed this exact
24
+ // text, which is now BOTH shapes — see the note there. It used to be
25
+ // `!v.file && remedyRepeatsMessage(v)`, and that asymmetry was load-bearing
26
+ // and pinned, because a located violation did not render `message` at all: its
27
+ // `Fix:` line was the remedy's only appearance. Rendering `message` for both
28
+ // shapes removed the premise, and the `!v.file` half became a defect —
29
+ // measured, two occurrences of the remedy for a located finding whose
30
+ // suggestion is its message.
31
+ //
32
+ // So do not "restore" the asymmetry from the old comment: check whether this
33
+ // formatter still prints `message` for a located violation first.
34
+ const fixLine = v.suggestion && !remedyRepeatsMessage(v) ? ` ${dim('Fix:')} ${v.suggestion}` : '';
14
35
  const docsLine = v.docs ? ` ${dim('Docs:')} ${v.docs}` : '';
15
- // The message is the finding itself. It was omitted here for years, which is
16
- // survivable for a one-sided rule — `element` plus the rule description plus a
17
- // code frame usually carry the meaning — and not survivable for a two-sided
18
- // one, where `message` is the ONLY place the correspondence states which side
19
- // drifted. Measured before adding: `check:spec` rendered a ghost ADR row as
20
- // `Rule: correspondence / CLAUDE.md:24 — 099 / Why: …` and never said what was
21
- // wrong. Rendered in full, not just the first line: `correspondence()` folds
22
- // its per-side `suggest` remedy onto a continuation line, so truncating here
23
- // is what made that remedy invisible.
24
- const messageLine = ` ${dim('What:')} ${v.message.split('\n').join('\n ')}`;
25
- const parts = [counter, '', ruleLine, '', location, messageLine];
36
+ const parts = [counter, '', ruleLine, '', location];
26
37
  if (codeLine)
27
38
  parts.push(codeLine);
28
39
  if (whyLine)
@@ -51,8 +62,10 @@ export function formatViolations(violations, reason, options) {
51
62
  /**
52
63
  * Format violations into a plain-text string (no ANSI codes).
53
64
  *
54
- * Used by ArchRuleError.message — error messages should be plain text
55
- * since they may be captured by test runners, serialized, or logged to files.
65
+ * Public export, for callers that aggregate violations themselves and need
66
+ * output free of ANSI codes — serialized, logged to a file, or embedded in
67
+ * another tool's report. `ArchRuleError.message` is a one-line summary and does
68
+ * not use this; the run's detail is written by `writeReport`.
56
69
  */
57
70
  export function formatViolationsPlain(violations, reason) {
58
71
  if (violations.length === 0)
@@ -61,12 +74,21 @@ export function formatViolationsPlain(violations, reason) {
61
74
  const reasonLine = reason ? `\nReason: ${reason}` : '';
62
75
  const details = violations
63
76
  .map((v, i) => {
77
+ // A configuration finding has no source location — it reports that a RULE
78
+ // enforces nothing, not that a line is wrong — so it carries `file: ''`
79
+ // and a `line` that means nothing. Rendering it produced a bare `(:1)`
80
+ // (bug 0047). The rich formatter and the GitHub formatter
81
+ // (`format-github.ts:58`) both already special-case this; plain and JSON
82
+ // were the two that did not.
83
+ const where = v.file === '' ? '' : ` (${v.file}:${String(v.line)})`;
64
84
  const parts = [
65
- ` [${String(i + 1)}/${String(violations.length)}] ${v.element}: ${v.message} (${v.file}:${String(v.line)})`,
85
+ ` [${String(i + 1)}/${String(violations.length)}] ${v.element}: ${v.message}${where}`,
66
86
  ];
67
87
  if (v.codeFrame)
68
88
  parts.push(v.codeFrame);
69
- if (v.suggestion)
89
+ // This format always renders `message`, so a remedy identical to it is
90
+ // already shown — see `remedyRepeatsMessage`.
91
+ if (v.suggestion && !remedyRepeatsMessage(v))
70
92
  parts.push(` Fix: ${v.suggestion}`);
71
93
  return parts.join('\n');
72
94
  })
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The declaration side of plan 0069: what a glob is, and how a set of globs
3
+ * combines, so that "this rule can never match anything" is answerable without
4
+ * running the rule.
5
+ *
6
+ * Nothing here evaluates. Evaluation lives in `glob-diagnosis.ts`, against the
7
+ * path universe; this module only describes.
8
+ */
9
+ /**
10
+ * Which string a glob is matched against.
11
+ *
12
+ * This names **the target**, never the API. `SmellBuilder.inFolder()` matches
13
+ * the full file path despite its name (`src/smells/duplicate-bodies.ts`), and
14
+ * exactly one selector in `src/` — `resideInFolder` — matches a directory.
15
+ * Two revisions of the plan's own census got this wrong by reading the method
16
+ * name, and each time reported a vacuous rule as satisfiable.
17
+ *
18
+ * - `file-path` — the whole absolute path of the file
19
+ * - `parent-dir` — the immediate parent directory of the file, and only the
20
+ * immediate parent: `resideInFolder` tests
21
+ * `filePath.substring(0, filePath.lastIndexOf('/'))`
22
+ * - `import-target` — a resolved module path or a bare specifier. Never
23
+ * checked against the path universe: `node_modules` is
24
+ * outside the project by construction, so checking it would
25
+ * fail every correct dependency rule in existence
26
+ * - `specifier` — a raw module specifier
27
+ * - `literal` — matched against source text, not a path
28
+ */
29
+ export type GlobKind = 'file-path' | 'parent-dir' | 'import-target' | 'specifier' | 'literal';
30
+ /**
31
+ * Where in a rule the glob was written, which decides whether being
32
+ * unsatisfiable is a fault.
33
+ *
34
+ * - `selector` — narrows the subject set. Unsatisfiable ⇒ the rule can never
35
+ * have subjects
36
+ * - `discovery` — defines a population (slices, layers, boundary folders).
37
+ * Unsatisfiable ⇒ nothing to check
38
+ * - `condition` — asserts something about subjects. Unsatisfiable is
39
+ * indistinguishable from an armed tripwire that has not fired
40
+ * - `exclusion` — subtracts. An exclusion matching zero is remedy-optional
41
+ * (proposal 006) and never a fault
42
+ */
43
+ export type GlobPosition = 'selector' | 'discovery' | 'condition' | 'exclusion';
44
+ /**
45
+ * Is a dead glob at this position a **fault**?
46
+ *
47
+ * One owner for a decision that was written twice, inversely, and disagreed —
48
+ * [ts-archunit plan 0080](https://github.com/nielspeter/ts-archunit/blob/main/plans/completed/0080-admit-discovery-globs-to-the-dead-glob-gate.md).
49
+ *
50
+ * | site | said | i.e. |
51
+ * | --- | --- | --- |
52
+ * | `diagnose.ts` | skip `exclusion` and `condition` | selector **and discovery** are faults |
53
+ * | `terminal-builder.ts` | skip anything but `selector` | only selector is |
54
+ *
55
+ * So `doctor` reported a dead `discovery` glob and the check that gates the build
56
+ * did not — the divergence bug 0040's silence half is made of. Two hand-maintained
57
+ * inverse lists that must agree is the shape ADR-009 rule 5 keeps charging this
58
+ * project for; a shared predicate is the whole fix.
59
+ *
60
+ * **`selector` and `discovery` are faults.** Both name what a rule will judge, so
61
+ * a dead one means the rule judges nothing.
62
+ *
63
+ * **`condition` and `exclusion` are not.** A condition glob is an assertion
64
+ * *about* the subjects — `onlyImportFrom('**\/domain/**')` matching nothing is a
65
+ * satisfied rule, not a broken one (bug 0014) — and an exclusion glob matching
66
+ * nothing is an unused exemption, which `.excluding()` reports on its own terms.
67
+ *
68
+ * ## A `switch`, not an `||`, and that is the point
69
+ *
70
+ * It shipped as `position === 'selector' || position === 'discovery'` — an
71
+ * allow-list with no exhaustiveness check. Review measured the consequence: a
72
+ * **fifth** `GlobPosition` added to the union above compiles clean and leaves the
73
+ * whole suite green, silently a non-fault in all three consumers. A dead glob
74
+ * written at it would be invisible in `doctor` *and* in the build — which is the
75
+ * original divergence's failure direction, one level up, in the predicate that
76
+ * exists to remove it.
77
+ *
78
+ * The predicate's own test enumerates the four known values, which is a
79
+ * restatement of the implementation and structurally cannot cover a fifth. The
80
+ * `never` assignment below can, and it fails at compile time rather than at
81
+ * review time.
82
+ */
83
+ export declare function isFaultPosition(position: GlobPosition): boolean;
84
+ /**
85
+ * Which set of paths the glob is written against.
86
+ *
87
+ * **This affects the verdict**, and the earlier claim here that it was
88
+ * message-only is withdrawn. Satisfiability is still taken against the union
89
+ * of the views for the glob's `kind`, but the ANCHOR check consults `base`:
90
+ * an unanchored glob can never match an absolute path, so for
91
+ * `base: 'absolute'` it is dead however the project is shaped — while for a
92
+ * base whose entry point rewrites or relativises the glob, an unanchored
93
+ * spelling is correct and telling the author to anchor it would break a
94
+ * working rule.
95
+ *
96
+ * The union alone was tried and was a false green on the commonest real
97
+ * mistake: the tsconfig-relative view accepts `'src/domain/**'`, which
98
+ * `resideInFolder` can never match. Unanchored globs are the entire subject of
99
+ * the 0.18.1 release, so a design that quietly calls them satisfiable defeats
100
+ * its own purpose.
101
+ *
102
+ * So a mis-declared `base` CAN cause a red build. It is set beside `kind` by
103
+ * the same code, from what the entry point does rather than from intent, and
104
+ * `tests/core/glob-declaration.test.ts` asserts the equivalent spellings agree.
105
+ */
106
+ export type GlobBase = 'absolute' | 'tsconfig-relative' | 'normalized';
107
+ /**
108
+ * A glob as a predicate, condition or builder declares it.
109
+ *
110
+ * This is the author-facing half. `position` and `origin` are deliberately
111
+ * absent: the code that mints a site — inside `resideInFolder()`, several
112
+ * frames below any builder — cannot know either, and the builder stamps them
113
+ * on. Exposing them here would also let a rule author write
114
+ * `position: 'exclusion'` and permanently exempt their own predicate from the
115
+ * check, with no signal that they had.
116
+ */
117
+ export interface DeclaredGlob {
118
+ readonly glob: string;
119
+ readonly kind: GlobKind;
120
+ /** Defaults to `'positive'`. A negative site can never be dead — see `GlobNode`. */
121
+ readonly polarity?: 'positive' | 'negative';
122
+ /**
123
+ * Defaults to `'absolute'`. **Affects the verdict** — see `GlobBase`. An
124
+ * unanchored glob is dead for `'absolute'` and correct for a base whose
125
+ * entry point rewrites or relativises it.
126
+ */
127
+ readonly base?: GlobBase;
128
+ }
129
+ /** A declared glob after a builder has stamped on what only it knows. */
130
+ export type GlobSite = DeclaredGlob & {
131
+ readonly position: GlobPosition;
132
+ /** For the message: `resideInFolder("**\/src/x/**") in rule "adr005/no-any"`. */
133
+ readonly origin: string;
134
+ };
135
+ /**
136
+ * A predicate or condition that declares no globs.
137
+ *
138
+ * Retained in the tree rather than dropped, and never dead. Dropping is safe
139
+ * under `all` — `some(dead)` is monotone — but `not()` turns an `all` into an
140
+ * `any`, and under `any` dropping an opaque child is what makes
141
+ * `or(deadGlob, exportSymbolNamed('Foo'))` red a working rule. Since most
142
+ * predicates declare no globs, that would have been the commonest failure of
143
+ * the whole mechanism.
144
+ *
145
+ * Retaining it costs no detection, removes the need for a separate `or()`
146
+ * propagation rule, and makes an empty node unreachable — which matters
147
+ * because `[].every()` is `true` and would otherwise fault a rule containing
148
+ * no globs at all. Verified exhaustively by
149
+ * `spikes/0069-tree-model-check.mjs`: 0 false reds and 0 missed emptiness
150
+ * across every expression of at most three combinator nodes.
151
+ */
152
+ export interface OpaqueGlob {
153
+ readonly opaque: true;
154
+ }
155
+ /** A leaf of a glob tree: a declaration of some kind, or an opaque input. */
156
+ export type GlobLeaf<L> = L | OpaqueGlob;
157
+ /**
158
+ * How a set of globs combines.
159
+ *
160
+ * - `all` — every child must match for the whole to match, so the whole is
161
+ * dead if **any** child is dead. `and()`.
162
+ * - `any` — one child matching is enough, so the whole is dead only if
163
+ * **every** child is dead. `or()`, a variadic predicate
164
+ * (`importFrom(...globs)` is `matchers.some`), repeated `.inFolder()` calls.
165
+ *
166
+ * A preset's option list is **not** an `any` node: both shipped presets fan
167
+ * out one rule per glob (`src/presets/layered.ts`, `src/presets/boundaries.ts`),
168
+ * so one dead layer glob is one vacuous rule, and `any` would say "no fault
169
+ * unless every layer is dead" — a false green inside a preset. Each generated
170
+ * builder declares its own root instead.
171
+ */
172
+ export interface GlobTree<L> {
173
+ readonly op: 'any' | 'all';
174
+ readonly children: readonly (GlobTree<L> | GlobLeaf<L>)[];
175
+ }
176
+ /**
177
+ * What a predicate, condition or builder method declares.
178
+ *
179
+ * Its leaves are `DeclaredGlob`, which has no `position` and no `origin`, so
180
+ * a rule author **cannot** express "this is an exclusion" and thereby exempt
181
+ * their own predicate from the check. That is a type-level guarantee rather
182
+ * than a convention.
183
+ */
184
+ export type DeclaredGlobs = GlobTree<DeclaredGlob>;
185
+ /** A declared tree after a builder has stamped position and origin onto it. */
186
+ export type GlobNode = GlobTree<GlobSite>;
187
+ /** How many real declarations a tree holds. Opaque leaves do not count. */
188
+ export declare function countDeclaredGlobs<L extends object>(tree: GlobTree<L>): number;
189
+ /** Narrow a tree position to an interior node. */
190
+ export declare function isGlobNode<L extends object>(value: GlobTree<L> | GlobLeaf<L>): value is GlobTree<L>;
191
+ /** Narrow a leaf to the opaque case. */
192
+ export declare function isOpaqueGlob<L extends object>(value: GlobTree<L> | GlobLeaf<L>): value is OpaqueGlob;
193
+ /**
194
+ * A variadic declaration: any one of these globs matching is enough.
195
+ *
196
+ * `importFrom(...globs)` is `matchers.some`, so the set is dead only when
197
+ * every glob in it is — which is exactly `any`. Getting this wrong in the
198
+ * other direction is what the 0.18.1 withdrawal was.
199
+ */
200
+ export declare function globAnyOf(globs: readonly string[], kind: GlobKind, base?: GlobBase): DeclaredGlobs;
201
+ /** A single declaration as a one-element `any` tree. */
202
+ export declare function globNode<L extends object>(leaf: L): GlobTree<L>;
203
+ /**
204
+ * Stamp a declared tree with what only the builder knows.
205
+ *
206
+ * `origin` is a function of the declaration so each site can name itself —
207
+ * `resideInFolder("**\/src/x/**")` — rather than every site in one predicate
208
+ * sharing a single label.
209
+ */
210
+ export declare function stampGlobs(declared: DeclaredGlobs, position: GlobPosition, origin: (glob: DeclaredGlob) => string): GlobNode;
211
+ /**
212
+ * Negate a glob tree: invert `op` at every node **and** flip `polarity` at
213
+ * every site.
214
+ *
215
+ * A polarity flip alone is not enough, and the shortfall is not exotic — it is
216
+ * reachable through public exports, since `and()` returns a `Predicate<T>` and
217
+ * `not()` takes one. `not(and(live, not(dead)))` selects a non-empty set and
218
+ * would be reported dead; `not(or(live, not(dead)))` selects nothing and would
219
+ * be missed. Inverting `op` as well is the standard negation-normal-form
220
+ * push-down and fixes both directions, leaving every simpler case unchanged:
221
+ * `not(not(dead))` still faults, `not(and(a, b))` still does not.
222
+ */
223
+ export declare function negateGlobs<L extends DeclaredGlob>(tree: GlobTree<L>): GlobTree<L>;
224
+ /**
225
+ * Combine inputs into one node, treating a missing declaration as opaque.
226
+ *
227
+ * Every input contributes exactly one child, so the arity of the node always
228
+ * matches the arity of the combinator — which is what keeps `negateGlobs`
229
+ * sound and empty nodes unreachable.
230
+ */
231
+ export declare function combineGlobs<L extends object>(op: 'any' | 'all', inputs: readonly (GlobTree<L> | undefined)[]): GlobTree<L>;
232
+ //# sourceMappingURL=glob-site.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob-site.d.ts","sourceRoot":"","sources":["../src/glob-site.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,eAAe,GAAG,WAAW,GAAG,SAAS,CAAA;AAE7F;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAA;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAe/D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,mBAAmB,GAAG,YAAY,CAAA;AAEtE;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,UAAU,CAAA;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAA;CACzB;AAED,yEAAyE;AACzE,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG;IACpC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CACtB;AAED,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,UAAU,CAAA;AAExC;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,KAAK,GAAG,KAAK,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;CAC1D;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAA;AAElD,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;AAEzC,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,CAO9E;AAED,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EACzC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAC/B,KAAK,IAAI,QAAQ,CAAC,CAAC,CAAC,CAEtB;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAC3C,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAC/B,KAAK,IAAI,UAAU,CAErB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,IAAI,EAAE,QAAQ,EACd,IAAI,CAAC,EAAE,QAAQ,GACd,aAAa,CAEf;AAED,wDAAwD;AACxD,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,GACrC,QAAQ,CASV;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAOlF;AAUD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAC3C,EAAE,EAAE,KAAK,GAAG,KAAK,EACjB,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,GAC3C,QAAQ,CAAC,CAAC,CAAC,CAKb"}