@descix/frqtl-node 0.6.1-p2.1 → 0.6.1-p3.2
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/CLAUDE.md +1 -1
- package/LICENSE.md +1 -1
- package/README.md +3 -3
- package/dist/dist-manifest.json +11 -9
- package/dist/frqtl/objects/RecipeContext.js +21 -41
- package/dist/frqtl/parts/GraphingDetectorWall.js +1 -1
- package/dist/frqtl/parts/Wall.js +96 -9
- package/dist/shim/engine/router.js +44 -18
- package/dist/shim/engine/site-graph.js +146 -6
- package/dist/shim/host/device.js +38 -0
- package/dist/shim/project/index.js +13 -3
- package/dist/shim/project/parts-spelling.js +77 -0
- package/dist/shim/project/record-format.js +96 -4
- package/dist/shim/registry/studio-landing.js +14 -11
- package/llms.txt +2 -2
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A discrete cellular-automata physics engine (compiled Rust → WASM) for node: the same modules as
|
|
4
4
|
`@descix/frqtl-web` (one source, built twice), plus the node engine door `bootNode` and the hosted eyes.
|
|
5
|
-
`0.6.1-
|
|
5
|
+
`0.6.1-p3.2` (npm dist-tag `next`). Node ≥ 22.12.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
npm install @descix/frqtl-node
|
package/LICENSE.md
CHANGED
|
@@ -389,7 +389,7 @@ For GUIs, use an "About box" with similar information.
|
|
|
389
389
|
|
|
390
390
|
**The Original License is not displaced.** Nothing in this Agreement relieves You of any obligation the Original License imposes on Pre-existing Material. By way of example, if Pre-existing Material is licensed under the Mozilla Public License, its file-level copyleft, notice and source-availability obligations continue to bind You for that material; this Agreement neither absolves You of them nor extends them to Covered Works.
|
|
391
391
|
|
|
392
|
-
**This Agreement is not displaced.** No Original License shall be read to invalidate, override, relicense or extend its terms to any Covered Work or Community IP. If You combine, link or otherwise mix a Covered Work with Pre-existing Material whose Original License would, by its terms, purport to govern the combination or require the Covered Work to be licensed under that Original License (by way of example, the GNU General Public License), You are in breach of this Agreement from the moment of the combination: Your license under this Agreement terminates as to that combination under Clause 7, You lose the protections this Agreement affords Members with respect to it, and the Original License shall not be read to apply to the Covered Work or Community IP, whose terms remain those of this Agreement.
|
|
392
|
+
**This Agreement is not displaced.** No Original License shall be read to invalidate, override, relicense or extend its terms to any Covered Work or Community IP. If You combine, link or otherwise mix a Covered Work with Pre-existing Material whose Original License would, by its terms, purport to govern the combination or require the Covered Work to be licensed under that Original License (by way of example, the GNU General Public License), You are in breach of this Agreement from the moment of the combination: Your license under this Agreement terminates as to that combination under Clause 7 of the DCSL (Section 3.1), You lose the protections this Agreement affords Members with respect to it, and the Original License shall not be read to apply to the Covered Work or Community IP, whose terms remain those of this Agreement.
|
|
393
393
|
|
|
394
394
|
**Marking.** An adopter that places this Agreement in a collection containing Pre-existing Material shall place it within, or clearly identify, the directories or files it covers, so that no Pre-existing Material is represented as licensed under this Agreement.
|
|
395
395
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ FRQTL for node: a discrete cellular-automata physics engine (compiled Rust → W
|
|
|
4
4
|
local per-pixel rules — a frame moves one pixel per tick, single occupancy, momentum — rather than force
|
|
5
5
|
equations. Interference, blackbody spectra and atom-like binding emerge from the automaton.
|
|
6
6
|
|
|
7
|
-
`0.6.1-
|
|
7
|
+
`0.6.1-p3.2` (npm dist-tag `next`). Node ≥ 22.12.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -31,11 +31,11 @@ const tick = u.doTick(); // TickData: fr
|
|
|
31
31
|
| `.` | the parts (`Wall`, `SphereEmitter`, `Tunnel`, `FrqtlVision`, …), `bootNode`, the engine accessors (`engine`, `engineState`), the capture contracts (`FrqtlCapture`, `RenderStages`, `FieldBuilder`, `CaptureEnvelope`) and `validateRendererProvider` |
|
|
32
32
|
| `./eyes` | `openHostedEyes` — a `composite` still through the REAL renderer, in a managed headless browser (pulls the pinned `playwright`) |
|
|
33
33
|
| `./parts`, `./parts/*` | the parts barrel; one part = one file |
|
|
34
|
-
| `./project` | `FRQTL_WEB_SPECIFIERS`, `libPathOf`, `importMapFor`, `pageImportMapFor`, `FRQTL_WEB_HOST_SPECIFIERS`, `linkProject`, `sketchProject`, `SKETCH_ENTRY`, `projectIdentity`, `MANIFEST_FILE`, `MANIFEST_KEYS`, `normalizeManifest`, `parseManifest`, `serializeManifest`, `manifestFor`, `RECORD_FILES`, `RECORD_KEYS`, `CHANGE_KEYS`, `OPS`, `BY`, `CLIP_SECONDS`, `CLIP_NAME`, `clipName`, `jsonText`, `recordFolderFaults` |
|
|
34
|
+
| `./project` | `FRQTL_WEB_SPECIFIERS`, `libPathOf`, `importMapFor`, `pageImportMapFor`, `FRQTL_WEB_HOST_SPECIFIERS`, `linkProject`, `sketchProject`, `SKETCH_ENTRY`, `projectIdentity`, `MANIFEST_FILE`, `MANIFEST_KEYS`, `normalizeManifest`, `parseManifest`, `serializeManifest`, `manifestFor`, `RECORD_FILES`, `RECORD_KEYS`, `CHANGE_KEYS`, `OPS`, `BY`, `CLIP_SECONDS`, `CLIP_NAME`, `clipName`, `jsonText`, `rowFaults`, `recordFolderFaults`, `applyRows` (stands alone: no other import is needed) |
|
|
35
35
|
| `./registry` | the walk-through rungs, the showcase roster, the scene list (`sceneGroups`), `projectOf`, a showcase tile's link (`showcaseHref(row)` relative to the serving site, `showcaseUrl(row)` absolute on `SHOWCASE_SITE.origin`; `row` is an element of the same imported `EXHIBITS`, and the link carries the row's effective mode on a multi-mode rung), and how STUDIO resolved the link it opened with (`STUDIO_LANDING_ATTR` on `<html>`, a `STUDIO_LANDING_OUTCOMES` id) |
|
|
36
36
|
| `./kinds` | `registerKind`, `kindNamed`, `kindOf`, `registeredKinds` |
|
|
37
37
|
| `./host-view` | `declareHostView`, `HOST_VIEW_OUTCOMES`, `CONTRACT_VIOLATIONS`, `HOST_SDK_SCRIPT` — the DeSciX host-view declarer (pure: a node test can drive a call site with a stub SDK) |
|
|
38
|
-
| `./site-graph` | `SITE_GRAPH_DIR`, `SITE_BUILD_ATTR`, `SITE_BUILD_ID_RE`, `STATIC_APP_PAGE`, `HOST_PROVIDED_ATTR` — the static site's shape (graph directory, build-stamp attribute, build id pattern, IDE entry page, the attribute declaring a file host-provided), for a reader of a served site (both packages) |
|
|
38
|
+
| `./site-graph` | `SITE_GRAPH_DIR`, `SITE_BUILD_ATTR`, `SITE_BUILD_ID_RE`, `siteBuildStampOf`, `STATIC_APP_PAGE`, `HOST_PROVIDED_ATTR`, `SITE_PLACEMENTS`, `SITE_ENTRY_PAGES`, `isSiteEntry`, `siteGraphFaults` — the static site's shape (graph directory, build-stamp attribute, build id pattern and its reader, IDE entry page, the attribute declaring a file host-provided, the graph directory of each placed package, the entry pages) and the rule that a served graph is the placed releases' bytes, for a reader of a served site (both packages) |
|
|
39
39
|
| `./register` | installs the resolve hook for a sketch's `@descix/frqtl-web/...` imports (`node --import @descix/frqtl-node/register`) |
|
|
40
40
|
| `./shim/controls/registry.js`, `./shim/render/sun-look.js`, `./compat/renderer/display-slot.js`, `./compat/renderer/orbit-bag.js`, `./objects/setup/_collisionMatrixLanes.js`, `./compat/engine.js` | the six non-part modules a scene may import (the rows of `FRQTL_WEB_SPECIFIERS`) |
|
|
41
41
|
| `./objects/objects/tagger.js`, `./objects/schema/index.js`, `./shim/render/heat-colormap.js`, `./objects/schema/valueNormalizer.js`, `./objects/objects/Box.js`, `./objects/objects/QuantumEmitter.js`, `./shim/expr/ast-schema.js`, `./objects/objects/actionCompiler.js`, `./objects/v1-replacements.js`, `./objects/objects/slitLinerScatter.js`, `./objects/setup/setupSphereEmitter3D.js`, `./objects/objects/directedRedirectProgram.js`, `./compat/renderer/lo-drawn-center.js`, `./objects/setup/setupTunnelPointExit.js`, `./objects/constants/WallBehaviors.js`, `./objects/objects/absorbRadiation.js`, `./objects/geometry/SlabFace.js` | the 17 part-internal modules a forked part file imports (the `tier: 'part-internal'` rows of `FRQTL_WEB_SPECIFIERS`): not a scene API — the linker admits them only from a `parts/*.js` file — and they move with the parts |
|
package/dist/dist-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/frqtl-node",
|
|
3
|
-
"version": "0.6.1-
|
|
3
|
+
"version": "0.6.1-p3.2",
|
|
4
4
|
"files": {
|
|
5
5
|
"eyes/index.js": "b3563421030bb7fdd277eb697959fbe9107463cbb5478160edc7d887daa67be3",
|
|
6
6
|
"fat/pkg-node/egpt_wasm.js": "5e6af1f6725c93c0e9ad7386f1785cbe639ce596a4caa19ca85729c1f2bea04d",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"frqtl/node-entry.js": "a401039b4fcf53ac620edbd80985aa4448122f2e3caaeaf4d5595f5e9d512b60",
|
|
41
41
|
"frqtl/objects/FrqtlApp.js": "e3232b09eb576427df2a52393a5603b4c084d6edba5667b03afb161fd6d76b9b",
|
|
42
42
|
"frqtl/objects/ObjectFactory.js": "a8f050ce61b7ab9c9b4f81e9eeaebf05c3cc0b532c741ce93d6e952489772b11",
|
|
43
|
-
"frqtl/objects/RecipeContext.js": "
|
|
43
|
+
"frqtl/objects/RecipeContext.js": "4d53e1d8a48f4567aeb882abac6f46cd649a51a3af4b543a6bec7ce74e77d7a8",
|
|
44
44
|
"frqtl/objects/collectors/DataCollector.js": "8736d6cb77eec36ce2adcf2bc3b0d2436eeb70a73b77182ff053efdc6ca071df",
|
|
45
45
|
"frqtl/objects/constants/WallBehaviors.js": "a05c6e06923962e2cc2cf7204131e5d20bf83c5c8e85fbb13867872040345e40",
|
|
46
46
|
"frqtl/objects/geometry/SlabFace.js": "847921b124d3bc1692e88c363c087d392a1baa7c652a3a61e381d0f8a8342e57",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"frqtl/parts/FrqtlControlOverlay.js": "f862b1529265224aa96878b56b0c1c6e65683ba3fd6b517df86e17e92147f9c4",
|
|
114
114
|
"frqtl/parts/FrqtlControlsPanel.js": "792d03dea218bdc335ba7e04362be620edbfa331bed9de087d61942299728cc1",
|
|
115
115
|
"frqtl/parts/FrqtlVision.js": "118d0874249fa55783b196fd01297b3347951267a4add93381433a72c46e8622",
|
|
116
|
-
"frqtl/parts/GraphingDetectorWall.js": "
|
|
116
|
+
"frqtl/parts/GraphingDetectorWall.js": "a2fef24c2ef00416792c4b46a09c9544601e45e84593fa2287d2311f27a1bcee",
|
|
117
117
|
"frqtl/parts/LargeObject.js": "d6cae738b30965f3eba4be4399fb4410f17afc3267b51e3ad9e1ba7423e17922",
|
|
118
118
|
"frqtl/parts/LightColor.js": "8637e5a09bbad3a4fdd2034ccb44dfc1fda743e29b12b4c685a64b9e1df28daa",
|
|
119
119
|
"frqtl/parts/LiveChart.js": "8bd0b862d2f7a4e818ae9cfc47e5e0f56eb97ea0a00fb836bba3f2374c12a1ce",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"frqtl/parts/SphereEmitter.js": "645664f5a20924b777d9b4ad6fef6338e7de251d62c7f8a4c93e6b0f97b47d4a",
|
|
127
127
|
"frqtl/parts/Trace.js": "f9d91bb9435bc5fabfd7bfdbfbf3f3ae34e2338090251d9e9d6c11abc2ed25fb",
|
|
128
128
|
"frqtl/parts/Tunnel.js": "66bfca3d3d88ebf9a8cb7aa248b92cfc7d78a278449830b03e6b2700a21df891",
|
|
129
|
-
"frqtl/parts/Wall.js": "
|
|
129
|
+
"frqtl/parts/Wall.js": "34f8e4f5143925b42e42561eb54e93b0c1244debeb5c07a914beba3e8c4c75fd",
|
|
130
130
|
"frqtl/parts/index.js": "70b8e227e09663889f4c7942d385f843d977579765723f829880fd6e9868ac53",
|
|
131
131
|
"frqtl/scenes/absorbing-wall-3d.js": "722bb433142833f03150052b95039686aba0d9db510a5ff13fad95b5f17e0cf8",
|
|
132
132
|
"frqtl/scenes/absorbing-wall.js": "0a47ca64e56481841339cd5882cf0e54ec7c14d87a4306d016ead1ff26591197",
|
|
@@ -166,12 +166,13 @@
|
|
|
166
166
|
"shim/engine/artifact-shell.js": "f23c327500f8174b12d464868f6ec9c1701e7a9a5faa1c43a2ddafc3191dc38c",
|
|
167
167
|
"shim/engine/boot-phases.js": "c6e12e12824bcf6f1a289f46e41c898a5416c80c131594525145aae9a3eefbfa",
|
|
168
168
|
"shim/engine/engine-stack-boot.js": "0abf57dcff2ca2bb025c787a17875d49d54d9218ac08c78cb9c678dbb6118d08",
|
|
169
|
-
"shim/engine/router.js": "
|
|
170
|
-
"shim/engine/site-graph.js": "
|
|
169
|
+
"shim/engine/router.js": "1c8bed954aecfcd06d1344051d8489f0f040967f9d0879ec7e7a0d1e58045d2c",
|
|
170
|
+
"shim/engine/site-graph.js": "b0d2569c67516d08bba17a152d5d3bfa0b036666eda7f11df74b4d68854e1b93",
|
|
171
171
|
"shim/experiment-frame/index.js": "5a0219aaa225d8f808c405b08623ce85ada37d83fb4f55f983db3303e431e47f",
|
|
172
172
|
"shim/expr/ast-schema.js": "4f1133b887639bbd86fab976292323bf4a955cdc873ff7ca8e8a8eb3b416923f",
|
|
173
173
|
"shim/field-descriptor/index.js": "6647d57d1523519114adc0a5cb66335ec5121c39ab294b9c97bdfb9e5d7f61e0",
|
|
174
174
|
"shim/frame-data/index.js": "df6957b0b4a8e0ee0035bc970647b90089b421ae7f05911cc047d48457164be6",
|
|
175
|
+
"shim/host/device.js": "85470e7c787398cb91516b724e884ac50104d56330ba7ff876308b6fb4b7b532",
|
|
175
176
|
"shim/host/host-view.js": "feec2f5fdc318e75cd01768c24891f85d2694a269696fcfffd9567d31983cf43",
|
|
176
177
|
"shim/host/index.js": "fade4b171218965570aa2e67e841920ebb3d972ff15e1cfa0394fd3fa123d82c",
|
|
177
178
|
"shim/index.js": "96165c6161ffddbace0be0cde0b3a83943298531537dd2cc9c6e281b8274ff19",
|
|
@@ -197,14 +198,15 @@
|
|
|
197
198
|
"shim/page-params/index.js": "c58732b8127da8c2b4ecfd922bf46a6c9a5e9ab7017f41c095a6ec937011f0fa",
|
|
198
199
|
"shim/project/fork.js": "07555f24c7e6d1bbe723853bc09ad570b9cd36cef248a07e98e7398d41ac70da",
|
|
199
200
|
"shim/project/identity.js": "8d3e0bb9df75eecf7d11a1ea7948ff66c559973a891574d0b208c9e8f8faf504",
|
|
200
|
-
"shim/project/index.js": "
|
|
201
|
+
"shim/project/index.js": "5121dd4c18aeea190d2a998fa7a13f86cb36b13cf043f8c549963c1ae51ba26e",
|
|
201
202
|
"shim/project/link.js": "0e3fc878bc45d9916baacd7cf129687ec6e82efd4f6f5036e4697180b3d0f87a",
|
|
202
203
|
"shim/project/manifest.js": "442267af07ae1b977ff6aeaca49d98ce82b77f548cfdd1ed0f258b3429d75f31",
|
|
203
204
|
"shim/project/node-specifier-hook.mjs": "8601edf762bd9cd011632bc681d0589dfc77368cda0e994125d0eb123fea89d0",
|
|
204
205
|
"shim/project/parts-module.js": "b08e0149f086640692e1b2ff8e0d7ae0379c2bc934a0891418de5c2b8c0e8e60",
|
|
206
|
+
"shim/project/parts-spelling.js": "4cc7b8a2f0aa1f3ad4c1c3cf15f74b2860a94ae2214dbef78d31c85ff8916cfe",
|
|
205
207
|
"shim/project/parts-writer.js": "121fa67261d6f366190c9ac4961fceed2f26ca7f35cf3fbffc767829afc69978",
|
|
206
208
|
"shim/project/read-text.js": "b89bd70e3a271e1f42c8ce9427a20cdd9a4c4c9c31b64c49ff1f56b095620136",
|
|
207
|
-
"shim/project/record-format.js": "
|
|
209
|
+
"shim/project/record-format.js": "87d5cc2eae8ff600db1db58a0e0047819f7c9839ff702acdedc366c7647eb0ef",
|
|
208
210
|
"shim/project/register-node.mjs": "9b408da7dc1b11d16237cd62caca281c823ea80173d386ad35e41ea17c34857f",
|
|
209
211
|
"shim/project/specifiers.js": "288e5e06e53b873449190f6f85855177a72e52623bd11eef016acafb8095d829",
|
|
210
212
|
"shim/registry/copy-gate.js": "cfbf52669c849b8837b326f2743458f1ead0720cad51a6c6d409c9bd6c7f9495",
|
|
@@ -213,7 +215,7 @@
|
|
|
213
215
|
"shim/registry/scene-roster.js": "318fe373f71b1c58d05425693f2fad4bd5368f6095b65817c6d637430a0bfb73",
|
|
214
216
|
"shim/registry/scene-source.js": "c9c4b48c24d4b1ebee1549a7891a1ba708909b94b9403ce28eefbd902cacf49f",
|
|
215
217
|
"shim/registry/showcase-link.js": "daf5c1426aeb3c578bace5e0edea9ba2538cd82336d2cdc944e5fed6decd7551",
|
|
216
|
-
"shim/registry/studio-landing.js": "
|
|
218
|
+
"shim/registry/studio-landing.js": "bd81ad9333332de4a29b3121f963edd354450c75c97f4f43170a98c2dcceb7f3",
|
|
217
219
|
"shim/registry/walkthrough.js": "8f8b6f59e9e81c67c9c62f8fb271eee2e4d6e1fba6e116b227a2f1e3f9930230",
|
|
218
220
|
"shim/render-state/apply.js": "94a9e60cb1722c9a82aa00f2f161fd65e6f438147333856a786e42a4ad5b7064",
|
|
219
221
|
"shim/render-state/index.js": "72a6a3dcd8037893cb8fbd7ef32b126770e7efaec70847f8e4f2915ddb690b43",
|
|
@@ -74,6 +74,7 @@ import { ObjectSchema } from './schema/index.js';
|
|
|
74
74
|
import { kindOf } from './kinds.js';
|
|
75
75
|
import { linkProject } from '../../shim/project/link.js';
|
|
76
76
|
import { patchParts, PARTS_EXPORT, entryKeys, entryLiteral } from '../../shim/project/parts-writer.js';
|
|
77
|
+
import { spellRect, spellCenter, captureCenter } from '../../shim/project/parts-spelling.js';
|
|
77
78
|
import { moduleOfKind, PARTS_SPECIFIER } from '../../shim/project/parts-module.js';
|
|
78
79
|
import { forkPartText, FORK_REFUSED } from '../../shim/project/fork.js';
|
|
79
80
|
import { readText } from '../../shim/project/read-text.js';
|
|
@@ -347,47 +348,22 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
347
348
|
var _JSON = function (v) { return JSON.stringify(v); };
|
|
348
349
|
|
|
349
350
|
/**
|
|
350
|
-
* Spell a touched
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* 6-tuple stands. A 4-tuple equal to the baseline's is not written (no literalizing an untouched expression).
|
|
354
|
-
* Mutates and returns `props`.
|
|
351
|
+
* Spell a touched value in the entry's OWN form, reading the form off the held TEXT: the rule is
|
|
352
|
+
* lib/shim/project/parts-spelling.js spellRect / spellCenter (applyRows asks the same rule of a parts block).
|
|
353
|
+
* Each mutates and returns `props`.
|
|
355
354
|
*/
|
|
356
355
|
function _spell(text, name, kind, props, baseline) {
|
|
357
|
-
|
|
358
|
-
if (!derived.length) { return props; }
|
|
359
|
-
var spelled = entryKeys(text, name);
|
|
360
|
-
var R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
|
|
361
|
-
Object.keys(props).forEach(function (f) {
|
|
362
|
-
var v = props[f];
|
|
363
|
-
var rows = derived.filter(function (d) { return d.field === f && d.tuple === 6 && spelled.indexOf(d.name) !== -1; });
|
|
364
|
-
if (!rows.length || !Array.isArray(v) || v.length !== 6) { return; }
|
|
365
|
-
var b = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f] : null;
|
|
366
|
-
var four = R4.map(function (k) { return v[R6.indexOf(k)]; });
|
|
367
|
-
if (b && _JSON(four) === _JSON(R4.map(function (k) { return b[R6.indexOf(k)]; }))) { delete props[f]; }
|
|
368
|
-
else { props[f] = four; }
|
|
369
|
-
rows.forEach(function (d) {
|
|
370
|
-
var nv = v[R6.indexOf(d.key)];
|
|
371
|
-
if (!b || nv !== b[R6.indexOf(d.key)]) { props[d.name] = nv; }
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
return props;
|
|
356
|
+
return spellRect(props, { kind: kind, baseline: baseline, entryKeys: function () { return entryKeys(text, name); } });
|
|
375
357
|
}
|
|
376
358
|
|
|
377
|
-
/**
|
|
378
|
-
* Spell a touched `center` in the entry's OWN arity: two numbers when the entry
|
|
379
|
-
* spells two (a literal, or an array expression — entryLiteral's `length`) and the live z is 0; otherwise the
|
|
380
|
-
* live [x, y, z] (an entry spelled three, a 3D hand off z 0, an entry whose center is not an array). The
|
|
381
|
-
* read-back (_verifyWritten) guards every case. Mutates and returns `props`.
|
|
382
|
-
*/
|
|
383
359
|
function _spellCenter(text, name, props) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
360
|
+
return spellCenter(props, {
|
|
361
|
+
entryKeys: function () { return entryKeys(text, name); },
|
|
362
|
+
centerLength: function () {
|
|
363
|
+
var r = entryLiteral(text, name, 'center');
|
|
364
|
+
return r.literal ? (Array.isArray(r.value) ? r.value.length : null) : (r.length === undefined ? null : r.length);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
391
367
|
}
|
|
392
368
|
|
|
393
369
|
/**
|
|
@@ -403,8 +379,8 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
403
379
|
Object.keys(written).forEach(function (k) {
|
|
404
380
|
var r = entryLiteral(text, name, k);
|
|
405
381
|
decl[k] = r.literal ? r.value : undefined;
|
|
406
|
-
// A two-number center declares z 0
|
|
407
|
-
if (k === 'center' && Array.isArray(decl[k]) && decl[k].length === 2) { decl[k] = decl[k]
|
|
382
|
+
// A two-number center declares z 0: read it in its capture form through the one inverse.
|
|
383
|
+
if (k === 'center' && Array.isArray(decl[k]) && decl[k].length === 2) { decl[k] = captureCenter(decl[k]); }
|
|
408
384
|
});
|
|
409
385
|
function refuse(k, declared, liveV) {
|
|
410
386
|
refused.push({ op: 'set', name: name, key: k, reason: 'the written text declares ' + _JSON(declared) + ' but the live object is ' +
|
|
@@ -651,8 +627,9 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
651
627
|
* `center` patch sets where the entry STARTS. The loop's run state is kept. REFUSES (throws) while a hand
|
|
652
628
|
* made since the last toSketch is unwritten (write it first: await exp.toSketch()), and a `name` patch (the
|
|
653
629
|
* key is the name). `from` and `to` are the patched keys' capture (ObjectSchema.captureProps) before and after the
|
|
654
|
-
* re-run — one form on both sides (a z-slab wall's rect is 6 numbers even when the patch typed 4)
|
|
655
|
-
*
|
|
630
|
+
* re-run — one form on both sides (a z-slab wall's rect is 6 numbers even when the patch typed 4). The patch is
|
|
631
|
+
* written in the entry's own spelling (a rect beside `z`/`depth` as 4 numbers plus those keys; a `center` in the
|
|
632
|
+
* arity the entry spells, three numbers when z is not 0), as a hand's is. With a Record open, its `setDeclared` row carries the same `from`/`to`, written after the re-run.
|
|
656
633
|
*/
|
|
657
634
|
RecipeContext.prototype.setDeclared = async function (name, patch, opts) {
|
|
658
635
|
_requireWorld(this);
|
|
@@ -696,7 +673,10 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
696
673
|
from[k] = live[k] === undefined ? null : live[k];
|
|
697
674
|
});
|
|
698
675
|
var entry = this._project.entry;
|
|
676
|
+
// The patch is written in the entry's own spelling, as a hand's is (toSketch's order: rect, then center).
|
|
699
677
|
var spelled = _spell(this._project.files[entry], name, kind, _plain(patch), live);
|
|
678
|
+
if (Array.isArray(spelled.center)) { spelled.center = captureCenter(spelled.center); }
|
|
679
|
+
_spellCenter(this._project.files[entry], name, spelled);
|
|
700
680
|
var r = patchParts(this._project.files[entry], [{ op: 'set', name: name, props: spelled }], { entry: entry });
|
|
701
681
|
if (r.refused.length) {
|
|
702
682
|
throw new Error('[RecipeContext.setDeclared] the sketch text refused the patch: ' +
|
|
@@ -716,7 +696,7 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
716
696
|
runner.cameraControl.setOrbit(pose);
|
|
717
697
|
kept.push('cameraPose');
|
|
718
698
|
}
|
|
719
|
-
// `to` is what the re-run declared, in the capture form `from` is in (the
|
|
699
|
+
// `to` is what the re-run declared, in the capture form `from` is in (the text holds the entry's spelling).
|
|
720
700
|
var rebuilt = null;
|
|
721
701
|
this._parts.forEach(function (row, o) { if (row.key === name) { rebuilt = o; } });
|
|
722
702
|
if (!rebuilt) {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* colour washed under the bins, so an un-hit face still reads as a plate and
|
|
37
37
|
* intensity rises monotonically from empty through every bin.
|
|
38
38
|
*
|
|
39
|
-
* Readouts: `getCollisionHistogram()` / `resetHistogram()` / `
|
|
39
|
+
* Readouts: `getCollisionHistogram()` / `resetHistogram()` / `getArrivals()` —
|
|
40
40
|
* inherited from `Wall`, UNCHANGED. `heatCanvas()` returns the ferry canvas.
|
|
41
41
|
*
|
|
42
42
|
* ── ESTIMATION BEFORE RUNNING: when will this plate have counted anything? ───
|
package/dist/frqtl/parts/Wall.js
CHANGED
|
@@ -774,14 +774,26 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
|
|
|
774
774
|
return out;
|
|
775
775
|
};
|
|
776
776
|
|
|
777
|
+
/** Does the op-program this wall's LargeObject is RUNNING remove the frame it triggers
|
|
778
|
+
* on (`set 0.is_alive = …`)? Read off the program itself — the one statement of what
|
|
779
|
+
* the wall does — never a hand list of behavior names (today: destroy and absorb). On
|
|
780
|
+
* such a wall a frame's listing in `tickData.objects[handle].frames` is not one
|
|
781
|
+
* arrival: the engine collects every frame in the mb_w-padded search rect
|
|
782
|
+
* (ENGINE_CONTRACT §5.1), so a frame is listed alive through its approach ticks and
|
|
783
|
+
* once more on its kill tick. */
|
|
784
|
+
function _runningProgramRemoves(wall) {
|
|
785
|
+
var prog = wall.lo && wall.lo.program;
|
|
786
|
+
return !!(prog && Array.isArray(prog.ops) &&
|
|
787
|
+
prog.ops.some(function (op) { return op && op.set === '0.is_alive'; }));
|
|
788
|
+
}
|
|
789
|
+
|
|
777
790
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
* thermostat's activeQuanta read).
|
|
791
|
+
* ZONE-OCCUPANCY readout (loop-polish B): the number of frames THIS wall's
|
|
792
|
+
* engine collect pass listed on the sampled tick — every frame in its search rect
|
|
793
|
+
* (the rect padded by the dimension's mb_w). For a FULL-AREA background wall
|
|
794
|
+
* (collisionBehavior 'nothing' + the count hook) that listing IS the current live
|
|
795
|
+
* population inside the wall — the ONE canonical "how many live" authority (the
|
|
796
|
+
* emission-cap gate and the LiveChart `liveCount` projector read it).
|
|
785
797
|
*
|
|
786
798
|
* Layer 1 (API): dual-lane, mirroring the LiveChart PROJECTOR_SPEC shape —
|
|
787
799
|
* getLiveCount(tickData) — SAMPLE lane (per tick): read
|
|
@@ -794,12 +806,23 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
|
|
|
794
806
|
* COUNT increment only (bench LO7); counting never moves, kills, or recolors a
|
|
795
807
|
* frame (SU1).
|
|
796
808
|
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
809
|
+
* Guards: THROWS on a REMOVING wall (destroy / absorb) — there the listing reads
|
|
810
|
+
* (1 + approach-halo ticks) x arrivals, never the arrivals; `getArrivals` is the
|
|
811
|
+
* reader. THROWS on a wall without the count hook (no collected-frames buffer → it
|
|
812
|
+
* would silently report 0 forever).
|
|
799
813
|
* @param {Object} [tickData] — the universe.doTick() result (sample lane).
|
|
800
814
|
* @returns {number}
|
|
801
815
|
*/
|
|
802
816
|
Wall.prototype.getLiveCount = function (tickData) {
|
|
817
|
+
if (_runningProgramRemoves(this)) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
'[Wall.getLiveCount] this wall REMOVES what it hits (collisionBehavior "' +
|
|
820
|
+
this.collisionBehavior + '") — getLiveCount is the zone-occupancy reader of a ' +
|
|
821
|
+
'non-removing wall, and on a removing wall it would count each frame once per ' +
|
|
822
|
+
'approach tick plus once on its kill tick. Read arrivals with ' +
|
|
823
|
+
'wall.getArrivals(tickData) → { total, delta } (the engine\'s own per-wall count).'
|
|
824
|
+
);
|
|
825
|
+
}
|
|
803
826
|
var counts = Array.isArray(this.hooks) && this.hooks.indexOf('count') !== -1;
|
|
804
827
|
if (!counts) {
|
|
805
828
|
throw new Error(
|
|
@@ -818,6 +841,70 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
|
|
|
818
841
|
return this._liveCount || 0;
|
|
819
842
|
};
|
|
820
843
|
|
|
844
|
+
/**
|
|
845
|
+
* ARRIVALS readout: how many frames have triggered THIS wall's collision program —
|
|
846
|
+
* read from the engine's own per-wall counter, `tickData.objects[handle].count`, the
|
|
847
|
+
* contract's one read authority for an LO count (ENGINE_CONTRACT §4.3). The `count`
|
|
848
|
+
* hook compiles `set 1.count = 1.count + 1` behind the trigger, so the engine counts
|
|
849
|
+
* one per trigger event; on a removing wall (destroy / absorb) each trigger is the
|
|
850
|
+
* frame's removal, so the count IS the arrivals. Never derived from the collected
|
|
851
|
+
* frame listing (that is the padded search-rect census — see getLiveCount).
|
|
852
|
+
*
|
|
853
|
+
* Layer 1 (API): dual-lane, the same shape as getLiveCount —
|
|
854
|
+
* getArrivals(tickData) — SAMPLE lane: read the engine count and return
|
|
855
|
+
* `{ total, delta }`: `total` = cumulative arrivals since the wall registered,
|
|
856
|
+
* `delta` = the change since the previously sampled TickData (this tick's
|
|
857
|
+
* arrivals when sampled every tick). IDEMPOTENT per TickData — sampling the same
|
|
858
|
+
* TickData twice returns the same `{ total, delta }`.
|
|
859
|
+
* getArrivals() — READ lane: the last sample (`{ total: 0, delta: 0 }` before the
|
|
860
|
+
* first tick).
|
|
861
|
+
*
|
|
862
|
+
* Layer 2 (physics): zero physics — a pure reader of a counter the compiled program
|
|
863
|
+
* already maintains (bench LO7); reading it moves nothing (SU1).
|
|
864
|
+
*
|
|
865
|
+
* Guards: THROWS on a wall without the count hook (the engine counter never moves →
|
|
866
|
+
* it would silently report 0 forever), and on a TickData that is not the one this
|
|
867
|
+
* wall's universe last produced (`universe._lastTickData`, the object `doTick()` returned
|
|
868
|
+
* and every tick listener receives). LO handles restart at 0 in every universe, so a
|
|
869
|
+
* TickData from another world indexes a different wall's count at the same handle; and
|
|
870
|
+
* an earlier TickData of this world would yield a wrong delta.
|
|
871
|
+
* @param {Object} [tickData] — the universe.doTick() result (sample lane).
|
|
872
|
+
* @returns {{ total: number, delta: number }}
|
|
873
|
+
*/
|
|
874
|
+
Wall.prototype.getArrivals = function (tickData) {
|
|
875
|
+
var counts = Array.isArray(this.hooks) && this.hooks.indexOf('count') !== -1;
|
|
876
|
+
if (!counts) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
'[Wall.getArrivals] this wall does not COUNT (collisionBehavior "' +
|
|
879
|
+
this.collisionBehavior + '", hooks ' + JSON.stringify(this.hooks || []) +
|
|
880
|
+
') — the engine increments a wall\'s count only through the `count` hook; ' +
|
|
881
|
+
'add hooks: [\'count\'] (preset \'detector-screen\' = destroy + count).'
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (!this._arrivals) { this._arrivals = { td: null, total: 0, delta: 0 }; }
|
|
885
|
+
var a = this._arrivals;
|
|
886
|
+
if (tickData != null && tickData !== a.td) {
|
|
887
|
+
if (tickData !== this.universe._lastTickData) {
|
|
888
|
+
throw new Error('[Wall.getArrivals] "' + this.name + '": this is not the TickData its ' +
|
|
889
|
+
'own universe last produced (universe._lastTickData) — it is another world\'s, ' +
|
|
890
|
+
'where handle ' + this.lo.handle + ' is a different wall, or an earlier tick of ' +
|
|
891
|
+
'this one. Sample the TickData this wall\'s universe.doTick() just returned. (FAIL-LOUD.)');
|
|
892
|
+
}
|
|
893
|
+
var handle = this.lo.handle;
|
|
894
|
+
var oc = tickData.objects && tickData.objects[handle];
|
|
895
|
+
if (!oc || typeof oc.count !== 'number') {
|
|
896
|
+
throw new Error('[Wall.getArrivals] "' + this.name + '": this world\'s TickData carries no ' +
|
|
897
|
+
'objects[' + handle + '].count — every registered LargeObject is marshalled each ' +
|
|
898
|
+
'tick, so the wall is not registered here. (FAIL-LOUD.)');
|
|
899
|
+
}
|
|
900
|
+
var total = oc.count;
|
|
901
|
+
a.delta = total - a.total;
|
|
902
|
+
a.total = total;
|
|
903
|
+
a.td = tickData;
|
|
904
|
+
}
|
|
905
|
+
return { total: a.total, delta: a.delta };
|
|
906
|
+
};
|
|
907
|
+
|
|
821
908
|
/**
|
|
822
909
|
* ABSORB v2 readout: the wall's live { stored, absorbedTotal, radiatedTotal } (the
|
|
823
910
|
* REAL backing-frame mass ledger). Throws on a non-absorb wall. Call between ticks.
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
'use strict';
|
|
37
37
|
|
|
38
|
-
// The static site's shape (graph dir, build stamp, build id, entry page) — owned by site-graph.js,
|
|
39
|
-
// there by every consumer; the router uses them and does not re-export them.
|
|
40
|
-
import { SITE_GRAPH_DIR, SITE_BUILD_ATTR, SITE_BUILD_ID_RE, STATIC_APP_PAGE } from './site-graph.js';
|
|
38
|
+
// The static site's shape (graph dir, build stamp, build id, entry page, package placements) — owned by site-graph.js,
|
|
39
|
+
// imported from there by every consumer; the router uses them and does not re-export them.
|
|
40
|
+
import { SITE_GRAPH_DIR, SITE_BUILD_ATTR, SITE_BUILD_ID_RE, STATIC_APP_PAGE, SITE_PLACEMENTS } from './site-graph.js';
|
|
41
41
|
|
|
42
42
|
// ── Path profiles ────────────────────────────────────────────────────────────
|
|
43
43
|
//
|
|
@@ -152,8 +152,8 @@ var DIST_PROFILE = buildDistProfile('../..');
|
|
|
152
152
|
// lib/shim/engine/test/router.test.mjs asserts them against the two
|
|
153
153
|
// package.json#version fields so a forgotten bump fails `make verify`, not a
|
|
154
154
|
// silent stale-CDN-URL bug.
|
|
155
|
-
var CDN_SDK_VERSION = '0.6.1-
|
|
156
|
-
var CDN_MATH_SDK_VERSION = '0.3.
|
|
155
|
+
var CDN_SDK_VERSION = '0.6.1-p3.2'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
|
|
156
|
+
var CDN_MATH_SDK_VERSION = '0.3.3-p2.1'; // sdk/egpt-math-sdk/package.json#version — bound by router.test.mjs; bumped with each npm publish
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
159
|
* The jsdelivr dist root of @descix/frqtl-web at `version` — the ONE spelling of the package's CDN URL.
|
|
@@ -270,39 +270,65 @@ var CDN_PROFILE = buildCdnProfile(CDN_BASE);
|
|
|
270
270
|
// THE DELIBERATE DIVERGENCE FROM CDN_PROFILE (design §A.3 / §E flag 4):
|
|
271
271
|
// CDN_PROFILE's math + notebook bases are hardwired CROSS-ORIGIN at CDN_MATH_BASE; the
|
|
272
272
|
// static-site semantic is "everything self-contained under ONE origin prefix": the math SDK
|
|
273
|
-
// is co-served at
|
|
274
|
-
// frqtl-web dist's OWN copy at
|
|
273
|
+
// is co-served at its SITE_PLACEMENTS directory under graphBase and the curated notebooks ride the
|
|
274
|
+
// frqtl-web dist's OWN copy at '<its placement>/notebooks'. A DISTINCT profile here is the
|
|
275
275
|
// correct DRY call — the SAME justification that keeps CDN_PROFILE distinct from DIST_PROFILE.
|
|
276
276
|
//
|
|
277
|
-
// buildStaticProfile(graphBase, entryBase) — every GRAPH key anchors at
|
|
278
|
-
//
|
|
279
|
-
//
|
|
277
|
+
// buildStaticProfile(graphBase, entryBase) — every GRAPH key anchors at its package's placement
|
|
278
|
+
// under `graphBase` (site-graph.js::SITE_PLACEMENTS — the directory fact; the router stays the URL
|
|
279
|
+
// authority); the general 'experiments/' key (the experiment ENTRY pages STUDIO/GYM frame) anchors
|
|
280
|
+
// at `entryBase`. Both are required: a host whose entries and graph share one directory passes
|
|
280
281
|
// the same base twice.
|
|
282
|
+
|
|
283
|
+
// The two packages the static profile places (site-graph.js::SITE_PLACEMENTS rows); router.test.mjs binds them to the
|
|
284
|
+
// sdk/*/package.json names and to the CDN bases above. The CDN URLs stay literal: G-PIN (scripts/test/cdn-pins.test.mjs)
|
|
285
|
+
// reads them as text.
|
|
286
|
+
var FRQTL_WEB_PACKAGE_NAME = '@descix/frqtl-web';
|
|
287
|
+
var MATH_SDK_PACKAGE = '@descix/egpt-math-sdk';
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The URL of `pkg`'s placement under `graphBase` (no trailing slash). THROWS for a package no
|
|
291
|
+
* SITE_PLACEMENTS row places.
|
|
292
|
+
* @param {string} graphBase
|
|
293
|
+
* @param {string} pkg
|
|
294
|
+
* @returns {string}
|
|
295
|
+
*/
|
|
296
|
+
function placedBase(graphBase, pkg) {
|
|
297
|
+
var row = SITE_PLACEMENTS.find(function (r) { return r.package === pkg; });
|
|
298
|
+
if (!row) {
|
|
299
|
+
throw new Error('[engineRouter] no site-graph.js::SITE_PLACEMENTS row places ' + pkg + ' (rows: ' +
|
|
300
|
+
SITE_PLACEMENTS.map(function (r) { return r.package; }).join(', ') + ').');
|
|
301
|
+
}
|
|
302
|
+
return row.dir ? graphBase + '/' + row.dir : graphBase;
|
|
303
|
+
}
|
|
304
|
+
|
|
281
305
|
function buildStaticProfile(graphBase, entryBase) {
|
|
282
306
|
if (!graphBase || !entryBase) {
|
|
283
307
|
throw new Error('[engineRouter] buildStaticProfile(graphBase, entryBase) needs both bases (got ' +
|
|
284
308
|
JSON.stringify(graphBase) + ', ' + JSON.stringify(entryBase) + ') — the graph and the entry ' +
|
|
285
309
|
'pages are anchored separately; a co-served host passes the same base twice.');
|
|
286
310
|
}
|
|
311
|
+
var webBase = placedBase(graphBase, FRQTL_WEB_PACKAGE_NAME);
|
|
312
|
+
var mathBase = placedBase(graphBase, MATH_SDK_PACKAGE);
|
|
287
313
|
return {
|
|
288
314
|
// THE package root: the GRAPH (immutable per build) at its lib-identity placement.
|
|
289
|
-
'__frqtlRoot':
|
|
290
|
-
'assets/models/':
|
|
315
|
+
'__frqtlRoot': webBase + '/',
|
|
316
|
+
'assets/models/': webBase + '/js/models/',
|
|
291
317
|
// The experiment pages are ENTRIES: resolve('experiments/{id}/index.html') is the fixed page,
|
|
292
318
|
// which itself points into the graph.
|
|
293
319
|
'experiments/': entryBase + '/experiments/',
|
|
294
320
|
// Math + notebook lanes anchored SAME-ORIGIN into the co-served payload —
|
|
295
321
|
// the deliberate divergence from CDN_PROFILE. See the factory docstring.
|
|
296
|
-
'__mathSdkUrl':
|
|
297
|
-
'__mathWasmUrl':
|
|
298
|
-
'__mathWasmModuleUrl':
|
|
322
|
+
'__mathSdkUrl': mathBase + '/index.js',
|
|
323
|
+
'__mathWasmUrl': mathBase + '/wasm/egpt_math_sdk_wasm_bg.wasm',
|
|
324
|
+
'__mathWasmModuleUrl': mathBase + '/wasm/egpt_math_sdk_wasm.js',
|
|
299
325
|
'__mathBackend': 'wasm-math',
|
|
300
326
|
// Notebooks come from the frqtl-web dist's OWN copy (build.js copyDir
|
|
301
327
|
// lib/notebooks → dist/notebooks) — NOT under egpt-math-sdk (design §A.3/§B.1).
|
|
302
|
-
'__notebookBase':
|
|
328
|
+
'__notebookBase': webBase + '/notebooks',
|
|
303
329
|
// Vendor import-map root — frqtl-web's OWN dist/vendor/ (same as CDN, self-
|
|
304
330
|
// sufficient), just same-origin instead of jsdelivr.
|
|
305
|
-
'__vendorBase':
|
|
331
|
+
'__vendorBase': webBase + '/vendor'
|
|
306
332
|
};
|
|
307
333
|
}
|
|
308
334
|
|
|
@@ -500,7 +526,7 @@ var _assertProfileParity = (function () {
|
|
|
500
526
|
* // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
|
|
501
527
|
* const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
|
|
502
528
|
* console.log(cdnRouter.resolve('compat/engine.js'));
|
|
503
|
-
* // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.1-
|
|
529
|
+
* // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.1-p3.2/dist/frqtl/compat/engine.js'
|
|
504
530
|
*
|
|
505
531
|
* // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
|
|
506
532
|
* const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* site-graph.js — the shape of a static FRQTL site: the names a site's documents carry
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
4
|
+
* site-graph.js — the shape of a static FRQTL site: the names a site's documents carry, the directory its
|
|
5
|
+
* build-stamped graph sits in, which published package owns which part of that graph, and the ONE rule that says
|
|
6
|
+
* whether a served graph is those packages' bytes. The ONE owner and the ONE import door of each:
|
|
7
|
+
* lib/shim/engine/router.js and every other consumer import them from here (the router never re-exports them), and a
|
|
8
|
+
* node consumer that reads a SERVED site (a publisher's gate) imports them through the package export
|
|
9
|
+
* `@descix/frqtl-web/site-graph` (or `@descix/frqtl-node/site-graph`), never typing them.
|
|
9
10
|
*
|
|
10
11
|
* SITE_GRAPH_DIR 'pub' — the site directory holding the fixed entries and, under
|
|
11
12
|
* SITE_GRAPH_DIR/<build>/, the build-stamped module graph
|
|
12
13
|
* SITE_BUILD_ATTR the <html> attribute of the IDE document carrying <build>
|
|
13
14
|
* SITE_BUILD_ID_RE a <build>: the 8-hex-digit short sha of the commit the bundle was built from
|
|
15
|
+
* siteBuildStampOf the ONE reader of that stamp
|
|
14
16
|
* STATIC_APP_PAGE the IDE bundle's entry document, beside the publisher's front page
|
|
15
17
|
* HOST_PROVIDED_ATTR the attribute declaring a file host-provided (the publisher vendors it beside the page)
|
|
18
|
+
* SITE_PLACEMENTS each published package placed in the graph, and the graph directory it is placed at
|
|
19
|
+
* SITE_ENTRY_PAGES, isSiteEntry
|
|
20
|
+
* the @descix/frqtl-web dist's HTML ENTRIES (reached by navigation or iframe, never loaded as a
|
|
21
|
+
* resource): the site keeps them at fixed URLs under SITE_GRAPH_DIR/, outside the stamped graph
|
|
22
|
+
* siteGraphFaults the served graph against the placed releases' dist-manifest.json records (empty = faithful)
|
|
16
23
|
* SITE_GRAPH_EXPORTS the published names (the parity gate holds both packages' ./site-graph to this table)
|
|
17
24
|
*
|
|
18
25
|
* Host-neutral ESM with no imports: it loads unchanged in node and in a page.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
/** The names `./site-graph` publishes. */
|
|
22
|
-
export const SITE_GRAPH_EXPORTS = Object.freeze([
|
|
29
|
+
export const SITE_GRAPH_EXPORTS = Object.freeze([
|
|
30
|
+
'SITE_GRAPH_DIR', 'SITE_BUILD_ATTR', 'SITE_BUILD_ID_RE', 'siteBuildStampOf', 'STATIC_APP_PAGE', 'HOST_PROVIDED_ATTR',
|
|
31
|
+
'SITE_PLACEMENTS', 'SITE_ENTRY_PAGES', 'isSiteEntry', 'siteGraphFaults',
|
|
32
|
+
]);
|
|
23
33
|
|
|
24
34
|
/** The static site's graph directory name (the entries sit in it, the graph under its build-stamped child). */
|
|
25
35
|
export const SITE_GRAPH_DIR = 'pub';
|
|
@@ -30,6 +40,20 @@ export const SITE_BUILD_ATTR = 'data-frqtl-site-build';
|
|
|
30
40
|
/** A build id: the 8-hex-digit short sha of the commit the bundle was built from. */
|
|
31
41
|
export const SITE_BUILD_ID_RE = /^[0-9a-f]{8}$/;
|
|
32
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The build stamp an HTML document carries on <html SITE_BUILD_ATTR>, or null when absent.
|
|
45
|
+
* THROWS on more than one stamp. The ONE reader of the stamp.
|
|
46
|
+
* @param {string} html
|
|
47
|
+
* @returns {string|null}
|
|
48
|
+
*/
|
|
49
|
+
export function siteBuildStampOf(html) {
|
|
50
|
+
const stamps = [...html.matchAll(new RegExp('<html\\b[^>]*\\b' + SITE_BUILD_ATTR + '="([^"]*)"', 'gi'))].map((m) => m[1]);
|
|
51
|
+
if (stamps.length > 1) {
|
|
52
|
+
throw new Error('[site-graph] the document carries ' + stamps.length + ' ' + SITE_BUILD_ATTR + ' stamps: ' + stamps.join(', '));
|
|
53
|
+
}
|
|
54
|
+
return stamps.length ? stamps[0] : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/**
|
|
34
58
|
* The static IDE bundle's entry document, beside its front page. The bundle ships THIS name and no
|
|
35
59
|
* index.html: a site's index.html is the publisher's front page (lib/shim/host/host-view.js::HOST_FRONT_PAGE).
|
|
@@ -42,3 +66,119 @@ export const STATIC_APP_PAGE = 'app.html';
|
|
|
42
66
|
* the bundle) and the publisher (the file must be PRESENT beside the page) read the set from this attribute.
|
|
43
67
|
*/
|
|
44
68
|
export const HOST_PROVIDED_ATTR = 'data-frqtl-host-provided';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Each published package the site places in its graph, and the graph-relative directory it is placed at ('' = the
|
|
72
|
+
* graph root). The router derives every same-origin URL into a package from its row; a publisher checks each
|
|
73
|
+
* placement against that package's installed `<package>/dist-manifest.json`.
|
|
74
|
+
*/
|
|
75
|
+
export const SITE_PLACEMENTS = Object.freeze([
|
|
76
|
+
Object.freeze({ package: '@descix/frqtl-web', dir: '' }),
|
|
77
|
+
Object.freeze({ package: '@descix/egpt-math-sdk', dir: 'egpt-math-sdk' }),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The @descix/frqtl-web dist's HTML entry pages written by its entry writer (dist-root-relative). The experiment pages
|
|
82
|
+
* (experiments/{id}/index.html) are entries too; isSiteEntry names both.
|
|
83
|
+
*/
|
|
84
|
+
export const SITE_ENTRY_PAGES = Object.freeze(['sdk.html', 'artifact/sun.html', 'artifact/double-slit.html']);
|
|
85
|
+
const EXPERIMENT_ENTRY = /^experiments\/[^/]+\/index\.html$/;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Is this graph-relative path an entry page (which the site serves at SITE_GRAPH_DIR/<path>, never in the graph)?
|
|
89
|
+
* @param {string} rel
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
export function isSiteEntry(rel) {
|
|
93
|
+
return SITE_ENTRY_PAGES.indexOf(rel) !== -1 || EXPERIMENT_ENTRY.test(rel);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Is a served site graph the placed releases' bytes? PURE: the caller enumerates and hashes.
|
|
98
|
+
*
|
|
99
|
+
* @param {{graph: Object<string,string>, entries: Set<string>}} served
|
|
100
|
+
* graph graph-relative path → sha256 hex, for EVERY file under SITE_GRAPH_DIR/<build>/
|
|
101
|
+
* entries the paths present under SITE_GRAPH_DIR/ outside the stamped directory (SITE_GRAPH_DIR-relative)
|
|
102
|
+
* @param {Array<{dir: string, name: string, version: string, files: Object<string,string>,
|
|
103
|
+
* manifest: {file: string, sha256: string}}>} releases
|
|
104
|
+
* one per SITE_PLACEMENTS row: its `dir`, the release's dist-manifest.json `name`, `version` and `files`, and the
|
|
105
|
+
* manifest file itself (its dist-relative name and the sha256 of its bytes — the record no manifest lists).
|
|
106
|
+
* @returns {string[]} the faults, in a stable order (empty = the graph IS the releases):
|
|
107
|
+
* - two placements at one dir, or a manifest key that falls under another placement's dir;
|
|
108
|
+
* - a graph file with no owning placement (longest dir prefix), not listed by its owner, or of another sha256;
|
|
109
|
+
* - a placement's manifest absent from the graph, or not the release's bytes;
|
|
110
|
+
* - a listed file absent from the graph, unless it is an entry (isSiteEntry) present among `entries`;
|
|
111
|
+
* - an entry still inside the graph (each entry has ONE URL).
|
|
112
|
+
*/
|
|
113
|
+
export function siteGraphFaults(served, releases) {
|
|
114
|
+
const graph = served && served.graph;
|
|
115
|
+
const entries = served && served.entries;
|
|
116
|
+
if (!graph || typeof graph !== 'object' || !(entries instanceof Set) || !Array.isArray(releases) || releases.length === 0) {
|
|
117
|
+
throw new Error('[site-graph] siteGraphFaults({graph: {path: sha256}, entries: Set}, releases[]) — got ' +
|
|
118
|
+
JSON.stringify({ graph: typeof graph, entries: Object.prototype.toString.call(entries), releases: Array.isArray(releases) ? releases.length : typeof releases }) + '.');
|
|
119
|
+
}
|
|
120
|
+
for (const r of releases) {
|
|
121
|
+
if (!r || typeof r.dir !== 'string' || !r.files || typeof r.files !== 'object' || !r.manifest ||
|
|
122
|
+
typeof r.manifest.file !== 'string' || typeof r.manifest.sha256 !== 'string') {
|
|
123
|
+
throw new Error('[site-graph] siteGraphFaults: each release is {dir, name, version, files, manifest: {file, sha256}} — got ' +
|
|
124
|
+
JSON.stringify(r && { dir: r.dir, name: r.name, version: r.version, manifest: r.manifest }) + '.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const placed = (r, key) => (r.dir ? r.dir + '/' + key : key);
|
|
128
|
+
const label = (r) => r.name + '@' + r.version + (r.dir ? ' (at ' + r.dir + '/)' : ' (at the graph root)');
|
|
129
|
+
const ownerOf = (rel) => {
|
|
130
|
+
let best = null;
|
|
131
|
+
for (const r of releases) {
|
|
132
|
+
if ((r.dir === '' || rel.startsWith(r.dir + '/')) && (!best || r.dir.length > best.dir.length)) { best = r; }
|
|
133
|
+
}
|
|
134
|
+
return best;
|
|
135
|
+
};
|
|
136
|
+
const faults = [];
|
|
137
|
+
|
|
138
|
+
// 1. The placements are disjoint.
|
|
139
|
+
const dirs = releases.map((r) => r.dir);
|
|
140
|
+
for (const d of dirs) {
|
|
141
|
+
if (dirs.indexOf(d) !== dirs.lastIndexOf(d)) { faults.push('two placements at one dir: ' + JSON.stringify(d)); break; }
|
|
142
|
+
}
|
|
143
|
+
for (const r of releases) {
|
|
144
|
+
for (const key of Object.keys(r.files).sort()) {
|
|
145
|
+
const o = ownerOf(placed(r, key));
|
|
146
|
+
if (o !== r) { faults.push(label(r) + ' lists ' + key + ', which falls under ' + label(o) + ' — the placements overlap'); }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. Every graph file is its owner's byte.
|
|
151
|
+
for (const rel of Object.keys(graph).sort()) {
|
|
152
|
+
const o = ownerOf(rel);
|
|
153
|
+
if (!o) { faults.push('graph file ' + rel + ' has no owning placement'); continue; }
|
|
154
|
+
const key = o.dir ? rel.slice(o.dir.length + 1) : rel;
|
|
155
|
+
if (key === o.manifest.file) {
|
|
156
|
+
if (graph[rel] !== o.manifest.sha256) {
|
|
157
|
+
faults.push('graph file ' + rel + ' is ' + graph[rel] + ', not ' + label(o) + '\'s integrity record (' + o.manifest.sha256 + ')');
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!Object.prototype.hasOwnProperty.call(o.files, key)) {
|
|
162
|
+
faults.push('graph file ' + rel + ' is not listed by ' + label(o));
|
|
163
|
+
} else if (o.files[key] !== graph[rel]) {
|
|
164
|
+
faults.push('graph file ' + rel + ' is ' + graph[rel] + ', ' + label(o) + ' lists ' + o.files[key]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 3. Every listed file is served: in the graph, or (an entry) at its fixed URL and NOT in the graph.
|
|
169
|
+
for (const r of releases) {
|
|
170
|
+
const m = placed(r, r.manifest.file);
|
|
171
|
+
if (!Object.prototype.hasOwnProperty.call(graph, m)) { faults.push(label(r) + '\'s ' + r.manifest.file + ' is absent from the graph'); }
|
|
172
|
+
for (const key of Object.keys(r.files).sort()) {
|
|
173
|
+
const rel = placed(r, key);
|
|
174
|
+
const inGraph = Object.prototype.hasOwnProperty.call(graph, rel);
|
|
175
|
+
if (isSiteEntry(rel)) {
|
|
176
|
+
if (inGraph) { faults.push('entry ' + rel + ' (' + label(r) + ') is still inside the graph — an entry has one URL, at ' + SITE_GRAPH_DIR + '/' + rel); }
|
|
177
|
+
if (!entries.has(rel)) { faults.push('entry ' + rel + ' (' + label(r) + ') is absent from ' + SITE_GRAPH_DIR + '/'); }
|
|
178
|
+
} else if (!inGraph) {
|
|
179
|
+
faults.push(label(r) + ' lists ' + key + ' but it is absent from the graph');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return faults;
|
|
184
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* device.js — the ONE rule for "this document is open on a phone".
|
|
5
|
+
*
|
|
6
|
+
* MOBILE_DEVICE_QUERY the media query: a coarse primary pointer (a finger, not a mouse) on a viewport no wider
|
|
7
|
+
* than the widest phone held in landscape (932 CSS px, the iPhone Pro Max). A tablet in
|
|
8
|
+
* portrait is narrower than that and counts as a phone; a touch laptop has a fine pointer
|
|
9
|
+
* and does not.
|
|
10
|
+
* isMobileDevice(win) win.matchMedia(MOBILE_DEVICE_QUERY).matches — evaluated in the window that LAYS OUT the
|
|
11
|
+
* page (an app framed by the DeSciX shell measures its own frame). A window without
|
|
12
|
+
* matchMedia THROWS: every browser has it, so its absence is a caller passing the wrong object.
|
|
13
|
+
*
|
|
14
|
+
* Its two consumers ask it once each, at load:
|
|
15
|
+
* - the host-view call site (apps/ide/src/host-view-entry.js) asks the shell for NoChrome on a phone, CodeSite
|
|
16
|
+
* elsewhere;
|
|
17
|
+
* - STUDIO (lib/features/ide/AuthorView.jsx) opens with its side panels collapsed on a phone.
|
|
18
|
+
* The static build compiles it into both: the stable host-view entry is bundled as its own self-contained module
|
|
19
|
+
* (apps/ide/vite.config.js frqtl-host-view-entry), the IDE's rollup graph carries its own copy of the same source.
|
|
20
|
+
*
|
|
21
|
+
* Host-neutral ESM with no imports.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** A phone: a coarse primary pointer on a viewport no wider than a phone in landscape. */
|
|
25
|
+
export const MOBILE_DEVICE_QUERY = '(pointer: coarse) and (max-width: 932px)';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{matchMedia: function(string): {matches: boolean}}} win the window the page is laid out in
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
* @throws {TypeError} a window without matchMedia
|
|
31
|
+
*/
|
|
32
|
+
export function isMobileDevice(win) {
|
|
33
|
+
if (!win || typeof win.matchMedia !== 'function') {
|
|
34
|
+
throw new TypeError('[device] isMobileDevice(win) needs the window the page is laid out in (a matchMedia ' +
|
|
35
|
+
'function); got ' + (win === null ? 'null' : typeof win) + '. (FAIL-LOUD.)');
|
|
36
|
+
}
|
|
37
|
+
return win.matchMedia(MOBILE_DEVICE_QUERY).matches === true;
|
|
38
|
+
}
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
* parseManifest, serializeManifest, manifestFor manifest.js (THE owner of frqtl.json)
|
|
14
14
|
* RECORD_FILES, RECORD_KEYS, CHANGE_KEYS, OPS, BY,
|
|
15
15
|
* CLIP_SECONDS, CLIP_NAME, clipName, jsonText,
|
|
16
|
-
* recordFolderFaults
|
|
16
|
+
* rowFaults, recordFolderFaults, applyRows record-format.js (THE owner of the Record folder's shape and
|
|
17
|
+
* of its change rows' shape and meaning: rowFaults checks one row,
|
|
18
|
+
* applyRows applies them to a block)
|
|
19
|
+
*
|
|
20
|
+
* The entry stands alone: it loads the parts roster (every published kind registers on import, objects/kinds.js),
|
|
21
|
+
* so applyRows can spell a 6-number rect by its kind's descriptor with nothing else imported. The roster is imported
|
|
22
|
+
* HERE, by the package door, and never by record-format.js: a scene page reaches record-format statically
|
|
23
|
+
* (RecipeContext → Record.js) and loads the roster only in its boot's modules phase, where a failed part is reported.
|
|
17
24
|
*
|
|
18
25
|
* PROJECT_EXPORTS is this entry's export list; a conformance test holds the module's exports equal to
|
|
19
26
|
* it, and the package gate holds it equal across @descix/frqtl-web and @descix/frqtl-node.
|
|
@@ -24,11 +31,14 @@ export const PROJECT_EXPORTS = Object.freeze(['FRQTL_WEB_SPECIFIERS', 'libPathOf
|
|
|
24
31
|
'linkProject', 'sketchProject', 'SKETCH_ENTRY', 'projectIdentity',
|
|
25
32
|
'MANIFEST_FILE', 'MANIFEST_KEYS', 'normalizeManifest', 'parseManifest', 'serializeManifest', 'manifestFor',
|
|
26
33
|
'RECORD_FILES', 'RECORD_KEYS', 'CHANGE_KEYS', 'OPS', 'BY', 'CLIP_SECONDS', 'CLIP_NAME', 'clipName', 'jsonText',
|
|
27
|
-
'recordFolderFaults']);
|
|
34
|
+
'rowFaults', 'recordFolderFaults', 'applyRows']);
|
|
35
|
+
|
|
36
|
+
// The parts roster (see above): a side-effect import, so ./project stands alone.
|
|
37
|
+
import '../../frqtl/parts/index.js';
|
|
28
38
|
|
|
29
39
|
export { FRQTL_WEB_SPECIFIERS, libPathOf, importMapFor, pageImportMapFor, FRQTL_WEB_HOST_SPECIFIERS } from './specifiers.js';
|
|
30
40
|
export { linkProject, sketchProject, SKETCH_ENTRY } from './link.js';
|
|
31
41
|
export { projectIdentity } from './identity.js';
|
|
32
42
|
export { MANIFEST_FILE, MANIFEST_KEYS, normalizeManifest, parseManifest, serializeManifest, manifestFor } from './manifest.js';
|
|
33
43
|
export { RECORD_FILES, RECORD_KEYS, CHANGE_KEYS, OPS, BY, CLIP_SECONDS, CLIP_NAME, clipName, jsonText,
|
|
34
|
-
recordFolderFaults } from './record-format.js';
|
|
44
|
+
rowFaults, recordFolderFaults, applyRows } from './record-format.js';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* parts-spelling.js — THE rule for spelling a written value in a `parts` entry's OWN form. A hand's value and a Record
|
|
5
|
+
* row's value are the CAPTURE form (ObjectSchema.captureProps); the entry keeps the form its author wrote. Two
|
|
6
|
+
* consumers ask it, so the text and the block agree by construction:
|
|
7
|
+
* - the writer (RecipeContext toSketch / setDeclared), reading the entry's form off the sketch TEXT;
|
|
8
|
+
* - applyRows (record-format.js), reading it off a parts BLOCK ({ name: [kind name, props] }).
|
|
9
|
+
*
|
|
10
|
+
* spellRect(props, { kind, entryKeys, baseline }) — a 6-tuple of a field whose descriptor derives reads from it
|
|
11
|
+
* (ObjectSchema.derivedReads: Wall `z`, `depth`) is written as the 4-tuple plus each derived key whose value
|
|
12
|
+
* changed, when the entry spells any of those keys as plain properties; otherwise the 6-tuple stands. A
|
|
13
|
+
* 4-tuple equal to the baseline's is not written (no literalizing an untouched expression). THROWS, naming the
|
|
14
|
+
* remedy, when `kind` is not registered in this realm (no imported module defines it).
|
|
15
|
+
* spellCenter(props, { entryKeys, centerLength }) — a `center` [x, y, 0] is written as two numbers when the entry
|
|
16
|
+
* spells two; otherwise the [x, y, z] stands.
|
|
17
|
+
* captureCenter(v) — the inverse: a spelled `center` in its capture form. [x, y] → [x, y, 0] (an omitted z is 0,
|
|
18
|
+
* the reading placeDeclaredAt gives it); [x, y, z] stands. THROWS on anything else.
|
|
19
|
+
*
|
|
20
|
+
* `entryKeys()` → the keys the entry spells; `centerLength()` → the element count its `center` spells, or null.
|
|
21
|
+
* Both are asked lazily (only when a value needs them). Each function mutates and returns `props`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { ObjectSchema } from '../../frqtl/objects/schema/index.js';
|
|
25
|
+
|
|
26
|
+
const _JSON = function (v) { return JSON.stringify(v); };
|
|
27
|
+
|
|
28
|
+
function _hasSix(props) {
|
|
29
|
+
return Object.keys(props).some(function (f) { return Array.isArray(props[f]) && props[f].length === 6; });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {object} props @param {{ kind: string, entryKeys: function(): string[], baseline: object }} o */
|
|
33
|
+
export function spellRect(props, o) {
|
|
34
|
+
if (!_hasSix(props)) { return props; }
|
|
35
|
+
if (!ObjectSchema.get(o.kind)) {
|
|
36
|
+
throw new Error('[parts-spelling] the kind "' + o.kind + '" is not registered in this realm: a 6-number rect is spelled by ' +
|
|
37
|
+
'its descriptor, and a kind registers when the module that defines it is imported (the published parts register ' +
|
|
38
|
+
'with ./project; a project\'s own part, parts/' + o.kind + '.js, when its module is imported) — import the module ' +
|
|
39
|
+
'that defines it. (FAIL-LOUD.)');
|
|
40
|
+
}
|
|
41
|
+
const derived = ObjectSchema.derivedReads(o.kind);
|
|
42
|
+
if (!derived.length) { return props; }
|
|
43
|
+
const spelled = o.entryKeys();
|
|
44
|
+
const baseline = o.baseline || {};
|
|
45
|
+
const R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
|
|
46
|
+
Object.keys(props).forEach(function (f) {
|
|
47
|
+
const v = props[f];
|
|
48
|
+
const rows = derived.filter(function (d) { return d.field === f && d.tuple === 6 && spelled.indexOf(d.name) !== -1; });
|
|
49
|
+
if (!rows.length || !Array.isArray(v) || v.length !== 6) { return; }
|
|
50
|
+
const b = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f] : null;
|
|
51
|
+
const four = R4.map(function (k) { return v[R6.indexOf(k)]; });
|
|
52
|
+
if (b && _JSON(four) === _JSON(R4.map(function (k) { return b[R6.indexOf(k)]; }))) { delete props[f]; }
|
|
53
|
+
else { props[f] = four; }
|
|
54
|
+
rows.forEach(function (d) {
|
|
55
|
+
const nv = v[R6.indexOf(d.key)];
|
|
56
|
+
if (!b || nv !== b[R6.indexOf(d.key)]) { props[d.name] = nv; }
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return props;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {number[]} v a spelled center @returns {number[]} its capture form [x, y, z] (a new array) */
|
|
63
|
+
export function captureCenter(v) {
|
|
64
|
+
if (!Array.isArray(v) || (v.length !== 2 && v.length !== 3) || !v.every(function (n) { return typeof n === 'number'; })) {
|
|
65
|
+
throw new TypeError('[parts-spelling] captureCenter: a center is [x, y] or [x, y, z] numbers, got ' + JSON.stringify(v) + '. (FAIL-LOUD.)');
|
|
66
|
+
}
|
|
67
|
+
return v.length === 2 ? [v[0], v[1], 0] : v.slice();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {object} props @param {{ entryKeys: function(): string[], centerLength: function(): (number|null) }} o */
|
|
71
|
+
export function spellCenter(props, o) {
|
|
72
|
+
const v = props.center;
|
|
73
|
+
if (!Array.isArray(v) || v.length !== 3 || v[2] !== 0) { return props; }
|
|
74
|
+
if (o.entryKeys().indexOf('center') === -1) { return props; }
|
|
75
|
+
if (o.centerLength() === 2) { props.center = [v[0], v[1]]; }
|
|
76
|
+
return props;
|
|
77
|
+
}
|
|
@@ -22,9 +22,14 @@
|
|
|
22
22
|
* CLIP_SECONDS { min, max } — the length of one clip, in seconds of playback
|
|
23
23
|
* CLIP_NAME the clip file pattern; clipName(n) → 'clip-NN.mp4' (n from 1)
|
|
24
24
|
* jsonText(v) JSON.stringify(v, null, 2) + '\n' — every JSON file of the folder
|
|
25
|
+
* rowFaults(row, at?) → string[] [] for a valid change row, every fault by name otherwise — THE one shape check of
|
|
26
|
+
* a row (its keys, tick, name, by, op in OPS, from/to per op); recordFolderFaults, applyRows and
|
|
27
|
+
* serializeRecordFolder ask it, so a host checking a row before it writes or applies one asks it too
|
|
25
28
|
* recordFolderFaults(files) → string[] [] for a complete §6 folder, every fault by name otherwise
|
|
26
29
|
* serializeRecordFolder(parts) → { files, record, clips } | { empty: true } THE writer (Record.js calls it;
|
|
27
30
|
* it runs recordFolderFaults on its own output and THROWS on any fault)
|
|
31
|
+
* applyRows(partsBlock, rows) → partsBlock THE applier: a parts block with change rows applied in order (a new
|
|
32
|
+
* block; the input is never mutated)
|
|
28
33
|
*
|
|
29
34
|
* `files` is a tree { path → string | Uint8Array }: JSON and project text as strings (or UTF-8 bytes), clips
|
|
30
35
|
* as bytes. Values in §6 files are strict JSON, in record.json's own vocabulary; a placeholder ("*",
|
|
@@ -33,6 +38,7 @@
|
|
|
33
38
|
|
|
34
39
|
import { MANIFEST_FILE } from './manifest.js';
|
|
35
40
|
import { entryOf } from './link.js';
|
|
41
|
+
import { spellRect, spellCenter } from './parts-spelling.js';
|
|
36
42
|
|
|
37
43
|
/** The folder's §6 paths. */
|
|
38
44
|
export const RECORD_FILES = Object.freeze({
|
|
@@ -125,8 +131,17 @@ function _placement(v) {
|
|
|
125
131
|
|
|
126
132
|
const MOVE_SHAPE = 'move carries from/to as ONE key, {center: [x, y, z]} or {rect: [4 numbers flat | 6 with a z-extent]}, the same key and length in both';
|
|
127
133
|
|
|
128
|
-
/**
|
|
129
|
-
|
|
134
|
+
/**
|
|
135
|
+
* rowFaults(row, at?) → string[] — THE shape check of one change row: [] when valid, otherwise every fault by name.
|
|
136
|
+
* Checks the keys (exactly CHANGE_KEYS), `tick` (a whole tick ≥ 0), `name` (the declared name), `by` (one of BY),
|
|
137
|
+
* `op` (one of OPS) and `from`/`to` (strict JSON, in that op's shape; a move that moves nothing is a fault).
|
|
138
|
+
* A row alone carries no folder facts: tickRange, tick order and one shape per name's moves are recordFolderFaults'.
|
|
139
|
+
* @param {*} c the row
|
|
140
|
+
* @param {string} [at='row'] the row's label at the head of each fault (e.g. 'changes[3]')
|
|
141
|
+
* @returns {string[]}
|
|
142
|
+
*/
|
|
143
|
+
export function rowFaults(c, at) {
|
|
144
|
+
if (at === undefined) { at = 'row'; }
|
|
130
145
|
const bad = [];
|
|
131
146
|
if (!_isPlain(c)) { return [at + ' is not a change {' + CHANGE_KEYS.join(', ') + '}']; }
|
|
132
147
|
const keys = Object.keys(c);
|
|
@@ -225,7 +240,7 @@ export function recordFolderFaults(files) {
|
|
|
225
240
|
let last = -Infinity;
|
|
226
241
|
rec.changes.forEach(function (c, i) {
|
|
227
242
|
const at = RECORD_FILES.record + ' changes[' + i + ']';
|
|
228
|
-
|
|
243
|
+
rowFaults(c, at).forEach(function (f) { bad.push(f); });
|
|
229
244
|
if (!_isPlain(c)) { return; }
|
|
230
245
|
if (Number.isInteger(c.tick)) {
|
|
231
246
|
if (range && (c.tick < range[0] || c.tick > range[1])) { bad.push(at + ' at tick ' + c.tick + ' lies outside tickRange ' + JSON.stringify(range)); }
|
|
@@ -329,7 +344,7 @@ export function serializeRecordFolder(p) {
|
|
|
329
344
|
// Check the rows as HELD, before any text: JSON.stringify writes NaN/Infinity as null and drops an undefined key,
|
|
330
345
|
// so a check of the written text would read a different value from the one the Record held.
|
|
331
346
|
const held = [];
|
|
332
|
-
record.changes.forEach(function (c, i) { held.push.apply(held,
|
|
347
|
+
record.changes.forEach(function (c, i) { held.push.apply(held, rowFaults(c, 'changes[' + i + ']')); });
|
|
333
348
|
if (held.length) { throw new Error('[record-format] a change row is not strict JSON: ' + held.join('; ') + ' (FAIL-LOUD.)'); }
|
|
334
349
|
const files = {};
|
|
335
350
|
files[RECORD_FILES.record] = jsonText(record);
|
|
@@ -343,3 +358,80 @@ export function serializeRecordFolder(p) {
|
|
|
343
358
|
}
|
|
344
359
|
return { files: files, record: record, clips: clips.map(function (c, i) { return { file: clipName(i + 1), firstTick: c.firstTick, lastTick: c.lastTick }; }) };
|
|
345
360
|
}
|
|
361
|
+
|
|
362
|
+
// ── the applier ─────────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
function _applyFail(i, msg) {
|
|
365
|
+
throw new Error('[record-format] applyRows: rows[' + i + '] ' + msg + '. (FAIL-LOUD.)');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The element count an entry's `center` value spells, or null. */
|
|
369
|
+
function _centerLength(props) {
|
|
370
|
+
return Array.isArray(props.center) ? props.center.length : null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* applyRows(partsBlock, rows) → partsBlock — THE applier of §6 change rows to a parts block
|
|
375
|
+
* ({ "<name>": ["<registered kind name>", {props}] }, the parts.before.json / parts.after.json value), in order. Pure:
|
|
376
|
+
* returns a NEW block; neither argument is mutated. Per op (OPS):
|
|
377
|
+
* move entry `name` takes `to`'s one position key, spelled in the entry's own form
|
|
378
|
+
* setDeclared entry `name` takes each key of `to`, spelled in the entry's own form
|
|
379
|
+
* add `to` becomes entry `name`, as the LAST key
|
|
380
|
+
* remove entry `name` is dropped
|
|
381
|
+
* fork entry `name`'s kind becomes `to.kind` (its props are unchanged)
|
|
382
|
+
* The spelling is the writer's own rule (parts-spelling.js spellRect / spellCenter: a 6-number rect beside spelled
|
|
383
|
+
* `z`/`depth` keys, a two-number center), read off the block, so a block a hand changed and the rows that change
|
|
384
|
+
* wrote agree: applyRows(parts.before, record.changes) is parts.after. A 6-number rect row asks its kind's descriptor
|
|
385
|
+
* (ObjectSchema.derivedReads): the ./project entry (index.js) imports the parts roster, so every published kind is
|
|
386
|
+
* registered wherever ./project is loaded, and a scene page has registered them by its boot's modules phase; a
|
|
387
|
+
* project's own kind (a fork's parts/<Kind>.js) is registered when its module is imported. This module itself imports
|
|
388
|
+
* no part: the scene page reaches it statically (RecipeContext → Record.js), and a static barrel import here would load
|
|
389
|
+
* every part with the page module, ahead of the boot record (a failed part would then leave no boot banner).
|
|
390
|
+
* THROWS, naming the row index and the offending value: a block that is not { name: [kind name, {props}] }; rows
|
|
391
|
+
* that are not an array; a row rowFaults names (an op that is not one of OPS, its keys, its from/to shape — the
|
|
392
|
+
* faults in the error); a 6-number rect row on a kind no imported module registered; a name the block does not
|
|
393
|
+
* hold (move, setDeclared, remove, fork) or already holds (add); a fork whose `from` is not the entry's kind.
|
|
394
|
+
* @param {Object<string, [string, object]>} partsBlock
|
|
395
|
+
* @param {object[]} rows
|
|
396
|
+
* @returns {Object<string, [string, object]>}
|
|
397
|
+
*/
|
|
398
|
+
export function applyRows(partsBlock, rows) {
|
|
399
|
+
const block = _partsValue(partsBlock, 'applyRows partsBlock');
|
|
400
|
+
if (!Array.isArray(rows)) {
|
|
401
|
+
throw new TypeError('[record-format] applyRows(partsBlock, rows): rows is the list of change rows (record.json changes), got ' +
|
|
402
|
+
JSON.stringify(rows) + '. (FAIL-LOUD.)');
|
|
403
|
+
}
|
|
404
|
+
rows.forEach(function (row, i) {
|
|
405
|
+
const faults = rowFaults(row, 'rows[' + i + ']');
|
|
406
|
+
if (faults.length) { _applyFail(i, 'is not a change row: ' + faults.join('; ')); }
|
|
407
|
+
const name = row.name;
|
|
408
|
+
const has = Object.prototype.hasOwnProperty.call(block, name);
|
|
409
|
+
if (row.op === 'add') {
|
|
410
|
+
if (has) { _applyFail(i, 'adds "' + name + '", which the block already holds as ' + JSON.stringify(block[name])); }
|
|
411
|
+
block[name] = JSON.parse(JSON.stringify(row.to));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!has) {
|
|
415
|
+
_applyFail(i, '(' + row.op + ') names the part "' + name + '", which the block does not hold (parts: ' +
|
|
416
|
+
Object.keys(block).join(', ') + ')');
|
|
417
|
+
}
|
|
418
|
+
const entry = block[name];
|
|
419
|
+
if (row.op === 'remove') { delete block[name]; return; }
|
|
420
|
+
if (row.op === 'fork') {
|
|
421
|
+
if (entry[0] !== row.from) {
|
|
422
|
+
_applyFail(i, 'forks "' + name + '" from the kind ' + JSON.stringify(row.from) + ', but the block declares it as ' + JSON.stringify(entry[0]));
|
|
423
|
+
}
|
|
424
|
+
block[name] = [row.to.kind, entry[1]];
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// move / setDeclared: the row's capture-form values, spelled in the entry's own form.
|
|
428
|
+
const props = JSON.parse(JSON.stringify(row.to));
|
|
429
|
+
const own = entry[1];
|
|
430
|
+
try {
|
|
431
|
+
spellRect(props, { kind: entry[0], baseline: row.from, entryKeys: function () { return Object.keys(own); } });
|
|
432
|
+
spellCenter(props, { entryKeys: function () { return Object.keys(own); }, centerLength: function () { return _centerLength(own); } });
|
|
433
|
+
} catch (e) { throw new Error('[record-format] applyRows: rows[' + i + '] (' + row.op + ') of "' + name + '": ' + ((e && e.message) || String(e))); }
|
|
434
|
+
block[name] = [entry[0], Object.assign({}, own, props)];
|
|
435
|
+
});
|
|
436
|
+
return block;
|
|
437
|
+
}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* its error copy.
|
|
9
9
|
*
|
|
10
10
|
* STUDIO_LANDING_ATTR the <html> attribute carrying the outcome id
|
|
11
|
-
* STUDIO_LANDING_OUTCOMES the closed table: one row per branch of STUDIO's link resolution, `{ id, means }
|
|
11
|
+
* STUDIO_LANDING_OUTCOMES the closed table: one row per branch of STUDIO's link resolution, `{ id, runs, means }`;
|
|
12
|
+
* `runs` says whether the scene that landing opens starts on load (through STUDIO's Run
|
|
13
|
+
* control's own path) — true only on `ok`, a deep link that named a scene; every other
|
|
14
|
+
* landing opens its scene in Design, waiting for Run
|
|
12
15
|
* recordStudioLanding(root, id) writes `id` on `root` (an Element, or null to only validate); an id outside the
|
|
13
16
|
* table THROWS — that is a defect in the writer, never a landing
|
|
14
17
|
*
|
|
@@ -29,23 +32,23 @@ export const STUDIO_LANDING_ATTR = 'data-frqtl-studio-landing';
|
|
|
29
32
|
|
|
30
33
|
/** The closed table of STUDIO deep-link landings, in the order the resolver tests them (lib/features/ide/test/studio-landing-of.test.mjs holds it). */
|
|
31
34
|
export const STUDIO_LANDING_OUTCOMES = Object.freeze([
|
|
32
|
-
{ id: 'refused-key',
|
|
35
|
+
{ id: 'refused-key', runs: false,
|
|
33
36
|
means: 'the link carries a key STUDIO does not own, or a deleted key (deep-link.js deepLinkRefusalOf): the refusal is on the deep-link strip and the opening rung opens' },
|
|
34
|
-
{ id: 'conflict',
|
|
37
|
+
{ id: 'conflict', runs: false,
|
|
35
38
|
means: 'the link names both a walkthrough and a saved project: nothing opens' },
|
|
36
|
-
{ id: 'mode-without-walkthrough',
|
|
39
|
+
{ id: 'mode-without-walkthrough', runs: false,
|
|
37
40
|
means: 'the link names a mode and no walkthrough: nothing opens' },
|
|
38
|
-
{ id: 'unknown-walkthrough',
|
|
41
|
+
{ id: 'unknown-walkthrough', runs: false,
|
|
39
42
|
means: 'the linked walkthrough is not in the walkthrough registry (walkthroughById): nothing opens' },
|
|
40
|
-
{ id: 'unknown-mode',
|
|
43
|
+
{ id: 'unknown-mode', runs: false,
|
|
41
44
|
means: 'the linked mode is not one the linked rung declares (walkthroughModes): nothing opens' },
|
|
42
|
-
{ id: 'project-unreadable',
|
|
45
|
+
{ id: 'project-unreadable', runs: false,
|
|
43
46
|
means: 'the linked saved project is present but its folder is refused by the load: nothing opens' },
|
|
44
|
-
{ id: 'project-not-found',
|
|
47
|
+
{ id: 'project-not-found', runs: false,
|
|
45
48
|
means: 'no saved project of the linked name in this browser: nothing opens' },
|
|
46
|
-
{ id: 'ok',
|
|
47
|
-
means: 'the linked walkthrough (in the linked mode, else its default) or saved project resolved and was handed to STUDIO\'s selection path' },
|
|
48
|
-
{ id: 'first-contact',
|
|
49
|
+
{ id: 'ok', runs: true,
|
|
50
|
+
means: 'the linked walkthrough (in the linked mode, else its default) or saved project resolved and was handed to STUDIO\'s selection path; the scene starts on load' },
|
|
51
|
+
{ id: 'first-contact', runs: false,
|
|
49
52
|
means: 'no STUDIO deep link: the walkthrough registry\'s opening rung opens' },
|
|
50
53
|
].map(Object.freeze));
|
|
51
54
|
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @descix/frqtl-node
|
|
2
2
|
|
|
3
3
|
> The FRQTL physics engine (compiled Rust → WASM) for node: the same modules as @descix/frqtl-web, plus
|
|
4
|
-
> `bootNode` and the hosted eyes. 0.6.1-
|
|
4
|
+
> `bootNode` and the hosted eyes. 0.6.1-p3.2 on npm dist-tag `next`. Node ≥ 22.12.
|
|
5
5
|
|
|
6
6
|
- Install: `npm install @descix/frqtl-node`
|
|
7
7
|
- `import { bootNode, Wall } from '@descix/frqtl-node'` (require() works too); `await bootNode()` → the engine handle.
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
- `./project`: `linkProject` links a scene file against this package's own copy.
|
|
10
10
|
- `./host-view`: `declareHostView`, `HOST_VIEW_OUTCOMES`, `CONTRACT_VIOLATIONS`, `HOST_SDK_SCRIPT` — the same declarer as @descix/frqtl-web/host-view, pure, for a node test of a call site.
|
|
11
11
|
- `./register`: `node --import @descix/frqtl-node/register scene.mjs` resolves a scene's `@descix/frqtl-web/...` imports to this package.
|
|
12
|
-
- Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.1-
|
|
12
|
+
- Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.1-p3.2/CLAUDE.md
|
|
13
13
|
- Acceptance is visual; no CLI ships. No account, credits or tokens are needed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/frqtl-node",
|
|
3
|
-
"version": "0.6.1-
|
|
3
|
+
"version": "0.6.1-p3.2",
|
|
4
4
|
"description": "FRQTL for node \u2014 the discrete fractal physics engine (compiled Rust \u2192 WASM), its parts as ES modules, the node engine door (bootNode) and the hosted eyes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|