@ecoma-io/archkeep 0.13.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/LICENSE +202 -0
- package/README.md +262 -0
- package/cli.mjs +2792 -0
- package/index.mjs +85 -0
- package/lsp.mjs +81 -0
- package/nx.mjs +24 -0
- package/package.json +81 -0
- package/presets/clean-architecture.json +78 -0
- package/presets/ddd-bounded-contexts.json +88 -0
- package/presets/hexagonal.json +68 -0
- package/presets/layered.json +92 -0
- package/presets/modular-monolith.json +85 -0
- package/presets/vertical-slice.json +68 -0
- package/src/analysis/analyze.mjs +218 -0
- package/src/analysis/contract.md +259 -0
- package/src/analysis/go.mjs +414 -0
- package/src/analysis/manifest-util.mjs +68 -0
- package/src/analysis/python.mjs +1266 -0
- package/src/analysis/registry.mjs +74 -0
- package/src/analysis/rust.mjs +674 -0
- package/src/analysis/source-util.mjs +230 -0
- package/src/analysis/typescript.mjs +1034 -0
- package/src/analysis/vue.mjs +156 -0
- package/src/architecture-intent/intent-fingerprint.mjs +29 -0
- package/src/architecture-intent/judge.mjs +539 -0
- package/src/architecture-intent/model.mjs +703 -0
- package/src/architecture-intent/selectors.mjs +170 -0
- package/src/canonical.mjs +48 -0
- package/src/commands/README.md +266 -0
- package/src/commands/adr.mjs +248 -0
- package/src/commands/check.mjs +989 -0
- package/src/commands/context-command.mjs +212 -0
- package/src/commands/context.mjs +790 -0
- package/src/commands/custom-rules.mjs +428 -0
- package/src/commands/debt.mjs +218 -0
- package/src/commands/diff.mjs +523 -0
- package/src/commands/discover.mjs +159 -0
- package/src/commands/drift.mjs +473 -0
- package/src/commands/edge-constraints.mjs +355 -0
- package/src/commands/explain.mjs +359 -0
- package/src/commands/fitness.mjs +226 -0
- package/src/commands/graph.mjs +297 -0
- package/src/commands/health.mjs +213 -0
- package/src/commands/history.mjs +614 -0
- package/src/commands/impact.mjs +226 -0
- package/src/commands/plan-context-command.mjs +496 -0
- package/src/commands/policy.mjs +138 -0
- package/src/commands/provenance-command.mjs +352 -0
- package/src/commands/provenance.mjs +159 -0
- package/src/commands/reconcile.mjs +219 -0
- package/src/commands/report.mjs +553 -0
- package/src/commands/snapshot-meta.mjs +107 -0
- package/src/commands/waivers.mjs +240 -0
- package/src/config.mjs +1308 -0
- package/src/containment.mjs +234 -0
- package/src/custom-rules/evidence.mjs +340 -0
- package/src/custom-rules/host.mjs +1023 -0
- package/src/custom-rules/values.mjs +43 -0
- package/src/entry-point.mjs +55 -0
- package/src/errors.mjs +36 -0
- package/src/eslint-config.mjs +542 -0
- package/src/go-work.mjs +394 -0
- package/src/governance/adr-registry.mjs +539 -0
- package/src/governance/clock.mjs +69 -0
- package/src/governance/debt-ledger.mjs +274 -0
- package/src/governance/discovery-proposal.mjs +423 -0
- package/src/governance/fitness-registry.mjs +504 -0
- package/src/governance/fitness-rules.mjs +668 -0
- package/src/governance/metrics.mjs +392 -0
- package/src/governance/preset-fingerprints.json +16 -0
- package/src/governance/profile-registry.mjs +366 -0
- package/src/governance/provenance-record.mjs +177 -0
- package/src/governance/reconcile-candidates.mjs +301 -0
- package/src/governance/reconcile-score.mjs +503 -0
- package/src/governance/row-schema.mjs +208 -0
- package/src/governance/verdict.mjs +127 -0
- package/src/governance/waiver.mjs +105 -0
- package/src/graph/create-dependencies.mjs +96 -0
- package/src/intent/intent-manifest.json +347 -0
- package/src/intent/mask-non-code.mjs +640 -0
- package/src/lsp/boundary-config.mjs +225 -0
- package/src/lsp/diagnose.mjs +202 -0
- package/src/lsp/diagnostics.mjs +241 -0
- package/src/lsp/protocol.mjs +215 -0
- package/src/lsp/server.mjs +922 -0
- package/src/lsp/workspace-index.mjs +891 -0
- package/src/nx-json.mjs +95 -0
- package/src/options.mjs +611 -0
- package/src/process.mjs +91 -0
- package/src/providers/moon.mjs +733 -0
- package/src/providers/native/README.md +204 -0
- package/src/providers/native/coverage.mjs +74 -0
- package/src/providers/native/differential.fixtures.mjs +1277 -0
- package/src/providers/native/discover.mjs +431 -0
- package/src/providers/native/graph.mjs +234 -0
- package/src/providers/native/index.mjs +152 -0
- package/src/providers/native/model.mjs +755 -0
- package/src/providers/nx.mjs +178 -0
- package/src/report/README.md +89 -0
- package/src/report/adr-text.mjs +129 -0
- package/src/report/context-text.mjs +109 -0
- package/src/report/debt-text.mjs +105 -0
- package/src/report/diff-text.mjs +219 -0
- package/src/report/discover-text.mjs +186 -0
- package/src/report/drift-text.mjs +194 -0
- package/src/report/envelope-shape.mjs +161 -0
- package/src/report/evidence.mjs +157 -0
- package/src/report/explain-text.mjs +159 -0
- package/src/report/graph-text.mjs +116 -0
- package/src/report/health-text.mjs +123 -0
- package/src/report/history-text.mjs +204 -0
- package/src/report/impact-text.mjs +128 -0
- package/src/report/json.mjs +173 -0
- package/src/report/plan-context-text.mjs +159 -0
- package/src/report/provenance-text.mjs +78 -0
- package/src/report/reconcile-text.mjs +159 -0
- package/src/report/report-text.mjs +264 -0
- package/src/report/sarif.mjs +953 -0
- package/src/report/text.mjs +823 -0
- package/src/report/waivers-text.mjs +100 -0
- package/src/rules/README.md +123 -0
- package/src/rules/index.mjs +962 -0
- package/src/rules/match.mjs +1708 -0
- package/src/rules/messages.mjs +73 -0
- package/src/rules/reachability.mjs +224 -0
- package/src/rules/specifiers.mjs +300 -0
- package/src/rules/tags.mjs +238 -0
- package/src/rules/topology.mjs +333 -0
- package/src/tsconfig-paths.mjs +237 -0
- package/src/verdict.mjs +145 -0
- package/src/workspace.mjs +580 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tsconfig paths hygiene — the workspace-level check that the `paths` alias
|
|
3
|
+
* table still points at code that exists.
|
|
4
|
+
*
|
|
5
|
+
* A `paths` alias whose every target rotted away is a silent lie with two
|
|
6
|
+
* faces. An import of it stops resolving through the table — TypeScript falls
|
|
7
|
+
* back to a node_modules lookup (measured: a dead mapping with an installed
|
|
8
|
+
* package of the same name resolves to the package, `isExternalLibraryImport`
|
|
9
|
+
* true) or reports the import unresolvable — so either the build breaks or,
|
|
10
|
+
* worse, every boundary decision that keys off the alias quietly reads the
|
|
11
|
+
* import as external instead of as the workspace source the alias promised.
|
|
12
|
+
* Nothing else reports either state: the table is only ever consulted, never
|
|
13
|
+
* judged. This module judges it.
|
|
14
|
+
*
|
|
15
|
+
* **This check judges the alias table itself and never resolves a specifier.**
|
|
16
|
+
* `../analysis/typescript.mjs` delegates every real import to
|
|
17
|
+
* `ts.resolveModuleName` precisely so this package never grows a second
|
|
18
|
+
* resolver (docs/reference/languages.md § TypeScript), and a hygiene check that
|
|
19
|
+
* probed candidate files per resolution mode would be that second resolver
|
|
20
|
+
* wearing a janitor's coat. What it may honestly decide instead follows from
|
|
21
|
+
* one measured fact about how TypeScript forms candidates from a target
|
|
22
|
+
* (typescript 5.9.3, probed under node10, nodenext and bundler resolution):
|
|
23
|
+
* every candidate derived from a target — extension substitution and
|
|
24
|
+
* appending, `index.*` and `package.json` lookups below a directory target,
|
|
25
|
+
* and the specifier text substituted at the target's `*` — lives at or below
|
|
26
|
+
* the directory of the target's static prefix (the text before the first `*`;
|
|
27
|
+
* the whole target when it has none).
|
|
28
|
+
*
|
|
29
|
+
* ## The rule, exactly
|
|
30
|
+
*
|
|
31
|
+
* Targets resolve against `baseUrl` when the tsconfig sets one, else against
|
|
32
|
+
* the directory of the config file that declared `paths` (TypeScript's own
|
|
33
|
+
* `pathsBasePath`; both measured). A target is **unreachable** when the
|
|
34
|
+
* directory of its static prefix does not exist — no candidate TypeScript can
|
|
35
|
+
* form from it can then name an existing file. An alias is **dead**, and a
|
|
36
|
+
* finding, when its target list is empty or every target is unreachable.
|
|
37
|
+
*
|
|
38
|
+
* ## Limits, each the honest side of a line this package refuses to cross
|
|
39
|
+
*
|
|
40
|
+
* - **A dead mapping whose prefix directory still exists is not reported.**
|
|
41
|
+
* A target `libs/a/src/index.ts` whose file is gone while `libs/a/src/`
|
|
42
|
+
* remains may still resolve: measured, TypeScript then probes `index.tsx`,
|
|
43
|
+
* `index.d.ts`, `index.js`, even `index.ts.ts` and an `index.ts/` directory,
|
|
44
|
+
* and the probe set differs by resolution mode. Deciding it would mean
|
|
45
|
+
* reproducing that per-mode candidate set — the second resolver. No record.
|
|
46
|
+
* - **A target whose prefix directory falls outside the workspace root is not
|
|
47
|
+
* judged** (a `../` escape, an absolute path elsewhere): this tool's view of
|
|
48
|
+
* the world ends at the root it was given. Counted, not reported.
|
|
49
|
+
* - **A pattern with more than one `*` is not judged**: measured, TypeScript
|
|
50
|
+
* ignores such an alias entirely — zero candidate probes even for a
|
|
51
|
+
* specifier shaped to match — so its targets decide nothing. Counted.
|
|
52
|
+
* - **A target with more than one `*` is judged**: only the first `*` is
|
|
53
|
+
* substituted (measured; later stars stay literal characters in the
|
|
54
|
+
* candidate), so every candidate still lives under the first star's prefix.
|
|
55
|
+
* - **A `paths` value that is not an array of strings is a malformed table,
|
|
56
|
+
* not an absent one.** `ts.parseConfigFileTextToJson` and
|
|
57
|
+
* `ts.parseJsonConfigFileContent` both accept these shapes without a
|
|
58
|
+
* diagnostic (measured), and resolution then misbehaves — a bare-string
|
|
59
|
+
* value is iterated character by character, a non-string element throws
|
|
60
|
+
* mid-resolve. Read as "no aliases" they would be the silent direction, so
|
|
61
|
+
* they come back in `malformed` and `../cli.mjs` turns each into a
|
|
62
|
+
* whole-file failure (exit 3), the same posture the analyzer takes for a
|
|
63
|
+
* tsconfig that does not parse at all.
|
|
64
|
+
* - **A substituted specifier containing `..` can escape the prefix
|
|
65
|
+
* directory.** The reachability model assumes the matched text stays inside
|
|
66
|
+
* it; an import written to traverse out of a mapping is outside this model,
|
|
67
|
+
* and the worst case is a finding naming an alias such an import still
|
|
68
|
+
* reaches — loud, never silent.
|
|
69
|
+
*
|
|
70
|
+
* **This check runs on the CLI surface only, not in the language server**, for
|
|
71
|
+
* the reason `./go-work.mjs` states: it describes the workspace's table, not
|
|
72
|
+
* any document being edited, and a workspace-level finding pinned to whichever
|
|
73
|
+
* file is open would put the report where its fix is not. The table itself
|
|
74
|
+
* comes from `../analysis/typescript.mjs`'s own parsed context — the same
|
|
75
|
+
* file, the same parse, the same `extends` handling the resolver uses — so the
|
|
76
|
+
* check and the resolver cannot disagree about what the workspace's tsconfig
|
|
77
|
+
* says (`tsconfigPathsFacts` there).
|
|
78
|
+
*/
|
|
79
|
+
import { posix } from "node:path";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What a hygiene finding means — one entry per `messageId`, the arrangement
|
|
83
|
+
* `../report/sarif.mjs` derives its rule descriptors from, the same as
|
|
84
|
+
* `./go-work.mjs`, so the id cannot be nameless in a code-scanning upload.
|
|
85
|
+
*/
|
|
86
|
+
export const TSCONFIG_PATHS_MESSAGES = Object.freeze({
|
|
87
|
+
tsconfigDeadPathAlias:
|
|
88
|
+
"A tsconfig paths alias maps only to targets whose directories do not exist: no import of it " +
|
|
89
|
+
"can resolve through the alias table, so the build breaks — or silently resolves to an " +
|
|
90
|
+
"installed package of the same name instead of the workspace source the alias promised.",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const TSCONFIG_PATHS_MESSAGE_IDS = Object.freeze(Object.keys(TSCONFIG_PATHS_MESSAGES));
|
|
94
|
+
|
|
95
|
+
/** A workspace-relative directory for display, `""` being the root. */
|
|
96
|
+
const displayDir = (dir) => (dir === "" ? "the workspace root" : `${dir}/`);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The workspace-relative directory every candidate formed from `target` must
|
|
100
|
+
* live under, or `null` when that directory falls outside the workspace and
|
|
101
|
+
* so cannot be judged from here.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} target A `paths` target as written.
|
|
104
|
+
* @param {string} base Absolute: `baseUrl` if set, else `pathsBasePath`.
|
|
105
|
+
* @param {string} root Absolute workspace root, no trailing slash.
|
|
106
|
+
* @returns {string|null} `""` for the root itself.
|
|
107
|
+
*/
|
|
108
|
+
function probeDirectory(target, base, root) {
|
|
109
|
+
const starIndex = target.indexOf("*");
|
|
110
|
+
const prefix = starIndex === -1 ? target : target.slice(0, starIndex);
|
|
111
|
+
const joined = posix.isAbsolute(prefix)
|
|
112
|
+
? posix.normalize(prefix)
|
|
113
|
+
: posix.normalize(posix.join(base, prefix));
|
|
114
|
+
// A prefix ending at a separator names its directory itself; anything else
|
|
115
|
+
// ends mid-segment (a partial name before `*`, or a whole target), and the
|
|
116
|
+
// candidates live in its parent.
|
|
117
|
+
const dir =
|
|
118
|
+
prefix === "" || prefix.endsWith("/")
|
|
119
|
+
? joined.replace(/\/+$/u, "") || "/"
|
|
120
|
+
: posix.dirname(joined);
|
|
121
|
+
if (dir === root) return "";
|
|
122
|
+
return dir.startsWith(`${root}/`) ? dir.slice(root.length + 1) : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The dead aliases in a `paths` table — pure, facts as arguments, so the tests
|
|
127
|
+
* need no filesystem: the table and its base come from the resolver's own
|
|
128
|
+
* parsed context, and existence arrives as a predicate.
|
|
129
|
+
*
|
|
130
|
+
* @param {{ paths: Record<string, unknown>,
|
|
131
|
+
* base: string,
|
|
132
|
+
* workspaceRoot: string,
|
|
133
|
+
* tsConfig: string,
|
|
134
|
+
* directoryExists: (dir: string) => boolean }} facts `base` is absolute
|
|
135
|
+
* (`baseUrl` if the config sets one, else TypeScript's `pathsBasePath`);
|
|
136
|
+
* `directoryExists` takes a workspace-relative directory, `""` for the root.
|
|
137
|
+
* @returns {{ tsConfig: string,
|
|
138
|
+
* findings: { messageId: string, file: string, line: null, column: null,
|
|
139
|
+
* alias: string, targets: string[], message: string }[],
|
|
140
|
+
* aliases: number, unjudged: number,
|
|
141
|
+
* malformed: { alias: string, reason: string }[] }} `aliases` counts the
|
|
142
|
+
* aliases that reached a dead-or-alive verdict and `unjudged` the ones the
|
|
143
|
+
* header's limits exclude, so the report can state coverage beside the
|
|
144
|
+
* verdict; `malformed` is for `../cli.mjs` to refuse loudly, never to skip.
|
|
145
|
+
*/
|
|
146
|
+
export function judgeTsconfigPaths({ paths, base, workspaceRoot, tsConfig, directoryExists }) {
|
|
147
|
+
const root = workspaceRoot.replace(/\/+$/u, "");
|
|
148
|
+
const findings = [];
|
|
149
|
+
const malformed = [];
|
|
150
|
+
let aliases = 0;
|
|
151
|
+
let unjudged = 0;
|
|
152
|
+
|
|
153
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
154
|
+
if (alias.indexOf("*") !== alias.lastIndexOf("*")) {
|
|
155
|
+
// TypeScript ignores a multi-star pattern outright (header), so its
|
|
156
|
+
// targets decide nothing — dead or alive would both be guesses.
|
|
157
|
+
unjudged += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!Array.isArray(targets)) {
|
|
161
|
+
malformed.push({
|
|
162
|
+
alias,
|
|
163
|
+
reason:
|
|
164
|
+
`${tsConfig} maps "${alias}" to ${JSON.stringify(targets)}, not to an array of ` +
|
|
165
|
+
`target paths, so the paths hygiene check reached no verdict about it — and ` +
|
|
166
|
+
`TypeScript itself only diagnoses this shape when a full Program is built, which ` +
|
|
167
|
+
`this tool never does`,
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (targets.some((target) => typeof target !== "string")) {
|
|
172
|
+
malformed.push({
|
|
173
|
+
alias,
|
|
174
|
+
reason:
|
|
175
|
+
`${tsConfig} maps "${alias}" to a target list with a non-string entry, so the ` +
|
|
176
|
+
`paths hygiene check reached no verdict about it`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const dirs = targets.map((target) => probeDirectory(target, base, root));
|
|
182
|
+
if (dirs.includes(null)) {
|
|
183
|
+
// At least one target lives outside the workspace root; whether it hits
|
|
184
|
+
// a file is not knowable from inside this tree (header).
|
|
185
|
+
unjudged += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
aliases += 1;
|
|
189
|
+
|
|
190
|
+
if (targets.length === 0) {
|
|
191
|
+
findings.push(deadAlias(tsConfig, alias, targets, null));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const missing = [...new Set(dirs)].filter((dir) => !directoryExists(dir));
|
|
195
|
+
if (missing.length === new Set(dirs).size) {
|
|
196
|
+
findings.push(deadAlias(tsConfig, alias, targets, missing));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { tsConfig, findings, aliases, unjudged, malformed };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* One dead-alias finding. Positionless on purpose: the parsed compiler options
|
|
205
|
+
* carry no source positions, and under `extends` the alias may not even be
|
|
206
|
+
* declared in the file the workspace names — the finding cites the table the
|
|
207
|
+
* resolver actually reads, and a fabricated line 1 would mark text the alias
|
|
208
|
+
* is not on (`../report/sarif.mjs` gives the same reasoning for go.work).
|
|
209
|
+
*
|
|
210
|
+
* @param {string} tsConfig The workspace tsconfig, workspace-relative.
|
|
211
|
+
* @param {string} alias The pattern as written.
|
|
212
|
+
* @param {string[]} targets The targets as written.
|
|
213
|
+
* @param {string[]|null} missing The unreachable directories, `null` for an
|
|
214
|
+
* alias with no targets at all.
|
|
215
|
+
*/
|
|
216
|
+
function deadAlias(tsConfig, alias, targets, missing) {
|
|
217
|
+
const message =
|
|
218
|
+
missing === null
|
|
219
|
+
? `${tsConfig} maps "${alias}" to an empty target list — no import matching it can ever ` +
|
|
220
|
+
`resolve through this alias. Give it a target, or delete it.`
|
|
221
|
+
: `${tsConfig} maps "${alias}" only to ${targets.map((t) => `"${t}"`).join(", ")}, and ` +
|
|
222
|
+
`every candidate those targets can name lives under ` +
|
|
223
|
+
`${missing.map(displayDir).join(", ")} — which ${missing.length === 1 ? "does" : "do"} ` +
|
|
224
|
+
`not exist. No import matching "${alias}" resolves through this alias: the build ` +
|
|
225
|
+
`breaks on it, or it silently resolves to an installed package of the same name and ` +
|
|
226
|
+
`every boundary decision reads the import as external. Point the alias at the moved ` +
|
|
227
|
+
`source, or delete it.`;
|
|
228
|
+
return {
|
|
229
|
+
messageId: "tsconfigDeadPathAlias",
|
|
230
|
+
file: tsConfig,
|
|
231
|
+
line: null,
|
|
232
|
+
column: null,
|
|
233
|
+
alias,
|
|
234
|
+
targets: [...targets],
|
|
235
|
+
message,
|
|
236
|
+
};
|
|
237
|
+
}
|
package/src/verdict.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process's exit-code contract, and the one function that turns a run's
|
|
3
|
+
* counts into the verdict every format agrees on.
|
|
4
|
+
*
|
|
5
|
+
* Both sit here rather than in `../cli.mjs` because two callers need them and
|
|
6
|
+
* only one of the two is the CLI shell: `./commands/check.mjs` words its own
|
|
7
|
+
* `--format json` envelope from `verdictFor`, and `../cli.mjs`'s `runCheck`
|
|
8
|
+
* takes the process's exit code from the same call. `../cli.mjs` re-exports
|
|
9
|
+
* `EXIT` under its own name, so every importer that already reads it from
|
|
10
|
+
* there keeps working.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { buildDecision } from "./report/evidence.mjs";
|
|
14
|
+
|
|
15
|
+
export const EXIT = Object.freeze({
|
|
16
|
+
ok: 0,
|
|
17
|
+
violations: 1,
|
|
18
|
+
usage: 2,
|
|
19
|
+
error: 3,
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* The one place that turns a run's counts into the verdict every format
|
|
23
|
+
* agrees on. `runCheck` uses it for the process's exit code; `check` uses the
|
|
24
|
+
* same function to word its own `--format json` envelope's `status` and
|
|
25
|
+
* `exitCode` fields — called once each, from the same counts, so the two can
|
|
26
|
+
* never disagree about a run neither of them re-derives from the other.
|
|
27
|
+
*
|
|
28
|
+
* Findings first — boundary violations, go.work drift, dead tsconfig path
|
|
29
|
+
* aliases and architecture-intent findings alike are verdicts, and a caller
|
|
30
|
+
* that gets `findings` knows the tree is dirty whatever else the run could not
|
|
31
|
+
* reach; the report lists the unreached files either way. A clean run with a
|
|
32
|
+
* file nobody could analyze — or an architecture-intent boundary nobody could
|
|
33
|
+
* verify — is the case that must not read `ok`, because `ok` is read as
|
|
34
|
+
* "checked, and fine".
|
|
35
|
+
*
|
|
36
|
+
* The `decision` is the canonical 4-state verb of the same verdict
|
|
37
|
+
* (`./report/evidence.mjs`), built from the same counts so the envelope's
|
|
38
|
+
* `status` and its `decision.verdict` cannot disagree: `ok`→`pass`,
|
|
39
|
+
* `findings`→`fail`, `no-verdict`→`unknown`. `buildDecision` throws on any
|
|
40
|
+
* invariant the counts violate (a `pass` over incomplete coverage, a `fail`
|
|
41
|
+
* with no findings), which makes a regression in this mapping a loud error
|
|
42
|
+
* rather than a silent one.
|
|
43
|
+
*
|
|
44
|
+
* @param {{violations: number, declaredEdgeFindings: number, goWorkDrift: number, tsconfigPathsDead: number, intentFindings: number, intentUnresolved: number, intentUnresolvedDecisionRefs?: number, unchecked: number, fitnessFail?: number, fitnessUnknown?: number, customRuleFail?: number, customRuleUnknown?: number}} counts
|
|
45
|
+
* @returns {{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3, decision: object}}
|
|
46
|
+
*/
|
|
47
|
+
export function verdictFor({
|
|
48
|
+
violations,
|
|
49
|
+
declaredEdgeFindings,
|
|
50
|
+
goWorkDrift,
|
|
51
|
+
tsconfigPathsDead,
|
|
52
|
+
intentFindings,
|
|
53
|
+
intentUnresolved,
|
|
54
|
+
intentUnresolvedDecisionRefs = 0,
|
|
55
|
+
unchecked,
|
|
56
|
+
fitnessFail = 0,
|
|
57
|
+
fitnessUnknown = 0,
|
|
58
|
+
customRuleFail = 0,
|
|
59
|
+
customRuleUnknown = 0,
|
|
60
|
+
}) {
|
|
61
|
+
if (
|
|
62
|
+
violations > 0 ||
|
|
63
|
+
declaredEdgeFindings > 0 ||
|
|
64
|
+
goWorkDrift > 0 ||
|
|
65
|
+
tsconfigPathsDead > 0 ||
|
|
66
|
+
intentFindings > 0 ||
|
|
67
|
+
fitnessFail > 0 ||
|
|
68
|
+
// A `fail`-verdict custom rule is a finding by the same argument a failing
|
|
69
|
+
// fitness function is one (D-09): the workspace declared the law, the law
|
|
70
|
+
// judged, and the law says no. It rides this lane rather than a new exit
|
|
71
|
+
// code, so a consumer's CI branches on the same 0/1/3 it already does.
|
|
72
|
+
customRuleFail > 0
|
|
73
|
+
) {
|
|
74
|
+
return {
|
|
75
|
+
status: "findings",
|
|
76
|
+
exitCode: EXIT.violations,
|
|
77
|
+
decision: buildDecision({
|
|
78
|
+
status: "findings",
|
|
79
|
+
coverageComplete: unchecked === 0,
|
|
80
|
+
findings:
|
|
81
|
+
violations +
|
|
82
|
+
declaredEdgeFindings +
|
|
83
|
+
goWorkDrift +
|
|
84
|
+
tsconfigPathsDead +
|
|
85
|
+
intentFindings +
|
|
86
|
+
fitnessFail +
|
|
87
|
+
customRuleFail,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
unchecked > 0 ||
|
|
93
|
+
intentUnresolved > 0 ||
|
|
94
|
+
intentUnresolvedDecisionRefs > 0 ||
|
|
95
|
+
fitnessUnknown > 0 ||
|
|
96
|
+
customRuleUnknown > 0
|
|
97
|
+
) {
|
|
98
|
+
return {
|
|
99
|
+
status: "no-verdict",
|
|
100
|
+
exitCode: EXIT.error,
|
|
101
|
+
decision: buildDecision({
|
|
102
|
+
status: "no-verdict",
|
|
103
|
+
coverageComplete: unchecked === 0,
|
|
104
|
+
findings: 0,
|
|
105
|
+
// The could-not-look condition, named so a reader knows WHICH half of
|
|
106
|
+
// the run did not reach a verdict (I3). When read-only coverage and
|
|
107
|
+
// intent both failed, name both — a reason naming only the file count
|
|
108
|
+
// would hide the unresolved intent boundary from a reader acting on
|
|
109
|
+
// the reason alone (it stays visible in result.intent.unresolved, and
|
|
110
|
+
// status is still no-verdict, so nothing is silent). Each clause below
|
|
111
|
+
// is independent of the others — none is gated on a sibling clause
|
|
112
|
+
// being zero — so a tree that fails on several axes at once names
|
|
113
|
+
// every one of them, not just the first the array happens to check.
|
|
114
|
+
reason: [
|
|
115
|
+
unchecked > 0
|
|
116
|
+
? `${unchecked} file${unchecked === 1 ? "" : "s"} could not be analyzed — coverage incomplete`
|
|
117
|
+
: null,
|
|
118
|
+
intentUnresolved > 0
|
|
119
|
+
? `${intentUnresolved} architecture-intent boundary or row${intentUnresolved === 1 ? "" : "s"} could not be established`
|
|
120
|
+
: null,
|
|
121
|
+
intentUnresolvedDecisionRefs > 0
|
|
122
|
+
? `${intentUnresolvedDecisionRefs} intent row${intentUnresolvedDecisionRefs === 1 ? "" : "s"} ${intentUnresolvedDecisionRefs === 1 ? "cites" : "cite"} a decisionRef that does not resolve`
|
|
123
|
+
: null,
|
|
124
|
+
fitnessUnknown > 0
|
|
125
|
+
? `${fitnessUnknown} fitness function${fitnessUnknown === 1 ? "" : "s"} could not be determined`
|
|
126
|
+
: null,
|
|
127
|
+
customRuleUnknown > 0
|
|
128
|
+
? `${customRuleUnknown} custom rule${customRuleUnknown === 1 ? "" : "s"} could not be judged`
|
|
129
|
+
: null,
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join("; "),
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
status: "ok",
|
|
138
|
+
exitCode: EXIT.ok,
|
|
139
|
+
decision: buildDecision({
|
|
140
|
+
status: "ok",
|
|
141
|
+
coverageComplete: true,
|
|
142
|
+
findings: 0,
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
}
|