@nielspeter/eess 0.2.1 → 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 +11 -5
  63. package/dist/execute-rule.d.ts.map +1 -1
  64. package/dist/execute-rule.js +87 -19
  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 +40 -8
  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
@@ -0,0 +1,55 @@
1
+ import type { ArchViolation } from './violation.js';
2
+ import type { RuleDescription } from './rule-description.js';
3
+ /**
4
+ * What a rule can be asked about itself, without running it.
5
+ *
6
+ * Every finding here is a function of these three and nothing else — none of
7
+ * them evaluates the rule. That is what let them leave `TerminalBuilder`: they
8
+ * were never terminal behaviour, they were explanations OF it.
9
+ */
10
+ export interface RuleFacts {
11
+ /** The builder's own class, for naming the rule in a finding. */
12
+ readonly ruleClass: {
13
+ name: string;
14
+ };
15
+ readonly reason: string | undefined;
16
+ describeRule(): RuleDescription;
17
+ }
18
+ /**
19
+ * The configuration finding for a rule that examined zero units with no
20
+ * declared exemption — ADR-009 rule 2: named as a distinct cause (dead
21
+ * selector, empty corpus, or an unreachable examining seam), not folded
22
+ * into an ordinary violation message.
23
+ */
24
+ export declare function zeroExaminedViolation(facts: RuleFacts): ArchViolation;
25
+ /**
26
+ * The configuration finding for a rule that examined zero units BECAUSE one
27
+ * of its declared globs is diagnosably dead — `deadGlob` is the pre-formed
28
+ * reason a dialect's `RuleBuilder.deadGlobDiagnosis()` computed (a typo, an
29
+ * unanchored pattern, a directory glob pointed at a file — whatever the
30
+ * dialect's own glob-evaluation determined). Strictly more actionable than
31
+ * `zeroExaminedViolation()`'s generic message, which this replaces when a
32
+ * diagnosis is available; see `evidencedViolations()`.
33
+ */
34
+ export declare function deadGlobViolation(facts: RuleFacts, deadGlob: string): ArchViolation;
35
+ /**
36
+ * The configuration finding for a `.expectNonEmpty()` declaration that
37
+ * wasn't met — the corpus the author said should never be empty is empty
38
+ * right now. Overrides `assertsCardinality()`'s silent pass on purpose:
39
+ * the declaration is a stronger, caller-level claim than what the
40
+ * condition itself would otherwise tolerate.
41
+ */
42
+ export declare function unmetExpectNonEmptyViolation(facts: RuleFacts): ArchViolation;
43
+ /**
44
+ * The configuration finding for the ADR-010 part 3 precedence case: the
45
+ * family's own upstream source loaded nothing at all — a stronger claim
46
+ * than an ordinary dead selector, and worded accordingly (the fix is not
47
+ * "widen the selection", there is no selection yet to widen).
48
+ */
49
+ export declare function zeroLoadedSourceViolation(facts: RuleFacts): ArchViolation;
50
+ /**
51
+ * The configuration finding for a `.expectEmpty()` declaration that has
52
+ * expired — ADR-010 part 3: the number IS the finding, so it is named.
53
+ */
54
+ export declare function expiredExpectEmptyViolation(facts: RuleFacts, examined: number): ArchViolation;
55
+ //# sourceMappingURL=vacuity-findings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vacuity-findings.d.ts","sourceRoot":"","sources":["../src/vacuity-findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,YAAY,IAAI,eAAe,CAAA;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,GAAG,aAAa,CAmBrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAenF;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,SAAS,GAAG,aAAa,CAmB5E;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,GAAG,aAAa,CAmBzE;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAkB7F"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The configuration finding for a rule that examined zero units with no
3
+ * declared exemption — ADR-009 rule 2: named as a distinct cause (dead
4
+ * selector, empty corpus, or an unreachable examining seam), not folded
5
+ * into an ordinary violation message.
6
+ */
7
+ export function zeroExaminedViolation(facts) {
8
+ const described = facts.describeRule();
9
+ const name = described.id ?? described.rule ?? facts.ruleClass.name;
10
+ const message = `this rule examined zero units. If this is expected (the corpus is legitimately ` +
11
+ `empty right now), declare it explicitly with .expectEmpty() — otherwise this is a ` +
12
+ `dead selector, an empty project, or a rule that never reaches its own examining ` +
13
+ `seam, and the fix is to widen the selection, not to suppress this finding.`;
14
+ return {
15
+ rule: described.rule ?? name,
16
+ ruleId: described.id,
17
+ element: name,
18
+ file: '',
19
+ line: 0,
20
+ message,
21
+ suggestion: message,
22
+ because: facts.reason,
23
+ bypassFilters: true,
24
+ };
25
+ }
26
+ /**
27
+ * The configuration finding for a rule that examined zero units BECAUSE one
28
+ * of its declared globs is diagnosably dead — `deadGlob` is the pre-formed
29
+ * reason a dialect's `RuleBuilder.deadGlobDiagnosis()` computed (a typo, an
30
+ * unanchored pattern, a directory glob pointed at a file — whatever the
31
+ * dialect's own glob-evaluation determined). Strictly more actionable than
32
+ * `zeroExaminedViolation()`'s generic message, which this replaces when a
33
+ * diagnosis is available; see `evidencedViolations()`.
34
+ */
35
+ export function deadGlobViolation(facts, deadGlob) {
36
+ const described = facts.describeRule();
37
+ const name = described.id ?? described.rule ?? facts.ruleClass.name;
38
+ const message = `this rule examined zero units — ${deadGlob}`;
39
+ return {
40
+ rule: described.rule ?? name,
41
+ ruleId: described.id,
42
+ element: name,
43
+ file: '',
44
+ line: 0,
45
+ message,
46
+ suggestion: message,
47
+ because: facts.reason,
48
+ bypassFilters: true,
49
+ };
50
+ }
51
+ /**
52
+ * The configuration finding for a `.expectNonEmpty()` declaration that
53
+ * wasn't met — the corpus the author said should never be empty is empty
54
+ * right now. Overrides `assertsCardinality()`'s silent pass on purpose:
55
+ * the declaration is a stronger, caller-level claim than what the
56
+ * condition itself would otherwise tolerate.
57
+ */
58
+ export function unmetExpectNonEmptyViolation(facts) {
59
+ const described = facts.describeRule();
60
+ const name = described.id ?? described.rule ?? facts.ruleClass.name;
61
+ const message = `this rule declared .expectNonEmpty() but examined zero units — the corpus this ` +
62
+ `rule asserted should never be empty is empty right now. If that's still true, fix ` +
63
+ `the selection (a glob typo, a missing folder); if the corpus legitimately can be ` +
64
+ `empty, remove .expectNonEmpty().`;
65
+ return {
66
+ rule: described.rule ?? name,
67
+ ruleId: described.id,
68
+ element: name,
69
+ file: '',
70
+ line: 0,
71
+ message,
72
+ suggestion: message,
73
+ because: facts.reason,
74
+ bypassFilters: true,
75
+ };
76
+ }
77
+ /**
78
+ * The configuration finding for the ADR-010 part 3 precedence case: the
79
+ * family's own upstream source loaded nothing at all — a stronger claim
80
+ * than an ordinary dead selector, and worded accordingly (the fix is not
81
+ * "widen the selection", there is no selection yet to widen).
82
+ */
83
+ export function zeroLoadedSourceViolation(facts) {
84
+ const described = facts.describeRule();
85
+ const name = described.id ?? described.rule ?? facts.ruleClass.name;
86
+ const message = `this rule's source loaded zero units before any selection ran — an empty project, ` +
87
+ `an unreadable tsconfig, or a glob resolving to nothing. This outranks any ` +
88
+ `.expectEmpty() declaration and any condition satisfied by emptiness: fix the ` +
89
+ `project/source configuration, not the rule.`;
90
+ return {
91
+ rule: described.rule ?? name,
92
+ ruleId: described.id,
93
+ element: name,
94
+ file: '',
95
+ line: 0,
96
+ message,
97
+ suggestion: message,
98
+ because: facts.reason,
99
+ bypassFilters: true,
100
+ };
101
+ }
102
+ /**
103
+ * The configuration finding for a `.expectEmpty()` declaration that has
104
+ * expired — ADR-010 part 3: the number IS the finding, so it is named.
105
+ */
106
+ export function expiredExpectEmptyViolation(facts, examined) {
107
+ const described = facts.describeRule();
108
+ const name = described.id ?? described.rule ?? facts.ruleClass.name;
109
+ const message = `this rule declared .expectEmpty() but examined ${String(examined)} unit(s) — the ` +
110
+ `declaration has expired. If the corpus legitimately grew past empty, remove ` +
111
+ `.expectEmpty() from this rule; the underlying violations (if any) above still stand.`;
112
+ return {
113
+ rule: described.rule ?? name,
114
+ ruleId: described.id,
115
+ element: name,
116
+ file: '',
117
+ line: 0,
118
+ message,
119
+ suggestion: message,
120
+ because: facts.reason,
121
+ bypassFilters: true,
122
+ };
123
+ }
124
+ //# sourceMappingURL=vacuity-findings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vacuity-findings.js","sourceRoot":"","sources":["../src/vacuity-findings.ts"],"names":[],"mappings":"AAiBA;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAgB;IACpD,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,EAAE,CAAA;IACtC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,IAAI,SAAS,CAAC,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,CAAA;IACnE,MAAM,OAAO,GACX,iFAAiF;QACjF,oFAAoF;QACpF,kFAAkF;QAClF,4EAA4E,CAAA;IAC9E,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,IAAI;QAC5B,MAAM,EAAE,SAAS,CAAC,EAAE;QACpB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,CAAC;QACP,OAAO;QACP,UAAU,EAAE,OAAO;QACnB,OAAO,EAAE,KAAK,CAAC,MAAM;QACrB,aAAa,EAAE,IAAI;KACpB,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAgB,EAAE,QAAgB;IAClE,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,EAAE,CAAA;IACtC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,IAAI,SAAS,CAAC,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,CAAA;IACnE,MAAM,OAAO,GAAG,mCAAmC,QAAQ,EAAE,CAAA;IAC7D,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,IAAI;QAC5B,MAAM,EAAE,SAAS,CAAC,EAAE;QACpB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,CAAC;QACP,OAAO;QACP,UAAU,EAAE,OAAO;QACnB,OAAO,EAAE,KAAK,CAAC,MAAM;QACrB,aAAa,EAAE,IAAI;KACpB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAgB;IAC3D,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,EAAE,CAAA;IACtC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,IAAI,SAAS,CAAC,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,CAAA;IACnE,MAAM,OAAO,GACX,iFAAiF;QACjF,oFAAoF;QACpF,mFAAmF;QACnF,kCAAkC,CAAA;IACpC,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,IAAI;QAC5B,MAAM,EAAE,SAAS,CAAC,EAAE;QACpB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,CAAC;QACP,OAAO;QACP,UAAU,EAAE,OAAO;QACnB,OAAO,EAAE,KAAK,CAAC,MAAM;QACrB,aAAa,EAAE,IAAI;KACpB,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAgB;IACxD,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,EAAE,CAAA;IACtC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,IAAI,SAAS,CAAC,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,CAAA;IACnE,MAAM,OAAO,GACX,oFAAoF;QACpF,4EAA4E;QAC5E,+EAA+E;QAC/E,6CAA6C,CAAA;IAC/C,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,IAAI;QAC5B,MAAM,EAAE,SAAS,CAAC,EAAE;QACpB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,CAAC;QACP,OAAO;QACP,UAAU,EAAE,OAAO;QACnB,OAAO,EAAE,KAAK,CAAC,MAAM;QACrB,aAAa,EAAE,IAAI;KACpB,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,KAAgB,EAAE,QAAgB;IAC5E,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,EAAE,CAAA;IACtC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,IAAI,SAAS,CAAC,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,CAAA;IACnE,MAAM,OAAO,GACX,kDAAkD,MAAM,CAAC,QAAQ,CAAC,iBAAiB;QACnF,8EAA8E;QAC9E,sFAAsF,CAAA;IACxF,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,IAAI;QAC5B,MAAM,EAAE,SAAS,CAAC,EAAE;QACpB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,CAAC;QACP,OAAO;QACP,UAAU,EAAE,OAAO;QACnB,OAAO,EAAE,KAAK,CAAC,MAAM;QACrB,aAAa,EAAE,IAAI;KACpB,CAAA;AACH,CAAC"}
@@ -1,10 +1,7 @@
1
1
  /**
2
2
  * A single architecture rule violation.
3
3
  *
4
- * Represents one element that failed to satisfy a condition. This is the
5
- * kernel's central data type — dialect-independent. Each dialect provides its
6
- * own adapter that builds an `ArchViolation` from that dialect's element type
7
- * (e.g. the TS dialect builds one from a ts-morph `Node`).
4
+ * Represents one element that failed to satisfy a condition.
8
5
  */
