@ecoma-io/archkeep 0.14.0 → 0.16.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/README.md +11 -5
- package/cli.mjs +571 -61
- package/commands.mjs +57 -0
- package/lsp.mjs +15 -2
- package/package.json +8 -2
- package/src/analysis/analyze.mjs +15 -0
- package/src/analysis/contract.md +36 -18
- package/src/analysis/csharp.mjs +485 -0
- package/src/analysis/dotnet/csproj.mjs +380 -0
- package/src/analysis/dotnet/mask.mjs +178 -0
- package/src/analysis/dotnet/namespaces.mjs +172 -0
- package/src/analysis/dotnet/resolve.mjs +89 -0
- package/src/analysis/go.mjs +289 -5
- package/src/analysis/java.mjs +329 -0
- package/src/analysis/jvm/gradle.mjs +545 -0
- package/src/analysis/jvm/mask.mjs +170 -0
- package/src/analysis/jvm/maven.mjs +612 -0
- package/src/analysis/jvm/packages.mjs +209 -0
- package/src/analysis/jvm/resolve.mjs +139 -0
- package/src/analysis/kotlin.mjs +210 -0
- package/src/analysis/manifest-util.mjs +30 -0
- package/src/analysis/python.mjs +3 -2
- package/src/analysis/registry.mjs +11 -0
- package/src/analysis/rust.mjs +171 -17
- package/src/analysis/source-util.mjs +155 -6
- package/src/analysis/typescript.mjs +11 -3
- package/src/commands/README.md +52 -1
- package/src/commands/change-intent.mjs +461 -0
- package/src/commands/change.mjs +612 -0
- package/src/commands/check.mjs +2 -1
- package/src/commands/context.mjs +124 -16
- package/src/commands/custom-rules.mjs +286 -2
- package/src/commands/delta-classify.mjs +195 -33
- package/src/commands/delta-snapshot.mjs +156 -1
- package/src/commands/delta.mjs +142 -17
- package/src/commands/diff.mjs +41 -13
- package/src/commands/evolution.mjs +473 -0
- package/src/commands/history.mjs +130 -103
- package/src/commands/policy.mjs +57 -0
- package/src/commands/provenance.mjs +7 -44
- package/src/commands/rules.mjs +775 -0
- package/src/commands/trajectory.mjs +437 -0
- package/src/governance/profile-registry.mjs +0 -1
- package/src/graph/create-dependencies.mjs +138 -15
- package/src/lsp/diagnose.mjs +1 -1
- package/src/lsp/server.mjs +97 -1
- package/src/lsp/workspace-index.mjs +106 -15
- package/src/options.mjs +30 -7
- package/src/path-util.mjs +40 -0
- package/src/process.mjs +10 -1
- package/src/providers/moon.mjs +287 -36
- package/src/providers/native/differential.fixtures.mjs +32 -6
- package/src/providers/native/discover.mjs +83 -4
- package/src/providers/native/graph.mjs +58 -0
- package/src/providers/native/model.mjs +59 -1
- package/src/report/change-text.mjs +148 -0
- package/src/report/delta-text.mjs +82 -1
- package/src/report/evolution-text.mjs +83 -0
- package/src/report/history-text.mjs +4 -114
- package/src/report/sarif.mjs +255 -0
- package/src/report/snapshot-text.mjs +123 -0
- package/src/report/trajectory-text.mjs +143 -0
- package/src/rules/index.mjs +21 -6
- package/src/rules/reachability.mjs +2 -0
- package/src/rules/tags.mjs +7 -5
- package/src/rules/topology.mjs +5 -3
- package/src/tsconfig-paths.mjs +3 -2
- package/src/workspace.mjs +115 -23
package/cli.mjs
CHANGED
|
@@ -51,15 +51,19 @@
|
|
|
51
51
|
* tree is dirty" from "you typed it wrong" from "the checker itself broke":
|
|
52
52
|
* 0 no violations, and every selected file was analyzed
|
|
53
53
|
* 1 findings — boundary violations, go.work drift, dead tsconfig path
|
|
54
|
-
* aliases,
|
|
55
|
-
*
|
|
54
|
+
* aliases, architecture-intent findings, a non-waived violation `delta`
|
|
55
|
+
* classifies as introduced, or a change-intent reconciliation that found
|
|
56
|
+
* undeclared material changes, unfulfilled declarations, or a failed
|
|
57
|
+
* declared constraint. `check`, `fitness`, `delta` and `change` are the
|
|
58
|
+
* verbs whose verdicts carry this code — every other verb in this table
|
|
56
59
|
* only ever reads.
|
|
57
60
|
* 2 usage error — unknown command, unknown flag, missing argument, path
|
|
58
61
|
* outside the tree
|
|
59
62
|
* 3 no verdict — no workspace, malformed config, the graph provider or git
|
|
60
63
|
* failed, a selected file could not be analyzed, an architecture-intent
|
|
61
|
-
* boundary matched no observed project,
|
|
62
|
-
* accepts nothing this run judged
|
|
64
|
+
* boundary matched no observed project, a `boundarySuppressions` row
|
|
65
|
+
* accepts nothing this run judged, or a change intent could not be
|
|
66
|
+
* verified against its declared base. Distinct from
|
|
63
67
|
* 1 on purpose: a checker that could not look must never be mistaken for
|
|
64
68
|
* one that looked and found nothing.
|
|
65
69
|
*
|
|
@@ -93,7 +97,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
93
97
|
import { containmentViolation } from "./src/containment.mjs";
|
|
94
98
|
import { UsageError } from "./src/errors.mjs";
|
|
95
99
|
import { check, sortViolations } from "./src/commands/check.mjs";
|
|
96
|
-
import {
|
|
100
|
+
import { resolveDescribedPolicy, resolvePolicy } from "./src/commands/policy.mjs";
|
|
97
101
|
import {
|
|
98
102
|
DEFAULT_OPTIONS,
|
|
99
103
|
WORKSPACE_MARKERS,
|
|
@@ -109,14 +113,23 @@ import { discoverCommand } from "./src/commands/discover.mjs";
|
|
|
109
113
|
import { driftCommand } from "./src/commands/drift.mjs";
|
|
110
114
|
import { fitnessCommand } from "./src/commands/fitness.mjs";
|
|
111
115
|
import { reconcileCommand } from "./src/commands/reconcile.mjs";
|
|
116
|
+
import { changeCommand } from "./src/commands/change.mjs";
|
|
112
117
|
import { computePolicyFingerprint, graphCommand } from "./src/commands/graph.mjs";
|
|
113
118
|
import { historyCommand } from "./src/commands/history.mjs";
|
|
119
|
+
import { trajectoryCommand } from "./src/commands/trajectory.mjs";
|
|
120
|
+
import { evolutionCommand } from "./src/commands/evolution.mjs";
|
|
114
121
|
import { healthCommand } from "./src/commands/health.mjs";
|
|
115
122
|
import { reportCommand } from "./src/commands/report.mjs";
|
|
116
123
|
import { debtCommand } from "./src/commands/debt.mjs";
|
|
117
124
|
import { explainCommand } from "./src/commands/explain.mjs";
|
|
118
125
|
import { impactCommand } from "./src/commands/impact.mjs";
|
|
119
126
|
import { provenanceCommand } from "./src/commands/provenance-command.mjs";
|
|
127
|
+
import {
|
|
128
|
+
rulesAddCommand,
|
|
129
|
+
rulesInfoCommand,
|
|
130
|
+
rulesListCommand,
|
|
131
|
+
rulesVerifyCommand,
|
|
132
|
+
} from "./src/commands/rules.mjs";
|
|
120
133
|
import { waiversCommand } from "./src/commands/waivers.mjs";
|
|
121
134
|
import { INTENT_FILE, loadIntent } from "./src/architecture-intent/model.mjs";
|
|
122
135
|
import { isProgramEntry } from "./src/entry-point.mjs";
|
|
@@ -170,6 +183,16 @@ const CHECK_FORMATS = Object.freeze(["text", "sarif", "json"]);
|
|
|
170
183
|
*/
|
|
171
184
|
const DESCRIBABLE_FORMATS = Object.freeze(["text", "json"]);
|
|
172
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Every format `delta --format` accepts. SARIF joins the two descriptive
|
|
188
|
+
* formats because `delta`'s compare mode is a gate that produces findings —
|
|
189
|
+
* the introduced bucket is exactly what a code-scanning upload annotates at
|
|
190
|
+
* head sites — while every other descriptive-family verb stays on
|
|
191
|
+
* `DESCRIBABLE_FORMATS`: none of them produces findings, and SARIF's
|
|
192
|
+
* `results[]` is a findings container.
|
|
193
|
+
*/
|
|
194
|
+
const DELTA_FORMATS = Object.freeze(["text", "sarif", "json"]);
|
|
195
|
+
|
|
173
196
|
/**
|
|
174
197
|
* Column `usage()`'s Options block aligns flag descriptions to. Matches the
|
|
175
198
|
* hand-written text this table-driven rendering replaced, so deriving the
|
|
@@ -804,54 +827,14 @@ async function runGraph(options, { cwd, env }) {
|
|
|
804
827
|
//
|
|
805
828
|
// `graph` describes the project graph, not the boundary law — it reads no
|
|
806
829
|
// constraint row and judges nothing against one — so a workspace that has
|
|
807
|
-
// not written a law yet must not be refused here.
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
// policy.
|
|
816
|
-
//
|
|
817
|
-
// What is skipped is the load of a file that is NOT THERE. A boundary
|
|
818
|
-
// config that exists and will not load still fails the run, because an
|
|
819
|
-
// absent law and a broken one must not report alike; a `--config`, a
|
|
820
|
-
// profile, and an inline `archkeep.json` policy are explicit declarations
|
|
821
|
-
// and stay loud. Every command that JUDGES against the law keeps loading
|
|
822
|
-
// it unconditionally — making it optional for those would turn a missing
|
|
823
|
-
// file into a silent no-law run.
|
|
824
|
-
//
|
|
825
|
-
// `boundaryConfigDeclared` is what keeps this guard to the un-overridden
|
|
826
|
-
// default, and it is load-bearing rather than belt-and-braces. The name
|
|
827
|
-
// alone cannot answer it: `commandContext.options.boundaryConfig` is a
|
|
828
|
-
// string BOTH when it came from `./src/options.mjs`'s `DEFAULT_OPTIONS`
|
|
829
|
-
// and when the consumer WROTE it into `nx.json`'s plugin options or
|
|
830
|
-
// `archkeep.json`, and a workspace is free to declare the convention
|
|
831
|
-
// filename itself, so comparing against the default would still read a
|
|
832
|
-
// deliberate declaration as an assumption. Without the bit, measured on a
|
|
833
|
-
// committed native tree whose `archkeep.json` declares `boundaryConfig:
|
|
834
|
-
// "policy-we-declared.mjs"` and does not contain that file: `graph` exited
|
|
835
|
-
// 0 with a snapshot carrying no `policy` field — byte-identical to a
|
|
836
|
-
// workspace that never had a law — where the same tree with that file
|
|
837
|
-
// present but unparseable exited 3. A law someone named and then renamed
|
|
838
|
-
// or deleted is exactly the case that must stay loud, so the provenance
|
|
839
|
-
// survives the options layer instead (`./src/options.mjs`'s
|
|
840
|
-
// `resolveOptions`, `./src/providers/native/model.mjs`'s
|
|
841
|
-
// `normalizeNativeModel`, and `./src/commands/context.mjs`'s three
|
|
842
|
-
// branches carry it; Moon answers `false` because it has no table to
|
|
843
|
-
// declare one in).
|
|
844
|
-
const workspaceDefault =
|
|
845
|
-
!options.config &&
|
|
846
|
-
!hasProfiles(commandContext.options) &&
|
|
847
|
-
commandContext.options.boundaryConfigDeclared === false &&
|
|
848
|
-
typeof commandContext.options.boundaryConfig === "string"
|
|
849
|
-
? resolve(commandContext.root, commandContext.options.boundaryConfig)
|
|
850
|
-
: null;
|
|
851
|
-
const { config } =
|
|
852
|
-
workspaceDefault !== null && !existsSync(workspaceDefault)
|
|
853
|
-
? { config: null }
|
|
854
|
-
: await resolvePolicy(options, commandContext, cwd);
|
|
830
|
+
// not written a law yet must not be refused here. Every arm of that
|
|
831
|
+
// decision — what is skipped is the load of a file that is NOT THERE, the
|
|
832
|
+
// `boundaryConfigDeclared` bit that keeps the guard to the un-overridden
|
|
833
|
+
// default, and why a law someone named and then deleted stays loud — lives
|
|
834
|
+
// in `resolveDescribedPolicy` (`./src/commands/policy.mjs`) rather than
|
|
835
|
+
// here, so the descriptive commands and the MCP face that serves them
|
|
836
|
+
// cannot disagree about what "no law declared" means.
|
|
837
|
+
const { config } = await resolveDescribedPolicy(options, commandContext, cwd);
|
|
855
838
|
|
|
856
839
|
result = graphCommand(commandContext, { config });
|
|
857
840
|
} catch (error) {
|
|
@@ -1001,14 +984,19 @@ async function runDelta(options, { cwd, env }) {
|
|
|
1001
984
|
const baselinePath = isAbsolute(options.paths[0])
|
|
1002
985
|
? resolve(options.paths[0])
|
|
1003
986
|
: resolve(cwd, options.paths[0]);
|
|
1004
|
-
result = deltaCommand(baselinePath, commandContext, { config });
|
|
987
|
+
result = await deltaCommand(baselinePath, commandContext, { config });
|
|
1005
988
|
} catch (error) {
|
|
1006
989
|
const usageError = error instanceof UsageError;
|
|
1007
990
|
env.err(String(error?.message ?? error));
|
|
1008
991
|
return usageError ? EXIT.usage : EXIT.error;
|
|
1009
992
|
}
|
|
1010
993
|
|
|
1011
|
-
const report =
|
|
994
|
+
const report =
|
|
995
|
+
options.format === "json"
|
|
996
|
+
? result.report.json
|
|
997
|
+
: options.format === "sarif"
|
|
998
|
+
? result.report.sarif
|
|
999
|
+
: result.report.text;
|
|
1012
1000
|
|
|
1013
1001
|
if (options.output) {
|
|
1014
1002
|
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring owns
|
|
@@ -1212,6 +1200,108 @@ async function runReconcile(options, { cwd, env }) {
|
|
|
1212
1200
|
return EXIT.ok;
|
|
1213
1201
|
}
|
|
1214
1202
|
|
|
1203
|
+
/**
|
|
1204
|
+
* `change`'s `run`: resolves the command context and the boundary law, drives
|
|
1205
|
+
* `changeCommand` over the baseline evidence snapshot and the intent manifest,
|
|
1206
|
+
* writes the report where it belongs, and returns the process's exit code.
|
|
1207
|
+
*
|
|
1208
|
+
* The fourth verb whose verdict carries exit 1, beside `check`, `fitness` and
|
|
1209
|
+
* `delta`: an undeclared material change, an unfulfilled declaration, or a
|
|
1210
|
+
* failed declared constraint is a finding; an unproven base identity or an
|
|
1211
|
+
* undeterminable constraint is a no-verdict. The workspace-law axis the
|
|
1212
|
+
* envelope reports is informational — it never moves this exit code, because
|
|
1213
|
+
* `check` remains the authority on the law.
|
|
1214
|
+
*
|
|
1215
|
+
* @param {{format: string, output: string|null, config: string|null,
|
|
1216
|
+
* intent: string|null, paths: string[]}} options
|
|
1217
|
+
* @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function,
|
|
1218
|
+
* listFiles?: Function}}} runContext
|
|
1219
|
+
* @returns {Promise<number>}
|
|
1220
|
+
*/
|
|
1221
|
+
async function runChange(options, { cwd, env }) {
|
|
1222
|
+
if (options.paths.length !== 1) {
|
|
1223
|
+
env.err(
|
|
1224
|
+
`archkeep: change takes exactly one positional argument (the baseline evidence snapshot ` +
|
|
1225
|
+
`from 'delta --capture'); got ${options.paths.length}`,
|
|
1226
|
+
);
|
|
1227
|
+
return EXIT.usage;
|
|
1228
|
+
}
|
|
1229
|
+
if (!options.intent) {
|
|
1230
|
+
env.err(
|
|
1231
|
+
"archkeep: change needs '--intent <file>' naming the change-intent manifest — without a " +
|
|
1232
|
+
"declaration there is nothing to reconcile against",
|
|
1233
|
+
);
|
|
1234
|
+
return EXIT.usage;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const baselinePath = isAbsolute(options.paths[0])
|
|
1238
|
+
? options.paths[0]
|
|
1239
|
+
: resolve(cwd, options.paths[0]);
|
|
1240
|
+
const intentPath = isAbsolute(options.intent) ? options.intent : resolve(cwd, options.intent);
|
|
1241
|
+
|
|
1242
|
+
// A self-footgun guard, the same shape `history`'s holds: writing the
|
|
1243
|
+
// reconciliation report over the very manifest this run just read would
|
|
1244
|
+
// destroy the declaration it verified, with the loss surfacing only later —
|
|
1245
|
+
// the first time someone tries to re-run the verification.
|
|
1246
|
+
if (options.output) {
|
|
1247
|
+
const outputAbs = isAbsolute(options.output)
|
|
1248
|
+
? resolve(options.output)
|
|
1249
|
+
: resolve(cwd, options.output);
|
|
1250
|
+
if (outputAbs === intentPath) {
|
|
1251
|
+
env.err(
|
|
1252
|
+
`archkeep: --output '${options.output}' resolves to the change-intent manifest itself — ` +
|
|
1253
|
+
`overwriting the declaration with its own reconciliation report would destroy it. ` +
|
|
1254
|
+
`Write the report somewhere else.`,
|
|
1255
|
+
);
|
|
1256
|
+
return EXIT.usage;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
let result;
|
|
1261
|
+
try {
|
|
1262
|
+
const commandContext = resolveCommandContext(
|
|
1263
|
+
{ cwd },
|
|
1264
|
+
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1265
|
+
);
|
|
1266
|
+
|
|
1267
|
+
// Declared constraints are judged under whichever law THIS run resolves,
|
|
1268
|
+
// and the envelope records that law's fingerprint beside the baseline's —
|
|
1269
|
+
// the same loading every judging command does (`resolvePolicy`),
|
|
1270
|
+
// profile-aware the same way `check` is.
|
|
1271
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1272
|
+
|
|
1273
|
+
result = await changeCommand(baselinePath, intentPath, commandContext, { config });
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
const usageError = error instanceof UsageError;
|
|
1276
|
+
env.err(String(error?.message ?? error));
|
|
1277
|
+
return usageError ? EXIT.usage : EXIT.error;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
1281
|
+
|
|
1282
|
+
if (options.output) {
|
|
1283
|
+
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring owns
|
|
1284
|
+
// the mechanism and the threat it closes.
|
|
1285
|
+
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1286
|
+
if (!writeOutputReport(options.output, reportText, env, cwd, options.config)) return EXIT.error;
|
|
1287
|
+
env.err(
|
|
1288
|
+
`archkeep: change ${result.changeIntent.reconciliation.verdict} ` +
|
|
1289
|
+
`(+${result.changeIntent.reconciliation.matched.length} matched, ` +
|
|
1290
|
+
`!${result.changeIntent.reconciliation.unexpected.length} undeclared, ` +
|
|
1291
|
+
`?${result.changeIntent.reconciliation.missingExpected.length} unfulfilled) → ${options.output}`,
|
|
1292
|
+
);
|
|
1293
|
+
} else {
|
|
1294
|
+
env.out(report);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// The verdict fold `changeCommand` computed, mapped here the way `delta`'s
|
|
1298
|
+
// and `fitness`' are.
|
|
1299
|
+
return (
|
|
1300
|
+
{ ok: EXIT.ok, findings: EXIT.violations, "no-verdict": EXIT.error }[result.status] ??
|
|
1301
|
+
EXIT.error
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1215
1305
|
/**
|
|
1216
1306
|
* `waivers`' `run`: resolves the command context, drives `waiversCommand`,
|
|
1217
1307
|
* writes the report where it belongs, and returns the process's exit code.
|
|
@@ -1590,8 +1680,8 @@ async function runAdr(options, { cwd, env }) {
|
|
|
1590
1680
|
const root = resolveWorkspaceRootForUsage(cwd);
|
|
1591
1681
|
if (root === null) {
|
|
1592
1682
|
env.err(
|
|
1593
|
-
`archkeep: adr needs a workspace root — no nx.json, archkeep.json, or
|
|
1594
|
-
|
|
1683
|
+
`archkeep: adr needs a workspace root — no nx.json, archkeep.json, or ` +
|
|
1684
|
+
`.moon/workspace.yml marker found walking up from ${cwd}`,
|
|
1595
1685
|
);
|
|
1596
1686
|
return EXIT.error;
|
|
1597
1687
|
}
|
|
@@ -1623,6 +1713,72 @@ async function runAdr(options, { cwd, env }) {
|
|
|
1623
1713
|
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
1624
1714
|
}
|
|
1625
1715
|
|
|
1716
|
+
/**
|
|
1717
|
+
* `rules`'s `run`: dispatches to the appropriate subcommand (list/info/verify/add),
|
|
1718
|
+
* drives it, writes the report, and returns the exit code.
|
|
1719
|
+
*
|
|
1720
|
+
* @param {{format: string, output: string|null, catalog: string|null, to: string|null, paths: string[]}} options
|
|
1721
|
+
* @param {{cwd: string, env: {out: Function, err: Function}}} runContext
|
|
1722
|
+
* @returns {Promise<number>}
|
|
1723
|
+
*/
|
|
1724
|
+
async function runRules(options, { cwd, env }) {
|
|
1725
|
+
if (options.paths.length === 0) {
|
|
1726
|
+
env.err(`archkeep: rules requires a subcommand (list, info, verify, or add)`);
|
|
1727
|
+
return EXIT.usage;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const subcommand = options.paths[0];
|
|
1731
|
+
const ruleName = options.paths[1];
|
|
1732
|
+
|
|
1733
|
+
const dispatch = {
|
|
1734
|
+
list: rulesListCommand,
|
|
1735
|
+
info: rulesInfoCommand,
|
|
1736
|
+
verify: rulesVerifyCommand,
|
|
1737
|
+
add: rulesAddCommand,
|
|
1738
|
+
}[subcommand];
|
|
1739
|
+
|
|
1740
|
+
if (!dispatch) {
|
|
1741
|
+
env.err(
|
|
1742
|
+
`archkeep: unknown rules subcommand '${subcommand}' — must be list, info, verify, or add`,
|
|
1743
|
+
);
|
|
1744
|
+
return EXIT.usage;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (subcommand === "info" && !ruleName) {
|
|
1748
|
+
env.err(`archkeep: rules info requires a rule name`);
|
|
1749
|
+
return EXIT.usage;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (subcommand === "add" && !ruleName) {
|
|
1753
|
+
env.err(`archkeep: rules add requires a rule name`);
|
|
1754
|
+
return EXIT.usage;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
let result;
|
|
1758
|
+
try {
|
|
1759
|
+
const commandOptions = { catalog: options.catalog, to: options.to };
|
|
1760
|
+
result = await dispatch(commandOptions, { cwd, ruleName });
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
env.err(String(error?.message ?? error));
|
|
1763
|
+
return EXIT.error;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
1767
|
+
|
|
1768
|
+
if (options.output) {
|
|
1769
|
+
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1770
|
+
if (!writeOutputReport(options.output, reportText, env, cwd, null)) return EXIT.error;
|
|
1771
|
+
env.err(`archkeep: rules ${subcommand} complete → ${options.output}`);
|
|
1772
|
+
} else {
|
|
1773
|
+
env.out(report);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Exit codes: 0 for ok, 1 for findings (verify only), 3 for no-verdict
|
|
1777
|
+
if (result.status === "ok") return EXIT.ok;
|
|
1778
|
+
if (result.status === "findings") return EXIT.violations;
|
|
1779
|
+
return EXIT.error;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1626
1782
|
/**
|
|
1627
1783
|
* `history`'s `run`: resolves the command context, optionally captures a
|
|
1628
1784
|
* snapshot of the current workspace, drives `historyCommand`, writes the
|
|
@@ -1715,6 +1871,157 @@ async function runHistory(options, { cwd, env }) {
|
|
|
1715
1871
|
return EXIT.ok;
|
|
1716
1872
|
}
|
|
1717
1873
|
|
|
1874
|
+
/**
|
|
1875
|
+
* `trajectory`'s run: resolves the command context, drives
|
|
1876
|
+
* `trajectoryCommand`, writes the report, and returns the exit code.
|
|
1877
|
+
*
|
|
1878
|
+
* The history directory is the single positional argument — the same
|
|
1879
|
+
* consumer-managed directory `history` and `debt` read. No boundary law is
|
|
1880
|
+
* loaded and no snapshot is captured: the fingerprints compared travel inside
|
|
1881
|
+
* the stored snapshots (`src/commands/trajectory.mjs`'s header owns both
|
|
1882
|
+
* halves of that posture).
|
|
1883
|
+
*
|
|
1884
|
+
* @param {{format: string, output: string|null, paths: string[]}} options
|
|
1885
|
+
* @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
|
|
1886
|
+
* @returns {Promise<number>}
|
|
1887
|
+
*/
|
|
1888
|
+
async function runTrajectory(options, { cwd, env }) {
|
|
1889
|
+
if (options.paths.length !== 1) {
|
|
1890
|
+
env.err(
|
|
1891
|
+
`archkeep: trajectory takes exactly one positional argument (the history directory); ` +
|
|
1892
|
+
`got ${options.paths.length}`,
|
|
1893
|
+
);
|
|
1894
|
+
return EXIT.usage;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
const dir = isAbsolute(options.paths[0])
|
|
1898
|
+
? resolve(options.paths[0])
|
|
1899
|
+
: resolve(cwd, options.paths[0]);
|
|
1900
|
+
|
|
1901
|
+
// The same self-footgun guard `runHistory` applies: a report written into
|
|
1902
|
+
// the directory being read would be read back as a snapshot on the next run
|
|
1903
|
+
// (the envelope is not a `graph` snapshot, which `parseBaseline` refuses) —
|
|
1904
|
+
// poison the record loudly refused rather than quietly planted.
|
|
1905
|
+
if (options.output) {
|
|
1906
|
+
const outputAbs = isAbsolute(options.output)
|
|
1907
|
+
? resolve(options.output)
|
|
1908
|
+
: resolve(cwd, options.output);
|
|
1909
|
+
if (dirname(outputAbs) === dir) {
|
|
1910
|
+
env.err(
|
|
1911
|
+
`archkeep: --output '${options.output}' is inside the history directory '${dir}' — ` +
|
|
1912
|
+
`writing the report there would be read back as a snapshot on the next run. ` +
|
|
1913
|
+
`Write it somewhere else.`,
|
|
1914
|
+
);
|
|
1915
|
+
return EXIT.usage;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
let result;
|
|
1920
|
+
try {
|
|
1921
|
+
const commandContext = resolveCommandContext(
|
|
1922
|
+
{ cwd },
|
|
1923
|
+
{ readGraph: env.readGraph, listFiles: env.listFiles },
|
|
1924
|
+
);
|
|
1925
|
+
result = trajectoryCommand(dir, commandContext);
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
const usageError = error instanceof UsageError;
|
|
1928
|
+
env.err(String(error?.message ?? error));
|
|
1929
|
+
return usageError ? EXIT.usage : EXIT.error;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
1933
|
+
|
|
1934
|
+
if (options.output) {
|
|
1935
|
+
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring owns
|
|
1936
|
+
// the mechanism and the threat it closes.
|
|
1937
|
+
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1938
|
+
// No `--config` flag (`TRAJECTORY_FLAG_HELP`) — there is no override to pass.
|
|
1939
|
+
if (!writeOutputReport(options.output, reportText, env, cwd, null)) return EXIT.error;
|
|
1940
|
+
env.err(`archkeep: trajectory complete → ${options.output}`);
|
|
1941
|
+
} else {
|
|
1942
|
+
env.out(report);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Trajectory is descriptive: 0 when the aggregation completes, never 1.
|
|
1946
|
+
return EXIT.ok;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* `evolution`'s `run`: resolves the workspace root the way `adr` does (a light
|
|
1951
|
+
* marker walk — this command describes OTHER trees, so it must not fail
|
|
1952
|
+
* because the caller's own working tree does not fully analyze right now),
|
|
1953
|
+
* drives `evolutionCommand`, writes the report, and returns the exit code.
|
|
1954
|
+
*
|
|
1955
|
+
* The range is `--base` plus optional `--head`; there are no positional
|
|
1956
|
+
* arguments. The Nx and git seams in `env` thread into EVERY analyzed
|
|
1957
|
+
* revision's context, the same way they thread into one tree elsewhere — in
|
|
1958
|
+
* production both are undefined and every revision is read for real.
|
|
1959
|
+
*
|
|
1960
|
+
* @param {{format: string, output: string|null, base: string|null, head: string|null,
|
|
1961
|
+
* paths: string[]}} options
|
|
1962
|
+
* @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
|
|
1963
|
+
* @returns {Promise<number>}
|
|
1964
|
+
*/
|
|
1965
|
+
async function runEvolution(options, { cwd, env }) {
|
|
1966
|
+
if (!options.base) {
|
|
1967
|
+
env.err(
|
|
1968
|
+
`archkeep: evolution needs --base <rev> — a commit, branch, or tag the range starts at ` +
|
|
1969
|
+
`(--head defaults to HEAD).`,
|
|
1970
|
+
);
|
|
1971
|
+
return EXIT.usage;
|
|
1972
|
+
}
|
|
1973
|
+
if (options.paths.length > 0) {
|
|
1974
|
+
env.err(
|
|
1975
|
+
`archkeep: evolution takes no positional arguments; the range is --base plus --head. ` +
|
|
1976
|
+
`Got '${options.paths.join("', '")}'`,
|
|
1977
|
+
);
|
|
1978
|
+
return EXIT.usage;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
const root = findWorkspaceRoot(cwd, WORKSPACE_MARKERS);
|
|
1982
|
+
if (root === null) {
|
|
1983
|
+
env.err(
|
|
1984
|
+
`archkeep: evolution needs a workspace root — no nx.json, archkeep.json, or ` +
|
|
1985
|
+
`.moon/workspace.yml marker found walking up from ${cwd}`,
|
|
1986
|
+
);
|
|
1987
|
+
return EXIT.error;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
let result;
|
|
1991
|
+
try {
|
|
1992
|
+
result = await evolutionCommand(
|
|
1993
|
+
root,
|
|
1994
|
+
{ base: options.base, head: options.head },
|
|
1995
|
+
{
|
|
1996
|
+
readGraph: env.readGraph,
|
|
1997
|
+
listFiles: env.listFiles,
|
|
1998
|
+
},
|
|
1999
|
+
);
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
const usageError = error instanceof UsageError;
|
|
2002
|
+
env.err(String(error?.message ?? error));
|
|
2003
|
+
return usageError ? EXIT.usage : EXIT.error;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
2007
|
+
|
|
2008
|
+
if (options.output) {
|
|
2009
|
+
// Atomic, symlink-safe write — `writeOutputReport`'s own docstring owns
|
|
2010
|
+
// the mechanism and the threat it closes.
|
|
2011
|
+
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
2012
|
+
// No `--config` flag exists here (`EVOLUTION_FLAG_HELP`) — there is no
|
|
2013
|
+
// override to pass, only the workspace's un-overridden default to guard.
|
|
2014
|
+
if (!writeOutputReport(options.output, reportText, env, cwd, null)) return EXIT.error;
|
|
2015
|
+
env.err(`archkeep: evolution complete → ${options.output}`);
|
|
2016
|
+
} else {
|
|
2017
|
+
env.out(report);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Descriptive: where the architecture changed is a fact about history,
|
|
2021
|
+
// never a finding.
|
|
2022
|
+
return EXIT.ok;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1718
2025
|
/**
|
|
1719
2026
|
* `debt`'s `run`: resolves the command context, drives `debtCommand`, writes
|
|
1720
2027
|
* the ledger report, and returns the exit code.
|
|
@@ -2127,10 +2434,11 @@ const DELTA_FLAG_HELP = Object.freeze([
|
|
|
2127
2434
|
Object.freeze({
|
|
2128
2435
|
flag: "--format",
|
|
2129
2436
|
key: "format",
|
|
2130
|
-
arg: "text|json",
|
|
2437
|
+
arg: "text|sarif|json",
|
|
2131
2438
|
describe: Object.freeze([
|
|
2132
|
-
"Terminal report (default)
|
|
2133
|
-
"
|
|
2439
|
+
"Terminal report (default), SARIF 2.1.0 of the introduced",
|
|
2440
|
+
"findings for GitHub code scanning, or the versioned JSON",
|
|
2441
|
+
"envelope docs/reference/json-output.md documents",
|
|
2134
2442
|
]),
|
|
2135
2443
|
}),
|
|
2136
2444
|
Object.freeze({
|
|
@@ -2156,6 +2464,54 @@ const DELTA_FLAG_HELP = Object.freeze([
|
|
|
2156
2464
|
}),
|
|
2157
2465
|
]);
|
|
2158
2466
|
|
|
2467
|
+
/**
|
|
2468
|
+
* `change`'s flags: text or JSON envelope, optional file output, and the
|
|
2469
|
+
* required `--intent` naming the change-intent manifest. `--config` joins for
|
|
2470
|
+
* the same reason `delta`'s does: declared constraints are re-judged under
|
|
2471
|
+
* whichever law this run resolves.
|
|
2472
|
+
*
|
|
2473
|
+
* @type {readonly FlagHelp[]}
|
|
2474
|
+
*/
|
|
2475
|
+
const CHANGE_FLAG_HELP = Object.freeze([
|
|
2476
|
+
Object.freeze({
|
|
2477
|
+
flag: "--intent",
|
|
2478
|
+
key: "intent",
|
|
2479
|
+
arg: "<file>",
|
|
2480
|
+
describe: Object.freeze([
|
|
2481
|
+
"The change-intent manifest declaring the material",
|
|
2482
|
+
"architectural consequences this change expects",
|
|
2483
|
+
"(required; see docs/usage/change.md)",
|
|
2484
|
+
]),
|
|
2485
|
+
}),
|
|
2486
|
+
Object.freeze({
|
|
2487
|
+
flag: "--format",
|
|
2488
|
+
key: "format",
|
|
2489
|
+
arg: "text|json",
|
|
2490
|
+
describe: Object.freeze([
|
|
2491
|
+
"Terminal report (default) or the versioned JSON envelope",
|
|
2492
|
+
"docs/reference/json-output.md documents",
|
|
2493
|
+
]),
|
|
2494
|
+
}),
|
|
2495
|
+
Object.freeze({
|
|
2496
|
+
flag: "--output",
|
|
2497
|
+
key: "output",
|
|
2498
|
+
arg: "<file>",
|
|
2499
|
+
describe: Object.freeze(["Write the report to a file instead of stdout"]),
|
|
2500
|
+
}),
|
|
2501
|
+
Object.freeze({
|
|
2502
|
+
flag: "--config",
|
|
2503
|
+
key: "config",
|
|
2504
|
+
arg: "<file>",
|
|
2505
|
+
describe: ({ boundaryConfig, inline }) =>
|
|
2506
|
+
Object.freeze([
|
|
2507
|
+
"Read the boundary law from here instead of",
|
|
2508
|
+
inline
|
|
2509
|
+
? "the inline boundaryConfig in archkeep.json"
|
|
2510
|
+
: `<workspace root>/${boundaryConfig}`,
|
|
2511
|
+
]),
|
|
2512
|
+
}),
|
|
2513
|
+
]);
|
|
2514
|
+
|
|
2159
2515
|
/**
|
|
2160
2516
|
* `drift`'s flags: text or JSON envelope, optional file output. The intent is
|
|
2161
2517
|
* always read from the tracked root `architecture-intent.json` — the same one
|
|
@@ -2372,6 +2728,51 @@ const HISTORY_FLAG_HELP = Object.freeze([
|
|
|
2372
2728
|
}),
|
|
2373
2729
|
]);
|
|
2374
2730
|
|
|
2731
|
+
/**
|
|
2732
|
+
* `evolution`'s flags: text or JSON envelope, optional file output, and the
|
|
2733
|
+
* two revisions that bound the range. There is no `--config` — each analyzed
|
|
2734
|
+
* revision is judged under the law its own tree declares (`src/commands/evolution.mjs`),
|
|
2735
|
+
* and a law carried from outside would misattribute policy changes to
|
|
2736
|
+
* revisions that never made them.
|
|
2737
|
+
*
|
|
2738
|
+
* @type {readonly FlagHelp[]}
|
|
2739
|
+
*/
|
|
2740
|
+
const EVOLUTION_FLAG_HELP = Object.freeze([
|
|
2741
|
+
Object.freeze({
|
|
2742
|
+
flag: "--format",
|
|
2743
|
+
key: "format",
|
|
2744
|
+
arg: "text|json",
|
|
2745
|
+
describe: Object.freeze([
|
|
2746
|
+
"Terminal report (default) or the versioned JSON envelope",
|
|
2747
|
+
"docs/reference/json-output.md documents",
|
|
2748
|
+
]),
|
|
2749
|
+
}),
|
|
2750
|
+
Object.freeze({
|
|
2751
|
+
flag: "--output",
|
|
2752
|
+
key: "output",
|
|
2753
|
+
arg: "<file>",
|
|
2754
|
+
describe: Object.freeze(["Write the report to a file instead of stdout"]),
|
|
2755
|
+
}),
|
|
2756
|
+
Object.freeze({
|
|
2757
|
+
flag: "--base",
|
|
2758
|
+
key: "base",
|
|
2759
|
+
arg: "<rev>",
|
|
2760
|
+
describe: Object.freeze([
|
|
2761
|
+
"The baseline revision — a commit, branch, tag,",
|
|
2762
|
+
"or HEAD~n; the first revision analyzed",
|
|
2763
|
+
]),
|
|
2764
|
+
}),
|
|
2765
|
+
Object.freeze({
|
|
2766
|
+
flag: "--head",
|
|
2767
|
+
key: "head",
|
|
2768
|
+
arg: "<rev>",
|
|
2769
|
+
describe: Object.freeze([
|
|
2770
|
+
"The tip revision (default HEAD); must be a",
|
|
2771
|
+
"linear descendant of --base with no merges between",
|
|
2772
|
+
]),
|
|
2773
|
+
}),
|
|
2774
|
+
]);
|
|
2775
|
+
|
|
2375
2776
|
/**
|
|
2376
2777
|
* `health`'s flags: text or JSON envelope, optional file output. The optional
|
|
2377
2778
|
* positional argument is the snapshot directory for trends, the same directory
|
|
@@ -2448,6 +2849,35 @@ const REPORT_FLAG_HELP = Object.freeze([
|
|
|
2448
2849
|
}),
|
|
2449
2850
|
]);
|
|
2450
2851
|
|
|
2852
|
+
/**
|
|
2853
|
+
* `trajectory`'s flags: text or JSON envelope and optional file output — and
|
|
2854
|
+
* deliberately no `--config` and no `--capture`. There is no current-law
|
|
2855
|
+
* input to override: the fingerprints being compared travel inside the
|
|
2856
|
+
* snapshots, so the law each observation is judged under is the one it was
|
|
2857
|
+
* captured under. And the command writes nothing into the history directory —
|
|
2858
|
+
* capture stays `history --capture`'s job, so there is exactly one way a
|
|
2859
|
+
* snapshot enters the record.
|
|
2860
|
+
*
|
|
2861
|
+
* @type {readonly FlagHelp[]}
|
|
2862
|
+
*/
|
|
2863
|
+
const TRAJECTORY_FLAG_HELP = Object.freeze([
|
|
2864
|
+
Object.freeze({
|
|
2865
|
+
flag: "--format",
|
|
2866
|
+
key: "format",
|
|
2867
|
+
arg: "text|json",
|
|
2868
|
+
describe: Object.freeze([
|
|
2869
|
+
"Terminal report (default) or the versioned JSON envelope",
|
|
2870
|
+
"docs/reference/json-output.md documents",
|
|
2871
|
+
]),
|
|
2872
|
+
}),
|
|
2873
|
+
Object.freeze({
|
|
2874
|
+
flag: "--output",
|
|
2875
|
+
key: "output",
|
|
2876
|
+
arg: "<file>",
|
|
2877
|
+
describe: Object.freeze(["Write the report to a file instead of stdout"]),
|
|
2878
|
+
}),
|
|
2879
|
+
]);
|
|
2880
|
+
|
|
2451
2881
|
/**
|
|
2452
2882
|
* `debt`'s flags: text or JSON envelope, optional file output, and the same
|
|
2453
2883
|
* `--config` the other descriptive commands take, so a ledger ages the
|
|
@@ -2633,6 +3063,46 @@ const ADR_FLAG_HELP = Object.freeze([
|
|
|
2633
3063
|
}),
|
|
2634
3064
|
]);
|
|
2635
3065
|
|
|
3066
|
+
/**
|
|
3067
|
+
* `rules`'s flags: catalog path, format, output, and target directory for add.
|
|
3068
|
+
* The first positional is the subcommand (list/info/verify/add), the second is
|
|
3069
|
+
* the rule name for info and add.
|
|
3070
|
+
*
|
|
3071
|
+
* @type {readonly FlagHelp[]}
|
|
3072
|
+
*/
|
|
3073
|
+
const RULES_FLAG_HELP = Object.freeze([
|
|
3074
|
+
Object.freeze({
|
|
3075
|
+
flag: "--catalog",
|
|
3076
|
+
key: "catalog",
|
|
3077
|
+
arg: "<path>",
|
|
3078
|
+
describe: Object.freeze([
|
|
3079
|
+
"Path to the catalog file",
|
|
3080
|
+
"(default: node_modules/@ecoma-io/archkeep-rules/catalog.json)",
|
|
3081
|
+
]),
|
|
3082
|
+
}),
|
|
3083
|
+
Object.freeze({
|
|
3084
|
+
flag: "--format",
|
|
3085
|
+
key: "format",
|
|
3086
|
+
arg: "text|json",
|
|
3087
|
+
describe: Object.freeze([
|
|
3088
|
+
"Terminal report (default) or the versioned JSON envelope",
|
|
3089
|
+
"docs/reference/json-output.md documents",
|
|
3090
|
+
]),
|
|
3091
|
+
}),
|
|
3092
|
+
Object.freeze({
|
|
3093
|
+
flag: "--output",
|
|
3094
|
+
key: "output",
|
|
3095
|
+
arg: "<file>",
|
|
3096
|
+
describe: Object.freeze(["Write the report to a file instead of stdout"]),
|
|
3097
|
+
}),
|
|
3098
|
+
Object.freeze({
|
|
3099
|
+
flag: "--to",
|
|
3100
|
+
key: "to",
|
|
3101
|
+
arg: "<dir>",
|
|
3102
|
+
describe: Object.freeze(["Target directory for the .wasm file", "(default: tools/rules/)"]),
|
|
3103
|
+
}),
|
|
3104
|
+
]);
|
|
3105
|
+
|
|
2636
3106
|
/**
|
|
2637
3107
|
* The command table `usage()` and `runCli` both read from — a command added
|
|
2638
3108
|
* later is a new entry here, not a new branch in either. `args` is the
|
|
@@ -2679,10 +3149,20 @@ const COMMANDS = Object.freeze({
|
|
|
2679
3149
|
flagHelp: DELTA_FLAG_HELP,
|
|
2680
3150
|
flags: Object.freeze(Object.fromEntries(DELTA_FLAG_HELP.map((f) => [f.flag, f.key]))),
|
|
2681
3151
|
defaults: Object.freeze({ format: "text", output: null, config: null, capture: false }),
|
|
2682
|
-
formats:
|
|
3152
|
+
formats: DELTA_FORMATS,
|
|
2683
3153
|
booleans: Object.freeze(["capture"]),
|
|
2684
3154
|
run: runDelta,
|
|
2685
3155
|
}),
|
|
3156
|
+
change: Object.freeze({
|
|
3157
|
+
name: "change",
|
|
3158
|
+
args: "<baseline> --intent <file>",
|
|
3159
|
+
summary: "Reconcile a declared change intent against the architectural delta",
|
|
3160
|
+
flagHelp: CHANGE_FLAG_HELP,
|
|
3161
|
+
flags: Object.freeze(Object.fromEntries(CHANGE_FLAG_HELP.map((f) => [f.flag, f.key]))),
|
|
3162
|
+
defaults: Object.freeze({ format: "text", output: null, config: null, intent: null }),
|
|
3163
|
+
formats: DESCRIBABLE_FORMATS,
|
|
3164
|
+
run: runChange,
|
|
3165
|
+
}),
|
|
2686
3166
|
discover: Object.freeze({
|
|
2687
3167
|
name: "discover",
|
|
2688
3168
|
args: "[--propose]",
|
|
@@ -2746,6 +3226,26 @@ const COMMANDS = Object.freeze({
|
|
|
2746
3226
|
booleans: Object.freeze(["capture"]),
|
|
2747
3227
|
run: runHistory,
|
|
2748
3228
|
}),
|
|
3229
|
+
trajectory: Object.freeze({
|
|
3230
|
+
name: "trajectory",
|
|
3231
|
+
args: "<dir>",
|
|
3232
|
+
summary: "Aggregate the deterministic drift trajectory across snapshots",
|
|
3233
|
+
flagHelp: TRAJECTORY_FLAG_HELP,
|
|
3234
|
+
flags: Object.freeze(Object.fromEntries(TRAJECTORY_FLAG_HELP.map((f) => [f.flag, f.key]))),
|
|
3235
|
+
defaults: Object.freeze({ format: "text", output: null }),
|
|
3236
|
+
formats: DESCRIBABLE_FORMATS,
|
|
3237
|
+
run: runTrajectory,
|
|
3238
|
+
}),
|
|
3239
|
+
evolution: Object.freeze({
|
|
3240
|
+
name: "evolution",
|
|
3241
|
+
args: "",
|
|
3242
|
+
summary: "Describe how the architecture evolved across a Git revision range",
|
|
3243
|
+
flagHelp: EVOLUTION_FLAG_HELP,
|
|
3244
|
+
flags: Object.freeze(Object.fromEntries(EVOLUTION_FLAG_HELP.map((f) => [f.flag, f.key]))),
|
|
3245
|
+
defaults: Object.freeze({ format: "text", output: null, base: null, head: null }),
|
|
3246
|
+
formats: DESCRIBABLE_FORMATS,
|
|
3247
|
+
run: runEvolution,
|
|
3248
|
+
}),
|
|
2749
3249
|
health: Object.freeze({
|
|
2750
3250
|
name: "health",
|
|
2751
3251
|
args: "[<snapshot-dir>]",
|
|
@@ -2827,6 +3327,16 @@ const COMMANDS = Object.freeze({
|
|
|
2827
3327
|
formats: DESCRIBABLE_FORMATS,
|
|
2828
3328
|
run: runAdr,
|
|
2829
3329
|
}),
|
|
3330
|
+
rules: Object.freeze({
|
|
3331
|
+
name: "rules",
|
|
3332
|
+
args: "<list|info|verify|add> [<rule-name>]",
|
|
3333
|
+
summary: "List official rules, show details, verify catalog integrity, or add a rule",
|
|
3334
|
+
flagHelp: RULES_FLAG_HELP,
|
|
3335
|
+
flags: Object.freeze(Object.fromEntries(RULES_FLAG_HELP.map((f) => [f.flag, f.key]))),
|
|
3336
|
+
defaults: Object.freeze({ format: "text", output: null, catalog: null, to: null }),
|
|
3337
|
+
formats: DESCRIBABLE_FORMATS,
|
|
3338
|
+
run: runRules,
|
|
3339
|
+
}),
|
|
2830
3340
|
});
|
|
2831
3341
|
|
|
2832
3342
|
/**
|