@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/dist/violation.js
CHANGED
|
@@ -1,2 +1,263 @@
|
|
|
1
|
-
|
|
1
|
+
import { registerCacheReset } from './cache-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract a human-readable name from a ts-morph Node.
|
|
4
|
+
*
|
|
5
|
+
* If the node itself is a named declaration (class, function, method, etc.),
|
|
6
|
+
* returns its name directly. Otherwise, walks up the AST ancestors to find
|
|
7
|
+
* the nearest named declaration and builds a qualified name like
|
|
8
|
+
* "ClassName.methodName". This ensures that inner nodes (e.g., AsExpression,
|
|
9
|
+
* CallExpression) produce meaningful element names for `.excluding()` matching.
|
|
10
|
+
*
|
|
11
|
+
* Falls back to the node's kind name only if no named ancestor is found
|
|
12
|
+
* (e.g., top-level expressions in a module).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Force a configuration meta-finding to `error`, whatever the rule asked for.
|
|
16
|
+
*
|
|
17
|
+
* A `bypassFilters` finding reports that the rule enforces **nothing**. That is
|
|
18
|
+
* not a violation the author gets to grade: `.asSeverity('warn')` says "these
|
|
19
|
+
* violations are advisory", and a rule that cannot fire has no violations to
|
|
20
|
+
* be advisory about. Under ADR-009 rule 1 an actionable finding must fail, and
|
|
21
|
+
* the other suppression paths already refuse to silence these — `.excluding()`
|
|
22
|
+
* refuses explicitly, baseline and diff honour the flag. The full roster is
|
|
23
|
+
* `UNSUPPRESSABLE_MECHANISMS` in `unsuppressable.ts`, which is also what the
|
|
24
|
+
* user-facing sentence is built from; it has grown twice, so it is named here
|
|
25
|
+
* rather than counted. (This paragraph said "three of the four" while the list
|
|
26
|
+
* stood at six.)
|
|
27
|
+
*
|
|
28
|
+
* Applied at all three severity-stamping sites. `stampSeverity` alone is not
|
|
29
|
+
* enough: `.violations()` inlines its own map in both root builders, and the
|
|
30
|
+
* `executeWarn` path resolves an unset severity to `warn`, which is where most
|
|
31
|
+
* producers landed.
|
|
32
|
+
*/
|
|
33
|
+
export function severityFor(violation, fallback) {
|
|
34
|
+
return violation.bypassFilters === true ? 'error' : fallback;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True when a violation's remedy **is** its message, so a renderer that has
|
|
38
|
+
* already shown the message must not append a `Fix:` line repeating it.
|
|
39
|
+
*
|
|
40
|
+
* The assertion gate's finding reports that a rule cannot fire; there, the fault
|
|
41
|
+
* and its remedy are one sentence, and both fields carry that sentence on
|
|
42
|
+
* purpose — the JSON payload reads `suggestion`, a human reads the body, and a
|
|
43
|
+
* configuration finding is the one kind that cannot fall back to the author's
|
|
44
|
+
* `suggestion` (`execute-rule.ts` refuses it, bug 0021). Measured before the
|
|
45
|
+
* fix: every surface printed the same paragraph twice.
|
|
46
|
+
*
|
|
47
|
+
* One definition, because the three renderers must not disagree about it.
|
|
48
|
+
* Whether a given renderer *did* render the message stays renderer-local: the
|
|
49
|
+
* rich terminal format shows it only for a location-less finding, while the
|
|
50
|
+
* plain and GitHub formats always do.
|
|
51
|
+
*/
|
|
52
|
+
export function remedyRepeatsMessage(violation) {
|
|
53
|
+
return violation.suggestion !== undefined && violation.suggestion === violation.message;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Unicode codepoint order — the only comparator admissible anywhere a baseline identity, an
|
|
57
|
+
* `element`, or a reported location is derived from a sort.
|
|
58
|
+
*
|
|
59
|
+
* `String.prototype.localeCompare` without an explicit locale reads the **host** locale from
|
|
60
|
+
* `LANG`/`LC_ALL`, so a value sorted with it differs between a developer's machine and CI.
|
|
61
|
+
* Measured on plain ASCII: `['zebra','aardvark']` sorts to `aardvark,zebra` under `en-US` and
|
|
62
|
+
* to `zebra,aardvark` under `da-DK`, because Danish collates `aa` as `å`, after `z`. One
|
|
63
|
+
* finding, two baseline hashes, diverging only in the place hardest to debug.
|
|
64
|
+
*
|
|
65
|
+
* The codebase already knew: `module-edges.ts` wrote *"`localeCompare` is ICU/locale sensitive
|
|
66
|
+
* — exactly the machine-dependent ordering `conditions/slice.ts` goes out of its way to
|
|
67
|
+
* eliminate, because a value that differs per machine gives one finding two identities"* — 60
|
|
68
|
+
* lines above a `localeCompare` in the identity discriminator, while `conditions/slice.ts`
|
|
69
|
+
* eliminated its own machine-dependence *using `localeCompare`*. Four sites in all.
|
|
70
|
+
*
|
|
71
|
+
* `tests/tools/scan-locale-ordering.test.ts` is what keeps that from recurring: a runtime row
|
|
72
|
+
* cannot catch it, because on an `en-US` machine both comparators agree — a guard written for
|
|
73
|
+
* this defect passed 16/16 with the defect reinstated.
|
|
74
|
+
*/
|
|
75
|
+
export function byCodepoint(a, b) {
|
|
76
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The subject `hashViolation` keys a baseline entry on: `identity`, or `element::message`.
|
|
80
|
+
*
|
|
81
|
+
* **Exported and consumed by `hashViolation`/`hashSubject`** rather than duplicated. An earlier
|
|
82
|
+
* draft kept a private copy here on the reasoning that `core/` must not depend on `helpers/` —
|
|
83
|
+
* true, but backwards: `helpers/baseline.ts` already imports from `core/`, which is the
|
|
84
|
+
* permitted direction, and `core/identity-root.ts` records the same move for the same reason.
|
|
85
|
+
* A copy plus a test asserting the copies agree is strictly worse than one definition: the
|
|
86
|
+
* test can only compare the formulas over fixtures it happens to build, and the first draft's
|
|
87
|
+
* fixture shared every field, so it agreed under any formula.
|
|
88
|
+
*/
|
|
89
|
+
export function subjectOf(violation) {
|
|
90
|
+
return violation.identity ?? `${violation.element}::${violation.message}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The full key `hashViolation` hashes — `rule::subject` — and therefore the unit
|
|
94
|
+
* {@link disambiguateIdentities} must group on.
|
|
95
|
+
*
|
|
96
|
+
* **Grouping on the subject alone was a defect**, measured: `hashViolation` includes `rule`, so
|
|
97
|
+
* two findings carrying the same `element::message` under *different* rule descriptions already
|
|
98
|
+
* hash apart and were never ambiguous. Suffixing the second moved a baseline entry that had no
|
|
99
|
+
* collision — the precise failure this mechanism exists to avoid, committed by the mechanism.
|
|
100
|
+
*
|
|
101
|
+
* Not live at the time it was found (instrumenting the whole suite showed one batch carrying
|
|
102
|
+
* more than one rule string and zero over-suffixed findings), but reachable by construction:
|
|
103
|
+
* several builders mix `metadata?.id ?? description` with a bare `description` inside one batch
|
|
104
|
+
* — `slice-rule-builder.ts`, `correspondence-builder.ts`, `tsconfig-builder.ts`,
|
|
105
|
+
* `inconsistent-siblings.ts`, `schema-rule-builder.ts` all do. Any of them emitting a colliding
|
|
106
|
+
* subject across their two rule strings would have moved a published entry.
|
|
107
|
+
*/
|
|
108
|
+
function groupKeyOf(violation) {
|
|
109
|
+
return `${violation.rule}::${subjectOf(violation)}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Give every finding in a rule a distinct identity, by suffixing only the ones that collide.
|
|
113
|
+
*
|
|
114
|
+
* {@link ArchViolation.identity} states the invariant this enforces: *"it must be unique per
|
|
115
|
+
* finding within a rule: two distinct violations sharing one identity are one violation to the
|
|
116
|
+
* baseline, and accepting either accepts both."* That was prose with nothing behind it, and
|
|
117
|
+
* three families broke it independently —
|
|
118
|
+
* [ts-archunit bug 0063](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0063-a-dependency-identity-collides-across-files-sharing-a-basename.md)
|
|
119
|
+
* (dependency), [ts-archunit plan 0088](https://github.com/nielspeter/ts-archunit/blob/main/plans/0088-a-slice-finding-identifies-itself.md) (slice), and
|
|
120
|
+
* then [0064](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0064-a-dependency-identity-collides-across-two-spellings-of-one-module.md)
|
|
121
|
+
* and [0065](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0065-reverse-dependency-findings-carry-no-identity.md). Each was fixed
|
|
122
|
+
* per-family, in the family that happened to be reviewed; this is the mechanism, so the next
|
|
123
|
+
* producer cannot reintroduce it.
|
|
124
|
+
*
|
|
125
|
+
* **A finding whose subject is unique is returned untouched — that is a theorem, not a
|
|
126
|
+
* measurement.** Only a subject that appears more than once is altered, and only from its
|
|
127
|
+
* second occurrence onward, so:
|
|
128
|
+
*
|
|
129
|
+
* - no existing baseline entry moves, because a *unique* entry is what a correct baseline holds;
|
|
130
|
+
* - the entry a colliding pair *did* record still matches, because the first occurrence keeps
|
|
131
|
+
* its subject verbatim — so the fail-open closes by making the hidden sibling report as new,
|
|
132
|
+
* which is the finding an adopter never got to see.
|
|
133
|
+
*
|
|
134
|
+
* Groups that collide were, by definition, one baseline entry standing for two findings: an
|
|
135
|
+
* entry that was already wrong. Those are the only ones that move.
|
|
136
|
+
*
|
|
137
|
+
* **Ordering — a limitation, not a guarantee.** The suffix follows the order the rule produced
|
|
138
|
+
* its findings, so a baseline entry accepts *a position in a group*, not *a finding*. An earlier
|
|
139
|
+
* draft of this paragraph said the residual "is not a fail-open, because the group still yields
|
|
140
|
+
* as many distinct identities as it has findings", and cited `ModuleEdge.ordinal` as its source.
|
|
141
|
+
* The premise is true and the conclusion does not follow — and that source, which measured the
|
|
142
|
+
* identical residual one layer down, concludes the opposite. Its table, reproduced because it
|
|
143
|
+
* applies verbatim here:
|
|
144
|
+
*
|
|
145
|
+
* | edit to an accepted colliding group | new reds reported |
|
|
146
|
+
* | ----------------------------------------- | ----------------- |
|
|
147
|
+
* | add a sibling | 1 — correct |
|
|
148
|
+
* | insert a sibling **above** the first | 1 — correct count |
|
|
149
|
+
* | **delete one, add a different one** | **0 — silent** |
|
|
150
|
+
*
|
|
151
|
+
* So the equal-count swap is a **fail-open**: a violation that did not exist when the baseline
|
|
152
|
+
* was written arrives pre-accepted. Strictly better than before this mechanism — where *any*
|
|
153
|
+
* number of added siblings was pre-accepted, not only an equal-count swap — and still a hole.
|
|
154
|
+
*
|
|
155
|
+
* The middle row is right about the number and wrong about the name: the finding reported may
|
|
156
|
+
* be a **sibling nobody touched**, while the newly-added one takes the bare subject and matches
|
|
157
|
+
* the existing entry. For ADR-008's stated primary consumer — an agent that acts on the message
|
|
158
|
+
* — pointing at the wrong file is worse than a miss.
|
|
159
|
+
*
|
|
160
|
+
* Sharper still where an entry carries state: a `measured` metric entry holds an accepted
|
|
161
|
+
* ceiling, so a swap can hand the survivor a ceiling belonging to a different element and
|
|
162
|
+
* silently accept a regression. That is bug 0028's shape recreated inside bug 0012's fix.
|
|
163
|
+
*
|
|
164
|
+
* The durable answer is a real per-finding identity — a `binding` field for the edge family,
|
|
165
|
+
* qualified names for the metric family — tracked in
|
|
166
|
+
* [ts-archunit plan 0094](https://github.com/nielspeter/ts-archunit/blob/main/plans/0094-the-residual-findings-from-the-v0-56-0-review.md). This is the
|
|
167
|
+
* floor: a collision can no longer be silent *at scale*. It is not the ceiling.
|
|
168
|
+
*
|
|
169
|
+
* **Why here and not per condition.** `applyFilters` is the one path every terminal shares, and
|
|
170
|
+
* its own contract is that identity is complete before any filter reads it. Running before the
|
|
171
|
+
* filters also makes identity a property of *what the rule found* rather than of what a
|
|
172
|
+
* `--changed` or `.excluding()` run happened to keep — the same identity in CI and locally.
|
|
173
|
+
*/
|
|
174
|
+
/**
|
|
175
|
+
* Subjects the mechanism had to disambiguate, by rule — the producer-quality signal.
|
|
176
|
+
*
|
|
177
|
+
* **Why this exists.** `disambiguateIdentities` guarantees that a rule's findings have distinct
|
|
178
|
+
* identities, which silently turned every `new Set(hashes).size === findings.length` assertion
|
|
179
|
+
* in the suite into a tautology. Measured: collapsing `duplicate-bodies.ts`'s identity to a
|
|
180
|
+
* literal constant — the worst producer defect available — left **all 234 files and 3178 tests
|
|
181
|
+
* green**, including `baseline-portability.test.ts`, whose own comment says *"collision is the
|
|
182
|
+
* failure mode this primitive introduces, so it gets its own guard."*
|
|
183
|
+
*
|
|
184
|
+
* The mechanism is a safety net, not a licence for producers to stop identifying their
|
|
185
|
+
* findings: a positional suffix makes an entry a *slot*, while a producer identity makes it a
|
|
186
|
+
* *reference*, and only the latter survives a sibling being deleted (see the ordering table
|
|
187
|
+
* above). So the guards must keep measuring the producer, and after this mechanism the only
|
|
188
|
+
* way to do that is to ask what the mechanism had to repair.
|
|
189
|
+
*
|
|
190
|
+
* Same shape as `recordEdgeCoverage`/`untestedRules` next door, for the same reason: a
|
|
191
|
+
* disclosure channel that a test can assert on without the production path knowing it exists.
|
|
192
|
+
*/
|
|
193
|
+
const collisions = [];
|
|
194
|
+
/** Every subject a rule produced more than once, since the last reset. */
|
|
195
|
+
export function identityCollisions() {
|
|
196
|
+
return collisions;
|
|
197
|
+
}
|
|
198
|
+
/** Clear the record — call in a `beforeEach` when asserting on a specific rule. */
|
|
199
|
+
export function resetIdentityCollisions() {
|
|
200
|
+
collisions.length = 0;
|
|
201
|
+
}
|
|
202
|
+
registerCacheReset(resetIdentityCollisions);
|
|
203
|
+
export function disambiguateIdentities(violations) {
|
|
204
|
+
const counts = new Map();
|
|
205
|
+
for (const violation of violations) {
|
|
206
|
+
const key = groupKeyOf(violation);
|
|
207
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
208
|
+
}
|
|
209
|
+
// Nothing collides: return the input untouched rather than rebuilding it, so the
|
|
210
|
+
// overwhelmingly common case costs one pass and no allocation.
|
|
211
|
+
let anyDuplicate = false;
|
|
212
|
+
for (const count of counts.values()) {
|
|
213
|
+
if (count > 1) {
|
|
214
|
+
anyDuplicate = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!anyDuplicate)
|
|
219
|
+
return violations;
|
|
220
|
+
// Disclose what had to be repaired, so a guard can still measure the PRODUCER — see
|
|
221
|
+
// `identityCollisions`. Recorded before the repair, because afterwards there is nothing left
|
|
222
|
+
// to see, which is precisely how this mechanism disarmed the existing collision guards.
|
|
223
|
+
for (const [key, count] of counts) {
|
|
224
|
+
if (count > 1) {
|
|
225
|
+
const split = key.indexOf('::');
|
|
226
|
+
collisions.push({
|
|
227
|
+
rule: key.slice(0, split),
|
|
228
|
+
subject: key.slice(split + 2),
|
|
229
|
+
findings: count,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Every key present before suffixing, so a generated `#n` can never land on one a producer
|
|
234
|
+
// already emits. Without it, a rule holding `X` twice plus a literal `X#1` closes one
|
|
235
|
+
// collision by opening another — and `taken.add` below is what keeps that true for the
|
|
236
|
+
// THIRD member: without it, `[X, X, X, X#1]` yields `X, X#2, X#2, X#1`, reintroducing the
|
|
237
|
+
// collision this function exists to remove. Measured; it takes a group of three to reach,
|
|
238
|
+
// which is why the guard for it carries three.
|
|
239
|
+
const taken = new Set(counts.keys());
|
|
240
|
+
const seen = new Map();
|
|
241
|
+
return violations.map((violation) => {
|
|
242
|
+
const key = groupKeyOf(violation);
|
|
243
|
+
if ((counts.get(key) ?? 0) < 2)
|
|
244
|
+
return violation;
|
|
245
|
+
const occurrence = (seen.get(key) ?? 0) + 1;
|
|
246
|
+
seen.set(key, occurrence);
|
|
247
|
+
// The first keeps its subject verbatim — this is what makes the migration empty.
|
|
248
|
+
if (occurrence === 1)
|
|
249
|
+
return violation;
|
|
250
|
+
// The suffix goes on the SUBJECT, because that is what `identity` is; the reservation is
|
|
251
|
+
// checked on the group key, because that is what collides.
|
|
252
|
+
const subject = subjectOf(violation);
|
|
253
|
+
let suffix = occurrence - 1;
|
|
254
|
+
let candidate = `${subject}#${String(suffix)}`;
|
|
255
|
+
while (taken.has(`${violation.rule}::${candidate}`)) {
|
|
256
|
+
suffix += 1;
|
|
257
|
+
candidate = `${subject}#${String(suffix)}`;
|
|
258
|
+
}
|
|
259
|
+
taken.add(`${violation.rule}::${candidate}`);
|
|
260
|
+
return { ...violation, identity: candidate };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
2
263
|
//# sourceMappingURL=violation.js.map
|
package/dist/violation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"violation.js","sourceRoot":"","sources":["../src/violation.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"violation.js","sourceRoot":"","sources":["../src/violation.ts"],"names":[],"mappings":"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"}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch-mode scheduling and the watch loop, shared by every dialect CLI.
|
|
3
|
+
*
|
|
4
|
+
* **Why this is in the kernel.** It was copied, and the copies drifted. Measured
|
|
5
|
+
* on 2026-08-31: `watchAndRerun` scored 98% between `eess-ts` and `eess-mermaid`,
|
|
6
|
+
* `RunScheduler.schedule` 100%, `RunScheduler.executeRun` 98% — and the drift was
|
|
7
|
+
* not cosmetic. Two fixes had landed on the ts copy alone:
|
|
8
|
+
*
|
|
9
|
+
* - `isArchRuleError` instead of `err instanceof ArchRuleError`. The structural
|
|
10
|
+
* check exists because a rule file does not load through the same module
|
|
11
|
+
* registry as the CLI loading it, so `instanceof` is FALSE for an error that is
|
|
12
|
+
* an `ArchRuleError` in every way that matters. With the `instanceof` form,
|
|
13
|
+
* `eess-mermaid --watch` printed an ordinary rule failure as an unexpected
|
|
14
|
+
* crash.
|
|
15
|
+
* - `isNullaryCallable(w.return)` instead of a `typeof` test plus
|
|
16
|
+
* `w.return.call(w)`, which the copy's own comment noted yields `any`.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here needs a project or a tokenizer, so this unification is not
|
|
19
|
+
* waiting on either decision plan 0188 turns on — see that plan's inventory.
|
|
20
|
+
*
|
|
21
|
+
* Kernel-safe: node builtins and this package only.
|
|
22
|
+
*/
|
|
23
|
+
export interface WatchOptions {
|
|
24
|
+
/** Directories to watch, recursively. */
|
|
25
|
+
watchDirs: string[];
|
|
26
|
+
/** Additional individual files to watch (e.g. rule files). */
|
|
27
|
+
watchFiles: string[];
|
|
28
|
+
/** Which filenames inside `watchDirs` count as a change. */
|
|
29
|
+
watchedFileRe: RegExp;
|
|
30
|
+
/** Runs on a debounced change. */
|
|
31
|
+
onChangeDetected: () => Promise<void>;
|
|
32
|
+
/** Debounce window in ms. Default: 250. */
|
|
33
|
+
debounceMs?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Scheduling logic for watch mode — separate from the loop so it is testable
|
|
37
|
+
* without a filesystem.
|
|
38
|
+
*
|
|
39
|
+
* Debounces rapid triggers. A trigger arriving mid-run is remembered and
|
|
40
|
+
* replayed once the run finishes, so a save during a run is never dropped and
|
|
41
|
+
* never starts a second concurrent run.
|
|
42
|
+
*/
|
|
43
|
+
export declare class RunScheduler {
|
|
44
|
+
#private;
|
|
45
|
+
private debounceTimer;
|
|
46
|
+
private running;
|
|
47
|
+
private _pendingRerun;
|
|
48
|
+
private readonly debounceMs;
|
|
49
|
+
private readonly onRun;
|
|
50
|
+
constructor(onRun: (trigger: string) => Promise<void>, debounceMs?: number);
|
|
51
|
+
/**
|
|
52
|
+
* Completed runs.
|
|
53
|
+
*
|
|
54
|
+
* Hard-private with a getter: the counter is this class's own bookkeeping, and
|
|
55
|
+
* a caller that could reassign it would make the number mean nothing.
|
|
56
|
+
*/
|
|
57
|
+
get runCount(): number;
|
|
58
|
+
get pendingRerun(): boolean;
|
|
59
|
+
get isRunning(): boolean;
|
|
60
|
+
/** Debounce a change on `trigger`, coalescing bursts into a single re-run. */
|
|
61
|
+
schedule(trigger: string): void;
|
|
62
|
+
private executeRun;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Watch for file changes and re-run the callback.
|
|
66
|
+
*
|
|
67
|
+
* Uses `fs.watch` with `recursive: true` (Node 24+). Debounces rapid events; a
|
|
68
|
+
* change during a run is queued and executed after it completes.
|
|
69
|
+
*/
|
|
70
|
+
export declare function watchAndRerun(options: WatchOptions): void;
|
|
71
|
+
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,4DAA4D;IAC5D,aAAa,EAAE,MAAM,CAAA;IACrB,kCAAkC;IAClC,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAOD;;;;;;;GAOG;AACH,qBAAa,YAAY;;IACvB,OAAO,CAAC,aAAa,CAA2C;IAChE,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;gBAG9C,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,SAAsB;IAKvF;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAW/B,OAAO,CAAC,UAAU;CAuBnB;AAmBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAsCzD"}
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { watch } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isArchRuleError } from './errors.js';
|
|
4
|
+
import { isNullaryCallable } from './type-guards.js';
|
|
5
|
+
const DEFAULT_DEBOUNCE_MS = 250;
|
|
6
|
+
/** `fs.watch` reports this when a watcher is closed; it is not a fault. */
|
|
7
|
+
const ABORTED = 'The operation was aborted';
|
|
8
|
+
/**
|
|
9
|
+
* Scheduling logic for watch mode — separate from the loop so it is testable
|
|
10
|
+
* without a filesystem.
|
|
11
|
+
*
|
|
12
|
+
* Debounces rapid triggers. A trigger arriving mid-run is remembered and
|
|
13
|
+
* replayed once the run finishes, so a save during a run is never dropped and
|
|
14
|
+
* never starts a second concurrent run.
|
|
15
|
+
*/
|
|
16
|
+
export class RunScheduler {
|
|
17
|
+
debounceTimer;
|
|
18
|
+
running = false;
|
|
19
|
+
_pendingRerun = false;
|
|
20
|
+
debounceMs;
|
|
21
|
+
onRun;
|
|
22
|
+
#runCount = 0;
|
|
23
|
+
constructor(onRun, debounceMs = DEFAULT_DEBOUNCE_MS) {
|
|
24
|
+
this.onRun = onRun;
|
|
25
|
+
this.debounceMs = debounceMs;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Completed runs.
|
|
29
|
+
*
|
|
30
|
+
* Hard-private with a getter: the counter is this class's own bookkeeping, and
|
|
31
|
+
* a caller that could reassign it would make the number mean nothing.
|
|
32
|
+
*/
|
|
33
|
+
get runCount() {
|
|
34
|
+
return this.#runCount;
|
|
35
|
+
}
|
|
36
|
+
get pendingRerun() {
|
|
37
|
+
return this._pendingRerun;
|
|
38
|
+
}
|
|
39
|
+
get isRunning() {
|
|
40
|
+
return this.running;
|
|
41
|
+
}
|
|
42
|
+
/** Debounce a change on `trigger`, coalescing bursts into a single re-run. */
|
|
43
|
+
schedule(trigger) {
|
|
44
|
+
if (this.debounceTimer)
|
|
45
|
+
clearTimeout(this.debounceTimer);
|
|
46
|
+
this.debounceTimer = setTimeout(() => {
|
|
47
|
+
if (this.running) {
|
|
48
|
+
this._pendingRerun = true;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.executeRun(trigger);
|
|
52
|
+
}, this.debounceMs);
|
|
53
|
+
}
|
|
54
|
+
executeRun(trigger) {
|
|
55
|
+
this.running = true;
|
|
56
|
+
this._pendingRerun = false;
|
|
57
|
+
this.#runCount++;
|
|
58
|
+
process.stdout.write('\x1B[2J\x1B[H'); // clear screen, preserve scrollback
|
|
59
|
+
process.stdout.write(`Change detected: ${trigger}\n\n`);
|
|
60
|
+
this.onRun(trigger)
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
// A rule failure is the expected outcome of a watch run — swallow it and
|
|
63
|
+
// let the reporter's own output stand. Anything else is a real fault.
|
|
64
|
+
if (!isArchRuleError(err) && err instanceof Error) {
|
|
65
|
+
console.error(err.message);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.finally(() => {
|
|
69
|
+
this.running = false;
|
|
70
|
+
if (this._pendingRerun) {
|
|
71
|
+
this.executeRun('(queued change)');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
process.stdout.write('\nWatching for changes...\n');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Consume one watcher, scheduling on each qualifying event. */
|
|
80
|
+
function consume(watcher, label, onEvent) {
|
|
81
|
+
void (async () => {
|
|
82
|
+
try {
|
|
83
|
+
for await (const event of watcher)
|
|
84
|
+
onEvent(event);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err instanceof Error && err.message !== ABORTED) {
|
|
88
|
+
console.error(`Watcher error on ${label}: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Watch for file changes and re-run the callback.
|
|
95
|
+
*
|
|
96
|
+
* Uses `fs.watch` with `recursive: true` (Node 24+). Debounces rapid events; a
|
|
97
|
+
* change during a run is queued and executed after it completes.
|
|
98
|
+
*/
|
|
99
|
+
export function watchAndRerun(options) {
|
|
100
|
+
const { watchDirs, watchFiles, watchedFileRe, onChangeDetected, debounceMs = DEFAULT_DEBOUNCE_MS, } = options;
|
|
101
|
+
const scheduler = new RunScheduler(onChangeDetected, debounceMs);
|
|
102
|
+
const watchers = [];
|
|
103
|
+
for (const dir of watchDirs) {
|
|
104
|
+
const watcher = watch(path.resolve(dir), { recursive: true });
|
|
105
|
+
watchers.push(watcher);
|
|
106
|
+
consume(watcher, dir, (event) => {
|
|
107
|
+
if (event.filename && watchedFileRe.test(event.filename)) {
|
|
108
|
+
scheduler.schedule(event.filename);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
for (const file of watchFiles) {
|
|
113
|
+
const watcher = watch(path.resolve(file));
|
|
114
|
+
watchers.push(watcher);
|
|
115
|
+
consume(watcher, file, () => {
|
|
116
|
+
scheduler.schedule(path.basename(file));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Graceful shutdown — close all watchers on SIGINT.
|
|
120
|
+
process.on('SIGINT', () => {
|
|
121
|
+
for (const w of watchers) {
|
|
122
|
+
if ('return' in w && isNullaryCallable(w.return)) {
|
|
123
|
+
void w.return();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
process.exit(0);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nielspeter/eess",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "eess kernel — the dialect-independent rule engine (RuleBuilder, Predicate, Condition, baseline, formatters). Specifications you can run.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./internal": {
|
|
12
|
+
"types": "./dist/internal.d.ts",
|
|
13
|
+
"import": "./dist/internal.js"
|
|
10
14
|
}
|
|
11
15
|
},
|
|
12
16
|
"files": [
|
|
@@ -26,6 +30,7 @@
|
|
|
26
30
|
"license": "MIT",
|
|
27
31
|
"scripts": {
|
|
28
32
|
"prepublishOnly": "npm run build",
|
|
33
|
+
"prebuild": "rm -rf dist",
|
|
29
34
|
"build": "tsc -p tsconfig.build.json",
|
|
30
35
|
"typecheck": "tsc --noEmit",
|
|
31
36
|
"test": "vitest run",
|