@ethogram/cli 0.1.0-alpha.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 ADDED
@@ -0,0 +1,17 @@
1
+ # Ethogram CLI
2
+
3
+ Local Ethogram initialization and read-only developer runtime for TypeScript/Node projects.
4
+
5
+ This package is prepared as `@ethogram/cli@0.1.0-alpha.0` but has not been published yet.
6
+
7
+ ```bash
8
+ npx ethogram init
9
+ npx ethogram init --existing
10
+ npx ethogram dev
11
+ ```
12
+
13
+ Use `ethogram init --existing` to create only `ethogram.config.mjs` in a project that already owns an agent implementation. Normal `ethogram init` adds the deterministic Access Request starter. Both modes preserve conflicting user-owned files and abort without partial writes.
14
+
15
+ `ethogram dev` accepts `--project <path>`, `--port <number>`, and `--no-open`. It serves a localhost-only, code-first UI: project files are the source of truth and the UI does not save edits. Relevant source changes reload automatically, and previous execution evidence is cleared or marked stale until the Story is rerun.
16
+
17
+ Node.js 20.9 or newer is required. Ethogram does not persist run history.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import { access, mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { constants } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { startDeveloperServer } from './server.js';
7
+ import { existingProjectFiles, starterFiles } from './templates.js';
8
+ import { TypeScriptAdapterError } from './typescript-adapter.js';
9
+ const help = `Ethogram CLI (alpha)
10
+
11
+ Usage:
12
+ ethogram init [--existing]
13
+ ethogram dev [--project <path>] [--port <number>] [--no-open]
14
+ ethogram --help
15
+ ethogram --version
16
+
17
+ Commands:
18
+ init Create a starter Agent, Story, and local execution profile in the current project.
19
+ Use --existing to create configuration only for an existing agent project.
20
+ dev Start the local, read-only Ethogram developer UI. The current directory is the default project.
21
+ Agent, Story, and execution-profile files remain the source of truth and reload automatically.
22
+
23
+ Examples:
24
+ npx ethogram init
25
+ npx ethogram init --existing
26
+ npx ethogram dev
27
+ npx ethogram dev --project ./my-agent-project --port 4317 --no-open
28
+ `;
29
+ async function version() {
30
+ const packagePath = fileURLToPath(new URL('../package.json', import.meta.url));
31
+ const packageJson = JSON.parse(await readFile(packagePath, 'utf8'));
32
+ return packageJson.version;
33
+ }
34
+ async function projectPackage(root) {
35
+ try {
36
+ const packagePath = path.join(root, 'package.json');
37
+ const value = JSON.parse(await readFile(packagePath, 'utf8'));
38
+ if (typeof value.name !== 'string' || !value.name.trim())
39
+ throw new Error('missing-name');
40
+ return { name: value.name };
41
+ }
42
+ catch {
43
+ throw new Error(`INIT_PROJECT_INVALID: ${root} must contain a package.json with a name.`);
44
+ }
45
+ }
46
+ async function initialize(options) {
47
+ const root = await realpath(process.cwd());
48
+ if (!(await stat(root)).isDirectory())
49
+ throw new Error(`INIT_PROJECT_INVALID: ${root} is not a directory.`);
50
+ const packageJson = await projectPackage(root);
51
+ const files = options.existing ? existingProjectFiles(packageJson.name) : starterFiles(packageJson.name);
52
+ const matching = [];
53
+ const missing = [];
54
+ const conflicts = [];
55
+ await access(root, constants.R_OK | constants.W_OK);
56
+ for (const file of files) {
57
+ const target = path.join(root, file.relativePath);
58
+ try {
59
+ const existing = await readFile(target, 'utf8');
60
+ if (existing === file.content)
61
+ matching.push(file.relativePath);
62
+ else
63
+ conflicts.push(file.relativePath);
64
+ }
65
+ catch (error) {
66
+ const code = error.code;
67
+ if (code === 'ENOENT')
68
+ missing.push(file.relativePath);
69
+ else
70
+ throw new Error(`INIT_PREFLIGHT_FAILED: Could not inspect ${file.relativePath}.`);
71
+ }
72
+ }
73
+ if (conflicts.length > 0) {
74
+ throw new Error(`INIT_CONFLICT: Existing files were preserved; nothing was written. Conflicts: ${conflicts.join(', ')}`);
75
+ }
76
+ for (const relativePath of missing) {
77
+ const file = files.find((candidate) => candidate.relativePath === relativePath);
78
+ if (!file)
79
+ continue;
80
+ const target = path.join(root, relativePath);
81
+ await mkdir(path.dirname(target), { recursive: true });
82
+ await writeFile(target, file.content, { encoding: 'utf8', flag: 'wx' });
83
+ }
84
+ if (missing.length === 0) {
85
+ process.stdout.write(`Ethogram is already initialized in ${root}. No files were changed.\n`);
86
+ }
87
+ else {
88
+ process.stdout.write(`Ethogram initialized in ${root}.\n`);
89
+ for (const relativePath of missing)
90
+ process.stdout.write(`Created ${relativePath}\n`);
91
+ for (const relativePath of matching)
92
+ process.stdout.write(`Preserved ${relativePath}\n`);
93
+ }
94
+ if (options.existing) {
95
+ process.stdout.write('Add an Agent descriptor, behavioral Story, and thin execution profile for your existing agent.\n');
96
+ }
97
+ process.stdout.write('Next: npx ethogram dev\n');
98
+ }
99
+ function parseDevArguments(args) {
100
+ let projectRoot = process.cwd();
101
+ let port = 4317;
102
+ let openBrowser = true;
103
+ for (let index = 0; index < args.length; index += 1) {
104
+ const argument = args[index];
105
+ if (argument === '--no-open') {
106
+ openBrowser = false;
107
+ continue;
108
+ }
109
+ if (argument === '--project') {
110
+ const value = args[index + 1];
111
+ if (!value)
112
+ throw new Error('CLI_USAGE: --project requires a path.');
113
+ projectRoot = path.resolve(value);
114
+ index += 1;
115
+ continue;
116
+ }
117
+ if (argument === '--port') {
118
+ const value = Number(args[index + 1]);
119
+ if (!Number.isInteger(value) || value < 0 || value > 65535) {
120
+ throw new Error('CLI_USAGE: --port requires an integer from 0 to 65535.');
121
+ }
122
+ port = value;
123
+ index += 1;
124
+ continue;
125
+ }
126
+ throw new Error(`CLI_USAGE: Unknown dev option ${argument}.`);
127
+ }
128
+ return { projectRoot, port, openBrowser };
129
+ }
130
+ async function main() {
131
+ const args = process.argv.slice(2);
132
+ const command = args[0];
133
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
134
+ process.stdout.write(help);
135
+ return;
136
+ }
137
+ if (command === '--version' || command === '-v') {
138
+ process.stdout.write(`${await version()}\n`);
139
+ return;
140
+ }
141
+ if (command === 'init') {
142
+ const initArgs = args.slice(1);
143
+ if (initArgs.some((argument) => argument !== '--existing') || initArgs.filter((argument) => argument === '--existing').length > 1) {
144
+ throw new Error('CLI_USAGE: ethogram init accepts only the optional --existing flag.');
145
+ }
146
+ await initialize({ existing: initArgs.includes('--existing') });
147
+ return;
148
+ }
149
+ if (command === 'dev') {
150
+ await startDeveloperServer(parseDevArguments(args.slice(1)));
151
+ return;
152
+ }
153
+ throw new Error(`CLI_USAGE: Unknown command ${command}. Run ethogram --help.`);
154
+ }
155
+ main().catch((error) => {
156
+ if (error instanceof TypeScriptAdapterError) {
157
+ process.stderr.write(`Ethogram ${error.code}: ${error.message}\n`);
158
+ }
159
+ else if (error instanceof Error) {
160
+ process.stderr.write(`Ethogram error: ${error.message}\n`);
161
+ }
162
+ else {
163
+ process.stderr.write('Ethogram error: The command could not be completed.\n');
164
+ }
165
+ process.exitCode = 1;
166
+ });
@@ -0,0 +1,114 @@
1
+ export type AgentDescriptor = {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ icon: string;
6
+ };
7
+ export type ExpectationMatcher = {
8
+ kind: 'tool-called';
9
+ tool: string;
10
+ } | {
11
+ kind: 'tool-not-called';
12
+ tool: string;
13
+ };
14
+ export type StoryExpectationDescriptor = {
15
+ id: string;
16
+ description: string;
17
+ failureDescription?: string;
18
+ matcher: ExpectationMatcher;
19
+ };
20
+ export type StoryGivenValue = string | number | boolean | null | readonly StoryGivenValue[] | {
21
+ readonly [key: string]: StoryGivenValue;
22
+ };
23
+ export type StoryGiven = string[] | Readonly<Record<string, StoryGivenValue>>;
24
+ export type StoryDescriptor = {
25
+ id: string;
26
+ name: string;
27
+ agent: AgentDescriptor;
28
+ description: string;
29
+ given: StoryGiven;
30
+ prompt: string;
31
+ expectations: StoryExpectationDescriptor[];
32
+ source: string;
33
+ executable: boolean;
34
+ };
35
+ export type ProjectDescriptor = {
36
+ projectRoot: string;
37
+ name: string;
38
+ adapter: {
39
+ id: string;
40
+ label: string;
41
+ };
42
+ agents: AgentDescriptor[];
43
+ stories: StoryDescriptor[];
44
+ };
45
+ export type ExecutionRequest = {
46
+ story: StoryDescriptor;
47
+ };
48
+ type ObservedToolCallBase = {
49
+ callId: string;
50
+ name: string;
51
+ input: string;
52
+ duration?: string;
53
+ startedAt?: string;
54
+ endedAt?: string;
55
+ };
56
+ export type ObservedToolCall = (ObservedToolCallBase & {
57
+ status: 'success';
58
+ output?: string;
59
+ error?: never;
60
+ }) | (ObservedToolCallBase & {
61
+ status: 'error';
62
+ output?: never;
63
+ error?: {
64
+ name?: string;
65
+ message: string;
66
+ };
67
+ });
68
+ export type ObservedTimelineStep = {
69
+ label: string;
70
+ detail: string;
71
+ duration?: string;
72
+ };
73
+ export type ObservedTokenUsage = {
74
+ availability: 'unavailable';
75
+ } | {
76
+ availability: 'available';
77
+ inputTokens?: number;
78
+ outputTokens?: number;
79
+ totalTokens?: number;
80
+ reasoningTokens?: number;
81
+ };
82
+ export type ObservedRun = {
83
+ decision: string;
84
+ reason: string;
85
+ finalResponse: string;
86
+ toolCalls: ObservedToolCall[];
87
+ timeline: ObservedTimelineStep[];
88
+ evidence: {
89
+ provider?: string;
90
+ model?: string;
91
+ startedAt?: string;
92
+ endedAt?: string;
93
+ latencyMs?: number;
94
+ finishReason?: string;
95
+ tokenUsage: ObservedTokenUsage;
96
+ };
97
+ };
98
+ export type BehavioralVerdict = 'PASS' | 'FAIL';
99
+ export type EvaluationResult = {
100
+ verdict: BehavioralVerdict;
101
+ expectations: Readonly<Record<string, BehavioralVerdict>>;
102
+ };
103
+ export type CompletedExecutionRecord = {
104
+ observedRun: ObservedRun;
105
+ evaluationResult: EvaluationResult;
106
+ };
107
+ export interface Runner {
108
+ run(request: ExecutionRequest): Promise<ObservedRun>;
109
+ }
110
+ export interface LanguageAdapter extends Runner {
111
+ readonly id: string;
112
+ loadProject(projectRoot: string): Promise<ProjectDescriptor>;
113
+ }
114
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { EvaluationResult, ObservedRun, StoryDescriptor } from './contracts.js';
2
+ export declare function evaluateStory(story: StoryDescriptor, observedRun: ObservedRun): EvaluationResult;
@@ -0,0 +1,15 @@
1
+ function evaluateMatcher(matcher, observedRun) {
2
+ const called = observedRun.toolCalls.some((toolCall) => toolCall.name === matcher.tool);
3
+ return matcher.kind === 'tool-called' ? called : !called;
4
+ }
5
+ export function evaluateStory(story, observedRun) {
6
+ const expectationEntries = story.expectations.map((expectation) => {
7
+ const verdict = evaluateMatcher(expectation.matcher, observedRun) ? 'PASS' : 'FAIL';
8
+ return [expectation.id, verdict];
9
+ });
10
+ const expectations = Object.freeze(Object.fromEntries(expectationEntries));
11
+ const verdict = Object.values(expectations).every((value) => value === 'PASS')
12
+ ? 'PASS'
13
+ : 'FAIL';
14
+ return Object.freeze({ verdict, expectations });
15
+ }
@@ -0,0 +1,7 @@
1
+ import type { ObservedRun } from './contracts.js';
2
+ export type NormalizedExternalEvidence = Pick<ObservedRun, 'toolCalls' | 'timeline' | 'evidence'>;
3
+ export declare class ExternalEvidenceValidationError extends Error {
4
+ readonly code = "INVALID_EXTERNAL_EXECUTION_EVIDENCE";
5
+ constructor(message: string);
6
+ }
7
+ export declare function normalizeExternalExecutionEvidence(value: unknown): NormalizedExternalEvidence;
@@ -0,0 +1,205 @@
1
+ export class ExternalEvidenceValidationError extends Error {
2
+ code = 'INVALID_EXTERNAL_EXECUTION_EVIDENCE';
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'ExternalEvidenceValidationError';
6
+ }
7
+ }
8
+ function isRecord(value) {
9
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
10
+ }
11
+ function fail(message) {
12
+ throw new ExternalEvidenceValidationError(message);
13
+ }
14
+ function requiredText(value, location) {
15
+ if (typeof value !== 'string' || !value.trim())
16
+ fail(`${location} must be a non-empty string.`);
17
+ return value;
18
+ }
19
+ function optionalText(record, key, location) {
20
+ if (!Object.prototype.hasOwnProperty.call(record, key))
21
+ return undefined;
22
+ return requiredText(record[key], `${location}.${key}`);
23
+ }
24
+ function optionalNonNegativeNumber(record, key, location) {
25
+ if (!Object.prototype.hasOwnProperty.call(record, key))
26
+ return undefined;
27
+ const value = record[key];
28
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
29
+ fail(`${location}.${key} must be a finite non-negative number.`);
30
+ }
31
+ return value;
32
+ }
33
+ function optionalNonNegativeInteger(record, key, location) {
34
+ const value = optionalNonNegativeNumber(record, key, location);
35
+ if (value !== undefined && !Number.isInteger(value))
36
+ fail(`${location}.${key} must be an integer.`);
37
+ return value;
38
+ }
39
+ function rejectEvaluationFields(record, location) {
40
+ for (const key of ['verdict', 'expectations', 'evaluationResult', 'passed', 'failed']) {
41
+ if (Object.prototype.hasOwnProperty.call(record, key)) {
42
+ fail(`${location} must not contain behavioral evaluation field "${key}".`);
43
+ }
44
+ }
45
+ }
46
+ function evidenceValue(value, location, ancestors) {
47
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
48
+ return value;
49
+ if (typeof value === 'number') {
50
+ if (!Number.isFinite(value))
51
+ fail(`${location} must contain only finite numbers.`);
52
+ return value;
53
+ }
54
+ if (!value || typeof value !== 'object')
55
+ fail(`${location} contains unsupported value type "${typeof value}".`);
56
+ if (ancestors.has(value))
57
+ fail(`${location} must not contain cyclic values.`);
58
+ ancestors.add(value);
59
+ try {
60
+ if (Array.isArray(value)) {
61
+ return value.map((entry, index) => {
62
+ if (!Object.prototype.hasOwnProperty.call(value, index))
63
+ fail(`${location}[${index}] must not be sparse.`);
64
+ return evidenceValue(entry, `${location}[${index}]`, ancestors);
65
+ });
66
+ }
67
+ const prototype = Object.getPrototypeOf(value);
68
+ if (prototype !== Object.prototype && prototype !== null) {
69
+ fail(`${location} must contain only plain records and arrays.`);
70
+ }
71
+ const result = Object.create(null);
72
+ for (const key of Reflect.ownKeys(value)) {
73
+ if (typeof key !== 'string')
74
+ fail(`${location} must not contain symbol keys.`);
75
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
76
+ if (!descriptor || descriptor.get || descriptor.set)
77
+ fail(`${location}.${key} must be a plain data property.`);
78
+ result[key] = evidenceValue(descriptor.value, `${location}.${key}`, ancestors);
79
+ }
80
+ return result;
81
+ }
82
+ finally {
83
+ ancestors.delete(value);
84
+ }
85
+ }
86
+ function safeError(value, location) {
87
+ if (!isRecord(value))
88
+ fail(`${location} must be a record.`);
89
+ rejectEvaluationFields(value, location);
90
+ const message = requiredText(value.message, `${location}.message`);
91
+ const name = optionalText(value, 'name', location);
92
+ return { ...(name === undefined ? {} : { name }), message };
93
+ }
94
+ function tokenUsage(value) {
95
+ if (value === undefined)
96
+ return { availability: 'unavailable' };
97
+ if (!isRecord(value))
98
+ fail('External execution evidence tokenUsage must be a record.');
99
+ rejectEvaluationFields(value, 'External execution evidence tokenUsage');
100
+ const usage = {
101
+ inputTokens: optionalNonNegativeNumber(value, 'inputTokens', 'External execution evidence tokenUsage'),
102
+ outputTokens: optionalNonNegativeNumber(value, 'outputTokens', 'External execution evidence tokenUsage'),
103
+ totalTokens: optionalNonNegativeNumber(value, 'totalTokens', 'External execution evidence tokenUsage'),
104
+ reasoningTokens: optionalNonNegativeNumber(value, 'reasoningTokens', 'External execution evidence tokenUsage'),
105
+ };
106
+ if (Object.values(usage).every((entry) => entry === undefined))
107
+ return { availability: 'unavailable' };
108
+ return {
109
+ availability: 'available',
110
+ ...(usage.inputTokens === undefined ? {} : { inputTokens: usage.inputTokens }),
111
+ ...(usage.outputTokens === undefined ? {} : { outputTokens: usage.outputTokens }),
112
+ ...(usage.totalTokens === undefined ? {} : { totalTokens: usage.totalTokens }),
113
+ ...(usage.reasoningTokens === undefined ? {} : { reasoningTokens: usage.reasoningTokens }),
114
+ };
115
+ }
116
+ function normalizeToolCall(value, index) {
117
+ const location = `External execution evidence toolCalls[${index}]`;
118
+ if (!isRecord(value))
119
+ fail(`${location} must be a record.`);
120
+ rejectEvaluationFields(value, location);
121
+ const callId = requiredText(value.callId, `${location}.callId`);
122
+ const name = requiredText(value.name, `${location}.name`);
123
+ if (!Object.prototype.hasOwnProperty.call(value, 'input'))
124
+ fail(`${location}.input is required.`);
125
+ const input = JSON.stringify(evidenceValue(value.input, `${location}.input`, new Set()));
126
+ const sequence = optionalNonNegativeInteger(value, 'sequence', location);
127
+ if (sequence === undefined)
128
+ fail(`${location}.sequence is required.`);
129
+ optionalNonNegativeInteger(value, 'step', location);
130
+ const startedAt = optionalText(value, 'startedAt', location);
131
+ const endedAt = optionalText(value, 'endedAt', location);
132
+ const durationMs = optionalNonNegativeNumber(value, 'durationMs', location);
133
+ const base = {
134
+ callId,
135
+ name,
136
+ input,
137
+ ...(durationMs === undefined ? {} : { duration: `${durationMs}ms` }),
138
+ ...(startedAt === undefined ? {} : { startedAt }),
139
+ ...(endedAt === undefined ? {} : { endedAt }),
140
+ };
141
+ if (value.status === 'success') {
142
+ if (Object.prototype.hasOwnProperty.call(value, 'error'))
143
+ fail(`${location}.error is not allowed for a successful call.`);
144
+ if (!Object.prototype.hasOwnProperty.call(value, 'output'))
145
+ return { ...base, status: 'success' };
146
+ const output = JSON.stringify(evidenceValue(value.output, `${location}.output`, new Set()));
147
+ return { ...base, status: 'success', output };
148
+ }
149
+ if (value.status === 'error') {
150
+ if (Object.prototype.hasOwnProperty.call(value, 'output'))
151
+ fail(`${location}.output is not allowed for an error call.`);
152
+ if (!Object.prototype.hasOwnProperty.call(value, 'error'))
153
+ return { ...base, status: 'error' };
154
+ return { ...base, status: 'error', error: safeError(value.error, `${location}.error`) };
155
+ }
156
+ return fail(`${location}.status must be "success" or "error".`);
157
+ }
158
+ export function normalizeExternalExecutionEvidence(value) {
159
+ if (!isRecord(value))
160
+ fail('External execution evidence must be a record.');
161
+ rejectEvaluationFields(value, 'External execution evidence');
162
+ requiredText(value.source, 'External execution evidence source');
163
+ if (!Array.isArray(value.toolCalls))
164
+ fail('External execution evidence toolCalls must be an array.');
165
+ const toolCalls = value.toolCalls.map(normalizeToolCall);
166
+ const callIds = new Set();
167
+ const sequences = new Set();
168
+ value.toolCalls.forEach((rawCall, index) => {
169
+ const call = rawCall;
170
+ const callId = call.callId;
171
+ const sequence = call.sequence;
172
+ if (callIds.has(callId))
173
+ fail(`External execution evidence contains duplicate callId "${callId}".`);
174
+ if (sequences.has(sequence))
175
+ fail(`External execution evidence contains duplicate sequence "${sequence}".`);
176
+ callIds.add(callId);
177
+ sequences.add(sequence);
178
+ if (sequence !== index)
179
+ fail('External execution evidence sequences must be contiguous and zero-based.');
180
+ });
181
+ const timeline = toolCalls.map((call) => ({
182
+ label: `Tool completed: ${call.name}`,
183
+ detail: `Operational status: ${call.status}`,
184
+ ...(call.duration === undefined ? {} : { duration: call.duration }),
185
+ }));
186
+ const provider = optionalText(value, 'provider', 'External execution evidence');
187
+ const model = optionalText(value, 'model', 'External execution evidence');
188
+ const startedAt = optionalText(value, 'startedAt', 'External execution evidence');
189
+ const endedAt = optionalText(value, 'endedAt', 'External execution evidence');
190
+ const latencyMs = optionalNonNegativeNumber(value, 'latencyMs', 'External execution evidence');
191
+ const finishReason = optionalText(value, 'finishReason', 'External execution evidence');
192
+ return {
193
+ toolCalls,
194
+ timeline,
195
+ evidence: {
196
+ ...(provider === undefined ? {} : { provider }),
197
+ ...(model === undefined ? {} : { model }),
198
+ ...(startedAt === undefined ? {} : { startedAt }),
199
+ ...(endedAt === undefined ? {} : { endedAt }),
200
+ ...(latencyMs === undefined ? {} : { latencyMs }),
201
+ ...(finishReason === undefined ? {} : { finishReason }),
202
+ tokenUsage: tokenUsage(value.tokenUsage),
203
+ },
204
+ };
205
+ }
@@ -0,0 +1,21 @@
1
+ import type { CompletedExecutionRecord, LanguageAdapter, ProjectDescriptor } from './contracts.js';
2
+ export type EngineRunResult = {
3
+ execution: CompletedExecutionRecord;
4
+ boundaryEvidence: {
5
+ executionId: string;
6
+ completedBehavioralRuns: 1;
7
+ adapter: string;
8
+ runner: 'LanguageAdapterRunner';
9
+ evaluator: 'deterministic';
10
+ storyUnchanged: boolean;
11
+ mockDataUsed: false;
12
+ };
13
+ };
14
+ export declare class EthogramEngine {
15
+ private readonly runner;
16
+ private project?;
17
+ constructor(runner: LanguageAdapter);
18
+ loadProject(projectRoot: string): Promise<ProjectDescriptor>;
19
+ getProject(): ProjectDescriptor;
20
+ runStory(storyId: string): Promise<EngineRunResult>;
21
+ }
@@ -0,0 +1,54 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { evaluateStory } from './evaluator.js';
3
+ function canonical(value) {
4
+ return JSON.stringify(value);
5
+ }
6
+ function deepFreeze(value) {
7
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
8
+ Object.freeze(value);
9
+ for (const nested of Object.values(value))
10
+ deepFreeze(nested);
11
+ }
12
+ return value;
13
+ }
14
+ export class EthogramEngine {
15
+ runner;
16
+ project;
17
+ constructor(runner) {
18
+ this.runner = runner;
19
+ }
20
+ async loadProject(projectRoot) {
21
+ this.project = await this.runner.loadProject(projectRoot);
22
+ return this.project;
23
+ }
24
+ getProject() {
25
+ if (!this.project)
26
+ throw new Error('PROJECT_NOT_LOADED');
27
+ return this.project;
28
+ }
29
+ async runStory(storyId) {
30
+ const project = this.getProject();
31
+ const story = project.stories.find((candidate) => candidate.id === storyId);
32
+ if (!story)
33
+ throw new Error(`STORY_NOT_FOUND: ${storyId}`);
34
+ if (!story.executable)
35
+ throw new Error(`STORY_NOT_EXECUTABLE: ${storyId}`);
36
+ const storySnapshot = canonical(story);
37
+ deepFreeze(story);
38
+ const request = Object.freeze({ story });
39
+ const observedRun = await this.runner.run(request);
40
+ const evaluationResult = evaluateStory(story, observedRun);
41
+ return {
42
+ execution: { observedRun, evaluationResult },
43
+ boundaryEvidence: {
44
+ executionId: randomUUID(),
45
+ completedBehavioralRuns: 1,
46
+ adapter: this.runner.id,
47
+ runner: 'LanguageAdapterRunner',
48
+ evaluator: 'deterministic',
49
+ storyUnchanged: canonical(story) === storySnapshot,
50
+ mockDataUsed: false,
51
+ },
52
+ };
53
+ }
54
+ }