@jarenjs/studio 0.34.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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @file The IDE shell as a JSLT view — the chrome is a document, rendered
3
+ * by the same engine as the rest of the suite (no imperative chrome, no
4
+ * syntax highlighting). Following the suite's rule convention: one mode
5
+ * (`project`), rules matched by their ABSOLUTE slice path, `$apply` and
6
+ * body references RELATIVE to the matched node. The host mounts the view
7
+ * model at `$.ui.project`.
8
+ *
9
+ * The stage's live nested app and the drag splitter are the only
10
+ * imperative islands — `jaren-widget`s the host registers. The layout
11
+ * mode rides a `data-mode` attribute so the grid switches in CSS with no
12
+ * computed class; the phone pane rides `data-pane` the same way, and a
13
+ * kind badge rides `data-badge`.
14
+ */
15
+ /** The one mode this view uses. */
16
+ export declare const PROJECT_MODE = "project";
17
+ /** The slice the host mounts the view model at. */
18
+ export declare const PROJECT_BASE = "$.ui.project";
19
+ /** The modes the host merges into the site stylesheet. */
20
+ export declare const projectModes: Readonly<{
21
+ project: {
22
+ unmatched: string;
23
+ };
24
+ }>;
25
+ /** The studio's JSLT rules — spread into the site stylesheet. */
26
+ export declare const projectRules: ({
27
+ match: string;
28
+ mode: string;
29
+ body: (string | (string | {
30
+ name: string;
31
+ class: string;
32
+ role: string;
33
+ 'aria-orientation': string;
34
+ 'aria-label': string;
35
+ 'aria-valuemin': string;
36
+ 'aria-valuemax': string;
37
+ 'aria-valuenow': string;
38
+ tabindex: string;
39
+ props: {
40
+ ratio: string;
41
+ mode: string;
42
+ };
43
+ })[] | (string | any[] | {
44
+ class: string;
45
+ role: string;
46
+ 'aria-label': string;
47
+ })[] | (string | {
48
+ $apply: string;
49
+ }[] | (string | (string | {
50
+ value: string;
51
+ })[] | {
52
+ class: string;
53
+ 'aria-label': string;
54
+ value: string;
55
+ on: {
56
+ change: string;
57
+ };
58
+ })[] | {
59
+ class: string;
60
+ 'aria-label': string;
61
+ })[] | (string | (string | {
62
+ class: string;
63
+ })[] | {
64
+ class: string;
65
+ $if?: undefined;
66
+ } | {
67
+ class?: undefined;
68
+ $if: ((string | (string | {
69
+ name: string;
70
+ props: string;
71
+ })[] | {
72
+ class: string;
73
+ })[] | {
74
+ $if?: undefined;
75
+ $eq: string[];
76
+ } | {
77
+ $if: ({
78
+ $if?: undefined;
79
+ $eq: string[];
80
+ } | {
81
+ $eq?: undefined;
82
+ $if: (string | (string | {
83
+ $apply: string[];
84
+ }[] | {
85
+ class: string;
86
+ })[])[];
87
+ } | {
88
+ $eq?: undefined;
89
+ $if: ((string | {
90
+ class: string;
91
+ })[] | {
92
+ $eq: string[];
93
+ })[];
94
+ })[];
95
+ $eq?: undefined;
96
+ })[];
97
+ })[] | (string | (string | {
98
+ class: string;
99
+ type: string;
100
+ on: {
101
+ click: string;
102
+ };
103
+ })[] | (string | any[] | {
104
+ class: string;
105
+ role: string;
106
+ 'aria-label': string;
107
+ })[] | (string | {
108
+ $apply: string;
109
+ }[] | (string | {
110
+ class: string;
111
+ })[] | {
112
+ class: string;
113
+ })[] | {
114
+ class: string;
115
+ })[] | (string | (string | {
116
+ class: string;
117
+ rows: number;
118
+ spellcheck: string;
119
+ autocapitalize: string;
120
+ autocorrect: string;
121
+ autocomplete: string;
122
+ value: any;
123
+ readonly?: string | undefined;
124
+ on: {
125
+ change: string;
126
+ input?: undefined;
127
+ } | {
128
+ input: string;
129
+ change: string;
130
+ };
131
+ })[] | (string | (string | {
132
+ class: string;
133
+ value: string;
134
+ spellcheck: string;
135
+ autocapitalize: string;
136
+ autocomplete: string;
137
+ 'aria-label': string;
138
+ on: {
139
+ input: string;
140
+ change: string;
141
+ };
142
+ })[] | (string | string[] | {
143
+ class: string;
144
+ })[] | {
145
+ class: string;
146
+ })[] | {
147
+ class: string;
148
+ $if?: undefined;
149
+ } | {
150
+ $if: (string | (string | {}[] | (string | {
151
+ type: string;
152
+ class: string;
153
+ title: string;
154
+ on: {
155
+ click: string;
156
+ };
157
+ })[] | {
158
+ class: string;
159
+ role: string;
160
+ })[])[];
161
+ class?: undefined;
162
+ } | {
163
+ $if: (string | (string | {
164
+ $apply: string;
165
+ }[] | {
166
+ class: string;
167
+ role: string;
168
+ })[])[];
169
+ class?: undefined;
170
+ })[] | {
171
+ class: string;
172
+ 'data-mode': string;
173
+ 'data-pane': string;
174
+ })[];
175
+ } | {
176
+ match: string;
177
+ mode: string;
178
+ body: (string | (string | {
179
+ type: string;
180
+ class: string;
181
+ title: string;
182
+ 'aria-label': string;
183
+ on: {
184
+ click: {
185
+ action: string;
186
+ with: string;
187
+ };
188
+ };
189
+ })[] | (string | (string | {
190
+ class: string;
191
+ 'data-badge': string;
192
+ })[] | (string | {
193
+ class: string;
194
+ })[] | {
195
+ $if?: undefined;
196
+ type: string;
197
+ class: {
198
+ $if: string[];
199
+ };
200
+ title: string;
201
+ on: {
202
+ click: {
203
+ action: string;
204
+ with: string;
205
+ };
206
+ };
207
+ } | {
208
+ class?: undefined;
209
+ type?: undefined;
210
+ title?: undefined;
211
+ on?: undefined;
212
+ $if: (string | (string | {
213
+ class: string;
214
+ title: string;
215
+ })[])[];
216
+ })[] | {
217
+ class: string;
218
+ })[];
219
+ } | {
220
+ match: string;
221
+ mode: string;
222
+ body: (string | {}[] | {
223
+ class: string;
224
+ type: string;
225
+ on: {
226
+ click: {
227
+ action: string;
228
+ with: string;
229
+ };
230
+ };
231
+ })[];
232
+ })[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @file `projectViewModel(state)` — the IDE's derivation boundary. Given
3
+ * the `state.project` slice it derives everything the JSLT shell renders:
4
+ * the file rail (from the engine's `describe`), the active file's editor
5
+ * value and its coded errors, the docked error strip across every file,
6
+ * and the stage — an assembled app document to mount, a run result to
7
+ * show, or an inert note for a kind that has no editor of its own yet.
8
+ * Pure: nothing here is stored back in state.
9
+ */
10
+ /**
11
+ * Derive the IDE view model from `state.project`.
12
+ * @param {{ project: any }} state - the site state carrying the `project` slice
13
+ * @param {{ operators?: { toOptions: () => any } }} [options]
14
+ * @returns {any}
15
+ */
16
+ export declare function projectViewModel(state: {
17
+ project: any;
18
+ }, options?: {
19
+ operators?: {
20
+ toOptions: () => any;
21
+ };
22
+ }): any;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @file Coded errors for `@jarenjs/studio`, on `@jarenjs/core`'s coded
3
+ * contract: a stable `code` (JS0xxx compile/parse time), a bare
4
+ * `reason`, and — where a position in the project exists — a `docPath`.
5
+ *
6
+ * Only the ENVELOPE raises these: a malformed project document, or a
7
+ * duplicate file name. A single FILE's grammar problem is never thrown —
8
+ * it is REPORTED by {@link module:validate.validateFile} as the file
9
+ * kind's own coded errors (`JQ`/`JA`/`JD`…) with their docPaths, so the
10
+ * IDE can show them without stopping the world.
11
+ */
12
+ import { CodedError } from '@jarenjs/core/errors';
13
+ /** The code table (kept in sync with docs/PROJECT-FORMAT.md). */
14
+ export declare const STUDIO_CODES: Readonly<{
15
+ JS0001: "the project document is invalid";
16
+ JS0002: "a file name is duplicated in the project";
17
+ }>;
18
+ /** A studio-envelope error. */
19
+ export declare class StudioError extends CodedError {
20
+ /**
21
+ * @param {keyof typeof STUDIO_CODES} code
22
+ * @param {string} reason
23
+ * @param {string | { docPath?: string }} [location]
24
+ * @param {{ cause?: unknown }} [options]
25
+ */
26
+ constructor(code: keyof typeof STUDIO_CODES, reason: string, location?: string | {
27
+ docPath?: string;
28
+ }, options?: {
29
+ cause?: unknown;
30
+ });
31
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file `@jarenjs/studio` — the ENGINE (part one of the two-layer
3
+ * package). Headless: it parses a `jaren-project` document, validates
4
+ * each file against its own kind grammar, assembles the runnable
5
+ * artifacts, and classifies a change as structural vs. state-only. It
6
+ * knows the suite's grammars (validate/json/app/flow/db) but nothing of
7
+ * the DOM, `@jarenjs/view` or `@jarenjs/app`'s runtime — the component
8
+ * layer (`./component`) imports the engine, never the reverse.
9
+ *
10
+ * The project is a THIN envelope over typed files; there is deliberately
11
+ * no single composed meta-schema, so a data file may use host-registered
12
+ * operators the closed grammars forbid — the per-file validators are the
13
+ * honest boundary.
14
+ */
15
+ export { KINDS, LAYOUT_DEFAULT, parseProject, fileOf } from './project.js';
16
+ export { validateFile } from './validate.js';
17
+ export { assembleArtifacts, classifyChange, describe } from './assemble.js';
18
+ export { STUDIO_CODES, StudioError } from './errors.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @file The project document model: the closed file-kind vocabulary, the
3
+ * frozen IDE `layout` shape, and `parseProject` — validate a candidate
4
+ * against the `jaren-project` envelope schema, reject duplicate file
5
+ * names, and return a NORMALIZED, frozen project (layout defaulted,
6
+ * `active` resolved to a real file). Only the ENVELOPE is gated here; a
7
+ * file's `text` is a string until its kind validator runs (`validate.js`).
8
+ */
9
+ /** The closed set of file kinds (matches the schema `kind` enum). */
10
+ export declare const KINDS: readonly string[];
11
+ /** The default IDE layout — the frozen `{ mode, ratio, autorun }` shape
12
+ * that rides the share link and the eject, so it must not drift. */
13
+ export declare const LAYOUT_DEFAULT: Readonly<{
14
+ mode: "classic";
15
+ ratio: 0.5;
16
+ autorun: true;
17
+ }>;
18
+ /**
19
+ * Parse and normalize a project: JSON text or an object in; a frozen,
20
+ * normalized project out. A malformed envelope is `JS0001`; a duplicate
21
+ * file name is `JS0002`.
22
+ * @param {string | object} input
23
+ * @returns {any}
24
+ */
25
+ export declare function parseProject(input: string | object): any;
26
+ /**
27
+ * The file with this name, or `null`.
28
+ * @param {any} project
29
+ * @param {string} name
30
+ */
31
+ export declare function fileOf(project: any, name: string): any;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @file Per-file validation — the honest heart of the per-file design.
3
+ * `validateFile(file)` dispatches on the file's kind and validates its
4
+ * `text` against THAT kind's grammar (never one composed mega-schema):
5
+ *
6
+ * - `app` → the composed jaren-app meta-schema (which `$ref`s the
7
+ * query + JSLT grammars) PLUS a headless render audit (a
8
+ * document that validates but throws on its first frame is
9
+ * still broken);
10
+ * - `jslt` / `query` → COMPILED by the engine WITH the operator registry,
11
+ * so host-registered operators ($npv, $sqrt) validate and a
12
+ * real error comes back as its own coded `JQ`/`JT` code with
13
+ * a docPath — the closed grammar would reject the operators;
14
+ * - `fsm` / `dag` / `model` → their published grammar (structural);
15
+ * - `schema` → compiled as a JSON Schema (is it well-formed?);
16
+ * - `state` / `data` → any JSON (structural only).
17
+ *
18
+ * Every result is `{ valid, kind, total, errors: [{ code, message,
19
+ * docPath }] }` — the shape the IDE's docked error strip reads.
20
+ */
21
+ /**
22
+ * Validate ONE file against its kind's grammar. Memoized on the file's
23
+ * identity; pass a fresh object to force a re-check.
24
+ * @param {{ name?: string, kind: string, text: string }} file
25
+ * @param {{ operators?: { toOptions: () => any } }} [options] - a host
26
+ * operator registry for the `jslt`/`query` kinds (defaults to the
27
+ * built-in math/finance/stats packs)
28
+ * @returns {{ valid: boolean, kind: string, total: number,
29
+ * errors: Array<{ code: string | null, message: string, docPath?: string }> }}
30
+ */
31
+ export declare function validateFile(file: {
32
+ name?: string;
33
+ kind: string;
34
+ text: string;
35
+ }, options?: {
36
+ operators?: {
37
+ toOptions: () => any;
38
+ };
39
+ }): {
40
+ valid: boolean;
41
+ kind: string;
42
+ total: number;
43
+ errors: Array<{
44
+ code: string | null;
45
+ message: string;
46
+ docPath?: string;
47
+ }>;
48
+ };
@@ -0,0 +1,112 @@
1
+ # PROJECT-FORMAT.md — the `jaren-project` document (v0.1)
2
+
3
+ A **project** is a jaren application seen as an IDE would see it: a small
4
+ tree of typed files, each one concern — a view, the actions, the state,
5
+ a schema, a data model, queries, a flow machine — the way HTML, CSS and
6
+ JavaScript are separate files. The document is a **thin envelope**; each
7
+ file's meaning lives in its own grammar, not in the envelope.
8
+
9
+ ```jsonc
10
+ {
11
+ "project": "0.1",
12
+ "files": [
13
+ { "name": "app.json", "kind": "app", "text": "…a jaren-app document…" },
14
+ { "name": "series.query", "kind": "query", "text": "{ \"$npv\": [\"$.rate\", \"$.cf[*]\"] }" },
15
+ { "name": "seed.data", "kind": "data", "text": "{ \"cf\": [-1000, 300, 400] }" }
16
+ ],
17
+ "active": "app.json",
18
+ "layout": { "mode": "classic", "ratio": 0.5, "autorun": true }
19
+ }
20
+ ```
21
+
22
+ The envelope is published as a JSON Schema (`schemas/jaren-project.schema.json`
23
+ + a draft-07 twin) and validated by `parseProject`, which returns a
24
+ frozen, normalized project (the layout defaulted, `active` resolved to a
25
+ real file).
26
+
27
+ ## The file kinds
28
+
29
+ Each file declares a `kind`; its `text` is a JSON document validated
30
+ against **that kind's grammar at its own boundary** — never composed into
31
+ one meta-schema. `validateFile(file)` returns `{ valid, kind, total,
32
+ errors: [{ code, message, docPath }] }` — the shape an editor's error
33
+ strip reads.
34
+
35
+ | kind | the file is | validated by |
36
+ |---|---|---|
37
+ | `app` | a `jaren-app` document | the composed app meta-schema (jslt + query by `$ref`) **plus a headless render audit** — a document that validates but throws on its first frame is still broken |
38
+ | `jslt` | a JSLT stylesheet | **compiled** by the engine with the operator registry |
39
+ | `query` | a query document | **compiled** by the engine with the operator registry |
40
+ | `state` / `data` | any JSON value (an input) | structural JSON only |
41
+ | `schema` | a JSON Schema | must be an object/boolean and compile |
42
+ | `fsm` / `dag` | a `jaren-fsm` / `jaren-dag` machine | its published flow grammar |
43
+ | `model` | a `jaren-model` store definition | the `jaren-model` grammar |
44
+
45
+ **Why per-file, not one composed schema.** The published `jaren-query` /
46
+ `jaren-jslt` grammars are *closed* — their operator vocabulary is
47
+ enumerated. A data query that uses a host-registered operator (`$npv`,
48
+ `$sqrt`) validates only when the studio's operator packs are mounted, so
49
+ `jslt`/`query` files are **compiled with the registry** rather than checked
50
+ against the closed grammar: registered operators pass, and a real mistake
51
+ comes back as its own coded code with a JSON Pointer (`JQ0002 — at /x:
52
+ unknown operator '$flter'`). Composing every file into one gate would make
53
+ that impossible; keeping each file on its own boundary is the honest
54
+ design, not a compromise. A host embeds its own vocabulary with
55
+ `validateFile(file, { operators })`.
56
+
57
+ ## Assembly — files → runnable artifacts
58
+
59
+ `assembleArtifacts(project)` composes the files into the runnable set. v0.1
60
+ ships the **whole-document** contract: a runnable file (`app`, `fsm`,
61
+ `dag`, `model`, `jslt`, `query`, `schema`) is its own artifact
62
+ (`{ name, kind, role, doc, sourceFiles }`); `state`/`data` files are
63
+ inputs, not artifacts. **Fragment assembly** — composing separate `state`
64
+ + `view` + `actions` files into ONE `jaren-app` document (the true
65
+ HTML/CSS/JS split) — is the model's headline enhancement and layers on
66
+ top without changing this contract (a future `sourceFiles` will list more
67
+ than one name).
68
+
69
+ ## `layout` is frozen
70
+
71
+ `layout` is `{ mode: "classic" | "right" | "top", ratio: number,
72
+ autorun: boolean }`, defaulted to `{ classic, 0.5, true }`. It rides the
73
+ share link and the eject, so its shape is fixed at v0.1.
74
+
75
+ ## `classifyChange` — reboot vs. hot-update
76
+
77
+ `classifyChange(prev, next)` reports, **per artifact**, whether a change
78
+ is `structural`, `state-only`, or `none`. It compares a structural key —
79
+ an `app` document *minus its `state`* — via the suite's own `contentKey`.
80
+ An IDE reads it to decide: a `state-only` edit hot-dispatches into a
81
+ running app (the user keeps scroll and inputs); a `structural` edit
82
+ reboots. Keeping this datum in the tested engine, and the policy in the
83
+ widget, is deliberate.
84
+
85
+ ## Errors
86
+
87
+ Only the envelope raises a coded `StudioError`; a single file's grammar
88
+ problem is reported by `validateFile`, never thrown.
89
+
90
+ | code | meaning |
91
+ |---|---|
92
+ | `JS0001` | the project document is invalid (bad JSON, or fails the envelope schema) |
93
+ | `JS0002` | a file name is duplicated in the project |
94
+
95
+ ## Status
96
+
97
+ v0.1 is the headless **engine** — parse, per-file validate, assemble,
98
+ classify — plus the IDE **component** that mounts it: the file rail, the
99
+ debounced editor with its typing buffer, the run stage, the three layout
100
+ modes with a drag splitter, the phone pane switcher, save/load/share, and
101
+ an assistant that can list, read, write and run a project's files.
102
+
103
+ Still open, and tracked with their constraints in
104
+ [ROADMAP.md](../../../docs/ROADMAP.md): the `fsm`/`dag`/`model` kinds
105
+ validate but have no editor or runner, so they cannot be added from the
106
+ IDE; fragment assembly (one artifact from several files) is unbuilt;
107
+ the assistant authors files as free-form tool arguments rather than under
108
+ constrained decoding; there is no whole-project export (`.zip` eject);
109
+ `layout.autorun` is declared and never read; and the stage collects a
110
+ nested app's failures without showing them. Syntax highlighting stays a
111
+ non-goal — the editor is a plain `<textarea>` so the whole IDE remains a
112
+ JSLT document with no imperative chrome.
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@jarenjs/studio",
3
+ "private": false,
4
+ "version": "0.34.0",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./dist/types/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./component": {
15
+ "types": "./dist/types/component/index.d.ts",
16
+ "default": "./src/component/index.js"
17
+ },
18
+ "./schemas/*": "./schemas/*",
19
+ "./styles/studio.css": "./styles/studio.css",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "dist/types/",
24
+ "src/",
25
+ "docs/",
26
+ "schemas/",
27
+ "styles/"
28
+ ],
29
+ "description": "The jaren project IDE: a multi-file project (schema, view, actions, model, queries, flow) is one document; each file is validated against its own grammar, assembled into runnable artifacts, and edited/hosted by the studio widget. The engine is headless; the component layer is the IDE.",
30
+ "author": "joham",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/jklarenbeek/jarenjs.git",
34
+ "directory": "components/studio"
35
+ },
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=24"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "keywords": [
45
+ "jaren",
46
+ "json",
47
+ "ide",
48
+ "studio",
49
+ "project",
50
+ "editor",
51
+ "codepen",
52
+ "playground",
53
+ "document",
54
+ "headless"
55
+ ],
56
+ "scripts": {
57
+ "build": "npm run build:types",
58
+ "build:types": "tsc -p tsconfig.json",
59
+ "prepack": "npm run build:types"
60
+ },
61
+ "dependencies": {
62
+ "@jarenjs/core": "^0.34.0",
63
+ "@jarenjs/validate": "^0.34.0",
64
+ "@jarenjs/json": "^0.34.0",
65
+ "@jarenjs/app": "^0.34.0",
66
+ "@jarenjs/flow": "^0.34.0",
67
+ "@jarenjs/db": "^0.34.0"
68
+ }
69
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://jarenjs.dev/schemas/jaren-project/0.1/draft-07",
4
+ "title": "jaren-project (draft-07)",
5
+ "description": "The draft-07 twin of jaren-project, for tools pinned to draft-07. Mechanically equivalent to the 2020-12 canonical artifact.",
6
+ "type": "object",
7
+ "required": ["project", "files"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "project": { "enum": ["0.1"] },
11
+ "files": {
12
+ "type": "array",
13
+ "items": { "$ref": "#/definitions/file" }
14
+ },
15
+ "active": { "type": "string" },
16
+ "layout": { "$ref": "#/definitions/layout" }
17
+ },
18
+ "definitions": {
19
+ "file": {
20
+ "type": "object",
21
+ "required": ["name", "kind", "text"],
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "name": { "type": "string", "minLength": 1 },
25
+ "kind": {
26
+ "enum": ["app", "jslt", "query", "state", "data", "schema", "fsm", "dag", "model"]
27
+ },
28
+ "text": { "type": "string" }
29
+ }
30
+ },
31
+ "layout": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "mode": { "enum": ["classic", "right", "top"] },
36
+ "ratio": { "type": "number", "minimum": 0, "maximum": 1 },
37
+ "autorun": { "type": "boolean" }
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://jarenjs.dev/schemas/jaren-project/0.1",
4
+ "title": "jaren-project",
5
+ "description": "A jaren studio project: a thin envelope over a list of typed files. The envelope is schema-gated; each file's `text` is validated against ITS OWN kind grammar at its boundary, never composed into one mega-schema — so a data file may use host-registered operators the closed grammars forbid.",
6
+ "type": "object",
7
+ "required": ["project", "files"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "project": { "const": "0.1" },
11
+ "files": {
12
+ "type": "array",
13
+ "description": "The project's files, each a named unit of one concern.",
14
+ "items": { "$ref": "#/$defs/file" }
15
+ },
16
+ "active": {
17
+ "type": "string",
18
+ "description": "The name of the file the editor shows (falls back to the first file)."
19
+ },
20
+ "layout": { "$ref": "#/$defs/layout" }
21
+ },
22
+ "$defs": {
23
+ "file": {
24
+ "type": "object",
25
+ "required": ["name", "kind", "text"],
26
+ "additionalProperties": false,
27
+ "properties": {
28
+ "name": { "type": "string", "minLength": 1 },
29
+ "kind": {
30
+ "description": "The file's grammar/role; its `text` is validated against this kind, not the envelope.",
31
+ "enum": ["app", "jslt", "query", "state", "data", "schema", "fsm", "dag", "model"]
32
+ },
33
+ "text": { "type": "string" }
34
+ }
35
+ },
36
+ "layout": {
37
+ "type": "object",
38
+ "description": "The IDE arrangement — frozen so it survives a share link and the eject.",
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "mode": { "enum": ["classic", "right", "top"] },
42
+ "ratio": { "type": "number", "minimum": 0, "maximum": 1 },
43
+ "autorun": { "type": "boolean" }
44
+ }
45
+ }
46
+ }
47
+ }