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,198 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, matchesGlob, relative, resolve } from "node:path";
|
|
3
|
+
import { CONCURRENCY, MAX_FILES, TIMEOUT_MS } from "../constants.js";
|
|
4
|
+
import { checkFileAdmission, collectFiles, resolveInsideRepo, } from "./files.js";
|
|
5
|
+
const excludedDirectories = new Set(["node_modules", ".git", "dist", "build"]);
|
|
6
|
+
const lockfile = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|Cargo\.lock|Gemfile\.lock|poetry\.lock|uv\.lock|composer\.lock|Pipfile\.lock|go\.sum)$/;
|
|
7
|
+
export async function collectAskFiles(cwd, paths, signal, exec) {
|
|
8
|
+
if (!paths.length)
|
|
9
|
+
return {
|
|
10
|
+
ok: false,
|
|
11
|
+
error: "Provide at least one path, directory or glob.",
|
|
12
|
+
};
|
|
13
|
+
for (const path of paths) {
|
|
14
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path))
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
error: `Internal URLs are not files; use read for ${path}.`,
|
|
18
|
+
};
|
|
19
|
+
if (isAbsolute(path) || path.split(/[\\/]/).includes(".."))
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: `Path must remain inside the repository: ${path}.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const skipped = new Set();
|
|
26
|
+
const secret = new Set();
|
|
27
|
+
const candidates = new Set();
|
|
28
|
+
try {
|
|
29
|
+
let inventory;
|
|
30
|
+
if (exec) {
|
|
31
|
+
const result = await exec("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { cwd, timeout: TIMEOUT_MS, signal });
|
|
32
|
+
if (result.code === 0)
|
|
33
|
+
inventory = new Set(result.stdout.split("\0").filter(Boolean));
|
|
34
|
+
}
|
|
35
|
+
const add = async (path) => {
|
|
36
|
+
const admission = await checkFileAdmission(cwd, path, exec, signal, inventory);
|
|
37
|
+
if (!admission.ok) {
|
|
38
|
+
const entry = `${path} (${admission.error})`;
|
|
39
|
+
skipped.add(entry);
|
|
40
|
+
if (admission.cause === "secret_pattern")
|
|
41
|
+
secret.add(entry);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (path.split("/").some((part) => excludedDirectories.has(part)))
|
|
45
|
+
skipped.add(`${path} (build output or dependencies)`);
|
|
46
|
+
else if (lockfile.test(path))
|
|
47
|
+
skipped.add(`${path} (lockfile)`);
|
|
48
|
+
else
|
|
49
|
+
candidates.add(path);
|
|
50
|
+
};
|
|
51
|
+
const walk = async (directory, pattern) => {
|
|
52
|
+
signal?.throwIfAborted();
|
|
53
|
+
const safe = await resolveInsideRepo(cwd, directory);
|
|
54
|
+
if (!safe.ok) {
|
|
55
|
+
skipped.add(`${directory} (${safe.error})`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const entries = await readdir(safe.abs, { withFileTypes: true });
|
|
59
|
+
await Promise.all(entries.map(async (entry) => {
|
|
60
|
+
const path = relative(cwd, resolve(cwd, directory, entry.name)).replaceAll("\\", "/");
|
|
61
|
+
if (entry.isSymbolicLink()) {
|
|
62
|
+
skipped.add(`${path} (symbolic link)`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
if (excludedDirectories.has(entry.name)) {
|
|
67
|
+
skipped.add(`${path}/ (build output or dependencies)`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await walk(path, pattern);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (entry.isFile() &&
|
|
74
|
+
(!pattern ||
|
|
75
|
+
matchesGlob(path
|
|
76
|
+
.split("/")
|
|
77
|
+
.map((part) => part.startsWith(".") ? `__dot__${part.slice(1)}` : part)
|
|
78
|
+
.join("/"), pattern
|
|
79
|
+
.split("/")
|
|
80
|
+
.map((part) => part.startsWith(".") ? `__dot__${part.slice(1)}` : part)
|
|
81
|
+
.join("/"))))
|
|
82
|
+
await add(path);
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
for (const input of paths) {
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
const safe = await resolveInsideRepo(cwd, input);
|
|
88
|
+
const info = safe.ok
|
|
89
|
+
? await stat(safe.abs).catch(() => undefined)
|
|
90
|
+
: undefined;
|
|
91
|
+
if (info && safe.ok) {
|
|
92
|
+
if (info.isFile()) {
|
|
93
|
+
const admission = await checkFileAdmission(cwd, safe.rel, exec, signal, inventory);
|
|
94
|
+
if (!admission.ok)
|
|
95
|
+
return admission;
|
|
96
|
+
}
|
|
97
|
+
if (input.split("/").some((part) => excludedDirectories.has(part))) {
|
|
98
|
+
skipped.add(`${input} (build output or dependencies)`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (info.isDirectory())
|
|
102
|
+
await walk(safe.rel || ".");
|
|
103
|
+
else if (info.isFile()) {
|
|
104
|
+
const admission = await checkFileAdmission(cwd, safe.rel, exec, signal, inventory);
|
|
105
|
+
if (!admission.ok)
|
|
106
|
+
return admission;
|
|
107
|
+
await add(safe.rel);
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
skipped.add(`${input} (not a regular file)`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const parts = input.split("/");
|
|
114
|
+
const wildcard = parts.findIndex((part) => /[?*[\]{}]/.test(part));
|
|
115
|
+
if (wildcard < 0) {
|
|
116
|
+
if (!safe.ok && !safe.error.includes("ENOENT"))
|
|
117
|
+
return safe;
|
|
118
|
+
skipped.add(`${input} (missing)`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const prefix = parts.slice(0, wildcard).join("/") || ".";
|
|
122
|
+
const prefixLocation = await resolveInsideRepo(cwd, prefix);
|
|
123
|
+
if (!prefixLocation.ok)
|
|
124
|
+
return prefixLocation;
|
|
125
|
+
const pattern = input.replace(/^\.\//, "");
|
|
126
|
+
if (inventory) {
|
|
127
|
+
for (const path of inventory) {
|
|
128
|
+
const dotPath = path
|
|
129
|
+
.split("/")
|
|
130
|
+
.map((part) => part.startsWith(".") ? `__dot__${part.slice(1)}` : part)
|
|
131
|
+
.join("/");
|
|
132
|
+
const dotPattern = pattern
|
|
133
|
+
.split("/")
|
|
134
|
+
.map((part) => part.startsWith(".") ? `__dot__${part.slice(1)}` : part)
|
|
135
|
+
.join("/");
|
|
136
|
+
if (matchesGlob(dotPath, dotPattern))
|
|
137
|
+
await add(path);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else
|
|
141
|
+
await walk(prefix, pattern);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const files = [];
|
|
145
|
+
const sorted = [...candidates].sort();
|
|
146
|
+
const contributions = new Map();
|
|
147
|
+
for (const path of sorted) {
|
|
148
|
+
const directory = dirname(path);
|
|
149
|
+
contributions.set(directory, (contributions.get(directory) ?? 0) + 1);
|
|
150
|
+
}
|
|
151
|
+
let accepted = 0;
|
|
152
|
+
for (let i = 0; i < sorted.length; i += CONCURRENCY) {
|
|
153
|
+
const rows = await Promise.all(sorted.slice(i, i + CONCURRENCY).map(async (path) => {
|
|
154
|
+
const result = await collectFiles(cwd, [path], signal, {
|
|
155
|
+
exec,
|
|
156
|
+
inventory,
|
|
157
|
+
});
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
skipped.add(`${path} (${result.error})`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const identity = result.identities[0];
|
|
163
|
+
if (!identity)
|
|
164
|
+
return;
|
|
165
|
+
if (identity.content.includes("\0") ||
|
|
166
|
+
identity.readSha256 !== identity.insertedSha256) {
|
|
167
|
+
skipped.add(`${path} (binary or invalid UTF-8)`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
return { path, content: identity.content, identity };
|
|
171
|
+
}));
|
|
172
|
+
for (const file of rows)
|
|
173
|
+
if (file) {
|
|
174
|
+
accepted++;
|
|
175
|
+
if (files.length < MAX_FILES)
|
|
176
|
+
files.push(file);
|
|
177
|
+
}
|
|
178
|
+
if (accepted > MAX_FILES) {
|
|
179
|
+
const largest = [...contributions]
|
|
180
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
181
|
+
.slice(0, 5);
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: `At least ${accepted} files exceeds MAX_FILES=${MAX_FILES}; narrow paths. Largest expanded directories: ${largest.map(([path, count]) => `${path}: ${count}`).join(", ")}.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
files,
|
|
191
|
+
skipped: [...skipped].sort(),
|
|
192
|
+
secret: [...secret].sort(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return { ok: false, error: `Cannot expand paths: ${String(error)}` };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { STATE_MAX_CHARS, TIMEOUT_MS } from "../constants.js";
|
|
3
|
+
import { checkFileAdmission, collectFiles } from "./files.js";
|
|
4
|
+
import { verifyGitUtf8 } from "./utf8.js";
|
|
5
|
+
export async function collectAskRepository(exec, cwd, base, signal) {
|
|
6
|
+
try {
|
|
7
|
+
const options = { cwd, timeout: TIMEOUT_MS, signal };
|
|
8
|
+
const location = await exec("git", ["rev-parse", "--show-toplevel"], options);
|
|
9
|
+
if (location.code !== 0 || location.killed)
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
error: location.stderr || "Cannot resolve repository root.",
|
|
13
|
+
};
|
|
14
|
+
const root = location.stdout.trimEnd();
|
|
15
|
+
const gitOptions = { ...options, cwd: root };
|
|
16
|
+
const [listed, ignored, revision] = await Promise.all([
|
|
17
|
+
exec("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], gitOptions),
|
|
18
|
+
exec("git", ["ls-files", "--cached", "--ignored", "--exclude-standard", "-z"], gitOptions),
|
|
19
|
+
base === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: exec("git", ["rev-parse", "--verify", "--end-of-options", `${base}^{commit}`], gitOptions),
|
|
22
|
+
]);
|
|
23
|
+
if (listed.code !== 0 || listed.killed)
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: listed.stderr || "Cannot list repository paths.",
|
|
27
|
+
};
|
|
28
|
+
if (revision && (revision.code !== 0 || revision.killed))
|
|
29
|
+
return { ok: false, error: revision.stderr || "Cannot resolve base." };
|
|
30
|
+
if (ignored.code !== 0 || ignored.killed)
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
error: ignored.stderr || "Cannot list ignored repository paths.",
|
|
34
|
+
};
|
|
35
|
+
const known = new Set(listed.stdout.split("\0").filter(Boolean));
|
|
36
|
+
for (const path of ignored.stdout.split("\0"))
|
|
37
|
+
known.delete(path);
|
|
38
|
+
const baseSha = revision?.stdout.trim();
|
|
39
|
+
const basePaths = baseSha
|
|
40
|
+
? await exec("git", ["ls-tree", "-r", "-z", baseSha], gitOptions)
|
|
41
|
+
: undefined;
|
|
42
|
+
if (basePaths && (basePaths.code !== 0 || basePaths.killed))
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: basePaths.stderr || "Cannot list base paths.",
|
|
46
|
+
};
|
|
47
|
+
const beforeEntries = new Map((basePaths?.stdout.split("\0").filter(Boolean) ?? []).map((entry) => [
|
|
48
|
+
entry.slice(entry.indexOf("\t") + 1),
|
|
49
|
+
entry,
|
|
50
|
+
]));
|
|
51
|
+
const beforeKnown = new Set(beforeEntries.keys());
|
|
52
|
+
const reads = new Map();
|
|
53
|
+
const beforeReads = new Map();
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
root,
|
|
57
|
+
known,
|
|
58
|
+
beforeKnown,
|
|
59
|
+
baseSha,
|
|
60
|
+
read(path) {
|
|
61
|
+
let pending = reads.get(path);
|
|
62
|
+
if (!pending) {
|
|
63
|
+
pending = (async () => {
|
|
64
|
+
if (!known.has(path))
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: `${path}: not in repository inventory`,
|
|
68
|
+
};
|
|
69
|
+
const loaded = await collectFiles(root, [path], signal, {
|
|
70
|
+
allowEmpty: true,
|
|
71
|
+
exec,
|
|
72
|
+
inventory: known,
|
|
73
|
+
});
|
|
74
|
+
return loaded.ok
|
|
75
|
+
? { ok: true, path, text: loaded.files[path] ?? "" }
|
|
76
|
+
: loaded;
|
|
77
|
+
})();
|
|
78
|
+
reads.set(path, pending);
|
|
79
|
+
}
|
|
80
|
+
return pending;
|
|
81
|
+
},
|
|
82
|
+
readBefore(path) {
|
|
83
|
+
let pending = beforeReads.get(path);
|
|
84
|
+
if (!pending) {
|
|
85
|
+
pending = (async () => {
|
|
86
|
+
try {
|
|
87
|
+
if (isAbsolute(path) || path.split(/[\\/]/).includes(".."))
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
cause: "forbidden_path",
|
|
91
|
+
error: `Path must remain inside the repository: ${path}.`,
|
|
92
|
+
};
|
|
93
|
+
const admission = await checkFileAdmission(root, path);
|
|
94
|
+
if (!admission.ok)
|
|
95
|
+
return admission;
|
|
96
|
+
const ignoredPath = await exec("git", ["check-ignore", "--no-index", "--", path], gitOptions);
|
|
97
|
+
if (ignoredPath.killed || ![0, 1].includes(ignoredPath.code))
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
cause: "git_failure",
|
|
101
|
+
error: `${path}: historical admission unavailable`,
|
|
102
|
+
};
|
|
103
|
+
if (ignoredPath.code === 0)
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
cause: "ignored_path",
|
|
107
|
+
error: `${path}: gitignored: not sent to Jev`,
|
|
108
|
+
};
|
|
109
|
+
if (!baseSha)
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: `${path}: comparison base unavailable`,
|
|
113
|
+
};
|
|
114
|
+
if (!beforeKnown.has(path))
|
|
115
|
+
return known.has(path)
|
|
116
|
+
? { ok: true, text: null }
|
|
117
|
+
: {
|
|
118
|
+
ok: false,
|
|
119
|
+
error: `${path}: not in current or comparison base inventory`,
|
|
120
|
+
};
|
|
121
|
+
const entry = beforeEntries.get(path) ?? "";
|
|
122
|
+
if (!/^(?:100644|100755) blob /.test(entry))
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
cause: entry.startsWith("120000 ")
|
|
126
|
+
? "symlink"
|
|
127
|
+
: "file_unavailable",
|
|
128
|
+
error: `${path}: not a regular file at comparison base: not sent to Jev`,
|
|
129
|
+
};
|
|
130
|
+
const object = await exec("git", [
|
|
131
|
+
"rev-parse",
|
|
132
|
+
"--verify",
|
|
133
|
+
"--end-of-options",
|
|
134
|
+
`${baseSha}:${path}`,
|
|
135
|
+
], gitOptions);
|
|
136
|
+
if (object.code !== 0 || object.killed)
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: `before evidence unavailable for ${path}`,
|
|
140
|
+
};
|
|
141
|
+
const id = object.stdout.trim();
|
|
142
|
+
const sizeResult = await exec("git", ["cat-file", "-s", id], gitOptions);
|
|
143
|
+
const size = Number(sizeResult.stdout);
|
|
144
|
+
if (sizeResult.code !== 0 ||
|
|
145
|
+
sizeResult.killed ||
|
|
146
|
+
!Number.isFinite(size) ||
|
|
147
|
+
size > STATE_MAX_CHARS * 4)
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: `before evidence unavailable for ${path}`,
|
|
151
|
+
};
|
|
152
|
+
const loaded = await exec("git", [
|
|
153
|
+
"show",
|
|
154
|
+
"--no-ext-diff",
|
|
155
|
+
"--no-textconv",
|
|
156
|
+
"--end-of-options",
|
|
157
|
+
id,
|
|
158
|
+
], gitOptions);
|
|
159
|
+
if (loaded.code !== 0 ||
|
|
160
|
+
loaded.killed ||
|
|
161
|
+
loaded.stdout.length > STATE_MAX_CHARS)
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: `before evidence unavailable for ${path}`,
|
|
165
|
+
};
|
|
166
|
+
if (loaded.stdout.includes("\0"))
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
cause: "binary_or_non_utf8",
|
|
170
|
+
error: `${path}: binary content: not sent to Jev`,
|
|
171
|
+
};
|
|
172
|
+
const decoded = verifyGitUtf8(loaded.stdout, id, size);
|
|
173
|
+
return decoded.ok
|
|
174
|
+
? decoded
|
|
175
|
+
: {
|
|
176
|
+
ok: false,
|
|
177
|
+
cause: "binary_or_non_utf8",
|
|
178
|
+
error: `${path}: ${decoded.error}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return { ok: false, error: String(error) };
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
beforeReads.set(path, pending);
|
|
186
|
+
}
|
|
187
|
+
return pending;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
error: `Cannot collect repository proof: ${String(error)}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export function repositoryPath(root, cwd, path) {
|
|
199
|
+
return relative(root, resolve(cwd, path)).split("\\").join("/");
|
|
200
|
+
}
|