@sprig-and-prose/sprig-scenes 0.1.1
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/CHANGELOG.md +7 -0
- package/biome.json +23 -0
- package/docs/projection-expectations.md +188 -0
- package/example/players.example.js +21 -0
- package/examples/expectations-demo.js +146 -0
- package/package.json +36 -0
- package/scripts/build-canonical.js +29 -0
- package/src/api/PortalHandle.js +22 -0
- package/src/api/SceneCompiler.js +24 -0
- package/src/api/SceneInstance.js +50 -0
- package/src/api/SceneRuntime.js +14 -0
- package/src/api/formatUnknownMemberError.js +18 -0
- package/src/attestations/attestations.js +65 -0
- package/src/attestations/compute_status.js +101 -0
- package/src/canonical/actor.scene.json +20 -0
- package/src/canonical/actor.scene.prose +8 -0
- package/src/canonical/actors.scene.json +74 -0
- package/src/canonical/actors.scene.prose +39 -0
- package/src/canonical/tools.scene.json +198 -0
- package/src/canonical/tools.scene.prose +92 -0
- package/src/compiler/compile_scene.js +1885 -0
- package/src/compiler/compile_scene_file.js +15 -0
- package/src/index.js +41 -0
- package/src/manifest/scene_manifest.d.ts +98 -0
- package/src/manifest/scene_manifest.js +84 -0
- package/test/api.test.js +110 -0
- package/test/compile_scene.test.js +311 -0
- package/test/expectations.test.js +508 -0
- package/test/fixtures/expectations-basic.attestations.json +33 -0
- package/test/fixtures/expectations-basic.scene.prose +79 -0
- package/test/fixtures/players.scene.prose +24 -0
- package/test/fixtures/skills-many-some.scene.prose +53 -0
- package/test/fixtures/skills-many.scene.prose +71 -0
- package/test/fixtures/skills.scene.prose +85 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview File wrapper for scene compilation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { compileSceneFromText } from './compile_scene.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} path
|
|
10
|
+
* @returns {Promise<import('../manifest/scene_manifest.js').SceneManifest>}
|
|
11
|
+
*/
|
|
12
|
+
export async function compileSceneFile(path) {
|
|
13
|
+
const sourceText = await readFile(path, 'utf-8');
|
|
14
|
+
return compileSceneFromText(sourceText);
|
|
15
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Scenes library API: compileFromPath / compileFromString return SceneInstance (portal handles + derived functions).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { compileFromPath as compileFromPathImpl, compileFromString as compileFromStringImpl } from './api/SceneCompiler.js';
|
|
6
|
+
import { createRuntime } from './api/SceneRuntime.js';
|
|
7
|
+
import { createSceneInstance } from './api/SceneInstance.js';
|
|
8
|
+
|
|
9
|
+
// Export attestation modules
|
|
10
|
+
export { loadAttestations, saveAttestations } from './attestations/attestations.js';
|
|
11
|
+
export { computeProjectionStatus } from './attestations/compute_status.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compile a scene from a file path. Returns a SceneInstance (Proxy).
|
|
15
|
+
* @param {string} path
|
|
16
|
+
* @returns {Promise<Proxy>}
|
|
17
|
+
*/
|
|
18
|
+
async function compileFromPath(path) {
|
|
19
|
+
const manifest = await compileFromPathImpl(path);
|
|
20
|
+
const runtime = createRuntime(manifest);
|
|
21
|
+
return createSceneInstance(manifest, runtime);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compile a scene from source text. Returns a SceneInstance (Proxy).
|
|
26
|
+
* @param {string} source
|
|
27
|
+
* @returns {Proxy}
|
|
28
|
+
*/
|
|
29
|
+
function compileFromString(source) {
|
|
30
|
+
const manifest = compileFromStringImpl(source);
|
|
31
|
+
const runtime = createRuntime(manifest);
|
|
32
|
+
return createSceneInstance(manifest, runtime);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const Scenes = {
|
|
36
|
+
compileFromPath,
|
|
37
|
+
compileFromString,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default Scenes;
|
|
41
|
+
export { compileFromPath, compileFromString };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene manifest types for canonical scene compilation (TypeScript declarations).
|
|
3
|
+
* Matches JSDoc in scene_manifest.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type PrimitiveKind =
|
|
7
|
+
| 'string'
|
|
8
|
+
| 'integer'
|
|
9
|
+
| 'boolean'
|
|
10
|
+
| 'float'
|
|
11
|
+
| 'date'
|
|
12
|
+
| 'route';
|
|
13
|
+
|
|
14
|
+
/** Uniform kind: primitive, object (named fields), or array. */
|
|
15
|
+
export type Kind =
|
|
16
|
+
| { primitive: PrimitiveKind }
|
|
17
|
+
| { object: Record<string, KindRef> }
|
|
18
|
+
| { array: KindRef };
|
|
19
|
+
|
|
20
|
+
/** Reference: full Kind or actor by name. */
|
|
21
|
+
export type KindRef = Kind | { actor: string };
|
|
22
|
+
|
|
23
|
+
/** Portal definition: declares boundary shape (read-only). */
|
|
24
|
+
export interface PortalDef {
|
|
25
|
+
kind: KindRef;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Reference to a prior step (by id), a portal (by name), or another derived block (by name). */
|
|
29
|
+
export type DerivedStepRef = { stepId: string } | { portal: string } | { derived: string };
|
|
30
|
+
|
|
31
|
+
/** Path expression: stored as string, e.g. "ContainerRows[].item.tools[].name". */
|
|
32
|
+
export type PathExpr = string;
|
|
33
|
+
|
|
34
|
+
export type DerivedStep =
|
|
35
|
+
| {
|
|
36
|
+
id: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
type: 'values';
|
|
39
|
+
pathExpr?: PathExpr;
|
|
40
|
+
items?: Array<{ ref: string; expand?: boolean }>;
|
|
41
|
+
}
|
|
42
|
+
| { id: string; name?: string; type: 'count'; from: DerivedStepRef }
|
|
43
|
+
| { id: string; name?: string; type: 'unique'; from: DerivedStepRef }
|
|
44
|
+
| {
|
|
45
|
+
id: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
type: 'sort';
|
|
48
|
+
from: DerivedStepRef;
|
|
49
|
+
order: 'ascending' | 'descending';
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
id: string;
|
|
53
|
+
name?: string;
|
|
54
|
+
type: 'join';
|
|
55
|
+
from: DerivedStepRef;
|
|
56
|
+
separator: string;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
id: string;
|
|
60
|
+
type: 'each';
|
|
61
|
+
witness: string;
|
|
62
|
+
source: { type: 'pathExpr'; value: string };
|
|
63
|
+
shape: { type: 'object'; fields: Record<string, EachShapeField> };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Field in an each step's shape: witness path, one-resolver, or many-resolver. */
|
|
67
|
+
export type EachShapeField =
|
|
68
|
+
| { type: 'witnessPath'; witness: string; path: string[] }
|
|
69
|
+
| {
|
|
70
|
+
type: 'one';
|
|
71
|
+
from: { type: 'pathExpr'; value: string };
|
|
72
|
+
by: string;
|
|
73
|
+
matches: { witness: string; path: string[] };
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
type: 'many';
|
|
77
|
+
from: { type: 'pathExpr'; value: string };
|
|
78
|
+
by: string;
|
|
79
|
+
matches: { witness: string; path: string[] };
|
|
80
|
+
expects?: 'any' | 'some';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export interface DerivedBlock {
|
|
84
|
+
steps: DerivedStep[];
|
|
85
|
+
kind?: KindRef;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ActorDef {
|
|
89
|
+
kind: Kind;
|
|
90
|
+
identity: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SceneManifest {
|
|
94
|
+
sceneName: string;
|
|
95
|
+
actors: Record<string, ActorDef>;
|
|
96
|
+
portals: Record<string, PortalDef>;
|
|
97
|
+
derived: Record<string, DerivedBlock>;
|
|
98
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Scene manifest types for canonical scene compilation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {'string' | 'integer' | 'boolean' | 'float' | 'date' | 'route'} PrimitiveKind
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Uniform kind: primitive, object (named fields), or array.
|
|
11
|
+
* @typedef {{ primitive: PrimitiveKind } | { object: Record<string, KindRef> } | { array: KindRef }} Kind
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reference: full Kind or actor by name.
|
|
16
|
+
* @typedef {Kind | { actor: string }} KindRef
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Portal definition: declares boundary shape (read-only).
|
|
21
|
+
* @typedef {{ kind: KindRef }} PortalDef
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reference to a prior step (by id), a portal (by name), or another derived block (by name).
|
|
26
|
+
* @typedef {{ stepId: string } | { portal: string } | { derived: string }} DerivedStepRef
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Path expression: stored as string.
|
|
31
|
+
* @typedef {string} PathExpr
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {{ id: string, name?: string, type: 'values', pathExpr?: PathExpr, items?: Array<{ ref: string, expand?: boolean }> }} DerivedStepValues
|
|
36
|
+
* @typedef {{ id: string, name?: string, type: 'count', from: DerivedStepRef }} DerivedStepCount
|
|
37
|
+
* @typedef {{ id: string, name?: string, type: 'unique', from: DerivedStepRef }} DerivedStepUnique
|
|
38
|
+
* @typedef {{ id: string, name?: string, type: 'sort', from: DerivedStepRef, order: 'ascending' | 'descending' }} DerivedStepSort
|
|
39
|
+
* @typedef {{ id: string, name?: string, type: 'join', from: DerivedStepRef, separator: string }} DerivedStepJoin
|
|
40
|
+
* @typedef {{ id: string, type: 'each', witness: string, source: { type: 'pathExpr', value: string }, shape: { type: 'object', fields: Record<string, EachShapeField> } }} DerivedStepEach
|
|
41
|
+
* @typedef {DerivedStepValues | DerivedStepCount | DerivedStepUnique | DerivedStepSort | DerivedStepJoin | DerivedStepEach} DerivedStep
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{ type: 'witnessPath', witness: string, path: string[] } | { type: 'one', from: { type: 'pathExpr', value: string }, by: string, matches: { witness: string, path: string[] } } | { type: 'many', from: { type: 'pathExpr', value: string }, by: string, matches: { witness: string, path: string[] }, expects?: 'any' | 'some' }} EachShapeField
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} DerivedBlock
|
|
50
|
+
* @property {DerivedStep[]} steps
|
|
51
|
+
* @property {KindRef} [kind]
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Location definition: declares a projection target.
|
|
56
|
+
* @typedef {Object} LocationDef
|
|
57
|
+
* @property {string} kind
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Projection expectation: declares where an actor is expected to be projected.
|
|
62
|
+
* @typedef {Object} ProjectionExpectation
|
|
63
|
+
* @property {string} location
|
|
64
|
+
* @property {'table' | 'schema' | 'dataset'} artifactType
|
|
65
|
+
* @property {string} name
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {Object} ActorDef
|
|
70
|
+
* @property {Kind} kind
|
|
71
|
+
* @property {string[]} identity
|
|
72
|
+
* @property {ProjectionExpectation[]} [expectations]
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {Object} SceneManifest
|
|
77
|
+
* @property {string} sceneName
|
|
78
|
+
* @property {Record<string, ActorDef>} actors
|
|
79
|
+
* @property {Record<string, PortalDef>} portals
|
|
80
|
+
* @property {Record<string, DerivedBlock>} derived
|
|
81
|
+
* @property {Record<string, LocationDef>} [locations]
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
export {};
|
package/test/api.test.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for Scenes library API: compileFromPath, compileFromString, SceneInstance (portal handles + derived functions).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { strictEqual, throws } from 'node:assert/strict';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import Scenes from '../src/index.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
const MINIMAL_PLAYERS_PROSE = `
|
|
15
|
+
scene TestScene {
|
|
16
|
+
actor Player {
|
|
17
|
+
kind {
|
|
18
|
+
id { integer }
|
|
19
|
+
name { string }
|
|
20
|
+
}
|
|
21
|
+
identity { id }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
portal Players {
|
|
25
|
+
kind { [ Player ] }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
derived PlayerCount {
|
|
29
|
+
count { Players }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
derived PlayerNames {
|
|
33
|
+
values { Players[].name }
|
|
34
|
+
unique { values }
|
|
35
|
+
sort { from { unique } order { ascending } }
|
|
36
|
+
join { from { sort } with { ', ' } }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
test('tutorial-style: compileFromString, set portal, call derived', () => {
|
|
42
|
+
const scene = Scenes.compileFromString(MINIMAL_PLAYERS_PROSE);
|
|
43
|
+
scene.Players.set([
|
|
44
|
+
{ id: 1, name: 'John' },
|
|
45
|
+
{ id: 2, name: 'Jane' },
|
|
46
|
+
]);
|
|
47
|
+
strictEqual(scene.PlayerCount(), 2);
|
|
48
|
+
strictEqual(scene.PlayerNames(), 'Jane, John'); // sorted ascending
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('portal set/get: get returns last set value', () => {
|
|
52
|
+
const scene = Scenes.compileFromString(MINIMAL_PLAYERS_PROSE);
|
|
53
|
+
const data = [{ id: 1, name: 'A' }];
|
|
54
|
+
scene.Players.set(data);
|
|
55
|
+
strictEqual(scene.Players.get(), data);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('portal clear: get returns undefined after clear', () => {
|
|
59
|
+
const scene = Scenes.compileFromString(MINIMAL_PLAYERS_PROSE);
|
|
60
|
+
scene.Players.set([{ id: 1, name: 'X' }]);
|
|
61
|
+
scene.Players.clear();
|
|
62
|
+
strictEqual(scene.Players.get(), undefined);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('derived evaluation: PlayerCount returns number', () => {
|
|
66
|
+
const scene = Scenes.compileFromString(MINIMAL_PLAYERS_PROSE);
|
|
67
|
+
scene.Players.set([
|
|
68
|
+
{ id: 1, name: 'a' },
|
|
69
|
+
{ id: 2, name: 'b' },
|
|
70
|
+
]);
|
|
71
|
+
strictEqual(scene.PlayerCount(), 2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('unknown property throws helpful message with known portals and derived', () => {
|
|
75
|
+
const scene = Scenes.compileFromString(MINIMAL_PLAYERS_PROSE);
|
|
76
|
+
throws(
|
|
77
|
+
() => {
|
|
78
|
+
scene.Playerss;
|
|
79
|
+
},
|
|
80
|
+
(err) => {
|
|
81
|
+
strictEqual(err instanceof Error, true);
|
|
82
|
+
strictEqual(err.message.includes('Unknown scene member "Playerss"'), true);
|
|
83
|
+
strictEqual(err.message.includes('Known portals:'), true);
|
|
84
|
+
strictEqual(err.message.includes('Players'), true);
|
|
85
|
+
strictEqual(err.message.includes('Known derived values:'), true);
|
|
86
|
+
strictEqual(err.message.includes('PlayerCount'), true);
|
|
87
|
+
strictEqual(err.message.includes('PlayerNames'), true);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('derived names return callable functions', () => {
|
|
94
|
+
const scene = Scenes.compileFromString(MINIMAL_PLAYERS_PROSE);
|
|
95
|
+
strictEqual(typeof scene.PlayerCount, 'function');
|
|
96
|
+
strictEqual(typeof scene.PlayerNames, 'function');
|
|
97
|
+
scene.Players.set([]);
|
|
98
|
+
strictEqual(scene.PlayerCount(), 0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('compileFromPath: load fixture and evaluate', async () => {
|
|
102
|
+
const path = join(__dirname, 'fixtures', 'players.scene.prose');
|
|
103
|
+
const scene = await Scenes.compileFromPath(path);
|
|
104
|
+
scene.Players.set([
|
|
105
|
+
{ id: 1, name: 'Alice' },
|
|
106
|
+
{ id: 2, name: 'Bob' },
|
|
107
|
+
]);
|
|
108
|
+
strictEqual(scene.PlayerCount(), 2);
|
|
109
|
+
strictEqual(scene.PlayerNames(), 'Alice, Bob');
|
|
110
|
+
});
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for scene compilation (actor, portal, derived only)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { deepStrictEqual, strictEqual, throws } from 'node:assert/strict';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { compileSceneFromText } from '../src/compiler/compile_scene.js';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
test('compiles canonical actor scene', async () => {
|
|
16
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'actor.scene.prose');
|
|
17
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
18
|
+
const manifest = compileSceneFromText(sourceText);
|
|
19
|
+
|
|
20
|
+
const jsonFile = join(
|
|
21
|
+
__dirname,
|
|
22
|
+
'..',
|
|
23
|
+
'src',
|
|
24
|
+
'canonical',
|
|
25
|
+
'actor.scene.json',
|
|
26
|
+
);
|
|
27
|
+
const expectedText = await readFile(jsonFile, 'utf-8');
|
|
28
|
+
const expected = JSON.parse(expectedText);
|
|
29
|
+
|
|
30
|
+
deepStrictEqual(JSON.parse(JSON.stringify(manifest)), expected);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('block comments do not affect manifest output', async () => {
|
|
34
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'actor.scene.prose');
|
|
35
|
+
const baseSource = await readFile(file, 'utf-8');
|
|
36
|
+
const withComments = baseSource
|
|
37
|
+
.replace('scene LeftPanel {', 'scene LeftPanel { {* scene def *}')
|
|
38
|
+
.replace('actor Player {', 'actor Player { {* player actor *}');
|
|
39
|
+
const manifest = compileSceneFromText(withComments);
|
|
40
|
+
|
|
41
|
+
const jsonFile = join(
|
|
42
|
+
__dirname,
|
|
43
|
+
'..',
|
|
44
|
+
'src',
|
|
45
|
+
'canonical',
|
|
46
|
+
'actor.scene.json',
|
|
47
|
+
);
|
|
48
|
+
const expectedText = await readFile(jsonFile, 'utf-8');
|
|
49
|
+
const expected = JSON.parse(expectedText);
|
|
50
|
+
|
|
51
|
+
deepStrictEqual(JSON.parse(JSON.stringify(manifest)), expected);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('compiles canonical actors scene', async () => {
|
|
55
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'actors.scene.prose');
|
|
56
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
57
|
+
const manifest = compileSceneFromText(sourceText);
|
|
58
|
+
|
|
59
|
+
const jsonFile = join(
|
|
60
|
+
__dirname,
|
|
61
|
+
'..',
|
|
62
|
+
'src',
|
|
63
|
+
'canonical',
|
|
64
|
+
'actors.scene.json',
|
|
65
|
+
);
|
|
66
|
+
const expectedText = await readFile(jsonFile, 'utf-8');
|
|
67
|
+
const expected = JSON.parse(expectedText);
|
|
68
|
+
|
|
69
|
+
deepStrictEqual(JSON.parse(JSON.stringify(manifest)), expected);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('compiles canonical tools scene', async () => {
|
|
73
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'tools.scene.prose');
|
|
74
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
75
|
+
const manifest = compileSceneFromText(sourceText);
|
|
76
|
+
|
|
77
|
+
const jsonFile = join(
|
|
78
|
+
__dirname,
|
|
79
|
+
'..',
|
|
80
|
+
'src',
|
|
81
|
+
'canonical',
|
|
82
|
+
'tools.scene.json',
|
|
83
|
+
);
|
|
84
|
+
const expectedText = await readFile(jsonFile, 'utf-8');
|
|
85
|
+
const expected = JSON.parse(expectedText);
|
|
86
|
+
|
|
87
|
+
deepStrictEqual(JSON.parse(JSON.stringify(manifest)), expected);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('tools scene: portals have array kind', async () => {
|
|
91
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'tools.scene.prose');
|
|
92
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
93
|
+
const manifest = compileSceneFromText(sourceText);
|
|
94
|
+
|
|
95
|
+
strictEqual(!!manifest.portals?.ContainerRows, true);
|
|
96
|
+
deepStrictEqual(manifest.portals.ContainerRows.kind, {
|
|
97
|
+
array: { actor: 'ContainerRow' },
|
|
98
|
+
});
|
|
99
|
+
strictEqual(!!manifest.portals?.AnotherToolPortal, true);
|
|
100
|
+
deepStrictEqual(manifest.portals.AnotherToolPortal.kind, {
|
|
101
|
+
array: { actor: 'Something' },
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('tools scene: derived RowCount has count from portal', async () => {
|
|
106
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'tools.scene.prose');
|
|
107
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
108
|
+
const manifest = compileSceneFromText(sourceText);
|
|
109
|
+
|
|
110
|
+
const rowCount = manifest.derived?.RowCount;
|
|
111
|
+
strictEqual(!!rowCount, true);
|
|
112
|
+
const steps = rowCount?.steps ?? [];
|
|
113
|
+
strictEqual(steps.length, 1);
|
|
114
|
+
strictEqual(steps[0]?.type, 'count');
|
|
115
|
+
deepStrictEqual(steps[0]?.from, { portal: 'ContainerRows' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('tools scene: derived ToolNames has values, unique, sort in order', async () => {
|
|
119
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'tools.scene.prose');
|
|
120
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
121
|
+
const manifest = compileSceneFromText(sourceText);
|
|
122
|
+
|
|
123
|
+
const toolNames = manifest.derived?.ToolNames;
|
|
124
|
+
strictEqual(!!toolNames, true);
|
|
125
|
+
const steps = toolNames?.steps ?? [];
|
|
126
|
+
strictEqual(steps.length, 3);
|
|
127
|
+
strictEqual(steps[0]?.type, 'values');
|
|
128
|
+
strictEqual(steps[1]?.type, 'unique');
|
|
129
|
+
strictEqual(steps[2]?.type, 'sort');
|
|
130
|
+
strictEqual(steps[0]?.id, 's1');
|
|
131
|
+
strictEqual(steps[1]?.id, 's2');
|
|
132
|
+
strictEqual(steps[2]?.id, 's3');
|
|
133
|
+
deepStrictEqual(steps[1]?.from, { stepId: 's1' });
|
|
134
|
+
deepStrictEqual(steps[2]?.from, { stepId: 's2' });
|
|
135
|
+
strictEqual(steps[2]?.order, 'ascending');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('tools scene: derived ToolCount has count from derived ToolNames', async () => {
|
|
139
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'tools.scene.prose');
|
|
140
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
141
|
+
const manifest = compileSceneFromText(sourceText);
|
|
142
|
+
|
|
143
|
+
const toolCount = manifest.derived?.ToolCount;
|
|
144
|
+
strictEqual(!!toolCount, true);
|
|
145
|
+
const steps = toolCount?.steps ?? [];
|
|
146
|
+
strictEqual(steps.length, 1);
|
|
147
|
+
strictEqual(steps[0]?.type, 'count');
|
|
148
|
+
deepStrictEqual(steps[0]?.from, { derived: 'ToolNames' });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('tools scene: derived ToolString1 has join step with separator', async () => {
|
|
152
|
+
const file = join(__dirname, '..', 'src', 'canonical', 'tools.scene.prose');
|
|
153
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
154
|
+
const manifest = compileSceneFromText(sourceText);
|
|
155
|
+
|
|
156
|
+
const toolString = manifest.derived?.ToolString1;
|
|
157
|
+
strictEqual(!!toolString, true);
|
|
158
|
+
const joinStep = toolString?.steps?.find((s) => s.type === 'join');
|
|
159
|
+
strictEqual(!!joinStep, true);
|
|
160
|
+
strictEqual(joinStep?.separator, ', ');
|
|
161
|
+
deepStrictEqual(joinStep?.from, { derived: 'ToolNames' });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('grouped actors: produces same manifest as singular', () => {
|
|
165
|
+
const grouped = `
|
|
166
|
+
scene G {
|
|
167
|
+
actors {
|
|
168
|
+
A {
|
|
169
|
+
kind { id { integer } }
|
|
170
|
+
identity { id }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
const singular = `
|
|
176
|
+
scene G {
|
|
177
|
+
actor A {
|
|
178
|
+
kind { id { integer } }
|
|
179
|
+
identity { id }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
`;
|
|
183
|
+
const manifestGrouped = compileSceneFromText(grouped);
|
|
184
|
+
const manifestSingular = compileSceneFromText(singular);
|
|
185
|
+
deepStrictEqual(manifestGrouped.actors?.A, manifestSingular.actors?.A);
|
|
186
|
+
strictEqual(manifestGrouped.sceneName, 'G');
|
|
187
|
+
strictEqual(Object.keys(manifestGrouped.actors ?? {}).length, 1);
|
|
188
|
+
deepStrictEqual(manifestGrouped.portals, {});
|
|
189
|
+
deepStrictEqual(manifestGrouped.derived, {});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('grouped portals: produces same manifest as singular', () => {
|
|
193
|
+
const grouped = `
|
|
194
|
+
scene G {
|
|
195
|
+
actor R { kind { id { integer } } identity { id } }
|
|
196
|
+
portals {
|
|
197
|
+
P { kind { [ R ] } }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
const singular = `
|
|
202
|
+
scene G {
|
|
203
|
+
actor R { kind { id { integer } } identity { id } }
|
|
204
|
+
portal P { kind { [ R ] } }
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
const manifestGrouped = compileSceneFromText(grouped);
|
|
208
|
+
const manifestSingular = compileSceneFromText(singular);
|
|
209
|
+
deepStrictEqual(manifestGrouped.portals?.P, manifestSingular.portals?.P);
|
|
210
|
+
strictEqual(Object.keys(manifestGrouped.portals ?? {}).length, 1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('grouped derived: singular derived C is in manifest', () => {
|
|
214
|
+
const source = `
|
|
215
|
+
scene G {
|
|
216
|
+
actor R { kind { id { integer } } identity { id } }
|
|
217
|
+
portal X { kind { [ R ] } }
|
|
218
|
+
derived C { count { X } }
|
|
219
|
+
}
|
|
220
|
+
`;
|
|
221
|
+
const manifest = compileSceneFromText(source);
|
|
222
|
+
strictEqual(!!manifest.derived?.C, true);
|
|
223
|
+
strictEqual(manifest.derived?.C?.steps?.length, 1);
|
|
224
|
+
strictEqual(manifest.derived?.C?.steps?.[0]?.type, 'count');
|
|
225
|
+
deepStrictEqual(manifest.derived?.C?.steps?.[0]?.from, { portal: 'X' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('mixed singular and grouped: actors and portals and derived appear in manifest', () => {
|
|
229
|
+
const source = `
|
|
230
|
+
scene M {
|
|
231
|
+
actor X { kind { id { integer } } identity { id } }
|
|
232
|
+
actors {
|
|
233
|
+
Y { kind { id { integer } } identity { id } }
|
|
234
|
+
}
|
|
235
|
+
portal P1 { kind { [ X ] } }
|
|
236
|
+
portals {
|
|
237
|
+
P2 { kind { [ Y ] } }
|
|
238
|
+
}
|
|
239
|
+
derived D1 { count { P1 } }
|
|
240
|
+
derived D2 { count { P2 } }
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
const manifest = compileSceneFromText(source);
|
|
244
|
+
strictEqual(manifest.sceneName, 'M');
|
|
245
|
+
strictEqual(!!manifest.actors?.X, true);
|
|
246
|
+
strictEqual(!!manifest.actors?.Y, true);
|
|
247
|
+
strictEqual(!!manifest.portals?.P1, true);
|
|
248
|
+
strictEqual(!!manifest.portals?.P2, true);
|
|
249
|
+
strictEqual(!!manifest.derived?.D1, true);
|
|
250
|
+
strictEqual(!!manifest.derived?.D2, true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('skills scene: derived PlayerSkills has each step with expected shape', async () => {
|
|
254
|
+
const file = join(__dirname, 'fixtures', 'skills.scene.prose');
|
|
255
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
256
|
+
const manifest = compileSceneFromText(sourceText);
|
|
257
|
+
|
|
258
|
+
const playerSkills = manifest.derived?.PlayerSkills;
|
|
259
|
+
strictEqual(!!playerSkills, true);
|
|
260
|
+
strictEqual(playerSkills?.kind?.array?.actor, 'PlayerSkill');
|
|
261
|
+
const steps = playerSkills?.steps ?? [];
|
|
262
|
+
strictEqual(steps.length, 1);
|
|
263
|
+
strictEqual(steps[0]?.type, 'each');
|
|
264
|
+
strictEqual(steps[0]?.witness, 'row');
|
|
265
|
+
deepStrictEqual(steps[0]?.source, { type: 'pathExpr', value: 'RawPlayerSkills[]' });
|
|
266
|
+
const fields = steps[0]?.shape?.fields ?? {};
|
|
267
|
+
strictEqual(fields.player?.type, 'one');
|
|
268
|
+
strictEqual(fields.skill?.type, 'one');
|
|
269
|
+
strictEqual(fields.experience?.type, 'witnessPath');
|
|
270
|
+
deepStrictEqual(fields.player?.matches, { witness: 'row', path: ['playerId'] });
|
|
271
|
+
deepStrictEqual(fields.skill?.matches, { witness: 'row', path: ['skillId'] });
|
|
272
|
+
deepStrictEqual(fields.experience?.path, ['experience']);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('skills-many scene: parses many block with from, matches, by', async () => {
|
|
276
|
+
const file = join(__dirname, 'fixtures', 'skills-many.scene.prose');
|
|
277
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
278
|
+
const manifest = compileSceneFromText(sourceText);
|
|
279
|
+
|
|
280
|
+
const block = manifest.derived?.PlayerSkillMatches;
|
|
281
|
+
strictEqual(!!block, true);
|
|
282
|
+
const steps = block?.steps ?? [];
|
|
283
|
+
strictEqual(steps.length, 1);
|
|
284
|
+
const fields = steps[0]?.shape?.fields ?? {};
|
|
285
|
+
strictEqual(fields.skillMatches?.type, 'many');
|
|
286
|
+
deepStrictEqual(fields.skillMatches?.from, { type: 'pathExpr', value: 'Skills[]' });
|
|
287
|
+
strictEqual(fields.skillMatches?.by, 'id');
|
|
288
|
+
deepStrictEqual(fields.skillMatches?.matches, { witness: 'row', path: ['skillId'] });
|
|
289
|
+
strictEqual(fields.skillMatches?.expects, undefined);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('skills-many-some scene: parses many with expects some', async () => {
|
|
293
|
+
const file = join(__dirname, 'fixtures', 'skills-many-some.scene.prose');
|
|
294
|
+
const sourceText = await readFile(file, 'utf-8');
|
|
295
|
+
const manifest = compileSceneFromText(sourceText);
|
|
296
|
+
|
|
297
|
+
const block = manifest.derived?.PlayerSkillMatchesSome;
|
|
298
|
+
strictEqual(!!block, true);
|
|
299
|
+
const fields = block?.steps?.[0]?.shape?.fields ?? {};
|
|
300
|
+
strictEqual(fields.skillMatches?.type, 'many');
|
|
301
|
+
strictEqual(fields.skillMatches?.expects, 'some');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('empty scene returns empty manifest shape', () => {
|
|
305
|
+
const source = 'not a scene';
|
|
306
|
+
const manifest = compileSceneFromText(source);
|
|
307
|
+
strictEqual(manifest.sceneName, '');
|
|
308
|
+
deepStrictEqual(manifest.actors, {});
|
|
309
|
+
deepStrictEqual(manifest.portals, {});
|
|
310
|
+
deepStrictEqual(manifest.derived, {});
|
|
311
|
+
});
|