@ecoma-io/archkeep 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +166 -506
- package/commands.mjs +7 -1
- package/package.json +1 -1
- package/src/architecture-intent/judge.mjs +1 -1
- package/src/architecture-intent/model.mjs +34 -0
- package/src/canonical.mjs +2 -1
- package/src/commands/adr-for-workspace.mjs +63 -0
- package/src/commands/analyze-capability.mjs +13 -0
- package/src/commands/change-intent.mjs +11 -0
- package/src/commands/change.mjs +247 -34
- package/src/commands/check-capability.mjs +13 -0
- package/src/commands/check.mjs +13 -3
- package/src/commands/compare-capability.mjs +20 -0
- package/src/commands/completeness.mjs +12 -0
- package/src/commands/context-command.mjs +31 -0
- package/src/commands/coverage-verdict.mjs +15 -2
- package/src/commands/debt.mjs +30 -0
- package/src/commands/decisions.mjs +59 -10
- package/src/commands/delta.mjs +263 -49
- package/src/commands/diff.mjs +45 -0
- package/src/commands/discover.mjs +37 -0
- package/src/commands/drift.mjs +45 -0
- package/src/commands/evolution.mjs +22 -2
- package/src/commands/explain-capability.mjs +11 -0
- package/src/commands/explain.mjs +24 -0
- package/src/commands/fitness.mjs +119 -10
- package/src/commands/govern-capability.mjs +16 -0
- package/src/commands/graph.mjs +39 -0
- package/src/commands/health.mjs +34 -0
- package/src/commands/history.mjs +85 -2
- package/src/commands/impact.mjs +23 -0
- package/src/commands/inspect-capability.mjs +16 -0
- package/src/commands/plan-context-command.mjs +35 -0
- package/src/commands/policy.mjs +42 -4
- package/src/commands/provenance-command.mjs +15 -24
- package/src/commands/report.mjs +38 -0
- package/src/commands/rules-capability.mjs +16 -0
- package/src/commands/rules.mjs +3 -1
- package/src/commands/scenario.mjs +23 -0
- package/src/commands/trajectory.mjs +35 -0
- package/src/commands/waivers.mjs +30 -0
- package/src/corpus/goldens/.gitkeep +0 -0
- package/src/corpus/goldens/adr.json +62 -0
- package/src/corpus/goldens/adr.text +6 -0
- package/src/corpus/goldens/change.json +98 -0
- package/src/corpus/goldens/change.text +6 -0
- package/src/corpus/goldens/check.json +90 -0
- package/src/corpus/goldens/check.sarif +479 -0
- package/src/corpus/goldens/check.text +9 -0
- package/src/corpus/goldens/context.json +47 -0
- package/src/corpus/goldens/context.text +6 -0
- package/src/corpus/goldens/debt.json +59 -0
- package/src/corpus/goldens/debt.text +7 -0
- package/src/corpus/goldens/decisions.json +74 -0
- package/src/corpus/goldens/decisions.text +7 -0
- package/src/corpus/goldens/delta.json +94 -0
- package/src/corpus/goldens/delta.sarif +472 -0
- package/src/corpus/goldens/delta.text +5 -0
- package/src/corpus/goldens/diff.json +58 -0
- package/src/corpus/goldens/diff.text +4 -0
- package/src/corpus/goldens/discover.json +77 -0
- package/src/corpus/goldens/discover.text +10 -0
- package/src/corpus/goldens/drift.json +43 -0
- package/src/corpus/goldens/drift.text +3 -0
- package/src/corpus/goldens/evolution.json +328 -0
- package/src/corpus/goldens/evolution.text +32 -0
- package/src/corpus/goldens/explain.json +60 -0
- package/src/corpus/goldens/explain.text +7 -0
- package/src/corpus/goldens/fitness.json +45 -0
- package/src/corpus/goldens/fitness.text +3 -0
- package/src/corpus/goldens/graph.json +78 -0
- package/src/corpus/goldens/graph.text +10 -0
- package/src/corpus/goldens/health.json +72 -0
- package/src/corpus/goldens/health.text +10 -0
- package/src/corpus/goldens/help.text +104 -0
- package/src/corpus/goldens/history.json +43 -0
- package/src/corpus/goldens/history.text +4 -0
- package/src/corpus/goldens/impact.json +327 -0
- package/src/corpus/goldens/impact.text +10 -0
- package/src/corpus/goldens/provenance.json +453 -0
- package/src/corpus/goldens/provenance.text +14 -0
- package/src/corpus/goldens/reconcile.json +168 -0
- package/src/corpus/goldens/reconcile.text +3 -0
- package/src/corpus/goldens/report.json +168 -0
- package/src/corpus/goldens/report.text +42 -0
- package/src/corpus/goldens/rules verify.json +0 -0
- package/src/corpus/goldens/rules verify.text +0 -0
- package/src/corpus/goldens/scenario.json +441 -0
- package/src/corpus/goldens/scenario.text +28 -0
- package/src/corpus/goldens/trajectory.json +83 -0
- package/src/corpus/goldens/trajectory.text +10 -0
- package/src/corpus/goldens/usage-error.text +105 -0
- package/src/corpus/goldens/waivers.json +40 -0
- package/src/corpus/goldens/waivers.text +1 -0
- package/src/corpus/goldens-lsp/initialize.result.json +15 -0
- package/src/corpus/goldens-lsp/publish-cleared-empty.json +4 -0
- package/src/corpus/goldens-lsp/publish-fixed-empty.json +5 -0
- package/src/corpus/goldens-lsp/publish-unparseable.json +54 -0
- package/src/corpus/goldens-lsp/publish-violation.json +22 -0
- package/src/corpus/goldens-lsp/register-watched-files.json +72 -0
- package/src/go-work.mjs +8 -23
- package/src/governance/evolution-event.mjs +15 -0
- package/src/governance/evolution-store.mjs +59 -31
- package/src/governance/provenance-record.mjs +0 -152
- package/src/governance/verdict.mjs +26 -3
- package/src/intent/intent-manifest.json +14 -14
- package/src/lsp/server.mjs +5 -8
- package/src/lsp/workspace-index.mjs +55 -220
- package/src/options.mjs +10 -0
- package/src/providers/native/discover.mjs +13 -12
- package/src/providers/native/model.mjs +8 -3
- package/src/providers/nx-static.mjs +231 -0
- package/src/report/json.mjs +3 -2
- package/src/report/sarif.mjs +8 -3
- package/src/rules/edge-constraints.mjs +9 -0
- package/src/rules/index.mjs +26 -10
- package/src/rules/messages.mjs +64 -14
- package/src/rules/specifiers.mjs +21 -20
- package/src/tsconfig-paths.mjs +8 -14
- package/src/verdict.mjs +127 -16
package/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 (PD-8; `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,69 @@ 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 (PD-18, docs/architecture/refactor/DECISIONS.md).
|
|
109
|
+
// Non-verb helper modules keep their direct imports.
|
|
106
110
|
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
|
-
|
|
111
|
+
discoverCommand,
|
|
112
|
+
intentJsonFromProposal,
|
|
113
|
+
intentWriteRefusal,
|
|
114
|
+
} from "./src/commands/analyze-capability.mjs";
|
|
115
|
+
import { check, fitness, scenario, sortViolations } from "./src/commands/check-capability.mjs";
|
|
116
|
+
import {
|
|
117
|
+
captureBaseline,
|
|
118
|
+
change,
|
|
119
|
+
changeOutputRefusal,
|
|
120
|
+
delta,
|
|
121
|
+
diff,
|
|
122
|
+
drift,
|
|
123
|
+
evolutionCommand,
|
|
124
|
+
history,
|
|
125
|
+
historyOutputRefusal,
|
|
126
|
+
reconcileCommand,
|
|
127
|
+
trajectoryCommand,
|
|
128
|
+
trajectoryOutputRefusal,
|
|
129
|
+
} from "./src/commands/compare-capability.mjs";
|
|
130
|
+
import { explain } from "./src/commands/explain-capability.mjs";
|
|
131
|
+
import { context, graph, health, impact, planContext } from "./src/commands/inspect-capability.mjs";
|
|
132
|
+
import {
|
|
133
|
+
debt,
|
|
134
|
+
decisions,
|
|
135
|
+
provenanceCommand,
|
|
136
|
+
report,
|
|
137
|
+
waivers,
|
|
138
|
+
} from "./src/commands/govern-capability.mjs";
|
|
134
139
|
import {
|
|
135
140
|
rulesAddCommand,
|
|
136
141
|
rulesInfoCommand,
|
|
137
142
|
rulesListCommand,
|
|
138
143
|
rulesVerifyCommand,
|
|
139
|
-
} from "./src/commands/rules.mjs";
|
|
140
|
-
import {
|
|
141
|
-
import {
|
|
144
|
+
} from "./src/commands/rules-capability.mjs";
|
|
145
|
+
import { nativePolicyOptions } from "./src/commands/policy.mjs";
|
|
146
|
+
import {
|
|
147
|
+
DEFAULT_OPTIONS,
|
|
148
|
+
WORKSPACE_MARKERS,
|
|
149
|
+
markersAt,
|
|
150
|
+
resolveCommandContext,
|
|
151
|
+
} from "./src/commands/context.mjs";
|
|
152
|
+
import { adrForWorkspace } from "./src/commands/adr-for-workspace.mjs";
|
|
153
|
+
import { INTENT_FILE } from "./src/architecture-intent/model.mjs";
|
|
142
154
|
import { isProgramEntry } from "./src/entry-point.mjs";
|
|
143
|
-
import { readPluginOptions } from "./src/options.mjs";
|
|
144
|
-
import { EXIT
|
|
155
|
+
import { ARCHKEEP_MODEL_FILE, readPluginOptions } from "./src/options.mjs";
|
|
156
|
+
import { EXIT } from "./src/verdict.mjs";
|
|
145
157
|
|
|
146
|
-
import {
|
|
147
|
-
import { findWorkspaceRoot, listTrackedFiles } from "./src/workspace.mjs";
|
|
158
|
+
import { findWorkspaceRoot } from "./src/workspace.mjs";
|
|
148
159
|
|
|
149
160
|
/**
|
|
150
161
|
* Workspace-relative read from `root`, the same default `createWorkspace`
|
|
151
162
|
* builds when no reader is injected (`./src/workspace.mjs`) — duplicated
|
|
152
163
|
* rather than imported for the reason `./src/commands/context.mjs` carries its
|
|
153
164
|
* 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
|
|
165
|
+
* `Workspace` exists, to hand `nativePolicyOptions` a reader for
|
|
166
|
+
* `archkeep.json` itself. `check` no longer needs a copy of its own —
|
|
167
|
+
* `resolveCommandContext` owns that read now — which is why this is the only
|
|
168
|
+
* one left in this file.
|
|
157
169
|
*
|
|
158
170
|
* @param {string} root
|
|
159
171
|
* @returns {(path: string) => string|null}
|
|
@@ -338,20 +350,7 @@ function optionsForUsage(cwd) {
|
|
|
338
350
|
if (root === null) return DEFAULT_OPTIONS;
|
|
339
351
|
const { hasNx, hasNative } = markersAt(root);
|
|
340
352
|
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
|
-
};
|
|
353
|
+
return nativePolicyOptions(root, { readFile: readWorkspaceRoot(root) });
|
|
355
354
|
}
|
|
356
355
|
return readPluginOptions(root);
|
|
357
356
|
} catch {
|
|
@@ -771,9 +770,9 @@ async function runCheck(options, { cwd, env }) {
|
|
|
771
770
|
(result.intentFindings > 0
|
|
772
771
|
? `, ${result.intentFindings} architecture-intent finding${result.intentFindings === 1 ? "" : "s"}`
|
|
773
772
|
: "") +
|
|
774
|
-
// Fitness drives the exit code exactly like every count above it
|
|
775
|
-
//
|
|
776
|
-
//
|
|
773
|
+
// Fitness drives the exit code exactly like every count above it —
|
|
774
|
+
// omitting it here is what let a fitness-only failure log "0
|
|
775
|
+
// violations …" beside a non-zero exit.
|
|
777
776
|
(result.fitnessFail > 0
|
|
778
777
|
? `, ${result.fitnessFail} fitness function${result.fitnessFail === 1 ? "" : "s"} failed`
|
|
779
778
|
: "") +
|
|
@@ -781,9 +780,9 @@ async function runCheck(options, { cwd, env }) {
|
|
|
781
780
|
? `, ${result.fitnessUnknown} fitness function${result.fitnessUnknown === 1 ? "" : "s"} undetermined`
|
|
782
781
|
: "") +
|
|
783
782
|
// Custom rules drive the exit code exactly like every count above
|
|
784
|
-
// them
|
|
785
|
-
//
|
|
786
|
-
//
|
|
783
|
+
// them, so they are named here for the same reason fitness is: a
|
|
784
|
+
// custom-rule-only failure would otherwise log "0 violations …"
|
|
785
|
+
// beside a non-zero exit.
|
|
787
786
|
(result.customRuleFail > 0
|
|
788
787
|
? `, ${result.customRuleFail} custom rule${result.customRuleFail === 1 ? "" : "s"} failed`
|
|
789
788
|
: "") +
|
|
@@ -799,7 +798,11 @@ async function runCheck(options, { cwd, env }) {
|
|
|
799
798
|
env.out(result.report);
|
|
800
799
|
}
|
|
801
800
|
|
|
802
|
-
return
|
|
801
|
+
// `check`'s own verdict rides its return (`./src/commands/check.mjs`): the
|
|
802
|
+
// command computes the one exit authority, and the process returns it
|
|
803
|
+
// unmodified — a second fold here would be a second chance for the printed
|
|
804
|
+
// verdict and the process exit to disagree.
|
|
805
|
+
return result.exitCode;
|
|
803
806
|
}
|
|
804
807
|
|
|
805
808
|
/**
|
|
@@ -818,32 +821,7 @@ async function runGraph(options, { cwd, env }) {
|
|
|
818
821
|
|
|
819
822
|
let result;
|
|
820
823
|
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 });
|
|
824
|
+
result = await graph(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
847
825
|
} catch (error) {
|
|
848
826
|
const usageError = error instanceof UsageError;
|
|
849
827
|
env.err(String(error?.message ?? error));
|
|
@@ -890,26 +868,9 @@ async function runDiff(options, { cwd, env }) {
|
|
|
890
868
|
return EXIT.usage;
|
|
891
869
|
}
|
|
892
870
|
|
|
893
|
-
const baselinePath = isAbsolute(options.paths[0])
|
|
894
|
-
? options.paths[0]
|
|
895
|
-
: resolve(cwd, options.paths[0]);
|
|
896
|
-
|
|
897
871
|
let result;
|
|
898
872
|
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 });
|
|
873
|
+
result = await diff(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
913
874
|
} catch (error) {
|
|
914
875
|
const usageError = error instanceof UsageError;
|
|
915
876
|
env.err(String(error?.message ?? error));
|
|
@@ -974,18 +935,12 @@ async function runDelta(options, { cwd, env }) {
|
|
|
974
935
|
|
|
975
936
|
let result;
|
|
976
937
|
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
938
|
if (options.capture) {
|
|
988
|
-
const { text } =
|
|
939
|
+
const { text } = await captureBaseline(options, {
|
|
940
|
+
cwd,
|
|
941
|
+
readGraph: env.readGraph,
|
|
942
|
+
listFiles: env.listFiles,
|
|
943
|
+
});
|
|
989
944
|
if (options.output) {
|
|
990
945
|
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring
|
|
991
946
|
if (!writeOutputReport(options.output, text, env, cwd, options.config)) return EXIT.error;
|
|
@@ -997,13 +952,7 @@ async function runDelta(options, { cwd, env }) {
|
|
|
997
952
|
return EXIT.ok;
|
|
998
953
|
}
|
|
999
954
|
|
|
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
|
-
});
|
|
955
|
+
result = await delta(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1007
956
|
} catch (error) {
|
|
1008
957
|
const usageError = error instanceof UsageError;
|
|
1009
958
|
env.err(String(error?.message ?? error));
|
|
@@ -1053,13 +1002,10 @@ async function runDelta(options, { cwd, env }) {
|
|
|
1053
1002
|
);
|
|
1054
1003
|
}
|
|
1055
1004
|
|
|
1056
|
-
// The exit
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
return
|
|
1060
|
-
{ ok: EXIT.ok, findings: EXIT.violations, "no-verdict": EXIT.error }[result.status] ??
|
|
1061
|
-
EXIT.error
|
|
1062
|
-
);
|
|
1005
|
+
// The exit `deltaCommand` computed: a non-waived introduced violation is a
|
|
1006
|
+
// finding, an unclassifiable item is a no-verdict, anything else is clean —
|
|
1007
|
+
// the command's return carries the pair, the process returns it as is.
|
|
1008
|
+
return result.exitCode;
|
|
1063
1009
|
}
|
|
1064
1010
|
|
|
1065
1011
|
/**
|
|
@@ -1081,36 +1027,7 @@ async function runDrift(options, { cwd, env }) {
|
|
|
1081
1027
|
|
|
1082
1028
|
let result;
|
|
1083
1029
|
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 });
|
|
1030
|
+
result = await drift(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1114
1031
|
} catch (error) {
|
|
1115
1032
|
const usageError = error instanceof UsageError;
|
|
1116
1033
|
env.err(String(error?.message ?? error));
|
|
@@ -1154,7 +1071,9 @@ async function runDrift(options, { cwd, env }) {
|
|
|
1154
1071
|
* Provenance reads no graph and judges nothing — it describes where the run's
|
|
1155
1072
|
* facts came from and which governance rows carry an origin. It is
|
|
1156
1073
|
* fail-closed the way every descriptive command is: a malformed intent or
|
|
1157
|
-
* boundary config throws out of `provenanceCommand` → exit 3, so
|
|
1074
|
+
* boundary config throws out of `provenanceCommand` → exit 3, and so does
|
|
1075
|
+
* a law the shared ladder refuses (`./src/commands/policy.mjs` — e.g. a
|
|
1076
|
+
* `coverage` key in a native workspace's `boundaryConfig` file), so "rows
|
|
1158
1077
|
* unlisted" never reads as "rows attested".
|
|
1159
1078
|
*
|
|
1160
1079
|
* @param {{format: string, output: string|null, paths: string[]}} options
|
|
@@ -1293,55 +1212,15 @@ async function runChange(options, { cwd, env }) {
|
|
|
1293
1212
|
return EXIT.usage;
|
|
1294
1213
|
}
|
|
1295
1214
|
|
|
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
|
-
}
|
|
1215
|
+
const refusal = changeOutputRefusal(options, cwd);
|
|
1216
|
+
if (refusal) {
|
|
1217
|
+
env.err(refusal);
|
|
1218
|
+
return EXIT.usage;
|
|
1317
1219
|
}
|
|
1318
1220
|
|
|
1319
1221
|
let result;
|
|
1320
1222
|
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
|
-
});
|
|
1223
|
+
result = await change(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1345
1224
|
} catch (error) {
|
|
1346
1225
|
const usageError = error instanceof UsageError;
|
|
1347
1226
|
env.err(String(error?.message ?? error));
|
|
@@ -1371,12 +1250,9 @@ async function runChange(options, { cwd, env }) {
|
|
|
1371
1250
|
env.out(report);
|
|
1372
1251
|
}
|
|
1373
1252
|
|
|
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
|
-
);
|
|
1253
|
+
// The verdict fold `changeCommand` computed, returned as the command
|
|
1254
|
+
// carries it.
|
|
1255
|
+
return result.exitCode;
|
|
1380
1256
|
}
|
|
1381
1257
|
|
|
1382
1258
|
/**
|
|
@@ -1400,25 +1276,7 @@ async function runWaivers(options, { cwd, env }) {
|
|
|
1400
1276
|
|
|
1401
1277
|
let result;
|
|
1402
1278
|
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 });
|
|
1279
|
+
result = await waivers(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1422
1280
|
} catch (error) {
|
|
1423
1281
|
const usageError = error instanceof UsageError;
|
|
1424
1282
|
env.err(String(error?.message ?? error));
|
|
@@ -1471,8 +1329,9 @@ async function runWaivers(options, { cwd, env }) {
|
|
|
1471
1329
|
* sits at the tail of this function, and `../src/commands/fitness.mjs` states
|
|
1472
1330
|
* the posture — a failing fitness function is a finding, not a print job
|
|
1473
1331
|
* (D-09). `check` folds the same `fail` into its own exit 1 by presence, so the
|
|
1474
|
-
* two faces agree;
|
|
1475
|
-
*
|
|
1332
|
+
* two faces agree; the verbs whose verdict carries that code are `check`,
|
|
1333
|
+
* `fitness`, `delta` and `change`, plus the `rules verify` artifact-integrity
|
|
1334
|
+
* fold (PD-8; `docs/reference/exit-codes.md` owns the roster).
|
|
1476
1335
|
*
|
|
1477
1336
|
* @param {{format: string, output: string|null, config: string|null, paths: string[]}} options
|
|
1478
1337
|
* @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
|
|
@@ -1486,24 +1345,7 @@ async function runFitness(options, { cwd, env }) {
|
|
|
1486
1345
|
|
|
1487
1346
|
let result;
|
|
1488
1347
|
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 });
|
|
1348
|
+
result = await fitness(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1507
1349
|
} catch (error) {
|
|
1508
1350
|
const usageError = error instanceof UsageError;
|
|
1509
1351
|
env.err(String(error?.message ?? error));
|
|
@@ -1532,13 +1374,10 @@ async function runFitness(options, { cwd, env }) {
|
|
|
1532
1374
|
}
|
|
1533
1375
|
|
|
1534
1376
|
// `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
|
-
);
|
|
1377
|
+
// exits 3, a run whose every function is `pass` (or not applicable) exits 0.
|
|
1378
|
+
// The command's return carries the pair the envelope asserts; this return
|
|
1379
|
+
// is that verdict, unmodified.
|
|
1380
|
+
return result.exitCode;
|
|
1542
1381
|
}
|
|
1543
1382
|
|
|
1544
1383
|
/**
|
|
@@ -1564,17 +1403,11 @@ async function runImpact(options, { cwd, env }) {
|
|
|
1564
1403
|
|
|
1565
1404
|
let result;
|
|
1566
1405
|
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);
|
|
1406
|
+
result = await impact(projectName, options, {
|
|
1407
|
+
cwd,
|
|
1408
|
+
readGraph: env.readGraph,
|
|
1409
|
+
listFiles: env.listFiles,
|
|
1410
|
+
});
|
|
1578
1411
|
} catch (error) {
|
|
1579
1412
|
const usageError = error instanceof UsageError;
|
|
1580
1413
|
env.err(String(error?.message ?? error));
|
|
@@ -1647,14 +1480,11 @@ async function runScenario(options, { cwd, env }) {
|
|
|
1647
1480
|
|
|
1648
1481
|
let result;
|
|
1649
1482
|
try {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1656
|
-
|
|
1657
|
-
result = scenarioCommand(projectName, scenarioJson, commandContext, config);
|
|
1483
|
+
result = await scenario(projectName, scenarioJson, options, {
|
|
1484
|
+
cwd,
|
|
1485
|
+
readGraph: env.readGraph,
|
|
1486
|
+
listFiles: env.listFiles,
|
|
1487
|
+
});
|
|
1658
1488
|
} catch (error) {
|
|
1659
1489
|
const usageError = error instanceof UsageError;
|
|
1660
1490
|
env.err(String(error?.message ?? error));
|
|
@@ -1702,22 +1532,9 @@ async function runExplain(options, { cwd, env }) {
|
|
|
1702
1532
|
return EXIT.usage;
|
|
1703
1533
|
}
|
|
1704
1534
|
|
|
1705
|
-
const site = options.paths[0];
|
|
1706
|
-
|
|
1707
1535
|
let result;
|
|
1708
1536
|
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);
|
|
1537
|
+
result = await explain(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1721
1538
|
} catch (error) {
|
|
1722
1539
|
const usageError = error instanceof UsageError;
|
|
1723
1540
|
env.err(String(error?.message ?? error));
|
|
@@ -1768,36 +1585,11 @@ async function runContextCommand(options, { cwd, env }) {
|
|
|
1768
1585
|
return EXIT.usage;
|
|
1769
1586
|
}
|
|
1770
1587
|
|
|
1771
|
-
const projectName = options.paths[0];
|
|
1772
|
-
const scopePaths = options.plan ? options.paths.slice(1) : [];
|
|
1773
|
-
|
|
1774
1588
|
let result;
|
|
1775
1589
|
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
1590
|
result = options.plan
|
|
1799
|
-
? await
|
|
1800
|
-
:
|
|
1591
|
+
? await planContext(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles })
|
|
1592
|
+
: await context(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1801
1593
|
} catch (error) {
|
|
1802
1594
|
const usageError = error instanceof UsageError;
|
|
1803
1595
|
env.err(String(error?.message ?? error));
|
|
@@ -1824,22 +1616,26 @@ async function runContextCommand(options, { cwd, env }) {
|
|
|
1824
1616
|
* `adr`'s `run`: reads the ADR registry at the workspace root and renders it.
|
|
1825
1617
|
*
|
|
1826
1618
|
* 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
|
-
*
|
|
1619
|
+
* package's own tree, and the preamble it needs — the root walked up from
|
|
1620
|
+
* `cwd`, the tracked file list — is the one `adrForWorkspace`
|
|
1621
|
+
* (`./src/commands/adr-for-workspace.mjs`) composes, the same composition
|
|
1622
|
+
* the MCP history adapter runs. This driver decides none of it: it words the
|
|
1623
|
+
* no-workspace refusal (`adrForWorkspace` returns the `null` and holds no
|
|
1624
|
+
* message of its own) and maps the command's throws. `adr` never exits 1: a
|
|
1625
|
+
* description of what is recorded is never a finding. An unreadable registry
|
|
1626
|
+
* (a malformed record, an unreadable file, a bad filename) throws → exit 3;
|
|
1627
|
+
* an id the user asked about that the registry does not know → exit 3, the
|
|
1628
|
+
* invariant.
|
|
1629
|
+
*
|
|
1630
|
+
* The tracked-file list is injected the way every other command's is —
|
|
1631
|
+
* `env.listFiles`, so a test can drive the read over a fixture tree with no
|
|
1632
|
+
* git — and reaches `adrCommand` through the driver, so the registry
|
|
1633
|
+
* resolves only git-tracked records (`src/governance/adr-registry.mjs`'s
|
|
1838
1634
|
* header). A `git ls-files` failure here throws the same as any other
|
|
1839
1635
|
* unreadable registry, mapped to exit 3 below.
|
|
1840
1636
|
*
|
|
1841
1637
|
* @param {{format: string, output: string|null, paths: string[]}} options
|
|
1842
|
-
* @param {{cwd: string, env: {out: Function, err: Function, listFiles?:
|
|
1638
|
+
* @param {{cwd: string, env: {out: Function, err: Function, listFiles?: Function}}} runContext
|
|
1843
1639
|
* @returns {Promise<number>}
|
|
1844
1640
|
*/
|
|
1845
1641
|
async function runAdr(options, { cwd, env }) {
|
|
@@ -1851,23 +1647,20 @@ async function runAdr(options, { cwd, env }) {
|
|
|
1851
1647
|
return EXIT.usage;
|
|
1852
1648
|
}
|
|
1853
1649
|
|
|
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
1650
|
let result;
|
|
1864
1651
|
try {
|
|
1865
|
-
|
|
1866
|
-
result = adrCommand(root, { id: options.paths[0] }, { tracked });
|
|
1652
|
+
result = adrForWorkspace({ cwd }, { id: options.paths[0] }, { listFiles: env.listFiles });
|
|
1867
1653
|
} catch (error) {
|
|
1868
1654
|
env.err(String(error?.message ?? error));
|
|
1869
1655
|
return EXIT.error;
|
|
1870
1656
|
}
|
|
1657
|
+
if (result === null) {
|
|
1658
|
+
env.err(
|
|
1659
|
+
`archkeep: adr needs a workspace root — no nx.json, archkeep.json, or ` +
|
|
1660
|
+
`.moon/workspace.yml marker found walking up from ${cwd}`,
|
|
1661
|
+
);
|
|
1662
|
+
return EXIT.error;
|
|
1663
|
+
}
|
|
1871
1664
|
|
|
1872
1665
|
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
1873
1666
|
|
|
@@ -1913,20 +1706,7 @@ async function runDecisions(options, { cwd, env }) {
|
|
|
1913
1706
|
|
|
1914
1707
|
let result;
|
|
1915
1708
|
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 });
|
|
1709
|
+
result = await decisions(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
1930
1710
|
} catch (error) {
|
|
1931
1711
|
const usageError = error instanceof UsageError;
|
|
1932
1712
|
env.err(String(error?.message ?? error));
|
|
@@ -2014,7 +1794,12 @@ async function runRules(options, { cwd, env }) {
|
|
|
2014
1794
|
env.out(report);
|
|
2015
1795
|
}
|
|
2016
1796
|
|
|
2017
|
-
// Exit codes:
|
|
1797
|
+
// Exit codes: `verify`'s return carries the pair its status asserts (0 ok,
|
|
1798
|
+
// 1 findings, 3 could-not-look). The descriptive subcommands (`list`,
|
|
1799
|
+
// `info`, `add`) expose no exit field — their statuses are `ok` or a
|
|
1800
|
+
// no-verdict class only, never findings — so this ladder keeps their exits
|
|
1801
|
+
// exactly as they were.
|
|
1802
|
+
if (subcommand === "verify") return result.exitCode;
|
|
2018
1803
|
if (result.status === "ok") return EXIT.ok;
|
|
2019
1804
|
if (result.status === "findings") return EXIT.violations;
|
|
2020
1805
|
return EXIT.error;
|
|
@@ -2040,56 +1825,15 @@ async function runHistory(options, { cwd, env }) {
|
|
|
2040
1825
|
return EXIT.usage;
|
|
2041
1826
|
}
|
|
2042
1827
|
|
|
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
|
-
}
|
|
1828
|
+
const refusal = historyOutputRefusal(options, cwd);
|
|
1829
|
+
if (refusal) {
|
|
1830
|
+
env.err(refusal);
|
|
1831
|
+
return EXIT.usage;
|
|
2067
1832
|
}
|
|
2068
1833
|
|
|
2069
1834
|
let result;
|
|
2070
1835
|
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
|
-
});
|
|
1836
|
+
result = await history(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2093
1837
|
} catch (error) {
|
|
2094
1838
|
const usageError = error instanceof UsageError;
|
|
2095
1839
|
env.err(String(error?.message ?? error));
|
|
@@ -2139,22 +1883,10 @@ async function runTrajectory(options, { cwd, env }) {
|
|
|
2139
1883
|
? resolve(options.paths[0])
|
|
2140
1884
|
: resolve(cwd, options.paths[0]);
|
|
2141
1885
|
|
|
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
|
-
}
|
|
1886
|
+
const refusal = trajectoryOutputRefusal(options, cwd);
|
|
1887
|
+
if (refusal) {
|
|
1888
|
+
env.err(refusal);
|
|
1889
|
+
return EXIT.usage;
|
|
2158
1890
|
}
|
|
2159
1891
|
|
|
2160
1892
|
let result;
|
|
@@ -2293,25 +2025,9 @@ async function runDebt(options, { cwd, env }) {
|
|
|
2293
2025
|
return EXIT.usage;
|
|
2294
2026
|
}
|
|
2295
2027
|
|
|
2296
|
-
const dir = isAbsolute(options.paths[0]) ? options.paths[0] : resolve(cwd, options.paths[0]);
|
|
2297
|
-
|
|
2298
2028
|
let result;
|
|
2299
2029
|
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
|
-
});
|
|
2030
|
+
result = await debt(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2315
2031
|
} catch (error) {
|
|
2316
2032
|
const usageError = error instanceof UsageError;
|
|
2317
2033
|
env.err(String(error?.message ?? error));
|
|
@@ -2402,22 +2118,17 @@ async function runDiscover(options, { cwd, env }) {
|
|
|
2402
2118
|
}
|
|
2403
2119
|
|
|
2404
2120
|
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
|
-
);
|
|
2121
|
+
// The one write that can turn a proposal into the law `check` gates on —
|
|
2122
|
+
// the refusal DECISION is discover's own (`intentWriteRefusal`, beside the
|
|
2123
|
+
// proposal it protects, the way `historyOutputRefusal` sits with history);
|
|
2124
|
+
// this driver keeps the mechanics only.
|
|
2125
|
+
const intentRefusal = intentWriteRefusal(options.writeIntent, { exists: existsSync });
|
|
2126
|
+
if (intentRefusal) {
|
|
2127
|
+
env.err(intentRefusal);
|
|
2415
2128
|
return EXIT.error;
|
|
2416
2129
|
}
|
|
2417
2130
|
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.
|
|
2131
|
+
const intentJson = intentJsonFromProposal(result.proposal);
|
|
2421
2132
|
writeFileSync(options.writeIntent, intentJson, { encoding: "utf-8", flag: "wx" });
|
|
2422
2133
|
env.err(`archkeep: proposed architecture written to ${options.writeIntent}`);
|
|
2423
2134
|
env.err(
|
|
@@ -2434,8 +2145,6 @@ async function runDiscover(options, { cwd, env }) {
|
|
|
2434
2145
|
}
|
|
2435
2146
|
|
|
2436
2147
|
/**
|
|
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
2148
|
* `health`'s `run`: resolves the command context, drives `healthCommand`,
|
|
2440
2149
|
* writes the report where it belongs, and returns the process's exit code.
|
|
2441
2150
|
*
|
|
@@ -2456,31 +2165,9 @@ async function runHealth(options, { cwd, env }) {
|
|
|
2456
2165
|
return EXIT.usage;
|
|
2457
2166
|
}
|
|
2458
2167
|
|
|
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
2168
|
let result;
|
|
2467
2169
|
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 });
|
|
2170
|
+
result = await health(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2484
2171
|
} catch (error) {
|
|
2485
2172
|
const usageError = error instanceof UsageError;
|
|
2486
2173
|
env.err(String(error?.message ?? error));
|
|
@@ -2528,47 +2215,20 @@ async function runReport(options, { cwd, env }) {
|
|
|
2528
2215
|
return EXIT.usage;
|
|
2529
2216
|
}
|
|
2530
2217
|
|
|
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
2218
|
let result;
|
|
2539
2219
|
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
|
-
});
|
|
2220
|
+
result = await report(options, { cwd, readGraph: env.readGraph, listFiles: env.listFiles });
|
|
2560
2221
|
} catch (error) {
|
|
2561
2222
|
const usageError = error instanceof UsageError;
|
|
2562
2223
|
env.err(String(error?.message ?? error));
|
|
2563
2224
|
return usageError ? EXIT.usage : EXIT.error;
|
|
2564
2225
|
}
|
|
2565
|
-
|
|
2566
|
-
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
2226
|
+
const rendered = options.format === "json" ? result.report.json : result.report.text;
|
|
2567
2227
|
|
|
2568
2228
|
if (options.output) {
|
|
2569
2229
|
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring owns
|
|
2570
2230
|
// the mechanism and the threat it closes.
|
|
2571
|
-
const reportText =
|
|
2231
|
+
const reportText = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
|
|
2572
2232
|
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
2573
2233
|
// The confirmation names the no-verdict case, so a reader who only
|
|
2574
2234
|
// glances at stderr cannot mistake a written document for an established
|
|
@@ -2581,7 +2241,7 @@ async function runReport(options, { cwd, env }) {
|
|
|
2581
2241
|
: `NO VERDICT — ${gaps} surface${gaps === 1 ? "" : "s"} could not be inspected`;
|
|
2582
2242
|
env.err(`archkeep: report complete (${verdict}) → ${options.output}`);
|
|
2583
2243
|
} else {
|
|
2584
|
-
env.out(
|
|
2244
|
+
env.out(rendered);
|
|
2585
2245
|
}
|
|
2586
2246
|
|
|
2587
2247
|
// Descriptive: 0 when every surface reached a verdict, 3 when any evidence
|