jev-agent-tools 0.2.0 → 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 (117) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +22 -14
  4. package/SECURITY.md +17 -1
  5. package/dist/adapters/ask-files.js +11 -2
  6. package/dist/adapters/ask-proof.js +63 -7
  7. package/dist/adapters/command.js +82 -29
  8. package/dist/adapters/docs.js +30 -10
  9. package/dist/adapters/evidence-context.js +119 -0
  10. package/dist/adapters/files.js +141 -16
  11. package/dist/adapters/find.js +34 -6
  12. package/dist/adapters/git-base.js +7 -1
  13. package/dist/adapters/git.js +51 -7
  14. package/dist/adapters/locate-file.js +47 -9
  15. package/dist/adapters/private-storage.js +14 -6
  16. package/dist/adapters/risk-callers.js +3 -0
  17. package/dist/adapters/shell.js +23 -7
  18. package/dist/adapters/test-inventory.js +10 -2
  19. package/dist/configuration.js +17 -7
  20. package/dist/constants.js +26 -5
  21. package/dist/core/ask-references.js +193 -109
  22. package/dist/core/asks.js +78 -7
  23. package/dist/core/locate.js +8 -8
  24. package/dist/core/output.js +17 -0
  25. package/dist/core/result-report.js +302 -0
  26. package/dist/core/secret-path.js +34 -0
  27. package/dist/core/state.js +8 -1
  28. package/dist/core/units.js +1 -1
  29. package/dist/jev/client.js +34 -12
  30. package/dist/mcp/protocol.js +50 -27
  31. package/dist/mcp/tools.js +20 -7
  32. package/dist/render.js +72 -0
  33. package/dist/report-schema.js +1356 -0
  34. package/dist/result-types.js +1 -0
  35. package/dist/texts/ask-files.js +3 -1
  36. package/dist/texts/ask.js +3 -1
  37. package/dist/texts/check-diff.js +7 -4
  38. package/dist/texts/find.js +7 -2
  39. package/dist/texts/guide.js +3 -16
  40. package/dist/texts/instructions.js +72 -0
  41. package/dist/texts/locate.js +7 -2
  42. package/dist/texts/select-tests.js +3 -1
  43. package/dist/tools/ask-files.js +248 -15
  44. package/dist/tools/ask.js +523 -62
  45. package/dist/tools/check-diff.js +222 -30
  46. package/dist/tools/docs-check.js +122 -13
  47. package/dist/tools/find.js +320 -27
  48. package/dist/tools/locate.js +317 -18
  49. package/dist/tools/review-report.js +230 -0
  50. package/dist/tools/select-tests.js +273 -19
  51. package/dist/tools/spec-check.js +119 -22
  52. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
  53. package/docs/agent-instructions.md +59 -30
  54. package/docs/design.md +13 -1
  55. package/docs/mcp.md +8 -6
  56. package/docs/tools/jev_ask.md +8 -5
  57. package/docs/tools/jev_ask_files.md +2 -1
  58. package/docs/tools/jev_check_diff.md +4 -1
  59. package/docs/tools/jev_find_files.md +2 -1
  60. package/docs/tools/jev_locate_in_file.md +5 -0
  61. package/docs/tools/jev_select_tests.md +4 -1
  62. package/package.json +1 -1
  63. package/rules/jev-ask.md +22 -1
  64. package/server.json +2 -2
  65. package/src/adapters/ask-files.ts +11 -3
  66. package/src/adapters/ask-proof.ts +69 -11
  67. package/src/adapters/command.ts +96 -33
  68. package/src/adapters/docs.ts +33 -14
  69. package/src/adapters/evidence-context.ts +169 -0
  70. package/src/adapters/files.ts +146 -16
  71. package/src/adapters/find.ts +37 -7
  72. package/src/adapters/git-base.ts +7 -1
  73. package/src/adapters/git.ts +61 -8
  74. package/src/adapters/locate-file.ts +51 -9
  75. package/src/adapters/private-storage.ts +17 -5
  76. package/src/adapters/risk-callers.ts +3 -0
  77. package/src/adapters/shell.ts +23 -7
  78. package/src/adapters/test-inventory.ts +12 -4
  79. package/src/configuration.ts +16 -2
  80. package/src/constants.ts +26 -5
  81. package/src/core/ask-references.ts +262 -146
  82. package/src/core/asks.ts +79 -7
  83. package/src/core/import-boundaries.ts +8 -3
  84. package/src/core/locate.ts +8 -5
  85. package/src/core/output.ts +34 -0
  86. package/src/core/result-report.ts +410 -0
  87. package/src/core/secret-path.ts +37 -0
  88. package/src/core/state.ts +8 -1
  89. package/src/core/units.ts +3 -2
  90. package/src/index.ts +3 -0
  91. package/src/jev/client.ts +54 -16
  92. package/src/jev/types.ts +18 -3
  93. package/src/mcp/protocol.ts +91 -41
  94. package/src/mcp/tools.ts +26 -13
  95. package/src/render.ts +109 -0
  96. package/src/report-schema.ts +1380 -0
  97. package/src/result-types.ts +234 -0
  98. package/src/result.ts +4 -1
  99. package/src/runtime.ts +6 -0
  100. package/src/texts/ask-files.ts +4 -1
  101. package/src/texts/ask.ts +8 -1
  102. package/src/texts/check-diff.ts +7 -4
  103. package/src/texts/find.ts +8 -2
  104. package/src/texts/guide.ts +8 -16
  105. package/src/texts/instructions.ts +98 -0
  106. package/src/texts/locate.ts +8 -2
  107. package/src/texts/run-end.ts +2 -2
  108. package/src/texts/select-tests.ts +4 -1
  109. package/src/tools/ask-files.ts +309 -14
  110. package/src/tools/ask.ts +700 -77
  111. package/src/tools/check-diff.ts +331 -28
  112. package/src/tools/docs-check.ts +241 -39
  113. package/src/tools/find.ts +386 -29
  114. package/src/tools/locate.ts +384 -19
  115. package/src/tools/review-report.ts +308 -0
  116. package/src/tools/select-tests.ts +479 -21
  117. package/src/tools/spec-check.ts +193 -19
