@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.
- package/README.md +94 -1
- package/dist/baseline-generator.d.ts +9 -0
- package/dist/baseline-generator.d.ts.map +1 -1
- package/dist/baseline-generator.js +9 -0
- package/dist/baseline-generator.js.map +1 -1
- package/dist/baseline.d.ts +1 -1
- package/dist/cardinality.d.ts.map +1 -1
- package/dist/cardinality.js +20 -5
- package/dist/cardinality.js.map +1 -1
- package/dist/cli-config.d.ts +59 -0
- package/dist/cli-config.d.ts.map +1 -0
- package/dist/cli-config.js +84 -0
- package/dist/cli-config.js.map +1 -0
- package/dist/collect-result.d.ts +94 -0
- package/dist/collect-result.d.ts.map +1 -0
- package/dist/collect-result.js +142 -0
- package/dist/collect-result.js.map +1 -0
- package/dist/combinators.d.ts +14 -0
- package/dist/combinators.d.ts.map +1 -1
- package/dist/combinators.js +53 -16
- package/dist/combinators.js.map +1 -1
- package/dist/condition.d.ts +37 -0
- package/dist/condition.d.ts.map +1 -1
- package/dist/condition.js +48 -1
- package/dist/condition.js.map +1 -1
- package/dist/correspondence.d.ts +30 -0
- package/dist/correspondence.d.ts.map +1 -1
- package/dist/correspondence.js +36 -1
- package/dist/correspondence.js.map +1 -1
- package/dist/dedupe-config-findings.d.ts.map +1 -1
- package/dist/dedupe-config-findings.js +27 -0
- package/dist/dedupe-config-findings.js.map +1 -1
- package/dist/diff-aware.d.ts +8 -9
- package/dist/diff-aware.d.ts.map +1 -1
- package/dist/diff-aware.js +38 -17
- package/dist/diff-aware.js.map +1 -1
- package/dist/emitter-findings.d.ts +167 -0
- package/dist/emitter-findings.d.ts.map +1 -0
- package/dist/emitter-findings.js +237 -0
- package/dist/emitter-findings.js.map +1 -0
- package/dist/errors.d.ts +48 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +61 -0
- package/dist/errors.js.map +1 -1
- package/dist/exclusion-comments.d.ts +36 -1
- package/dist/exclusion-comments.d.ts.map +1 -1
- package/dist/exclusion-comments.js +185 -37
- package/dist/exclusion-comments.js.map +1 -1
- package/dist/execute-rule.d.ts +12 -3
- package/dist/execute-rule.d.ts.map +1 -1
- package/dist/execute-rule.js +184 -7
- package/dist/execute-rule.js.map +1 -1
- package/dist/file-walk.d.ts +22 -0
- package/dist/file-walk.d.ts.map +1 -0
- package/dist/file-walk.js +51 -0
- package/dist/file-walk.js.map +1 -0
- package/dist/format-json.d.ts +13 -0
- package/dist/format-json.d.ts.map +1 -1
- package/dist/format-json.js +24 -0
- package/dist/format-json.js.map +1 -1
- package/dist/index.d.ts +14 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -41
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +55 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +92 -0
- package/dist/internal.js.map +1 -0
- package/dist/mask-non-comment.d.ts +49 -0
- package/dist/mask-non-comment.d.ts.map +1 -0
- package/dist/mask-non-comment.js +261 -0
- package/dist/mask-non-comment.js.map +1 -0
- package/dist/owns-empty-discovery.d.ts.map +1 -1
- package/dist/owns-empty-discovery.js +12 -4
- package/dist/owns-empty-discovery.js.map +1 -1
- package/dist/path-suffix.d.ts +57 -0
- package/dist/path-suffix.d.ts.map +1 -0
- package/dist/path-suffix.js +73 -0
- package/dist/path-suffix.js.map +1 -0
- package/dist/predicate.d.ts +12 -0
- package/dist/predicate.d.ts.map +1 -1
- package/dist/predicate.js +20 -1
- package/dist/predicate.js.map +1 -1
- package/dist/preset-dispatch.d.ts +6 -34
- package/dist/preset-dispatch.d.ts.map +1 -1
- package/dist/preset-dispatch.js +30 -52
- package/dist/preset-dispatch.js.map +1 -1
- package/dist/report.d.ts +31 -7
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +174 -15
- package/dist/report.js.map +1 -1
- package/dist/rule-builder-like.d.ts +8 -2
- package/dist/rule-builder-like.d.ts.map +1 -1
- package/dist/rule-builder.d.ts +15 -0
- package/dist/rule-builder.d.ts.map +1 -1
- package/dist/rule-builder.js +41 -36
- package/dist/rule-builder.js.map +1 -1
- package/dist/rule-description.d.ts +50 -0
- package/dist/rule-description.d.ts.map +1 -1
- package/dist/rule-description.js +58 -0
- package/dist/rule-description.js.map +1 -1
- package/dist/selection-memo.d.ts +1 -3
- package/dist/selection-memo.d.ts.map +1 -1
- package/dist/selection-memo.js +1 -3
- package/dist/selection-memo.js.map +1 -1
- package/dist/silent-exclusion.d.ts +12 -0
- package/dist/silent-exclusion.d.ts.map +1 -1
- package/dist/silent-exclusion.js +22 -0
- package/dist/silent-exclusion.js.map +1 -1
- package/dist/terminal-builder.d.ts +29 -15
- package/dist/terminal-builder.d.ts.map +1 -1
- package/dist/terminal-builder.js +97 -51
- package/dist/terminal-builder.js.map +1 -1
- package/dist/vacuity-findings.d.ts +0 -6
- package/dist/vacuity-findings.d.ts.map +1 -1
- package/dist/vacuity-findings.js +27 -57
- package/dist/vacuity-findings.js.map +1 -1
- package/dist/violation.d.ts +22 -1
- package/dist/violation.d.ts.map +1 -1
- package/dist/violation.js.map +1 -1
- package/dist/watch.d.ts +71 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +129 -0
- package/dist/watch.js.map +1 -0
- 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
|
|
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
|
|
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
|
|
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"}
|
package/dist/baseline.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/cardinality.js
CHANGED
|
@@ -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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
/**
|
package/dist/cardinality.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cardinality.js","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AAAA
|
|
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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"}
|