@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
package/dist/define.js CHANGED
@@ -12,9 +12,41 @@
12
12
  *
13
13
  * classes(p).that().satisfy(isAbstract).should().beExported().check()
14
14
  * ```
15
+ *
16
+ * ## Declaring a glob
17
+ *
18
+ * If the predicate matches paths against a glob, pass it as `globs` — otherwise
19
+ * `doctor` cannot see it, and a typo'd glob narrows the selection to nothing
20
+ * while reporting a clean bill of health. This is a **selector**-position glob,
21
+ * the one position that is acted on today, which is why the omission was bug
22
+ * 0030's higher-severity half.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const inGeneratedFolder = definePredicate<SourceFile>(
27
+ * "reside in '**\/generated/**'",
28
+ * (file) => picomatch('**\/generated/**')(file.getFilePath()),
29
+ * globNode({ glob: '**\/generated/**', kind: 'file-path' }),
30
+ * )
31
+ * ```
32
+ *
33
+ * The `kind` must say what the glob is really matched against, because it
34
+ * selects which paths the glob is checked for satisfiability:
35
+ *
36
+ * - `file-path` — an **absolute** file path. Anchor the glob (`'**\/src/**'`,
37
+ * not `'src/**'`), or it matches nothing.
38
+ * - `parent-dir` — the immediate parent directory of a file.
39
+ * - `import-target` — a resolved module path **or a bare specifier**.
40
+ * Deliberately has no path universe, so `'fastify'` is never reported dead.
41
+ * - `specifier` / `literal` — a string in the source rather than a path.
42
+ *
43
+ * A wrong `kind` is believed: declaring a bare specifier as `file-path` earns a
44
+ * false dead-glob report, and declaring a real path as `import-target` silently
45
+ * exempts it from checking. When unsure, the honest choice is to declare
46
+ * nothing — the same position as before this parameter existed.
15
47
  */
