specrails-core 6.0.1 → 6.1.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 +1 -1
- package/dist/agent-runtime/capabilities.js +3 -1
- package/dist/agent-runtime/cli-executor.d.ts +3 -0
- package/dist/agent-runtime/cli-executor.js +50 -13
- package/dist/agent-runtime/cli-process.js +6 -2
- package/dist/agent-runtime/cli.d.ts +15 -0
- package/dist/agent-runtime/cli.js +43 -9
- package/dist/agent-runtime/compact/chat-client.js +2 -1
- package/dist/agent-runtime/config.d.ts +4 -1
- package/dist/agent-runtime/config.js +41 -3
- package/dist/agent-runtime/core-host.d.ts +9 -3
- package/dist/agent-runtime/core-host.js +21 -18
- package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
- package/dist/agent-runtime/efficiency-summary.js +30 -27
- package/dist/agent-runtime/efficiency-types.d.ts +4 -4
- package/dist/agent-runtime/efficiency.d.ts +1 -1
- package/dist/agent-runtime/efficiency.js +7 -6
- package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
- package/dist/agent-runtime/engine/budget.d.ts +9 -0
- package/dist/agent-runtime/engine/budget.js +33 -0
- package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
- package/dist/agent-runtime/engine/canonical-json.js +162 -0
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
- package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
- package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
- package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
- package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
- package/dist/agent-runtime/engine/cli.d.ts +10 -0
- package/dist/agent-runtime/engine/cli.js +172 -0
- package/dist/agent-runtime/engine/compiler.d.ts +23 -0
- package/dist/agent-runtime/engine/compiler.js +261 -0
- package/dist/agent-runtime/engine/composition.d.ts +16 -0
- package/dist/agent-runtime/engine/composition.js +28 -0
- package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
- package/dist/agent-runtime/engine/concurrency.js +55 -0
- package/dist/agent-runtime/engine/contracts.d.ts +273 -0
- package/dist/agent-runtime/engine/contracts.js +11 -0
- package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
- package/dist/agent-runtime/engine/definition-schema.js +313 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
- package/dist/agent-runtime/engine/definition-types.js +3 -0
- package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
- package/dist/agent-runtime/engine/definition-validator.js +187 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
- package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
- package/dist/agent-runtime/engine/events.d.ts +19 -0
- package/dist/agent-runtime/engine/events.js +88 -0
- package/dist/agent-runtime/engine/execution.d.ts +29 -0
- package/dist/agent-runtime/engine/execution.js +121 -0
- package/dist/agent-runtime/engine/expressions.d.ts +11 -0
- package/dist/agent-runtime/engine/expressions.js +172 -0
- package/dist/agent-runtime/engine/fork.d.ts +28 -0
- package/dist/agent-runtime/engine/fork.js +211 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
- package/dist/agent-runtime/engine/graph-description.js +16 -0
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
- package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
- package/dist/agent-runtime/engine/index.d.ts +14 -0
- package/dist/agent-runtime/engine/index.js +14 -0
- package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
- package/dist/agent-runtime/engine/invocation-context.js +128 -0
- package/dist/agent-runtime/engine/otel.d.ts +15 -0
- package/dist/agent-runtime/engine/otel.js +132 -0
- package/dist/agent-runtime/engine/output.d.ts +4 -0
- package/dist/agent-runtime/engine/output.js +73 -0
- package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
- package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
- package/dist/agent-runtime/engine/piece-registry.js +62 -0
- package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
- package/dist/agent-runtime/engine/pieces/assign.js +35 -0
- package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/control.js +49 -0
- package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/decider.js +70 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
- package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
- package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
- package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/index.js +22 -0
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
- package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
- package/dist/agent-runtime/engine/pieces/ports.js +1 -0
- package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
- package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
- package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
- package/dist/agent-runtime/engine/pieces/shared.js +40 -0
- package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
- package/dist/agent-runtime/engine/pieces/shell.js +47 -0
- package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/verify.js +52 -0
- package/dist/agent-runtime/engine/preflight.d.ts +27 -0
- package/dist/agent-runtime/engine/preflight.js +54 -0
- package/dist/agent-runtime/engine/regex.d.ts +3 -0
- package/dist/agent-runtime/engine/regex.js +19 -0
- package/dist/agent-runtime/engine/run-status.d.ts +563 -0
- package/dist/agent-runtime/engine/run-status.js +61 -0
- package/dist/agent-runtime/engine/runs.d.ts +605 -0
- package/dist/agent-runtime/engine/runs.js +247 -0
- package/dist/agent-runtime/engine/state.d.ts +64 -0
- package/dist/agent-runtime/engine/state.js +98 -0
- package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
- package/dist/agent-runtime/engine/steering/inbox.js +113 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
- package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
- package/dist/agent-runtime/engine/storage/private-path.js +74 -0
- package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
- package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
- package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
- package/dist/agent-runtime/evaluation-corpus.js +4 -0
- package/dist/agent-runtime/evaluation.d.ts +9 -3
- package/dist/agent-runtime/evaluation.js +50 -8
- package/dist/agent-runtime/executor-types.d.ts +53 -3
- package/dist/agent-runtime/executor-types.js +35 -2
- package/dist/agent-runtime/executors.js +2 -2
- package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
- package/dist/agent-runtime/graph/artifacts.js +82 -9
- package/dist/agent-runtime/graph/nodes.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +2 -2
- package/dist/agent-runtime/graph/roles.d.ts +6 -0
- package/dist/agent-runtime/graph/roles.js +23 -17
- package/dist/agent-runtime/kimi-acp.js +6 -6
- package/dist/agent-runtime/openai-executor.js +17 -12
- package/dist/agent-runtime/openspec.d.ts +17 -2
- package/dist/agent-runtime/openspec.js +54 -20
- package/dist/agent-runtime/prompts.d.ts +4 -3
- package/dist/agent-runtime/prompts.js +26 -5
- package/dist/agent-runtime/recovery.d.ts +1 -1
- package/dist/agent-runtime/repository-context.js +1 -1
- package/dist/agent-runtime/role-routing.js +4 -2
- package/dist/agent-runtime/role-state.d.ts +5 -0
- package/dist/agent-runtime/verification-plan.d.ts +2 -0
- package/dist/agent-runtime/verification-plan.js +10 -0
- package/dist/agent-runtime/workflow-types.d.ts +2 -0
- package/dist/agent-runtime/workflow.d.ts +3 -1
- package/dist/agent-runtime/workflow.js +7 -3
- package/dist/agent-runtime/workspace-tools.d.ts +2 -2
- package/dist/agent-runtime/workspace-tools.js +6 -6
- package/dist/installer/cli.js +5 -0
- package/dist/installer/phases/scaffold.d.ts +2 -2
- package/dist/installer/phases/scaffold.js +3 -7
- package/dist/pipeline/pipeline-state.d.ts +38 -4
- package/dist/pipeline/pipeline-state.js +94 -21
- package/integration-contract.json +83 -11
- package/package.json +12 -5
- package/schemas/agent-runtime.schema.json +93 -1
- package/schemas/workflow-definition.schema.json +312 -0
- package/templates/kimi/specrails/run-skill.mjs +15 -1
- package/templates/commands/specrails/batch-implement.md +0 -32
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { contentDigest } from '../canonical-json.js';
|
|
6
|
+
import { EngineError } from '../contracts.js';
|
|
7
|
+
import { ensurePrivateDirectory, privateSqlitePath } from './private-path.js';
|
|
8
|
+
function flushDirectory(directory) {
|
|
9
|
+
if (process.platform === 'win32')
|
|
10
|
+
return;
|
|
11
|
+
const fd = openSync(directory, 'r');
|
|
12
|
+
try {
|
|
13
|
+
fsyncSync(fd);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
closeSync(fd);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** SQLite holds the construction lock across awaits and releases it on process
|
|
20
|
+
* death. The separate, flushed manifest survives that rollback. Directories are
|
|
21
|
+
* recorded by inode before their atomic rename, so recovery never removes a
|
|
22
|
+
* replacement directory or a child already published by Core.
|
|
23
|
+
*/
|
|
24
|
+
export class ForkAllocation {
|
|
25
|
+
lock;
|
|
26
|
+
directory;
|
|
27
|
+
parents;
|
|
28
|
+
manifest;
|
|
29
|
+
constructor(lock, directory, parents, requestId, digest) {
|
|
30
|
+
this.lock = lock;
|
|
31
|
+
this.directory = directory;
|
|
32
|
+
this.parents = parents;
|
|
33
|
+
this.manifest = { version: 1, requestId: requestId ?? null, digest, allocations: [] };
|
|
34
|
+
}
|
|
35
|
+
static async open(target, artifactRoot, requestId, digest) {
|
|
36
|
+
const directory = path.join(path.dirname(target), '.fork-allocations', contentDigest(target));
|
|
37
|
+
await ensurePrivateDirectory(directory);
|
|
38
|
+
const filename = path.join(directory, 'lock.sqlite');
|
|
39
|
+
try {
|
|
40
|
+
closeSync(openSync(filename, 'wx', 0o600));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code !== 'EEXIST')
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const lock = new DatabaseSync(await privateSqlitePath(filename));
|
|
47
|
+
try {
|
|
48
|
+
lock.exec('PRAGMA busy_timeout=0; BEGIN IMMEDIATE');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
lock.close();
|
|
52
|
+
throw new EngineError('lease_held', `Fork construction is already active: ${String(error)}`);
|
|
53
|
+
}
|
|
54
|
+
return new ForkAllocation(lock, directory, [path.dirname(target), path.join(artifactRoot, 'openspec', 'changes')], requestId, digest);
|
|
55
|
+
}
|
|
56
|
+
/** Call only after ruling out a published child. No request ID means no retry. */
|
|
57
|
+
recover() {
|
|
58
|
+
const filename = path.join(this.directory, 'intent.json');
|
|
59
|
+
if (!existsSync(filename))
|
|
60
|
+
return;
|
|
61
|
+
const previous = JSON.parse(readFileSync(filename, 'utf8'));
|
|
62
|
+
if (previous.version === 1 && Array.isArray(previous.allocations) && previous.allocations.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
if (!this.manifest.requestId || previous.version !== 1 || previous.requestId !== this.manifest.requestId ||
|
|
65
|
+
previous.digest !== this.manifest.digest || !Array.isArray(previous.allocations))
|
|
66
|
+
throw new EngineError('run_exists', 'Unpublished fork belongs to another request');
|
|
67
|
+
this.validate(previous);
|
|
68
|
+
this.manifest = previous;
|
|
69
|
+
this.cleanup();
|
|
70
|
+
}
|
|
71
|
+
/** Reserve an empty directory before any journal/artifact writes occur. */
|
|
72
|
+
reserve(target) {
|
|
73
|
+
if (!this.parents.includes(path.dirname(target)) || existsSync(target))
|
|
74
|
+
throw new EngineError('run_exists', 'Fork refuses to replace an existing allocation');
|
|
75
|
+
mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
76
|
+
const allocation = { target, temporary: path.join(path.dirname(target), `.${path.basename(target)}.fork-${randomUUID()}`) };
|
|
77
|
+
this.manifest.allocations.push(allocation);
|
|
78
|
+
this.save();
|
|
79
|
+
mkdirSync(allocation.temporary, { mode: 0o700 });
|
|
80
|
+
const info = lstatSync(allocation.temporary, { bigint: true });
|
|
81
|
+
allocation.inode = String(info.ino);
|
|
82
|
+
allocation.device = String(info.dev);
|
|
83
|
+
allocation.birth = String(info.birthtimeNs);
|
|
84
|
+
this.save();
|
|
85
|
+
// A single construction lock serializes every request for this child.
|
|
86
|
+
if (existsSync(target))
|
|
87
|
+
throw new EngineError('run_exists', 'Fork allocation was concurrently created');
|
|
88
|
+
renameSync(allocation.temporary, target);
|
|
89
|
+
flushDirectory(path.dirname(target));
|
|
90
|
+
}
|
|
91
|
+
cleanup() {
|
|
92
|
+
this.validate(this.manifest);
|
|
93
|
+
// Validate every path before removing any: uncertain ownership is preserved.
|
|
94
|
+
for (const allocation of this.manifest.allocations)
|
|
95
|
+
for (const target of [allocation.target, allocation.temporary]) {
|
|
96
|
+
if (!existsSync(target))
|
|
97
|
+
continue;
|
|
98
|
+
const info = lstatSync(target, { bigint: true });
|
|
99
|
+
if (!allocation.inode && target === allocation.temporary && info.isDirectory() && !info.isSymbolicLink())
|
|
100
|
+
continue;
|
|
101
|
+
if (!info.isDirectory() || info.isSymbolicLink() || String(info.ino) !== allocation.inode || String(info.dev) !== allocation.device || String(info.birthtimeNs) !== allocation.birth)
|
|
102
|
+
throw new EngineError('run_exists', 'Fork allocation ownership changed');
|
|
103
|
+
}
|
|
104
|
+
for (const allocation of [...this.manifest.allocations].reverse())
|
|
105
|
+
for (const target of [allocation.target, allocation.temporary]) {
|
|
106
|
+
if (!existsSync(target))
|
|
107
|
+
continue;
|
|
108
|
+
if (!allocation.inode)
|
|
109
|
+
rmdirSync(target); // A crash before recording its inode can leave only an empty temporary directory.
|
|
110
|
+
else
|
|
111
|
+
rmSync(target, { recursive: true });
|
|
112
|
+
flushDirectory(path.dirname(target));
|
|
113
|
+
}
|
|
114
|
+
this.manifest.allocations = [];
|
|
115
|
+
this.save();
|
|
116
|
+
}
|
|
117
|
+
close() { try {
|
|
118
|
+
this.lock.exec('ROLLBACK');
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
this.lock.close();
|
|
122
|
+
} }
|
|
123
|
+
validate(manifest) {
|
|
124
|
+
for (const entry of manifest.allocations) {
|
|
125
|
+
if (typeof entry.target !== 'string' || typeof entry.temporary !== 'string' || !this.parents.includes(path.dirname(entry.target)) ||
|
|
126
|
+
path.dirname(entry.temporary) !== path.dirname(entry.target) || !path.basename(entry.temporary).startsWith(`.${path.basename(entry.target)}.fork-`))
|
|
127
|
+
throw new EngineError('unsafe_storage_path', 'Fork allocation escapes its frozen roots');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
save() {
|
|
131
|
+
const temporary = path.join(this.directory, 'intent.tmp');
|
|
132
|
+
const fd = openSync(temporary, 'w', 0o600);
|
|
133
|
+
try {
|
|
134
|
+
writeFileSync(fd, JSON.stringify(this.manifest));
|
|
135
|
+
fsyncSync(fd);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
closeSync(fd);
|
|
139
|
+
}
|
|
140
|
+
renameSync(temporary, path.join(this.directory, 'intent.json'));
|
|
141
|
+
flushDirectory(this.directory);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EngineError } from '../contracts.js';
|
|
2
|
+
export declare class PersistenceError extends EngineError {
|
|
3
|
+
constructor(code: string, message: string);
|
|
4
|
+
}
|
|
5
|
+
/** This directory belongs to one run. Never apply this to the user's repository root. */
|
|
6
|
+
export declare function ensurePrivateDirectory(directory: string): Promise<string>;
|
|
7
|
+
/** Storage adapters use this before opening SQLite; reads never alter permissions. */
|
|
8
|
+
export declare function privateSqlitePath(filename: string, options?: {
|
|
9
|
+
create?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runCliProcess } from '../../cli-process.js';
|
|
4
|
+
import { EngineError } from '../contracts.js';
|
|
5
|
+
export class PersistenceError extends EngineError {
|
|
6
|
+
constructor(code, message) { super(code, message); this.name = 'PersistenceError'; }
|
|
7
|
+
}
|
|
8
|
+
const aclScript = String.raw `$ErrorActionPreference='Stop'; $target=$env:SPECRAILS_PRIVATE_PATH;
|
|
9
|
+
$identity=[System.Security.Principal.WindowsIdentity]::GetCurrent();
|
|
10
|
+
$isFile=[System.IO.File]::Exists($target);
|
|
11
|
+
$acl=if($isFile){New-Object System.Security.AccessControl.FileSecurity}else{New-Object System.Security.AccessControl.DirectorySecurity};
|
|
12
|
+
$acl.SetOwner($identity.User); $acl.SetAccessRuleProtection($true,$false);
|
|
13
|
+
$inherit=if($isFile){[System.Security.AccessControl.InheritanceFlags]::None}else{[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'};
|
|
14
|
+
foreach($sid in @($identity.User,(New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18')))) {
|
|
15
|
+
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl',$inherit,[System.Security.AccessControl.PropagationFlags]::None,'Allow')))
|
|
16
|
+
}; Set-Acl -LiteralPath $target -AclObject $acl;
|
|
17
|
+
$actual=Get-Acl -LiteralPath $target;
|
|
18
|
+
if($actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $identity.User.Value -or !$actual.AreAccessRulesProtected){throw 'Private directory owner/protection mismatch'};
|
|
19
|
+
foreach($entry in $actual.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])) {
|
|
20
|
+
if($entry.IdentityReference.Value -notin @($identity.User.Value,'S-1-5-18') -or $entry.AccessControlType -ne 'Allow'){throw 'Private directory grants broad access'}
|
|
21
|
+
}`;
|
|
22
|
+
async function setPrivateWindowsAcl(target, cwd) {
|
|
23
|
+
const env = { ...process.env, SPECRAILS_PRIVATE_PATH: target };
|
|
24
|
+
for (const key of Object.keys(env))
|
|
25
|
+
if (key.toLowerCase() === 'psmodulepath')
|
|
26
|
+
delete env[key];
|
|
27
|
+
const result = await runCliProcess({ command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(aclScript, 'utf16le').toString('base64')] }, { cwd, timeoutMs: 30_000, env });
|
|
28
|
+
if (result.exitCode !== 0)
|
|
29
|
+
throw new PersistenceError('private_storage_unavailable', `Cannot protect engine storage: ${result.stderr.slice(0, 2000)}`);
|
|
30
|
+
}
|
|
31
|
+
/** This directory belongs to one run. Never apply this to the user's repository root. */
|
|
32
|
+
export async function ensurePrivateDirectory(directory) {
|
|
33
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
34
|
+
if (lstatSync(directory).isSymbolicLink())
|
|
35
|
+
throw new PersistenceError('unsafe_storage_path', 'A engine storage directory cannot be a symbolic link');
|
|
36
|
+
const resolved = realpathSync(directory);
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
await setPrivateWindowsAcl(resolved, resolved);
|
|
39
|
+
}
|
|
40
|
+
else
|
|
41
|
+
chmodSync(resolved, 0o700);
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
/** Storage adapters use this before opening SQLite; reads never alter permissions. */
|
|
45
|
+
export async function privateSqlitePath(filename, options = {}) {
|
|
46
|
+
const absolute = path.resolve(filename);
|
|
47
|
+
if (!options.create && !existsSync(absolute))
|
|
48
|
+
throw new PersistenceError('run_not_found', 'Engine database does not exist');
|
|
49
|
+
if (options.create && existsSync(absolute))
|
|
50
|
+
throw new PersistenceError('run_exists', 'Engine database already exists');
|
|
51
|
+
if (options.readOnly && options.create)
|
|
52
|
+
throw new EngineError('invalid_arguments', 'Read-only mode cannot create a database');
|
|
53
|
+
const parentPath = path.dirname(absolute);
|
|
54
|
+
if (options.readOnly && lstatSync(parentPath).isSymbolicLink())
|
|
55
|
+
throw new PersistenceError('unsafe_storage_path', 'An engine storage directory cannot be a symbolic link');
|
|
56
|
+
const parent = options.readOnly ? realpathSync(parentPath) : await ensurePrivateDirectory(parentPath);
|
|
57
|
+
const target = path.join(parent, path.basename(absolute));
|
|
58
|
+
if (existsSync(target)) {
|
|
59
|
+
const info = lstatSync(target);
|
|
60
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink > 1)
|
|
61
|
+
throw new PersistenceError('unsafe_storage_path', 'Engine database must be a regular unlinked file');
|
|
62
|
+
}
|
|
63
|
+
if (options.create) {
|
|
64
|
+
const fd = openSync(target, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
if (!options.readOnly) {
|
|
68
|
+
if (process.platform !== 'win32')
|
|
69
|
+
chmodSync(target, 0o600);
|
|
70
|
+
else
|
|
71
|
+
await setPrivateWindowsAcl(target, parent);
|
|
72
|
+
}
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseStore, type Item, type Operation, type OperationResults, type SearchItem, type SearchOperation } from '@langchain/langgraph-checkpoint';
|
|
2
|
+
import { type JsonObject } from '../contracts.js';
|
|
3
|
+
export interface ProjectMemory {
|
|
4
|
+
get(namespace: string[], key: string): Promise<Item | null>;
|
|
5
|
+
search(namespace: string[], options?: Omit<SearchOperation, 'namespacePrefix'>): Promise<SearchItem[]>;
|
|
6
|
+
put(namespace: string[], key: string, value: JsonObject): Promise<void>;
|
|
7
|
+
delete(namespace: string[], key: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** One local project owns this store. It is independent of run history and is never copied by fork. */
|
|
10
|
+
export declare class SqliteProjectStore extends BaseStore {
|
|
11
|
+
readonly filename: string;
|
|
12
|
+
private readonly sqlite;
|
|
13
|
+
private readonly readOnly;
|
|
14
|
+
private closed;
|
|
15
|
+
private constructor();
|
|
16
|
+
static open(backlogRoot: string, options?: {
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
}): Promise<SqliteProjectStore>;
|
|
19
|
+
batch<Op extends Operation[]>(operations: Op): Promise<OperationResults<Op>>;
|
|
20
|
+
private operation;
|
|
21
|
+
/** Composition grants only a declared capability and namespace subtree to a piece. */
|
|
22
|
+
forAccess(access: 'none' | 'read' | 'write', options?: {
|
|
23
|
+
namespacePrefixes?: readonly (readonly string[])[];
|
|
24
|
+
}): ProjectMemory;
|
|
25
|
+
close(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { BaseStore } from '@langchain/langgraph-checkpoint';
|
|
5
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
6
|
+
import { EngineError } from '../contracts.js';
|
|
7
|
+
import { privateSqlitePath } from '../storage/private-path.js';
|
|
8
|
+
const DEFAULT_NAMESPACES = [['roles', '*', 'sessions'], ['verification', 'known-commands'], ['review', 'notes']];
|
|
9
|
+
const matchesPrefix = (namespace, prefix) => namespace.length >= prefix.length && prefix.every((part, index) => part === '*' || part === namespace[index]);
|
|
10
|
+
const fail = (message) => { throw new EngineError('invalid_arguments', message); };
|
|
11
|
+
function namespaceCheck(namespace, empty = false) {
|
|
12
|
+
if (!Array.isArray(namespace) || (!empty && !namespace.length) || namespace.length > 16 || namespace.some(part => typeof part !== 'string' || !part || part.length > 128 || part.includes('\0')))
|
|
13
|
+
fail('Store namespaces require 1–16 bounded string components');
|
|
14
|
+
}
|
|
15
|
+
function page(value, defaultValue, max) {
|
|
16
|
+
const result = value ?? defaultValue;
|
|
17
|
+
if (!Number.isSafeInteger(result) || result < 0 || result > max)
|
|
18
|
+
fail('Invalid bounded store pagination');
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
const equal = (a, b) => a !== undefined && b !== undefined && canonicalJson(a) === canonicalJson(b);
|
|
22
|
+
function filterCheck(filter) {
|
|
23
|
+
if (filter === undefined)
|
|
24
|
+
return;
|
|
25
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter))
|
|
26
|
+
fail('Store filters must be JSON objects');
|
|
27
|
+
canonicalJson(filter);
|
|
28
|
+
for (const expected of Object.values(filter)) {
|
|
29
|
+
if (expected && typeof expected === 'object' && !Array.isArray(expected) && Object.keys(expected).some(key => key.startsWith('$'))) {
|
|
30
|
+
for (const operator of Object.keys(expected))
|
|
31
|
+
if (!['$eq', '$ne', '$gt', '$gte', '$lt', '$lte'].includes(operator))
|
|
32
|
+
fail(`Unsupported store filter ${operator}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function filtered(value, filter) {
|
|
37
|
+
return Object.entries(filter ?? {}).every(([key, expected]) => {
|
|
38
|
+
const actual = Object.hasOwn(value, key) ? value[key] : undefined;
|
|
39
|
+
if (expected && typeof expected === 'object' && !Array.isArray(expected) && Object.keys(expected).some(name => name.startsWith('$'))) {
|
|
40
|
+
return Object.entries(expected).every(([operator, operand]) => {
|
|
41
|
+
if (operator === '$eq')
|
|
42
|
+
return equal(actual, operand);
|
|
43
|
+
if (operator === '$ne')
|
|
44
|
+
return !equal(actual, operand);
|
|
45
|
+
if (!['$gt', '$gte', '$lt', '$lte'].includes(operator))
|
|
46
|
+
return fail(`Unsupported store filter ${operator}`);
|
|
47
|
+
if (typeof actual !== typeof operand || !['number', 'string'].includes(typeof actual))
|
|
48
|
+
return false;
|
|
49
|
+
const left = actual, right = operand;
|
|
50
|
+
return operator === '$gt' ? left > right : operator === '$gte' ? left >= right : operator === '$lt' ? left < right : left <= right;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return equal(actual, expected);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const item = (row) => ({ namespace: JSON.parse(String(row.namespace)), key: String(row.key), value: JSON.parse(String(row.value)), createdAt: new Date(String(row.created_at)), updatedAt: new Date(String(row.updated_at)) });
|
|
57
|
+
/** One local project owns this store. It is independent of run history and is never copied by fork. */
|
|
58
|
+
export class SqliteProjectStore extends BaseStore {
|
|
59
|
+
filename;
|
|
60
|
+
sqlite;
|
|
61
|
+
readOnly;
|
|
62
|
+
closed = false;
|
|
63
|
+
constructor(filename, sqlite, readOnly) {
|
|
64
|
+
super();
|
|
65
|
+
this.filename = filename;
|
|
66
|
+
this.sqlite = sqlite;
|
|
67
|
+
this.readOnly = readOnly;
|
|
68
|
+
}
|
|
69
|
+
static async open(backlogRoot, options = {}) {
|
|
70
|
+
const filename = path.join(path.resolve(backlogRoot), '.specrails', 'engine-store.sqlite');
|
|
71
|
+
let target;
|
|
72
|
+
try {
|
|
73
|
+
target = await privateSqlitePath(filename, { create: !options.readOnly && !existsSync(filename), readOnly: options.readOnly });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Independent runs may race to create the same project-owned file.
|
|
77
|
+
if (error.code !== 'EEXIST')
|
|
78
|
+
throw error;
|
|
79
|
+
target = await privateSqlitePath(filename, { readOnly: options.readOnly });
|
|
80
|
+
}
|
|
81
|
+
const sqlite = new DatabaseSync(target, { readOnly: options.readOnly ?? false });
|
|
82
|
+
try {
|
|
83
|
+
sqlite.exec('PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON;');
|
|
84
|
+
if (!options.readOnly) {
|
|
85
|
+
sqlite.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; BEGIN IMMEDIATE;');
|
|
86
|
+
try {
|
|
87
|
+
const version = Number(sqlite.prepare('PRAGMA user_version').get().user_version);
|
|
88
|
+
if (version === 0)
|
|
89
|
+
sqlite.exec('CREATE TABLE items(namespace TEXT NOT NULL,key TEXT NOT NULL,value TEXT NOT NULL,created_at TEXT NOT NULL,updated_at TEXT NOT NULL,PRIMARY KEY(namespace,key)); PRAGMA user_version=1;');
|
|
90
|
+
else if (version !== 1)
|
|
91
|
+
throw new EngineError('store_incompatible', `Unsupported project store schema ${version}`);
|
|
92
|
+
sqlite.exec('COMMIT');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
sqlite.exec('ROLLBACK');
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (Number(sqlite.prepare('PRAGMA user_version').get().user_version) !== 1)
|
|
100
|
+
throw new EngineError('store_incompatible', 'Unsupported project store schema');
|
|
101
|
+
return new SqliteProjectStore(target, sqlite, options.readOnly ?? false);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
sqlite.close();
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async batch(operations) {
|
|
109
|
+
if (!Array.isArray(operations) || operations.length > 1000)
|
|
110
|
+
fail('A store batch is limited to 1000 operations');
|
|
111
|
+
if (this.closed)
|
|
112
|
+
throw new EngineError('store_closed', 'Project store is closed');
|
|
113
|
+
const write = operations.some(operation => 'value' in operation);
|
|
114
|
+
if (write && this.readOnly)
|
|
115
|
+
throw new EngineError('store_access_denied', 'This store connection is read-only');
|
|
116
|
+
this.sqlite.exec(write ? 'BEGIN IMMEDIATE' : 'BEGIN');
|
|
117
|
+
try {
|
|
118
|
+
const results = operations.map(operation => this.operation(operation));
|
|
119
|
+
this.sqlite.exec('COMMIT');
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
this.sqlite.exec('ROLLBACK');
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
operation(operation) {
|
|
128
|
+
if ('namespace' in operation) {
|
|
129
|
+
namespaceCheck(operation.namespace);
|
|
130
|
+
if (typeof operation.key !== 'string' || !operation.key || operation.key.length > 256 || operation.key.includes('\0'))
|
|
131
|
+
fail('Store keys require 1–256 characters');
|
|
132
|
+
const namespace = canonicalJson(operation.namespace);
|
|
133
|
+
if ('value' in operation) {
|
|
134
|
+
if (operation.index !== undefined && operation.index !== false)
|
|
135
|
+
throw new EngineError('store_index_unsupported', 'Project memory does not configure semantic indexing');
|
|
136
|
+
if (operation.value === null)
|
|
137
|
+
this.sqlite.prepare('DELETE FROM items WHERE namespace=? AND key=?').run(namespace, operation.key);
|
|
138
|
+
else {
|
|
139
|
+
if (!operation.value || typeof operation.value !== 'object' || Array.isArray(operation.value))
|
|
140
|
+
fail('Store values must be JSON objects');
|
|
141
|
+
const encoded = canonicalJson(operation.value);
|
|
142
|
+
if (Buffer.byteLength(encoded) > 2 * 1024 * 1024)
|
|
143
|
+
throw new EngineError('output_limit', 'Project store item exceeds 2 MiB');
|
|
144
|
+
const at = new Date().toISOString();
|
|
145
|
+
this.sqlite.prepare('INSERT INTO items VALUES(?,?,?,?,?) ON CONFLICT(namespace,key) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at').run(namespace, operation.key, encoded, at, at);
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const row = this.sqlite.prepare('SELECT * FROM items WHERE namespace=? AND key=?').get(namespace, operation.key);
|
|
150
|
+
return row ? item(row) : null;
|
|
151
|
+
}
|
|
152
|
+
if ('namespacePrefix' in operation) {
|
|
153
|
+
namespaceCheck(operation.namespacePrefix, true);
|
|
154
|
+
if (operation.query !== undefined)
|
|
155
|
+
throw new EngineError('store_query_unsupported', 'Project memory supports metadata filters, without semantic search');
|
|
156
|
+
const limit = page(operation.limit, 10, 1000), offset = page(operation.offset, 0, 1_000_000);
|
|
157
|
+
filterCheck(operation.filter);
|
|
158
|
+
if (!limit)
|
|
159
|
+
return [];
|
|
160
|
+
// Prefix predicates are parameterized JSON array components, never SQL identifiers.
|
|
161
|
+
const prefix = operation.namespacePrefix;
|
|
162
|
+
const where = ['json_array_length(namespace)>=?', ...prefix.map((_, i) => `json_extract(namespace,'$[${i}]')=?`)].join(' AND ');
|
|
163
|
+
const result = [];
|
|
164
|
+
let skipped = 0;
|
|
165
|
+
for (const row of this.sqlite.prepare(`SELECT * FROM items WHERE ${where} ORDER BY namespace,key`).iterate(prefix.length, ...prefix)) {
|
|
166
|
+
const found = item(row);
|
|
167
|
+
if (!filtered(found.value, operation.filter))
|
|
168
|
+
continue;
|
|
169
|
+
if (skipped++ < offset)
|
|
170
|
+
continue;
|
|
171
|
+
result.push(found);
|
|
172
|
+
if (result.length === limit)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
const limit = page(operation.limit, 100, 1000), offset = page(operation.offset, 0, 1_000_000), maxDepth = operation.maxDepth === undefined ? 16 : page(operation.maxDepth, 16, 16);
|
|
178
|
+
if (!maxDepth)
|
|
179
|
+
fail('Namespace maximum depth must be positive');
|
|
180
|
+
const conditions = operation.matchConditions ?? [];
|
|
181
|
+
for (const condition of conditions) {
|
|
182
|
+
namespaceCheck(condition.path, true);
|
|
183
|
+
if (!['prefix', 'suffix'].includes(condition.matchType))
|
|
184
|
+
fail('Invalid namespace match type');
|
|
185
|
+
}
|
|
186
|
+
if (!limit)
|
|
187
|
+
return [];
|
|
188
|
+
const seen = new Set(), result = [];
|
|
189
|
+
let skipped = 0;
|
|
190
|
+
for (const row of this.sqlite.prepare('SELECT DISTINCT namespace FROM items ORDER BY namespace').iterate()) {
|
|
191
|
+
const namespace = JSON.parse(String(row.namespace));
|
|
192
|
+
if (!conditions.every(condition => condition.matchType === 'prefix' ? matchesPrefix(namespace, condition.path) : matchesPrefix([...namespace].reverse(), [...condition.path].reverse())))
|
|
193
|
+
continue;
|
|
194
|
+
const selected = namespace.slice(0, maxDepth), encoded = canonicalJson(selected);
|
|
195
|
+
if (seen.has(encoded))
|
|
196
|
+
continue;
|
|
197
|
+
seen.add(encoded);
|
|
198
|
+
if (skipped++ < offset)
|
|
199
|
+
continue;
|
|
200
|
+
result.push(selected);
|
|
201
|
+
if (result.length === limit)
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
/** Composition grants only a declared capability and namespace subtree to a piece. */
|
|
207
|
+
forAccess(access, options = {}) {
|
|
208
|
+
const allowed = (options.namespacePrefixes ?? DEFAULT_NAMESPACES).map(prefix => [...prefix]);
|
|
209
|
+
for (const prefix of allowed)
|
|
210
|
+
namespaceCheck(prefix);
|
|
211
|
+
const authorize = (namespace, write = false) => {
|
|
212
|
+
namespaceCheck(namespace);
|
|
213
|
+
if (access === 'none' || (write && access !== 'write') || !allowed.some(prefix => matchesPrefix(namespace, prefix)))
|
|
214
|
+
throw new EngineError('store_access_denied', 'Piece has no declared access to this project memory namespace');
|
|
215
|
+
};
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
get: async (namespace, key) => { authorize(namespace); return this.get(namespace, key); },
|
|
218
|
+
search: async (namespace, options) => { authorize(namespace); return this.search(namespace, options); },
|
|
219
|
+
put: async (namespace, key, value) => { authorize(namespace, true); await this.put(namespace, key, value); },
|
|
220
|
+
delete: async (namespace, key) => { authorize(namespace, true); await this.delete(namespace, key); },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
close() { if (!this.closed) {
|
|
224
|
+
this.sqlite.close();
|
|
225
|
+
this.closed = true;
|
|
226
|
+
} }
|
|
227
|
+
}
|
|
@@ -11,3 +11,128 @@ export interface EvaluationCase {
|
|
|
11
11
|
correction?: 'verification' | 'review';
|
|
12
12
|
}
|
|
13
13
|
export declare const EVALUATION_CORPUS: EvaluationCase[];
|
|
14
|
+
/** Published reference definitions use the same independently checked behavioral corpus. */
|
|
15
|
+
export declare const DEFINITION_EVALUATION_CORPUS: readonly [{
|
|
16
|
+
schemaVersion: number;
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
journal: string;
|
|
20
|
+
change: string;
|
|
21
|
+
entry: string;
|
|
22
|
+
maxTransitions: number;
|
|
23
|
+
roles: string[];
|
|
24
|
+
nodes: {
|
|
25
|
+
implement: {
|
|
26
|
+
kind: string;
|
|
27
|
+
params: {
|
|
28
|
+
approvalBeforeArchive: boolean;
|
|
29
|
+
};
|
|
30
|
+
ends: {
|
|
31
|
+
next: string;
|
|
32
|
+
rejected: string;
|
|
33
|
+
failed: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
done: {
|
|
37
|
+
kind: string;
|
|
38
|
+
params: {
|
|
39
|
+
outcome: string;
|
|
40
|
+
requiresVerified: boolean;
|
|
41
|
+
};
|
|
42
|
+
ends: {};
|
|
43
|
+
};
|
|
44
|
+
failed: {
|
|
45
|
+
kind: string;
|
|
46
|
+
params: {
|
|
47
|
+
outcome: string;
|
|
48
|
+
};
|
|
49
|
+
ends: {};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
version: string;
|
|
53
|
+
}, {
|
|
54
|
+
schemaVersion: number;
|
|
55
|
+
id: string;
|
|
56
|
+
title: string;
|
|
57
|
+
journal: string;
|
|
58
|
+
change: string;
|
|
59
|
+
entry: string;
|
|
60
|
+
maxTransitions: number;
|
|
61
|
+
roles: string[];
|
|
62
|
+
nodes: {
|
|
63
|
+
work: {
|
|
64
|
+
kind: string;
|
|
65
|
+
params: {
|
|
66
|
+
ref: string;
|
|
67
|
+
};
|
|
68
|
+
ends: {
|
|
69
|
+
next: string;
|
|
70
|
+
failed: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
done: {
|
|
74
|
+
kind: string;
|
|
75
|
+
params: {
|
|
76
|
+
outcome: string;
|
|
77
|
+
requiresVerified: boolean;
|
|
78
|
+
};
|
|
79
|
+
ends: {};
|
|
80
|
+
};
|
|
81
|
+
failed: {
|
|
82
|
+
kind: string;
|
|
83
|
+
params: {
|
|
84
|
+
outcome: string;
|
|
85
|
+
};
|
|
86
|
+
ends: {};
|
|
87
|
+
};
|
|
88
|
+
verify: {
|
|
89
|
+
kind: string;
|
|
90
|
+
params: {
|
|
91
|
+
commands: string;
|
|
92
|
+
};
|
|
93
|
+
ends: {
|
|
94
|
+
pass: string;
|
|
95
|
+
fail: string;
|
|
96
|
+
failed: string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
components: {
|
|
101
|
+
work: {
|
|
102
|
+
entry: string;
|
|
103
|
+
nodes: {
|
|
104
|
+
implement: {
|
|
105
|
+
kind: string;
|
|
106
|
+
params: {
|
|
107
|
+
approvalBeforeArchive: boolean;
|
|
108
|
+
};
|
|
109
|
+
ends: {
|
|
110
|
+
next: string;
|
|
111
|
+
rejected: string;
|
|
112
|
+
failed: string;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
done: {
|
|
116
|
+
kind: string;
|
|
117
|
+
params: {
|
|
118
|
+
outcome: string;
|
|
119
|
+
exit: string;
|
|
120
|
+
};
|
|
121
|
+
ends: {};
|
|
122
|
+
};
|
|
123
|
+
failed: {
|
|
124
|
+
kind: string;
|
|
125
|
+
params: {
|
|
126
|
+
outcome: string;
|
|
127
|
+
exit: string;
|
|
128
|
+
};
|
|
129
|
+
ends: {};
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
delivery: {
|
|
135
|
+
requiresVerified: boolean;
|
|
136
|
+
};
|
|
137
|
+
version: string;
|
|
138
|
+
}];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import implementation from './engine/__fixtures__/implementation.json' with { type: 'json' };
|
|
2
|
+
import componentImplementation from './engine/__fixtures__/implementation-component.json' with { type: 'json' };
|
|
1
3
|
export const EVALUATION_CORPUS = [
|
|
2
4
|
{ id: 'static-tetris', verification: 'assert.deepStrictEqual(api.clearRows([[1,1]]), [[0,0]])', description: 'Clear complete rows in a static browser game without removing partial rows.', repositories: ['game'], source: 'exports.clearRows = board => board', solution: 'exports.clearRows = board => { const remaining = board.filter(row => row.some(cell => !cell)); return [...Array.from({length: board.length - remaining.length}, () => Array(board[0].length).fill(0)), ...remaining.map(row => [...row])]; }', defects: ['exports.clearRows = board => board', 'exports.clearRows = board => board.filter(row => row.every(Boolean))'], oracle: 'assert.deepStrictEqual(api.clearRows([[1,1],[1,0],[1,1]]), [[0,0],[0,0],[1,0]]); assert.deepStrictEqual(api.clearRows([[0,1],[1,0]]), [[0,1],[1,0]]);' },
|
|
3
5
|
{ id: 'local-tested-feature', verification: 'assert.strictEqual(api.subtotal([]), 0)', description: 'Calculate an integer-cent subtotal including repeated quantities and empty baskets.', repositories: ['shop'], source: 'exports.subtotal = items => 0', solution: 'exports.subtotal = items => items.reduce((sum, item) => sum + item.cents * item.quantity, 0)', defects: ['exports.subtotal = items => items.length', 'exports.subtotal = items => items.reduce((sum, item) => sum + item.cents, 0)'], oracle: 'assert.strictEqual(api.subtotal([]), 0); assert.strictEqual(api.subtotal([{cents: 125, quantity: 3}, {cents: 20, quantity: 2}]), 415);' },
|
|
@@ -5,3 +7,5 @@ export const EVALUATION_CORPUS = [
|
|
|
5
7
|
{ id: 'verification-correction', verification: 'assert.strictEqual(api.clamp(11, 0, 10), 10)', description: 'Clamp values to both boundaries, including an already valid value.', repositories: ['math'], source: 'exports.clamp = value => value', solution: 'exports.clamp = (value, min, max) => Math.min(max, Math.max(min, value))', defects: ['exports.clamp = (value, min, max) => Math.max(min, value)', 'exports.clamp = (value, min, max) => Math.min(max, value)'], oracle: 'assert.strictEqual(api.clamp(-1, 0, 10), 0); assert.strictEqual(api.clamp(11, 0, 10), 10); assert.strictEqual(api.clamp(5, 0, 10), 5);', correction: 'verification' },
|
|
6
8
|
{ id: 'review-correction', verification: 'assert.strictEqual(typeof api.unique, "function")', description: 'Deduplicate stable values while preserving the first occurrence order.', repositories: ['collections'], source: 'exports.unique = values => values', solution: 'exports.unique = values => [...new Set(values)]', defects: ['exports.unique = values => [...new Set(values)].sort()', 'exports.unique = values => values'], oracle: 'assert.deepStrictEqual(api.unique([3, 1, 3, 2, 1]), [3, 1, 2]); assert.deepStrictEqual(api.unique([]), []);', correction: 'review' },
|
|
7
9
|
];
|
|
10
|
+
/** Published reference definitions use the same independently checked behavioral corpus. */
|
|
11
|
+
export const DEFINITION_EVALUATION_CORPUS = [implementation, componentImplementation];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { RuntimeConfig } from './executor-types.js';
|
|
2
|
-
interface EvaluationOptions {
|
|
2
|
+
export interface EvaluationOptions {
|
|
3
|
+
capturePrompts?: boolean;
|
|
4
|
+
definition?: unknown;
|
|
5
|
+
caseIds?: string[];
|
|
3
6
|
output: string;
|
|
4
7
|
config?: RuntimeConfig;
|
|
5
8
|
maxCostUsd?: number;
|
|
@@ -7,7 +10,8 @@ interface EvaluationOptions {
|
|
|
7
10
|
repetitions?: number;
|
|
8
11
|
}
|
|
9
12
|
export declare function runEvaluation(options: EvaluationOptions): Promise<{
|
|
10
|
-
|
|
13
|
+
acceptedCases: number;
|
|
14
|
+
allCasesAccepted: boolean;
|
|
11
15
|
experiment: string;
|
|
12
16
|
noExtraInvocations: boolean;
|
|
13
17
|
mode: string;
|
|
@@ -40,5 +44,7 @@ export declare function runEvaluation(options: EvaluationOptions): Promise<{
|
|
|
40
44
|
stopReason: string | null;
|
|
41
45
|
reportedSpendUsd: number;
|
|
42
46
|
observations: Record<string, unknown>[];
|
|
47
|
+
definitionHash?: string | null | undefined;
|
|
48
|
+
schemaVersion: number;
|
|
49
|
+
isDefinitionEvaluation: boolean;
|
|
43
50
|
}>;
|
|
44
|
-
export {};
|