jev-agent-tools 0.1.4 → 0.3.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 (180) hide show
  1. package/CHANGELOG.md +106 -1
  2. package/CONTRIBUTING.md +43 -0
  3. package/README.md +58 -17
  4. package/SECURITY.md +43 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +198 -0
  7. package/dist/adapters/ask-proof.js +200 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +234 -0
  11. package/dist/adapters/docs.js +192 -0
  12. package/dist/adapters/evidence-context.js +119 -0
  13. package/dist/adapters/exec.js +207 -0
  14. package/dist/adapters/files.js +418 -0
  15. package/dist/adapters/find.js +150 -0
  16. package/dist/adapters/git-base.js +32 -0
  17. package/dist/adapters/git-inventory.js +71 -0
  18. package/dist/adapters/git.js +483 -0
  19. package/dist/adapters/locate-file.js +197 -0
  20. package/dist/adapters/output-lines.js +46 -0
  21. package/dist/adapters/private-storage.js +106 -0
  22. package/dist/adapters/risk-callers.js +429 -0
  23. package/dist/adapters/runner-version.js +78 -0
  24. package/dist/adapters/shell.js +92 -0
  25. package/dist/adapters/syntax.js +187 -0
  26. package/dist/adapters/test-inventory.js +139 -0
  27. package/dist/adapters/usage.js +20 -0
  28. package/dist/adapters/utf8.js +47 -0
  29. package/dist/configuration.js +267 -0
  30. package/dist/constants.js +140 -0
  31. package/dist/core/ask-closure.js +282 -0
  32. package/dist/core/ask-proof.js +1 -0
  33. package/dist/core/ask-references.js +278 -0
  34. package/dist/core/asks.js +507 -0
  35. package/dist/core/batches.js +65 -0
  36. package/dist/core/command-output.js +224 -0
  37. package/dist/core/diff.js +178 -0
  38. package/dist/core/docs.js +302 -0
  39. package/dist/core/find.js +108 -0
  40. package/dist/core/git.js +1 -0
  41. package/dist/core/imports.js +550 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/lexical.js +132 -0
  44. package/dist/core/locate.js +169 -0
  45. package/dist/core/output.js +137 -0
  46. package/dist/core/pointer.js +29 -0
  47. package/dist/core/result-report.js +302 -0
  48. package/dist/core/risk-callers.js +851 -0
  49. package/dist/core/runner-version.js +45 -0
  50. package/dist/core/secret-path.js +34 -0
  51. package/dist/core/sections.js +230 -0
  52. package/dist/core/state.js +51 -0
  53. package/dist/core/syntax.js +1 -0
  54. package/dist/core/test-commands.js +334 -0
  55. package/dist/core/test-coverage.js +74 -0
  56. package/dist/core/test-discovery.js +1382 -0
  57. package/dist/core/test-evidence.js +527 -0
  58. package/dist/core/test-state.js +81 -0
  59. package/dist/core/truncate.js +12 -0
  60. package/dist/core/units.js +349 -0
  61. package/dist/describe.js +23 -0
  62. package/dist/guide.js +33 -0
  63. package/dist/host.js +24 -0
  64. package/dist/jev/client.js +456 -0
  65. package/dist/jev/pool.js +54 -0
  66. package/dist/jev/types.js +1 -0
  67. package/dist/mcp/main.js +124 -0
  68. package/dist/mcp/protocol.js +210 -0
  69. package/dist/mcp/tools.js +129 -0
  70. package/dist/presets/docs.js +62 -0
  71. package/dist/presets/risk.js +179 -0
  72. package/dist/presets/spec.js +81 -0
  73. package/dist/presets/witnesses.js +249 -0
  74. package/dist/render.js +114 -0
  75. package/dist/report-schema.js +1356 -0
  76. package/dist/result-types.js +1 -0
  77. package/dist/result.js +3 -0
  78. package/dist/runtime.js +1 -0
  79. package/dist/session.js +147 -0
  80. package/dist/texts/ask-files.js +3 -0
  81. package/dist/texts/ask.js +4 -0
  82. package/dist/texts/check-diff.js +20 -0
  83. package/dist/texts/configuration.js +1 -0
  84. package/dist/texts/find.js +19 -0
  85. package/dist/texts/guide.js +3 -0
  86. package/dist/texts/instructions.js +72 -0
  87. package/dist/texts/locate.js +15 -0
  88. package/dist/texts/select-tests.js +4 -0
  89. package/dist/tools/ask-files.js +450 -0
  90. package/dist/tools/ask-schema.js +70 -0
  91. package/dist/tools/ask.js +1147 -0
  92. package/dist/tools/check-diff.js +594 -0
  93. package/dist/tools/docs-check.js +408 -0
  94. package/dist/tools/find.js +682 -0
  95. package/dist/tools/locate.js +602 -0
  96. package/dist/tools/review-report.js +230 -0
  97. package/dist/tools/select-tests.js +821 -0
  98. package/dist/tools/spec-check.js +263 -0
  99. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  100. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  101. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  102. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  103. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  104. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  105. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  106. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  107. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  108. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  109. package/docs/agent-instructions.md +120 -0
  110. package/docs/design.md +16 -4
  111. package/docs/mcp.md +233 -0
  112. package/docs/tools/jev_ask.md +8 -5
  113. package/docs/tools/jev_ask_files.md +2 -1
  114. package/docs/tools/jev_check_diff.md +4 -1
  115. package/docs/tools/jev_find_files.md +2 -1
  116. package/docs/tools/jev_locate_in_file.md +5 -0
  117. package/docs/tools/jev_select_tests.md +4 -1
  118. package/package.json +19 -4
  119. package/rules/jev-ask.md +22 -1
  120. package/server.json +57 -0
  121. package/src/adapters/ask-files.ts +11 -3
  122. package/src/adapters/ask-proof.ts +69 -11
  123. package/src/adapters/canonical-path.ts +18 -0
  124. package/src/adapters/command.ts +102 -36
  125. package/src/adapters/docs.ts +33 -14
  126. package/src/adapters/evidence-context.ts +169 -0
  127. package/src/adapters/exec.ts +226 -0
  128. package/src/adapters/files.ts +146 -16
  129. package/src/adapters/find.ts +37 -7
  130. package/src/adapters/git-base.ts +7 -1
  131. package/src/adapters/git.ts +61 -8
  132. package/src/adapters/locate-file.ts +51 -9
  133. package/src/adapters/private-storage.ts +155 -0
  134. package/src/adapters/risk-callers.ts +7 -2
  135. package/src/adapters/shell.ts +113 -0
  136. package/src/adapters/test-inventory.ts +12 -4
  137. package/src/configuration.ts +55 -14
  138. package/src/constants.ts +37 -5
  139. package/src/core/ask-references.ts +262 -146
  140. package/src/core/asks.ts +79 -7
  141. package/src/core/command-output.ts +17 -1
  142. package/src/core/import-boundaries.ts +8 -3
  143. package/src/core/locate.ts +8 -5
  144. package/src/core/output.ts +34 -0
  145. package/src/core/result-report.ts +410 -0
  146. package/src/core/secret-path.ts +37 -0
  147. package/src/core/state.ts +8 -1
  148. package/src/core/units.ts +3 -2
  149. package/src/host.ts +11 -0
  150. package/src/index.ts +3 -0
  151. package/src/jev/client.ts +66 -16
  152. package/src/jev/types.ts +24 -3
  153. package/src/mcp/main.ts +135 -0
  154. package/src/mcp/protocol.ts +332 -0
  155. package/src/mcp/tools.ts +179 -0
  156. package/src/render.ts +109 -0
  157. package/src/report-schema.ts +1380 -0
  158. package/src/result-types.ts +234 -0
  159. package/src/result.ts +4 -1
  160. package/src/runtime.ts +6 -0
  161. package/src/session.ts +59 -0
  162. package/src/setup.ts +13 -5
  163. package/src/texts/ask-files.ts +4 -1
  164. package/src/texts/ask.ts +8 -1
  165. package/src/texts/check-diff.ts +7 -4
  166. package/src/texts/find.ts +8 -2
  167. package/src/texts/guide.ts +8 -16
  168. package/src/texts/instructions.ts +98 -0
  169. package/src/texts/locate.ts +8 -2
  170. package/src/texts/run-end.ts +2 -2
  171. package/src/texts/select-tests.ts +4 -1
  172. package/src/tools/ask-files.ts +311 -18
  173. package/src/tools/ask.ts +722 -95
  174. package/src/tools/check-diff.ts +337 -31
  175. package/src/tools/docs-check.ts +241 -38
  176. package/src/tools/find.ts +389 -29
  177. package/src/tools/locate.ts +387 -25
  178. package/src/tools/review-report.ts +308 -0
  179. package/src/tools/select-tests.ts +484 -23
  180. package/src/tools/spec-check.ts +194 -19
