@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
@@ -0,0 +1,836 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { strictItem } from "../strict-schema.js";
6
+ import { codeChangesSince, inDirs, scopeChanges } from "../../analyzers/code-changes.js";
7
+ import { SOURCE_EXT, VENDOR_EXCLUDE } from "../../analyzers/cards/build.js";
8
+ import { ExtractionTimeoutError } from "../../analyzers/cards/extract.js";
9
+ import { attributeChangedLines, decisionKey, needsAttributesCard, planElements, planRelationships, sortUndecided, sourceSymbolOf } from "../../analyzers/extraction-plan.js";
10
+ import { Changeset } from "../../core/changeset.js";
11
+ import { ChangesetGraph } from "../../core/changeset-graph.js";
12
+ import { Element } from "../../core/element.js";
13
+ import { Layer } from "../../core/layer.js";
14
+ import { setStagedSourceReference, stagedSourceReference } from "../../core/staged-source-reference.js";
15
+ import { StagingAreaManager } from "../../core/staging-area.js";
16
+ import { getAllLayerIds, isValidLayer } from "../../generated/layer-registry.js";
17
+ import { getValidRelationships } from "../../generated/relationship-index.js";
18
+ import { generateUUID } from "../../utils/id-generator.js";
19
+ import { CLIError, ErrorCategory, formatValidOptions } from "../../utils/errors.js";
20
+ import { FileLock } from "../../utils/file-lock.js";
21
+ import { SchemaValidator } from "../../validators/schema-validator.js";
22
+ import { compactJson, jsonResult, loadModel, reloadModel, rootPathSchema, runTool } from "./shared.js";
23
+ import { changesetUnstageHandler } from "./changeset-editing.js";
24
+ import { analyzerNotFoundError, canonicalRoots, defaultCodebaseToolDeps, describeIndex, requireFreshIndex } from "./codebase-shared.js";
25
+ const DEFAULT_PAGE_SIZE = 25, MAX_PAGE_SIZE = 50, RESPONSE_BUDGET_BYTES = 28 * 1024, MAX_LISTED = 100, inputSchema = {
26
+ changeset: z.string().min(1).optional().describe("Name (or id) of the as-built changeset to stage into. Created when absent, reused when it is staged. Default: extract-<short git HEAD>, or extract-<since base>..<HEAD> with since."),
27
+ since: z.string().min(1).optional().describe("A git ref (branch, tag or commit). Extract only the files changed since its merge base with HEAD, plus uncommitted ones. To verify a branch's build, pass the branch it started from."),
28
+ layers: z.array(z.string()).optional().describe("Only these layers (canonical names). Default: every layer."),
29
+ pageSize: z.number().int().min(1).max(200).optional().describe(`Undecided cards per page (default ${DEFAULT_PAGE_SIZE}, at most ${MAX_PAGE_SIZE}). A page that would make the response larger than about ${RESPONSE_BUDGET_BYTES / 1024} KB has its cards shortened, and says so.`),
30
+ page: z.number().int().min(1).optional().describe("Page of undecided cards (default 1). Page 2 and later read the last run's cards, with its since and layers: nothing is extracted or staged."),
31
+ reject: z.array(strictItem("A reject[] item", {
32
+ card: z.string().min(1).describe("The card's cardId, its symbol (or file#symbol), or the id of an element this extraction staged."),
33
+ reason: z.string().min(1).describe("Why it is not a model element, in a few words.")
34
+ })).optional().describe("Code that is not a model element: a card of this extraction (cardId, or a symbol only one card has), a symbol it staged (Tier 0 decided it), or the id of an element staged from it. Recorded for the project until cleared, so later runs leave them out; an element still staged in the changeset for it is unstaged, with its relationships. A call with only reject or clearRejected records and runs nothing."),
35
+ clearRejected: z.union([z.literal(!0), z.array(z.string().min(1))]).optional().describe("Forget not-an-element decisions: true for all, or a list of symbols (or file#symbol) as notElements lists them."),
36
+ rootPath: rootPathSchema
37
+ };
38
+ function stagedAs(name, attributes) {
39
+ return { ...name ? { name } : {}, ...attributes && Object.keys(attributes).length ? { attributes: structuredClone(attributes) } : {} };
40
+ }
41
+ const DESCRIPTIONS_FILE = "descriptions.json", DECISIONS_FILE = "decisions.json", PENDING_FILE = "pending.json", KEEP_FILES = /* @__PURE__ */ new Set([DESCRIPTIONS_FILE, DECISIONS_FILE, PENDING_FILE]);
42
+ function extractDir(projectRoot) {
43
+ return path.join(projectRoot, ".dr", "analyzers", "extract");
44
+ }
45
+ function cardsPath(projectRoot, changesetId) {
46
+ return path.join(extractDir(projectRoot), `${changesetId.replace(/[^\w.-]/g, "_")}.json`);
47
+ }
48
+ async function isExtractionChangeset(projectRoot, changesetId, changesetName) {
49
+ const stored = await readJson(cardsPath(projectRoot, changesetId));
50
+ if (stored && !stored.pending && stored.changesetId === changesetId)
51
+ return !0;
52
+ if (!changesetName)
53
+ return !1;
54
+ const pending = await readJson(path.join(extractDir(projectRoot), PENDING_FILE));
55
+ return !!pending?.pending && pending.changesetName === changesetName;
56
+ }
57
+ function hasTriple(id) {
58
+ const parts = id.split(".");
59
+ return parts.length !== 5 ? !1 : getValidRelationships(`${parts[0]}.${parts[1]}`, parts[2], `${parts[3]}.${parts[4]}`).length > 0;
60
+ }
61
+ function emptyChangeset() {
62
+ const now = (/* @__PURE__ */ new Date()).toISOString();
63
+ return new Changeset({ id: "extract-preview", name: "extract", created: now, modified: now, status: "staged", baseSnapshot: "", changes: [] });
64
+ }
65
+ function page(items, pageNumber, pageSize) {
66
+ const pages = Math.ceil(items.length / pageSize);
67
+ return {
68
+ total: items.length,
69
+ page: pageNumber,
70
+ pageSize,
71
+ pages,
72
+ items: items.slice((pageNumber - 1) * pageSize, pageNumber * pageSize)
73
+ };
74
+ }
75
+ function capped(items, limit = MAX_LISTED) {
76
+ return items.length > limit ? { total: items.length, items: items.slice(0, limit), truncated: !0 } : { total: items.length, items };
77
+ }
78
+ const GENERIC_REASON = /* @__PURE__ */ new Set(["no-rule", "narrowed"]);
79
+ function clip(text, max) {
80
+ return text && (text.length > max ? `${text.slice(0, max - 3)}...` : text);
81
+ }
82
+ function cardAt(card, detail) {
83
+ if (detail === "full")
84
+ return card;
85
+ const { reason, docstring, signature, decorators, candidates, qualifiedName, ...rest } = card, open = candidates.length ? { candidates } : {};
86
+ return detail === "shortened" ? {
87
+ ...rest,
88
+ qualifiedName,
89
+ ...open,
90
+ ...signature ? { signature: clip(signature, 80) } : {},
91
+ ...docstring ? { docstring: clip(docstring, 120) } : {},
92
+ ...decorators ? { decorators } : {},
93
+ ...GENERIC_REASON.has(card.kind) ? {} : { reason }
94
+ } : { ...rest, ...open, ...card.kind === "ambiguous" || card.kind === "needs-attributes" ? { reason: clip(reason, 200) } : {} };
95
+ }
96
+ const PAGE_NOTES = {
97
+ shortened: "Docstrings and signatures are shortened, and empty candidates and reasons that only name the card's kind left out, to keep this response small.",
98
+ compact: "Docstrings, signatures, decorators, qualified names, empty candidates and most reasons are left out, to keep this response small."
99
+ }, LIST_LIMITS = [MAX_LISTED, 20, 5, 0];
100
+ function cutInstruction(p, shown) {
101
+ const next = (p.page - 1) * p.pageSize + shown;
102
+ return `call codebase_extract with page: ${Math.floor(next / shown) + 1} and pageSize: ${shown}${next % shown ? " (its first cards may repeat ones you have)" : ""}`;
103
+ }
104
+ function fitCards(cards, p, render) {
105
+ const fits = (result2) => Buffer.byteLength(compactJson(result2), "utf-8") <= RESPONSE_BUDGET_BYTES;
106
+ let lists = {}, listsFit = !1;
107
+ for (const limit of LIST_LIMITS)
108
+ if (lists = limit < MAX_LISTED ? { listLimit: limit } : {}, fits(render([], lists))) {
109
+ listsFit = !0;
110
+ break;
111
+ }
112
+ const compact = cards.map((c) => cardAt(c, "compact")), smallest = () => render(compact.slice(0, 1), {
113
+ ...lists,
114
+ ...cards.length ? { detail: "compact", note: PAGE_NOTES.compact } : {},
115
+ ...cards.length > 1 ? { shown: 1, readOn: cutInstruction(p, 1) } : {},
116
+ overBudget: !0
117
+ });
118
+ if (!listsFit)
119
+ return smallest();
120
+ let result = render(cards, lists);
121
+ if (fits(result))
122
+ return result;
123
+ for (const detail of ["shortened", "compact"])
124
+ if (result = render(cards.map((c) => cardAt(c, detail)), { ...lists, detail, note: PAGE_NOTES[detail] }), fits(result))
125
+ return result;
126
+ for (let shown = compact.length - 1; shown >= 1; shown--)
127
+ if (result = render(compact.slice(0, shown), { ...lists, detail: "compact", note: PAGE_NOTES.compact, shown, readOn: cutInstruction(p, shown) }), fits(result))
128
+ return result;
129
+ return smallest();
130
+ }
131
+ function fitNotes(fit) {
132
+ return {
133
+ ...fit.listLimit !== void 0 ? {
134
+ listsShortened: `candidateDeletes, staged.updated, retyped and notElements.recorded list at most ${fit.listLimit} item(s) here, to keep the response small; each total is the full count. Narrow the run with layers or since to list the rest.`
135
+ } : {},
136
+ ...fit.overBudget ? { overBudget: `Even shortened, this response is over about ${RESPONSE_BUDGET_BYTES / 1024} KB.` } : {}
137
+ };
138
+ }
139
+ function undecidedSection(p, items, fit, requestedPageSize, extra = {}) {
140
+ return {
141
+ ...p,
142
+ items,
143
+ ...extra,
144
+ ...requestedPageSize > p.pageSize ? { pageSizeCapped: `pageSize ${requestedPageSize} was capped at ${p.pageSize}; use page: 2 and on for more.` } : {},
145
+ ...fit.detail ? { detail: fit.detail, detailNote: `${fit.note} A smaller pageSize gives full cards.` } : {},
146
+ ...fit.shown !== void 0 ? { cut: `This page was cut to its first ${fit.shown} of ${p.items.length} cards to keep the response small. For the rest, ${fit.readOn}.` } : {}
147
+ };
148
+ }
149
+ function elementOfAdd(add, now) {
150
+ const element = new Element({
151
+ id: generateUUID(),
152
+ path: add.id,
153
+ spec_node_id: `${add.layer}.${add.type}`,
154
+ layer_id: add.layer,
155
+ type: add.type,
156
+ name: add.name,
157
+ description: add.description,
158
+ attributes: add.attributes,
159
+ metadata: { created_at: now, updated_at: now, version: 1 },
160
+ layer: add.layer
161
+ });
162
+ return add.sourceReference && element.setSourceReference(add.sourceReference), element;
163
+ }
164
+ async function validateAdds(adds, project, now, hunks) {
165
+ const valid = [], invalid = [], validator = new SchemaValidator(), byLayer2 = /* @__PURE__ */ new Map();
166
+ for (const add of adds)
167
+ (byLayer2.get(add.layer) ?? byLayer2.set(add.layer, []).get(add.layer)).push({ add, element: elementOfAdd(add, now) });
168
+ for (const [layerName, items] of byLayer2) {
169
+ const layer = new Layer(layerName);
170
+ for (const item of items)
171
+ layer.addElement(item.element);
172
+ const result = await validator.validateLayer(layer), errors = /* @__PURE__ */ new Map();
173
+ for (const e of result.errors)
174
+ e.elementId && (errors.get(e.elementId) ?? errors.set(e.elementId, []).get(e.elementId)).push(e.message);
175
+ for (const item of items) {
176
+ const errs = errors.get(item.add.id);
177
+ errs?.length ? invalid.push(needsAttributesCard(item.add, project, errs, hunks)) : valid.push(item);
178
+ }
179
+ }
180
+ return { valid, invalid };
181
+ }
182
+ async function currentSnapshot(model, graph, changeset, id, layer) {
183
+ const element = graph.getElement(id);
184
+ if (element && element.status !== "unchanged") {
185
+ const changes = changeset.changes.filter((c) => (c.type === "add" || c.type === "update") && c.after && graph.resolve(c.elementId) === id), last = changes[changes.length - 1];
186
+ if (last?.after)
187
+ return { snapshot: structuredClone(last.after), stagedAdd: element.status === "added" ? changes.find((c) => c.type === "add") : void 0 };
188
+ }
189
+ const base = (await model.getBaseLayer(layer))?.getElement(id);
190
+ return base ? { snapshot: structuredClone(base.toJSON()), stagedAdd: void 0 } : null;
191
+ }
192
+ function patched(snapshot, update, now) {
193
+ const after = structuredClone(snapshot);
194
+ for (const [field, value] of Object.entries(update.fields))
195
+ field === "source_reference" ? setStagedSourceReference(after, value) : field.startsWith("attributes.") ? after.attributes = { ...after.attributes ?? {}, [field.slice(11)]: value } : after[field] = value;
196
+ const metadata = after.metadata;
197
+ return metadata && (after.metadata = { ...metadata, updated_at: now }), after;
198
+ }
199
+ function relationshipChange(r) {
200
+ return {
201
+ type: "relationship-add",
202
+ elementId: `${r.source}::${r.predicate}::${r.target}`,
203
+ layerName: r.sourceLayer,
204
+ after: {
205
+ source: r.source,
206
+ target: r.target,
207
+ predicate: r.predicate,
208
+ layer: r.sourceLayer,
209
+ ...r.targetLayer !== r.sourceLayer ? { targetLayer: r.targetLayer } : {},
210
+ category: "structural"
211
+ }
212
+ };
213
+ }
214
+ function byLayer(adds, updates, relationships) {
215
+ const out = {}, at = (layer) => out[layer] ??= { adds: 0, updates: 0, relationships: 0 };
216
+ for (const a of adds)
217
+ at(a.layer).adds++;
218
+ for (const u of updates)
219
+ at(u.layer).updates++;
220
+ for (const r of relationships)
221
+ at(r.sourceLayer).relationships++;
222
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
223
+ }
224
+ function notElementsResult(decisions, skipped, recorded, cleared, listLimit = MAX_LISTED) {
225
+ const total = Object.keys(decisions.notElements).length;
226
+ return !total && !skipped && !recorded.length && !cleared ? {} : {
227
+ notElements: {
228
+ total,
229
+ skipped,
230
+ ...recorded.length ? { recorded: capped(recorded, listLimit) } : {},
231
+ ...cleared ? { cleared } : {},
232
+ note: "Symbols decided not to be elements (rejected, or unstaged after this tool staged them). Runs leave them out; clearRejected forgets them."
233
+ }
234
+ };
235
+ }
236
+ function verifyCardStep(undecidedPages, readOn) {
237
+ return `Stage the element cards with model_add, named after the card's symbol (the card's name when it has one), never the plan's element, and never copy plan elements here: a layer and type from its candidates or hint, sourceFile = file, sourceSymbol = symbol, sourceProvenance: "extracted". Ask the user about cards you can't place, in one short list. ${readFurther(undecidedPages, readOn)}`;
238
+ }
239
+ function verifyLandStep(plan, why = "") {
240
+ return `After verify, the plan '${plan.name}' lands (changeset_commit when the user asks), not this changeset: discard this one or keep it as a record. Landing it first drifts the plan's base, so the plan would need force. Call changeset_activate with "${plan.id}" to resume the plan${why}.`;
241
+ }
242
+ function verifyNothingSteps(plan, since) {
243
+ return [
244
+ `Nothing in the range since ${since} is observable to extraction or differs from the model, so no as-built changeset was created: there is none to verify against. '${plan.name}' is still active.`,
245
+ `Call changeset_verify with planned: "${plan.id}" and against: {base: true, extracted: true}: it compares the plan with the model, and what extraction can't see is unverifiable rather than missing.`
246
+ ];
247
+ }
248
+ function readFurther(undecidedPages, readOn) {
249
+ return readOn ? `This page was cut to keep the response small: for the cards after it, ${readOn}, and page on with that pageSize.` : `Read further pages with page: 2 and on (${undecidedPages} in all).`;
250
+ }
251
+ function nextSteps(changeset, previous, undecidedPages, plan, changedCards = 0, since, readOn) {
252
+ const steps = [
253
+ `The as-built changeset '${changeset.name}' is active: model_add, model_update, model_delete and relationship_add stage into it. Nothing reaches the base model until the user lands it.`
254
+ ];
255
+ changedCards > 0 && steps.push(`${changedCards} card(s) have changedInRange: their lines are new or changed in the range, so they come first. The rest are symbols in the same files that the range didn't touch.`);
256
+ const verify = !!since && !!previous, rerun = `codebase_extract again with changeset: "${changeset.name}"${since ? `, since: "${since}"` : ""}`;
257
+ return undecidedPages > 0 && steps.push(verify ? verifyCardStep(undecidedPages, readOn) : `Decide the undecided cards: for each one that is a model element, call model_add with a layer and type from its candidates (or its hint; spec_type says what a type needs), a name (the card's name when it has one), sourceFile = file, sourceSymbol = symbol and sourceProvenance: "extracted". ` + readFurther(undecidedPages, readOn), "For cards that are not elements, call codebase_extract with reject: [{card: cardId, reason}] so later runs leave them out.", verify ? `Call ${rerun} for their relationships.` : `Then call ${rerun} (it keeps that run's since and layers). It finds your elements by their source reference and stages the relationships the edge mapping gives them.`), plan.candidateDeletes.length && steps.push("Check candidateDeletes: call model_delete only for elements whose code is really gone (replacedBy names the element staged for a renamed file). reference-never-matched means the element's reference was wrong before the range: the range removed nothing, so fix or delete the element without blaming the range."), plan.retyped.length && steps.push("retyped lists elements whose type the rules now decide differently. Leave them unless the rule is right; changing a type means adding the new element and deleting the old."), verify ? (steps.push(`Then call changeset_verify with planned: "${previous.id}" and against: {changesetId: "${changeset.id}"}.`, verifyLandStep(previous)), steps) : (steps.push("Call changeset_brief to see what the changeset covers and its gaps, then review_open and give the user the link."), previous ? steps.push(`'${previous.name}' was active before this call. To check it against this build, call changeset_verify with planned: "${previous.id}" and against: {changesetId: "${changeset.id}"}. Call changeset_activate with "${previous.id}" to resume it.`) : steps.push("The user lands the changeset with changeset_commit (they confirm in a dialog) or in the viewer."), steps);
258
+ }
259
+ function pendingSteps(name, active, undecidedPages, plan, changedCards, since, readOn) {
260
+ const verify = !!since && !!active, steps = [
261
+ verify ? `Nothing was staged, so no as-built changeset was created and '${active.name}' is still active. Before you stage a card or delete a candidate, call changeset_create with name: "${name}" (it becomes active, so model_add and model_delete stage into it and not into the plan).` : `Nothing was staged, so no changeset was created or activated. Before you model a card or delete a candidate, call changeset_create with name: "${name}" (it becomes active, and model_add and model_delete stage into it); without it they would ${active ? `stage into '${active.name}'` : "write the base model"}.`
262
+ ];
263
+ return changedCards > 0 && steps.push(`${changedCards} card(s) have changedInRange: their lines are new or changed in the range, so they come first. The rest are symbols in the same files that the range didn't touch.`), undecidedPages > 0 && steps.push(verify ? verifyCardStep(undecidedPages, readOn) : `For each card that is a model element, call model_add (into that changeset) with a layer and type from its candidates (or its hint), a name (the card's name when it has one), sourceFile = file, sourceSymbol = symbol and sourceProvenance: "extracted". ${readFurther(undecidedPages, readOn)}`, "For cards that are not elements, call codebase_extract with reject: [{card: cardId, reason}] so later runs leave them out; that needs no changeset.", verify ? `Call codebase_extract again with changeset: "${name}", since: "${since}" for their relationships.` : `After adding elements, call codebase_extract with changeset: "${name}"${since ? `, since: "${since}"` : ""}: it keeps this run's since and layers, and stages the relationships the edge mapping gives them.`), plan.candidateDeletes.length && steps.push("Check candidateDeletes: call model_delete (into that changeset) only for elements whose code is really gone. reference-never-matched means the element's reference was wrong before the range: the range removed nothing."), verify ? (steps.push(`Then call changeset_verify with planned: "${active.id}" and against: {changesetId: "${name}"}.`, verifyLandStep(active, " (changeset_create deactivated it)"), `If no card is an element and nothing is deleted, there is no as-built changeset to verify against: call changeset_verify with planned: "${active.id}" and against: {base: true, extracted: true} instead.`), steps) : (steps.push("If none of these need modelling now, there is nothing to land and nothing to clean up."), steps);
264
+ }
265
+ const DEFAULT_BUDGET_MS = 1e5, NARROW = "Narrow the run with layers, or with since (a git ref), and call codebase_extract again";
266
+ function sameLayers(a, b) {
267
+ return JSON.stringify([...a ?? []].sort()) === JSON.stringify([...b ?? []].sort());
268
+ }
269
+ async function readJson(file) {
270
+ try {
271
+ return JSON.parse(await readFile(file, "utf-8"));
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+ async function pruneCardFiles(projectRoot, manager) {
277
+ const dir = extractDir(projectRoot);
278
+ let entries;
279
+ try {
280
+ entries = await readdir(dir);
281
+ } catch {
282
+ return;
283
+ }
284
+ const staged = new Set((await manager.list()).filter((c) => c.status === "staged").map((c) => c.id));
285
+ for (const entry of entries) {
286
+ if (!entry.endsWith(".json") || KEEP_FILES.has(entry))
287
+ continue;
288
+ const stored = await readJson(path.join(dir, entry));
289
+ (!stored || !staged.has(stored.changesetId)) && await rm(path.join(dir, entry), { force: !0 });
290
+ }
291
+ }
292
+ async function runsAt(projectRoot, manager, head) {
293
+ let entries;
294
+ try {
295
+ entries = await readdir(extractDir(projectRoot));
296
+ } catch {
297
+ return [];
298
+ }
299
+ const out = [];
300
+ for (const entry of entries) {
301
+ if (!entry.endsWith(".json") || KEEP_FILES.has(entry))
302
+ continue;
303
+ const stored = await readJson(path.join(extractDir(projectRoot), entry));
304
+ if (!stored || stored.pending || stored.gitHead !== head || !stored.changesetId)
305
+ continue;
306
+ const changeset = await manager.load(stored.changesetId);
307
+ changeset?.status === "staged" && out.push(changeset);
308
+ }
309
+ return out;
310
+ }
311
+ async function readDecisions(projectRoot) {
312
+ const stored = await readJson(path.join(extractDir(projectRoot), DECISIONS_FILE));
313
+ return { version: 1, notElements: stored?.notElements && typeof stored.notElements == "object" ? stored.notElements : {} };
314
+ }
315
+ async function writeDecisions(projectRoot, decisions) {
316
+ await mkdir(extractDir(projectRoot), { recursive: !0 }), await writeFile(path.join(extractDir(projectRoot), DECISIONS_FILE), JSON.stringify(decisions, null, 2), "utf-8");
317
+ }
318
+ function normFile(file) {
319
+ return file ? file.replace(/\\/g, "/").replace(/^\.\//, "") : null;
320
+ }
321
+ function changesetRejectables(changeset, graph) {
322
+ const out = [];
323
+ for (const e of graph?.elements.values() ?? [])
324
+ if (!(e.status !== "added" || e.sourceReference?.provenance !== "extracted"))
325
+ for (const l of e.sourceReference.locations ?? [])
326
+ l.symbol && out.push({ file: normFile(l.file), symbol: l.symbol, elementId: e.id });
327
+ for (const t of changeset?.unstaged ?? []) {
328
+ if (t.rewritten || t.change.type !== "add")
329
+ continue;
330
+ const ref = stagedSourceReference(t.change.after);
331
+ if (ref?.provenance === "extracted")
332
+ for (const l of ref.locations ?? [])
333
+ l.symbol && out.push({ file: normFile(l.file), symbol: l.symbol, elementId: t.change.elementId });
334
+ }
335
+ return out;
336
+ }
337
+ function cardsToReject(stored, fromChangeset, changesetName, refs) {
338
+ const known = [
339
+ ...(stored?.cards ?? []).map((c) => ({ file: c.file, symbol: c.symbol, label: c.label, cardId: c.cardId, qualifiedName: c.qualifiedName })),
340
+ ...(stored?.earlierCards ?? []).map((c) => ({ file: c.file, symbol: c.symbol, label: c.label, cardId: c.cardId, qualifiedName: c.qualifiedName })),
341
+ ...(stored?.staged ?? []).map((s) => ({ file: s.file, symbol: s.symbol, label: s.label, elementId: s.id })),
342
+ ...fromChangeset
343
+ ];
344
+ if (!known.length)
345
+ throw new CLIError(`No cards recorded for '${changesetName}' to reject from`, ErrorCategory.USER, [
346
+ "Call codebase_extract first, then pass the cardId (or symbol) of each card that is not an element"
347
+ ]);
348
+ const found = [], unknown = [], ambiguous = [], distinctSymbols = (matches) => {
349
+ const bySymbol = /* @__PURE__ */ new Map();
350
+ for (const m of matches) {
351
+ const key = decisionKey(m.file, m.symbol), seen = bySymbol.get(key);
352
+ (!seen || !seen.label && m.label) && bySymbol.set(key, m);
353
+ }
354
+ return [...bySymbol.values()];
355
+ };
356
+ for (const ref of refs) {
357
+ const ofElement = known.filter((k) => k.elementId === ref.card);
358
+ if (ofElement.length) {
359
+ for (const d of distinctSymbols(ofElement))
360
+ found.push({ file: d.file, symbol: d.symbol, label: d.label, reason: ref.reason });
361
+ continue;
362
+ }
363
+ const byCard = known.filter((k) => k.cardId === ref.card), matches = byCard.length ? byCard : known.filter((k) => k.symbol === ref.card || `${k.file ?? ""}#${k.symbol}` === ref.card || k.qualifiedName === ref.card), distinct = distinctSymbols(matches);
364
+ distinct.length === 1 ? found.push({ file: distinct[0].file, symbol: distinct[0].symbol, label: distinct[0].label, reason: ref.reason }) : distinct.length > 1 ? ambiguous.push(`${ref.card} (${distinct.map((d) => d.cardId ?? `${d.file ?? ""}#${d.symbol}`).join(", ")})`) : unknown.push(ref.card);
365
+ }
366
+ if (unknown.length || ambiguous.length) {
367
+ const parts = [
368
+ ...unknown.length ? [`not among the cards or staged symbols of '${changesetName}': ${unknown.join(", ")}`] : [],
369
+ ...ambiguous.length ? [`names more than one card: ${ambiguous.join("; ")}`] : []
370
+ ];
371
+ throw new CLIError(`Can't reject: ${parts.join("; ")}. Nothing was recorded`, ErrorCategory.USER, [
372
+ "Pass the cardId (or file#symbol) of a card, or the id of an element this extraction staged"
373
+ ]);
374
+ }
375
+ return found;
376
+ }
377
+ function clearDecisions(decisions, which) {
378
+ let n = 0;
379
+ for (const [key, d] of Object.entries(decisions.notElements))
380
+ (which === !0 || which.some((w) => w === key || w === d.symbol || w === `${d.file ?? ""}#${d.symbol}`)) && (delete decisions.notElements[key], n++);
381
+ return n;
382
+ }
383
+ function createCodebaseExtractHandler(deps = defaultCodebaseToolDeps) {
384
+ return async function(args, context) {
385
+ return runTool(async () => {
386
+ const startedAt = Date.now(), deadline = startedAt + (deps.budgetMs ?? DEFAULT_BUDGET_MS), model = await loadModel(args.rootPath), { projectRoot, codebaseRoot } = await canonicalRoots(model), argLayers = args.layers?.length ? [...new Set(args.layers)] : void 0, unknownLayers = (argLayers ?? []).filter((l) => !isValidLayer(l));
387
+ if (unknownLayers.length)
388
+ throw new CLIError(`Unknown layer${unknownLayers.length > 1 ? "s" : ""}: ${unknownLayers.join(", ")}`, ErrorCategory.USER, [
389
+ `Use canonical layer names: ${formatValidOptions(getAllLayerIds())}`
390
+ ]);
391
+ const { analyzer, freshness, index: indexJudgement } = await requireFreshIndex(projectRoot, codebaseRoot, deps, "codebase_extract", {
392
+ uncommittedEdits: !0
393
+ }), head = freshness.currentGitHead, manager = new StagingAreaManager(model.rootPath, model), pageSize = args.pageSize ?? DEFAULT_PAGE_SIZE, effectivePageSize = Math.min(pageSize, MAX_PAGE_SIZE), baseModified = model.manifest.modified, paging = (args.page ?? 1) > 1, decisionsOnly = !paging && (!!args.reject?.length || args.clearRejected !== void 0);
394
+ let name = args.changeset ?? "", changeset = args.changeset ? await manager.load(args.changeset) : null;
395
+ if (!args.changeset && (paging || decisionsOnly)) {
396
+ const activeId = await manager.getActiveId(), active = activeId ? await manager.load(activeId) : null;
397
+ if (active?.status === "staged" && existsSync(cardsPath(projectRoot, active.id)))
398
+ changeset = active, name = active.name;
399
+ else {
400
+ const runs = await runsAt(projectRoot, manager, head);
401
+ if (runs.length === 1)
402
+ changeset = runs[0], name = runs[0].name;
403
+ else if (runs.length > 1)
404
+ throw new CLIError(`Several extraction changesets have runs at this commit (${runs.map((r) => `'${r.name}'`).sort().join(", ")}), and none is active`, ErrorCategory.USER, ["Pass changeset with the name of the one you mean"]);
405
+ }
406
+ }
407
+ const pendingRecord = await readJson(path.join(extractDir(projectRoot), PENDING_FILE)), pending = pendingRecord?.pending && pendingRecord.gitHead === head ? pendingRecord : null, stalePending = !!pendingRecord?.pending && pendingRecord.gitHead !== head, pendingFor = (n) => pending && n && pending.changesetName === n ? pending : null;
408
+ let stored = changeset ? await readJson(cardsPath(projectRoot, changeset.id)) : null;
409
+ if (stored ??= pendingFor(changeset?.name ?? args.changeset), !args.changeset && (paging || decisionsOnly) && pending && (!changeset || !stored || stored.createdAt < pending.createdAt)) {
410
+ const named = await manager.load(pending.changesetName);
411
+ changeset = named?.status === "staged" ? named : null, name = pending.changesetName, stored = pending;
412
+ }
413
+ let since = args.since, layers = argLayers;
414
+ if (stored && (changeset ? changeset.status === "staged" : stored.pending) && !decisionsOnly) {
415
+ const conflicts = [];
416
+ if (args.since !== void 0 && args.since !== (stored.since ?? void 0) && conflicts.push(`since: ${args.since} (its run had since: ${stored.since ?? "none"})`), argLayers && !sameLayers(stored.layers, argLayers) && conflicts.push(`layers: ${argLayers.join(", ")} (its run had layers: ${stored.layers?.join(", ") ?? "all"})`), conflicts.length)
417
+ throw new CLIError(`'${stored.changesetName}' holds another run's extraction: this call names ${conflicts.join(" and ")}`, ErrorCategory.USER, [
418
+ "Leave since and layers out to use that run's",
419
+ "Or pass another changeset name for a run with these arguments"
420
+ ]);
421
+ since = stored.since ?? void 0, layers = stored.layers ?? void 0;
422
+ }
423
+ let changes = null;
424
+ if (since && (!(paging || decisionsOnly) || !changeset && !stored) && (changes = await (deps.codeChanges ?? codeChangesSince)(codebaseRoot, since)), !changeset && !args.changeset && !stored?.pending) {
425
+ const defaultName = changes ? changes.base === head ? `extract-${head.slice(0, 7)}+worktree` : `extract-${changes.base.slice(0, 7)}..${head.slice(0, 7)}` : `extract-${head.slice(0, 7)}`;
426
+ name = defaultName, changeset = await manager.load(name);
427
+ for (let n = 2; changeset && changeset.status !== "staged"; n++)
428
+ name = `${defaultName}-${n}`, changeset = await manager.load(name);
429
+ stored = (changeset ? await readJson(cardsPath(projectRoot, changeset.id)) : null) ?? pendingFor(name);
430
+ }
431
+ if (changeset && changeset.status !== "staged")
432
+ throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status} and can't take the extraction`, ErrorCategory.USER, [
433
+ "Pass another name in changeset"
434
+ ]);
435
+ const backend = paging || decisionsOnly ? null : await deps.getAnalyzer(analyzer);
436
+ if (!paging && !decisionsOnly) {
437
+ if (!backend)
438
+ throw analyzerNotFoundError(analyzer);
439
+ if (!backend.extract)
440
+ throw new CLIError(`The ${analyzer} analyzer can't extract (it has no Tier-0 rules)`, ErrorCategory.USER);
441
+ }
442
+ await mkdir(extractDir(projectRoot), { recursive: !0 });
443
+ const lock = new FileLock(path.join(extractDir(projectRoot), "run"));
444
+ try {
445
+ await lock.acquire({ timeout: 2e3, staleLockThreshold: (deps.budgetMs ?? DEFAULT_BUDGET_MS) + 6e4 });
446
+ } catch {
447
+ throw new CLIError("Another codebase_extract run is in progress for this project", ErrorCategory.USER, [
448
+ "Wait for it to finish, then call codebase_extract again"
449
+ ]);
450
+ }
451
+ try {
452
+ if (stalePending) {
453
+ const again = await readJson(path.join(extractDir(projectRoot), PENDING_FILE));
454
+ again?.pending && again.gitHead !== head && await rm(path.join(extractDir(projectRoot), PENDING_FILE), { force: !0 });
455
+ }
456
+ const decisions = await readDecisions(projectRoot), stagedIds = new Set((await manager.list()).filter((c) => c.status === "staged").map((c) => c.id));
457
+ let pruned = !1;
458
+ for (const [key, d] of Object.entries(decisions.notElements))
459
+ d.how === "unstaged" && !(d.changesetId && stagedIds.has(d.changesetId)) && (delete decisions.notElements[key], pruned = !0);
460
+ const recordedNow = [];
461
+ let cleared = 0;
462
+ args.clearRejected && (cleared = clearDecisions(decisions, args.clearRejected));
463
+ const toUnstage = /* @__PURE__ */ new Map(), inBase = [], keptElements = [];
464
+ if (args.reject?.length) {
465
+ const now2 = (/* @__PURE__ */ new Date()).toISOString(), extractionChangeset = changeset && stored ? changeset : null, graph = extractionChangeset ? await ChangesetGraph.build(model, extractionChangeset) : null, rejected = cardsToReject(stored, changesetRejectables(extractionChangeset, graph), changeset?.name ?? stored?.changesetName ?? name, args.reject), rejectedKeys = /* @__PURE__ */ new Map();
466
+ for (const card of rejected) {
467
+ const key = decisionKey(card.file, card.symbol);
468
+ rejectedKeys.set(key, card.symbol), decisions.notElements[key] = {
469
+ file: card.file,
470
+ symbol: card.symbol,
471
+ ...card.label ? { label: card.label } : {},
472
+ reason: card.reason,
473
+ how: "rejected",
474
+ changeset: changeset?.name ?? stored?.changesetName ?? name,
475
+ ...changeset ? { changesetId: changeset.id } : {},
476
+ at: now2
477
+ }, recordedNow.push({ symbol: card.symbol, file: card.file, how: "rejected" });
478
+ }
479
+ const stagedByExtraction = /* @__PURE__ */ new Map();
480
+ for (const st of stored?.staged ?? []) {
481
+ const key = decisionKey(st.file, st.symbol);
482
+ rejectedKeys.has(key) && stagedByExtraction.set(st.id, key);
483
+ }
484
+ for (const e of graph?.elements.values() ?? []) {
485
+ if (e.status === "deleted")
486
+ continue;
487
+ const keys = new Set((e.sourceReference?.locations ?? []).filter((l) => l.symbol).map((l) => decisionKey(normFile(l.file), l.symbol))), hit = stagedByExtraction.get(e.id) ?? [...keys].find((k) => rejectedKeys.has(k));
488
+ if (!hit || !stagedByExtraction.has(e.id) && e.sourceReference?.provenance !== "extracted")
489
+ continue;
490
+ if (e.status !== "added" || e.alreadyInBase) {
491
+ inBase.push(e.id);
492
+ continue;
493
+ }
494
+ const others = [...keys].filter((k) => !rejectedKeys.has(k));
495
+ others.length ? keptElements.push({ id: e.id, alsoNames: others }) : toUnstage.set(e.id, rejectedKeys.get(hit));
496
+ }
497
+ }
498
+ (pruned || cleared || recordedNow.length) && await writeDecisions(projectRoot, decisions);
499
+ const notElementKeys = new Set(Object.keys(decisions.notElements)), unstaged = [], unstageFailed = [];
500
+ for (const [id, symbol] of toUnstage) {
501
+ const result = await changesetUnstageHandler({ elementId: id, changesetId: changeset.id, rootPath: args.rootPath }), body = JSON.parse(result.content[0].text);
502
+ result.isError ? unstageFailed.push({ id, error: body.error ?? body.message ?? "unstage failed" }) : body.unstaged?.length && unstaged.push({ id, symbol, changes: body.unstaged.length });
503
+ }
504
+ if (unstaged.length && stored && changeset) {
505
+ const gone = new Set(unstaged.map((u) => u.id)), { pending: wasPending, ...rest } = stored, updated = (stored.updated ?? []).filter((u) => !gone.has(u.id));
506
+ stored = {
507
+ ...rest,
508
+ changesetId: changeset.id,
509
+ changesetName: changeset.name,
510
+ staged: (stored.staged ?? []).filter((s) => !gone.has(s.id)),
511
+ ...updated.length ? { updated } : { updated: void 0 }
512
+ }, await writeFile(cardsPath(projectRoot, changeset.id), JSON.stringify(stored), "utf-8"), wasPending && await rm(path.join(extractDir(projectRoot), PENDING_FILE), { force: !0 });
513
+ }
514
+ unstaged.length && await reloadModel(args.rootPath);
515
+ const rejectResult = {
516
+ ...unstaged.length ? {
517
+ unstaged: {
518
+ items: unstaged,
519
+ note: `Rejected while staged: unstaged from '${changeset.name}', with the relationships staged for them (changes counts both).`
520
+ }
521
+ } : {},
522
+ ...unstageFailed.length ? { unstageFailed } : {},
523
+ ...inBase.length ? { inModel: { ids: [...new Set(inBase)], note: "Already in the base model: rejecting doesn't remove them. model_delete them if they should go." } } : {},
524
+ ...keptElements.length ? {
525
+ kept: {
526
+ items: keptElements,
527
+ note: "Still staged: each also names code that wasn't rejected (alsoNames). Unstage it with changeset_unstage, or drop the rejected location with model_update, if it should change."
528
+ }
529
+ } : {}
530
+ };
531
+ if (decisionsOnly) {
532
+ const rerunName = changeset?.name ?? stored?.changesetName, rerun = rerunName ? `changeset: "${rerunName}"` : "the arguments of the last run";
533
+ return jsonResult({
534
+ changeset: changeset ? { id: changeset.id, name: changeset.name } : null,
535
+ ...notElementsResult(decisions, 0, recordedNow, cleared),
536
+ ...rejectResult,
537
+ message: unstaged.length ? `Recorded, and unstaged ${unstaged.length} element(s) it had staged; nothing was extracted.` : "Recorded; nothing was extracted or staged.",
538
+ next: [`Call codebase_extract with ${rerun} to refresh the cards: it leaves out what is recorded, and says how many.`]
539
+ }, { compact: !0 });
540
+ }
541
+ if (paging) {
542
+ if (!stored)
543
+ throw new CLIError(changeset ? `No cards recorded for '${changeset.name}'` : `No extraction changeset '${name}' yet`, ErrorCategory.USER, ["Call codebase_extract without page first"]);
544
+ const runName = changeset?.name ?? stored.changesetName;
545
+ if (stored.gitHead !== head)
546
+ throw new CLIError(`The cards for '${runName}' were read at ${stored.gitHead.slice(0, 7)}; the code is now at ${head.slice(0, 7)}`, ErrorCategory.USER, [
547
+ "Call codebase_extract without page to extract again"
548
+ ]);
549
+ const graph = await ChangesetGraph.build(model, changeset ?? emptyChangeset()), modelledAt = /* @__PURE__ */ new Map();
550
+ for (const e of graph.elements.values())
551
+ if (e.status !== "deleted")
552
+ for (const l of e.sourceReference?.locations ?? [])
553
+ l.symbol && modelledAt.set(`${l.file}#${l.symbol}`, e.id);
554
+ const cards2 = stored.cards.filter((c) => !notElementKeys.has(decisionKey(c.file, c.symbol))).map((c) => {
555
+ const modelled = c.file ? modelledAt.get(`${c.file}#${c.symbol}`) : void 0;
556
+ return modelled ? { ...c, modelled } : c;
557
+ }), p = page(cards2, args.page, effectivePageSize);
558
+ return jsonResult(fitCards(p.items, p, (items, fit) => ({
559
+ changeset: changeset ? { id: changeset.id, name: changeset.name } : null,
560
+ ...changeset ? {} : { pending: `No changeset yet: the run that read these cards staged nothing. Call changeset_create with name: "${runName}" before staging.` },
561
+ ...fitNotes(fit),
562
+ scope: { since: stored.since, layers: stored.layers ?? "all" },
563
+ undecided: undecidedSection(p, items, fit, pageSize),
564
+ ...notElementsResult(decisions, stored.cards.length - cards2.length, recordedNow, cleared, fit.listLimit),
565
+ ...rejectResult,
566
+ ...stored.baseModified !== baseModified ? { warning: "The base model changed since these cards were read; call codebase_extract without page to refresh them." } : {},
567
+ ...fit.readOn ? { next: `This page was cut to keep the response small: for the cards after it, ${fit.readOn}.` } : args.page < p.pages ? { next: `Call codebase_extract with page: ${args.page + 1} for the next cards.` } : {}
568
+ })), { compact: !0 });
569
+ }
570
+ const edited = indexJudgement.editedSince, before = await ChangesetGraph.build(model, changeset ?? emptyChangeset()), referenced = /* @__PURE__ */ new Set(), liveKeys = /* @__PURE__ */ new Set(), liveIds = /* @__PURE__ */ new Set(), addedAt = /* @__PURE__ */ new Map();
571
+ for (const e of before.elements.values())
572
+ if (e.status !== "deleted") {
573
+ liveIds.add(e.id);
574
+ for (const l of e.sourceReference?.locations ?? []) {
575
+ const file = l.file?.replace(/\\/g, "/").replace(/^\.\//, "");
576
+ if (file && referenced.add(file), !l.symbol)
577
+ continue;
578
+ const key = decisionKey(file || null, l.symbol);
579
+ liveKeys.add(key), e.status === "added" && !e.alreadyInBase && !addedAt.has(key) && addedAt.set(key, e.id);
580
+ }
581
+ }
582
+ const storedBefore = stored, unstagedAdds = new Set((changeset?.unstaged ?? []).filter((t) => !t.rewritten && t.change.type === "add").map((t) => t.change.elementId)), stillStaged = [];
583
+ for (const st of storedBefore?.staged ?? []) {
584
+ const key = decisionKey(st.file, st.symbol);
585
+ liveIds.has(st.id) || liveKeys.has(key) ? stillStaged.push(st) : unstagedAdds.has(st.id) && !decisions.notElements[key] && (decisions.notElements[key] = {
586
+ file: st.file,
587
+ symbol: st.symbol,
588
+ label: st.label,
589
+ reason: `${st.id} was staged by codebase_extract and then unstaged`,
590
+ how: "unstaged",
591
+ changeset: changeset.name,
592
+ changesetId: changeset.id,
593
+ at: (/* @__PURE__ */ new Date()).toISOString()
594
+ }, notElementKeys.add(key), recordedNow.push({ symbol: st.symbol, file: st.file, how: "unstaged" }));
595
+ }
596
+ const recorded = new Set(stillStaged.map((st) => `${st.id} ${decisionKey(st.file, st.symbol)}`));
597
+ for (const c of [...storedBefore?.cards ?? [], ...storedBefore?.earlierCards ?? []]) {
598
+ const key = decisionKey(c.file, c.symbol), id = addedAt.get(key);
599
+ if (!id || recorded.has(`${id} ${key}`))
600
+ continue;
601
+ recorded.add(`${id} ${key}`);
602
+ const element = before.getElement(id);
603
+ stillStaged.push({ id, file: c.file, symbol: c.symbol, label: c.label, ...stagedAs(element?.name, element?.attributes) });
604
+ }
605
+ if (recordedNow.some((r) => r.how === "unstaged") && await writeDecisions(projectRoot, decisions), changes) {
606
+ const drDirs = [path.join(projectRoot, "documentation-robotics"), path.join(projectRoot, ".dr")].map((d) => path.relative(codebaseRoot, d).split(path.sep).join("/")).filter((d) => d && !d.startsWith(".."));
607
+ changes = scopeChanges(changes, (f) => !inDirs(f, drDirs) && (SOURCE_EXT.test(f) && !VENDOR_EXCLUDE.test(f) || referenced.has(f)));
608
+ }
609
+ const checkFiles = /* @__PURE__ */ new Set();
610
+ for (const e of before.elements.values())
611
+ if (!(e.status === "deleted" || layers && !layers.includes(e.layer)))
612
+ for (const l of e.sourceReference?.locations ?? []) {
613
+ const file = l.file?.replace(/\\/g, "/").replace(/^\.\//, "");
614
+ file && l.symbol && (!changes || changes.changed.has(file)) && checkFiles.add(file);
615
+ }
616
+ let extraction;
617
+ try {
618
+ extraction = await backend.extract(projectRoot, {
619
+ codebaseRoot,
620
+ ...changes ? { files: changes.changed } : {},
621
+ checkFiles: [...checkFiles],
622
+ deadline
623
+ });
624
+ } catch (error) {
625
+ throw error instanceof ExtractionTimeoutError ? new CLIError(`codebase_extract ran past its ${Math.round((deadline - startedAt) / 1e3)} s budget ${error.stage}; nothing was staged`, ErrorCategory.USER, [NARROW]) : error;
626
+ }
627
+ const descriptionsFile = path.join(extractDir(projectRoot), DESCRIPTIONS_FILE), extractedDescriptions = await readJson(descriptionsFile) ?? {}, plan = planElements(extraction, before, {
628
+ layers,
629
+ changes,
630
+ extractedDescriptions,
631
+ notElements: notElementKeys,
632
+ fileExists: (file) => existsSync(path.join(codebaseRoot, file)),
633
+ fileText: (file) => {
634
+ try {
635
+ return readFileSync(path.join(codebaseRoot, file), "utf-8");
636
+ } catch {
637
+ return null;
638
+ }
639
+ }
640
+ }), now = (/* @__PURE__ */ new Date()).toISOString(), { valid, invalid } = await validateAdds(plan.adds, extraction.project, now, changes?.hunks), adds = valid.map((v) => v.add), undecided = sortUndecided(attributeChangedLines([...plan.undecided, ...invalid], changes?.hunks)), kept = new Set(adds);
641
+ for (const add of plan.adds)
642
+ kept.has(add) || (plan.elementOf.delete(add.symbol.qualifiedName), delete plan.descriptions[add.id]);
643
+ const elements = /* @__PURE__ */ new Map();
644
+ for (const e of before.elements.values())
645
+ e.status !== "deleted" && elements.set(e.id, { layer: e.layer, type: e.type });
646
+ for (const a of adds)
647
+ elements.set(a.id, { layer: a.layer, type: a.type });
648
+ const { relationships, alreadyPresent } = planRelationships(extraction, before, plan.elementOf, elements, hasTriple, layers), batch = [], rewrites = [];
649
+ for (const { add, element } of valid)
650
+ batch.push({ type: "add", elementId: add.id, layerName: add.layer, after: element.toJSON() });
651
+ const stagedUpdates = [];
652
+ for (const update of plan.updates) {
653
+ const current = await currentSnapshot(model, before, changeset ?? emptyChangeset(), update.id, update.layer);
654
+ if (!current)
655
+ continue;
656
+ const after = patched(current.snapshot, update, now);
657
+ current.stagedAdd ? rewrites.push({
658
+ elementId: current.stagedAdd.elementId,
659
+ change: {
660
+ type: "add",
661
+ elementId: current.stagedAdd.elementId,
662
+ layerName: current.stagedAdd.layerName,
663
+ ...current.stagedAdd.before !== void 0 ? { before: current.stagedAdd.before } : {},
664
+ after
665
+ }
666
+ }) : batch.push({ type: "update", elementId: update.id, layerName: update.layer, before: current.snapshot, after }), stagedUpdates.push(update);
667
+ }
668
+ for (const r of relationships)
669
+ batch.push(relationshipChange(r));
670
+ const summary = {
671
+ adds: adds.length,
672
+ updates: stagedUpdates.length,
673
+ relationships: relationships.length,
674
+ cards: undecided.length,
675
+ candidateDeletes: plan.candidateDeletes.length
676
+ };
677
+ if (Date.now() > deadline)
678
+ throw new CLIError(`codebase_extract ran past its ${Math.round((deadline - startedAt) / 1e3)} s budget before staging. It had planned ${summary.adds} adds, ${summary.updates} updates and ${summary.relationships} relationships, with ${summary.cards} cards; nothing was staged`, ErrorCategory.USER, [NARROW]);
679
+ if (context?.signal?.aborted)
680
+ throw new CLIError("codebase_extract was cancelled before staging; nothing was staged", ErrorCategory.USER);
681
+ const activeId = await manager.getActiveId();
682
+ let previousActive = null;
683
+ if (activeId && activeId !== changeset?.id) {
684
+ const prev = await manager.load(activeId);
685
+ previousActive = { id: activeId, name: prev?.name ?? activeId };
686
+ } else if (storedBefore?.previousActive) {
687
+ const prev = await manager.load(storedBefore.previousActive.id);
688
+ prev?.status === "staged" && (previousActive = { id: prev.id, name: prev.name });
689
+ }
690
+ let recordedPrevious = previousActive;
691
+ if (storedBefore?.previousActive && storedBefore.previousActive.id !== previousActive?.id) {
692
+ const first = await manager.load(storedBefore.previousActive.id);
693
+ first?.status === "staged" && (recordedPrevious = { id: first.id, name: first.name });
694
+ }
695
+ const stages = batch.length > 0 || rewrites.length > 0, hasWork = stages || undecided.length > 0 || plan.candidateDeletes.length > 0;
696
+ let created = !1, active = !!changeset && activeId === changeset.id;
697
+ if (changeset ? hasWork : stages) {
698
+ changeset || (changeset = await manager.create(name, `As-built extraction of the code at ${head.slice(0, 7)}${since ? `, files changed since ${since}` : ""}. Staged by codebase_extract.`), created = !0);
699
+ try {
700
+ await manager.stageMany(changeset.id, batch, rewrites);
701
+ } catch (error) {
702
+ throw created && await manager.delete(changeset.id).catch(() => {
703
+ }), error;
704
+ }
705
+ activeId !== changeset.id && await manager.setActive(changeset.id), active = !0, await reloadModel(args.rootPath), Object.keys(plan.descriptions).length && await writeFile(descriptionsFile, JSON.stringify({ ...extractedDescriptions, ...plan.descriptions }), "utf-8");
706
+ }
707
+ const nowCards = new Set(undecided.map((c) => c.cardId)), earlierCards = /* @__PURE__ */ new Map();
708
+ for (const c of [...storedBefore?.earlierCards ?? [], ...storedBefore?.cards ?? []])
709
+ nowCards.has(c.cardId) || earlierCards.set(c.cardId, c);
710
+ const updatedFields = /* @__PURE__ */ new Map();
711
+ for (const u of storedBefore?.updated ?? []) {
712
+ const status = before.getElement(u.id)?.status;
713
+ status !== "updated" && status !== "added" || updatedFields.set(u.id, new Map(u.fields.map((f) => [f, u.values && Object.prototype.hasOwnProperty.call(u.values, f) ? u.values[f] : void 0])));
714
+ }
715
+ for (const u of stagedUpdates) {
716
+ const fields = updatedFields.get(u.id) ?? updatedFields.set(u.id, /* @__PURE__ */ new Map()).get(u.id);
717
+ for (const [f, value] of Object.entries(u.fields))
718
+ fields.set(f, structuredClone(value));
719
+ }
720
+ const updatedRecord = [...updatedFields].map(([id, fields]) => {
721
+ const names = [...fields.keys()].sort(), values = Object.fromEntries(names.filter((f) => fields.get(f) !== void 0).map((f) => [f, fields.get(f)]));
722
+ return { id, fields: names, ...Object.keys(values).length ? { values } : {} };
723
+ }), candidateDeleteIds = [.../* @__PURE__ */ new Set([...storedBefore?.candidateDeletes ?? [], ...plan.candidateDeletes.map((d) => d.id)])], accumulated = {
724
+ ...updatedRecord.length ? { updated: updatedRecord } : {},
725
+ ...candidateDeleteIds.length ? { candidateDeletes: candidateDeleteIds } : {}
726
+ }, pendingPath = path.join(extractDir(projectRoot), PENDING_FILE);
727
+ if (changeset) {
728
+ const staged = [...stillStaged];
729
+ for (const a of adds)
730
+ staged.push({
731
+ id: a.id,
732
+ file: a.symbol.card.file_path,
733
+ symbol: sourceSymbolOf(a.symbol, extraction.project),
734
+ label: a.symbol.card.label,
735
+ ...stagedAs(a.name, a.attributes)
736
+ });
737
+ const record = {
738
+ changesetId: changeset.id,
739
+ changesetName: changeset.name,
740
+ gitHead: head,
741
+ since: since ?? null,
742
+ layers: layers ?? null,
743
+ baseModified,
744
+ previousActive: recordedPrevious,
745
+ createdAt: now,
746
+ cards: undecided,
747
+ staged,
748
+ ...earlierCards.size ? { earlierCards: [...earlierCards.values()] } : {},
749
+ ...accumulated
750
+ };
751
+ await writeFile(cardsPath(projectRoot, changeset.id), JSON.stringify(record), "utf-8"), pending?.changesetName === changeset.name && await rm(pendingPath, { force: !0 });
752
+ } else if (undecided.length || plan.candidateDeletes.length) {
753
+ const record = {
754
+ changesetId: "",
755
+ changesetName: name,
756
+ gitHead: head,
757
+ since: since ?? null,
758
+ layers: layers ?? null,
759
+ baseModified,
760
+ previousActive,
761
+ createdAt: now,
762
+ cards: undecided,
763
+ staged: [],
764
+ ...earlierCards.size ? { earlierCards: [...earlierCards.values()] } : {},
765
+ ...accumulated,
766
+ pending: !0
767
+ };
768
+ await writeFile(pendingPath, JSON.stringify(record), "utf-8");
769
+ } else pending?.changesetName === name && await rm(pendingPath, { force: !0 });
770
+ await pruneCardFiles(projectRoot, manager);
771
+ const cards = page(undecided, 1, effectivePageSize), changedCards = undecided.filter((c) => c.changedInRange).length, changesetRef = changeset ? { id: changeset.id, name: changeset.name } : null, verifyMode = !!since && !!recordedPrevious, message = hasWork ? changeset ? void 0 : `Nothing to stage, so no changeset was created or activated. ${undecided.length} card(s)${changedCards ? ` (${changedCards} changed in the range)` : ""} and ${plan.candidateDeletes.length} candidate delete(s) are left for you to decide; to stage any, first call changeset_create with name: "${name}".` : verifyMode && !changeset ? `Nothing to stage: nothing in the range is observable to extraction or differs from the model. No as-built changeset was created; ${previousActive ? `'${previousActive.name}' is still active.` : `the plan is '${recordedPrevious.name}'.`}` : changeset ? `Nothing more to stage: '${changeset.name}' already says what this code says.` : "Nothing to stage: the model already says what this code says. No changeset was created or activated.";
772
+ return jsonResult(fitCards(cards.items, cards, (items, fit) => ({
773
+ changeset: changesetRef ? { ...changesetRef, created, active } : null,
774
+ previousActive,
775
+ ...message ? { message } : {},
776
+ ...fitNotes(fit),
777
+ ...edited.length ? {
778
+ warning: `${edited.length} source file(s) have uncommitted edits made after the index was taken, which this run may not have seen (${edited.slice(0, 5).join(", ")}${edited.length > 5 ? ", ..." : ""}). Re-index with codebase_index, then call codebase_extract again.`
779
+ } : {},
780
+ ...changes?.hunksError ? {
781
+ hunksWarning: `The range's changed lines couldn't be read (${changes.hunksError}), so no card says changedInRange; the cards are those of every changed file.`
782
+ } : {},
783
+ analyzer,
784
+ // The shared judgement: `fresh-with-uncommitted-edits` when the warning above is given.
785
+ index: describeIndex(freshness, indexJudgement),
786
+ scope: {
787
+ since: since ?? null,
788
+ ...changes ? { base: changes.base, changedFiles: changes.changed.size, deletedFiles: changes.deleted.size } : { files: "all" },
789
+ layers: layers ?? "all"
790
+ },
791
+ cards: { ...extraction.summary, needsAttributes: invalid.length },
792
+ staged: {
793
+ adds: adds.length,
794
+ updates: stagedUpdates.length,
795
+ relationships: relationships.length,
796
+ byLayer: byLayer(adds, stagedUpdates, relationships),
797
+ ...stagedUpdates.length ? { updated: capped(stagedUpdates.map((u) => ({ id: u.id, fields: Object.keys(u.fields) })), fit.listLimit) } : {}
798
+ },
799
+ unchanged: {
800
+ elements: plan.unchanged,
801
+ modelled: plan.modelled,
802
+ relationships: alreadyPresent,
803
+ rejected: plan.rejected,
804
+ ...plan.outsideLayers ? { outsideLayers: plan.outsideLayers } : {}
805
+ },
806
+ candidateDeletes: {
807
+ ...capped(plan.candidateDeletes, fit.listLimit),
808
+ note: "Not staged: deleting is left to you. Each has its evidence."
809
+ },
810
+ ...plan.retyped.length ? { retyped: capped(plan.retyped, fit.listLimit) } : {},
811
+ ...notElementsResult(decisions, plan.skipped, recordedNow, cleared, fit.listLimit),
812
+ undecided: undecidedSection(cards, items, fit, pageSize, changes ? { changedInRange: changedCards } : {}),
813
+ tier1: "not wired: cards Tier 0 can't decide come to you (docs/EXTRACTION_STRATEGY.md, B6)",
814
+ durationMs: Date.now() - startedAt,
815
+ next: hasWork ? changesetRef ? nextSteps(changesetRef, recordedPrevious, cards.pages, plan, changedCards, since, fit.readOn) : pendingSteps(name, recordedPrevious, cards.pages, plan, changedCards, since, fit.readOn) : verifyMode ? changesetRef ? nextSteps(changesetRef, recordedPrevious, 0, plan, 0, since) : verifyNothingSteps(recordedPrevious, since) : []
816
+ })), { compact: !0 });
817
+ } finally {
818
+ await lock.release().catch(() => {
819
+ });
820
+ }
821
+ });
822
+ };
823
+ }
824
+ const codebaseExtractHandler = createCodebaseExtractHandler(), codebaseExtractTool = {
825
+ name: "codebase_extract",
826
+ description: "Model the indexed code as an as-built changeset: Tier-0 rules decide what they can and those elements and their relationships are staged; the rest come back as undecided cards (paged) for you to stage with model_add. Stages only what differs from the model (matched by source file and symbol): new symbols are adds, changed ones updates; elements whose code is gone are listed as candidateDeletes, not staged. since (a git ref) limits it to files changed since then, for verifying a branch; cards whose lines the range changed say changedInRange and come first. Code that is not an element goes in reject (with a reason): a card, or a symbol or element it staged, which is then unstaged; kept until clearRejected, later runs leave them out. An element it staged that you unstage from a changeset is also left out, but only while that changeset is staged: discarding or landing it forgets that. A run or page into an extraction changeset keeps that run's since and layers. The changeset (default extract-<sha>) is created and made active only when the run stages something; a run that only leaves cards or candidate deletes creates nothing and says which changeset to create first. previousActive is the one active before the first run. Needs a fresh index (codebase_index). Never writes the base model or commits.",
827
+ inputSchema,
828
+ handler: codebaseExtractHandler
829
+ };
830
+ export {
831
+ codebaseExtractHandler,
832
+ codebaseExtractTool,
833
+ createCodebaseExtractHandler,
834
+ isExtractionChangeset
835
+ };
836
+ //# sourceMappingURL=codebase-extract.js.map