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.
- package/CHANGELOG.md +37 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +22 -14
- package/SECURITY.md +17 -1
- package/dist/adapters/ask-files.js +11 -2
- package/dist/adapters/ask-proof.js +63 -7
- package/dist/adapters/command.js +82 -29
- package/dist/adapters/docs.js +30 -10
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/files.js +141 -16
- package/dist/adapters/find.js +34 -6
- package/dist/adapters/git-base.js +7 -1
- package/dist/adapters/git.js +51 -7
- package/dist/adapters/locate-file.js +47 -9
- package/dist/adapters/private-storage.js +14 -6
- package/dist/adapters/risk-callers.js +3 -0
- package/dist/adapters/shell.js +23 -7
- package/dist/adapters/test-inventory.js +10 -2
- package/dist/configuration.js +17 -7
- package/dist/constants.js +26 -5
- package/dist/core/ask-references.js +193 -109
- package/dist/core/asks.js +78 -7
- package/dist/core/locate.js +8 -8
- package/dist/core/output.js +17 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/state.js +8 -1
- package/dist/core/units.js +1 -1
- package/dist/jev/client.js +34 -12
- package/dist/mcp/protocol.js +50 -27
- package/dist/mcp/tools.js +20 -7
- package/dist/render.js +72 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/texts/ask-files.js +3 -1
- package/dist/texts/ask.js +3 -1
- package/dist/texts/check-diff.js +7 -4
- package/dist/texts/find.js +7 -2
- package/dist/texts/guide.js +3 -16
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +7 -2
- package/dist/texts/select-tests.js +3 -1
- package/dist/tools/ask-files.js +248 -15
- package/dist/tools/ask.js +523 -62
- package/dist/tools/check-diff.js +222 -30
- package/dist/tools/docs-check.js +122 -13
- package/dist/tools/find.js +320 -27
- package/dist/tools/locate.js +317 -18
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +273 -19
- package/dist/tools/spec-check.js +119 -22
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
- package/docs/agent-instructions.md +59 -30
- package/docs/design.md +13 -1
- package/docs/mcp.md +8 -6
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +1 -1
- package/rules/jev-ask.md +22 -1
- package/server.json +2 -2
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/command.ts +96 -33
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +17 -5
- package/src/adapters/risk-callers.ts +3 -0
- package/src/adapters/shell.ts +23 -7
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +16 -2
- package/src/constants.ts +26 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/index.ts +3 -0
- package/src/jev/client.ts +54 -16
- package/src/jev/types.ts +18 -3
- package/src/mcp/protocol.ts +91 -41
- package/src/mcp/tools.ts +26 -13
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +309 -14
- package/src/tools/ask.ts +700 -77
- package/src/tools/check-diff.ts +331 -28
- package/src/tools/docs-check.ts +241 -39
- package/src/tools/find.ts +386 -29
- package/src/tools/locate.ts +384 -19
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +479 -21
- package/src/tools/spec-check.ts +193 -19
package/src/core/locate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 >
|
|
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 >
|
|
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,
|
|
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) {
|
package/src/core/output.ts
CHANGED
|
@@ -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 = [
|
|
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