@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
package/CHANGELOG.md
ADDED
package/biome.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"organizeImports": {
|
|
4
|
+
"enabled": true
|
|
5
|
+
},
|
|
6
|
+
"linter": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"rules": {
|
|
9
|
+
"recommended": true
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2
|
|
16
|
+
},
|
|
17
|
+
"javascript": {
|
|
18
|
+
"formatter": {
|
|
19
|
+
"quoteStyle": "single",
|
|
20
|
+
"semicolons": "always"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Projection Expectations
|
|
2
|
+
|
|
3
|
+
A metadata layer for declaring and tracking where actors are expected to be projected to external systems.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Projection Expectations provide a **TDD-style approach for system boundaries**. During planning, you declare where actors should be projected (e.g., to Postgres tables, Snowflake warehouses, or OpenAPI schemas). Later, attestations verify whether those projections actually exist.
|
|
8
|
+
|
|
9
|
+
This is metadata-only. No live database connections or verification logic is implemented in v0.
|
|
10
|
+
|
|
11
|
+
## Syntax
|
|
12
|
+
|
|
13
|
+
### 1. Declare Locations (Top-Level)
|
|
14
|
+
|
|
15
|
+
```sprig
|
|
16
|
+
locations {
|
|
17
|
+
TeamPostgres {
|
|
18
|
+
kind { 'postgres' }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Analytics {
|
|
22
|
+
kind { 'snowflake' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
PublicAPI {
|
|
26
|
+
kind { 'openapi' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Location kinds are arbitrary strings:**
|
|
32
|
+
- Use any string literal that describes your datastore
|
|
33
|
+
- Examples: `'postgres'`, `'snowflake'`, `'openapi'`, `'fhir.r4'`, `'legacy-mainframe'`
|
|
34
|
+
- No hardcoded validation - sprig is in the truth business, not the integration catalog business
|
|
35
|
+
|
|
36
|
+
### 2. Declare Expectations (Actor-Level)
|
|
37
|
+
|
|
38
|
+
```sprig
|
|
39
|
+
actor PlayerSkill {
|
|
40
|
+
kind {
|
|
41
|
+
player { Player }
|
|
42
|
+
skill { Skill }
|
|
43
|
+
experience { integer }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
identity { player skill }
|
|
47
|
+
|
|
48
|
+
expects {
|
|
49
|
+
project {
|
|
50
|
+
to { TeamPostgres }
|
|
51
|
+
as { table { player_skills } }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
project {
|
|
55
|
+
to { PublicAPI }
|
|
56
|
+
as { schema { PlayerSkill } }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Supported artifact types:**
|
|
63
|
+
- `table { name }` - Database table
|
|
64
|
+
- `schema { name }` - API schema
|
|
65
|
+
|
|
66
|
+
## Attestations
|
|
67
|
+
|
|
68
|
+
Attestations are stored in `.sprig/SCENENAME.attestations.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"version": 1,
|
|
73
|
+
"actors": {
|
|
74
|
+
"PlayerSkill": [
|
|
75
|
+
{
|
|
76
|
+
"location": "TeamPostgres",
|
|
77
|
+
"artifactType": "table",
|
|
78
|
+
"name": "player_skills",
|
|
79
|
+
"status": "present",
|
|
80
|
+
"observedAt": "2026-02-13T10:00:00Z",
|
|
81
|
+
"shapeHash": "sha256:abc123"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Status Computation
|
|
89
|
+
|
|
90
|
+
Use `computeProjectionStatus()` to compare expectations with attestations:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import { compileSceneFromText } from '@sprig-and-prose/sprig-scenes';
|
|
94
|
+
import { loadAttestations, computeProjectionStatus } from '@sprig-and-prose/sprig-scenes';
|
|
95
|
+
|
|
96
|
+
const manifest = compileSceneFromText(sceneSource);
|
|
97
|
+
const attestations = await loadAttestations('.sprig/MyScene.attestations.json');
|
|
98
|
+
|
|
99
|
+
const status = computeProjectionStatus(manifest, attestations);
|
|
100
|
+
|
|
101
|
+
// Result:
|
|
102
|
+
// {
|
|
103
|
+
// actors: {
|
|
104
|
+
// PlayerSkill: [
|
|
105
|
+
// {
|
|
106
|
+
// location: "TeamPostgres",
|
|
107
|
+
// artifactType: "table",
|
|
108
|
+
// name: "player_skills",
|
|
109
|
+
// status: "attested", // or "expected" or "failed"
|
|
110
|
+
// expectation: { ... },
|
|
111
|
+
// attestation: { ... } // present if attested/failed
|
|
112
|
+
// }
|
|
113
|
+
// ]
|
|
114
|
+
// }
|
|
115
|
+
// }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Status Values
|
|
119
|
+
|
|
120
|
+
- **`expected`** (grey) - Projection declared but not yet attested
|
|
121
|
+
- **`attested`** (green) - Attestation exists with status "present"
|
|
122
|
+
- **`failed`** (red) - Attestation exists but status is not "present"
|
|
123
|
+
- **`drifted`** (orange) - Reserved for future shape comparison (not implemented)
|
|
124
|
+
|
|
125
|
+
## API
|
|
126
|
+
|
|
127
|
+
### Compilation
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
import { compileSceneFromText } from '@sprig-and-prose/sprig-scenes/compiler';
|
|
131
|
+
|
|
132
|
+
const manifest = compileSceneFromText(sourceCode);
|
|
133
|
+
// manifest.locations → { LocationName: { kind: string } }
|
|
134
|
+
// manifest.actors.ActorName.expectations → ProjectionExpectation[]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Attestations
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
import { loadAttestations, saveAttestations } from '@sprig-and-prose/sprig-scenes/attestations';
|
|
141
|
+
|
|
142
|
+
// Load (returns empty structure if file doesn't exist)
|
|
143
|
+
const attestations = await loadAttestations('path/to/file.json');
|
|
144
|
+
|
|
145
|
+
// Save
|
|
146
|
+
await saveAttestations('path/to/file.json', attestations);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Status Computation
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
import { computeProjectionStatus } from '@sprig-and-prose/sprig-scenes/attestations/compute-status';
|
|
153
|
+
|
|
154
|
+
const status = computeProjectionStatus(manifest, attestations);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Validation
|
|
158
|
+
|
|
159
|
+
The compiler validates:
|
|
160
|
+
- Location names are unique
|
|
161
|
+
- Location kinds are string literals (any string is valid)
|
|
162
|
+
- Artifact types are valid (`table`, `schema`)
|
|
163
|
+
- Expectation locations reference declared locations
|
|
164
|
+
|
|
165
|
+
Invalid expectations throw errors during compilation.
|
|
166
|
+
|
|
167
|
+
## Example
|
|
168
|
+
|
|
169
|
+
See `examples/expectations-demo.js` for a complete working example.
|
|
170
|
+
|
|
171
|
+
## Future Work (Not Implemented in v0)
|
|
172
|
+
|
|
173
|
+
- Live database verification
|
|
174
|
+
- Shape diffing and migration detection (status: "drifted")
|
|
175
|
+
- Automated attestation generation
|
|
176
|
+
- Additional artifact types (views, indices, etc.)
|
|
177
|
+
|
|
178
|
+
Note: Location kinds are intentionally unrestricted - sprig stays in the truth business, not the integration catalog business.
|
|
179
|
+
|
|
180
|
+
## Philosophy
|
|
181
|
+
|
|
182
|
+
**Expectations are declarations, not implementations.**
|
|
183
|
+
|
|
184
|
+
They specify *what should exist* without prescribing *how to create it*. This separation allows:
|
|
185
|
+
- Early planning without infrastructure
|
|
186
|
+
- UI visualization of projection status
|
|
187
|
+
- Future integration with verification tools
|
|
188
|
+
- Clear boundaries between truth layer and projection layer
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tutorial-style example: compile scene, set portal, call derived.
|
|
3
|
+
* Run from repo root: node --import tsx packages/scenes/example/players.example.js
|
|
4
|
+
* Or from packages/scenes: node --import tsx example/players.example.js
|
|
5
|
+
*/
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import Scenes from '../src/index.js';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const scenePath = join(__dirname, '..', 'test', 'fixtures', 'players.scene.prose');
|
|
12
|
+
|
|
13
|
+
const scene = await Scenes.compileFromPath(scenePath);
|
|
14
|
+
|
|
15
|
+
scene.Players.set([
|
|
16
|
+
{ id: 1, name: 'John' },
|
|
17
|
+
{ id: 2, name: 'Jane' },
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
console.log(scene.PlayerCount()); // 2
|
|
21
|
+
console.log(scene.PlayerNames()); // 'Jane, John' (sorted ascending)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Demo: How to use projection expectations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { compileSceneFromText } from '../src/compiler/compile_scene.js';
|
|
6
|
+
import { computeProjectionStatus } from '../src/attestations/compute_status.js';
|
|
7
|
+
|
|
8
|
+
// Example scene with locations and expectations
|
|
9
|
+
const sceneSource = `
|
|
10
|
+
scene DemoScene {
|
|
11
|
+
locations {
|
|
12
|
+
ProductionDB {
|
|
13
|
+
kind { 'postgres' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Analytics {
|
|
17
|
+
kind { 'snowflake' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
PublicAPI {
|
|
21
|
+
kind { 'openapi' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
actors {
|
|
26
|
+
Player {
|
|
27
|
+
kind {
|
|
28
|
+
id { integer }
|
|
29
|
+
name { string }
|
|
30
|
+
email { string }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
identity { id }
|
|
34
|
+
|
|
35
|
+
expects {
|
|
36
|
+
project {
|
|
37
|
+
to { ProductionDB }
|
|
38
|
+
as { table { players } }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
project {
|
|
42
|
+
to { Analytics }
|
|
43
|
+
as { table { dim_players } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
project {
|
|
47
|
+
to { PublicAPI }
|
|
48
|
+
as { schema { Player } }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Game {
|
|
54
|
+
kind {
|
|
55
|
+
id { integer }
|
|
56
|
+
title { string }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
identity { id }
|
|
60
|
+
|
|
61
|
+
expects {
|
|
62
|
+
project {
|
|
63
|
+
to { ProductionDB }
|
|
64
|
+
as { table { games } }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
portals {
|
|
71
|
+
Players {
|
|
72
|
+
kind { [ Player ] }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
// Compile the scene
|
|
79
|
+
console.log('📝 Compiling scene...\n');
|
|
80
|
+
const manifest = compileSceneFromText(sceneSource);
|
|
81
|
+
|
|
82
|
+
console.log('✅ Scene compiled successfully!');
|
|
83
|
+
console.log(` Scene name: ${manifest.sceneName}`);
|
|
84
|
+
console.log(` Locations: ${Object.keys(manifest.locations).join(', ')}`);
|
|
85
|
+
console.log(` Actors: ${Object.keys(manifest.actors).join(', ')}\n`);
|
|
86
|
+
|
|
87
|
+
// Show expectations
|
|
88
|
+
console.log('🎯 Projection Expectations:\n');
|
|
89
|
+
for (const [actorName, actorDef] of Object.entries(manifest.actors)) {
|
|
90
|
+
if (actorDef.expectations) {
|
|
91
|
+
console.log(` ${actorName}:`);
|
|
92
|
+
for (const exp of actorDef.expectations) {
|
|
93
|
+
console.log(` → ${exp.location} / ${exp.artifactType} / ${exp.name}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Simulate attestations (some present, some missing)
|
|
99
|
+
const attestations = {
|
|
100
|
+
version: 1,
|
|
101
|
+
actors: {
|
|
102
|
+
Player: [
|
|
103
|
+
{
|
|
104
|
+
location: 'ProductionDB',
|
|
105
|
+
artifactType: 'table',
|
|
106
|
+
name: 'players',
|
|
107
|
+
status: 'present',
|
|
108
|
+
observedAt: '2026-02-13T12:00:00Z',
|
|
109
|
+
shapeHash: 'sha256:abc123',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
location: 'PublicAPI',
|
|
113
|
+
artifactType: 'schema',
|
|
114
|
+
name: 'Player',
|
|
115
|
+
status: 'present',
|
|
116
|
+
observedAt: '2026-02-13T12:00:00Z',
|
|
117
|
+
shapeHash: 'sha256:def456',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
// Note: Analytics dim_players is missing (expected but not verified)
|
|
121
|
+
// Note: Game is completely missing (all expected but not verified)
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Compute status
|
|
126
|
+
console.log('\n📊 Computing projection status...\n');
|
|
127
|
+
const status = computeProjectionStatus(manifest, attestations);
|
|
128
|
+
|
|
129
|
+
// Display results
|
|
130
|
+
console.log('Status Summary:\n');
|
|
131
|
+
for (const [actorName, statuses] of Object.entries(status.actors)) {
|
|
132
|
+
console.log(` ${actorName}:`);
|
|
133
|
+
for (const s of statuses) {
|
|
134
|
+
const icon =
|
|
135
|
+
s.status === 'attested' ? '🟢' : s.status === 'expected' ? '⚪' : '🔴';
|
|
136
|
+
console.log(
|
|
137
|
+
` ${icon} ${s.status.toUpperCase().padEnd(10)} → ${s.location} / ${s.artifactType} / ${s.name}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('\n✨ Legend:');
|
|
143
|
+
console.log(' ⚪ EXPECTED - Projection declared but not yet attested');
|
|
144
|
+
console.log(' 🟢 ATTESTED - Projection exists and is attested');
|
|
145
|
+
console.log(' 🟠 DRIFTED - Shape has changed (not yet implemented)');
|
|
146
|
+
console.log(' 🔴 FAILED - Attestation exists but status is not "present"');
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sprig-and-prose/sprig-scenes",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Scene compiler and canonical outputs for sprig",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./compiler": "./src/compiler/compile_scene_file.js",
|
|
10
|
+
"./compiler/compile_scene": "./src/compiler/compile_scene.js",
|
|
11
|
+
"./manifest": "./src/manifest/scene_manifest.js",
|
|
12
|
+
"./attestations": "./src/attestations/attestations.js",
|
|
13
|
+
"./attestations/compute-status": "./src/attestations/compute_status.js",
|
|
14
|
+
"./canonical/layout": "./src/canonical/layout.scene.json",
|
|
15
|
+
"./canonical/tools": "./src/canonical/tools.scene.json"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"format": "biome format . --write",
|
|
19
|
+
"lint": "biome lint .",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "node --import tsx --test",
|
|
22
|
+
"build:canonical": "node scripts/build-canonical.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@sprig-and-prose/prose-parser": "*",
|
|
29
|
+
"@sprig-and-prose/sprig-scene-engine": "*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@biomejs/biome": "^1.9.4",
|
|
33
|
+
"tsx": "^4.19.2",
|
|
34
|
+
"typescript": "^5.7.2"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Build canonical scene manifest JSON
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdir, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { compileSceneFile } from '../src/compiler/compile_scene_file.js';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const canonicalDir = join(__dirname, '..', 'src', 'canonical');
|
|
14
|
+
|
|
15
|
+
const entries = await readdir(canonicalDir);
|
|
16
|
+
const canonicalFiles = entries
|
|
17
|
+
.filter((entry) => entry.endsWith('.scene.prose'))
|
|
18
|
+
.sort();
|
|
19
|
+
|
|
20
|
+
for (const entry of canonicalFiles) {
|
|
21
|
+
const canonicalSource = join(canonicalDir, entry);
|
|
22
|
+
const outputFile = join(
|
|
23
|
+
canonicalDir,
|
|
24
|
+
entry.replace(/\.scene\.prose$/u, '.scene.json'),
|
|
25
|
+
);
|
|
26
|
+
const manifest = await compileSceneFile(canonicalSource);
|
|
27
|
+
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
28
|
+
await writeFile(outputFile, json, 'utf-8');
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal handle: set/get/clear for one portal, synced to runtime.
|
|
3
|
+
* @param {string} name - Portal name
|
|
4
|
+
* @param {Record<string, unknown>} portalValues - Mirror of portal data (for get())
|
|
5
|
+
* @param {{ setPortalData(name: string, value: unknown): void }} runtime - Engine runtime
|
|
6
|
+
* @returns {{ set(value: unknown): void; get(): unknown; clear(): void }}
|
|
7
|
+
*/
|
|
8
|
+
export function createPortalHandle(name, portalValues, runtime) {
|
|
9
|
+
return {
|
|
10
|
+
set(value) {
|
|
11
|
+
portalValues[name] = value;
|
|
12
|
+
runtime.setPortalData(name, value);
|
|
13
|
+
},
|
|
14
|
+
get() {
|
|
15
|
+
return portalValues[name];
|
|
16
|
+
},
|
|
17
|
+
clear() {
|
|
18
|
+
delete portalValues[name];
|
|
19
|
+
runtime.setPortalData(name, undefined);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps the scene compiler: compileFromPath (async) and compileFromString (sync).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { compileSceneFile } from '../compiler/compile_scene_file.js';
|
|
6
|
+
import { compileSceneFromText } from '../compiler/compile_scene.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compile a scene from a file path.
|
|
10
|
+
* @param {string} path
|
|
11
|
+
* @returns {Promise<import('../manifest/scene_manifest.js').SceneManifest>}
|
|
12
|
+
*/
|
|
13
|
+
export async function compileFromPath(path) {
|
|
14
|
+
return compileSceneFile(path);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compile a scene from source text.
|
|
19
|
+
* @param {string} source
|
|
20
|
+
* @returns {import('../manifest/scene_manifest.js').SceneManifest}
|
|
21
|
+
*/
|
|
22
|
+
export function compileFromString(source) {
|
|
23
|
+
return compileSceneFromText(source);
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SceneInstance: Proxy that returns PortalHandle for portal names, callable for derived names,
|
|
3
|
+
* and throws formatUnknownMemberError for unknown names.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createPortalHandle } from './PortalHandle.js';
|
|
7
|
+
import { formatUnknownMemberError } from './formatUnknownMemberError.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a SceneInstance (Proxy) for the given manifest and runtime.
|
|
11
|
+
* @param {import('../manifest/scene_manifest.js').SceneManifest} manifest
|
|
12
|
+
* @param {{ setPortalData(name: string, value: unknown): void; getDerived(blockName: string): unknown }} runtime
|
|
13
|
+
* @returns {Proxy}
|
|
14
|
+
*/
|
|
15
|
+
export function createSceneInstance(manifest, runtime) {
|
|
16
|
+
const portalValues = /** @type {Record<string, unknown>} */ ({});
|
|
17
|
+
|
|
18
|
+
const portalHandles = /** @type {Record<string, { set(value: unknown): void; get(): unknown; clear(): void }} */ ({});
|
|
19
|
+
function getPortalHandle(name) {
|
|
20
|
+
if (!portalHandles[name]) {
|
|
21
|
+
portalHandles[name] = createPortalHandle(name, portalValues, runtime);
|
|
22
|
+
}
|
|
23
|
+
return portalHandles[name];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const target = {};
|
|
27
|
+
return new Proxy(target, {
|
|
28
|
+
get(_obj, prop) {
|
|
29
|
+
const name = typeof prop === 'string' ? prop : String(prop);
|
|
30
|
+
// Allow then/catch/finally so the Proxy is not treated as thenable (e.g. by test runners).
|
|
31
|
+
if (name === 'then' || name === 'catch' || name === 'finally') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(manifest.portals, name)) {
|
|
35
|
+
return getPortalHandle(name);
|
|
36
|
+
}
|
|
37
|
+
if (Object.prototype.hasOwnProperty.call(manifest.derived, name)) {
|
|
38
|
+
return () => runtime.getDerived(name);
|
|
39
|
+
}
|
|
40
|
+
throw formatUnknownMemberError(name, manifest);
|
|
41
|
+
},
|
|
42
|
+
has(_obj, prop) {
|
|
43
|
+
const name = typeof prop === 'string' ? prop : String(prop);
|
|
44
|
+
return (
|
|
45
|
+
Object.prototype.hasOwnProperty.call(manifest.portals, name) ||
|
|
46
|
+
Object.prototype.hasOwnProperty.call(manifest.derived, name)
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps the scene-engine runtime: given manifest, returns engine's createSceneRuntime(manifest).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createSceneRuntime } from '@sprig-and-prose/sprig-scene-engine';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a runtime for the given manifest.
|
|
9
|
+
* @param {import('../manifest/scene_manifest.js').SceneManifest} manifest
|
|
10
|
+
* @returns {import('@sprig-and-prose/sprig-scene-engine').SceneRuntime}
|
|
11
|
+
*/
|
|
12
|
+
export function createRuntime(manifest) {
|
|
13
|
+
return createSceneRuntime(manifest);
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a helpful error for unknown scene member access.
|
|
3
|
+
* @param {string} memberName
|
|
4
|
+
* @param {{ portals: Record<string, unknown>; derived: Record<string, unknown> }} manifest
|
|
5
|
+
* @returns {Error}
|
|
6
|
+
*/
|
|
7
|
+
export function formatUnknownMemberError(memberName, manifest) {
|
|
8
|
+
const portalNames = Object.keys(manifest.portals ?? {}).sort();
|
|
9
|
+
const derivedNames = Object.keys(manifest.derived ?? {}).sort();
|
|
10
|
+
const portalList = portalNames.length ? portalNames.join(', ') : '(none)';
|
|
11
|
+
const derivedList = derivedNames.length ? derivedNames.join(', ') : '(none)';
|
|
12
|
+
const message = [
|
|
13
|
+
`Unknown scene member "${memberName}".`,
|
|
14
|
+
`Known portals: ${portalList}.`,
|
|
15
|
+
`Known derived values: ${derivedList}.`,
|
|
16
|
+
].join('\n');
|
|
17
|
+
return new Error(message);
|
|
18
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Attestation file management for projection expectations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Single attestation for an actor projection.
|
|
9
|
+
* @typedef {Object} Attestation
|
|
10
|
+
* @property {string} location - Location name
|
|
11
|
+
* @property {'table' | 'schema'} artifactType - Type of artifact
|
|
12
|
+
* @property {string} name - Artifact name
|
|
13
|
+
* @property {'present' | 'absent'} status - Attestation status
|
|
14
|
+
* @property {string} observedAt - ISO 8601 timestamp
|
|
15
|
+
* @property {string} [shapeHash] - Optional shape hash for verification
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Attestation file structure.
|
|
20
|
+
* @typedef {Object} AttestationFile
|
|
21
|
+
* @property {1} version - File format version
|
|
22
|
+
* @property {Record<string, Attestation[]>} actors - Attestations by actor name
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load attestations from a JSON file.
|
|
27
|
+
* @param {string} filePath - Path to attestation file
|
|
28
|
+
* @returns {Promise<AttestationFile>}
|
|
29
|
+
*/
|
|
30
|
+
export async function loadAttestations(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
const text = await readFile(filePath, 'utf-8');
|
|
33
|
+
const data = JSON.parse(text);
|
|
34
|
+
|
|
35
|
+
if (!data.version || data.version !== 1) {
|
|
36
|
+
throw new Error(`Invalid attestation file version. Expected 1, got ${data.version}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!data.actors || typeof data.actors !== 'object') {
|
|
40
|
+
throw new Error('Attestation file must have an actors object');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return data;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === 'ENOENT') {
|
|
46
|
+
// File doesn't exist, return empty attestations
|
|
47
|
+
return {
|
|
48
|
+
version: 1,
|
|
49
|
+
actors: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Save attestations to a JSON file.
|
|
58
|
+
* @param {string} filePath - Path to attestation file
|
|
59
|
+
* @param {AttestationFile} data - Attestation data to save
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
export async function saveAttestations(filePath, data) {
|
|
63
|
+
const json = JSON.stringify(data, null, 2);
|
|
64
|
+
await writeFile(filePath, json, 'utf-8');
|
|
65
|
+
}
|