9
6
  /**
10
7
  * A deterministic, unique text edit that repairs a violation (plan 0066). Emitted
@@ -37,6 +34,56 @@ export interface ArchViolation {
37
34
  line: number;
38
35
  /** Human-readable description of what went wrong */
39
36
  message: string;
37
+ /**
38
+ * Stable identity for baseline matching, when the rendered message is not a
39
+ * safe identifier.
40
+ *
41
+ * By default a violation is identified by `rule::element::message`, which is
42
+ * right as long as the message says only what is wrong. It breaks when the
43
+ * message also encodes *circumstances*:
44
+ *
45
+ * - a derived population — `"3 of 5 files … use X"` becomes `"4 of 6"` when
46
+ * an unrelated sibling is added, and every accepted finding in that folder
47
+ * changes identity;
48
+ * - an ordering — a pairwise detector that reports `A → B` reports `B → A`
49
+ * when the file walk runs in a different order, which is a property of the
50
+ * filesystem, not of the code;
51
+ * - a coordinate — `"at line 12"` moves when anything above it is edited.
52
+ *
53
+ * Set this to a canonical form and identity survives all three. It replaces
54
+ * both `element` and `message` in the hash, so it must be unique per finding
55
+ * within a rule: two distinct violations sharing one identity are one
56
+ * violation to the baseline, and accepting either accepts both. Absolute
57
+ * paths inside it are fine — they are normalised away with the rest
58
+ * (`src/core/identity-root.ts`).
59
+ *
60
+ * The rendered output is unaffected; this is identity only.
61
+ */
62
+ identity?: string;
63
+ /**
64
+ * The measurement this finding reports, for a metric condition — bug 0012.
65
+ *
66
+ * The baseline stores it and compares rather than equates, so improving a
67
+ * metric stays green while regressing past the accepted value fails. Absent
68
+ * on every non-metric finding, where equality of identity is the right test.
69
+ */
70
+ measured?: number;
71
+ /**
72
+ * What {@link measured} counts — `code-lines`, `methods`, `complexity`.
73
+ *
74
+ * The baseline persists it and refuses to compare a stored measurement
75
+ * against a current one carrying a different unit
76
+ * ([bug 0171](../../../work/bugs/0171-a-metric-unit-change-silently-loosens-every-baselined-ratchet.md)).
77
+ * Without it, changing what a metric COUNTS silently moved every baselined
78
+ * ceiling: `linesOfCode` went from span lines to code lines, entries kept
79
+ * matching on identity, and a class could then grow to roughly three times
80
+ * its accepted size while the build stayed green.
81
+ *
82
+ * Absent on findings from a producer that predates this, which the baseline
83
+ * treats as "unit unknown" — comparable only for the metrics whose meaning
84
+ * has never changed.
85
+ */
86
+ measuredUnit?: string;
40
87
  /** Optional rationale provided via .because() */
