@nielspeter/eess 0.2.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -4
- package/dist/assertion-advice.d.ts +26 -0
- package/dist/assertion-advice.d.ts.map +1 -0
- package/dist/assertion-advice.js +29 -0
- package/dist/assertion-advice.js.map +1 -0
- 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 +69 -4
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +119 -13
- package/dist/baseline.js.map +1 -1
- package/dist/cache-registry.d.ts +14 -0
- package/dist/cache-registry.d.ts.map +1 -0
- package/dist/cache-registry.js +37 -0
- package/dist/cache-registry.js.map +1 -0
- package/dist/cardinality.d.ts +35 -0
- package/dist/cardinality.d.ts.map +1 -0
- package/dist/cardinality.js +80 -0
- package/dist/cardinality.js.map +1 -0
- package/dist/check-options.d.ts +14 -0
- package/dist/check-options.d.ts.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/comment-suppression.d.ts +60 -0
- package/dist/comment-suppression.d.ts.map +1 -0
- package/dist/comment-suppression.js +89 -0
- package/dist/comment-suppression.js.map +1 -0
- package/dist/condition.d.ts +43 -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-core.d.ts +35 -0
- package/dist/correspondence-core.d.ts.map +1 -0
- package/dist/correspondence-core.js +38 -0
- package/dist/correspondence-core.js.map +1 -0
- package/dist/correspondence.d.ts +57 -5
- package/dist/correspondence.d.ts.map +1 -1
- package/dist/correspondence.js +74 -5
- package/dist/correspondence.js.map +1 -1
- package/dist/dedupe-config-findings.d.ts +56 -0
- package/dist/dedupe-config-findings.d.ts.map +1 -0
- package/dist/dedupe-config-findings.js +101 -0
- package/dist/dedupe-config-findings.js.map +1 -0
- package/dist/define.d.ts +44 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +45 -4
- package/dist/define.js.map +1 -1
- package/dist/diff-aware.d.ts +8 -1
- package/dist/diff-aware.d.ts.map +1 -1
- package/dist/diff-aware.js +39 -9
- package/dist/diff-aware.js.map +1 -1
- package/dist/diff-disclosure.d.ts +72 -0
- package/dist/diff-disclosure.d.ts.map +1 -0
- package/dist/diff-disclosure.js +110 -0
- package/dist/diff-disclosure.js.map +1 -0
- package/dist/disk-set.d.ts +27 -0
- package/dist/disk-set.d.ts.map +1 -0
- package/dist/disk-set.js +12 -0
- package/dist/disk-set.js.map +1 -0
- package/dist/edge-coverage.d.ts +96 -0
- package/dist/edge-coverage.d.ts.map +1 -0
- package/dist/edge-coverage.js +108 -0
- package/dist/edge-coverage.js.map +1 -0
- 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 +73 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +93 -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 +11 -1
- package/dist/execute-rule.d.ts.map +1 -1
- package/dist/execute-rule.js +220 -11
- 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-github.d.ts.map +1 -1
- package/dist/format-github.js +37 -5
- package/dist/format-github.js.map +1 -1
- package/dist/format-json.d.ts +96 -1
- package/dist/format-json.d.ts.map +1 -1
- package/dist/format-json.js +94 -9
- package/dist/format-json.js.map +1 -1
- package/dist/format.d.ts +4 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +41 -19
- package/dist/format.js.map +1 -1
- package/dist/glob-site.d.ts +232 -0
- package/dist/glob-site.d.ts.map +1 -0
- package/dist/glob-site.js +156 -0
- package/dist/glob-site.js.map +1 -0
- package/dist/identity-root.d.ts +55 -0
- package/dist/identity-root.d.ts.map +1 -0
- package/dist/identity-root.js +170 -0
- package/dist/identity-root.js.map +1 -0
- package/dist/index.d.ts +18 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- 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 +5 -0
- package/dist/owns-empty-discovery.d.ts.map +1 -0
- package/dist/owns-empty-discovery.js +85 -0
- package/dist/owns-empty-discovery.js.map +1 -0
- 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/path-universe.d.ts +44 -0
- package/dist/path-universe.d.ts.map +1 -0
- package/dist/path-universe.js +21 -0
- package/dist/path-universe.js.map +1 -0
- package/dist/predicate.d.ts +45 -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 -15
- package/dist/preset-dispatch.d.ts.map +1 -1
- package/dist/preset-dispatch.js +35 -17
- package/dist/preset-dispatch.js.map +1 -1
- package/dist/project-relative.d.ts +25 -0
- package/dist/project-relative.d.ts.map +1 -0
- package/dist/project-relative.js +54 -0
- package/dist/project-relative.js.map +1 -0
- package/dist/report.d.ts +42 -12
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +190 -14
- package/dist/report.js.map +1 -1
- package/dist/rule-builder-like.d.ts +17 -0
- package/dist/rule-builder-like.d.ts.map +1 -0
- package/dist/rule-builder-like.js +2 -0
- package/dist/rule-builder-like.js.map +1 -0
- package/dist/rule-builder.d.ts +124 -84
- package/dist/rule-builder.d.ts.map +1 -1
- package/dist/rule-builder.js +230 -181
- package/dist/rule-builder.js.map +1 -1
- package/dist/rule-description.d.ts +83 -1
- package/dist/rule-description.d.ts.map +1 -1
- package/dist/rule-description.js +75 -1
- package/dist/rule-description.js.map +1 -1
- package/dist/rule-metadata.d.ts +20 -3
- package/dist/rule-metadata.d.ts.map +1 -1
- package/dist/selection-memo.d.ts +45 -0
- package/dist/selection-memo.d.ts.map +1 -0
- package/dist/selection-memo.js +66 -0
- package/dist/selection-memo.js.map +1 -0
- package/dist/shallow-clone.d.ts +20 -0
- package/dist/shallow-clone.d.ts.map +1 -0
- package/dist/shallow-clone.js +27 -0
- package/dist/shallow-clone.js.map +1 -0
- 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/stderr.d.ts +16 -0
- package/dist/stderr.d.ts.map +1 -0
- package/dist/stderr.js +73 -0
- package/dist/stderr.js.map +1 -0
- package/dist/terminal-builder.d.ts +224 -10
- package/dist/terminal-builder.d.ts.map +1 -1
- package/dist/terminal-builder.js +336 -46
- package/dist/terminal-builder.js.map +1 -1
- package/dist/type-guards.d.ts +38 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +42 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/unsuppressable.d.ts +54 -0
- package/dist/unsuppressable.d.ts.map +1 -0
- package/dist/unsuppressable.js +76 -0
- package/dist/unsuppressable.js.map +1 -0
- package/dist/vacuity-findings.d.ts +49 -0
- package/dist/vacuity-findings.d.ts.map +1 -0
- package/dist/vacuity-findings.js +94 -0
- package/dist/vacuity-findings.js.map +1 -0
- package/dist/violation.d.ts +173 -4
- package/dist/violation.d.ts.map +1 -1
- package/dist/violation.js +262 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @nielspeter/eess
|
|
2
2
|
|
|
3
|
-
The dialect-independent **kernel** of the [eess](
|
|
3
|
+
The dialect-independent **kernel** of the [eess](https://github.com/nielspeter/eess/blob/main/README.md) family.
|
|
4
4
|
|
|
5
5
|
This package is the engine every eess dialect runs on. It knows nothing about TypeScript, Mermaid, or any specific artifact format; it is generic over the element type being validated.
|
|
6
6
|
|
|
@@ -12,14 +12,107 @@ 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:
|
|
18
102
|
|
|
19
|
-
- [`@nielspeter/eess-ts`](
|
|
20
|
-
- [`@nielspeter/eess-mermaid`](
|
|
103
|
+
- [`@nielspeter/eess-ts`](https://www.npmjs.com/package/@nielspeter/eess-ts) — TypeScript
|
|
104
|
+
- [`@nielspeter/eess-mermaid`](https://www.npmjs.com/package/@nielspeter/eess-mermaid) — Mermaid class diagrams
|
|
105
|
+
|
|
106
|
+
Install `@nielspeter/eess` directly only when authoring a new dialect or a cross-dialect
|
|
107
|
+
tool on top of the kernel.
|
|
21
108
|
|
|
22
|
-
|
|
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
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assertion-less remedy, by state — bug 0155.
|
|
3
|
+
*
|
|
4
|
+
* A pure function of three facts about a rule, deliberately outside
|
|
5
|
+
* `RuleBuilder`: the class is gated at 300 lines and 20 methods by this repo's
|
|
6
|
+
* own `arch.internal.rules.ts`, and this capability pushed it over both. It is
|
|
7
|
+
* also genuinely independent of the builder — three inputs, one string — so a
|
|
8
|
+
* dialect builder with its own hierarchy can reuse it rather than reinvent the
|
|
9
|
+
* wording.
|
|
10
|
+
*
|
|
11
|
+
* **Branches on `reachedShould`/`misplaced`, never on the builder's phase:**
|
|
12
|
+
* `.should().that()` lands back in the predicate phase having reached
|
|
13
|
+
* `.should()`, and any phase-derived message would tell that author a
|
|
14
|
+
* verifiable falsehood.
|
|
15
|
+
*/
|
|
16
|
+
interface AssertionState {
|
|
17
|
+
/** Whether `.should()` was ever reached. */
|
|
18
|
+
readonly reachedShould: boolean;
|
|
19
|
+
/** Descriptions of predicate-only methods written after `.should()`. */
|
|
20
|
+
readonly misplaced: readonly string[];
|
|
21
|
+
/** How many conditions the rule actually carries. */
|
|
22
|
+
readonly conditionCount: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function assertionAdviceFor(state: AssertionState): string;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=assertion-advice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertion-advice.d.ts","sourceRoot":"","sources":["../src/assertion-advice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,UAAU,cAAc;IACtB,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,qDAAqD;IACrD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAmChE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function assertionAdviceFor(state) {
|
|
2
|
+
if (!state.reachedShould) {
|
|
3
|
+
return ('This rule never reached .should(), so it asserts nothing and can never fail. ' +
|
|
4
|
+
'Add .should() and a condition, or delete the rule.');
|
|
5
|
+
}
|
|
6
|
+
if (state.misplaced.length > 0) {
|
|
7
|
+
const names = state.misplaced.map((d) => `"${d}"`).join(', ');
|
|
8
|
+
const one = state.misplaced.length === 1;
|
|
9
|
+
const verb = one ? 'is a predicate, which filters' : 'are predicates, which filter';
|
|
10
|
+
const it = one ? 'it' : 'them';
|
|
11
|
+
// Two faults, two remedies. With conditions present the rule is not
|
|
12
|
+
// "asserting nothing" in the reader's sense — it asserts something over a
|
|
13
|
+
// set the misplaced predicate silently shrank. Telling this author to
|
|
14
|
+
// "add a condition" would name a fix that leaves the rule as broken.
|
|
15
|
+
if (state.conditionCount > 0) {
|
|
16
|
+
return (`This rule's ${names} ${verb} subjects rather than asserting anything about them, ` +
|
|
17
|
+
`and ${one ? 'it comes' : 'they come'} after .should() — so ${it} narrowed the ` +
|
|
18
|
+
"selection this rule's conditions are evaluated over, and if that narrowed it to " +
|
|
19
|
+
`nothing the conditions hold vacuously. Move ${it} before .should(), where the ` +
|
|
20
|
+
'filtering is explicit.');
|
|
21
|
+
}
|
|
22
|
+
return (`This rule asserts nothing: ${names} ${verb} subjects rather than asserting ` +
|
|
23
|
+
`anything about them. Move ${it} before .should(), then add a condition.`);
|
|
24
|
+
}
|
|
25
|
+
return ('This rule reached .should() but no condition follows, so it asserts nothing and can ' +
|
|
26
|
+
'never fail. Add a condition after .should() — or, if this rule is generated from ' +
|
|
27
|
+
'configuration, skip generating it when there is nothing to assert.');
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=assertion-advice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertion-advice.js","sourceRoot":"","sources":["../src/assertion-advice.ts"],"names":[],"mappings":"AAwBA,MAAM,UAAU,kBAAkB,CAAC,KAAqB;IACtD,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;QACzB,OAAO,CACL,+EAA+E;YAC/E,oDAAoD,CACrD,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAA;QACxC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,8BAA8B,CAAA;QACnF,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;QAC9B,oEAAoE;QACpE,0EAA0E;QAC1E,sEAAsE;QACtE,qEAAqE;QACrE,IAAI,KAAK,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;YAC7B,OAAO,CACL,eAAe,KAAK,IAAI,IAAI,uDAAuD;gBACnF,OAAO,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,yBAAyB,EAAE,gBAAgB;gBAChF,kFAAkF;gBAClF,+CAA+C,EAAE,+BAA+B;gBAChF,wBAAwB,CACzB,CAAA;QACH,CAAC;QACD,OAAO,CACL,8BAA8B,KAAK,IAAI,IAAI,kCAAkC;YAC7E,6BAA6B,EAAE,0CAA0C,CAC1E,CAAA;IACH,CAAC;IACD,OAAO,CACL,sFAAsF;QACtF,mFAAmF;QACnF,oEAAoE,CACrE,CAAA;AACH,CAAC"}
|
|
@@ -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
|
@@ -16,6 +16,33 @@ export interface BaselineEntry {
|
|
|
16
16
|
line: number;
|
|
17
17
|
/** Stable identity hash: sha256(rule + element + message) */
|
|
18
18
|
hash: string;
|
|
19
|
+
/**
|
|
20
|
+
* The measurement this entry accepted, for a metric finding
|
|
21
|
+
* (`ArchViolation.measured`). Absent for every non-metric entry, so a
|
|
22
|
+
* baseline with no metric findings is byte-identical to one without this
|
|
23
|
+
* field at all.
|
|
24
|
+
*/
|
|
25
|
+
measured?: number;
|
|
26
|
+
/**
|
|
27
|
+
* What {@link measured} counts — `code-lines`, `methods`, `complexity`.
|
|
28
|
+
*
|
|
29
|
+
* Written since [bug 0171](../../../work/bugs/fixed/0171-a-metric-unit-change-silently-loosens-every-baselined-ratchet.md).
|
|
30
|
+
* An accepted ceiling is a number IN A UNIT, and comparing across a change of
|
|
31
|
+
* unit does not measure a regression — it moves the bar. Absent on entries
|
|
32
|
+
* written before stamping, which {@link Baseline.isKnown} treats as "unknown
|
|
33
|
+
* unit": comparable only for metrics whose meaning has never changed.
|
|
34
|
+
*
|
|
35
|
+
* No dialect on the kernel emits `measured` today; this is here so the hole
|
|
36
|
+
* cannot open silently in the one that does, and so the two copies of this
|
|
37
|
+
* file do not disagree about what a baseline entry means.
|
|
38
|
+
*/
|
|
39
|
+
measuredUnit?: string;
|
|
40
|
+
}
|
|
41
|
+
/** A measurement an entry accepted, and what that number counts (bug 0171). */
|
|
42
|
+
interface AcceptedMeasurement {
|
|
43
|
+
value: number;
|
|
44
|
+
/** `undefined` on entries written before unit stamping. */
|
|
45
|
+
unit?: string;
|
|
19
46
|
}
|
|
20
47
|
/**
|
|
21
48
|
* The baseline file structure.
|
|
@@ -31,19 +58,32 @@ export interface BaselineFile {
|
|
|
31
58
|
/**
|
|
32
59
|
* Compute a stable hash for a violation.
|
|
33
60
|
*
|
|
34
|
-
* Uses
|
|
61
|
+
* Uses `violation.identity` when the condition set one (a canonical,
|
|
62
|
+
* position-independent key — see `ArchViolation.identity`'s own doc comment);
|
|
63
|
+
* otherwise falls back to rule + element + message. The fallback survives:
|
|
35
64
|
* - Line number changes (code moved)
|
|
36
65
|
* - Unrelated code changes in the same file
|
|
37
66
|
*
|
|
38
67
|
* Does NOT survive:
|
|
39
68
|
* - Rule description changes (rewording .because())
|
|
40
69
|
* - Element renames (class renamed)
|
|
41
|
-
* - Message text changes (condition wording updated)
|
|
70
|
+
* - Message text changes (condition wording updated) — including a message
|
|
71
|
+
* that embeds a line number, which changes whenever code shifts above the
|
|
72
|
+
* match. A condition that reports such a message sets `identity` instead
|
|
73
|
+
* of relying on this fallback.
|
|
42
74
|
*
|
|
43
75
|
* This is intentional — if the rule or element changes,
|
|
44
76
|
* the violation should be re-evaluated.
|
|
77
|
+
*
|
|
78
|
+
* `root`, when supplied, is scrubbed out of both fields via
|
|
79
|
+
* `normalizeIdentityText` before hashing — an `identity` string that
|
|
80
|
+
* interpolates a file path (e.g. `identifyMatches`'s per-declaration
|
|
81
|
+
* identities) otherwise embeds the absolute checkout path, and the same
|
|
82
|
+
* baseline generated on two machines (or locally vs. CI) would never match.
|
|
83
|
+
* Omitted, hashing is unchanged — this stays opt-in so a caller with no
|
|
84
|
+
* portability need (or no discoverable root) pays nothing for it.
|
|
45
85
|
*/
|
|
46
|
-
export declare function hashViolation(violation: ArchViolation): string;
|
|
86
|
+
export declare function hashViolation(violation: ArchViolation, root?: string): string;
|
|
47
87
|
/**
|
|
48
88
|
* Load a baseline from a JSON file.
|
|
49
89
|
*
|
|
@@ -67,17 +107,42 @@ export declare function generateBaseline(violations: ArchViolation[], outputPath
|
|
|
67
107
|
export declare class Baseline {
|
|
68
108
|
private readonly knownHashes;
|
|
69
109
|
private readonly baselineDir;
|
|
70
|
-
|
|
110
|
+
/** Computed once at load time — must match `generateBaseline()`'s root for entries to line up. */
|
|
111
|
+
private readonly root?;
|
|
112
|
+
/** hash -> accepted measurement, for metric findings. */
|
|
113
|
+
private readonly accepted;
|
|
114
|
+
constructor(knownHashes: Set<string>, baselineDir: string,
|
|
115
|
+
/** Computed once at load time — must match `generateBaseline()`'s root for entries to line up. */
|
|
116
|
+
root?: string | undefined,
|
|
117
|
+
/** hash -> accepted measurement, for metric findings. */
|
|
118
|
+
accepted?: Map<string, AcceptedMeasurement>);
|
|
71
119
|
/**
|
|
72
120
|
* Check if a violation is known (exists in the baseline).
|
|
73
121
|
* Known violations are filtered out — they don't cause failures.
|
|
122
|
+
*
|
|
123
|
+
* For a metric finding (`violation.measured` set), "known" is a **ratchet**,
|
|
124
|
+
* not hash equality: the finding is known if the baseline has an accepted
|
|
125
|
+
* measurement for this identity AND the current measurement is no worse
|
|
126
|
+
* than what was accepted. Identity alone (the hash) cannot express this —
|
|
127
|
+
* it deliberately excludes the count, so an improved measurement keeps the
|
|
128
|
+
* same hash as a regressed one, and hash-only matching would treat both as
|
|
129
|
+
* "known" alike. Comparing the two numbers is what tells them apart: an
|
|
130
|
+
* improvement stays known (still <= accepted), a regression does not (now >
|
|
131
|
+
* accepted), even though neither changed which entry it matches.
|
|
74
132
|
*/
|
|
75
133
|
isKnown(violation: ArchViolation): boolean;
|
|
76
134
|
/**
|
|
77
135
|
* Filter out known violations, returning only new ones.
|
|
136
|
+
*
|
|
137
|
+
* A `bypassFilters` configuration finding is never treated as known, even
|
|
138
|
+
* if an older or hand-edited baseline file happens to carry a matching
|
|
139
|
+
* hash — `generateBaseline()` no longer writes these, but a baseline
|
|
140
|
+
* generated before that fix (or hand-edited) must not be able to
|
|
141
|
+
* resurrect the suppression this defends against.
|
|
78
142
|
*/
|
|
79
143
|
filterNew(violations: ArchViolation[]): ArchViolation[];
|
|
80
144
|
/** Number of known violations in the baseline */
|
|
81
145
|
get size(): number;
|
|
82
146
|
}
|
|
147
|
+
export {};
|
|
83
148
|
//# sourceMappingURL=baseline.d.ts.map
|
package/dist/baseline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAInD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,+EAA+E;AAC/E,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AA6BD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAA;IACnB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,UAAU,EAAE,aAAa,EAAE,CAAA;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAa7E;AAUD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAwC3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAgCtF;AAED;;GAEG;AACH,qBAAa,QAAQ;IAEjB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,kGAAkG;IAClG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;IACtB,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBALR,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,MAAM;IACpC,kGAAkG;IACjF,IAAI,CAAC,EAAE,MAAM,YAAA;IAC9B,yDAAyD;IACxC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAa;IAGzE;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO;IAU1C;;;;;;;;OAQG;IACH,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE;IAIvD,iDAAiD;IACjD,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
package/dist/baseline.js
CHANGED
|
@@ -1,23 +1,73 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import { discoverIdentityRoot, normalizeIdentityText } from './identity-root.js';
|
|
5
|
+
import { writeStderr } from './stderr.js';
|
|
6
|
+
/**
|
|
7
|
+
* Units whose meaning has never changed, so an entry predating unit stamping is
|
|
8
|
+
* still safely comparable against one.
|
|
9
|
+
*
|
|
10
|
+
* `code-lines` is deliberately absent: an unstamped `lines` entry was written
|
|
11
|
+
* when the metric counted a SPAN, so its number is denominated in something no
|
|
12
|
+
* longer produced. Comparing across that raises the ceiling silently.
|
|
13
|
+
*/
|
|
14
|
+
const MEANING_NEVER_CHANGED = new Set([
|
|
15
|
+
'methods',
|
|
16
|
+
'parameters',
|
|
17
|
+
'properties',
|
|
18
|
+
'named-exports',
|
|
19
|
+
'complexity',
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* Whether a stored measurement may be compared against a current one. Fails
|
|
23
|
+
* CLOSED — anything but a demonstrable match is incomparable, because assuming
|
|
24
|
+
* agreement is how a ratchet loosens without anyone being told (ADR-010).
|
|
25
|
+
*/
|
|
26
|
+
function measurementComparable(stored, current) {
|
|
27
|
+
if (stored === current)
|
|
28
|
+
return true;
|
|
29
|
+
if (stored === undefined)
|
|
30
|
+
return current !== undefined && MEANING_NEVER_CHANGED.has(current);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
4
33
|
/**
|
|
5
34
|
* Compute a stable hash for a violation.
|
|
6
35
|
*
|
|
7
|
-
* Uses
|
|
36
|
+
* Uses `violation.identity` when the condition set one (a canonical,
|
|
37
|
+
* position-independent key — see `ArchViolation.identity`'s own doc comment);
|
|
38
|
+
* otherwise falls back to rule + element + message. The fallback survives:
|
|
8
39
|
* - Line number changes (code moved)
|
|
9
40
|
* - Unrelated code changes in the same file
|
|
10
41
|
*
|
|
11
42
|
* Does NOT survive:
|
|
12
43
|
* - Rule description changes (rewording .because())
|
|
13
44
|
* - Element renames (class renamed)
|
|
14
|
-
* - Message text changes (condition wording updated)
|
|
45
|
+
* - Message text changes (condition wording updated) — including a message
|
|
46
|
+
* that embeds a line number, which changes whenever code shifts above the
|
|
47
|
+
* match. A condition that reports such a message sets `identity` instead
|
|
48
|
+
* of relying on this fallback.
|
|
15
49
|
*
|
|
16
50
|
* This is intentional — if the rule or element changes,
|
|
17
51
|
* the violation should be re-evaluated.
|
|
52
|
+
*
|
|
53
|
+
* `root`, when supplied, is scrubbed out of both fields via
|
|
54
|
+
* `normalizeIdentityText` before hashing — an `identity` string that
|
|
55
|
+
* interpolates a file path (e.g. `identifyMatches`'s per-declaration
|
|
56
|
+
* identities) otherwise embeds the absolute checkout path, and the same
|
|
57
|
+
* baseline generated on two machines (or locally vs. CI) would never match.
|
|
58
|
+
* Omitted, hashing is unchanged — this stays opt-in so a caller with no
|
|
59
|
+
* portability need (or no discoverable root) pays nothing for it.
|
|
18
60
|
*/
|
|
19
|
-
export function hashViolation(violation) {
|
|
20
|
-
const
|
|
61
|
+
export function hashViolation(violation, root) {
|
|
62
|
+
const scrub = (text) => root === undefined ? text : normalizeIdentityText(text, root);
|
|
63
|
+
// `rule` is always part of the hash, even when `identity` is set — two
|
|
64
|
+
// different rules that happened to produce the same identity string (e.g.
|
|
65
|
+
// two metric conditions on the same class) must not collide in one
|
|
66
|
+
// baseline entry.
|
|
67
|
+
const subject = violation.identity !== undefined
|
|
68
|
+
? violation.identity
|
|
69
|
+
: `${violation.element}::${violation.message}`;
|
|
70
|
+
const content = `${scrub(violation.rule)}::${scrub(subject)}`;
|
|
21
71
|
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
22
72
|
}
|
|
23
73
|
/**
|
|
@@ -36,9 +86,10 @@ function toRelativePath(absolutePath, baselineDir) {
|
|
|
36
86
|
export function withBaseline(baselinePath) {
|
|
37
87
|
const resolved = path.resolve(baselinePath);
|
|
38
88
|
const baselineDir = path.dirname(resolved);
|
|
89
|
+
const root = discoverIdentityRoot(baselineDir);
|
|
39
90
|
if (!fs.existsSync(resolved)) {
|
|
40
91
|
// No baseline file = no known violations = all violations are new
|
|
41
|
-
return new Baseline(new Set(), baselineDir);
|
|
92
|
+
return new Baseline(new Set(), baselineDir, root);
|
|
42
93
|
}
|
|
43
94
|
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
44
95
|
const parsed = JSON.parse(raw);
|
|
@@ -46,17 +97,26 @@ export function withBaseline(baselinePath) {
|
|
|
46
97
|
typeof parsed !== 'object' ||
|
|
47
98
|
!('violations' in parsed) ||
|
|
48
99
|
!Array.isArray(parsed.violations)) {
|
|
49
|
-
|
|
50
|
-
return new Baseline(new Set(), baselineDir);
|
|
100
|
+
writeStderr(`[eess] Invalid baseline file format at ${resolved} — treating as empty`);
|
|
101
|
+
return new Baseline(new Set(), baselineDir, root);
|
|
51
102
|
}
|
|
52
103
|
const violations = parsed.violations;
|
|
53
104
|
const hashes = new Set();
|
|
105
|
+
const accepted = new Map();
|
|
54
106
|
for (const entry of violations) {
|
|
55
107
|
if (entry && typeof entry === 'object' && 'hash' in entry && typeof entry.hash === 'string') {
|
|
56
108
|
hashes.add(entry.hash);
|
|
109
|
+
if ('measured' in entry && typeof entry.measured === 'number') {
|
|
110
|
+
accepted.set(entry.hash, {
|
|
111
|
+
value: entry.measured,
|
|
112
|
+
unit: 'measuredUnit' in entry && typeof entry.measuredUnit === 'string'
|
|
113
|
+
? entry.measuredUnit
|
|
114
|
+
: undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
57
117
|
}
|
|
58
118
|
}
|
|
59
|
-
return new Baseline(hashes, baselineDir);
|
|
119
|
+
return new Baseline(hashes, baselineDir, root, accepted);
|
|
60
120
|
}
|
|
61
121
|
/**
|
|
62
122
|
* Generate a baseline file from a list of violations.
|
|
@@ -70,11 +130,24 @@ export function withBaseline(baselinePath) {
|
|
|
70
130
|
export function generateBaseline(violations, outputPath) {
|
|
71
131
|
const resolved = path.resolve(outputPath);
|
|
72
132
|
const baselineDir = path.dirname(resolved);
|
|
73
|
-
const
|
|
133
|
+
const root = discoverIdentityRoot(baselineDir);
|
|
134
|
+
// A bypassFilters configuration finding (ADR-010) can never legitimately
|
|
135
|
+
// become "known, pre-existing debt" — it means the rule's own instrument
|
|
136
|
+
// is broken right now. Recording it would let a future run of `check
|
|
137
|
+
// --baseline` silently treat a dead selector as already-accepted.
|
|
138
|
+
const entries = violations
|
|
139
|
+
.filter((v) => v.bypassFilters !== true)
|
|
140
|
+
.map((v) => ({
|
|
74
141
|
rule: v.rule,
|
|
75
142
|
file: toRelativePath(v.file, baselineDir),
|
|
76
143
|
line: v.line,
|
|
77
|
-
hash: hashViolation(v),
|
|
144
|
+
hash: hashViolation(v, root),
|
|
145
|
+
// Written only for metric findings, so a baseline with none is
|
|
146
|
+
// byte-identical to one from before this field existed.
|
|
147
|
+
...(v.measured === undefined ? {} : { measured: v.measured }),
|
|
148
|
+
// Bug 0171: what that number counts, so a later change to the metric
|
|
149
|
+
// cannot silently re-denominate an accepted ceiling.
|
|
150
|
+
...(v.measuredUnit === undefined ? {} : { measuredUnit: v.measuredUnit }),
|
|
78
151
|
}));
|
|
79
152
|
const baseline = {
|
|
80
153
|
generatedAt: new Date().toISOString(),
|
|
@@ -90,22 +163,55 @@ export function generateBaseline(violations, outputPath) {
|
|
|
90
163
|
export class Baseline {
|
|
91
164
|
knownHashes;
|
|
92
165
|
baselineDir;
|
|
93
|
-
|
|
166
|
+
root;
|
|
167
|
+
accepted;
|
|
168
|
+
constructor(knownHashes, baselineDir,
|
|
169
|
+
/** Computed once at load time — must match `generateBaseline()`'s root for entries to line up. */
|
|
170
|
+
root,
|
|
171
|
+
/** hash -> accepted measurement, for metric findings. */
|
|
172
|
+
accepted = new Map()) {
|
|
94
173
|
this.knownHashes = knownHashes;
|
|
95
174
|
this.baselineDir = baselineDir;
|
|
175
|
+
this.root = root;
|
|
176
|
+
this.accepted = accepted;
|
|
96
177
|
}
|
|
97
178
|
/**
|
|
98
179
|
* Check if a violation is known (exists in the baseline).
|
|
99
180
|
* Known violations are filtered out — they don't cause failures.
|
|
181
|
+
*
|
|
182
|
+
* For a metric finding (`violation.measured` set), "known" is a **ratchet**,
|
|
183
|
+
* not hash equality: the finding is known if the baseline has an accepted
|
|
184
|
+
* measurement for this identity AND the current measurement is no worse
|
|
185
|
+
* than what was accepted. Identity alone (the hash) cannot express this —
|
|
186
|
+
* it deliberately excludes the count, so an improved measurement keeps the
|
|
187
|
+
* same hash as a regressed one, and hash-only matching would treat both as
|
|
188
|
+
* "known" alike. Comparing the two numbers is what tells them apart: an
|
|
189
|
+
* improvement stays known (still <= accepted), a regression does not (now >
|
|
190
|
+
* accepted), even though neither changed which entry it matches.
|
|
100
191
|
*/
|
|
101
192
|
isKnown(violation) {
|
|
102
|
-
|
|
193
|
+
const hash = hashViolation(violation, this.root);
|
|
194
|
+
if (violation.measured === undefined)
|
|
195
|
+
return this.knownHashes.has(hash);
|
|
196
|
+
const acceptedMeasurement = this.accepted.get(hash);
|
|
197
|
+
if (acceptedMeasurement === undefined)
|
|
198
|
+
return false;
|
|
199
|
+
// Bug 0171: `<=` means nothing until both numbers count the same thing.
|
|
200
|
+
if (!measurementComparable(acceptedMeasurement.unit, violation.measuredUnit))
|
|
201
|
+
return false;
|
|
202
|
+
return violation.measured <= acceptedMeasurement.value;
|
|
103
203
|
}
|
|
104
204
|
/**
|
|
105
205
|
* Filter out known violations, returning only new ones.
|
|
206
|
+
*
|
|
207
|
+
* A `bypassFilters` configuration finding is never treated as known, even
|
|
208
|
+
* if an older or hand-edited baseline file happens to carry a matching
|
|
209
|
+
* hash — `generateBaseline()` no longer writes these, but a baseline
|
|
210
|
+
* generated before that fix (or hand-edited) must not be able to
|
|
211
|
+
* resurrect the suppression this defends against.
|
|
106
212
|
*/
|
|
107
213
|
filterNew(violations) {
|
|
108
|
-
return violations.filter((v) => !this.isKnown(v));
|
|
214
|
+
return violations.filter((v) => v.bypassFilters === true || !this.isKnown(v));
|
|
109
215
|
}
|
|
110
216
|
/** Number of known violations in the baseline */
|
|
111
217
|
get size() {
|
package/dist/baseline.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Register a cache's reset. Call once, at module scope. */
|
|
2
|
+
export declare function registerCacheReset(reset: () => void): void;
|
|
3
|
+
/**
|
|
4
|
+
* Drop every registered cache.
|
|
5
|
+
*
|
|
6
|
+
* Called by `resetProjectCache()`. The caches are keyed on object identity, so
|
|
7
|
+
* a caller who obtains projects through `project()` / `workspace()` is already
|
|
8
|
+
* covered — those functions hand back a new object. This is the escape hatch
|
|
9
|
+
* for a consumer holding an `ArchProject` **they** built across a mutation of
|
|
10
|
+
* the underlying ts-morph project, where identity does not change and the
|
|
11
|
+
* cached population would otherwise be frozen.
|
|
12
|
+
*/
|
|
13
|
+
export declare function clearRegisteredCaches(): void;
|
|
14
|
+
//# sourceMappingURL=cache-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-registry.d.ts","sourceRoot":"","sources":["../src/cache-registry.ts"],"names":[],"mappings":"AAmBA,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,IAAI,GAAG,IAAI,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where invalidatable caches register themselves.
|
|
3
|
+
*
|
|
4
|
+
* This module exists to break an import cycle, and the cycle was found by this
|
|
5
|
+
* repository's own `beFreeOfCycles` rule rather than by review. Wiring
|
|
6
|
+
* `resetProjectCache()` directly to `clearElementCaches()` made
|
|
7
|
+
* `project.ts → element-cache.ts → project.ts` — the back edge being
|
|
8
|
+
* `import type { ArchProject }`, which counts, because dependency conditions
|
|
9
|
+
* have seen type-expression edges since v0.28.0.
|
|
10
|
+
*
|
|
11
|
+
* So the registry depends on nothing. `element-cache.ts` and `module-edges.ts`
|
|
12
|
+
* register a reset closure here; `project.ts` calls {@link clearRegisteredCaches}
|
|
13
|
+
* and never learns what is in it.
|
|
14
|
+
*
|
|
15
|
+
* A `WeakMap` cannot be enumerated, so "clear" means "replace the map". Each
|
|
16
|
+
* cache contributes the closure that replaces its own.
|
|
17
|
+
*/
|
|
18
|
+
const resets = [];
|
|
19
|
+
/** Register a cache's reset. Call once, at module scope. */
|
|
20
|
+
export function registerCacheReset(reset) {
|
|
21
|
+
resets.push(reset);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Drop every registered cache.
|
|
25
|
+
*
|
|
26
|
+
* Called by `resetProjectCache()`. The caches are keyed on object identity, so
|
|
27
|
+
* a caller who obtains projects through `project()` / `workspace()` is already
|
|
28
|
+
* covered — those functions hand back a new object. This is the escape hatch
|
|
29
|
+
* for a consumer holding an `ArchProject` **they** built across a mutation of
|
|
30
|
+
* the underlying ts-morph project, where identity does not change and the
|
|
31
|
+
* cached population would otherwise be frozen.
|
|
32
|
+
*/
|
|
33
|
+
export function clearRegisteredCaches() {
|
|
34
|
+
for (const reset of resets)
|
|
35
|
+
reset();
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=cache-registry.js.map
|