jev-agent-tools 0.1.3

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 (95) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -0
  4. package/docs/design.md +187 -0
  5. package/docs/tools/jev_ask.md +92 -0
  6. package/docs/tools/jev_ask_files.md +80 -0
  7. package/docs/tools/jev_check_diff.md +77 -0
  8. package/docs/tools/jev_find_files.md +59 -0
  9. package/docs/tools/jev_locate_in_file.md +51 -0
  10. package/docs/tools/jev_select_tests.md +59 -0
  11. package/package.json +85 -0
  12. package/rules/jev-ask.md +4 -0
  13. package/src/adapters/analysis-context.ts +100 -0
  14. package/src/adapters/ask-files.ts +236 -0
  15. package/src/adapters/ask-proof.ts +202 -0
  16. package/src/adapters/ask-syntax.ts +463 -0
  17. package/src/adapters/command.ts +240 -0
  18. package/src/adapters/docs.ts +222 -0
  19. package/src/adapters/files.ts +411 -0
  20. package/src/adapters/find.ts +151 -0
  21. package/src/adapters/git-base.ts +32 -0
  22. package/src/adapters/git-inventory.ts +94 -0
  23. package/src/adapters/git.ts +525 -0
  24. package/src/adapters/locate-file.ts +197 -0
  25. package/src/adapters/output-lines.ts +50 -0
  26. package/src/adapters/risk-callers.ts +525 -0
  27. package/src/adapters/runner-version.ts +102 -0
  28. package/src/adapters/syntax.ts +229 -0
  29. package/src/adapters/test-inventory.ts +168 -0
  30. package/src/adapters/usage.ts +21 -0
  31. package/src/adapters/utf8.ts +57 -0
  32. package/src/constants.ts +109 -0
  33. package/src/core/ask-closure.ts +419 -0
  34. package/src/core/ask-proof.ts +32 -0
  35. package/src/core/ask-references.ts +249 -0
  36. package/src/core/asks.ts +616 -0
  37. package/src/core/batches.ts +83 -0
  38. package/src/core/command-output.ts +249 -0
  39. package/src/core/diff.ts +226 -0
  40. package/src/core/docs.ts +399 -0
  41. package/src/core/find.ts +157 -0
  42. package/src/core/git.ts +5 -0
  43. package/src/core/import-boundaries.ts +102 -0
  44. package/src/core/imports.ts +691 -0
  45. package/src/core/integrity.ts +64 -0
  46. package/src/core/lexical.ts +130 -0
  47. package/src/core/locate.ts +213 -0
  48. package/src/core/output.ts +264 -0
  49. package/src/core/pointer.ts +51 -0
  50. package/src/core/risk-callers.ts +1270 -0
  51. package/src/core/runner-version.ts +66 -0
  52. package/src/core/sections.ts +269 -0
  53. package/src/core/state.ts +53 -0
  54. package/src/core/syntax.ts +8 -0
  55. package/src/core/test-commands.ts +430 -0
  56. package/src/core/test-coverage.ts +103 -0
  57. package/src/core/test-discovery.ts +1695 -0
  58. package/src/core/test-evidence.ts +649 -0
  59. package/src/core/test-state.ts +99 -0
  60. package/src/core/truncate.ts +14 -0
  61. package/src/core/units.ts +531 -0
  62. package/src/describe.ts +26 -0
  63. package/src/guide.ts +42 -0
  64. package/src/host.ts +22 -0
  65. package/src/index.ts +40 -0
  66. package/src/jev/client.ts +505 -0
  67. package/src/jev/pool.ts +60 -0
  68. package/src/jev/types.ts +60 -0
  69. package/src/presets/docs.ts +85 -0
  70. package/src/presets/risk.ts +263 -0
  71. package/src/presets/spec.ts +111 -0
  72. package/src/presets/witnesses.ts +313 -0
  73. package/src/render.ts +45 -0
  74. package/src/result.ts +4 -0
  75. package/src/run-end.ts +157 -0
  76. package/src/runtime.ts +16 -0
  77. package/src/session.ts +106 -0
  78. package/src/texts/ask-files.ts +2 -0
  79. package/src/texts/ask.ts +4 -0
  80. package/src/texts/check-diff.ts +24 -0
  81. package/src/texts/configuration.ts +2 -0
  82. package/src/texts/find.ts +14 -0
  83. package/src/texts/guide.ts +16 -0
  84. package/src/texts/locate.ts +10 -0
  85. package/src/texts/run-end.ts +13 -0
  86. package/src/texts/select-tests.ts +3 -0
  87. package/src/tools/ask-files.ts +263 -0
  88. package/src/tools/ask-schema.ts +116 -0
  89. package/src/tools/ask.ts +925 -0
  90. package/src/tools/check-diff.ts +510 -0
  91. package/src/tools/docs-check.ts +399 -0
  92. package/src/tools/find.ts +529 -0
  93. package/src/tools/locate.ts +369 -0
  94. package/src/tools/select-tests.ts +746 -0
  95. package/src/tools/spec-check.ts +210 -0
