@nielspeter/eess 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +94 -1
  2. package/dist/baseline-generator.d.ts +9 -0
  3. package/dist/baseline-generator.d.ts.map +1 -1
  4. package/dist/baseline-generator.js +9 -0
  5. package/dist/baseline-generator.js.map +1 -1
  6. package/dist/baseline.d.ts +1 -1
  7. package/dist/cardinality.d.ts.map +1 -1
  8. package/dist/cardinality.js +20 -5
  9. package/dist/cardinality.js.map +1 -1
  10. package/dist/cli-config.d.ts +59 -0
  11. package/dist/cli-config.d.ts.map +1 -0
  12. package/dist/cli-config.js +84 -0
  13. package/dist/cli-config.js.map +1 -0
  14. package/dist/collect-result.d.ts +94 -0
  15. package/dist/collect-result.d.ts.map +1 -0
  16. package/dist/collect-result.js +142 -0
  17. package/dist/collect-result.js.map +1 -0
  18. package/dist/combinators.d.ts +14 -0
  19. package/dist/combinators.d.ts.map +1 -1
  20. package/dist/combinators.js +53 -16
  21. package/dist/combinators.js.map +1 -1
  22. package/dist/condition.d.ts +37 -0
  23. package/dist/condition.d.ts.map +1 -1
  24. package/dist/condition.js +48 -1
  25. package/dist/condition.js.map +1 -1
  26. package/dist/correspondence.d.ts +30 -0
  27. package/dist/correspondence.d.ts.map +1 -1
  28. package/dist/correspondence.js +36 -1
  29. package/dist/correspondence.js.map +1 -1
  30. package/dist/dedupe-config-findings.d.ts.map +1 -1
  31. package/dist/dedupe-config-findings.js +27 -0
  32. package/dist/dedupe-config-findings.js.map +1 -1
  33. package/dist/diff-aware.d.ts +8 -9
  34. package/dist/diff-aware.d.ts.map +1 -1
  35. package/dist/diff-aware.js +38 -17
  36. package/dist/diff-aware.js.map +1 -1
  37. package/dist/emitter-findings.d.ts +167 -0
  38. package/dist/emitter-findings.d.ts.map +1 -0
  39. package/dist/emitter-findings.js +237 -0
  40. package/dist/emitter-findings.js.map +1 -0
  41. package/dist/errors.d.ts +48 -0
  42. package/dist/errors.d.ts.map +1 -1
  43. package/dist/errors.js +61 -0
  44. package/dist/errors.js.map +1 -1
  45. package/dist/exclusion-comments.d.ts +36 -1
  46. package/dist/exclusion-comments.d.ts.map +1 -1
  47. package/dist/exclusion-comments.js +185 -37
  48. package/dist/exclusion-comments.js.map +1 -1
  49. package/dist/execute-rule.d.ts +12 -3
  50. package/dist/execute-rule.d.ts.map +1 -1
  51. package/dist/execute-rule.js +184 -7
  52. package/dist/execute-rule.js.map +1 -1
  53. package/dist/file-walk.d.ts +22 -0
  54. package/dist/file-walk.d.ts.map +1 -0
  55. package/dist/file-walk.js +51 -0
  56. package/dist/file-walk.js.map +1 -0
  57. package/dist/format-json.d.ts +13 -0
  58. package/dist/format-json.d.ts.map +1 -1
  59. package/dist/format-json.js +24 -0
  60. package/dist/format-json.js.map +1 -1
  61. package/dist/index.d.ts +14 -46
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +13 -41
  64. package/dist/index.js.map +1 -1
  65. package/dist/internal.d.ts +55 -0
  66. package/dist/internal.d.ts.map +1 -0
  67. package/dist/internal.js +92 -0
  68. package/dist/internal.js.map +1 -0
  69. package/dist/mask-non-comment.d.ts +49 -0
  70. package/dist/mask-non-comment.d.ts.map +1 -0
  71. package/dist/mask-non-comment.js +261 -0
  72. package/dist/mask-non-comment.js.map +1 -0
  73. package/dist/owns-empty-discovery.d.ts.map +1 -1
  74. package/dist/owns-empty-discovery.js +12 -4
  75. package/dist/owns-empty-discovery.js.map +1 -1
  76. package/dist/path-suffix.d.ts +57 -0
  77. package/dist/path-suffix.d.ts.map +1 -0
  78. package/dist/path-suffix.js +73 -0
  79. package/dist/path-suffix.js.map +1 -0
  80. package/dist/predicate.d.ts +12 -0
  81. package/dist/predicate.d.ts.map +1 -1
  82. package/dist/predicate.js +20 -1
  83. package/dist/predicate.js.map +1 -1
  84. package/dist/preset-dispatch.d.ts +6 -34
  85. package/dist/preset-dispatch.d.ts.map +1 -1
  86. package/dist/preset-dispatch.js +30 -52
  87. package/dist/preset-dispatch.js.map +1 -1
  88. package/dist/report.d.ts +31 -7
  89. package/dist/report.d.ts.map +1 -1
  90. package/dist/report.js +174 -15
  91. package/dist/report.js.map +1 -1
  92. package/dist/rule-builder-like.d.ts +8 -2
  93. package/dist/rule-builder-like.d.ts.map +1 -1
  94. package/dist/rule-builder.d.ts +15 -0
  95. package/dist/rule-builder.d.ts.map +1 -1
  96. package/dist/rule-builder.js +41 -36
  97. package/dist/rule-builder.js.map +1 -1
  98. package/dist/rule-description.d.ts +50 -0
  99. package/dist/rule-description.d.ts.map +1 -1
  100. package/dist/rule-description.js +58 -0
  101. package/dist/rule-description.js.map +1 -1
  102. package/dist/selection-memo.d.ts +1 -3
  103. package/dist/selection-memo.d.ts.map +1 -1
  104. package/dist/selection-memo.js +1 -3
  105. package/dist/selection-memo.js.map +1 -1
  106. package/dist/silent-exclusion.d.ts +12 -0
  107. package/dist/silent-exclusion.d.ts.map +1 -1
  108. package/dist/silent-exclusion.js +22 -0
  109. package/dist/silent-exclusion.js.map +1 -1
  110. package/dist/terminal-builder.d.ts +29 -15
  111. package/dist/terminal-builder.d.ts.map +1 -1
  112. package/dist/terminal-builder.js +97 -51
  113. package/dist/terminal-builder.js.map +1 -1
  114. package/dist/vacuity-findings.d.ts +0 -6
  115. package/dist/vacuity-findings.d.ts.map +1 -1
  116. package/dist/vacuity-findings.js +27 -57
  117. package/dist/vacuity-findings.js.map +1 -1
  118. package/dist/violation.d.ts +22 -1
  119. package/dist/violation.d.ts.map +1 -1
  120. package/dist/violation.js.map +1 -1
  121. package/dist/watch.d.ts +71 -0
  122. package/dist/watch.d.ts.map +1 -0
  123. package/dist/watch.js +129 -0
  124. package/dist/watch.js.map +1 -0
  125. package/package.json +6 -1