41
88
  because?: string;
42
89
  /** Source code snippet around the violation line */
@@ -47,5 +94,106 @@ export interface ArchViolation {
47
94
  docs?: string;
48
95
  /** A deterministic, unique text edit that repairs this violation (plan 0066). */
49
96
  fix?: ArchFix;
97
+ /** Severity of this violation. Absent means 'error' (the default). */
98
+ severity?: 'error' | 'warn';
99
+ /**
100
+ * When true, this is a meta-finding about rule *configuration* (e.g. an empty
101
+ * selector or empty discovery), not about a source file. It has no changed
102
+ * file to attribute to, so the diff-aware and baseline filters must NOT drop
103
+ * it — otherwise the guard silently re-greens under the standard CI mode
104
+ * (ADR-008; plan 0067).
105
+ */
106
+ bypassFilters?: boolean;
50
107
  }
108
+ /**
109
+ * Extract a human-readable name from a ts-morph Node.
110
+ *
111
+ * If the node itself is a named declaration (class, function, method, etc.),
112
+ * returns its name directly. Otherwise, walks up the AST ancestors to find
113
+ * the nearest named declaration and builds a qualified name like
114
+ * "ClassName.methodName". This ensures that inner nodes (e.g., AsExpression,
115
+ * CallExpression) produce meaningful element names for `.excluding()` matching.
116
+ *
117
+ * Falls back to the node's kind name only if no named ancestor is found
118
+ * (e.g., top-level expressions in a module).
119
+ */
120
+ /**
121
+ * Force a configuration meta-finding to `error`, whatever the rule asked for.
122
+ *
123
+ * A `bypassFilters` finding reports that the rule enforces **nothing**. That is
124
+ * not a violation the author gets to grade: `.asSeverity('warn')` says "these
125
+ * violations are advisory", and a rule that cannot fire has no violations to
126
+ * be advisory about. Under ADR-009 rule 1 an actionable finding must fail, and
127
+ * the other suppression paths already refuse to silence these — `.excluding()`
128
+ * refuses explicitly, baseline and diff honour the flag. The full roster is
129
+ * `UNSUPPRESSABLE_MECHANISMS` in `unsuppressable.ts`, which is also what the
130
+ * user-facing sentence is built from; it has grown twice, so it is named here
131
+ * rather than counted. (This paragraph said "three of the four" while the list
132
+ * stood at six.)
133
+ *
134
+ * Applied at all three severity-stamping sites. `stampSeverity` alone is not
135
+ * enough: `.violations()` inlines its own map in both root builders, and the
136
+ * `executeWarn` path resolves an unset severity to `warn`, which is where most
137
+ * producers landed.
138
+ */
139
+ export declare function severityFor(violation: ArchViolation, fallback: 'error' | 'warn'): 'error' | 'warn';
140
+ /**
141
+ * True when a violation's remedy **is** its message, so a renderer that has
142
+ * already shown the message must not append a `Fix:` line repeating it.
143
+ *
144
+ * The assertion gate's finding reports that a rule cannot fire; there, the fault
145
+ * and its remedy are one sentence, and both fields carry that sentence on
146
+ * purpose — the JSON payload reads `suggestion`, a human reads the body, and a
147
+ * configuration finding is the one kind that cannot fall back to the author's
148
+ * `suggestion` (`execute-rule.ts` refuses it, bug 0021). Measured before the
149
+ * fix: every surface printed the same paragraph twice.
150
+ *
151
+ * One definition, because the three renderers must not disagree about it.
152
+ * Whether a given renderer *did* render the message stays renderer-local: the
153
+ * rich terminal format shows it only for a location-less finding, while the
154
+ * plain and GitHub formats always do.
155
+ */
156
+ export declare function remedyRepeatsMessage(violation: ArchViolation): boolean;
157
+ /**
158
+ * Unicode codepoint order — the only comparator admissible anywhere a baseline identity, an
159
+ * `element`, or a reported location is derived from a sort.
160
+ *
161
+ * `String.prototype.localeCompare` without an explicit locale reads the **host** locale from
162
+ * `LANG`/`LC_ALL`, so a value sorted with it differs between a developer's machine and CI.
163
+ * Measured on plain ASCII: `['zebra','aardvark']` sorts to `aardvark,zebra` under `en-US` and
164
+ * to `zebra,aardvark` under `da-DK`, because Danish collates `aa` as `å`, after `z`. One
165
+ * finding, two baseline hashes, diverging only in the place hardest to debug.
166
+ *
167
+ * The codebase already knew: `module-edges.ts` wrote *"`localeCompare` is ICU/locale sensitive
168
+ * — exactly the machine-dependent ordering `conditions/slice.ts` goes out of its way to
169
+ * eliminate, because a value that differs per machine gives one finding two identities"* — 60
170
+ * lines above a `localeCompare` in the identity discriminator, while `conditions/slice.ts`
171
+ * eliminated its own machine-dependence *using `localeCompare`*. Four sites in all.
172
+ *
173
+ * `tests/tools/scan-locale-ordering.test.ts` is what keeps that from recurring: a runtime row
174
+ * cannot catch it, because on an `en-US` machine both comparators agree — a guard written for
175
+ * this defect passed 16/16 with the defect reinstated.
176
+ */
177
+ export declare function byCodepoint(a: string, b: string): number;
178
+ /**
179
+ * The subject `hashViolation` keys a baseline entry on: `identity`, or `element::message`.
180
+ *
181
+ * **Exported and consumed by `hashViolation`/`hashSubject`** rather than duplicated. An earlier
182
+ * draft kept a private copy here on the reasoning that `core/` must not depend on `helpers/` —
183
+ * true, but backwards: `helpers/baseline.ts` already imports from `core/`, which is the
184
+ * permitted direction, and `core/identity-root.ts` records the same move for the same reason.
185
+ * A copy plus a test asserting the copies agree is strictly worse than one definition: the
186
+ * test can only compare the formulas over fixtures it happens to build, and the first draft's
187
+ * fixture shared every field, so it agreed under any formula.
188
+ */
189
+ export declare function subjectOf(violation: ArchViolation): string;
190
+ /** Every subject a rule produced more than once, since the last reset. */
191
+ export declare function identityCollisions(): readonly {
192
+ rule: string;
193
+ subject: string;
194
+ findings: number;
195
+ }[];
196
+ /** Clear the record — call in a `beforeEach` when asserting on a specific rule. */
197
+ export declare function resetIdentityCollisions(): void;
198
+ export declare function disambiguateIdentities(violations: ArchViolation[]): ArchViolation[];
51
199
  //# sourceMappingURL=violation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"violation.d.ts","sourceRoot":"","sources":["../src/violation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAA;IACX,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAA;IACf,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0GAA0G;IAC1G,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iFAAiF;IACjF,GAAG,CAAC,EAAE,OAAO,CAAA;CACd"}
