@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/dist/define.js
CHANGED
|
@@ -12,9 +12,41 @@
|
|
|
12
12
|
*
|
|
13
13
|
* classes(p).that().satisfy(isAbstract).should().beExported().check()
|
|
14
14
|
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## Declaring a glob
|
|
17
|
+
*
|
|
18
|
+
* If the predicate matches paths against a glob, pass it as `globs` — otherwise
|
|
19
|
+
* `doctor` cannot see it, and a typo'd glob narrows the selection to nothing
|
|
20
|
+
* while reporting a clean bill of health. This is a **selector**-position glob,
|
|
21
|
+
* the one position that is acted on today, which is why the omission was bug
|
|
22
|
+
* 0030's higher-severity half.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const inGeneratedFolder = definePredicate<SourceFile>(
|
|
27
|
+
* "reside in '**\/generated/**'",
|
|
28
|
+
* (file) => picomatch('**\/generated/**')(file.getFilePath()),
|
|
29
|
+
* globNode({ glob: '**\/generated/**', kind: 'file-path' }),
|
|
30
|
+
* )
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The `kind` must say what the glob is really matched against, because it
|
|
34
|
+
* selects which paths the glob is checked for satisfiability:
|
|
35
|
+
*
|
|
36
|
+
* - `file-path` — an **absolute** file path. Anchor the glob (`'**\/src/**'`,
|
|
37
|
+
* not `'src/**'`), or it matches nothing.
|
|
38
|
+
* - `parent-dir` — the immediate parent directory of a file.
|
|
39
|
+
* - `import-target` — a resolved module path **or a bare specifier**.
|
|
40
|
+
* Deliberately has no path universe, so `'fastify'` is never reported dead.
|
|
41
|
+
* - `specifier` / `literal` — a string in the source rather than a path.
|
|
42
|
+
*
|
|
43
|
+
* A wrong `kind` is believed: declaring a bare specifier as `file-path` earns a
|
|
44
|
+
* false dead-glob report, and declaring a real path as `import-target` silently
|
|
45
|
+
* exempts it from checking. When unsure, the honest choice is to declare
|
|
46
|
+
* nothing — the same position as before this parameter existed.
|
|
15
47
|
*/
|
|
16
|
-
export function definePredicate(description, test) {
|
|
17
|
-
return { description, test };
|
|
48
|
+
export function definePredicate(description, test, globs) {
|
|
49
|
+
return { description, test, globs };
|
|
18
50
|
}
|
|
19
51
|
/**
|
|
20
52
|
* Create a custom condition for use in `.should().satisfy()` chains.
|
|
@@ -35,8 +67,17 @@ export function definePredicate(description, test) {
|
|
|
35
67
|
*
|
|
36
68
|
* classes(p).that().extend('Base').should().satisfy(useSharedHelper).check()
|
|
37
69
|
* ```
|
|
70
|
+
*
|
|
71
|
+
* ## Declaring a glob
|
|
72
|
+
*
|
|
73
|
+
* As with `definePredicate`, pass `globs` if the condition matches paths against
|
|
74
|
+
* one — see that function for the `kind` values and what a wrong one costs. The
|
|
75
|
+
* difference is what happens next: a **condition**-position glob is deliberately
|
|
76
|
+
* *not* reported when it matches nothing, because a denylist glob matching
|
|
77
|
+
* nothing is indistinguishable from a ban being respected. Declaring it makes it
|
|
78
|
+
* visible to `globs()` and `explain`; it does not make it a finding.
|
|
38
79
|
*/
|
|
39
|
-
export function defineCondition(description, evaluate) {
|
|
40
|
-
return { description, evaluate };
|
|
80
|
+
export function defineCondition(description, evaluate, globs) {
|
|
81
|
+
return { description, evaluate, globs };
|
|
41
82
|
}
|
|
42
83
|
//# sourceMappingURL=define.js.map
|
package/dist/define.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAmB,EACnB,IAA6B,EAC7B,KAAqB;IAErB,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAmB,EACnB,QAAuE,EACvE,KAAqB;IAErB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;AACzC,CAAC"}
|
package/dist/diff-aware.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export declare class DiffFilter {
|
|
|
14
14
|
/**
|
|
15
15
|
* Filter violations to only those in changed files.
|
|
16
16
|
* If changedFiles is null (git error), returns all violations unfiltered.
|
|
17
|
+
*
|
|
18
|
+
* A `bypassFilters` configuration finding (ADR-010's zero-examined /
|
|
19
|
+
* expired-`.expectEmpty()` violations) is never filtered here, even
|
|
20
|
+
* though its `file` is always `''` and can never be a member of
|
|
21
|
+
* `changedFiles`. Without this, "unsuppressable" was false for the most
|
|
22
|
+
* realistic adoption path: a rule whose instrument silently broke would
|
|
23
|
+
* report nothing at all under `--changed`, with no diagnostic that
|
|
24
|
+
* anything was ever hidden.
|
|
17
25
|
*/
|
|
18
26
|
filterToChanged(violations: ArchViolation[]): ArchViolation[];
|
|
19
27
|
/** Number of changed files detected, or -1 if diff unavailable */
|
package/dist/diff-aware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff-aware.d.ts","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"diff-aware.d.ts","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnD;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;gBAErC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI;IAI5C;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE;IAM7D,kEAAkE;IAClE,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,UAAU,GAAE,MAAe,GAAG,UAAU,CA6BjE"}
|
package/dist/diff-aware.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { writeStderr } from './stderr.js';
|
|
3
4
|
/**
|
|
4
5
|
* A diff filter that restricts violation reporting to files
|
|
5
6
|
* changed since a base branch.
|
|
@@ -17,12 +18,20 @@ export class DiffFilter {
|
|
|
17
18
|
/**
|
|
18
19
|
* Filter violations to only those in changed files.
|
|
19
20
|
* If changedFiles is null (git error), returns all violations unfiltered.
|
|
21
|
+
*
|
|
22
|
+
* A `bypassFilters` configuration finding (ADR-010's zero-examined /
|
|
23
|
+
* expired-`.expectEmpty()` violations) is never filtered here, even
|
|
24
|
+
* though its `file` is always `''` and can never be a member of
|
|
25
|
+
* `changedFiles`. Without this, "unsuppressable" was false for the most
|
|
26
|
+
* realistic adoption path: a rule whose instrument silently broke would
|
|
27
|
+
* report nothing at all under `--changed`, with no diagnostic that
|
|
28
|
+
* anything was ever hidden.
|
|
20
29
|
*/
|
|
21
30
|
filterToChanged(violations) {
|
|
22
31
|
const files = this.changedFiles;
|
|
23
32
|
if (files === null)
|
|
24
33
|
return violations;
|
|
25
|
-
return violations.filter((v) => files.has(v.file));
|
|
34
|
+
return violations.filter((v) => v.bypassFilters === true || files.has(v.file));
|
|
26
35
|
}
|
|
27
36
|
/** Number of changed files detected, or -1 if diff unavailable */
|
|
28
37
|
get size() {
|
|
@@ -56,7 +65,7 @@ export function diffAware(baseBranch = 'main') {
|
|
|
56
65
|
catch (err) {
|
|
57
66
|
// Not a git repo, or base branch doesn't exist — skip filtering (report all violations)
|
|
58
67
|
const detail = err instanceof Error ? ` (${err.message.split('\n')[0] ?? ''})` : '';
|
|
59
|
-
|
|
68
|
+
writeStderr(`[eess] Could not run git diff against '${baseBranch}'${detail}. All violations will be reported.`);
|
|
60
69
|
return new DiffFilter(null);
|
|
61
70
|
}
|
|
62
71
|
if (output === '') {
|
package/dist/diff-aware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff-aware.js","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,IAAI,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"diff-aware.js","sourceRoot":"","sources":["../src/diff-aware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC;;;;;;;;GAQG;AACH,MAAM,OAAO,UAAU;IACJ,YAAY,CAAoB;IAEjD,YAAY,YAAgC;QAC1C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;IAClC,CAAC;IAED;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,UAA2B;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAA;QAC/B,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,UAAU,CAAA;QACrC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAChF,CAAC;IAED,kEAAkE;IAClE,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAA;IACjE,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAC,aAAqB,MAAM;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAEzB,IAAI,MAAc,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,SAAS,CAAC,EAAE;YAC5E,QAAQ,EAAE,OAAO;YACjB,GAAG;YACH,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC,IAAI,EAAE,CAAA;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wFAAwF;QACxF,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;QACnF,WAAW,CACT,0CAA0C,UAAU,IAAI,MAAM,oCAAoC,CACnG,CAAA;QACD,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAClB,wFAAwF;QACxF,OAAO,IAAI,UAAU,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;IAClC,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAC1B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAC1E,CAAA;IAED,OAAO,IAAI,UAAU,CAAC,YAAY,CAAC,CAAA;AACrC,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What diff-aware mode suppressed, said out loud.
|
|
3
|
+
*
|
|
4
|
+
* Plan 0071's first instrument. `--changed` (and `check({ diff })`) filter
|
|
5
|
+
* **reporting**, not evaluation — so a run with every finding suppressed is
|
|
6
|
+
* indistinguishable from a clean run: exit 0, no output, `total: 0`. That is
|
|
7
|
+
* the false green ADR-008 exists to prevent, and it is worse here than
|
|
8
|
+
* elsewhere because the reader chose the flag and then forgot it: CI configures
|
|
9
|
+
* `--changed` once and every subsequent run reads as clean.
|
|
10
|
+
*
|
|
11
|
+
* **The count is derived by the caller, never self-reported.** Each call site
|
|
12
|
+
* computes `before.length - after.length` around its own `filterToChanged`
|
|
13
|
+
* call. `DiffFilter` could keep that tally itself, and it deliberately does
|
|
14
|
+
* not: `CheckOptions.diff` is a structural interface (`DiffFilterLike`), so a
|
|
15
|
+
* consumer can pass any object with a `filterToChanged` method, and a filter
|
|
16
|
+
* that under-reports its own suppression would produce exactly the silence
|
|
17
|
+
* this module exists to break. Caller-side subtraction is a differently-derived
|
|
18
|
+
* value (ADR-009 rule 5) and it works for filters we did not write.
|
|
19
|
+
*
|
|
20
|
+
* **Two surfaces, because only one of them can count.**
|
|
21
|
+
*
|
|
22
|
+
* | surface | disclosure |
|
|
23
|
+
* | ---------------------------------------- | -------------------- |
|
|
24
|
+
* | CLI `check --changed` | `suppressionNotice` |
|
|
25
|
+
* | `checkAll([...])` | `suppressionNotice` |
|
|
26
|
+
* | `.check({ diff })` / `.warn({ diff })` | `activeNotice`, once |
|
|
27
|
+
*
|
|
28
|
+
* The first two filter once over every collected violation, so they know the
|
|
29
|
+
* whole number. The per-rule terminals do not: `filterToChanged` is called
|
|
30
|
+
* once per rule (`execute-rule.ts`), and a diff-aware suite with 79 rules
|
|
31
|
+
* would print 79 lines — on the channel v0.26.0 made unconditionally visible.
|
|
32
|
+
* There is no per-run aggregation point on that path, so those terminals state
|
|
33
|
+
* the **configuration** instead, once per process. A configuration statement
|
|
34
|
+
* cannot be wrong; a per-rule count presented as a run total would be.
|
|
35
|
+
*
|
|
36
|
+
* A `process.on('exit')` flush was considered and rejected: it would let the
|
|
37
|
+
* per-rule path print one complete total, but a library that installs an exit
|
|
38
|
+
* hook to deliver a correctness-relevant message has made that message
|
|
39
|
+
* dependent on stderr surviving process teardown — untestable in the shape the
|
|
40
|
+
* rest of this project asserts (a real child `vitest run`, bug 0024) without
|
|
41
|
+
* asserting a race. Stating the configuration is weaker and honest.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* One line naming what was suppressed, or `undefined` when nothing was.
|
|
45
|
+
*
|
|
46
|
+
* `undefined` rather than an empty string so a call site cannot print a blank
|
|
47
|
+
* line by forgetting to check — the two callers both feed this straight into
|
|
48
|
+
* `writeStderr`.
|
|
49
|
+
*
|
|
50
|
+
* @param suppressed - `before.length - after.length`, computed by the caller.
|
|
51
|
+
* @param changedFiles - How many files the filter considered changed, when the
|
|
52
|
+
* filter exposes it. A user-supplied `DiffFilterLike` need not, in which case
|
|
53
|
+
* the sentence drops that clause rather than guessing.
|
|
54
|
+
* @param baseBranch - The branch diffed against, when known.
|
|
55
|
+
*/
|
|
56
|
+
export declare function suppressionNotice(suppressed: number, changedFiles?: number, baseBranch?: string): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* State that diff-aware filtering is on, once per process. Returns `undefined`
|
|
59
|
+
* on every call after the first, and whenever nothing was suppressed.
|
|
60
|
+
*
|
|
61
|
+
* Keyed on a call that actually suppressed something: a diff-aware run whose
|
|
62
|
+
* findings all happen to be in changed files has hidden nothing, and a notice
|
|
63
|
+
* there would train the reader to skip the line that matters.
|
|
64
|
+
*/
|
|
65
|
+
export declare function activeNotice(suppressed: number, changedFiles?: number, baseBranch?: string): string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Forget that {@link activeNotice} has spoken. **Tests only** — a suite
|
|
68
|
+
* asserting the once-per-process contract has to observe the first call, and
|
|
69
|
+
* whichever earlier test filtered first would otherwise have consumed it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resetDiffDisclosureForTests(): void;
|
|
72
|
+
//# sourceMappingURL=diff-disclosure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-disclosure.d.ts","sourceRoot":"","sources":["../src/diff-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAKH;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CAkBpB;AAYD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CAepB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAElD"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What diff-aware mode suppressed, said out loud.
|
|
3
|
+
*
|
|
4
|
+
* Plan 0071's first instrument. `--changed` (and `check({ diff })`) filter
|
|
5
|
+
* **reporting**, not evaluation — so a run with every finding suppressed is
|
|
6
|
+
* indistinguishable from a clean run: exit 0, no output, `total: 0`. That is
|
|
7
|
+
* the false green ADR-008 exists to prevent, and it is worse here than
|
|
8
|
+
* elsewhere because the reader chose the flag and then forgot it: CI configures
|
|
9
|
+
* `--changed` once and every subsequent run reads as clean.
|
|
10
|
+
*
|
|
11
|
+
* **The count is derived by the caller, never self-reported.** Each call site
|
|
12
|
+
* computes `before.length - after.length` around its own `filterToChanged`
|
|
13
|
+
* call. `DiffFilter` could keep that tally itself, and it deliberately does
|
|
14
|
+
* not: `CheckOptions.diff` is a structural interface (`DiffFilterLike`), so a
|
|
15
|
+
* consumer can pass any object with a `filterToChanged` method, and a filter
|
|
16
|
+
* that under-reports its own suppression would produce exactly the silence
|
|
17
|
+
* this module exists to break. Caller-side subtraction is a differently-derived
|
|
18
|
+
* value (ADR-009 rule 5) and it works for filters we did not write.
|
|
19
|
+
*
|
|
20
|
+
* **Two surfaces, because only one of them can count.**
|
|
21
|
+
*
|
|
22
|
+
* | surface | disclosure |
|
|
23
|
+
* | ---------------------------------------- | -------------------- |
|
|
24
|
+
* | CLI `check --changed` | `suppressionNotice` |
|
|
25
|
+
* | `checkAll([...])` | `suppressionNotice` |
|
|
26
|
+
* | `.check({ diff })` / `.warn({ diff })` | `activeNotice`, once |
|
|
27
|
+
*
|
|
28
|
+
* The first two filter once over every collected violation, so they know the
|
|
29
|
+
* whole number. The per-rule terminals do not: `filterToChanged` is called
|
|
30
|
+
* once per rule (`execute-rule.ts`), and a diff-aware suite with 79 rules
|
|
31
|
+
* would print 79 lines — on the channel v0.26.0 made unconditionally visible.
|
|
32
|
+
* There is no per-run aggregation point on that path, so those terminals state
|
|
33
|
+
* the **configuration** instead, once per process. A configuration statement
|
|
34
|
+
* cannot be wrong; a per-rule count presented as a run total would be.
|
|
35
|
+
*
|
|
36
|
+
* A `process.on('exit')` flush was considered and rejected: it would let the
|
|
37
|
+
* per-rule path print one complete total, but a library that installs an exit
|
|
38
|
+
* hook to deliver a correctness-relevant message has made that message
|
|
39
|
+
* dependent on stderr surviving process teardown — untestable in the shape the
|
|
40
|
+
* rest of this project asserts (a real child `vitest run`, bug 0024) without
|
|
41
|
+
* asserting a race. Stating the configuration is weaker and honest.
|
|
42
|
+
*/
|
|
43
|
+
/** Prefix shared with the rest of the library's stderr output. */
|
|
44
|
+
const TAG = '[eess]';
|
|
45
|
+
/**
|
|
46
|
+
* One line naming what was suppressed, or `undefined` when nothing was.
|
|
47
|
+
*
|
|
48
|
+
* `undefined` rather than an empty string so a call site cannot print a blank
|
|
49
|
+
* line by forgetting to check — the two callers both feed this straight into
|
|
50
|
+
* `writeStderr`.
|
|
51
|
+
*
|
|
52
|
+
* @param suppressed - `before.length - after.length`, computed by the caller.
|
|
53
|
+
* @param changedFiles - How many files the filter considered changed, when the
|
|
54
|
+
* filter exposes it. A user-supplied `DiffFilterLike` need not, in which case
|
|
55
|
+
* the sentence drops that clause rather than guessing.
|
|
56
|
+
* @param baseBranch - The branch diffed against, when known.
|
|
57
|
+
*/
|
|
58
|
+
export function suppressionNotice(suppressed, changedFiles, baseBranch) {
|
|
59
|
+
if (suppressed <= 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
const findings = suppressed === 1 ? 'finding' : 'findings';
|
|
62
|
+
// A negative size is `DiffFilter`'s "git was unavailable" sentinel. That path
|
|
63
|
+
// filters nothing, so `suppressed` is 0 and we never reach here — but the
|
|
64
|
+
// guard is cheap and keeps the sentinel from ever reaching a user's screen.
|
|
65
|
+
const scope = changedFiles !== undefined && changedFiles >= 0
|
|
66
|
+
? ` outside the ${String(changedFiles)} changed ${changedFiles === 1 ? 'file' : 'files'}`
|
|
67
|
+
: ' outside the changed files';
|
|
68
|
+
const since = baseBranch === undefined ? '' : ` (diffed against '${baseBranch}')`;
|
|
69
|
+
return (`${TAG} Diff-aware mode suppressed ${String(suppressed)} ${findings}${scope}${since}. ` +
|
|
70
|
+
`Those findings are real and still present — this run did not check them. ` +
|
|
71
|
+
`Re-run without diff-aware mode to see every finding.`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether {@link activeNotice} has already spoken in this process.
|
|
75
|
+
*
|
|
76
|
+
* Module-scoped rather than per-`DiffFilter`, because the documented pattern
|
|
77
|
+
* constructs a filter **inline per rule** — `.check({ diff: diffAware('main') })`
|
|
78
|
+
* in `docs/core-concepts.md:333` and `docs/what-to-check.md:515` — so
|
|
79
|
+
* per-instance state would still produce one line per rule.
|
|
80
|
+
*/
|
|
81
|
+
let noticed = false;
|
|
82
|
+
/**
|
|
83
|
+
* State that diff-aware filtering is on, once per process. Returns `undefined`
|
|
84
|
+
* on every call after the first, and whenever nothing was suppressed.
|
|
85
|
+
*
|
|
86
|
+
* Keyed on a call that actually suppressed something: a diff-aware run whose
|
|
87
|
+
* findings all happen to be in changed files has hidden nothing, and a notice
|
|
88
|
+
* there would train the reader to skip the line that matters.
|
|
89
|
+
*/
|
|
90
|
+
export function activeNotice(suppressed, changedFiles, baseBranch) {
|
|
91
|
+
if (suppressed <= 0 || noticed)
|
|
92
|
+
return undefined;
|
|
93
|
+
noticed = true;
|
|
94
|
+
const scope = changedFiles !== undefined && changedFiles >= 0
|
|
95
|
+
? `the ${String(changedFiles)} ${changedFiles === 1 ? 'file' : 'files'} changed`
|
|
96
|
+
: 'the files changed';
|
|
97
|
+
const since = baseBranch === undefined ? '' : ` since '${baseBranch}'`;
|
|
98
|
+
return (`${TAG} Diff-aware mode is active: this run reports only findings in ${scope}${since}. ` +
|
|
99
|
+
`Findings in unchanged files are suppressed and will not appear, however many there are. ` +
|
|
100
|
+
`Remove \`diff\` from the check options to see every finding.`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Forget that {@link activeNotice} has spoken. **Tests only** — a suite
|
|
104
|
+
* asserting the once-per-process contract has to observe the first call, and
|
|
105
|
+
* whichever earlier test filtered first would otherwise have consumed it.
|
|
106
|
+
*/
|
|
107
|
+
export function resetDiffDisclosureForTests() {
|
|
108
|
+
noticed = false;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=diff-disclosure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-disclosure.js","sourceRoot":"","sources":["../src/diff-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,kEAAkE;AAClE,MAAM,GAAG,GAAG,QAAQ,CAAA;AAEpB;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAkB,EAClB,YAAqB,EACrB,UAAmB;IAEnB,IAAI,UAAU,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IAErC,MAAM,QAAQ,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAA;IAC1D,8EAA8E;IAC9E,0EAA0E;IAC1E,4EAA4E;IAC5E,MAAM,KAAK,GACT,YAAY,KAAK,SAAS,IAAI,YAAY,IAAI,CAAC;QAC7C,CAAC,CAAC,gBAAgB,MAAM,CAAC,YAAY,CAAC,YAAY,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE;QACzF,CAAC,CAAC,4BAA4B,CAAA;IAClC,MAAM,KAAK,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,qBAAqB,UAAU,IAAI,CAAA;IAEjF,OAAO,CACL,GAAG,GAAG,+BAA+B,MAAM,CAAC,UAAU,CAAC,IAAI,QAAQ,GAAG,KAAK,GAAG,KAAK,IAAI;QACvF,2EAA2E;QAC3E,sDAAsD,CACvD,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,IAAI,OAAO,GAAG,KAAK,CAAA;AAEnB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,UAAkB,EAClB,YAAqB,EACrB,UAAmB;IAEnB,IAAI,UAAU,IAAI,CAAC,IAAI,OAAO;QAAE,OAAO,SAAS,CAAA;IAChD,OAAO,GAAG,IAAI,CAAA;IAEd,MAAM,KAAK,GACT,YAAY,KAAK,SAAS,IAAI,YAAY,IAAI,CAAC;QAC7C,CAAC,CAAC,OAAO,MAAM,CAAC,YAAY,CAAC,IAAI,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,UAAU;QAChF,CAAC,CAAC,mBAAmB,CAAA;IACzB,MAAM,KAAK,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,UAAU,GAAG,CAAA;IAEtE,OAAO,CACL,GAAG,GAAG,iEAAiE,KAAK,GAAG,KAAK,IAAI;QACxF,0FAA0F;QAC1F,8DAA8D,CAC/D,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,GAAG,KAAK,CAAA;AACjB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the filesystem says about a glob that the compiler's file set does
|
|
3
|
+
* not — the shape of the data, not how to build it.
|
|
4
|
+
*
|
|
5
|
+
* `OnDisk`/`DiskSet` are pure: a materialized `DiskSet` is just a closure
|
|
6
|
+
* over already-walked filesystem data, so the type stays in the kernel.
|
|
7
|
+
* Walking a real project's filesystem needs Node's `fs`/`path` and the
|
|
8
|
+
* dialect's own project type to find the root, so the MATERIALIZER lives in
|
|
9
|
+
* that dialect (e.g. `packages/ts/src/core/disk-set.ts`'s `diskSet()`).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* What the filesystem knows that the compiler's file set does not.
|
|
13
|
+
*
|
|
14
|
+
* Distinguishes "your glob is misspelled" from "your glob is fine and your
|
|
15
|
+
* tsconfig scope excludes it" — the cheapest wrong action an agent can take
|
|
16
|
+
* on a dead-glob finding, and the common case in a real monorepo where a
|
|
17
|
+
* build-output or docs directory sits outside the loaded project.
|
|
18
|
+
*/
|
|
19
|
+
export type OnDisk = 'holds-typescript' | 'no-typescript' | 'absent' | 'not-determined';
|
|
20
|
+
/**
|
|
21
|
+
* What the filesystem knows that the compiler's file set does not.
|
|
22
|
+
*/
|
|
23
|
+
export interface DiskSet {
|
|
24
|
+
/** Classify a glob by what exists on disk under the paths it matches. */
|
|
25
|
+
classify(glob: string): OnDisk;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=disk-set.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disk-set.d.ts","sourceRoot":"","sources":["../src/disk-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,kBAAkB,GAAG,eAAe,GAAG,QAAQ,GAAG,gBAAgB,CAAA;AAEvF;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAC/B"}
|
package/dist/disk-set.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the filesystem says about a glob that the compiler's file set does
|
|
3
|
+
* not — the shape of the data, not how to build it.
|
|
4
|
+
*
|
|
5
|
+
* `OnDisk`/`DiskSet` are pure: a materialized `DiskSet` is just a closure
|
|
6
|
+
* over already-walked filesystem data, so the type stays in the kernel.
|
|
7
|
+
* Walking a real project's filesystem needs Node's `fs`/`path` and the
|
|
8
|
+
* dialect's own project type to find the root, so the MATERIALIZER lives in
|
|
9
|
+
* that dialect (e.g. `packages/ts/src/core/disk-set.ts`'s `diskSet()`).
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=disk-set.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disk-set.js","sourceRoot":"","sources":["../src/disk-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much evidence an allowlist condition actually had — bug 0015.
|
|
3
|
+
*
|
|
4
|
+
* The `only*` family constrains **edges**, not subjects. Each iterates a
|
|
5
|
+
* subject's imports (or importers) and reports one violation per edge outside
|
|
6
|
+
* the allowlist, so a subject with **zero edges has nothing to violate and
|
|
7
|
+
* passes**, however broken the allowlist. Measured: one file with no imports,
|
|
8
|
+
* `onlyImportFrom('**\/nowhere/**')` → 1 subject selected, 0 violations.
|
|
9
|
+
*
|
|
10
|
+
* The shape it fails on is the target case, not an edge case: in a layered
|
|
11
|
+
* architecture the innermost layer is the one an allowlist protects and is
|
|
12
|
+
* characteristically the layer with the fewest outbound imports.
|
|
13
|
+
*
|
|
14
|
+
* ## Why this reports rather than fails
|
|
15
|
+
*
|
|
16
|
+
* [ts-archunit Bug 0015](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0015-allowlist-conditions-pass-vacuously-on-edgeless-subjects.md)
|
|
17
|
+
* refuted failing, per-subject and per-rule, on measurement:
|
|
18
|
+
*
|
|
19
|
+
* - **Per-subject has no statable remedy.** 14 of this repo's `src/` files have
|
|
20
|
+
* zero static imports and 10 are pure leaf modules. `tarjan.ts` is a
|
|
21
|
+
* dependency-free algorithm — the ideal innermost-layer citizen — and would
|
|
22
|
+
* fail at error severity. The remedies available are: add an import (harmful),
|
|
23
|
+
* exclude a working rule, narrow the selector, or delete the rule. None
|
|
24
|
+
* improves anything, because **for the `only*` family zero edges is maximal
|
|
25
|
+
* compliance**, not absent evidence. That fails ADR-009 rule 2.
|
|
26
|
+
* - **Per-rule multiplies.** Six boundaries and one dependency-free shared file
|
|
27
|
+
* make `strictBoundaries` emit 13 rules, **12** with subjects and zero edges.
|
|
28
|
+
* - **`ignoreTypeImports` inverts it.** Counting edges after the filter means a
|
|
29
|
+
* layer whose only dependency is `import type` counts zero and fires on the
|
|
30
|
+
* best possible outcome — under the option the docs recommend for layers.
|
|
31
|
+
*
|
|
32
|
+
* So the remedy is genuinely optional and the reader must judge it, which under
|
|
33
|
+
* ADR-009 rule 1 is a disclosure, not a failure. This module is the disclosure.
|
|
34
|
+
*
|
|
35
|
+
* ## Why module state rather than a field on `ConditionContext`
|
|
36
|
+
*
|
|
37
|
+
* `ConditionContext` is a **public exported type** backing the documented
|
|
38
|
+
* `defineCondition()`, and bug 0015 records that extending `Condition<T>` for
|
|
39
|
+
* this was one reason the failing design could not ship. `diff-disclosure.ts`
|
|
40
|
+
* established the pattern for a run-scoped notice that conditions produce and
|
|
41
|
+
* the CLI consumes; this follows it, including the test reset.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Why a rule tested no edges. The three are not interchangeable, and saying the
|
|
45
|
+
* wrong one is an ADR-009 rule 2 defect — a stated cause that is wrong for the
|
|
46
|
+
* input.
|
|
47
|
+
*
|
|
48
|
+
* The first version of this module printed "correct for a genuinely
|
|
49
|
+
* dependency-free module" for all three. Measured, that is false for two of
|
|
50
|
+
* them: a subject whose imports were filtered by `ignoreTypeImports` **has**
|
|
51
|
+
* dependencies, and a reader who opens the folder finds them and concludes the
|
|
52
|
+
* tool is broken; and a rule whose allowlist matched no import is pointing at
|
|
53
|
+
* the interesting case — the glob may be a typo — which that sentence hides.
|
|
54
|
+
*/
|
|
55
|
+
export type UntestedReason =
|
|
56
|
+
/** The subjects have no edges of any kind. Usually correct, and the layered case. */
|
|
57
|
+
'no-edges'
|
|
58
|
+
/** Edges existed, and every one was filtered out before the check saw it. */
|
|
59
|
+
| 'all-filtered'
|
|
60
|
+
/** Edges existed, and none of them matched the allowlist glob. */
|
|
61
|
+
| 'none-matched';
|
|
62
|
+
/** One rule's evidence: how many subjects it saw and how many edges it tested. */
|
|
63
|
+
export interface EdgeCoverage {
|
|
64
|
+
readonly rule: string;
|
|
65
|
+
readonly subjects: number;
|
|
66
|
+
readonly edges: number;
|
|
67
|
+
readonly reason: UntestedReason;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Record what a condition had to work with.
|
|
71
|
+
*
|
|
72
|
+
* Keyed on the rule description, and **accumulated** rather than overwritten:
|
|
73
|
+
* one rule can evaluate its condition more than once (a `.check()` after a
|
|
74
|
+
* `.violations()`, or the same description in two files), and taking the last
|
|
75
|
+
* write would report the smaller run.
|
|
76
|
+
*/
|
|
77
|
+
export declare function recordEdgeCoverage(rule: string, subjects: number, edges: number, reason?: UntestedReason): void;
|
|
78
|
+
/**
|
|
79
|
+
* Rules that had subjects and tested no edges — the vacuous passes.
|
|
80
|
+
*
|
|
81
|
+
* A rule with **no subjects** is deliberately excluded: that is an empty
|
|
82
|
+
* selector, which is `.expectNonEmpty()`'s business and plan 0067's, and
|
|
83
|
+
* reporting it here would give one fault two owners.
|
|
84
|
+
*/
|
|
85
|
+
export declare function untestedRules(): readonly EdgeCoverage[];
|
|
86
|
+
/**
|
|
87
|
+
* The disclosure, or `undefined` when every allowlist was exercised.
|
|
88
|
+
*
|
|
89
|
+
* Names the rules rather than counting them (ADR-009 rule 4): "3 rules tested
|
|
90
|
+
* nothing" sends the reader to grep, and the whole point is that they must
|
|
91
|
+
* judge whether an edge-free population is correct here.
|
|
92
|
+
*/
|
|
93
|
+
export declare function edgeCoverageNotice(): string | undefined;
|
|
94
|
+
/** Drop the tally. The CLI resets per run; tests reset per case. */
|
|
95
|
+
export declare function resetEdgeCoverage(): void;
|
|
96
|
+
//# sourceMappingURL=edge-coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-coverage.d.ts","sourceRoot":"","sources":["../src/edge-coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc;AACxB,qFAAqF;AACnF,UAAU;AACZ,6EAA6E;GAC3E,cAAc;AAChB,kEAAkE;GAChE,cAAc,CAAA;AAElB,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;CAChC;AAID;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,cAA2B,GAClC,IAAI,CAeN;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,SAAS,YAAY,EAAE,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CA0BvD;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much evidence an allowlist condition actually had — bug 0015.
|
|
3
|
+
*
|
|
4
|
+
* The `only*` family constrains **edges**, not subjects. Each iterates a
|
|
5
|
+
* subject's imports (or importers) and reports one violation per edge outside
|
|
6
|
+
* the allowlist, so a subject with **zero edges has nothing to violate and
|
|
7
|
+
* passes**, however broken the allowlist. Measured: one file with no imports,
|
|
8
|
+
* `onlyImportFrom('**\/nowhere/**')` → 1 subject selected, 0 violations.
|
|
9
|
+
*
|
|
10
|
+
* The shape it fails on is the target case, not an edge case: in a layered
|
|
11
|
+
* architecture the innermost layer is the one an allowlist protects and is
|
|
12
|
+
* characteristically the layer with the fewest outbound imports.
|
|
13
|
+
*
|
|
14
|
+
* ## Why this reports rather than fails
|
|
15
|
+
*
|
|
16
|
+
* [ts-archunit Bug 0015](https://github.com/nielspeter/ts-archunit/blob/main/bugs/fixed/0015-allowlist-conditions-pass-vacuously-on-edgeless-subjects.md)
|
|
17
|
+
* refuted failing, per-subject and per-rule, on measurement:
|
|
18
|
+
*
|
|
19
|
+
* - **Per-subject has no statable remedy.** 14 of this repo's `src/` files have
|
|
20
|
+
* zero static imports and 10 are pure leaf modules. `tarjan.ts` is a
|
|
21
|
+
* dependency-free algorithm — the ideal innermost-layer citizen — and would
|
|
22
|
+
* fail at error severity. The remedies available are: add an import (harmful),
|
|
23
|
+
* exclude a working rule, narrow the selector, or delete the rule. None
|
|
24
|
+
* improves anything, because **for the `only*` family zero edges is maximal
|
|
25
|
+
* compliance**, not absent evidence. That fails ADR-009 rule 2.
|
|
26
|
+
* - **Per-rule multiplies.** Six boundaries and one dependency-free shared file
|
|
27
|
+
* make `strictBoundaries` emit 13 rules, **12** with subjects and zero edges.
|
|
28
|
+
* - **`ignoreTypeImports` inverts it.** Counting edges after the filter means a
|
|
29
|
+
* layer whose only dependency is `import type` counts zero and fires on the
|
|
30
|
+
* best possible outcome — under the option the docs recommend for layers.
|
|
31
|
+
*
|
|
32
|
+
* So the remedy is genuinely optional and the reader must judge it, which under
|
|
33
|
+
* ADR-009 rule 1 is a disclosure, not a failure. This module is the disclosure.
|
|
34
|
+
*
|
|
35
|
+
* ## Why module state rather than a field on `ConditionContext`
|
|
36
|
+
*
|
|
37
|
+
* `ConditionContext` is a **public exported type** backing the documented
|
|
38
|
+
* `defineCondition()`, and bug 0015 records that extending `Condition<T>` for
|
|
39
|
+
* this was one reason the failing design could not ship. `diff-disclosure.ts`
|
|
40
|
+
* established the pattern for a run-scoped notice that conditions produce and
|
|
41
|
+
* the CLI consumes; this follows it, including the test reset.
|
|
42
|
+
*/
|
|
43
|
+
const coverage = new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Record what a condition had to work with.
|
|
46
|
+
*
|
|
47
|
+
* Keyed on the rule description, and **accumulated** rather than overwritten:
|
|
48
|
+
* one rule can evaluate its condition more than once (a `.check()` after a
|
|
49
|
+
* `.violations()`, or the same description in two files), and taking the last
|
|
50
|
+
* write would report the smaller run.
|
|
51
|
+
*/
|
|
52
|
+
export function recordEdgeCoverage(rule, subjects, edges, reason = 'no-edges') {
|
|
53
|
+
const prior = coverage.get(rule);
|
|
54
|
+
// The MINIMUM edge count, not the maximum. Two evaluations of the same rule
|
|
55
|
+
// description — the same preset over two projects, or the same chain written
|
|
56
|
+
// twice — merge on this key, and taking the max let a run that tested edges
|
|
57
|
+
// erase a run that tested none. Measured: the vacuous run vanished from the
|
|
58
|
+
// report. For a disclosure OF vacuity the conservative direction is to keep
|
|
59
|
+
// the smaller evidence; over-disclosing costs a footnote, under-disclosing is
|
|
60
|
+
// the silent pass this module exists to surface.
|
|
61
|
+
coverage.set(rule, {
|
|
62
|
+
rule,
|
|
63
|
+
subjects: Math.max(prior?.subjects ?? 0, subjects),
|
|
64
|
+
edges: prior === undefined ? edges : Math.min(prior.edges, edges),
|
|
65
|
+
reason: prior !== undefined && prior.edges <= edges ? prior.reason : reason,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Rules that had subjects and tested no edges — the vacuous passes.
|
|
70
|
+
*
|
|
71
|
+
* A rule with **no subjects** is deliberately excluded: that is an empty
|
|
72
|
+
* selector, which is `.expectNonEmpty()`'s business and plan 0067's, and
|
|
73
|
+
* reporting it here would give one fault two owners.
|
|
74
|
+
*/
|
|
75
|
+
export function untestedRules() {
|
|
76
|
+
return [...coverage.values()].filter((c) => c.subjects > 0 && c.edges === 0);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The disclosure, or `undefined` when every allowlist was exercised.
|
|
80
|
+
*
|
|
81
|
+
* Names the rules rather than counting them (ADR-009 rule 4): "3 rules tested
|
|
82
|
+
* nothing" sends the reader to grep, and the whole point is that they must
|
|
83
|
+
* judge whether an edge-free population is correct here.
|
|
84
|
+
*/
|
|
85
|
+
export function edgeCoverageNotice() {
|
|
86
|
+
const untested = untestedRules();
|
|
87
|
+
if (untested.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
const because = {
|
|
90
|
+
'no-edges': 'those subjects have no imports at all, which is correct for a dependency-free module and ' +
|
|
91
|
+
'means the rule certified nothing otherwise',
|
|
92
|
+
'all-filtered': 'those subjects DO have imports — every one was excluded by `ignoreTypeImports` before the ' +
|
|
93
|
+
'check saw it, so the allowlist was never consulted',
|
|
94
|
+
'none-matched': 'those subjects have imports, and none matched the allowlist glob — so either nothing is ' +
|
|
95
|
+
'in scope by design, or the glob is wrong',
|
|
96
|
+
};
|
|
97
|
+
const lines = untested.map((c) => ` - ${c.rule}\n ${String(c.subjects)} subject${c.subjects === 1 ? '' : 's'}, 0 edges tested — ${because[c.reason]}.`);
|
|
98
|
+
const count = untested.length;
|
|
99
|
+
return (`[eess] ${String(count)} allowlist rule${count === 1 ? '' : 's'} passed without ` +
|
|
100
|
+
`testing a single edge:\n${lines.join('\n')}\n` +
|
|
101
|
+
` An allowlist constrains edges, so a subject with none cannot violate it. Only you can ` +
|
|
102
|
+
`tell an intended shape from a rule that certified nothing.`);
|
|
103
|
+
}
|
|
104
|
+
/** Drop the tally. The CLI resets per run; tests reset per case. */
|
|
105
|
+
export function resetEdgeCoverage() {
|
|
106
|
+
coverage.clear();
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=edge-coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-coverage.js","sourceRoot":"","sources":["../src/edge-coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AA8BH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,QAAgB,EAChB,KAAa,EACb,SAAyB,UAAU;IAEnC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAChC,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,4EAA4E;IAC5E,8EAA8E;IAC9E,iDAAiD;IACjD,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE;QACjB,IAAI;QACJ,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,IAAI,CAAC,EAAE,QAAQ,CAAC;QAClD,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC;QACjE,MAAM,EAAE,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;KAC5E,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,QAAQ,GAAG,aAAa,EAAE,CAAA;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAE3C,MAAM,OAAO,GAAmC;QAC9C,UAAU,EACR,2FAA2F;YAC3F,4CAA4C;QAC9C,cAAc,EACZ,4FAA4F;YAC5F,oDAAoD;QACtD,cAAc,EACZ,0FAA0F;YAC1F,0CAA0C;KAC7C,CAAA;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CACxB,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,sBAAsB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAC3H,CAAA;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAA;IAC7B,OAAO,CACL,UAAU,MAAM,CAAC,KAAK,CAAC,kBAAkB,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,kBAAkB;QACjF,2BAA2B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QAC/C,0FAA0F;QAC1F,4DAA4D,CAC7D,CAAA;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,iBAAiB;IAC/B,QAAQ,CAAC,KAAK,EAAE,CAAA;AAClB,CAAC"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -11,4 +11,29 @@ export declare class ArchRuleError extends Error {
|
|
|
11
11
|
/** The violations that failed the rule, in report order. */
|
|
12
12
|
get violations(): ArchViolation[];
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Is this an `ArchRuleError` — including one thrown by a DIFFERENT copy of this
|
|
16
|
+
* module?
|
|
17
|
+
*
|
|
18
|
+
* `instanceof` is an identity check, and a rule file does not necessarily load
|
|
19
|
+
* through the same module registry as the CLI that loads it. `load-rules.ts`
|
|
20
|
+
* uses jiti (bug 0074: a `.ts` rule file must load inside a `"type": "commonjs"`
|
|
21
|
+
* consumer project), and jiti keeps its own registry — so the `ArchRuleError`
|
|
22
|
+
* a self-executing rule file throws is a different class object from this one
|
|
23
|
+
* and `instanceof` is FALSE for an error that is, in every way that matters,
|
|
24
|
+
* an ArchRuleError.
|
|
25
|
+
*
|
|
26
|
+
* Measured: that made `check.ts` skip `ruleFileTruncated()`, so a rule file
|
|
27
|
+
* whose first rule threw reported the finding and said nothing about the rules
|
|
28
|
+
* after it that never ran — a silent gap in a red run, which is bug 0029
|
|
29
|
+
* reopened by the loader. The same hazard exists for any consumer with two
|
|
30
|
+
* copies of eess-ts on disk (a nested `node_modules`), where the loader is
|
|
31
|
+
* irrelevant.
|
|
32
|
+
*
|
|
33
|
+
* Structural, therefore: the `name` this class stamps in its constructor plus
|
|
34
|
+
* the `violations` array that is its whole reason to exist. Both are part of the
|
|
35
|
+
* documented public shape (`ArchViolation[]` on a thrown error), so matching on
|
|
36
|
+
* them is matching on the contract rather than on a coincidence.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isArchRuleError(error: unknown): error is ArchRuleError;
|
|
14
39
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,KAAK;;gBAG1B,UAAU,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM;IAQxD,4DAA4D;IAC5D,IAAI,UAAU,IAAI,aAAa,EAAE,CAEhC;CACF"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,KAAK;;gBAG1B,UAAU,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM;IAQxD,4DAA4D;IAC5D,IAAI,UAAU,IAAI,aAAa,EAAE,CAEhC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAQtE"}
|