@ecoma-io/archkeep 0.26.0 → 0.27.1

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 (62) hide show
  1. package/cli.mjs +4 -3
  2. package/package.json +2 -1
  3. package/src/architecture-intent/model.mjs +2 -2
  4. package/src/commands/README.md +7 -4
  5. package/src/commands/analyze-capability.mjs +2 -2
  6. package/src/commands/change-intent.mjs +2 -2
  7. package/src/commands/change.mjs +9 -5
  8. package/src/commands/check-capability.mjs +2 -2
  9. package/src/commands/check.mjs +3 -5
  10. package/src/commands/compare-capability.mjs +2 -2
  11. package/src/commands/completeness.mjs +2 -2
  12. package/src/commands/delta-snapshot.mjs +17 -0
  13. package/src/commands/delta.mjs +9 -5
  14. package/src/commands/diff.mjs +2 -2
  15. package/src/commands/evolution.mjs +10 -0
  16. package/src/commands/explain-capability.mjs +2 -2
  17. package/src/commands/fitness.mjs +2 -2
  18. package/src/commands/govern-capability.mjs +2 -2
  19. package/src/commands/inspect-capability.mjs +2 -2
  20. package/src/commands/rules-capability.mjs +2 -2
  21. package/src/commands/scenario-evaluation.mjs +97 -3
  22. package/src/corpus/goldens/adr.json +2 -2
  23. package/src/corpus/goldens/change.json +6 -5
  24. package/src/corpus/goldens/change.text +2 -2
  25. package/src/corpus/goldens/check.json +2 -2
  26. package/src/corpus/goldens/context.json +2 -2
  27. package/src/corpus/goldens/debt.json +4 -4
  28. package/src/corpus/goldens/debt.text +2 -2
  29. package/src/corpus/goldens/decisions.json +2 -2
  30. package/src/corpus/goldens/delta.json +4 -4
  31. package/src/corpus/goldens/delta.text +1 -1
  32. package/src/corpus/goldens/diff.json +4 -4
  33. package/src/corpus/goldens/diff.text +1 -1
  34. package/src/corpus/goldens/discover.json +2 -2
  35. package/src/corpus/goldens/drift.json +2 -2
  36. package/src/corpus/goldens/evolution.json +4 -2
  37. package/src/corpus/goldens/explain.json +2 -2
  38. package/src/corpus/goldens/fitness.json +2 -2
  39. package/src/corpus/goldens/graph.json +2 -2
  40. package/src/corpus/goldens/health.json +2 -2
  41. package/src/corpus/goldens/history.json +3 -3
  42. package/src/corpus/goldens/history.text +1 -1
  43. package/src/corpus/goldens/impact.json +2 -2
  44. package/src/corpus/goldens/provenance.json +2 -2
  45. package/src/corpus/goldens/reconcile.json +2 -2
  46. package/src/corpus/goldens/report.json +2 -2
  47. package/src/corpus/goldens/report.text +1 -1
  48. package/src/corpus/goldens/scenario.json +2 -2
  49. package/src/corpus/goldens/trajectory.json +3 -3
  50. package/src/corpus/goldens/trajectory.text +1 -1
  51. package/src/corpus/goldens/waivers.json +2 -2
  52. package/src/go-work.mjs +4 -5
  53. package/src/governance/debt-ledger.mjs +5 -2
  54. package/src/governance/evolution-event.mjs +1 -2
  55. package/src/governance/reconcile-score.mjs +59 -9
  56. package/src/intent/intent-manifest.json +8 -8
  57. package/src/providers/nx-static.mjs +4 -4
  58. package/src/report/text.mjs +1 -1
  59. package/src/rules/edge-constraints.mjs +4 -5
  60. package/src/rules/index.mjs +8 -10
  61. package/src/tsconfig-paths.mjs +4 -5
  62. package/src/fixtures/evolution-lifecycle/workspace.mjs +0 -248
package/cli.mjs CHANGED
@@ -57,7 +57,7 @@
57
57
  * declared constraint. `check`, `fitness`, `delta` and `change` are the
58
58
  * verbs whose verdicts carry this code, plus `rules verify` — the
59
59
  * artifact-integrity fold, a bounded verification surface, not a fifth