1
+ {"version":3,"file":"violation.d.ts","sourceRoot":"","sources":["../src/violation.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAA;IACX,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAA;IACf;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0GAA0G;IAC1G,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iFAAiF;IACjF,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,sEAAsE;IACtE,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC3B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED;;;;;;;;;;;GAWG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,aAAa,EACxB,QAAQ,EAAE,OAAO,GAAG,MAAM,GACzB,OAAO,GAAG,MAAM,CAElB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO,CAEtE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAE1D;AA0GD,0EAA0E;AAC1E,wBAAgB,kBAAkB,IAAI,SAAS;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;CACjB,EAAE,CAEF;AAED,mFAAmF;AACnF,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAID,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CA6DnF"}
package/dist/violation.js CHANGED
@@ -1,2 +1,263 @@
1
- export {};
1
+ import { registerCacheReset } from './cache-registry.js';
2
+ /**
3
+ * Extract a human-readable name from a ts-morph Node.
4
+ *
5
+ * If the node itself is a named declaration (class, function, method, etc.),
6
+ * returns its name directly. Otherwise, walks up the AST ancestors to find
7
+ * the nearest named declaration and builds a qualified name like
8
+ * "ClassName.methodName". This ensures that inner nodes (e.g., AsExpression,
9
+ * CallExpression) produce meaningful element names for `.excluding()` matching.
10
+ *
11
+ * Falls back to the node's kind name only if no named ancestor is found
12
+ * (e.g., top-level expressions in a module).
13
+ */
14
+ /**
15
+ * Force a configuration meta-finding to `error`, whatever the rule asked for.
16
+ *
17
+ * A `bypassFilters` finding reports that the rule enforces **nothing**. That is
18
+ * not a violation the author gets to grade: `.asSeverity('warn')` says "these
19
+ * violations are advisory", and a rule that cannot fire has no violations to
20
+ * be advisory about. Under ADR-009 rule 1 an actionable finding must fail, and
21
+ * the other suppression paths already refuse to silence these — `.excluding()`
22
+ * refuses explicitly, baseline and diff honour the flag. The full roster is
23
+ * `UNSUPPRESSABLE_MECHANISMS` in `unsuppressable.ts`, which is also what the
24
+ * user-facing sentence is built from; it has grown twice, so it is named here
25
+ * rather than counted. (This paragraph said "three of the four" while the list
26
+ * stood at six.)
27
+ *
28
+ * Applied at all three severity-stamping sites. `stampSeverity` alone is not
29
+ * enough: `.violations()` inlines its own map in both root builders, and the
30
+ * `executeWarn` path resolves an unset severity to `warn`, which is where most
31
+ * producers landed.
32
+ */
33
+ export function severityFor(violation, fallback) {
34
+ return violation.bypassFilters === true ? 'error' : fallback;
35
+ }
36
+ /**
37
+ * True when a violation's remedy **is** its message, so a renderer that has
38
+ * already shown the message must not append a `Fix:` line repeating it.
39
+ *
40
+ * The assertion gate's finding reports that a rule cannot fire; there, the fault
41
+ * and its remedy are one sentence, and both fields carry that sentence on
42
+ * purpose — the JSON payload reads `suggestion`, a human reads the body, and a
43
+ * configuration finding is the one kind that cannot fall back to the author's
44
+ * `suggestion` (`execute-rule.ts` refuses it, bug 0021). Measured before the
45
+ * fix: every surface printed the same paragraph twice.
46
+ *
47
+ * One definition, because the three renderers must not disagree about it.
48
+ * Whether a given renderer *did* render the message stays renderer-local: the
49
+ * rich terminal format shows it only for a location-less finding, while the
50
+ * plain and GitHub formats always do.
51
+ */
52
+ export function remedyRepeatsMessage(violation) {
53
+ return violation.suggestion !== undefined && violation.suggestion === violation.message;
54
+ }
55
+ /**
56
+ * Unicode codepoint order — the only comparator admissible anywhere a baseline identity, an
57
+ * `element`, or a reported location is derived from a sort.
58
+ *
59
+ * `String.prototype.localeCompare` without an explicit locale reads the **host** locale from
60
+ * `LANG`/`LC_ALL`, so a value sorted with it differs between a developer's machine and CI.
61
+ * Measured on plain ASCII: `['zebra','aardvark']` sorts to `aardvark,zebra` under `en-US` and
62
+ * to `zebra,aardvark` under `da-DK`, because Danish collates `aa` as `å`, after `z`. One
63
+ * finding, two baseline hashes, diverging only in the place hardest to debug.
64
+ *
65
+ * The codebase already knew: `module-edges.ts` wrote *"`localeCompare` is ICU/locale sensitive
66
+ * — exactly the machine-dependent ordering `conditions/slice.ts` goes out of its way to
67
+ * eliminate, because a value that differs per machine gives one finding two identities"* — 60
68
+ * lines above a `localeCompare` in the identity discriminator, while `conditions/slice.ts`
69
+ * eliminated its own machine-dependence *using `localeCompare`*. Four sites in all.
70
+ *
71
+ * `tests/tools/scan-locale-ordering.test.ts` is what keeps that from recurring: a runtime row
72
+ * cannot catch it, because on an `en-US` machine both comparators agree — a guard written for
73
+ * this defect passed 16/16 with the defect reinstated.
74
+ */
75
+ export function byCodepoint(a, b) {
76
+ return a < b ? -1 : a > b ? 1 : 0;
77
+ }
78
+ /**
79
+ * The subject `hashViolation` keys a baseline entry on: `identity`, or `element::message`.
80
+ *
81
+ * **Exported and consumed by `hashViolation`/`hashSubject`** rather than duplicated. An earlier
82
+ * draft kept a private copy here on the reasoning that `core/` must not depend on `helpers/` —
83
+ * true, but backwards: `helpers/baseline.ts` already imports from `core/`, which is the
84
+ * permitted direction, and `core/identity-root.ts` records the same move for the same reason.
85
+ * A copy plus a test asserting the copies agree is strictly worse than one definition: the
86
+ * test can only compare the formulas over fixtures it happens to build, and the first draft's
87
+ * fixture shared every field, so it agreed under any formula.
88
+ */
89
+ export function subjectOf(violation) {
90
+ return violation.identity ?? `${violation.element}::${violation.message}`;
91
+ }
92
+ /**
93
+ * The full key `hashViolation` hashes — `rule::subject` — and therefore the unit
94
+ * {@link disambiguateIdentities} must group on.
95
+ *
96
+ * **Grouping on the subject alone was a defect**, measured: `hashViolation` includes `rule`, so
97
+ * two findings carrying the same `element::message` under *different* rule descriptions already
98
+ * hash apart and were never ambiguous. Suffixing the second moved a baseline entry that had no
99
+ * collision — the precise failure this mechanism exists to avoid, committed by the mechanism.
100
+ *
101
+ * Not live at the time it was found (instrumenting the whole suite showed one batch carrying
102
+ * more than one rule string and zero over-suffixed findings), but reachable by construction:
103
+ * several builders mix `metadata?.id ?? description` with a bare `description` inside one batch
104
+ * — `slice-rule-builder.ts`, `correspondence-builder.ts`, `tsconfig-builder.ts`,
105
+ * `inconsistent-siblings.ts`, `schema-rule-builder.ts` all do. Any of them emitting a colliding
106
+ * subject across their two rule strings would have moved a published entry.
107
+ */
108
+ function groupKeyOf(violation) {
109
+ return `${violation.rule}::${subjectOf(violation)}`;
110
+ }
111
+ /**
112
+ * Give every finding in a rule a distinct identity, by suffixing only the ones that collide.
113
+ *
114
+ * {@link ArchViolation.identity} states the invariant this enforces: *"it must be unique per
115
+ * finding within a rule: two distinct violations sharing one identity are one violation to the
116
+ * baseline, and accepting either accepts both."* That was prose with nothing behind it, and
117
+ * three families broke it independently —
118
+ * [ts-archunit bug 0063](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0063-a-dependency-identity-collides-across-files-sharing-a-basename.md)
119
+ * (dependency), [ts-archunit plan 0088](https://github.com/nielspeter/ts-archunit/blob/main/plans/0088-a-slice-finding-identifies-itself.md) (slice), and
120
+ * then [0064](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0064-a-dependency-identity-collides-across-two-spellings-of-one-module.md)
121
+ * and [0065](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0065-reverse-dependency-findings-carry-no-identity.md). Each was fixed
122
+ * per-family, in the family that happened to be reviewed; this is the mechanism, so the next
123
+ * producer cannot reintroduce it.
124
+ *
125
+ * **A finding whose subject is unique is returned untouched — that is a theorem, not a
126
+ * measurement.** Only a subject that appears more than once is altered, and only from its
127
+ * second occurrence onward, so:
128
+ *
129
+ * - no existing baseline entry moves, because a *unique* entry is what a correct baseline holds;
130
+ * - the entry a colliding pair *did* record still matches, because the first occurrence keeps
131
+ * its subject verbatim — so the fail-open closes by making the hidden sibling report as new,
132
+ * which is the finding an adopter never got to see.
133
+ *
134
+ * Groups that collide were, by definition, one baseline entry standing for two findings: an
135
+ * entry that was already wrong. Those are the only ones that move.
136
+ *
137
+ * **Ordering — a limitation, not a guarantee.** The suffix follows the order the rule produced
138
+ * its findings, so a baseline entry accepts *a position in a group*, not *a finding*. An earlier
139
+ * draft of this paragraph said the residual "is not a fail-open, because the group still yields
140
+ * as many distinct identities as it has findings", and cited `ModuleEdge.ordinal` as its source.
141
+ * The premise is true and the conclusion does not follow — and that source, which measured the
142
+ * identical residual one layer down, concludes the opposite. Its table, reproduced because it
143
+ * applies verbatim here:
144
+ *
145
+ * | edit to an accepted colliding group | new reds reported |
146
+ * | ----------------------------------------- | ----------------- |
147
+ * | add a sibling | 1 — correct |
148
+ * | insert a sibling **above** the first | 1 — correct count |
149
+ * | **delete one, add a different one** | **0 — silent** |
150
+ *
151
+ * So the equal-count swap is a **fail-open**: a violation that did not exist when the baseline
152
+ * was written arrives pre-accepted. Strictly better than before this mechanism — where *any*
153
+ * number of added siblings was pre-accepted, not only an equal-count swap — and still a hole.
154
+ *
155
+ * The middle row is right about the number and wrong about the name: the finding reported may
156
+ * be a **sibling nobody touched**, while the newly-added one takes the bare subject and matches
157
+ * the existing entry. For ADR-008's stated primary consumer — an agent that acts on the message
158
+ * — pointing at the wrong file is worse than a miss.
159
+ *
160
+ * Sharper still where an entry carries state: a `measured` metric entry holds an accepted
161
+ * ceiling, so a swap can hand the survivor a ceiling belonging to a different element and
162
+ * silently accept a regression. That is bug 0028's shape recreated inside bug 0012's fix.
163
+ *
164
+ * The durable answer is a real per-finding identity — a `binding` field for the edge family,
165
+ * qualified names for the metric family — tracked in
166
+ * [ts-archunit plan 0094](https://github.com/nielspeter/ts-archunit/blob/main/plans/0094-the-residual-findings-from-the-v0-56-0-review.md). This is the
167
+ * floor: a collision can no longer be silent *at scale*. It is not the ceiling.
168
+ *
169
+ * **Why here and not per condition.** `applyFilters` is the one path every terminal shares, and
170
+ * its own contract is that identity is complete before any filter reads it. Running before the
171
+ * filters also makes identity a property of *what the rule found* rather than of what a
172
+ * `--changed` or `.excluding()` run happened to keep — the same identity in CI and locally.
173
+ */
174
+ /**
175
+ * Subjects the mechanism had to disambiguate, by rule — the producer-quality signal.
176
+ *
177
+ * **Why this exists.** `disambiguateIdentities` guarantees that a rule's findings have distinct
178
+ * identities, which silently turned every `new Set(hashes).size === findings.length` assertion
179
+ * in the suite into a tautology. Measured: collapsing `duplicate-bodies.ts`'s identity to a
180
+ * literal constant — the worst producer defect available — left **all 234 files and 3178 tests
181
+ * green**, including `baseline-portability.test.ts`, whose own comment says *"collision is the
182
+ * failure mode this primitive introduces, so it gets its own guard."*
183
+ *
184
+ * The mechanism is a safety net, not a licence for producers to stop identifying their
185
+ * findings: a positional suffix makes an entry a *slot*, while a producer identity makes it a
186
+ * *reference*, and only the latter survives a sibling being deleted (see the ordering table
187
+ * above). So the guards must keep measuring the producer, and after this mechanism the only
188
+ * way to do that is to ask what the mechanism had to repair.
189
+ *
190
+ * Same shape as `recordEdgeCoverage`/`untestedRules` next door, for the same reason: a
191
+ * disclosure channel that a test can assert on without the production path knowing it exists.
192
+ */
193
+ const collisions = [];
194
+ /** Every subject a rule produced more than once, since the last reset. */
195
+ export function identityCollisions() {
196
+ return collisions;
197
+ }
198
+ /** Clear the record — call in a `beforeEach` when asserting on a specific rule. */
199
+ export function resetIdentityCollisions() {
200
+ collisions.length = 0;
201
+ }
202
+ registerCacheReset(resetIdentityCollisions);
203
+ export function disambiguateIdentities(violations) {
204
+ const counts = new Map();
205
+ for (const violation of violations) {
206
+ const key = groupKeyOf(violation);
207
+ counts.set(key, (counts.get(key) ?? 0) + 1);
208
+ }
209
+ // Nothing collides: return the input untouched rather than rebuilding it, so the
210
+ // overwhelmingly common case costs one pass and no allocation.
211
+ let anyDuplicate = false;
212
+ for (const count of counts.values()) {
213
+ if (count > 1) {
214
+ anyDuplicate = true;
215
+ break;
216
+ }
217
+ }
218
+ if (!anyDuplicate)
219
+ return violations;
220
+ // Disclose what had to be repaired, so a guard can still measure the PRODUCER — see
221
+ // `identityCollisions`. Recorded before the repair, because afterwards there is nothing left
222
+ // to see, which is precisely how this mechanism disarmed the existing collision guards.
223
+ for (const [key, count] of counts) {
224
+ if (count > 1) {
225
+ const split = key.indexOf('::');
226
+ collisions.push({
227
+ rule: key.slice(0, split),
228
+ subject: key.slice(split + 2),
229
+ findings: count,
230
+ });
231
+ }
232
+ }
233
+ // Every key present before suffixing, so a generated `#n` can never land on one a producer
234
+ // already emits. Without it, a rule holding `X` twice plus a literal `X#1` closes one
235
+ // collision by opening another — and `taken.add` below is what keeps that true for the
236
+ // THIRD member: without it, `[X, X, X, X#1]` yields `X, X#2, X#2, X#1`, reintroducing the
237
+ // collision this function exists to remove. Measured; it takes a group of three to reach,
238
+ // which is why the guard for it carries three.
239
+ const taken = new Set(counts.keys());
240
+ const seen = new Map();
241
+ return violations.map((violation) => {
242
+ const key = groupKeyOf(violation);
243
+ if ((counts.get(key) ?? 0) < 2)
244
+ return violation;
245
+ const occurrence = (seen.get(key) ?? 0) + 1;
246
+ seen.set(key, occurrence);
247
+ // The first keeps its subject verbatim — this is what makes the migration empty.
248
+ if (occurrence === 1)
249
+ return violation;
250
+ // The suffix goes on the SUBJECT, because that is what `identity` is; the reservation is
251
+ // checked on the group key, because that is what collides.
252
+ const subject = subjectOf(violation);
253
+ let suffix = occurrence - 1;
254
+ let candidate = `${subject}#${String(suffix)}`;
255
+ while (taken.has(`${violation.rule}::${candidate}`)) {
256
+ suffix += 1;
257
+ candidate = `${subject}#${String(suffix)}`;
258
+ }
259
+ taken.add(`${violation.rule}::${candidate}`);
260
+ return { ...violation, identity: candidate };
261
+ });
262
+ }
2
263
  //# sourceMappingURL=violation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"violation.js","sourceRoot":"","sources":["../src/violation.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"violation.js","sourceRoot":"","sources":["../src/violation.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nielspeter/eess",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "eess kernel — the dialect-independent rule engine (RuleBuilder, Predicate, Condition, baseline, formatters). Specifications you can run.",
5
5
  "type": "module",
6
6
  "exports": {