@documentation-robotics/cli 0.3.0 → 0.4.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 (182) hide show
  1. package/README.md +10 -14
  2. package/dist/analyzers/active-analyzer.js +62 -0
  3. package/dist/analyzers/active-analyzer.js.map +7 -0
  4. package/dist/analyzers/cards/build.js +21 -1
  5. package/dist/analyzers/cards/build.js.map +2 -2
  6. package/dist/analyzers/cards/extract.js +138 -0
  7. package/dist/analyzers/cards/extract.js.map +7 -0
  8. package/dist/analyzers/cards/routes.js +96 -0
  9. package/dist/analyzers/cards/routes.js.map +7 -0
  10. package/dist/analyzers/cbm-analyzer.js +76 -4
  11. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  12. package/dist/analyzers/cbm-responses.js +12 -6
  13. package/dist/analyzers/cbm-responses.js.map +2 -2
  14. package/dist/analyzers/code-changes.js +159 -0
  15. package/dist/analyzers/code-changes.js.map +7 -0
  16. package/dist/analyzers/edge-relationships.js +6 -2
  17. package/dist/analyzers/edge-relationships.js.map +2 -2
  18. package/dist/analyzers/extraction-plan.js +541 -0
  19. package/dist/analyzers/extraction-plan.js.map +7 -0
  20. package/dist/analyzers/index-job.js +229 -0
  21. package/dist/analyzers/index-job.js.map +7 -0
  22. package/dist/cli.js +12 -10
  23. package/dist/cli.js.map +2 -2
  24. package/dist/coding-agents/drbot-mcp.js +120 -18
  25. package/dist/coding-agents/drbot-mcp.js.map +2 -2
  26. package/dist/commands/add.js +4 -2
  27. package/dist/commands/add.js.map +2 -2
  28. package/dist/commands/analyzer.js +35 -35
  29. package/dist/commands/analyzer.js.map +2 -2
  30. package/dist/commands/changeset-verify.js +16 -8
  31. package/dist/commands/changeset-verify.js.map +2 -2
  32. package/dist/commands/changeset.js +41 -11
  33. package/dist/commands/changeset.js.map +2 -2
  34. package/dist/commands/init.js +9 -26
  35. package/dist/commands/init.js.map +2 -2
  36. package/dist/commands/mcp.js +35 -31
  37. package/dist/commands/mcp.js.map +3 -3
  38. package/dist/commands/relationship.js +2 -12
  39. package/dist/commands/relationship.js.map +2 -2
  40. package/dist/commands/reports.js +3 -3
  41. package/dist/commands/reports.js.map +2 -2
  42. package/dist/commands/update.js +14 -13
  43. package/dist/commands/update.js.map +2 -2
  44. package/dist/commands/upgrade.js +28 -120
  45. package/dist/commands/upgrade.js.map +2 -2
  46. package/dist/commands/validate.js +19 -3
  47. package/dist/commands/validate.js.map +2 -2
  48. package/dist/commands/version.js +1 -1
  49. package/dist/commands/visualize.js +30 -12
  50. package/dist/commands/visualize.js.map +2 -2
  51. package/dist/core/annotation-store.js +15 -5
  52. package/dist/core/annotation-store.js.map +2 -2
  53. package/dist/core/catalog-migration.js +2 -1
  54. package/dist/core/catalog-migration.js.map +2 -2
  55. package/dist/core/changeset-base-index.js +7 -0
  56. package/dist/core/changeset-base-index.js.map +2 -2
  57. package/dist/core/changeset-brief-compact.js +256 -0
  58. package/dist/core/changeset-brief-compact.js.map +7 -0
  59. package/dist/core/changeset-verify.js +356 -26
  60. package/dist/core/changeset-verify.js.map +3 -3
  61. package/dist/core/extraction-observability.js +161 -0
  62. package/dist/core/extraction-observability.js.map +7 -0
  63. package/dist/core/migration-registry.js +13 -1
  64. package/dist/core/migration-registry.js.map +2 -2
  65. package/dist/core/project-init.js +53 -0
  66. package/dist/core/project-init.js.map +7 -0
  67. package/dist/core/relationship-staging.js +94 -1
  68. package/dist/core/relationship-staging.js.map +2 -2
  69. package/dist/core/staged-changeset-storage.js +83 -3
  70. package/dist/core/staged-changeset-storage.js.map +2 -2
  71. package/dist/core/staging-area.js +85 -8
  72. package/dist/core/staging-area.js.map +2 -2
  73. package/dist/core/upgrade-plan.js +117 -0
  74. package/dist/core/upgrade-plan.js.map +7 -0
  75. package/dist/generated/relationship-index.js +124 -15
  76. package/dist/generated/relationship-index.js.map +2 -2
  77. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  78. package/dist/integrations/claude_code/agents/dr-architect.md +19 -5
  79. package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
  80. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  81. package/dist/integrations/claude_code/commands/dr-map.md +7 -1
  82. package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
  83. package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
  84. package/dist/integrations/claude_code/docs/README.md +20 -1
  85. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  86. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  87. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +3 -0
  88. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
  89. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
  90. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
  91. package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
  92. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
  93. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
  94. package/dist/mcp/changeset-only.js +14 -3
  95. package/dist/mcp/changeset-only.js.map +2 -2
  96. package/dist/mcp/project-root.js +85 -0
  97. package/dist/mcp/project-root.js.map +7 -0
  98. package/dist/mcp/server-instructions.js +27 -0
  99. package/dist/mcp/server-instructions.js.map +7 -0
  100. package/dist/mcp/strict-schema.js +52 -0
  101. package/dist/mcp/strict-schema.js.map +7 -0
  102. package/dist/mcp/tool-registry.js +39 -5
  103. package/dist/mcp/tool-registry.js.map +2 -2
  104. package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
  105. package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
  106. package/dist/mcp/tools/changeset-commit.js +224 -0
  107. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  108. package/dist/mcp/tools/changeset-editing.js +133 -57
  109. package/dist/mcp/tools/changeset-editing.js.map +3 -3
  110. package/dist/mcp/tools/changeset-lifecycle.js +1 -1
  111. package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
  112. package/dist/mcp/tools/changeset-verify.js +12 -6
  113. package/dist/mcp/tools/changeset-verify.js.map +2 -2
  114. package/dist/mcp/tools/codebase-extract.js +836 -0
  115. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  116. package/dist/mcp/tools/codebase-index.js +125 -0
  117. package/dist/mcp/tools/codebase-index.js.map +7 -0
  118. package/dist/mcp/tools/codebase-shared.js +265 -0
  119. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  120. package/dist/mcp/tools/codebase-verify.js +48 -0
  121. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  122. package/dist/mcp/tools/index.js +15 -0
  123. package/dist/mcp/tools/index.js.map +2 -2
  124. package/dist/mcp/tools/model-add.js +189 -73
  125. package/dist/mcp/tools/model-add.js.map +2 -2
  126. package/dist/mcp/tools/model-update.js +16 -12
  127. package/dist/mcp/tools/model-update.js.map +2 -2
  128. package/dist/mcp/tools/model-validate.js +46 -4
  129. package/dist/mcp/tools/model-validate.js.map +2 -2
  130. package/dist/mcp/tools/project-status-collectors.js +381 -0
  131. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  132. package/dist/mcp/tools/project-status.js +237 -0
  133. package/dist/mcp/tools/project-status.js.map +7 -0
  134. package/dist/mcp/tools/project-tools.js +505 -0
  135. package/dist/mcp/tools/project-tools.js.map +7 -0
  136. package/dist/mcp/tools/relationship-tools.js +89 -20
  137. package/dist/mcp/tools/relationship-tools.js.map +2 -2
  138. package/dist/mcp/tools/review-open.js +210 -0
  139. package/dist/mcp/tools/review-open.js.map +7 -0
  140. package/dist/mcp/tools/shared.js +10 -2
  141. package/dist/mcp/tools/shared.js.map +2 -2
  142. package/dist/reports/model-report-orchestrator.js +31 -2
  143. package/dist/reports/model-report-orchestrator.js.map +2 -2
  144. package/dist/schemas/bundled/analyzers/cbm.json +22 -10
  145. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  146. package/dist/schemas/bundled/api.json +1 -1
  147. package/dist/schemas/bundled/apm.json +1 -1
  148. package/dist/schemas/bundled/application.json +1 -1
  149. package/dist/schemas/bundled/base.json +1 -1
  150. package/dist/schemas/bundled/business.json +1 -1
  151. package/dist/schemas/bundled/data-model.json +1 -1
  152. package/dist/schemas/bundled/data-store.json +1 -1
  153. package/dist/schemas/bundled/manifest.json +3 -3
  154. package/dist/schemas/bundled/motivation.json +1 -1
  155. package/dist/schemas/bundled/navigation.json +1 -1
  156. package/dist/schemas/bundled/product.json +1 -1
  157. package/dist/schemas/bundled/security.json +1 -1
  158. package/dist/schemas/bundled/technology.json +1 -1
  159. package/dist/schemas/bundled/testing.json +145 -1
  160. package/dist/schemas/bundled/ux.json +1 -1
  161. package/dist/server/server.js +508 -237
  162. package/dist/server/server.js.map +3 -3
  163. package/dist/server/viewer-registry.js +264 -0
  164. package/dist/server/viewer-registry.js.map +7 -0
  165. package/dist/telemetry/index.js +1 -1
  166. package/dist/utils/id-generator.js +5 -2
  167. package/dist/utils/id-generator.js.map +2 -2
  168. package/dist/utils/source-reference.js +58 -27
  169. package/dist/utils/source-reference.js.map +2 -2
  170. package/dist/utils/spec-version.js +2 -2
  171. package/dist/utils/spec-version.js.map +1 -1
  172. package/dist/validators/model-warnings.js +28 -0
  173. package/dist/validators/model-warnings.js.map +7 -0
  174. package/dist/viewer/README.md +6 -6
  175. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  176. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  177. package/dist/viewer/index.html +2 -2
  178. package/dist/viewer/manifest.json +10 -10
  179. package/dist/viewer/version.json +1 -1
  180. package/package.json +1 -1
  181. package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
  182. package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
