@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
@@ -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": "debt",
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": {
@@ -31,10 +31,10 @@
31
31
  ]
32
32
  },
33
33
  "result": {
34
- "dir": "/tmp/archkeep-golden-fixture/.archkeep-history",
34
+ "dir": "<fixture-root>/.archkeep-history",
35
35
  "snapshots": 1,
36
36
  "agings": false,
37
- "sampleTime": "2026-09-07T06:50:29.512Z",
37
+ "sampleTime": "2026-09-08T10:33:14.692Z",
38
38
  "entries": [],
39
39
  "resolved": [],
40
40
  "total": 0,
@@ -1,7 +1,7 @@
1
- debt /tmp/archkeep-golden-fixture/.archkeep-history
1
+ debt <fixture-root>/.archkeep-history
2
2
  0 entries across 1 snapshot — ages not yet established (fewer than two snapshots)
3
3
  ✔ no architecture debt — no waivers, aspirational gaps, drift or unresolved intent
4
4
  no event store linked — lifecycle refs unavailable
5
5
  total 0 entries · byKind: waiver 0, expired-waiver 0, aspirational-gap 0, drift 0, unresolved 0 · bySeverity: high 0, medium 0, low 0
6
- sampled 2026-09-07T06:50:29.192Z
6
+ sampled 2026-09-08T10:33:14.369Z
7
7
  ✔ complete (2 imports in 3 files across 3 projects)
@@ -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": "decisions",
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": "delta",
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,10 +28,10 @@
28
28
  },
