@ecoma-io/archkeep 0.25.0 → 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 +247 -34
- 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.mjs +263 -49
- package/src/commands/diff.mjs +45 -0
- 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 +85 -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/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/go-work.mjs +8 -23
- package/src/governance/evolution-event.mjs +15 -0
- package/src/governance/evolution-store.mjs +59 -31
- package/src/governance/provenance-record.mjs +0 -152
- package/src/governance/verdict.mjs +26 -3
- package/src/intent/intent-manifest.json +14 -14
- 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 +3 -2
- 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/verdict.mjs +127 -16
package/src/commands/diff.mjs
CHANGED
|
@@ -42,12 +42,15 @@ import { readFileSync } from "node:fs";
|
|
|
42
42
|
|
|
43
43
|
import { buildDependencies, buildProjects, computePolicyFingerprint } from "./graph.mjs";
|
|
44
44
|
import { computeRuleImpact } from "../rules/edge-constraints.mjs";
|
|
45
|
+
import { isAbsolute, resolve } from "node:path";
|
|
45
46
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
46
47
|
import { SCHEMA_VERSION } from "../report/json.mjs";
|
|
47
48
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
48
49
|
import { formatDiffReport } from "../report/diff-text.mjs";
|
|
49
50
|
import { compareSnapshotMetadata, dirtyBaselineNote, dirtyHeadNote } from "./snapshot-meta.mjs";
|
|
50
51
|
import { resolveProvenance } from "./provenance.mjs";
|
|
52
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
53
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
51
54
|
|
|
52
55
|
/**
|
|
53
56
|
* Reads and validates a baseline snapshot from `path`.
|
|
@@ -236,6 +239,20 @@ function buildHeadSnapshot(commandContext) {
|
|
|
236
239
|
* consumer wants to see: it is a real architectural event, not an
|
|
237
240
|
* implementation detail.
|
|
238
241
|
*
|
|
242
|
+
* This key is in-memory arithmetic and nothing else: `Map`/`Set` keys inside
|
|
243
|
+
* one run, never persisted and never emitted — only counts derived from
|
|
244
|
+
* those sets reach an envelope. The STORED spelling of edge identity is
|
|
245
|
+
* `edgeEvolutionIdentity` (`../governance/evolution-event.mjs`), the escaped
|
|
246
|
+
* `source>target:type` string evolution events carry, and the two spellings
|
|
247
|
+
* are deliberately not unified — each medium keeps exactly one identity
|
|
248
|
+
* constructor (`../../../../docs/adr/0008-snapshot-identity-per-family.md`;
|
|
249
|
+
* INV-6 in `../../../../docs/architecture/refactor/INVARIANTS.md`). An edge
|
|
250
|
+
* crosses from a structural diff into an event as its raw
|
|
251
|
+
* `{source, target, type}` triple, mapped through that function at the
|
|
252
|
+
* command boundary; a `\0`-joined key written into an event record would
|
|
253
|
+
* give one edge two spellings inside the store, and anything comparing
|
|
254
|
+
* identity strings across events would read them as different boundaries.
|
|
255
|
+
*
|
|
239
256
|
* @param {{source: string, target: string, type: string}} edge
|
|
240
257
|
* @returns {string}
|
|
241
258
|
*/
|
|
@@ -558,3 +575,31 @@ export function diffCommand(
|
|
|
558
575
|
},
|
|
559
576
|
};
|
|
560
577
|
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* `diff` as the CLI drives it: the baseline path resolved from the single
|
|
581
|
+
* positional argument, then the shared preamble — command context, boundary
|
|
582
|
+
* law — so `../../cli.mjs`'s driver only wires options, IO seams, and where
|
|
583
|
+
* output lands (`./README.md`). The engine this returns from is
|
|
584
|
+
* `diffCommand` above, unchanged.
|
|
585
|
+
*
|
|
586
|
+
* @param {{format: string, output: string|null, config: string|null, paths: string[]}} options
|
|
587
|
+
* This run's parsed flags; `paths[0]` is the baseline file.
|
|
588
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
589
|
+
* seams a test injects, the same ones `check` takes.
|
|
590
|
+
* @returns {Promise<object>} `diffCommand`'s result, unmodified.
|
|
591
|
+
*/
|
|
592
|
+
export async function diff(options, { cwd, readGraph, listFiles }) {
|
|
593
|
+
const baselinePath = isAbsolute(options.paths[0])
|
|
594
|
+
? options.paths[0]
|
|
595
|
+
: resolve(cwd, options.paths[0]);
|
|
596
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
597
|
+
// Load the boundary config when --config is given or when the workspace
|
|
598
|
+
// declares one, so rule-impact analysis is computed. Without a config,
|
|
599
|
+
// the diff reports only structural changes — same as before. A
|
|
600
|
+
// profile-selected workspace resolves the same way `check` does
|
|
601
|
+
// (`resolvePolicy`), so a policy edit under an unchanged profile NAME is
|
|
602
|
+
// still visible as a fingerprint change here.
|
|
603
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
604
|
+
return diffCommand(baselinePath, commandContext, { config });
|
|
605
|
+
}
|
|
@@ -122,6 +122,43 @@ export function proposalToIntent(proposal) {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Serialize a proposal into the exact bytes `--write-intent` plants at the
|
|
127
|
+
* target path — `proposalToIntent` rendered as stable, two-space JSON with a
|
|
128
|
+
* trailing newline, so the file a human reviews as a diff is the file
|
|
129
|
+
* `check` would read. Owned here, beside the conversion it renders, so the
|
|
130
|
+
* CLI's write door can never re-derive a second spelling of it.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} proposal The proposal from `discoverCommand`'s result.
|
|
133
|
+
* @returns {string} The serialized intent document.
|
|
134
|
+
*/
|
|
135
|
+
export function intentJsonFromProposal(proposal) {
|
|
136
|
+
return JSON.stringify(proposalToIntent(proposal), null, 2) + "\n";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The `--write-intent` self-footgun refusal, decided beside the proposal it
|
|
141
|
+
* protects. A proposal is a suggestion; a file already at the target is a law
|
|
142
|
+
* (or a candidate someone holds), and silently overwriting it with a proposal
|
|
143
|
+
* is the adoption this command must never perform by itself — the same
|
|
144
|
+
* posture `--output`'s fixed-name table holds at the write door, specialized
|
|
145
|
+
* to the one target `discover` itself names. The CLI keeps the mechanics
|
|
146
|
+
* (the `wx` write, the stderr wording around it); the DECISION to refuse
|
|
147
|
+
* travels with the verb, the way `historyOutputRefusal` does for history.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} target The `--write-intent` path as the user wrote it.
|
|
150
|
+
* @param {{exists: (path: string) => boolean}} io Injectable existence read —
|
|
151
|
+
* the same seam style `loadNativeModel`'s `{readFile}` keeps.
|
|
152
|
+
* @returns {string|null} The refusal message, or `null` when the write may
|
|
153
|
+
* proceed.
|
|
154
|
+
*/
|
|
155
|
+
export function intentWriteRefusal(target, { exists }) {
|
|
156
|
+
return exists(target)
|
|
157
|
+
? `archkeep: ${target} already exists, and a proposal must never ` +
|
|
158
|
+
`silently replace what is there. Move or delete the file first, then run this again.`
|
|
159
|
+
: null;
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
/**
|
|
126
163
|
* Runs the `discover` command: observes the workspace, optionally proposes the
|
|
127
164
|
* candidate architecture over it, and returns the report.
|
package/src/commands/drift.mjs
CHANGED
|
@@ -77,6 +77,8 @@
|
|
|
77
77
|
* intent produce byte-identical text and JSON.
|
|
78
78
|
*/
|
|
79
79
|
import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
80
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
81
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
80
82
|
import { buildDependencies, buildProjects } from "./graph.mjs";
|
|
81
83
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
82
84
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
@@ -471,3 +473,46 @@ export async function driftCommand(commandContext, io = {}) {
|
|
|
471
473
|
},
|
|
472
474
|
};
|
|
473
475
|
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* `drift` as the CLI drives it: the shared preamble — command context, then
|
|
479
|
+
* the boundary law — resolved here so `../../cli.mjs`'s driver only wires
|
|
480
|
+
* options, IO seams, and where output lands (`./README.md`). The engine this
|
|
481
|
+
* returns from is `driftCommand` above, unchanged.
|
|
482
|
+
*
|
|
483
|
+
* The loaded policy — profile-aware the same way `check` is
|
|
484
|
+
* (`resolvePolicy`), `null` when the workspace declares none. Drift reads
|
|
485
|
+
* the intent's rows, and the fitness half of a row's `decisionRef` resolves
|
|
486
|
+
* against the ids THIS policy declares (F04), so the same policy that made
|
|
487
|
+
* the boundary law answerable to the model must answer here. `drift` has no
|
|
488
|
+
* `--config` (`DRIFT_FLAG_HELP`), so `config` is always the workspace's own
|
|
489
|
+
* default — resolvePolicy reads `options.config` as the override, hence
|
|
490
|
+
* `null` here, which selects the workspace's configured boundary law (or a
|
|
491
|
+
* profile, when one is registered).
|
|
492
|
+
*
|
|
493
|
+
* The failure is DEFERRED rather than thrown here. `drift`'s only reader of
|
|
494
|
+
* this policy is the non-verdict decisionRef axis, and only for rows that
|
|
495
|
+
* carry one, so a workspace with an intent and no boundary config was
|
|
496
|
+
* exiting 3 over a law drift would never have opened — a fifth refusal
|
|
497
|
+
* neither `docs/usage/drift.md` nor `reconcile`, which makes the same four,
|
|
498
|
+
* ever had. `driftCommand` rethrows it, unchanged, at the one site that
|
|
499
|
+
* reads the policy, so every workspace whose intent cites anything keeps the
|
|
500
|
+
* exact exit-3 it had.
|
|
501
|
+
*
|
|
502
|
+
* @param {{format: string, output: string|null, paths: string[]}} options
|
|
503
|
+
* This run's parsed flags.
|
|
504
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
505
|
+
* seams a test injects, the same ones `check` takes.
|
|
506
|
+
* @returns {Promise<object>} `driftCommand`'s result, unmodified.
|
|
507
|
+
*/
|
|
508
|
+
export async function drift(options, { cwd, readGraph, listFiles }) {
|
|
509
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
510
|
+
let config = null;
|
|
511
|
+
let configError = null;
|
|
512
|
+
try {
|
|
513
|
+
({ config } = await resolvePolicy({ ...options, config: null }, commandContext, cwd));
|
|
514
|
+
} catch (error) {
|
|
515
|
+
configError = /** @type {Error} */ (error);
|
|
516
|
+
}
|
|
517
|
+
return driftCommand(commandContext, { config, configError });
|
|
518
|
+
}
|
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
classifyEvolution,
|
|
83
83
|
edgeEvolutionIdentity,
|
|
84
84
|
eventDedupeKey,
|
|
85
|
+
EVENT_DISPOSITIONS,
|
|
85
86
|
eventId,
|
|
86
87
|
EVOLUTION_EVENT_SCHEMA_VERSION,
|
|
87
88
|
} from "../governance/evolution-event.mjs";
|
|
@@ -838,14 +839,33 @@ function buildTransitionEvent(from, to, transition, comparison) {
|
|
|
838
839
|
* axis at any transition is surfaced as `{available: false, reason}` naming
|
|
839
840
|
* the transition index, never folded into a fabricated clean aggregate.
|
|
840
841
|
*
|
|
842
|
+
* The disposition loop latches the event vocabulary: a comparison whose
|
|
843
|
+
* disposition is unknown or absent THROWS naming it — the `?? 1` it replaced
|
|
844
|
+
* defaulted a stranger to rank 1, accepted's own rank, so a garbled
|
|
845
|
+
* disposition could never be the summary's worst. Production dispositions
|
|
846
|
+
* arrive validated (`classifyEvolution`, `readEvents`), so the throw fires
|
|
847
|
+
* only where archkeep itself is buggy — the same latch `deltaDisposition`
|
|
848
|
+
* holds.
|
|
849
|
+
*
|
|
841
850
|
* @param {object[]} comparisons The per-transition comparison objects.
|
|
842
851
|
* @returns {object} The summary.
|
|
852
|
+
* @throws {Error} On a comparison whose `disposition` is outside
|
|
853
|
+
* `EVENT_DISPOSITIONS`, naming the value.
|
|
843
854
|
*/
|
|
844
|
-
function buildEvolutionSummary(comparisons) {
|
|
855
|
+
export function buildEvolutionSummary(comparisons) {
|
|
845
856
|
const dispositionRank = { accepted: 1, rejected: 2, "no-verdict": 3 };
|
|
846
857
|
let disposition = "accepted";
|
|
847
858
|
for (const comparison of comparisons) {
|
|
848
|
-
if ((
|
|
859
|
+
if (!EVENT_DISPOSITIONS.includes(comparison.disposition)) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`the evolution summary folds dispositions by rank, and ${JSON.stringify(
|
|
862
|
+
comparison.disposition,
|
|
863
|
+
)} is not one of [${EVENT_DISPOSITIONS.join(", ")}] — an unknown disposition would ` +
|
|
864
|
+
`default to rank 1, accepted's own rank, and could never be the summary's worst. ` +
|
|
865
|
+
`This is a bug in archkeep, not a fact about the workspace.`,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (dispositionRank[comparison.disposition] > dispositionRank[disposition]) {
|
|
849
869
|
disposition = comparison.disposition;
|
|
850
870
|
}
|
|
851
871
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `explain` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `explain`.
|
|
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 { explain } from "./explain.mjs";
|
package/src/commands/explain.mjs
CHANGED
|
@@ -86,8 +86,10 @@ import { findConstraintsFor } from "../rules/tags.mjs";
|
|
|
86
86
|
import { findProjectForPath, createProjectRootMappings } from "../rules/specifiers.mjs";
|
|
87
87
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
88
88
|
import { coverageVerdict } from "./coverage-verdict.mjs";
|
|
89
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
89
90
|
import { formatExplainReport } from "../report/explain-text.mjs";
|
|
90
91
|
import { resolveProvenance } from "./provenance.mjs";
|
|
92
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
91
93
|
import { readAdrContext } from "./adr.mjs";
|
|
92
94
|
import { lineage } from "../governance/decision-graph.mjs";
|
|
93
95
|
import { unresolvedDecisionRefNote } from "./provenance-command.mjs";
|
|
@@ -613,3 +615,25 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
613
615
|
},
|
|
614
616
|
};
|
|
615
617
|
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* `explain` as the CLI drives it: the shared preamble — command context,
|
|
621
|
+
* then the boundary law — resolved here so `../../cli.mjs`'s driver only
|
|
622
|
+
* wires options, IO seams, and where output lands (`./README.md`). The
|
|
623
|
+
* engine this returns from is `explainCommand` above, unchanged.
|
|
624
|
+
*
|
|
625
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
626
|
+
* flags; `paths[0]` is the site, a `file:line:column` string.
|
|
627
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
628
|
+
* seams a test injects, the same ones `check` takes.
|
|
629
|
+
* @returns {Promise<object>} `explainCommand`'s result, unmodified.
|
|
630
|
+
*/
|
|
631
|
+
export async function explain(options, { cwd, readGraph, listFiles }) {
|
|
632
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
633
|
+
// The config's location is a separate fact from the workspace root.
|
|
634
|
+
// Same loading logic as `check` (`resolvePolicy`) — a `--config`
|
|
635
|
+
// overrides the workspace's own `boundaryConfig`, profile-aware the same
|
|
636
|
+
// way `check` is.
|
|
637
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
638
|
+
return explainCommand(options.paths[0], commandContext, config);
|
|
639
|
+
}
|
package/src/commands/fitness.mjs
CHANGED
|
@@ -49,13 +49,18 @@ import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
|
49
49
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
50
50
|
import { formatFitnessSection } from "../report/text.mjs";
|
|
51
51
|
import { resolveProvenance } from "./provenance.mjs";
|
|
52
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
52
53
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
53
54
|
import { driftForCheck } from "./drift.mjs";
|
|
55
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
54
56
|
import {
|
|
55
57
|
evaluateFitness,
|
|
56
58
|
fitnessSnapshot,
|
|
57
59
|
fitnessVerdictFor,
|
|
58
60
|
} from "../governance/fitness-registry.mjs";
|
|
61
|
+
import { VERDICTS, isVerdict } from "../governance/verdict.mjs";
|
|
62
|
+
import { buildDecision } from "../report/evidence.mjs";
|
|
63
|
+
import { describe } from "../values.mjs";
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
66
|
* The `fitness` command's own text report: the same verdict table `check`'s
|
|
@@ -104,6 +109,55 @@ export function declaresFitness(config) {
|
|
|
104
109
|
return config !== null && config !== undefined && config.fitness !== undefined;
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The `fitness` exit fold, lifted out of `fitnessCommand` as a pure function
|
|
114
|
+
* so the mapping from the overall verdict to the status is a fact a test can
|
|
115
|
+
* pin (`./change.mjs`'s `reconcileDisposition` is the arrangement's
|
|
116
|
+
* precedent). D-09, as below: `fail` → `findings`/1, `unknown` →
|
|
117
|
+
* `no-verdict`/3, `pass` and an all-`not_applicable` run alike → `ok`/0.
|
|
118
|
+
*
|
|
119
|
+
* The input latch runs first: `overall.verdict` is the handshake with
|
|
120
|
+
* `fitnessVerdictFor`, and a value the fold does not recognize matches
|
|
121
|
+
* neither the `fail` nor the `unknown` arm — it would fall to `ok`, a clean
|
|
122
|
+
* exit over a run the fold could not read. A verdict outside the four states
|
|
123
|
+
* is refused as a no-verdict whose single reason names it (`refused`), never
|
|
124
|
+
* folded past. The literals stay hand-rolled per site by decision — the
|
|
125
|
+
* carrier folds are the pinned baseline INV-2 names, not a table to be
|
|
126
|
+
* converged (`docs/architecture/refactor/AUTHORITY-MAP.md`).
|
|
127
|
+
*
|
|
128
|
+
* `overall.decisions` is validated for the same reason the delta fold
|
|
129
|
+
* validates buckets the fold itself does not count first: `result.functions`
|
|
130
|
+
* and the text table render them downstream of this fold, so a malformed one
|
|
131
|
+
* is refused while there is still a verdict to withhold.
|
|
132
|
+
*
|
|
133
|
+
* @param {{verdict: string, decisions: object[]}} overall From
|
|
134
|
+
* `fitnessVerdictFor`.
|
|
135
|
+
* @returns {{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3,
|
|
136
|
+
* refused?: undefined}|
|
|
137
|
+
* {status: "no-verdict", exitCode: 3, refused: string}}
|
|
138
|
+
*/
|
|
139
|
+
export function fitnessFold(overall) {
|
|
140
|
+
if (!isVerdict(overall?.verdict)) {
|
|
141
|
+
return {
|
|
142
|
+
status: "no-verdict",
|
|
143
|
+
exitCode: 3,
|
|
144
|
+
refused: `the fitness fold maps "overall.verdict", which is ${describe(overall?.verdict)} — a verdict outside ${VERDICTS.join(", ")} matches neither the fail nor the unknown lane, and reads "ok". This is a bug in archkeep, not a fact about the workspace.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(overall?.decisions)) {
|
|
148
|
+
return {
|
|
149
|
+
status: "no-verdict",
|
|
150
|
+
exitCode: 3,
|
|
151
|
+
refused: `the fitness fold reads "overall.decisions", which is ${describe(overall?.decisions)} — the verdict table the run reports is built from it below, and a list that is not an array renders as an empty table. This is a bug in archkeep, not a fact about the workspace.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return overall.verdict === "fail"
|
|
155
|
+
? { status: "findings", exitCode: 1 }
|
|
156
|
+
: overall.verdict === "unknown"
|
|
157
|
+
? { status: "no-verdict", exitCode: 3 }
|
|
158
|
+
: { status: "ok", exitCode: 0 };
|
|
159
|
+
}
|
|
160
|
+
|
|
107
161
|
/**
|
|
108
162
|
* Runs the `fitness` command: loads the boundary policy, evaluates every
|
|
109
163
|
* declared function against the workspace's facts, and renders the verdict
|
|
@@ -115,8 +169,8 @@ export function declaresFitness(config) {
|
|
|
115
169
|
*
|
|
116
170
|
* @param {object} commandContext From `resolveCommandContext`.
|
|
117
171
|
* @param {{config?: object|null}} [io] The loaded policy, injectable for tests.
|
|
118
|
-
* @returns {Promise<{status: "ok"|"findings"|"no-verdict",
|
|
119
|
-
* coverage: object, report: {text: string, json: string}}>}
|
|
172
|
+
* @returns {Promise<{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3,
|
|
173
|
+
* fitness?: object, coverage: object, report: {text: string, json: string}}>}
|
|
120
174
|
* `status: "no-verdict"` from the coverage refusal carries no `fitness`
|
|
121
175
|
* payload — the verdict was withheld, and the envelope's `coverage` block is
|
|
122
176
|
* the whole answer (#608).
|
|
@@ -207,13 +261,40 @@ export async function fitnessCommand(commandContext, io = {}) {
|
|
|
207
261
|
// every function is `not_applicable` are both `ok`: nothing failed and
|
|
208
262
|
// nothing stayed undetermined. The status↔exitCode pair is asserted by
|
|
209
263
|
// `jsonEnvelope` (3-on-no-verdict), so a wrong mapping here cannot ship.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
264
|
+
// `fitnessFold` above owns the mapping and its input latch; a refusal
|
|
265
|
+
// withholds the verdict table exactly the coverage refusal does, and the
|
|
266
|
+
// decision it carries names the input the fold could not read.
|
|
267
|
+
const fold = fitnessFold(overall);
|
|
268
|
+
if (fold.refused !== undefined) {
|
|
269
|
+
return {
|
|
270
|
+
status: fold.status,
|
|
271
|
+
exitCode: fold.exitCode,
|
|
272
|
+
coverage: { ...coverage, notes: [...coverage.notes, fold.refused] },
|
|
273
|
+
report: {
|
|
274
|
+
text: `fitness: no verdict — ${fold.refused}\n`,
|
|
275
|
+
json: renderJson(
|
|
276
|
+
jsonEnvelope({
|
|
277
|
+
command: "fitness",
|
|
278
|
+
context,
|
|
279
|
+
status: fold.status,
|
|
280
|
+
exitCode: fold.exitCode,
|
|
281
|
+
coverage: { ...coverage, notes: [...coverage.notes, fold.refused] },
|
|
282
|
+
// The refusal-withheld payload, stated the way `coverageRefusal`
|
|
283
|
+
// states it: `jsonEnvelope` requires the key, the refusal has no
|
|
284
|
+
// result to report.
|
|
285
|
+
result: undefined,
|
|
286
|
+
decision: buildDecision({
|
|
287
|
+
status: fold.status,
|
|
288
|
+
coverageComplete: true,
|
|
289
|
+
findings: 0,
|
|
290
|
+
reason: fold.refused,
|
|
291
|
+
}),
|
|
292
|
+
}),
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const { status, exitCode } = fold;
|
|
217
298
|
const result = { verdict: overall.verdict, functions: decisions };
|
|
218
299
|
|
|
219
300
|
const report = {
|
|
@@ -223,5 +304,33 @@ export async function fitnessCommand(commandContext, io = {}) {
|
|
|
223
304
|
),
|
|
224
305
|
};
|
|
225
306
|
|
|
226
|
-
return { status, fitness: result, coverage, report };
|
|
307
|
+
return { status, exitCode, fitness: result, coverage, report };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* `fitness` as the CLI drives it: the shared preamble — command context,
|
|
312
|
+
* then the boundary law — resolved here so `../../cli.mjs`'s driver only
|
|
313
|
+
* wires options, IO seams, and where output lands (`./README.md`). The
|
|
314
|
+
* engine this returns from is `fitnessCommand` above, unchanged.
|
|
315
|
+
*
|
|
316
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
317
|
+
* flags.
|
|
318
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
319
|
+
* seams a test injects, the same ones `check` takes.
|
|
320
|
+
* @returns {Promise<object>} `fitnessCommand`'s result, unmodified.
|
|
321
|
+
*/
|
|
322
|
+
export async function fitness(options, { cwd, readGraph, listFiles }) {
|
|
323
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
324
|
+
// Fitness is part of the run's boundary law, so the law is loaded the same
|
|
325
|
+
// way `check` loads it (`resolvePolicy`) and `--config` wins the same
|
|
326
|
+
// way — resolved against the working directory, never against this
|
|
327
|
+
// tool's own location, profile-aware the same way `check` is. A malformed
|
|
328
|
+
// law throws here, exit 3, exactly as in `check`. A profile's `block` may
|
|
329
|
+
// carry a `fitness` key (`docs/concepts/profiles.md` names four block
|
|
330
|
+
// keys, fitness among them), so a profile-selected workspace folds the
|
|
331
|
+
// declared functions the same way a file-selected one does — a profile
|
|
332
|
+
// that declares none reaches `fitnessCommand`'s own "declares no fitness
|
|
333
|
+
// functions" refusal rather than a config-loading failure.
|
|
334
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
335
|
+
return fitnessCommand(commandContext, { config });
|
|
227
336
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `govern` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `waivers`, `report`, `debt`, `provenance`, `decisions`, `adr`.
|
|
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 { waivers } from "./waivers.mjs";
|
|
12
|
+
export { report } from "./report.mjs";
|
|
13
|
+
export { debt } from "./debt.mjs";
|
|
14
|
+
export { provenanceCommand } from "./provenance-command.mjs";
|
|
15
|
+
export { decisions } from "./decisions.mjs";
|
|
16
|
+
export { adrCommand } from "./adr.mjs";
|
package/src/commands/graph.mjs
CHANGED
|
@@ -41,7 +41,9 @@ import { DEFAULT_WORKSPACE_LAYOUT } from "../rules/specifiers.mjs";
|
|
|
41
41
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
42
42
|
import { formatGraphReport } from "../report/graph-text.mjs";
|
|
43
43
|
import { coverageIncompleteReasons } from "../verdict.mjs";
|
|
44
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
44
45
|
import { coverageVerdict } from "./coverage-verdict.mjs";
|
|
46
|
+
import { resolveDescribedPolicy } from "./policy.mjs";
|
|
45
47
|
import { resolveProvenance } from "./provenance.mjs";
|
|
46
48
|
|
|
47
49
|
/**
|
|
@@ -342,3 +344,40 @@ export function graphCommand(commandContext, { config = null } = {}) {
|
|
|
342
344
|
},
|
|
343
345
|
};
|
|
344
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* `graph` as the CLI drives it: the shared preamble — command context, then
|
|
350
|
+
* the workspace's described policy — resolved here, so `../../cli.mjs`'s
|
|
351
|
+
* driver only wires options, IO seams, and where output lands
|
|
352
|
+
* (`./README.md`). The engine this returns from is `graphCommand` above,
|
|
353
|
+
* unchanged.
|
|
354
|
+
*
|
|
355
|
+
* The policy is DESCRIBED, not judged: `graph` describes the project graph,
|
|
356
|
+
* not the boundary law — it reads no constraint row and judges nothing
|
|
357
|
+
* against one — so a workspace that has not written a law yet must not be
|
|
358
|
+
* refused here. Every arm of that decision — what is skipped is the load of
|
|
359
|
+
* a file that is NOT THERE, the `boundaryConfigDeclared` bit that keeps the
|
|
360
|
+
* guard to the un-overridden default, and why a law someone named and then
|
|
361
|
+
* deleted stays loud — lives in `resolveDescribedPolicy`
|
|
362
|
+
* (`./policy.mjs`) rather than here, so the descriptive commands and the
|
|
363
|
+
* MCP face that serves them cannot disagree about what "no law declared"
|
|
364
|
+
* means.
|
|
365
|
+
*
|
|
366
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
367
|
+
* flags — `graph` has no `--config` flag, so `config` is always `null`
|
|
368
|
+
* and the workspace's own default is what resolves.
|
|
369
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
370
|
+
* seams a test injects, the same ones `check` takes.
|
|
371
|
+
* @returns {Promise<object>} `graphCommand`'s result, unmodified.
|
|
372
|
+
*/
|
|
373
|
+
export async function graph(options, { cwd, readGraph, listFiles }) {
|
|
374
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
375
|
+
// The fingerprint is why the config loads even though `graph` judges
|
|
376
|
+
// nothing: `diff` warns when the policy changed between runs, and a
|
|
377
|
+
// profile-selected workspace's `boundaryConfig` names a profile rather
|
|
378
|
+
// than a file, resolved the same way `check` resolves it
|
|
379
|
+
// (`resolveDescribedPolicy`), so the fingerprint moves with a profile edit
|
|
380
|
+
// the same way it already does with a file or inline-object edit.
|
|
381
|
+
const { config } = await resolveDescribedPolicy(options, commandContext, cwd);
|
|
382
|
+
return graphCommand(commandContext, { config });
|
|
383
|
+
}
|
package/src/commands/health.mjs
CHANGED
|
@@ -49,10 +49,12 @@ import {
|
|
|
49
49
|
unresolvableLiteralCount,
|
|
50
50
|
} from "../analysis/source-util.mjs";
|
|
51
51
|
import { buildDependencies, buildProjects } from "./graph.mjs";
|
|
52
|
+
import { isAbsolute, resolve } from "node:path";
|
|
52
53
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
53
54
|
import { formatHealthReport } from "../report/health-text.mjs";
|
|
54
55
|
import { resolveProvenance } from "./provenance.mjs";
|
|
55
56
|
import { readSnapshots } from "./history.mjs";
|
|
57
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
56
58
|
import {
|
|
57
59
|
boundaryMetrics,
|
|
58
60
|
couplingMetrics,
|
|
@@ -61,6 +63,8 @@ import {
|
|
|
61
63
|
structuralMetrics,
|
|
62
64
|
} from "../governance/metrics.mjs";
|
|
63
65
|
import { judgeIntent } from "../architecture-intent/judge.mjs";
|
|
66
|
+
import { loadIntentIfTracked } from "../architecture-intent/model.mjs";
|
|
67
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
70
|
* Computes the intent verdict the fitness metric reads — the same `judgeIntent`
|
|
@@ -222,3 +226,33 @@ export function healthCommand(commandContext, io = {}) {
|
|
|
222
226
|
},
|
|
223
227
|
};
|
|
224
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `health` as the CLI drives it: the shared preamble — command context, the
|
|
232
|
+
* boundary law, the tracked intent — resolved here, so `../../cli.mjs`'s
|
|
233
|
+
* driver only wires options, IO seams, and where output lands
|
|
234
|
+
* (`./README.md`). The engine this returns from is `healthCommand` above,
|
|
235
|
+
* unchanged.
|
|
236
|
+
*
|
|
237
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
238
|
+
* flags; at most one positional, the snapshot directory for trends.
|
|
239
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
240
|
+
* seams a test injects, the same ones `check` takes.
|
|
241
|
+
* @returns {Promise<object>} `healthCommand`'s result, unmodified.
|
|
242
|
+
*/
|
|
243
|
+
export async function health(options, { cwd, readGraph, listFiles }) {
|
|
244
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
245
|
+
// The boundary law and the intent, the same loading every command does
|
|
246
|
+
// (`resolvePolicy`) — a `--config` overrides the workspace's own
|
|
247
|
+
// `boundaryConfig`, profile-aware the same way `check` is, and the
|
|
248
|
+
// intent is the tracked root `architecture-intent.json` (or absent).
|
|
249
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
250
|
+
const intent = await loadIntentIfTracked(commandContext.root, commandContext.tracked);
|
|
251
|
+
const trendDir =
|
|
252
|
+
options.paths.length === 1
|
|
253
|
+
? isAbsolute(options.paths[0])
|
|
254
|
+
? options.paths[0]
|
|
255
|
+
: resolve(cwd, options.paths[0])
|
|
256
|
+
: null;
|
|
257
|
+
return healthCommand(commandContext, { config, intent, trendDir });
|
|
258
|
+
}
|
package/src/commands/history.mjs
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
*/
|
|
73
73
|
import { createHash } from "node:crypto";
|
|
74
74
|
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
75
|
-
import { basename, join, resolve } from "node:path";
|
|
75
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
76
76
|
|
|
77
77
|
import {
|
|
78
78
|
blindSpotRows,
|
|
@@ -85,9 +85,11 @@ import { classifyEvolution } from "../governance/evolution-event.mjs";
|
|
|
85
85
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
86
86
|
import { formatHistoryReport } from "../report/history-text.mjs";
|
|
87
87
|
import { computeDiff, parseBaseline } from "./diff.mjs";
|
|
88
|
-
import { buildDependencies, buildProjects } from "./graph.mjs";
|
|
88
|
+
import { buildDependencies, buildProjects, computePolicyFingerprint } from "./graph.mjs";
|
|
89
89
|
import { resolveProvenance } from "./provenance.mjs";
|
|
90
90
|
import { compareSnapshotMetadata } from "./snapshot-meta.mjs";
|
|
91
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
92
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
95
|
* A graph envelope's architecture identity: the part that determines whether
|
|
@@ -766,3 +768,84 @@ function envelopeToSnapshot(envelope, path, id) {
|
|
|
766
768
|
id,
|
|
767
769
|
};
|
|
768
770
|
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* The history directory, resolved once from the single positional argument —
|
|
774
|
+
* shared by the CLI entry and the output guard below.
|
|
775
|
+
*
|
|
776
|
+
* @param {{paths: string[]}} options This run's parsed flags.
|
|
777
|
+
* @param {string} cwd The run's working directory.
|
|
778
|
+
* @returns {string}
|
|
779
|
+
*/
|
|
780
|
+
function historyDirFrom(options, cwd) {
|
|
781
|
+
return isAbsolute(options.paths[0]) ? resolve(options.paths[0]) : resolve(cwd, options.paths[0]);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* `history`'s self-footgun guard: writing the history report back into the
|
|
786
|
+
* very directory `history` reads would poison every later run (the report
|
|
787
|
+
* envelope is a `history` envelope, which `parseBaseline` refuses as a
|
|
788
|
+
* non-`graph` snapshot). Declared by the command that owns the law and
|
|
789
|
+
* enforced by the driver's write door; `null` means no refusal.
|
|
790
|
+
*
|
|
791
|
+
* @param {{output: string|null, paths: string[]}} options This run's parsed
|
|
792
|
+
* flags.
|
|
793
|
+
* @param {string} cwd The run's working directory, for relative flag
|
|
794
|
+
* resolution.
|
|
795
|
+
* @returns {string|null} The refusal message, or `null` when the output is
|
|
796
|
+
* safe.
|
|
797
|
+
*/
|
|
798
|
+
export function historyOutputRefusal(options, cwd) {
|
|
799
|
+
if (!options.output) return null;
|
|
800
|
+
// `resolve()` on the absolute branch too — not just the raw path — the
|
|
801
|
+
// same normalization `writeOutputReport` applies, so an absolute
|
|
802
|
+
// `--output` carrying a `..` segment that resolves INTO the history
|
|
803
|
+
// directory cannot slip past this guard unnormalized.
|
|
804
|
+
const outputAbs = isAbsolute(options.output)
|
|
805
|
+
? resolve(options.output)
|
|
806
|
+
: resolve(cwd, options.output);
|
|
807
|
+
const dir = historyDirFrom(options, cwd);
|
|
808
|
+
if (dirname(outputAbs) === dir) {
|
|
809
|
+
return (
|
|
810
|
+
`archkeep: --output '${options.output}' is inside the history directory '${dir}' — ` +
|
|
811
|
+
`writing the report there would be read back as a snapshot on the next run. ` +
|
|
812
|
+
`Write it somewhere else.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* `history` as the CLI drives it: the history directory resolved from the
|
|
820
|
+
* single positional argument, then the shared preamble — command context,
|
|
821
|
+
* and under `--capture` the boundary law whose fingerprint the snapshot
|
|
822
|
+
* records — so `../../cli.mjs`'s driver only wires options, IO seams, and
|
|
823
|
+
* where output lands (`./README.md`). The engine this returns from is
|
|
824
|
+
* `historyCommand` above, unchanged.
|
|
825
|
+
*
|
|
826
|
+
* @param {{capture: boolean, config: string|null, paths: string[]}} options
|
|
827
|
+
* This run's parsed flags.
|
|
828
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
829
|
+
* seams a test injects, the same ones `check` takes.
|
|
830
|
+
* @returns {Promise<object>} `historyCommand`'s result, unmodified.
|
|
831
|
+
*/
|
|
832
|
+
export async function history(options, { cwd, readGraph, listFiles }) {
|
|
833
|
+
const dir = historyDirFrom(options, cwd);
|
|
834
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
835
|
+
// The boundary law's fingerprint when the workspace declares one, so a
|
|
836
|
+
// captured snapshot records the policy it was taken under — the same
|
|
837
|
+
// config loading `graph` uses (`resolvePolicy`, profile-aware the same
|
|
838
|
+
// way `check` is), kept in one place so a capture and a standalone
|
|
839
|
+
// `graph` never disagree about the current policy.
|
|
840
|
+
let fingerprint = null;
|
|
841
|
+
if (options.capture) {
|
|
842
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
843
|
+
if (config) {
|
|
844
|
+
fingerprint = computePolicyFingerprint(config);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return historyCommand(dir, commandContext, {
|
|
848
|
+
capture: options.capture,
|
|
849
|
+
policyFingerprint: fingerprint,
|
|
850
|
+
});
|
|
851
|
+
}
|