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.
- package/CHANGELOG.md +106 -1
- package/CONTRIBUTING.md +43 -0
- package/README.md +58 -17
- package/SECURITY.md +43 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +198 -0
- package/dist/adapters/ask-proof.js +200 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +234 -0
- package/dist/adapters/docs.js +192 -0
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +418 -0
- package/dist/adapters/find.js +150 -0
- package/dist/adapters/git-base.js +32 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +483 -0
- package/dist/adapters/locate-file.js +197 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +106 -0
- package/dist/adapters/risk-callers.js +429 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +92 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +139 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +267 -0
- package/dist/constants.js +140 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +278 -0
- package/dist/core/asks.js +507 -0
- package/dist/core/batches.js +65 -0
- package/dist/core/command-output.js +224 -0
- package/dist/core/diff.js +178 -0
- package/dist/core/docs.js +302 -0
- package/dist/core/find.js +108 -0
- package/dist/core/git.js +1 -0
- package/dist/core/imports.js +550 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/lexical.js +132 -0
- package/dist/core/locate.js +169 -0
- package/dist/core/output.js +137 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +51 -0
- package/dist/core/syntax.js +1 -0
- package/dist/core/test-commands.js +334 -0
- package/dist/core/test-coverage.js +74 -0
- package/dist/core/test-discovery.js +1382 -0
- package/dist/core/test-evidence.js +527 -0
- package/dist/core/test-state.js +81 -0
- package/dist/core/truncate.js +12 -0
- package/dist/core/units.js +349 -0
- package/dist/describe.js +23 -0
- package/dist/guide.js +33 -0
- package/dist/host.js +24 -0
- package/dist/jev/client.js +456 -0
- package/dist/jev/pool.js +54 -0
- package/dist/jev/types.js +1 -0
- package/dist/mcp/main.js +124 -0
- package/dist/mcp/protocol.js +210 -0
- package/dist/mcp/tools.js +129 -0
- package/dist/presets/docs.js +62 -0
- package/dist/presets/risk.js +179 -0
- package/dist/presets/spec.js +81 -0
- package/dist/presets/witnesses.js +249 -0
- package/dist/render.js +114 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/result.js +3 -0
- package/dist/runtime.js +1 -0
- package/dist/session.js +147 -0
- package/dist/texts/ask-files.js +3 -0
- package/dist/texts/ask.js +4 -0
- package/dist/texts/check-diff.js +20 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +19 -0
- package/dist/texts/guide.js +3 -0
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +15 -0
- package/dist/texts/select-tests.js +4 -0
- package/dist/tools/ask-files.js +450 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +1147 -0
- package/dist/tools/check-diff.js +594 -0
- package/dist/tools/docs-check.js +408 -0
- package/dist/tools/find.js +682 -0
- package/dist/tools/locate.js +602 -0
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +821 -0
- package/dist/tools/spec-check.js +263 -0
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
- package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
- package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
- package/docs/adr/0004-compiled-typed-intents.md +19 -0
- package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
- package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
- package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
- package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
- package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
- package/docs/adr/0010-mcp-server-thin-host.md +23 -0
- package/docs/agent-instructions.md +120 -0
- package/docs/design.md +16 -4
- package/docs/mcp.md +233 -0
- 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 +19 -4
- package/rules/jev-ask.md +22 -1
- package/server.json +57 -0
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +102 -36
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/exec.ts +226 -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 +155 -0
- package/src/adapters/risk-callers.ts +7 -2
- package/src/adapters/shell.ts +113 -0
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +55 -14
- package/src/constants.ts +37 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/command-output.ts +17 -1
- 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/host.ts +11 -0
- package/src/index.ts +3 -0
- package/src/jev/client.ts +66 -16
- package/src/jev/types.ts +24 -3
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +332 -0
- package/src/mcp/tools.ts +179 -0
- 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/session.ts +59 -0
- package/src/setup.ts +13 -5
- 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 +311 -18
- package/src/tools/ask.ts +722 -95
- package/src/tools/check-diff.ts +337 -31
- package/src/tools/docs-check.ts +241 -38
- package/src/tools/find.ts +389 -29
- package/src/tools/locate.ts +387 -25
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +484 -23
- package/src/tools/spec-check.ts +194 -19
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { createAnalysisContext } from "../adapters/analysis-context.js";
|
|
5
|
+
import { collectAskRepository, repositoryPath } from "../adapters/ask-proof.js";
|
|
6
|
+
import { collectProofSyntax } from "../adapters/ask-syntax.js";
|
|
7
|
+
import { canonicalPath } from "../adapters/canonical-path.js";
|
|
8
|
+
import { captureCommand } from "../adapters/command.js";
|
|
9
|
+
import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
|
|
10
|
+
import { collectFiles } from "../adapters/files.js";
|
|
11
|
+
import { shareGitInventory } from "../adapters/git-inventory.js";
|
|
12
|
+
import { hostUsage } from "../adapters/usage.js";
|
|
13
|
+
import { ASK_MAX_FILES, ASK_NOTE_MAX_CHARS, ASK_TIMEOUT_MAX_S, ASK_TIMEOUT_S, OUTPUT_CHUNK_CHARS, OUTPUT_FIND_MAX_CALLS, OUTPUT_SHAPE_MAX_COUNT, STATE_MAX_CHARS, } from "../constants.js";
|
|
14
|
+
import { buildAskClosure } from "../core/ask-closure.js";
|
|
15
|
+
import { blockedAskReadings, resolveAskReferences, } from "../core/ask-references.js";
|
|
16
|
+
import { compileAsks, readAsks, reverseQuestions } from "../core/asks.js";
|
|
17
|
+
import { outputChunks, selectOutput } from "../core/command-output.js";
|
|
18
|
+
import { createImportGraphBuilder, } from "../core/imports.js";
|
|
19
|
+
import { checkIntegrity } from "../core/integrity.js";
|
|
20
|
+
import { buildEnvelope } from "../core/output.js";
|
|
21
|
+
import { answerItemFromInput, buildResultReport, contextFromEvidence, known, notApplicable, unknown, } from "../core/result-report.js";
|
|
22
|
+
import { assembleState } from "../core/state.js";
|
|
23
|
+
import { discoverTests } from "../core/test-discovery.js";
|
|
24
|
+
import { describeAsk } from "../describe.js";
|
|
25
|
+
import { renderResultReport } from "../render.js";
|
|
26
|
+
import { isRecord } from "../result.js";
|
|
27
|
+
import { COMMAND_LIMIT_NOTICE } from "../texts/ask.js";
|
|
28
|
+
import { NOT_CONFIGURED } from "../texts/configuration.js";
|
|
29
|
+
import { ASK_GUIDELINE } from "../texts/instructions.js";
|
|
30
|
+
import { asksParameter } from "./ask-schema.js";
|
|
31
|
+
export const askParameters = Type.Object({
|
|
32
|
+
root: Type.Optional(Type.String()),
|
|
33
|
+
state: Type.Optional(Type.String({ maxLength: ASK_NOTE_MAX_CHARS })),
|
|
34
|
+
paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
35
|
+
base: Type.Optional(Type.String({ minLength: 1 })),
|
|
36
|
+
command: Type.Optional(Type.String({ minLength: 1 })),
|
|
37
|
+
timeout_s: Type.Optional(Type.Number({
|
|
38
|
+
minimum: 1,
|
|
39
|
+
maximum: ASK_TIMEOUT_MAX_S,
|
|
40
|
+
default: ASK_TIMEOUT_S,
|
|
41
|
+
})),
|
|
42
|
+
asks: asksParameter("ask"),
|
|
43
|
+
max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
44
|
+
}, { additionalProperties: false });
|
|
45
|
+
export function createAskTool(dependencies) {
|
|
46
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
47
|
+
const { isOmp, names } = host;
|
|
48
|
+
const allowCommand = process.env.JEV_TOOLS_ALLOW_COMMAND !== "0";
|
|
49
|
+
const parameters = allowCommand
|
|
50
|
+
? askParameters
|
|
51
|
+
: Type.Omit(askParameters, ["command", "timeout_s"]);
|
|
52
|
+
return {
|
|
53
|
+
name: "jev_ask",
|
|
54
|
+
label: "Jev ask",
|
|
55
|
+
description: `${describeAsk(names)}\n\n${COMMAND_LIMIT_NOTICE}`,
|
|
56
|
+
parameters,
|
|
57
|
+
...(isOmp
|
|
58
|
+
? {
|
|
59
|
+
approval: (args) => args.command ? "exec" : "read",
|
|
60
|
+
loadMode: "essential",
|
|
61
|
+
}
|
|
62
|
+
: {
|
|
63
|
+
promptSnippet: "Typed answers about one situation built from a note, files and a command's output",
|
|
64
|
+
promptGuidelines: [ASK_GUIDELINE],
|
|
65
|
+
}),
|
|
66
|
+
async execute(_id, args, signal, _update, ctx) {
|
|
67
|
+
const client = dependencies.client;
|
|
68
|
+
const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
|
|
69
|
+
exec: execute,
|
|
70
|
+
signal,
|
|
71
|
+
origin: dependencies.evidenceOrigin,
|
|
72
|
+
});
|
|
73
|
+
const evidenceContext = evidence.context;
|
|
74
|
+
const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
|
|
75
|
+
const started = performance.now();
|
|
76
|
+
const evidenceFacts = {
|
|
77
|
+
omissions: [],
|
|
78
|
+
blockedGroups: [],
|
|
79
|
+
inventory: [],
|
|
80
|
+
};
|
|
81
|
+
evidenceContext.requestedBase = args.base;
|
|
82
|
+
const asks = compileAsks(args.asks, { surface: "ask" });
|
|
83
|
+
let commandContext = args.command
|
|
84
|
+
? {
|
|
85
|
+
execution: "not_started",
|
|
86
|
+
cwd: evidence.ok
|
|
87
|
+
? known(cwd)
|
|
88
|
+
: unknown("effective root not established"),
|
|
89
|
+
exitCode: unknown("command not started"),
|
|
90
|
+
timedOut: unknown("command not started"),
|
|
91
|
+
}
|
|
92
|
+
: {
|
|
93
|
+
execution: "not_requested",
|
|
94
|
+
cwd: notApplicable("no command"),
|
|
95
|
+
exitCode: notApplicable("no command"),
|
|
96
|
+
timedOut: notApplicable("no command"),
|
|
97
|
+
};
|
|
98
|
+
let refusalCause = "invalid_arguments";
|
|
99
|
+
let inventoryCollected = false;
|
|
100
|
+
const auxiliary = {
|
|
101
|
+
controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
|
|
102
|
+
passages: { fresh: 0, cache: 0, notJudged: 0 },
|
|
103
|
+
};
|
|
104
|
+
const attributionAnswers = {};
|
|
105
|
+
const attributionMissing = new Set();
|
|
106
|
+
const findResults = [];
|
|
107
|
+
const exec = shareGitInventory(execute);
|
|
108
|
+
const finish = (result, input) => {
|
|
109
|
+
const envelope = buildEnvelope({
|
|
110
|
+
...input,
|
|
111
|
+
yield: {
|
|
112
|
+
calls: result.calls ?? 0,
|
|
113
|
+
questions: result.questions ?? 0,
|
|
114
|
+
costUsd: result.usage?.costUsd,
|
|
115
|
+
cacheHits: result.cacheHits ?? 0,
|
|
116
|
+
cacheRequests: result.cacheRequests ?? 0,
|
|
117
|
+
elapsedMs: performance.now() - started,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
runtime.session.record(envelope);
|
|
121
|
+
const diagnostics = [];
|
|
122
|
+
const actions = [];
|
|
123
|
+
const globalMissing = new Map();
|
|
124
|
+
const diagnose = (cause, fact, scope, target, origin, effect = "blocking", next) => {
|
|
125
|
+
const id = `diagnostic:${diagnostics.length}`;
|
|
126
|
+
const actionId = `action:${actions.length}`;
|
|
127
|
+
const code = cause === "invalid_root" || cause === "invalid_base"
|
|
128
|
+
? "correct_context"
|
|
129
|
+
: cause === "forbidden_path"
|
|
130
|
+
? "correct_reference"
|
|
131
|
+
: cause === "missing_required" || cause === "empty_required"
|
|
132
|
+
? "provide_evidence"
|
|
133
|
+
: cause === "not_configured"
|
|
134
|
+
? "configure_client"
|
|
135
|
+
: "inspect_native";
|
|
136
|
+
actions.push({
|
|
137
|
+
id: actionId,
|
|
138
|
+
code,
|
|
139
|
+
target: target === undefined
|
|
140
|
+
? unknown("target not established")
|
|
141
|
+
: known(target),
|
|
142
|
+
scope,
|
|
143
|
+
condition: "Only if materially new evidence or corrected context is available",
|
|
144
|
+
instruction: next ??
|
|
145
|
+
(code === "provide_evidence"
|
|
146
|
+
? `Provide the actually missing evidence${target ? `: ${target}` : ""}; do not repeat an already captured command.`
|
|
147
|
+
: "Inspect the supplied evidence natively; correct the named cause before any useful new Jev call."),
|
|
148
|
+
repeatUnchanged: false,
|
|
149
|
+
});
|
|
150
|
+
diagnostics.push({
|
|
151
|
+
id,
|
|
152
|
+
cause,
|
|
153
|
+
fact,
|
|
154
|
+
target: target === undefined
|
|
155
|
+
? unknown("target not established")
|
|
156
|
+
: known(target),
|
|
157
|
+
origin,
|
|
158
|
+
scope,
|
|
159
|
+
effect,
|
|
160
|
+
material: true,
|
|
161
|
+
omittedMembers: [],
|
|
162
|
+
memberCount: known(0),
|
|
163
|
+
actionIds: [actionId],
|
|
164
|
+
});
|
|
165
|
+
return { diagnosticIds: [id], actionIds: [actionId] };
|
|
166
|
+
};
|
|
167
|
+
const items = asks.ok
|
|
168
|
+
? asks.readings.map((reading, index) => {
|
|
169
|
+
const groupId = `group:${asks.groups.findIndex((group) => group.includes(reading.id))}`;
|
|
170
|
+
const target = asks.asks[reading.askIndex]?.about ?? "state";
|
|
171
|
+
const pieces = [
|
|
172
|
+
{
|
|
173
|
+
target,
|
|
174
|
+
canonicalPath: unknown("selector does not establish a canonical file identity"),
|
|
175
|
+
aliases: [],
|
|
176
|
+
side: target === "output" ? "output" : "state",
|
|
177
|
+
revision: notApplicable("non-file evidence"),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
for (const [alias, path] of Object.entries(evidenceFacts.references?.aliases ?? {})) {
|
|
181
|
+
const before = evidenceFacts.references?.additions.some((addition) => addition.reference === alias &&
|
|
182
|
+
addition.side === "files_before") || target.includes("files_before");
|
|
183
|
+
pieces.push({
|
|
184
|
+
target: alias,
|
|
185
|
+
canonicalPath: evidenceContext.effectiveRoot
|
|
186
|
+
? known(resolve(evidenceContext.effectiveRoot.path, path))
|
|
187
|
+
: unknown("effective root unavailable"),
|
|
188
|
+
aliases: [alias],
|
|
189
|
+
side: before ? "before" : "current",
|
|
190
|
+
revision: before && evidenceContext.resolvedBase
|
|
191
|
+
? known(evidenceContext.resolvedBase)
|
|
192
|
+
: unknown("working tree evidence"),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const common = {
|
|
196
|
+
id: reading.id,
|
|
197
|
+
kind: reading.kind === "verify"
|
|
198
|
+
? "claim"
|
|
199
|
+
: "question",
|
|
200
|
+
label: reading.label,
|
|
201
|
+
groupId,
|
|
202
|
+
evidence: pieces,
|
|
203
|
+
diagnosticIds: [],
|
|
204
|
+
actionIds: [],
|
|
205
|
+
};
|
|
206
|
+
const originalAnswer = input.answers?.[index];
|
|
207
|
+
const attributionControls = attributionAnswers[reading.id];
|
|
208
|
+
const answer = originalAnswer && attributionMissing.has(reading.id)
|
|
209
|
+
? {
|
|
210
|
+
...originalAnswer,
|
|
211
|
+
unjudged: true,
|
|
212
|
+
reason: "Required attribution control is incomplete",
|
|
213
|
+
}
|
|
214
|
+
: originalAnswer && attributionControls
|
|
215
|
+
? {
|
|
216
|
+
...originalAnswer,
|
|
217
|
+
reportControls: [
|
|
218
|
+
...(originalAnswer.reportControls ?? []),
|
|
219
|
+
...attributionControls,
|
|
220
|
+
],
|
|
221
|
+
}
|
|
222
|
+
: originalAnswer;
|
|
223
|
+
if (answer &&
|
|
224
|
+
!answer.unjudged &&
|
|
225
|
+
(answer.source || answer.answer?.source))
|
|
226
|
+
return answerItemFromInput(common, input.controls?.length
|
|
227
|
+
? {
|
|
228
|
+
...answer,
|
|
229
|
+
band: "unsure",
|
|
230
|
+
reason: input.controls
|
|
231
|
+
.map((control) => control.fact)
|
|
232
|
+
.join("; "),
|
|
233
|
+
}
|
|
234
|
+
: answer);
|
|
235
|
+
const blocked = evidenceFacts.blockedGroups.find((group) => group.ids.includes(reading.id));
|
|
236
|
+
const omission = evidenceFacts.omissions.find((item) => item.required &&
|
|
237
|
+
(item.origin === "note" || item.scope === reading.id));
|
|
238
|
+
const groupFailure = result.ok
|
|
239
|
+
? (asks.groups.find((group) => group.includes(reading.id)) ??
|
|
240
|
+
[])
|
|
241
|
+
.map((id) => result.answers[id])
|
|
242
|
+
.find((answer) => answer?.type === "unjudged" && answer.cause)
|
|
243
|
+
: undefined;
|
|
244
|
+
const cause = blocked
|
|
245
|
+
? (blocked.cause ?? "missing_required")
|
|
246
|
+
: groupFailure?.type === "unjudged" && groupFailure.cause
|
|
247
|
+
? groupFailure.cause
|
|
248
|
+
: input.budget
|
|
249
|
+
? input.budget.kind === "session"
|
|
250
|
+
? "session_budget"
|
|
251
|
+
: "call_budget"
|
|
252
|
+
: (result.failureCause ??
|
|
253
|
+
(input.refusal ? refusalCause : "control_failure"));
|
|
254
|
+
const globalKey = omission?.origin === "note" ? omission.reference : undefined;
|
|
255
|
+
const existingLinks = globalKey
|
|
256
|
+
? globalMissing.get(globalKey)
|
|
257
|
+
: undefined;
|
|
258
|
+
const links = existingLinks ??
|
|
259
|
+
diagnose(cause, blocked?.reason ??
|
|
260
|
+
answer?.reason ??
|
|
261
|
+
(result.ok
|
|
262
|
+
? "Required judgment group incomplete or response provenance not established"
|
|
263
|
+
: result.error), omission?.origin === "note"
|
|
264
|
+
? { kind: "call" }
|
|
265
|
+
: { kind: "group", groupIds: [groupId] }, omission?.reference, omission?.origin === "note"
|
|
266
|
+
? "note"
|
|
267
|
+
: blocked
|
|
268
|
+
? "ask"
|
|
269
|
+
: input.budget
|
|
270
|
+
? "budget"
|
|
271
|
+
: "provider");
|
|
272
|
+
if (globalKey)
|
|
273
|
+
globalMissing.set(globalKey, links);
|
|
274
|
+
return {
|
|
275
|
+
...common,
|
|
276
|
+
...links,
|
|
277
|
+
treatment: "not_judged",
|
|
278
|
+
source: "none",
|
|
279
|
+
};
|
|
280
|
+
})
|
|
281
|
+
: [];
|
|
282
|
+
if (!items.length)
|
|
283
|
+
diagnose(input.refusal ? refusalCause : "collection_empty", input.refusal ?? "No requested results collected", { kind: "call" }, undefined, "input");
|
|
284
|
+
for (const control of input.controls ?? [])
|
|
285
|
+
diagnose("control_failure", control.fact, { kind: "call" }, undefined, "control", "reservation", control.next);
|
|
286
|
+
for (const limitation of input.limitations ?? [])
|
|
287
|
+
diagnose("evidence_limit", limitation.fact, { kind: "call" }, limitation.path, "closure", "reservation", limitation.next);
|
|
288
|
+
const report = buildResultReport({
|
|
289
|
+
tool: "jev_ask",
|
|
290
|
+
context: contextFromEvidence(evidenceContext, {
|
|
291
|
+
command: commandContext,
|
|
292
|
+
inventories: inventoryCollected
|
|
293
|
+
? [
|
|
294
|
+
{
|
|
295
|
+
id: "repository",
|
|
296
|
+
kind: "repository",
|
|
297
|
+
rules: ["Git tracked and admitted repository evidence"],
|
|
298
|
+
restrictions: args.paths ?? [],
|
|
299
|
+
discovered: known(evidenceFacts.inventory.length),
|
|
300
|
+
considered: known(evidenceFacts.inventory.length),
|
|
301
|
+
scopeRestricted: false,
|
|
302
|
+
criteria: [],
|
|
303
|
+
},
|
|
304
|
+
]
|
|
305
|
+
: [],
|
|
306
|
+
}),
|
|
307
|
+
items,
|
|
308
|
+
diagnostics,
|
|
309
|
+
actions,
|
|
310
|
+
metrics: {
|
|
311
|
+
calls: result.calls ?? 0,
|
|
312
|
+
questions: result.questions ?? 0,
|
|
313
|
+
cacheHits: result.cacheHits ?? 0,
|
|
314
|
+
cacheRequests: result.cacheRequests ?? 0,
|
|
315
|
+
costUsd: result.usage?.costUsd,
|
|
316
|
+
elapsedMs: performance.now() - started,
|
|
317
|
+
},
|
|
318
|
+
auxiliary,
|
|
319
|
+
total: known(items.length),
|
|
320
|
+
refused: !!input.refusal &&
|
|
321
|
+
refusalCause !== "not_configured" &&
|
|
322
|
+
!result.failureCause,
|
|
323
|
+
});
|
|
324
|
+
runtime.guide.deliver(ctx);
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: "text",
|
|
329
|
+
text: renderResultReport(report, { details: envelope }),
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
details: {
|
|
333
|
+
...result,
|
|
334
|
+
result: report,
|
|
335
|
+
evidenceContext,
|
|
336
|
+
evidenceFacts,
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
const textResult = (text, cause = "invalid_arguments") => {
|
|
341
|
+
refusalCause = cause;
|
|
342
|
+
const technical = findResults.reduce((totals, judgment) => ({
|
|
343
|
+
calls: totals.calls + (judgment.calls ?? 0),
|
|
344
|
+
questions: totals.questions + (judgment.questions ?? 0),
|
|
345
|
+
cacheHits: totals.cacheHits + (judgment.cacheHits ?? 0),
|
|
346
|
+
cacheRequests: totals.cacheRequests + (judgment.cacheRequests ?? 0),
|
|
347
|
+
}), { calls: 0, questions: 0, cacheHits: 0, cacheRequests: 0 });
|
|
348
|
+
return finish({ ok: false, error: text, ...technical }, { refusal: text });
|
|
349
|
+
};
|
|
350
|
+
if (!evidence.ok)
|
|
351
|
+
return textResult(evidence.error, evidence.cause ?? "invalid_root");
|
|
352
|
+
if (!asks.ok)
|
|
353
|
+
return textResult(asks.error);
|
|
354
|
+
const preliminary = resolveAskReferences(asks, args.state, {}, []);
|
|
355
|
+
if (preliminary.invalid.length) {
|
|
356
|
+
evidenceFacts.references = preliminary;
|
|
357
|
+
return textResult(preliminary.invalid.map((item) => item.reason).join("; "), "forbidden_path");
|
|
358
|
+
}
|
|
359
|
+
if (!client)
|
|
360
|
+
return textResult(NOT_CONFIGURED, "not_configured");
|
|
361
|
+
if (args.command && !allowCommand)
|
|
362
|
+
return textResult("command disabled by JEV_TOOLS_ALLOW_COMMAND=0");
|
|
363
|
+
if (args.timeout_s !== undefined &&
|
|
364
|
+
(!Number.isFinite(args.timeout_s) ||
|
|
365
|
+
args.timeout_s < 1 ||
|
|
366
|
+
args.timeout_s > ASK_TIMEOUT_MAX_S))
|
|
367
|
+
return textResult(`timeout_s must be between 1 and ${ASK_TIMEOUT_MAX_S}`);
|
|
368
|
+
if (args.state === undefined && !args.paths?.length && !args.command)
|
|
369
|
+
return textResult("Provide state, command or at least one path.");
|
|
370
|
+
const files = await collectFiles(cwd, args.paths ?? [], signal, {
|
|
371
|
+
exec,
|
|
372
|
+
allowEmpty: true,
|
|
373
|
+
skipInvalidUtf8: true,
|
|
374
|
+
});
|
|
375
|
+
if (!files.ok)
|
|
376
|
+
return textResult(files.error, files.cause ?? "file_unavailable");
|
|
377
|
+
if (!Object.keys(files.files).length &&
|
|
378
|
+
args.state === undefined &&
|
|
379
|
+
!args.command &&
|
|
380
|
+
files.skipped.length)
|
|
381
|
+
return textResult(`No readable evidence: ${files.skipped.join("; ")}`, "collection_empty");
|
|
382
|
+
const proofLimitations = files.skipped.map((fact) => ({
|
|
383
|
+
fact,
|
|
384
|
+
next: "Provide this file as UTF-8 text if the judgment needs it.",
|
|
385
|
+
}));
|
|
386
|
+
let repository = await collectAskRepository(exec, cwd, args.base, signal);
|
|
387
|
+
inventoryCollected = repository.ok;
|
|
388
|
+
if (repository.ok) {
|
|
389
|
+
evidenceFacts.inventory = [...repository.known];
|
|
390
|
+
evidenceContext.resolvedBase = repository.baseSha;
|
|
391
|
+
}
|
|
392
|
+
if (!repository.ok && args.base)
|
|
393
|
+
return textResult(repository.error, "invalid_base");
|
|
394
|
+
if (!repository.ok)
|
|
395
|
+
proofLimitations.push({
|
|
396
|
+
fact: `closure unavailable: ${repository.error}`,
|
|
397
|
+
next: "pass dependencies explicitly in paths",
|
|
398
|
+
});
|
|
399
|
+
if (args.command)
|
|
400
|
+
commandContext = {
|
|
401
|
+
...commandContext,
|
|
402
|
+
execution: "started",
|
|
403
|
+
cwd: known(repository.ok ? repository.root : cwd),
|
|
404
|
+
};
|
|
405
|
+
const command = args.command
|
|
406
|
+
? await captureCommand(exec, repository.ok ? repository.root : cwd, args.command, args.timeout_s, signal, dependencies.apiKey ?? process.env.JEV_TOOLS_API_KEY)
|
|
407
|
+
: undefined;
|
|
408
|
+
if (command?.ok)
|
|
409
|
+
commandContext = {
|
|
410
|
+
execution: "finished",
|
|
411
|
+
cwd: known(repository.ok ? repository.root : cwd),
|
|
412
|
+
exitCode: known(command.output.exit_code),
|
|
413
|
+
timedOut: known(command.output.timed_out),
|
|
414
|
+
};
|
|
415
|
+
if (command && !command.ok) {
|
|
416
|
+
commandContext = {
|
|
417
|
+
// Reported execution states are not_requested, not_started, started
|
|
418
|
+
// and finished. A spawn whose completion was not observed (adapter
|
|
419
|
+
// "unknown") did start, so it reports "started" with unknown exit
|
|
420
|
+
// code and timeout.
|
|
421
|
+
execution: command.commandExecution === "unknown"
|
|
422
|
+
? "started"
|
|
423
|
+
: command.commandExecution,
|
|
424
|
+
cwd: known(repository.ok ? repository.root : cwd),
|
|
425
|
+
exitCode: command.commandExitCode !== undefined
|
|
426
|
+
? known(command.commandExitCode)
|
|
427
|
+
: unknown("command completion not observed"),
|
|
428
|
+
timedOut: command.commandTimedOut !== undefined
|
|
429
|
+
? known(command.commandTimedOut)
|
|
430
|
+
: unknown("command completion not observed"),
|
|
431
|
+
};
|
|
432
|
+
return textResult(command.error, command.cause ?? "file_unavailable");
|
|
433
|
+
}
|
|
434
|
+
if (command?.ok) {
|
|
435
|
+
repository = await collectAskRepository(exec, cwd, args.base, signal);
|
|
436
|
+
if (!repository.ok && args.base)
|
|
437
|
+
return textResult(repository.error, "invalid_base");
|
|
438
|
+
}
|
|
439
|
+
const initialState = assembleState(args.state, files.files);
|
|
440
|
+
if (!initialState.ok)
|
|
441
|
+
return textResult(initialState.error);
|
|
442
|
+
if (command?.ok && Object.hasOwn(initialState.state, "output"))
|
|
443
|
+
return textResult("state contains reserved key output when command is supplied; rename it in your note", "reserved_key");
|
|
444
|
+
let outputNotIdentifiable = false;
|
|
445
|
+
if (command?.ok && repository.ok) {
|
|
446
|
+
for (const target of command.targets) {
|
|
447
|
+
if (target.path.startsWith(`${repository.root}/`))
|
|
448
|
+
target.path = target.path.slice(repository.root.length + 1);
|
|
449
|
+
else if (isAbsolute(target.path)) {
|
|
450
|
+
// Windows reports C:/... or short 8.3 forms; compare canonically.
|
|
451
|
+
const local = relative(repository.root, await canonicalPath(target.path))
|
|
452
|
+
.split("\\")
|
|
453
|
+
.join("/");
|
|
454
|
+
if (local && !local.startsWith("../") && !isAbsolute(local))
|
|
455
|
+
target.path = local;
|
|
456
|
+
}
|
|
457
|
+
let matches = [...repository.known].filter((path) => path === target.path || path.endsWith(`/${target.path}`));
|
|
458
|
+
if (target.testName) {
|
|
459
|
+
const matched = [];
|
|
460
|
+
for (const candidate of matches) {
|
|
461
|
+
const source = await collectFiles(repository.root, [candidate], signal, { exec, allowEmpty: true, inventory: repository.known });
|
|
462
|
+
if (source.ok &&
|
|
463
|
+
discoverTests([
|
|
464
|
+
{ path: candidate, text: source.files[candidate] ?? "" },
|
|
465
|
+
]).entries.some((entry) => entry.scenarios.some((scenario) => scenario.name === target.testName)))
|
|
466
|
+
matched.push(candidate);
|
|
467
|
+
}
|
|
468
|
+
matches = matched;
|
|
469
|
+
}
|
|
470
|
+
if (matches.length !== 1) {
|
|
471
|
+
outputNotIdentifiable ||= command.assertion && target.assertion;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const path = matches[0];
|
|
475
|
+
if (!path)
|
|
476
|
+
continue;
|
|
477
|
+
const local = repositoryPath(cwd, repository.root, path);
|
|
478
|
+
if (!Object.hasOwn(files.files, local)) {
|
|
479
|
+
const added = await collectFiles(repository.root, [path], signal, {
|
|
480
|
+
exec,
|
|
481
|
+
allowEmpty: true,
|
|
482
|
+
inventory: repository.known,
|
|
483
|
+
});
|
|
484
|
+
if (added.ok && Object.keys(files.files).length < ASK_MAX_FILES) {
|
|
485
|
+
files.files[local] = added.files[path] ?? "";
|
|
486
|
+
files.identities.push(...added.identities.map((identity) => ({
|
|
487
|
+
...identity,
|
|
488
|
+
requestedPath: local,
|
|
489
|
+
})));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
outputNotIdentifiable ||= command.assertion && target.assertion;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (target.assertion && target.nodeId?.includes("::")) {
|
|
497
|
+
const discovery = discoverTests([
|
|
498
|
+
{ path, text: files.files[local] ?? "" },
|
|
499
|
+
]);
|
|
500
|
+
const name = target.nodeId
|
|
501
|
+
.split("::")
|
|
502
|
+
.slice(1)
|
|
503
|
+
.join("::")
|
|
504
|
+
.replace(/\[[\s\S]*\]$/, "");
|
|
505
|
+
if (!discovery.entries.some((entry) => entry.scenarios.some((scenario) => scenario.id === name ||
|
|
506
|
+
scenario.name === name.split("::").at(-1))))
|
|
507
|
+
outputNotIdentifiable ||= command.assertion;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (command?.ok &&
|
|
512
|
+
command.assertion &&
|
|
513
|
+
(!command.targets.length || !repository.ok) &&
|
|
514
|
+
!discoverTests(Object.entries(files.files).map(([path, text]) => ({ path, text }))).entries.length)
|
|
515
|
+
outputNotIdentifiable = true;
|
|
516
|
+
const references = resolveAskReferences(asks, args.state, repository.ok
|
|
517
|
+
? {
|
|
518
|
+
files: Object.fromEntries(Object.entries(files.files).map(([path, text]) => [
|
|
519
|
+
repositoryPath(repository.root, cwd, path),
|
|
520
|
+
text,
|
|
521
|
+
])),
|
|
522
|
+
beforeInventory: args.base ? [...repository.beforeKnown] : [],
|
|
523
|
+
}
|
|
524
|
+
: files.files, repository?.ok ? [...repository.known] : []);
|
|
525
|
+
evidenceFacts.references = references;
|
|
526
|
+
evidenceFacts.inventory = repository.ok ? [...repository.known] : [];
|
|
527
|
+
if (references.invalid.length)
|
|
528
|
+
return textResult(references.invalid.map((item) => item.reason).join("; "), "forbidden_path");
|
|
529
|
+
const unresolved = [...references.unresolved];
|
|
530
|
+
const historical = {};
|
|
531
|
+
if (repository.ok && args.base) {
|
|
532
|
+
for (const reference of references.additions.filter((reference) => reference.side === "files_before" && reference.required)) {
|
|
533
|
+
const before = await repository.readBefore(reference.path);
|
|
534
|
+
if (before.ok)
|
|
535
|
+
historical[reference.path] = before.text;
|
|
536
|
+
else {
|
|
537
|
+
if (before.cause === "forbidden_path")
|
|
538
|
+
return textResult(before.error, before.cause);
|
|
539
|
+
unresolved.push({
|
|
540
|
+
...reference,
|
|
541
|
+
reason: before.error,
|
|
542
|
+
...(before.cause ? { cause: before.cause } : {}),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (repository?.ok) {
|
|
548
|
+
const root = repository.root;
|
|
549
|
+
const additions = await Promise.all(references.additions
|
|
550
|
+
.filter((addition) => addition.side !== "files_before")
|
|
551
|
+
.map(async (addition) => ({
|
|
552
|
+
addition,
|
|
553
|
+
added: await collectFiles(root, [addition.path], signal, {
|
|
554
|
+
allowEmpty: true,
|
|
555
|
+
exec,
|
|
556
|
+
inventory: repository.known,
|
|
557
|
+
}),
|
|
558
|
+
})));
|
|
559
|
+
for (const { addition, added } of additions) {
|
|
560
|
+
if (added.ok) {
|
|
561
|
+
const local = repositoryPath(cwd, root, addition.path);
|
|
562
|
+
files.files[local] = added.files[addition.path] ?? "";
|
|
563
|
+
files.identities.push(...added.identities.map((identity) => ({
|
|
564
|
+
...identity,
|
|
565
|
+
requestedPath: local,
|
|
566
|
+
})));
|
|
567
|
+
}
|
|
568
|
+
else
|
|
569
|
+
unresolved.push({
|
|
570
|
+
...addition,
|
|
571
|
+
reason: added.error,
|
|
572
|
+
...(added.cause ? { cause: added.cause } : {}),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const state = assembleState(args.state, files.files);
|
|
577
|
+
if (!state.ok)
|
|
578
|
+
return textResult(state.error);
|
|
579
|
+
if (command?.ok && Object.hasOwn(state.state, "output"))
|
|
580
|
+
return textResult("state contains reserved key output when command is supplied; rename it in your note", "reserved_key");
|
|
581
|
+
if (command?.ok)
|
|
582
|
+
state.state.output = { ...command.output };
|
|
583
|
+
if (repository?.ok) {
|
|
584
|
+
if (evidenceContext.effectiveRoot)
|
|
585
|
+
evidenceContext.effectiveRoot.path = repository.root;
|
|
586
|
+
evidenceContext.requestedBase = args.base;
|
|
587
|
+
evidenceContext.resolvedBase = repository.baseSha;
|
|
588
|
+
const canonical = Object.fromEntries(Object.entries(files.files).map(([path, text]) => [
|
|
589
|
+
repositoryPath(repository.root, cwd, path),
|
|
590
|
+
text,
|
|
591
|
+
]));
|
|
592
|
+
state.state.files = canonical;
|
|
593
|
+
state.state.evidence = {
|
|
594
|
+
context: evidenceContext,
|
|
595
|
+
aliases: Object.fromEntries(Object.entries(references.aliases)
|
|
596
|
+
.map(([alias, path]) => [
|
|
597
|
+
alias,
|
|
598
|
+
repository.known.has(path) || repository.beforeKnown.has(path)
|
|
599
|
+
? path
|
|
600
|
+
: repositoryPath(repository.root, cwd, path),
|
|
601
|
+
])
|
|
602
|
+
.sort(([a], [b]) => a.localeCompare(b))),
|
|
603
|
+
};
|
|
604
|
+
// Canonical repository paths are the sole serialized content keys.
|
|
605
|
+
const sources = Object.entries(canonical).map(([path, text]) => ({ path, text }));
|
|
606
|
+
const before = { ...historical };
|
|
607
|
+
if (args.base) {
|
|
608
|
+
for (const path of Object.keys(canonical)) {
|
|
609
|
+
const old = await repository.readBefore(path);
|
|
610
|
+
if (!old.ok)
|
|
611
|
+
return textResult(old.error, old.cause ?? "file_unavailable");
|
|
612
|
+
before[path] = old.text;
|
|
613
|
+
}
|
|
614
|
+
state.state.base_sha = repository.baseSha ?? "";
|
|
615
|
+
state.state.files_before = before;
|
|
616
|
+
const stateSize = JSON.stringify(command?.ok ? { ...state.state, output: undefined } : state.state).length;
|
|
617
|
+
if (stateSize > STATE_MAX_CHARS) {
|
|
618
|
+
const partSizes = Object.entries(state.state)
|
|
619
|
+
.map(([part, value]) => `${part} ${JSON.stringify(value).length}`)
|
|
620
|
+
.join(", ");
|
|
621
|
+
return textResult(`serialized state including files_before exceeds ${STATE_MAX_CHARS} chars (${stateSize}); serialized parts: ${partSizes}; split the situation into smaller states.`, "evidence_too_large");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const configuration = [];
|
|
625
|
+
for (const path of repository.known) {
|
|
626
|
+
if (!/(^|\/)(?:(tsconfig|jsconfig)(?:\.[^/]+)?|package)\.json$/.test(path))
|
|
627
|
+
continue;
|
|
628
|
+
const source = await repository.read(path);
|
|
629
|
+
if (source.ok)
|
|
630
|
+
configuration.push(source);
|
|
631
|
+
}
|
|
632
|
+
const analysis = await createAnalysisContext();
|
|
633
|
+
const buildGraph = createImportGraphBuilder(configuration, repository.known, analysis);
|
|
634
|
+
const edges = new Map();
|
|
635
|
+
const limits = [];
|
|
636
|
+
const declarations = [];
|
|
637
|
+
const bindings = [];
|
|
638
|
+
const syntaxLimits = [];
|
|
639
|
+
const uses = new Map();
|
|
640
|
+
const reached = new Set(sources.map((source) => source.path));
|
|
641
|
+
for (const path of Object.keys(canonical)) {
|
|
642
|
+
if (!/(^|\/)(test[^/]*|[^/]*_test)\.py$/.test(path))
|
|
643
|
+
continue;
|
|
644
|
+
let parent = path.slice(0, path.lastIndexOf("/") + 1);
|
|
645
|
+
while (true) {
|
|
646
|
+
const fixture = `${parent}conftest.py`;
|
|
647
|
+
if (repository.known.has(fixture) && !reached.has(fixture)) {
|
|
648
|
+
reached.add(fixture);
|
|
649
|
+
const loaded = await repository.read(fixture);
|
|
650
|
+
if (loaded.ok)
|
|
651
|
+
sources.push(loaded);
|
|
652
|
+
}
|
|
653
|
+
if (!parent)
|
|
654
|
+
break;
|
|
655
|
+
parent = parent
|
|
656
|
+
.slice(0, -1)
|
|
657
|
+
.slice(0, parent.slice(0, -1).lastIndexOf("/") + 1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (let index = 0; index < sources.length; index++) {
|
|
661
|
+
const source = sources[index];
|
|
662
|
+
if (!source)
|
|
663
|
+
continue;
|
|
664
|
+
if (args.base && !Object.hasOwn(before, source.path)) {
|
|
665
|
+
const old = await repository.readBefore(source.path);
|
|
666
|
+
if (old.ok)
|
|
667
|
+
before[source.path] = old.text;
|
|
668
|
+
else
|
|
669
|
+
proofLimitations.push({
|
|
670
|
+
fact: old.error,
|
|
671
|
+
next: `provide before evidence for ${source.path}`,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
const parsed = await collectProofSyntax([source], args.base ? before : undefined, analysis.parser);
|
|
675
|
+
declarations.push(...parsed.declarations);
|
|
676
|
+
bindings.push(...parsed.bindings);
|
|
677
|
+
syntaxLimits.push(...parsed.limits);
|
|
678
|
+
for (const [path, names] of parsed.uses ?? [])
|
|
679
|
+
uses.set(path, names);
|
|
680
|
+
const graph = buildGraph([source]);
|
|
681
|
+
for (const [path, targets] of graph.edges)
|
|
682
|
+
edges.set(path, targets);
|
|
683
|
+
limits.push(...graph.limits);
|
|
684
|
+
if (!Object.hasOwn(canonical, source.path) &&
|
|
685
|
+
!source.path.endsWith("conftest.py") &&
|
|
686
|
+
!parsed.bindings.some((binding) => binding.reexport))
|
|
687
|
+
continue;
|
|
688
|
+
for (const path of graph.edges.get(source.path) ?? []) {
|
|
689
|
+
if (reached.has(path))
|
|
690
|
+
continue;
|
|
691
|
+
reached.add(path);
|
|
692
|
+
const loaded = await repository.read(path);
|
|
693
|
+
if (loaded.ok)
|
|
694
|
+
sources.push(loaded);
|
|
695
|
+
else
|
|
696
|
+
proofLimitations.push({
|
|
697
|
+
fact: loaded.error,
|
|
698
|
+
next: `provide readable evidence for ${path}`,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const syntax = {
|
|
703
|
+
declarations,
|
|
704
|
+
bindings,
|
|
705
|
+
limits: syntaxLimits,
|
|
706
|
+
uses,
|
|
707
|
+
sources,
|
|
708
|
+
...(args.base ? { before } : {}),
|
|
709
|
+
};
|
|
710
|
+
const closureState = { ...state.state };
|
|
711
|
+
if (command?.ok)
|
|
712
|
+
delete closureState.output;
|
|
713
|
+
const closure = buildAskClosure({
|
|
714
|
+
graph: { edges, limits },
|
|
715
|
+
syntax,
|
|
716
|
+
paths: Object.keys(canonical),
|
|
717
|
+
intentions: JSON.stringify(asks.asks),
|
|
718
|
+
state: closureState,
|
|
719
|
+
});
|
|
720
|
+
state.state = closure.state;
|
|
721
|
+
if (command?.ok)
|
|
722
|
+
state.state.output = { ...command.output };
|
|
723
|
+
proofLimitations.push(...closure.limitations.filter((limit) => !limit.fact.startsWith("Import closure only,")));
|
|
724
|
+
proofLimitations.push({
|
|
725
|
+
fact: `closure: +${closure.added.length}${closure.added.length ? ` (${closure.added.map((piece) => `${piece.path} ${piece.name}, ${piece.chars} chars`).join("; ")})` : ""}; imports only, depth 1; files with no import link are not checked`,
|
|
726
|
+
next: "pass unrelated config, docs and other flows in paths when the answer depends on them",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
else
|
|
730
|
+
proofLimitations.push({
|
|
731
|
+
fact: "closure: +0; imports only, depth 1; repository inventory unavailable",
|
|
732
|
+
next: "pass dependencies explicitly in paths",
|
|
733
|
+
});
|
|
734
|
+
if (!repository.ok)
|
|
735
|
+
state.state.evidence = {
|
|
736
|
+
context: evidenceContext,
|
|
737
|
+
aliases: references.aliases,
|
|
738
|
+
};
|
|
739
|
+
// Command output has its own passage-selection budget below. Only immutable
|
|
740
|
+
// file/note evidence can refuse here, before selection has had a chance.
|
|
741
|
+
const serializedSize = JSON.stringify(command?.ok ? { ...state.state, output: undefined } : state.state).length;
|
|
742
|
+
if (serializedSize > STATE_MAX_CHARS)
|
|
743
|
+
return textResult(`serialized state exceeds ${STATE_MAX_CHARS} chars (${serializedSize}); files and versions are never truncated`, "evidence_too_large");
|
|
744
|
+
evidenceFacts.omissions = unresolved;
|
|
745
|
+
const blocked = blockedAskReadings(asks, state.state, unresolved, args.state);
|
|
746
|
+
const inserted = isRecord(state.state.files) ? state.state.files : {};
|
|
747
|
+
const identities = files.identities.map((file) => {
|
|
748
|
+
const insertedPath = repository.ok
|
|
749
|
+
? repositoryPath(repository.root, cwd, file.requestedPath)
|
|
750
|
+
: file.requestedPath;
|
|
751
|
+
const content = inserted[insertedPath];
|
|
752
|
+
return {
|
|
753
|
+
...file,
|
|
754
|
+
insertedPath: resolve(repository.ok ? repository.root : cwd, insertedPath),
|
|
755
|
+
content: typeof content === "string" ? content : "",
|
|
756
|
+
insertedSha256: createHash("sha256")
|
|
757
|
+
.update(typeof content === "string" ? content : "")
|
|
758
|
+
.digest("hex"),
|
|
759
|
+
};
|
|
760
|
+
});
|
|
761
|
+
const blockedMembers = new Set(asks.groups
|
|
762
|
+
.filter((group) => group.some((id) => blocked.has(id)))
|
|
763
|
+
.flat());
|
|
764
|
+
evidenceFacts.blockedGroups = asks.groups
|
|
765
|
+
.filter((group) => group.some((id) => blocked.has(id)))
|
|
766
|
+
.map((ids) => {
|
|
767
|
+
const reason = ids
|
|
768
|
+
.map((id) => blocked.get(id))
|
|
769
|
+
.find((reason) => reason !== undefined) ??
|
|
770
|
+
"Required evidence unavailable";
|
|
771
|
+
// A secret refusal stays named as such instead of missing_required.
|
|
772
|
+
const secret = unresolved.some((item) => item.reason === reason && item.cause === "secret_pattern");
|
|
773
|
+
return {
|
|
774
|
+
ids,
|
|
775
|
+
reason,
|
|
776
|
+
...(secret ? { cause: "secret_pattern" } : {}),
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
const questions = Object.fromEntries(Object.entries(asks.questions).filter(([id]) => !blockedMembers.has(id)));
|
|
780
|
+
const integrity = checkIntegrity(state.state, asks.asks, identities.filter((identity) => identity.content.trim().length > 0));
|
|
781
|
+
let sent = 0;
|
|
782
|
+
let refusal;
|
|
783
|
+
const options = {
|
|
784
|
+
signal,
|
|
785
|
+
cache: !args.command,
|
|
786
|
+
...runtime.session.requestGate(),
|
|
787
|
+
admissionCause: () => refusal?.kind === "session"
|
|
788
|
+
? "session_budget"
|
|
789
|
+
: "call_budget",
|
|
790
|
+
beforeRequest: (questionCount) => {
|
|
791
|
+
if (args.max_calls !== undefined && sent >= args.max_calls) {
|
|
792
|
+
refusal = {
|
|
793
|
+
kind: "max_calls",
|
|
794
|
+
message: `max_calls=${args.max_calls} reached`,
|
|
795
|
+
};
|
|
796
|
+
return { ok: false, error: refusal.message };
|
|
797
|
+
}
|
|
798
|
+
const admitted = runtime.session.admit(questionCount);
|
|
799
|
+
if (!admitted.ok) {
|
|
800
|
+
refusal = { kind: "session", message: admitted.error };
|
|
801
|
+
return admitted;
|
|
802
|
+
}
|
|
803
|
+
sent++;
|
|
804
|
+
return admitted;
|
|
805
|
+
},
|
|
806
|
+
onUsage: (usage) => runtime.session.recordUsage(usage),
|
|
807
|
+
};
|
|
808
|
+
if (command?.ok) {
|
|
809
|
+
const output = command.output;
|
|
810
|
+
const text = output.stdout + output.stderr;
|
|
811
|
+
const size = JSON.stringify({ ...state.state, output }).length;
|
|
812
|
+
if (size > STATE_MAX_CHARS) {
|
|
813
|
+
const stdoutChunks = outputChunks(output.stdout);
|
|
814
|
+
const stderrChunks = outputChunks(output.stderr);
|
|
815
|
+
const chunks = [...stdoutChunks, ...stderrChunks];
|
|
816
|
+
if (command.compressedChars >
|
|
817
|
+
OUTPUT_CHUNK_CHARS * OUTPUT_FIND_MAX_CALLS ||
|
|
818
|
+
chunks.length > OUTPUT_FIND_MAX_CALLS)
|
|
819
|
+
return textResult(`output requires more than ${OUTPUT_FIND_MAX_CALLS} find calls: ${command.originalBytes} bytes before, ${command.compressedChars} chars after compression; narrow command (remove verbosity or filter)`, "evidence_too_large");
|
|
820
|
+
const chunkStates = chunks.map((chunk) => withEvidenceContext({ output: chunk.text }, evidenceContext));
|
|
821
|
+
if (chunkStates.some((chunkState) => JSON.stringify(chunkState).length > STATE_MAX_CHARS))
|
|
822
|
+
return textResult(`serialized output chunk including evidence context exceeds ${STATE_MAX_CHARS} chars; narrow command`, "evidence_too_large");
|
|
823
|
+
const found = await Promise.all(chunkStates.map(async (chunkState) => {
|
|
824
|
+
const judgment = await client.judge(chunkState, {
|
|
825
|
+
find: {
|
|
826
|
+
type: "bool",
|
|
827
|
+
instructions: "Does this excerpt of a command's output show something failing (a failed test or assertion, a compiler error, a crash, an error message) with its details?",
|
|
828
|
+
},
|
|
829
|
+
}, options);
|
|
830
|
+
findResults.push(judgment);
|
|
831
|
+
return judgment.ok && judgment.answers.find?.type === "bool"
|
|
832
|
+
? judgment.answers.find.p
|
|
833
|
+
: 0;
|
|
834
|
+
}));
|
|
835
|
+
if (findResults.some((judgment) => !judgment.ok || judgment.answers.find?.type !== "bool"))
|
|
836
|
+
integrity.controls.push({
|
|
837
|
+
fact: "output find incomplete: some chunks were not judged",
|
|
838
|
+
next: "narrow command or raise max_calls",
|
|
839
|
+
});
|
|
840
|
+
const emptySize = JSON.stringify({
|
|
841
|
+
...state.state,
|
|
842
|
+
output: { ...output, stdout: "", stderr: "", truncated: true },
|
|
843
|
+
}).length;
|
|
844
|
+
const budget = STATE_MAX_CHARS - emptySize - 16;
|
|
845
|
+
if (budget < 100)
|
|
846
|
+
return textResult("No space for command output; reduce paths or state.", "evidence_too_large");
|
|
847
|
+
const stderrSize = JSON.stringify(output.stderr).length;
|
|
848
|
+
const stdoutSize = JSON.stringify(output.stdout).length;
|
|
849
|
+
const stdoutBudget = !output.stderr
|
|
850
|
+
? budget
|
|
851
|
+
: stderrSize < budget / 2
|
|
852
|
+
? budget - stderrSize
|
|
853
|
+
: stdoutSize < budget / 2
|
|
854
|
+
? stdoutSize
|
|
855
|
+
: Math.floor((budget * output.stdout.length) / Math.max(1, text.length));
|
|
856
|
+
output.stdout = output.stdout
|
|
857
|
+
? selectOutput(output.stdout, stdoutChunks, found.slice(0, stdoutChunks.length), stdoutBudget)
|
|
858
|
+
: "";
|
|
859
|
+
output.stderr = output.stderr
|
|
860
|
+
? selectOutput(output.stderr, stderrChunks, found.slice(stdoutChunks.length), budget - stdoutBudget)
|
|
861
|
+
: "";
|
|
862
|
+
if (chunks.some((chunk, index) => (found[index] ?? 0) >= 0.5 &&
|
|
863
|
+
!(index < stdoutChunks.length ? output.stdout : output.stderr).includes(chunk.text)))
|
|
864
|
+
integrity.controls.push({
|
|
865
|
+
fact: "failing output passages omitted: insufficient state budget",
|
|
866
|
+
next: "narrow command or reduce paths",
|
|
867
|
+
});
|
|
868
|
+
output.truncated = true;
|
|
869
|
+
command.selectedPassages =
|
|
870
|
+
output.stdout.length + output.stderr.length < text.length;
|
|
871
|
+
}
|
|
872
|
+
state.state.output = { ...output };
|
|
873
|
+
const checked = checkIntegrity(state.state, asks.asks, [], {
|
|
874
|
+
text: output.stdout + output.stderr,
|
|
875
|
+
truncated: output.truncated,
|
|
876
|
+
omittedChars: Math.max(0, command.lineOmittedChars +
|
|
877
|
+
text.length -
|
|
878
|
+
output.stdout.length -
|
|
879
|
+
output.stderr.length),
|
|
880
|
+
});
|
|
881
|
+
integrity.controls.push(...checked.controls);
|
|
882
|
+
integrity.notes.push(...checked.notes);
|
|
883
|
+
if (JSON.stringify(state.state).length > STATE_MAX_CHARS)
|
|
884
|
+
return textResult("serialized command output exceeds state budget; narrow command or reduce paths", "evidence_too_large");
|
|
885
|
+
if (outputNotIdentifiable)
|
|
886
|
+
integrity.controls.push({
|
|
887
|
+
fact: "not identifiable: the output shows an assertion failure and the failing test is absent; a wrong test and a bug look alike",
|
|
888
|
+
next: "pass the failing test and its code in paths",
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
let result = Object.keys(questions).length
|
|
892
|
+
? await client.judge(state.state, questions, {
|
|
893
|
+
...options,
|
|
894
|
+
groups: asks.groups.filter((group) => !group.some((id) => blockedMembers.has(id))),
|
|
895
|
+
})
|
|
896
|
+
: { ok: true, answers: {}, calls: 0, questions: 0 };
|
|
897
|
+
if (result.ok) {
|
|
898
|
+
for (const id of blockedMembers)
|
|
899
|
+
result.answers[id] = {
|
|
900
|
+
type: "unjudged",
|
|
901
|
+
reason: blocked.get(id) ?? "required evidence is absent or empty",
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
let reversed;
|
|
905
|
+
if (result.ok) {
|
|
906
|
+
const reverse = reverseQuestions(asks, result.answers);
|
|
907
|
+
if (Object.keys(reverse).length) {
|
|
908
|
+
reversed = await client.judge(state.state, reverse, options);
|
|
909
|
+
if (!reversed.ok)
|
|
910
|
+
reversed = {
|
|
911
|
+
...reversed,
|
|
912
|
+
ok: true,
|
|
913
|
+
answers: Object.fromEntries(Object.keys(reverse).map((id) => [
|
|
914
|
+
id,
|
|
915
|
+
{
|
|
916
|
+
type: "unjudged",
|
|
917
|
+
reason: reversed && !reversed.ok
|
|
918
|
+
? reversed.error
|
|
919
|
+
: "reverse control failed",
|
|
920
|
+
},
|
|
921
|
+
])),
|
|
922
|
+
};
|
|
923
|
+
const first = result;
|
|
924
|
+
result = {
|
|
925
|
+
...first,
|
|
926
|
+
calls: (first.calls ?? 0) + (reversed.calls ?? 0),
|
|
927
|
+
questions: (first.questions ?? 0) + (reversed.questions ?? 0),
|
|
928
|
+
cacheHits: (first.cacheHits ?? 0) + (reversed.cacheHits ?? 0),
|
|
929
|
+
cacheRequests: (first.cacheRequests ?? 0) + (reversed.cacheRequests ?? 0),
|
|
930
|
+
usage: first.usage && reversed.usage
|
|
931
|
+
? {
|
|
932
|
+
inputTokens: first.usage.inputTokens + reversed.usage.inputTokens,
|
|
933
|
+
costUsd: first.usage.costUsd + reversed.usage.costUsd,
|
|
934
|
+
}
|
|
935
|
+
: undefined,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
for (const found of findResults)
|
|
940
|
+
result = {
|
|
941
|
+
...result,
|
|
942
|
+
calls: (result.calls ?? 0) + (found.calls ?? 0),
|
|
943
|
+
questions: (result.questions ?? 0) + (found.questions ?? 0),
|
|
944
|
+
cacheHits: (result.cacheHits ?? 0) + (found.cacheHits ?? 0),
|
|
945
|
+
cacheRequests: (result.cacheRequests ?? 0) + (found.cacheRequests ?? 0),
|
|
946
|
+
usage: result.usage && found.usage
|
|
947
|
+
? {
|
|
948
|
+
inputTokens: result.usage.inputTokens + found.usage.inputTokens,
|
|
949
|
+
costUsd: result.usage.costUsd + found.usage.costUsd,
|
|
950
|
+
}
|
|
951
|
+
: undefined,
|
|
952
|
+
};
|
|
953
|
+
const attribution = [];
|
|
954
|
+
if (result.ok) {
|
|
955
|
+
const initial = readAsks(asks, result.answers, reversed?.ok ? reversed.answers : undefined);
|
|
956
|
+
for (const reading of asks.readings.filter((reading) => reading.attributable)) {
|
|
957
|
+
const selected = initial.find((answer) => answer.label === reading.label)?.value?.head;
|
|
958
|
+
if (typeof selected !== "string" ||
|
|
959
|
+
!reading.among?.includes(selected)) {
|
|
960
|
+
attribution.push({
|
|
961
|
+
fact: `attribution ${reading.label}: no designated candidate`,
|
|
962
|
+
next: "provide decisive candidate evidence before acting",
|
|
963
|
+
});
|
|
964
|
+
attributionMissing.add(reading.id);
|
|
965
|
+
auxiliary.controls.notJudged++;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (!Object.hasOwn(inserted, selected)) {
|
|
969
|
+
attribution.push({
|
|
970
|
+
fact: `attribution ${reading.label}: designated piece ${selected} is absent`,
|
|
971
|
+
next: `add ${selected} to paths before acting`,
|
|
972
|
+
});
|
|
973
|
+
attributionMissing.add(reading.id);
|
|
974
|
+
auxiliary.controls.notJudged++;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
const selectedPath = repository?.ok
|
|
978
|
+
? (references.additions.find((addition) => addition.reference === selected)?.path ?? repositoryPath(repository.root, cwd, selected))
|
|
979
|
+
: selected;
|
|
980
|
+
const retained = (path) => {
|
|
981
|
+
if (!repository?.ok)
|
|
982
|
+
return path !== selected;
|
|
983
|
+
return ((references.additions.find((addition) => addition.reference === path)?.path ?? repositoryPath(repository.root, cwd, path)) !==
|
|
984
|
+
selectedPath);
|
|
985
|
+
};
|
|
986
|
+
const ablated = {
|
|
987
|
+
...state.state,
|
|
988
|
+
files: Object.fromEntries(Object.entries(inserted).filter(([path]) => retained(path))),
|
|
989
|
+
...(isRecord(state.state.files_before)
|
|
990
|
+
? {
|
|
991
|
+
files_before: Object.fromEntries(Object.entries(state.state.files_before).filter(([path]) => retained(path))),
|
|
992
|
+
}
|
|
993
|
+
: {}),
|
|
994
|
+
...(isRecord(state.state.closure)
|
|
995
|
+
? {
|
|
996
|
+
closure: Object.fromEntries(Object.entries(state.state.closure).filter(([piece]) => !piece.startsWith(`${selectedPath} (`))),
|
|
997
|
+
}
|
|
998
|
+
: {}),
|
|
999
|
+
};
|
|
1000
|
+
const question = questions[reading.id];
|
|
1001
|
+
if (!question)
|
|
1002
|
+
continue;
|
|
1003
|
+
const control = await client.judge(ablated, { [reading.id]: question }, options);
|
|
1004
|
+
const answer = control.ok ? control.answers[reading.id] : undefined;
|
|
1005
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1006
|
+
attributionAnswers[reading.id] = [
|
|
1007
|
+
{
|
|
1008
|
+
id: `${reading.id}:attribution`,
|
|
1009
|
+
kind: "attribution",
|
|
1010
|
+
source: answer.source,
|
|
1011
|
+
outcome: answer.type === "choice"
|
|
1012
|
+
? answer.choice === selected
|
|
1013
|
+
? "does not rest on it"
|
|
1014
|
+
: "attributed"
|
|
1015
|
+
: "invalid attribution response",
|
|
1016
|
+
rawValues: answer.type === "bool"
|
|
1017
|
+
? [
|
|
1018
|
+
{
|
|
1019
|
+
label: "attribution",
|
|
1020
|
+
value: answer.p >= 0.5,
|
|
1021
|
+
probability: known(answer.p),
|
|
1022
|
+
},
|
|
1023
|
+
]
|
|
1024
|
+
: Object.entries(answer.probabilities).map(([label, value]) => ({
|
|
1025
|
+
label,
|
|
1026
|
+
value: label,
|
|
1027
|
+
probability: known(value),
|
|
1028
|
+
})),
|
|
1029
|
+
},
|
|
1030
|
+
];
|
|
1031
|
+
if (answer?.type !== "choice")
|
|
1032
|
+
attributionMissing.add(reading.id);
|
|
1033
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1034
|
+
auxiliary.controls[answer.source]++;
|
|
1035
|
+
else
|
|
1036
|
+
auxiliary.controls.notJudged++;
|
|
1037
|
+
attribution.push(answer?.type === "choice"
|
|
1038
|
+
? {
|
|
1039
|
+
fact: `attribution ${reading.label}: ${answer.choice === selected ? "does not rest on it" : "attributed"} (${selected})`,
|
|
1040
|
+
next: "read the designated evidence before acting",
|
|
1041
|
+
}
|
|
1042
|
+
: {
|
|
1043
|
+
fact: `attribution ${reading.label}: unjudged${!control.ok ? `: ${control.error}` : answer?.type === "unjudged" ? `: ${answer.reason}` : ""}`,
|
|
1044
|
+
next: `rerun attribution for ${selected} before acting`,
|
|
1045
|
+
});
|
|
1046
|
+
result = {
|
|
1047
|
+
...result,
|
|
1048
|
+
calls: (result.calls ?? 0) + (control.calls ?? 0),
|
|
1049
|
+
questions: (result.questions ?? 0) + (control.questions ?? 0),
|
|
1050
|
+
cacheHits: (result.cacheHits ?? 0) + (control.cacheHits ?? 0),
|
|
1051
|
+
cacheRequests: (result.cacheRequests ?? 0) + (control.cacheRequests ?? 0),
|
|
1052
|
+
usage: result.usage && control.usage
|
|
1053
|
+
? {
|
|
1054
|
+
inputTokens: result.usage.inputTokens + control.usage.inputTokens,
|
|
1055
|
+
costUsd: result.usage.costUsd + control.usage.costUsd,
|
|
1056
|
+
}
|
|
1057
|
+
: undefined,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const unchecked = asks.readings
|
|
1062
|
+
.filter((reading) => !result.ok ||
|
|
1063
|
+
[
|
|
1064
|
+
reading.id,
|
|
1065
|
+
...(reading.twin ? [reading.twin] : []),
|
|
1066
|
+
...Object.values(reading.controls ?? {}),
|
|
1067
|
+
].some((id) => !result.answers[id] || result.answers[id]?.type === "unjudged") ||
|
|
1068
|
+
(reversed?.ok && reversed.answers[reading.id]?.type === "unjudged"))
|
|
1069
|
+
.map((reading) => reading.label);
|
|
1070
|
+
for (const reading of asks.readings) {
|
|
1071
|
+
for (const id of [
|
|
1072
|
+
reading.twin,
|
|
1073
|
+
...Object.values(reading.controls ?? {}),
|
|
1074
|
+
].filter((id) => !!id)) {
|
|
1075
|
+
const answer = result.ok ? result.answers[id] : undefined;
|
|
1076
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1077
|
+
auxiliary.controls[answer.source]++;
|
|
1078
|
+
else
|
|
1079
|
+
auxiliary.controls.notJudged++;
|
|
1080
|
+
}
|
|
1081
|
+
if (reversed &&
|
|
1082
|
+
Object.hasOwn(reversed.ok ? reversed.answers : {}, reading.id)) {
|
|
1083
|
+
const answer = reversed.ok ? reversed.answers[reading.id] : undefined;
|
|
1084
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1085
|
+
auxiliary.controls[answer.source]++;
|
|
1086
|
+
else
|
|
1087
|
+
auxiliary.controls.notJudged++;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
for (const found of findResults) {
|
|
1091
|
+
const answer = found.ok ? found.answers.find : undefined;
|
|
1092
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1093
|
+
auxiliary.passages[answer.source]++;
|
|
1094
|
+
else
|
|
1095
|
+
auxiliary.passages.notJudged++;
|
|
1096
|
+
}
|
|
1097
|
+
const output = finish(result, {
|
|
1098
|
+
...(result.ok
|
|
1099
|
+
? {
|
|
1100
|
+
answers: readAsks(asks, result.answers, reversed?.ok ? reversed.answers : undefined),
|
|
1101
|
+
}
|
|
1102
|
+
: !refusal
|
|
1103
|
+
? { refusal: result.error }
|
|
1104
|
+
: {}),
|
|
1105
|
+
...(refusal ? { budget: refusal, unchecked } : {}),
|
|
1106
|
+
controls: integrity.controls,
|
|
1107
|
+
state: {
|
|
1108
|
+
files: Object.keys(inserted).length,
|
|
1109
|
+
integrity: integrity.controls.length ? "failed" : "ok",
|
|
1110
|
+
warnings: asks.warnings.map((w) => w.fact),
|
|
1111
|
+
},
|
|
1112
|
+
limitationPriorityPaths: [],
|
|
1113
|
+
limitations: [
|
|
1114
|
+
...(command?.ok
|
|
1115
|
+
? [
|
|
1116
|
+
{
|
|
1117
|
+
fact: `output: ${command.output.command} → ${command.output.timed_out ? "timed out" : `exit ${command.output.exit_code ?? "unavailable"}`} (${command.originalBytes} bytes → ${command.compressedChars} chars, repetitive lines collapsed${command.lineOmittedChars ? `, ${command.lineOmittedChars} line chars omitted` : ""}${command.selectedPassages ? ", selected passages" : ""})`,
|
|
1118
|
+
next: "read command output with bash when exact text is needed",
|
|
1119
|
+
},
|
|
1120
|
+
]
|
|
1121
|
+
: []),
|
|
1122
|
+
...(command?.ok && command.redactions
|
|
1123
|
+
? [
|
|
1124
|
+
{
|
|
1125
|
+
fact: `output: ${command.redactions} occurrence${command.redactions === 1 ? "" : "s"} of the configured Jev API key replaced with [redacted]`,
|
|
1126
|
+
next: "remove the key from the command's output; it is never sent to Jev",
|
|
1127
|
+
},
|
|
1128
|
+
]
|
|
1129
|
+
: []),
|
|
1130
|
+
...(command?.ok && command.shapeLimitExceeded
|
|
1131
|
+
? [
|
|
1132
|
+
{
|
|
1133
|
+
fact: `output shape limit exceeded at ${OUTPUT_SHAPE_MAX_COUNT}; rarity grouping disabled`,
|
|
1134
|
+
next: "narrow command or reduce verbosity",
|
|
1135
|
+
},
|
|
1136
|
+
]
|
|
1137
|
+
: []),
|
|
1138
|
+
...asks.warnings,
|
|
1139
|
+
...integrity.notes,
|
|
1140
|
+
...proofLimitations,
|
|
1141
|
+
...attribution,
|
|
1142
|
+
],
|
|
1143
|
+
});
|
|
1144
|
+
return { ...output, ...hostUsage(isOmp, result.usage) };
|
|
1145
|
+
},
|
|
1146
|
+
};
|
|
1147
|
+
}
|