@ecoma-io/archkeep 0.21.0 → 0.22.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 (53) hide show
  1. package/cli.mjs +156 -66
  2. package/gate-attestation.mjs +23 -0
  3. package/package.json +3 -1
  4. package/src/analysis/analyze.mjs +6 -0
  5. package/src/analysis/contract.md +32 -5
  6. package/src/analysis/csharp.mjs +18 -0
  7. package/src/analysis/go.mjs +18 -0
  8. package/src/analysis/java.mjs +15 -0
  9. package/src/analysis/kotlin.mjs +15 -0
  10. package/src/analysis/python.mjs +25 -3
  11. package/src/analysis/rust.mjs +18 -0
  12. package/src/analysis/source-util.mjs +113 -0
  13. package/src/analysis/typescript.mjs +86 -5
  14. package/src/canonical.mjs +43 -25
  15. package/src/commands/README.md +63 -12
  16. package/src/commands/change-intent.mjs +25 -1
  17. package/src/commands/change.mjs +90 -40
  18. package/src/commands/check.mjs +65 -26
  19. package/src/commands/completeness.mjs +126 -19
  20. package/src/commands/context-command.mjs +13 -5
  21. package/src/commands/context.mjs +31 -4
  22. package/src/commands/coverage-verdict.mjs +191 -0
  23. package/src/commands/debt.mjs +18 -15
  24. package/src/commands/delta-classify.mjs +13 -18
  25. package/src/commands/delta-snapshot.mjs +13 -5
  26. package/src/commands/delta.mjs +95 -33
  27. package/src/commands/diff.mjs +31 -24
  28. package/src/commands/discover.mjs +70 -29
  29. package/src/commands/drift.mjs +21 -21
  30. package/src/commands/edge-constraints.mjs +47 -1
  31. package/src/commands/evaluation-primitives.mjs +194 -2
  32. package/src/commands/evolution.mjs +27 -10
  33. package/src/commands/explain.mjs +14 -13
  34. package/src/commands/fitness.mjs +20 -19
  35. package/src/commands/graph.mjs +29 -11
  36. package/src/commands/health.mjs +12 -5
  37. package/src/commands/history.mjs +41 -26
  38. package/src/commands/impact.mjs +17 -18
  39. package/src/commands/plan-context-command.mjs +10 -5
  40. package/src/commands/reconcile.mjs +14 -17
  41. package/src/commands/scenario-evaluation.mjs +93 -16
  42. package/src/commands/scenario.mjs +28 -18
  43. package/src/commands/waivers.mjs +36 -28
  44. package/src/governance/evolution-event.mjs +96 -9
  45. package/src/intent/intent-manifest.json +83 -39
  46. package/src/lsp/diagnose.mjs +12 -3
  47. package/src/report/discover-text.mjs +31 -9
  48. package/src/report/graph-text.mjs +25 -5
  49. package/src/report/json.mjs +32 -5
  50. package/src/report/text.mjs +82 -12
  51. package/src/verdict.mjs +78 -36
  52. package/src/verify-gate-attestation.mjs +323 -0
  53. package/src/workspace.mjs +126 -2
@@ -107,15 +107,102 @@ export function declarationDigest(intent) {
107
107
  });
108
108
  }
109
109
 