@@ -0,0 +1,408 @@
1
+ import { createAnalysisContext } from "../adapters/analysis-context.js";
2
+ import { collectDocsInventory, docsDeclarationSearch, } from "../adapters/docs.js";
3
+ import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
4
+ import { collectUnits } from "../adapters/git.js";
5
+ import { resolveBase } from "../adapters/git-base.js";
6
+ import { shareGitInventory } from "../adapters/git-inventory.js";
7
+ import { CHOICE_MAX_OPTIONS, DOCS_COLLECT_BUDGET_MS, DOCS_DISPLAY_MAX_SECTIONS, DOCS_MAX_SECTIONS, STATE_MAX_CHARS, TIMEOUT_MS, } from "../constants.js";
8
+ import { collectDocsCandidates } from "../core/docs.js";
9
+ import { buildEnvelope, } from "../core/output.js";
10
+ import { known, } from "../core/result-report.js";
11
+ import { prepareDocsCheck, readDocsJudgment, } from "../presets/docs.js";
12
+ import { NOT_CONFIGURED } from "../texts/configuration.js";
13
+ import { ReviewReport, reportMetrics } from "./review-report.js";
14
+ /** Shared judgment seam: no guide delivery, rendering, or session answer recording. */
15
+ export async function runDocsCheck(deps, input) {
16
+ const started = performance.now();
17
+ const report = new ReviewReport();
18
+ const admission = input.evidenceContext
19
+ ? undefined
20
+ : await resolveEvidenceContext(input.cwd, undefined, {
21
+ exec: deps.exec,
22
+ signal: input.signal,
23
+ origin: deps.evidenceOrigin,
24
+ });
25
+ const evidenceContext = input.evidenceContext ?? admission?.context;
26
+ if (!evidenceContext)
27
+ throw new Error("Evidence context was not established");
28
+ evidenceContext.requestedBase = input.base ?? "HEAD";
29
+ deps = { ...deps, exec: shareGitInventory(deps.exec) };
30
+ const timeout = input.budgetMs === undefined
31
+ ? undefined
32
+ : AbortSignal.timeout(Math.max(0, Math.ceil(input.budgetMs)));
33
+ const signals = [input.signal, timeout].filter((value) => value !== undefined);
34
+ const signal = signals.length ? AbortSignal.any(signals) : undefined;
35
+ const judgments = [];
36
+ const findings = [];
37
+ const unchecked = [];
38
+ let unjudged;
39
+ let collection;
40
+ const limitations = [
41
+ {
42
+ fact: "Measured D22 limit: 2/45 documentation obligations found across 180 partial got/zod commits.",
43
+ next: "Also review missing documentation: this check evaluates existing sentences, not documentation completeness.",
44
+ },
45
+ ];
46
+ let budget;
47
+ let sent = 0;
48
+ let emptyBase;
49
+ const finish = (refusal, cause = "internal_error") => {
50
+ if (unchecked.length > 5) {
51
+ unjudged = {
52
+ count: unchecked.length,
53
+ sections: unchecked.slice(0, DOCS_MAX_SECTIONS),
54
+ truncated: unchecked.length > DOCS_MAX_SECTIONS,
55
+ };
56
+ limitations.push({
57
+ fact: `${unchecked.length} matching sections unjudged.`,
58
+ next: "Narrow the diff with base=… or rerun on the relevant documentation.",
59
+ });
60
+ }
61
+ const answers = findings.map((finding) => ({
62
+ label: `${finding.section.path}:${finding.section.start}-${finding.section.end} § ${finding.section.heading} — ${finding.sentence ? JSON.stringify(finding.sentence.text) : "sentence not identified"} — ${finding.band === "verdict" ? "now false" : "needs checking"} after ${finding.units.map((unit) => `${unit.name} (${unit.file})`).join(", ")}`,
63
+ value: { head: "now_false", p: finding.probability },
64
+ band: finding.band,
65
+ reason: finding.reason,
66
+ }));
67
+ const envelope = buildEnvelope({
68
+ answers,
69
+ limitations: emptyBase !== undefined && !refusal
70
+ ? [
71
+ ...limitations,
72
+ {
73
+ fact: `no changed units against ${emptyBase}`,
74
+ next: "nothing judged; pass base= or check the working directory",
75
+ },
76
+ ]
77
+ : limitations,
78
+ ...(collection || unjudged
79
+ ? {
80
+ lines: [
81
+ ...(collection
82
+ ? [
83
+ {
84
+ type: "collection",
85
+ title: "anchored documentation unexplored; directly cited sections were checked",
86
+ count: collection.count,
87
+ items: collection.sections.slice(0, DOCS_DISPLAY_MAX_SECTIONS),
88
+ next: "Read these sections; narrow the diff with base=… to target their dependency.",
89
+ },
90
+ ]
91
+ : []),
92
+ ...(unjudged
93
+ ? [
94
+ {
95
+ type: "collection",
96
+ title: "matching documentation unjudged",
97
+ count: unjudged.count,
98
+ items: unjudged.sections.slice(0, DOCS_DISPLAY_MAX_SECTIONS),
99
+ next: "Read these sections or rerun on the relevant documentation.",
100
+ },
101
+ ]
102
+ : []),
103
+ ],
104
+ }
105
+ : {}),
106
+ unchecked: unjudged ? [] : unchecked,
107
+ budget,
108
+ refusal,
109
+ ...(!refusal &&
110
+ !budget &&
111
+ !collection &&
112
+ !unchecked.length &&
113
+ !findings.length &&
114
+ emptyBase === undefined &&
115
+ report.items.size > 0 &&
116
+ [...report.items.values()].every((item) => item.treatment === "judged")
117
+ ? {
118
+ lines: [
119
+ {
120
+ type: "list",
121
+ title: "docs: no stale sentences reported",
122
+ items: [],
123
+ },
124
+ ],
125
+ }
126
+ : {}),
127
+ yield: {
128
+ calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
129
+ questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
130
+ costUsd: judgments.length && judgments.every((j) => j.usage !== undefined)
131
+ ? judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0)
132
+ : undefined,
133
+ cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
134
+ cacheRequests: judgments.reduce((n, j) => n + (j.cacheRequests ?? 0), 0),
135
+ elapsedMs: performance.now() - started,
136
+ },
137
+ });
138
+ report.diagnose("collection_omitted", "Existing documentation sentences only; missing documentation is not exhaustively detected (measured 2/45 obligations across 180 partial commits).", undefined, [], false);
139
+ if (emptyBase !== undefined)
140
+ report.diagnose("no_changed_units", `No changed units against ${emptyBase}`, emptyBase, [], false);
141
+ if (!refusal && emptyBase === undefined && !report.items.size)
142
+ report.diagnose("collection_empty", "No matching documentation sections were collected; this does not establish documentation completeness.", undefined, [], false);
143
+ if (refusal) {
144
+ report.refusal =
145
+ cause === "invalid_base" ||
146
+ cause === "invalid_root" ||
147
+ cause === "git_failure";
148
+ report.diagnose(cause, refusal);
149
+ }
150
+ const result = report.build("jev_check_diff", evidenceContext, reportMetrics(envelope));
151
+ return {
152
+ ok: !refusal,
153
+ status: timeout?.aborted
154
+ ? "budget_exceeded"
155
+ : refusal
156
+ ? "refused"
157
+ : collection
158
+ ? "collect_budget_reached"
159
+ : "completed",
160
+ envelope,
161
+ judgments,
162
+ findings,
163
+ result,
164
+ ...(collection ? { collection } : {}),
165
+ ...(unjudged ? { unjudged } : {}),
166
+ };
167
+ };
168
+ const client = deps.client;
169
+ if (!client)
170
+ return finish(NOT_CONFIGURED, "not_configured");
171
+ const sessionRefusal = deps.runtime.session.refusal();
172
+ if (sessionRefusal)
173
+ return finish(sessionRefusal, "session_budget");
174
+ try {
175
+ const comparison = await resolveBase(deps.exec, input.cwd, input.base, signal);
176
+ if (!comparison.ok)
177
+ return finish(comparison.error, comparison.cause ?? "invalid_base");
178
+ const base = comparison.base;
179
+ evidenceContext.resolvedBase = base;
180
+ const analysis = await createAnalysisContext();
181
+ const [collected, inventory] = await Promise.all([
182
+ collectUnits(deps.exec, { cwd: input.cwd, base, signal }, analysis.parser),
183
+ collectDocsInventory(deps.exec, input.cwd, signal),
184
+ ]);
185
+ if (!collected.ok)
186
+ return finish(collected.error, collected.cause ?? "git_failure");
187
+ if (!inventory.ok)
188
+ return finish(inventory.error, inventory.cause ?? "file_unavailable");
189
+ const repositoryRoot = await deps.exec("git", ["rev-parse", "--show-toplevel"], { cwd: input.cwd, timeout: TIMEOUT_MS, signal });
190
+ if (!repositoryRoot.code &&
191
+ !repositoryRoot.killed &&
192
+ evidenceContext.effectiveRoot)
193
+ evidenceContext.effectiveRoot.path = repositoryRoot.stdout.trim();
194
+ for (const limit of collected.limits) {
195
+ report.diagnose(limit.kind === "secret_pattern"
196
+ ? "secret_pattern"
197
+ : "collection_omitted", limit.kind, limit.file);
198
+ limitations.push({
199
+ fact: `${limit.file} : ${limit.kind}`,
200
+ next: "Read the complete change before concluding.",
201
+ });
202
+ }
203
+ if (!collected.units.length) {
204
+ emptyBase = base;
205
+ report.inventories.push({
206
+ id: "changed-units",
207
+ kind: "units",
208
+ rules: ["Changed source units against resolved base"],
209
+ restrictions: [],
210
+ discovered: known(0),
211
+ considered: known(0),
212
+ scopeRestricted: false,
213
+ criteria: [],
214
+ });
215
+ return finish();
216
+ }
217
+ const collectDeadline = performance.now() + DOCS_COLLECT_BUDGET_MS;
218
+ const candidates = await collectDocsCandidates(inventory.files, collected.units, {
219
+ known: inventory.tracked,
220
+ read: inventory.read,
221
+ declarations: docsDeclarationSearch(deps.exec, inventory, signal),
222
+ changedFiles: collected.files,
223
+ lexical: analysis,
224
+ shouldStop: () => performance.now() >= collectDeadline || !!signal?.aborted,
225
+ });
226
+ report.inventories.push({
227
+ id: "docs-sections",
228
+ kind: "sections",
229
+ rules: [
230
+ "Tracked Markdown sections referring to changed declarations or static import closure",
231
+ `At most ${DOCS_MAX_SECTIONS} admitted sections`,
232
+ ],
233
+ restrictions: [],
234
+ discovered: known(candidates.candidates.length + candidates.omitted.length),
235
+ considered: known(candidates.candidates.length),
236
+ scopeRestricted: false,
237
+ criteria: [],
238
+ });
239
+ for (const candidate of candidates.candidates)
240
+ report.expect(`docs:${candidate.path}:${candidate.start}`, `${candidate.path} § ${candidate.heading}`, "section");
241
+ for (const candidate of candidates.omitted) {
242
+ const id = `docs:${candidate.path}:${candidate.start}`;
243
+ report.expect(id, `${candidate.path} § ${candidate.heading}`, "section");
244
+ report.diagnose("collection_omitted", "Section exceeds DOCS_MAX_SECTIONS", candidate.path, [id]);
245
+ }
246
+ for (const limit of candidates.limits) {
247
+ if (limit.kind === "collection_budget") {
248
+ report.missingWork = true;
249
+ report.diagnose("collection_omitted", limit.reason, limit.path, [], true, [...(limit.sections ?? [])]);
250
+ }
251
+ else
252
+ report.diagnose("dynamic_dependency", limit.reason, limit.path, [], false);
253
+ }
254
+ for (const limit of inventory.limits)
255
+ report.diagnose(limit.cause === "secret_pattern"
256
+ ? "secret_pattern"
257
+ : "collection_omitted", limit.reason, limit.path);
258
+ for (const limit of inventory.limits)
259
+ limitations.push({
260
+ fact: `${limit.path} : ${limit.reason}`,
261
+ next: "Read the omitted file; its documentation or imports are unchecked.",
262
+ });
263
+ for (const limit of candidates.limits) {
264
+ if (limit.kind === "collection_budget") {
265
+ const changedParts = collected.units.map((unit) => unit.file.split("/"));
266
+ const sections = (limit.sections ?? [])
267
+ .map((section) => {
268
+ const parts = (section.split(" § ")[0] ?? "").split("/");
269
+ const proximity = changedParts.reduce((best, changed) => {
270
+ let common = 0;
271
+ while (common < parts.length &&
272
+ common < changed.length &&
273
+ parts[common] === changed[common])
274
+ common++;
275
+ return Math.max(best, common);
276
+ }, 0);
277
+ return { section, proximity };
278
+ })
279
+ .sort((a, b) => b.proximity - a.proximity);
280
+ collection = {
281
+ kind: limit.kind,
282
+ count: sections.length,
283
+ sections: sections
284
+ .slice(0, DOCS_MAX_SECTIONS)
285
+ .map((item) => item.section),
286
+ truncated: sections.length > DOCS_MAX_SECTIONS,
287
+ };
288
+ }
289
+ else
290
+ limitations.push({
291
+ fact: `${limit.path} : ${limit.reason}`,
292
+ next: "Read the unresolved dependency; closure follows static imports only.",
293
+ });
294
+ }
295
+ for (const omitted of candidates.omitted)
296
+ unchecked.push(`${omitted.path} § ${omitted.heading} (DOCS_MAX_SECTIONS)`);
297
+ await Promise.all(candidates.candidates.map(async (candidate) => {
298
+ const label = `${candidate.path} § ${candidate.heading}`;
299
+ const reportId = `docs:${candidate.path}:${candidate.start}`;
300
+ if (candidate.units.some((unit) => unit.before === null && unit.after === null)) {
301
+ unchecked.push(`${label} (changed source unavailable)`);
302
+ report.diagnose("binary_or_non_utf8", "Changed source unavailable", label, [reportId]);
303
+ return;
304
+ }
305
+ if (budget?.kind === "session") {
306
+ unchecked.push(`${label} (${budget.message})`);
307
+ report.diagnose("session_budget", budget.message, label, [reportId]);
308
+ return;
309
+ }
310
+ const prepared = prepareDocsCheck(candidate);
311
+ const state = withEvidenceContext(prepared.state, evidenceContext);
312
+ if (JSON.stringify(state).length > STATE_MAX_CHARS ||
313
+ candidate.sentences.length + 1 > CHOICE_MAX_OPTIONS) {
314
+ unchecked.push(`${label} (state or pointer too large)`);
315
+ report.diagnose("evidence_too_large", "Section state or sentence pointer exceeds limits", label, [reportId]);
316
+ return;
317
+ }
318
+ if (signal?.aborted) {
319
+ unchecked.push(`${label} (budget exceeded or canceled)`);
320
+ report.diagnose("cancelled", "Collection budget exceeded or check canceled", label, [reportId]);
321
+ return;
322
+ }
323
+ const judgment = await client.judge(state, prepared.questions, {
324
+ signal,
325
+ groups: [["status", "sentence"]],
326
+ ...deps.runtime.session.requestGate(),
327
+ admissionCause: () => budget?.kind === "session" ? "session_budget" : "call_budget",
328
+ beforeRequest(questionCount) {
329
+ if (budget?.kind === "session")
330
+ return { ok: false, error: budget.message };
331
+ if (signal?.aborted)
332
+ return {
333
+ ok: false,
334
+ error: "Budget exceeded or call canceled.",
335
+ };
336
+ if (input.maxCalls !== undefined && sent >= input.maxCalls) {
337
+ budget = {
338
+ kind: "max_calls",
339
+ message: `max_calls=${input.maxCalls} reached`,
340
+ };
341
+ return { ok: false, error: budget.message };
342
+ }
343
+ const admitted = deps.runtime.session.admit(questionCount);
344
+ if (!admitted.ok)
345
+ budget = { kind: "session", message: admitted.error };
346
+ else
347
+ sent++;
348
+ return admitted;
349
+ },
350
+ onUsage: (usage) => deps.runtime.session.recordUsage(usage),
351
+ });
352
+ judgments.push(judgment);
353
+ if (!judgment.ok)
354
+ report.failure(judgment, [reportId]);
355
+ else {
356
+ const status = judgment.answers.status;
357
+ const pointer = judgment.answers.sentence;
358
+ const finding = readDocsJudgment(candidate, judgment.answers);
359
+ const item = report.items.get(reportId);
360
+ if (finding && item)
361
+ item.label = `${label} — ${finding.sentence ? JSON.stringify(finding.sentence.text) : "sentence not identified"} — after ${finding.units.map((unit) => `${unit.name} (${unit.file})`).join(", ")}`;
362
+ report.answer(reportId, status, { band: finding?.band ?? "verdict", reason: finding?.reason }, [
363
+ pointer ?? {
364
+ type: "unjudged",
365
+ reason: "Required sentence pointer missing",
366
+ },
367
+ ]);
368
+ }
369
+ if (!judgment.ok) {
370
+ unchecked.push(`${label} (${judgment.error})`);
371
+ return;
372
+ }
373
+ if (!judgment.answers.status ||
374
+ !judgment.answers.sentence ||
375
+ Object.values(judgment.answers).some((answer) => answer.type === "unjudged")) {
376
+ unchecked.push(`${label} (unjudged question)`);
377
+ const finding = readDocsJudgment(candidate, judgment.answers);
378
+ if (finding)
379
+ findings.push({
380
+ ...finding,
381
+ band: "unsure",
382
+ reason: "unjudged question: read the section",
383
+ });
384
+ return;
385
+ }
386
+ const finding = readDocsJudgment(candidate, judgment.answers);
387
+ if (finding) {
388
+ const incomplete = candidate.units.some((unit) => collected.limits.some((limit) => limit.file === unit.file));
389
+ findings.push(incomplete
390
+ ? {
391
+ ...finding,
392
+ band: "unsure",
393
+ reason: "incomplete change evidence: read the omitted pieces",
394
+ }
395
+ : finding);
396
+ }
397
+ }));
398
+ findings.sort((a, b) => a.section.path.localeCompare(b.section.path) ||
399
+ a.section.start - b.section.start);
400
+ unchecked.sort();
401
+ return finish();
402
+ }
403
+ catch (error) {
404
+ if (signal?.aborted)
405
+ return finish(timeout?.aborted ? "Docs budget exceeded." : "Docs check canceled.", "cancelled");
406
+ throw error;
407
+ }
408
+ }