@descix/frqtl-node 0.6.1-p3.4 → 0.7.0-p1

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.
Files changed (64) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +4 -3
  3. package/dist/dist-manifest.json +26 -56
  4. package/dist/frqtl/experiments/_shared/empty-sketch.js +17 -0
  5. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +6 -6
  6. package/dist/frqtl/objects/setup/setupFxSmoke.js +2 -2
  7. package/dist/frqtl/objects/setup/setupG3Collide.js +1 -1
  8. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +2 -2
  9. package/dist/frqtl/objects/v1-replacements.js +1 -1
  10. package/dist/frqtl/parts/OvenBox.js +2 -2
  11. package/dist/frqtl/parts/SphereEmitter.js +1 -1
  12. package/dist/shim/engine/router.js +18 -6
  13. package/dist/shim/engine/site-assemble.mjs +268 -0
  14. package/dist/shim/engine/site-graph.js +42 -14
  15. package/dist/shim/host/device.js +1 -1
  16. package/dist/shim/index.js +3 -7
  17. package/dist/shim/notebook/catalog.js +2 -2
  18. package/dist/shim/notebook/format.js +1 -1
  19. package/dist/shim/notebook/knob.js +1 -1
  20. package/dist/shim/notebook/notebook-knobs.js +1 -1
  21. package/dist/shim/project/specifiers.js +8 -8
  22. package/dist/shim/registry/copy-gate.js +3 -3
  23. package/dist/shim/registry/index.js +9 -12
  24. package/dist/shim/registry/roster-link.js +142 -0
  25. package/dist/shim/registry/roster.js +595 -0
  26. package/dist/shim/registry/scene-source.js +80 -105
  27. package/dist/shim/registry/studio-landing.js +15 -7
  28. package/dist/shim/render/sun-look.js +1 -2
  29. package/llms.txt +4 -2
  30. package/package.json +1 -1
  31. package/dist/frqtl/scenes/absorbing-wall-3d.js +0 -37
  32. package/dist/frqtl/scenes/absorbing-wall.js +0 -36
  33. package/dist/frqtl/scenes/atomic-model.js +0 -91
  34. package/dist/frqtl/scenes/big-bang.js +0 -49
  35. package/dist/frqtl/scenes/blackbody.js +0 -84
  36. package/dist/frqtl/scenes/circuit-sat.js +0 -131
  37. package/dist/frqtl/scenes/collision-matrix-3d.js +0 -110
  38. package/dist/frqtl/scenes/counting-wall-3d.js +0 -40
  39. package/dist/frqtl/scenes/counting-wall.js +0 -42
  40. package/dist/frqtl/scenes/double-slit.js +0 -34
  41. package/dist/frqtl/scenes/dse-one-emitter.js +0 -200
  42. package/dist/frqtl/scenes/dse-one-slit.js +0 -118
  43. package/dist/frqtl/scenes/dse-photoelectric.js +0 -74
  44. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +0 -279
  45. package/dist/frqtl/scenes/dse-two-emitters.js +0 -201
  46. package/dist/frqtl/scenes/dse-two-slits.js +0 -136
  47. package/dist/frqtl/scenes/empty.js +0 -17
  48. package/dist/frqtl/scenes/laser-buildup.js +0 -56
  49. package/dist/frqtl/scenes/mirror-3d.js +0 -60
  50. package/dist/frqtl/scenes/mirror-route.js +0 -164
  51. package/dist/frqtl/scenes/mirror.js +0 -42
  52. package/dist/frqtl/scenes/omni-3d.js +0 -30
  53. package/dist/frqtl/scenes/omni-source.js +0 -25
  54. package/dist/frqtl/scenes/self-interference.js +0 -42
  55. package/dist/frqtl/scenes/smoking-gun.js +0 -126
  56. package/dist/frqtl/scenes/straight-walk.js +0 -25
  57. package/dist/frqtl/scenes/sun.js +0 -42
  58. package/dist/frqtl/scenes/two-source.js +0 -42
  59. package/dist/frqtl/scenes/wave.js +0 -29
  60. package/dist/shim/engine/artifact-shell.js +0 -211
  61. package/dist/shim/registry/exhibits.js +0 -436
  62. package/dist/shim/registry/scene-roster.js +0 -35
  63. package/dist/shim/registry/showcase-link.js +0 -61
  64. package/dist/shim/registry/walkthrough.js +0 -752
