@nielspeter/eess 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- 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.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 +65 -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/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 +6 -0
- package/dist/condition.d.ts.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 +27 -5
- package/dist/correspondence.d.ts.map +1 -1
- package/dist/correspondence.js +39 -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 +74 -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 -0
- package/dist/diff-aware.d.ts.map +1 -1
- package/dist/diff-aware.js +11 -2
- 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/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -1
- package/dist/execute-rule.d.ts +11 -5
- package/dist/execute-rule.d.ts.map +1 -1
- package/dist/execute-rule.js +87 -19
- package/dist/execute-rule.js.map +1 -1
- 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 +83 -1
- package/dist/format-json.d.ts.map +1 -1
- package/dist/format-json.js +70 -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 +40 -8
- 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 +41 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- 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 +77 -0
- package/dist/owns-empty-discovery.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 +33 -0
- package/dist/predicate.d.ts.map +1 -1
- package/dist/preset-dispatch.d.ts +19 -0
- package/dist/preset-dispatch.d.ts.map +1 -1
- package/dist/preset-dispatch.js +43 -3
- 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 +11 -5
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +17 -0
- package/dist/report.js.map +1 -1
- package/dist/rule-builder-like.d.ts +11 -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 +109 -84
- package/dist/rule-builder.d.ts.map +1 -1
- package/dist/rule-builder.js +202 -158
- package/dist/rule-builder.js.map +1 -1
- package/dist/rule-description.d.ts +33 -1
- package/dist/rule-description.d.ts.map +1 -1
- package/dist/rule-description.js +17 -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 +47 -0
- package/dist/selection-memo.d.ts.map +1 -0
- package/dist/selection-memo.js +68 -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/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 +209 -9
- package/dist/terminal-builder.d.ts.map +1 -1
- package/dist/terminal-builder.js +264 -20
- 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 +55 -0
- package/dist/vacuity-findings.d.ts.map +1 -0
- package/dist/vacuity-findings.js +124 -0
- package/dist/vacuity-findings.js.map +1 -0
- package/dist/violation.d.ts +152 -4
- package/dist/violation.d.ts.map +1 -1
- package/dist/violation.js +262 -1
- package/dist/violation.js.map +1 -1
- package/package.json +1 -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
|
|
|
@@ -16,8 +16,8 @@ This package is the engine every eess dialect runs on. It knows nothing about Ty
|
|
|
16
16
|
|
|
17
17
|
You normally don't install this directly — you install a dialect:
|
|
18
18
|
|
|
19
|
-
- [`@nielspeter/eess-ts`](
|
|
20
|
-
- [`@nielspeter/eess-mermaid`](
|
|
19
|
+
- [`@nielspeter/eess-ts`](https://www.npmjs.com/package/@nielspeter/eess-ts) — TypeScript
|
|
20
|
+
- [`@nielspeter/eess-mermaid`](https://www.npmjs.com/package/@nielspeter/eess-mermaid) — Mermaid class diagrams
|
|
21
21
|
|
|
22
22
|
Install `@nielspeter/eess` directly only when authoring a new dialect or a cross-dialect tool on top of the kernel.
|
|
23
23
|
|
|
@@ -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"}
|
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/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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-registry.js","sourceRoot":"","sources":["../src/cache-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,GAAmB,EAAE,CAAA;AAEjC,4DAA4D;AAC5D,MAAM,UAAU,kBAAkB,CAAC,KAAiB;IAClD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACpB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB;IACnC,KAAK,MAAM,KAAK,IAAI,MAAM;QAAE,KAAK,EAAE,CAAA;AACrC,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declare that this condition is satisfied by an empty selection.
|
|
3
|
+
*
|
|
4
|
+
* **A registry, because a symbol was forgeable.** The first version keyed a
|
|
5
|
+
* module-private `unique symbol` onto the condition, reasoning that a consumer
|
|
6
|
+
* could not import it to name the key. They do not need to import it — four
|
|
7
|
+
* shipped conditions carry it as an own property, and `notExist` is publicly
|
|
8
|
+
* exported, so:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const stolen = Object.getOwnPropertySymbols(notExist())[0]
|
|
12
|
+
* const mine = { description: 'x', evaluate: () => [], [stolen]: true }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Measured: an honest condition on an empty selection produces **1** configuration
|
|
16
|
+
* finding and that forgery produces **0**. One line, through documented exports,
|
|
17
|
+
* to exempt any rule from the empty-selection gate — the gate this library is
|
|
18
|
+
* named around, and precisely the `.allowEmpty()` hazard the symbol was chosen to
|
|
19
|
+
* prevent.
|
|
20
|
+
*
|
|
21
|
+
* Found by review, immediately after the identical hole was closed in
|
|
22
|
+
* [ts-archunit plan 0081](https://github.com/nielspeter/ts-archunit/blob/main/plans/completed/0081-a-condition-declares-discovery-ownership.md)'s
|
|
23
|
+
* symbol — while this one was being cited in that module's docstring as the safe
|
|
24
|
+
* precedent. The lesson is worth more than the fix: **"module-private" describes
|
|
25
|
+
* the binding, not the value.** A symbol keyed onto a public object is unlisted,
|
|
26
|
+
* not unreachable.
|
|
27
|
+
*
|
|
28
|
+
* `WeakSet` membership cannot be read off an object, copied, or forged: a caller
|
|
29
|
+
* would need this module's binding, and it is not exported. Weak so a condition is
|
|
30
|
+
* not retained after its rule is discarded.
|
|
31
|
+
*/
|
|
32
|
+
export declare function marksAssertsCardinality<T extends object>(condition: T): T;
|
|
33
|
+
/** Is this condition satisfied by an empty selection? */
|
|
34
|
+
export declare function assertsCardinality(condition: object): boolean;
|
|
35
|
+
//# sourceMappingURL=cardinality.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cardinality.d.ts","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAGzE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE7D"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cardinality exemption, keyed so that only this library can set it.
|
|
3
|
+
*
|
|
4
|
+
* Plan 0074 (R3b), and specifically constraint 2 of 0069's appendix:
|
|
5
|
+
*
|
|
6
|
+
* > `Condition` is a public export and `defineCondition` is its sanctioned
|
|
7
|
+
* > constructor. If the flag is expressible there, it is a one-line silent
|
|
8
|
+
* > opt-out on any user condition — the hazard the Decisions section used to
|
|
9
|
+
* > reject `.allowEmpty()`, relocated onto the condition object. A
|
|
10
|
+
* > module-private `unique symbol`, with no `defineCondition` parameter, closes
|
|
11
|
+
* > it by construction (ADR-009 rule 3's corollary).
|
|
12
|
+
*
|
|
13
|
+
* A first cut of this shipped as a plain `assertsCardinality?: true` property,
|
|
14
|
+
* which is exactly the rejected shape: `Condition` is exported from
|
|
15
|
+
* `src/index.ts`, so any user object literal could set it and silence the
|
|
16
|
+
* empty-selection gate on any rule, permanently and invisibly. That is
|
|
17
|
+
* `.allowEmpty()` wearing a different hat.
|
|
18
|
+
*
|
|
19
|
+
* This symbol is **not** re-exported from `src/index.ts`, and `package.json`'s
|
|
20
|
+
* `exports` map has no wildcard subpath, so a consumer cannot import it to
|
|
21
|
+
* name the key. The escape hatch users *do* get is `.expectEmpty()`, which is
|
|
22
|
+
* an assertion that fails when the selection stops being empty — not a
|
|
23
|
+
* silencer.
|
|
24
|
+
*/
|
|
25
|
+
const CARDINALITY_ASSERTERS = new WeakSet();
|
|
26
|
+
/**
|
|
27
|
+
* Declare that this condition is satisfied by an empty selection.
|
|
28
|
+
*
|
|
29
|
+
* **A registry, because a symbol was forgeable.** The first version keyed a
|
|
30
|
+
* module-private `unique symbol` onto the condition, reasoning that a consumer
|
|
31
|
+
* could not import it to name the key. They do not need to import it — four
|
|
32
|
+
* shipped conditions carry it as an own property, and `notExist` is publicly
|
|
33
|
+
* exported, so:
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const stolen = Object.getOwnPropertySymbols(notExist())[0]
|
|
37
|
+
* const mine = { description: 'x', evaluate: () => [], [stolen]: true }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* Measured: an honest condition on an empty selection produces **1** configuration
|
|
41
|
+
* finding and that forgery produces **0**. One line, through documented exports,
|
|
42
|
+
* to exempt any rule from the empty-selection gate — the gate this library is
|
|
43
|
+
* named around, and precisely the `.allowEmpty()` hazard the symbol was chosen to
|
|
44
|
+
* prevent.
|
|
45
|
+
*
|
|
46
|
+
* Found by review, immediately after the identical hole was closed in
|
|
47
|
+
* [ts-archunit plan 0081](https://github.com/nielspeter/ts-archunit/blob/main/plans/completed/0081-a-condition-declares-discovery-ownership.md)'s
|
|
48
|
+
* symbol — while this one was being cited in that module's docstring as the safe
|
|
49
|
+
* precedent. The lesson is worth more than the fix: **"module-private" describes
|
|
50
|
+
* the binding, not the value.** A symbol keyed onto a public object is unlisted,
|
|
51
|
+
* not unreachable.
|
|
52
|
+
*
|
|
53
|
+
* `WeakSet` membership cannot be read off an object, copied, or forged: a caller
|
|
54
|
+
* would need this module's binding, and it is not exported. Weak so a condition is
|
|
55
|
+
* not retained after its rule is discarded.
|
|
56
|
+
*/
|
|
57
|
+
export function marksAssertsCardinality(condition) {
|
|
58
|
+
CARDINALITY_ASSERTERS.add(condition);
|
|
59
|
+
return condition;
|
|
60
|
+
}
|
|
61
|
+
/** Is this condition satisfied by an empty selection? */
|
|
62
|
+
export function assertsCardinality(condition) {
|
|
63
|
+
return CARDINALITY_ASSERTERS.has(condition);
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=cardinality.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cardinality.js","sourceRoot":"","sources":["../src/cardinality.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,qBAAqB,GAAG,IAAI,OAAO,EAAU,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,uBAAuB,CAAmB,SAAY;IACpE,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACpC,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,OAAO,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AAC7C,CAAC"}
|
package/dist/check-options.d.ts
CHANGED
|
@@ -22,6 +22,20 @@ export interface BaselineFilter {
|
|
|
22
22
|
export interface DiffFilterLike {
|
|
23
23
|
/** Filter violations to only those in changed files. */
|
|
24
24
|
filterToChanged(violations: ArchViolation[]): ArchViolation[];
|
|
25
|
+
/**
|
|
26
|
+
* How many files this filter considers changed, or a negative number when it
|
|
27
|
+
* could not tell (`DiffFilter` returns -1 when git was unavailable).
|
|
28
|
+
*
|
|
29
|
+
* **Optional, and the disclosure degrades rather than guesses.** This is a
|
|
30
|
+
* structural interface, so a consumer may pass any object with
|
|
31
|
+
* `filterToChanged`; when `size` is absent the suppression notice omits the
|
|
32
|
+
* file count instead of inventing one. What it never omits is the suppressed
|
|
33
|
+
* count itself, because the caller derives that by subtraction rather than
|
|
34
|
+
* asking the filter (see `core/diff-disclosure.ts`).
|
|
35
|
+
*/
|
|
36
|
+
readonly size?: number;
|
|
37
|
+
/** The branch diffed against, when the filter knows it. Optional, as `size`. */
|
|
38
|
+
readonly baseBranch?: string;
|
|
25
39
|
}
|
|
26
40
|
/**
|
|
27
41
|
* Options passed to .check() and .warn() to filter violations.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-options.d.ts","sourceRoot":"","sources":["../src/check-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEzD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;CACxD;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"check-options.d.ts","sourceRoot":"","sources":["../src/check-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEzD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;CACxD;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;IAC7D;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,iDAAiD;IACjD,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,wDAAwD;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB"}
|