@@ -1,5 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { canonicalPath } from "../adapters/canonical-path.js";
2
+ import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
3
3
  import { shareGitInventory } from "../adapters/git-inventory.js";
4
4
  import { readLocateFile, scanRange } from "../adapters/locate-file.js";
5
5
  import { loadSyntaxParser } from "../adapters/syntax.js";
@@ -8,12 +8,17 @@ import { CHOICE_MAX_OPTIONS, LOCATE_GRAY_MIN, LOCATE_MIN_KB, LOCATE_SECTION_MAX_
8
8
  import { locateDisplay, outlineEvidence, sectionOutline, sectionState, sectionStateFits, } from "../core/locate.js";
9
9
  import { buildEnvelope, } from "../core/output.js";
10
10
  import { needsReverse, pointerQuestion, readPointer } from "../core/pointer.js";
11
+ import { answerItemFromInput, buildResultReport, contextFromEvidence, known, unknown, } from "../core/result-report.js";
11
12
  import { sectionFile } from "../core/sections.js";
12
13
  import { interpolate } from "../describe.js";
13
- import { renderEnvelope } from "../render.js";
14
+ import { renderResultReport } from "../render.js";
14
15
  import { NOT_CONFIGURED } from "../texts/configuration.js";
15
16
  import { LOCATE_DESCRIPTION } from "../texts/locate.js";
16
- const parameters = Type.Object({ path: Type.String({ minLength: 1 }), goal: Type.String({ minLength: 1 }) }, { additionalProperties: false });
17
+ const parameters = Type.Object({
18
+ root: Type.Optional(Type.String()),
19
+ path: Type.String({ minLength: 1 }),
20
+ goal: Type.String({ minLength: 1 }),
21
+ }, { additionalProperties: false });
17
22
  export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
18
23
  const { host, runtime, exec: execute } = dependencies;
19
24
  return {
@@ -30,11 +35,87 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
30
35
  }),
31
36
  async execute(_id, args, signal, _update, ctx) {
32
37
  const client = dependencies.client;
33
- const cwd = await canonicalPath(ctx.cwd);
38
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
39
+ exec: execute,
40
+ signal,
41
+ origin: dependencies.evidenceOrigin,
42
+ });
43
+ const evidenceContext = evidence.context;
44
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
34
45
  const started = performance.now();
35
46
  const exec = shareGitInventory(execute);
36
47
  const results = [];