@@ -2,6 +2,11 @@ import { Changeset } from "./changeset.js";
2
2
  import { ChangesetGraph } from "./changeset-graph.js";
3
3
  import { tryReadBaseIndex } from "./changeset-base-index.js";
4
4
  import { stagedSourceReference } from "./staged-source-reference.js";
5
+ import { loadExtractionCoverage } from "./extraction-observability.js";
6
+ import { currentGitHead, resolveAnalyzerName } from "../analyzers/active-analyzer.js";
7
+ import { decisionKey } from "../analyzers/extraction-plan.js";
8
+ import { readFile } from "node:fs/promises";
9
+ import path from "node:path";
5
10
  const LIFECYCLE_FIELDS = ["attributes.status"], LIFECYCLE_STAGES = [
6
11
  ["proposed", "draft"],
7
12
  ["planned", "under-review"],
@@ -21,6 +26,37 @@ function isLifecycleMove(difference) {
21
26
  const from = lifecycleStage(difference.planned), to = lifecycleStage(difference.built);
22
27
  return from >= 0 && to > from;
23
28
  }
29
+ function readList(value, keep) {
30
+ return Array.isArray(value) ? value.filter((e) => e && typeof e == "object" && keep(e)) : [];
31
+ }
32
+ async function readRecordFile(file) {
33
+ try {
34
+ const parsed = JSON.parse(await readFile(file, "utf-8"));
35
+ return parsed && typeof parsed == "object" ? parsed : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ async function readExtractionRecord(rootPath, changeset, gitHead) {
41
+ const dir = path.join(rootPath, ".dr", "analyzers", "extract");
42
+ let raw = await readRecordFile(path.join(dir, `${changeset.id.replace(/[^\w.-]/g, "_")}.json`)), source = "changeset";
43
+ if (raw && (raw.pending || raw.changesetId !== void 0 && raw.changesetId !== changeset.id) && (raw = null), !raw) {
44
+ const pending = await readRecordFile(path.join(dir, "pending.json"));
45
+ pending?.pending && pending.changesetName === changeset.name && (raw = pending, source = "pending");
46
+ }
47
+ if (!raw)
48
+ return null;
49
+ const symbol = (e) => typeof e.symbol == "string", readAt = source === "pending" && typeof raw.gitHead == "string" && raw.gitHead !== gitHead ? raw.gitHead : void 0;
50
+ return {
51
+ source,
52
+ ...readAt ? { readAt } : {},
53
+ staged: readList(raw.staged, (e) => typeof e.id == "string"),
54
+ cards: readList(raw.cards, symbol),
55
+ earlierCards: readList(raw.earlierCards, symbol),
56
+ updated: readList(raw.updated, (e) => typeof e.id == "string"),
57
+ candidateDeletes: Array.isArray(raw.candidateDeletes) ? raw.candidateDeletes.filter((d) => typeof d == "string") : []
58
+ };
59
+ }
24
60
  function planFromChangeset(changeset, context = {}) {
25
61
  const resolve = context.resolve ?? ((id) => id), notes = [], elements = /* @__PURE__ */ new Map(), relationships = /* @__PURE__ */ new Map(), changes = [...changeset.changes].map((change, index) => ({ change, index })).sort((a, b) => (a.change.sequenceNumber ?? a.index) - (b.change.sequenceNumber ?? b.index));
26
62
  for (const { change, index } of changes) {
@@ -84,9 +120,17 @@ async function planFromModel(model, changeset) {
84
120
  function builtFromGraph(graph, label = graph.changeset.name) {
85
121
  const elements = [], changedElements = [];
86
122
  for (const element of graph.elements.values()) {
123
+ const staged = element.status === "added" || element.status === "updated" && !sameValue(element.sourceReference ?? null, (element.before && stagedSourceReference(element.before)) ?? null), current = fromGraphElement(element);
124
+ let updatedFrom;
125
+ if (element.status === "updated" && element.before) {
126
+ const before = toVerifyElement(element.before, element.layer, element.id);
127
+ updatedFrom = Object.fromEntries(changedFields(before, current).map((f) => [f, fieldValue(before, f)])), sameValue(current.sourceReference ?? null, before.sourceReference ?? null) || (updatedFrom.source_reference = before.sourceReference ?? null);
128
+ }
87
129
  const built = {
88
- ...fromGraphElement(element),
89
- ...element.status !== "unchanged" ? { change: element.status } : {}
130
+ ...current,
131
+ ...element.status !== "unchanged" ? { change: element.status } : {},
132
+ ...staged ? { stagedProvenance: element.sourceReference?.provenance ?? "none" } : {},
133
+ ...updatedFrom ? { updatedFrom } : {}
90
134
  };
91
135
  element.status !== "deleted" && elements.push(built), element.status !== "unchanged" && changedElements.push(built);
92
136
  }
@@ -143,9 +187,29 @@ function verifyChangeset(plan, built, options = {}) {
143
187
  verdict: "match",
144
188
  verdictText: "",
145
189
  summary: { elements: emptyCounts(), relationships: emptyCounts() },
146
- elements: { matched: [], divergent: [], missing: [], unplanned: [], deletionsNotDone: [] },
147
- relationships: { matched: [], divergent: [], missing: [], unplanned: [], deletionsNotDone: [] },
190
+ elements: { matched: [], divergent: [], missing: [], unverifiable: [], unplanned: [], deletionsNotDone: [] },
191
+ relationships: { matched: [], divergent: [], missing: [], unverifiable: [], unplanned: [], deletionsNotDone: [] },
148
192
  notes
193
+ }, observability = options.observability, unverifiableIds = /* @__PURE__ */ new Map(), missingHints = /* @__PURE__ */ new Map(), markUnverifiable = (planned) => {
194
+ if (!observability)
195
+ return !1;
196
+ const verdict2 = observability.element(planned);
197
+ if (verdict2.observable)
198
+ return !1;
199
+ if (verdict2.unlocated) {
200
+ const others = (built.changedElements ?? []).filter((e) => e.change !== "deleted" && e.layer === planned.layer && sameType(e, planned) && !claimed.has(e.id));
201
+ if (others.length > 0)
202
+ return missingHints.set(planned.id, `not found in the build by id or name, and it has no method, path or source reference to find it by; the build has ${others.length} unplanned ${planned.layer}.${planned.type} element(s) (${others.slice(0, 5).map((e) => e.id).join(", ")}${others.length > 5 ? ", ..." : ""}). If one of them is this one, give the plan's element its http_method and http_path, or its sourceFile and sourceSymbol, and verify again`), !1;
203
+ }
204
+ const reason = verdict2.reason ?? "extraction can't produce it";
205
+ return unverifiableIds.set(planned.id, reason), report.elements.unverifiable.push({
206
+ planned: planned.id,
207
+ layer: planned.layer,
208
+ type: planned.type,
209
+ name: planned.name,
210
+ op: planned.op,
211
+ reason
212
+ }), !0;
149
213
  }, live = plan.elements.filter((e) => e.op !== "delete"), deletions = plan.elements.filter((e) => e.op === "delete");
150
214
  for (const planned of live)
151
215
  builtById.has(planned.id) && (claimed.add(planned.id), elementMap.set(planned.id, { built: planned.id, match: "exact" }));
@@ -219,6 +283,8 @@ function verifyChangeset(plan, built, options = {}) {
219
283
  for (const planned of live) {
220
284
  const mapping = elementMap.get(planned.id);
221
285
  if (!mapping) {
286
+ if (markUnverifiable(planned))
287
+ continue;
222
288
  const candidates = ambiguous.get(planned.id);
223
289
  report.elements.missing.push({
224
290
  planned: planned.id,
@@ -226,21 +292,39 @@ function verifyChangeset(plan, built, options = {}) {
226
292
  type: planned.type,
227
293
  name: planned.name,
228
294
  op: planned.op,
229
- reason: candidates ? `several built elements could be it (${candidates.length}); none matched uniquely` : planned.op === "update" ? "the element the plan updates does not exist in the build" : "not found in the build by id, name or source file",
295
+ reason: candidates ? `several built elements could be it (${candidates.length}); none matched uniquely` : planned.op === "update" ? "the element the plan updates does not exist in the build" : missingHints.get(planned.id) ?? "not found in the build by id, name or source file",
230
296
  ...candidates ? { candidates } : {}
231
297
  });
232
298
  continue;
233
299
  }
234
- const builtElement = builtById.get(mapping.built), fields = compareElements(planned, builtElement, planned.op === "update" ? planned.changedFields : void 0), match = {
300
+ const builtElement = builtById.get(mapping.built);
301
+ let fields = compareElements(planned, builtElement, planned.op === "update" ? planned.changedFields : void 0);
302
+ const match = {
235
303
  planned: planned.id,
236
304
  built: mapping.built,
237
305
  op: planned.op,
238
306
  match: mapping.match,
239
307
  ...mapping.reason ? { reason: mapping.reason } : {}
240
- };
241
- if (planned.op === "update" && planned.changedFields && planned.changedFields.length > 0) {
242
- const unmet = fields.filter((f) => planned.changedFields.includes(f.field)), touched = unmet.some((f) => buildTouched(planned, builtElement, f.field));
243
- if (unmet.length === planned.changedFields.length && !touched) {
308
+ }, unverifiableFields = observability?.field ? fields.filter((f) => !observability.field(planned, f.field) && buildLeft(planned, builtElement, f)) : [];
309
+ if (unverifiableFields.length > 0 && (match.unverifiableFields = unverifiableFields, fields = fields.filter((f) => !unverifiableFields.includes(f))), planned.op === "update" && planned.changedFields && planned.changedFields.length > 0) {
310
+ const checkable = planned.changedFields.filter((field) => !unverifiableFields.some((u) => u.field === field));
311
+ if (checkable.length === 0) {
312
+ const reason = `extraction doesn't write ${unverifiableFields.map((f) => f.field).join(", ")}`;
313
+ report.elements.unverifiable.push({
314
+ planned: planned.id,
315
+ layer: planned.layer,
316
+ type: planned.type,
317
+ name: planned.name,
318
+ op: planned.op,
319
+ reason,
320
+ fields: unverifiableFields
321
+ });
322
+ continue;
323
+ }
324
+ const unmet = fields.filter((f) => checkable.includes(f.field)), touched = unmet.some((f) => buildTouched(planned, builtElement, f.field));
325
+ if (unmet.length === checkable.length && !touched) {
326
+ if (markUnverifiable(planned))
327
+ continue;
244
328
  report.elements.missing.push({
245
329
  planned: planned.id,
246
330
  layer: planned.layer,
@@ -253,18 +337,27 @@ function verifyChangeset(plan, built, options = {}) {
253
337
  continue;
254
338
  }
255
339
  }
256
- const lifecycle = fields.filter(isLifecycleMove), differences = fields.filter((f) => !isLifecycleMove(f));
340
+ const lifecycle = fields.filter(isLifecycleMove), descriptionChecked = planned.op === "update" && (planned.changedFields ?? []).includes("description"), informational = fields.filter((f) => f.field === "description" && !descriptionChecked), differences = fields.filter((f) => !isLifecycleMove(f) && !informational.includes(f));
257
341
  if (lifecycle.length > 0) {
258
342
  match.lifecycle = lifecycle;
259
343
  for (const f of lifecycle)
260
344
  lifecycleMoves.push(`${planned.id} ${f.field} ${String(f.planned)} \u2192 ${String(f.built)}`);
261
345
  }
262
- differences.length > 0 ? report.elements.divergent.push({ ...match, fields: differences }) : report.elements.matched.push(match);
346
+ informational.length > 0 && (match.notes = informational), differences.length > 0 ? report.elements.divergent.push({ ...match, fields: differences }) : report.elements.matched.push(match);
263
347
  }
264
348
  lifecycleMoves.length > 0 && notes.push(`Lifecycle progress, not counted as divergence (${LIFECYCLE_FIELDS.join(", ")} moving to a later stage of ${LIFECYCLE_STAGES.map((stage) => stage.join("/")).join(" \u2192 ")}): ${lifecycleMoves.join("; ")}.`);
349
+ const described = [...report.elements.matched, ...report.elements.divergent].filter((m) => m.notes);
350
+ described.length > 0 && notes.push(`${described.length} matched element${described.length === 1 ? " has a description that differs" : "s have descriptions that differ"} from the plan's (under notes on each match). Descriptions are not compared: no need to edit the plan to match the build's text.`);
351
+ const unwritten = [...report.elements.matched, ...report.elements.divergent].filter((m) => m.unverifiableFields);
352
+ unwritten.length > 0 && notes.push(`${unwritten.length} matched element${unwritten.length === 1 ? " has" : "s have"} planned fields extraction doesn't write (under unverifiableFields on each match): check them in the code or the review.`);
265
353
  for (const planned of deletions) {
266
354
  const still = builtById.get(planned.id);
267
- still ? (claimed.add(still.id), report.elements.deletionsNotDone.push({ planned: planned.id, built: still.id })) : (claimed.add(planned.id), report.elements.matched.push({ planned: planned.id, built: planned.id, op: "delete", match: "exact" }));
355
+ if (still) {
356
+ if (claimed.add(still.id), markUnverifiable(planned))
357
+ continue;
358
+ report.elements.deletionsNotDone.push({ planned: planned.id, built: still.id });
359
+ } else
360
+ claimed.add(planned.id), report.elements.matched.push({ planned: planned.id, built: planned.id, op: "delete", match: "exact" });
268
361
  }
269
362
  if (built.changedElements)
270
363
  for (const element of built.changedElements)
@@ -275,7 +368,25 @@ function verifyChangeset(plan, built, options = {}) {
275
368
  name: element.name,
276
369
  change: element.change ?? "added"
277
370
  });
278
- const builtRels = new Map(built.relationships.map((r) => [relKey(r), r])), claimedRels = /* @__PURE__ */ new Set(), mapId = (id) => elementMap.get(id)?.built ?? id, probableEndpoints = (rel) => [rel.source, rel.target].filter((id, i, all) => all.indexOf(id) === i).filter((id) => elementMap.get(id)?.match === "probable").map((id) => `${id} \u2192 ${elementMap.get(id).built}`), missingEndpoints = new Set(report.elements.missing.map((m) => m.planned)), mapRel = (planned) => ({
371
+ const builtRels = new Map(built.relationships.map((r) => [relKey(r), r])), claimedRels = /* @__PURE__ */ new Set(), mapId = (id) => elementMap.get(id)?.built ?? id, probableEndpoints = (rel) => [rel.source, rel.target].filter((id, i, all) => all.indexOf(id) === i).filter((id) => elementMap.get(id)?.match === "probable").map((id) => `${id} \u2192 ${elementMap.get(id).built}`), missingEndpoints = new Set(report.elements.missing.map((m) => m.planned)), plannedById = new Map(plan.elements.map((e) => [e.id, e])), elementOf = (id) => builtById.get(elementMap.get(id)?.built ?? "") ?? plannedById.get(id) ?? builtById.get(id) ?? fromId(id, ""), unverifiableReason = (rel) => {
372
+ if (!observability)
373
+ return;
374
+ for (const end of [rel.source, rel.target]) {
375
+ const known = unverifiableIds.get(end);
376
+ if (known !== void 0)
377
+ return `${end} is unverifiable (${known})`;
378
+ if (missingHints.has(end))
379
+ continue;
380
+ const verdict3 = observability.element(elementOf(end));
381
+ if (!verdict3.observable)
382
+ return `extraction can't see ${end}: ${verdict3.reason ?? "no rule produces it"}`;
383
+ }
384
+ const verdict2 = observability.relationship(elementOf(rel.source), rel.predicate, elementOf(rel.target));
385
+ return verdict2.observable ? void 0 : verdict2.reason ?? "no mapped code edge produces it";
386
+ }, pushUnverifiable = (planned) => {
387
+ const reason = unverifiableReason(planned);
388
+ return reason === void 0 ? !1 : (report.relationships.unverifiable.push({ planned: strip(planned), op: planned.op, reason }), !0);
389
+ }, mapRel = (planned) => ({
279
390
  source: mapId(planned.source),
280
391
  predicate: planned.predicate,
281
392
  target: mapId(planned.target)
@@ -291,7 +402,12 @@ function verifyChangeset(plan, built, options = {}) {
291
402
  }
292
403
  claimedRels.add(relKey(found));
293
404
  const plannedRef = strip(planned);
294
- planned.op === "delete" ? report.relationships.deletionsNotDone.push({ planned: plannedRef, built: strip(found) }) : report.relationships.matched.push({ planned: plannedRef, built: strip(found), op: "add", ...matchInfoFor(planned) });
405
+ if (planned.op === "delete") {
406
+ if (pushUnverifiable(planned))
407
+ continue;
408
+ report.relationships.deletionsNotDone.push({ planned: plannedRef, built: strip(found) });
409
+ } else
410
+ report.relationships.matched.push({ planned: plannedRef, built: strip(found), op: "add", ...matchInfoFor(planned) });
295
411
  }
296
412
  const addedBuiltIds = new Set(live.filter((e) => e.op === "add" && elementMap.has(e.id)).map((e) => elementMap.get(e.id).built));
297
413
  for (const planned of unresolved) {
@@ -323,6 +439,8 @@ function verifyChangeset(plan, built, options = {}) {
323
439
  });
324
440
  continue;
325
441
  }
442
+ if (pushUnverifiable(planned))
443
+ continue;
326
444
  const candidates = built.changedRelationships ? [] : built.relationships.filter(isAlternative).map(strip), absent = [planned.source, planned.target].filter((id) => missingEndpoints.has(id) || !elementMap.has(id) && !builtById.has(id));
327
445
  report.relationships.missing.push({
328
446
  planned: plannedRef,
@@ -343,14 +461,187 @@ function verifyChangeset(plan, built, options = {}) {
343
461
  async function verifyChangesetInModel(model, options) {
344
462
  const { planned } = options, plan = await planFromModel(model, planned), notes = [], baseIndex = await tryReadBaseIndex(model.rootPath, planned.id);
345
463
  baseIndex.index && (plan.baseElementIds = Object.keys(baseIndex.index.elements)), baseIndex.warning && notes.push(baseIndex.warning);
346
- let built;
464
+ let built, extraction, observability, warning;
347
465
  if ("changeset" in options.against) {
348
466
  const asBuilt = options.against.changeset;
349
467
  asBuilt.id === planned.id && notes.push("The planned and as-built changesets are the same changeset, so everything matches by definition."), built = builtFromGraph(await ChangesetGraph.build(model, asBuilt), `${asBuilt.name} (${asBuilt.id})`);
468
+ const record = await readExtractionRecord(model.rootPath, { id: asBuilt.id, name: asBuilt.name }, currentGitHead(model.codebaseRoot));
469
+ if (!record)
470
+ extraction = {
471
+ applied: !1,
472
+ reason: "codebase_extract has no record of the as-built changeset (.dr/analyzers/extract/), so it is not taken as extraction output: every planned change is checked, and its elements count as added by hand, whatever provenance they declare.",
473
+ asBuilt: asBuiltShare(built, plan, { record: null })
474
+ }, EXTRACTION_MARKER.test(asBuilt.description ?? "") && notes.push("The as-built changeset's description says codebase_extract staged it, but its record is gone. Run codebase_extract on it again (with the same since) to record what it stages, then verify again.");
475
+ else {
476
+ const codeFiles = [...new Set((built.changedElements ?? []).flatMap((e) => sourceFiles(e)))];
477
+ ({ extraction, observability } = await extractionRule(model, options, codeFiles, "The as-built changeset came from codebase_extract"));
478
+ const share2 = asBuiltShare(built, plan, { record, ...observability ? { observe: (e) => observability.element(e) } : {} });
479
+ extraction.asBuilt = share2, record.readAt && notes.push(`The extraction record is from a run that staged nothing, at ${record.readAt.slice(0, 7)}; the code has moved since (a commit after the extract). Its cards still name what extraction offered then; run codebase_extract on this changeset again if the code changed since.`), share2.copiedFromPlan?.length && notes.push(`${share2.copiedFromPlan.length} as-built element(s) share an id with the plan's adds but did not come from extraction (${share2.copiedFromPlan.join(", ")}). If they were copied from the plan, their match says nothing about the code; extract again with codebase_extract and leave them out.`);
480
+ }
481
+ warning = asBuiltWarning(extraction.asBuilt, extraction.applied);
350
482
  } else
351
- built = await builtFromBase(model), planned.status === "staged" && notes.push("The planned changeset is still staged, so its own changes are not in the base model: anything reported missing is missing from what was committed to the base (e.g. an as-built changeset).");
352
- const report = verifyChangeset(plan, built, { matchBySource: options.matchBySource });
353
- return report.notes.push(...notes), { plannedChangeset: { id: planned.id, name: planned.name }, ...report };
483
+ built = await builtFromBase(model), planned.status === "staged" && notes.push("The planned changeset is still staged, so its own changes are not in the base model: anything reported missing is missing from what was committed to the base (e.g. an as-built changeset)."), options.against.extracted ? { extraction, observability } = await extractionRule(model, options, [], "Compared with the base model, which holds a landed extraction (extracted: true)") : extraction = {
484
+ applied: !1,
485
+ reason: "Compared with the base model: every planned change is checked. Pass extracted: true when the base holds a landed extraction of the build."
486
+ };
487
+ const report = verifyChangeset(plan, built, { matchBySource: options.matchBySource, observability });
488
+ report.extraction = extraction, report.notes.push(...notes);
489
+ const share = extraction.asBuilt;
490
+ if (share) {
491
+ const agentIds = new Set((share.agentAdded ?? []).map((a) => a.id)), edits = new Map((share.editedAfterExtraction ?? []).map((e) => [e.id, e.edits])), matches = [...report.elements.matched, ...report.elements.divergent], onEdited = matches.filter((m) => edits.has(m.built));
492
+ for (const m of onEdited)
493
+ m.editedAfterExtraction = edits.get(m.built);
494
+ const onAgent = matches.filter((m) => agentIds.has(m.built) || edits.has(m.built));
495
+ for (const m of onAgent)
496
+ m.builtByAgent = !0;
497
+ onEdited.length > 0 && report.notes.push(`${onEdited.length} match(es) are against extracted elements changed after extraction staged them (editedAfterExtraction on each): the match shows the edit, not the code.`), warning && (report.warning = warning, report.verdictText += ` But ${warning.charAt(0).toLowerCase()}${warning.slice(1)}`), onAgent.length > 0 && share.record !== "not-found" && (report.verdict === "match" && (report.verdict = "probable-match"), report.verdictText += ` ${onAgent.length} of the matches ${onAgent.length === 1 ? "rests" : "rest"} on elements extraction didn't stage` + (onEdited.length > 0 ? ` or that were edited after it (${onEdited.length})` : "") + " (builtByAgent): confirm them in the code.");
498
+ }
499
+ return { plannedChangeset: { id: planned.id, name: planned.name }, ...report };
500
+ }
501
+ async function extractionRule(model, options, codeFiles, why) {
502
+ const analyzer = options.analyzer ?? await resolveAnalyzerName(model.rootPath).catch(() => "cbm"), coverage = options.coverage !== void 0 ? options.coverage : await loadExtractionCoverage(analyzer);
503
+ return coverage ? {
504
+ observability: {
505
+ element: (element) => coverage.element(element, codeFiles),
506
+ relationship: (source, predicate, target) => coverage.relationship(source, predicate, target),
507
+ field: (element, field) => coverage.field(element, field)
508
+ },
509
+ extraction: {
510
+ applied: !0,
511
+ reason: `${why}: planned changes extraction can't produce (intent, types or files no rule covers, relationships no code edge maps to, fields it doesn't write) are unverifiable, not missing.`,
512
+ analyzer: coverage.analyzer,
513
+ observableTypes: coverage.observableTypes()
514
+ }
515
+ } : {
516
+ extraction: {
517
+ applied: !1,
518
+ reason: `${why}, but the ${analyzer} mapping could not be read, so extraction's blind spots are not known: every planned change is checked.`,
519
+ analyzer
520
+ }
521
+ };
522
+ }
523
+ const EXTRACTION_MARKER = /Staged by codebase_extract/;
524
+ function asBuiltShare(built, plan, context = {}) {
525
+ const record = context.record ?? null, changed = built.changedElements ?? [], planAdds = (plan?.elements ?? []).filter((e) => e.op === "add"), plannedAdds = new Set(planAdds.map((e) => e.id)), plannedNames = new Set(planAdds.flatMap((e) => [...nameKeys(e)])), stagedById = new Map((record?.staged ?? []).map((e) => [e.id, e])), updatedById = new Map((record?.updated ?? []).map((u) => [u.id, u])), candidateDeletes = new Set(record?.candidateDeletes ?? []), offered = /* @__PURE__ */ new Map();
526
+ for (const entry of [...record?.staged ?? [], ...record?.cards ?? [], ...record?.earlierCards ?? []]) {
527
+ const key = decisionKey(entry.file, entry.symbol);
528
+ offered.set(key, [...offered.get(key) ?? [], entry]);
529
+ }
530
+ const notFromExtraction = (e) => {
531
+ if (!record)
532
+ return "no codebase_extract record of this changeset confirms it";
533
+ if (e.change === "deleted")
534
+ return candidateDeletes.has(e.id) ? void 0 : "a delete that is not one of extraction's candidate deletes";
535
+ if (e.change === "updated")
536
+ return updatedById.has(e.id) ? void 0 : "an update extraction did not stage";
537
+ const type = `${e.layer}.${e.type}`, looksPlanned = plannedAdds.has(e.id) || [...nameKeys(e)].some((k) => plannedNames.has(k)), staged = stagedById.get(e.id), located = (e.sourceReference?.locations ?? []).filter((l) => typeof l?.symbol == "string" && l.symbol.length > 0), cards = located.flatMap((l) => offered.get(decisionKey(normalizeFile(l.file), l.symbol)) ?? []);
538
+ if (!staged && cards.length === 0) {
539
+ const verdict = context.observe?.(e);
540
+ return verdict && !verdict.observable ? `extraction can't produce it: ${verdict.reason ?? type}` : located.length === 0 ? "extraction did not stage it, and it names no source symbol" : "extraction did not stage it, and no card of its runs has its file and symbol";
541
+ }
542
+ const producible = context.observe ? context.observe(e) : { observable: !0 }, allowed = (c) => c.candidates?.length ? c.candidates.some((t) => t === type || t === `${e.layer}.*`) : producible.observable, fitting = cards.filter(allowed), withCandidates = cards.filter((c) => c.candidates?.length);
543
+ if (withCandidates.length > 0 && !withCandidates.some(allowed))
544
+ return `its card allows ${[...new Set(withCandidates.flatMap((c) => c.candidates))].join(", ")}, not ${type}`;
545
+ if (!producible.observable && withCandidates.length === 0)
546
+ return `extraction can't produce it ${staged ? "there" : "from its card"}: ${producible.reason ?? type}`;
547
+ if (looksPlanned && !(staged && namedAfterCard(e, staged) || fitting.some((c) => namedAfterCard(e, c))))
548
+ return staged ? "named after the plan's element, not after the symbol extraction staged it from" : "named after the plan's element, not after its card";
549
+ }, byProvenance = {}, extracted = /* @__PURE__ */ new Set(), agentAdded = [], edited = [];
550
+ for (const e of changed) {
551
+ const declared = e.change === "deleted" ? "deleted" : e.stagedProvenance ?? (e.change === "updated" ? "sourceUnchanged" : e.sourceReference?.provenance ?? "none");
552
+ byProvenance[declared] = (byProvenance[declared] ?? 0) + 1;
553
+ const reason = notFromExtraction(e);
554
+ if (reason !== void 0) {
555
+ agentAdded.push({ id: e.id, declared, reason });
556
+ continue;
557
+ }
558
+ extracted.add(e.id);
559
+ const edits = editsAfterExtraction(e, stagedById.get(e.id), updatedById.get(e.id));
560
+ edits.length > 0 && edited.push({ id: e.id, edits });
561
+ }
562
+ const relationships = (built.changedRelationships ?? []).filter((r) => r.change !== "deleted"), betweenExtracted = relationships.filter((r) => extracted.has(r.source) && extracted.has(r.target)).length, addedIds = new Set(changed.filter((e) => e.change === "added").map((e) => e.id)), copied = agentAdded.filter((a) => plannedAdds.has(a.id) && addedIds.has(a.id)).map((a) => a.id);
563
+ return {
564
+ elements: { total: changed.length, byProvenance, fromExtraction: extracted.size, agentAdded: agentAdded.length },
565
+ relationships: { total: relationships.length, betweenExtracted, other: relationships.length - betweenExtracted },
566
+ record: record ? record.source === "pending" ? "pending" : "found" : "not-found",
567
+ ...agentAdded.length > 0 ? { agentAdded } : {},
568
+ ...edited.length > 0 ? { editedAfterExtraction: edited } : {},
569
+ ...copied.length > 0 ? { copiedFromPlan: copied } : {}
570
+ };
571
+ }
572
+ function recordFieldValue(element, field) {
573
+ return field === "source_reference" ? element.sourceReference ?? null : fieldValue(element, field);
574
+ }
575
+ function sameRecordValue(field, a, b) {
576
+ if (field !== "source_reference")
577
+ return sameFieldValue(field, a, b);
578
+ const where = (v) => {
579
+ const locations = v?.locations;
580
+ return Array.isArray(locations) ? locations.map((l) => `${String(l?.file ?? "")}#${String(l?.symbol ?? "")}`).sort() : [];
581
+ };
582
+ return sameValue(where(a), where(b));
583
+ }
584
+ function editsAfterExtraction(e, staged, own) {
585
+ const edits = [], values = own?.values && typeof own.values == "object" ? own.values : void 0, byValue = (field) => {
586
+ if (!values || !Object.hasOwn(values, field))
587
+ return;
588
+ const now = recordFieldValue(e, field);
589
+ return sameRecordValue(field, values[field], now) ? void 0 : { field, extracted: values[field] ?? null, now: now ?? null };
590
+ };
591
+ if (e.change === "updated") {
592
+ if (!own)
593
+ return edits;
594
+ for (const [field, was] of Object.entries(e.updatedFrom ?? {}))
595
+ own.fields && !own.fields.includes(field) && edits.push({ field, extracted: was ?? null, now: recordFieldValue(e, field) ?? null });
596
+ for (const field of own.fields ?? []) {
597
+ const edit = byValue(field);
598
+ edit && edits.push(edit);
599
+ }
600
+ return edits;
601
+ }
602
+ if (e.change !== "added")
603
+ return edits;
604
+ const folded = new Set(own?.fields ?? []);
605
+ staged && edits.push(...editsSinceExtraction(e, staged).filter((d) => !folded.has(d.field) && !(own && !own.fields)));
606
+ for (const field of folded) {
607
+ const edit = byValue(field);
608
+ edit && edits.push(edit);
609
+ }
610
+ return edits;
611
+ }
612
+ function editsSinceExtraction(element, staged) {
613
+ const edits = [];
614
+ if (typeof staged.name != "string")
615
+ return edits;
616
+ staged.name !== element.name && edits.push({ field: "name", extracted: staged.name, now: element.name });
617
+ const stagedAttributes = staged.attributes && typeof staged.attributes == "object" ? staged.attributes : {};
618
+ {
619
+ const keys = /* @__PURE__ */ new Set([...Object.keys(stagedAttributes), ...Object.keys(element.attributes ?? {})]);
620
+ for (const key of [...keys].sort()) {
621
+ const was = stagedAttributes[key], now = element.attributes?.[key];
622
+ sameValue(was, now) || edits.push({ field: `attributes.${key}`, extracted: was ?? null, now: now ?? null });
623
+ }
624
+ }
625
+ return edits;
626
+ }
627
+ function namedAfterCard(element, card) {
628
+ if (card.proposed?.id && card.proposed.id === element.id)
629
+ return !0;
630
+ const route = routeKey(card.symbol) ?? (card.name ? routeKey(card.name) : void 0);
631
+ if (route && routeKey(element.name) === route)
632
+ return !0;
633
+ const keys = nameKeys(element), parts = card.symbol.split(/[.#/\s]+/).filter(Boolean), file = card.file ?? "";
634
+ return [card.symbol, card.name, card.proposed?.name, parts[parts.length - 1], file, file.replace(/\.[^./]+$/, "")].some((n) => typeof n == "string" && n.length > 0 && keys.has(normalizeName(n)));
635
+ }
636
+ function normalizeFile(file) {
637
+ return typeof file == "string" && file.length > 0 ? file.replace(/\\/g, "/").replace(/^\.\//, "") : null;
638
+ }
639
+ function asBuiltWarning(share, blindSpotsApplied = !0) {
640
+ const { total, agentAdded } = share.elements, edited = share.editedAfterExtraction?.length ?? 0;
641
+ if (total === 0 || (agentAdded + edited) * 2 <= total)
642
+ return;
643
+ const copied = share.copiedFromPlan?.length ?? 0;
644
+ return `${agentAdded + edited} of the ${total} as-built elements were not staged by extraction` + (edited > 0 ? ` or were edited after it (${edited})` : "") + (copied > 0 ? ` (${copied} share an id with the plan's adds)` : "") + ", so this comparison says little about the code." + (blindSpotsApplied ? " Extraction's blind spots were still applied." : share.record === "not-found" ? " No extraction record covers this changeset, so extraction's blind spots were not applied." : " Extraction's blind spots were not applied.") + " To verify the code, discard this changeset and stage the build with codebase_extract: from its cards, named after them (see extraction.asBuilt.agentAdded for why each one doesn't count).";
354
645
  }
355
646
  function normalizeName(name) {
356
647
  return name.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -386,7 +677,7 @@ function compareElements(planned, built, removedBy) {
386
677
  planned.type && !sameType(planned, built) && fields.push({ field: "type", planned: planned.type, built: built.type }), planned.name && normalizeName(planned.name) !== normalizeName(built.name) && fields.push({ field: "name", planned: planned.name, built: built.name }), planned.description !== void 0 && planned.description.trim() !== (built.description ?? "").trim() && fields.push({ field: "description", planned: planned.description, built: built.description });
387
678
  for (const [key, value] of Object.entries(planned.attributes ?? {})) {
388
679
  const builtValue = built.attributes?.[key];
389
- sameValue(value, builtValue) || fields.push({ field: `attributes.${key}`, planned: value, built: builtValue });
680
+ sameAttribute(key, value, builtValue) || fields.push({ field: `attributes.${key}`, planned: value, built: builtValue });
390
681
  }
391
682
  for (const field of removedBy ?? [])
392
683
  if (field === "description")
@@ -395,7 +686,7 @@ function compareElements(planned, built, removedBy) {
395
686
  const key = field.slice(11);
396
687
  !(planned.attributes !== void 0 && Object.hasOwn(planned.attributes, key)) && built.attributes?.[key] !== void 0 && fields.push({ field, planned: void 0, built: built.attributes[key] });
397
688
  }
398
- return fields;
689
+ return fields.map((f) => ({ field: f.field, planned: f.planned ?? null, built: f.built ?? null }));
399
690
  }
400
691
  function fieldValue(element, field) {
401
692
  if (field === "type")
@@ -408,7 +699,26 @@ function fieldValue(element, field) {
408
699
  return element.attributes?.[field.slice(11)];
409
700
  }
410
701
  function sameFieldValue(field, a, b) {
411
- return field === "type" || field === "name" ? normalizeName(String(a ?? "")) === normalizeName(String(b ?? "")) : field === "description" ? String(a ?? "").trim() === String(b ?? "").trim() : sameValue(a, b);
702
+ return field === "type" || field === "name" ? normalizeName(String(a ?? "")) === normalizeName(String(b ?? "")) : field === "description" ? String(a ?? "").trim() === String(b ?? "").trim() : field.startsWith("attributes.") ? sameAttribute(field.slice(11), a, b) : sameValue(a, b);
703
+ }
704
+ function normalizeHttpPath(p) {
705
+ return p.replace(/\{[^}]*\}|:[A-Za-z_]\w*/g, "{}").replace(/\/+$/, "") || "/";
706
+ }
707
+ function routeKey(text) {
708
+ const m = /^__route__([A-Z]+)__(\/\S*)$/.exec(text.trim()) ?? /^([A-Z]+)\s+(\/\S*)$/.exec(text.trim());
709
+ return m ? `${m[1]} ${normalizeHttpPath(m[2])}` : void 0;
710
+ }
711
+ function sameAttribute(key, a, b) {
712
+ if (typeof a == "string" && typeof b == "string") {
713
+ if (key === "http_path")
714
+ return normalizeHttpPath(a) === normalizeHttpPath(b);
715
+ if (key === "http_method")
716
+ return a.toUpperCase() === b.toUpperCase();
717
+ }
718
+ return sameValue(a, b);
719
+ }
720
+ function buildLeft(planned, built, difference) {
721
+ return difference.built === null ? !0 : planned.op !== "update" ? !1 : planned.baseValues && Object.hasOwn(planned.baseValues, difference.field) ? sameFieldValue(difference.field, fieldValue(built, difference.field), planned.baseValues[difference.field]) : built.change !== "updated" && built.change !== "added";
412
722
  }
413
723
  function buildTouched(planned, built, field) {
414
724
  return planned.baseValues && Object.hasOwn(planned.baseValues, field) ? !sameFieldValue(field, fieldValue(built, field), planned.baseValues[field]) : built.change === "updated" || built.change === "added";
@@ -475,7 +785,7 @@ function toVerifyElement(record, layerName, id) {
475
785
  };
476
786
  }
477
787
  function emptyCounts() {
478
- return { planned: 0, matched: 0, probable: 0, missing: 0, divergent: 0, unplanned: 0, deletionsNotDone: 0 };
788
+ return { planned: 0, matched: 0, probable: 0, missing: 0, divergent: 0, unplanned: 0, deletionsNotDone: 0, unverifiable: 0 };
479
789
  }
480
790
  function countCategories(planned, section) {
481
791
  return {
@@ -485,7 +795,8 @@ function countCategories(planned, section) {
485
795
  missing: section.missing.length,
486
796
  divergent: section.divergent.length,
487
797
  unplanned: section.unplanned.length,
488
- deletionsNotDone: section.deletionsNotDone.length
798
+ deletionsNotDone: section.deletionsNotDone.length,
799
+ unverifiable: section.unverifiable.length
489
800
  };
490
801
  }
491
802
  function decideVerdict(report) {
@@ -496,8 +807,24 @@ function decideVerdict(report) {
496
807
  n > 0 && parts.push(`${n} ${what}`);
497
808
  };
498
809
  say(e.missing, plural(e.missing, "planned element", "planned elements") + " missing"), say(r.missing, plural(r.missing, "planned relationship", "planned relationships") + " missing"), say(e.deletionsNotDone + r.deletionsNotDone, "planned deletion(s) not done"), say(e.divergent, plural(e.divergent, "element differs", "elements differ")), say(r.divergent, plural(r.divergent, "relationship differs", "relationships differ")), say(e.unplanned, plural(e.unplanned, "unplanned element change", "unplanned element changes")), say(r.unplanned, plural(r.unplanned, "unplanned relationship", "unplanned relationships"));
499
- const probable = e.probable + r.probable, probableText = probable > 0 ? ` ${probable} match(es) are probable, not exact; confirm them.` : "";
500
- return e.missing + r.missing + e.deletionsNotDone + r.deletionsNotDone > 0 ? { verdict: "incomplete", text: `The build is incomplete: ${parts.join(", ")}.${probableText}` } : e.divergent + r.divergent + e.unplanned + r.unplanned > 0 ? { verdict: "diverges", text: `The build diverges from the plan: ${parts.join(", ")}.${probableText}` } : probable > 0 ? { verdict: "probable-match", text: `Everything planned was found.${probableText}` } : { verdict: "match", text: "The build matches the plan." };
810
+ const probable = e.probable + r.probable, probableText = probable > 0 ? ` ${probable} match(es) are probable, not exact; confirm them.` : "", unverifiable = e.unverifiable + r.unverifiable, unverifiableText = unverifiable > 0 ? ` ${[
811
+ e.unverifiable > 0 ? `${e.unverifiable} planned ${plural(e.unverifiable, "element", "elements")}` : "",
812
+ r.unverifiable > 0 ? `${r.unverifiable} planned ${plural(r.unverifiable, "relationship", "relationships")}` : ""
813
+ ].filter(Boolean).join(" and ")} can't be seen by extraction (unverifiable, not counted): check them in the code or the review.` : "";
814
+ if (e.planned + r.planned === unverifiable && e.unplanned + r.unplanned === 0)
815
+ return {
816
+ verdict: "unverifiable",
817
+ text: `Nothing in the plan can be checked against extracted code.${unverifiableText}`
818
+ };
819
+ if (e.missing + r.missing + e.deletionsNotDone + r.deletionsNotDone > 0)
820
+ return { verdict: "incomplete", text: `The build is incomplete: ${parts.join(", ")}.${probableText}${unverifiableText}` };
821
+ if (e.divergent + r.divergent + e.unplanned + r.unplanned > 0)
822
+ return { verdict: "diverges", text: `The build diverges from the plan: ${parts.join(", ")}.${probableText}${unverifiableText}` };
823
+ const scope = unverifiable > 0 ? "Everything extraction can see was found." : "Everything planned was found.";
824
+ return probable > 0 ? { verdict: "probable-match", text: `${scope}${probableText}${unverifiableText}` } : {
825
+ verdict: "match",
826
+ text: unverifiable > 0 ? `The build matches what extraction can see of the plan.${unverifiableText}` : "The build matches the plan."
827
+ };
501
828
  }
502
829
  function plural(n, one, many) {
503
830
  return n === 1 ? one : many;
@@ -505,6 +832,8 @@ function plural(n, one, many) {
505
832
  export {
506
833
  LIFECYCLE_FIELDS,
507
834
  LIFECYCLE_STAGES,
835
+ asBuiltShare,
836
+ asBuiltWarning,
508
837
  builtFromBase,
509
838
  builtFromGraph,
510
839
  builtFromSnapshot,
@@ -512,6 +841,7 @@ export {
512
841
  normalizeName,
513
842
  planFromChangeset,
514
843
  planFromModel,
844
+ readExtractionRecord,
515
845
  verifyChangeset,
516
846
  verifyChangesetInModel
517
847
  };