@mamund/grail 0.0.0-stage → 0.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -2
  3. package/affordanceModel.js +23 -0
  4. package/affordanceRegistry.js +14 -0
  5. package/bindings/binding.js +229 -0
  6. package/bindings/httpBinding.js +173 -0
  7. package/bindings/nodeBinding.js +25 -0
  8. package/bindings/stdioBinding.js +58 -0
  9. package/cli/grail-cli.js +565 -0
  10. package/client.js +77 -0
  11. package/config-loader/loadEnvironment.js +71 -0
  12. package/docs/ENVIRONMENT.md +381 -0
  13. package/docs/grail-trust-model.md +665 -0
  14. package/examples/cli-tour/README.md +346 -0
  15. package/examples/cli-tour/capabilities/farewell.js +3 -0
  16. package/examples/cli-tour/capabilities/hello.js +3 -0
  17. package/examples/cli-tour/config/goal.json +3 -0
  18. package/examples/cli-tour/config/inputs.json +3 -0
  19. package/examples/cli-tour/config/observations.json +23 -0
  20. package/examples/cli-tour/config/registry.json +48 -0
  21. package/examples/cli-tour/config/worldstate.json +4 -0
  22. package/examples/cli-tour/g-loop.sh +7 -0
  23. package/examples/cli-tour/inputs/jane.json +3 -0
  24. package/examples/cli-tour/inputs/mike.json +3 -0
  25. package/examples/cli-tour/inputs/ruth.json +3 -0
  26. package/grail.js +59 -0
  27. package/images/grail-release-banner.png +0 -0
  28. package/index.js +2 -0
  29. package/observationStore.js +110 -0
  30. package/package.json +57 -4
  31. package/schemas/goal.schema.json +14 -0
  32. package/schemas/inputs.schema.json +6 -0
  33. package/schemas/observations.schema.json +75 -0
  34. package/schemas/registry.schema.json +285 -0
  35. package/schemas/worldstate.schema.json +7 -0
  36. package/server.js +201 -0
  37. package/utils/loadJSON.js +33 -0
  38. package/utils/validateWithSchema.js +42 -0
  39. package/worldState.js +36 -0
