@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.
Files changed (120) hide show
  1. package/cli.mjs +166 -506
  2. package/commands.mjs +7 -1
  3. package/package.json +1 -1
  4. package/src/architecture-intent/judge.mjs +1 -1
  5. package/src/architecture-intent/model.mjs +34 -0
  6. package/src/canonical.mjs +2 -1
  7. package/src/commands/adr-for-workspace.mjs +63 -0
  8. package/src/commands/analyze-capability.mjs +13 -0
  9. package/src/commands/change-intent.mjs +11 -0
  10. package/src/commands/change.mjs +247 -34
  11. package/src/commands/check-capability.mjs +13 -0
  12. package/src/commands/check.mjs +13 -3
  13. package/src/commands/compare-capability.mjs +20 -0
  14. package/src/commands/completeness.mjs +12 -0
  15. package/src/commands/context-command.mjs +31 -0
  16. package/src/commands/coverage-verdict.mjs +15 -2
  17. package/src/commands/debt.mjs +30 -0
  18. package/src/commands/decisions.mjs +59 -10
  19. package/src/commands/delta.mjs +263 -49
  20. package/src/commands/diff.mjs +45 -0
  21. package/src/commands/discover.mjs +37 -0
  22. package/src/commands/drift.mjs +45 -0
  23. package/src/commands/evolution.mjs +22 -2
  24. package/src/commands/explain-capability.mjs +11 -0
  25. package/src/commands/explain.mjs +24 -0
  26. package/src/commands/fitness.mjs +119 -10
  27. package/src/commands/govern-capability.mjs +16 -0
  28. package/src/commands/graph.mjs +39 -0
  29. package/src/commands/health.mjs +34 -0
  30. package/src/commands/history.mjs +85 -2
  31. package/src/commands/impact.mjs +23 -0
  32. package/src/commands/inspect-capability.mjs +16 -0
  33. package/src/commands/plan-context-command.mjs +35 -0
  34. package/src/commands/policy.mjs +42 -4
  35. package/src/commands/provenance-command.mjs +15 -24
  36. package/src/commands/report.mjs +38 -0
  37. package/src/commands/rules-capability.mjs +16 -0
  38. package/src/commands/rules.mjs +3 -1
  39. package/src/commands/scenario.mjs +23 -0
  40. package/src/commands/trajectory.mjs +35 -0
  41. package/src/commands/waivers.mjs +30 -0
  42. package/src/corpus/goldens/.gitkeep +0 -0
  43. package/src/corpus/goldens/adr.json +62 -0
  44. package/src/corpus/goldens/adr.text +6 -0
  45. package/src/corpus/goldens/change.json +98 -0
  46. package/src/corpus/goldens/change.text +6 -0
  47. package/src/corpus/goldens/check.json +90 -0
  48. package/src/corpus/goldens/check.sarif +479 -0
  49. package/src/corpus/goldens/check.text +9 -0
  50. package/src/corpus/goldens/context.json +47 -0
  51. package/src/corpus/goldens/context.text +6 -0
  52. package/src/corpus/goldens/debt.json +59 -0
  53. package/src/corpus/goldens/debt.text +7 -0
  54. package/src/corpus/goldens/decisions.json +74 -0
  55. package/src/corpus/goldens/decisions.text +7 -0
  56. package/src/corpus/goldens/delta.json +94 -0
  57. package/src/corpus/goldens/delta.sarif +472 -0
  58. package/src/corpus/goldens/delta.text +5 -0
  59. package/src/corpus/goldens/diff.json +58 -0
  60. package/src/corpus/goldens/diff.text +4 -0
  61. package/src/corpus/goldens/discover.json +77 -0
  62. package/src/corpus/goldens/discover.text +10 -0
  63. package/src/corpus/goldens/drift.json +43 -0
  64. package/src/corpus/goldens/drift.text +3 -0
  65. package/src/corpus/goldens/evolution.json +328 -0
  66. package/src/corpus/goldens/evolution.text +32 -0
  67. package/src/corpus/goldens/explain.json +60 -0
  68. package/src/corpus/goldens/explain.text +7 -0
  69. package/src/corpus/goldens/fitness.json +45 -0
  70. package/src/corpus/goldens/fitness.text +3 -0
  71. package/src/corpus/goldens/graph.json +78 -0
  72. package/src/corpus/goldens/graph.text +10 -0
  73. package/src/corpus/goldens/health.json +72 -0
  74. package/src/corpus/goldens/health.text +10 -0
  75. package/src/corpus/goldens/help.text +104 -0
  76. package/src/corpus/goldens/history.json +43 -0
  77. package/src/corpus/goldens/history.text +4 -0
  78. package/src/corpus/goldens/impact.json +327 -0
  79. package/src/corpus/goldens/impact.text +10 -0
  80. package/src/corpus/goldens/provenance.json +453 -0
  81. package/src/corpus/goldens/provenance.text +14 -0
  82. package/src/corpus/goldens/reconcile.json +168 -0
  83. package/src/corpus/goldens/reconcile.text +3 -0
  84. package/src/corpus/goldens/report.json +168 -0
  85. package/src/corpus/goldens/report.text +42 -0
  86. package/src/corpus/goldens/rules verify.json +0 -0
  87. package/src/corpus/goldens/rules verify.text +0 -0
  88. package/src/corpus/goldens/scenario.json +441 -0
  89. package/src/corpus/goldens/scenario.text +28 -0
  90. package/src/corpus/goldens/trajectory.json +83 -0
  91. package/src/corpus/goldens/trajectory.text +10 -0
  92. package/src/corpus/goldens/usage-error.text +105 -0
  93. package/src/corpus/goldens/waivers.json +40 -0
  94. package/src/corpus/goldens/waivers.text +1 -0
  95. package/src/corpus/goldens-lsp/initialize.result.json +15 -0
  96. package/src/corpus/goldens-lsp/publish-cleared-empty.json +4 -0
  97. package/src/corpus/goldens-lsp/publish-fixed-empty.json +5 -0
  98. package/src/corpus/goldens-lsp/publish-unparseable.json +54 -0
  99. package/src/corpus/goldens-lsp/publish-violation.json +22 -0
  100. package/src/corpus/goldens-lsp/register-watched-files.json +72 -0
  101. package/src/go-work.mjs +8 -23
  102. package/src/governance/evolution-event.mjs +15 -0
  103. package/src/governance/evolution-store.mjs +59 -31
  104. package/src/governance/provenance-record.mjs +0 -152
  105. package/src/governance/verdict.mjs +26 -3
  106. package/src/intent/intent-manifest.json +14 -14
  107. package/src/lsp/server.mjs +5 -8
  108. package/src/lsp/workspace-index.mjs +55 -220
  109. package/src/options.mjs +10 -0
  110. package/src/providers/native/discover.mjs +13 -12
  111. package/src/providers/native/model.mjs +8 -3
  112. package/src/providers/nx-static.mjs +231 -0
  113. package/src/report/json.mjs +3 -2
  114. package/src/report/sarif.mjs +8 -3
  115. package/src/rules/edge-constraints.mjs +9 -0
  116. package/src/rules/index.mjs +26 -10
  117. package/src/rules/messages.mjs +64 -14
  118. package/src/rules/specifiers.mjs +21 -20
  119. package/src/tsconfig-paths.mjs +8 -14
  120. package/src/verdict.mjs +127 -16
