@ecoma-io/archkeep 0.21.0 → 0.22.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 (37) hide show
  1. package/cli.mjs +156 -66
  2. package/package.json +1 -1
  3. package/src/analysis/contract.md +32 -5
  4. package/src/analysis/source-util.mjs +107 -0
  5. package/src/analysis/typescript.mjs +86 -5
  6. package/src/commands/change.mjs +59 -28
  7. package/src/commands/check.mjs +65 -26
  8. package/src/commands/completeness.mjs +126 -19
  9. package/src/commands/context-command.mjs +13 -5
  10. package/src/commands/context.mjs +31 -4
  11. package/src/commands/coverage-verdict.mjs +184 -0
  12. package/src/commands/debt.mjs +18 -15
  13. package/src/commands/delta-classify.mjs +13 -18
  14. package/src/commands/delta.mjs +95 -33
  15. package/src/commands/diff.mjs +31 -24
  16. package/src/commands/discover.mjs +30 -10
  17. package/src/commands/drift.mjs +21 -21
  18. package/src/commands/edge-constraints.mjs +47 -1
  19. package/src/commands/evaluation-primitives.mjs +194 -2
  20. package/src/commands/evolution.mjs +27 -10
  21. package/src/commands/explain.mjs +14 -13
  22. package/src/commands/fitness.mjs +20 -19
  23. package/src/commands/graph.mjs +14 -5
  24. package/src/commands/health.mjs +12 -5
  25. package/src/commands/history.mjs +29 -15
  26. package/src/commands/impact.mjs +17 -18
  27. package/src/commands/plan-context-command.mjs +10 -5
  28. package/src/commands/reconcile.mjs +14 -17
  29. package/src/commands/scenario-evaluation.mjs +93 -16
  30. package/src/commands/scenario.mjs +28 -18
  31. package/src/commands/waivers.mjs +36 -28
  32. package/src/governance/evolution-event.mjs +62 -9
  33. package/src/intent/intent-manifest.json +83 -39
  34. package/src/report/json.mjs +32 -5
  35. package/src/report/text.mjs +82 -12
  36. package/src/verdict.mjs +78 -36
  37. package/src/workspace.mjs +126 -2
@@ -37,10 +37,11 @@
37
37
  * `now` and the output is reproducible byte-for-byte. Defaults to the wall
38
38
  * clock, the same injection `evaluate` uses for waiver expiry.
39
39
  */
40
- import { isWholeFileFailure } from "../analysis/source-util.mjs";
40
+ import { blindSpotRows } from "../analysis/source-util.mjs";
41
41
  import { suppressionCovers } from "../config.mjs";
42
42
  import { referenceTime } from "../governance/clock.mjs";
43
43
  import { isWaiver, remainingMs, waiverStatus } from "../governance/waiver.mjs";
44
+ import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
44
45
  import { jsonEnvelope, renderJson } from "../report/json.mjs";
45
46
  import { formatWaiversReport } from "../report/waivers-text.mjs";
46
47
  import { partitionUnownedCoverage } from "./coverage-acceptance.mjs";
