@descryy/mcp 0.9.1 → 0.11.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 (206) hide show
  1. package/dist/browser/auth.d.ts +76 -0
  2. package/dist/browser/auth.d.ts.map +1 -0
  3. package/dist/browser/auth.js +223 -0
  4. package/dist/browser/auth.js.map +1 -0
  5. package/dist/browser/capability-probe.d.ts +109 -0
  6. package/dist/browser/capability-probe.d.ts.map +1 -0
  7. package/dist/browser/capability-probe.js +201 -0
  8. package/dist/browser/capability-probe.js.map +1 -0
  9. package/dist/browser/driver.d.ts +342 -4
  10. package/dist/browser/driver.d.ts.map +1 -1
  11. package/dist/browser/driver.js +79 -1
  12. package/dist/browser/driver.js.map +1 -1
  13. package/dist/browser/evidence.d.ts +3 -0
  14. package/dist/browser/evidence.d.ts.map +1 -1
  15. package/dist/browser/evidence.js +6 -1
  16. package/dist/browser/evidence.js.map +1 -1
  17. package/dist/browser/fake-driver.d.ts +65 -8
  18. package/dist/browser/fake-driver.d.ts.map +1 -1
  19. package/dist/browser/fake-driver.js +235 -8
  20. package/dist/browser/fake-driver.js.map +1 -1
  21. package/dist/browser/fault-attribution.d.ts +178 -0
  22. package/dist/browser/fault-attribution.d.ts.map +1 -0
  23. package/dist/browser/fault-attribution.js +266 -0
  24. package/dist/browser/fault-attribution.js.map +1 -0
  25. package/dist/browser/identity-graph-write.d.ts +55 -0
  26. package/dist/browser/identity-graph-write.d.ts.map +1 -0
  27. package/dist/browser/identity-graph-write.js +45 -0
  28. package/dist/browser/identity-graph-write.js.map +1 -0
  29. package/dist/browser/page-probe.d.ts +81 -0
  30. package/dist/browser/page-probe.d.ts.map +1 -0
  31. package/dist/browser/page-probe.js +202 -0
  32. package/dist/browser/page-probe.js.map +1 -0
  33. package/dist/browser/playwright-driver.d.ts +79 -0
  34. package/dist/browser/playwright-driver.d.ts.map +1 -1
  35. package/dist/browser/playwright-driver.js +1138 -180
  36. package/dist/browser/playwright-driver.js.map +1 -1
  37. package/dist/browser/reach-recording-session.d.ts +44 -0
  38. package/dist/browser/reach-recording-session.d.ts.map +1 -0
  39. package/dist/browser/reach-recording-session.js +143 -0
  40. package/dist/browser/reach-recording-session.js.map +1 -0
  41. package/dist/browser/reachability.d.ts +168 -0
  42. package/dist/browser/reachability.d.ts.map +1 -0
  43. package/dist/browser/reachability.js +294 -0
  44. package/dist/browser/reachability.js.map +1 -0
  45. package/dist/browser/registry.d.ts +24 -0
  46. package/dist/browser/registry.d.ts.map +1 -1
  47. package/dist/browser/registry.js +21 -2
  48. package/dist/browser/registry.js.map +1 -1
  49. package/dist/browser/scenario-provenance.d.ts +1 -1
  50. package/dist/browser/scenario-provenance.d.ts.map +1 -1
  51. package/dist/browser/scenario-provenance.js +15 -1
  52. package/dist/browser/scenario-provenance.js.map +1 -1
  53. package/dist/browser/scenario-runner.d.ts +10 -2
  54. package/dist/browser/scenario-runner.d.ts.map +1 -1
  55. package/dist/browser/scenario-runner.js +77 -9
  56. package/dist/browser/scenario-runner.js.map +1 -1
  57. package/dist/browser/tool-support.d.ts +5 -1
  58. package/dist/browser/tool-support.d.ts.map +1 -1
  59. package/dist/browser/tool-support.js +80 -4
  60. package/dist/browser/tool-support.js.map +1 -1
  61. package/dist/browser/wait-target.d.ts +37 -0
  62. package/dist/browser/wait-target.d.ts.map +1 -0
  63. package/dist/browser/wait-target.js +54 -0
  64. package/dist/browser/wait-target.js.map +1 -0
  65. package/dist/heap.d.ts +61 -0
  66. package/dist/heap.d.ts.map +1 -0
  67. package/dist/heap.js +82 -0
  68. package/dist/heap.js.map +1 -0
  69. package/dist/heartbeat.d.ts +46 -0
  70. package/dist/heartbeat.d.ts.map +1 -0
  71. package/dist/heartbeat.js +66 -0
  72. package/dist/heartbeat.js.map +1 -0
  73. package/dist/index.d.ts +2 -2
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/planner/predict-then-propose.d.ts +101 -0
  78. package/dist/planner/predict-then-propose.d.ts.map +1 -0
  79. package/dist/planner/predict-then-propose.js +88 -0
  80. package/dist/planner/predict-then-propose.js.map +1 -0
  81. package/dist/planner/propose-journey.d.ts +60 -0
  82. package/dist/planner/propose-journey.d.ts.map +1 -0
  83. package/dist/planner/propose-journey.js +96 -0
  84. package/dist/planner/propose-journey.js.map +1 -0
  85. package/dist/registry.d.ts +10 -1
  86. package/dist/registry.d.ts.map +1 -1
  87. package/dist/registry.js +5 -1
  88. package/dist/registry.js.map +1 -1
  89. package/dist/render.d.ts +4 -4
  90. package/dist/render.js +3 -3
  91. package/dist/scenarios/browser-run-plan-projection.d.ts +143 -0
  92. package/dist/scenarios/browser-run-plan-projection.d.ts.map +1 -0
  93. package/dist/scenarios/browser-run-plan-projection.js +228 -0
  94. package/dist/scenarios/browser-run-plan-projection.js.map +1 -0
  95. package/dist/scenarios/credential-ref.d.ts +102 -0
  96. package/dist/scenarios/credential-ref.d.ts.map +1 -0
  97. package/dist/scenarios/credential-ref.js +148 -0
  98. package/dist/scenarios/credential-ref.js.map +1 -0
  99. package/dist/scenarios/index.d.ts +4 -1
  100. package/dist/scenarios/index.d.ts.map +1 -1
  101. package/dist/scenarios/index.js +4 -1
  102. package/dist/scenarios/index.js.map +1 -1
  103. package/dist/scenarios/parse.d.ts.map +1 -1
  104. package/dist/scenarios/parse.js +42 -9
  105. package/dist/scenarios/parse.js.map +1 -1
  106. package/dist/scenarios/scenario.d.ts +39 -5
  107. package/dist/scenarios/scenario.d.ts.map +1 -1
  108. package/dist/scenarios/scenario.js +15 -2
  109. package/dist/scenarios/scenario.js.map +1 -1
  110. package/dist/scenarios/secret-ref.d.ts +86 -0
  111. package/dist/scenarios/secret-ref.d.ts.map +1 -0
  112. package/dist/scenarios/secret-ref.js +125 -0
  113. package/dist/scenarios/secret-ref.js.map +1 -0
  114. package/dist/server.d.ts +1 -1
  115. package/dist/server.d.ts.map +1 -1
  116. package/dist/server.js +8 -4
  117. package/dist/server.js.map +1 -1
  118. package/dist/session.d.ts +19 -0
  119. package/dist/session.d.ts.map +1 -1
  120. package/dist/session.js +36 -0
  121. package/dist/session.js.map +1 -1
  122. package/dist/tools/analyze-workspace.d.ts +52 -0
  123. package/dist/tools/analyze-workspace.d.ts.map +1 -0
  124. package/dist/tools/analyze-workspace.js +207 -0
  125. package/dist/tools/analyze-workspace.js.map +1 -0
  126. package/dist/tools/analyze.d.ts +50 -4
  127. package/dist/tools/analyze.d.ts.map +1 -1
  128. package/dist/tools/analyze.js +225 -5
  129. package/dist/tools/analyze.js.map +1 -1
  130. package/dist/tools/browser-close-session.d.ts +21 -1
  131. package/dist/tools/browser-close-session.d.ts.map +1 -1
  132. package/dist/tools/browser-close-session.js +71 -7
  133. package/dist/tools/browser-close-session.js.map +1 -1
  134. package/dist/tools/browser-navigate.d.ts +1 -1
  135. package/dist/tools/browser-navigate.d.ts.map +1 -1
  136. package/dist/tools/browser-navigate.js +5 -3
  137. package/dist/tools/browser-navigate.js.map +1 -1
  138. package/dist/tools/browser-run-scenario.d.ts.map +1 -1
  139. package/dist/tools/browser-run-scenario.js +6 -1
  140. package/dist/tools/browser-run-scenario.js.map +1 -1
  141. package/dist/tools/browser-save-scenario.d.ts.map +1 -1
  142. package/dist/tools/browser-save-scenario.js +84 -4
  143. package/dist/tools/browser-save-scenario.js.map +1 -1
  144. package/dist/tools/browser-select.d.ts +20 -0
  145. package/dist/tools/browser-select.d.ts.map +1 -0
  146. package/dist/tools/browser-select.js +120 -0
  147. package/dist/tools/browser-select.js.map +1 -0
  148. package/dist/tools/browser-snapshot.d.ts +17 -1
  149. package/dist/tools/browser-snapshot.d.ts.map +1 -1
  150. package/dist/tools/browser-snapshot.js +18 -5
  151. package/dist/tools/browser-snapshot.js.map +1 -1
  152. package/dist/tools/browser-start-session.d.ts.map +1 -1
  153. package/dist/tools/browser-start-session.js +161 -9
  154. package/dist/tools/browser-start-session.js.map +1 -1
  155. package/dist/tools/browser-submit.d.ts +18 -0
  156. package/dist/tools/browser-submit.d.ts.map +1 -0
  157. package/dist/tools/browser-submit.js +106 -0
  158. package/dist/tools/browser-submit.js.map +1 -0
  159. package/dist/tools/browser-wait-for.d.ts +41 -0
  160. package/dist/tools/browser-wait-for.d.ts.map +1 -0
  161. package/dist/tools/browser-wait-for.js +148 -0
  162. package/dist/tools/browser-wait-for.js.map +1 -0
  163. package/dist/tools/chain.d.ts +41 -0
  164. package/dist/tools/chain.d.ts.map +1 -0
  165. package/dist/tools/chain.js +105 -0
  166. package/dist/tools/chain.js.map +1 -0
  167. package/dist/tools/draw-conclusion.d.ts +44 -0
  168. package/dist/tools/draw-conclusion.d.ts.map +1 -0
  169. package/dist/tools/draw-conclusion.js +250 -0
  170. package/dist/tools/draw-conclusion.js.map +1 -0
  171. package/dist/tools/index.d.ts +7 -0
  172. package/dist/tools/index.d.ts.map +1 -1
  173. package/dist/tools/index.js +16 -0
  174. package/dist/tools/index.js.map +1 -1
  175. package/dist/tools/kit.d.ts +17 -0
  176. package/dist/tools/kit.d.ts.map +1 -1
  177. package/dist/tools/kit.js.map +1 -1
  178. package/dist/tools/observe-runtime.d.ts +17 -0
  179. package/dist/tools/observe-runtime.d.ts.map +1 -1
  180. package/dist/tools/observe-runtime.js +174 -10
  181. package/dist/tools/observe-runtime.js.map +1 -1
  182. package/dist/tools/predict-reach.d.ts +57 -0
  183. package/dist/tools/predict-reach.d.ts.map +1 -0
  184. package/dist/tools/predict-reach.js +159 -0
  185. package/dist/tools/predict-reach.js.map +1 -0
  186. package/dist/tools/propagation.d.ts +8 -3
  187. package/dist/tools/propagation.d.ts.map +1 -1
  188. package/dist/tools/propagation.js +16 -9
  189. package/dist/tools/propagation.js.map +1 -1
  190. package/dist/tools/propose-journey.d.ts +16 -0
  191. package/dist/tools/propose-journey.d.ts.map +1 -0
  192. package/dist/tools/propose-journey.js +121 -0
  193. package/dist/tools/propose-journey.js.map +1 -0
  194. package/dist/tools/runtime-journey-drive.d.ts +102 -0
  195. package/dist/tools/runtime-journey-drive.d.ts.map +1 -0
  196. package/dist/tools/runtime-journey-drive.js +247 -0
  197. package/dist/tools/runtime-journey-drive.js.map +1 -0
  198. package/dist/tools/scope.d.ts +3 -0
  199. package/dist/tools/scope.d.ts.map +1 -1
  200. package/dist/tools/scope.js +33 -6
  201. package/dist/tools/scope.js.map +1 -1
  202. package/dist/workspace-index.d.ts +164 -0
  203. package/dist/workspace-index.d.ts.map +1 -0
  204. package/dist/workspace-index.js +381 -0
  205. package/dist/workspace-index.js.map +1 -0
  206. package/package.json +32 -31
