@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.
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/affordanceModel.js +23 -0
- package/affordanceRegistry.js +14 -0
- package/bindings/binding.js +229 -0
- package/bindings/httpBinding.js +173 -0
- package/bindings/nodeBinding.js +25 -0
- package/bindings/stdioBinding.js +58 -0
- package/cli/grail-cli.js +565 -0
- package/client.js +77 -0
- package/config-loader/loadEnvironment.js +71 -0
- package/docs/ENVIRONMENT.md +381 -0
- package/docs/grail-trust-model.md +665 -0
- package/examples/cli-tour/README.md +346 -0
- package/examples/cli-tour/capabilities/farewell.js +3 -0
- package/examples/cli-tour/capabilities/hello.js +3 -0
- package/examples/cli-tour/config/goal.json +3 -0
- package/examples/cli-tour/config/inputs.json +3 -0
- package/examples/cli-tour/config/observations.json +23 -0
- package/examples/cli-tour/config/registry.json +48 -0
- package/examples/cli-tour/config/worldstate.json +4 -0
- package/examples/cli-tour/g-loop.sh +7 -0
- package/examples/cli-tour/inputs/jane.json +3 -0
- package/examples/cli-tour/inputs/mike.json +3 -0
- package/examples/cli-tour/inputs/ruth.json +3 -0
- package/grail.js +59 -0
- package/images/grail-release-banner.png +0 -0
- package/index.js +2 -0
- package/observationStore.js +110 -0
- package/package.json +57 -4
- package/schemas/goal.schema.json +14 -0
- package/schemas/inputs.schema.json +6 -0
- package/schemas/observations.schema.json +75 -0
- package/schemas/registry.schema.json +285 -0
- package/schemas/worldstate.schema.json +7 -0
- package/server.js +201 -0
- package/utils/loadJSON.js +33 -0
- package/utils/validateWithSchema.js +42 -0
- package/worldState.js +36 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# A Tour of GRAIL at the Command Line
|
|
2
|
+
|
|
3
|
+
GRAIL is a runtime for pursuing a goal within a bounded environment of available capabilities.
|
|
4
|
+
|
|
5
|
+
The easiest way to understand GRAIL is to use it. This tour starts with a small working GRAIL world and changes one thing at a time. You will change inputs, invoke the world from different locations, pursue a different goal, ask for a goal the world cannot resolve, and finally use the shell to repeat pursuits.
|
|
6
|
+
|
|
7
|
+
The important thing to watch is what does not change: the world continues to define the available possibilities while each invocation supplies the context for a particular pursuit. Along the way, `grail show` lets you inspect the configured world, and `grail run --output` lets you inspect the result of a pursuit.
|
|
8
|
+
|
|
9
|
+
## Before you begin
|
|
10
|
+
|
|
11
|
+
This directory is a complete GRAIL world prepared for the tour:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
cli-tour/
|
|
15
|
+
├── capabilities/
|
|
16
|
+
│ ├── farewell.js
|
|
17
|
+
│ └── hello.js
|
|
18
|
+
├── config/
|
|
19
|
+
│ ├── goal.json
|
|
20
|
+
│ ├── inputs.json
|
|
21
|
+
│ ├── registry.json
|
|
22
|
+
│ └── worldstate.json
|
|
23
|
+
└── inputs/
|
|
24
|
+
├── jane.json
|
|
25
|
+
├── mike.json
|
|
26
|
+
└── ruth.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The examples assume that the `grail` command is installed or linked and that your shell is initially in this `cli-tour` directory.
|
|
30
|
+
|
|
31
|
+
## 1. Validate and run the world
|
|
32
|
+
|
|
33
|
+
Start by validating the environment:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
grail validate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
GRAIL uses `./config` as the default configuration directory, so no `--config` option is needed when `config` is a subdirectory of the current directory.
|
|
40
|
+
|
|
41
|
+
Before running, inspect the world:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
grail show
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This displays the four configured documents. You can also inspect one at a time:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
grail show goal
|
|
51
|
+
grail show registry
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`show` displays the configured environment; it does not execute capabilities or display the results of earlier pursuits.
|
|
55
|
+
|
|
56
|
+
Now run it:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
grail run
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The configured goal is `greetingCreated`. The registry contains an affordance that can establish that effect, so GRAIL can select it, invoke the bound `hello.js` capability, and reach the goal.
|
|
63
|
+
|
|
64
|
+
You have just pursued a GRAIL goal.
|
|
65
|
+
|
|
66
|
+
To see a concise account of the pursuit, run it again with:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
grail run --output summary
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The summary reports the goal, outcome, capability invocations, and any captured outputs. Unlike the default run, it focuses on the result rather than the running trace.
|
|
73
|
+
|
|
74
|
+
## 2. Change the input
|
|
75
|
+
|
|
76
|
+
The default input is stored in `config/inputs.json`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"name": "World"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
You do not need to edit that file to use a different value. Try:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
grail run --inputs '{"name":"Mike"}' --output summary
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then try another:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
grail run --inputs '{"name":"Jane"}' --output summary
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The same world is being used each time. Only the invocation input changes. The summaries let you compare the captured greeting outputs directly.
|
|
97
|
+
|
|
98
|
+
For a `run` invocation, an `--inputs` value replaces the value loaded from `inputs.json`. It does not modify `inputs.json` itself.
|
|
99
|
+
|
|
100
|
+
## 3. Supply inputs from a file
|
|
101
|
+
|
|
102
|
+
The `inputs/` directory contains several input documents. For example, `inputs/ruth.json` contains:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"name": "Ruth"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Use it with:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
grail run --inputs-file ./inputs/ruth.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
An `--inputs-file` path is resolved relative to the caller's current working directory. This is different from capability binding paths, which belong to the GRAIL world.
|
|
117
|
+
|
|
118
|
+
`--inputs` and `--inputs-file` are alternative ways to supply invocation inputs and cannot be used together.
|
|
119
|
+
|
|
120
|
+
## 4. Leave the world
|
|
121
|
+
|
|
122
|
+
So far the shell has been inside `cli-tour`. Move to its parent directory:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd ..
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Now run the same world explicitly:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
grail run --config ./cli-tour/config
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The world still works.
|
|
135
|
+
|
|
136
|
+
You can also inspect its registry from here:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
grail show registry --config ./cli-tour/config
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The selected configuration directory tells GRAIL where the world lives. Relative capability binding paths are resolved from the **world root**, which is the parent of the configuration directory, rather than from the shell's current working directory.
|
|
143
|
+
|
|
144
|
+
That means the caller and the world do not need to occupy the same place.
|
|
145
|
+
|
|
146
|
+
For the remaining examples, stay in this parent directory and use `--config ./cli-tour/config`.
|
|
147
|
+
|
|
148
|
+
## 5. Explore the possibilities in the world
|
|
149
|
+
|
|
150
|
+
This tour world contains two capabilities:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
hello.js -> greetingCreated
|
|
154
|
+
farewell.js -> farewellCreated
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The default `config/goal.json` contains:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"goal": "greetingCreated"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Confirm the configured goal and available affordances without opening the files:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
grail show goal --config ./cli-tour/config
|
|
169
|
+
grail show registry --config ./cli-tour/config
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
But the registry describes both possibilities. It is not a workflow saying that greeting must happen before farewell or that both must happen. It describes capabilities that are available in this world and the effects they can establish.
|
|
173
|
+
|
|
174
|
+
## 6. Change the goal
|
|
175
|
+
|
|
176
|
+
Ask the same world to pursue its other available effect:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
grail run \
|
|
180
|
+
--config ./cli-tour/config \
|
|
181
|
+
--goal farewellCreated \
|
|
182
|
+
--inputs '{"name":"Mike"}' \
|
|
183
|
+
--output summary
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
GRAIL now pursues `farewellCreated` instead of the goal stored in `goal.json`.
|
|
187
|
+
|
|
188
|
+
For a `run` invocation, `--goal` takes precedence over `goal.json`. The file itself is not changed. If you run `grail show goal --config ./cli-tour/config` again, you will still see `greetingCreated`.
|
|
189
|
+
|
|
190
|
+
You can therefore make different requests of the same world:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
grail run \
|
|
194
|
+
--config ./cli-tour/config \
|
|
195
|
+
--goal greetingCreated \
|
|
196
|
+
--inputs '{"name":"Mike"}'
|
|
197
|
+
|
|
198
|
+
grail run \
|
|
199
|
+
--config ./cli-tour/config \
|
|
200
|
+
--goal farewellCreated \
|
|
201
|
+
--inputs '{"name":"Mike"}'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The world defines the possibilities. The invocation identifies which possibility GRAIL should pursue.
|
|
205
|
+
|
|
206
|
+
## 7. Ask for something the world cannot do
|
|
207
|
+
|
|
208
|
+
Now request a goal for which the world has no producer:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
grail run \
|
|
212
|
+
--config ./cli-tour/config \
|
|
213
|
+
--goal makeCoffee
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
GRAIL should report that `makeCoffee` cannot be resolved with the available capabilities.
|
|
217
|
+
|
|
218
|
+
Check the process exit code immediately afterward:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
echo $?
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The result should be:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The command was valid and GRAIL was able to examine the world. The pursuit failed because the requested goal could not be reached using the capabilities available in that world.
|
|
231
|
+
|
|
232
|
+
This is different from an invalid command or invalid configuration, which exits with code `2`.
|
|
233
|
+
|
|
234
|
+
You can inspect the complete structured pursuit result as JSON:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
grail run \
|
|
238
|
+
--config ./cli-tour/config \
|
|
239
|
+
--goal makeCoffee \
|
|
240
|
+
--output json
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The result includes the requested goal, whether it was reached, the resulting worldstate, and observations. In JSON mode, routine trace messages do not mix into stdout. This unsuccessful pursuit still exits with code `1`.
|
|
244
|
+
|
|
245
|
+
To inspect only the observations, you can pipe a successful pursuit through `jq` (if installed):
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
grail run \
|
|
249
|
+
--config ./cli-tour/config \
|
|
250
|
+
--output json | jq '.observations'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The `json` mode includes observations, so a separate observations output mode is unnecessary.
|
|
254
|
+
|
|
255
|
+
## 8. Let the shell provide the loop
|
|
256
|
+
|
|
257
|
+
The `inputs/` directory contains three cases:
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
inputs/
|
|
261
|
+
├── jane.json
|
|
262
|
+
├── mike.json
|
|
263
|
+
└── ruth.json
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Use the shell to invoke GRAIL once for each input document:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
for input in ./cli-tour/inputs/*.json
|
|
270
|
+
do
|
|
271
|
+
grail run \
|
|
272
|
+
--config ./cli-tour/config \
|
|
273
|
+
--inputs-file "$input"
|
|
274
|
+
done
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The shell owns the loop. Each iteration starts a separate GRAIL pursuit with a different invocation context.
|
|
278
|
+
|
|
279
|
+
For a compact view of each result, add `--output summary`:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
for input in ./cli-tour/inputs/*.json
|
|
283
|
+
do
|
|
284
|
+
grail run \
|
|
285
|
+
--config ./cli-tour/config \
|
|
286
|
+
--inputs-file "$input" \
|
|
287
|
+
--output summary
|
|
288
|
+
done
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
GRAIL itself does not need looping semantics to participate in a repetitive or larger process.
|
|
292
|
+
|
|
293
|
+
The exit-code contract also makes it possible for the caller to decide what to do when a pursuit fails:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
for input in ./cli-tour/inputs/*.json
|
|
297
|
+
do
|
|
298
|
+
grail run \
|
|
299
|
+
--config ./cli-tour/config \
|
|
300
|
+
--inputs-file "$input" || break
|
|
301
|
+
done
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Here the shell stops the loop if GRAIL returns a nonzero exit code.
|
|
305
|
+
|
|
306
|
+
## What changed?
|
|
307
|
+
|
|
308
|
+
During this tour, you changed:
|
|
309
|
+
|
|
310
|
+
- the inputs supplied to a pursuit;
|
|
311
|
+
- the location from which GRAIL was invoked;
|
|
312
|
+
- the goal GRAIL was asked to pursue; and
|
|
313
|
+
- the number of times the caller invoked GRAIL.
|
|
314
|
+
|
|
315
|
+
You also inspected the configured world with `show` and the results of individual pursuits with `--output`.
|
|
316
|
+
|
|
317
|
+
You did not create a new workflow for each variation. The same world continued to describe its conditions, affordances, effects, and bound capabilities.
|
|
318
|
+
|
|
319
|
+
A useful way to think about the separation is:
|
|
320
|
+
|
|
321
|
+
```text
|
|
322
|
+
World
|
|
323
|
+
defines what is possible
|
|
324
|
+
|
|
325
|
+
Invocation
|
|
326
|
+
supplies the goal and inputs
|
|
327
|
+
|
|
328
|
+
GRAIL
|
|
329
|
+
pursues the goal within the world
|
|
330
|
+
|
|
331
|
+
Capabilities
|
|
332
|
+
perform the domain work
|
|
333
|
+
|
|
334
|
+
Caller
|
|
335
|
+
decides when and how often to invoke GRAIL
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The distinction is worth keeping in mind: **`show` describes the configured world; invocation options choose the goal and inputs; `--output` reports the pursuit result.**
|
|
339
|
+
|
|
340
|
+
This is the central idea behind working with GRAIL from the command line.
|
|
341
|
+
|
|
342
|
+
> Define the environment, not the path.
|
|
343
|
+
|
|
344
|
+
## Where to go next
|
|
345
|
+
|
|
346
|
+
This tour focused on using an existing GRAIL world rather than authoring one. The next step is to inspect `config/registry.json`, `config/worldstate.json`, and the two files in `capabilities/` and see how the world's possibilities are declared and bound to executable behavior.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"invocation": {
|
|
4
|
+
"id": "inv-001",
|
|
5
|
+
"affordance": "createGreeting",
|
|
6
|
+
"timestamp": "2026-10-08T19:03:06.523Z",
|
|
7
|
+
"module": "./capabilities/hello.js",
|
|
8
|
+
"function": "createGreeting",
|
|
9
|
+
"inputs": {
|
|
10
|
+
"name": "Ruth"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"response": {
|
|
14
|
+
"result": {
|
|
15
|
+
"message": "Hello, Ruth!"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"outputs": {
|
|
19
|
+
"message": "Hello, Ruth!"
|
|
20
|
+
},
|
|
21
|
+
"result": "SUCCESS"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"createGreeting": {
|
|
3
|
+
"id": "aff-create-greeting",
|
|
4
|
+
"action": "createGreeting",
|
|
5
|
+
"type": "task",
|
|
6
|
+
"preconditions": [],
|
|
7
|
+
"inputs": {
|
|
8
|
+
"name": "$inputs.name"
|
|
9
|
+
},
|
|
10
|
+
"effects": [
|
|
11
|
+
"greetingCreated"
|
|
12
|
+
],
|
|
13
|
+
"binding": {
|
|
14
|
+
"protocol": "node",
|
|
15
|
+
"module": "./capabilities/hello.js",
|
|
16
|
+
"function": "createGreeting",
|
|
17
|
+
"outputs": {
|
|
18
|
+
"message": {
|
|
19
|
+
"from": "result",
|
|
20
|
+
"path": "message"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"createFarewell": {
|
|
26
|
+
"id": "aff-create-farewell",
|
|
27
|
+
"action": "createFarewell",
|
|
28
|
+
"type": "task",
|
|
29
|
+
"preconditions": [],
|
|
30
|
+
"inputs": {
|
|
31
|
+
"name": "$inputs.name"
|
|
32
|
+
},
|
|
33
|
+
"effects": [
|
|
34
|
+
"farewellCreated"
|
|
35
|
+
],
|
|
36
|
+
"binding": {
|
|
37
|
+
"protocol": "node",
|
|
38
|
+
"module": "./capabilities/farewell.js",
|
|
39
|
+
"function": "createFarewell",
|
|
40
|
+
"outputs": {
|
|
41
|
+
"message": {
|
|
42
|
+
"from": "result",
|
|
43
|
+
"path": "message"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/grail.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { loadAffordanceRegistry } from './affordanceRegistry.js';
|
|
2
|
+
import { WorldState } from './worldState.js';
|
|
3
|
+
import { ObservationStore } from './observationStore.js';
|
|
4
|
+
import { Server } from './server.js';
|
|
5
|
+
import { Client } from './client.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Public facade for the GRAIL runtime.
|
|
9
|
+
*
|
|
10
|
+
* This class deliberately composes the existing runtime components rather
|
|
11
|
+
* than replacing them. Configuration loading and validation remain concerns
|
|
12
|
+
* of the calling application.
|
|
13
|
+
*/
|
|
14
|
+
export class Grail {
|
|
15
|
+
constructor({ registry, worldstate, inputs = {}, observationPath, baseDir = process.cwd() }) {
|
|
16
|
+
if (!registry) {
|
|
17
|
+
throw new Error('Grail requires a registry.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!worldstate) {
|
|
21
|
+
throw new Error('Grail requires worldstate.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.inputs = inputs;
|
|
25
|
+
this.affordanceRegistry = loadAffordanceRegistry(registry);
|
|
26
|
+
this.initialWorldstate = { ...worldstate };
|
|
27
|
+
this.observationPath = observationPath;
|
|
28
|
+
this.baseDir = baseDir;
|
|
29
|
+
this.resetPursuit();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
resetPursuit() {
|
|
33
|
+
this.worldState = new WorldState(this.affordanceRegistry, this.initialWorldstate);
|
|
34
|
+
this.observationStore = new ObservationStore(this.observationPath);
|
|
35
|
+
this.server = new Server(
|
|
36
|
+
this.worldState,
|
|
37
|
+
this.affordanceRegistry,
|
|
38
|
+
this.observationStore,
|
|
39
|
+
this.baseDir
|
|
40
|
+
);
|
|
41
|
+
this.client = new Client(this.server, this.inputs);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async pursue(goal) {
|
|
45
|
+
if (!goal) {
|
|
46
|
+
throw new Error('Grail.pursue requires a goal.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.resetPursuit();
|
|
50
|
+
await this.client.pursue(goal);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
goal,
|
|
54
|
+
reached: this.worldState.isPreconditionMet(goal),
|
|
55
|
+
worldstate: { ...this.worldState.state },
|
|
56
|
+
observations: [...this.observationStore.observations]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export class ObservationStore {
|
|
4
|
+
constructor(filePath) {
|
|
5
|
+
this.filePath = filePath;
|
|
6
|
+
this.observations = [];
|
|
7
|
+
this.invocationCounter = 0;
|
|
8
|
+
this.persist();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
nextInvocationId() {
|
|
12
|
+
this.invocationCounter += 1;
|
|
13
|
+
return `inv-${String(this.invocationCounter).padStart(3, "0")}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
append(observation) {
|
|
17
|
+
this.observations.push(observation);
|
|
18
|
+
this.persist();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getByAffordance(affordance) {
|
|
22
|
+
return this.observations.filter(
|
|
23
|
+
observation => observation.invocation.affordance === affordance
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resolve(source) {
|
|
28
|
+
const prefix = "$outputs.";
|
|
29
|
+
|
|
30
|
+
if (!source.startsWith(prefix)) {
|
|
31
|
+
return { resolved: false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const reference = source.slice(prefix.length);
|
|
35
|
+
const parts = reference.split(".");
|
|
36
|
+
|
|
37
|
+
// Scenario-level:
|
|
38
|
+
// $outputs.latest.<outputName>
|
|
39
|
+
if (parts.length === 2) {
|
|
40
|
+
const [selector, outputName] = parts;
|
|
41
|
+
|
|
42
|
+
if (selector !== "latest" || !outputName) {
|
|
43
|
+
return { resolved: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (let i = this.observations.length - 1; i >= 0; i--) {
|
|
47
|
+
const observation = this.observations[i];
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
observation.outputs &&
|
|
51
|
+
Object.prototype.hasOwnProperty.call(
|
|
52
|
+
observation.outputs,
|
|
53
|
+
outputName
|
|
54
|
+
)
|
|
55
|
+
) {
|
|
56
|
+
return {
|
|
57
|
+
resolved: true,
|
|
58
|
+
value: observation.outputs[outputName]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { resolved: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (parts.length !== 3) {
|
|
67
|
+
return { resolved: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [affordance, selector, outputName] = parts;
|
|
71
|
+
|
|
72
|
+
if (!affordance || selector !== "latest" || !outputName) {
|
|
73
|
+
return { resolved: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const matching = this.observations.filter(
|
|
77
|
+
observation => observation.invocation.affordance === affordance
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (matching.length === 0) {
|
|
81
|
+
return { resolved: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const observation = matching[matching.length - 1];
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
!observation.outputs ||
|
|
88
|
+
!Object.prototype.hasOwnProperty.call(observation.outputs, outputName)
|
|
89
|
+
) {
|
|
90
|
+
return { resolved: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
resolved: true,
|
|
95
|
+
value: observation.outputs[outputName]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
persist() {
|
|
100
|
+
if (!this.filePath) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
this.filePath,
|
|
106
|
+
`${JSON.stringify(this.observations, null, 2)}\n`,
|
|
107
|
+
"utf8"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|