110
+ /**
111
+ * The escape character every identity spelling below uses, and the one
112
+ * function that applies it. A field that carries none of `\`, `>` or `:` and
113
+ * is not exactly `-` is returned byte-identical — the overwhelmingly common
114
+ * case, which is what keeps this escaping from rewriting the stored events of
115
+ * workspaces whose names never carried a delimiter (#627's fix is
116
+ * conditional by design; a wholesale re-encode on the `boundaryKey`
117
+ * pattern would change the persisted bytes of every workspace). A field that
118
+ * does carry one is escaped, so the delimiters that remain unescaped in an
119
+ * identity string are exactly the separators, and distinct field tuples can
120
+ * no longer join to the same string (#627). The sentinel `-` (`#628` writes
121
+ * it for an absent source project) escapes to `\-`, so a field that literally
122
+ * is `-` can no longer read as "absent" — `\-` in an identity string can only
123
+ * ever have come from field data.
124
+ *
125
+ * @param {string} value One field of an identity string.
126
+ * @returns {string} The field, escaped iff escaping is needed.
127
+ */
128
+ export function escapeIdentityField(value) {
129
+ if (!value.includes("\\") && !value.includes(">") && !value.includes(":") && value !== "-") {
130
+ return value;
131
+ }
132
+ // Backslash first, so it never escapes an escape this pass itself wrote.
133
+ return value
134
+ .replaceAll("\\", "\\\\")
135
+ .replaceAll(">", "\\>")
136
+ .replaceAll(":", "\\:")
137
+ .replace(/^-$/u, "\\-");
138
+ }
139
+
140
+ /**
141
+ * The identity string of one graph edge, in the canonical spelling
142
+ * `source>target:type` — the `(source, target, type)` identity design §1
143
+ * names. The ONE spelling the evolution events' `observed.edges` and
144
+ * `affected.boundaries` use: this module owns it, and `classifyEvolution`
145
+ * maps every edge it is handed through this function, so there is exactly
146
+ * one definition of "same edge" and no second spelling to drift. Fields are
147
+ * escaped through `escapeIdentityField`, so the unescaped `>` and `:` in the
148
+ * result are the separators and two distinct triples never join to one
149
+ * string (#627) — while a triple with no delimiter in any field spells
150
+ * exactly what earlier versions spelled, byte for byte.
151
+ *
152
+ * @param {{source: string, target: string, type: string}} edge
153
+ * @returns {string}
154
+ */
155
+ export function edgeEvolutionIdentity({ source, target, type }) {
156
+ return `${escapeIdentityField(source)}>${escapeIdentityField(target)}:${escapeIdentityField(type)}`;
157
+ }
158
+
159
+ /**
160
+ * The one accepted input shape for an `observed.edges` entry: the raw
161
+ * `{source, target, type}` triple. A caller handing over a ready-made string
162
+ * would be choosing a second spelling of "same edge", so a string is refused
163
+ * loudly rather than accepted as one shape more — the identity string is this
164
+ * module's output, never its input.
165
+ *
166
+ * @param {unknown} entry
167
+ * @returns {string}
168
+ */
169
+ function evolutionBoundary(entry) {
170
+ if (
171
+ typeof entry !== "object" ||
172
+ entry === null ||
173
+ !("source" in entry) ||
174
+ typeof entry.source !== "string" ||
175
+ entry.source === "" ||
176
+ !("target" in entry) ||
177
+ typeof entry.target !== "string" ||
178
+ entry.target === "" ||
179
+ !("type" in entry) ||
180
+ typeof entry.type !== "string"
181
+ ) {
182
+ throw new TypeError(
183
+ "classifyEvolution: observed.edges entries must be {source, target, type} triples — " +
184
+ "the identity string is classifyEvolution's own output spelling, never an input",
185
+ );
186
+ }
187
+ // The guard above has verified every property; the annotation only states
188
+ // what it proved.
189
+ return edgeEvolutionIdentity(
190
+ /** @type {{source: string, target: string, type: string}} */ (entry),
191
+ );
192
+ }
193
+
110
194
  /**
111
195
  * @typedef {object} EvolutionEvidence
112
196
  * @property {{projects?: {added: string[], removed: string[], changed: string[]},
113
- * edges?: {added: string[], removed: string[]},
197
+ * edges?: {added: {source: string, target: string, type: string}[],
198
+ * removed: {source: string, target: string, type: string}[]},
114
199
  * policyChanged?: boolean|null, policyOneSided?: boolean,
115
200
  * 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
201
+ * The structural diff between base and head: project names and raw edge
202
+ * triples that were added, removed, or changed. The triples are mapped
203
+ * through `edgeEvolutionIdentity` here — the identity spelling is this
204
+ * module's own, so `affected.boundaries` comes out as identity strings
205
+ * whichever shape the caller held. Empty by default. `policyChanged` — whether the policy fingerprint changed
119
206
  * between base and head; `null` is "could not be compared": exactly one side
120
207
  * records the policy (`policyOneSided: true`) or neither does
121
208
  * (both-absent). `true` is a disclosure, never a refusal. `policyOneSided`
@@ -197,9 +284,9 @@ export function declarationDigest(intent) {
197
284
  * absent (`null`) ⇒ NOT asserted, note added |
198
285
  *
199
286
  * 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.
287
+ * second opinion: changed project names, the changed edges under the one
288
+ * identity spelling (`edgeEvolutionIdentity`), the constraint/intent rows
289
+ * whose verdict was not `pass`/`matched`, and the ADR ids whose lineage moved.
203
290
  *
204
291
  * @param {EvolutionEvidence} [input]
205
292
  * @returns {EvolutionClassification}
@@ -211,8 +298,8 @@ export function classifyEvolution(input = {}) {
211
298
  const addedProjects = projects.added ?? [];
212
299
  const removedProjects = projects.removed ?? [];
213
300
  const changedProjects = projects.changed ?? [];
214
- const addedEdges = edges.added ?? [];
215
- const removedEdges = edges.removed ?? [];
301
+ const addedEdges = (edges.added ?? []).map(evolutionBoundary);
302
+ const removedEdges = (edges.removed ?? []).map(evolutionBoundary);
216
303
  const structureChanged =
217
304
  addedProjects.length +
218
305
  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 — plus a document whose only positioned failure is an external disclosure, which was judged and so is not published (#603)",
49
+ "sha256": "72196def14055475ccb23284a3c5d34167535a506985b26fb95af097545cc740"
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": "25ece527ff5ea847f5863314a12c50c95a80f995d36d3aeb756ca022dd72f283"
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": "25ece527ff5ea847f5863314a12c50c95a80f995d36d3aeb756ca022dd72f283"
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": "25ece527ff5ea847f5863314a12c50c95a80f995d36d3aeb756ca022dd72f283"
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"
@@ -42,7 +42,7 @@
42
42
  * places outside the boundary system entirely.
43
43
  */
