@descix/frqtl-node 0.6.1-p3.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 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-p3.1` (npm dist-tag `next`). Node ≥ 22.12.
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/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-p3.1` (npm dist-tag `next`). Node ≥ 22.12.
7
+ `0.6.1-p3.2` (npm dist-tag `next`). Node ≥ 22.12.
8
8
 
9
9
  ## Install
10
10
 
@@ -31,7 +31,7 @@ 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`, `applyRows` |
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) |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/frqtl-node",
3
- "version": "0.6.1-p3.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",
@@ -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": "83aba36a6a6c15f92c99bbe5d7e63b12b38e07be65e02e89020c065514ef5b3d",
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": "7dcb520daee3facad8698b363bb49557003439dc5841706b105bd1acabc2e0bf",
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": "62a1995871557536f48903eec14849b135f7d5d5e68c77139b61289a405072b3",
169
+ "shim/engine/router.js": "1c8bed954aecfcd06d1344051d8489f0f040967f9d0879ec7e7a0d1e58045d2c",
170
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,15 +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": "ba0d7812445aad52c36d6dd806e2d8c6b87d43e4d7c4494e9c0bfa9da8503a03",
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",
205
- "shim/project/parts-spelling.js": "f43c356d8c08077a76190cb88bad4daa4280c112d120ee478ba67993c77c41ea",
206
+ "shim/project/parts-spelling.js": "4cc7b8a2f0aa1f3ad4c1c3cf15f74b2860a94ae2214dbef78d31c85ff8916cfe",
206
207
  "shim/project/parts-writer.js": "121fa67261d6f366190c9ac4961fceed2f26ca7f35cf3fbffc767829afc69978",
207
208
  "shim/project/read-text.js": "b89bd70e3a271e1f42c8ce9427a20cdd9a4c4c9c31b64c49ff1f56b095620136",
208
- "shim/project/record-format.js": "95cbb99d294e66c94f9f1921664cd932649c6d6b187b17928b8fb4b955ca880f",
209
+ "shim/project/record-format.js": "87d5cc2eae8ff600db1db58a0e0047819f7c9839ff702acdedc366c7647eb0ef",
209
210
  "shim/project/register-node.mjs": "9b408da7dc1b11d16237cd62caca281c823ea80173d386ad35e41ea17c34857f",
210
211
  "shim/project/specifiers.js": "288e5e06e53b873449190f6f85855177a72e52623bd11eef016acafb8095d829",
211
212
  "shim/registry/copy-gate.js": "cfbf52669c849b8837b326f2743458f1ead0720cad51a6c6d409c9bd6c7f9495",
@@ -214,7 +215,7 @@
214
215
  "shim/registry/scene-roster.js": "318fe373f71b1c58d05425693f2fad4bd5368f6095b65817c6d637430a0bfb73",
215
216
  "shim/registry/scene-source.js": "c9c4b48c24d4b1ebee1549a7891a1ba708909b94b9403ce28eefbd902cacf49f",
216
217
  "shim/registry/showcase-link.js": "daf5c1426aeb3c578bace5e0edea9ba2538cd82336d2cdc944e5fed6decd7551",
217
- "shim/registry/studio-landing.js": "c34c9e7e77087c2bfa6be9c34dafedb1e3256e92eb1aeb629b7f7182cd2edf9f",
218
+ "shim/registry/studio-landing.js": "bd81ad9333332de4a29b3121f963edd354450c75c97f4f43170a98c2dcceb7f3",
218
219
  "shim/registry/walkthrough.js": "8f8b6f59e9e81c67c9c62f8fb271eee2e4d6e1fba6e116b227a2f1e3f9930230",
219
220
  "shim/render-state/apply.js": "94a9e60cb1722c9a82aa00f2f161fd65e6f438147333856a786e42a4ad5b7064",
220
221
  "shim/render-state/index.js": "72a6a3dcd8037893cb8fbd7ef32b126770e7efaec70847f8e4f2915ddb690b43",
@@ -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()` / `getLiveCount()` —
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? ───
@@ -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
- * LIVE-POPULATION readout (loop-polish B): the number of frames
779
- * that collided with THIS wall on the sampled tick. For a FULL-AREA background
780
- * wall (collisionBehavior 'nothing' + the count hook, intersects trigger) every
781
- * live leaf inside the rect collides every tick, so this per-tick collided-frame
782
- * count IS the current live population inside the wall — the ONE canonical
783
- * "how many live" authority (replaces any host-side scan, e.g. an emission-cap
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
- * Guard: THROWS on a wall without the count hook / legacy detect behavior (no
798
- * collected-frames buffer → it would silently report 0 forever).
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.
@@ -152,7 +152,7 @@ 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-p3.1'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
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
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
  /**
@@ -526,7 +526,7 @@ var _assertProfileParity = (function () {
526
526
  * // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
527
527
  * const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
528
528
  * console.log(cdnRouter.resolve('compat/engine.js'));
529
- * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.1-p3.1/dist/frqtl/compat/engine.js'
529
+ * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.1-p3.2/dist/frqtl/compat/engine.js'
530
530
  *
531
531
  * // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
532
532
  * const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });
@@ -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,8 +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, applyRows record-format.js (THE owner of the Record folder's shape and
17
- * of its change rows' meaning: applyRows applies them to a block)
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.
18
24
  *
19
25
  * PROJECT_EXPORTS is this entry's export list; a conformance test holds the module's exports equal to
20
26
  * it, and the package gate holds it equal across @descix/frqtl-web and @descix/frqtl-node.
@@ -25,11 +31,14 @@ export const PROJECT_EXPORTS = Object.freeze(['FRQTL_WEB_SPECIFIERS', 'libPathOf
25
31
  'linkProject', 'sketchProject', 'SKETCH_ENTRY', 'projectIdentity',
26
32
  'MANIFEST_FILE', 'MANIFEST_KEYS', 'normalizeManifest', 'parseManifest', 'serializeManifest', 'manifestFor',
27
33
  'RECORD_FILES', 'RECORD_KEYS', 'CHANGE_KEYS', 'OPS', 'BY', 'CLIP_SECONDS', 'CLIP_NAME', 'clipName', 'jsonText',
28
- 'recordFolderFaults', 'applyRows']);
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';
29
38
 
30
39
  export { FRQTL_WEB_SPECIFIERS, libPathOf, importMapFor, pageImportMapFor, FRQTL_WEB_HOST_SPECIFIERS } from './specifiers.js';
31
40
  export { linkProject, sketchProject, SKETCH_ENTRY } from './link.js';
32
41
  export { projectIdentity } from './identity.js';
33
42
  export { MANIFEST_FILE, MANIFEST_KEYS, normalizeManifest, parseManifest, serializeManifest, manifestFor } from './manifest.js';
34
43
  export { RECORD_FILES, RECORD_KEYS, CHANGE_KEYS, OPS, BY, CLIP_SECONDS, CLIP_NAME, clipName, jsonText,
35
- recordFolderFaults, applyRows } from './record-format.js';
44
+ rowFaults, recordFolderFaults, applyRows } from './record-format.js';
@@ -10,7 +10,8 @@
10
10
  * spellRect(props, { kind, entryKeys, baseline }) — a 6-tuple of a field whose descriptor derives reads from it
11
11
  * (ObjectSchema.derivedReads: Wall `z`, `depth`) is written as the 4-tuple plus each derived key whose value
12
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).
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).
14
15
  * spellCenter(props, { entryKeys, centerLength }) — a `center` [x, y, 0] is written as two numbers when the entry
15
16
  * spells two; otherwise the [x, y, z] stands.
16
17
  * captureCenter(v) — the inverse: a spelled `center` in its capture form. [x, y] → [x, y, 0] (an omitted z is 0,
@@ -31,6 +32,12 @@ function _hasSix(props) {
31
32
  /** @param {object} props @param {{ kind: string, entryKeys: function(): string[], baseline: object }} o */
32
33
  export function spellRect(props, o) {
33
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
+ }
34
41
  const derived = ObjectSchema.derivedReads(o.kind);
35
42
  if (!derived.length) { return props; }
36
43
  const spelled = o.entryKeys();
@@ -22,6 +22,9 @@
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)
@@ -128,8 +131,17 @@ function _placement(v) {
128
131
 
129
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';
130
133
 
131
- /** The faults of one change row against its op. */
132
- function _rowFaults(c, at) {
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'; }
133
145
  const bad = [];
134
146
  if (!_isPlain(c)) { return [at + ' is not a change {' + CHANGE_KEYS.join(', ') + '}']; }
135
147
  const keys = Object.keys(c);
@@ -228,7 +240,7 @@ export function recordFolderFaults(files) {
228
240
  let last = -Infinity;
229
241
  rec.changes.forEach(function (c, i) {
230
242
  const at = RECORD_FILES.record + ' changes[' + i + ']';
231
- _rowFaults(c, at).forEach(function (f) { bad.push(f); });
243
+ rowFaults(c, at).forEach(function (f) { bad.push(f); });
232
244
  if (!_isPlain(c)) { return; }
233
245
  if (Number.isInteger(c.tick)) {
234
246
  if (range && (c.tick < range[0] || c.tick > range[1])) { bad.push(at + ' at tick ' + c.tick + ' lies outside tickRange ' + JSON.stringify(range)); }
@@ -332,7 +344,7 @@ export function serializeRecordFolder(p) {
332
344
  // Check the rows as HELD, before any text: JSON.stringify writes NaN/Infinity as null and drops an undefined key,
333
345
  // so a check of the written text would read a different value from the one the Record held.
334
346
  const held = [];
335
- record.changes.forEach(function (c, i) { held.push.apply(held, _rowFaults(c, 'changes[' + i + ']')); });
347
+ record.changes.forEach(function (c, i) { held.push.apply(held, rowFaults(c, 'changes[' + i + ']')); });
336
348
  if (held.length) { throw new Error('[record-format] a change row is not strict JSON: ' + held.join('; ') + ' (FAIL-LOUD.)'); }
337
349
  const files = {};
338
350
  files[RECORD_FILES.record] = jsonText(record);
@@ -369,12 +381,16 @@ function _centerLength(props) {
369
381
  * fork entry `name`'s kind becomes `to.kind` (its props are unchanged)
370
382
  * The spelling is the writer's own rule (parts-spelling.js spellRect / spellCenter: a 6-number rect beside spelled
371
383
  * `z`/`depth` keys, a two-number center), read off the block, so a block a hand changed and the rows that change
372
- * wrote agree: applyRows(parts.before, record.changes) is parts.after. A rect row on a kind whose descriptor derives
373
- * reads (Wall) asks ObjectSchema, so that kind is registered in the realm (its part module imported).
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).
374
390
  * THROWS, naming the row index and the offending value: a block that is not { name: [kind name, {props}] }; rows
375
- * that are not an array; a row recordFolderFaults would name (an op that is not one of OPS, its keys, its from/to
376
- * shape); a name the block does not hold (move, setDeclared, remove, fork) or already holds (add); a fork whose
377
- * `from` is not the entry's kind.
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.
378
394
  * @param {Object<string, [string, object]>} partsBlock
379
395
  * @param {object[]} rows
380
396
  * @returns {Object<string, [string, object]>}
@@ -386,7 +402,7 @@ export function applyRows(partsBlock, rows) {
386
402
  JSON.stringify(rows) + '. (FAIL-LOUD.)');
387
403
  }
388
404
  rows.forEach(function (row, i) {
389
- const faults = _rowFaults(row, 'rows[' + i + ']');
405
+ const faults = rowFaults(row, 'rows[' + i + ']');
390
406
  if (faults.length) { _applyFail(i, 'is not a change row: ' + faults.join('; ')); }
391
407
  const name = row.name;
392
408
  const has = Object.prototype.hasOwnProperty.call(block, name);
@@ -411,8 +427,10 @@ export function applyRows(partsBlock, rows) {
411
427
  // move / setDeclared: the row's capture-form values, spelled in the entry's own form.
412
428
  const props = JSON.parse(JSON.stringify(row.to));
413
429
  const own = entry[1];
414
- spellRect(props, { kind: entry[0], baseline: row.from, entryKeys: function () { return Object.keys(own); } });
415
- spellCenter(props, { entryKeys: function () { return Object.keys(own); }, centerLength: function () { return _centerLength(own); } });
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))); }
416
434
  block[name] = [entry[0], Object.assign({}, own, props)];
417
435
  });
418
436
  return block;
@@ -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-p3.1 on npm dist-tag `next`. Node ≥ 22.12.
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-p3.1/CLAUDE.md
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-p3.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": {