16
- export function definePredicate(description, test) {
17
- return { description, test };
48
+ export function definePredicate(description, test, globs) {
49
+ return { description, test, globs };
18
50
  }
19
51
  /**
20
52
  * Create a custom condition for use in `.should().satisfy()` chains.
@@ -35,8 +67,17 @@ export function definePredicate(description, test) {
35
67
  *
36
68
  * classes(p).that().extend('Base').should().satisfy(useSharedHelper).check()
37
69
  * ```
70
+ *
71
+ * ## Declaring a glob
72
+ *
73
+ * As with `definePredicate`, pass `globs` if the condition matches paths against
74
+ * one — see that function for the `kind` values and what a wrong one costs. The
75
+ * difference is what happens next: a **condition**-position glob is deliberately
76
+ * *not* reported when it matches nothing, because a denylist glob matching
77
+ * nothing is indistinguishable from a ban being respected. Declaring it makes it
78
+ * visible to `globs()` and `explain`; it does not make it a finding.
38
79
  */
39
- export function defineCondition(description, evaluate) {
40
- return { description, evaluate };
80
+ export function defineCondition(description, evaluate, globs) {
81
+ return { description, evaluate, globs };
41
82
  }
42
83
  //# sourceMappingURL=define.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAmB,EACnB,IAA6B;IAE7B,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;AAC9B,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAmB,EACnB,QAAuE;IAEvE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAA;AAClC,CAAC"}
1
+ {"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAmB,EACnB,IAA6B,EAC7B,KAAqB;IAErB,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAmB,EACnB,QAAuE,EACvE,KAAqB;IAErB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;AACzC,CAAC"}
@@ -14,6 +14,14 @@ export declare class DiffFilter {
14
14
  /**
15
15
  * Filter violations to only those in changed files.
16
16
  * If changedFiles is null (git error), returns all violations unfiltered.
17
+ *
18
+ * A `bypassFilters` configuration finding (ADR-010's zero-examined /
19
+ * expired-`.expectEmpty()` violations) is never filtered here, even
20
+ * though its `file` is always `''` and can never be a member of
21
+ * `changedFiles`. Without this, "unsuppressable" was false for the most
22
+ * realistic adoption path: a rule whose instrument silently broke would
23
+ * report nothing at all under `--changed`, with no diagnostic that
24
+ * anything was ever hidden.
17
25
  */
18
26
  filterToChanged(violations: ArchViolation[]): ArchViolation[];
19
27
  /** Number of changed files detected, or -1 if diff unavailable */
@@ -1 +1 @@
1
- {"version":3,"file":"diff-aware.d.ts","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;gBAErC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI;IAI5C;;;OAGG;IACH,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE;IAM7D,kEAAkE;IAClE,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,UAAU,GAAE,MAAe,GAAG,UAAU,CA6BjE"}
1
+ {"version":3,"file":"diff-aware.d.ts","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnD;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;gBAErC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI;IAI5C;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE;IAM7D,kEAAkE;IAClE,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,UAAU,GAAE,MAAe,GAAG,UAAU,CA6BjE"}
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import path from 'node:path';
3
+ import { writeStderr } from './stderr.js';
3
4
  /**
4
5
  * A diff filter that restricts violation reporting to files
5
6
  * changed since a base branch.
@@ -17,12 +18,20 @@ export class DiffFilter {
17
18
  /**
18
19
  * Filter violations to only those in changed files.
19
20
  * If changedFiles is null (git error), returns all violations unfiltered.
21
+ *
22
+ * A `bypassFilters` configuration finding (ADR-010's zero-examined /
23
+ * expired-`.expectEmpty()` violations) is never filtered here, even
24
+ * though its `file` is always `''` and can never be a member of
25
+ * `changedFiles`. Without this, "unsuppressable" was false for the most
26
+ * realistic adoption path: a rule whose instrument silently broke would
27
+ * report nothing at all under `--changed`, with no diagnostic that
28
+ * anything was ever hidden.
20
29
  */
21
30
  filterToChanged(violations) {
22
31
  const files = this.changedFiles;
23
32
  if (files === null)
24
33
  return violations;
25
- return violations.filter((v) => files.has(v.file));
34
+ return violations.filter((v) => v.bypassFilters === true || files.has(v.file));
26
35
  }
27
36
  /** Number of changed files detected, or -1 if diff unavailable */
28
37
  get size() {
@@ -56,7 +65,7 @@ export function diffAware(baseBranch = 'main') {
56
65
  catch (err) {
57
66
  // Not a git repo, or base branch doesn't exist — skip filtering (report all violations)
58
67
  const detail = err instanceof Error ? ` (${err.message.split('\n')[0] ?? ''})` : '';
59
- console.warn(`[eess] Could not run git diff against '${baseBranch}'${detail}. All violations will be reported.`);
68
+ writeStderr(`[eess] Could not run git diff against '${baseBranch}'${detail}. All violations will be reported.`);
60
69
  return new DiffFilter(null);
61
70
  }
62
71
  if (output === '') {
@@ -1 +1 @@
1
- {"version":3,"file":"diff-aware.js","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,IAAI,MAAM,WAAW,CAAA;AAG5B;;;;;;;;GAQG;AACH,MAAM,OAAO,UAAU;IACJ,YAAY,CAAoB;IAEjD,YAAY,YAAgC;QAC1C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;IAClC,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,UAA2B;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAA;QAC/B,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,UAAU,CAAA;QACrC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACpD,CAAC;IAED,kEAAkE;IAClE,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAA;IACjE,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAC,aAAqB,MAAM;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAEzB,IAAI,MAAc,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,SAAS,CAAC,EAAE;YAC5E,QAAQ,EAAE,OAAO;YACjB,GAAG;YACH,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC,IAAI,EAAE,CAAA;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wFAAwF;QACxF,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;QACnF,OAAO,CAAC,IAAI,CACV,0CAA0C,UAAU,IAAI,MAAM,oCAAoC,CACnG,CAAA;QACD,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAClB,wFAAwF;QACxF,OAAO,IAAI,UAAU,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;IAClC,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAC1B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAC1E,CAAA;IAED,OAAO,IAAI,UAAU,CAAC,YAAY,CAAC,CAAA;AACrC,CAAC"}
1
+ {"version":3,"file":"diff-aware.js","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC;;;;;;;;GAQG;AACH,MAAM,OAAO,UAAU;IACJ,YAAY,CAAoB;IAEjD,YAAY,YAAgC;QAC1C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;IAClC,CAAC;IAED;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,UAA2B;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAA;QAC/B,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,UAAU,CAAA;QACrC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAChF,CAAC;IAED,kEAAkE;IAClE,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAA;IACjE,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAC,aAAqB,MAAM;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAEzB,IAAI,MAAc,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,SAAS,CAAC,EAAE;YAC5E,QAAQ,EAAE,OAAO;YACjB,GAAG;YACH,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC,IAAI,EAAE,CAAA;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wFAAwF;QACxF,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;QACnF,WAAW,CACT,0CAA0C,UAAU,IAAI,MAAM,oCAAoC,CACnG,CAAA;QACD,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAClB,wFAAwF;QACxF,OAAO,IAAI,UAAU,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;IAClC,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAC1B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAC1E,CAAA;IAED,OAAO,IAAI,UAAU,CAAC,YAAY,CAAC,CAAA;AACrC,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * What diff-aware mode suppressed, said out loud.
3
+ *
4
+ * Plan 0071's first instrument. `--changed` (and `check({ diff })`) filter
5
+ * **reporting**, not evaluation — so a run with every finding suppressed is
6
+ * indistinguishable from a clean run: exit 0, no output, `total: 0`. That is
7
+ * the false green ADR-008 exists to prevent, and it is worse here than
8
+ * elsewhere because the reader chose the flag and then forgot it: CI configures
9
+ * `--changed` once and every subsequent run reads as clean.
10
+ *
11
+ * **The count is derived by the caller, never self-reported.** Each call site
12
+ * computes `before.length - after.length` around its own `filterToChanged`
13
+ * call. `DiffFilter` could keep that tally itself, and it deliberately does
14
+ * not: `CheckOptions.diff` is a structural interface (`DiffFilterLike`), so a
15
+ * consumer can pass any object with a `filterToChanged` method, and a filter
16
+ * that under-reports its own suppression would produce exactly the silence
17
+ * this module exists to break. Caller-side subtraction is a differently-derived
18
+ * value (ADR-009 rule 5) and it works for filters we did not write.
19
+ *
20
+ * **Two surfaces, because only one of them can count.**
21
+ *
22
+ * | surface | disclosure |
23
+ * | ---------------------------------------- | -------------------- |
24
+ * | CLI `check --changed` | `suppressionNotice` |
25
+ * | `checkAll([...])` | `suppressionNotice` |
26
+ * | `.check({ diff })` / `.warn({ diff })` | `activeNotice`, once |
27
+ *
28
+ * The first two filter once over every collected violation, so they know the
29
+ * whole number. The per-rule terminals do not: `filterToChanged` is called
30
+ * once per rule (`execute-rule.ts`), and a diff-aware suite with 79 rules
31
+ * would print 79 lines — on the channel v0.26.0 made unconditionally visible.
32
+ * There is no per-run aggregation point on that path, so those terminals state
33
+ * the **configuration** instead, once per process. A configuration statement
34
+ * cannot be wrong; a per-rule count presented as a run total would be.
35
+ *
36
+ * A `process.on('exit')` flush was considered and rejected: it would let the
37
+ * per-rule path print one complete total, but a library that installs an exit
38
+ * hook to deliver a correctness-relevant message has made that message
39
+ * dependent on stderr surviving process teardown — untestable in the shape the
40
+ * rest of this project asserts (a real child `vitest run`, bug 0024) without
41
+ * asserting a race. Stating the configuration is weaker and honest.
42
+ */
43
+ /**
44
+ * One line naming what was suppressed, or `undefined` when nothing was.
45
+ *
46
+ * `undefined` rather than an empty string so a call site cannot print a blank
47
+ * line by forgetting to check — the two callers both feed this straight into
48
+ * `writeStderr`.
49
+ *
50
+ * @param suppressed - `before.length - after.length`, computed by the caller.
51
+ * @param changedFiles - How many files the filter considered changed, when the
52
+ * filter exposes it. A user-supplied `DiffFilterLike` need not, in which case
53
+ * the sentence drops that clause rather than guessing.
54
+ * @param baseBranch - The branch diffed against, when known.
55
+ */
56
+ export declare function suppressionNotice(suppressed: number, changedFiles?: number, baseBranch?: string): string | undefined;
57
+ /**
58
+ * State that diff-aware filtering is on, once per process. Returns `undefined`
59
+ * on every call after the first, and whenever nothing was suppressed.
60
+ *
61
+ * Keyed on a call that actually suppressed something: a diff-aware run whose
62
+ * findings all happen to be in changed files has hidden nothing, and a notice
63
+ * there would train the reader to skip the line that matters.
64
+ */
65
+ export declare function activeNotice(suppressed: number, changedFiles?: number, baseBranch?: string): string | undefined;
66
+ /**
67
+ * Forget that {@link activeNotice} has spoken. **Tests only** — a suite
68
+ * asserting the once-per-process contract has to observe the first call, and
69
+ * whichever earlier test filtered first would otherwise have consumed it.
70
+ */
71
+ export declare function resetDiffDisclosureForTests(): void;
72
+ //# sourceMappingURL=diff-disclosure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-disclosure.d.ts","sourceRoot":"","sources":["../src/diff-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAKH;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CAkBpB;AAYD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CAepB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAElD"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * What diff-aware mode suppressed, said out loud.
3
+ *
4
+ * Plan 0071's first instrument. `--changed` (and `check({ diff })`) filter
5
+ * **reporting**, not evaluation — so a run with every finding suppressed is
6
+ * indistinguishable from a clean run: exit 0, no output, `total: 0`. That is
7
+ * the false green ADR-008 exists to prevent, and it is worse here than
8
+ * elsewhere because the reader chose the flag and then forgot it: CI configures
9
+ * `--changed` once and every subsequent run reads as clean.
10
+ *
11
+ * **The count is derived by the caller, never self-reported.** Each call site
12
+ * computes `before.length - after.length` around its own `filterToChanged`
13
+ * call. `DiffFilter` could keep that tally itself, and it deliberately does
14
+ * not: `CheckOptions.diff` is a structural interface (`DiffFilterLike`), so a
15
+ * consumer can pass any object with a `filterToChanged` method, and a filter
16
+ * that under-reports its own suppression would produce exactly the silence
17
+ * this module exists to break. Caller-side subtraction is a differently-derived
18
+ * value (ADR-009 rule 5) and it works for filters we did not write.
19
+ *
20
+ * **Two surfaces, because only one of them can count.**
21
+ *
22
+ * | surface | disclosure |
23
+ * | ---------------------------------------- | -------------------- |
24
+ * | CLI `check --changed` | `suppressionNotice` |
25
+ * | `checkAll([...])` | `suppressionNotice` |
26
+ * | `.check({ diff })` / `.warn({ diff })` | `activeNotice`, once |
27
+ *
28
+ * The first two filter once over every collected violation, so they know the
29
+ * whole number. The per-rule terminals do not: `filterToChanged` is called
30
+ * once per rule (`execute-rule.ts`), and a diff-aware suite with 79 rules
31
+ * would print 79 lines — on the channel v0.26.0 made unconditionally visible.
32
+ * There is no per-run aggregation point on that path, so those terminals state
33
+ * the **configuration** instead, once per process. A configuration statement
34
+ * cannot be wrong; a per-rule count presented as a run total would be.
35
+ *
36
+ * A `process.on('exit')` flush was considered and rejected: it would let the
37
+ * per-rule path print one complete total, but a library that installs an exit
38
+ * hook to deliver a correctness-relevant message has made that message
39
+ * dependent on stderr surviving process teardown — untestable in the shape the
40
+ * rest of this project asserts (a real child `vitest run`, bug 0024) without
41
+ * asserting a race. Stating the configuration is weaker and honest.
42
+ */
43
+ /** Prefix shared with the rest of the library's stderr output. */
44
+ const TAG = '[eess]';
45
+ /**
46
+ * One line naming what was suppressed, or `undefined` when nothing was.
47
+ *
48
+ * `undefined` rather than an empty string so a call site cannot print a blank
49
+ * line by forgetting to check — the two callers both feed this straight into
50
+ * `writeStderr`.
51
+ *
52
+ * @param suppressed - `before.length - after.length`, computed by the caller.
53
+ * @param changedFiles - How many files the filter considered changed, when the
54
+ * filter exposes it. A user-supplied `DiffFilterLike` need not, in which case
55
+ * the sentence drops that clause rather than guessing.
56
+ * @param baseBranch - The branch diffed against, when known.
57
+ */
58
+ export function suppressionNotice(suppressed, changedFiles, baseBranch) {
59
+ if (suppressed <= 0)
60
+ return undefined;
61
+ const findings = suppressed === 1 ? 'finding' : 'findings';
62
+ // A negative size is `DiffFilter`'s "git was unavailable" sentinel. That path
63
+ // filters nothing, so `suppressed` is 0 and we never reach here — but the
64
+ // guard is cheap and keeps the sentinel from ever reaching a user's screen.
65
+ const scope = changedFiles !== undefined && changedFiles >= 0
66
+ ? ` outside the ${String(changedFiles)} changed ${changedFiles === 1 ? 'file' : 'files'}`
67
+ : ' outside the changed files';
68
+ const since = baseBranch === undefined ? '' : ` (diffed against '${baseBranch}')`;
69
+ return (`${TAG} Diff-aware mode suppressed ${String(suppressed)} ${findings}${scope}${since}. ` +
70
+ `Those findings are real and still present — this run did not check them. ` +
71
+ `Re-run without diff-aware mode to see every finding.`);
72
+ }
73
+ /**
74
+ * Whether {@link activeNotice} has already spoken in this process.
75
+ *
76
+ * Module-scoped rather than per-`DiffFilter`, because the documented pattern
77
+ * constructs a filter **inline per rule** — `.check({ diff: diffAware('main') })`
78
+ * in `docs/core-concepts.md:333` and `docs/what-to-check.md:515` — so
79
+ * per-instance state would still produce one line per rule.
80
+ */
81
+ let noticed = false;
82
+ /**
83
+ * State that diff-aware filtering is on, once per process. Returns `undefined`
84
+ * on every call after the first, and whenever nothing was suppressed.
85
+ *
86
+ * Keyed on a call that actually suppressed something: a diff-aware run whose
87
+ * findings all happen to be in changed files has hidden nothing, and a notice
88
+ * there would train the reader to skip the line that matters.
89
+ */
90
+ export function activeNotice(suppressed, changedFiles, baseBranch) {
91
+ if (suppressed <= 0 || noticed)
92
+ return undefined;
93
+ noticed = true;
94
+ const scope = changedFiles !== undefined && changedFiles >= 0
95
+ ? `the ${String(changedFiles)} ${changedFiles === 1 ? 'file' : 'files'} changed`
96
+ : 'the files changed';
97
+ const since = baseBranch === undefined ? '' : ` since '${baseBranch}'`;
98
+ return (`${TAG} Diff-aware mode is active: this run reports only findings in ${scope}${since}. ` +
99
+ `Findings in unchanged files are suppressed and will not appear, however many there are. ` +
100
+ `Remove \`diff\` from the check options to see every finding.`);
101
+ }
102
+ /**
103
+ * Forget that {@link activeNotice} has spoken. **Tests only** — a suite
104
+ * asserting the once-per-process contract has to observe the first call, and
105
+ * whichever earlier test filtered first would otherwise have consumed it.
106
+ */
107
+ export function resetDiffDisclosureForTests() {
108
+ noticed = false;
109
+ }
110
+ //# sourceMappingURL=diff-disclosure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-disclosure.js","sourceRoot":"","sources":["../src/diff-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,kEAAkE;AAClE,MAAM,GAAG,GAAG,QAAQ,CAAA;AAEpB;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAkB,EAClB,YAAqB,EACrB,UAAmB;IAEnB,IAAI,UAAU,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IAErC,MAAM,QAAQ,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAA;IAC1D,8EAA8E;IAC9E,0EAA0E;IAC1E,4EAA4E;IAC5E,MAAM,KAAK,GACT,YAAY,KAAK,SAAS,IAAI,YAAY,IAAI,CAAC;QAC7C,CAAC,CAAC,gBAAgB,MAAM,CAAC,YAAY,CAAC,YAAY,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE;QACzF,CAAC,CAAC,4BAA4B,CAAA;IAClC,MAAM,KAAK,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,qBAAqB,UAAU,IAAI,CAAA;IAEjF,OAAO,CACL,GAAG,GAAG,+BAA+B,MAAM,CAAC,UAAU,CAAC,IAAI,QAAQ,GAAG,KAAK,GAAG,KAAK,IAAI;QACvF,2EAA2E;QAC3E,sDAAsD,CACvD,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,IAAI,OAAO,GAAG,KAAK,CAAA;AAEnB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,UAAkB,EAClB,YAAqB,EACrB,UAAmB;IAEnB,IAAI,UAAU,IAAI,CAAC,IAAI,OAAO;QAAE,OAAO,SAAS,CAAA;IAChD,OAAO,GAAG,IAAI,CAAA;IAEd,MAAM,KAAK,GACT,YAAY,KAAK,SAAS,IAAI,YAAY,IAAI,CAAC;QAC7C,CAAC,CAAC,OAAO,MAAM,CAAC,YAAY,CAAC,IAAI,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,UAAU;QAChF,CAAC,CAAC,mBAAmB,CAAA;IACzB,MAAM,KAAK,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,UAAU,GAAG,CAAA;IAEtE,OAAO,CACL,GAAG,GAAG,iEAAiE,KAAK,GAAG,KAAK,IAAI;QACxF,0FAA0F;QAC1F,8DAA8D,CAC/D,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,GAAG,KAAK,CAAA;AACjB,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * What the filesystem says about a glob that the compiler's file set does
3
+ * not — the shape of the data, not how to build it.
4
+ *
5
+ * `OnDisk`/`DiskSet` are pure: a materialized `DiskSet` is just a closure
6
+ * over already-walked filesystem data, so the type stays in the kernel.
7
+ * Walking a real project's filesystem needs Node's `fs`/`path` and the
8
+ * dialect's own project type to find the root, so the MATERIALIZER lives in
9
+ * that dialect (e.g. `packages/ts/src/core/disk-set.ts`'s `diskSet()`).
10
+ */
11
+ /**
12
+ * What the filesystem knows that the compiler's file set does not.
13
+ *
14
+ * Distinguishes "your glob is misspelled" from "your glob is fine and your
15
+ * tsconfig scope excludes it" — the cheapest wrong action an agent can take
16
+ * on a dead-glob finding, and the common case in a real monorepo where a
17
+ * build-output or docs directory sits outside the loaded project.
18
+ */
19
+ export type OnDisk = 'holds-typescript' | 'no-typescript' | 'absent' | 'not-determined';
20
+ /**
21
+ * What the filesystem knows that the compiler's file set does not.
22
+ */
23
+ export interface DiskSet {
24
+ /** Classify a glob by what exists on disk under the paths it matches. */
25
+ classify(glob: string): OnDisk;
26
+ }
27
+ //# sourceMappingURL=disk-set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk-set.d.ts","sourceRoot":"","sources":["../src/disk-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,kBAAkB,GAAG,eAAe,GAAG,QAAQ,GAAG,gBAAgB,CAAA;AAEvF;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAC/B"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * What the filesystem says about a glob that the compiler's file set does
3
+ * not — the shape of the data, not how to build it.
4
+ *
5
+ * `OnDisk`/`DiskSet` are pure: a materialized `DiskSet` is just a closure
6
+ * over already-walked filesystem data, so the type stays in the kernel.
7
+ * Walking a real project's filesystem needs Node's `fs`/`path` and the
8
+ * dialect's own project type to find the root, so the MATERIALIZER lives in
9
+ * that dialect (e.g. `packages/ts/src/core/disk-set.ts`'s `diskSet()`).
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=disk-set.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk-set.js","sourceRoot":"","sources":["../src/disk-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * How much evidence an allowlist condition actually had — bug 0015.
3
+ *
4
+ * The `only*` family constrains **edges**, not subjects. Each iterates a
5
+ * subject's imports (or importers) and reports one violation per edge outside
6
+ * the allowlist, so a subject with **zero edges has nothing to violate and
7
+ * passes**, however broken the allowlist. Measured: one file with no imports,
8
+ * `onlyImportFrom('**\/nowhere/**')` → 1 subject selected, 0 violations.
9
+ *
10
+ * The shape it fails on is the target case, not an edge case: in a layered
11
+ * architecture the innermost layer is the one an allowlist protects and is
12
+ * characteristically the layer with the fewest outbound imports.
13
+ *
14
+ * ## Why this reports rather than fails
15
+ *
16
+ * [ts-archunit Bug 0015](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0015-allowlist-conditions-pass-vacuously-on-edgeless-subjects.md)
17
+ * refuted failing, per-subject and per-rule, on measurement:
18
+ *
19
+ * - **Per-subject has no statable remedy.** 14 of this repo's `src/` files have
20
+ * zero static imports and 10 are pure leaf modules. `tarjan.ts` is a
21
+ * dependency-free algorithm — the ideal innermost-layer citizen — and would
22
+ * fail at error severity. The remedies available are: add an import (harmful),
23
+ * exclude a working rule, narrow the selector, or delete the rule. None
24
+ * improves anything, because **for the `only*` family zero edges is maximal
25
+ * compliance**, not absent evidence. That fails ADR-009 rule 2.
26
+ * - **Per-rule multiplies.** Six boundaries and one dependency-free shared file
27
+ * make `strictBoundaries` emit 13 rules, **12** with subjects and zero edges.
28
+ * - **`ignoreTypeImports` inverts it.** Counting edges after the filter means a
29
+ * layer whose only dependency is `import type` counts zero and fires on the
30
+ * best possible outcome — under the option the docs recommend for layers.
31
+ *
32
+ * So the remedy is genuinely optional and the reader must judge it, which under
33
+ * ADR-009 rule 1 is a disclosure, not a failure. This module is the disclosure.
34
+ *
35
+ * ## Why module state rather than a field on `ConditionContext`
36
+ *
37
+ * `ConditionContext` is a **public exported type** backing the documented
38
+ * `defineCondition()`, and bug 0015 records that extending `Condition<T>` for
39
+ * this was one reason the failing design could not ship. `diff-disclosure.ts`
40
+ * established the pattern for a run-scoped notice that conditions produce and
41
+ * the CLI consumes; this follows it, including the test reset.
42
+ */
43
+ /**
44
+ * Why a rule tested no edges. The three are not interchangeable, and saying the
45
+ * wrong one is an ADR-009 rule 2 defect — a stated cause that is wrong for the
46
+ * input.
47
+ *
48
+ * The first version of this module printed "correct for a genuinely
49
+ * dependency-free module" for all three. Measured, that is false for two of
50
+ * them: a subject whose imports were filtered by `ignoreTypeImports` **has**
51
+ * dependencies, and a reader who opens the folder finds them and concludes the
52
+ * tool is broken; and a rule whose allowlist matched no import is pointing at
53
+ * the interesting case — the glob may be a typo — which that sentence hides.
54
+ */
55
+ export type UntestedReason =
56
+ /** The subjects have no edges of any kind. Usually correct, and the layered case. */
57
+ 'no-edges'
58
+ /** Edges existed, and every one was filtered out before the check saw it. */
59
+ | 'all-filtered'
60
+ /** Edges existed, and none of them matched the allowlist glob. */
61
+ | 'none-matched';
62
+ /** One rule's evidence: how many subjects it saw and how many edges it tested. */
63
+ export interface EdgeCoverage {
64
+ readonly rule: string;
65
+ readonly subjects: number;
66
+ readonly edges: number;
67
+ readonly reason: UntestedReason;
68
+ }
69
+ /**
70
+ * Record what a condition had to work with.
71
+ *
72
+ * Keyed on the rule description, and **accumulated** rather than overwritten:
73
+ * one rule can evaluate its condition more than once (a `.check()` after a
74
+ * `.violations()`, or the same description in two files), and taking the last
75
+ * write would report the smaller run.
76
+ */
77
+ export declare function recordEdgeCoverage(rule: string, subjects: number, edges: number, reason?: UntestedReason): void;
78
+ /**
79
+ * Rules that had subjects and tested no edges — the vacuous passes.
80
+ *
81
+ * A rule with **no subjects** is deliberately excluded: that is an empty
82
+ * selector, which is `.expectNonEmpty()`'s business and plan 0067's, and
83
+ * reporting it here would give one fault two owners.
84
+ */
85
+ export declare function untestedRules(): readonly EdgeCoverage[];
86
+ /**
87
+ * The disclosure, or `undefined` when every allowlist was exercised.
88
+ *
89
+ * Names the rules rather than counting them (ADR-009 rule 4): "3 rules tested
90
+ * nothing" sends the reader to grep, and the whole point is that they must
91
+ * judge whether an edge-free population is correct here.
92
+ */
93
+ export declare function edgeCoverageNotice(): string | undefined;
94
+ /** Drop the tally. The CLI resets per run; tests reset per case. */
95
+ export declare function resetEdgeCoverage(): void;
96
+ //# sourceMappingURL=edge-coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-coverage.d.ts","sourceRoot":"","sources":["../src/edge-coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc;AACxB,qFAAqF;AACnF,UAAU;AACZ,6EAA6E;GAC3E,cAAc;AAChB,kEAAkE;GAChE,cAAc,CAAA;AAElB,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;CAChC;AAID;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,cAA2B,GAClC,IAAI,CAeN;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,SAAS,YAAY,EAAE,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CA0BvD;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * How much evidence an allowlist condition actually had — bug 0015.
3
+ *
4
+ * The `only*` family constrains **edges**, not subjects. Each iterates a
5
+ * subject's imports (or importers) and reports one violation per edge outside
6
+ * the allowlist, so a subject with **zero edges has nothing to violate and
7
+ * passes**, however broken the allowlist. Measured: one file with no imports,
8
+ * `onlyImportFrom('**\/nowhere/**')` → 1 subject selected, 0 violations.
9
+ *
10
+ * The shape it fails on is the target case, not an edge case: in a layered
11
+ * architecture the innermost layer is the one an allowlist protects and is
12
+ * characteristically the layer with the fewest outbound imports.
13
+ *
14
+ * ## Why this reports rather than fails
15
+ *
16
+ * [ts-archunit Bug 0015](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0015-allowlist-conditions-pass-vacuously-on-edgeless-subjects.md)
17
+ * refuted failing, per-subject and per-rule, on measurement:
18
+ *
19
+ * - **Per-subject has no statable remedy.** 14 of this repo's `src/` files have
20
+ * zero static imports and 10 are pure leaf modules. `tarjan.ts` is a
21
+ * dependency-free algorithm — the ideal innermost-layer citizen — and would
22
+ * fail at error severity. The remedies available are: add an import (harmful),
23
+ * exclude a working rule, narrow the selector, or delete the rule. None
24
+ * improves anything, because **for the `only*` family zero edges is maximal
25
+ * compliance**, not absent evidence. That fails ADR-009 rule 2.
26
+ * - **Per-rule multiplies.** Six boundaries and one dependency-free shared file
27
+ * make `strictBoundaries` emit 13 rules, **12** with subjects and zero edges.
28
+ * - **`ignoreTypeImports` inverts it.** Counting edges after the filter means a
29
+ * layer whose only dependency is `import type` counts zero and fires on the
30
+ * best possible outcome — under the option the docs recommend for layers.
31
+ *
32
+ * So the remedy is genuinely optional and the reader must judge it, which under
33
+ * ADR-009 rule 1 is a disclosure, not a failure. This module is the disclosure.
34
+ *
35
+ * ## Why module state rather than a field on `ConditionContext`
36
+ *
37
+ * `ConditionContext` is a **public exported type** backing the documented
38
+ * `defineCondition()`, and bug 0015 records that extending `Condition<T>` for
39
+ * this was one reason the failing design could not ship. `diff-disclosure.ts`
40
+ * established the pattern for a run-scoped notice that conditions produce and
41
+ * the CLI consumes; this follows it, including the test reset.
42
+ */
43
+ const coverage = new Map();
44
+ /**
45
+ * Record what a condition had to work with.
46
+ *
47
+ * Keyed on the rule description, and **accumulated** rather than overwritten:
48
+ * one rule can evaluate its condition more than once (a `.check()` after a
49
+ * `.violations()`, or the same description in two files), and taking the last
50
+ * write would report the smaller run.
51
+ */
52
+ export function recordEdgeCoverage(rule, subjects, edges, reason = 'no-edges') {
53
+ const prior = coverage.get(rule);
54
+ // The MINIMUM edge count, not the maximum. Two evaluations of the same rule
55
+ // description — the same preset over two projects, or the same chain written
56
+ // twice — merge on this key, and taking the max let a run that tested edges
57
+ // erase a run that tested none. Measured: the vacuous run vanished from the
58
+ // report. For a disclosure OF vacuity the conservative direction is to keep
59
+ // the smaller evidence; over-disclosing costs a footnote, under-disclosing is
60
+ // the silent pass this module exists to surface.
61
+ coverage.set(rule, {
62
+ rule,
63
+ subjects: Math.max(prior?.subjects ?? 0, subjects),
64
+ edges: prior === undefined ? edges : Math.min(prior.edges, edges),
65
+ reason: prior !== undefined && prior.edges <= edges ? prior.reason : reason,
66
+ });
67
+ }
68
+ /**
69
+ * Rules that had subjects and tested no edges — the vacuous passes.
70
+ *
71
+ * A rule with **no subjects** is deliberately excluded: that is an empty
72
+ * selector, which is `.expectNonEmpty()`'s business and plan 0067's, and
73
+ * reporting it here would give one fault two owners.
74
+ */
75
+ export function untestedRules() {
76
+ return [...coverage.values()].filter((c) => c.subjects > 0 && c.edges === 0);
77
+ }
78
+ /**
79
+ * The disclosure, or `undefined` when every allowlist was exercised.
80
+ *
81
+ * Names the rules rather than counting them (ADR-009 rule 4): "3 rules tested
82
+ * nothing" sends the reader to grep, and the whole point is that they must
83
+ * judge whether an edge-free population is correct here.
84
+ */
85
+ export function edgeCoverageNotice() {
86
+ const untested = untestedRules();
87
+ if (untested.length === 0)
88
+ return undefined;
89
+ const because = {
90
+ 'no-edges': 'those subjects have no imports at all, which is correct for a dependency-free module and ' +
91
+ 'means the rule certified nothing otherwise',
92
+ 'all-filtered': 'those subjects DO have imports — every one was excluded by `ignoreTypeImports` before the ' +
93
+ 'check saw it, so the allowlist was never consulted',
94
+ 'none-matched': 'those subjects have imports, and none matched the allowlist glob — so either nothing is ' +
95
+ 'in scope by design, or the glob is wrong',
96
+ };
97
+ const lines = untested.map((c) => ` - ${c.rule}\n ${String(c.subjects)} subject${c.subjects === 1 ? '' : 's'}, 0 edges tested — ${because[c.reason]}.`);
98
+ const count = untested.length;
99
+ return (`[eess] ${String(count)} allowlist rule${count === 1 ? '' : 's'} passed without ` +
100
+ `testing a single edge:\n${lines.join('\n')}\n` +
101
+ ` An allowlist constrains edges, so a subject with none cannot violate it. Only you can ` +
102
+ `tell an intended shape from a rule that certified nothing.`);
103
+ }
104
+ /** Drop the tally. The CLI resets per run; tests reset per case. */
105
+ export function resetEdgeCoverage() {
106
+ coverage.clear();
107
+ }
108
+ //# sourceMappingURL=edge-coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-coverage.js","sourceRoot":"","sources":["../src/edge-coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AA8BH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,QAAgB,EAChB,KAAa,EACb,SAAyB,UAAU;IAEnC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAChC,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,4EAA4E;IAC5E,8EAA8E;IAC9E,iDAAiD;IACjD,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE;QACjB,IAAI;QACJ,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,IAAI,CAAC,EAAE,QAAQ,CAAC;QAClD,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC;QACjE,MAAM,EAAE,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;KAC5E,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,QAAQ,GAAG,aAAa,EAAE,CAAA;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAE3C,MAAM,OAAO,GAAmC;QAC9C,UAAU,EACR,2FAA2F;YAC3F,4CAA4C;QAC9C,cAAc,EACZ,4FAA4F;YAC5F,oDAAoD;QACtD,cAAc,EACZ,0FAA0F;YAC1F,0CAA0C;KAC7C,CAAA;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CACxB,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,sBAAsB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAC3H,CAAA;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAA;IAC7B,OAAO,CACL,UAAU,MAAM,CAAC,KAAK,CAAC,kBAAkB,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,kBAAkB;QACjF,2BAA2B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QAC/C,0FAA0F;QAC1F,4DAA4D,CAC7D,CAAA;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,iBAAiB;IAC/B,QAAQ,CAAC,KAAK,EAAE,CAAA;AAClB,CAAC"}
package/dist/errors.d.ts CHANGED
@@ -11,4 +11,29 @@ export declare class ArchRuleError extends Error {
11
11
  /** The violations that failed the rule, in report order. */
12
12
  get violations(): ArchViolation[];
13
13
  }
14
+ /**
15
+ * Is this an `ArchRuleError` — including one thrown by a DIFFERENT copy of this
16
+ * module?
17
+ *
18
+ * `instanceof` is an identity check, and a rule file does not necessarily load
19
+ * through the same module registry as the CLI that loads it. `load-rules.ts`
20
+ * uses jiti (bug 0074: a `.ts` rule file must load inside a `"type": "commonjs"`
21
+ * consumer project), and jiti keeps its own registry — so the `ArchRuleError`
22
+ * a self-executing rule file throws is a different class object from this one
23
+ * and `instanceof` is FALSE for an error that is, in every way that matters,
24
+ * an ArchRuleError.
25
+ *
26
+ * Measured: that made `check.ts` skip `ruleFileTruncated()`, so a rule file
27
+ * whose first rule threw reported the finding and said nothing about the rules
28
+ * after it that never ran — a silent gap in a red run, which is bug 0029
29
+ * reopened by the loader. The same hazard exists for any consumer with two
30
+ * copies of eess-ts on disk (a nested `node_modules`), where the loader is
31
+ * irrelevant.
32
+ *
33
+ * Structural, therefore: the `name` this class stamps in its constructor plus
34
+ * the `violations` array that is its whole reason to exist. Both are part of the
35
+ * documented public shape (`ArchViolation[]` on a thrown error), so matching on
36
+ * them is matching on the contract rather than on a coincidence.
37
+ */
38
+ export declare function isArchRuleError(error: unknown): error is ArchRuleError;
14
39
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,KAAK;;gBAG1B,UAAU,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM;IAQxD,4DAA4D;IAC5D,IAAI,UAAU,IAAI,aAAa,EAAE,CAEhC;CACF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,KAAK;;gBAG1B,UAAU,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM;IAQxD,4DAA4D;IAC5D,IAAI,UAAU,IAAI,aAAa,EAAE,CAEhC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAQtE"}