@@ -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
+ }
@@ -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` — the duplication is what let two defects land in it independently:
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
- * eleven sites, is what makes that defect class structurally impossible to
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
- * eleven callers keep sharing the one ladder this function exists to be.
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 { loadBoundaryConfig } from "../config.mjs";
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, options } = commandContext;
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 is either a filename (the string form `loadBoundaryConfig`
258
- // reads) or an inline policy object living directly in `archkeep.json`
259
- // (`../providers/native/model.mjs`, `normalizeNativeModel`'s
260
- // `inlineBoundaryConfig`). Both are walked — a policy whose rows the report
261
- // never inspected would claim "every row attests" over an unread table,
262
- // which is the silent direction this command exists to end.
263
- const boundaryConfig = options.boundaryConfig;
264
- const walked = [];
265
- let loadedConfig = null;
266
- if (typeof boundaryConfig === "string") {
267
- const config = await (io.loadConfigOverride ?? loadBoundaryConfig)(root, boundaryConfig);
268
- loadedConfig = config;
269
- walked.push(...configRows(config));
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 });
@@ -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";
@@ -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", catalog: object, report: {text: string, json: string}}>}
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
+ }
@@ -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,6 @@
1
+ 0001-layers (accepted)
2
+ -----------------------
3
+ bindings: intentForbiddenEdge
4
+ fitness: unverifiable — no bound constraint for 0001-layers resolves or was evaluated — none can be verified
5
+ status set: proposed, accepted, active, superseded, retired
6
+
@@ -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)