@@ -0,0 +1,64 @@
1
+ import type { State } from "../jev/types.ts";
2
+ import type { Ask } from "./asks.ts";
3
+ import type { Limitation } from "./output.ts";
4
+ export interface FileIdentity {
5
+ requestedPath: string;
6
+ resolvedPath: string;
7
+ insertedPath: string;
8
+ readSha256: string;
9
+ insertedSha256: string;
10
+ content: string;
11
+ }
12
+ export function checkIntegrity(
13
+ state: State,
14
+ asks: readonly Ask[],
15
+ files: readonly FileIdentity[] = [],
16
+ output?: { text: string; truncated: boolean; omittedChars: number },
17
+ ): { controls: Limitation[]; notes: Limitation[] } {
18
+ const controls: Limitation[] = [];
19
+ const notes: Limitation[] = [];
20
+ for (const file of files) {
21
+ if (file.resolvedPath !== file.insertedPath || !file.content.length)
22
+ controls.push({
23
+ fact: `integrity: wrong identity or empty file ${file.requestedPath}`,
24
+ next: `add the complete file to paths: ${file.requestedPath}`,
25
+ });
26
+ if (file.readSha256 !== file.insertedSha256)
27
+ controls.push({
28
+ fact: `integrity: fingerprint mismatch ${file.requestedPath}`,
29
+ next: `read the complete file again: ${file.requestedPath}`,
30
+ });
31
+ }
32
+ const out =
33
+ output?.text ??
34
+ (typeof state.output === "string" ? state.output : undefined);
35
+ if (out !== undefined)
36
+ for (const ask of asks) {
37
+ if (ask.about !== undefined && ask.about !== "output") continue;
38
+ const statements =
39
+ ask.intent === "verify"
40
+ ? Object.values(ask.claims)
41
+ : ask.intent === "decide"
42
+ ? Object.values(ask.hypotheses)
43
+ : ask.intent === "classify"
44
+ ? Object.values(ask.categories)
45
+ : [];
46
+ for (const statement of statements)
47
+ for (const match of statement.matchAll(
48
+ /`([^`]{4,})`|"([^"\n]{4,})"/g,
49
+ )) {
50
+ const literal = match[1] ?? match[2] ?? "";
51
+ if (!out.includes(literal))
52
+ controls.push({
53
+ fact: `integrity: cited line not in state: ${literal}`,
54
+ next: "add the cited output through command or state",
55
+ });
56
+ }
57
+ }
58
+ if (output?.truncated)
59
+ notes.push({
60
+ fact: `output truncated, ${output.omittedChars} chars omitted`,
61
+ next: "narrow command to retain decisive lines",
62
+ });
63
+ return { controls, notes };
64
+ }
@@ -0,0 +1,130 @@
1
+ export interface LexicalToken {
2
+ value: string;
3
+ line: number;
4
+ offset: number;
5
+ string: boolean;
6
+ }
7
+ export interface LexicalSource {
8
+ tokenize(path: string, text: string, python?: boolean): LexicalToken[];
9
+ resolved?(path: string, text: string): void;
10
+ }
11
+ /** A lexical pass excludes comments and keeps literals separate from identifiers. */
12
+ export function tokenize(text: string, python = false): LexicalToken[] {
13
+ const tokens: LexicalToken[] = [];
14
+ let i = 0;
15
+ let line = 1;
16
+ while (i < text.length) {
17
+ const c = text[i] ?? "";
18
+ if (/\s/.test(c)) {
19
+ if (c === "\n") line++;
20
+ i++;
21
+ continue;
22
+ }
23
+ if (python ? c === "#" : text.slice(i, i + 2) === "//") {
24
+ while (i < text.length && text[i] !== "\n") i++;
25
+ continue;
26
+ }
27
+ if (!python && text.slice(i, i + 2) === "/*") {
28
+ i += 2;
29
+ while (i < text.length && text.slice(i, i + 2) !== "*/") {
30
+ if (text[i] === "\n") line++;
31
+ i++;
32
+ }
33
+ i += 2;
34
+ continue;
35
+ }
36
+ const start = i;
37
+ const firstLine = line;
38
+ const previous = tokens[tokens.length - 1];
39
+ // A slash can begin a regex only where an expression can begin, not after
40
+ // an operand (where it is division). Keep the entire literal opaque.
41
+ if (
42
+ !python &&
43
+ c === "/" &&
44
+ (!previous ||
45
+ (!previous.string &&
46
+ (/^[([{,:;=!?&|+*%~^<>-]$/.test(previous.value) ||
47
+ [
48
+ "return",
49
+ "throw",
50
+ "case",
51
+ "delete",
52
+ "void",
53
+ "typeof",
54
+ "yield",
55
+ "await",
56
+ "in",
57
+ "of",
58
+ ].includes(previous.value))))
59
+ ) {
60
+ let end = i + 1;
61
+ let characterClass = false;
62
+ while (end < text.length && text[end] !== "\n" && text[end] !== "\r") {
63
+ const next = text[end];
64
+ if (next === "\\") {
65
+ end += 2;
66
+ continue;
67
+ }
68
+ if (next === "[") characterClass = true;
69
+ else if (next === "]") characterClass = false;
70
+ else if (next === "/" && !characterClass) break;
71
+ end++;
72
+ }
73
+ if (text[end] === "/") {
74
+ end++;
75
+ while (/[a-z]/i.test(text[end] ?? "")) end++;
76
+ tokens.push({
77
+ value: text.slice(i, end),
78
+ line,
79
+ offset: start,
80
+ string: false,
81
+ });
82
+ i = end;
83
+ continue;
84
+ }
85
+ }
86
+ if (c === "'" || c === '"' || c === "`") {
87
+ const quote =
88
+ python && text.slice(i, i + 3) === c.repeat(3) ? c.repeat(3) : c;
89
+ i += quote.length;
90
+ let value = "";
91
+ let dynamic = false;
92
+ while (i < text.length && text.slice(i, i + quote.length) !== quote) {
93
+ if (text[i] === "\n") line++;
94
+ if (c === "`" && text.slice(i, i + 2) === "${") dynamic = true;
95
+ if (text[i] === "\\") {
96
+ i++;
97
+ const escaped = text[i++] ?? "";
98
+ value +=
99
+ escaped === "n"
100
+ ? "\n"
101
+ : escaped === "t"
102
+ ? "\t"
103
+ : escaped === "r"
104
+ ? "\r"
105
+ : escaped;
106
+ } else value += text[i++];
107
+ }
108
+ const closed = text.slice(i, i + quote.length) === quote;
109
+ if (closed) i += quote.length;
110
+ tokens.push({
111
+ value,
112
+ line: firstLine,
113
+ offset: start,
114
+ string: closed && !dynamic,
115
+ });
116
+ } else if (/[\w$]/.test(c)) {
117
+ while (i < text.length && /[\w$]/.test(text[i] ?? "")) i++;
118
+ tokens.push({
119
+ value: text.slice(start, i),
120
+ line,
121
+ offset: start,
122
+ string: false,
123
+ });
124
+ } else {
125
+ i++;
126
+ tokens.push({ value: c, line, offset: start, string: false });
127
+ }
128
+ }
129
+ return tokens;
130
+ }
@@ -0,0 +1,213 @@
1
+ import {
2
+ CHOICE_MAX_OPTIONS,
3
+ LOCATE_VERDICT_MIN,
4
+ STATE_MAX_CHARS,
5
+ } from "../constants.ts";
6
+ import type { State } from "../jev/types.ts";
7
+ import type { Candidate } from "./output.ts";
8
+ import type { Section } from "./sections.ts";
9
+ import { truncate } from "./truncate.ts";
10
+
11
+ export function sectionState(
12
+ path: string,
13
+ goal: string,
14
+ sections: readonly Section[],
15
+ outline = false,
16
+ ): State {
17
+ return {
18
+ path,
19
+ goal,
20
+ sections: sections.map((section) => ({
21
+ id: section.id,
22
+ lines: `${section.start}-${section.end}`,
23
+ text: outline ? section.label : section.text,
24
+ })),
25
+ };
26
+ }
27
+ export function sectionStateFits(
28
+ path: string,
29
+ goal: string,
30
+ sections: readonly Section[],
31
+ outline = false,
32
+ ): boolean {
33
+ return (
34
+ sections.length < CHOICE_MAX_OPTIONS &&
35
+ JSON.stringify(sectionState(path, goal, sections, outline)).length <=
36
+ STATE_MAX_CHARS
37
+ );
38
+ }
39
+ /** Every planned block has at least two sections and fits its full-text refinement budget. */
40
+ export function sectionOutline(
41
+ path: string,
42
+ goal: string,
43
+ sections: readonly Section[],
44
+ ): Section[] {
45
+ const base = JSON.stringify(sectionState(path, goal, [])).length;
46
+ const costs = new Map(
47
+ sections.map((section) => [
48
+ section,
49
+ JSON.stringify({
50
+ id: section.id,
51
+ lines: `${section.start}-${section.end}`,
52
+ text: "",
53
+ }).length +
54
+ (section.serializedTextChars ?? JSON.stringify(section.text).length) -
55
+ 2,
56
+ ]),
57
+ );
58
+ const fits = (items: readonly Section[]) =>
59
+ items.length < CHOICE_MAX_OPTIONS &&
60
+ base + items.reduce((sum, item) => sum + (costs.get(item) ?? 0) + 1, 0) <=
61
+ STATE_MAX_CHARS;
62
+ const groups: Section[][] = [];
63
+ let current: Section[] = [],
64
+ currentSize = base;
65
+ for (const section of sections) {
66
+ const length = (costs.get(section) ?? 0) + 1;
67
+ if (base + length > STATE_MAX_CHARS) return [];
68
+ const owner = section.label.split(".")[0],
69
+ previousOwner = current.at(-1)?.label.split(".")[0];
70
+ if (
71
+ current.length &&
72
+ (currentSize + length > STATE_MAX_CHARS ||
73
+ current.length + 1 >= CHOICE_MAX_OPTIONS ||
74
+ (current.length >= 2 &&
75
+ section.label.includes(".") &&
76
+ owner !== previousOwner))
77
+ ) {
78
+ groups.push(current);
79
+ current = [];
80
+ currentSize = base;
81
+ }
82
+ current.push(section);
83
+ currentSize += length;
84
+ }
85
+ if (current.length) groups.push(current);
86
+ for (let index = 0; index < groups.length; index++) {
87
+ const group = groups[index];
88
+ if (!group || group.length >= 2) continue;
89
+ const previous = groups[index - 1],
90
+ next = groups[index + 1];
91
+ if (previous && fits([...previous, ...group])) {
92
+ previous.push(...group);
93
+ groups.splice(index, 1);
94
+ index--;
95
+ continue;
96
+ }
97
+ if (next && fits([...group, ...next])) {
98
+ next.unshift(...group);
99
+ groups.splice(index, 1);
100
+ index--;
101
+ continue;
102
+ }
103
+ if (previous && previous.length > 2) {
104
+ const moved = previous.at(-1);
105
+ if (moved && fits([moved, ...group])) {
106
+ previous.pop();
107
+ group.unshift(moved);
108
+ continue;
109
+ }
110
+ }
111
+ if (next && next.length > 2) {
112
+ const moved = next[0];
113
+ if (moved && fits([...group, moved])) {
114
+ next.shift();
115
+ group.push(moved);
116
+ continue;
117
+ }
118
+ }
119
+ return [];
120
+ }
121
+ return groups.map((group, index) => ({
122
+ id: `B${index + 1}`,
123
+ start: group[0]?.start ?? 0,
124
+ end: group.at(-1)?.end ?? 0,
125
+ label: group.map((s) => s.label).join(" / "),
126
+ text: group.map((s) => s.text).join("\n"),
127
+ }));
128
+ }
129
+ /** JSON-escaped budget, allocated equally then redistributed to larger blocks. */
130
+ export function outlineEvidence(
131
+ path: string,
132
+ goal: string,
133
+ blocks: readonly Section[],
134
+ ): Section[] {
135
+ const overhead = JSON.stringify(
136
+ sectionState(
137
+ path,
138
+ goal,
139
+ blocks.map((block) => ({ ...block, text: "" })),
140
+ ),
141
+ ).length;
142
+ let remaining = Math.max(0, STATE_MAX_CHARS - overhead);
143
+ const allocations = blocks.map(() => 0);
144
+ let active = blocks.map((_, index) => index);
145
+ while (remaining > 0 && active.length) {
146
+ const share = Math.max(1, Math.floor(remaining / active.length));
147
+ const next: number[] = [];
148
+ for (const index of active) {
149
+ const block = blocks[index];
150
+ if (!block) continue;
151
+ const need =
152
+ JSON.stringify(block.text).length - 2 - (allocations[index] ?? 0);
153
+ const granted = Math.min(need, share, remaining);
154
+ allocations[index] = (allocations[index] ?? 0) + granted;
155
+ remaining -= granted;
156
+ if (need > granted) next.push(index);
157
+ }
158
+ active = next;
159
+ }
160
+ return blocks.map((block, index) => {
161
+ const budget = allocations[index] ?? 0;
162
+ if (JSON.stringify(block.text).length - 2 <= budget) return { ...block };
163
+ const excerpt = (chars: number) => {
164
+ const part = Math.floor(chars / 3);
165
+ const slice = (start: number) => {
166
+ if (start > 0 && /[\uDC00-\uDFFF]/.test(block.text[start] ?? ""))
167
+ start++;
168
+ return truncate(block.text.slice(start), part);
169
+ };
170
+ return `${slice(0)}\n…\n${slice(Math.max(0, Math.floor(block.text.length / 2) - Math.floor(part / 2)))}\n…\n${slice(Math.max(0, block.text.length - part))}`;
171
+ };
172
+ let low = 0,
173
+ high = Math.min(block.text.length, budget),
174
+ text = "";
175
+ while (low <= high) {
176
+ const middle = Math.floor((low + high) / 2),
177
+ candidate = excerpt(middle);
178
+ if (JSON.stringify(candidate).length - 2 <= budget) {
179
+ text = candidate;
180
+ low = middle + 1;
181
+ } else high = middle - 1;
182
+ }
183
+ return { ...block, text };
184
+ });
185
+ }
186
+ export function locateDisplay(
187
+ path: string,
188
+ sections: readonly Section[],
189
+ ranked: readonly { id: string; p: number }[],
190
+ forceUnsure: boolean,
191
+ ) {
192
+ const head = ranked[0];
193
+ if (!head) return undefined;
194
+ const display = (id: string, p: number): Candidate => {
195
+ const section = sections.find((s) => s.id === id);
196
+ return {
197
+ label: section ? `${path}:${section.start}-${section.end}` : "none",
198
+ value: { head: section?.label ?? "no section fits", p },
199
+ };
200
+ };
201
+ const band =
202
+ forceUnsure || head.p < LOCATE_VERDICT_MIN
203
+ ? ("unsure" as const)
204
+ : ("verdict" as const);
205
+ return {
206
+ primary: display(head.id, head.p),
207
+ band,
208
+ candidates:
209
+ band === "unsure"
210
+ ? ranked.slice(1, 2).map((s) => display(s.id, s.p))
211
+ : undefined,
212
+ };
213
+ }
@@ -0,0 +1,264 @@
1
+ import {
2
+ BAND_BOOL_GRAY_A,
3
+ BAND_BOOL_YES_MIN,
4
+ BAND_CHOICE_VERDICT_MIN,
5
+ CANNOT_TELL_MIN,
6
+ LIMIT_DISPLAY_MAX_PATHS,
7
+ } from "../constants.ts";
8
+ import type { Answer } from "../jev/types.ts";
9
+
10
+ export interface Limitation {
11
+ fact: string;
12
+ next: string;
13
+ cause?: string;
14
+ path?: string;
15
+ dependency?: string;
16
+ count?: number;
17
+ }
18
+ export interface Yield {
19
+ calls: number;
20
+ questions: number;
21
+ costUsd?: number;
22
+ cacheHits: number;
23
+ cacheRequests: number;
24
+ elapsedMs: number;
25
+ }
26
+ export type Band = "verdict" | "unsure" | "abstain";
27
+ export interface DisplayValue {
28
+ head: string | number;
29
+ p: number;
30
+ }
31
+ export interface Candidate {
32
+ label: string;
33
+ value: DisplayValue;
34
+ }
35
+ interface CandidateFields {
36
+ orderDependent?: boolean;
37
+ candidates?: readonly Candidate[];
38
+ }
39
+ export type BandPolicy = (value: DisplayValue, input: AnswerInput) => Band;
40
+ export type BudgetRefusal = { kind: "max_calls" | "session"; message: string };
41
+ export interface AnswerInput extends CandidateFields {
42
+ label: string;
43
+ answer?: Exclude<Answer, { type: "unjudged" }>;
44
+ value?: DisplayValue;
45
+ band?: Band;
46
+ uncalibrated?: boolean;
47
+ missing?: string;
48
+ reason?: string;
49
+ verify?: boolean;
50
+ }
51
+ export type OutputLine =
52
+ | (CandidateFields & {
53
+ type: "answer";
54
+ label: string;
55
+ value: DisplayValue;
56
+ band: Band;
57
+ uncalibrated: boolean;
58
+ reason?: string;
59
+ })
60
+ | { type: "unjudged"; label: string; reason: string; next: string }
61
+ | { type: "fact"; fact: string; next: string }
62
+ | {
63
+ type: "limit-group";
64
+ cause: string;
65
+ count: number;
66
+ priority: boolean;
67
+ examples: readonly string[];
68
+ remaining: number;
69
+ next: string;
70
+ }
71
+ | {
72
+ type: "collection";
73
+ title: string;
74
+ count: number;
75
+ items: readonly string[];
76
+ next: string;
77
+ }
78
+ | { type: "unchecked"; items: readonly string[] }
79
+ | { type: "refusal"; message: string }
80
+ | { type: "list"; title: string; items: readonly string[] }
81
+ | (CandidateFields & {
82
+ type: "entry";
83
+ candidate: Candidate;
84
+ band: Band;
85
+ relevant?: readonly Candidate[];
86
+ nextRead?: readonly string[];
87
+ orderProbabilities?: readonly [number, number];
88
+ })
89
+ | {
90
+ type: "state";
91
+ files: number;
92
+ integrity: "ok" | "failed";
93
+ warnings: readonly string[];
94
+ }
95
+ | {
96
+ type: "command";
97
+ cwd: string;
98
+ framework: string;
99
+ config?: string;
100
+ project?: string;
101
+ executable: string;
102
+ args: readonly string[];
103
+ }
104
+ | { type: "yield"; value: Yield };
105
+ export interface Envelope {
106
+ lines: readonly OutputLine[];
107
+ }
108
+ export interface EnvelopeInput {
109
+ state?: Omit<Extract<OutputLine, { type: "state" }>, "type">;
110
+ answers?: readonly AnswerInput[];
111
+ unjudged?: readonly { label: string; reason: string; next: string }[];
112
+ lines?: readonly Extract<
113
+ OutputLine,
114
+ { type: "list" | "entry" | "command" | "collection" }
115
+ >[];
116
+ policy?: BandPolicy;
117
+ controls?: readonly Limitation[];
118
+ limitations?: readonly Limitation[];
119
+ limitationPriorityPaths?: readonly string[];
120
+ unchecked?: readonly string[];
121
+ refusal?: string;
122
+ budget?: BudgetRefusal;
123
+ yield: Yield;
124
+ }
125
+ export function askBand(value: DisplayValue, input: AnswerInput): Band {
126
+ const answer = input.answer;
127
+ if (answer?.type === "bool")
128
+ return value.p <= BAND_BOOL_GRAY_A || value.p >= BAND_BOOL_YES_MIN
129
+ ? "verdict"
130
+ : "unsure";
131
+ if (answer && (answer.probabilities.cannot_tell ?? 0) >= CANNOT_TELL_MIN)
132
+ return "abstain";
133
+ return value.p >= BAND_CHOICE_VERDICT_MIN ? "verdict" : "unsure";
134
+ }
135
+ function displayValue(input: AnswerInput): DisplayValue {
136
+ if (input.value) return input.value;
137
+ const answer = input.answer;
138
+ if (!answer)
139
+ throw new Error("An answer requires a typed value or a Jev answer");
140
+ if (answer.type === "bool")
141
+ return { head: answer.p >= 0.5 ? "yes" : "no (not shown)", p: answer.p };
142
+ const p = input.verify
143
+ ? Math.max(
144
+ answer.probabilities.holds ?? 0,
145
+ answer.probabilities.contradicted ?? 0,
146
+ (answer.probabilities.not_addressed ?? 0) +
147
+ (answer.probabilities.cannot_tell ?? 0),
148
+ )
149
+ : Math.max(...Object.values(answer.probabilities));
150
+ return { head: answer.type === "choice" ? answer.choice : answer.score, p };
151
+ }
152
+ export function buildEnvelope(input: EnvelopeInput): Envelope {
153
+ const lines: OutputLine[] = [];
154
+ if (input.budget?.kind === "session" && input.yield.calls === 0)
155
+ return {
156
+ lines: [
157
+ { type: "refusal", message: input.budget.message },
158
+ { type: "yield", value: input.yield },
159
+ ],
160
+ };
161
+ if (input.state) lines.push({ type: "state", ...input.state });
162
+ const failed = (input.controls ?? [])
163
+ .map((control) => control.fact)
164
+ .join("; ");
165
+ for (const item of input.answers ?? []) {
166
+ const value = displayValue(item);
167
+ const initial = item.band ?? (input.policy ?? askBand)(value, item);
168
+ const band = initial === "verdict" && failed ? "unsure" : initial;
169
+ const reason =
170
+ band === "abstain"
171
+ ? (item.reason ??
172
+ item.missing ??
173
+ "proof missing; add decisive evidence to paths or command")
174
+ : failed && initial === "verdict"
175
+ ? failed
176
+ : item.reason;
177
+ lines.push({
178
+ type: "answer",
179
+ label: item.label,
180
+ value,
181
+ band,
182
+ uncalibrated: item.uncalibrated ?? false,
183
+ ...(reason ? { reason } : {}),
184
+ ...(item.orderDependent !== undefined
185
+ ? { orderDependent: item.orderDependent }
186
+ : {}),
187
+ ...(item.candidates ? { candidates: item.candidates } : {}),
188
+ });
189
+ }
190
+ for (const item of input.unjudged ?? [])
191
+ lines.push({ type: "unjudged", ...item });
192
+ for (const line of input.lines ?? [])
193
+ lines.push(
194
+ line.type === "entry" && line.band === "verdict" && failed
195
+ ? { ...line, band: "unsure" }
196
+ : line,
197
+ );
198
+ if (input.refusal) lines.push({ type: "refusal", message: input.refusal });
199
+ if (input.budget?.kind === "session")
200
+ lines.push({ type: "refusal", message: input.budget.message });
201
+ for (const fact of input.controls ?? [])
202
+ lines.push({ type: "fact", ...fact });
203
+ const groups = new Map<
204
+ string,
205
+ Omit<Extract<OutputLine, { type: "limit-group" }>, "examples"> & {
206
+ examples: string[];
207
+ }
208
+ >();
209
+ for (const limit of input.limitations ?? []) {
210
+ if (
211
+ !limit.cause ||
212
+ !limit.path ||
213
+ input.limitationPriorityPaths === undefined
214
+ ) {
215
+ lines.push({ type: "fact", fact: limit.fact, next: limit.next });
216
+ continue;
217
+ }
218
+ const priority =
219
+ input.limitationPriorityPaths?.includes(limit.path) ?? false;
220
+ const key = JSON.stringify([priority, limit.cause, limit.next]);
221
+ const group = groups.get(key) ?? {
222
+ type: "limit-group",
223
+ cause: limit.cause,
224
+ count: 0,
225
+ priority,
226
+ examples: [],
227
+ remaining: 0,
228
+ next: limit.next,
229
+ };
230
+ group.count += limit.count ?? 1;
231
+ if (priority || group.examples.length < LIMIT_DISPLAY_MAX_PATHS)
232
+ group.examples.push(
233
+ `${limit.path}${limit.dependency ? ` (${limit.dependency})` : ""}`,
234
+ );
235
+ else group.remaining++;
236
+ groups.set(key, group);
237
+ }
238
+ lines.push(
239
+ ...[...groups.values()].sort(
240
+ (a, b) => Number(b.priority) - Number(a.priority),
241
+ ),
242
+ );
243
+ if (input.budget?.kind === "max_calls")
244
+ lines.push({
245
+ type: "fact",
246
+ fact: input.budget.message,
247
+ next: "raise max_calls",
248
+ });
249
+ if (input.unchecked?.length)
250
+ lines.push({ type: "unchecked", items: input.unchecked });
251
+ const gray = lines.reduce(
252
+ (count, line) =>
253
+ count + Number(line.type === "answer" && line.band === "unsure"),
254
+ 0,
255
+ );
256
+ if (gray > 0)
257
+ lines.push({
258
+ type: "fact",
259
+ fact: `${gray}/${input.answers?.length ?? 0} answers unsure`,
260
+ next: "read the decisive passage or add its file to paths",
261
+ });
262
+ lines.push({ type: "yield", value: input.yield });
263
+ return { lines };
264
+ }