@@ -150,10 +151,13 @@ export function computeWaivers(suppressions, rawViolations, now = referenceTime(
150
151
  * clock, and — from `cli.mjs`'s `runWaivers` — the workspace-relative path
151
152
  * the run's law actually resolved from, so the `coverage.unowned` matching
152
153
  * below subtracts the same configuration files `check` subtracts.
153
- * @returns {Promise<{status: "ok", waivers: object, report: {text: string, json: string}}>}
154
- * @throws {Error} whenever the run's law is malformed, or the tree has
155
- * whole-file analysis failures exit-3 class, the same posture `check` takes
156
- * on a malformed config and `impact`/`drift` take on incomplete coverage.
154
+ * @returns {Promise<{status: "ok"|"no-verdict", waivers?: object, coverage: object,
155
+ * report: {text: string, json: string}}>}
156
+ * `status: "no-verdict"` carries no `waivers` payload the verdict was
157
+ * withheld, and the envelope's `coverage` block is the whole answer (#608).
158
+ * @throws {Error} whenever the run's law is malformed — exit-3 class, the same
159
+ * posture `check` takes on a malformed config. Incomplete coverage returns
160
+ * the structured no-verdict envelope instead of throwing (#608).
157
161
  */
158
162
  export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
159
163
  const { root, provider, marker, analysis, graph } = commandContext;
@@ -180,25 +184,23 @@ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
180
184
  // A waiver surface over a tree it could not fully read is a lottery ticket,
181
185
  // not a surface: a file the analyzer never judged contributes no raw
182
186
  // violation, so every waiver that names it reads as stale and the report
183
- // says "covers nothing" about a finding the run never looked at. Refuse
184
- // loudly on whole-file failures, the same posture `impact`, `drift`, and
185
- // `history` take — "could not look" must never read as "looked and found
186
- // nothing" (`./impact.mjs`'s refusal names the same silence). A whole-file
187
- // failure whose file a `coverage.unowned` row accepts is withdrawn first,
188
- // exactly as `check` withdraws it (`./check.mjs`'s `acceptedUnclaimed`):
189
- // its state is a recorded acceptance this very report is about to name,
190
- // not a hole the run failed to look at.
191
- const notAnalyzed = analysis.failures
192
- .filter(isWholeFileFailure)
193
- .filter(({ sourceFile }) => !unownedCoverage.acceptedFiles.has(sourceFile))
194
- .map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
195
-
196
- if (notAnalyzed.length > 0) {
197
- throw new Error(
198
- `archkeep: waivers has incomplete coverage — ${notAnalyzed.length} file` +
199
- `${notAnalyzed.length === 1 ? "" : "s"} could not be analyzed, so every waiver naming one ` +
200
- `would read as covering nothing it never saw. Fix the unanalyzed files and re-run.`,
201
- );
187
+ // says "covers nothing" about a finding the run never looked at. Refused
188
+ // through the one structured contract `./coverage-verdict.mjs` builds
189
+ // (#608) — "could not look" must never read as "looked and found nothing".
190
+ // A whole-file failure whose file a `coverage.unowned` row accepts is
191
+ // withdrawn first, exactly as `check` withdraws it (`./check.mjs`'s
192
+ // `acceptedUnclaimed`): its state is a recorded acceptance this very report
193
+ // is about to name, not a hole the run failed to look at.
194
+ const completeness = coverageVerdict(commandContext, {
195
+ acceptedFiles: unownedCoverage.acceptedFiles,
196
+ });
197
+ if (!completeness.complete) {
198
+ return coverageRefusal({
199
+ command: "waivers",
200
+ commandContext,
201
+ what: "measuring the waiver surface",
202
+ acceptedFiles: unownedCoverage.acceptedFiles,
203
+ });
202
204
  }
203
205
 
204
206
  // F07: a waiver surface measured against a graph that cannot see the
@@ -226,10 +228,13 @@ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
226
228
  projects: Object.keys(graph.nodes).length,
227
229
  analyzedFiles: analysis.analyzed,
228
230
  imports: analysis.imports.length,
229
- notAnalyzed,
230
- blindSpots: analysis.failures
231
- .filter((failure) => !isWholeFileFailure(failure))
232
- .map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
231
+ // The withdrawn list — whole-file failures a `coverage.unowned` row
232
+ // accepts are already named in `result.unownedAcceptances` below, so
233
+ // repeating them here would double-count the same acceptance. On this
234
+ // path the list is empty by construction: anything unwithdrawn refused
235
+ // above.
236
+ notAnalyzed: completeness.notAnalyzed,
237
+ blindSpots: blindSpotRows(analysis.failures),
233
238
  // `remainingMs` reflects the wall clock at the moment of THIS run, not the
234
239
  // workspace — it is expected to differ between two runs of an unchanged
235
240
  // tree, by design (`../governance/clock.mjs`). Disclosed here, in-band,
@@ -282,6 +287,9 @@ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
282
287
  return {
283
288
  status: "ok",
284
289
  waivers: result,
290
+ // The same `coverage` block the envelope carries, so both return shapes —
291
+ // this one and the no-verdict refusal's (#608) — expose it under one key.
292
+ coverage,
285
293
  report: {
286
294
  text: formatWaiversReport(result),
287
295
  json: renderJson(envelope),
@@ -107,15 +107,68 @@ export function declarationDigest(intent) {
107
107
  });
108
108
  }
109
109
 
110
+ /**
111
+ * The identity string of one graph edge, in the canonical spelling
112
+ * `source>target:type` — the `(source, target, type)` identity design §1
113
+ * names. The ONE spelling the evolution events' `observed.edges` and
114
+ * `affected.boundaries` use: this module owns it, and `classifyEvolution`
115
+ * maps every edge it is handed through this function, so there is exactly
116
+ * one definition of "same edge" and no second spelling to drift.
117
+ *
118
+ * @param {{source: string, target: string, type: string}} edge
119
+ * @returns {string}
120
+ */
121
+ export function edgeEvolutionIdentity({ source, target, type }) {
122
+ return `${source}>${target}:${type}`;
123
+ }
124
+
125
+ /**
126
+ * The one accepted input shape for an `observed.edges` entry: the raw
127
+ * `{source, target, type}` triple. A caller handing over a ready-made string
128
+ * would be choosing a second spelling of "same edge", so a string is refused
129
+ * loudly rather than accepted as one shape more — the identity string is this
130
+ * module's output, never its input.
131
+ *
132
+ * @param {unknown} entry
133
+ * @returns {string}
134
+ */
135
+ function evolutionBoundary(entry) {
136
+ if (
137
+ typeof entry !== "object" ||
138
+ entry === null ||
139
+ !("source" in entry) ||
140
+ typeof entry.source !== "string" ||
141
+ entry.source === "" ||
142
+ !("target" in entry) ||
143
+ typeof entry.target !== "string" ||
144
+ entry.target === "" ||
145
+ !("type" in entry) ||
146
+ typeof entry.type !== "string"
147
+ ) {
148
+ throw new TypeError(
149
+ "classifyEvolution: observed.edges entries must be {source, target, type} triples — " +
150
+ "the identity string is classifyEvolution's own output spelling, never an input",
151
+ );
152
+ }
153
+ // The guard above has verified every property; the annotation only states
154
+ // what it proved.
155
+ return edgeEvolutionIdentity(
156
+ /** @type {{source: string, target: string, type: string}} */ (entry),
157
+ );
158
+ }
159
+
110
160
  /**
111
161
  * @typedef {object} EvolutionEvidence
112
162
  * @property {{projects?: {added: string[], removed: string[], changed: string[]},
113
- * edges?: {added: string[], removed: string[]},
163
+ * edges?: {added: {source: string, target: string, type: string}[],
164
+ * removed: {source: string, target: string, type: string}[]},
114
165
  * policyChanged?: boolean|null, policyOneSided?: boolean,
115
166
  * provenanceChanged?: boolean|null}} [observed]
116
- * The structural diff between base and head: project names and edge identity
117
- * strings (source,target,type) that were added, removed, or changed. Empty
118
- * by default. `policyChanged` — whether the policy fingerprint changed
167
+ * The structural diff between base and head: project names and raw edge
168
+ * triples that were added, removed, or changed. The triples are mapped
169
+ * through `edgeEvolutionIdentity` here — the identity spelling is this
170
+ * module's own, so `affected.boundaries` comes out as identity strings
171
+ * whichever shape the caller held. Empty by default. `policyChanged` — whether the policy fingerprint changed
119
172
  * between base and head; `null` is "could not be compared": exactly one side
120
173
  * records the policy (`policyOneSided: true`) or neither does
121
174
  * (both-absent). `true` is a disclosure, never a refusal. `policyOneSided`
@@ -197,9 +250,9 @@ export function declarationDigest(intent) {
197
250
  * absent (`null`) ⇒ NOT asserted, note added |
198
251
  *
199
252
  * The `affected` identities are derived from the same signals, never from a
200
- * second opinion: changed project names, changed edge identity strings, the
201
- * constraint/intent rows whose verdict was not `pass`/`matched`, and the ADR
202
- * ids whose lineage moved.
253
+ * second opinion: changed project names, the changed edges under the one
254
+ * identity spelling (`edgeEvolutionIdentity`), the constraint/intent rows
255
+ * whose verdict was not `pass`/`matched`, and the ADR ids whose lineage moved.
203
256
  *
204
257
  * @param {EvolutionEvidence} [input]
205
258
  * @returns {EvolutionClassification}
@@ -211,8 +264,8 @@ export function classifyEvolution(input = {}) {
211
264
  const addedProjects = projects.added ?? [];
212
265
  const removedProjects = projects.removed ?? [];
213
266
  const changedProjects = projects.changed ?? [];
214
- const addedEdges = edges.added ?? [];
215
- const removedEdges = edges.removed ?? [];
267
+ const addedEdges = (edges.added ?? []).map(evolutionBoundary);
268
+ const removedEdges = (edges.removed ?? []).map(evolutionBoundary);
216
269
  const structureChanged =
217
270
  addedProjects.length +
218
271
  removedProjects.length +
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 2,
3
3
  "generatedAt": "v1.0.0-hardening",
4
- "description": "Machine-readable manifest declaring every v1.0 intent and the evidence that supports it. A contract is 'proven' only when at least one evidence entry is an executable proof (behavioral-test, e2e-test, or architecture-test). Source-evidence and documentation support the claim but cannot prove it alone.",
4
+ "description": "Machine-readable manifest declaring every v1.0 intent and the evidence that supports it. A contract is 'proven' only when at least one evidence entry is an executable proof (behavioral-test, e2e-test, or architecture-test). Source-evidence and documentation support the claim but cannot prove it alone. Every evidence entry carries the sha256 of the artifact it names, and the gate recomputes each digest, so evidence is content-addressed: an artifact that changed after its claim was certified is a loud mismatch, not a silent swap.",
5
5
  "evidenceTaxonomy": {
6
6
  "behavioral-test": "A unit or integration test that imports and calls the code, asserting on runtime behavior (not source text). Proves the intent is regression-protected.",
7
7
  "e2e-test": "A CLI subprocess test that exercises the real binary against a consumer workspace. Proves the intent holds end-to-end.",
@@ -19,12 +19,14 @@
19
19
  {
20
20
  "type": "architecture-test",
21
21
  "path": "src/conformance/boundary.test.mjs",
22
- "assertion": "SHIPPED_PACKAGES allow-list prevents unapproved dependencies; specifiersIn() walk catches all imports including createRequire; no provider import found in core layers"
22
+ "assertion": "SHIPPED_PACKAGES allow-list prevents unapproved dependencies; specifiersIn() walk catches all imports including createRequire; no provider import found in core layers",
23
+ "sha256": "12d4e179f88de2622a41e56f42b2bab1c88a677ec2232430752138c57ba5fb18"
23
24
  },
24
25
  {
25
26
  "type": "source-evidence",
26
27
  "path": "src/commands/context.mjs",
27
- "assertion": "Only commands/context.mjs imports providers — the designated orchestration layer"
28
+ "assertion": "Only commands/context.mjs imports providers — the designated orchestration layer",
29
+ "sha256": "7f566c06afbb175337529bd9bdc42465f8849498b932abf6a77fe0290a0cd13c"
28
30
  }
29
31
  ],
30
32
  "status": "proven"
@@ -37,17 +39,20 @@
37
39
  {
38
40
  "type": "behavioral-test",
39
41
  "path": "src/report/json.test.mjs",
40
- "assertion": "JSON envelope throws on status=ok + incomplete coverage, status/exitCode disagreement, coverage.complete/notAnalyzed disagreement"
42
+ "assertion": "JSON envelope throws on status=ok + incomplete coverage, status/exitCode disagreement, coverage.complete/notAnalyzed disagreement",
43
+ "sha256": "a371d36b89b10c5a4bda4d9e9a2e3bde114256c1377eb95699c9f0f195aabc4a"
41
44
  },
42
45
  {
43
46
  "type": "source-evidence",
44
47
  "path": "src/lsp/diagnose.mjs",
45
- "assertion": "analyzed:false on every non-verdict path; empty diagnostic list only from two named places"
48
+ "assertion": "analyzed:false on every non-verdict path; empty diagnostic list only from two named places",
49
+ "sha256": "4eea19bf245c33963cd292e02f676eb9c5238a5783a3a960976f53afc7cf8b27"
46
50
  },
47
51
  {
48
52
  "type": "source-evidence",
49
53
  "path": "cli.mjs",
50
- "assertion": "Exit codes 0/1/3 distinguish clean/findings/cannot-look"
54
+ "assertion": "Exit codes 0/1/3 distinguish clean/findings/cannot-look",
55
+ "sha256": "81abe8243660b3cca913ee903c1fc0bfcdb6a240640c5302f18848e6a9473fe6"
51
56
  }
52
57
  ],
53
58
  "status": "proven"
@@ -60,17 +65,26 @@
60
65
  {
61
66
  "type": "behavioral-test",
62
67
  "path": "src/intent/intent.test.mjs",
63
- "assertion": "analysis output conforms to the frozen contract schema (no extra verdict/policy fields); analysis output is invariant under project tag changes; analysis source contains no judging vocabulary"
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": "6ed86cb5e7af94b56fa2af3d2da5d45d58937fbbf316105da0224d0060e88582"
70
+ },
71
+ {
72
+ "type": "architecture-test",
73
+ "path": "src/intent/intent.test.mjs",
74
+ "assertion": "the gate walks every production analysis module and fails when judging vocabulary (judge/forbid/permit/allow/ban) appears in code",
75
+ "sha256": "6ed86cb5e7af94b56fa2af3d2da5d45d58937fbbf316105da0224d0060e88582"
64
76
  },
65
77
  {
66
78
  "type": "source-evidence",
67
79
  "path": "src/analysis/contract.md",
68
- "assertion": "Analysis record is a superset of a graph edge — 5 of 15 violations decided on raw specifier"
80
+ "assertion": "Analysis record is a superset of a graph edge — 5 of 15 violations decided on raw specifier",
81
+ "sha256": "56d2db7816ba5ffc5fbe9e299f7d5e270a857b42eb54bd3bea7c05da17be595b"
69
82
  },
70
83
  {
71
84
  "type": "source-evidence",
72
85
  "path": "AGENTS.md",
73
- "assertion": "src/graph/ is a lossy view of analysis, on purpose; src/analysis/ never judges"
86
+ "assertion": "src/graph/ is a lossy view of analysis, on purpose; src/analysis/ never judges",
87
+ "sha256": "3209154935cc1463127721203da74dc8bf8ba40258b182c5faa7345946611e38"
74
88
  }
75
89
  ],
76
90
  "status": "proven"
@@ -83,17 +97,20 @@
83
97
  {
84
98
  "type": "e2e-test",
85
99
  "path": "e2e/determinism.e2e.mjs",
86
- "assertion": "Graph, check, impact run twice — byte-identical JSON output"
100
+ "assertion": "Graph, check, impact run twice — byte-identical JSON output",
101
+ "sha256": "90c0f2a62a111013990ff96ec07042e961c1aae50710c597192657cc270df5ec"
87
102
  },
88
103
  {
89
104
  "type": "source-evidence",
90
105
  "path": "src/commands/graph.mjs",
91
- "assertion": "Plain string comparison, never localeCompare; INTERNAL_DATA_FIELDS stripped; SCHEMA_VERSION = 2"
106
+ "assertion": "Plain string comparison, never localeCompare; INTERNAL_DATA_FIELDS stripped; SCHEMA_VERSION = 2",
107
+ "sha256": "cce9730e33e83e0a183c0139afcc9a0845f2f95f67b07875f9c583f3ca95d046"
92
108
  },
93
109
  {
94
110
  "type": "source-evidence",
95
111
  "path": "src/commands/graph.mjs",
96
- "assertion": "computePolicyFingerprint produces SHA-256 of canonicalized policy"
112
+ "assertion": "computePolicyFingerprint produces SHA-256 of canonicalized policy",
113
+ "sha256": "cce9730e33e83e0a183c0139afcc9a0845f2f95f67b07875f9c583f3ca95d046"
97
114
  }
98
115
  ],
99
116
  "status": "proven"
@@ -106,12 +123,14 @@
106
123
  {
107
124
  "type": "e2e-test",
108
125
  "path": "e2e/diff.e2e.mjs",
109
- "assertion": "Invalid baseline schemaVersion exits 3; incompatible version exits 3"
126
+ "assertion": "Invalid baseline schemaVersion exits 3; incompatible version exits 3",
127
+ "sha256": "7892a6dc56f5e1ec725fd6d029bba76bc493d8c34790f346e15af0d7d0dbbdc1"
110
128
  },
111
129
  {
112
130
  "type": "source-evidence",
113
131
  "path": "src/commands/diff.mjs",
114
- "assertion": "parseBaseline validates schemaVersion; refuses unknown versions"
132
+ "assertion": "parseBaseline validates schemaVersion; refuses unknown versions",
133
+ "sha256": "6190013ec55eeb7f1a9e4cdd910c7be73bfb1fa87051bdeace7c9581c6a18426"
115
134
  }
116
135
  ],
117
136
  "status": "proven"
@@ -124,12 +143,14 @@
124
143
  {
125
144
  "type": "e2e-test",
126
145
  "path": "e2e/diff.e2e.mjs",
127
- "assertion": "Self-baseline exits 0; added/removed edges reported; policy mismatch warned; rule-impact with config"
146
+ "assertion": "Self-baseline exits 0; added/removed edges reported; policy mismatch warned; rule-impact with config",
147
+ "sha256": "7892a6dc56f5e1ec725fd6d029bba76bc493d8c34790f346e15af0d7d0dbbdc1"
128
148
  },
129
149
  {
130
150
  "type": "source-evidence",
131
151
  "path": "src/commands/diff.mjs",
132
- "assertion": "computeDiff returns structural diff; policyMismatch detected via fingerprint; computeRuleImpact for depConstraints-only context"
152
+ "assertion": "computeDiff returns structural diff; policyMismatch detected via fingerprint; computeRuleImpact for depConstraints-only context",
153
+ "sha256": "6190013ec55eeb7f1a9e4cdd910c7be73bfb1fa87051bdeace7c9581c6a18426"
133
154
  }
134
155
  ],
135
156
  "status": "proven"
@@ -142,12 +163,14 @@
142
163
  {
143
164
  "type": "e2e-test",
144
165
  "path": "e2e/determinism.e2e.mjs",
145
- "assertion": "Impact --format json produces identical output on two runs"
166
+ "assertion": "Impact --format json produces identical output on two runs",
167
+ "sha256": "90c0f2a62a111013990ff96ec07042e961c1aae50710c597192657cc270df5ec"
146
168
  },
147
169
  {
148
170
  "type": "behavioral-test",
149
171
  "path": "src/commands/impact.test.mjs",
150
- "assertion": "Sorts results using plain string comparison (never localeCompare)"
172
+ "assertion": "Sorts results using plain string comparison (never localeCompare)",
173
+ "sha256": "b188d7001d146c203be490727414f0c585d8e589f10e3a9add40a34b5aedd532"
151
174
  }
152
175
  ],
153
176
  "status": "proven"
@@ -160,17 +183,20 @@
160
183
  {
161
184
  "type": "behavioral-test",
162
185
  "path": "src/commands/context-command.test.mjs",
163
- "assertion": "Returns tags, matched constraints, per-edge violations; coverage.notes warns about depConstraints-only scope"
186
+ "assertion": "Returns tags, matched constraints, per-edge violations; coverage.notes warns about depConstraints-only scope",
187
+ "sha256": "eccd31e3057bbb0cb45785e0047e0d71e83dbbc00703ee231f085cf52fb76db8"
164
188
  },
165
189
  {
166
190
  "type": "e2e-test",
167
191
  "path": "e2e/context.e2e.mjs",
168
- "assertion": "Native consumer: tags, constraints, coverage.notes verified; Moon consumer: context E2E"
192
+ "assertion": "Native consumer: tags, constraints, coverage.notes verified; Moon consumer: context E2E",
193
+ "sha256": "336994ea66e2b7b7ff1d374e3127b478d530508b8c8e5ef927805c5aa11b6cb8"
169
194
  },
170
195
  {
171
196
  "type": "documentation",
172
197
  "path": "../../docs/concepts/agentic-development.md",
173
- "assertion": "Section 'What context and impact do not check' warns agents about the semantic gap"
198
+ "assertion": "Section 'What context and impact do not check' warns agents about the semantic gap",
199
+ "sha256": "a42bca1e927a59d28160eb947a285c20d1a6cc9633a69c91a45e9d46bbe1686b"
174
200
  }
175
201
  ],
176
202
  "status": "proven"
@@ -183,12 +209,14 @@
183
209
  {
184
210
  "type": "behavioral-test",
185
211
  "path": "src/commands/explain.test.mjs",
186
- "assertion": "Returns all violations at a site; unresolvable site returns unresolvable:true with reason"
212
+ "assertion": "Returns all violations at a site; unresolvable site returns unresolvable:true with reason",
213
+ "sha256": "017581d978f277fadecedd6a4a0a4befca3743dbf2b084e1192792a1891466ad"
187
214
  },
188
215
  {
189
216
  "type": "e2e-test",
190
217
  "path": "e2e/explain.e2e.mjs",
191
- "assertion": "Native and Moon consumers: specifier, sourceProject, targetProject, violations verified"
218
+ "assertion": "Native and Moon consumers: specifier, sourceProject, targetProject, violations verified",
219
+ "sha256": "857e198249c0614af643f784d985ae782ab8dfa9906f92f44193e2570ee0c343"
192
220
  }
193
221
  ],
194
222
  "status": "proven"
@@ -201,37 +229,44 @@
201
229
  {
202
230
  "type": "behavioral-test",
203
231
  "path": "src/intent/intent.test.mjs",
204
- "assertion": "depConstraints verdicts from judgeEdge agree with evaluate; explain includes the same violations as evaluate at a given site; diff warns in coverage.notes when ruleImpact is computed"
232
+ "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",
233
+ "sha256": "6ed86cb5e7af94b56fa2af3d2da5d45d58937fbbf316105da0224d0060e88582"
205
234
  },
206
235
  {
207
236
  "type": "behavioral-test",
208
237
  "path": "src/commands/context-command.test.mjs",
209
- "assertion": "Test verifies coverage.notes contains depConstraints warning"
238
+ "assertion": "Test verifies coverage.notes contains depConstraints warning",
239
+ "sha256": "eccd31e3057bbb0cb45785e0047e0d71e83dbbc00703ee231f085cf52fb76db8"
210
240
  },
211
241
  {
212
242
  "type": "e2e-test",
213
243
  "path": "e2e/context.e2e.mjs",
214
- "assertion": "E2E verifies coverage.notes in JSON envelope"
244
+ "assertion": "E2E verifies coverage.notes in JSON envelope",
245
+ "sha256": "336994ea66e2b7b7ff1d374e3127b478d530508b8c8e5ef927805c5aa11b6cb8"
215
246
  },
216
247
  {
217
248
  "type": "source-evidence",
218
249
  "path": "src/commands/context-command.mjs",
219
- "assertion": "coverage.notes warns that per-edge violations cover only depConstraints (3 of 15 violation types)"
250
+ "assertion": "coverage.notes warns that per-edge violations cover only depConstraints (3 of 15 violation types)",
251
+ "sha256": "72526cdaf038da4d9a50b33ab3cb30828713e62bbb891f4e2e51e008a403338e"
220
252
  },
221
253
  {
222
254
  "type": "source-evidence",
223
255
  "path": "src/commands/impact.mjs",
224
- "assertion": "coverage.notes warns that per-edge violations cover only depConstraints (3 of 15 violation types)"
256
+ "assertion": "coverage.notes warns that per-edge violations cover only depConstraints (3 of 15 violation types)",
257
+ "sha256": "38cff166a8944860c51a73316447ce789898751489d783e75fd6befbfd30be3c"
225
258
  },
226
259
  {
227
260
  "type": "source-evidence",
228
261
  "path": "src/commands/diff.mjs",
229
- "assertion": "coverage.notes warns when ruleImpact is computed (depConstraints only, 3 of 15)"
262
+ "assertion": "coverage.notes warns when ruleImpact is computed (depConstraints only, 3 of 15)",
263
+ "sha256": "6190013ec55eeb7f1a9e4cdd910c7be73bfb1fa87051bdeace7c9581c6a18426"
230
264
  },
231
265
  {
232
266
  "type": "documentation",
233
267
  "path": "../../docs/concepts/agentic-development.md",
234
- "assertion": "Section warns agents: violations:[] means allowed by constraint table, not free of all boundary violations"
268
+ "assertion": "Section warns agents: violations:[] means allowed by constraint table, not free of all boundary violations",
269
+ "sha256": "a42bca1e927a59d28160eb947a285c20d1a6cc9633a69c91a45e9d46bbe1686b"
235
270
  }
236
271
  ],
237
272
  "status": "proven"
@@ -244,12 +279,14 @@
244
279
  {
245
280
  "type": "e2e-test",
246
281
  "path": "e2e/determinism.e2e.mjs",
247
- "assertion": "Graph, check, impact: two runs produce byte-identical JSON"
282
+ "assertion": "Graph, check, impact: two runs produce byte-identical JSON",
283
+ "sha256": "90c0f2a62a111013990ff96ec07042e961c1aae50710c597192657cc270df5ec"
248
284
  },
249
285
  {
250
286
  "type": "source-evidence",
251
287
  "path": "src/commands/graph.mjs",
252
- "assertion": "Plain string comparison throughout; never localeCompare"
288
+ "assertion": "Plain string comparison throughout; never localeCompare",
289
+ "sha256": "cce9730e33e83e0a183c0139afcc9a0845f2f95f67b07875f9c583f3ca95d046"
253
290
  }
254
291
  ],
255
292
  "status": "proven"
@@ -262,22 +299,26 @@
262
299
  {
263
300
  "type": "e2e-test",
264
301
  "path": "e2e/parity.e2e.mjs",
265
- "assertion": "Nx vs Native: project names, edge source/target/type, violation rule IDs, envelope structure"
302
+ "assertion": "Nx vs Native: project names, edge source/target/type, violation rule IDs, envelope structure",
303
+ "sha256": "106f59315e4661ae67f5c1e44895d56f7f7562eacd4c44101a9ed4b0e56f774e"
266
304
  },
267
305
  {
268
306
  "type": "e2e-test",
269
307
  "path": "e2e/moon.e2e.mjs",
270
- "assertion": "Moon: check, graph, diff, impact, explain, context verified against Moon provider (conditional on moon CLI availability)"
308
+ "assertion": "Moon: check, graph, diff, impact, explain, context verified against Moon provider (conditional on moon CLI availability)",
309
+ "sha256": "8293a65ff407ea43a3985c742ae6333970ec3b0400f3df46189793a3dc87af6b"
271
310
  },
272
311
  {
273
312
  "type": "source-evidence",
274
313
  "path": "src/providers/moon.mjs",
275
- "assertion": "inferWorkspaceLayout returns null for partial layouts — same all-or-nothing contract as Nx and Native"
314
+ "assertion": "inferWorkspaceLayout returns null for partial layouts — same all-or-nothing contract as Nx and Native",
315
+ "sha256": "232dd8e2cbe70c9149ba3a75a03dcebe0db355894c280964ebbf6a5a536b5567"
276
316
  },
277
317
  {
278
318
  "type": "behavioral-test",
279
319
  "path": "src/providers/moon.test.mjs",
280
- "assertion": "Partial layout (apps-only, libs-only) returns undefined workspaceLayout"
320
+ "assertion": "Partial layout (apps-only, libs-only) returns undefined workspaceLayout",
321
+ "sha256": "084a0061b3236e680274d837fb2f12d27dd4b5a379040157f7afe2be5088e8ac"
281
322
  }
282
323
  ],
283
324
  "status": "proven"
@@ -290,17 +331,20 @@
290
331
  {
291
332
  "type": "behavioral-test",
292
333
  "path": "src/commands/drift.test.mjs",
293
- "assertion": "driftCommand returns status ok with observed facts and findings; refuses incomplete coverage, absent intent, and unregistered-plugin incomplete graph; buildObserved counts implicit edges separately"
334
+ "assertion": "driftCommand returns status ok with observed facts and findings; refuses incomplete coverage, absent intent, and unregistered-plugin incomplete graph; buildObserved counts implicit edges separately",
335
+ "sha256": "dbb0dfa1ecce9123ad0a38cdd6bc97e6645a47cba78496357318375db952e43a"
294
336
  },
295
337
  {
296
338
  "type": "behavioral-test",
297
339
  "path": "src/report/drift-text.test.mjs",
298
- "assertion": "formatDriftReport states comparison facts even when clean (no-drift is a claim about coverage); groups findings by rule in taxonomy order; byte-identical determinism"
340
+ "assertion": "formatDriftReport states comparison facts even when clean (no-drift is a claim about coverage); groups findings by rule in taxonomy order; byte-identical determinism",
341
+ "sha256": "767d395c40cb0fa61f19ec06d587708e4ba065323bb1ee170254365847b49cc5"
299
342
  },
300
343
  {
301
344
  "type": "e2e-test",
302
345
  "path": "e2e/drift.e2e.mjs",
303
- "assertion": "Through the installed CLI: drift exits 0 on a matching tree and names fingerprint/rows; reports dependencyForbidden and projectMissing; JSON envelope carries findings; malformed intent exits 3 for both drift and check"
346
+ "assertion": "Through the installed CLI: drift exits 0 on a matching tree and names fingerprint/rows; reports dependencyForbidden and projectMissing; JSON envelope carries findings; malformed intent exits 3 for both drift and check",
347
+ "sha256": "1e060ba0f02497259fdde2481dc59bd74bf6658f2fc354100de4f89ff6ffb926"
304
348
  }
305
349
  ],
306
350
  "status": "proven"
@@ -94,12 +94,39 @@ export function jsonEnvelope({ command, context, status, exitCode, coverage, res
94
94
  `them disagree would make one of them a lie.`,
95
95
  );
96
96
  }
97
- if (coverage.complete !== (coverage.notAnalyzed.length === 0)) {
97
+ // Completeness law, one-directional (#595, #599): `complete: true`
98
+ // claims the run judged everything its coverage block describes, and a
99
+ // whole-file failure or an unresolvable import site is exactly the kind
100
+ // of thing it did not judge — so the claim is refused over either. The
101
+ // reverse is deliberately allowed: `complete: false` with both lists
102
+ // empty is the zero-analysis state (a scope that selected no owned file,
103
+ // in-scope files no analyzer claims), which names its reason in the
104
+ // envelope's status and decision rather than in these lists, and refusing
105
+ // it would leave a run that judged nothing unable to say so. blindSpots
106
+ // is optional because not every command's coverage carries it; where it
107
+ // is present, the law is enforced.
108
+ //
109
+ // Within blindSpots only the unresolvable-LITERAL class counts as unjudged
110
+ // work (#595, narrowed): a row carrying `dynamic: true` is the declared
111
+ // non-literal-import limit — the language itself saying the target is
112
+ // computed at runtime, unknowable to static analysis in principle — and a
113
+ // declared limit is disclosed, not withheld over. The classifier lives once
114
+ // in `analysis/source-util.mjs`; this guard reads the row field the same
115
+ // helper that built the rows set, so the two can never disagree.
116
+ const blindSpotList = Array.isArray(coverage.blindSpots) ? coverage.blindSpots : [];
117
+ const unjudgedBlindSpots = blindSpotList.filter(
118
+ (row) => row.dynamic !== true && row.external !== true,
119
+ );
120
+ if (
121
+ coverage.complete === true &&
122
+ (coverage.notAnalyzed.length > 0 || unjudgedBlindSpots.length > 0)
123
+ ) {
98
124
  throw new Error(
99
- `archkeep: refusing to build a JSON envelope where coverage.complete (${coverage.complete}) ` +
100
- `disagrees with coverage.notAnalyzed (${coverage.notAnalyzed.length} entr${coverage.notAnalyzed.length === 1 ? "y" : "ies"}) ` +
101
- `— the two must always agree, or a reader checking only one of them could mistake a partial ` +
102
- `run for a complete one.`,
125
+ `archkeep: refusing to build a JSON envelope claiming coverage.complete (${coverage.complete}) ` +
126
+ `over unjudged work (notAnalyzed: ${coverage.notAnalyzed.length} entr${coverage.notAnalyzed.length === 1 ? "y" : "ies"}, ` +
127
+ `blindSpots: ${unjudgedBlindSpots.length}) a run ` +
128
+ `that could not read everything cannot claim to have. This is a bug in the command that built ` +
129
+ `this envelope, not a fact about the workspace being judged.`,
103
130
  );
104
131
  }
105
132
  // A bare `null` decision is the same programming error as a hand-built