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
@@ -29,11 +29,12 @@ export function sectionStateFits(
29
29
  goal: string,
30
30
  sections: readonly Section[],
31
31
  outline = false,
32
+ maxChars = STATE_MAX_CHARS,
32
33
  ): boolean {
33
34
  return (
34
35
  sections.length < CHOICE_MAX_OPTIONS &&
35
36
  JSON.stringify(sectionState(path, goal, sections, outline)).length <=
36
- STATE_MAX_CHARS
37
+ maxChars
37
38
  );
38
39
  }
39
40
  /** Every planned block has at least two sections and fits its full-text refinement budget. */
@@ -41,6 +42,7 @@ export function sectionOutline(
41
42
  path: string,
42
43
  goal: string,
43
44
  sections: readonly Section[],
45
+ maxChars = STATE_MAX_CHARS,
44
46
  ): Section[] {
45
47
  const base = JSON.stringify(sectionState(path, goal, [])).length;
46
48
  const costs = new Map(
@@ -58,18 +60,18 @@ export function sectionOutline(
58
60
  const fits = (items: readonly Section[]) =>
59
61
  items.length < CHOICE_MAX_OPTIONS &&
60
62
  base + items.reduce((sum, item) => sum + (costs.get(item) ?? 0) + 1, 0) <=
61
- STATE_MAX_CHARS;
63
+ maxChars;
62
64
  const groups: Section[][] = [];
63
65
  let current: Section[] = [],
64
66
  currentSize = base;
65
67
  for (const section of sections) {
66
68
  const length = (costs.get(section) ?? 0) + 1;
67
- if (base + length > STATE_MAX_CHARS) return [];
69
+ if (base + length > maxChars) return [];
68
70
  const owner = section.label.split(".")[0],
69
71
  previousOwner = current.at(-1)?.label.split(".")[0];
70
72
  if (
71
73
  current.length &&
72
- (currentSize + length > STATE_MAX_CHARS ||
74
+ (currentSize + length > maxChars ||
73
75
  current.length + 1 >= CHOICE_MAX_OPTIONS ||
74
76
  (current.length >= 2 &&
75
77
  section.label.includes(".") &&
@@ -131,6 +133,7 @@ export function outlineEvidence(
131
133
  path: string,
132
134
  goal: string,
133
135
  blocks: readonly Section[],
136
+ maxChars = STATE_MAX_CHARS,
134
137
  ): Section[] {
135
138
  const overhead = JSON.stringify(
136
139
  sectionState(
@@ -139,7 +142,7 @@ export function outlineEvidence(
139
142
  blocks.map((block) => ({ ...block, text: "" })),
140
143
  ),
141
144
  ).length;
142
- let remaining = Math.max(0, STATE_MAX_CHARS - overhead);
145
+ let remaining = Math.max(0, maxChars - overhead);
143
146
  const allocations = blocks.map(() => 0);
144
147
  let active = blocks.map((_, index) => index);
145
148
  while (remaining > 0 && active.length) {
@@ -6,6 +6,7 @@ import {
6
6
  LIMIT_DISPLAY_MAX_PATHS,
7
7
  } from "../constants.ts";
8
8
  import type { Answer } from "../jev/types.ts";
9
+ import type { Item, RawValue } from "../result-types.ts";
9
10
 
10
11
  export interface Limitation {
11
12
  fact: string;
@@ -39,6 +40,14 @@ interface CandidateFields {
39
40
  export type BandPolicy = (value: DisplayValue, input: AnswerInput) => Band;
40
41
  export type BudgetRefusal = { kind: "max_calls" | "session"; message: string };
41
42
  export interface AnswerInput extends CandidateFields {
43
+ unjudged?: boolean;
44
+ source?: "fresh" | "cache";
45
+ publicResult?: string | number | boolean;
46
+ rawValues?: RawValue[];
47
+ reportControls?: Extract<
48
+ Item,
49
+ { treatment: "judged" }
50
+ >["judgment"]["controls"];
42
51
  label: string;
43
52
  answer?: Exclude<Answer, { type: "unjudged" }>;
44
53
  value?: DisplayValue;
@@ -55,6 +64,13 @@ export type OutputLine =
55
64
  value: DisplayValue;
56
65
  band: Band;
57
66
  uncalibrated: boolean;
67
+ source?: "fresh" | "cache";
68
+ publicResult?: string | number | boolean;
69
+ rawValues?: RawValue[];
70
+ reportControls?: Extract<
71
+ Item,
72
+ { treatment: "judged" }
73
+ >["judgment"]["controls"];
58
74
  reason?: string;
59
75
  })
60
76
  | { type: "unjudged"; label: string; reason: string; next: string }
@@ -163,6 +179,16 @@ export function buildEnvelope(input: EnvelopeInput): Envelope {
163
179
  .map((control) => control.fact)
164
180
  .join("; ");
165
181
  for (const item of input.answers ?? []) {
182
+ if (item.unjudged) {
183
+ lines.push({
184
+ type: "unjudged",
185
+ label: item.label,
186
+ reason: item.reason ?? "required group answer missing",
187
+ next:
188
+ item.missing ?? "inspect the available evidence without a judgment",
189
+ });
190
+ continue;
191
+ }
166
192
  const value = displayValue(item);
167
193
  const initial = item.band ?? (input.policy ?? askBand)(value, item);
168
194
  const band = initial === "verdict" && failed ? "unsure" : initial;
@@ -180,6 +206,14 @@ export function buildEnvelope(input: EnvelopeInput): Envelope {
180
206
  value,
181
207
  band,
182
208
  uncalibrated: item.uncalibrated ?? false,
209
+ ...((item.source ?? item.answer?.source)
210
+ ? { source: item.source ?? item.answer?.source }
211
+ : {}),
212
+ ...(item.publicResult !== undefined
213
+ ? { publicResult: item.publicResult }
214
+ : {}),
215
+ ...(item.rawValues ? { rawValues: item.rawValues } : {}),
216
+ ...(item.reportControls ? { reportControls: item.reportControls } : {}),
183
217
  ...(reason ? { reason } : {}),
184
218
  ...(item.orderDependent !== undefined
185
219
  ? { orderDependent: item.orderDependent }
@@ -0,0 +1,410 @@
1
+ import type {
2
+ Accounting,
3
+ Action,
4
+ Context,
5
+ Diagnostic,
6
+ Inventory,
7
+ Item,
8
+ ItemCommon,
9
+ Knowledge,
10
+ ResultReportV1,
11
+ Scope,
12
+ Tool,
13
+ } from "../result-types.ts";
14
+ import { type AnswerInput, askBand, type Yield } from "./output.ts";
15
+
16
+ export type {
17
+ Accounting,
18
+ Action,
19
+ Cause,
20
+ Context,
21
+ Diagnostic,
22
+ Execution,
23
+ Inventory,
24
+ Item,
25
+ ItemCommon,
26
+ Knowledge,
27
+ McpStructuredResultV1,
28
+ RawValue,
29
+ ResultReportV1,
30
+ Scope,
31
+ Tool,
32
+ } from "../result-types.ts";
33
+ /** Structural input prevents the pure core from depending on adapters. */
34
+ export interface EvidenceContextInput {
35
+ authority?: { path: string; origin: "host" | "server" };
36
+ authorityReason?: string;
37
+ requestedRoot?: string;
38
+ effectiveRoot?: {
39
+ path: string;
40
+ origin: "host" | "server" | "override";
41
+ commonDir?: string;
42
+ };
43
+ requestedBase?: string;
44
+ resolvedBase?: string;
45
+ }
46
+
47
+ export const known = <T>(value: T): Knowledge<T> => ({
48
+ status: "known",
49
+ value,
50
+ });
51
+ export const unknown = (reason: string): Knowledge<never> => ({
52
+ status: "unknown",
53
+ reason,
54
+ });
55
+ export const notApplicable = (reason: string): Knowledge<never> => ({
56
+ status: "not_applicable",
57
+ reason,
58
+ });
59
+ export const notCollected = (reason: string): Knowledge<never> => ({
60
+ status: "not_collected",
61
+ reason,
62
+ });
63
+
64
+ export function uncollectedContext(input: {
65
+ authority: Context["authority"];
66
+ requestedRoot?: Knowledge<string>;
67
+ requestedBase?: Knowledge<string>;
68
+ command?: Context["command"];
69
+ }): Context {
70
+ return {
71
+ authority: input.authority,
72
+ requestedRoot:
73
+ input.requestedRoot ?? notApplicable("no override requested"),
74
+ effectiveRoot: unknown("effective root not established"),
75
+ requestedBase: input.requestedBase ?? notApplicable("no base requested"),
76
+ resolvedBase: input.requestedBase
77
+ ? notCollected("base resolution not performed")
78
+ : notApplicable("no base requested"),
79
+ inventories: [],
80
+ command: input.command ?? {
81
+ execution: "not_requested",
82
+ cwd: notApplicable("no command"),
83
+ exitCode: notApplicable("no command"),
84
+ timedOut: notApplicable("no command"),
85
+ },
86
+ };
87
+ }
88
+
89
+ export function contextFromEvidence(
90
+ evidence: EvidenceContextInput | undefined,
91
+ options: {
92
+ inventories?: Inventory[];
93
+ command?: Context["command"];
94
+ } = {},
95
+ ): Context {
96
+ const context = uncollectedContext({
97
+ authority: evidence?.authorityReason
98
+ ? unknown(evidence.authorityReason)
99
+ : evidence?.authority
100
+ ? known(evidence.authority)
101
+ : unknown("canonical authority not established"),
102
+ ...(evidence?.requestedRoot !== undefined
103
+ ? { requestedRoot: known(evidence.requestedRoot) }
104
+ : {}),
105
+ ...(evidence?.requestedBase !== undefined
106
+ ? { requestedBase: known(evidence.requestedBase) }
107
+ : {}),
108
+ ...(options.command ? { command: options.command } : {}),
109
+ });
110
+ if (evidence?.effectiveRoot)
111
+ context.effectiveRoot = known({
112
+ path: evidence.effectiveRoot.path,
113
+ origin: evidence.effectiveRoot.origin,
114
+ commonDir:
115
+ evidence.effectiveRoot.commonDir === undefined
116
+ ? unknown("Git common directory not collected")
117
+ : known(evidence.effectiveRoot.commonDir),
118
+ });
119
+ if (evidence?.resolvedBase !== undefined)
120
+ context.resolvedBase = known(evidence.resolvedBase);
121
+ else if (evidence?.requestedBase !== undefined)
122
+ context.resolvedBase = unknown("requested base not resolved");
123
+ context.inventories = options.inventories ?? [];
124
+ return context;
125
+ }
126
+
127
+ export interface ReportInput {
128
+ tool: Tool;
129
+ context: Context;
130
+ items: Item[];
131
+ diagnostics: Diagnostic[];
132
+ actions: Action[];
133
+ metrics: Yield;
134
+ auxiliary: Accounting["auxiliary"];
135
+ total: Knowledge<number>;
136
+ /** Call-wide input/admission refusal, not a refusal of one group. */
137
+ refused?: boolean;
138
+ /** Explicit missing requested/candidate work outside enumerated items. */
139
+ missingWork?: boolean;
140
+ }
141
+
142
+ export function buildResultReport(input: ReportInput): ResultReportV1 {
143
+ const counts = { fresh: 0, cache: 0, notJudged: 0, static: 0 };
144
+ for (const item of input.items) {
145
+ if (item.treatment === "judged") counts[item.source]++;
146
+ else if (item.treatment === "static") counts.static++;
147
+ else counts.notJudged++;
148
+ }
149
+ const admissible = counts.fresh + counts.cache + counts.static;
150
+ const usefulFallback = input.items.some(
151
+ (item) => item.treatment === "not_judged" && item.selection?.selected,
152
+ );
153
+ const missing =
154
+ input.missingWork ||
155
+ counts.notJudged > 0 ||
156
+ input.diagnostics.some(
157
+ (d) => d.cause === "criteria_no_match" || d.effect === "blocking",
158
+ );
159
+ const report: ResultReportV1 = {
160
+ schemaVersion: 1,
161
+ tool: input.tool,
162
+ execution:
163
+ !admissible && !usefulFallback
164
+ ? input.refused
165
+ ? "refused"
166
+ : "not_judged"
167
+ : missing
168
+ ? "partial"
169
+ : "complete",
170
+ context: input.context,
171
+ items: input.items,
172
+ diagnostics: input.diagnostics,
173
+ actions: input.actions,
174
+ accounting: {
175
+ toolInvocations: 1,
176
+ httpAttempts: input.metrics.calls,
177
+ questionsSent: input.metrics.questions,
178
+ cacheHits: input.metrics.cacheHits,
179
+ cacheRequests: input.metrics.cacheRequests,
180
+ requestedResults: { total: input.total, ...counts },
181
+ auxiliary: input.auxiliary,
182
+ costUsd:
183
+ input.metrics.costUsd === undefined
184
+ ? unknown("current request cost not reported")
185
+ : known(input.metrics.costUsd),
186
+ elapsedMs: input.metrics.elapsedMs,
187
+ },
188
+ };
189
+ const errors = validateResultReport(report);
190
+ if (errors.length)
191
+ throw new Error(`Invalid ResultReportV1: ${errors.join("; ")}`);
192
+ return report;
193
+ }
194
+
195
+ /** Semantic checks complement, rather than replace, the public JSON Schema. */
196
+ export function validateResultReport(report: ResultReportV1): string[] {
197
+ const errors: string[] = [];
198
+ const counter = (value: number, label: string) => {
199
+ if (!Number.isSafeInteger(value) || value < 0)
200
+ errors.push(`invalid counter ${label}`);
201
+ };
202
+ const measurement = (
203
+ value: Knowledge<number>,
204
+ label: string,
205
+ probability = false,
206
+ ) => {
207
+ if (
208
+ value.status === "known" &&
209
+ (!Number.isFinite(value.value) ||
210
+ value.value < 0 ||
211
+ (probability && value.value > 1))
212
+ )
213
+ errors.push(`invalid measure ${label}`);
214
+ };
215
+ const accounting = report.accounting;
216
+ for (const key of [
217
+ "httpAttempts",
218
+ "questionsSent",
219
+ "cacheHits",
220
+ "cacheRequests",
221
+ ] as const)
222
+ counter(accounting[key], key);
223
+ if (!Number.isFinite(accounting.elapsedMs) || accounting.elapsedMs < 0)
224
+ errors.push("invalid elapsedMs");
225
+ measurement(accounting.costUsd, "costUsd");
226
+ for (const [kind, values] of Object.entries(accounting.auxiliary))
227
+ for (const [source, value] of Object.entries(values))
228
+ counter(value, `${kind}.${source}`);
229
+ for (const inv of report.context.inventories) {
230
+ for (const [name, value] of [
231
+ ["discovered", inv.discovered],
232
+ ["considered", inv.considered],
233
+ ] as const)
234
+ if (value.status === "known") counter(value.value, `${inv.id}.${name}`);
235
+ for (const criterion of inv.criteria)
236
+ if (criterion.matches.status === "known")
237
+ counter(criterion.matches.value, `${inv.id}.matches`);
238
+ }
239
+ const ids = (values: { id: string }[], label: string) => {
240
+ const set = new Set(values.map((v) => v.id));
241
+ if (set.size !== values.length) errors.push(`duplicate ${label} IDs`);
242
+ return set;
243
+ };
244
+ const items = ids(report.items, "item");
245
+ const diagnostics = ids(report.diagnostics, "diagnostic");
246
+ const actions = ids(report.actions, "action");
247
+ const inventories = ids(report.context.inventories, "inventory");
248
+ const groups = new Set(report.items.map((i) => i.groupId));
249
+ const refs = (values: string[], set: Set<string>, label: string) => {
250
+ for (const id of values)
251
+ if (!set.has(id)) errors.push(`unknown ${label} ID ${id}`);
252
+ };
253
+ const scope = (s: Scope) => {
254
+ if (s.kind === "item") refs(s.itemIds, items, "item");
255
+ else if (s.kind === "group") refs(s.groupIds, groups, "group");
256
+ else if (s.kind === "inventory")
257
+ refs(s.inventoryIds, inventories, "inventory");
258
+ };
259
+ for (const item of report.items) {
260
+ refs(item.diagnosticIds, diagnostics, "diagnostic");
261
+ refs(item.actionIds, actions, "action");
262
+ if (item.treatment === "judged") {
263
+ ids(item.judgment.controls, `control in ${item.id}`);
264
+ measurement(item.judgment.measure.value, `${item.id}.measure`, true);
265
+ for (const raw of [
266
+ ...item.judgment.rawValues,
267
+ ...item.judgment.controls.flatMap((c) => c.rawValues),
268
+ ])
269
+ measurement(raw.probability, `${item.id}.${raw.label}`, true);
270
+ if (
271
+ typeof item.judgment.result === "number" &&
272
+ !Number.isFinite(item.judgment.result)
273
+ )
274
+ errors.push(`invalid result ${item.id}`);
275
+ }
276
+ }
277
+ for (const d of report.diagnostics) {
278
+ scope(d.scope);
279
+ refs(d.actionIds, actions, "action");
280
+ }
281
+ for (const a of report.actions) scope(a.scope);
282
+ for (const inv of report.context.inventories)
283
+ for (const criterion of inv.criteria)
284
+ refs(criterion.diagnosticIds, diagnostics, "diagnostic");
285
+ const c = report.accounting.requestedResults;
286
+ for (const key of ["fresh", "cache", "static", "notJudged"] as const)
287
+ counter(c[key], `requestedResults.${key}`);
288
+ if (c.total.status === "known")
289
+ counter(c.total.value, "requestedResults.total");
290
+ const actual = { fresh: 0, cache: 0, static: 0, notJudged: 0 };
291
+ for (const i of report.items) {
292
+ if (i.treatment === "judged") actual[i.source]++;
293
+ else if (i.treatment === "static") actual.static++;
294
+ else actual.notJudged++;
295
+ }
296
+ for (const key of ["fresh", "cache", "static", "notJudged"] as const)
297
+ if (c[key] !== actual[key])
298
+ errors.push(`requestedResults.${key} does not match items`);
299
+ if (c.total.status === "known" && c.total.value !== report.items.length)
300
+ errors.push("known requested total does not match items");
301
+ const admissible = actual.fresh + actual.cache + actual.static;
302
+ const usefulFallback = report.items.some(
303
+ (item) => item.treatment === "not_judged" && item.selection?.selected,
304
+ );
305
+ if (
306
+ (report.execution === "refused" || report.execution === "not_judged") &&
307
+ (admissible || usefulFallback)
308
+ )
309
+ errors.push("non-result execution has admissible items or useful fallback");
310
+ if (
311
+ (report.execution === "complete" || report.execution === "partial") &&
312
+ !admissible &&
313
+ !usefulFallback
314
+ )
315
+ errors.push("result execution has no admissible items or useful fallback");
316
+ if (
317
+ report.execution === "complete" &&
318
+ (actual.notJudged ||
319
+ report.diagnostics.some(
320
+ (d) => d.effect === "blocking" || d.cause === "criteria_no_match",
321
+ ))
322
+ )
323
+ errors.push("complete execution omits requested work");
324
+ if (!admissible && !report.diagnostics.length)
325
+ errors.push("absent judgment needs an explicit cause");
326
+ return errors;
327
+ }
328
+
329
+ export function resultIsError(report: ResultReportV1): boolean {
330
+ if (report.execution === "refused") return true;
331
+ if (
332
+ report.items.some(
333
+ (i) => i.treatment !== "not_judged" || i.selection?.selected,
334
+ )
335
+ )
336
+ return false;
337
+ return report.diagnostics.some(
338
+ (d) =>
339
+ d.effect === "blocking" &&
340
+ [
341
+ "provider_context_refusal",
342
+ "not_configured",
343
+ "service_unavailable",
344
+ "transport_failure",
345
+ "invalid_response",
346
+ "internal_error",
347
+ ].includes(d.cause),
348
+ );
349
+ }
350
+
351
+ /** Source must be established by the client, never guessed from footer totals. */
352
+ export function answerItemFromInput(
353
+ common: ItemCommon,
354
+ input: AnswerInput,
355
+ ): Item {
356
+ if (input.unjudged)
357
+ return { ...common, treatment: "not_judged", source: "none" };
358
+ const source = input.source ?? input.answer?.source;
359
+ if (source !== "fresh" && source !== "cache")
360
+ throw new Error(`Answer source not established: ${common.id}`);
361
+ const value = input.value;
362
+ const answer = input.answer;
363
+ if (!value && !answer)
364
+ throw new Error(`Answer value not established: ${common.id}`);
365
+ const result =
366
+ input.publicResult ??
367
+ (answer?.type === "bool"
368
+ ? answer.p >= 0.5
369
+ : (value?.head ??
370
+ (answer?.type === "choice" ? answer.choice : answer?.score)));
371
+ if (result === undefined)
372
+ throw new Error(`Answer result not established: ${common.id}`);
373
+ const p =
374
+ value?.p ??
375
+ (answer?.type === "bool"
376
+ ? answer.p
377
+ : answer
378
+ ? Math.max(...Object.values(answer.probabilities))
379
+ : undefined);
380
+ return {
381
+ ...common,
382
+ treatment: "judged",
383
+ source,
384
+ judgment: {
385
+ band:
386
+ input.band ??
387
+ (p === undefined
388
+ ? "unsure"
389
+ : askBand(
390
+ {
391
+ head: typeof result === "boolean" ? String(result) : result,
392
+ p,
393
+ },
394
+ input,
395
+ )),
396
+ result,
397
+ measure: {
398
+ kind: "probability",
399
+ value:
400
+ p === undefined
401
+ ? unknown("leading probability not collected")
402
+ : known(p),
403
+ },
404
+ reason: input.reason ?? "",
405
+ uncalibrated: input.uncalibrated ?? false,
406
+ rawValues: input.rawValues ?? [],
407
+ controls: input.reportControls ?? [],
408
+ },
409
+ };
410
+ }
@@ -0,0 +1,37 @@
1
+ /** Base-name globs of likely secret files, matched case-insensitively. */
2
+ export const SECRET_NAME_GLOBS = [
3
+ ".env",
4
+ ".env.*",
5
+ "*.pem",
6
+ "id_rsa*",
7
+ "*.p12",
8
+ "credentials*",
9
+ "secrets*",
10
+ ] as const;
11
+ /** Value-free templates that stay admitted despite the `.env.*` rule. */
12
+ const templates = [".env.example", ".env.sample", ".env.template"];
13
+
14
+ /**
15
+ * True when a path's base name matches SECRET_NAME_GLOBS (any depth,
16
+ * case-insensitive) and is not a value-free `.env` template. Such files are
17
+ * refused before their content is read, on every evidence admission path;
18
+ * there is no per-call override.
19
+ */
20
+ export function isSecretPath(path: string): boolean {
21
+ const name = (path.split(/[\\/]/).pop() ?? "").toLowerCase();
22
+ if (templates.includes(name)) return false;
23
+ return (
24
+ name === ".env" ||
25
+ name.startsWith(".env.") ||
26
+ name.endsWith(".pem") ||
27
+ name.startsWith("id_rsa") ||
28
+ name.endsWith(".p12") ||
29
+ name.startsWith("credentials") ||
30
+ name.startsWith("secrets")
31
+ );
32
+ }
33
+
34
+ /** Refusal text for a secret-named path: names the path, never its content. */
35
+ export function secretRefusal(path: string): string {
36
+ return `${path}: secret file name (secret_pattern): content not read, not sent to Jev`;
37
+ }
package/src/core/state.ts CHANGED
@@ -2,7 +2,14 @@ import { ASK_NOTE_MAX_CHARS, STATE_MAX_CHARS } from "../constants.ts";
2
2
  import type { State } from "../jev/types.ts";
3
3
  import { isRecord, type Result } from "../result.ts";
4
4
 
5
- const reservedKeys = ["files", "files_before", "base_sha", "closure"] as const;
5
+ const reservedKeys = [
6
+ "files",
7
+ "files_before",
8
+ "base_sha",
9
+ "closure",
10
+ "evidence",
11
+ "output",
12
+ ] as const;
6
13
 
7
14
  export function assembleState(
8
15
  note: string | undefined,
package/src/core/units.ts CHANGED
@@ -5,7 +5,7 @@ import { truncate } from "./truncate.ts";
5
5
  export interface SourceFile extends DiffFile {
6
6
  before: string | null;
7
7
  after: string | null;
8
- limitation?: "too_large" | "unreadable" | "not UTF-8 text";
8
+ limitation?: "too_large" | "unreadable" | "not UTF-8 text" | "secret_pattern";
9
9
  }
10
10
  export interface Declaration {
11
11
  kind: "function" | "declaration";
@@ -43,6 +43,7 @@ export type UnitLimit =
43
43
  | "too_large"
44
44
  | "unreadable"
45
45
  | "not UTF-8 text"
46
+ | "secret_pattern"
46
47
  | "rename_unpaired";
47
48
  file: string;
48
49
  }
@@ -148,7 +149,7 @@ export function buildUnits(
148
149
  const units: EvidenceUnit[] = [];
149
150
  const limits: UnitLimit[] = [];
150
151
  for (const file of files) {
151
- if (isTestFile(file.path)) continue;
152
+ if (isTestFile(file.path) && file.limitation !== "secret_pattern") continue;
152
153
  if (file.binary) {
153
154
  limits.push({ kind: "binary_ignored", file: file.path });
154
155
  continue;
package/src/index.ts CHANGED
@@ -34,6 +34,9 @@ export default function extension(api: ExtensionAPI & { pi?: unknown }): void {
34
34
  get client() {
35
35
  return config.client;
36
36
  },
37
+ get apiKey() {
38
+ return config.values().apiKey || undefined;
39
+ },
37
40
  host,
38
41
  runtime,
39
42
  exec: api.exec.bind(api),