@@ -0,0 +1,142 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * roster-link.js — THE loader of a host's roster: the one reader of `<link rel="frqtl-roster" href>` and the one
5
+ * fetch of the file it names. A surface never fetches a roster itself; the host loads it once per document and
6
+ * passes the bag down.
7
+ *
8
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
9
+ *
10
+ * ROSTER_LINK_REL 'frqtl-roster'
11
+ * ROSTER_CACHE_BUCKET_MS the width of the cache-busting bucket a request URL carries on http(s)
12
+ * rosterLinksOfHtml(html) → [{ tag, href }] for every `<link rel="frqtl-roster">` in a document's TEXT (an assembler,
13
+ * a server or a harness reading HTML it has not parsed; `href` decoded, null when absent).
14
+ * The one reader of the link in text; a parsed document goes through rosterHrefOf.
15
+ * rosterHrefOf(doc) → the link's absolute href. THROWS on no link, a second link, a relative href (the
16
+ * store-shell reason) or an href with a query or a hash.
17
+ * rosterRequestUrl(href, now)
18
+ * → the URL to request: `href?b=<floor(now / ROSTER_CACHE_BUCKET_MS)>` on http(s), `href`
19
+ * unchanged on file: (node). Any other scheme THROWS.
20
+ * loadRoster(href, { fetch, now })
21
+ * → Promise of normalizeRoster(json, href). REJECTS naming the URL on a failed read (a
22
+ * non-200 names its status), an empty body or invalid JSON.
23
+ * sceneOriginRefusalOf(url, { pageOrigin, roster })
24
+ * → null when a `?scene=` URL's origin is the page's or the roster's; else the refusal text
25
+ * naming the rule and both allowed origins.
26
+ *
27
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
28
+ *
29
+ * Zero physics — a file read.
30
+ */
31
+
32
+ import { normalizeRoster, rosterHrefRefusalOf, assertRosterBag } from './roster.js';
33
+ import { readText } from '../project/read-text.js';
34
+
35
+ /** The `rel` of the one link that names a document's roster. */
36
+ export const ROSTER_LINK_REL = 'frqtl-roster';
37
+
38
+ /** Five minutes. Why: the edge caches the uploaded roster for 1 h; this query and the constant are deleted when
39
+ * the platform's media store gains per-object cache control and the roster uploads uncached. */
40
+ export const ROSTER_CACHE_BUCKET_MS = 300000;
41
+
42
+ const LINK_SHAPE = '<link rel="' + ROSTER_LINK_REL + '" href="https://…">';
43
+
44
+ const LINK_TAG = new RegExp('<link\\b[^>]*\\brel=["\']' + ROSTER_LINK_REL + '["\'][^>]*>', 'gi');
45
+ const HREF_ATTR = /\shref=(["'])([^"']*)\1/i;
46
+ const decodeAttr = (v) => v.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
47
+
48
+ /**
49
+ * @param {string} html a document's text
50
+ * @returns {Array<{tag: string, href: string|null}>} every roster link tag in it, in order, with its decoded href
51
+ */
52
+ export function rosterLinksOfHtml(html) {
53
+ if (typeof html !== 'string') {
54
+ throw new Error('[roster-link] rosterLinksOfHtml(html) takes the document\'s text; got ' + typeof html + '. (FAIL-LOUD.)');
55
+ }
56
+ return (html.match(LINK_TAG) || []).map((tag) => {
57
+ const m = tag.match(HREF_ATTR);
58
+ return { tag, href: m ? decodeAttr(m[2]) : null };
59
+ });
60
+ }
61
+
62
+ /**
63
+ * @param {Document} doc
64
+ * @returns {string} the roster link's absolute href
65
+ * @throws {Error} no link, two links, or an href that is not an absolute plain file URL
66
+ */
67
+ export function rosterHrefOf(doc) {
68
+ if (!doc || typeof doc.querySelectorAll !== 'function') {
69
+ throw new Error('[roster-link] rosterHrefOf(doc) takes a document; got ' + String(doc) + '. (FAIL-LOUD.)');
70
+ }
71
+ const links = Array.from(doc.querySelectorAll('link[rel~="' + ROSTER_LINK_REL + '"]'));
72
+ if (links.length === 0) {
73
+ throw new Error('[roster-link] this document has no ' + LINK_SHAPE + ' — STUDIO and the GYM read their scene ' +
74
+ 'list from the roster it names; the host writes the link into the page. (FAIL-LOUD.)');
75
+ }
76
+ if (links.length > 1) {
77
+ throw new Error('[roster-link] this document has ' + links.length + ' ' + LINK_SHAPE + ' links; a document ' +
78
+ 'names exactly one roster. (FAIL-LOUD.)');
79
+ }
80
+ const href = links[0].getAttribute('href');
81
+ const why = rosterHrefRefusalOf(href);
82
+ if (why) { throw new Error('[roster-link] ' + LINK_SHAPE + ': ' + why + ' (FAIL-LOUD.)'); }
83
+ return href;
84
+ }
85
+
86
+ /**
87
+ * @param {string} href an absolute roster URL
88
+ * @param {number} now epoch milliseconds
89
+ * @returns {string} the URL to request
90
+ */
91
+ export function rosterRequestUrl(href, now) {
92
+ const why = rosterHrefRefusalOf(href);
93
+ if (why) { throw new Error('[roster-link] rosterRequestUrl(href, now): ' + why + ' (FAIL-LOUD.)'); }
94
+ if (!Number.isFinite(now)) {
95
+ throw new Error('[roster-link] rosterRequestUrl(href, now): now is epoch milliseconds (got ' + String(now) + '). (FAIL-LOUD.)');
96
+ }
97
+ const protocol = new URL(href).protocol;
98
+ if (protocol === 'file:') { return href; }
99
+ return href + '?b=' + Math.floor(now / ROSTER_CACHE_BUCKET_MS);
100
+ }
101
+
102
+ /**
103
+ * @param {string} href an absolute roster URL (rosterHrefOf in a page; a file: URL in node)
104
+ * @param {{fetch?: function, now?: number}} [opts] fetch is injectable; now defaults to Date.now()
105
+ * @returns {Promise<object>} the normalized roster bag
106
+ */
107
+ export function loadRoster(href, opts) {
108
+ return Promise.resolve().then(() => {
109
+ const o = opts || {};
110
+ const url = rosterRequestUrl(href, o.now === undefined ? Date.now() : o.now);
111
+ return readText(url, { fetch: o.fetch }).then((text) => {
112
+ if (text.trim() === '') { throw new Error('[roster-link] roster ' + url + ' is empty. (FAIL-LOUD.)'); }
113
+ let json = null;
114
+ try { json = JSON.parse(text); }
115
+ catch (e) { throw new Error('[roster-link] roster ' + url + ' is not JSON: ' + e.message + ' (FAIL-LOUD.)'); }
116
+ return normalizeRoster(json, href);
117
+ }, (e) => {
118
+ throw new Error('[roster-link] loading the roster failed: ' + e.message + ' (FAIL-LOUD.)');
119
+ });
120
+ });
121
+ }
122
+
123
+ /**
124
+ * @param {string} url the `?scene=` value, resolved (absolute)
125
+ * @param {{pageOrigin: string, roster: object}} ctx
126
+ * @returns {string|null} null when allowed; else the refusal text
127
+ */
128
+ export function sceneOriginRefusalOf(url, ctx) {
129
+ const c = ctx || {};
130
+ assertRosterBag(c.roster, 'sceneOriginRefusalOf(url, { pageOrigin, roster })');
131
+ if (typeof c.pageOrigin !== 'string' || c.pageOrigin === '') {
132
+ throw new Error('[roster-link] sceneOriginRefusalOf(url, { pageOrigin, roster }): pageOrigin is the page\'s ' +
133
+ 'location.origin (got ' + JSON.stringify(c.pageOrigin) + '). (FAIL-LOUD.)');
134
+ }
135
+ const rosterOrigin = new URL(c.roster.href).origin;
136
+ let origin = null;
137
+ try { origin = new URL(url).origin; } catch (e) { origin = null; }
138
+ // An opaque origin (a file: or data: URL serializes it as "null") equals nothing, itself included.
139
+ if (origin !== null && origin !== 'null' && (origin === c.pageOrigin || origin === rosterOrigin)) { return null; }
140
+ return 'a scene opens from this page\'s origin (' + c.pageOrigin + ') or its roster\'s origin (' + rosterOrigin +
141
+ '); ' + JSON.stringify(url) + ' is ' + (origin === null ? 'not an absolute URL' : 'on ' + origin) + '.';
142
+ }