@ecoma-io/archkeep 0.24.1 → 0.26.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/cli.mjs +166 -506
- package/commands.mjs +7 -1
- package/package.json +1 -1
- package/src/architecture-intent/judge.mjs +1 -1
- package/src/architecture-intent/model.mjs +34 -0
- package/src/canonical.mjs +2 -1
- package/src/commands/adr-for-workspace.mjs +63 -0
- package/src/commands/analyze-capability.mjs +13 -0
- package/src/commands/change-intent.mjs +11 -0
- package/src/commands/change.mjs +315 -55
- package/src/commands/check-capability.mjs +13 -0
- package/src/commands/check.mjs +13 -3
- package/src/commands/compare-capability.mjs +20 -0
- package/src/commands/completeness.mjs +12 -0
- package/src/commands/context-command.mjs +31 -0
- package/src/commands/coverage-verdict.mjs +15 -2
- package/src/commands/debt.mjs +30 -0
- package/src/commands/decisions.mjs +59 -10
- package/src/commands/delta-snapshot.mjs +25 -4
- package/src/commands/delta.mjs +301 -67
- package/src/commands/diff.mjs +48 -9
- package/src/commands/discover.mjs +37 -0
- package/src/commands/drift.mjs +45 -0
- package/src/commands/evolution.mjs +22 -2
- package/src/commands/explain-capability.mjs +11 -0
- package/src/commands/explain.mjs +24 -0
- package/src/commands/fitness.mjs +119 -10
- package/src/commands/govern-capability.mjs +16 -0
- package/src/commands/graph.mjs +39 -0
- package/src/commands/health.mjs +34 -0
- package/src/commands/history.mjs +119 -2
- package/src/commands/impact.mjs +23 -0
- package/src/commands/inspect-capability.mjs +16 -0
- package/src/commands/plan-context-command.mjs +35 -0
- package/src/commands/policy.mjs +42 -4
- package/src/commands/provenance-command.mjs +15 -24
- package/src/commands/report.mjs +38 -0
- package/src/commands/rules-capability.mjs +16 -0
- package/src/commands/rules.mjs +3 -1
- package/src/commands/scenario.mjs +23 -0
- package/src/commands/snapshot-meta.mjs +33 -0
- package/src/commands/trajectory.mjs +35 -0
- package/src/commands/waivers.mjs +30 -0
- package/src/corpus/goldens/.gitkeep +0 -0
- package/src/corpus/goldens/adr.json +62 -0
- package/src/corpus/goldens/adr.text +6 -0
- package/src/corpus/goldens/change.json +98 -0
- package/src/corpus/goldens/change.text +6 -0
- package/src/corpus/goldens/check.json +90 -0
- package/src/corpus/goldens/check.sarif +479 -0
- package/src/corpus/goldens/check.text +9 -0
- package/src/corpus/goldens/context.json +47 -0
- package/src/corpus/goldens/context.text +6 -0
- package/src/corpus/goldens/debt.json +59 -0
- package/src/corpus/goldens/debt.text +7 -0
- package/src/corpus/goldens/decisions.json +74 -0
- package/src/corpus/goldens/decisions.text +7 -0
- package/src/corpus/goldens/delta.json +94 -0
- package/src/corpus/goldens/delta.sarif +472 -0
- package/src/corpus/goldens/delta.text +5 -0
- package/src/corpus/goldens/diff.json +58 -0
- package/src/corpus/goldens/diff.text +4 -0
- package/src/corpus/goldens/discover.json +77 -0
- package/src/corpus/goldens/discover.text +10 -0
- package/src/corpus/goldens/drift.json +43 -0
- package/src/corpus/goldens/drift.text +3 -0
- package/src/corpus/goldens/evolution.json +328 -0
- package/src/corpus/goldens/evolution.text +32 -0
- package/src/corpus/goldens/explain.json +60 -0
- package/src/corpus/goldens/explain.text +7 -0
- package/src/corpus/goldens/fitness.json +45 -0
- package/src/corpus/goldens/fitness.text +3 -0
- package/src/corpus/goldens/graph.json +78 -0
- package/src/corpus/goldens/graph.text +10 -0
- package/src/corpus/goldens/health.json +72 -0
- package/src/corpus/goldens/health.text +10 -0
- package/src/corpus/goldens/help.text +104 -0
- package/src/corpus/goldens/history.json +43 -0
- package/src/corpus/goldens/history.text +4 -0
- package/src/corpus/goldens/impact.json +327 -0
- package/src/corpus/goldens/impact.text +10 -0
- package/src/corpus/goldens/provenance.json +453 -0
- package/src/corpus/goldens/provenance.text +14 -0
- package/src/corpus/goldens/reconcile.json +168 -0
- package/src/corpus/goldens/reconcile.text +3 -0
- package/src/corpus/goldens/report.json +168 -0
- package/src/corpus/goldens/report.text +42 -0
- package/src/corpus/goldens/rules verify.json +0 -0
- package/src/corpus/goldens/rules verify.text +0 -0
- package/src/corpus/goldens/scenario.json +441 -0
- package/src/corpus/goldens/scenario.text +28 -0
- package/src/corpus/goldens/trajectory.json +83 -0
- package/src/corpus/goldens/trajectory.text +10 -0
- package/src/corpus/goldens/usage-error.text +105 -0
- package/src/corpus/goldens/waivers.json +40 -0
- package/src/corpus/goldens/waivers.text +1 -0
- package/src/corpus/goldens-lsp/initialize.result.json +15 -0
- package/src/corpus/goldens-lsp/publish-cleared-empty.json +4 -0
- package/src/corpus/goldens-lsp/publish-fixed-empty.json +5 -0
- package/src/corpus/goldens-lsp/publish-unparseable.json +54 -0
- package/src/corpus/goldens-lsp/publish-violation.json +22 -0
- package/src/corpus/goldens-lsp/register-watched-files.json +72 -0
- package/src/custom-rules/values.mjs +1 -4
- package/src/go-work.mjs +8 -23
- package/src/governance/evolution-event.mjs +15 -0
- package/src/governance/evolution-store.mjs +59 -31
- package/src/governance/fitness-registry.mjs +14 -1
- package/src/governance/provenance-record.mjs +0 -152
- package/src/governance/verdict.mjs +62 -7
- package/src/intent/intent-manifest.json +22 -16
- package/src/lsp/server.mjs +5 -8
- package/src/lsp/workspace-index.mjs +55 -220
- package/src/options.mjs +10 -0
- package/src/providers/native/discover.mjs +13 -12
- package/src/providers/native/model.mjs +8 -3
- package/src/providers/nx-static.mjs +231 -0
- package/src/report/json.mjs +7 -4
- package/src/report/sarif.mjs +8 -3
- package/src/rules/edge-constraints.mjs +9 -0
- package/src/rules/index.mjs +26 -10
- package/src/rules/messages.mjs +64 -14
- package/src/rules/specifiers.mjs +21 -20
- package/src/tsconfig-paths.mjs +8 -14
- package/src/values.mjs +11 -0
- package/src/verdict.mjs +127 -16
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `check` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `check`, `fitness`, `scenario`.
|
|
4
|
+
*
|
|
5
|
+
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
6
|
+
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
7
|
+
* the facade so the vocabulary's code referent is visible to the import
|
|
8
|
+
* graph, not just to a comment (PD-18,
|
|
9
|
+
* ../../../../docs/architecture/refactor/DECISIONS.md).
|
|
10
|
+
*/
|
|
11
|
+
export { check, sortViolations } from "./check.mjs";
|
|
12
|
+
export { fitness } from "./fitness.mjs";
|
|
13
|
+
export { scenario } from "./scenario.mjs";
|
package/src/commands/check.mjs
CHANGED
|
@@ -209,7 +209,7 @@ function declaredEdgeManifest({ provider, graph }, sourceProject) {
|
|
|
209
209
|
* evidenceOut?: string|null}} options
|
|
210
210
|
* @param {{cwd: string, readGraph?: Function, listFiles?: Function,
|
|
211
211
|
* listUntracked?: Function}} context
|
|
212
|
-
* @returns {Promise<{report: string, violations: number, declaredEdgeFindings: number,
|
|
212
|
+
* @returns {Promise<{exitCode: 0|1|3, report: string, violations: number, declaredEdgeFindings: number,
|
|
213
213
|
* goWorkDrift: number, tsconfigPathsDead: number, intentFindings: number,
|
|
214
214
|
* intentUnresolved: number, intentUnresolvedDecisionRefs: number, fitnessFail: number,
|
|
215
215
|
* fitnessUnknown: number, customRuleFail: number, customRuleUnknown: number,
|
|
@@ -521,6 +521,15 @@ export async function check(
|
|
|
521
521
|
// from the MARKER (the line a reader edits is the document's), project and
|
|
522
522
|
// constraint from the edge — so suppression, waiver annotation, sorting and
|
|
523
523
|
// the SARIF face all treat it as the ordinary violation it is.
|
|
524
|
+
//
|
|
525
|
+
// PD-13 (2026-09-06) pins THIS fold as the documented normalization seam
|
|
526
|
+
// (`../../../../docs/architecture/refactor/DECISIONS.md`): the one place
|
|
527
|
+
// `judgeEdge`'s markdown-pairing verdicts are reshaped into the exact
|
|
528
|
+
// `Violation` record `violationOf` (`../rules/index.mjs`) builds — the
|
|
529
|
+
// families then fold into the one verdict lane as count keys into
|
|
530
|
+
// `verdictFor` (`../verdict.mjs`). The canonical statement is the
|
|
531
|
+
// "Finding — the unowned concept" section of
|
|
532
|
+
// `../../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
|
|
524
533
|
/** @type {object[]} */
|
|
525
534
|
const markdownRaw = [];
|
|
526
535
|
if (config !== null && markdownTrack !== null && config.depConstraints.length > 0) {
|
|
@@ -1289,6 +1298,7 @@ export async function check(
|
|
|
1289
1298
|
});
|
|
1290
1299
|
|
|
1291
1300
|
return {
|
|
1301
|
+
exitCode: verdict.exitCode,
|
|
1292
1302
|
report,
|
|
1293
1303
|
violations: violations.length,
|
|
1294
1304
|
waived,
|
|
@@ -1311,8 +1321,8 @@ export async function check(
|
|
|
1311
1321
|
customRulesDeclared: customRules !== null,
|
|
1312
1322
|
analyzed,
|
|
1313
1323
|
unchecked,
|
|
1314
|
-
// The
|
|
1315
|
-
//
|
|
1324
|
+
// The exit authority is `verdict` above, surfaced on this return: the
|
|
1325
|
+
// process exit code comes straight from this field, so a count the
|
|
1316
1326
|
// envelope saw but the exit code did not would let the two disagree.
|
|
1317
1327
|
blindSpots: blindSpotCount,
|
|
1318
1328
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `compare` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `diff`, `delta` (both modes), `change`, `drift`, `reconcile`,
|
|
4
|
+
* `history`, `trajectory`, `evolution`, plus the output guards the two
|
|
5
|
+
* verbs that write reports declare (`change`, `history`, `trajectory`).
|
|
6
|
+
*
|
|
7
|
+
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
8
|
+
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
9
|
+
* the facade so the vocabulary's code referent is visible to the import
|
|
10
|
+
* graph, not just to a comment (PD-18,
|
|
11
|
+
* ../../../../docs/architecture/refactor/DECISIONS.md).
|
|
12
|
+
*/
|
|
13
|
+
export { diff } from "./diff.mjs";
|
|
14
|
+
export { captureBaseline, delta } from "./delta.mjs";
|
|
15
|
+
export { change, changeOutputRefusal } from "./change.mjs";
|
|
16
|
+
export { drift } from "./drift.mjs";
|
|
17
|
+
export { reconcileCommand } from "./reconcile.mjs";
|
|
18
|
+
export { history, historyOutputRefusal } from "./history.mjs";
|
|
19
|
+
export { trajectoryCommand, trajectoryOutputRefusal } from "./trajectory.mjs";
|
|
20
|
+
export { evolutionCommand } from "./evolution.mjs";
|
|
@@ -24,6 +24,18 @@
|
|
|
24
24
|
* `overallComplete` implies ALL gates pass. Any failed gate MUST prevent
|
|
25
25
|
* `overallComplete = true`.
|
|
26
26
|
*
|
|
27
|
+
* ## Not the coverage refusal (register R1)
|
|
28
|
+
*
|
|
29
|
+
* These statuses grade the composed evaluation — how completely the
|
|
30
|
+
* Evidence-Complete gates were satisfied for the statement being made. A
|
|
31
|
+
* different question is whether the run saw enough input to make any claim
|
|
32
|
+
* at all, and that one is answered only by the graph family's shared
|
|
33
|
+
* refusal contract (`./coverage-verdict.mjs`): `coverageVerdict` refuses
|
|
34
|
+
* `no-verdict` (exit 3), and no status here is derived from it or folds
|
|
35
|
+
* into it. Two models of "did we see everything", owned by different
|
|
36
|
+
* waves — register R1 in `docs/architecture/refactor/SEMANTIC-MODEL.md`
|
|
37
|
+
* keeps them separate.
|
|
38
|
+
*
|
|
27
39
|
* @module
|
|
28
40
|
*/
|
|
29
41
|
|
|
@@ -37,10 +37,12 @@ import { judgeEdge } from "../rules/edge-constraints.mjs";
|
|
|
37
37
|
import { findConstraintsFor } from "../rules/tags.mjs";
|
|
38
38
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
39
39
|
import { formatContextReport } from "../report/context-text.mjs";
|
|
40
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
40
41
|
import { coverageVerdict } from "./coverage-verdict.mjs";
|
|
41
42
|
import { resolveProvenance } from "./provenance.mjs";
|
|
42
43
|
import { readAdrContext } from "./adr.mjs";
|
|
43
44
|
import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/adr-registry.mjs";
|
|
45
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* Collects the architecture context for a project: its tags, which constraint
|
|
@@ -210,3 +212,32 @@ export function contextCommand(projectName, commandContext, config) {
|
|
|
210
212
|
},
|
|
211
213
|
};
|
|
212
214
|
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* `context` as the CLI drives it (the non-plan read): the shared preamble —
|
|
218
|
+
* command context, then the boundary law — resolved here so
|
|
219
|
+
* `../../cli.mjs`'s driver only wires options, IO seams, and where output
|
|
220
|
+
* lands (`./README.md`). The engine this returns from is `contextCommand`
|
|
221
|
+
* above, unchanged.
|
|
222
|
+
*
|
|
223
|
+
* The command context is resolved over the WHOLE workspace. Scoping by path
|
|
224
|
+
* is the plan command's decision (which projects the change touches), not
|
|
225
|
+
* the preamble's: the rule verdict and the architecture snapshot must be
|
|
226
|
+
* over the whole tree, and only reporting is narrowed. Passing no paths
|
|
227
|
+
* keeps the non-plan `context` path byte-for-byte identical.
|
|
228
|
+
*
|
|
229
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
230
|
+
* flags; `paths[0]` is the project name.
|
|
231
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
232
|
+
* seams a test injects, the same ones `check` takes.
|
|
233
|
+
* @returns {Promise<object>} `contextCommand`'s result, unmodified.
|
|
234
|
+
*/
|
|
235
|
+
export async function context(options, { cwd, readGraph, listFiles }) {
|
|
236
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
237
|
+
// The config's location is a separate fact from the workspace root.
|
|
238
|
+
// Same loading logic as `check` and `explain` (`resolvePolicy`) — a
|
|
239
|
+
// `--config` overrides the workspace's own `boundaryConfig`,
|
|
240
|
+
// profile-aware the same way `check` is.
|
|
241
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
242
|
+
return contextCommand(options.paths[0], commandContext, config);
|
|
243
|
+
}
|
|
@@ -135,7 +135,7 @@ export function coverageVerdict(commandContext, { acceptedFiles } = {}) {
|
|
|
135
135
|
* command was asking (the text face's one free clause), `acceptedFiles`
|
|
136
136
|
* forwards to `coverageVerdict` (see above), and `decision: true` attaches
|
|
137
137
|
* the canonical `unknown` decision.
|
|
138
|
-
* @returns {{status: "no-verdict", coverage: object,
|
|
138
|
+
* @returns {{status: "no-verdict", exitCode: 3, coverage: object,
|
|
139
139
|
* report: {text: string, json: string}}}
|
|
140
140
|
*/
|
|
141
141
|
export function coverageRefusal({ command, commandContext, what, acceptedFiles, decision }) {
|
|
@@ -151,6 +151,18 @@ export function coverageRefusal({ command, commandContext, what, acceptedFiles,
|
|
|
151
151
|
}
|
|
152
152
|
/** @type {"no-verdict"} The completeness is false, so this is the status. */
|
|
153
153
|
const status = "no-verdict";
|
|
154
|
+
// The `complete` throw above pins the STATUS; this guard pins the EXIT the
|
|
155
|
+
// same way, so the `{3}` annotation below is a checked fact rather than an
|
|
156
|
+
// unchecked assertion — a future `coverageVerdict` that let an incomplete
|
|
157
|
+
// run claim a clean exit dies here, loudly, instead of typing the lie.
|
|
158
|
+
if (verdict.exitCode !== EXIT.error) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`coverageRefusal: ${command}'s incomplete verdict carries exit ${verdict.exitCode} — ` +
|
|
161
|
+
`a refusal never exits clean`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
/** @type {3} The value flows from the one verdict table; the type is proven above. */
|
|
165
|
+
const refusalExitCode = verdict.exitCode;
|
|
154
166
|
const reasons = coverageIncompleteReasons({
|
|
155
167
|
unchecked: verdict.notAnalyzed.length,
|
|
156
168
|
blindSpots: verdict.blindSpotCount,
|
|
@@ -174,7 +186,7 @@ export function coverageRefusal({ command, commandContext, what, acceptedFiles,
|
|
|
174
186
|
provenance: resolveProvenance(commandContext.root),
|
|
175
187
|
},
|
|
176
188
|
status,
|
|
177
|
-
exitCode:
|
|
189
|
+
exitCode: refusalExitCode,
|
|
178
190
|
coverage,
|
|
179
191
|
result: undefined,
|
|
180
192
|
...(decision === true
|
|
@@ -190,6 +202,7 @@ export function coverageRefusal({ command, commandContext, what, acceptedFiles,
|
|
|
190
202
|
});
|
|
191
203
|
return {
|
|
192
204
|
status,
|
|
205
|
+
exitCode: refusalExitCode,
|
|
193
206
|
coverage,
|
|
194
207
|
report: {
|
|
195
208
|
text:
|
package/src/commands/debt.mjs
CHANGED
|
@@ -51,11 +51,14 @@ import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
|
51
51
|
import { computeDebtLedger } from "../governance/debt-ledger.mjs";
|
|
52
52
|
import { readEvents } from "../governance/evolution-store.mjs";
|
|
53
53
|
import { formatDebtReport } from "../report/debt-text.mjs";
|
|
54
|
+
import { isAbsolute, resolve } from "node:path";
|
|
54
55
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
55
56
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
56
57
|
import { resolveProvenance } from "./provenance.mjs";
|
|
57
58
|
import { buildObserved, refuseIncompleteGraph } from "./drift.mjs";
|
|
58
59
|
import { readSnapshots } from "./history.mjs";
|
|
60
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
61
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Runs the `debt` command: computes the current candid facts, ages them across
|
|
@@ -240,3 +243,30 @@ export async function debtCommand(dir, commandContext, options = {}) {
|
|
|
240
243
|
},
|
|
241
244
|
};
|
|
242
245
|
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* `debt` as the CLI drives it: the history directory resolved from the
|
|
249
|
+
* single positional argument, then the shared preamble — command context,
|
|
250
|
+
* then the boundary law — so `../../cli.mjs`'s driver only wires options,
|
|
251
|
+
* IO seams, and where output lands (`./README.md`). The engine this returns
|
|
252
|
+
* from is `debtCommand` above, unchanged.
|
|
253
|
+
*
|
|
254
|
+
* @param {{config: string|null, events: string|null, paths: string[]}} options
|
|
255
|
+
* This run's parsed flags; `paths[0]` is the history directory.
|
|
256
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
257
|
+
* seams a test injects, the same ones `check` takes.
|
|
258
|
+
* @returns {Promise<object>} `debtCommand`'s result, unmodified.
|
|
259
|
+
*/
|
|
260
|
+
export async function debt(options, { cwd, readGraph, listFiles }) {
|
|
261
|
+
const dir = isAbsolute(options.paths[0]) ? options.paths[0] : resolve(cwd, options.paths[0]);
|
|
262
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
263
|
+
// The boundary law the ledger ages waivers against — resolved the same way
|
|
264
|
+
// `graph` and `diff` resolve it (`resolvePolicy`), so a `debt` run and a
|
|
265
|
+
// `check` run never disagree about the current suppressions, and a
|
|
266
|
+
// profile-selected workspace resolves the same way `check` does.
|
|
267
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
268
|
+
return debtCommand(dir, commandContext, {
|
|
269
|
+
config,
|
|
270
|
+
events: options.events,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
@@ -46,9 +46,13 @@ import { computeDecisionFitness } from "../governance/decision-fitness.mjs";
|
|
|
46
46
|
import { stripAdrPrefix, stripRuleFitnessPrefix } from "../governance/adr-registry.mjs";
|
|
47
47
|
import { intentRows, configRows, rowLabel } from "./provenance-command.mjs";
|
|
48
48
|
import { evaluateFitness, fitnessSnapshot } from "../governance/fitness-registry.mjs";
|
|
49
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
50
|
+
import { driftForCheck } from "./drift.mjs";
|
|
49
51
|
import { hasTag, isComboDepConstraint } from "../rules/tags.mjs";
|
|
50
52
|
import { resolveMembers } from "../architecture-intent/selectors.mjs";
|
|
53
|
+
import { loadIntentIfTracked } from "../architecture-intent/model.mjs";
|
|
51
54
|
import { evaluate } from "../rules/index.mjs";
|
|
55
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
58
|
* The projects whose tags satisfy a constraint row's source selector — the
|
|
@@ -119,12 +123,33 @@ function intentRowGoverns(intent, graph, row) {
|
|
|
119
123
|
*
|
|
120
124
|
* @param {object} commandContext From `resolveCommandContext`.
|
|
121
125
|
* @param {object} config The resolved boundary law.
|
|
122
|
-
* @
|
|
123
|
-
* workspace declares none.
|
|
124
|
-
* @returns {object[]} `[{name, verdict}, ...]`.
|
|
126
|
+
* @returns {Promise<object[]>} `[{name, verdict}, ...]`.
|
|
125
127
|
*/
|
|
126
|
-
function fitnessVerdictsFor(commandContext, config
|
|
128
|
+
async function fitnessVerdictsFor(commandContext, config) {
|
|
127
129
|
if (!Array.isArray(config?.fitness) || config.fitness.length === 0) return [];
|
|
130
|
+
// A `drift-free` gate judges the SAME verdict-shaped intent the `fitness`
|
|
131
|
+
// command feeds its registry — `driftForCheck`'s, not the raw normalized
|
|
132
|
+
// model the walk's intent rows are built from: that model carries no
|
|
133
|
+
// `.verdict`, and a gate dispatched on it read as a fail over a clean tree
|
|
134
|
+
// (#737). The construction mirrors `./fitness.mjs`'s `fitnessCommand` —
|
|
135
|
+
// the same `drift.intent === undefined` → `null` resolution — so the two
|
|
136
|
+
// faces derive identical rows from identical facts.
|
|
137
|
+
const drift = await driftForCheck(commandContext);
|
|
138
|
+
const intent =
|
|
139
|
+
drift.intent === undefined
|
|
140
|
+
? null
|
|
141
|
+
: {
|
|
142
|
+
verdict:
|
|
143
|
+
drift.findings.length > 0
|
|
144
|
+
? "findings"
|
|
145
|
+
: drift.unresolved.length > 0
|
|
146
|
+
? "no-verdict"
|
|
147
|
+
: "ok",
|
|
148
|
+
boundaries: drift.boundaries,
|
|
149
|
+
findings: drift.findings,
|
|
150
|
+
unresolved: drift.unresolved,
|
|
151
|
+
notes: drift.notes,
|
|
152
|
+
};
|
|
128
153
|
const snapshot = fitnessSnapshot(commandContext, {
|
|
129
154
|
intent,
|
|
130
155
|
suppressions: config.suppressions ?? [],
|
|
@@ -145,12 +170,14 @@ function fitnessVerdictsFor(commandContext, config, intent) {
|
|
|
145
170
|
* @param {{intent?: object|null, fitnessVerdicts?: object[]}} [io]
|
|
146
171
|
* `intent` is the normalized intent model (or null); `fitnessVerdicts`
|
|
147
172
|
* overrides the run's own evaluation — a test supplies a fixed list.
|
|
148
|
-
* @returns {{status: "ok"|"no-verdict", result: object, coverage: object,
|
|
149
|
-
* report: {text: string, json: string}}}
|
|
150
|
-
* @throws {Error} on an unreadable registry, a malformed law,
|
|
151
|
-
*
|
|
173
|
+
* @returns {Promise<{status: "ok"|"no-verdict", result: object, coverage: object,
|
|
174
|
+
* report: {text: string, json: string}}>}
|
|
175
|
+
* @throws {Error} on an unreadable registry, a malformed law, a
|
|
176
|
+
* `driftForCheck` refusal on the fitness leg's intent (an unreadable or
|
|
177
|
+
* invalid `architecture-intent.json`, an unregistered-plugin graph), or a
|
|
178
|
+
* config declaring fitness that fails to evaluate — exit-3 class.
|
|
152
179
|
*/
|
|
153
|
-
export function decisionsCommand(decisionId, commandContext, config, io = {}) {
|
|
180
|
+
export async function decisionsCommand(decisionId, commandContext, config, io = {}) {
|
|
154
181
|
const intent = io.intent ?? null;
|
|
155
182
|
|
|
156
183
|
// The registry read — throws on an unreadable `docs/adr/`, which the caller
|
|
@@ -225,7 +252,7 @@ export function decisionsCommand(decisionId, commandContext, config, io = {}) {
|
|
|
225
252
|
// the `{name, verdict}` list from the declared fitness gates above (or the
|
|
226
253
|
// caller's override). `computeDecisionFitness`'s second argument carries
|
|
227
254
|
// verdicts but is unused; the lookup is the single door, so it is null.
|
|
228
|
-
const verdicts = io.fitnessVerdicts ?? fitnessVerdictsFor(commandContext, config
|
|
255
|
+
const verdicts = io.fitnessVerdicts ?? (await fitnessVerdictsFor(commandContext, config));
|
|
229
256
|
const verdictByName = new Map(verdicts.map((v) => [v.name, v]));
|
|
230
257
|
const fitnessLookup = (bindingId) => verdictByName.get(stripRuleFitnessPrefix(bindingId));
|
|
231
258
|
const fitnessById = new Map(
|
|
@@ -289,3 +316,25 @@ export function decisionsCommand(decisionId, commandContext, config, io = {}) {
|
|
|
289
316
|
},
|
|
290
317
|
};
|
|
291
318
|
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* `decisions` as the CLI drives it: the shared preamble — command context,
|
|
322
|
+
* the boundary law, the tracked intent — resolved here so `../../cli.mjs`'s
|
|
323
|
+
* driver only wires options, IO seams, and where output lands
|
|
324
|
+
* (`./README.md`). The engine this returns from is `decisionsCommand`
|
|
325
|
+
* above, unchanged.
|
|
326
|
+
*
|
|
327
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
328
|
+
* flags; `paths[0]` is the ADR id.
|
|
329
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
330
|
+
* seams a test injects, the same ones `check` takes.
|
|
331
|
+
* @returns {Promise<object>} `decisionsCommand`'s result, unmodified.
|
|
332
|
+
*/
|
|
333
|
+
export async function decisions(options, { cwd, readGraph, listFiles }) {
|
|
334
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
335
|
+
// ONE law for the chain, resolved exactly like `report` — the Fitness leg
|
|
336
|
+
// reads this law's declared gates, so a `--config` override must reach it.
|
|
337
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
338
|
+
const intent = await loadIntentIfTracked(commandContext.root, commandContext.tracked);
|
|
339
|
+
return decisionsCommand(options.paths[0], commandContext, config, { intent });
|
|
340
|
+
}
|
|
@@ -274,7 +274,9 @@ export function buildEvidenceSnapshot({
|
|
|
274
274
|
* order is the only order the format keeps, and `buildEvidenceSnapshot` sorts
|
|
275
275
|
* every array whose source does not guarantee it. Two captures over one
|
|
276
276
|
* unchanged tree produce byte-identical files, which is what makes a plain
|
|
277
|
-
* `diff` of two baselines meaningful.
|
|
277
|
+
* `diff` of two baselines meaningful. That determinism is also why this
|
|
278
|
+
* family carries no identity field, by decision: the bytes are the identity
|
|
279
|
+
* (`../../../../docs/adr/0008-snapshot-identity-per-family.md`).
|
|
278
280
|
*
|
|
279
281
|
* @param {object} snapshot From `buildEvidenceSnapshot`.
|
|
280
282
|
* @returns {string} The JSON text, newline-terminated.
|
|
@@ -330,7 +332,11 @@ export function readEvidenceSnapshot(path, io = {}) {
|
|
|
330
332
|
* What is deliberately NOT a refusal: dirty base provenance. A baseline from
|
|
331
333
|
* an uncommitted tree is weaker evidence, not unreadable evidence — the parsed
|
|
332
334
|
* snapshot exposes `provenance.dirty` so the renderer can say so loudly, and
|
|
333
|
-
* classification itself proceeds.
|
|
335
|
+
* classification itself proceeds. A dirty BIT that is not a boolean is a
|
|
336
|
+
* different thing — malformed, not weaker: the gates read `dirty === true`
|
|
337
|
+
* while the renderers interpolate truthiness, so any other type would read
|
|
338
|
+
* clean to some consumers and dirty to others. The provenance block below
|
|
339
|
+
* refuses it by name.
|
|
334
340
|
*
|
|
335
341
|
* @param {string} text The file contents.
|
|
336
342
|
* @param {string} path The path the text came from, for error messages.
|
|
@@ -392,8 +398,23 @@ export function parseEvidenceSnapshot(text, path) {
|
|
|
392
398
|
|
|
393
399
|
if (parsed.provenance !== null && !isPlainObject(parsed.provenance)) {
|
|
394
400
|
problems.push("provenance: must be an object ({commit, remote, dirty}) or null");
|
|
395
|
-
} else if (isPlainObject(parsed.provenance)
|
|
396
|
-
|
|
401
|
+
} else if (isPlainObject(parsed.provenance)) {
|
|
402
|
+
if (typeof parsed.provenance.commit !== "string") {
|
|
403
|
+
problems.push("provenance.commit: must be a string when provenance is present");
|
|
404
|
+
}
|
|
405
|
+
if (parsed.provenance.remote !== null && typeof parsed.provenance.remote !== "string") {
|
|
406
|
+
problems.push("provenance.remote: must be a string or null when provenance is present");
|
|
407
|
+
}
|
|
408
|
+
if (typeof parsed.provenance.dirty !== "boolean") {
|
|
409
|
+
// Malformed, not weaker: the gates read `dirty === true` while the
|
|
410
|
+
// renderers interpolate truthiness, so a value of any other type reads
|
|
411
|
+
// clean to some consumers and dirty to others — the one disagreement
|
|
412
|
+
// this record must never carry.
|
|
413
|
+
problems.push(
|
|
414
|
+
"provenance.dirty: must be a boolean when provenance is present — the gates decide " +
|
|
415
|
+
"between committed and uncommitted evidence on this bit",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
397
418
|
}
|
|
398
419
|
|
|
399
420
|
if (typeof parsed.policyFingerprint !== "string" || parsed.policyFingerprint === "") {
|