praxis-agent 0.67.2 → 0.68.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/README.md CHANGED
@@ -407,7 +407,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
407
407
  `npm run test:coverage` measures all production code under `src/**` with V8 and
408
408
  enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
409
409
  and rejects any production runtime module with zero covered statements (while allowing
410
- type-only modules). `npm run test:fixtures` executes the 74-behavior native contract; 66 behaviors
410
+ type-only modules). `npm run test:fixtures` executes the 75-behavior native contract; 67 behaviors
411
411
  are qualified and 8 are explicitly excluded. Schema-v2 risk tiers and executable evidence dimensions
412
412
  are enforced fail-closed. `npm run verify:fixture-contracts` performs the structural check and is part
413
413
  of `npm run check`.
@@ -1 +1 @@
1
- {"schema_version":"1.0","source_revision":"git:cdfb04df03516521ee1f366319665ef07c41fdb7","source_dirty":false,"artifact_sha256":"sha256:506df1f26b80d8092b1748de2a1130ad9b4b9fa423776d31f911d81cd5976610"}
1
+ {"schema_version":"1.0","source_revision":"git:ef170d59a802ce59a82fd5cae23c0adaff912d8a","source_dirty":false,"artifact_sha256":"sha256:60ff6f473f030f3a5cd5c2db430f866898607b9f1c205f55ed68abcc2eff1b0d"}
@@ -0,0 +1,28 @@
1
+ import { type ProjectEvalCase } from './project-eval-schema.js';
2
+ export interface HeldOutCorpusPolicy {
3
+ readonly execution: 'opt-in-only';
4
+ readonly tuning: 'forbidden';
5
+ readonly resultInformedChanges: 'require-new-version';
6
+ }
7
+ export interface HeldOutCorpusRepository {
8
+ readonly id: string;
9
+ readonly path: string;
10
+ readonly target: string;
11
+ readonly tasks: readonly string[];
12
+ readonly cases: readonly ProjectEvalCase[];
13
+ }
14
+ export interface HeldOutCorpus {
15
+ readonly root: string;
16
+ readonly schemaVersion: '1.0';
17
+ readonly id: 'praxis-held-out-v1';
18
+ readonly version: 1;
19
+ readonly split: 'held-out';
20
+ readonly repetitions: 3;
21
+ readonly policy: HeldOutCorpusPolicy;
22
+ readonly contentSha256: `sha256:${string}`;
23
+ readonly repositories: readonly HeldOutCorpusRepository[];
24
+ readonly taskCount: number;
25
+ readonly plannedRunCount: number;
26
+ }
27
+ export declare function loadHeldOutCorpus(root: string): Promise<HeldOutCorpus>;
28
+ //# sourceMappingURL=held-out-corpus.d.ts.map
@@ -0,0 +1,303 @@
1
+ import { lstat, opendir, readFile, realpath } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { Minimatch } from 'minimatch';
5
+ import { parse as parseYaml } from 'yaml';
6
+ import { discoverProjectEvalCases, } from './project-eval-schema.js';
7
+ const MAX_MANIFEST_BYTES = 1024 * 1024;
8
+ const MAX_FILES = 4096;
9
+ const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
10
+ const MAX_ENTRIES = 16_384;
11
+ const REQUIRED_TAGS = ['held-out', 'praxis-held-out-v1'];
12
+ const FORBIDDEN_TAGS = new Set([
13
+ 'tuning',
14
+ 'calibration',
15
+ 'baseline',
16
+ 'candidate',
17
+ 'admission',
18
+ ]);
19
+ const MUTATION_GLOB_OPTIONS = {
20
+ dot: true,
21
+ magicalBraces: true,
22
+ nocomment: true,
23
+ nonegate: true,
24
+ };
25
+ function object(value, label) {
26
+ if (!value || typeof value !== 'object' || Array.isArray(value))
27
+ throw new Error(`${label} must be an object`);
28
+ return value;
29
+ }
30
+ function exactKeys(value, keys, label) {
31
+ const expected = new Set(keys);
32
+ const actual = Object.keys(value);
33
+ if (actual.some((key) => !expected.has(key)) ||
34
+ actual.length !== expected.size)
35
+ throw new Error(`${label} has unexpected or missing fields`);
36
+ }
37
+ function bounded(value, label = 'manifest', depth = 0, state = { nodes: 0 }) {
38
+ state.nodes += 1;
39
+ if (state.nodes > 4096)
40
+ throw new Error(`${label} exceeds object node limit`);
41
+ if (depth > 16)
42
+ throw new Error(`${label} exceeds object depth limit`);
43
+ if (typeof value === 'string' && value.length > 16 * 1024)
44
+ throw new Error(`${label} contains oversized string`);
45
+ if (Array.isArray(value)) {
46
+ if (value.length > 256)
47
+ throw new Error(`${label} contains oversized collection`);
48
+ for (const item of value)
49
+ bounded(item, label, depth + 1, state);
50
+ }
51
+ else if (value && typeof value === 'object') {
52
+ const entries = Object.entries(value);
53
+ if (entries.length > 256)
54
+ throw new Error(`${label} contains oversized collection`);
55
+ for (const [key, item] of entries) {
56
+ if (key.length > 256)
57
+ throw new Error(`${label} contains oversized key`);
58
+ bounded(item, label, depth + 1, state);
59
+ }
60
+ }
61
+ }
62
+ function text(value, label) {
63
+ if (typeof value !== 'string' || !value.trim() || value.length > 16 * 1024)
64
+ throw new Error(`${label} must be a non-empty string`);
65
+ return value;
66
+ }
67
+ function compareStrings(left, right) {
68
+ return left < right ? -1 : left > right ? 1 : 0;
69
+ }
70
+ function safeRelativePath(value, label) {
71
+ const input = text(value, label);
72
+ const normalized = input.replaceAll('\\', '/');
73
+ if (isAbsolute(input) ||
74
+ /^[A-Za-z]:\//u.test(normalized) ||
75
+ normalized.includes('\0') ||
76
+ normalized.split('/').some((part) => !part || part === '.' || part === '..'))
77
+ throw new Error(`${label} must be a contained relative path`);
78
+ return normalized;
79
+ }
80
+ function contained(root, candidate, label) {
81
+ if (candidate !== root && !candidate.startsWith(`${root}${sep}`))
82
+ throw new Error(`${label} escapes corpus root`);
83
+ }
84
+ async function regularContainedPath(root, path, label) {
85
+ const candidate = resolve(root, path);
86
+ contained(root, candidate, label);
87
+ const parts = relative(root, candidate).split(sep);
88
+ let current = root;
89
+ for (const part of parts) {
90
+ current = join(current, part);
91
+ const component = await lstat(current).catch(() => null);
92
+ if (!component)
93
+ throw new Error(`${label} does not exist`);
94
+ if (component.isSymbolicLink())
95
+ throw new Error(`${label} contains symlink`);
96
+ }
97
+ const info = await lstat(candidate);
98
+ if (!info.isDirectory())
99
+ throw new Error(`${label} must be a directory`);
100
+ const canonical = await realpath(candidate);
101
+ contained(root, canonical, label);
102
+ return canonical;
103
+ }
104
+ async function enumerateFiles(root, repositoryRoots) {
105
+ const files = [];
106
+ let totalBytes = 0;
107
+ let visitedEntries = 0;
108
+ async function walk(directory) {
109
+ const handle = await opendir(directory);
110
+ for await (const entry of handle) {
111
+ visitedEntries += 1;
112
+ if (visitedEntries > MAX_ENTRIES)
113
+ throw new Error('Corpus exceeds directory entry limit');
114
+ if (entry.name === '.git' || entry.name === 'node_modules')
115
+ throw new Error(`Corpus contains forbidden entry: ${entry.name}`);
116
+ const path = join(directory, entry.name);
117
+ const info = await lstat(path);
118
+ if (info.isSymbolicLink())
119
+ throw new Error(`Corpus contains symlink: ${path}`);
120
+ if (info.isDirectory()) {
121
+ await walk(path);
122
+ continue;
123
+ }
124
+ if (!info.isFile())
125
+ throw new Error(`Corpus contains unsupported entry: ${path}`);
126
+ if (files.length >= MAX_FILES)
127
+ throw new Error('Corpus exceeds file limit');
128
+ totalBytes += info.size;
129
+ if (totalBytes > MAX_TOTAL_BYTES)
130
+ throw new Error('Corpus exceeds byte limit');
131
+ const content = await readFile(path);
132
+ const digest = createHash('sha256').update(content).digest('hex');
133
+ const rel = relative(root, path).split(sep).join('/');
134
+ files.push({
135
+ path: rel,
136
+ mode: info.mode & 0o777,
137
+ size: info.size,
138
+ digest,
139
+ });
140
+ }
141
+ }
142
+ for (const repositoryRoot of repositoryRoots)
143
+ await walk(repositoryRoot);
144
+ return files;
145
+ }
146
+ function contentDigest(files) {
147
+ const records = [...files]
148
+ .sort((a, b) => compareStrings(a.path, b.path))
149
+ .map((file) => `${file.path}\0${file.mode.toString(8)}\0${file.size}\0${file.digest}\n`)
150
+ .join('');
151
+ return `sha256:${createHash('sha256').update(records, 'utf8').digest('hex')}`;
152
+ }
153
+ export async function loadHeldOutCorpus(root) {
154
+ const corpusRoot = await realpath(resolve(root));
155
+ const manifestPath = join(corpusRoot, 'corpus.yaml');
156
+ const manifestInfo = await lstat(manifestPath);
157
+ if (manifestInfo.isSymbolicLink() || !manifestInfo.isFile())
158
+ throw new Error('corpus.yaml must be a regular file');
159
+ if (manifestInfo.size > MAX_MANIFEST_BYTES)
160
+ throw new Error('corpus.yaml exceeds 1 MiB');
161
+ const raw = parseYaml(await readFile(manifestPath, 'utf8'), {
162
+ maxAliasCount: 20,
163
+ });
164
+ bounded(raw);
165
+ const manifest = object(raw, 'corpus');
166
+ exactKeys(manifest, [
167
+ 'schema_version',
168
+ 'id',
169
+ 'version',
170
+ 'split',
171
+ 'repetitions',
172
+ 'policy',
173
+ 'content_sha256',
174
+ 'repositories',
175
+ ], 'corpus');
176
+ if (manifest.schema_version !== '1.0')
177
+ throw new Error('Unsupported corpus schema_version');
178
+ if (manifest.id !== 'praxis-held-out-v1')
179
+ throw new Error('Unsupported corpus id');
180
+ if (manifest.version !== 1)
181
+ throw new Error('Unsupported corpus version');
182
+ if (manifest.split !== 'held-out')
183
+ throw new Error('corpus split must be held-out');
184
+ if (manifest.repetitions !== 3)
185
+ throw new Error('corpus repetitions must be 3');
186
+ const policyRaw = object(manifest.policy, 'corpus.policy');
187
+ exactKeys(policyRaw, ['execution', 'tuning', 'result_informed_changes'], 'corpus.policy');
188
+ if (policyRaw.execution !== 'opt-in-only' ||
189
+ policyRaw.tuning !== 'forbidden' ||
190
+ policyRaw.result_informed_changes !== 'require-new-version')
191
+ throw new Error('corpus policy is invalid');
192
+ const declaredDigest = text(manifest.content_sha256, 'corpus.content_sha256');
193
+ if (!/^sha256:[0-9a-f]{64}$/u.test(declaredDigest))
194
+ throw new Error('corpus.content_sha256 is invalid');
195
+ if (!Array.isArray(manifest.repositories) || manifest.repositories.length < 3)
196
+ throw new Error('corpus must declare at least three repositories');
197
+ const repositories = [];
198
+ const ids = new Set();
199
+ const targets = [];
200
+ const globalTasks = new Set();
201
+ for (const [index, value] of manifest.repositories.entries()) {
202
+ const item = object(value, `repositories[${index}]`);
203
+ exactKeys(item, ['id', 'path', 'tasks'], `repositories[${index}]`);
204
+ const id = text(item.id, `repositories[${index}].id`);
205
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(id) || ids.has(id))
206
+ throw new Error('repository IDs must be unique lowercase dash-delimited identifiers');
207
+ ids.add(id);
208
+ const path = safeRelativePath(item.path, `repositories[${index}].path`);
209
+ const tasksRaw = item.tasks;
210
+ if (!Array.isArray(tasksRaw) ||
211
+ tasksRaw.length < 4 ||
212
+ tasksRaw.some((task) => typeof task !== 'string'))
213
+ throw new Error(`repositories[${index}].tasks must contain at least four task names`);
214
+ const tasks = tasksRaw.map((task) => text(task, `repositories[${index}].tasks`));
215
+ if (new Set(tasks).size !== tasks.length ||
216
+ [...tasks].sort(compareStrings).some((task, i) => task !== tasks[i]))
217
+ throw new Error(`repositories[${index}].tasks must be unique and sorted`);
218
+ for (const task of tasks) {
219
+ if (globalTasks.has(task))
220
+ throw new Error(`Task name is duplicated across repositories: ${task}`);
221
+ globalTasks.add(task);
222
+ }
223
+ const target = await regularContainedPath(corpusRoot, path, `repositories[${index}].path`);
224
+ targets.push(target);
225
+ repositories.push({ id, path, target, tasks, cases: [] });
226
+ }
227
+ if (globalTasks.size < 12)
228
+ throw new Error('corpus must contain at least twelve tasks');
229
+ for (let i = 0; i < targets.length; i += 1) {
230
+ for (let j = i + 1; j < targets.length; j += 1) {
231
+ const left = targets[i];
232
+ const right = targets[j];
233
+ if (left === undefined || right === undefined)
234
+ continue;
235
+ if (left === right ||
236
+ left.startsWith(`${right}${sep}`) ||
237
+ right.startsWith(`${left}${sep}`))
238
+ throw new Error('repository roots must be unique and non-nested');
239
+ }
240
+ }
241
+ // Preflight every repository before Project Eval discovery. This bounds the
242
+ // tree before the existing discovery walker inspects any case definitions.
243
+ const files = await enumerateFiles(corpusRoot, targets);
244
+ const finalRepositories = [];
245
+ for (const repository of repositories) {
246
+ const cases = await discoverProjectEvalCases(repository.target);
247
+ cases.sort((left, right) => compareStrings(left.name, right.name));
248
+ const names = cases.map((item) => item.name);
249
+ if (names.length !== repository.tasks.length ||
250
+ names.some((name, index) => name !== repository.tasks[index]))
251
+ throw new Error(`Repository ${repository.id} task declaration does not match discovery`);
252
+ for (const item of cases) {
253
+ if (item.runs !== 3)
254
+ throw new Error(`${item.name} must have three repetitions`);
255
+ if (!item.verification.some((verifier) => verifier.required))
256
+ throw new Error(`${item.name} must have a required verifier`);
257
+ if (!item.expect.allowedChangedPaths.length ||
258
+ !item.expect.expectedChangedPaths.length ||
259
+ !item.expect.forbiddenChangedPaths.length)
260
+ throw new Error(`${item.name} must declare allowed, expected, and forbidden mutations`);
261
+ if (item.execution.model !== undefined)
262
+ throw new Error(`${item.name} must not pin a model`);
263
+ const allowed = new Set(item.expect.allowedChangedPaths);
264
+ const expected = new Set(item.expect.expectedChangedPaths);
265
+ const mutationPaths = [...allowed, ...expected];
266
+ if (mutationPaths.some((path) => new Minimatch(path, MUTATION_GLOB_OPTIONS).hasMagic()))
267
+ throw new Error(`${item.name} allowed and expected mutations must use exact paths`);
268
+ if ([...expected].some((path) => !allowed.has(path)))
269
+ throw new Error(`${item.name} expected mutations must be allowed`);
270
+ const forbidden = item.expect.forbiddenChangedPaths.map((pattern) => new Minimatch(pattern, MUTATION_GLOB_OPTIONS));
271
+ if (mutationPaths.some((path) => forbidden.some((matcher) => matcher.match(path))))
272
+ throw new Error(`${item.name} mutation paths overlap`);
273
+ if (!REQUIRED_TAGS.every((tag) => item.tags.includes(tag)) ||
274
+ !item.tags.includes(repository.id))
275
+ throw new Error(`${item.name} is missing required held-out tags`);
276
+ if (item.tags.some((tag) => FORBIDDEN_TAGS.has(tag)))
277
+ throw new Error(`${item.name} contains a forbidden tuning tag`);
278
+ }
279
+ finalRepositories.push({ ...repository, cases });
280
+ }
281
+ const actualDigest = contentDigest(files);
282
+ if (actualDigest !== declaredDigest)
283
+ throw new Error(`corpus content digest mismatch: expected ${declaredDigest}, got ${actualDigest}`);
284
+ const taskCount = globalTasks.size;
285
+ return {
286
+ root: corpusRoot,
287
+ schemaVersion: '1.0',
288
+ id: 'praxis-held-out-v1',
289
+ version: 1,
290
+ split: 'held-out',
291
+ repetitions: 3,
292
+ policy: {
293
+ execution: 'opt-in-only',
294
+ tuning: 'forbidden',
295
+ resultInformedChanges: 'require-new-version',
296
+ },
297
+ contentSha256: actualDigest,
298
+ repositories: finalRepositories,
299
+ taskCount,
300
+ plannedRunCount: taskCount * 3,
301
+ };
302
+ }
303
+ //# sourceMappingURL=held-out-corpus.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.67.2",
3
+ "version": "0.68.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -62,7 +62,8 @@
62
62
  "verify:ci-coverage": "node scripts/verify-ci-coverage.mjs",
63
63
  "verify:fixture-contracts": "node scripts/verify-fixture-contracts.mjs",
64
64
  "test:fixtures": "node scripts/run-fixture-contracts.mjs",
65
- "test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts src/evals/lsp-diagnostics-admission.test.ts src/evals/glob-ripgrep-admission.test.ts"
65
+ "test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts src/evals/lsp-diagnostics-admission.test.ts src/evals/glob-ripgrep-admission.test.ts",
66
+ "test:eval:held-out-contract": "vitest run src/evals/held-out-corpus.test.ts"
66
67
  },
67
68
  "engines": {
68
69
  "node": ">=24"