44
44
  import { analyzeFile } from "../analysis/analyze.mjs";
45
- import { projectOwning } from "../analysis/source-util.mjs";
45
+ import { isExternalSiteFailure, projectOwning } from "../analysis/source-util.mjs";
46
46
  import { declaredEdgeViolationsForCheck } from "../commands/edge-constraints.mjs";
47
47
  import { evaluate } from "../rules/index.mjs";
48
48
 
@@ -97,10 +97,19 @@ export function diagnoseDocument({ sourceFile, text, index, config }) {
97
97
 
98
98
  // Recorded failures come next, and they are published whether or not the
99
99
  // rule pass below succeeds: they are the part of the file that was NOT
100
- // judged, and a reader needs that before they read what was.
100
+ // judged, and a reader needs that before they read what was. The external
101
+ // class is not that part (`isExternalSiteFailure`): a bare coordinate that
102
+ // resolves to the dependency universe was judged — resolved external,
103
+ // disclosed in the run's blind-spot rows, excluded from the withholding
104
+ // count — so a warning on it would say "not checked" about a site the
105
+ // verdict below covers, and one per third-party import would be a wall of
106
+ // warnings a reader rightly learns to ignore (#603). The workspace-surface
107
+ // and whole-file classes keep publishing.
101
108
  const diagnostics = [
102
109
  ...prelude,
103
- ...analysis.failures.map((failure) => failureDiagnostic(failure, lines)),
110
+ ...analysis.failures
111
+ .filter((failure) => !isExternalSiteFailure(failure))
112
+ .map((failure) => failureDiagnostic(failure, lines)),
104
113
  ];
105
114
 
106
115
  // The engine derives its evidence index from exactly the records it is
@@ -4,15 +4,27 @@
4
4
  *
5
5
  * The coverage claim sits ABOVE everything — the reader knows whether the
6
6
  * observations are complete before reading any entry, exactly like
7
- * `./graph-text.mjs`'s report. The proposal, when present, is rendered below
8
- * the observations with the proposal-only banner (`proposed`, `not
9
- * authoritative`) repeated on every line of every candidate, so a reader who
10
- * scans the report cannot mistake a candidate for a decision.
7
+ * `./graph-text.mjs`'s report. Under an incomplete claim sit the reason
8
+ * clauses the run withheld the verdict over, worded by
9
+ * `../verdict.mjs`'s `coverageIncompleteReasons` and rendered through
10
+ * `./text.mjs`'s `formatCoverageIncomplete` the same clauses, in the
11
+ * same `⚠` rendering, `check`'s text report prints, so a terminal reader is
12
+ * told why the verdict is withheld whichever face ran. A zero-analysis run
13
+ * is the case that needs this: its `notAnalyzed` list is empty, so a
14
+ * count-bearing headline would blame zero failures for an incomplete
15
+ * discovery (#619).
16
+ *
17
+ * The proposal, when present, is rendered below the observations with the
18
+ * proposal-only banner (`proposed`, `not authoritative`) repeated on every
19
+ * line of every candidate, so a reader who scans the report cannot mistake a
20
+ * candidate for a decision.
11
21
  *
12
22
  * This module decides nothing. A formatter that filtered would be a rule
13
23
  * wearing a formatter's name (`../README.md`).
14
24
  */
15
25
 
26
+ import { formatCoverageIncomplete } from "./text.mjs";
27
+
16
28
  /** The three confidence markers, in the order the legend prints them. */
17
29
  const CONFIDENCE_ORDER = ["high", "medium", "low"];
18
30
 
@@ -96,10 +108,16 @@ function formatRule(item) {
96
108
  *
97
109
  * @param {{discovery: {projects: object[], edges: object[], tags: string[]},
98
110
  * proposal: object|null,
99
- * coverage: object}} input
111
+ * coverage: object,
112
+ * coverageIncomplete?: string[]}} input
113
+ * `coverageIncomplete` is the withheld-verdict clause list
114
+ * (`../verdict.mjs`'s `coverageIncompleteReasons`, handed through
115
+ * `../commands/discover.mjs`) — rendered below the incomplete headline,
116
+ * empty exactly when the discovery is complete, and optional because a
117
+ * complete discovery carries no clauses to render.
100
118
  * @returns {string}
101
119
  */
102
- export function formatDiscoverReport({ discovery, proposal, coverage }) {
120
+ export function formatDiscoverReport({ discovery, proposal, coverage, coverageIncomplete }) {
103
121
  const sections = [];
104
122
 
105
123
  const inspected =
@@ -110,11 +128,15 @@ export function formatDiscoverReport({ discovery, proposal, coverage }) {
110
128
  if (coverage.complete) {
111
129
  sections.push(`✔ discovery complete (${inspected})`);
112
130
  } else {
113
- const notAnalyzedCount = coverage.notAnalyzed.length;
131
+ // The headline states the incompleteness and its consequence; the clauses
132
+ // below state WHY, one per failed coverage axis. Blaming the whole-file
133
+ // count in the headline alone would read "0 files could not be analyzed"
134
+ // over a zero-analysis run (#619) — incomplete, with a reason of nothing.
114
135
  sections.push(
115
- `✖ discovery incomplete — ${notAnalyzedCount} file${notAnalyzedCount === 1 ? "" : "s"} ` +
116
- `could not be analyzed, so these observations may under-represent the workspace (${inspected})`,
136
+ `✖ discovery incomplete — these observations may under-represent the workspace (${inspected})`,
117
137
  );
138
+ const clauses = formatCoverageIncomplete(coverageIncomplete ?? []);
139
+ if (clauses !== "") sections.push(clauses);
118
140
  }
119
141
 
120
142
  const projectWord = discovery.projects.length === 1 ? "project" : "projects";
@@ -12,12 +12,21 @@
12
12
  * The coverage claim sits ABOVE the listing, not below it, so the reader knows
13
13
  * whether the snapshot is complete before reading any entry — an incomplete
14
14
  * snapshot printed in full would have the "this may under-represent" warning
15
- * buried at the bottom.
15
+ * buried at the bottom. Under an incomplete claim sit the reason clauses the
16
+ * run withheld the verdict over, worded by `../verdict.mjs`'s
17
+ * `coverageIncompleteReasons` and rendered through `./text.mjs`'s
18
+ * `formatCoverageIncomplete` — the same clauses, in the same `⚠` rendering,
19
+ * `check`'s text report prints, so a terminal reader is told why the verdict
20
+ * is withheld whichever face ran. A zero-analysis run is the case that needs
21
+ * this: its `notAnalyzed` list is empty, so a count-bearing headline would
22
+ * blame zero failures for an incomplete snapshot.
16
23
  *
17
24
  * This module decides nothing. A formatter that filtered would be a rule
18
25
  * wearing a formatter's name (`../README.md`).
19
26
  */
20
27
 
28
+ import { formatCoverageIncomplete } from "./text.mjs";
29
+
21
30
  /**
22
31
  * One project as a line: name, root, type, and tags.
23
32
  *
@@ -44,7 +53,13 @@ function formatEdge(edge) {
44
53
  * The whole graph report.
45
54
  *
46
55
  * @param {{projects: object[], dependencies: object[], workspaceLayout: object,
47
- * workspaceLayoutSource: string, coverage: object}} input
56
+ * workspaceLayoutSource: string, coverage: object,
57
+ * coverageIncomplete?: string[]}} input
58
+ * `coverageIncomplete` is the withheld-verdict clause list
59
+ * (`../verdict.mjs`'s `coverageIncompleteReasons`, handed through
60
+ * `../../commands/graph.mjs`) — rendered below the incomplete headline,
61
+ * empty exactly when the snapshot is complete, and optional because a
62
+ * complete snapshot carries no clauses to render.
48
63
  * @returns {string}
49
64
  */
50
65
  export function formatGraphReport({
@@ -53,6 +68,7 @@ export function formatGraphReport({
53
68
  workspaceLayout,
54
69
  workspaceLayoutSource,
55
70
  coverage,
71
+ coverageIncomplete,
56
72
  }) {
57
73
  const sections = [];
58
74
 
@@ -66,11 +82,15 @@ export function formatGraphReport({
66
82
  if (coverage.complete) {
67
83
  sections.push(`✔ graph snapshot complete (${inspected})`);
68
84
  } else {
69
- const notAnalyzedCount = coverage.notAnalyzed.length;
85
+ // The headline states the incompleteness and its consequence; the clauses
86
+ // below state WHY, one per failed coverage axis. Blaming the whole-file
87
+ // count in the headline alone would read "0 files could not be analyzed"
88
+ // over a zero-analysis run (#612) — incomplete, with a reason of nothing.
70
89
  sections.push(
71
- `✖ graph snapshot incomplete — ${notAnalyzedCount} file${notAnalyzedCount === 1 ? "" : "s"} ` +
72
- `could not be analyzed, so this snapshot may under-represent the architecture (${inspected})`,
90
+ `✖ graph snapshot incomplete — this snapshot may under-represent the architecture (${inspected})`,
73
91
  );
92
+ const clauses = formatCoverageIncomplete(coverageIncomplete ?? []);
93
+ if (clauses !== "") sections.push(clauses);
74
94
  }
75
95
 
76
96
  // Layout line