37
48
  const limitations = [];
49
+ const context = contextFromEvidence(evidenceContext);
50
+ const diagnostics = [];
51
+ const actions = [];
52
+ const auxiliary = {
53
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
54
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
55
+ };
56
+ let primary;
57
+ let control;
58
+ let requiredControl = false;
59
+ let failureCause = "invalid_response";
60
+ const raw = (answer) => answer.type === "choice"
61
+ ? Object.entries(answer.probabilities).map(([label, probability]) => ({
62
+ label,
63
+ value: label,
64
+ probability: known(probability),
65
+ }))
66
+ : [];
67
+ const diagnose = (cause, fact, members = []) => {
68
+ const id = `d${diagnostics.length + 1}`, actionId = `a${actions.length + 1}`;
69
+ const code = cause === "invalid_root"
70
+ ? "correct_context"
71
+ : cause === "not_configured"
72
+ ? "configure_client"
73
+ : cause === "transport_failure" || cause === "service_unavailable"
74
+ ? "recover_service"
75
+ : "inspect_native";
76
+ actions.push({
77
+ id: actionId,
78
+ code,
79
+ target: known(args.path),
80
+ scope: { kind: "call" },
81
+ condition: code === "correct_context"
82
+ ? "If the requested root is incorrect"
83
+ : code === "recover_service"
84
+ ? "Only after service recovery is observed"
85
+ : "If the range decision remains open",
86
+ instruction: code === "correct_context"
87
+ ? "Use the exact registered worktree top-level; no fallback was performed."
88
+ : code === "configure_client"
89
+ ? "Configure the Jev client or inspect the file natively."
90
+ : "Read or search the cited file within existing permissions; revisit Jev only with decisive new evidence or materially changed context.",
91
+ repeatUnchanged: false,
92
+ });
93
+ diagnostics.push({
94
+ id,
95
+ cause,
96
+ fact,
97
+ target: known(cause === "invalid_root" ? (args.root ?? cwd) : args.path),
98
+ origin: cause === "invalid_root" || cause === "forbidden_path"
99
+ ? "input"
100
+ : cause === "control_failure"
101
+ ? "control"
102
+ : cause === "session_budget"
103
+ ? "budget"
104
+ : cause === "service_unavailable" ||
105
+ cause === "transport_failure" ||
106
+ cause === "invalid_response" ||
107
+ cause === "provider_context_refusal" ||
108
+ cause === "not_configured"
109
+ ? "provider"
110
+ : "collection",
111
+ scope: { kind: "call" },
112
+ effect: "blocking",
113
+ material: true,
114
+ omittedMembers: members,
115
+ memberCount: known(members.length),
116
+ actionIds: [actionId],
117
+ });
118
+ };
38
119
  const finish = (input) => {
39
120
  const usage = results.reduce((sum, result) => ({
40
121
  inputTokens: sum.inputTokens + (result.usage?.inputTokens ?? 0),
@@ -61,23 +142,159 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
61
142
  elapsedMs: performance.now() - started,
62
143
  },
63
144
  });
145
+ if (input.refusal)
146
+ diagnose(failureCause, input.refusal);
147
+ if (input.answers?.[0] && primary?.source)
148
+ actions.push({
149
+ id: `a${actions.length + 1}`,
150
+ code: "inspect_native",
151
+ target: known(input.answers[0].label === "none"
152
+ ? args.path
153
+ : input.answers[0].label),
154
+ scope: { kind: "group", groupIds: ["pointer"] },
155
+ condition: "If the range decision remains open",
156
+ instruction: input.answers[0].label === "none"
157
+ ? "Search elsewhere for the goal; read any cited alternative range if the result is unsure. Do not retry unchanged evidence."
158
+ : "Read the current primary and alternative ranges; retain uncertainty if no decisive new evidence is available. Do not retry unchanged evidence.",
159
+ repeatUnchanged: false,
160
+ });
161
+ if (!input.refusal &&
162
+ (!primary?.source || (requiredControl && !control?.source)))
163
+ diagnose("invalid_response", "pointer decision lacks established answer provenance or its required control");
164
+ const common = {
165
+ id: "pointer",
166
+ kind: "pointer",
167
+ label: args.path,
168
+ groupId: "pointer",
169
+ evidence: primary
170
+ ? [
171
+ {
172
+ target: args.path,
173
+ canonicalPath: evidenceContext.effectiveRoot
174
+ ? known(`${evidenceContext.effectiveRoot.path}/${args.path}`)
175
+ : unknown("file admission not established"),
176
+ aliases: [],
177
+ side: "current",
178
+ revision: context.resolvedBase,
179
+ },
180
+ ]
181
+ : [],
182
+ diagnosticIds: diagnostics.map((d) => d.id),
183
+ actionIds: actions.map((a) => a.id),
184
+ };
185
+ if (input.answers?.[0])
186
+ common.label = input.answers[0].label;
187
+ const displayed = input.answers?.[0];
188
+ let item = { ...common, treatment: "not_judged", source: "none" };
189
+ if (displayed &&
190
+ primary?.type === "choice" &&
191
+ primary.source &&
192
+ (!requiredControl || (control?.type === "choice" && control.source))) {
193
+ auxiliary.passages[primary.source]--;
194
+ item = answerItemFromInput(common, {
195
+ ...displayed,
196
+ answer: primary,
197
+ publicResult: displayed.value?.head ?? primary.choice,
198
+ reason: [...limitations, ...(input.limitations ?? [])]
199
+ .map((limit) => limit.fact)
200
+ .join("; "),
201
+ rawValues: raw(primary),
202
+ reportControls: control?.type === "choice" && control.source
203
+ ? [
204
+ {
205
+ id: "pointer-order",
206
+ kind: "order",
207
+ source: control.source,
208
+ outcome: displayed.orderDependent
209
+ ? "order-dependent"
210
+ : "consistent",
211
+ rawValues: raw(control),
212
+ },
213
+ ]
214
+ : [],
215
+ });
216
+ }
217
+ const report = buildResultReport({
218
+ tool: "jev_locate_in_file",
219
+ context,
220
+ items: [item],
221
+ diagnostics,
222
+ actions,
223
+ metrics: {
224
+ ...metrics,
225
+ costUsd: results.every((r) => !r.calls || r.usage !== undefined)
226
+ ? usage.costUsd
227
+ : undefined,
228
+ elapsedMs: performance.now() - started,
229
+ },
230
+ auxiliary,
231
+ total: known(1),
232
+ refused: Boolean(input.refusal &&
233
+ (failureCause === "invalid_root" ||
234
+ failureCause === "forbidden_path" ||
235
+ failureCause === "outside_inventory" ||
236
+ failureCause === "group_too_large" ||
237
+ failureCause === "evidence_too_large")),
238
+ });
64
239
  runtime.session.record(envelope);
65
240
  runtime.guide.deliver(ctx);
66
241
  return {
67
- content: [{ type: "text", text: renderEnvelope(envelope) }],
68
- details: { results },
242
+ content: [
243
+ {
244
+ type: "text",
245
+ text: renderResultReport(report, {
246
+ details: {
247
+ lines: [
248
+ ...envelope.lines,
249
+ {
250
+ type: "list",
251
+ title: "pointer reservations",
252
+ items: [...limitations, ...(input.limitations ?? [])].map((limit) => limit.fact),
253
+ },
254
+ ...(displayed
255
+ ? [
256
+ {
257
+ type: "list",
258
+ title: "read ranges",
259
+ items: [
260
+ ...(displayed.label === "none"
261
+ ? []
262
+ : [displayed.label]),
263
+ ...(displayed.candidates ?? [])
264
+ .filter((candidate) => candidate.label !== "none")
265
+ .map((candidate) => `${candidate.label} (${candidate.value.p})`),
266
+ ],
267
+ },
268
+ ]
269
+ : []),
270
+ ],
271
+ },
272
+ }),
273
+ },
274
+ ],
275
+ details: { results, evidenceContext, result: report },
69
276
  ...hostUsage(host.isOmp, results.some((r) => r.usage) ? usage : undefined),
70
277
  };
71
278
  };
