@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
package/commands.mjs
CHANGED
|
@@ -25,7 +25,12 @@
|
|
|
25
25
|
* one. What is deliberately absent: the `run*` drivers and argv parsing
|
|
26
26
|
* (`cli.mjs` alone owns what a process's stdout and exit code mean), and the
|
|
27
27
|
* renderers (`src/report/` shapes bytes for humans; an importer reads the
|
|
28
|
-
* envelopes the commands already return).
|
|
28
|
+
* envelopes the commands already return). `adrForWorkspace`
|
|
29
|
+
* (`./src/commands/adr-for-workspace.mjs`) is on the roster without bending
|
|
30
|
+
* that: it composes the workspace preamble two faces (`cli.mjs`'s `runAdr`
|
|
31
|
+
* and the MCP history adapter) used to duplicate — one of them past this
|
|
32
|
+
* subpath — and returns exactly what `adrCommand` returns, printing no byte
|
|
33
|
+
* and deciding no exit code of its own.
|
|
29
34
|
*
|
|
30
35
|
* The seams the CLI threads are threaded the same way here: `readGraph` and
|
|
31
36
|
* `listFiles` are injectable on every entry point that reaches outside the
|
|
@@ -45,6 +50,7 @@ export { driftCommand } from "./src/commands/drift.mjs";
|
|
|
45
50
|
export { planContextCommand } from "./src/commands/plan-context-command.mjs";
|
|
46
51
|
export { historyCommand } from "./src/commands/history.mjs";
|
|
47
52
|
export { adrCommand } from "./src/commands/adr.mjs";
|
|
53
|
+
export { adrForWorkspace } from "./src/commands/adr-for-workspace.mjs";
|
|
48
54
|
export { discoverCommand } from "./src/commands/discover.mjs";
|
|
49
55
|
export { reconcileCommand } from "./src/commands/reconcile.mjs";
|
|
50
56
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecoma-io/archkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Architecture authority for human and agentic software development — deterministic, evidence-backed enforcement of declared architecture.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"architecture",
|
|
@@ -34,7 +34,7 @@ import { buildReachability, getPath, pathExists } from "../../src/rules/reachabi
|
|
|
34
34
|
/**
|
|
35
35
|
* What a finding means — one entry per `messageId`, the arrangement
|
|
36
36
|
* `../../src/report/sarif.mjs` derives its rule descriptors from, the same as
|
|
37
|
-
*
|
|
37
|
+
* the per-domain tables in `../rules/messages.mjs`.
|
|
38
38
|
*/
|
|
39
39
|
export const INTENT_MESSAGES = Object.freeze({
|
|
40
40
|
intentForbiddenEdge:
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
* observed architecture stays the graph Archkeep derives from source; governance
|
|
12
12
|
* is a deterministic comparison (`./judge.mjs`). NO LLM/AI anywhere in the core.
|
|
13
13
|
*
|
|
14
|
+
* Which "intent" this is: one of four unrelated surfaces sharing the word —
|
|
15
|
+
* PD-13 outcome (c), four surfaces, never one type
|
|
16
|
+
* (`../../../../docs/architecture/refactor/DECISIONS.md`). This module is
|
|
17
|
+
* the workspace-declared law: the grammar, validation, and loading of
|
|
18
|
+
* `architecture-intent.json`, judged against the observed graph by `check`
|
|
19
|
+
* and `drift`. The other three: the v1.0 evidence-manifest registry
|
|
20
|
+
* (`../intent/` — `intent-manifest.json` and its executable gate), the
|
|
21
|
+
* declared-change grammar (`../commands/change-intent.mjs`, one change's
|
|
22
|
+
* expected consequences, verified by the `change` command), and the fileless
|
|
23
|
+
* run-envelope intent shape (`result.intent` in a `--format json` envelope).
|
|
24
|
+
*
|
|
14
25
|
* This module mirrors `../../config.mjs`'s and
|
|
15
26
|
* `../../providers/native/model.mjs`'s split: a pure `(raw) -> string[]`
|
|
16
27
|
* validator and a thin loader that reads, parses, validates and throws — one
|
|
@@ -696,3 +707,26 @@ export async function loadIntent(root, { read = readFileFromDisk, tracked } = {}
|
|
|
696
707
|
}
|
|
697
708
|
return normalizeIntent(raw);
|
|
698
709
|
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* The driver gate over `loadIntent`: load the workspace's declared law only
|
|
713
|
+
* when the tracked-file list names it, `null` otherwise.
|
|
714
|
+
*
|
|
715
|
+
* Three `cli.mjs` drivers (decisions, health, report) need the same answer:
|
|
716
|
+
* an intent file `git ls-files` does not list is absence — the callers render
|
|
717
|
+
* their no-intent face — while a tracked-but-unreadable file is a no-verdict
|
|
718
|
+
* throw that the shared catch folds to exit 3. The membership pre-test is a
|
|
719
|
+
* pure equivalence, not a containment guard: `loadIntent` early-returns for
|
|
720
|
+
* untracked input before any containment check, so this helper changes no
|
|
721
|
+
* verdict on any input.
|
|
722
|
+
*
|
|
723
|
+
* @param {string} root Absolute workspace root.
|
|
724
|
+
* @param {string[]} tracked The `git ls-files` list.
|
|
725
|
+
* @returns {Promise<object|null>} The normalized, validated model, or `null`
|
|
726
|
+
* when the file is not tracked.
|
|
727
|
+
* @throws {Error} exactly what `loadIntent` throws — tracked-but-unreadable
|
|
728
|
+
* stays a no-verdict, never absence.
|
|
729
|
+
*/
|
|
730
|
+
export async function loadIntentIfTracked(root, tracked) {
|
|
731
|
+
return tracked.includes(INTENT_FILE) ? loadIntent(root, { tracked }) : null;
|
|
732
|
+
}
|
package/src/canonical.mjs
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* correct.
|
|
12
12
|
*
|
|
13
13
|
* Used by `computePolicyFingerprint` (`../commands/graph.mjs`), the intent
|
|
14
|
-
* fingerprint (`./intent-fingerprint.mjs`), the
|
|
14
|
+
* fingerprint (`./architecture-intent/intent-fingerprint.mjs`), the
|
|
15
|
+
* evidence-snapshot serializer
|
|
15
16
|
* (`./commands/delta-snapshot.mjs`, through the exported replacer below), and
|
|
16
17
|
* anything else a fingerprint or a byte-deterministic file is produced from —
|
|
17
18
|
* one canonicalizer, in one place, so two serializations cannot drift.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `adr` preamble, composed once: the workspace root walked up from a
|
|
3
|
+
* working directory, the tracked file list, then `./adr.mjs`'s `adrCommand`.
|
|
4
|
+
* `../../cli.mjs`'s `runAdr` and the MCP history adapter
|
|
5
|
+
* (`../../../archkeep-mcp/src/engine.mjs`) each inlined these three steps,
|
|
6
|
+
* and the adapter's copy reached past the `./commands` subpath into the
|
|
7
|
+
* engine's root entry for `findWorkspaceRoot` and `listTrackedFiles` — the
|
|
8
|
+
* one import in that package that made it something other than a client of
|
|
9
|
+
* the command layer. The composition lives here, beside the command it
|
|
10
|
+
* feeds, so both faces run one path and neither needs the root entry for it.
|
|
11
|
+
*
|
|
12
|
+
* It holds no policy of its own. The two preamble decisions move verbatim,
|
|
13
|
+
* because inventing either here would be a second opinion about what a
|
|
14
|
+
* workspace is:
|
|
15
|
+
*
|
|
16
|
+
* - the root is `findWorkspaceRoot(cwd, WORKSPACE_MARKERS)` — the same
|
|
17
|
+
* marker list `./context.mjs`'s `resolveCommandContext` walks, whose own
|
|
18
|
+
* header owns why callers of a workspace root may not differ in what a
|
|
19
|
+
* workspace root IS;
|
|
20
|
+
* - the files are `(io.listFiles ?? listTrackedFiles)(root)` — the injectable
|
|
21
|
+
* default every command reads through, so a test drives the read over a
|
|
22
|
+
* fixture tree with no git.
|
|
23
|
+
*
|
|
24
|
+
* Everything a caller might decide stays with the caller. The `null` — no
|
|
25
|
+
* ancestor of `cwd` is a workspace root — is the caller's refusal to render,
|
|
26
|
+
* never an answer: the CLI words its own refusal and picks its own exit
|
|
27
|
+
* code, the adapter throws its own, and a caller that ignored the null
|
|
28
|
+
* crashes one line later instead of reading a clean result. The throws
|
|
29
|
+
* (`adrCommand`'s unreadable registry, `listTrackedFiles`' failed
|
|
30
|
+
* `git ls-files`) propagate unchanged for the same reason. And it prints no
|
|
31
|
+
* byte and decides no exit code, so it is not one of the `run*` drivers
|
|
32
|
+
* `../../commands.mjs` leaves to `../../cli.mjs` — it returns exactly what
|
|
33
|
+
* the command returns.
|
|
34
|
+
*/
|
|
35
|
+
import { findWorkspaceRoot, listTrackedFiles } from "../workspace.mjs";
|
|
36
|
+
|
|
37
|
+
import { adrCommand } from "./adr.mjs";
|
|
38
|
+
import { WORKSPACE_MARKERS } from "./context.mjs";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Runs the `adr` command for the workspace `cwd` falls in.
|
|
42
|
+
*
|
|
43
|
+
* @param {{cwd: string}} request The working directory the root is walked up
|
|
44
|
+
* from — the same walk `resolveCommandContext` makes, without the project
|
|
45
|
+
* graph it builds.
|
|
46
|
+
* @param {{id?: string}} [options] Forwarded to `adrCommand` unchanged.
|
|
47
|
+
* @param {{listFiles?: Function}} [io] The tracked-file seam, typed like
|
|
48
|
+
* every command's (`Function`, the shape `./check.mjs` and
|
|
49
|
+
* `./context.mjs` declare), defaulted to `git ls-files`
|
|
50
|
+
* (`../workspace.mjs`) — injected where a test answers for git.
|
|
51
|
+
* @returns {object|null} What `adrCommand` returns, verbatim; `null` when no
|
|
52
|
+
* ancestor of `cwd` is a workspace root — the caller's refusal to render,
|
|
53
|
+
* never a clean answer (the module header owns why the message is not
|
|
54
|
+
* worded here).
|
|
55
|
+
* @throws {Error} on an unreadable registry or a failed `git ls-files`,
|
|
56
|
+
* exactly as `adrCommand` and `listTrackedFiles` throw — each caller maps
|
|
57
|
+
* that the way it already did.
|
|
58
|
+
*/
|
|
59
|
+
export function adrForWorkspace({ cwd }, options = {}, io = {}) {
|
|
60
|
+
const root = findWorkspaceRoot(cwd, WORKSPACE_MARKERS);
|
|
61
|
+
if (root === null) return null;
|
|
62
|
+
return adrCommand(root, options, { tracked: (io.listFiles ?? listTrackedFiles)(root) });
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `analyze` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `discover`, with the two proposal→intent steps the
|
|
4
|
+
* `--write-intent` lane drives — the serialized bytes and the target
|
|
5
|
+
* refusal.
|
|
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 { discoverCommand, intentJsonFromProposal, intentWriteRefusal } from "./discover.mjs";
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
* architectural delta (`./change.mjs`), and then belongs to the pull request
|
|
10
10
|
* as the reviewable answer to "what did this change do to the architecture?".
|
|
11
11
|
*
|
|
12
|
+
* Which "intent" this is: one of four unrelated surfaces sharing the word —
|
|
13
|
+
* PD-13 outcome (c), four surfaces, never one type
|
|
14
|
+
* (`../../../../docs/architecture/refactor/DECISIONS.md`). This module is
|
|
15
|
+
* the declared-change grammar: one change's expected architectural
|
|
16
|
+
* consequences, verified by the `change` command against the actual delta.
|
|
17
|
+
* The other three: the workspace-declared law (`../architecture-intent/` —
|
|
18
|
+
* the `architecture-intent.json` file this module deliberately does not
|
|
19
|
+
* extend, per the paragraph below), the v1.0 evidence-manifest registry
|
|
20
|
+
* (`../intent/`), and the fileless run-envelope intent shape
|
|
21
|
+
* (`result.intent` in a `--format json` envelope).
|
|
22
|
+
*
|
|
12
23
|
* This file mirrors `../architecture-intent/model.mjs`'s split — a pure
|
|
13
24
|
* `(raw) -> string[]` validator, a thin loader that reads, parses, validates
|
|
14
25
|
* and throws one Error naming every violation at once — and deliberately does
|
package/src/commands/change.mjs
CHANGED
|
@@ -101,14 +101,17 @@ import {
|
|
|
101
101
|
} from "./delta.mjs";
|
|
102
102
|
import { providerMismatch, readEvidenceSnapshot } from "./delta-snapshot.mjs";
|
|
103
103
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
104
|
+
import { isAbsolute, resolve } from "node:path";
|
|
104
105
|
import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
105
106
|
import { cyclicProjects } from "../governance/fitness-rules.mjs";
|
|
106
|
-
import {
|
|
107
|
+
import { eventSnapshotSide } from "./history.mjs";
|
|
108
|
+
import { VERDICTS, fitnessVerdict, isVerdict } from "../governance/verdict.mjs";
|
|
109
|
+
import { describe, isStringArray } from "../values.mjs";
|
|
107
110
|
import { buildDecision } from "../report/evidence.mjs";
|
|
108
111
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
109
112
|
import { formatChangeReport } from "../report/change-text.mjs";
|
|
110
113
|
import { evaluateRun } from "../rules/index.mjs";
|
|
111
|
-
import { compareSnapshotMetadata } from "./snapshot-meta.mjs";
|
|
114
|
+
import { compareSnapshotMetadata, dirtyBaselineNote, dirtyHeadNote } from "./snapshot-meta.mjs";
|
|
112
115
|
import { resolveProvenance } from "./provenance.mjs";
|
|
113
116
|
import { referenceTime } from "../governance/clock.mjs";
|
|
114
117
|
import {
|
|
@@ -124,6 +127,8 @@ import { writeEvent } from "../governance/evolution-store.mjs";
|
|
|
124
127
|
import { judgeIntent } from "../architecture-intent/judge.mjs";
|
|
125
128
|
import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
126
129
|
import { debtChangeDiff } from "../governance/debt-ledger.mjs";
|
|
130
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
131
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
127
132
|
|
|
128
133
|
/**
|
|
129
134
|
* One expected-fact row as the report and JSON carry it. Kept in one builder
|
|
@@ -368,6 +373,9 @@ function reconciliationVerdict(lists, unprovenReasons) {
|
|
|
368
373
|
return "matched";
|
|
369
374
|
}
|
|
370
375
|
|
|
376
|
+
/** The verdict axis `reconcileDisposition` maps — the four `reconciliationVerdict` answers. */
|
|
377
|
+
const RECONCILE_VERDICTS = Object.freeze(["matched", "undeclared", "unfulfilled", "unproven"]);
|
|
378
|
+
|
|
371
379
|
/**
|
|
372
380
|
* The reconcile DISPOSITION mapping (wave 3, design §5) — the verdict axis
|
|
373
381
|
* first, then the declared constraints' verdicts. Pure and exported so the
|
|
@@ -390,8 +398,33 @@ function reconciliationVerdict(lists, unprovenReasons) {
|
|
|
390
398
|
* this run — empty when none were declared or when the base identity was
|
|
391
399
|
* unproven (constraints are left unevaluated then).
|
|
392
400
|
* @returns {"accepted"|"rejected"|"no-verdict"}
|
|
401
|
+
* @throws {Error} on an axis value outside the four, or a constraint row
|
|
402
|
+
* whose verdict is outside the canonical vocabulary — a stranger would fold
|
|
403
|
+
* to `accepted`, and a fabricated acceptance is the one answer this mapping
|
|
404
|
+
* never hands out.
|
|
393
405
|
*/
|
|
394
406
|
export function reconcileDisposition(verdict, constraints = []) {
|
|
407
|
+
// The mapping owns its input latch — it is exported and pure, the one place
|
|
408
|
+
// the precedence is stated, and a stranger axis value would match no named
|
|
409
|
+
// arm and land on `accepted`: a fabricated acceptance, the one answer this
|
|
410
|
+
// function must never hand out over input it did not understand.
|
|
411
|
+
if (!RECONCILE_VERDICTS.includes(verdict)) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`archkeep: refusing to map a reconcile verdict ${describe(verdict)} — ` +
|
|
414
|
+
`expected one of ${RECONCILE_VERDICTS.join(", ")}. This is a bug in the ` +
|
|
415
|
+
`reconciliation that produced it.`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
// Same latch one axis down: a constraint row verdict outside the four states
|
|
419
|
+
// matches neither `unknown` nor `fail`, and the mapping reads it as consent.
|
|
420
|
+
for (const row of constraints) {
|
|
421
|
+
if (!isVerdict(row.verdict)) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`archkeep: refusing to map a constraint row whose verdict is ${describe(row.verdict)} — ` +
|
|
424
|
+
`expected one of ${VERDICTS.join(", ")}. This is a bug in the judge that built the row.`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
395
428
|
// used by its own test
|
|
396
429
|
if (verdict === "unproven") return "no-verdict";
|
|
397
430
|
if (verdict === "undeclared" || verdict === "unfulfilled") return "rejected";
|
|
@@ -400,6 +433,124 @@ export function reconcileDisposition(verdict, constraints = []) {
|
|
|
400
433
|
return "accepted";
|
|
401
434
|
}
|
|
402
435
|
|
|
436
|
+
/**
|
|
437
|
+
* The input latch for `changeFold`: names the first way its inputs are not a
|
|
438
|
+
* shape the fold can count, or `null` when they are. A returned problem
|
|
439
|
+
* becomes the fold's refusal — never a throw, the same in-band lane
|
|
440
|
+
* `deltaFold`'s latch takes one module over.
|
|
441
|
+
*
|
|
442
|
+
* @param {object} reconciliation From `reconcileMaterialDelta`.
|
|
443
|
+
* @param {object[]} constraints Judged `fitnessVerdict` rows.
|
|
444
|
+
* @param {string[]} unprovenReasons Why the base identity could not be proven.
|
|
445
|
+
* @returns {string|null}
|
|
446
|
+
*/
|
|
447
|
+
function changeFoldInputProblem(reconciliation, constraints, unprovenReasons) {
|
|
448
|
+
for (const key of ["matched", "unexpected", "missingExpected"]) {
|
|
449
|
+
if (!Array.isArray(reconciliation?.[key])) {
|
|
450
|
+
return `the change fold counts "reconciliation.${key}", which is ${describe(reconciliation?.[key])} — a list that is not an array counts as empty in every lane, and an empty reconciliation reads "matched". This is a bug in archkeep, not a fact about the workspace.`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!Array.isArray(constraints)) {
|
|
454
|
+
return `the change fold counts "constraints", which is ${describe(constraints)} — a list that is not an array counts as empty in every lane, and an empty constraint list reads "every declared constraint passing". This is a bug in archkeep, not a fact about the workspace.`;
|
|
455
|
+
}
|
|
456
|
+
for (const [index, row] of constraints.entries()) {
|
|
457
|
+
if (!isVerdict(row?.verdict)) {
|
|
458
|
+
return `the change fold counts the verdict of constraint row ${index}, which is ${describe(row?.verdict)} — a verdict outside ${VERDICTS.join(", ")} matches neither the fail nor the unknown lane, and reads as consent. This is a bug in archkeep, not a fact about the workspace.`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!isStringArray(unprovenReasons)) {
|
|
462
|
+
return `the change fold reads "unprovenReasons", which is ${describe(unprovenReasons)} — reasons that are not a string array read as "no reasons" below, and an unproven base then reads "matched". This is a bug in archkeep, not a fact about the workspace.`;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The `change` exit fold, lifted out of `changeCommand` as a pure function so
|
|
469
|
+
* the mapping from the reconciliation lists and the judged constraints to the
|
|
470
|
+
* verdict is a fact a test can pin (`reconcileDisposition` directly above is
|
|
471
|
+
* the arrangement's precedent — it latches the EVENT disposition; this is the
|
|
472
|
+
* STATUS fold the envelope and the exit code read). Lane order: an unproven
|
|
473
|
+
* base identity or any undetermined constraint → `no-verdict`/3; else any
|
|
474
|
+
* unexpected fact, missing expected fact, or failed constraint →
|
|
475
|
+
* `findings`/1; else `ok`/0.
|
|
476
|
+
*
|
|
477
|
+
* The input latch runs first: input the fold cannot read is refused as a
|
|
478
|
+
* no-verdict whose single reason names it (`refused`), never folded past as a
|
|
479
|
+
* silently-empty count — `reconciliationVerdict` reads `unprovenReasons`
|
|
480
|
+
* before any list, so a malformed one reaching it unguarded can name the
|
|
481
|
+
* whole run unproven or matched for reasons the counts never supported. The
|
|
482
|
+
* literals stay hand-rolled per site by decision — the carrier folds are the
|
|
483
|
+
* pinned baseline INV-2 names, not a table to be converged
|
|
484
|
+
* (`docs/architecture/refactor/AUTHORITY-MAP.md`).
|
|
485
|
+
*
|
|
486
|
+
* @param {{matched: object[], unexpected: object[], missingExpected: object[]}} reconciliation
|
|
487
|
+
* @param {{verdict: string}[]} constraints Judged `fitnessVerdict` rows —
|
|
488
|
+
* empty when none were declared or the base identity was unproven.
|
|
489
|
+
* @param {string[]} unprovenReasons Why the base identity could not be proven.
|
|
490
|
+
* @returns {{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3,
|
|
491
|
+
* decision: object, verdict: "matched"|"undeclared"|"unfulfilled"|"unproven",
|
|
492
|
+
* refused?: undefined}|
|
|
493
|
+
* {status: "no-verdict", exitCode: 3, decision: object, verdict?: undefined,
|
|
494
|
+
* refused: string}}
|
|
495
|
+
* `verdict` rides every judged lane because the event's `declaredIntentRows`
|
|
496
|
+
* and `result.reconciliation` report it whichever lane fired; a refusal
|
|
497
|
+
* carries `refused` and no verdict, and is the one return whose
|
|
498
|
+
* `decision.reason` names a malformed input rather than a fact about the
|
|
499
|
+
* trees.
|
|
500
|
+
*/
|
|
501
|
+
export function changeFold(reconciliation, constraints, unprovenReasons) {
|
|
502
|
+
const problem = changeFoldInputProblem(reconciliation, constraints, unprovenReasons);
|
|
503
|
+
if (problem !== null) {
|
|
504
|
+
return {
|
|
505
|
+
status: "no-verdict",
|
|
506
|
+
exitCode: 3,
|
|
507
|
+
decision: buildDecision({
|
|
508
|
+
status: "no-verdict",
|
|
509
|
+
coverageComplete: true,
|
|
510
|
+
findings: 0,
|
|
511
|
+
reason: problem,
|
|
512
|
+
}),
|
|
513
|
+
refused: problem,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const verdict = reconciliationVerdict(reconciliation, unprovenReasons);
|
|
517
|
+
const failedConstraints = constraints.filter((row) => row.verdict === "fail").length;
|
|
518
|
+
const unknownConstraints = constraints.filter((row) => row.verdict === "unknown").length;
|
|
519
|
+
const findings =
|
|
520
|
+
reconciliation.unexpected.length + reconciliation.missingExpected.length + failedConstraints;
|
|
521
|
+
|
|
522
|
+
if (verdict === "unproven" || unknownConstraints > 0) {
|
|
523
|
+
return {
|
|
524
|
+
status: "no-verdict",
|
|
525
|
+
exitCode: 3,
|
|
526
|
+
decision: buildDecision({
|
|
527
|
+
status: "no-verdict",
|
|
528
|
+
coverageComplete: true,
|
|
529
|
+
findings: 0,
|
|
530
|
+
reason:
|
|
531
|
+
verdict === "unproven"
|
|
532
|
+
? `the change intent could not be verified against the declared base: ${unprovenReasons[0]}`
|
|
533
|
+
: `${unknownConstraints} declared constraint${unknownConstraints === 1 ? "" : "s"} could not be determined`,
|
|
534
|
+
}),
|
|
535
|
+
verdict,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (findings > 0) {
|
|
539
|
+
return {
|
|
540
|
+
status: "findings",
|
|
541
|
+
exitCode: 1,
|
|
542
|
+
decision: buildDecision({ status: "findings", coverageComplete: true, findings }),
|
|
543
|
+
verdict,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
status: "ok",
|
|
548
|
+
exitCode: 0,
|
|
549
|
+
decision: buildDecision({ status: "ok", coverageComplete: true, findings: 0 }),
|
|
550
|
+
verdict,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
403
554
|
/**
|
|
404
555
|
* Judges the constraints the contract declares, through the shared engine —
|
|
405
556
|
* both sides re-judged under the CURRENT law and one shared instant, exactly
|
|
@@ -509,7 +660,7 @@ function judgeDeclaredConstraints(intent, io) {
|
|
|
509
660
|
* architecture-intent seam `drift` uses (defaults to `loadIntent`); the
|
|
510
661
|
* change event's `debt` diff judges the intent over this run's base and
|
|
511
662
|
* head graphs and would be untestable without it.
|
|
512
|
-
* @returns {Promise<{status: "ok"|"findings"|"no-verdict",
|
|
663
|
+
* @returns {Promise<{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3,
|
|
513
664
|
* changeIntent?: object, coverage: object, report: {text: string, json: string}}>}
|
|
514
665
|
* `status: "no-verdict"` from the coverage refusal (#608) carries no
|
|
515
666
|
* `changeIntent` payload — the reconciliation was withheld, and the
|
|
@@ -648,6 +799,28 @@ export async function changeCommand(
|
|
|
648
799
|
dependencies: buildDependencies(graph.dependencies),
|
|
649
800
|
};
|
|
650
801
|
const structural = computeDiff(baseGraphForDiff, headGraphForDiff);
|
|
802
|
+
// The event's identity sides are frozen HERE — at the same instant the diff
|
|
803
|
+
// above was computed from these same graph objects — so the identity always
|
|
804
|
+
// describes the graph the diff actually compared. Everything between this
|
|
805
|
+
// line and the event assembly (constraint re-judgement, custom rules,
|
|
806
|
+
// classification, debt) reads the graphs too; the engine's purity is what
|
|
807
|
+
// keeps them honest today, but the WS-I adversarial audit named the seam:
|
|
808
|
+
// identity and diff agreeing must not rest on purity conventions alone when
|
|
809
|
+
// one structural line closes it.
|
|
810
|
+
const baseCommit = baseline.provenance?.commit;
|
|
811
|
+
const headCommit = headProvenance?.commit;
|
|
812
|
+
const eventBase = eventSnapshotSide({
|
|
813
|
+
revision: baseCommit,
|
|
814
|
+
projects: baseGraphForDiff.projects,
|
|
815
|
+
dependencies: baseGraphForDiff.dependencies,
|
|
816
|
+
policyFingerprint: baseline.policyFingerprint,
|
|
817
|
+
});
|
|
818
|
+
const eventHead = eventSnapshotSide({
|
|
819
|
+
revision: headCommit,
|
|
820
|
+
projects: headGraphForDiff.projects,
|
|
821
|
+
dependencies: headGraphForDiff.dependencies,
|
|
822
|
+
policyFingerprint: headFingerprint,
|
|
823
|
+
});
|
|
651
824
|
const reconciliation = reconcileMaterialDelta(intent, structural);
|
|
652
825
|
|
|
653
826
|
// Constraints and the law axis are computed over both sides re-judged under
|
|
@@ -694,38 +867,13 @@ export async function changeCommand(
|
|
|
694
867
|
liveViolations = headEval.violations.length;
|
|
695
868
|
}
|
|
696
869
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
let status;
|
|
705
|
-
/** @type {0|1|3} */
|
|
706
|
-
let exitCode;
|
|
707
|
-
let decision;
|
|
708
|
-
if (verdict === "unproven" || unknownConstraints > 0) {
|
|
709
|
-
status = "no-verdict";
|
|
710
|
-
exitCode = 3;
|
|
711
|
-
decision = buildDecision({
|
|
712
|
-
status,
|
|
713
|
-
coverageComplete: true,
|
|
714
|
-
findings: 0,
|
|
715
|
-
reason:
|
|
716
|
-
verdict === "unproven"
|
|
717
|
-
? `the change intent could not be verified against the declared base: ${unprovenReasons[0]}`
|
|
718
|
-
: `${unknownConstraints} declared constraint${unknownConstraints === 1 ? "" : "s"} could not be determined`,
|
|
719
|
-
});
|
|
720
|
-
} else if (findings > 0) {
|
|
721
|
-
status = "findings";
|
|
722
|
-
exitCode = 1;
|
|
723
|
-
decision = buildDecision({ status, coverageComplete: true, findings });
|
|
724
|
-
} else {
|
|
725
|
-
status = "ok";
|
|
726
|
-
exitCode = 0;
|
|
727
|
-
decision = buildDecision({ status, coverageComplete: true, findings: 0 });
|
|
728
|
-
}
|
|
870
|
+
// The verdict comes from the one fold — `changeFold` above owns the lane
|
|
871
|
+
// order and the input latch — but it is TAKEN after the notes and coverage
|
|
872
|
+
// blocks below, so a refusal can carry the disclosures (policy-changed,
|
|
873
|
+
// dirty sides) the run already computed: a withheld verdict that dropped
|
|
874
|
+
// them would hide a weaker-evidence fact from the only envelope the run
|
|
875
|
+
// emits. Nothing between this comment and the fold call reads the fold's
|
|
876
|
+
// inputs or outputs.
|
|
729
877
|
|
|
730
878
|
/** @type {string[]} */
|
|
731
879
|
const notes = [];
|
|
@@ -736,16 +884,10 @@ export async function changeCommand(
|
|
|
736
884
|
);
|
|
737
885
|
}
|
|
738
886
|
if (meta.dirtyBaseline) {
|
|
739
|
-
notes.push(
|
|
740
|
-
"the baseline was captured from a dirty working tree — its evidence is not a reproducible " +
|
|
741
|
-
"claim about the commit the contract pins",
|
|
742
|
-
);
|
|
887
|
+
notes.push(dirtyBaselineNote(true));
|
|
743
888
|
}
|
|
744
889
|
if (meta.dirtyHead) {
|
|
745
|
-
notes.push(
|
|
746
|
-
"this run's working tree is dirty — the head side describes uncommitted state, not the " +
|
|
747
|
-
"commit HEAD names",
|
|
748
|
-
);
|
|
890
|
+
notes.push(dirtyHeadNote());
|
|
749
891
|
}
|
|
750
892
|
if (meta.provenanceOneSided) {
|
|
751
893
|
notes.push(
|
|
@@ -769,6 +911,40 @@ export async function changeCommand(
|
|
|
769
911
|
notes,
|
|
770
912
|
};
|
|
771
913
|
|
|
914
|
+
// The exit fold, at the input boundary: input the fold cannot read is
|
|
915
|
+
// refused in-band (no-verdict, exit 3, the field named), never counted past
|
|
916
|
+
// as a silently-empty list. The refusal withholds `result` and the event
|
|
917
|
+
// exactly the coverage refusal does: a comparison whose counts could not be
|
|
918
|
+
// read has no verdict to report, and the refusal rides the envelope beside
|
|
919
|
+
// the coverage notes the run already earned.
|
|
920
|
+
const fold = changeFold(reconciliation, constraints, unprovenReasons);
|
|
921
|
+
if (fold.refused !== undefined) {
|
|
922
|
+
const refusalCoverage = { ...coverage, notes: [...notes, fold.refused] };
|
|
923
|
+
return {
|
|
924
|
+
status: fold.status,
|
|
925
|
+
exitCode: fold.exitCode,
|
|
926
|
+
coverage: refusalCoverage,
|
|
927
|
+
report: {
|
|
928
|
+
text: `change: no verdict — ${fold.refused}\n`,
|
|
929
|
+
json: renderJson(
|
|
930
|
+
jsonEnvelope({
|
|
931
|
+
command: "change",
|
|
932
|
+
context: { root, provider, marker, provenance: headProvenance },
|
|
933
|
+
status: fold.status,
|
|
934
|
+
exitCode: fold.exitCode,
|
|
935
|
+
coverage: refusalCoverage,
|
|
936
|
+
// The refusal-withheld payload, stated the way `coverageRefusal`
|
|
937
|
+
// states it: `jsonEnvelope` requires the key, the refusal has no
|
|
938
|
+
// result to report.
|
|
939
|
+
result: undefined,
|
|
940
|
+
decision: fold.decision,
|
|
941
|
+
}),
|
|
942
|
+
),
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const { status, exitCode, decision, verdict } = fold;
|
|
947
|
+
|
|
772
948
|
// Wave 3 (design §1, §2, §5): the evolution classification and the reconcile
|
|
773
949
|
// event. The classification is ALWAYS computed — it rides the envelope
|
|
774
950
|
// result in memory even when no event is written (design §4) — and the
|
|
@@ -855,23 +1031,24 @@ export async function changeCommand(
|
|
|
855
1031
|
}
|
|
856
1032
|
}
|
|
857
1033
|
|
|
858
|
-
const baseCommit = baseline.provenance?.commit;
|
|
859
|
-
const headCommit = headProvenance?.commit;
|
|
860
1034
|
/** @type {object} */
|
|
861
1035
|
const event = {
|
|
862
1036
|
schemaVersion: EVOLUTION_EVENT_SCHEMA_VERSION,
|
|
863
1037
|
kind: "reconcile",
|
|
864
1038
|
source: "change",
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1039
|
+
// Both sides were frozen through the ONE identity spelling
|
|
1040
|
+
// (`eventSnapshotSide`, ./history.mjs) at the diff site above: a revision
|
|
1041
|
+
// when one is known, plus the snapshot identity of the graph the diff
|
|
1042
|
+
// compared — never the baseline's storage path, which is machine-local
|
|
1043
|
+
// and must not make the identity a per-machine property.
|
|
1044
|
+
base: eventBase,
|
|
1045
|
+
head: eventHead,
|
|
1046
|
+
// The caller's own evidence ref — the baseline file this run consumed,
|
|
1047
|
+
// spelled as the run received it (the same convention `declaration.file`
|
|
1048
|
+
// uses for the intent path). Disclosed OUTSIDE the identity: the tuple
|
|
1049
|
+
// above names the state itself, so a relocated baseline is still the
|
|
1050
|
+
// same event.
|
|
1051
|
+
evidence: baselinePath,
|
|
875
1052
|
declaration: { file: intentPath, digest: declarationDigest(intent) },
|
|
876
1053
|
observed,
|
|
877
1054
|
affected: evolution.affected,
|
|
@@ -983,6 +1160,7 @@ export async function changeCommand(
|
|
|
983
1160
|
|
|
984
1161
|
return {
|
|
985
1162
|
status,
|
|
1163
|
+
exitCode,
|
|
986
1164
|
changeIntent: result,
|
|
987
1165
|
coverage,
|
|
988
1166
|
report: {
|
|
@@ -997,3 +1175,85 @@ export async function changeCommand(
|
|
|
997
1175
|
},
|
|
998
1176
|
};
|
|
999
1177
|
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* The paths `change` runs over, resolved once and shared by the CLI entry
|
|
1181
|
+
* and the output guard below — one spelling of "where does this flag point",
|
|
1182
|
+
* the same relative-to-cwd resolution every path flag uses.
|
|
1183
|
+
*
|
|
1184
|
+
* @param {{intent: string, paths: string[]}} options This run's parsed flags.
|
|
1185
|
+
* @param {string} cwd The run's working directory.
|
|
1186
|
+
* @returns {{baselinePath: string, intentPath: string}}
|
|
1187
|
+
*/
|
|
1188
|
+
function changePaths(options, cwd) {
|
|
1189
|
+
return {
|
|
1190
|
+
baselinePath: isAbsolute(options.paths[0]) ? options.paths[0] : resolve(cwd, options.paths[0]),
|
|
1191
|
+
intentPath: isAbsolute(options.intent) ? options.intent : resolve(cwd, options.intent),
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* `change`'s self-footgun guard, the same shape `history`'s holds: writing
|
|
1197
|
+
* the reconciliation report over the very manifest this run just read would
|
|
1198
|
+
* destroy the declaration it verified, with the loss surfacing only later —
|
|
1199
|
+
* the first time someone tries to re-run the verification. Declared by the
|
|
1200
|
+
* command that owns the law and enforced by the driver's write door; `null`
|
|
1201
|
+
* means no refusal.
|
|
1202
|
+
*
|
|
1203
|
+
* @param {{output: string|null, intent: string, paths: string[]}} options
|
|
1204
|
+
* This run's parsed flags.
|
|
1205
|
+
* @param {string} cwd The run's working directory, for relative flag
|
|
1206
|
+
* resolution.
|
|
1207
|
+
* @returns {string|null} The refusal message, or `null` when the output is
|
|
1208
|
+
* safe.
|
|
1209
|
+
*/
|
|
1210
|
+
export function changeOutputRefusal(options, cwd) {
|
|
1211
|
+
if (!options.output) return null;
|
|
1212
|
+
const outputAbs = isAbsolute(options.output)
|
|
1213
|
+
? resolve(options.output)
|
|
1214
|
+
: resolve(cwd, options.output);
|
|
1215
|
+
if (outputAbs === changePaths(options, cwd).intentPath) {
|
|
1216
|
+
return (
|
|
1217
|
+
`archkeep: --output '${options.output}' resolves to the change-intent manifest itself — ` +
|
|
1218
|
+
`overwriting the declaration with its own reconciliation report would destroy it. ` +
|
|
1219
|
+
`Write the report somewhere else.`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* `change` as the CLI drives it: the baseline and manifest paths resolved,
|
|
1227
|
+
* then the shared preamble — command context, boundary law, the optional
|
|
1228
|
+
* `--event-out` directory — so `../../cli.mjs`'s driver only wires options,
|
|
1229
|
+
* IO seams, and where output lands (`./README.md`). The engine this returns
|
|
1230
|
+
* from is `changeCommand` above, unchanged.
|
|
1231
|
+
*
|
|
1232
|
+
* @param {{config: string|null, eventOut?: string|null, intent: string, paths: string[]}} options
|
|
1233
|
+
* This run's parsed flags; `paths[0]` is the baseline evidence snapshot.
|
|
1234
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
1235
|
+
* seams a test injects, the same ones `check` takes.
|
|
1236
|
+
* @returns {Promise<object>} `changeCommand`'s result, unmodified.
|
|
1237
|
+
*/
|
|
1238
|
+
export async function change(options, { cwd, readGraph, listFiles }) {
|
|
1239
|
+
const { baselinePath, intentPath } = changePaths(options, cwd);
|
|
1240
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
1241
|
+
// Declared constraints are judged under whichever law THIS run resolves,
|
|
1242
|
+
// and the envelope records that law's fingerprint beside the baseline's —
|
|
1243
|
+
// the same loading every judging command does (`resolvePolicy`),
|
|
1244
|
+
// profile-aware the same way `check` is.
|
|
1245
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1246
|
+
// `--event-out` names the reconcile event store directory, resolved from
|
|
1247
|
+
// cwd like the other path flags; `undefined` when absent, so a run
|
|
1248
|
+
// without the flag writes no event and stays byte-identical.
|
|
1249
|
+
const eventOut =
|
|
1250
|
+
typeof options.eventOut === "string" && options.eventOut !== ""
|
|
1251
|
+
? isAbsolute(options.eventOut)
|
|
1252
|
+
? options.eventOut
|
|
1253
|
+
: resolve(cwd, options.eventOut)
|
|
1254
|
+
: undefined;
|
|
1255
|
+
return changeCommand(baselinePath, intentPath, commandContext, {
|
|
1256
|
+
config,
|
|
1257
|
+
...(eventOut === undefined ? {} : { eventOut }),
|
|
1258
|
+
});
|
|
1259
|
+
}
|