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,450 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { collectAskFiles } from "../adapters/ask-files.js";
5
+ import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
6
+ import { shareGitInventory } from "../adapters/git-inventory.js";
7
+ import { hostUsage } from "../adapters/usage.js";
8
+ import { STATE_MAX_CHARS } from "../constants.js";
9
+ import { compileAsks, readAsks, reverseQuestions } from "../core/asks.js";
10
+ import { checkIntegrity } from "../core/integrity.js";
11
+ import { buildEnvelope } from "../core/output.js";
12
+ import { answerItemFromInput, buildResultReport, contextFromEvidence, known, unknown, } from "../core/result-report.js";
13
+ import { interpolate } from "../describe.js";
14
+ import { renderResultReport } from "../render.js";
15
+ import { ASK_FILES_DESCRIPTION } from "../texts/ask-files.js";
16
+ import { NOT_CONFIGURED } from "../texts/configuration.js";
17
+ import { asksParameter } from "./ask-schema.js";
18
+ export const askFilesParameters = Type.Object({
19
+ root: Type.Optional(Type.String()),
20
+ paths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
21
+ asks: asksParameter("files"),
22
+ max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
23
+ }, { additionalProperties: false });
24
+ export function createAskFilesTool(dependencies) {
25
+ const { host, runtime, exec: execute } = dependencies;
26
+ return {
27
+ name: "jev_ask_files",
28
+ label: "Jev ask files",
29
+ description: interpolate(ASK_FILES_DESCRIPTION, host.names),
30
+ parameters: askFilesParameters,
31
+ ...(host.isOmp
32
+ ? { approval: "read", loadMode: "essential" }
33
+ : {
34
+ promptSnippet: "Typed answers about many files from intents you declare, without reading them",
35
+ promptGuidelines: [
36
+ "jev_ask_files: put every ask in one call; declare intents (verify, classify, rate, decide), never raw questions",
37
+ "jev_ask_files: use it to decide what to read, then read only the files that matter",
38
+ ],
39
+ }),
40
+ async execute(_id, args, signal, _update, ctx) {
41
+ const client = dependencies.client;
42
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
43
+ exec: execute,
44
+ signal,
45
+ origin: dependencies.evidenceOrigin,
46
+ });
47
+ const evidenceContext = evidence.context;
48
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
49
+ const started = performance.now();
50
+ const exec = shareGitInventory(execute);
51
+ const plan = compileAsks(args.asks, { surface: "files" });
52
+ let refusalCause = "invalid_arguments";
53
+ let collectedPaths;
54
+ let costKnown = true;
55
+ const reportItems = [];
56
+ const diagnostics = [];
57
+ const actions = [];
58
+ const auxiliary = {
59
+ controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
60
+ passages: { fresh: 0, cache: 0, notJudged: 0 },
61
+ };
62
+ const diagnose = (cause, fact, scope, target) => {
63
+ const id = `diagnostic:${diagnostics.length}`, actionId = `action:${actions.length}`;
64
+ actions.push({
65
+ id: actionId,
66
+ code: cause === "invalid_root"
67
+ ? "correct_context"
68
+ : cause === "not_configured"
69
+ ? "configure_client"
70
+ : "inspect_native",
71
+ target: target === undefined
72
+ ? unknown("target not established")
73
+ : known(target),
74
+ scope,
75
+ condition: "Only after correcting context or supplying materially new evidence",
76
+ instruction: "Read the admitted files natively; do not repeat the unchanged Jev call.",
77
+ repeatUnchanged: false,
78
+ });
79
+ diagnostics.push({
80
+ id,
81
+ cause,
82
+ fact,
83
+ target: target === undefined
84
+ ? unknown("target not established")
85
+ : known(target),
86
+ origin: cause === "call_budget" || cause === "session_budget"
87
+ ? "budget"
88
+ : "collection",
89
+ scope,
90
+ effect: "blocking",
91
+ material: true,
92
+ omittedMembers: [],
93
+ memberCount: known(0),
94
+ actionIds: [actionId],
95
+ });
96
+ return { diagnosticIds: [id], actionIds: [actionId] };
97
+ };
98
+ const totals = {
99
+ calls: 0,
100
+ questions: 0,
101
+ cacheHits: 0,
102
+ cacheRequests: 0,
103
+ inputTokens: 0,
104
+ costUsd: 0,
105
+ };
106
+ const records = [];
107
+ let skipped = [];
108
+ const finish = (input) => {
109
+ const envelope = buildEnvelope({
110
+ ...input,
111
+ yield: { ...totals, elapsedMs: performance.now() - started },
112
+ });
113
+ runtime.session.record(envelope);
114
+ runtime.guide.deliver(ctx);
115
+ if (!reportItems.length && !diagnostics.length)
116
+ diagnose(input.refusal ? refusalCause : "collection_empty", input.refusal ?? "No file-question results collected", { kind: "call" });
117
+ if (!reportItems.length && collectedPaths && plan.ok)
118
+ for (const path of collectedPaths)
119
+ for (const reading of plan.readings)
120
+ reportItems.push({
121
+ id: `${path}:${reading.id}`,
122
+ kind: "file_question",
123
+ label: `${path} ${reading.label}`,
124
+ groupId: `${path}:group:${plan.groups.findIndex((group) => group.includes(reading.id))}`,
125
+ evidence: [
126
+ {
127
+ target: path,
128
+ canonicalPath: known(resolve(cwd, path)),
129
+ aliases: [],
130
+ side: "current",
131
+ revision: unknown("working tree evidence"),
132
+ },
133
+ ],
134
+ diagnosticIds: diagnostics.map((diagnostic) => diagnostic.id),
135
+ actionIds: actions.map((action) => action.id),
136
+ treatment: "not_judged",
137
+ source: "none",
138
+ });
139
+ for (const limitation of input.limitations ?? []) {
140
+ diagnose("evidence_limit", limitation.fact, { kind: "call" }, limitation.path);
141
+ const diagnostic = diagnostics.at(-1);
142
+ if (diagnostic)
143
+ diagnostic.effect = "reservation";
144
+ }
145
+ const report = buildResultReport({
146
+ tool: "jev_ask_files",
147
+ context: contextFromEvidence(evidenceContext, {
148
+ inventories: collectedPaths
149
+ ? [
150
+ {
151
+ id: "files",
152
+ kind: "repository",
153
+ rules: [
154
+ "Requested paths expanded with admission and pruning",
155
+ ],
156
+ restrictions: args.paths,
157
+ discovered: unknown("collector does not expose pre-pruning candidate count"),
158
+ considered: known(collectedPaths.length),
159
+ scopeRestricted: true,
160
+ criteria: [],
161
+ },
162
+ ]
163
+ : [],
164
+ }),
165
+ items: reportItems,
166
+ diagnostics,
167
+ actions,
168
+ metrics: {
169
+ ...totals,
170
+ costUsd: costKnown && collectedPaths ? totals.costUsd : undefined,
171
+ elapsedMs: performance.now() - started,
172
+ },
173
+ auxiliary,
174
+ total: collectedPaths && plan.ok
175
+ ? known(collectedPaths.length * plan.readings.length)
176
+ : unknown("file-question inventory not collected"),
177
+ refused: !!input.refusal && refusalCause !== "not_configured",
178
+ });
179
+ const usage = hostUsage(host.isOmp, {
180
+ inputTokens: totals.inputTokens,
181
+ costUsd: totals.costUsd,
182
+ });
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: renderResultReport(report, { details: envelope }),
188
+ },
189
+ ],
190
+ details: {
191
+ files: records,
192
+ skipped,
193
+ envelope,
194
+ evidenceContext,
195
+ result: report,
196
+ },
197
+ ...usage,
198
+ };
199
+ };
200
+ if (!evidence.ok) {
201
+ refusalCause = evidence.cause ?? "invalid_root";
202
+ return finish({ refusal: evidence.error });
203
+ }
204
+ if (!plan.ok)
205
+ return finish({ refusal: plan.error });
206
+ if (!client) {
207
+ refusalCause = "not_configured";
208
+ return finish({ refusal: NOT_CONFIGURED });
209
+ }
210
+ const collected = await collectAskFiles(cwd, args.paths, signal, exec);
211
+ if (!collected.ok) {
212
+ refusalCause = collected.cause ?? "file_unavailable";
213
+ return finish({ refusal: collected.error });
214
+ }
215
+ collectedPaths = collected.files.map((file) => file.path);
216
+ skipped = collected.skipped;
217
+ for (const skippedPath of skipped)
218
+ diagnose(collected.secret.includes(skippedPath)
219
+ ? "secret_pattern"
220
+ : "collection_omitted", skippedPath, { kind: "inventory", inventoryIds: ["files"] });
221
+ if (!collected.files.length && skipped.length)
222
+ return finish({
223
+ refusal: `No readable evidence: ${skipped.join("; ")}`,
224
+ });
225
+ const states = collected.files.map((file) => ({
226
+ file,
227
+ state: withEvidenceContext({ path: file.path, content: file.content }, evidenceContext),
228
+ }));
229
+ if (states.some(({ state }) => JSON.stringify(state).length > STATE_MAX_CHARS)) {
230
+ refusalCause = "evidence_too_large";
231
+ return finish({
232
+ refusal: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
233
+ });
234
+ }
235
+ let sent = 0;
236
+ let budget;
237
+ const options = {
238
+ signal,
239
+ ...runtime.session.requestGate(),
240
+ admissionCause: () => budget?.kind === "session"
241
+ ? "session_budget"
242
+ : "call_budget",
243
+ beforeRequest: (count) => {
244
+ if (args.max_calls !== undefined && sent >= args.max_calls) {
245
+ budget = {
246
+ kind: "max_calls",
247
+ message: `max_calls=${args.max_calls} reached`,
248
+ };
249
+ return { ok: false, error: budget.message };
250
+ }
251
+ const admission = runtime.session.admit(count);
252
+ if (!admission.ok) {
253
+ budget = { kind: "session", message: admission.error };
254
+ return admission;
255
+ }
256
+ sent++;
257
+ return admission;
258
+ },
259
+ onUsage: (usage) => runtime.session.recordUsage(usage),
260
+ };
261
+ const accumulate = (result) => {
262
+ totals.calls += result.calls ?? 0;
263
+ totals.questions += result.questions ?? 0;
264
+ totals.cacheHits += result.cacheHits ?? 0;
265
+ totals.cacheRequests += result.cacheRequests ?? 0;
266
+ totals.inputTokens += result.usage?.inputTokens ?? 0;
267
+ totals.costUsd += result.usage?.costUsd ?? 0;
268
+ if (!result.usage && result.calls !== 0)
269
+ costKnown = false;
270
+ };
271
+ const rows = await Promise.all(states.map(async ({ file, state }) => {
272
+ const integrity = checkIntegrity(state, plan.asks, [
273
+ {
274
+ ...file.identity,
275
+ insertedPath: resolve(cwd, file.path),
276
+ content: file.content,
277
+ insertedSha256: createHash("sha256")
278
+ .update(file.content)
279
+ .digest("hex"),
280
+ },
281
+ ]);
282
+ const initial = await client.judge(state, plan.questions, {
283
+ ...options,
284
+ groups: plan.groups,
285
+ });
286
+ accumulate(initial);
287
+ let reversed;
288
+ let reverseAnswers;
289
+ if (initial.ok) {
290
+ const questions = reverseQuestions(plan, initial.answers);
291
+ if (Object.keys(questions).length) {
292
+ reversed = await client.judge(state, questions, options);
293
+ accumulate(reversed);
294
+ reverseAnswers = reversed.ok
295
+ ? reversed.answers
296
+ : Object.fromEntries(Object.keys(questions).map((id) => [
297
+ id,
298
+ {
299
+ type: "unjudged",
300
+ reason: reversed && !reversed.ok
301
+ ? reversed.error
302
+ : "reverse control failed",
303
+ },
304
+ ]));
305
+ }
306
+ }
307
+ const answers = initial.ok
308
+ ? readAsks(plan, initial.answers, reverseAnswers)
309
+ : [];
310
+ return { file, initial, reversed, integrity, answers };
311
+ }));
312
+ const answers = [];
313
+ const unchecked = [];
314
+ const unjudged = [];
315
+ for (const row of rows) {
316
+ const readings = plan.readings;
317
+ for (const [index, reading] of readings.entries()) {
318
+ const groupId = `${row.file.path}:group:${plan.groups.findIndex((group) => group.includes(reading.id))}`;
319
+ const common = {
320
+ id: `${row.file.path}:${reading.id}`,
321
+ kind: "file_question",
322
+ label: `${row.file.path} ${reading.label}`,
323
+ groupId,
324
+ evidence: [
325
+ {
326
+ target: row.file.path,
327
+ canonicalPath: known(resolve(cwd, row.file.path)),
328
+ aliases: [],
329
+ side: "current",
330
+ revision: unknown("working tree evidence"),
331
+ },
332
+ ],
333
+ diagnosticIds: [],
334
+ actionIds: [],
335
+ };
336
+ const answer = row.answers[index];
337
+ if (answer &&
338
+ !answer.unjudged &&
339
+ (answer.source || answer.answer?.source))
340
+ reportItems.push(answerItemFromInput(common, row.integrity.controls.length
341
+ ? {
342
+ ...answer,
343
+ band: "unsure",
344
+ reason: row.integrity.controls
345
+ .map((control) => control.fact)
346
+ .join("; "),
347
+ }
348
+ : answer));
349
+ else {
350
+ const raw = row.initial.ok
351
+ ? row.initial.answers[reading.id]
352
+ : undefined;
353
+ const cause = raw?.type === "unjudged" ? raw.cause : row.initial.failureCause;
354
+ const links = diagnose(cause ??
355
+ (budget
356
+ ? budget.kind === "session"
357
+ ? "session_budget"
358
+ : "call_budget"
359
+ : "control_failure"), answer?.reason ??
360
+ (row.initial.ok
361
+ ? "Required group incomplete or response provenance not established"
362
+ : row.initial.error), { kind: "group", groupIds: [groupId] }, row.file.path);
363
+ reportItems.push({
364
+ ...common,
365
+ ...links,
366
+ treatment: "not_judged",
367
+ source: "none",
368
+ });
369
+ }
370
+ for (const id of [
371
+ reading.twin,
372
+ ...Object.values(reading.controls ?? {}),
373
+ ].filter((id) => !!id)) {
374
+ const control = row.initial.ok
375
+ ? row.initial.answers[id]
376
+ : undefined;
377
+ if (control && control.type !== "unjudged" && control.source)
378
+ auxiliary.controls[control.source]++;
379
+ else
380
+ auxiliary.controls.notJudged++;
381
+ }
382
+ const reverse = row.reversed?.ok
383
+ ? row.reversed.answers[reading.id]
384
+ : undefined;
385
+ if (row.reversed) {
386
+ if (reverse && reverse.type !== "unjudged" && reverse.source)
387
+ auxiliary.controls[reverse.source]++;
388
+ else
389
+ auxiliary.controls.notJudged++;
390
+ }
391
+ }
392
+ records.push({
393
+ path: row.file.path,
394
+ initial: row.initial,
395
+ ...(row.reversed ? { reversed: row.reversed } : {}),
396
+ });
397
+ if (!row.initial.ok) {
398
+ unchecked.push(row.file.path);
399
+ unjudged.push({
400
+ label: row.file.path,
401
+ reason: row.initial.error,
402
+ next: "read the supplied file natively; a new Jev call needs materially changed evidence or recovered service",
403
+ });
404
+ continue;
405
+ }
406
+ if (Object.values(row.initial.answers).some((answer) => answer.type === "unjudged") ||
407
+ (row.reversed &&
408
+ (!row.reversed.ok ||
409
+ Object.values(row.reversed.answers).some((answer) => answer.type === "unjudged"))))
410
+ unchecked.push(row.file.path);
411
+ for (const answer of row.answers)
412
+ answers.push({
413
+ ...answer,
414
+ label: `${row.file.path} ${answer.label}`,
415
+ ...(row.integrity.controls.length
416
+ ? {
417
+ band: "unsure",
418
+ reason: row.integrity.controls
419
+ .map((control) => control.fact)
420
+ .join("; "),
421
+ }
422
+ : {}),
423
+ });
424
+ }
425
+ return finish({
426
+ state: {
427
+ files: collected.files.length,
428
+ integrity: rows.some((row) => row.integrity.controls.length)
429
+ ? "failed"
430
+ : "ok",
431
+ warnings: plan.warnings.map((warning) => warning.fact),
432
+ },
433
+ answers,
434
+ unjudged,
435
+ unchecked,
436
+ budget,
437
+ lines: skipped.length
438
+ ? [{ type: "list", title: "skipped", items: skipped }]
439
+ : [],
440
+ limitations: [
441
+ ...plan.warnings,
442
+ ...rows.flatMap((row) => [
443
+ ...row.integrity.controls,
444
+ ...row.integrity.notes,
445
+ ]),
446
+ ],
447
+ });
448
+ },
449
+ };
450
+ }
@@ -0,0 +1,70 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { CHOICE_MAX_OPTIONS, SCORE_MAX_LEVELS, SCORE_MIN_LEVELS, } from "../constants.js";
3
+ const criteria = Type.Record(Type.String(), Type.String(), {
4
+ minProperties: 1,
5
+ maxProperties: CHOICE_MAX_OPTIONS,
6
+ });
7
+ const question = Type.Union([
8
+ Type.Object({
9
+ type: Type.Literal("bool"),
10
+ instructions: Type.String({ minLength: 1 }),
11
+ criteria: Type.Optional(Type.Object({
12
+ true: Type.Optional(Type.String()),
13
+ false: Type.Optional(Type.String()),
14
+ }, { additionalProperties: false })),
15
+ }, { additionalProperties: false }),
16
+ Type.Object({
17
+ type: Type.Literal("choice"),
18
+ instructions: Type.String({ minLength: 1 }),
19
+ criteria,
20
+ }, { additionalProperties: false }),
21
+ Type.Object({
22
+ type: Type.Literal("score"),
23
+ instructions: Type.String({ minLength: 1 }),
24
+ criteria: Type.Array(Type.String(), {
25
+ minItems: SCORE_MIN_LEVELS,
26
+ maxItems: SCORE_MAX_LEVELS,
27
+ }),
28
+ }, { additionalProperties: false }),
29
+ ]);
30
+ const fullAskSchema = Type.Union(createAskVariants({ about: Type.Optional(Type.String({ minLength: 1 })) }));
31
+ const filesAskSchema = Type.Union(createAskVariants({}).slice(0, -1));
32
+ export function askSchema(surface) {
33
+ return surface === "ask" ? fullAskSchema : filesAskSchema;
34
+ }
35
+ function createAskVariants(about) {
36
+ const variants = [
37
+ Type.Object({ intent: Type.Literal("free"), ...about, question }, { additionalProperties: false }),
38
+ Type.Object({ intent: Type.Literal("verify"), ...about, claims: criteria }, { additionalProperties: false }),
39
+ Type.Object({
40
+ intent: Type.Literal("classify"),
41
+ ...about,
42
+ categories: criteria,
43
+ pick: Type.Optional(Type.Union([Type.Literal("one"), Type.Literal("many")])),
44
+ by: Type.Optional(Type.String()),
45
+ }, { additionalProperties: false }),
46
+ Type.Object({ intent: Type.Literal("decide"), ...about, hypotheses: criteria }, { additionalProperties: false }),
47
+ Type.Object({
48
+ intent: Type.Literal("rate"),
49
+ ...about,
50
+ dimension: Type.String(),
51
+ levels: Type.Array(Type.String(), {
52
+ minItems: SCORE_MIN_LEVELS,
53
+ maxItems: SCORE_MAX_LEVELS,
54
+ }),
55
+ }, { additionalProperties: false }),
56
+ Type.Object({
57
+ intent: Type.Literal("locate"),
58
+ ...about,
59
+ target: Type.String(),
60
+ among: Type.Array(Type.String(), { minItems: 1 }),
61
+ count: Type.Optional(Type.Union([Type.Literal("one"), Type.Literal("many")])),
62
+ attribution: Type.Optional(Type.Boolean()),
63
+ }, { additionalProperties: false }),
64
+ ];
65
+ return variants;
66
+ }
67
+ export function asksParameter(surface) {
68
+ const ask = askSchema(surface);
69
+ return Type.Union([ask, Type.Array(ask, { minItems: 1 }), Type.String()]);
70
+ }