@@ -0,0 +1,207 @@
1
+ /**
2
+ * `analyze_workspace` — Gap 1 of
3
+ * `documents/plans/site-claims-closure-production-plan.md`: the cross-repo
4
+ * counterpart of `analyze`. Resolves the repository list from
5
+ * `session.config.workspaceRepos` (written by `link_workspace`, never
6
+ * inferred — same rule `link_workspace`'s own header states), migrates any
7
+ * existing per-repository graph aside per `migrateToWorkspaceGraph`
8
+ * (`@descryy/core`), and calls `indexWorkspace` to read every repository into
9
+ * one shared store.
10
+ *
11
+ * Deliberately a **new** tool rather than a mode on `analyze` — `analyze`'s
12
+ * contract is "read this one repository" and every one of its callers reads
13
+ * its reply as describing `session.repoPath` alone; overloading it would
14
+ * change that contract for all of them. `analyze_workspace` instead composes
15
+ * `analyze`'s own building blocks (via `indexWorkspace`) without touching
16
+ * `analyze.ts` at all.
17
+ *
18
+ * The shared graph's location is a disclosed design choice, not an inferred
19
+ * one — see `workspaceGraphPath`'s own doc in `@descryy/core` for why it
20
+ * lives under the *invoking* repository's `.descry/` directory rather than
21
+ * some central, app-wide location this package (unlike `descry-desktop`) has
22
+ * no concept of.
23
+ */
24
+ import { join, resolve } from "node:path";
25
+ import { isGitRepository, migrateToWorkspaceGraph, workspaceGraphPath, } from "@descryy/core";
26
+ import { indexWorkspace } from "../workspace-index.js";
27
+ import { loadConfig } from "../session.js";
28
+ import { answer, optionalBoolean, optionalInteger } from "./kit.js";
29
+ const SCHEMA = {
30
+ type: "object",
31
+ properties: {
32
+ includeHistory: {
33
+ type: "boolean",
34
+ description: "Read each repository's git history for co-change coupling. Default true.",
35
+ },
36
+ maxCommits: {
37
+ type: "integer",
38
+ minimum: 1,
39
+ description: "Cap on commits read from each repository's history. Default 2000.",
40
+ },
41
+ },
42
+ additionalProperties: false,
43
+ };
44
+ /** Where `repository`'s own pre-workspace graph lives today, per its own config — `null` when that cannot be determined (config unreadable), which `migrateToWorkspaceGraph` treats as nothing to move for it. */
45
+ async function ownGraphPath(repository) {
46
+ try {
47
+ const config = await loadConfig(repository.path);
48
+ const graphPath = config.graph.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(config.graph)
49
+ ? config.graph
50
+ : join(repository.path, config.graph);
51
+ return { repo: repository.repo, graphPath };
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function run(args, ctx) {
58
+ const { session } = ctx;
59
+ const includeHistory = optionalBoolean(args, "includeHistory") ?? true;
60
+ const maxCommits = optionalInteger(args, "maxCommits", 1);
61
+ const workspace = session.config.workspace;
62
+ if (workspace === undefined) {
63
+ return answer({
64
+ headline: "No workspace is configured for this repository.",
65
+ state: "refused",
66
+ nameLevel: true,
67
+ resolutionFloor: 0,
68
+ commitSha: null,
69
+ graphBuiltAt: null,
70
+ irSchemaVersion: session.provider().baseStamp().irSchemaVersion,
71
+ commitSpread: session.provider().baseStamp().commitSpread,
72
+ repoCount: session.provider().baseStamp().repoCount,
73
+ notes: [
74
+ 'Call "link_workspace" first, with the repositories the user has explicitly confirmed ' +
75
+ "belong together, then run analyze_workspace again.",
76
+ ],
77
+ data: {
78
+ workspace: "",
79
+ graphPath: "",
80
+ repos: [],
81
+ stored: { nodes: 0, edges: 0, unresolvedRefs: 0 },
82
+ merge: { mergedNodes: 0, mergedEdges: 0, collisions: 0, droppedEdges: 0, rejections: 0 },
83
+ migration: { migrated: false, movedDatabases: [] },
84
+ },
85
+ });
86
+ }
87
+ const declaredRepos = session.config.workspaceRepos ?? [];
88
+ if (declaredRepos.length < 2) {
89
+ return answer({
90
+ headline: `Workspace "${workspace}" has fewer than two declared repositories — nothing to join.`,
91
+ state: "refused",
92
+ nameLevel: true,
93
+ resolutionFloor: 0,
94
+ commitSha: null,
95
+ graphBuiltAt: null,
96
+ irSchemaVersion: session.provider().baseStamp().irSchemaVersion,
97
+ commitSpread: session.provider().baseStamp().commitSpread,
98
+ repoCount: session.provider().baseStamp().repoCount,
99
+ notes: [
100
+ `session.config.workspaceRepos has ${declaredRepos.length} entr${declaredRepos.length === 1 ? "y" : "ies"}. ` +
101
+ 'Re-run "link_workspace" naming every repository that belongs to this workspace, including ' +
102
+ "this one, then run analyze_workspace again.",
103
+ ],
104
+ data: {
105
+ workspace,
106
+ graphPath: "",
107
+ repos: [],
108
+ stored: { nodes: 0, edges: 0, unresolvedRefs: 0 },
109
+ merge: { mergedNodes: 0, mergedEdges: 0, collisions: 0, droppedEdges: 0, rejections: 0 },
110
+ migration: { migrated: false, movedDatabases: [] },
111
+ },
112
+ });
113
+ }
114
+ // The shared graph lives under the *invoking* repository's own `.descry/`
115
+ // directory — a disclosed design choice, not an inferred one; see this
116
+ // module's own header and `workspaceGraphPath`'s doc in `@descryy/core`.
117
+ const rootDir = join(session.repoPath, ".descry");
118
+ const graphDbPath = workspaceGraphPath(rootDir, workspace);
119
+ const notes = [];
120
+ const locations = (await Promise.all(declaredRepos.map(ownGraphPath))).filter((location) => location !== null && location.graphPath !== graphDbPath);
121
+ const migration = migrateToWorkspaceGraph({ rootDir, workspaceId: workspace, repositories: locations });
122
+ if (migration.disclosure !== null)
123
+ notes.push(migration.disclosure);
124
+ const missingRepos = [];
125
+ const repoInputs = [];
126
+ for (const declared of declaredRepos) {
127
+ const absolute = resolve(declared.path);
128
+ if (!(await isGitRepository(absolute).catch(() => false))) {
129
+ // `indexWorkspace` itself would also refuse this repository and say so
130
+ // in its own per-repo notes — checked here too only so a repository
131
+ // that cannot even be reached at all is named before the run starts,
132
+ // not discovered only in a per-repo note buried in the reply.
133
+ missingRepos.push(declared.path);
134
+ }
135
+ repoInputs.push({ repoPath: absolute, repo: declared.repo });
136
+ }
137
+ if (missingRepos.length > 0) {
138
+ notes.push(`${missingRepos.length} declared repositor${missingRepos.length === 1 ? "y is" : "ies are"} not a ` +
139
+ `git repository at the declared path (${missingRepos.join(", ")}) — each will be reported as not ` +
140
+ "indexed rather than skipped silently.");
141
+ }
142
+ const result = await indexWorkspace({
143
+ workspaceId: workspace,
144
+ repos: repoInputs,
145
+ graphDbPath,
146
+ includeHistory,
147
+ ...(maxCommits === undefined ? {} : { maxCommits }),
148
+ onProgress: (progress) => ctx.progress(`${progress.repo}: ${progress.phase}`, progress.completed, progress.total),
149
+ signal: ctx.signal,
150
+ });
151
+ for (const note of result.notes)
152
+ notes.push(note);
153
+ for (const repo of result.repos)
154
+ for (const note of repo.notes)
155
+ notes.push(`${repo.repo}: ${note}`);
156
+ const readRepos = result.repos.filter((r) => r.commitSha !== null).length;
157
+ const headline = readRepos === 0
158
+ ? `Nothing was read for workspace "${workspace}" — none of its ${result.repos.length} declared ` +
159
+ "repositories could be indexed."
160
+ : `Analysed workspace "${workspace}": ${readRepos} of ${result.repos.length} repositories indexed ` +
161
+ `into one shared graph, which now holds ${result.stored.nodes} nodes, ${result.stored.edges} edges.`;
162
+ return answer({
163
+ headline,
164
+ state: readRepos === 0 ? "refused" : "ok",
165
+ nameLevel: true,
166
+ resolutionFloor: result.stamp.resolutionFloor,
167
+ // No single commit describes a workspace spanning several repositories —
168
+ // each repository's own commit is in `data.repos[].commitSha` instead.
169
+ commitSha: null,
170
+ graphBuiltAt: Date.now(),
171
+ irSchemaVersion: result.stamp.irSchemaVersion,
172
+ commitSpread: result.stamp.commitSpread,
173
+ repoCount: result.stamp.repoCount,
174
+ notes,
175
+ data: {
176
+ workspace,
177
+ graphPath: graphDbPath,
178
+ repos: result.repos,
179
+ stored: {
180
+ nodes: result.stored.nodes,
181
+ edges: result.stored.edges,
182
+ unresolvedRefs: result.stored.unresolvedRefs,
183
+ },
184
+ merge: result.merge,
185
+ migration: {
186
+ migrated: migration.migrated,
187
+ movedDatabases: migration.movedDatabases.map((m) => ({ repo: m.repo, from: m.from, to: m.to })),
188
+ },
189
+ },
190
+ });
191
+ }
192
+ export const analyzeWorkspaceTool = {
193
+ name: "analyze_workspace",
194
+ class: "stateful-read",
195
+ tier: "evidence",
196
+ version: "1.0.0",
197
+ title: "Analyse every repository in a workspace into one shared code graph",
198
+ description: 'Read every repository declared by "link_workspace" with its own configured adapters, and ' +
199
+ "store the merged result as one shared code graph — so a frontend's USES_API and a backend's " +
200
+ "SERVES_API can join on the same API_ENDPOINT id (DEC-054). Requires a workspace to already be " +
201
+ 'linked; call "link_workspace" first if it is not. Moves any existing per-repository graph ' +
202
+ "aside (never deletes it) the first time a repository joins a shared graph, and re-indexes it " +
203
+ "into the shared store in the same call.",
204
+ inputSchema: SCHEMA,
205
+ run,
206
+ };
207
+ //# sourceMappingURL=analyze-workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-workspace.js","sourceRoot":"","sources":["../../src/tools/analyze-workspace.ts"],"names":[],"mappings":"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"}
@@ -4,9 +4,10 @@
4
4
  * reached (caps reliability, rule 3), id collisions (DEC-024), evicted files, and the
5
5
  * cross-repo join count. No config → `repo` defaults to dir name, hashed into every id (DEC-004). */
6
6
  import type { IRBatch, UnresolvedRef } from "@descryy/ir";
7
+ import { getCachedFiles } from "@descryy/core";
7
8
  import type { BuildResult, JoinMetric, StoreCounts } from "@descryy/core";
8
9
  import { type ToolDefinition } from "./kit.ts";
9
- import { type LoadedSource } from "../registry.ts";
10
+ import { type DetectionEvidence, type LoadedSource } from "../registry.ts";
10
11
  export { configDriftNote } from "../session.ts";
11
12
  /** What one source read, and how well. `reachedResolution` is what rule 3 caps everything
12
13
  * above it by — a run degraded to R0 looks identical to a healthy one until this is read. */
@@ -101,9 +102,48 @@ export interface AnalyzeData {
101
102
  readonly evictedFiles: number;
102
103
  };
103
104
  }
104
- /** §5.4's `refusals` field for this run only, not the graph's whole ledger
105
- * (`refusal_fetch`'s job) — a fresh population per call, honestly scoped to what
106
- * this run found. `undefined` on zero rows so `refusals` stays `null`, not an empty summary. */
105
+ /**
106
+ * P6-B1 — the run stopped at the heap fence, before the phase that would have
107
+ * aborted the process.
108
+ *
109
+ * A distinct shape rather than a zeroed `AnalyzeData`, for the same reason
110
+ * `scope` has `ScopeNotSeeded`: a halted run has no source table, no merge
111
+ * counts and no stored totals, and reporting zeros for them would say the
112
+ * repository is empty rather than that Descry stopped looking. Narrow on
113
+ * `"halted" in data`.
114
+ */
115
+ export interface AnalyzeHalted {
116
+ readonly halted: "heap";
117
+ /** The phase that was refused — what would have run next, not what failed. */
118
+ readonly phase: string;
119
+ readonly heapUsedMib: number;
120
+ readonly heapLimitMib: number;
121
+ /** Sources that had already emitted when the fence tripped. Read, then discarded unwritten. */
122
+ readonly sourcesRead: readonly SourceRun[];
123
+ }
124
+ export type AnalyzeResult = AnalyzeData | AnalyzeHalted;
125
+ /** Gap 6 step 3: attempts whole-producer cache reuse for one `LanguageAdapter`
126
+ * source, so a repeat `analyze` run can skip `emit()` (and the re-parse inside
127
+ * it) entirely when nothing this producer read has changed. Every failure path
128
+ * here returns `{ reused: false }` rather than throwing — declining to reuse is
129
+ * always safe, and the caller's normal `emit()` path is the fallback. See
130
+ * `planIncrementalReuse`'s doc (`@descryy/core`) for why this is whole-producer,
131
+ * never per-file. */
132
+ export declare function attemptIncrementalReuse(params: {
133
+ readonly driver: Parameters<typeof getCachedFiles>[0];
134
+ readonly producedBy: string;
135
+ readonly repo: string;
136
+ readonly repoPath: string;
137
+ readonly repoFileListing: readonly string[] | null;
138
+ readonly commitSha: string;
139
+ readonly workspace: string | undefined;
140
+ }): Promise<{
141
+ readonly reused: true;
142
+ readonly batch: IRBatch;
143
+ } | {
144
+ readonly reused: false;
145
+ readonly reason: string;
146
+ }>;
107
147
  export declare function refusalSummaryField(unresolved: readonly UnresolvedRef[]): {
108
148
  readonly count: number;
109
149
  readonly exemplar: string;
@@ -113,5 +153,11 @@ export declare function refusalSummaryField(unresolved: readonly UnresolvedRef[]
113
153
  * every join metric's `suppliedByTwoOrMoreRepos: 0` looks like `NO_CROSS_REPO_JOIN`'s
114
154
  * failure case; this states outright it's just scope, not a failed join. */
115
155
  export declare function crossRepoJoinNote(joins: readonly JoinMetric[]): string | undefined;
156
+ /** Turns one adapter's `DetectionResult.evidence` into the same kind of disclosed note the
157
+ * `skippedFiles` rollup below builds — grouped by `reason` where an entry is a
158
+ * `StructuredSkipEntry`, left under an "other" bucket, verbatim, where it is still the
159
+ * legacy plain-string shape (`isStructuredSkipEntry`, `@descryy/ir`). `null` on an empty
160
+ * list — nothing to disclose, not a note saying so. */
161
+ export declare function summariseDetectionEvidence(evidence: DetectionEvidence): string | null;
116
162
  export declare const analyzeTool: ToolDefinition;
117
163
  //# sourceMappingURL=analyze.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/tools/analyze.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/tools/analyze.ts"],"names":[],"mappings":"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"}
@@ -3,8 +3,14 @@
3
3
  * didn't claim the repo (§20.2 — not-analysable ≠ clean), each source's resolution
4
4
  * reached (caps reliability, rule 3), id collisions (DEC-024), evicted files, and the
5
5
  * cross-repo join count. No config → `repo` defaults to dir name, hashed into every id (DEC-004). */
6
- import { applyConfirmedFacts, buildGraph, CONFIRMED_INCIDENT_SOURCE_ID, CONFIRMED_INCIDENT_SOURCE_VERSION, counts, createConfirmedIncidentSource, createGitSource, evictStaleConfirmedIncidentNodes, mintEdgesFromConfirmedFacts, MINT_PRODUCED_BY, persistGraph, summariseRefusals, } from "@descryy/core";
6
+ import { createHash } from "node:crypto";
7
+ import { readFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { isStructuredSkipEntry } from "@descryy/ir";
10
+ import { applyConfirmedFacts, buildGraph, CONFIRMED_INCIDENT_SOURCE_ID, CONFIRMED_INCIDENT_SOURCE_VERSION, counts, createConfirmedIncidentSource, createGitSource, evictStaleConfirmedIncidentNodes, FILELESS_CACHE_KEY, getCachedFiles, listWorkingTreeFiles, mintEdgesFromConfirmedFacts, MINT_PRODUCED_BY, partitionBatchByFile, persistGraph, planIncrementalReuse, replaceCachedFiles, summariseRefusals, } from "@descryy/core";
7
11
  import { answer, optionalBoolean, ToolInputError } from "./kit.js";
12
+ import { heapGuard, readHeap } from "../heap.js";
13
+ import { withHeartbeat } from "../heartbeat.js";
8
14
  import { detectSources, loadSources } from "../registry.js";
9
15
  // Re-exported for `analyze.test.ts`, which covers this pure helper in isolation.
10
16
  // The check itself now runs centrally in `server.ts` (13.3.16 / N2 steps 2-3) —
@@ -90,6 +96,63 @@ const SCHEMA = {
90
96
  /** §5.4's `refusals` field for this run only, not the graph's whole ledger
91
97
  * (`refusal_fetch`'s job) — a fresh population per call, honestly scoped to what
92
98
  * this run found. `undefined` on zero rows so `refusals` stays `null`, not an empty summary. */
99
+ async function hashFileContent(absolutePath) {
100
+ try {
101
+ return createHash("sha256").update(await readFile(absolutePath)).digest("hex");
102
+ }
103
+ catch {
104
+ // Deleted, unreadable, or otherwise gone — left absent so the caller's
105
+ // hash lookup mismatches the cached value and correctly reports "changed".
106
+ return null;
107
+ }
108
+ }
109
+ /** Gap 6 step 3: attempts whole-producer cache reuse for one `LanguageAdapter`
110
+ * source, so a repeat `analyze` run can skip `emit()` (and the re-parse inside
111
+ * it) entirely when nothing this producer read has changed. Every failure path
112
+ * here returns `{ reused: false }` rather than throwing — declining to reuse is
113
+ * always safe, and the caller's normal `emit()` path is the fallback. See
114
+ * `planIncrementalReuse`'s doc (`@descryy/core`) for why this is whole-producer,
115
+ * never per-file. */
116
+ export async function attemptIncrementalReuse(params) {
117
+ const cache = getCachedFiles(params.driver, params.producedBy, params.repo);
118
+ const cachedPaths = [...cache.files.keys()].filter((path) => path !== FILELESS_CACHE_KEY);
119
+ const cachedHashes = new Map(cachedPaths.map((path) => [path, cache.files.get(path).contentHash]));
120
+ const currentHashes = new Map();
121
+ for (const path of cachedPaths) {
122
+ const hash = await hashFileContent(join(params.repoPath, path));
123
+ if (hash !== null)
124
+ currentHashes.set(path, hash);
125
+ }
126
+ const decision = planIncrementalReuse({
127
+ cachedPaths,
128
+ repoPaths: params.repoFileListing,
129
+ currentHashes,
130
+ cachedHashes,
131
+ });
132
+ if (!decision.canReuse)
133
+ return { reused: false, reason: decision.reason };
134
+ const nodes = [];
135
+ const edges = [];
136
+ const unresolved = [];
137
+ for (const entry of cache.files.values()) {
138
+ nodes.push(...entry.nodes);
139
+ edges.push(...entry.edges);
140
+ unresolved.push(...entry.unresolved);
141
+ }
142
+ const batch = {
143
+ repo: params.repo,
144
+ ...(params.workspace === undefined ? {} : { workspace: params.workspace }),
145
+ commitSha: params.commitSha,
146
+ producedBy: params.producedBy,
147
+ sourceFiles: cachedPaths,
148
+ skippedFiles: cache.skippedFiles,
149
+ reachedResolution: (cache.reachedResolution ?? 0),
150
+ nodes,
151
+ edges,
152
+ unresolved,
153
+ };
154
+ return { reused: true, batch };
155
+ }
93
156
  export function refusalSummaryField(unresolved) {
94
157
  const summary = summariseRefusals(unresolved);
95
158
  if (summary.count === 0 || summary.exemplar === null || summary.handle === null)
@@ -109,6 +172,30 @@ export function crossRepoJoinNote(joins) {
109
172
  "take for them; this is not the NO_CROSS_REPO_JOIN warning, which is for two or more " +
110
173
  "repositories that failed to join.");
111
174
  }
175
+ /** Turns one adapter's `DetectionResult.evidence` into the same kind of disclosed note the
176
+ * `skippedFiles` rollup below builds — grouped by `reason` where an entry is a
177
+ * `StructuredSkipEntry`, left under an "other" bucket, verbatim, where it is still the
178
+ * legacy plain-string shape (`isStructuredSkipEntry`, `@descryy/ir`). `null` on an empty
179
+ * list — nothing to disclose, not a note saying so. */
180
+ export function summariseDetectionEvidence(evidence) {
181
+ if (evidence.entries.length === 0)
182
+ return null;
183
+ const byReason = new Map();
184
+ const legacy = [];
185
+ for (const entry of evidence.entries) {
186
+ if (isStructuredSkipEntry(entry)) {
187
+ byReason.set(entry.reason, (byReason.get(entry.reason) ?? 0) + 1);
188
+ }
189
+ else {
190
+ legacy.push(entry);
191
+ }
192
+ }
193
+ const parts = [...byReason.entries()].map(([reason, count]) => `${count} ${reason}`);
194
+ if (legacy.length > 0)
195
+ parts.push(`${legacy.length} other (${legacy.join("; ")})`);
196
+ return (`${evidence.source} reported ${evidence.entries.length} detection note(s) for this ` +
197
+ `repository (${parts.join(", ")}).`);
198
+ }
112
199
  async function run(args, ctx) {
113
200
  const { session } = ctx;
114
201
  const includeHistory = args["includeHistory"] !== false;
@@ -169,11 +256,16 @@ async function run(args, ctx) {
169
256
  "specifiers. This engine does not know the name of any adapter: the IR boundary is a " +
170
257
  "dependency direction, so the list has to come from configuration.");
171
258
  }
172
- const { applicable, skipped } = await detectSources(registry.loaded, root);
259
+ const { applicable, skipped, evidence } = await detectSources(registry.loaded, root);
173
260
  for (const entry of skipped) {
174
261
  notes.push(`${entry.source.source.id} did not run: it ${entry.reason}. Any code only that adapter ` +
175
262
  "could read is not analysable.");
176
263
  }
264
+ for (const item of evidence) {
265
+ const note = summariseDetectionEvidence(item);
266
+ if (note !== null)
267
+ notes.push(note);
268
+ }
177
269
  // --- emit ----------------------------------------------------------------
178
270
  const context = {
179
271
  root,
@@ -188,14 +280,111 @@ async function run(args, ctx) {
188
280
  const codeResolutions = [];
189
281
  const total = applicable.length + (includeHistory ? 1 : 0);
190
282
  let done = 0;
283
+ const driver = session.store().driver;
284
+ // Computed once for the whole run, not per source: every `LanguageAdapter`
285
+ // source's Gap 6 reuse check needs the same "what does the working tree have
286
+ // right now" answer, and it is meaningless to ask git for it twice. `null`
287
+ // when it cannot be determined at all (no git, or git unavailable) — every
288
+ // incremental check below then conservatively declines to reuse anything.
289
+ const repoFileListing = await listWorkingTreeFiles(session.repoPath).catch(() => null);
290
+ /**
291
+ * P6-B1. The fence, at the two points where this tool's memory actually
292
+ * peaks: before an adapter is asked to read the repository (its batch is
293
+ * retained for the whole run), and before the merge, which holds every
294
+ * batch and the built graph live at once. A V8 heap abort cannot be caught
295
+ * — this is the only place the failure can be turned into a reply.
296
+ *
297
+ * `sourcesRead` reports what had already emitted, because those batches are
298
+ * discarded unwritten: without it the reply would be silent about work that
299
+ * did happen and produced nothing.
300
+ */
301
+ const halted = (phase) => {
302
+ const reading = readHeap();
303
+ const note = heapGuard(reading, phase);
304
+ if (note === null)
305
+ return null;
306
+ const base = session.provider().baseStamp();
307
+ return answer({
308
+ headline: `Analysis of ${session.config.repo} stopped at the heap ceiling before "${phase}". ` +
309
+ "Nothing was written to the graph.",
310
+ state: "failed",
311
+ nameLevel: true,
312
+ resolutionFloor: 0,
313
+ commitSha: root.commitSha,
314
+ graphBuiltAt: base.graphBuiltAt,
315
+ irSchemaVersion: base.irSchemaVersion,
316
+ commitSpread: base.commitSpread,
317
+ repoCount: base.repoCount,
318
+ notes: [...notes, note],
319
+ data: {
320
+ halted: "heap",
321
+ phase,
322
+ heapUsedMib: Math.round(reading.usedBytes / (1024 * 1024)),
323
+ heapLimitMib: Math.round(reading.limitBytes / (1024 * 1024)),
324
+ sourcesRead: perSource,
325
+ },
326
+ });
327
+ };
191
328
  for (const entry of applicable) {
329
+ const fence = halted(`Reading ${entry.source.id}`);
330
+ if (fence !== null)
331
+ return fence;
192
332
  ctx.progress(`Reading ${entry.source.id}`, done, total);
193
333
  const started = Date.now();
334
+ // Gap 6: only `LanguageAdapter` sources pay the re-parse cost this exists
335
+ // to avoid — the git-history/confirmed-incidents/mint sources below are
336
+ // already cheap and deterministic and are out of scope for this cache.
337
+ if (entry.adapter !== null) {
338
+ const expectedProducedBy = `${entry.source.id}@${entry.source.version}`;
339
+ const reuse = await attemptIncrementalReuse({
340
+ driver,
341
+ producedBy: expectedProducedBy,
342
+ repo: session.config.repo,
343
+ repoPath: session.repoPath,
344
+ repoFileListing,
345
+ commitSha: root.commitSha,
346
+ workspace: session.config.workspace,
347
+ });
348
+ if (reuse.reused) {
349
+ const batch = reuse.batch;
350
+ batches.push(batch);
351
+ perSource.push({
352
+ source: batch.producedBy,
353
+ installedVersion: entry.packageVersion,
354
+ files: batch.sourceFiles.length,
355
+ nodes: batch.nodes.length,
356
+ edges: batch.edges.length,
357
+ unresolved: batch.unresolved.length,
358
+ skippedFiles: batch.skippedFiles.length,
359
+ reachedResolution: batch.reachedResolution,
360
+ ms: 0,
361
+ });
362
+ const ceiling = entry.adapter.capabilities().maxResolution;
363
+ codeResolutions.push(batch.reachedResolution);
364
+ notes.push(`${entry.source.id}: reused ${String(batch.sourceFiles.length)} cached file(s) from the ` +
365
+ "previous run, unchanged since — not re-parsed this run (Gap 6 incremental indexing). " +
366
+ "Cross-file effects (a change in one file altering another file's resolved edges) are " +
367
+ "not re-checked by this cache; see documents/plans/site-claims-closure-production-plan.md Gap 6.");
368
+ if (batch.reachedResolution < ceiling) {
369
+ notes.push(`${entry.source.id} reached R${String(batch.reachedResolution)} where it is capable of ` +
370
+ `R${String(ceiling)} (cached from its last real run). Findings resting on its edges are ` +
371
+ "capped accordingly, and shape-level checks (contract matching, field propagation) need R3.");
372
+ }
373
+ done += 1;
374
+ continue;
375
+ }
376
+ }
194
377
  // Adapter's own work and Descry's handling of its output are separate phases with
195
378
  // separate blame — folding them into one `try` let a `TypeError` here be blamed on the adapter.
196
379
  let batch;
197
380
  try {
198
- batch = await entry.source.emit(context);
381
+ // P6-B2. An adapter reading a large repository can hold this await for
382
+ // minutes, and until now that was the whole gap between two progress
383
+ // frames — long enough for the client's timeout to fire on a run that
384
+ // was working. The beat keeps the request visibly alive; it does not
385
+ // claim to know how far through the adapter is, because this side
386
+ // cannot see that and will not invent it.
387
+ batch = await withHeartbeat(`Reading ${entry.source.id}`, (message) => ctx.progress(message, done, total), () => entry.source.emit(context));
199
388
  }
200
389
  catch (error) {
201
390
  notes.push(sourceFailureNote(entry.source.id, error, "emit"));
@@ -217,6 +406,29 @@ async function run(args, ctx) {
217
406
  reachedResolution: batch.reachedResolution,
218
407
  ms: Date.now() - started,
219
408
  });
409
+ // Gap 6 step 3: this was a real parse (reuse would have `continue`d above),
410
+ // so refresh the per-file cache from its output — the only place the
411
+ // cache is ever written. A producer whose declared `id@version` drifted
412
+ // from its self-declared `producedBy` (see `sourceProvenanceNote`) simply
413
+ // caches under the real `producedBy`; the next run's reuse check looks
414
+ // for `${id}@${version}` and will not find a mismatched entry, which is
415
+ // the safe direction for this to fail (always falls back to full reparse).
416
+ try {
417
+ const partitioned = partitionBatchByFile(batch);
418
+ const cacheEntries = [];
419
+ for (const [filePath, contribution] of partitioned) {
420
+ const contentHash = filePath === FILELESS_CACHE_KEY
421
+ ? "fileless"
422
+ : (await hashFileContent(join(session.repoPath, filePath))) ?? "unreadable";
423
+ cacheEntries.push({ filePath, contentHash, ...contribution });
424
+ }
425
+ replaceCachedFiles(driver, batch.producedBy, session.config.repo, cacheEntries, batch.reachedResolution, skippedFiles, Date.now());
426
+ }
427
+ catch {
428
+ // Caching is an optimisation, never a correctness requirement — a
429
+ // failure here must not fail the run or lose the batch already pushed
430
+ // above. Worst case: the next run finds no cache and reparses fully.
431
+ }
220
432
  // Declared ceiling vs what this run reached — a gap is honest degradation (rule 7):
221
433
  // the adapter can do better but something in this repo stopped it.
222
434
  const ceiling = entry.adapter?.capabilities().maxResolution;
@@ -252,11 +464,14 @@ async function run(args, ctx) {
252
464
  ctx.progress("Reading git history", done, total);
253
465
  const historyStarted = Date.now();
254
466
  try {
255
- const history = await createGitSource({
467
+ // P6-B2, same reason as the adapter read above: `maxCommits` defaults to
468
+ // 2000, and walking that much history on a large repository is its own
469
+ // multi-minute silence.
470
+ const history = await withHeartbeat("Reading git history", (message) => ctx.progress(message, done, total), () => createGitSource({
256
471
  repo: root.repo,
257
472
  cwd: session.repoPath,
258
473
  ...(typeof maxCommits === "number" ? { maxCommits } : {}),
259
- }).emit(context);
474
+ }).emit(context));
260
475
  batches.push(history);
261
476
  perSource.push({
262
477
  source: history.producedBy,
@@ -310,6 +525,11 @@ async function run(args, ctx) {
310
525
  "an edge resting on one is capped at reliability class B (DEC-223).");
311
526
  }
312
527
  // --- build and store -----------------------------------------------------
528
+ // The real peak: every batch is live here and `buildGraph` allocates the
529
+ // merged graph beside them. This is the checkpoint the field report needed.
530
+ const mergeFence = halted("Merging into the graph");
531
+ if (mergeFence !== null)
532
+ return mergeFence;
313
533
  ctx.progress("Merging into the graph", done, total);
314
534
  const built = buildGraph(batches, {
315
535
  nodeExists: (id) => session.provider().node(id) !== undefined,