package/README.md CHANGED
@@ -12,6 +12,90 @@ This package is the engine every eess dialect runs on. It knows nothing about Ty
12
12
  - Baseline mode, diff-aware filtering, exclusions, and the terminal/GitHub/JSON formatters
13
13
  - `definePredicate` / `defineCondition` — extension points for advanced users
14
14
 
15
+ ## Two entry points
16
+
17
+ `@nielspeter/eess` publishes two:
18
+
19
+ | Specifier | What it is |
20
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `@nielspeter/eess` | The public API. Everything below is documented here, and a change to it is a versioned change. |
22
+ | `@nielspeter/eess/internal` | Family plumbing the sibling dialects share — cache registries, stderr guards, suppression counters, identity hashing, glob-tree internals. Not API. |
23
+
24
+ You want the root. `/internal` exists so the dialects can share one engine without
25
+ each name they touch becoming a public commitment; it is published because npm has
26
+ no way to ship a package-private module, not because it is for you. Nothing there is
27
+ documented, and no dialect re-exports it _as its own API_. Two names — `DiskSet`
28
+ and `OnDisk` — are also on `@nielspeter/eess-ts`'s root, where they are eess-ts's
29
+ own types that happen to share a name; that is the whole of the overlap.
30
+
31
+ Breaking something here is still a versioned change and still needs a changeset
32
+ (ADR-011 clause 4). What you are not owed is migration guidance, because it is not
33
+ API. See
34
+ [ADR-011](https://github.com/nielspeter/eess/blob/main/adr/011-the-kernels-public-api-is-explicit.md).
35
+
36
+ If a symbol you used disappeared from the root, it moved here — the import path is
37
+ the only change.
38
+
39
+ ## Kernel API reference
40
+
41
+ The fluent surface (`RuleBuilder`, `Predicate`, `Condition`, the combinators, the
42
+ formatters) is covered in the [docs site](https://nielspeter.github.io/eess/). This
43
+ section covers the supporting types you will meet in signatures.
44
+
45
+ ### Reporting and severity
46
+
47
+ A preset never emits on its own — the caller owns emission (ADR-008).
48
+
49
+ - **`ReportMode`** — `'throw' | 'return' | 'warn'`. What a finisher does with findings: raise an `ArchRuleError`, hand them back, or print them as advisory and continue.
50
+ - **`ReportOptions`** — the emission controls: `format` (terminal/json/github) and `reason`, the rationale threaded into the output.
51
+ - **`RuleSeverity`** — `'error' | 'warn' | 'off'`, the value of a per-rule override.
52
+ - **`PresetBaseOptions`** — what every preset accepts: an `overrides` map of rule id → `RuleSeverity`, plus the reporting controls. Extend it when you write a preset so `{ report: 'return' }` works without your own plumbing.
53
+ - **`RuleBuilderLike`** — the one-method shape (`violations()`) a preset needs from anything it dispatches, so a preset is not coupled to a dialect's builder class.
54
+ - **`Dispatchable`** — what `dispatchRule` accepts: a builder that can produce violations _and_ name itself, so the dispatcher can apply a per-rule override to it. Strictly more than `RuleBuilderLike`; that one is the narrower shape used where only the findings matter.
55
+ - **`isArchRuleError(err)`** — use this instead of `instanceof ArchRuleError` when catching. `instanceof` compares class identity, so it returns `false` whenever two copies of the package are on disk — a nested `node_modules`, or a rule file loaded through a separate module registry. The check is structural (the stamped `name` plus the `violations` array), so it survives that. The thrown shape itself — `violations: ArchViolation[]` — is public either way.
56
+ - **`ArchConfigError`** / **`isArchConfigError(err)`** — thrown when a _rule_ is misconfigured (bad arguments to a condition, a malformed rule file), as opposed to `ArchRuleError`, which means the _architecture_ is wrong. Two different faults with two different audiences: one tells a rule author they mistyped something, the other tells a team their code drifted. Carries `subject` — the condition, predicate or file at fault — so a caller can route the failure without parsing the message. `isArchConfigError` is structural for the same two-copies-on-disk reason as `isArchRuleError`.
57
+ - **`ArchFix`** — a machine-applicable edit on a violation: `file`, a `[start, end)` character span, and the `replacement` text. This is what `--fix` applies.
58
+
59
+ ### The JSON report
60
+
61
+ `--format json` emits an **`ArchJsonReport`**: a `summary` (`total`, `errors`, `warnings`, `reason`), then **`ArchJsonViolation`** entries, plus **`ArchJsonSuppression`** for findings silenced by an `eess-exclude` comment and **`ArchJsonUntestedAllowlist`** for allowlists nothing exercised. Import these to type a tool that consumes eess output rather than re-declaring the shape.
62
+
63
+ ### Filtering a run
64
+
65
+ - **`BaselineFilter`** — decides whether an already-accepted finding is suppressed by the baseline file.
66
+ - **`DiffFilterLike`** — decides whether a finding falls inside the changed region, for `--changed` runs.
67
+
68
+ Both are interfaces so a host can supply its own; the kernel ships implementations.
69
+
70
+ ### Correspondence
71
+
72
+ `correspondence()` binds two `Selection`s and checks they agree in both directions.
73
+
74
+ - **`CorrespondenceOptions`** — its argument: the `left` and `right` selections, a join strategy, and optional per-side `suggest` callbacks for the two-sided message.
75
+ - **`KeyBy`** — the fast join: a key extracted per element, giving an O(n+m) match. Prefer it. The fallback `matchBy` predicate is O(n×m).
76
+ - **`RelationSpec`** — what `preserveRelations()` takes: how to read edges from each side, so the check covers relationships and not just membership.
77
+ - **`ElementInfo`** — the `name` / `file` / `line` a dialect reports for one element, which is what the message and the violation's location are built from.
78
+
79
+ ### Declaring globs (for custom predicates)
80
+
81
+ A predicate written with `definePredicate` should declare the globs it matches on.
82
+ Without that it is opaque, so a vacuity diagnosis cannot tell "matched nothing
83
+ because the glob is dead" from "matched nothing because nothing was there" — and
84
+ any `or()` containing it becomes undiagnosable too.
85
+
86
+ - **`globNode(leaf)`** — wrap a single declared glob into a tree. The common case.
87
+ - **`globAnyOf(globs, kind, base?)`** — declare several alternatives at once.
88
+ - **`DeclaredGlob`** — one glob with its `kind` and `base`. **`GlobKind`** is what the glob matches against (`'file-path' | 'parent-dir' | 'import-target' | 'specifier' | 'literal'`) and **`GlobBase`** is what it is resolved against (`'absolute' | 'tsconfig-relative' | 'normalized'`).
89
+ - **`GlobTree<L>`** — the `and`/`any` tree the declarations form; **`DeclaredGlobs`** is that tree over `DeclaredGlob`, and **`GlobLeaf<L>`** is one position in it.
90
+ - **`OpaqueGlob`** — the explicit "this branch cannot be described" marker. Honest, and it is what keeps an undeclared predicate from silently reading as a dead one.
91
+ - **`GlobSite`** — a declared glob plus where it was written; **`GlobPosition`** is that where (`'selector' | 'discovery' | 'condition' | 'exclusion'`), which is what lets a diagnosis point at the glob you actually got wrong.
92
+ - **`GlobNode`** — the tree over `GlobSite`, i.e. a fully-stamped declaration ready to diagnose.
93
+
94
+ ### Coverage evidence
95
+
96
+ - **`EdgeCoverage`** — one rule's evidence: how many `subjects` it saw, how many `edges` it actually tested, and a `reason` when that number is zero.
97
+ - **`UntestedReason`** — why zero: `'no-edges'` (nothing to test, usually correct), `'all-filtered'` (edges existed and every one was filtered out), `'none-matched'` (edges existed and none matched the allowlist). The distinction is the point — a rule that tested nothing should say which of the three it was, per ADR-010.
98
+
15
99
  ## Who uses it
16
100
 
17
101
  You normally don't install this directly — you install a dialect:
@@ -19,7 +103,16 @@ You normally don't install this directly — you install a dialect:
19
103
  - [`@nielspeter/eess-ts`](https://www.npmjs.com/package/@nielspeter/eess-ts) — TypeScript
20
104
  - [`@nielspeter/eess-mermaid`](https://www.npmjs.com/package/@nielspeter/eess-mermaid) — Mermaid class diagrams
21
105
 
22
- Install `@nielspeter/eess` directly only when authoring a new dialect or a cross-dialect tool on top of the kernel.
106
+ Install `@nielspeter/eess` directly only when authoring a new dialect or a cross-dialect
107
+ tool on top of the kernel.
108
+
109
+ **If that is you, `/internal` is your surface, not a wall.** The two sentences above look
110
+ like they contradict each other — "not for you", then "install it if you are writing a
111
+ dialect" — so to be explicit: a dialect author is exactly who `/internal` exists for, and
112
+ the five in this repo import from it. What the root alone gives you is the rule-authoring
113
+ vocabulary, not the engine. Nothing there is documented and it changes without migration
114
+ notes, so treat it as a contract you re-check on each kernel bump; if you are building a
115
+ dialect you intend to publish, open an issue and we will work out what belongs on the root.
23
116
 
24
117
  ## License
25
118
 
@@ -5,6 +5,15 @@ import type { ArchViolation } from './violation.js';
5
5
  * Used to generate a baseline: run all rules, collect violations,
6
6
  * write them to a baseline file.
7
7
  *
8
+ * **This bypasses the evidence gate, deliberately and knowingly** — see ADR-014
9
+ * and proposal 011 ask C. Prefer a dialect's `baseline` command, which gates per
10
+ * rule file and refuses a builder that certified nothing.
11
+ *
12
+ * **Not the same function as `eess-ts`'s `collectViolations`.** This one takes
13
+ * builders with a throwing `check()` and harvests the `ArchRuleError`; the
14
+ * dialect's takes builders with a non-throwing `violations()`. Same name, same
15
+ * `@example`, different contract.
16
+ *
8
17
  * @example
9
18
  * const violations = collectViolations(
10
19
  * classes(p).that().extend('Base').should().notContain(call('parseInt')),
@@ -1 +1 @@
1
- {"version":3,"file":"baseline-generator.d.ts","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,GAAG,aAAa,EAAE,CAc5F"}
1
+ {"version":3,"file":"baseline-generator.d.ts","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,GAAG,aAAa,EAAE,CAc5F"}
@@ -5,6 +5,15 @@ import { ArchRuleError } from './errors.js';
5
5
  * Used to generate a baseline: run all rules, collect violations,
6
6
  * write them to a baseline file.
7
7
  *
8
+ * **This bypasses the evidence gate, deliberately and knowingly** — see ADR-014
9
+ * and proposal 011 ask C. Prefer a dialect's `baseline` command, which gates per
10
+ * rule file and refuses a builder that certified nothing.
11
+ *
12
+ * **Not the same function as `eess-ts`'s `collectViolations`.** This one takes
13
+ * builders with a throwing `check()` and harvests the `ArchRuleError`; the
14
+ * dialect's takes builders with a non-throwing `violations()`. Same name, same
15
+ * `@example`, different contract.
16
+ *
8
17
  * @example
9
18
  * const violations = collectViolations(
10
19
  * classes(p).that().extend('Base').should().notContain(call('parseInt')),
@@ -1 +1 @@
1
- {"version":3,"file":"baseline-generator.js","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAG,QAAsC;IACzE,MAAM,aAAa,GAAoB,EAAE,CAAA;IAEzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,OAAO,CAAC,KAAK,EAAE,CAAA;QACjB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;gBACnC,aAAa,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAA;YACzC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,aAAa,CAAA;AACtB,CAAC"}
1
+ {"version":3,"file":"baseline-generator.js","sourceRoot":"","sources":["../src/baseline-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAG,QAAsC;IACzE,MAAM,aAAa,GAAoB,EAAE,CAAA;IAEzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,OAAO,CAAC,KAAK,EAAE,CAAA;QACjB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;gBACnC,aAAa,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAA;YACzC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,aAAa,CAAA;AACtB,CAAC"}
@@ -26,7 +26,7 @@ export interface BaselineEntry {
26
26
  /**
27
27
  * What {@link measured} counts — `code-lines`, `methods`, `complexity`.
28
28
  *
29
- * Written since [bug 0171](../../../work/bugs/0171-a-metric-unit-change-silently-loosens-every-baselined-ratchet.md).
29
+ * Written since [bug 0171](../../../work/bugs/fixed/0171-a-metric-unit-change-silently-loosens-every-baselined-ratchet.md).
30
30
  * An accepted ceiling is a number IN A UNIT, and comparing across a change of
31
31
  * unit does not measure a regression — it moves the bar. Absent on entries
32
32
  * written before stamping, which {@link Baseline.isKnown} treats as "unknown
@@ -1 +1 @@
1
- {"version":3,"file":"cardinality.d.ts","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAGzE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE7D"}
1
+ {"version":3,"file":"cardinality.d.ts","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AAyCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAGzE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE7D"}
@@ -16,11 +16,26 @@
16
16
  * empty-selection gate on any rule, permanently and invisibly. That is
17
17
  * `.allowEmpty()` wearing a different hat.
18
18
  *
19
- * This symbol is **not** re-exported from `src/index.ts`, and `package.json`'s
20
- * `exports` map has no wildcard subpath, so a consumer cannot import it to
21
- * name the key. The escape hatch users *do* get is `.expectEmpty()`, which is
22
- * an assertion that fails when the selection stops being empty — not a
23
- * silencer.
19
+ * **What actually stops a consumer doing this: not enough.** Say it plainly,
20
+ * because two earlier versions of this comment did not.
21
+ *
22
+ * The `WeakSet` below defends the KEY — membership cannot be read off an object,
23
+ * copied, or forged. It does not defend the CALL. Anyone holding
24
+ * `marksAssertsCardinality` can mark any condition, and it is reachable from
25
+ * `@nielspeter/eess/internal`. The first rationale here ("the exports map has no
26
+ * wildcard subpath") stopped being true when ADR-011 added the subpath; the
27
+ * second ("the `WeakSet` is the real defence") was simply wrong about which half
28
+ * it guards, and was written in the same branch that added the subpath.
29
+ *
30
+ * The honest position: this is narrower than it was — on `main` the marker was
31
+ * exported from the kernel ROOT and re-exported by eess-ts's root, so it sat on
32
+ * two published surfaces; it is now on one, named `/internal`, which no dialect
33
+ * re-exports. It is not closed. Closing it means validating at the registration
34
+ * site (accept only conditions from a sanctioned constructor), which is a design
35
+ * change, not a comment.
36
+ *
37
+ * The escape hatch users are *meant* to have is `.expectEmpty()` — an assertion
38
+ * that fails when the selection stops being empty, not a silencer.
24
39
  */
25
40
  const CARDINALITY_ASSERTERS = new WeakSet();
26
41
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cardinality.js","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,qBAAqB,GAAG,IAAI,OAAO,EAAU,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,uBAAuB,CAAmB,SAAY;IACpE,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACpC,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,OAAO,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AAC7C,CAAC"}
1
+ {"version":3,"file":"cardinality.js","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,qBAAqB,GAAG,IAAI,OAAO,EAAU,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,uBAAuB,CAAmB,SAAY;IACpE,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACpC,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,OAAO,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AAC7C,CAAC"}
@@ -0,0 +1,59 @@
1
+ import type { OutputFormat } from './check-options.js';
2
+ /**
3
+ * The parts of dialect CLI config handling that are the same everywhere.
4
+ *
5
+ * **Split along the kernel's existing seam, not behind a new injection point.**
6
+ * `PathUniverse` sets the pattern: the kernel takes materialized data and the
7
+ * dialect does the I/O. Here the pure halves are finding the config file and
8
+ * validating what a loaded module claims — both were byte-identical between
9
+ * `eess-ts` and `eess-mermaid` (`findConfigFile` scored 100%). The impure half,
10
+ * actually importing the file, stays in each dialect because it needs `jiti`,
11
+ * which the kernel cannot depend on.
12
+ *
13
+ * That leaves one genuinely shared subtlety in the impure half —
14
+ * {@link isModuleFormatRefusal} — so it lives here too. It is pure, and it is
15
+ * the part that is easy to get dangerously wrong.
16
+ */
17
+ /** Config fields every dialect CLI understands. A dialect may add its own. */
18
+ export interface SharedCliConfig {
19
+ rules?: string[];
20
+ format?: OutputFormat | 'auto';
21
+ watchDirs?: string[];
22
+ }
23
+ /** The first of `filenames` that exists in the working directory. */
24
+ export declare function findConfigFile(filenames: readonly string[]): string | undefined;
25
+ /**
26
+ * The shared config a loaded module claims, validated.
27
+ *
28
+ * Everything is checked rather than trusted: a config file is arbitrary user
29
+ * code and its default export can be any shape at all. An unrecognised `format`
30
+ * is dropped rather than passed through, so a typo falls back to the default
31
+ * instead of reaching a formatter that has no case for it.
32
+ */
33
+ export declare function extractSharedConfig(mod: unknown): SharedCliConfig;
34
+ /**
35
+ * Did Node refuse this file for its MODULE FORMAT, rather than for anything the
36
+ * file does?
37
+ *
38
+ * Narrow on purpose. A broad `catch` that fell back to a transpiling loader on
39
+ * any error would re-execute a rule file that had already run — doubling its
40
+ * output — and would hide a genuine syntax error behind a second,
41
+ * differently-worded failure.
42
+ *
43
+ * The condition it detects is bug 0074: the CONSUMER project is
44
+ * `"type": "commonjs"` (what `npm init -y` writes) and the config or rule file
45
+ * uses ESM syntax.
46
+ */
47
+ export declare function isModuleFormatRefusal(error: unknown): boolean;
48
+ /**
49
+ * Refuse a run that named no rule files, saying where to name them.
50
+ *
51
+ * Sets `process.exitCode` rather than throwing: the caller is a CLI entry point
52
+ * that still wants to return normally. Returns whether the run may proceed.
53
+ *
54
+ * `configName` is the dialect's own config filename, because "set them in
55
+ * config" is not an instruction — the two copies of this differed in exactly
56
+ * that word, one naming the file and one not.
57
+ */
58
+ export declare function requireRuleFiles(ruleFiles: readonly string[], configName: string): boolean;
59
+ //# sourceMappingURL=cli-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-config.d.ts","sourceRoot":"","sources":["../src/cli-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGtD;;;;;;;;;;;;;;GAcG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAAA;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAID,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAO/E;AAOD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,CAiBjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAO1F"}
@@ -0,0 +1,84 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { isRecord } from './type-guards.js';
4
+ const FORMATS = new Set(['terminal', 'json', 'github', 'auto']);
5
+ /** The first of `filenames` that exists in the working directory. */
6
+ export function findConfigFile(filenames) {
7
+ const cwd = process.cwd();
8
+ for (const name of filenames) {
9
+ const candidate = path.join(cwd, name);
10
+ if (fs.existsSync(candidate))
11
+ return candidate;
12
+ }
13
+ return undefined;
14
+ }
15
+ /** Strings from an unknown array, dropping anything that is not one. */
16
+ function stringsOf(value) {
17
+ return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : undefined;
18
+ }
19
+ /**
20
+ * The shared config a loaded module claims, validated.
21
+ *
22
+ * Everything is checked rather than trusted: a config file is arbitrary user
23
+ * code and its default export can be any shape at all. An unrecognised `format`
24
+ * is dropped rather than passed through, so a typo falls back to the default
25
+ * instead of reaching a formatter that has no case for it.
26
+ */
27
+ export function extractSharedConfig(mod) {
28
+ if (!isRecord(mod) || !('default' in mod))
29
+ return {};
30
+ const defaultExport = mod.default;
31
+ if (!isRecord(defaultExport))
32
+ return {};
33
+ const config = {};
34
+ const format = defaultExport['format'];
35
+ if (typeof format === 'string' && FORMATS.has(format)) {
36
+ // Narrowed by the set above; the cast-free form is this re-check.
37
+ config.format =
38
+ format === 'terminal' || format === 'json' || format === 'github' ? format : 'auto';
39
+ }
40
+ const rules = stringsOf(defaultExport['rules']);
41
+ if (rules)
42
+ config.rules = rules;
43
+ const watchDirs = stringsOf(defaultExport['watchDirs']);
44
+ if (watchDirs)
45
+ config.watchDirs = watchDirs;
46
+ return config;
47
+ }
48
+ /**
49
+ * Did Node refuse this file for its MODULE FORMAT, rather than for anything the
50
+ * file does?
51
+ *
52
+ * Narrow on purpose. A broad `catch` that fell back to a transpiling loader on
53
+ * any error would re-execute a rule file that had already run — doubling its
54
+ * output — and would hide a genuine syntax error behind a second,
55
+ * differently-worded failure.
56
+ *
57
+ * The condition it detects is bug 0074: the CONSUMER project is
58
+ * `"type": "commonjs"` (what `npm init -y` writes) and the config or rule file
59
+ * uses ESM syntax.
60
+ */
61
+ export function isModuleFormatRefusal(error) {
62
+ if (!(error instanceof SyntaxError))
63
+ return false;
64
+ return (error.message.includes('Cannot use import statement outside a module') ||
65
+ error.message.includes("Unexpected token 'export'"));
66
+ }
67
+ /**
68
+ * Refuse a run that named no rule files, saying where to name them.
69
+ *
70
+ * Sets `process.exitCode` rather than throwing: the caller is a CLI entry point
71
+ * that still wants to return normally. Returns whether the run may proceed.
72
+ *
73
+ * `configName` is the dialect's own config filename, because "set them in
74
+ * config" is not an instruction — the two copies of this differed in exactly
75
+ * that word, one naming the file and one not.
76
+ */
77
+ export function requireRuleFiles(ruleFiles, configName) {
78
+ if (ruleFiles.length > 0)
79
+ return true;
80
+ console.error(`Error: No rule files specified. Pass rule files as arguments or set them in ${configName}.`);
81
+ process.exitCode = 1;
82
+ return false;
83
+ }
84
+ //# sourceMappingURL=cli-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-config.js","sourceRoot":"","sources":["../src/cli-config.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,MAAM,SAAS,CAAA;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAyB3C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAA;AAE/D,qEAAqE;AACrE,MAAM,UAAU,cAAc,CAAC,SAA4B;IACzD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IACzB,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACtC,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAA;IAChD,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,wEAAwE;AACxE,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACnG,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IACpD,MAAM,aAAa,GAAG,GAAG,CAAC,OAAO,CAAA;IACjC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC;QAAE,OAAO,EAAE,CAAA;IAEvC,MAAM,MAAM,GAAoB,EAAE,CAAA;IAClC,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;IACtC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACtD,kEAAkE;QAClE,MAAM,CAAC,MAAM;YACX,MAAM,KAAK,UAAU,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IACvF,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;IAC/C,IAAI,KAAK;QAAE,MAAM,CAAC,KAAK,GAAG,KAAK,CAAA;IAC/B,MAAM,SAAS,GAAG,SAAS,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAA;IACvD,IAAI,SAAS;QAAE,MAAM,CAAC,SAAS,GAAG,SAAS,CAAA;IAC3C,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,IAAI,CAAC,CAAC,KAAK,YAAY,WAAW,CAAC;QAAE,OAAO,KAAK,CAAA;IACjD,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,8CAA8C,CAAC;QACtE,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAC,CACpD,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAA4B,EAAE,UAAkB;IAC/E,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACrC,OAAO,CAAC,KAAK,CACX,+EAA+E,UAAU,GAAG,CAC7F,CAAA;IACD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;IACpB,OAAO,KAAK,CAAA;AACd,CAAC"}
@@ -0,0 +1,94 @@
1
+ import type { ArchViolation } from './violation.js';
2
+ /**
3
+ * The receipt — [ADR-014](../../../adr/014-the-emitter-refuses-a-verdict-without-evidence.md).
4
+ *
5
+ * **An `ArchViolation[]` carrying its own evidence**, not an object wrapping
6
+ * one. That shape is load-bearing and the review measured why: a plain object
7
+ * turns every untyped consumer green, because `result.length` reads `undefined`
8
+ * and `undefined > 0` is `false`. An array keeps `.length`, iteration,
9
+ * `flatMap` and every existing `for…of` working and correct, so a JavaScript
10
+ * consumer who never reads `examined` still exits non-zero on violations.
11
+ * "`CollectResult`'s shape by name" is a tidiness argument and it loses to that.
12
+ *
13
+ * **The name stays `CollectResult`.** Plan 0235 calls it a "receipt" in prose;
14
+ * there is deliberately no second exported name for one concept.
15
+ *
16
+ * `JSON.stringify` drops an array's own properties, which is why
17
+ * `formatViolationsJson` carries `examined` across by hand — see the note there.
18
+ */
19
+ export interface CollectResult extends Array<ArchViolation> {
20
+ /** Units this rule actually reached its assertion over. The denominator. */
21
+ readonly examined: number;
22
+ /** The source loaded nothing at all, before any selection ran. */
23
+ readonly sourceEmpty?: boolean;
24
+ /** A caller declared this set legitimately empty, and the claim can expire. */
25
+ readonly declaredEmpty?: boolean;
26
+ /**
27
+ * This rule was never run — explicitly disabled, not merely empty.
28
+ *
29
+ * A third state, and it has to be distinguishable from both. A rule that RAN
30
+ * and examined nothing is dead and the merge must say so. A rule turned off
31
+ * never ran, so it has no denominator to be suspicious about — and marking it
32
+ * `declaredEmpty` instead would mint the declaration ADR-014 §3's amendment
33
+ * refuses to infer from `overrides: { id: 'off' }`.
34
+ *
35
+ * **It is falsifiable, like every other declaration here.** A rule that never
36
+ * ran examined nothing and found nothing, so `notRun` beside a non-zero
37
+ * `examined` or a violation is `emitter/contradictory-evidence` — checked at
38
+ * the gate and in the merge. Without that check this would be the one
39
+ * suppressing flag in the vocabulary that nothing can contradict, which is the
40
+ * shape ADR-014 §3's amendment exists to refuse.
41
+ *
42
+ * The two are not interchangeable: without this, one disabled rule reddened a
43
+ * whole preset whose other rules examined plenty (measured — `adrEnforcement`
44
+ * with `adr/valid-tiers` off). With it, a preset where EVERY member is `notRun`
45
+ * still sums to zero examined with no declaration, so the all-off case reports
46
+ * exactly as §3 requires.
47
+ */
48
+ readonly notRun?: boolean;
49
+ /**
50
+ * A specific dead-glob explanation for a zero-examined rule, pre-formatted
51
+ * by the dialect that computed it (`RuleBuilder.deadGlobDiagnosis()`) — the
52
+ * kernel never touches picomatch or a project type to build this itself.
53
+ */
54
+ readonly deadGlob?: string;
55
+ }
56
+ /** Evidence a caller supplies alongside the violations. */
57
+ export interface Evidence {
58
+ readonly examined: number;
59
+ readonly sourceEmpty?: boolean;
60
+ readonly declaredEmpty?: boolean;
61
+ readonly notRun?: boolean;
62
+ readonly deadGlob?: string;
63
+ }
64
+ /**
65
+ * The one constructor. Every terminal produces its receipt through this.
66
+ *
67
+ * **Stamps a FRESH array, deliberately.** `applyFilters` returns the *same
68
+ * reference* when no exclusion applies, so stamping `examined` onto what it
69
+ * hands back would mutate whatever a family memoized — a rule's evidence
70
+ * appearing on someone else's array, changing when it is re-read. The copy is
71
+ * the cost of not having that bug.
72
+ */
73
+ export declare function collectResult(violations: readonly ArchViolation[], evidence: Evidence): CollectResult;
74
+ /** Does this value carry evidence at all, or is it a bare array? */
75
+ export declare function hasEvidence(value: readonly ArchViolation[]): value is CollectResult;
76
+ /**
77
+ * The one merge — ADR-014 §7. Fail-closed, and each rule below is a rule a
78
+ * hand-rolled sum got wrong somewhere (bug 0205's class: four emitters
79
+ * restating one rule and disagreeing).
80
+ *
81
+ * - **`examined` sums.** Zero members sums to zero — an absent denominator is
82
+ * not a large one.
83
+ * - **`declaredEmpty` requires EVERY zero-contributing member to declare.** A
84
+ * `some()` here lets one declared part vouch for a hand-counted zero beside
85
+ * it, which is the whole failure this merge exists to prevent.
86
+ * - **`sourceEmpty` on any member outranks every declaration.** ADR-010 §3's
87
+ * precedence: a declaration asserts a fact about a loaded corpus, and over
88
+ * an empty source it asserts nothing.
89
+ * - **A member with no evidence poisons the merge**, rather than being counted
90
+ * as zero. A dead member must not be able to hide inside a healthy sum, so
91
+ * the result carries no evidence either and the emitter says so.
92
+ */
93
+ export declare function mergeCollectResults(parts: readonly (readonly ArchViolation[])[]): CollectResult;
94
+ //# sourceMappingURL=collect-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-result.d.ts","sourceRoot":"","sources":["../src/collect-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAQnD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,aAAa,CAAC;IACzD,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAChC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,2DAA2D;AAC3D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,SAAS,aAAa,EAAE,EACpC,QAAQ,EAAE,QAAQ,GACjB,aAAa,CAWf;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,KAAK,IAAI,aAAa,CAGnF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,aAAa,EAAE,CAAC,EAAE,GAAG,aAAa,CA0G/F"}
@@ -0,0 +1,142 @@
1
+ import { noReceiptViolation, passWithoutEvidenceViolation, contradictoryEvidenceViolation, sourceEmptyViolation, } from './emitter-findings.js';
2
+ /**
3
+ * The one constructor. Every terminal produces its receipt through this.
4
+ *
5
+ * **Stamps a FRESH array, deliberately.** `applyFilters` returns the *same
6
+ * reference* when no exclusion applies, so stamping `examined` onto what it
7
+ * hands back would mutate whatever a family memoized — a rule's evidence
8
+ * appearing on someone else's array, changing when it is re-read. The copy is
9
+ * the cost of not having that bug.
10
+ */
11
+ export function collectResult(violations, evidence) {
12
+ // `Object.assign` returns the intersection type, so no assertion is needed —
13
+ // ADR-005. An earlier cut wrote `[...violations] as ArchViolation[] & {…}` and
14
+ // the repo's own `check:arch` reported it, in the module this plan adds.
15
+ return Object.assign([...violations], {
16
+ examined: evidence.examined,
17
+ ...(evidence.sourceEmpty === true ? { sourceEmpty: true } : {}),
18
+ ...(evidence.declaredEmpty === true ? { declaredEmpty: true } : {}),
19
+ ...(evidence.notRun === true ? { notRun: true } : {}),
20
+ ...(evidence.deadGlob === undefined ? {} : { deadGlob: evidence.deadGlob }),
21
+ });
22
+ }
23
+ /** Does this value carry evidence at all, or is it a bare array? */
24
+ export function hasEvidence(value) {
25
+ // `in` narrows without an assertion — ADR-005.
26
+ return 'examined' in value && typeof value.examined === 'number';
27
+ }
28
+ /**
29
+ * The one merge — ADR-014 §7. Fail-closed, and each rule below is a rule a
30
+ * hand-rolled sum got wrong somewhere (bug 0205's class: four emitters
31
+ * restating one rule and disagreeing).
32
+ *
33
+ * - **`examined` sums.** Zero members sums to zero — an absent denominator is
34
+ * not a large one.
35
+ * - **`declaredEmpty` requires EVERY zero-contributing member to declare.** A
36
+ * `some()` here lets one declared part vouch for a hand-counted zero beside
37
+ * it, which is the whole failure this merge exists to prevent.
38
+ * - **`sourceEmpty` on any member outranks every declaration.** ADR-010 §3's
39
+ * precedence: a declaration asserts a fact about a loaded corpus, and over
40
+ * an empty source it asserts nothing.
41
+ * - **A member with no evidence poisons the merge**, rather than being counted
42
+ * as zero. A dead member must not be able to hide inside a healthy sum, so
43
+ * the result carries no evidence either and the emitter says so.
44
+ */
45
+ export function mergeCollectResults(parts) {
46
+ const violations = parts.flat();
47
+ if (!parts.every(hasEvidence)) {
48
+ // A member with no evidence at all. The merge REPORTS it rather than
49
+ // returning an evidence-free value for the emitter to notice later: the
50
+ // guarantee is this function's, and deferring it was also the last `as` in
51
+ // this file (an evidence-free value is not a `CollectResult`).
52
+ return collectResult([...violations, noReceiptViolation()], { examined: 0 });
53
+ }
54
+ // **ADR-014 §7's headline guarantee, and it needs its own pass.** Summing
55
+ // alone lets a dead member hide: measured, a check examining 0 merged with one
56
+ // examining 900 produced `examined: 900` and no finding — nine checks in
57
+ // `check-corpus.mjs`, one of them with a `continue` planted at the top of its
58
+ // loop, and the total still reads healthy. That is the plan's own top success
59
+ // criterion, and the field failure proposal 009 records, reproduced inside the
60
+ // primitive built to prevent it.
61
+ //
62
+ // A member is dead when it examined nothing, said nothing, and declared
63
+ // nothing. One that carries its own finding has spoken; one that declared
64
+ // empty has been accounted for.
65
+ // A member that never RAN is not a dead check — it is an absent one, with no
66
+ // denominator to be suspicious about. Excluding it is what keeps ONE disabled
67
+ // rule from reddening a preset whose others examined plenty (measured:
68
+ // `adrEnforcement` with `adr/valid-tiers` off). The all-off case still
69
+ // reports: every member is then `notRun`, the sum is zero and nobody
70
+ // declared, which the emitter's own gate catches downstream.
71
+ // A member whose `notRun` contradicts its own evidence, checked HERE and not
72
+ // only at the gate: the merged receipt carries no `notRun`, so a contradictory
73
+ // member is invisible downstream — it would slip through the one door that
74
+ // could see it. `notRun` is also the flag that exempts a member from the dead
75
+ // filter below, so an unchecked one is a suppressor with no falsifier (the
76
+ // asymmetry with `declaredEmpty` an enforcement review found in plan 0235).
77
+ const contradictory = parts.find((p) => p.notRun === true && (p.examined > 0 || p.length > 0));
78
+ if (contradictory !== undefined) {
79
+ return collectResult([...violations, contradictoryEvidenceViolation(contradictory.examined, contradictory.length)], { examined: parts.reduce((n, p) => n + p.examined, 0) });
80
+ }
81
+ // **An empty source is a member-level fault, checked HERE for the same reason
82
+ // the contradictory `notRun` above is** — ADR-014 §4: "an empty source
83
+ // outranks any declaration and names the source."
84
+ //
85
+ // The dead filter below EXEMPTS a `sourceEmpty` member, which is right for
86
+ // "did this member contribute nothing without saying why" — it said why. But
87
+ // §4 makes that answer a fault rather than an excuse, so the exemption alone
88
+ // left the merge disagreeing with the gate about the same receipt: measured,
89
+ // `finishPreset` reds a `{ examined: 0, sourceEmpty: true, declaredEmpty: true }`
90
+ // member while `mergeCollectResults([thatMember, aHealthyOne])` was GREEN,
91
+ // because the healthy sibling's `examined` carries the merged receipt past the
92
+ // gate's own check. That is the `notRun` asymmetry this ADR already fixed once,
93
+ // recreated for `sourceEmpty` by the change that fixed it at the gate.
94
+ const emptySource = parts.find((p) => p.sourceEmpty === true && p.examined === 0);
95
+ if (emptySource !== undefined) {
96
+ const summed = parts.reduce((n, p) => n + p.examined, 0);
97
+ return collectResult([...violations, sourceEmptyViolation()], {
98
+ examined: summed,
99
+ // **Only when it is true of the WHOLE.** Stamping it unconditionally
100
+ // minted a receipt saying "the source loaded nothing" beside a non-zero
101
+ // sum from healthy siblings — and the gate then contradicted it, blaming a
102
+ // flag nobody set. Four reviewers measured that on this branch's own
103
+ // headline scenario. The member-level fact is already carried by the
104
+ // finding above; the flag describes the merged whole, and of the whole it
105
+ // is only true when nothing was examined anywhere.
106
+ sourceEmpty: summed === 0 ? true : undefined,
107
+ });
108
+ }
109
+ const dead = parts.filter((p) => p.examined === 0 &&
110
+ p.length === 0 &&
111
+ p.notRun !== true &&
112
+ p.declaredEmpty !== true &&
113
+ p.sourceEmpty !== true);
114
+ if (dead.length > 0 && parts.length > 1) {
115
+ return collectResult([...violations, passWithoutEvidenceViolation()], {
116
+ examined: parts.reduce((n, p) => n + p.examined, 0),
117
+ });
118
+ }
119
+ const examined = parts.reduce((n, p) => n + p.examined, 0);
120
+ const anySourceEmpty = parts.some((p) => p.sourceEmpty === true);
121
+ return collectResult(violations, {
122
+ examined,
123
+ sourceEmpty: anySourceEmpty ? true : undefined,
124
+ // **Only when the WHOLE verdict examined nothing.** A member's declaration
125
+ // is about that member: `honestyAtClose` merges three rules and one of them
126
+ // legitimately declares itself empty while the other two examine plenty.
127
+ // Propagating that up marked the merged verdict declared-empty over a
128
+ // non-zero total, and the emitter's expiry then fired on it — correctly,
129
+ // which is how this was found. The member's own declaration is already
130
+ // satisfied by its own zero; the merge has nothing to add.
131
+ //
132
+ // `every`, not `some`: one declared part must never vouch for a
133
+ // hand-counted zero beside it.
134
+ declaredEmpty: !anySourceEmpty &&
135
+ examined === 0 &&
136
+ parts.length > 0 &&
137
+ parts.every((p) => p.declaredEmpty === true)
138
+ ? true
139
+ : undefined,
140
+ });
141
+ }
142
+ //# sourceMappingURL=collect-result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-result.js","sourceRoot":"","sources":["../src/collect-result.ts"],"names":[],"mappings":"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"}