@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.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @jarenjs/studio
2
+
3
+ **The jaren project IDE — a jaren application as a multi-file project.**
4
+
5
+ CodePen's model, for JSON-all-the-way-down: an application is a small tree
6
+ of typed files (a view, the actions, the state, a schema, a data model,
7
+ queries, a flow), each edited on its own, each validated against its own
8
+ grammar, assembled into runnable artifacts, and — where the driver allows
9
+ — run live. The same document the human edits is the document an AI
10
+ authors; the studio is a controlled component whose value is that project.
11
+
12
+ This package ships in two layers, the suite's convention:
13
+
14
+ - **the engine** (`@jarenjs/studio`) — headless: parse a `jaren-project`
15
+ document, validate each file against its kind, assemble the runnable
16
+ artifacts, and classify a change as structural vs. state-only. Knows the
17
+ grammars, nothing of the DOM.
18
+ - **the component** (`@jarenjs/studio/component`) — the IDE itself, a
19
+ `createStudioComponent()` factory (like `@jarenjs/calc`): the shell is a
20
+ **JSLT view** (file rail with add/delete, an editable file name, a
21
+ template gallery, editor, docked coded-error strip, run stage, layout
22
+ switcher) over a pure `projectViewModel`, plus the two hard-problem
23
+ policies (`hostPolicy` reboot-vs-hot-update, `reconcileBuffer`). The
24
+ chrome and its derivation render headlessly and are tested as such; the
25
+ host registers the DOM-touching islands — the live nested-app stage
26
+ (hot-update via `app.setState`) and the drag splitter — which are
27
+ browser-verified. Mounted live at the website's `#/project`, where `app`
28
+ files boot, `jslt`/`query` files run against a data file, and files are
29
+ added, renamed, deleted and opened from templates.
30
+
31
+ ## The project document
32
+
33
+ A thin envelope over typed files — the full contract is
34
+ [PROJECT-FORMAT.md](docs/PROJECT-FORMAT.md).
35
+
36
+ ```js
37
+ import { parseProject, validateFile, assembleArtifacts, classifyChange, describe }
38
+ from '@jarenjs/studio';
39
+
40
+ const project = parseProject({
41
+ project: '0.1',
42
+ files: [
43
+ { name: 'app.json', kind: 'app', text: '{ "view": [ … ], "state": { … } }' },
44
+ { name: 'npv.query', kind: 'query', text: '{ "$npv": ["$.rate", "$.cf[*]"] }' },
45
+ { name: 'seed.data', kind: 'data', text: '{ "cf": [-1000, 300, 400] }' },
46
+ ],
47
+ });
48
+
49
+ describe(project); // per-file kind / validity / role — the file rail
50
+ validateFile(project.files[1]); // { valid, kind, total, errors: [{ code, message, docPath }] }
51
+ assembleArtifacts(project); // the runnable set
52
+ ```
53
+
54
+ ## Why per-file validation
55
+
56
+ The published `jaren-query` / `jaren-jslt` grammars are *closed*, so a data
57
+ query using a host-registered operator (`$npv`, `$sqrt`) can't be checked
58
+ against them. The studio validates each file on **its own boundary** —
59
+ `jslt`/`query` files are *compiled* with the operator packs mounted, so
60
+ registered operators pass and a real mistake surfaces as its own coded
61
+ error with a JSON Pointer. There is deliberately no single composed
62
+ mega-schema; that is what lets a project mix an app, a store and a data
63
+ query at once. See [PROJECT-FORMAT.md](docs/PROJECT-FORMAT.md).
64
+
65
+ ## Install
66
+
67
+ ```
68
+ npm install @jarenjs/studio
69
+ ```
70
+
71
+ Zero third-party runtime dependencies — only other `@jarenjs/*` packages.
72
+ Node ≥ 24, ESM.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file Files → runnable artifacts, and the change-classification the IDE
3
+ * needs to decide reboot-vs-hot-update.
4
+ *
5
+ * `assembleArtifacts` composes the project's files into the runnable set.
6
+ * This order ships the WHOLE-DOCUMENT contract: a runnable file (`app`,
7
+ * `fsm`, `dag`, `model`, `jslt`, `query`, `schema`) is its own artifact;
8
+ * `state`/`data` files are inputs, not artifacts. Fragment assembly —
9
+ * composing separate `state` + `view` + `actions` files into ONE
10
+ * `jaren-app` document (the true HTML/CSS/JS split) — is the model's
11
+ * headline enhancement and layers on top without changing this contract.
12
+ *
13
+ * `classifyChange` is the load-bearing UX datum: a `state`-only edit must
14
+ * HOT-DISPATCH into a running app (no reboot, the user keeps scroll and
15
+ * inputs), while a `view`/`actions` change must reboot. It compares a
16
+ * STRUCTURAL key (an app doc minus its `state`) via the suite's own
17
+ * `contentKey`, so the policy in the widget reads clean, tested data.
18
+ */
19
+ /**
20
+ * Compose the project's files into runnable artifacts (whole-document).
21
+ * @param {any} project - a normalized project (from `parseProject`)
22
+ * @returns {{ artifacts: Array<{ name: string, kind: string, role: string,
23
+ * doc: any, sourceFiles: string[] }>, errors: Array<{ file: string, message: string }> }}
24
+ */
25
+ export declare function assembleArtifacts(project: any): {
26
+ artifacts: Array<{
27
+ name: string;
28
+ kind: string;
29
+ role: string;
30
+ doc: any;
31
+ sourceFiles: string[];
32
+ }>;
33
+ errors: Array<{
34
+ file: string;
35
+ message: string;
36
+ }>;
37
+ };
38
+ /**
39
+ * Classify what changed between two projects, PER artifact — the datum
40
+ * the widget's reboot-vs-hot-update policy consumes. `state-only` means
41
+ * an app's state moved but its structure did not (hot-dispatch it);
42
+ * `structural` means reboot; `none` means nothing changed.
43
+ * @param {any} prevProject
44
+ * @param {any} nextProject
45
+ * @returns {{ overall: 'none' | 'state-only' | 'structural',
46
+ * perArtifact: Record<string, 'none' | 'state-only' | 'structural'> }}
47
+ */
48
+ export declare function classifyChange(prevProject: any, nextProject: any): {
49
+ overall: 'none' | 'state-only' | 'structural';
50
+ perArtifact: Record<string, 'none' | 'state-only' | 'structural'>;
51
+ };
52
+ /**
53
+ * Per-file metadata for the IDE file rail and the AI `list_files` tool:
54
+ * kind, validity, size, its role, and which runnable artifact it is (a
55
+ * `state`/`data` input is not itself an artifact).
56
+ * @param {any} project - a normalized project
57
+ * @param {{ operators?: { toOptions: () => any } }} [options]
58
+ */
59
+ export declare function describe(project: any, options?: {
60
+ operators?: {
61
+ toOptions: () => any;
62
+ };
63
+ }): {
64
+ active: any;
65
+ layout: any;
66
+ files: any;
67
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @file The editor + rail vnode primitives — the baseline IDE, graduated
3
+ * from the site's studio-kit. Everything is a plain `@jarenjs/view` vnode
4
+ * (a tagged array), so the whole IDE renders through the same JSLT engine
5
+ * the rest of the suite uses; no imperative editor widget, no syntax
6
+ * highlighting (a later concern). The concrete kind→badge map lives here;
7
+ * its colours are in `styles/studio.css`.
8
+ */
9
+ /**
10
+ * A baseline code editor: a `<textarea>` with the ergonomics that keep it
11
+ * from fighting the shell (no browser resize — the splitter owns width;
12
+ * spell/autocap/autocorrect off so a mobile keyboard does not rewrite
13
+ * JSON keys). Tab-to-indent and the debounce live in the shell's edit
14
+ * loop; the value is bound to the active file.
15
+ *
16
+ * `inputAction` is load-bearing, not a convenience: this is a CONTROLLED
17
+ * textarea, and the renderer reasserts a control's authoritative value
18
+ * after every settled render. If the document only learned about an edit
19
+ * on `change` (blur), any render in between would rewrite the box with
20
+ * the still-stale text — and writing `.value` clears the browser's
21
+ * dirty-value flag, so `change` would then never fire and the typing
22
+ * would vanish. Publishing each keystroke to the typing buffer keeps the
23
+ * authoritative value equal to what the user typed, so the reassert is a
24
+ * no-op and the caret survives.
25
+ * @param {{ value: any, action: string, inputAction?: string, rows?: number,
26
+ * readonly?: boolean }} options
27
+ */
28
+ export declare function editorTextarea(options: {
29
+ value: any;
30
+ action: string;
31
+ inputAction?: string;
32
+ rows?: number;
33
+ readonly?: boolean;
34
+ }): (string | {
35
+ class: string;
36
+ rows: number;
37
+ spellcheck: string;
38
+ autocapitalize: string;
39
+ autocorrect: string;
40
+ autocomplete: string;
41
+ value: any;
42
+ readonly?: string | undefined;
43
+ on: {
44
+ change: string;
45
+ input?: undefined;
46
+ } | {
47
+ input: string;
48
+ change: string;
49
+ };
50
+ })[];
51
+ /** One line of the docked error strip. */
52
+ export declare function errorLine(content: any): any[];
53
+ /**
54
+ * The five concrete kind badge classes (colours are CONSTANTS in
55
+ * studio.css, not aliased status tokens): view (blue), query (cyan),
56
+ * json (slate), model (green), flow (amber).
57
+ */
58
+ export declare const KIND_BADGE: Readonly<{
59
+ app: "view";
60
+ jslt: "view";
61
+ query: "query";
62
+ state: "json";
63
+ data: "json";
64
+ schema: "json";
65
+ model: "model";
66
+ fsm: "flow";
67
+ dag: "flow";
68
+ }>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @file The two hard-problem POLICIES, pure and tested — the DOM
3
+ * mechanics that consume them live in the widget.
4
+ *
5
+ * `hostPolicy` (reboot vs. hot-update): a `state-only` edit hot-dispatches
6
+ * the new state into the RUNNING nested app (the user keeps scroll and
7
+ * inputs, no reboot); a `structural` edit reboots; an unchanged artifact
8
+ * is skipped.
9
+ *
10
+ * `reconcileBuffer` (editor buffer ↔ document): a CLEAN buffer adopts an
11
+ * incoming write (share / undo / an AI edit lands); a DIRTY buffer whose
12
+ * text differs from the incoming write keeps the human's text and records
13
+ * the write as a recoverable draft — never a silent clobber, never a
14
+ * hidden write.
15
+ */
16
+ /**
17
+ * Per-artifact action a stage host should take between two project
18
+ * revisions: `reboot` (destroy + boot), `hot` (dispatch new state into
19
+ * the running app), or `skip` (nothing changed).
20
+ * @param {any} prevProject
21
+ * @param {any} nextProject
22
+ * @returns {Record<string, 'reboot' | 'hot' | 'skip'>}
23
+ */
24
+ export declare function hostPolicy(prevProject: any, nextProject: any): Record<string, 'reboot' | 'hot' | 'skip'>;
25
+ /**
26
+ * Reconcile the editor's local typing buffer against an incoming
27
+ * committed text (a share/undo restore, or an AI write onto the same
28
+ * file). A clean buffer adopts; a dirty buffer that already matches the
29
+ * incoming text simply clears (the commit landed); a dirty buffer that
30
+ * differs keeps the human's text and surfaces the incoming version as a
31
+ * recoverable `conflict`.
32
+ * @param {{ text: string, dirty: boolean }} buffer
33
+ * @param {string} incoming - the file's committed text
34
+ * @returns {{ text: string, dirty: boolean, conflict: { incoming: string } | null }}
35
+ */
36
+ export declare function reconcileBuffer(buffer: {
37
+ text: string;
38
+ dirty: boolean;
39
+ }, incoming: string): {
40
+ text: string;
41
+ dirty: boolean;
42
+ conflict: {
43
+ incoming: string;
44
+ } | null;
45
+ };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @file The Studio COMPONENT — the IDE. Following the suite's component
3
+ * convention (`createXComponent`), `createStudioComponent(options)` hands
4
+ * the host the pieces it composes into the site's `@jarenjs/app` document:
5
+ * the JSLT view (`rules` + `mode`), the derivation (`viewModel`), the two
6
+ * hard-problem policies (`hostPolicy`, `reconcileBuffer`), and the engine
7
+ * surface. The reducer `project/*` actions, the DOM stage/splitter
8
+ * widgets, and the live site mount are wired at the host; the chrome and
9
+ * its derivation — everything renderable without a DOM — live here and
10
+ * are tested headlessly.
11
+ */
12
+ import { classifyChange, parseProject } from '../index.js';
13
+ import { projectViewModel } from './viewmodel.js';
14
+ import { projectRules, projectModes, PROJECT_MODE, PROJECT_BASE } from './view.js';
15
+ import { hostPolicy, reconcileBuffer } from './host.js';
16
+ import { editorTextarea, errorLine, KIND_BADGE } from './editor.js';
17
+ /**
18
+ * Build the Studio component.
19
+ * @param {{ operators?: { toOptions: () => any } }} [options] - a host
20
+ * operator registry threaded to every per-file validator/derivation
21
+ */
22
+ export declare function createStudioComponent(options?: {
23
+ operators?: {
24
+ toOptions: () => any;
25
+ };
26
+ }): {
27
+ mode: string;
28
+ rules: ({
29
+ match: string;
30
+ mode: string;
31
+ body: (string | (string | {
32
+ name: string;
33
+ class: string;
34
+ role: string;
35
+ 'aria-orientation': string;
36
+ 'aria-label': string;
37
+ 'aria-valuemin': string;
38
+ 'aria-valuemax': string;
39
+ 'aria-valuenow': string;
40
+ tabindex: string;
41
+ props: {
42
+ ratio: string;
43
+ mode: string;
44
+ };
45
+ })[] | (string | any[] | {
46
+ class: string;
47
+ role: string;
48
+ 'aria-label': string;
49
+ })[] | (string | {
50
+ $apply: string;
51
+ }[] | (string | (string | {
52
+ value: string;
53
+ })[] | {
54
+ class: string;
55
+ 'aria-label': string;
56
+ value: string;
57
+ on: {
58
+ change: string;
59
+ };
60
+ })[] | {
61
+ class: string;
62
+ 'aria-label': string;
63
+ })[] | (string | (string | {
64
+ class: string;
65
+ })[] | {
66
+ class: string;
67
+ $if?: undefined;
68
+ } | {
69
+ class?: undefined;
70
+ $if: ((string | (string | {
71
+ name: string;
72
+ props: string;
73
+ })[] | {
74
+ class: string;
75
+ })[] | {
76
+ $if?: undefined;
77
+ $eq: string[];
78
+ } | {
79
+ $if: ({
80
+ $if?: undefined;
81
+ $eq: string[];
82
+ } | {
83
+ $eq?: undefined;
84
+ $if: (string | (string | {
85
+ $apply: string[];
86
+ }[] | {
87
+ class: string;
88
+ })[])[];
89
+ } | {
90
+ $eq?: undefined;
91
+ $if: ((string | {
92
+ class: string;
93
+ })[] | {
94
+ $eq: string[];
95
+ })[];
96
+ })[];
97
+ $eq?: undefined;
98
+ })[];
99
+ })[] | (string | (string | {
100
+ class: string;
101
+ type: string;
102
+ on: {
103
+ click: string;
104
+ };
105
+ })[] | (string | any[] | {
106
+ class: string;
107
+ role: string;
108
+ 'aria-label': string;
109
+ })[] | (string | {
110
+ $apply: string;
111
+ }[] | (string | {
112
+ class: string;
113
+ })[] | {
114
+ class: string;
115
+ })[] | {
116
+ class: string;
117
+ })[] | (string | (string | {
118
+ class: string;
119
+ rows: number;
120
+ spellcheck: string;
121
+ autocapitalize: string;
122
+ autocorrect: string;
123
+ autocomplete: string;
124
+ value: any;
125
+ readonly?: string | undefined;
126
+ on: {
127
+ change: string;
128
+ input?: undefined;
129
+ } | {
130
+ input: string;
131
+ change: string;
132
+ };
133
+ })[] | (string | (string | {
134
+ class: string;
135
+ value: string;
136
+ spellcheck: string;
137
+ autocapitalize: string;
138
+ autocomplete: string;
139
+ 'aria-label': string;
140
+ on: {
141
+ input: string;
142
+ change: string;
143
+ };
144
+ })[] | (string | string[] | {
145
+ class: string;
146
+ })[] | {
147
+ class: string;
148
+ })[] | {
149
+ class: string;
150
+ $if?: undefined;
151
+ } | {
152
+ $if: (string | (string | {}[] | (string | {
153
+ type: string;
154
+ class: string;
155
+ title: string;
156
+ on: {
157
+ click: string;
158
+ };
159
+ })[] | {
160
+ class: string;
161
+ role: string;
162
+ })[])[];
163
+ class?: undefined;
164
+ } | {
165
+ $if: (string | (string | {
166
+ $apply: string;
167
+ }[] | {
168
+ class: string;
169
+ role: string;
170
+ })[])[];
171
+ class?: undefined;
172
+ })[] | {
173
+ class: string;
174
+ 'data-mode': string;
175
+ 'data-pane': string;
176
+ })[];
177
+ } | {
178
+ match: string;
179
+ mode: string;
180
+ body: (string | (string | {
181
+ type: string;
182
+ class: string;
183
+ title: string;
184
+ 'aria-label': string;
185
+ on: {
186
+ click: {
187
+ action: string;
188
+ with: string;
189
+ };
190
+ };
191
+ })[] | (string | (string | {
192
+ class: string;
193
+ 'data-badge': string;
194
+ })[] | (string | {
195
+ class: string;
196
+ })[] | {
197
+ $if?: undefined;
198
+ type: string;
199
+ class: {
200
+ $if: string[];
201
+ };
202
+ title: string;
203
+ on: {
204
+ click: {
205
+ action: string;
206
+ with: string;
207
+ };
208
+ };
209
+ } | {
210
+ class?: undefined;
211
+ type?: undefined;
212
+ title?: undefined;
213
+ on?: undefined;
214
+ $if: (string | (string | {
215
+ class: string;
216
+ title: string;
217
+ })[])[];
218
+ })[] | {
219
+ class: string;
220
+ })[];
221
+ } | {
222
+ match: string;
223
+ mode: string;
224
+ body: (string | {}[] | {
225
+ class: string;
226
+ type: string;
227
+ on: {
228
+ click: {
229
+ action: string;
230
+ with: string;
231
+ };
232
+ };
233
+ })[];
234
+ })[];
235
+ modes: Readonly<{
236
+ project: {
237
+ unmatched: string;
238
+ };
239
+ }>;
240
+ /** The IDE view model for the `$.project` slice. */
241
+ viewModel: (state: any) => any;
242
+ hostPolicy: typeof hostPolicy;
243
+ reconcileBuffer: typeof reconcileBuffer;
244
+ describe: (project: any) => {
245
+ active: any;
246
+ layout: any;
247
+ files: any;
248
+ };
249
+ validateFile: (file: any) => {
250
+ valid: boolean;
251
+ kind: string;
252
+ total: number;
253
+ errors: Array<{
254
+ code: string | null;
255
+ message: string;
256
+ docPath?: string;
257
+ }>;
258
+ };
259
+ classifyChange: typeof classifyChange;
260
+ parseProject: typeof parseProject;
261
+ };
262
+ export { projectViewModel, projectRules, projectModes, PROJECT_MODE, PROJECT_BASE, hostPolicy, reconcileBuffer, editorTextarea, errorLine, KIND_BADGE, };