@@ -0,0 +1,565 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const packageJson = JSON.parse(
10
+ fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')
11
+ );
12
+
13
+ function showHelp() {
14
+ console.log(`GRAIL
15
+
16
+ Usage:
17
+ grail <command> [options]
18
+
19
+ Commands:
20
+ init Create a runnable GRAIL world
21
+ run Run a GRAIL environment
22
+ show Display validated configuration
23
+ validate Validate a GRAIL environment
24
+
25
+ Options:
26
+ --config <directory> Configuration directory (default: ./config)
27
+ --goal <effect> Override the configured goal (run only)
28
+ --inputs <json> Override inputs with a JSON object (run only)
29
+ --inputs-file <file> Override inputs from a JSON file (run only)
30
+ --output <mode> Result format: summary or json (run only)
31
+ --help, -h Show help
32
+ --version, -v Show version
33
+
34
+ Examples:
35
+ grail init my-world
36
+ grail validate --config ./config
37
+ grail show registry
38
+ grail run --config ./config
39
+ grail run --config ./config --goal greetingCreated
40
+ grail run --config ./config --inputs '{"name":"Mike"}'
41
+ grail run --config ./config --inputs-file ./cases/mike.json
42
+ grail run --output summary
43
+ grail run --output json`);
44
+ }
45
+
46
+ function showCommandHelp(command) {
47
+
48
+ if (command === 'init') {
49
+ console.log(`GRAIL init
50
+
51
+ Usage:
52
+ grail init <directory>
53
+
54
+ Creates a complete, runnable GRAIL world.
55
+
56
+ Example:
57
+ grail init my-world`);
58
+ return;
59
+ }
60
+
61
+ if (command === 'run') {
62
+ console.log(`GRAIL run
63
+
64
+ Usage:
65
+ grail run [--config <directory>] [--goal <effect>] [--inputs <json> | --inputs-file <file>] [--output summary|json]
66
+
67
+ Runs the goal declared by the GRAIL environment, with optional invocation-specific goal and input overrides.
68
+
69
+ Options:
70
+ --config <directory> Configuration directory (default: ./config)
71
+ --goal <effect> Override the goal declared in goal.json for this run
72
+ --inputs <json> Replace inputs.json for this run with an inline JSON object
73
+ --inputs-file <file> Replace inputs.json for this run with a JSON file
74
+ --output <mode> Print a summary or complete JSON result (default: existing trace)
75
+ --help, -h Show help`);
76
+ return;
77
+ }
78
+
79
+ if (command === 'show') {
80
+ console.log(`GRAIL show
81
+
82
+ Usage:
83
+ grail show [registry|worldstate|inputs|goal] [--config <directory>]
84
+
85
+ Displays validated configuration as formatted JSON without executing capabilities.
86
+ Without a target, displays all four configuration documents.
87
+
88
+ Options:
89
+ --config <directory> Configuration directory (default: ./config)
90
+ --help, -h Show help`);
91
+ return;
92
+ }
93
+
94
+ if (command === 'validate') {
95
+ console.log(`GRAIL validate
96
+
97
+ Usage:
98
+ grail validate [--config <directory>]
99
+
100
+ Loads and validates the GRAIL environment without running it.
101
+
102
+ Options:
103
+ --config <directory> Configuration directory (default: ./config)
104
+ --help, -h Show help`);
105
+ }
106
+ }
107
+
108
+ function parseConfig(args) {
109
+ let configDir = './config';
110
+
111
+ for (let i = 0; i < args.length; i += 1) {
112
+ const arg = args[i];
113
+
114
+ if (arg === '--config') {
115
+ const value = args[i + 1];
116
+ if (!value || value.startsWith('-')) {
117
+ const error = new Error('--config requires a directory.');
118
+ error.hint = 'Run \"grail run --help\" or \"grail validate --help\" for usage.';
119
+ throw error;
120
+ }
121
+ configDir = value;
122
+ i += 1;
123
+ continue;
124
+ }
125
+
126
+ if (arg.startsWith('--config=')) {
127
+ const value = arg.slice('--config='.length);
128
+ if (!value) {
129
+ const error = new Error('--config requires a directory.');
130
+ error.hint = 'Run \"grail run --help\" or \"grail validate --help\" for usage.';
131
+ throw error;
132
+ }
133
+ configDir = value;
134
+ continue;
135
+ }
136
+
137
+ const error = new Error(`Unknown option: ${arg}`);
138
+ error.hint = 'Run the command with --help for supported options.';
139
+ throw error;
140
+ }
141
+
142
+ return path.resolve(configDir);
143
+ }
144
+
145
+ function parseRunOptions(args) {
146
+ let configDir = './config';
147
+ let goal;
148
+ let inputs;
149
+ let inputsFile;
150
+ let output;
151
+
152
+ for (let i = 0; i < args.length; i += 1) {
153
+ const arg = args[i];
154
+
155
+ if (arg === '--config') {
156
+ const value = args[i + 1];
157
+ if (!value || value.startsWith('-')) {
158
+ const error = new Error('--config requires a directory.');
159
+ error.hint = 'Run "grail run --help" for usage.';
160
+ throw error;
161
+ }
162
+ configDir = value;
163
+ i += 1;
164
+ continue;
165
+ }
166
+
167
+ if (arg.startsWith('--config=')) {
168
+ const value = arg.slice('--config='.length);
169
+ if (!value) {
170
+ const error = new Error('--config requires a directory.');
171
+ error.hint = 'Run "grail run --help" for usage.';
172
+ throw error;
173
+ }
174
+ configDir = value;
175
+ continue;
176
+ }
177
+
178
+ if (arg === '--goal') {
179
+ const value = args[i + 1];
180
+ if (!value || value.startsWith('-')) {
181
+ const error = new Error('--goal requires an effect.');
182
+ error.hint = 'Run "grail run --help" for usage.';
183
+ throw error;
184
+ }
185
+ goal = value;
186
+ i += 1;
187
+ continue;
188
+ }
189
+
190
+ if (arg.startsWith('--goal=')) {
191
+ const value = arg.slice('--goal='.length);
192
+ if (!value) {
193
+ const error = new Error('--goal requires an effect.');
194
+ error.hint = 'Run "grail run --help" for usage.';
195
+ throw error;
196
+ }
197
+ goal = value;
198
+ continue;
199
+ }
200
+
201
+ if (arg === '--inputs') {
202
+ const value = args[i + 1];
203
+ if (!value || value.startsWith('-')) {
204
+ const error = new Error('--inputs requires a JSON object.');
205
+ error.hint = 'Run "grail run --help" for usage.';
206
+ throw error;
207
+ }
208
+ inputs = value;
209
+ i += 1;
210
+ continue;
211
+ }
212
+
213
+ if (arg.startsWith('--inputs=')) {
214
+ const value = arg.slice('--inputs='.length);
215
+ if (!value) {
216
+ const error = new Error('--inputs requires a JSON object.');
217
+ error.hint = 'Run "grail run --help" for usage.';
218
+ throw error;
219
+ }
220
+ inputs = value;
221
+ continue;
222
+ }
223
+
224
+ if (arg === '--inputs-file') {
225
+ const value = args[i + 1];
226
+ if (!value || value.startsWith('-')) {
227
+ const error = new Error('--inputs-file requires a file.');
228
+ error.hint = 'Run "grail run --help" for usage.';
229
+ throw error;
230
+ }
231
+ inputsFile = value;
232
+ i += 1;
233
+ continue;
234
+ }
235
+
236
+ if (arg.startsWith('--inputs-file=')) {
237
+ const value = arg.slice('--inputs-file='.length);
238
+ if (!value) {
239
+ const error = new Error('--inputs-file requires a file.');
240
+ error.hint = 'Run "grail run --help" for usage.';
241
+ throw error;
242
+ }
243
+ inputsFile = value;
244
+ continue;
245
+ }
246
+
247
+ if (arg === '--output' || arg.startsWith('--output=')) {
248
+ const value = arg === '--output' ? args[++i] : arg.slice('--output='.length);
249
+ if (!value || value.startsWith('-')) {
250
+ throw new Error('--output requires summary or json.');
251
+ }
252
+ if (!['summary', 'json'].includes(value)) {
253
+ throw new Error(`Invalid --output value: ${value}. Expected summary or json.`);
254
+ }
255
+ output = value;
256
+ continue;
257
+ }
258
+
259
+ const error = new Error(`Unknown option: ${arg}`);
260
+ error.hint = 'Run "grail run --help" for supported options.';
261
+ throw error;
262
+ }
263
+
264
+ if (inputs !== undefined && inputsFile !== undefined) {
265
+ const error = new Error('--inputs and --inputs-file cannot be used together.');
266
+ error.hint = 'Run "grail run --help" for usage.';
267
+ throw error;
268
+ }
269
+
270
+ return { configDir: path.resolve(configDir), goal, inputs, inputsFile, output };
271
+ }
272
+
273
+ function parseInputsJson(value, source) {
274
+ let parsed;
275
+
276
+ try {
277
+ parsed = JSON.parse(value);
278
+ } catch (error) {
279
+ throw new Error(`Invalid JSON in ${source}: ${error.message}`);
280
+ }
281
+
282
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
283
+ throw new Error(`${source} must contain a JSON object.`);
284
+ }
285
+
286
+ return parsed;
287
+ }
288
+
289
+ function loadInputsFile(file) {
290
+ const filePath = path.resolve(file);
291
+
292
+ if (!fs.existsSync(filePath)) {
293
+ throw new Error(`Inputs file not found: ${filePath}`);
294
+ }
295
+
296
+ if (!fs.statSync(filePath).isFile()) {
297
+ throw new Error(`Inputs file is not a file: ${filePath}`);
298
+ }
299
+
300
+ return parseInputsJson(fs.readFileSync(filePath, 'utf8'), `inputs file ${filePath}`);
301
+ }
302
+
303
+ function writeJson(filePath, value) {
304
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
305
+ }
306
+
307
+ function initCommand(args) {
308
+ if (args.includes('--help') || args.includes('-h')) {
309
+ showCommandHelp('init');
310
+ return;
311
+ }
312
+
313
+ if (args.length !== 1 || args[0].startsWith('-')) {
314
+ throw new Error('Usage: grail init <directory>');
315
+ }
316
+
317
+ const targetDir = path.resolve(args[0]);
318
+
319
+ if (fs.existsSync(targetDir)) {
320
+ throw new Error(`Target already exists: ${targetDir}`);
321
+ }
322
+
323
+ const configDir = path.join(targetDir, 'config');
324
+ const capabilitiesDir = path.join(targetDir, 'capabilities');
325
+
326
+ fs.mkdirSync(configDir, { recursive: true });
327
+ fs.mkdirSync(capabilitiesDir, { recursive: true });
328
+
329
+ writeJson(path.join(configDir, 'registry.json'), {
330
+ createGreeting: {
331
+ id: 'aff-create-greeting',
332
+ action: 'createGreeting',
333
+ type: 'task',
334
+ preconditions: [],
335
+ inputs: {
336
+ name: '$inputs.name'
337
+ },
338
+ effects: ['greetingCreated'],
339
+ binding: {
340
+ protocol: 'node',
341
+ module: './capabilities/hello.mjs',
342
+ function: 'createGreeting',
343
+ outputs: {
344
+ message: {
345
+ from: 'result',
346
+ path: 'message'
347
+ }
348
+ }
349
+ }
350
+ }
351
+ });
352
+
353
+ writeJson(path.join(configDir, 'worldstate.json'), {
354
+ greetingCreated: false
355
+ });
356
+
357
+ writeJson(path.join(configDir, 'inputs.json'), {
358
+ name: 'World'
359
+ });
360
+
361
+ writeJson(path.join(configDir, 'goal.json'), {
362
+ goal: 'greetingCreated'
363
+ });
364
+
365
+ fs.writeFileSync(
366
+ path.join(capabilitiesDir, 'hello.mjs'),
367
+ `export async function createGreeting({ name }) {\n return {\n message: \`Hello, \${name}!\`\n };\n}\n`,
368
+ 'utf8'
369
+ );
370
+
371
+ console.log(`Created GRAIL world: ${args[0]}\n\n config/registry.json\n config/worldstate.json\n config/inputs.json\n config/goal.json\n capabilities/hello.mjs\n\nNext:\n\n cd ${args[0]}\n grail validate\n grail run`);
372
+ }
373
+
374
+ function formatSummary(result) {
375
+ const lines = [
376
+ `Goal: ${result.goal}`,
377
+ `Result: ${result.reached ? 'SUCCESS' : 'FAIL'}`,
378
+ `Capability invocations: ${result.observations.length}`
379
+ ];
380
+ for (const observation of result.observations) {
381
+ lines.push('', `${observation.invocation?.affordance ?? 'Unknown affordance'}: ${observation.result}`);
382
+ for (const [key, value] of Object.entries(observation.outputs ?? {})) {
383
+ lines.push(` ${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`);
384
+ }
385
+ }
386
+ return lines.join('\n');
387
+ }
388
+
389
+ async function runCommand(args) {
390
+ if (args.includes('--help') || args.includes('-h')) {
391
+ showCommandHelp('run');
392
+ return;
393
+ }
394
+
395
+ const { Grail, loadEnvironment } = await import('../index.js');
396
+ const {
397
+ configDir,
398
+ goal: goalOverride,
399
+ inputs: inputsOverride,
400
+ inputsFile,
401
+ output
402
+ } = parseRunOptions(args);
403
+ const environment = loadEnvironment(configDir);
404
+ const goal = goalOverride ?? environment.goal;
405
+ const inputs = inputsOverride !== undefined
406
+ ? parseInputsJson(inputsOverride, '--inputs')
407
+ : inputsFile !== undefined
408
+ ? loadInputsFile(inputsFile)
409
+ : environment.inputs;
410
+
411
+ const grail = new Grail({
412
+ registry: environment.registry,
413
+ worldstate: environment.worldstate,
414
+ inputs,
415
+ baseDir: environment.baseDir,
416
+ observationPath: path.join(configDir, 'observations.json')
417
+ });
418
+
419
+ try {
420
+ // Preserve the historical trace unless an explicit result format was requested.
421
+ // Restore logging even when pursuit throws; structured stdout must remain clean.
422
+ const originalLog = console.log;
423
+ let result;
424
+ try {
425
+ if (output) console.log = () => {};
426
+ result = await grail.pursue(goal);
427
+ } finally {
428
+ console.log = originalLog;
429
+ }
430
+
431
+ if (output === 'json') {
432
+ console.log(JSON.stringify(result, null, 2));
433
+ } else if (output === 'summary') {
434
+ console.log(formatSummary(result));
435
+ }
436
+
437
+ if (!result.reached) {
438
+ const failedObservation = [...result.observations]
439
+ .reverse()
440
+ .find(observation => observation.result === 'FAIL');
441
+
442
+ if (failedObservation) {
443
+ const detail =
444
+ failedObservation.response?.error ||
445
+ failedObservation.response?.stderr ||
446
+ 'a capability invocation failed';
447
+
448
+ console.error(
449
+ `GRAIL: Execution failed while pursuing goal \"${goal}\": ${detail}`
450
+ );
451
+ } else {
452
+ console.error(
453
+ `GRAIL: Pursuit failed: goal \"${goal}\" cannot be resolved with the available capabilities.`
454
+ );
455
+ }
456
+
457
+ process.exitCode = 1;
458
+ }
459
+ } catch (error) {
460
+ error.exitCode = 1;
461
+ throw error;
462
+ }
463
+ }
464
+
465
+ async function showCommand(args) {
466
+ if (args.includes('--help') || args.includes('-h')) {
467
+ showCommandHelp('show');
468
+ return;
469
+ }
470
+
471
+ const targets = new Set(['registry', 'worldstate', 'inputs', 'goal']);
472
+ let target;
473
+ const configArgs = [];
474
+ for (let i = 0; i < args.length; i += 1) {
475
+ const arg = args[i];
476
+ if (arg === '--config') {
477
+ configArgs.push(arg);
478
+ if (i + 1 < args.length) configArgs.push(args[++i]);
479
+ } else if (arg.startsWith('--config=')) {
480
+ configArgs.push(arg);
481
+ } else if (!arg.startsWith('-') && target === undefined) {
482
+ target = arg;
483
+ } else {
484
+ const error = new Error(`Unknown option or extra argument: ${arg}`);
485
+ error.hint = 'Run "grail show --help" for usage.';
486
+ throw error;
487
+ }
488
+ }
489
+
490
+ if (target !== undefined && !targets.has(target)) {
491
+ const error = new Error(`Unknown show target: ${target}`);
492
+ error.hint = 'Choose registry, worldstate, inputs, or goal.';
493
+ throw error;
494
+ }
495
+
496
+ const { loadEnvironment } = await import('../index.js');
497
+ const environment = loadEnvironment(parseConfig(configArgs));
498
+ const documents = {
499
+ registry: environment.registry,
500
+ worldstate: environment.worldstate,
501
+ inputs: environment.inputs,
502
+ goal: { goal: environment.goal }
503
+ };
504
+ console.log(JSON.stringify(target ? documents[target] : documents, null, 2));
505
+ }
506
+
507
+ async function validateCommand(args) {
508
+ if (args.includes('--help') || args.includes('-h')) {
509
+ showCommandHelp('validate');
510
+ return;
511
+ }
512
+
513
+ const { loadEnvironment } = await import('../index.js');
514
+ const configDir = parseConfig(args);
515
+ loadEnvironment(configDir);
516
+ console.log(`VALID: ${configDir}`);
517
+ }
518
+
519
+ async function main() {
520
+ const args = process.argv.slice(2);
521
+
522
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
523
+ showHelp();
524
+ return;
525
+ }
526
+
527
+ if (args[0] === '--version' || args[0] === '-v') {
528
+ console.log(packageJson.version);
529
+ return;
530
+ }
531
+
532
+ const [command, ...commandArgs] = args;
533
+
534
+ if (command === 'init') {
535
+ initCommand(commandArgs);
536
+ return;
537
+ }
538
+
539
+ if (command === 'run') {
540
+ await runCommand(commandArgs);
541
+ return;
542
+ }
543
+
544
+ if (command === 'show') {
545
+ await showCommand(commandArgs);
546
+ return;
547
+ }
548
+
549
+ if (command === 'validate') {
550
+ await validateCommand(commandArgs);
551
+ return;
552
+ }
553
+
554
+ const error = new Error(`Unknown command: ${command}`);
555
+ error.hint = 'Run \"grail --help\" for available commands.';
556
+ throw error;
557
+ }
558
+
559
+ main().catch(error => {
560
+ console.error(`GRAIL: ${error.message}`);
561
+ if (error.hint) {
562
+ console.error(error.hint);
563
+ }
564
+ process.exitCode = error.exitCode ?? 2;
565
+ });
package/client.js ADDED
@@ -0,0 +1,77 @@
1
+ // client.js
2
+ export class Client {
3
+ constructor(server, inputs) {
4
+ this.server = server;
5
+ this.inputs = inputs;
6
+ this.stack = [];
7
+ }
8
+
9
+ async pursue(goal) {
10
+ // Each pursuit has its own traversal stack. Worldstate and observations persist.
11
+ this.stack = [];
12
+ console.log(`\n[CLIENT] Starting pursuit: ${goal}`);
13
+
14
+ while (true) {
15
+
16
+ // If there is no current work, resolve the goal.
17
+ if (this.stack.length === 0) {
18
+ const response = this.server.resolveGoal(goal);
19
+
20
+ if (response.result === "SUCCESS") {
21
+ console.log(`[CLIENT] Goal reached: ${goal}`);
22
+ break;
23
+ }
24
+
25
+ if (response.offeredAffordances.length > 0) {
26
+ const next = response.offeredAffordances[0];
27
+
28
+ console.log(
29
+ `[CLIENT] Pushing goal affordance: ${next.action}`
30
+ );
31
+
32
+ this.stack.push(next);
33
+ continue;
34
+ }
35
+
36
+ console.log(
37
+ `[CLIENT] Cannot proceed: goal is unresolvable - ${goal}`
38
+ );
39
+ break;
40
+ }
41
+
42
+ const current = this.stack[this.stack.length - 1];
43
+ const response = await this.server.attempt(current, this.inputs);
44
+
45
+ if (response.result === "SUCCESS") {
46
+ console.log(`[CLIENT] Affordance succeeded: ${current.action}`);
47
+ this.stack.pop();
48
+
49
+ } else if (
50
+ response.result === "BLOCKED" &&
51
+ response.offeredAffordances.length > 0
52
+ ) {
53
+ const next = response.offeredAffordances[0];
54
+
55
+ console.log(`[CLIENT] Pushing next affordance: ${next.action}`);
56
+ this.stack.push(next);
57
+
58
+ } else if (response.result === "FAIL") {
59
+ console.log(`[CLIENT] Affordance failed: ${current.action}`);
60
+ this.stack.pop();
61
+
62
+ } else if (
63
+ response.result === "BLOCKED" &&
64
+ response.condition
65
+ ) {
66
+ console.log(
67
+ `[CLIENT] Cannot proceed: condition is unresolvable - ${response.condition}`
68
+ );
69
+ break;
70
+
71
+ } else {
72
+ console.log(`[CLIENT] Cannot proceed: ${response.result}`);
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadAndValidateJSON } from '../utils/loadJSON.js';
4
+
5
+ /**
6
+ * Load and validate a GRAIL environment from a configuration directory.
7
+ *
8
+ * Filesystem loading belongs outside the runtime itself. The returned object
9
+ * can be passed directly to Grail; the goal is returned alongside the runtime
10
+ * configuration for the caller to pursue explicitly.
11
+ *
12
+ * @param {string} configDir - Directory containing the GRAIL JSON files.
13
+ * @returns {{registry: object, worldstate: object, inputs: object, goal: string, baseDir: string}}
14
+ */
15
+ export function loadEnvironment(configDir) {
16
+ if (!configDir) {
17
+ throw new Error('Configuration error: no configuration directory was provided.');
18
+ }
19
+
20
+ const resolvedConfigDir = path.resolve(configDir);
21
+
22
+ if (!fs.existsSync(resolvedConfigDir)) {
23
+ throw new Error(`Configuration error: directory not found: ${resolvedConfigDir}`);
24
+ }
25
+
26
+ if (!fs.statSync(resolvedConfigDir).isDirectory()) {
27
+ throw new Error(`Configuration error: not a directory: ${resolvedConfigDir}`);
28
+ }
29
+
30
+ const loadConfig = (filename, schemaName) => {
31
+ const filePath = path.join(resolvedConfigDir, filename);
32
+
33
+ if (!fs.existsSync(filePath)) {
34
+ throw new Error(`Configuration error: ${filename} not found in ${resolvedConfigDir}`);
35
+ }
36
+
37
+ try {
38
+ return loadAndValidateJSON(filePath, schemaName, { silent: true });
39
+ } catch (error) {
40
+ if (error instanceof SyntaxError) {
41
+ throw new Error(`Configuration error: invalid JSON in ${filename}: ${error.message}`);
42
+ }
43
+
44
+ if (error.validationErrors) {
45
+ const details = error.validationErrors
46
+ .map(validationError => {
47
+ const location = validationError.instancePath || '/';
48
+ return `${location} ${validationError.message}`;
49
+ })
50
+ .join('; ');
51
+
52
+ throw new Error(`Configuration error: ${filename} failed schema validation: ${details}`);
53
+ }
54
+
55
+ throw error;
56
+ }
57
+ };
58
+
59
+ const inputs = loadConfig('inputs.json', 'inputs.schema.json');
60
+ const registry = loadConfig('registry.json', 'registry.schema.json');
61
+ const worldstate = loadConfig('worldstate.json', 'worldstate.schema.json');
62
+ const goalObj = loadConfig('goal.json', 'goal.schema.json');
63
+
64
+ return {
65
+ registry,
66
+ worldstate,
67
+ inputs,
68
+ goal: goalObj.goal,
69
+ baseDir: path.dirname(resolvedConfigDir)
70
+ };
71
+ }