@immediately-run/sandbox-protocol 0.2.0 → 0.3.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/dist/fixtures.d.ts +40 -0
- package/dist/fixtures.js +144 -0
- package/package.json +5 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The shape grammar a snapshot channel is written in (mirrors `WireShape`). */
|
|
2
|
+
interface Shape {
|
|
3
|
+
fields?: Field[];
|
|
4
|
+
union?: Shape[];
|
|
5
|
+
array?: Shape;
|
|
6
|
+
tuple?: Shape[];
|
|
7
|
+
type?: string;
|
|
8
|
+
reads?: string[];
|
|
9
|
+
}
|
|
10
|
+
interface Field {
|
|
11
|
+
name: string;
|
|
12
|
+
optional: boolean;
|
|
13
|
+
type?: string;
|
|
14
|
+
union?: Shape[];
|
|
15
|
+
array?: Shape;
|
|
16
|
+
tuple?: Shape[];
|
|
17
|
+
fields?: Field[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A sample message for every wire name R3-274e resolved.
|
|
21
|
+
*
|
|
22
|
+
* Values are deliberately DISTINGUISHABLE — no empty arrays, no all-null unions, no
|
|
23
|
+
* two fields sharing a value — so a parser that drops, reorders or conflates a field
|
|
24
|
+
* produces a visibly different result instead of an accidentally-equal one. Both legs
|
|
25
|
+
* of every nullable union are exercised across the set (`activeFile` a string,
|
|
26
|
+
* `viewedFile` null).
|
|
27
|
+
*/
|
|
28
|
+
export declare const WIRE_FIXTURES: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
29
|
+
/** The wire names `WIRE_FIXTURES` covers. */
|
|
30
|
+
export declare const FIXTURE_NAMES: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* Validate `value` against a snapshot `Shape`, returning one message per problem
|
|
33
|
+
* (empty = conformant). Exported so BOTH consuming repos check the fixture with the
|
|
34
|
+
* same code — a per-repo validator would be one more thing that can agree by accident.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately strict about EXTRA fields: a fixture carrying a key the side does not
|
|
37
|
+
* declare is exactly the drift this is here to catch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function shapeProblems(shape: Shape, value: unknown, path?: string): string[];
|
|
40
|
+
export {};
|
package/dist/fixtures.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// One sample message per resolved wire name — the cross-side proof for R3-274e1.
|
|
3
|
+
//
|
|
4
|
+
// R3-274e resolved three names whose two sides had been declaring different shapes
|
|
5
|
+
// under one name (`fs-change`, `editor-context`, `sdk-handshake`; the decisions are in
|
|
6
|
+
// PLATFORM_LAYERING_SPEC §6). Each side's `protocol:check` compares that side's source
|
|
7
|
+
// against its own snapshot — which proves each side matches the CONTRACT, and proves
|
|
8
|
+
// nothing about the two sides matching EACH OTHER. Two snapshots projected from one
|
|
9
|
+
// descriptor set will agree by construction until someone edits the descriptors, and
|
|
10
|
+
// then they will disagree quietly, because nothing reads both.
|
|
11
|
+
//
|
|
12
|
+
// This module is the thing that reads both. It carries ONE sample per name, and this
|
|
13
|
+
// package's own test validates each sample against BOTH sides' declared shapes at once
|
|
14
|
+
// (`fixtures.test.ts`). The consuming repos then drive the SAME object through their
|
|
15
|
+
// real parsers. That is what makes deleting a field from a fixture below fail on both
|
|
16
|
+
// sides rather than one:
|
|
17
|
+
//
|
|
18
|
+
// epoch removed from `fs-change` → sandbox: payload.fields requires it
|
|
19
|
+
// → sdk: value.fields requires it, and
|
|
20
|
+
// payload.reads names it
|
|
21
|
+
//
|
|
22
|
+
// Put a fixture here, never in a consuming repo. A per-repo fixture re-creates exactly
|
|
23
|
+
// the "two shapes, one name" condition R3-274e closed — this time in the test data.
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.FIXTURE_NAMES = exports.WIRE_FIXTURES = void 0;
|
|
26
|
+
exports.shapeProblems = shapeProblems;
|
|
27
|
+
/**
|
|
28
|
+
* A sample message for every wire name R3-274e resolved.
|
|
29
|
+
*
|
|
30
|
+
* Values are deliberately DISTINGUISHABLE — no empty arrays, no all-null unions, no
|
|
31
|
+
* two fields sharing a value — so a parser that drops, reorders or conflates a field
|
|
32
|
+
* produces a visibly different result instead of an accidentally-equal one. Both legs
|
|
33
|
+
* of every nullable union are exercised across the set (`activeFile` a string,
|
|
34
|
+
* `viewedFile` null).
|
|
35
|
+
*/
|
|
36
|
+
exports.WIRE_FIXTURES = Object.freeze({
|
|
37
|
+
// `{paths, epoch}` — both sides declare both fields. The frame reads only `paths`
|
|
38
|
+
// (it recompiles on every batch); the SDK's consumers read `epoch` as an ordering
|
|
39
|
+
// token. Keeping `epoch` was the resolution: apps read it.
|
|
40
|
+
'fs-change': Object.freeze({
|
|
41
|
+
epoch: 7,
|
|
42
|
+
paths: Object.freeze(['/app/src/App.tsx', '/app/content/home.mdx']),
|
|
43
|
+
}),
|
|
44
|
+
// The frame's declaration caught up to the host's four fields. What each side
|
|
45
|
+
// CACHES stays a subset; what travels is all four.
|
|
46
|
+
'editor-context': Object.freeze({
|
|
47
|
+
activeFile: '/app/src/App.tsx',
|
|
48
|
+
dirtyPaths: Object.freeze(['/app/src/App.tsx']),
|
|
49
|
+
openFiles: Object.freeze(['/app/src/App.tsx', '/app/src/main.tsx']),
|
|
50
|
+
viewedFile: null,
|
|
51
|
+
}),
|
|
52
|
+
// Two legitimate producers, each populating what it owns — hence the union with
|
|
53
|
+
// every field optional. The fixture populates ALL of them on purpose: a sample
|
|
54
|
+
// that omitted a field could not detect a side that stopped declaring it.
|
|
55
|
+
'sdk-handshake': Object.freeze({
|
|
56
|
+
protocolVersion: '1',
|
|
57
|
+
sandboxProtocolVersion: '2',
|
|
58
|
+
sdkVersion: '0.44.0',
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
/** The wire names `WIRE_FIXTURES` covers. */
|
|
62
|
+
exports.FIXTURE_NAMES = Object.freeze(Object.keys(exports.WIRE_FIXTURES));
|
|
63
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
64
|
+
const primitiveMatches = (type, v) => {
|
|
65
|
+
switch (type) {
|
|
66
|
+
case 'string':
|
|
67
|
+
return typeof v === 'string';
|
|
68
|
+
case 'number':
|
|
69
|
+
return typeof v === 'number';
|
|
70
|
+
case 'boolean':
|
|
71
|
+
return typeof v === 'boolean';
|
|
72
|
+
case 'null':
|
|
73
|
+
return v === null;
|
|
74
|
+
case 'undefined':
|
|
75
|
+
return v === undefined;
|
|
76
|
+
default:
|
|
77
|
+
// A named type (`FsChange`, `EditorContext`) carries no structure here — the
|
|
78
|
+
// sibling `fields` does. Unknown type text is NOT a failure, or every named
|
|
79
|
+
// type would have to be duplicated in this file.
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Validate `value` against a snapshot `Shape`, returning one message per problem
|
|
85
|
+
* (empty = conformant). Exported so BOTH consuming repos check the fixture with the
|
|
86
|
+
* same code — a per-repo validator would be one more thing that can agree by accident.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately strict about EXTRA fields: a fixture carrying a key the side does not
|
|
89
|
+
* declare is exactly the drift this is here to catch.
|
|
90
|
+
*/
|
|
91
|
+
function shapeProblems(shape, value, path = '$') {
|
|
92
|
+
const problems = [];
|
|
93
|
+
if (shape.union) {
|
|
94
|
+
const ok = shape.union.some((m) => shapeProblems(m, value, path).length === 0);
|
|
95
|
+
if (!ok)
|
|
96
|
+
problems.push(`${path}: matches no member of the declared union`);
|
|
97
|
+
return problems;
|
|
98
|
+
}
|
|
99
|
+
if (shape.array) {
|
|
100
|
+
if (!Array.isArray(value)) {
|
|
101
|
+
problems.push(`${path}: declared an array, got ${typeof value}`);
|
|
102
|
+
return problems;
|
|
103
|
+
}
|
|
104
|
+
value.forEach((el, i) => problems.push(...shapeProblems(shape.array, el, `${path}[${i}]`)));
|
|
105
|
+
return problems;
|
|
106
|
+
}
|
|
107
|
+
if (shape.tuple) {
|
|
108
|
+
if (!Array.isArray(value)) {
|
|
109
|
+
problems.push(`${path}: declared a tuple, got ${typeof value}`);
|
|
110
|
+
return problems;
|
|
111
|
+
}
|
|
112
|
+
if (value.length !== shape.tuple.length) {
|
|
113
|
+
problems.push(`${path}: tuple declares ${shape.tuple.length} slot(s), got ${value.length}`);
|
|
114
|
+
return problems;
|
|
115
|
+
}
|
|
116
|
+
shape.tuple.forEach((m, i) => problems.push(...shapeProblems(m, value[i], `${path}[${i}]`)));
|
|
117
|
+
return problems;
|
|
118
|
+
}
|
|
119
|
+
if (shape.fields) {
|
|
120
|
+
if (!isRecord(value)) {
|
|
121
|
+
problems.push(`${path}: declared an object, got ${Array.isArray(value) ? 'array' : typeof value}`);
|
|
122
|
+
return problems;
|
|
123
|
+
}
|
|
124
|
+
for (const f of shape.fields) {
|
|
125
|
+
const present = Object.prototype.hasOwnProperty.call(value, f.name);
|
|
126
|
+
if (!present) {
|
|
127
|
+
if (!f.optional)
|
|
128
|
+
problems.push(`${path}.${f.name}: required by the declaration, absent`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
problems.push(...shapeProblems(f, value[f.name], `${path}.${f.name}`));
|
|
132
|
+
}
|
|
133
|
+
const declared = new Set(shape.fields.map((f) => f.name));
|
|
134
|
+
for (const k of Object.keys(value)) {
|
|
135
|
+
if (!declared.has(k))
|
|
136
|
+
problems.push(`${path}.${k}: present but not declared`);
|
|
137
|
+
}
|
|
138
|
+
return problems;
|
|
139
|
+
}
|
|
140
|
+
if (shape.type && !primitiveMatches(shape.type, value)) {
|
|
141
|
+
problems.push(`${path}: declared \`${shape.type}\`, got ${value === null ? 'null' : typeof value}`);
|
|
142
|
+
}
|
|
143
|
+
return problems;
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@immediately-run/sandbox-protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The sandbox<->SDK wire protocol: ONE descriptor set, projected into the wire-name constants, the payload types, and the per-side snapshots both repos gate against (PLATFORM_LAYERING_SPEC S1). Consumed by sandbox and immediately-run-sdk so a wire change is a version bump, not a hand-copied file.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./dist/sdk.d.ts",
|
|
24
24
|
"default": "./dist/sdk.js"
|
|
25
25
|
},
|
|
26
|
+
"./fixtures": {
|
|
27
|
+
"types": "./dist/fixtures.d.ts",
|
|
28
|
+
"default": "./dist/fixtures.js"
|
|
29
|
+
},
|
|
26
30
|
"./snapshots/sandbox": "./snapshots/sandbox.json",
|
|
27
31
|
"./snapshots/sdk": "./snapshots/sdk.json",
|
|
28
32
|
"./package.json": "./package.json"
|