Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ jobs:
# or a custom SVG. Non-UI text (a chat shortcode table) carries `emoji-ok` / `emoji-ok-file`.
- name: no emoji in the UI
run: node scripts/check-emoji.cjs
# 101 F2: three's generic loaders only through src/lib/loadGuard.js (the object schema runs first)
- name: object loaders behind the schema
run: node scripts/check-loaders.cjs

# 34 B3: scene-lint over every scene core ships (the seed) and every scene of the scenes ref
# the perf budget is about (perf/budgets.json), with the packs checkout for the kit rules.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2631,7 +2631,7 @@ sub-entry with its line: grep the index for a word, then read this file from tha
it hears one, is alone, or `GRACE_MS` (2 s) after its last connection change — under the smallest-id rule a
joiner IS the authority on connect, and its initial state would overwrite the game. LOADING: `analyze` (acorn;
the LINT bans Math.random/Date/performance.now/storage/DOM/globalThis/network/eval/bare timers/import — an error
stops the load on EVERY peer), then the MODULE LOADER (`userModules.importModuleText`, a fresh blob import) with
stops the load on EVERY peer), then `behaviours/source.js compileBehaviour(source, importer)` (the caller's importer; `dataUrlImporter` in node/tests) with
`source.js`'s one-line prologue ON line 1 (`behaviour, kit, dist, clamp, lerp`; `kit` is a proxy bound to the
behaviour's traced kit face when it starts) and the 27-D loop guard (counter reset per dispatch by an appended
epilogue line). T2: one lifecycle module per behaviour, `behaviour:<nodeId>`, with ONE kit face for its whole
Expand Down
23 changes: 23 additions & 0 deletions OPEN-CORE.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,29 @@ everyone leaving). Each is inert without a plugin: nothing subscribes until `onS
A plugin restoring a SCENE uses `loadRemoteScene({sceneUrl, keepGameId: true})` — the owner reopening their own
scene keeps its game id instead of forking it as a remix.

### 6. Object schema (roadmap 101 F2) — a contract, not a hook

Objects and materials are validated on receive and on load. Every three.js object JSON core takes from a
peer, a file or its own stores passes [src/lib/sceneSchema.js](src/lib/sceneSchema.js) (through
`src/lib/loadGuard.js`) before three parses it, and so does every multi-slot `materials` payload. A CI step
(`npm run check:loaders`) fails on a generic three loader called anywhere else in `src/`.

What a module or plugin can rely on, and what it must keep to:

| | the rule |
|---|---|
| object types | three's own (`Object3D`, `Group`, `Mesh`, `SkinnedMesh`, `InstancedMesh`, `Line*`, `Points`, `Sprite`, `LOD`, `Bone`, the two cameras, the lights). Anything else arrives as an `Object3D` with its children |
| material types | the standard ones (`Mesh*Material`, `Line*Material`, `PointsMaterial`, `SpriteMaterial`, `ShadowMaterial`). Any other material arrives as the standard fallback material; custom looks are made with the shader graph |
| fields | the fields three's `toJSON()` writes for that type, each with a value rule. An unknown field is stripped; a bad value is stripped and three uses its default |
| images | embedded data URLs only, each under a size cap |
| `userData` | any plain JSON under a size cap (no `__proto__` / `constructor` / `prototype` keys, bounded depth). The keys core and the shipped modules write keep a per-key kind |

Every change is counted (the wire diagnostics and one toast per session), never silent. The inventory
(`scripts/schema-inventory.cjs`, kept as `tests/fixtures/schema-1.65/inventory.json`) found that the shipped
modules build only the types above and write only plain-JSON `userData`, so modules get no registration
call. A later release that saves a new field adds it to the type's table in `sceneSchema.js` with a value
rule and a fixture in `tests/unit/sceneSchema.test.js`; a new `userData` key needs nothing.

## Versioning

`CLOUD_HOOKS_VERSION` ([src/lib/cloudHooks.js](src/lib/cloudHooks.js)) is the contract
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"sync-llms": "node scripts/sync-llms.cjs",
"test:unit": "vitest run",
"check:emoji": "node scripts/check-emoji.cjs",
"check:loaders": "node scripts/check-loaders.cjs",
"check:storage": "node scripts/check-storage.cjs",
"check:tokens": "node scripts/check-tokens.cjs",
"check:zindex": "node scripts/check-zindex.cjs"
Expand Down
73 changes: 73 additions & 0 deletions scripts/check-loaders.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env node
// 101 F2: three's generic loaders are reached ONLY through the checked helpers in src/lib/loadGuard.js
// (parseCheckedObject / parseCheckedMaterials), which run the object schema (src/lib/sceneSchema.js)
// first. A new `new THREE.ObjectLoader()`, `.parseMaterials(` or `new MaterialLoader(` anywhere else in
// src/ fails this check. Comment lines are ignored.
//
// node scripts/check-loaders.cjs → exit 1 and one line per call outside the helper
//
// Run by CI (.github/workflows/ci.yml) and tests/unit/checkLoaders.test.js.
const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const ALLOWED = new Set(['src/lib/loadGuard.js']);
const PATTERN = /\bObjectLoader\s*\(|\.parseMaterials\s*\(|\bMaterialLoader\s*\(/;

/** @param {string} dir @returns {string[]} */
function files(dir) {
/** @type {string[]} */
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...files(p));
else if (/\.(js|ts|svelte|mjs)$/.test(e.name) && !/\.(test|spec)\.[jt]s$/.test(e.name)) out.push(p);
}
return out;
}

/** @param {string} text @returns {number[]} 1-based lines with a call */
function hitsIn(text) {
/** @type {number[]} */
const out = [];
let inBlock = false;
text.split('\n').forEach((line, i) => {
let code = line;
if (inBlock) {
const end = code.indexOf('*/');
if (end < 0) return;
code = code.slice(end + 2);
inBlock = false;
}
code = code.replace(/\/\*.*?\*\//g, '');
const open = code.indexOf('/*');
if (open >= 0) {
inBlock = true;
code = code.slice(0, open);
}
code = code.replace(/\/\/.*$/, '');
// a JSDoc continuation line (` * …`) is comment text even where the opening /** was not seen
if (/^\s*\*/.test(code)) return;
if (PATTERN.test(code)) out.push(i + 1);
});
return out;
}

function main() {
/** @type {string[]} */
const bad = [];
for (const file of files(path.join(ROOT, 'src'))) {
const rel = path.relative(ROOT, file).split(path.sep).join('/');
if (ALLOWED.has(rel)) continue;
for (const line of hitsIn(fs.readFileSync(file, 'utf8'))) bad.push(`${rel}:${line}`);
}
if (bad.length) {
console.log('check-loaders: three generic loader calls outside src/lib/loadGuard.js (use parseCheckedObject / parseCheckedMaterials):');
for (const b of bad) console.log(' ' + b);
process.exit(1);
}
console.log('check-loaders — clean (every generic loader call goes through src/lib/loadGuard.js)');
}

if (require.main === module) main();
module.exports = { hitsIn };
Loading
Loading