@ecoma-io/archkeep 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +166 -506
- package/commands.mjs +7 -1
- package/package.json +1 -1
- package/src/architecture-intent/judge.mjs +1 -1
- package/src/architecture-intent/model.mjs +34 -0
- package/src/canonical.mjs +2 -1
- package/src/commands/adr-for-workspace.mjs +63 -0
- package/src/commands/analyze-capability.mjs +13 -0
- package/src/commands/change-intent.mjs +11 -0
- package/src/commands/change.mjs +247 -34
- package/src/commands/check-capability.mjs +13 -0
- package/src/commands/check.mjs +13 -3
- package/src/commands/compare-capability.mjs +20 -0
- package/src/commands/completeness.mjs +12 -0
- package/src/commands/context-command.mjs +31 -0
- package/src/commands/coverage-verdict.mjs +15 -2
- package/src/commands/debt.mjs +30 -0
- package/src/commands/decisions.mjs +59 -10
- package/src/commands/delta.mjs +263 -49
- package/src/commands/diff.mjs +45 -0
- package/src/commands/discover.mjs +37 -0
- package/src/commands/drift.mjs +45 -0
- package/src/commands/evolution.mjs +22 -2
- package/src/commands/explain-capability.mjs +11 -0
- package/src/commands/explain.mjs +24 -0
- package/src/commands/fitness.mjs +119 -10
- package/src/commands/govern-capability.mjs +16 -0
- package/src/commands/graph.mjs +39 -0
- package/src/commands/health.mjs +34 -0
- package/src/commands/history.mjs +85 -2
- package/src/commands/impact.mjs +23 -0
- package/src/commands/inspect-capability.mjs +16 -0
- package/src/commands/plan-context-command.mjs +35 -0
- package/src/commands/policy.mjs +42 -4
- package/src/commands/provenance-command.mjs +15 -24
- package/src/commands/report.mjs +38 -0
- package/src/commands/rules-capability.mjs +16 -0
- package/src/commands/rules.mjs +3 -1
- package/src/commands/scenario.mjs +23 -0
- package/src/commands/trajectory.mjs +35 -0
- package/src/commands/waivers.mjs +30 -0
- package/src/corpus/goldens/.gitkeep +0 -0
- package/src/corpus/goldens/adr.json +62 -0
- package/src/corpus/goldens/adr.text +6 -0
- package/src/corpus/goldens/change.json +98 -0
- package/src/corpus/goldens/change.text +6 -0
- package/src/corpus/goldens/check.json +90 -0
- package/src/corpus/goldens/check.sarif +479 -0
- package/src/corpus/goldens/check.text +9 -0
- package/src/corpus/goldens/context.json +47 -0
- package/src/corpus/goldens/context.text +6 -0
- package/src/corpus/goldens/debt.json +59 -0
- package/src/corpus/goldens/debt.text +7 -0
- package/src/corpus/goldens/decisions.json +74 -0
- package/src/corpus/goldens/decisions.text +7 -0
- package/src/corpus/goldens/delta.json +94 -0
- package/src/corpus/goldens/delta.sarif +472 -0
- package/src/corpus/goldens/delta.text +5 -0
- package/src/corpus/goldens/diff.json +58 -0
- package/src/corpus/goldens/diff.text +4 -0
- package/src/corpus/goldens/discover.json +77 -0
- package/src/corpus/goldens/discover.text +10 -0
- package/src/corpus/goldens/drift.json +43 -0
- package/src/corpus/goldens/drift.text +3 -0
- package/src/corpus/goldens/evolution.json +328 -0
- package/src/corpus/goldens/evolution.text +32 -0
- package/src/corpus/goldens/explain.json +60 -0
- package/src/corpus/goldens/explain.text +7 -0
- package/src/corpus/goldens/fitness.json +45 -0
- package/src/corpus/goldens/fitness.text +3 -0
- package/src/corpus/goldens/graph.json +78 -0
- package/src/corpus/goldens/graph.text +10 -0
- package/src/corpus/goldens/health.json +72 -0
- package/src/corpus/goldens/health.text +10 -0
- package/src/corpus/goldens/help.text +104 -0
- package/src/corpus/goldens/history.json +43 -0
- package/src/corpus/goldens/history.text +4 -0
- package/src/corpus/goldens/impact.json +327 -0
- package/src/corpus/goldens/impact.text +10 -0
- package/src/corpus/goldens/provenance.json +453 -0
- package/src/corpus/goldens/provenance.text +14 -0
- package/src/corpus/goldens/reconcile.json +168 -0
- package/src/corpus/goldens/reconcile.text +3 -0
- package/src/corpus/goldens/report.json +168 -0
- package/src/corpus/goldens/report.text +42 -0
- package/src/corpus/goldens/rules verify.json +0 -0
- package/src/corpus/goldens/rules verify.text +0 -0
- package/src/corpus/goldens/scenario.json +441 -0
- package/src/corpus/goldens/scenario.text +28 -0
- package/src/corpus/goldens/trajectory.json +83 -0
- package/src/corpus/goldens/trajectory.text +10 -0
- package/src/corpus/goldens/usage-error.text +105 -0
- package/src/corpus/goldens/waivers.json +40 -0
- package/src/corpus/goldens/waivers.text +1 -0
- package/src/corpus/goldens-lsp/initialize.result.json +15 -0
- package/src/corpus/goldens-lsp/publish-cleared-empty.json +4 -0
- package/src/corpus/goldens-lsp/publish-fixed-empty.json +5 -0
- package/src/corpus/goldens-lsp/publish-unparseable.json +54 -0
- package/src/corpus/goldens-lsp/publish-violation.json +22 -0
- package/src/corpus/goldens-lsp/register-watched-files.json +72 -0
- package/src/go-work.mjs +8 -23
- package/src/governance/evolution-event.mjs +15 -0
- package/src/governance/evolution-store.mjs +59 -31
- package/src/governance/provenance-record.mjs +0 -152
- package/src/governance/verdict.mjs +26 -3
- package/src/intent/intent-manifest.json +14 -14
- package/src/lsp/server.mjs +5 -8
- package/src/lsp/workspace-index.mjs +55 -220
- package/src/options.mjs +10 -0
- package/src/providers/native/discover.mjs +13 -12
- package/src/providers/native/model.mjs +8 -3
- package/src/providers/nx-static.mjs +231 -0
- package/src/report/json.mjs +3 -2
- package/src/report/sarif.mjs +8 -3
- package/src/rules/edge-constraints.mjs +9 -0
- package/src/rules/index.mjs +26 -10
- package/src/rules/messages.mjs +64 -14
- package/src/rules/specifiers.mjs +21 -20
- package/src/tsconfig-paths.mjs +8 -14
- package/src/verdict.mjs +127 -16
package/src/commands/impact.mjs
CHANGED
|
@@ -40,8 +40,10 @@ import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
|
40
40
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
41
41
|
import { formatImpactReport } from "../report/impact-text.mjs";
|
|
42
42
|
import { resolveProvenance } from "./provenance.mjs";
|
|
43
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
43
44
|
import { composeImpactStatement } from "./impact-statement.mjs";
|
|
44
45
|
import { computeImpact } from "./impact-reachability.mjs";
|
|
46
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* The reachability walk this command reports, shared with the canonical
|
|
@@ -171,3 +173,24 @@ export function impactCommand(projectName, commandContext, config = null) {
|
|
|
171
173
|
},
|
|
172
174
|
};
|
|
173
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `impact` as the CLI drives it: the shared preamble — command context, then
|
|
179
|
+
* the boundary law — resolved here so `../../cli.mjs`'s driver only wires
|
|
180
|
+
* options, IO seams, and where output lands (`./README.md`). The engine this
|
|
181
|
+
* returns from is `impactCommand` above, unchanged.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} projectName The project whose dependents are walked.
|
|
184
|
+
* @param {{config: string|null}} options This run's parsed flags.
|
|
185
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
186
|
+
* seams a test injects, the same ones `check` takes.
|
|
187
|
+
* @returns {Promise<object>} `impactCommand`'s result, unmodified.
|
|
188
|
+
*/
|
|
189
|
+
export async function impact(projectName, options, { cwd, readGraph, listFiles }) {
|
|
190
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
191
|
+
// Load the boundary config when --config is given or when the workspace
|
|
192
|
+
// declares one, so constraint-impact analysis is computed — profile-aware
|
|
193
|
+
// the same way `check` is (`resolvePolicy`).
|
|
194
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
195
|
+
return impactCommand(projectName, commandContext, config);
|
|
196
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `inspect` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `graph`, `impact`, `context` (the non-plan read; `--plan`
|
|
4
|
+
* dispatches to `planContext`), `health`.
|
|
5
|
+
*
|
|
6
|
+
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
7
|
+
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
8
|
+
* the facade so the vocabulary's code referent is visible to the import
|
|
9
|
+
* graph, not just to a comment (PD-18,
|
|
10
|
+
* ../../../../docs/architecture/refactor/DECISIONS.md).
|
|
11
|
+
*/
|
|
12
|
+
export { graph } from "./graph.mjs";
|
|
13
|
+
export { impact } from "./impact.mjs";
|
|
14
|
+
export { context } from "./context-command.mjs";
|
|
15
|
+
export { planContext } from "./plan-context-command.mjs";
|
|
16
|
+
export { health } from "./health.mjs";
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
* reporting set. This makes the plan's verdict correct on every provider.
|
|
62
62
|
*/
|
|
63
63
|
import { statSync } from "node:fs";
|
|
64
|
+
import { isAbsolute, resolve } from "node:path";
|
|
64
65
|
import { join } from "node:path";
|
|
65
66
|
|
|
66
67
|
import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
@@ -82,6 +83,8 @@ import { buildDependencies, buildProjects, computePolicyFingerprint } from "./gr
|
|
|
82
83
|
import { resolveProvenance } from "./provenance.mjs";
|
|
83
84
|
import { readAdrContext } from "./adr.mjs";
|
|
84
85
|
import { declaresFitness, fitnessForCheck } from "./fitness.mjs";
|
|
86
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
87
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
85
88
|
import { computeWaivers } from "./waivers.mjs";
|
|
86
89
|
import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/adr-registry.mjs";
|
|
87
90
|
import { formatPlanContextReport } from "../report/plan-context-text.mjs";
|
|
@@ -667,3 +670,35 @@ function goWorkResult(goWork) {
|
|
|
667
670
|
function tsconfigPathsResult(tsconfigPaths) {
|
|
668
671
|
return tsconfigPaths === null ? null : { checked: true, findings: tsconfigPaths.findings };
|
|
669
672
|
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* `context --plan` as the CLI drives it: the shared preamble — command
|
|
676
|
+
* context, then the boundary law — resolved here, plus the plan's own
|
|
677
|
+
* arguments (the project, the change's scope paths, the optional history
|
|
678
|
+
* directory), so `../../cli.mjs`'s driver only wires options, IO seams, and
|
|
679
|
+
* where output lands (`./README.md`). The engine this returns from is
|
|
680
|
+
* `planContextCommand` above, unchanged.
|
|
681
|
+
*
|
|
682
|
+
* @param {{config: string|null, historyDir: string|null, paths: string[]}} options
|
|
683
|
+
* This run's parsed flags; `paths[0]` is the project name, the rest are
|
|
684
|
+
* the change's scope.
|
|
685
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
686
|
+
* seams a test injects, the same ones `check` takes.
|
|
687
|
+
* @returns {Promise<object>} `planContextCommand`'s result, unmodified.
|
|
688
|
+
*/
|
|
689
|
+
export async function planContext(options, { cwd, readGraph, listFiles }) {
|
|
690
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
691
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
692
|
+
const historyDir = options.historyDir
|
|
693
|
+
? isAbsolute(options.historyDir)
|
|
694
|
+
? options.historyDir
|
|
695
|
+
: resolve(cwd, options.historyDir)
|
|
696
|
+
: null;
|
|
697
|
+
return planContextCommand(
|
|
698
|
+
options.paths[0],
|
|
699
|
+
options.paths.slice(1),
|
|
700
|
+
commandContext,
|
|
701
|
+
config,
|
|
702
|
+
historyDir,
|
|
703
|
+
);
|
|
704
|
+
}
|
package/src/commands/policy.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { isAbsolute, relative, resolve } from "node:path";
|
|
|
14
14
|
import { containmentViolation } from "../containment.mjs";
|
|
15
15
|
import { loadBoundaryConfig, loadBoundaryConfigFile, policyFrom } from "../config.mjs";
|
|
16
16
|
import { profilePolicy } from "../governance/profile-registry.mjs";
|
|
17
|
-
import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
|
|
17
|
+
import { ARCHKEEP_MODEL_FILE, loadNativeModel } from "../providers/native/model.mjs";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Whether the workspace's resolved options name a `profiles` registry — the
|
|
@@ -35,7 +35,9 @@ function hasProfiles(options) {
|
|
|
35
35
|
* shares, in the one place all of them now call it from. Hand-copied 11
|
|
36
36
|
* times before this — `check`, `graph`, `diff`, `waivers`, `fitness`,
|
|
37
37
|
* `impact`, `explain`, `context`, `history`'s `--capture` branch, `debt`,
|
|
38
|
-
* `health` —
|
|
38
|
+
* `health` — and `provenance` joined at WI-2's fold, its private walk until
|
|
39
|
+
* then keeping the P1-26 class alive there too — the duplication is what
|
|
40
|
+
* let two defects land in it independently:
|
|
39
41
|
* P1-25 found `graph`'s copy alone missing the inline-object arm, and P1-26
|
|
40
42
|
* found only `check`'s copy aware of a `profiles` registry at all — the other
|
|
41
43
|
* ten tried to resolve a profile NAME as a file, and named the wrong problem
|
|
@@ -43,7 +45,7 @@ function hasProfiles(options) {
|
|
|
43
45
|
* `"strict"` as "names an unsupported boundaryConfig extension '(none)'",
|
|
44
46
|
* which blames a typo that was never made rather than naming the real gap —
|
|
45
47
|
* that command never knew profiles existed. One function, called from all
|
|
46
|
-
*
|
|
48
|
+
* twelve sites, is what makes that defect class structurally impossible to
|
|
47
49
|
* reintroduce one copy at a time.
|
|
48
50
|
*
|
|
49
51
|
* Checked in order, and the first match wins:
|
|
@@ -76,7 +78,7 @@ function hasProfiles(options) {
|
|
|
76
78
|
* intent's `file`). Only `check` reads either field today (P1-01, naming the
|
|
77
79
|
* law that governed a run in its own report), but they are returned
|
|
78
80
|
* unconditionally rather than as a second, `check`-only code path, so the
|
|
79
|
-
*
|
|
81
|
+
* twelve callers keep sharing the one ladder this function exists to be.
|
|
80
82
|
*
|
|
81
83
|
* @param {{config: string|null}} options The command's own parsed flags —
|
|
82
84
|
* only `config` is read here, so a command with no `--config` flag at all
|
|
@@ -231,3 +233,39 @@ async function resolvePolicyArm(options, commandContext, cwd) {
|
|
|
231
233
|
}
|
|
232
234
|
return { config: null, profile: null, source: null };
|
|
233
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The options `--help` words itself with over a native-only workspace — the
|
|
239
|
+
* `loadNativeModel` read the entry file used to carry inline. Help names the
|
|
240
|
+
* boundary law a run would actually read, and on a native tree that law
|
|
241
|
+
* lives on `archkeep.json` itself: a string `boundaryConfig` is a filename,
|
|
242
|
+
* an object is the inline policy (there is no other channel).
|
|
243
|
+
*
|
|
244
|
+
* Throws exactly what `loadNativeModel` throws; the caller owns the
|
|
245
|
+
* best-effort posture (`--help` falls back to the defaults rather than
|
|
246
|
+
* refusing to print), this function owns only the reading and the wording
|
|
247
|
+
* shape.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} root Absolute workspace root.
|
|
250
|
+
* @param {{readFile: (path: string) => string|null}} io Workspace-relative
|
|
251
|
+
* read, injectable for the same reason `loadNativeModel`'s is.
|
|
252
|
+
* @returns {{boundaryConfig: string, tsConfig: string, inline?: boolean}}
|
|
253
|
+
* `inline: true` marks the object-policy case: the boundaryConfig "name"
|
|
254
|
+
* is then prose, not a file.
|
|
255
|
+
*/
|
|
256
|
+
export function nativePolicyOptions(root, { readFile }) {
|
|
257
|
+
const model = loadNativeModel(root, { readFile });
|
|
258
|
+
// An inline policy object has no filename to print — `${boundaryConfig}`
|
|
259
|
+
// would otherwise coerce it to the literal text "[object Object]", which
|
|
260
|
+
// reads as a real (and wrong) filename rather than as the "there is no
|
|
261
|
+
// file" it actually means. `inline: true` is what tells the usage text to
|
|
262
|
+
// print the paragraph that says so, instead of the one describing a named
|
|
263
|
+
// file.
|
|
264
|
+
return typeof model.boundaryConfig === "string"
|
|
265
|
+
? { boundaryConfig: model.boundaryConfig, tsConfig: model.tsConfig }
|
|
266
|
+
: {
|
|
267
|
+
boundaryConfig: `an inline policy in ${ARCHKEEP_MODEL_FILE}`,
|
|
268
|
+
tsConfig: model.tsConfig,
|
|
269
|
+
inline: true,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
74
74
|
import { formatProvenanceReport } from "../report/provenance-text.mjs";
|
|
75
75
|
import { loadIntent } from "../architecture-intent/model.mjs";
|
|
76
|
-
import {
|
|
76
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
77
77
|
import { resolveFileAttribution, resolveProvenance } from "./provenance.mjs";
|
|
78
78
|
import { readAdrContext } from "./adr.mjs";
|
|
79
79
|
import {
|
|
@@ -206,7 +206,6 @@ export function unresolvedDecisionRefNote(decisionRef) {
|
|
|
206
206
|
* options: {boundaryConfig: string|object, inline?: boolean}}} commandContext
|
|
207
207
|
* From `resolveCommandContext`.
|
|
208
208
|
* @param {{loadIntentOverride?: (root: string, io: object) => Promise<object>,
|
|
209
|
-
* loadConfigOverride?: (root: string, boundaryConfig: string) => Promise<object>,
|
|
210
209
|
* loadAdrRegistryOverride?: typeof import("../governance/adr-registry.mjs").loadAdrRegistry,
|
|
211
210
|
* fileAttribution?: (root: string, file: string) =>
|
|
212
211
|
* {createdBy: import("../governance/provenance-record.mjs").OriginRecord,
|
|
@@ -234,7 +233,7 @@ export function unresolvedDecisionRefNote(decisionRef) {
|
|
|
234
233
|
* exit 3, the loud refusal every command that reads them makes.
|
|
235
234
|
*/
|
|
236
235
|
export async function provenanceCommand(commandContext, io = {}) {
|
|
237
|
-
const { root, tracked
|
|
236
|
+
const { root, tracked } = commandContext;
|
|
238
237
|
|
|
239
238
|
const repo = resolveProvenance(root);
|
|
240
239
|
const rowList = [];
|
|
@@ -254,27 +253,19 @@ export async function provenanceCommand(commandContext, io = {}) {
|
|
|
254
253
|
}
|
|
255
254
|
}
|
|
256
255
|
|
|
257
|
-
// The boundary law
|
|
258
|
-
// reads
|
|
259
|
-
//
|
|
260
|
-
// `
|
|
261
|
-
//
|
|
262
|
-
// which is the silent direction
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
} else if (
|
|
271
|
-
boundaryConfig !== null &&
|
|
272
|
-
typeof boundaryConfig === "object" &&
|
|
273
|
-
!Array.isArray(boundaryConfig)
|
|
274
|
-
) {
|
|
275
|
-
loadedConfig = boundaryConfig;
|
|
276
|
-
walked.push(...configRows(boundaryConfig));
|
|
277
|
-
}
|
|
256
|
+
// The boundary law resolves through the one policy ladder every command
|
|
257
|
+
// that reads a law shares (`./policy.mjs`'s `resolvePolicy`) — the
|
|
258
|
+
// profile, file, and inline arms included — so the rows attested here are
|
|
259
|
+
// exactly the rows `check` judges, never a law resolved by a private
|
|
260
|
+
// second copy. A policy whose rows the report never inspected would claim
|
|
261
|
+
// "every row attests" over an unread table, which is the silent direction
|
|
262
|
+
// this command exists to end. `config: null` — there is no `--config`
|
|
263
|
+
// flag here (`PROVENANCE_FLAG_HELP`): the workspace's own law,
|
|
264
|
+
// profile-selected when a registry is named, is the only one a provenance
|
|
265
|
+
// report may cite. A `null` config (the workspace declares no law) walks
|
|
266
|
+
// zero rows.
|
|
267
|
+
const { config: loadedConfig } = await resolvePolicy({ config: null }, commandContext, root);
|
|
268
|
+
const walked = configRows(loadedConfig);
|
|
278
269
|
for (const { kind, row } of walked) {
|
|
279
270
|
const attested = hasOrigin(row);
|
|
280
271
|
rowList.push({ kind, attested, origin: attested ? row.origin : null });
|
package/src/commands/report.mjs
CHANGED
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
*/
|
|
108
108
|
import { formatGovernanceReport } from "../report/report-text.mjs";
|
|
109
109
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
110
|
+
import { isAbsolute, resolve } from "node:path";
|
|
110
111
|
import { healthCommand } from "./health.mjs";
|
|
111
112
|
import { declaresFitness, fitnessCommand } from "./fitness.mjs";
|
|
112
113
|
import { waiversCommand } from "./waivers.mjs";
|
|
@@ -128,6 +129,9 @@ import {
|
|
|
128
129
|
} from "../governance/adr-registry.mjs";
|
|
129
130
|
import { computeDecisionFitness } from "../governance/decision-fitness.mjs";
|
|
130
131
|
import { hasAuthority, stripRuleFitnessPrefix } from "../governance/adr-registry.mjs";
|
|
132
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
133
|
+
import { loadIntentIfTracked } from "../architecture-intent/model.mjs";
|
|
134
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
131
135
|
|
|
132
136
|
/**
|
|
133
137
|
* The message a thrown refusal carries, as the report's reason for a surface
|
|
@@ -598,3 +602,37 @@ export async function reportCommand(commandContext, io = {}) {
|
|
|
598
602
|
},
|
|
599
603
|
};
|
|
600
604
|
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* `report` as the CLI drives it: the shared preamble — command context, the
|
|
608
|
+
* ONE boundary law the whole document is written against, the tracked
|
|
609
|
+
* intent — resolved here, so `../../cli.mjs`'s driver only wires options,
|
|
610
|
+
* IO seams, and where output lands (`./README.md`). The engine this returns
|
|
611
|
+
* from is `reportCommand` above, unchanged.
|
|
612
|
+
*
|
|
613
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
614
|
+
* flags; at most one positional, the snapshot directory for trends.
|
|
615
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
616
|
+
* seams a test injects, the same ones `check` takes.
|
|
617
|
+
* @returns {Promise<object>} `reportCommand`'s result, unmodified.
|
|
618
|
+
*/
|
|
619
|
+
export async function report(options, { cwd, readGraph, listFiles }) {
|
|
620
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
621
|
+
// ONE law for the whole document — resolved exactly the way `check` and
|
|
622
|
+
// `health` resolve theirs, and handed to every surface the report
|
|
623
|
+
// composes, so no two sections can cite different laws.
|
|
624
|
+
const { config, source } = await resolvePolicy(options, commandContext, cwd);
|
|
625
|
+
const intent = await loadIntentIfTracked(commandContext.root, commandContext.tracked);
|
|
626
|
+
const trendDir =
|
|
627
|
+
options.paths.length === 1
|
|
628
|
+
? isAbsolute(options.paths[0])
|
|
629
|
+
? options.paths[0]
|
|
630
|
+
: resolve(cwd, options.paths[0])
|
|
631
|
+
: null;
|
|
632
|
+
return reportCommand(commandContext, {
|
|
633
|
+
config,
|
|
634
|
+
intent,
|
|
635
|
+
trendDir,
|
|
636
|
+
policySource: source,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rules` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `rules` (the `list`, `info`, `verify`, and `add` subcommands).
|
|
4
|
+
*
|
|
5
|
+
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
6
|
+
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
7
|
+
* the facade so the vocabulary's code referent is visible to the import
|
|
8
|
+
* graph, not just to a comment (PD-18,
|
|
9
|
+
* ../../../../docs/architecture/refactor/DECISIONS.md).
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
rulesAddCommand,
|
|
13
|
+
rulesInfoCommand,
|
|
14
|
+
rulesListCommand,
|
|
15
|
+
rulesVerifyCommand,
|
|
16
|
+
} from "./rules.mjs";
|
package/src/commands/rules.mjs
CHANGED
|
@@ -341,7 +341,8 @@ export async function rulesInfoCommand(options, { cwd, ruleName }) {
|
|
|
341
341
|
*
|
|
342
342
|
* @param {{catalog?: string}} options The parsed command options.
|
|
343
343
|
* @param {{cwd: string}} runContext The command context.
|
|
344
|
-
* @returns {Promise<{status: "ok"|"findings"|"no-verdict",
|
|
344
|
+
* @returns {Promise<{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3,
|
|
345
|
+
* catalog: object, report: {text: string, json: string}}>}
|
|
345
346
|
*/
|
|
346
347
|
export async function rulesVerifyCommand(options, { cwd }) {
|
|
347
348
|
const catalogPath = resolveCatalogPath(options, cwd);
|
|
@@ -473,6 +474,7 @@ export async function rulesVerifyCommand(options, { cwd }) {
|
|
|
473
474
|
|
|
474
475
|
return {
|
|
475
476
|
status,
|
|
477
|
+
exitCode,
|
|
476
478
|
catalog,
|
|
477
479
|
report: {
|
|
478
480
|
text,
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { resolveProvenance } from "./provenance.mjs";
|
|
13
13
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
14
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
14
15
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
15
16
|
import { evaluateScenario, parseScenarioInput } from "./scenario-evaluation.mjs";
|
|
16
17
|
export { parseScenarioInput } from "./scenario-evaluation.mjs";
|
|
18
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Runs the `scenario` command: parses the scenario input, evaluates it, and
|
|
@@ -195,3 +197,24 @@ function formatScenarioReport(scenario, coverage) {
|
|
|
195
197
|
|
|
196
198
|
return lines.join("\n");
|
|
197
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `scenario` as the CLI drives it: the shared preamble — command context,
|
|
203
|
+
* then the boundary law — resolved here so `../../cli.mjs`'s driver only
|
|
204
|
+
* wires options, IO seams, and where output lands (`./README.md`). The
|
|
205
|
+
* engine this returns from is `scenarioCommand` above, unchanged. The
|
|
206
|
+
* scenario file itself is read by the driver, because a file the CLI cannot
|
|
207
|
+
* read is a usage error (exit 2), not a run failure (exit 3).
|
|
208
|
+
*
|
|
209
|
+
* @param {string} projectName The target project.
|
|
210
|
+
* @param {string} scenarioJson The scenario description, read by the driver.
|
|
211
|
+
* @param {{config: string|null}} options This run's parsed flags.
|
|
212
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
213
|
+
* seams a test injects, the same ones `check` takes.
|
|
214
|
+
* @returns {Promise<object>} `scenarioCommand`'s result, unmodified.
|
|
215
|
+
*/
|
|
216
|
+
export async function scenario(projectName, scenarioJson, options, { cwd, readGraph, listFiles }) {
|
|
217
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
218
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
219
|
+
return scenarioCommand(projectName, scenarioJson, commandContext, config);
|
|
220
|
+
}
|
|
@@ -92,6 +92,8 @@
|
|
|
92
92
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
93
93
|
import { formatTrajectoryReport } from "../report/trajectory-text.mjs";
|
|
94
94
|
import { edgeIdentityKey } from "./diff.mjs";
|
|
95
|
+
import { isAbsolute, resolve } from "node:path";
|
|
96
|
+
import { dirname } from "node:path";
|
|
95
97
|
import { classifyTransition, readSnapshots } from "./history.mjs";
|
|
96
98
|
import { resolveProvenance } from "./provenance.mjs";
|
|
97
99
|
|
|
@@ -522,3 +524,36 @@ export function trajectoryCommand(dir, commandContext, options = {}) {
|
|
|
522
524
|
},
|
|
523
525
|
};
|
|
524
526
|
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* `trajectory`'s self-footgun guard, declared by the command that owns the
|
|
530
|
+
* law and enforced by the driver's write door: a report written into the
|
|
531
|
+
* directory being read would be read back as a snapshot on the next run
|
|
532
|
+
* (the envelope is not a `graph` snapshot, which `parseBaseline` refuses) —
|
|
533
|
+
* poison the record loudly refused rather than quietly planted. `null`
|
|
534
|
+
* means no refusal.
|
|
535
|
+
*
|
|
536
|
+
* @param {{output: string|null, paths: string[]}} options This run's parsed
|
|
537
|
+
* flags; `paths[0]` is the history directory.
|
|
538
|
+
* @param {string} cwd The run's working directory, for relative flag
|
|
539
|
+
* resolution.
|
|
540
|
+
* @returns {string|null} The refusal message, or `null` when the output is
|
|
541
|
+
* safe.
|
|
542
|
+
*/
|
|
543
|
+
export function trajectoryOutputRefusal(options, cwd) {
|
|
544
|
+
if (!options.output) return null;
|
|
545
|
+
const dir = isAbsolute(options.paths[0])
|
|
546
|
+
? resolve(options.paths[0])
|
|
547
|
+
: resolve(cwd, options.paths[0]);
|
|
548
|
+
const outputAbs = isAbsolute(options.output)
|
|
549
|
+
? resolve(options.output)
|
|
550
|
+
: resolve(cwd, options.output);
|
|
551
|
+
if (dirname(outputAbs) === dir) {
|
|
552
|
+
return (
|
|
553
|
+
`archkeep: --output '${options.output}' is inside the history directory '${dir}' — ` +
|
|
554
|
+
`writing the report there would be read back as a snapshot on the next run. ` +
|
|
555
|
+
`Write it somewhere else.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
package/src/commands/waivers.mjs
CHANGED
|
@@ -44,11 +44,13 @@ import { isWaiver, remainingMs, waiverStatus } from "../governance/waiver.mjs";
|
|
|
44
44
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
45
45
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
46
46
|
import { formatWaiversReport } from "../report/waivers-text.mjs";
|
|
47
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
47
48
|
import { partitionUnownedCoverage } from "./coverage-acceptance.mjs";
|
|
48
49
|
import { unownedGapWithoutRunConfiguration } from "./context.mjs";
|
|
49
50
|
import { refuseIncompleteGraph } from "./drift.mjs";
|
|
50
51
|
import { resolveProvenance } from "./provenance.mjs";
|
|
51
52
|
import { evaluateRun } from "../rules/index.mjs";
|
|
53
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* The waivers verdict for a run: every waiver with its term and what it
|
|
@@ -296,3 +298,31 @@ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
|
|
|
296
298
|
},
|
|
297
299
|
};
|
|
298
300
|
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* `waivers` as the CLI drives it: the shared preamble — command context,
|
|
304
|
+
* then the boundary law — resolved here so `../../cli.mjs`'s driver only
|
|
305
|
+
* wires options, IO seams, and where output lands (`./README.md`). The
|
|
306
|
+
* engine this returns from is `waiversCommand` above, unchanged.
|
|
307
|
+
*
|
|
308
|
+
* @param {{config: string|null, paths: string[]}} options This run's parsed
|
|
309
|
+
* flags.
|
|
310
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
311
|
+
* seams a test injects, the same ones `check` takes.
|
|
312
|
+
* @returns {Promise<object>} `waiversCommand`'s result, unmodified.
|
|
313
|
+
*/
|
|
314
|
+
export async function waivers(options, { cwd, readGraph, listFiles }) {
|
|
315
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
316
|
+
// The waivers surface is part of the run's boundary law, so the law is
|
|
317
|
+
// loaded the same way `check` loads it (`resolvePolicy`) and `--config`
|
|
318
|
+
// wins the same way — resolved against the working directory, never
|
|
319
|
+
// against this tool's own location, and a `profiles` registry resolves
|
|
320
|
+
// `--config`/`boundaryConfig` as a profile NAME the same way `check`
|
|
321
|
+
// does. A malformed law throws here, exit 3, exactly as in `check`.
|
|
322
|
+
const { config, source } = await resolvePolicy(options, commandContext, cwd);
|
|
323
|
+
// `source` rides along for one job: `waiversCommand` subtracts the law's
|
|
324
|
+
// own file from the unowned-file set the `coverage.unowned` acceptances
|
|
325
|
+
// are matched against, exactly as `check` does — the law is not source
|
|
326
|
+
// judged by the law (`./context.mjs`'s `unownedGapWithoutRunConfiguration`).
|
|
327
|
+
return waiversCommand(commandContext, config, { policySource: source });
|
|
328
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"tool": {
|
|
4
|
+
"name": "@ecoma-io/archkeep",
|
|
5
|
+
"version": "0.26.0"
|
|
6
|
+
},
|
|
7
|
+
"command": "adr",
|
|
8
|
+
"workspace": {
|
|
9
|
+
"root": "/tmp/archkeep-golden-fixture",
|
|
10
|
+
"provider": "native",
|
|
11
|
+
"marker": "docs/adr",
|
|
12
|
+
"provenance": null
|
|
13
|
+
},
|
|
14
|
+
"status": "ok",
|
|
15
|
+
"exitCode": 0,
|
|
16
|
+
"coverage": {
|
|
17
|
+
"complete": true,
|
|
18
|
+
"projects": 0,
|
|
19
|
+
"analyzedFiles": 1,
|
|
20
|
+
"imports": 0,
|
|
21
|
+
"notAnalyzed": [],
|
|
22
|
+
"blindSpots": [],
|
|
23
|
+
"notes": []
|
|
24
|
+
},
|
|
25
|
+
"result": {
|
|
26
|
+
"adrs": [
|
|
27
|
+
"0001-layers"
|
|
28
|
+
],
|
|
29
|
+
"registry": {
|
|
30
|
+
"dir": "docs/adr",
|
|
31
|
+
"count": 1
|
|
32
|
+
},
|
|
33
|
+
"statuses": [
|
|
34
|
+
{
|
|
35
|
+
"id": "0001-layers",
|
|
36
|
+
"status": "accepted"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"bindings": [
|
|
40
|
+
{
|
|
41
|
+
"adr": "0001-layers",
|
|
42
|
+
"binding": "intentForbiddenEdge"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"supersedes": [],
|
|
46
|
+
"supersededBy": [],
|
|
47
|
+
"fitness": [
|
|
48
|
+
{
|
|
49
|
+
"id": "0001-layers",
|
|
50
|
+
"status": "accepted",
|
|
51
|
+
"level": "unverifiable",
|
|
52
|
+
"verified": false,
|
|
53
|
+
"reason": "no bound constraint for 0001-layers resolves or was evaluated — none can be verified"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"unresolved": [],
|
|
57
|
+
"knownFitness": [
|
|
58
|
+
"intentForbiddenEdge"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"tool": {
|
|
4
|
+
"name": "@ecoma-io/archkeep",
|
|
5
|
+
"version": "0.26.0"
|
|
6
|
+
},
|
|
7
|
+
"command": "change",
|
|
8
|
+
"workspace": {
|
|
9
|
+
"root": "/tmp/archkeep-golden-fixture",
|
|
10
|
+
"provider": "native",
|
|
11
|
+
"marker": "archkeep.json",
|
|
12
|
+
"provenance": {
|
|
13
|
+
"commit": "1fd51709c377d99a6139891e1200291cf08aede9",
|
|
14
|
+
"remote": null,
|
|
15
|
+
"dirty": false
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"status": "ok",
|
|
19
|
+
"exitCode": 0,
|
|
20
|
+
"coverage": {
|
|
21
|
+
"complete": true,
|
|
22
|
+
"projects": 3,
|
|
23
|
+
"analyzedFiles": 3,
|
|
24
|
+
"imports": 2,
|
|
25
|
+
"notAnalyzed": [],
|
|
26
|
+
"blindSpots": [],
|
|
27
|
+
"notes": []
|
|
28
|
+
},
|
|
29
|
+
"result": {
|
|
30
|
+
"intent": {
|
|
31
|
+
"file": "/tmp/archkeep-golden-fixture/.archkeep-change.json",
|
|
32
|
+
"version": "1",
|
|
33
|
+
"base": {
|
|
34
|
+
"commit": "1fd51709c377d99a6139891e1200291cf08aede9"
|
|
35
|
+
},
|
|
36
|
+
"declared": {
|
|
37
|
+
"projectsAdd": 0,
|
|
38
|
+
"projectsRemove": 0,
|
|
39
|
+
"edgesAdd": 0,
|
|
40
|
+
"edgesRemove": 0,
|
|
41
|
+
"constraints": []
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"baseline": {
|
|
45
|
+
"path": "/tmp/archkeep-golden-fixture/.archkeep-delta.json",
|
|
46
|
+
"tool": {
|
|
47
|
+
"name": "@ecoma-io/archkeep",
|
|
48
|
+
"version": "0.26.0"
|
|
49
|
+
},
|
|
50
|
+
"provider": "native",
|
|
51
|
+
"provenance": {
|
|
52
|
+
"commit": "1fd51709c377d99a6139891e1200291cf08aede9",
|
|
53
|
+
"dirty": false,
|
|
54
|
+
"remote": null
|
|
55
|
+
},
|
|
56
|
+
"policyFingerprint": "e18ba4307f6baaaa0d558822327788e8925cb3c52dfac6b170ce4d89b5a728ca",
|
|
57
|
+
"projects": 3,
|
|
58
|
+
"records": 2
|
|
59
|
+
},
|
|
60
|
+
"head": {
|
|
61
|
+
"provenance": {
|
|
62
|
+
"commit": "1fd51709c377d99a6139891e1200291cf08aede9",
|
|
63
|
+
"remote": null,
|
|
64
|
+
"dirty": false
|
|
65
|
+
},
|
|
66
|
+
"policyFingerprint": "e18ba4307f6baaaa0d558822327788e8925cb3c52dfac6b170ce4d89b5a728ca",
|
|
67
|
+
"projects": 3
|
|
68
|
+
},
|
|
69
|
+
"reconciliation": {
|
|
70
|
+
"verdict": "matched",
|
|
71
|
+
"reasons": [],
|
|
72
|
+
"matched": [],
|
|
73
|
+
"unexpected": [],
|
|
74
|
+
"missingExpected": []
|
|
75
|
+
},
|
|
76
|
+
"constraints": [],
|
|
77
|
+
"policy": {
|
|
78
|
+
"fingerprint": "e18ba4307f6baaaa0d558822327788e8925cb3c52dfac6b170ce4d89b5a728ca",
|
|
79
|
+
"changedSinceBase": false,
|
|
80
|
+
"liveViolations": 0
|
|
81
|
+
},
|
|
82
|
+
"classifications": [],
|
|
83
|
+
"affected": {
|
|
84
|
+
"projects": [],
|
|
85
|
+
"boundaries": [],
|
|
86
|
+
"constraints": [],
|
|
87
|
+
"decisions": []
|
|
88
|
+
},
|
|
89
|
+
"debt": {
|
|
90
|
+
"introduced": [],
|
|
91
|
+
"resolved": []
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"decision": {
|
|
95
|
+
"verdict": "pass"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
intent /tmp/archkeep-golden-fixture/.archkeep-change.json — base 1fd51709
|
|
2
|
+
baseline /tmp/archkeep-golden-fixture/.archkeep-delta.json — 1fd51709, 2 records, 3 projects
|
|
3
|
+
head 1fd51709, 3 projects
|
|
4
|
+
reconciliation ✔ MATCHED — the delta is exactly the declared change
|
|
5
|
+
workspace law 0 live violations under the current law — informational; archkeep check remains the authoritative verdict
|
|
6
|
+
reconciled 0 declared changes and 0 declared constraints — base 1fd51709 (3 projects, 2 records) against head 1fd51709 (3 projects)
|