@ecoma-io/archkeep 0.25.0 → 0.27.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 +167 -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 +251 -35
- package/src/commands/check-capability.mjs +13 -0
- package/src/commands/check.mjs +11 -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 +266 -50
- 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 +32 -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 +99 -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 +330 -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 +7 -23
- package/src/governance/debt-ledger.mjs +5 -2
- package/src/governance/evolution-event.mjs +14 -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 +8 -0
- package/src/rules/index.mjs +24 -10
- package/src/rules/messages.mjs +64 -14
- package/src/rules/specifiers.mjs +21 -20
- package/src/tsconfig-paths.mjs +7 -14
- package/src/verdict.mjs +127 -16
package/cli.mjs
CHANGED
|
@@ -55,8 +55,10 @@
|
|
|
55
55
|
* classifies as introduced, or a change-intent reconciliation that found
|
|
56
56
|
* undeclared material changes, unfulfilled declarations, or a failed
|
|
57
57
|
* declared constraint. `check`, `fitness`, `delta` and `change` are the
|
|
58
|
-
* verbs whose verdicts carry this code
|
|
59
|
-
*
|
|
58
|
+
* verbs whose verdicts carry this code, plus `rules verify` — the
|
|
59
|
+
* artifact-integrity fold, a bounded verification surface, not a fifth
|
|
60
|
+
* architecture carrier (`docs/reference/exit-codes.md` owns the
|
|
61
|
+
* roster). Every other verb only ever reads.
|
|
60
62
|
* 2 usage error — unknown command, unknown flag, missing argument, path
|
|
61
63
|
* outside the tree
|
|
62
64
|
* 3 no verdict — no workspace, malformed config, the graph provider or git
|
|
@@ -92,7 +94,7 @@
|
|
|
92
94
|
* only once a later command needs something this table cannot express.
|
|
93
95
|
*/
|
|
94
96
|
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
95
|
-
import {
|
|
97
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
96
98
|
import { createRequire } from "node:module";
|
|
97
99
|
|
|
98
100
|
const require = createRequire(import.meta.url);
|
|
@@ -101,59 +103,70 @@ const { name: TOOL_NAME, version: TOOL_VERSION } = require("./package.json");
|
|
|
101
103
|
|
|
102
104
|
import { containmentViolation } from "./src/containment.mjs";
|
|
103
105
|
import { UsageError } from "./src/errors.mjs";
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
// Verb imports route through the capability facades: each
|
|
107
|
+
// `<word>-capability.mjs` module is its word's explicit verb roster — pure
|
|
108
|
+
// re-exports, zero judgment. The vocabulary's roster lives in
|
|
109
|
+
// docs/concepts/architecture.md's "The 24 commands".
|
|
110
|
+
// Non-verb helper modules keep their direct imports.
|
|
106
111
|
import {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} from "./src/commands/
|
|
112
|
-
import {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
import {
|
|
127
|
-
import {
|
|
128
|
-
import {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
112
|
+
discoverCommand,
|
|
113
|
+
intentJsonFromProposal,
|
|
114
|
+
intentWriteRefusal,
|
|
115
|
+
} from "./src/commands/analyze-capability.mjs";
|
|
116
|
+
import { check, fitness, scenario, sortViolations } from "./src/commands/check-capability.mjs";
|
|
117
|
+
import {
|
|
118
|
+
captureBaseline,
|
|
119
|
+
change,
|
|
120
|
+
changeOutputRefusal,
|
|
121
|
+
delta,
|
|
122
|
+
diff,
|
|
123
|
+
drift,
|
|
124
|
+
evolutionCommand,
|
|
125
|
+
history,
|
|
126
|
+
historyOutputRefusal,
|
|
127
|
+
reconcileCommand,
|
|
128
|
+
trajectoryCommand,
|
|
129
|
+
trajectoryOutputRefusal,
|
|
130
|
+
} from "./src/commands/compare-capability.mjs";
|
|
131
|
+
import { explain } from "./src/commands/explain-capability.mjs";
|
|
132
|
+
import { context, graph, health, impact, planContext } from "./src/commands/inspect-capability.mjs";
|
|
133
|
+
import {
|
|
134
|
+
debt,
|
|
135
|
+
decisions,
|
|
136
|
+
provenanceCommand,
|
|
137
|
+
report,
|
|
138
|
+
waivers,
|
|
139
|
+
} from "./src/commands/govern-capability.mjs";
|
|
134
140
|
import {
|
|
135
141
|
rulesAddCommand,
|
|
136
142
|
rulesInfoCommand,
|
|
137
143
|
rulesListCommand,
|
|
138
144
|
rulesVerifyCommand,
|
|
139
|
-
} from "./src/commands/rules.mjs";
|
|
140
|
-
import {
|
|
141
|
-
import {
|
|
145
|
+
} from "./src/commands/rules-capability.mjs";
|
|
146
|
+
import { nativePolicyOptions } from "./src/commands/policy.mjs";
|
|
147
|
+
import {
|
|
148
|
+
DEFAULT_OPTIONS,
|
|
149
|
+
WORKSPACE_MARKERS,
|
|
150
|
+
markersAt,
|
|
151
|
+
resolveCommandContext,
|
|
152
|
+
} from "./src/commands/context.mjs";
|
|
153
|
+
import { adrForWorkspace } from "./src/commands/adr-for-workspace.mjs";
|
|
154
|
+
import { INTENT_FILE } from "./src/architecture-intent/model.mjs";
|
|
142
155
|
import { isProgramEntry } from "./src/entry-point.mjs";
|
|
143
|
-
import { readPluginOptions } from "./src/options.mjs";
|
|
144
|
-
import { EXIT
|
|
156
|
+
import { ARCHKEEP_MODEL_FILE, readPluginOptions } from "./src/options.mjs";
|
|
157
|
+
import { EXIT } from "./src/verdict.mjs";
|
|
145
158
|
|
|
146
|
-
import {
|
|
147
|
-
import { findWorkspaceRoot, listTrackedFiles } from "./src/workspace.mjs";
|
|
159
|
+
import { findWorkspaceRoot } from "./src/workspace.mjs";
|
|
148
160
|
|
|
149
161
|
/**
|
|
150
162
|
* Workspace-relative read from `root`, the same default `createWorkspace`
|
|
151
163
|
* builds when no reader is injected (`./src/workspace.mjs`) — duplicated
|
|
152
164
|
* rather than imported for the reason `./src/commands/context.mjs` carries its
|
|
153
165
|
* own copy of the same helper: `optionsForUsage` below needs one BEFORE any
|
|
154
|
-
* `Workspace` exists, to hand `
|
|
155
|
-
* itself. `check` no longer needs a copy of its own —
|
|
156
|
-
* owns that read now — which is why this is the only
|
|
166
|
+
* `Workspace` exists, to hand `nativePolicyOptions` a reader for
|
|
167
|
+
* `archkeep.json` itself. `check` no longer needs a copy of its own —
|
|
168
|
+
* `resolveCommandContext` owns that read now — which is why this is the only
|
|
169
|
+
* one left in this file.
|
|
157
170
|
*
|
|
158
171
|
* @param {string} root
|
|
159
172
|
* @returns {(path: string) => string|null}
|
|
@@ -338,20 +351,7 @@ function optionsForUsage(cwd) {
|
|
|
338
351
|
if (root === null) return DEFAULT_OPTIONS;
|
|
339
352
|
const { hasNx, hasNative } = markersAt(root);
|
|
340
353
|
if (hasNative && !hasNx) {
|
|
341
|
-
|
|
342
|
-
// An inline policy object has no filename to print — `${boundaryConfig}`
|
|
343
|
-
// below would otherwise coerce it to the literal text "[object Object]",
|
|
344
|
-
// which reads as a real (and wrong) filename rather than as the "there
|
|
345
|
-
// is no file" it actually means. `inline: true` is what tells `usage()`
|
|
346
|
-
// to print the paragraph that says so, instead of the one describing a
|
|
347
|
-
// named file.
|
|
348
|
-
return typeof model.boundaryConfig === "string"
|
|
349
|
-
? { boundaryConfig: model.boundaryConfig, tsConfig: model.tsConfig }
|
|
350
|
-
: {
|
|
351
|
-
boundaryConfig: "an inline policy in archkeep.json",
|
|
352
|
-
tsConfig: model.tsConfig,
|
|
353
|
-
inline: true,
|
|
354
|
-
};
|
|
354
|
+
return nativePolicyOptions(root, { readFile: readWorkspaceRoot(root) });
|
|
355
355
|
}
|
|
356
356
|
return readPluginOptions(root);
|
|
357
357
|
} catch {
|
|
@@ -771,9 +771,9 @@ async function runCheck(options, { cwd, env }) {
|
|
|
771
771
|
(result.intentFindings > 0
|
|
772
772
|
? `, ${result.intentFindings} architecture-intent finding${result.intentFindings === 1 ? "" : "s"}`
|
|
773
773
|
: "") +
|
|
774
|
-
// Fitness drives the exit code exactly like every count above it
|
|
775
|
-
//
|
|
776
|
-
//
|
|
774
|
+
// Fitness drives the exit code exactly like every count above it —
|
|
775
|
+
// omitting it here is what let a fitness-only failure log "0
|
|
776
|
+
// violations …" beside a non-zero exit.
|
|
777
777
|
(result.fitnessFail > 0
|
|
778
778
|
? `, ${result.fitnessFail} fitness function${result.fitnessFail === 1 ? "" : "s"} failed`
|
|
779
779
|
: "") +
|
|
@@ -781,9 +781,9 @@ async function runCheck(options, { cwd, env }) {
|
|
|
781
781
|
? `, ${result.fitnessUnknown} fitness function${result.fitnessUnknown === 1 ? "" : "s"} undetermined`
|
|
782
782
|
: "") +
|
|
783
783
|
// Custom rules drive the exit code exactly like every count above
|
|
784
|
-
// them
|
|
785
|
-
//
|
|
786
|
-
//
|
|
784
|
+
// them, so they are named here for the same reason fitness is: a
|
|
785
|
+
// custom-rule-only failure would otherwise log "0 violations …"
|
|
786
|
+
// beside a non-zero exit.
|
|
787
787
|
(result.customRuleFail > 0
|
|
788
788
|
? `, ${result.customRuleFail} custom rule${result.customRuleFail === 1 ? "" : "s"} failed`
|
|
789
789
|
: "") +
|
|
@@ -799,7 +799,11 @@ async function runCheck(options, { cwd, env }) {
|
|
|
799
799
|
env.out(result.report);
|
|
800
800
|
}
|
|
801
801
|
|
|
802
|
-
return
|
|
802
|
+
// `check`'s own verdict rides its return (`./src/commands/check.mjs`): the
|
|
803
|
+
// command computes the one exit authority, and the process returns it
|
|
804
|
+
// unmodified — a second fold here would be a second chance for the printed
|
|
805
|
+
// verdict and the process exit to disagree.
|
|
806
|
+
return result.exitCode;
|
|
803
807
|
}
|
|
804
808
|
|
|
805
809
|
/**
|
|
@@ -818,32 +822,7 @@ async function runGraph(options, { cwd, env }) {
|
|
|
818
822
|
|
|
819
823
|
let result;
|
|
820
824
|
try {
|
|
821
|
-
|
|
822
|
-
{ cwd },
|
|
823
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
824
|
-
);
|
|
825
|
-
|
|
826
|
-
// Load the boundary config so the snapshot carries a policy fingerprint
|
|
827
|
-
// that `diff` can use to warn when the policy changed between runs. Without
|
|
828
|
-
// a config, the snapshot carries no policy identity — the consumer did not
|
|
829
|
-
// provide one (`./src/commands/graph.mjs` makes that field conditional). A
|
|
830
|
-
// profile-selected workspace's `boundaryConfig` names a profile rather than
|
|
831
|
-
// a file, resolved the same way `check` resolves it (`resolvePolicy`), so
|
|
832
|
-
// the fingerprint moves with a profile edit the same way it already does
|
|
833
|
-
// with a file or inline-object edit.
|
|
834
|
-
//
|
|
835
|
-
// `graph` describes the project graph, not the boundary law — it reads no
|
|
836
|
-
// constraint row and judges nothing against one — so a workspace that has
|
|
837
|
-
// not written a law yet must not be refused here. Every arm of that
|
|
838
|
-
// decision — what is skipped is the load of a file that is NOT THERE, the
|
|
839
|
-
// `boundaryConfigDeclared` bit that keeps the guard to the un-overridden
|
|
840
|
-
// default, and why a law someone named and then deleted stays loud — lives
|
|
841
|
-
// in `resolveDescribedPolicy` (`./src/commands/policy.mjs`) rather than
|
|
842
|
-
// here, so the descriptive commands and the MCP face that serves them
|
|
843
|
-
// cannot disagree about what "no law declared" means.
|
|
844
|
-
const { config } = await resolveDescribedPolicy(options, commandContext, cwd);
|
|
845
|
-
|
|
846
|
-
result = graphCommand(commandContext, { config });
|
|
825
|
+
result = await graph(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
847
826
|
} catch (error) {
|
|
848
827
|
const usageError = error instanceof UsageError;
|
|
849
828
|
env.err(String(error?.message ?? error));
|
|
@@ -890,26 +869,9 @@ async function runDiff(options, { cwd, env }) {
|
|
|
890
869
|
return EXIT.usage;
|
|
891
870
|
}
|
|
892
871
|
|
|
893
|
-
const baselinePath = isAbsolute(options.paths[0])
|
|
894
|
-
? options.paths[0]
|
|
895
|
-
: resolve(cwd, options.paths[0]);
|
|
896
|
-
|
|
897
872
|
let result;
|
|
898
873
|
try {
|
|
899
|
-
|
|
900
|
-
{ cwd },
|
|
901
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
902
|
-
);
|
|
903
|
-
|
|
904
|
-
// Load the boundary config when --config is given or when the workspace
|
|
905
|
-
// declares one, so rule-impact analysis is computed. Without a config,
|
|
906
|
-
// the diff reports only structural changes — same as before. A
|
|
907
|
-
// profile-selected workspace resolves the same way `check` does
|
|
908
|
-
// (`resolvePolicy`), so a policy edit under an unchanged profile NAME is
|
|
909
|
-
// still visible as a fingerprint change here.
|
|
910
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
911
|
-
|
|
912
|
-
result = diffCommand(baselinePath, commandContext, { config });
|
|
874
|
+
result = await diff(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
913
875
|
} catch (error) {
|
|
914
876
|
const usageError = error instanceof UsageError;
|
|
915
877
|
env.err(String(error?.message ?? error));
|
|
@@ -974,18 +936,12 @@ async function runDelta(options, { cwd, env }) {
|
|
|
974
936
|
|
|
975
937
|
let result;
|
|
976
938
|
try {
|
|
977
|
-
const commandContext = resolveCommandContext(
|
|
978
|
-
{ cwd },
|
|
979
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
980
|
-
);
|
|
981
|
-
|
|
982
|
-
// Both modes need the boundary law: capture fingerprints it, compare
|
|
983
|
-
// re-judges both sides under it — the same ladder every judging command
|
|
984
|
-
// resolves through (`resolvePolicy`).
|
|
985
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
986
|
-
|
|
987
939
|
if (options.capture) {
|
|
988
|
-
const { text } =
|
|
940
|
+
const { text } = await captureBaseline(options, {
|
|
941
|
+
cwd,
|
|
942
|
+
readGraph: env.readGraph,
|
|
943
|
+
listFiles: env.listFiles,
|
|
944
|
+
});
|
|
989
945
|
if (options.output) {
|
|
990
946
|
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring
|
|
991
947
|
if (!writeOutputReport(options.output, text, env, cwd, options.config)) return EXIT.error;
|
|
@@ -997,13 +953,7 @@ async function runDelta(options, { cwd, env }) {
|
|
|
997
953
|
return EXIT.ok;
|
|
998
954
|
}
|
|
999
955
|
|
|
1000
|
-
|
|
1001
|
-
? resolve(options.paths[0])
|
|
1002
|
-
: resolve(cwd, options.paths[0]);
|
|
1003
|
-
result = await deltaCommand(baselinePath, commandContext, {
|
|
1004
|
-
config,
|
|
1005
|
-
eventOut: options.eventOut,
|
|
1006
|
-
});
|
|
956
|
+
result = await delta(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1007
957
|
} catch (error) {
|
|
1008
958
|
const usageError = error instanceof UsageError;
|
|
1009
959
|
env.err(String(error?.message ?? error));
|
|
@@ -1053,13 +1003,10 @@ async function runDelta(options, { cwd, env }) {
|
|
|
1053
1003
|
);
|
|
1054
1004
|
}
|
|
1055
1005
|
|
|
1056
|
-
// The exit
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
return
|
|
1060
|
-
{ ok: EXIT.ok, findings: EXIT.violations, "no-verdict": EXIT.error }[result.status] ??
|
|
1061
|
-
EXIT.error
|
|
1062
|
-
);
|
|
1006
|
+
// The exit `deltaCommand` computed: a non-waived introduced violation is a
|
|
1007
|
+
// finding, an unclassifiable item is a no-verdict, anything else is clean —
|
|
1008
|
+
// the command's return carries the pair, the process returns it as is.
|
|
1009
|
+
return result.exitCode;
|
|
1063
1010
|
}
|
|
1064
1011
|
|
|
1065
1012
|
/**
|
|
@@ -1081,36 +1028,7 @@ async function runDrift(options, { cwd, env }) {
|
|
|
1081
1028
|
|
|
1082
1029
|
let result;
|
|
1083
1030
|
try {
|
|
1084
|
-
|
|
1085
|
-
{ cwd },
|
|
1086
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1087
|
-
);
|
|
1088
|
-
// The loaded policy — profile-aware the same way `check` is
|
|
1089
|
-
// (`resolvePolicy`), `null` when the workspace declares none. Drift reads
|
|
1090
|
-
// the intent's rows, and the fitness half of a row's `decisionRef`
|
|
1091
|
-
// resolves against the ids THIS policy declares (F04), so the same policy
|
|
1092
|
-
// that made the boundary law answerable to the model must answer here.
|
|
1093
|
-
// `drift` has no `--config` (`DRIFT_FLAG_HELP`), so `config` is always the
|
|
1094
|
-
// workspace's own default — resolvePolicy reads `options.config` as the
|
|
1095
|
-
// override, hence `null` here, which selects the workspace's configured
|
|
1096
|
-
// boundary law (or a profile, when one is registered).
|
|
1097
|
-
//
|
|
1098
|
-
// The failure is DEFERRED rather than thrown here. `drift`'s only reader of
|
|
1099
|
-
// this policy is the non-verdict decisionRef axis, and only for rows that
|
|
1100
|
-
// carry one, so a workspace with an intent and no boundary config was
|
|
1101
|
-
// exiting 3 over a law drift would never have opened — a fifth refusal
|
|
1102
|
-
// neither `docs/usage/drift.md` nor `reconcile`, which makes the same four,
|
|
1103
|
-
// ever had. `driftCommand` rethrows it, unchanged, at the one site that
|
|
1104
|
-
// reads the policy, so every workspace whose intent cites anything keeps the
|
|
1105
|
-
// exact exit-3 it had.
|
|
1106
|
-
let config = null;
|
|
1107
|
-
let configError = null;
|
|
1108
|
-
try {
|
|
1109
|
-
({ config } = await resolvePolicy({ ...options, config: null }, commandContext, cwd));
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
configError = /** @type {Error} */ (error);
|
|
1112
|
-
}
|
|
1113
|
-
result = await driftCommand(commandContext, { config, configError });
|
|
1031
|
+
result = await drift(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1114
1032
|
} catch (error) {
|
|
1115
1033
|
const usageError = error instanceof UsageError;
|
|
1116
1034
|
env.err(String(error?.message ?? error));
|
|
@@ -1154,7 +1072,9 @@ async function runDrift(options, { cwd, env }) {
|
|
|
1154
1072
|
* Provenance reads no graph and judges nothing — it describes where the run's
|
|
1155
1073
|
* facts came from and which governance rows carry an origin. It is
|
|
1156
1074
|
* fail-closed the way every descriptive command is: a malformed intent or
|
|
1157
|
-
* boundary config throws out of `provenanceCommand` → exit 3, so
|
|
1075
|
+
* boundary config throws out of `provenanceCommand` → exit 3, and so does
|
|
1076
|
+
* a law the shared ladder refuses (`./src/commands/policy.mjs` — e.g. a
|
|
1077
|
+
* `coverage` key in a native workspace's `boundaryConfig` file), so "rows
|
|
1158
1078
|
* unlisted" never reads as "rows attested".
|
|
1159
1079
|
*
|
|
1160
1080
|
* @param {{format: string, output: string|null, paths: string[]}} options
|
|
@@ -1293,55 +1213,15 @@ async function runChange(options, { cwd, env }) {
|
|
|
1293
1213
|
return EXIT.usage;
|
|
1294
1214
|
}
|
|
1295
1215
|
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
// A self-footgun guard, the same shape `history`'s holds: writing the
|
|
1302
|
-
// reconciliation report over the very manifest this run just read would
|
|
1303
|
-
// destroy the declaration it verified, with the loss surfacing only later —
|
|
1304
|
-
// the first time someone tries to re-run the verification.
|
|
1305
|
-
if (options.output) {
|
|
1306
|
-
const outputAbs = isAbsolute(options.output)
|
|
1307
|
-
? resolve(options.output)
|
|
1308
|
-
: resolve(cwd, options.output);
|
|
1309
|
-
if (outputAbs === intentPath) {
|
|
1310
|
-
env.err(
|
|
1311
|
-
`archkeep: --output '${options.output}' resolves to the change-intent manifest itself — ` +
|
|
1312
|
-
`overwriting the declaration with its own reconciliation report would destroy it. ` +
|
|
1313
|
-
`Write the report somewhere else.`,
|
|
1314
|
-
);
|
|
1315
|
-
return EXIT.usage;
|
|
1316
|
-
}
|
|
1216
|
+
const refusal = changeOutputRefusal(options, cwd);
|
|
1217
|
+
if (refusal) {
|
|
1218
|
+
env.err(refusal);
|
|
1219
|
+
return EXIT.usage;
|
|
1317
1220
|
}
|
|
1318
1221
|
|
|
1319
1222
|
let result;
|
|
1320
1223
|
try {
|
|
1321
|
-
|
|
1322
|
-
{ cwd },
|
|
1323
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1324
|
-
);
|
|
1325
|
-
|
|
1326
|
-
// Declared constraints are judged under whichever law THIS run resolves,
|
|
1327
|
-
// and the envelope records that law's fingerprint beside the baseline's —
|
|
1328
|
-
// the same loading every judging command does (`resolvePolicy`),
|
|
1329
|
-
// profile-aware the same way `check` is.
|
|
1330
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1331
|
-
|
|
1332
|
-
// `--event-out` names the reconcile event store directory, resolved from
|
|
1333
|
-
// cwd like the other path flags; `undefined` when absent, so a run
|
|
1334
|
-
// without the flag writes no event and stays byte-identical.
|
|
1335
|
-
const eventOut =
|
|
1336
|
-
typeof options.eventOut === "string" && options.eventOut !== ""
|
|
1337
|
-
? isAbsolute(options.eventOut)
|
|
1338
|
-
? options.eventOut
|
|
1339
|
-
: resolve(cwd, options.eventOut)
|
|
1340
|
-
: undefined;
|
|
1341
|
-
result = await changeCommand(baselinePath, intentPath, commandContext, {
|
|
1342
|
-
config,
|
|
1343
|
-
...(eventOut === undefined ? {} : { eventOut }),
|
|
1344
|
-
});
|
|
1224
|
+
result = await change(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1345
1225
|
} catch (error) {
|
|
1346
1226
|
const usageError = error instanceof UsageError;
|
|
1347
1227
|
env.err(String(error?.message ?? error));
|
|
@@ -1371,12 +1251,9 @@ async function runChange(options, { cwd, env }) {
|
|
|
1371
1251
|
env.out(report);
|
|
1372
1252
|
}
|
|
1373
1253
|
|
|
1374
|
-
// The verdict fold `changeCommand` computed,
|
|
1375
|
-
//
|
|
1376
|
-
return
|
|
1377
|
-
{ ok: EXIT.ok, findings: EXIT.violations, "no-verdict": EXIT.error }[result.status] ??
|
|
1378
|
-
EXIT.error
|
|
1379
|
-
);
|
|
1254
|
+
// The verdict fold `changeCommand` computed, returned as the command
|
|
1255
|
+
// carries it.
|
|
1256
|
+
return result.exitCode;
|
|
1380
1257
|
}
|
|
1381
1258
|
|
|
1382
1259
|
/**
|
|
@@ -1400,25 +1277,7 @@ async function runWaivers(options, { cwd, env }) {
|
|
|
1400
1277
|
|
|
1401
1278
|
let result;
|
|
1402
1279
|
try {
|
|
1403
|
-
|
|
1404
|
-
{ cwd },
|
|
1405
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1406
|
-
);
|
|
1407
|
-
|
|
1408
|
-
// The waivers surface is part of the run's boundary law, so the law is
|
|
1409
|
-
// loaded the same way `check` loads it (`resolvePolicy`) and `--config`
|
|
1410
|
-
// wins the same way — resolved against the working directory, never
|
|
1411
|
-
// against this tool's own location, and a `profiles` registry resolves
|
|
1412
|
-
// `--config`/`boundaryConfig` as a profile NAME the same way `check`
|
|
1413
|
-
// does. A malformed law throws here, exit 3, exactly as in `check`.
|
|
1414
|
-
const { config, source } = await resolvePolicy(options, commandContext, cwd);
|
|
1415
|
-
|
|
1416
|
-
// `source` rides along for one job: `waiversCommand` subtracts the law's
|
|
1417
|
-
// own file from the unowned-file set the `coverage.unowned` acceptances
|
|
1418
|
-
// are matched against, exactly as `check` does — the law is not source
|
|
1419
|
-
// judged by the law (`../src/commands/context.mjs`'s
|
|
1420
|
-
// `unownedGapWithoutRunConfiguration`).
|
|
1421
|
-
result = await waiversCommand(commandContext, config, { policySource: source });
|
|
1280
|
+
result = await waivers(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1422
1281
|
} catch (error) {
|
|
1423
1282
|
const usageError = error instanceof UsageError;
|
|
1424
1283
|
env.err(String(error?.message ?? error));
|
|
@@ -1471,8 +1330,9 @@ async function runWaivers(options, { cwd, env }) {
|
|
|
1471
1330
|
* sits at the tail of this function, and `../src/commands/fitness.mjs` states
|
|
1472
1331
|
* the posture — a failing fitness function is a finding, not a print job
|
|
1473
1332
|
* (D-09). `check` folds the same `fail` into its own exit 1 by presence, so the
|
|
1474
|
-
* two faces agree;
|
|
1475
|
-
*
|
|
1333
|
+
* two faces agree; the verbs whose verdict carries that code are `check`,
|
|
1334
|
+
* `fitness`, `delta` and `change`, plus the `rules verify` artifact-integrity
|
|
1335
|
+
* fold (`docs/reference/exit-codes.md` owns the roster).
|
|
1476
1336
|
*
|
|
1477
1337
|
* @param {{format: string, output: string|null, config: string|null, paths: string[]}} options
|
|
1478
1338
|
* @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
|
|
@@ -1486,24 +1346,7 @@ async function runFitness(options, { cwd, env }) {
|
|
|
1486
1346
|
|
|
1487
1347
|
let result;
|
|
1488
1348
|
try {
|
|
1489
|
-
|
|
1490
|
-
{ cwd },
|
|
1491
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1492
|
-
);
|
|
1493
|
-
|
|
1494
|
-
// Fitness is part of the run's boundary law, so the law is loaded the same
|
|
1495
|
-
// way `check` loads it (`resolvePolicy`) and `--config` wins the same
|
|
1496
|
-
// way — resolved against the working directory, never against this
|
|
1497
|
-
// tool's own location, profile-aware the same way `check` is. A malformed
|
|
1498
|
-
// law throws here, exit 3, exactly as in `check`. A profile's `block` may
|
|
1499
|
-
// carry a `fitness` key (`docs/concepts/profiles.md` now names four
|
|
1500
|
-
// block keys, fitness among them), so a profile-selected workspace folds
|
|
1501
|
-
// the declared functions the same way a file-selected one does — a
|
|
1502
|
-
// profile that declares none reaches `fitnessCommand`'s own "declares no
|
|
1503
|
-
// fitness functions" refusal below rather than a config-loading failure.
|
|
1504
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1505
|
-
|
|
1506
|
-
result = await fitnessCommand(commandContext, { config });
|
|
1349
|
+
result = await fitness(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1507
1350
|
} catch (error) {
|
|
1508
1351
|
const usageError = error instanceof UsageError;
|
|
1509
1352
|
env.err(String(error?.message ?? error));
|
|
@@ -1532,13 +1375,10 @@ async function runFitness(options, { cwd, env }) {
|
|
|
1532
1375
|
}
|
|
1533
1376
|
|
|
1534
1377
|
// `fitness` is a verdict, not a print job (D-09): `fail` exits 1, `unknown`
|
|
1535
|
-
// exits 3,
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
return
|
|
1539
|
-
{ ok: EXIT.ok, findings: EXIT.violations, "no-verdict": EXIT.error }[result.status] ??
|
|
1540
|
-
EXIT.error
|
|
1541
|
-
);
|
|
1378
|
+
// exits 3, a run whose every function is `pass` (or not applicable) exits 0.
|
|
1379
|
+
// The command's return carries the pair the envelope asserts; this return
|
|
1380
|
+
// is that verdict, unmodified.
|
|
1381
|
+
return result.exitCode;
|
|
1542
1382
|
}
|
|
1543
1383
|
|
|
1544
1384
|
/**
|
|
@@ -1564,17 +1404,11 @@ async function runImpact(options, { cwd, env }) {
|
|
|
1564
1404
|
|
|
1565
1405
|
let result;
|
|
1566
1406
|
try {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
// Load the boundary config when --config is given or when the workspace
|
|
1573
|
-
// declares one, so constraint-impact analysis is computed — profile-aware
|
|
1574
|
-
// the same way `check` is (`resolvePolicy`).
|
|
1575
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1576
|
-
|
|
1577
|
-
result = impactCommand(projectName, commandContext, config);
|
|
1407
|
+
result = await impact(projectName, options, {
|
|
1408
|
+
cwd,
|
|
1409
|
+
readGraph: env.readGraph,
|
|
1410
|
+
listFiles: env.listFiles,
|
|
1411
|
+
});
|
|
1578
1412
|
} catch (error) {
|
|
1579
1413
|
const usageError = error instanceof UsageError;
|
|
1580
1414
|
env.err(String(error?.message ?? error));
|
|
@@ -1647,14 +1481,11 @@ async function runScenario(options, { cwd, env }) {
|
|
|
1647
1481
|
|
|
1648
1482
|
let result;
|
|
1649
1483
|
try {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1656
|
-
|
|
1657
|
-
result = scenarioCommand(projectName, scenarioJson, commandContext, config);
|
|
1484
|
+
result = await scenario(projectName, scenarioJson, options, {
|
|
1485
|
+
cwd,
|
|
1486
|
+
readGraph: env.readGraph,
|
|
1487
|
+
listFiles: env.listFiles,
|
|
1488
|
+
});
|
|
1658
1489
|
} catch (error) {
|
|
1659
1490
|
const usageError = error instanceof UsageError;
|
|
1660
1491
|
env.err(String(error?.message ?? error));
|
|
@@ -1702,22 +1533,9 @@ async function runExplain(options, { cwd, env }) {
|
|
|
1702
1533
|
return EXIT.usage;
|
|
1703
1534
|
}
|
|
1704
1535
|
|
|
1705
|
-
const site = options.paths[0];
|
|
1706
|
-
|
|
1707
1536
|
let result;
|
|
1708
1537
|
try {
|
|
1709
|
-
|
|
1710
|
-
{ cwd },
|
|
1711
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1712
|
-
);
|
|
1713
|
-
|
|
1714
|
-
// The config's location is a separate fact from the workspace root.
|
|
1715
|
-
// Same loading logic as `check` (`resolvePolicy`) — a `--config`
|
|
1716
|
-
// overrides the workspace's own `boundaryConfig`, profile-aware the same
|
|
1717
|
-
// way `check` is.
|
|
1718
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1719
|
-
|
|
1720
|
-
result = explainCommand(site, commandContext, config);
|
|
1538
|
+
result = await explain(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1721
1539
|
} catch (error) {
|
|
1722
1540
|
const usageError = error instanceof UsageError;
|
|
1723
1541
|
env.err(String(error?.message ?? error));
|
|
@@ -1768,36 +1586,11 @@ async function runContextCommand(options, { cwd, env }) {
|
|
|
1768
1586
|
return EXIT.usage;
|
|
1769
1587
|
}
|
|
1770
1588
|
|
|
1771
|
-
const projectName = options.paths[0];
|
|
1772
|
-
const scopePaths = options.plan ? options.paths.slice(1) : [];
|
|
1773
|
-
|
|
1774
1589
|
let result;
|
|
1775
1590
|
try {
|
|
1776
|
-
// The command context is resolved over the WHOLE workspace. Scoping by
|
|
1777
|
-
// path is the plan command's decision (which projects the change touches),
|
|
1778
|
-
// not the preamble's: the rule verdict and the architecture snapshot must
|
|
1779
|
-
// be over the whole tree, and only reporting is narrowed. Passing no paths
|
|
1780
|
-
// here keeps the non-plan `context` path byte-for-byte identical to before.
|
|
1781
|
-
const commandContext = resolveCommandContext(
|
|
1782
|
-
{ cwd },
|
|
1783
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1784
|
-
);
|
|
1785
|
-
|
|
1786
|
-
// The config's location is a separate fact from the workspace root.
|
|
1787
|
-
// Same loading logic as `check` and `explain` (`resolvePolicy`) — a
|
|
1788
|
-
// `--config` overrides the workspace's own `boundaryConfig`,
|
|
1789
|
-
// profile-aware the same way `check` is.
|
|
1790
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1791
|
-
|
|
1792
|
-
const historyDir = options.historyDir
|
|
1793
|
-
? isAbsolute(options.historyDir)
|
|
1794
|
-
? options.historyDir
|
|
1795
|
-
: resolve(cwd, options.historyDir)
|
|
1796
|
-
: null;
|
|
1797
|
-
|
|
1798
1591
|
result = options.plan
|
|
1799
|
-
? await
|
|
1800
|
-
:
|
|
1592
|
+
? await planContext(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles })
|
|
1593
|
+
: await context(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1801
1594
|
} catch (error) {
|
|
1802
1595
|
const usageError = error instanceof UsageError;
|
|
1803
1596
|
env.err(String(error?.message ?? error));
|
|
@@ -1824,22 +1617,26 @@ async function runContextCommand(options, { cwd, env }) {
|
|
|
1824
1617
|
* `adr`'s `run`: reads the ADR registry at the workspace root and renders it.
|
|
1825
1618
|
*
|
|
1826
1619
|
* The registry lives in `docs/adr/` in the tree being described, not in this
|
|
1827
|
-
* package's own tree,
|
|
1828
|
-
* the
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1834
|
-
*
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
1620
|
+
* package's own tree, and the preamble it needs — the root walked up from
|
|
1621
|
+
* `cwd`, the tracked file list — is the one `adrForWorkspace`
|
|
1622
|
+
* (`./src/commands/adr-for-workspace.mjs`) composes, the same composition
|
|
1623
|
+
* the MCP history adapter runs. This driver decides none of it: it words the
|
|
1624
|
+
* no-workspace refusal (`adrForWorkspace` returns the `null` and holds no
|
|
1625
|
+
* message of its own) and maps the command's throws. `adr` never exits 1: a
|
|
1626
|
+
* description of what is recorded is never a finding. An unreadable registry
|
|
1627
|
+
* (a malformed record, an unreadable file, a bad filename) throws → exit 3;
|
|
1628
|
+
* an id the user asked about that the registry does not know → exit 3, the
|
|
1629
|
+
* invariant.
|
|
1630
|
+
*
|
|
1631
|
+
* The tracked-file list is injected the way every other command's is —
|
|
1632
|
+
* `env.listFiles`, so a test can drive the read over a fixture tree with no
|
|
1633
|
+
* git — and reaches `adrCommand` through the driver, so the registry
|
|
1634
|
+
* resolves only git-tracked records (`src/governance/adr-registry.mjs`'s
|
|
1838
1635
|
* header). A `git ls-files` failure here throws the same as any other
|
|
1839
1636
|
* unreadable registry, mapped to exit 3 below.
|
|
1840
1637
|
*
|
|
1841
1638
|
* @param {{format: string, output: string|null, paths: string[]}} options
|
|
1842
|
-
* @param {{cwd: string, env: {out: Function, err: Function, listFiles?:
|
|
1639
|
+
* @param {{cwd: string, env: {out: Function, err: Function, listFiles?: Function}}} runContext
|
|
1843
1640
|
* @returns {Promise<number>}
|
|
1844
1641
|
*/
|
|
1845
1642
|
async function runAdr(options, { cwd, env }) {
|
|
@@ -1851,23 +1648,20 @@ async function runAdr(options, { cwd, env }) {
|
|
|
1851
1648
|
return EXIT.usage;
|
|
1852
1649
|
}
|
|
1853
1650
|
|
|
1854
|
-
const root = resolveWorkspaceRootForUsage(cwd);
|
|
1855
|
-
if (root === null) {
|
|
1856
|
-
env.err(
|
|
1857
|
-
`archkeep: adr needs a workspace root — no nx.json, archkeep.json, or ` +
|
|
1858
|
-
`.moon/workspace.yml marker found walking up from ${cwd}`,
|
|
1859
|
-
);
|
|
1860
|
-
return EXIT.error;
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
1651
|
let result;
|
|
1864
1652
|
try {
|
|
1865
|
-
|
|
1866
|
-
result = adrCommand(root, { id: options.paths[0] }, { tracked });
|
|
1653
|
+
result = adrForWorkspace({ cwd }, { id: options.paths[0] }, { listFiles: env.listFiles });
|
|
1867
1654
|
} catch (error) {
|
|
1868
1655
|
env.err(String(error?.message ?? error));
|
|
1869
1656
|
return EXIT.error;
|
|
1870
1657
|
}
|
|
1658
|
+
if (result === null) {
|
|
1659
|
+
env.err(
|
|
1660
|
+
`archkeep: adr needs a workspace root — no nx.json, archkeep.json, or ` +
|
|
1661
|
+
`.moon/workspace.yml marker found walking up from ${cwd}`,
|
|
1662
|
+
);
|
|
1663
|
+
return EXIT.error;
|
|
1664
|
+
}
|
|
1871
1665
|
|
|
1872
1666
|
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
1873
1667
|
|
|
@@ -1913,20 +1707,7 @@ async function runDecisions(options, { cwd, env }) {
|
|
|
1913
1707
|
|
|
1914
1708
|
let result;
|
|
1915
1709
|
try {
|
|
1916
|
-
|
|
1917
|
-
{ cwd },
|
|
1918
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1919
|
-
);
|
|
1920
|
-
|
|
1921
|
-
// ONE law for the chain, resolved exactly like `report` — the Fitness leg
|
|
1922
|
-
// reads this law's declared gates, so a `--config` override must reach it.
|
|
1923
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1924
|
-
|
|
1925
|
-
const intent = commandContext.tracked.includes(INTENT_FILE)
|
|
1926
|
-
? await loadIntent(commandContext.root, { tracked: commandContext.tracked })
|
|
1927
|
-
: null;
|
|
1928
|
-
|
|
1929
|
-
result = decisionsCommand(options.paths[0], commandContext, config, { intent });
|
|
1710
|
+
result = await decisions(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1930
1711
|
} catch (error) {
|
|
1931
1712
|
const usageError = error instanceof UsageError;
|
|
1932
1713
|
env.err(String(error?.message ?? error));
|
|
@@ -2014,7 +1795,12 @@ async function runRules(options, { cwd, env }) {
|
|
|
2014
1795
|
env.out(report);
|
|
2015
1796
|
}
|
|
2016
1797
|
|
|
2017
|
-
// Exit codes:
|
|
1798
|
+
// Exit codes: `verify`'s return carries the pair its status asserts (0 ok,
|
|
1799
|
+
// 1 findings, 3 could-not-look). The descriptive subcommands (`list`,
|
|
1800
|
+
// `info`, `add`) expose no exit field — their statuses are `ok` or a
|
|
1801
|
+
// no-verdict class only, never findings — so this ladder keeps their exits
|
|
1802
|
+
// exactly as they were.
|
|
1803
|
+
if (subcommand === "verify") return result.exitCode;
|
|
2018
1804
|
if (result.status === "ok") return EXIT.ok;
|
|
2019
1805
|
if (result.status === "findings") return EXIT.violations;
|
|
2020
1806
|
return EXIT.error;
|
|
@@ -2040,56 +1826,15 @@ async function runHistory(options, { cwd, env }) {
|
|
|
2040
1826
|
return EXIT.usage;
|
|
2041
1827
|
}
|
|
2042
1828
|
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
// A self-footgun guard: writing the history report back into the very
|
|
2048
|
-
// directory `history` reads would poison every later run (the report envelope
|
|
2049
|
-
// is a `history` envelope, which `parseBaseline` refuses as a non-`graph`
|
|
2050
|
-
// snapshot). Refuse loudly instead of eventually failing on a poisoned dir.
|
|
2051
|
-
if (options.output) {
|
|
2052
|
-
// `resolve()` on the absolute branch too — not just the raw path — the
|
|
2053
|
-
// same normalization `writeOutputReport` applies, so an absolute
|
|
2054
|
-
// `--output` carrying a `..` segment that resolves INTO the history
|
|
2055
|
-
// directory cannot slip past this guard unnormalized.
|
|
2056
|
-
const outputAbs = isAbsolute(options.output)
|
|
2057
|
-
? resolve(options.output)
|
|
2058
|
-
: resolve(cwd, options.output);
|
|
2059
|
-
if (dirname(outputAbs) === dir) {
|
|
2060
|
-
env.err(
|
|
2061
|
-
`archkeep: --output '${options.output}' is inside the history directory '${dir}' — ` +
|
|
2062
|
-
`writing the report there would be read back as a snapshot on the next run. ` +
|
|
2063
|
-
`Write it somewhere else.`,
|
|
2064
|
-
);
|
|
2065
|
-
return EXIT.usage;
|
|
2066
|
-
}
|
|
1829
|
+
const refusal = historyOutputRefusal(options, cwd);
|
|
1830
|
+
if (refusal) {
|
|
1831
|
+
env.err(refusal);
|
|
1832
|
+
return EXIT.usage;
|
|
2067
1833
|
}
|
|
2068
1834
|
|
|
2069
1835
|
let result;
|
|
2070
1836
|
try {
|
|
2071
|
-
|
|
2072
|
-
{ cwd },
|
|
2073
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
2074
|
-
);
|
|
2075
|
-
|
|
2076
|
-
// The boundary law's fingerprint when the workspace declares one, so a
|
|
2077
|
-
// captured snapshot records the policy it was taken under — the same
|
|
2078
|
-
// config loading `graph` uses (`resolvePolicy`, profile-aware the same
|
|
2079
|
-
// way `check` is), kept in one place so a capture and a standalone
|
|
2080
|
-
// `graph` never disagree about the current policy.
|
|
2081
|
-
let fingerprint = null;
|
|
2082
|
-
if (options.capture) {
|
|
2083
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
2084
|
-
if (config) {
|
|
2085
|
-
fingerprint = computePolicyFingerprint(config);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
result = historyCommand(dir, commandContext, {
|
|
2090
|
-
capture: options.capture,
|
|
2091
|
-
policyFingerprint: fingerprint,
|
|
2092
|
-
});
|
|
1837
|
+
result = await history(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2093
1838
|
} catch (error) {
|
|
2094
1839
|
const usageError = error instanceof UsageError;
|
|
2095
1840
|
env.err(String(error?.message ?? error));
|
|
@@ -2139,22 +1884,10 @@ async function runTrajectory(options, { cwd, env }) {
|
|
|
2139
1884
|
? resolve(options.paths[0])
|
|
2140
1885
|
: resolve(cwd, options.paths[0]);
|
|
2141
1886
|
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
if (options.output) {
|
|
2147
|
-
const outputAbs = isAbsolute(options.output)
|
|
2148
|
-
? resolve(options.output)
|
|
2149
|
-
: resolve(cwd, options.output);
|
|
2150
|
-
if (dirname(outputAbs) === dir) {
|
|
2151
|
-
env.err(
|
|
2152
|
-
`archkeep: --output '${options.output}' is inside the history directory '${dir}' — ` +
|
|
2153
|
-
`writing the report there would be read back as a snapshot on the next run. ` +
|
|
2154
|
-
`Write it somewhere else.`,
|
|
2155
|
-
);
|
|
2156
|
-
return EXIT.usage;
|
|
2157
|
-
}
|
|
1887
|
+
const refusal = trajectoryOutputRefusal(options, cwd);
|
|
1888
|
+
if (refusal) {
|
|
1889
|
+
env.err(refusal);
|
|
1890
|
+
return EXIT.usage;
|
|
2158
1891
|
}
|
|
2159
1892
|
|
|
2160
1893
|
let result;
|
|
@@ -2293,25 +2026,9 @@ async function runDebt(options, { cwd, env }) {
|
|
|
2293
2026
|
return EXIT.usage;
|
|
2294
2027
|
}
|
|
2295
2028
|
|
|
2296
|
-
const dir = isAbsolute(options.paths[0]) ? options.paths[0] : resolve(cwd, options.paths[0]);
|
|
2297
|
-
|
|
2298
2029
|
let result;
|
|
2299
2030
|
try {
|
|
2300
|
-
|
|
2301
|
-
{ cwd },
|
|
2302
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
2303
|
-
);
|
|
2304
|
-
|
|
2305
|
-
// The boundary law the ledger ages waivers against — resolved the same way
|
|
2306
|
-
// `graph` and `diff` resolve it (`resolvePolicy`), so a `debt` run and a
|
|
2307
|
-
// `check` run never disagree about the current suppressions, and a
|
|
2308
|
-
// profile-selected workspace resolves the same way `check` does.
|
|
2309
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
2310
|
-
|
|
2311
|
-
result = await debtCommand(dir, commandContext, {
|
|
2312
|
-
config,
|
|
2313
|
-
events: options.events,
|
|
2314
|
-
});
|
|
2031
|
+
result = await debt(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2315
2032
|
} catch (error) {
|
|
2316
2033
|
const usageError = error instanceof UsageError;
|
|
2317
2034
|
env.err(String(error?.message ?? error));
|
|
@@ -2402,22 +2119,17 @@ async function runDiscover(options, { cwd, env }) {
|
|
|
2402
2119
|
}
|
|
2403
2120
|
|
|
2404
2121
|
if (options.writeIntent) {
|
|
2405
|
-
// The one write that can turn a proposal into the law `check` gates on
|
|
2406
|
-
//
|
|
2407
|
-
//
|
|
2408
|
-
//
|
|
2409
|
-
|
|
2410
|
-
if (
|
|
2411
|
-
env.err(
|
|
2412
|
-
`archkeep: ${options.writeIntent} already exists, and a proposal must never ` +
|
|
2413
|
-
`silently replace what is there. Move or delete the file first, then run this again.`,
|
|
2414
|
-
);
|
|
2122
|
+
// The one write that can turn a proposal into the law `check` gates on —
|
|
2123
|
+
// the refusal DECISION is discover's own (`intentWriteRefusal`, beside the
|
|
2124
|
+
// proposal it protects, the way `historyOutputRefusal` sits with history);
|
|
2125
|
+
// this driver keeps the mechanics only.
|
|
2126
|
+
const intentRefusal = intentWriteRefusal(options.writeIntent, { exists: existsSync });
|
|
2127
|
+
if (intentRefusal) {
|
|
2128
|
+
env.err(intentRefusal);
|
|
2415
2129
|
return EXIT.error;
|
|
2416
2130
|
}
|
|
2417
2131
|
try {
|
|
2418
|
-
const intentJson =
|
|
2419
|
-
// `wx` refuses the file materializing between the check above and this
|
|
2420
|
-
// write, so the refusal above cannot be raced past.
|
|
2132
|
+
const intentJson = intentJsonFromProposal(result.proposal);
|
|
2421
2133
|
writeFileSync(options.writeIntent, intentJson, { encoding: "utf-8", flag: "wx" });
|
|
2422
2134
|
env.err(`archkeep: proposed architecture written to ${options.writeIntent}`);
|
|
2423
2135
|
env.err(
|
|
@@ -2434,8 +2146,6 @@ async function runDiscover(options, { cwd, env }) {
|
|
|
2434
2146
|
}
|
|
2435
2147
|
|
|
2436
2148
|
/**
|
|
2437
|
-
* `health`'s `run`: resolves the command context, drives `healthCommand`,
|
|
2438
|
-
* writes the report where it belongs, and returns the process's exit code.
|
|
2439
2149
|
* `health`'s `run`: resolves the command context, drives `healthCommand`,
|
|
2440
2150
|
* writes the report where it belongs, and returns the process's exit code.
|
|
2441
2151
|
*
|
|
@@ -2456,31 +2166,9 @@ async function runHealth(options, { cwd, env }) {
|
|
|
2456
2166
|
return EXIT.usage;
|
|
2457
2167
|
}
|
|
2458
2168
|
|
|
2459
|
-
const trendDir =
|
|
2460
|
-
options.paths.length === 1
|
|
2461
|
-
? isAbsolute(options.paths[0])
|
|
2462
|
-
? options.paths[0]
|
|
2463
|
-
: resolve(cwd, options.paths[0])
|
|
2464
|
-
: null;
|
|
2465
|
-
|
|
2466
2169
|
let result;
|
|
2467
2170
|
try {
|
|
2468
|
-
|
|
2469
|
-
{ cwd },
|
|
2470
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
2471
|
-
);
|
|
2472
|
-
|
|
2473
|
-
// The boundary law and the intent, the same loading every command does
|
|
2474
|
-
// (`resolvePolicy`) — a `--config` overrides the workspace's own
|
|
2475
|
-
// `boundaryConfig`, profile-aware the same way `check` is, and the
|
|
2476
|
-
// intent is the tracked root `architecture-intent.json` (or absent).
|
|
2477
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
2478
|
-
|
|
2479
|
-
const intent = commandContext.tracked.includes(INTENT_FILE)
|
|
2480
|
-
? await loadIntent(commandContext.root, { tracked: commandContext.tracked })
|
|
2481
|
-
: null;
|
|
2482
|
-
|
|
2483
|
-
result = healthCommand(commandContext, { config, intent, trendDir });
|
|
2171
|
+
result = await health(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2484
2172
|
} catch (error) {
|
|
2485
2173
|
const usageError = error instanceof UsageError;
|
|
2486
2174
|
env.err(String(error?.message ?? error));
|
|
@@ -2528,47 +2216,20 @@ async function runReport(options, { cwd, env }) {
|
|
|
2528
2216
|
return EXIT.usage;
|
|
2529
2217
|
}
|
|
2530
2218
|
|
|
2531
|
-
const trendDir =
|
|
2532
|
-
options.paths.length === 1
|
|
2533
|
-
? isAbsolute(options.paths[0])
|
|
2534
|
-
? options.paths[0]
|
|
2535
|
-
: resolve(cwd, options.paths[0])
|
|
2536
|
-
: null;
|
|
2537
|
-
|
|
2538
2219
|
let result;
|
|
2539
2220
|
try {
|
|
2540
|
-
|
|
2541
|
-
{ cwd },
|
|
2542
|
-
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
2543
|
-
);
|
|
2544
|
-
|
|
2545
|
-
// ONE law for the whole document — resolved exactly the way `check` and
|
|
2546
|
-
// `health` resolve theirs, and handed to every surface the report
|
|
2547
|
-
// composes, so no two sections can cite different laws.
|
|
2548
|
-
const { config, source } = await resolvePolicy(options, commandContext, cwd);
|
|
2549
|
-
|
|
2550
|
-
const intent = commandContext.tracked.includes(INTENT_FILE)
|
|
2551
|
-
? await loadIntent(commandContext.root, { tracked: commandContext.tracked })
|
|
2552
|
-
: null;
|
|
2553
|
-
|
|
2554
|
-
result = await reportCommand(commandContext, {
|
|
2555
|
-
config,
|
|
2556
|
-
intent,
|
|
2557
|
-
trendDir,
|
|
2558
|
-
policySource: source,
|
|
2559
|
-
});
|
|
2221
|
+
result = await report(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2560
2222
|
} catch (error) {
|
|
2561
2223
|
const usageError = error instanceof UsageError;
|
|
2562
2224
|
env.err(String(error?.message ?? error));
|
|
2563
2225
|
return usageError ? EXIT.usage : EXIT.error;
|
|
2564
2226
|
}
|
|
2565
|
-
|
|
2566
|
-
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
2227
|
+
const rendered = options.format === "json" ? result.report.json : result.report.text;
|
|
2567
2228
|
|
|
2568
2229
|
if (options.output) {
|
|
2569
2230
|
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring owns
|
|
2570
2231
|
// the mechanism and the threat it closes.
|
|
2571
|
-
const reportText =
|
|
2232
|
+
const reportText = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
|
|
2572
2233
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
2573
2234
|
// The confirmation names the no-verdict case, so a reader who only
|
|
2574
2235
|
// glances at stderr cannot mistake a written document for an established
|
|
@@ -2581,7 +2242,7 @@ async function runReport(options, { cwd, env }) {
|
|
|
2581
2242
|
: `NO VERDICT — ${gaps} surface${gaps === 1 ? "" : "s"} could not be inspected`;
|
|
2582
2243
|
env.err(`archkeep: report complete (${verdict}) → ${options.output}`);
|
|
2583
2244
|
} else {
|
|
2584
|
-
env.out(
|
|
2245
|
+
env.out(rendered);
|
|
2585
2246
|
}
|
|
2586
2247
|
|
|
2587
2248
|
// Descriptive: 0 when every surface reached a verdict, 3 when any evidence
|