279
+ if (!evidence.ok) {
280
+ failureCause = evidence.cause ?? "invalid_root";
281
+ return finish({ refusal: evidence.error });
282
+ }
283
+ failureCause = "file_unavailable";
72
284
  const file = await readLocateFile(cwd, args.path, signal, exec);
73
- if (!file.ok)
285
+ if (!file.ok) {
286
+ failureCause = file.cause ?? "file_unavailable";
74
287
  return finish({ refusal: file.error });
288
+ }
289
+ failureCause = "outside_inventory";
75
290
  if (file.bytes < LOCATE_MIN_KB * 1000)
76
291
  return finish({
77
292
  refusal: `small file (${file.lines} lines): read it directly`,
78
293
  });
79
- if (!client)
294
+ if (!client) {
295
+ failureCause = "not_configured";
80
296
  return finish({ refusal: NOT_CONFIGURED });
297
+ }
81
298
  let sections;
82
299
  if (file.kind === "whole") {
83
300
  const parser = await loadParser();
@@ -115,8 +332,56 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
115
332
  });
116
333
  }
117
334
  const sectionCount = sections.length;
335
+ context.inventories.push({
336
+ id: "sections",
337
+ kind: "sections",
338
+ rules: [
339
+ file.kind === "whole"
340
+ ? "declarations or heuristic sections from complete file"
341
+ : "bounded window outline",
342
+ ],
343
+ restrictions: [
344
+ args.path,
345
+ ...sections.map((section) => `${section.id}: ${section.start}-${section.end}`),
346
+ ],
347
+ discovered: known(sectionCount),
348
+ considered: known(sectionCount),
349
+ scopeRestricted: true,
350
+ criteria: [],
351
+ });
352
+ if (!sectionCount) {
353
+ diagnose("collection_empty", "no sections discovered; no pointer judgment");
354
+ return finish({});
355
+ }
356
+ failureCause = "group_too_large";
118
357
  const judge = async (candidates, outline = false) => {
119
- const state = sectionState(args.path, args.goal, candidates, outline);
358
+ primary = undefined;
359
+ control = undefined;
360
+ requiredControl = false;
361
+ const inventory = context.inventories.find((inventory) => inventory.id === "pointer-candidates");
362
+ const restricted = {
363
+ id: "pointer-candidates",
364
+ kind: "sections",
365
+ rules: ["actual candidates for the latest pointer stage"],
366
+ restrictions: [
367
+ args.path,
368
+ ...candidates.map((section) => `${section.id}: ${section.start}-${section.end}`),
369
+ ],
370
+ discovered: known(candidates.length),
371
+ considered: known(candidates.length),
372
+ scopeRestricted: true,
373
+ criteria: [],
374
+ };
375
+ if (inventory)
376
+ Object.assign(inventory, restricted);
377
+ else
378
+ context.inventories.push(restricted);
379
+ const state = withEvidenceContext(sectionState(args.path, args.goal, candidates, outline), evidenceContext);
380
+ if (JSON.stringify(state).length > STATE_MAX_CHARS)
381
+ return {
382
+ ok: false,
383
+ error: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
384
+ };
120
385
  const ask = async (reverse) => {
121
386
  const result = await client.judge(state, {
122
387
  pointer: pointerQuestion({
@@ -129,12 +394,36 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
129
394
  signal,
130
395
  ...runtime.session.requestGate(),
131
396
  beforeRequest: (n) => runtime.session.admit(n),
397
+ admissionCause: () => "session_budget",
132
398
  onUsage: (usage) => runtime.session.recordUsage(usage),
133
399
  });
134
400
  results.push(result);
401
+ if (!result.ok) {
402
+ failureCause = result.failureCause ?? "invalid_response";
403
+ if (reverse)
404
+ auxiliary.controls.notJudged++;
405
+ else
406
+ auxiliary.passages.notJudged++;
407
+ }
135
408
  if (!result.ok)
136
409
  return { ok: false, error: result.error };
137
410
  const answer = result.answers.pointer;
411
+ if (reverse)
412
+ control = answer;
413
+ else
414
+ primary = answer;
415
+ if (answer?.type === "unjudged")
416
+ failureCause = answer.cause ?? "invalid_response";
417
+ if (answer?.type === "choice" && answer.source) {
418
+ if (reverse)
419
+ auxiliary.controls[answer.source]++;
420
+ else
421
+ auxiliary.passages[answer.source]++;
422
+ }
423
+ else if (reverse)
424
+ auxiliary.controls.notJudged++;
425
+ else
426
+ auxiliary.passages.notJudged++;
138
427
  return answer?.type === "choice"
139
428
  ? { ok: true, probabilities: answer.probabilities }
140
429
  : {
@@ -149,24 +438,32 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
149
438
  return first;
150
439
  let pointer = readPointer(first.probabilities);
151
440
  if (needsReverse(first.probabilities, candidates.length)) {
441
+ requiredControl = true;
152
442
  const second = await ask(true);
153
- if (!second.ok)
443
+ if (!second.ok) {
444
+ if (failureCause === "invalid_response")
445
+ failureCause = "control_failure";
154
446
  return second;
447
+ }
155
448
  pointer = readPointer(first.probabilities, second.probabilities);
156
449
  }
157
450
  return { ok: true, ...pointer };
158
451
  };
159
- const fits = (candidates) => sectionStateFits(args.path, args.goal, candidates);
452
+ const emptyState = sectionState(args.path, args.goal, []);
453
+ const contextChars = JSON.stringify(withEvidenceContext(emptyState, evidenceContext))
454
+ .length - JSON.stringify(emptyState).length;
455
+ const evidenceMaxChars = STATE_MAX_CHARS - contextChars;
456
+ const fits = (candidates) => sectionStateFits(args.path, args.goal, candidates, false, evidenceMaxChars);
160
457
  const twoStage = file.kind === "outline" || !fits(sections);
161
458
  let planUnsure = false;
162
459
  if (twoStage) {
163
- const outline = sectionOutline(args.path, args.goal, sections);
460
+ const outline = sectionOutline(args.path, args.goal, sections, evidenceMaxChars);
164
461
  if (outline.length < 2 || outline.length >= CHOICE_MAX_OPTIONS)
165
462
  return finish({
166
463
  refusal: "No usable multi-section plan fits the budget; use grep or read directly.",
167
464
  });
168
- const evidence = outlineEvidence(args.path, args.goal, outline);
169
- if (!sectionStateFits(args.path, args.goal, evidence))
465
+ const evidence = outlineEvidence(args.path, args.goal, outline, evidenceMaxChars);
466
+ if (!fits(evidence))
170
467
  return finish({
171
468
  refusal: "Block plan exceeds the state budget; use grep.",
172
469
  });
@@ -217,8 +514,10 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
217
514
  }
218
515
  if (file.kind === "outline") {
219
516
  const selected = await scanRange(cwd, args.path, block, signal, exec);
220
- if (!selected.ok)
517
+ if (!selected.ok) {
518
+ failureCause = selected.cause ?? "file_unavailable";
221
519
  return finish({ refusal: selected.error });
520
+ }
222
521
  const lines = selected.text.split("\n");
223
522
  sections = sections
224
523
  .filter((s) => s.start >= block.start && s.end <= block.end)
@@ -271,7 +570,7 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
271
570
  const displayed = locateDisplay(args.path, sections, pointer.ranked, shrunk || planUnsure);
272
571
  if (!displayed)
273
572
  return finish({ refusal: "Pointer probabilities missing." });
274
- const { primary, band, candidates } = displayed;
573
+ const { primary: displayedPrimary, band, candidates } = displayed;
275
574
  if (band === "unsure" || head.id === "none")
276
575
  limitations.push({
277
576
  fact: head.id === "none"
@@ -282,14 +581,14 @@ export function createLocateTool(dependencies, loadParser = loadSyntaxParser) {
282
581
  return finish({
283
582
  answers: [
284
583
  {
285
- ...primary,
584
+ ...displayedPrimary,
286
585
  band,
287
586
  orderDependent: pointer.orderDependent,
288
587
  candidates,
289
588
  },
290
589
  ],
291
590
  lines: head.id !== "none"
292
- ? [{ type: "list", title: "read", items: [primary.label] }]
591
+ ? [{ type: "list", title: "read", items: [displayedPrimary.label] }]
293
592
  : [],
294
593
  limitations: [
295
594
  {
@@ -0,0 +1,230 @@
1
+ import { answerItemFromInput, buildResultReport, contextFromEvidence, known, unknown, } from "../core/result-report.js";
2
+ export function reportMetrics(envelope) {
3
+ const line = envelope.lines.find((line) => line.type === "yield");
4
+ if (line?.type !== "yield")
5
+ throw new Error("Report metrics missing from envelope");
6
+ return line.value;
7
+ }
8
+ export function controlsFor(id, judgment, witnessIds) {
9
+ const batches = judgment.batches?.filter((batch) => batch.questionIds.includes(id));
10
+ const responses = batches?.length
11
+ ? batches.map((batch) => batch.answers)
12
+ : [judgment.ok ? judgment.answers : {}];
13
+ return responses.flatMap((answers) => witnessIds.map((witnessId) => answers[witnessId] ?? {
14
+ type: "unjudged",
15
+ reason: "Required witness response unavailable",
16
+ }));
17
+ }
18
+ /** Producer-owned inventory: register before admission, retain negative answers too. */
19
+ export class ReviewReport {
20
+ items = new Map();
21
+ diagnostics = [];
22
+ actions = [];
23
+ inventories = [];
24
+ auxiliary = {
25
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
26
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
27
+ };
28
+ refusal = false;
29
+ missingWork = false;
30
+ totalUnknown = false;
31
+ countControls(judgment, ids) {
32
+ const batches = judgment.batches ?? [
33
+ { answers: judgment.ok ? judgment.answers : {}, questionIds: ids },
34
+ ];
35
+ for (const batch of batches)
36
+ for (const id of ids) {
37
+ if (!batch.questionIds.includes(id) && judgment.batches)
38
+ continue;
39
+ const answer = batch.answers[id];
40
+ this.auxiliary.controls[answer && answer.type !== "unjudged" && answer.source
41
+ ? answer.source
42
+ : "notJudged"]++;
43
+ }
44
+ }
45
+ expect(id, label, kind, groupId = id) {
46
+ if (!this.items.has(id))
47
+ this.items.set(id, {
48
+ id,
49
+ label,
50
+ kind,
51
+ groupId,
52
+ evidence: [],
53
+ diagnosticIds: [],
54
+ actionIds: [],
55
+ treatment: "not_judged",
56
+ source: "none",
57
+ });
58
+ }
59
+ diagnose(cause, fact, target, itemIds = [], blocking = true, omittedMembers = []) {
60
+ const id = `diagnostic:${this.diagnostics.length}`;
61
+ const actionId = `action:${this.actions.length}`;
62
+ const scope = itemIds.length
63
+ ? { kind: "item", itemIds }
64
+ : { kind: "call" };
65
+ const code = cause === "invalid_root" || cause === "invalid_base"
66
+ ? "correct_context"
67
+ : cause === "not_configured"
68
+ ? "configure_client"
69
+ : cause === "service_unavailable" || cause === "transport_failure"
70
+ ? "recover_service"
71
+ : "inspect_native";
72
+ this.actions.push({
73
+ id: actionId,
74
+ code,
75
+ target: target === undefined
76
+ ? unknown("target not established")
77
+ : known(target),
78
+ scope,
79
+ condition: "Only after the named context, evidence, or service condition materially changes; otherwise use native inspection or the retained runner plan.",
80
+ instruction: code === "correct_context"
81
+ ? "Correct the named repository root or base if it does not match the intended comparison."
82
+ : code === "configure_client"
83
+ ? "Configure the required Jev endpoint before requesting judgment; use native inspection meanwhile."
84
+ : code === "recover_service"
85
+ ? "Establish service recovery before another useful judgment request; do not repeat the unchanged call."
86
+ : "Inspect the named evidence natively or execute the retained runner plan; preserve this limitation.",
87
+ repeatUnchanged: false,
88
+ });
89
+ this.diagnostics.push({
90
+ id,
91
+ cause,
92
+ fact,
93
+ target: target === undefined
94
+ ? unknown("target not established")
95
+ : known(target),
96
+ origin: cause === "control_failure"
97
+ ? "control"
98
+ : cause === "call_budget" || cause === "session_budget"
99
+ ? "budget"
100
+ : cause === "not_configured" ||
101
+ cause === "service_unavailable" ||
102
+ cause === "transport_failure" ||
103
+ cause === "invalid_response" ||
104
+ cause === "provider_context_refusal"
105
+ ? "provider"
106
+ : "collection",
107
+ scope,
108
+ effect: blocking ? "blocking" : "reservation",
109
+ material: true,
110
+ omittedMembers,
111
+ memberCount: known(omittedMembers.length),
112
+ actionIds: [actionId],
113
+ });
114
+ for (const itemId of itemIds) {
115
+ const item = this.items.get(itemId);
116
+ if (item) {
117
+ item.diagnosticIds.push(id);
118
+ item.actionIds.push(actionId);
119
+ }
120
+ }
121
+ }
122
+ static(id, reason, selected) {
123
+ const item = this.items.get(id);
124
+ if (!item)
125
+ throw new Error(`Unregistered review result ${id}`);
126
+ this.items.set(id, {
127
+ ...item,
128
+ treatment: "static",
129
+ source: "none",
130
+ staticReason: reason,
131
+ ...(selected === undefined ? {} : { selection: { selected, reason } }),
132
+ });
133
+ }
134
+ answer(id, answer, input = {}, controls = [], countControls = true) {
135
+ const item = this.items.get(id);
136
+ if (!item)
137
+ throw new Error(`Unregistered review result ${id}`);
138
+ const unavailable = !answer || answer.type === "unjudged" || !answer.source;
139
+ const missingControl = controls.some((control) => control.type === "unjudged" || !control.source);
140
+ if (countControls)
141
+ for (const control of controls)
142
+ this.auxiliary.controls[control.type === "unjudged" || !control.source
143
+ ? "notJudged"
144
+ : control.source]++;
145
+ if (unavailable || missingControl) {
146
+ this.diagnose(missingControl
147
+ ? "control_failure"
148
+ : answer?.type === "unjudged"
149
+ ? (answer.cause ?? "invalid_response")
150
+ : "invalid_response", missingControl
151
+ ? "Required control response is unavailable; the requested group has no admissible judgment."
152
+ : answer?.type === "unjudged"
153
+ ? answer.reason
154
+ : "Requested answer or its transport provenance is unavailable.", item.label, [id]);
155
+ return;
156
+ }
157
+ const valid = answer;
158
+ const rawValues = valid.type === "bool"
159
+ ? [{ label: "true", value: true, probability: known(valid.p) }]
160
+ : Object.entries(valid.probabilities).map(([label, p]) => ({
161
+ label,
162
+ value: label,
163
+ probability: known(p),
164
+ }));
165
+ this.items.set(id, answerItemFromInput(item, {
166
+ label: item.label,
167
+ answer: valid,
168
+ source: valid.source,
169
+ band: "verdict",
170
+ rawValues,
171
+ ...input,
172
+ reportControls: controls.map((control, index) => {
173
+ const source = control.source;
174
+ if (!source)
175
+ throw new Error("Admitted control lacks provenance");
176
+ return {
177
+ id: `${id}:control:${index}`,
178
+ kind: "integrity",
179
+ source,
180
+ outcome: control.type === "bool"
181
+ ? String(control.p)
182
+ : control.type === "choice"
183
+ ? control.choice
184
+ : control.type === "score"
185
+ ? String(control.score)
186
+ : control.reason,
187
+ rawValues: control.type === "choice" || control.type === "score"
188
+ ? Object.entries(control.probabilities).map(([label, p]) => ({
189
+ label,
190
+ value: label,
191
+ probability: known(p),
192
+ }))
193
+ : control.type === "bool"
194
+ ? [
195
+ {
196
+ label: "true",
197
+ value: true,
198
+ probability: known(control.p),
199
+ },
200
+ ]
201
+ : [],
202
+ };
203
+ }),
204
+ }));
205
+ }
206
+ failure(judgment, ids, cause) {
207
+ this.diagnose(cause ??
208
+ (!judgment.ok ? judgment.cause : undefined) ??
209
+ judgment.failureCause ??
210
+ "invalid_response", judgment.ok ? "Required responses unavailable" : judgment.error, undefined, ids);
211
+ }
212
+ build(tool, evidence, metrics) {
213
+ const context = contextFromEvidence(evidence);
214
+ context.inventories = this.inventories;
215
+ return buildResultReport({
216
+ tool,
217
+ context,
218
+ items: [...this.items.values()],
219
+ diagnostics: this.diagnostics,
220
+ actions: this.actions,
221
+ metrics,
222
+ auxiliary: this.auxiliary,
223
+ total: this.totalUnknown || (!this.inventories.length && !this.items.size)
224
+ ? unknown("Expected inventory was not completely enumerable")
225
+ : known(this.items.size),
226
+ refused: this.refusal,
227
+ missingWork: this.missingWork,
228
+ });
229
+ }
230
+ }