60
- * architecture carrier (PD-8; `docs/reference/exit-codes.md` owns the
60
+ * architecture carrier (`docs/reference/exit-codes.md` owns the
61
61
  * roster). Every other verb only ever reads.
62
62
  * 2 usage error — unknown command, unknown flag, missing argument, path
63
63
  * outside the tree
@@ -105,7 +105,8 @@ import { containmentViolation } from "./src/containment.mjs";
105
105
  import { UsageError } from "./src/errors.mjs";
106
106
  // Verb imports route through the capability facades: each
107
107
  // `<word>-capability.mjs` module is its word's explicit verb roster — pure
108
- // re-exports, zero judgment (PD-18, docs/architecture/refactor/DECISIONS.md).
108
+ // re-exports, zero judgment. The vocabulary's roster lives in
109
+ // docs/concepts/architecture.md's "The 24 commands".
109
110
  // Non-verb helper modules keep their direct imports.
110
111
  import {
111
112
  discoverCommand,
@@ -1331,7 +1332,7 @@ async function runWaivers(options, { cwd, env }) {
1331
1332
  * (D-09). `check` folds the same `fail` into its own exit 1 by presence, so the
1332
1333
  * two faces agree; the verbs whose verdict carries that code are `check`,
1333
1334
  * `fitness`, `delta` and `change`, plus the `rules verify` artifact-integrity
1334
- * fold (PD-8; `docs/reference/exit-codes.md` owns the roster).
1335
+ * fold (`docs/reference/exit-codes.md` owns the roster).
1335
1336
  *
1336
1337
  * @param {{format: string, output: string|null, config: string|null, paths: string[]}} options
1337
1338
  * @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecoma-io/archkeep",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "Architecture authority for human and agentic software development — deterministic, evidence-backed enforcement of declared architecture.",
5
5
  "keywords": [
6
6
  "architecture",
@@ -73,6 +73,7 @@
73
73
  "!src/custom-rules/wasm-fixture.mjs",
74
74
  "!src/report/envelope-shape.json",
75
75
  "!src/custom-rules/evidence-golden.json",
76
+ "!src/fixtures/",
76
77
  "presets/",
77
78
  "LICENSE",
78
79
  "README.md"
@@ -12,8 +12,8 @@
12
12
  * is a deterministic comparison (`./judge.mjs`). NO LLM/AI anywhere in the core.
13
13
  *
14
14
  * Which "intent" this is: one of four unrelated surfaces sharing the word —
15
- * PD-13 outcome (c), four surfaces, never one type
16
- * (`../../../../docs/architecture/refactor/DECISIONS.md`). This module is
15
+ * four surfaces, never one type
16
+ * (`../../../../docs/concepts/architecture.md`, "The 24 commands"). This module is
17
17
  * the workspace-declared law: the grammar, validation, and loading of
18
18
  * `architecture-intent.json`, judged against the observed graph by `check`
19
19
  * and `drift`. The other three: the v1.0 evidence-manifest registry
@@ -54,8 +54,10 @@ the resolution order.
54
54
  introduced/resolved/unchanged/unknown (`./delta-classify.mjs`), with
55
55
  unresolvable import sites carried as their own category, never counted as
56
56
  violations. Refuses an unreadable, malformed, foreign-schema, or
57
- incomplete-coverage baseline, a provider mismatch (stricter than `diff`'s
58
- note violation identity across two project models is not evidence),
57
+ incomplete-coverage baseline, a `command`-carrying report envelope (a graph
58
+ snapshot is `diff`'s input, not delta evidence), a provider mismatch
59
+ (stricter than `diff`'s note — violation identity across two project models
60
+ is not evidence),
59
61
  incomplete head coverage, and an Nx workspace with polyglot manifests but no
60
62
  plugin registration; a policy-fingerprint change is a loud coverage note, not
61
63
  a refusal. A verdict, not a description: a non-waived introduced violation is
@@ -84,8 +86,9 @@ the resolution order.
84
86
  pins); an unproven base identity or an undeterminable
85
87
  constraint is exit 3, and constraints are left unevaluated over a base the
86
88
  run cannot vouch for. Refuses a manifest that fails shape or reference
87
- validation, an unreadable/malformed/incomplete baseline, a provider
88
- mismatch, incomplete head coverage, and the unregistered-plugin graph.
89
+ validation, an unreadable/malformed/incomplete baseline, a `command`-carrying
90
+ report envelope in its place, a provider mismatch, incomplete head coverage,
91
+ and the unregistered-plugin graph.
89
92
 
90
93
  - **`impact`** (`./impact.mjs`'s `impactCommand`) — reverse reachability from
91
94
  the project graph: given a project name, lists every project that transitively
@@ -7,7 +7,7 @@
7
7
  * Pure surface module: no judgment, no defaults, no logic — every export is
8
8
  * a re-export of a command module. `cli.mjs` routes its verb imports through
9
9
  * the facade so the vocabulary's code referent is visible to the import
10
- * graph, not just to a comment (PD-18,
11
- * ../../../../docs/architecture/refactor/DECISIONS.md).
10
+ * graph, not just to a comment — the vocabulary's roster lives in
11
+ * ../../../../docs/concepts/architecture.md's "The 24 commands".
12
12
  */
13
13
  export { discoverCommand, intentJsonFromProposal, intentWriteRefusal } from "./discover.mjs";
@@ -10,8 +10,8 @@
10
10
  * as the reviewable answer to "what did this change do to the architecture?".
11
11
  *
12
12
  * Which "intent" this is: one of four unrelated surfaces sharing the word —
13
- * PD-13 outcome (c), four surfaces, never one type
14
- * (`../../../../docs/architecture/refactor/DECISIONS.md`). This module is
13
+ * four surfaces, never one type
14
+ * (`../../../../docs/concepts/architecture.md`, "The 24 commands"). This module is
15
15
  * the declared-change grammar: one change's expected architectural
16
16
  * consequences, verified by the `change` command against the actual delta.
17
17
  * The other three: the workspace-declared law (`../architecture-intent/` —
@@ -76,8 +76,9 @@
76
76
  * exit 3, a `coverage` block naming every file and site the run could not
77
77
  * judge — where a parser and `--output` can read it; the rest are throws →
78
78
  * exit 3 upstream: a manifest that fails shape or reference validation, an
79
- * unreadable/malformed/foreign-schema baseline, incomplete baseline coverage,
80
- * a provider mismatch, an unregistered-plugin graph over polyglot manifests,
79
+ * unreadable/malformed/foreign-schema baseline, a `command`-carrying report
80
+ * envelope in its place, incomplete baseline coverage, a provider mismatch,
81
+ * an unregistered-plugin graph over polyglot manifests,
81
82
  * and a run with no boundary law (constraints and the law fingerprint need
82
83
  * one).
83
84
  *
@@ -480,8 +481,8 @@ function changeFoldInputProblem(reconciliation, constraints, unprovenReasons) {
480
481
  * before any list, so a malformed one reaching it unguarded can name the
481
482
  * whole run unproven or matched for reasons the counts never supported. The
482
483
  * literals stay hand-rolled per site by decision — the carrier folds are the
483
- * pinned baseline INV-2 names, not a table to be converged
484
- * (`docs/architecture/refactor/AUTHORITY-MAP.md`).
484
+ * pinned baseline, not a table to be converged
485
+ * (`docs/reference/exit-codes.md`'s fold-site statement).
485
486
  *
486
487
  * @param {{matched: object[], unexpected: object[], missingExpected: object[]}} reconciliation
487
488
  * @param {{verdict: string}[]} constraints Judged `fitnessVerdict` rows —
@@ -1000,10 +1001,11 @@ export async function changeCommand(
1000
1001
  // `findings`, never in `debt`. An unproven base (F-CHG-1) or an unjudgeable
1001
1002
  // intent is a no-verdict: no ids are emitted, an in-band note says so, and a
1002
1003
  // change run never fabricates ledger ids over evidence it cannot vouch for.
1003
- /** @type {{introduced: string[], resolved: string[], note?: string}} */
1004
+ /** @type {{judged: boolean, introduced: string[], resolved: string[], note?: string}} the marker is in-band on every shape: `false` on a fail-closed path, `true` from `debtChangeDiff` */
1004
1005
  let debt;
1005
1006
  if (baseEngineGraph === null) {
1006
1007
  debt = {
1008
+ judged: false,
1007
1009
  introduced: [],
1008
1010
  resolved: [],
1009
1011
  note: "base identity unproven — no architecture debt diff can be trusted",
@@ -1013,6 +1015,7 @@ export async function changeCommand(
1013
1015
  const archIntent = await (loadIntentOverride ?? loadIntent)(root, { tracked });
1014
1016
  if (archIntent === undefined || archIntent === null) {
1015
1017
  debt = {
1018
+ judged: false,
1016
1019
  introduced: [],
1017
1020
  resolved: [],
1018
1021
  note: `no '${INTENT_FILE}' tracked — the change event carries no architecture debt ids`,
@@ -1024,6 +1027,7 @@ export async function changeCommand(
1024
1027
  }
1025
1028
  } catch (error) {
1026
1029
  debt = {
1030
+ judged: false,
1027
1031
  introduced: [],
1028
1032
  resolved: [],
1029
1033
  note: `architecture intent could not be judged — no debt ids emitted (${error.message})`,
@@ -5,8 +5,8 @@
5
5
  * Pure surface module: no judgment, no defaults, no logic — every export is
6
6
  * a re-export of a command module. `cli.mjs` routes its verb imports through
7
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).
8
+ * graph, not just to a comment — the vocabulary's roster lives in
9
+ * ../../../../docs/concepts/architecture.md's "The 24 commands".
10
10
  */
11
11
  export { check, sortViolations } from "./check.mjs";
12
12
  export { fitness } from "./fitness.mjs";
@@ -522,14 +522,12 @@ export async function check(
522
522
  // constraint from the edge — so suppression, waiver annotation, sorting and
523
523
  // the SARIF face all treat it as the ordinary violation it is.
524
524
  //
525
- // PD-13 (2026-09-06) pins THIS fold as the documented normalization seam
526
- // (`../../../../docs/architecture/refactor/DECISIONS.md`): the one place
525
+ // This fold is the documented normalization seam — the one place
527
526
  // `judgeEdge`'s markdown-pairing verdicts are reshaped into the exact
528
527
  // `Violation` record `violationOf` (`../rules/index.mjs`) builds — the
529
528
  // families then fold into the one verdict lane as count keys into
530
- // `verdictFor` (`../verdict.mjs`). The canonical statement is the
531
- // "Finding the unowned concept" section of
532
- // `../../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
529
+ // `verdictFor` (`../verdict.mjs`). The canonical statement is
530
+ // `../../../../docs/concepts/evidence.md`'s "Four finding families".
533
531
  /** @type {object[]} */
534
532
  const markdownRaw = [];
535
533
  if (config !== null && markdownTrack !== null && config.depConstraints.length > 0) {
@@ -7,8 +7,8 @@
7
7
  * Pure surface module: no judgment, no defaults, no logic — every export is
8
8
  * a re-export of a command module. `cli.mjs` routes its verb imports through
9
9
  * the facade so the vocabulary's code referent is visible to the import
10
- * graph, not just to a comment (PD-18,
11
- * ../../../../docs/architecture/refactor/DECISIONS.md).
10
+ * graph, not just to a comment — the vocabulary's roster lives in
11
+ * ../../../../docs/concepts/architecture.md's "The 24 commands".
12
12
  */
13
13
  export { diff } from "./diff.mjs";
14
14
  export { captureBaseline, delta } from "./delta.mjs";
@@ -33,8 +33,8 @@
33
33
  * refusal contract (`./coverage-verdict.mjs`): `coverageVerdict` refuses
34
34
  * `no-verdict` (exit 3), and no status here is derived from it or folds
35
35
  * into it. Two models of "did we see everything", owned by different
36
- * waves — register R1 in `docs/architecture/refactor/SEMANTIC-MODEL.md`
37
- * keeps them separate.
36
+ * waves — register R1 in `../../../../docs/concepts/evidence.md`'s "Register
37
+ * R1" keeps them separate.
38
38
  *
39
39
  * @module
40
40
  */
@@ -320,6 +320,10 @@ export function readEvidenceSnapshot(path, io = {}) {
320
320
  *
321
321
  * Refusals, each loud:
322
322
  * - unreadable/malformed JSON — named with the path and the parse error;
323
+ * - a `command` field — that marker belongs to a report envelope (the graph
324
+ * family), so the document is not delta evidence at all; the family is
325
+ * decided before the schemaVersion refusals, whose "newer version" advice
326
+ * would be false for a file this binary itself wrote;
323
327
  * - a `schemaVersion` that is not the integer this format uses — a FUTURE
324
328
  * version refuses too: a reader that half-understood a newer format would
325
329
  * classify over evidence it misread;
@@ -360,6 +364,19 @@ export function parseEvidenceSnapshot(text, path) {
360
364
  );
361
365
  }
362
366
 
367
+ // A `command` field marks a report envelope — the graph family's document,
368
+ // not delta evidence. The family decides before the schemaVersion refusals
369
+ // below: that number is the OTHER format's version, so its "newer version;
370
+ // upgrade" advice would be false for a file this binary itself wrote (#810).
371
+ if (typeof parsed.command === "string") {
372
+ const pointer = parsed.command === "graph" ? " For graph snapshots use 'diff <baseline>'." : "";
373
+ throw new Error(
374
+ `archkeep: the evidence snapshot '${path}' has a 'command' field — it is not a delta ` +
375
+ `evidence snapshot, it is a '${parsed.command}' envelope. delta requires an evidence ` +
376
+ `snapshot (from 'delta --capture').${pointer}`,
377
+ );
378
+ }
379
+
363
380
  const problems = [];
364
381
  if (!Number.isInteger(parsed.schemaVersion)) {
365
382
  problems.push(
@@ -29,8 +29,10 @@
29
29
  * status "no-verdict", exit 3, a `coverage` block naming every file and site
30
30
  * the run could not judge — where a parser and `--output` can read it; the
31
31
  * rest are throws, exit 3 upstream:
32
- * - a baseline that cannot be read, parsed, or holds a foreign schemaVersion
33
- * (`./delta-snapshot.mjs`'s loader owns those);
32
+ * - a baseline that cannot be read, parsed, or holds a foreign schemaVersion,
33
+ * or a report envelope — a document carrying a `command` field (a graph
34
+ * snapshot is `diff`'s input), refused as the wrong family before any
35
+ * schemaVersion reading (`./delta-snapshot.mjs`'s loader owns those);
34
36
  * - a provider mismatch between baseline and this run (`providerMismatch`) —
35
37
  * a THROW here where `diff` settles for a note, because violation IDENTITY
36
38
  * across two different project models is not trustworthy: the same tree
@@ -479,8 +481,8 @@ function deltaFoldInputProblem(classification, custom) {
479
481
  * The input latch runs first: a bucket the fold cannot read is refused as a
480
482
  * no-verdict whose single reason names it (`refused`), never folded past as a
481
483
  * silently-empty count. The literals stay hand-rolled per site by decision —
482
- * the carrier folds are the pinned baseline INV-2 names, not a table to be
483
- * converged (`docs/architecture/refactor/AUTHORITY-MAP.md`).
484
+ * the carrier folds are the pinned baseline, not a table to be converged
485
+ * (`docs/reference/exit-codes.md`'s fold-site statement).
484
486
  *
485
487
  * Pure and exported for the fold's own tests; `deltaCommand` is its only
486
488
  * production caller.
@@ -1029,12 +1031,13 @@ export async function deltaCommand(
1029
1031
  // gate; the fail-closed branches are an absent intent and an unjudgeable
1030
1032
  // one, each emitting no ids and an in-band note rather than a fabricated
1031
1033
  // clean ledger.
1032
- /** @type {{introduced: string[], resolved: string[], note?: string}} */
1034
+ /** @type {{judged: boolean, introduced: string[], resolved: string[], note?: string}} the marker is in-band on every shape: `false` on a fail-closed path, `true` from `debtChangeDiff` */
1033
1035
  let debt;
1034
1036
  try {
1035
1037
  const archIntent = await (loadIntentOverride ?? loadIntent)(root, { tracked });
1036
1038
  if (archIntent === undefined || archIntent === null) {
1037
1039
  debt = {
1040
+ judged: false,
1038
1041
  introduced: [],
1039
1042
  resolved: [],
1040
1043
  note: `no '${INTENT_FILE}' tracked — the delta event carries no architecture debt ids`,
@@ -1046,6 +1049,7 @@ export async function deltaCommand(
1046
1049
  }
1047
1050
  } catch (error) {
1048
1051
  debt = {
1052
+ judged: false,
1049
1053
  introduced: [],
1050
1054
  resolved: [],
1051
1055
  note: `architecture intent could not be judged — no debt ids emitted (${error.message})`,
@@ -245,8 +245,8 @@ function buildHeadSnapshot(commandContext) {
245
245
  * `edgeEvolutionIdentity` (`../governance/evolution-event.mjs`), the escaped
246
246
  * `source>target:type` string evolution events carry, and the two spellings
247
247
  * are deliberately not unified — each medium keeps exactly one identity
248
- * constructor (`../../../../docs/adr/0008-snapshot-identity-per-family.md`;
249
- * INV-6 in `../../../../docs/architecture/refactor/INVARIANTS.md`). An edge
248
+ * constructor (`../../../../docs/adr/0008-snapshot-identity-per-family.md`).
249
+ * An edge
250
250
  * crosses from a structural diff into an event as its raw
251
251
  * `{source, target, type}` triple, mapped through that function at the
252
252
  * command boundary; a `\0`-joined key written into an event record would
@@ -940,6 +940,16 @@ export function buildEvolutionSummary(comparisons) {
940
940
  });
941
941
  return { verdictDeltas };
942
942
  }
943
+ if (axis === "debt") {
944
+ // The aggregate states whether the debt it unions was judged at every
945
+ // transition (`debtChangeDiff`'s marker) — a summary over a transition
946
+ // that could not judge its debt never reads as a judged clean union.
947
+ return {
948
+ judged: comparisons.every((c) => c.debt === undefined || c.debt.judged === true),
949
+ introduced: collect("introduced"),
950
+ resolved: collect("resolved"),
951
+ };
952
+ }
943
953
  return {
944
954
  introduced: collect("introduced"),
945
955
  resolved: collect("resolved"),
@@ -5,7 +5,7 @@
5
5
  * Pure surface module: no judgment, no defaults, no logic — every export is
6
6
  * a re-export of a command module. `cli.mjs` routes its verb imports through
7
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).
8
+ * graph, not just to a comment — the vocabulary's roster lives in
9
+ * ../../../../docs/concepts/architecture.md's "The 24 commands".
10
10
  */
11
11
  export { explain } from "./explain.mjs";
@@ -122,8 +122,8 @@ export function declaresFitness(config) {
122
122
  * exit over a run the fold could not read. A verdict outside the four states
123
123
  * is refused as a no-verdict whose single reason names it (`refused`), never
124
124
  * folded past. The literals stay hand-rolled per site by decision — the
125
- * carrier folds are the pinned baseline INV-2 names, not a table to be
126
- * converged (`docs/architecture/refactor/AUTHORITY-MAP.md`).
125
+ * carrier folds are the pinned baseline, not a table to be
126
+ * converged (`docs/reference/exit-codes.md`'s fold-site statement).
127
127
  *
128
128
  * `overall.decisions` is validated for the same reason the delta fold
129
129
  * validates buckets the fold itself does not count first: `result.functions`
@@ -5,8 +5,8 @@
5
5
  * Pure surface module: no judgment, no defaults, no logic — every export is
6
6
  * a re-export of a command module. `cli.mjs` routes its verb imports through
7
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).
8
+ * graph, not just to a comment — the vocabulary's roster lives in
9
+ * ../../../../docs/concepts/architecture.md's "The 24 commands".
10
10
  */
11
11
  export { waivers } from "./waivers.mjs";
12
12
  export { report } from "./report.mjs";
@@ -6,8 +6,8 @@
6
6
  * Pure surface module: no judgment, no defaults, no logic — every export is
7
7
  * a re-export of a command module. `cli.mjs` routes its verb imports through
8
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).
9
+ * graph, not just to a comment — the vocabulary's roster lives in
10
+ * ../../../../docs/concepts/architecture.md's "The 24 commands".
11
11
  */
12
12
  export { graph } from "./graph.mjs";
13
13
  export { impact } from "./impact.mjs";
@@ -5,8 +5,8 @@
5
5
  * Pure surface module: no judgment, no defaults, no logic — every export is
6
6
  * a re-export of a command module. `cli.mjs` routes its verb imports through
7
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).
8
+ * graph, not just to a comment — the vocabulary's roster lives in
9
+ * ../../../../docs/concepts/architecture.md's "The 24 commands".
10
10
  */
11
11
  export {
12
12
  rulesAddCommand,
@@ -152,11 +152,13 @@ function cloneGraph(graph) {
152
152
  *
153
153
  * @param {object} graph The base graph to apply changes to.
154
154
  * @param {DependencyChange[]} changes The hypothetical changes.
155
- * @returns {{graph: object, applied: string[], refused: string[]}}
155
+ * @returns {{graph: object, applied: string[],
156
+ * mutations: {type: string, source: string, target: string}[], refused: string[]}}
156
157
  */
157
158
  function applyChanges(graph, changes) {
158
159
  const cloned = cloneGraph(graph);
159
160
  const applied = [];
161
+ const mutations = [];
160
162
  const refused = [];
161
163
 
162
164
  for (const change of changes) {
@@ -203,6 +205,7 @@ function applyChanges(graph, changes) {
203
205
  source: change.source,
204
206
  });
205
207
  applied.push(`added dependency: ${change.source} → ${change.target} (${change.edgeType})`);
208
+ mutations.push({ type: "dependency_added", source: change.source, target: change.target });
206
209
  }
207
210
 
208
211
  if (change.type === "dependency_removed") {
@@ -238,6 +241,7 @@ function applyChanges(graph, changes) {
238
241
  existing.splice(idx, 1);
239
242
  const typeLabel = change.edgeType ? ` (${change.edgeType})` : "";
240
243
  applied.push(`removed dependency: ${change.source} → ${change.target}${typeLabel}`);
244
+ mutations.push({ type: "dependency_removed", source: change.source, target: change.target });
241
245
  }
242
246
  }
243
247
  // Clean up empty dependency arrays
@@ -247,7 +251,7 @@ function applyChanges(graph, changes) {
247
251
  }
248
252
  }
249
253
 
250
- return { graph: cloned, applied, refused };
254
+ return { graph: cloned, applied, mutations, refused };
251
255
  }
252
256
 
253
257
  /**
@@ -410,6 +414,50 @@ function resolveBaseRevision(root, userBase) {
410
414
  };
411
415
  }
412
416
 
417
+ /**
418
+ * Merges constraint-impact rows for the named project's own changed edges
419
+ * into one side's rows (#809), preserving the row shape the primitive
420
+ * returns — no new fields, no second judgment.
421
+ *
422
+ * One row per project: when the side already carries a row for the project,
423
+ * the edge, constraint-row and violation lists union without duplicates
424
+ * (constraint rows match by identity — they are the config's own row
425
+ * objects; violations by value, since `judgeEdge` builds fresh objects);
426
+ * otherwise the row is appended. Appending keeps every pre-existing row
427
+ * byte-identical and the order deterministic, because it follows the
428
+ * input changes' order.
429
+ *
430
+ * @param {{project: string, edges: object[], constraintRows: object[],
431
+ * violations: object[]}[]} rows The side's constraint impact, mutated in place.
432
+ * @param {{project: string, edges: object[], constraintRows: object[],
433
+ * violations: object[]}[]} merged Rows computed for the changed edge.
434
+ */
435
+ function mergeConstraintImpact(rows, merged) {
436
+ for (const row of merged) {
437
+ const existing = rows.find((r) => r.project === row.project);
438
+ if (!existing) {
439
+ rows.push(row);
440
+ continue;
441
+ }
442
+ for (const edge of row.edges) {
443
+ if (!existing.edges.some((e) => e.target === edge.target && e.type === edge.type)) {
444
+ existing.edges.push(edge);
445
+ }
446
+ }
447
+ for (const constraintRow of row.constraintRows) {
448
+ if (!existing.constraintRows.includes(constraintRow)) {
449
+ existing.constraintRows.push(constraintRow);
450
+ }
451
+ }
452
+ for (const violation of row.violations) {
453
+ const identity = JSON.stringify(violation);
454
+ if (!existing.violations.some((v) => JSON.stringify(v) === identity)) {
455
+ existing.violations.push(violation);
456
+ }
457
+ }
458
+ }
459
+ }
460
+
413
461
  /**
414
462
  * Evaluates a scenario against the current workspace.
415
463
  *
@@ -452,7 +500,12 @@ export function evaluateScenario(
452
500
  }
453
501
 
454
502
  // Step 3: Apply scenario changes to the graph
455
- const { graph: scenarioGraph, applied, refused } = applyChanges(graph, scenarioInput.changes);
503
+ const {
504
+ graph: scenarioGraph,
505
+ applied,
506
+ mutations,
507
+ refused,
508
+ } = applyChanges(graph, scenarioInput.changes);
456
509
 
457
510
  // Step 4: Compute scenario impact
458
511
  const scenarioImpact = computeImpact(projectName, scenarioGraph);
@@ -468,6 +521,47 @@ export function evaluateScenario(
468
521
  );
469
522
  }
470
523
 
524
+ // Step 4b: the named project's own changed edges (#809).
525
+ //
526
+ // `computeImpactConstraints` judges edges INTO the target from its
527
+ // dependents, so a change whose SOURCE is the named project never enters
528
+ // that frame — an edge pointing out of the target is invisible to it, and
529
+ // the source-named run reported `unchanged` where the target-named run of
530
+ // the same change reported the violation. The rows for the changed edge
531
+ // are computed here by the same primitive — never a second judgment — and
532
+ // merged into the frame the edge actually belongs to: additions into the
533
+ // scenario side (the edge exists only there), removals into the current
534
+ // side (it exists only there).
535
+ if (config && config.depConstraints) {
536
+ for (const mutation of mutations) {
537
+ if (mutation.source !== projectName) continue;
538
+ if (mutation.type === "dependency_added") {
539
+ mergeConstraintImpact(
540
+ scenarioConstraintImpact,
541
+ computeImpactConstraints(
542
+ mutation.target,
543
+ [mutation.source],
544
+ scenarioGraph.nodes,
545
+ scenarioGraph.dependencies,
546
+ config.depConstraints,
547
+ ),
548
+ );
549
+ }
550
+ if (mutation.type === "dependency_removed") {
551
+ mergeConstraintImpact(
552
+ currentConstraintImpact,
553
+ computeImpactConstraints(
554
+ mutation.target,
555
+ [mutation.source],
556
+ graph.nodes,
557
+ graph.dependencies,
558
+ config.depConstraints,
559
+ ),
560
+ );
561
+ }
562
+ }
563
+ }
564
+
471
565
  // Step 5: Build decision impact for both sides
472
566
  const currentDecisionImpact = buildDecisionImpact(root, currentConstraintImpact, config);
473
567
  const scenarioDecisionImpact = buildDecisionImpact(root, scenarioConstraintImpact, config);
@@ -2,11 +2,11 @@
2
2
  "schemaVersion": 2,
3
3
  "tool": {
4
4
  "name": "@ecoma-io/archkeep",
5
- "version": "0.26.0"
5
+ "version": "0.27.1"
6
6
  },
7
7
  "command": "adr",
8
8
  "workspace": {
9
- "root": "/tmp/archkeep-golden-fixture",
9
+ "root": "<fixture-root>",
10
10
  "provider": "native",
11
11
  "marker": "docs/adr",
12
12
  "provenance": null
@@ -2,11 +2,11 @@
2
2
  "schemaVersion": 2,
3
3
  "tool": {
4
4
  "name": "@ecoma-io/archkeep",
5
- "version": "0.26.0"
5
+ "version": "0.27.1"
6
6
  },
7
7
  "command": "change",
8
8
  "workspace": {
9
- "root": "/tmp/archkeep-golden-fixture",
9
+ "root": "<fixture-root>",
10
10
  "provider": "native",
11
11
  "marker": "archkeep.json",
12
12
  "provenance": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "result": {
30
30
  "intent": {
31
- "file": "/tmp/archkeep-golden-fixture/.archkeep-change.json",
31
+ "file": "<fixture-root>/.archkeep-change.json",
32
32
  "version": "1",
33
33
  "base": {
34
34
  "commit": "1fd51709c377d99a6139891e1200291cf08aede9"
@@ -42,10 +42,10 @@
42
42
  }
43
43
  },
44
44
  "baseline": {
45
- "path": "/tmp/archkeep-golden-fixture/.archkeep-delta.json",
45
+ "path": "<fixture-root>/.archkeep-delta.json",
46
46
  "tool": {
47
47
  "name": "@ecoma-io/archkeep",
48
- "version": "0.26.0"
48
+ "version": "0.27.1"
49
49
  },
50
50
  "provider": "native",
51
51
  "provenance": {
@@ -87,6 +87,7 @@
87
87
  "decisions": []
88
88
  },
89
89
  "debt": {
90
+ "judged": true,
90
91
  "introduced": [],
91
92
  "resolved": []
92
93
  }
@@ -1,5 +1,5 @@
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
1
+ intent <fixture-root>/.archkeep-change.json — base 1fd51709
2
+ baseline <fixture-root>/.archkeep-delta.json — 1fd51709, 2 records, 3 projects
3
3
  head 1fd51709, 3 projects
4
4
  reconciliation ✔ MATCHED — the delta is exactly the declared change
5
5
  workspace law 0 live violations under the current law — informational; archkeep check remains the authoritative verdict
@@ -2,11 +2,11 @@
2
2
  "schemaVersion": 2,
3
3
  "tool": {
4
4
  "name": "@ecoma-io/archkeep",
5
- "version": "0.26.0"
5
+ "version": "0.27.1"
6
6
  },
7
7
  "command": "check",
8
8
  "workspace": {
9
- "root": "/tmp/archkeep-golden-fixture",
9
+ "root": "<fixture-root>",
10
10
  "provider": "native",
11
11
  "marker": "archkeep.json",
12
12
  "provenance": {
@@ -2,11 +2,11 @@
2
2
  "schemaVersion": 2,
3
3
  "tool": {
4
4
  "name": "@ecoma-io/archkeep",
5
- "version": "0.26.0"
5
+ "version": "0.27.1"
6
6
  },
7
7
  "command": "context",
8
8
  "workspace": {
9
- "root": "/tmp/archkeep-golden-fixture",
9
+ "root": "<fixture-root>",
10
10
  "provider": "native",
11
11
  "marker": "archkeep.json",
12
12
  "provenance": {