29
29
  "result": {
30
30
  "baseline": {
31
- "path": "/tmp/archkeep-golden-fixture/.archkeep-delta.json",
31
+ "path": "<fixture-root>/.archkeep-delta.json",
32
32
  "tool": {
33
33
  "name": "@ecoma-io/archkeep",
34
- "version": "0.26.0"
34
+ "version": "0.27.1"
35
35
  },
36
36
  "provider": "native",
37
37
  "provenance": {
@@ -1,4 +1,4 @@
1
- baseline /tmp/archkeep-golden-fixture/.archkeep-delta.json — 1fd51709, 2 records, 3 projects
1
+ baseline <fixture-root>/.archkeep-delta.json — 1fd51709, 2 records, 3 projects
2
2
  head 1fd51709, 2 records, 3 projects
3
3
  ✔ no introduced violations — compared baseline 1fd51709 (2 records) against head 1fd51709 (2 records, 3 analyzed files, 3 projects)
4
4
  classifications none
@@ -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": "diff",
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": {
@@ -30,10 +30,10 @@
30
30
  },
31
31
  "result": {
32
32
  "baseline": {
33
- "path": "/tmp/archkeep-golden-fixture/.archkeep-graph.json",
33
+ "path": "<fixture-root>/.archkeep-graph.json",
34
34
  "projects": 3,
35
35
  "edges": 2,
36
- "toolVersion": "0.26.0",
36
+ "toolVersion": "0.27.1",
37
37
  "provenance": {
38
38
  "commit": "1fd51709c377d99a6139891e1200291cf08aede9",
39
39
  "remote": null,
@@ -1,4 +1,4 @@
1
- baseline /tmp/archkeep-golden-fixture/.archkeep-graph.json — 3 projects, 2 edges
1
+ baseline <fixture-root>/.archkeep-graph.json — 3 projects, 2 edges
2
2
  head 3 projects, 2 edges
3
3
  ✔ no boundary-rule impact
4
4
  ✔ no changes between baseline and head (2 imports in 3 files across 3 projects; per-edge rule-impact covers only depConstraints (3 of 15 violation types; standing edges adjacent to a tags-changed project are re-judged under both sides' tags). A dependency with no rule-impact may still violate npm-ban, circular-dependency, lazy-load, or other rules that require import-site details. Run check for the complete 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": "discover",
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": "drift",
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": "evolution",
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": {
@@ -228,6 +228,7 @@
228
228
  "unknown": []
229
229
  },
230
230
  "debt": {
231
+ "judged": true,
231
232
  "introduced": [],
232
233
  "resolved": []
233
234
  },
@@ -309,6 +310,7 @@
309
310
  "unknown": []
310
311
  },
311
312
  "debt": {
313
+ "judged": true,
312
314
  "introduced": [],
313
315
  "resolved": []
314
316
  },
@@ -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": "explain",
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": "fitness",
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": "graph",
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": "health",
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": "history",
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": {
@@ -29,7 +29,7 @@
29
29
  ]
30
30
  },
31
31
  "result": {
32
- "dir": "/tmp/archkeep-golden-fixture/.archkeep-history",
32
+ "dir": "<fixture-root>/.archkeep-history",
33
33
  "captured": null,
34
34
  "snapshots": [
35
35
  {
@@ -1,4 +1,4 @@
1
- history /tmp/archkeep-golden-fixture/.archkeep-history
1
+ history <fixture-root>/.archkeep-history
2
2
  1 snapshot, 0 transitions (2 imports in 3 files across 3 projects)
3
3
  0 0001-71341a04.json 71341a04
4
4
  ✔ one snapshot, no transitions yet — capture again after an architectural change
@@ -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": "impact",
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": "provenance",
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": "reconcile",
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": "report",
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": {
@@ -1,7 +1,7 @@
1
1
  architecture governance report — every surface reached a verdict
2
2
 
3
3
  provenance
4
- root /tmp/archkeep-golden-fixture
4
+ root <fixture-root>
5
5
  provider native (archkeep.json)
6
6
  policy module-boundaries.config.mjs
7
7
  commit 1fd51709c377d99a6139891e1200291cf08aede9
@@ -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": "scenario",
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": "trajectory",
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": {
@@ -30,7 +30,7 @@
30
30
  ]
31
31
  },
32
32
  "result": {
33
- "dir": "/tmp/archkeep-golden-fixture/.archkeep-history",
33
+ "dir": "<fixture-root>/.archkeep-history",
34
34
  "observations": {
35
35
  "count": 1,
36
36
  "basis": "graph_snapshots",
@@ -1,4 +1,4 @@
1
- trajectory /tmp/archkeep-golden-fixture/.archkeep-history
1
+ trajectory <fixture-root>/.archkeep-history
2
2
  1 observation (graph_snapshots), 0 transitions
3
3
  ✖ insufficient_history: a trajectory needs at least two observations — derived values are unavailable, not zero
4
4
  signals architecture 0 · policy 0 · provider 0 · code drift 0 · incomparable 0 · unchanged 0
@@ -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": "waivers",
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": {
package/src/go-work.mjs CHANGED
@@ -276,13 +276,12 @@ function useDirectory(usePath, workspaceRoot) {
276
276
  * - **`goWorkOutsideUse`** — a `use` entry above the workspace root. Whatever
277
277
  * it builds, no run over this workspace can cover it.
278
278
  *
279
- * One of the FOUR finding families — PD-13 (2026-09-06) outcome (c): no
279
+ * One of the FOUR finding families: no
280
280
  * Finding supertype exists, and the relationship pin lives on
281
- * `./rules/index.mjs`'s `violationOf` header. These findings stay this
281
+ * `./rules/index.mjs`'s `violationOf` header (stated in
282
+ * `../../../docs/concepts/evidence.md`'s "Four finding families"). These findings stay this
282
283
  * family's own shape and fold into the one verdict lane as count keys into
283
- * `verdictFor` (`./verdict.mjs`). The canonical statement is the
284
- * "Finding — the unowned concept" section of
285
- * `../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
284
+ * `verdictFor` (`./verdict.mjs`).
286
285
  *
287
286
  * @param {{ uses: { path: string, line: number, column: number }[],
288
287
  * workspaceRoot: string,
@@ -208,7 +208,10 @@ export function driftFactOf(finding) {
208
208
  *
209
209
  * @param {object} baseVerdict A `judgeIntent` result over the base graph.
210
210
  * @param {object} headVerdict A `judgeIntent` result over the head graph.
211
- * @returns {{introduced: string[], resolved: string[]}} Stable debt ids.
211
+ * @returns {{judged: true, introduced: string[], resolved: string[]}} Stable
212
+ * debt ids, marked judged — the in-band complement of `judged: false`, so a
213
+ * consumer can always distinguish "no debt existed" from "debt could not be
214
+ * judged" (see `../../AGENTS.md`'s invariant).
212
215
  */
213
216
  export function debtChangeDiff(baseVerdict, headVerdict) {
214
217
  const driftOf = (v) => (v.findings ?? []).map((f) => debtFactId("drift", driftFactOf(f)));
@@ -218,7 +221,7 @@ export function debtChangeDiff(baseVerdict, headVerdict) {
218
221
  const headIds = new Set([...driftOf(headVerdict), ...gapOf(headVerdict)]);
219
222
  const introduced = [...headIds].filter((id) => !baseIds.has(id)).sort();
220
223
  const resolved = [...baseIds].filter((id) => !headIds.has(id)).sort();
221
- return { introduced, resolved };
224
+ return { judged: true, introduced, resolved };
222
225
  }
223
226
  /**
224
227
  * Reduces an `opts.events` value to a loaded event array, or `null` when no
@@ -198,8 +198,7 @@ export function escapeIdentityField(value) {
198
198
  * index maps, `trajectory`'s persistence sets — and is never stored. Different
199
199
  * media, one law: each family keeps exactly one identity constructor, and
200
200
  * consumers consume a constructor's output rather than re-deriving a second
201
- * spelling of it (`../../../../docs/adr/0008-snapshot-identity-per-family.md`,
202
- * INV-6 in `../../../../docs/architecture/refactor/INVARIANTS.md`). An edge
201
+ * spelling of it (`../../../../docs/adr/0008-snapshot-identity-per-family.md`). An edge
203
202
  * reaches this module only as its raw triple, never as a pre-built string
204
203
  * from either spelling (`evolutionBoundary` below refuses one), and a
205
204
  * `\0`-joined diff key written into an event would give one edge two
@@ -268,8 +268,13 @@ export function scoreEdge(edge, keys, intentForbiddenPairs, tagForbiddenPairs) {
268
268
  * Boundary `allowed`/`forbidden` rows are scored from the canonical judge's
269
269
  * findings (matched exactly by `from`/`to`): a `forbidden` row with an
270
270
  * `intentForbiddenEdge` finding is `unexpected`, an `allowed` row with an
271
- * `intentAllowedMissing` finding is `absent`. Project and dependency rows are
272
- * scored directly against the observed names and edges. A
271
+ * `intentAllowedMissing` finding is `absent`. Project rows are scored against
272
+ * the observed names, and the two forbidden drift planes are scored from the
273
+ * judge's findings as well — `dependencies.forbidden` through its
274
+ * `dependencyForbidden` findings, `forbiddenTags` through its
275
+ * `tagDependencyForbidden` witnesses attributed via `tagsByProject` — so a
276
+ * row's verdict IS the verdict `check` and `drift` render, including the
277
+ * transitive closure a direct-edge re-derivation would read as "match". A
273
278
  * `dependencies.allowed` row is an allowlist entry, not an existence claim —
274
279
  * its absence in the graph is not divergence, so it scores `match` either
275
280
  * way (the divergent direction is the observed edge outside the list, scored
@@ -285,7 +290,29 @@ export function scoreIntentRows(intent, judgeVerdict, observed, tagsByProject) {
285
290
  // used by its own test
286
291
  const rows = [];
287
292
  const observedNames = new Set(observed.projects.map((p) => p.name));
288
- const observedEdgeKeys = new Set(observed.edges.map((e) => `${e.source} ${e.target}`));
293
+ // The judge has already judged every forbidden plane on the any-path
294
+ // closure (`../architecture-intent/judge.mjs` emits `dependencyForbidden`
295
+ // and `tagDependencyForbidden` for direct AND transitive paths, as concrete
296
+ // `source`/`target` project names with `boundaryFrom: null`). These two
297
+ // projections score the drift rows from those findings — projecting the
298
+ // canonical verdict, never re-deriving reachability here: a re-walk of the
299
+ // direct-edge list would score a transitive violation the judge reported
300
+ // as a row "match", the divergence `check` and `drift` do report. The
301
+ // judge's pairless findings (`intentUnknownProject`, `intentUnknownTag`)
302
+ // carry no witness pair; the rows they belong to are scored below from the
303
+ // observed names and tags themselves — the same existence predicate the
304
+ // judge used to emit them: a row whose endpoint can never resolve is
305
+ // unknown/unverifiable, never a silent "match".
306
+ const dependencyForbiddenPairs = new Set();
307
+ const tagForbiddenWitnesses = [];
308
+ for (const finding of judgeVerdict.findings) {
309
+ if (finding.source === null || finding.target === null) continue;
310
+ if (finding.rule === "dependencyForbidden") {
311
+ dependencyForbiddenPairs.add(`${finding.source} → ${finding.target}`);
312
+ } else if (finding.rule === "tagDependencyForbidden") {
313
+ tagForbiddenWitnesses.push([finding.source, finding.target]);
314
+ }
315
+ }
289
316
 
290
317
  const boundaryFinding = new Map();
291
318
  for (const finding of judgeVerdict.findings) {
@@ -341,22 +368,45 @@ export function scoreIntentRows(intent, judgeVerdict, observed, tagsByProject) {
341
368
  }
342
369
  for (const forbidden of dependencies.forbidden ?? []) {
343
370
  const key = `${forbidden.source} → ${forbidden.target}`;
371
+ // A row naming a project the observed architecture does not have can
372
+ // never fire — the judge reports it as `intentUnknownProject` with no
373
+ // witness pair, so the projection above stays empty for it. Reading
374
+ // that as "match — the ban holds" would be the silent direction; score
375
+ // it unknown, mirroring the boundary plane (`reconcileScores`).
376
+ if (!observedNames.has(forbidden.source) || !observedNames.has(forbidden.target)) {
377
+ row("edge", key, "unknown", "intentUnknownProject", "forbidden", key);
378
+ continue;
379
+ }
380
+ const violated = dependencyForbiddenPairs.has(key);
344
381
  row(
345
382
  "edge",
346
383
  key,
347
- observedEdgeKeys.has(key) ? "unexpected" : "match",
348
- observedEdgeKeys.has(key) ? "dependencyForbidden" : "match",
384
+ violated ? "unexpected" : "match",
385
+ violated ? "dependencyForbidden" : "match",
349
386
  "forbidden",
350
387
  key,
351
388
  );
352
389
  }
353
390
 
391
+ // Every tag any observed project carries — the existence side of a
392
+ // `forbiddenTags` row, the same vocabulary the judge checks when it emits
393
+ // `intentUnknownTag`.
394
+ const allTags = new Set();
395
+ for (const tags of tagsByProject.values()) {
396
+ for (const tag of tags) allTags.add(tag);
397
+ }
354
398
  for (const tagRow of intent.forbiddenTags ?? []) {
355
399
  const key = `${tagRow.from} → ${tagRow.to}`;
356
- const violated = observed.edges.some((e) => {
357
- if (e.source === e.target) return false;
358
- const sourceTags = tagsByProject.get(e.source) ?? [];
359
- const targetTags = tagsByProject.get(e.target) ?? [];
400
+ // A row naming a tag no observed project carries can never fire — the
401
+ // judge reports it as `intentUnknownTag` with no witness pair. Score it
402
+ // unknown, never a silent "match".
403
+ if (!allTags.has(tagRow.from) || !allTags.has(tagRow.to)) {
404
+ row("tag", key, "unknown", "intentUnknownTag", "tag-forbidden", key);
405
+ continue;
406
+ }
407
+ const violated = tagForbiddenWitnesses.some(([source, target]) => {
408
+ const sourceTags = tagsByProject.get(source) ?? [];
409
+ const targetTags = tagsByProject.get(target) ?? [];
360
410
  return sourceTags.includes(tagRow.from) && targetTags.includes(tagRow.to);
361
411
  });
362
412
  row(
@@ -52,7 +52,7 @@
52
52
  "type": "source-evidence",
53
53
  "path": "cli.mjs",
54
54
  "assertion": "Exit codes 0/1/3 distinguish clean/findings/cannot-look",
55
- "sha256": "2b6590f4039464d3569727d0a73db61dd86c18648a519ee034ebcc2d845ecfc7"
55
+ "sha256": "536fef05cdf8d6ec8a5c364819d2dcbf61cdcf75b638463e98b89d6073e27851"
56
56
  }
57
57
  ],
58
58
  "status": "proven"
@@ -66,13 +66,13 @@
66
66
  "type": "behavioral-test",
67
67
  "path": "src/intent/intent.test.mjs",
68
68
  "assertion": "analysis output conforms to the frozen contract schema (no extra verdict/policy fields); analysis output is invariant under project tag changes (three workspaces differing only in project tag fields compared byte-for-byte)",
69
- "sha256": "749414363ef9991ae3697fc00ab5969751666867792d4bb4dbe528ec47ccd740"
69
+ "sha256": "8c481f1230ae8700469816018fba99017a0014920b12bebdf262ceb241cba745"
70
70
  },
71
71
  {
72
72
  "type": "architecture-test",
73
73
  "path": "src/intent/intent.test.mjs",
74
74
  "assertion": "the gate walks every production analysis module and fails when judging vocabulary (judge/forbid/permit/allow/ban) appears in code",
75
- "sha256": "749414363ef9991ae3697fc00ab5969751666867792d4bb4dbe528ec47ccd740"
75
+ "sha256": "8c481f1230ae8700469816018fba99017a0014920b12bebdf262ceb241cba745"
76
76
  },
77
77
  {
78
78
  "type": "source-evidence",
@@ -84,7 +84,7 @@
84
84
  "type": "source-evidence",
85
85
  "path": "AGENTS.md",
86
86
  "assertion": "src/graph/ is a lossy view of analysis, on purpose; src/analysis/ never judges",
87
- "sha256": "552b54c1956370f024ffeecec95c0e0941a0c66d4a14094045b444a25a27f913"
87
+ "sha256": "6ab8eef7a1ec2a8bde81393880cc6d29b1f089b01ffec403b31846c5d3a1a830"
88
88
  }
89
89
  ],
90
90
  "status": "proven"
@@ -136,7 +136,7 @@
136
136
  "type": "source-evidence",
137
137
  "path": "src/commands/diff.mjs",
138
138
  "assertion": "parseBaseline validates schemaVersion; refuses unknown versions",
139
- "sha256": "4d44eea475cd4b06122a405718203d2e0018a163c1d42b17324400e7e4de5eea"
139
+ "sha256": "59e6e8f1cb0535cbec241f6daf99c10f5a679c798e66fcc1f4afb5329bc6497a"
140
140
  }
141
141
  ],
142
142
  "status": "proven"
@@ -156,7 +156,7 @@
156
156
  "type": "source-evidence",
157
157
  "path": "src/commands/diff.mjs",
158
158
  "assertion": "computeDiff returns structural diff; policyMismatch detected via fingerprint; computeRuleImpact for depConstraints-only context",
159
- "sha256": "4d44eea475cd4b06122a405718203d2e0018a163c1d42b17324400e7e4de5eea"
159
+ "sha256": "59e6e8f1cb0535cbec241f6daf99c10f5a679c798e66fcc1f4afb5329bc6497a"
160
160
  }
161
161
  ],
162
162
  "status": "proven"
@@ -236,7 +236,7 @@
236
236
  "type": "behavioral-test",
237
237
  "path": "src/intent/intent.test.mjs",
238
238
  "assertion": "depConstraints verdicts from judgeEdge agree with evaluate in both directions (violating edge found by both, legal edge reported by neither); explain includes the same violations as evaluate at a given site",
239
- "sha256": "749414363ef9991ae3697fc00ab5969751666867792d4bb4dbe528ec47ccd740"
239
+ "sha256": "8c481f1230ae8700469816018fba99017a0014920b12bebdf262ceb241cba745"
240
240
  },
241
241
  {
242
242
  "type": "behavioral-test",
@@ -266,7 +266,7 @@
266
266
  "type": "source-evidence",
267
267
  "path": "src/commands/diff.mjs",
268
268
  "assertion": "coverage.notes warns when ruleImpact is computed (depConstraints only, 3 of 15)",
269
- "sha256": "4d44eea475cd4b06122a405718203d2e0018a163c1d42b17324400e7e4de5eea"
269
+ "sha256": "59e6e8f1cb0535cbec241f6daf99c10f5a679c798e66fcc1f4afb5329bc6497a"
270
270
  },
271
271
  {
272
272
  "type": "documentation",