Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/site-refactor.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"mode": "semantic",
"moves": {},
"outputs": [],
"layout": "none",
"change": "S3-5 navigation requests read subjects from their owner instead of the selection module"
"layout": "changes",
"change": "S3-4 moon catalogue"
}
3 changes: 1 addition & 2 deletions docs/site-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ A folder imports itself or strictly lower layers. Sibling folders, including L0
| 1 | `directory/` | Startup reads and object catalogue directory | 16; `startup-world.mts`, `startup-requests.mts`, `object-entries.mts`, `object-directory.mts` |
| 1 | `minimap/` | Surface-map measurements and view formatting | 4 |
| 2 | `world/` | Shared framing, visibility and camera context | 73; `world-objects.mts`, `world-system-views.mts`, `context-availability.mts`, `context-datasets.mts` |
| 3 | `content/` | Card content, citations and metadata | 29; `dataset-content.mts`, `dataset-context.mts`, `object-text.mts`, `prepared-panel-content.mts` |
| 3 | `content/` | Card content, citations and metadata | 31; `dataset-content.mts`, `dataset-context.mts`, `object-text.mts`, `prepared-panel-content.mts` |
| 3 | `navigation/` | History, requests, flights and arrivals | 25; `prepared-arrival.mts`, `prepared-scene-ownership.mts`, `arrival-billboard.mts`, `prepared-world-navigation.mts` |
| 4 | `search/` | Catalogue search and result presentation | 6 |
| 4 | `selection/` | Committed selection and camera handovers | 8; `scene/scene-selection.mts`, `satellite-selection.mts`, `overview-selection.mts`, `showcase.mts` |
Expand Down Expand Up @@ -60,7 +60,6 @@ The changes array partitions stable edit ids into atomic groups. All seven group
| Tier | Change / PR | Edit ids | Required code change |
| ---: | --- | --- | --- |
| 0 | Extract held address reader | add-file-navigation-href, add-import-navigation-history-navigation-href, retarget-application-world-resources-navigation-href | Move navigationHref AND its owners WeakMap. Export a registration function for history; preserve deferred URL semantics. History registers and unregisters its held address reader in the same WeakMap; re-export the getter for existing navigation callers. Only lower world caller uses navigationHref; other callers retain history behavior. |
| 3 | Split the moon catalogue reader | add-file-moon-catalogue, retarget-object-children-moon-catalogue, remove-import-prepare-body-moons-moon-catalogues, add-import-prepare-body-moons-moon-catalogue | Split catalogue parsing, cached source catalogue and catalogueMoons into a runtime-safe reader; preparation keeps search-based ordering. Read catalogueMoons from the split reader; production must never import build preparation. Move the source catalogue import with its parser into the reader; preparation calls that reader. Use the single catalogue reader for preparation too; preserve the named-moon eligibility policy in preparation. |
| 0 | Relocate queued loader owners | add-file-scene-imports, add-file-world-imports, remove-queue-runtime, remove-shared-registry, remove-shared-world, retarget-adapter-scene-imports, retarget-router-world-imports, add-router-scene-imports, remove-directory-queue, retarget-boot-scene-imports, add-boot-world-imports | Decided by the owner: loader modules hold only import() calls through the existing queue. Own registry and packaged-runtime functions in scene; move the single last chain to lower browser/import-queue. Prove one queue chain, registration before the first scene call, the same entry-chunk modules and bytes, identical L1/L2/L3, L7 no worse and the real-iPad startup journey. Own the queued application-world import in world; no initializer inside the thunk. Move the runtime function out of the import-free browser queue implementation. Leave only importSceneRouter in startup/shared-imports. Move the world function to its owner. Adapter directly uses the same queued runtime function; preserve the frozen default adapter. Split router loader imports; preserve module-evaluation autostart. Import registry/runtime functions and register only the directory runtime loader using the existing object-directory import before first scene call. Directory accepts a typed runtime-loader registration; read the slot at scene invocation, not metadata construction. Missing registration fails explicitly. Body prestart and registry prestart reuse scene-owned promises. View prestart reuses the world-owned promise. |
<!-- /generated:changes -->

Expand Down
55 changes: 0 additions & 55 deletions docs/site-architecture/edits.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,52 +28,6 @@
"id": "retarget-application-world-resources-navigation-href",
"line": 18
},
{
"op": "add-file",
"path": "site/content/moon-catalogue.mts",
"note": "Split catalogue parsing, cached source catalogue and catalogueMoons into a runtime-safe reader; preparation keeps search-based ordering.",
"change": "Split the moon catalogue reader",
"imports": [
{
"to": "site/source/moon-catalogues.json",
"kind": "asset"
},
{
"to": "packages/objects/src/sources/index.ts",
"kind": "value"
}
],
"id": "add-file-moon-catalogue"
},
{
"op": "retarget",
"from": "site/content/object-children.mts",
"to": "site/build/prepare/prepare-body-moons.mts",
"note": "Read catalogueMoons from the split reader; production must never import build preparation.",
"newTo": "site/content/moon-catalogue.mts",
"change": "Split the moon catalogue reader",
"id": "retarget-object-children-moon-catalogue",
"line": 3
},
{
"op": "remove-import",
"from": "site/build/prepare/prepare-body-moons.mts",
"to": "site/source/moon-catalogues.json",
"note": "Move the source catalogue import with its parser into the reader; preparation calls that reader.",
"kind": "asset",
"change": "Split the moon catalogue reader",
"id": "remove-import-prepare-body-moons-moon-catalogues",
"line": 4
},
{
"op": "add-import",
"from": "site/build/prepare/prepare-body-moons.mts",
"to": "site/content/moon-catalogue.mts",
"kind": "value",
"note": "Use the single catalogue reader for preparation too; preserve the named-moon eligibility policy in preparation.",
"change": "Split the moon catalogue reader",
"id": "add-import-prepare-body-moons-moon-catalogue"
},
{
"id": "add-file-scene-imports",
"op": "add-file",
Expand Down Expand Up @@ -209,15 +163,6 @@
"retarget-application-world-resources-navigation-href"
]
},
{
"id": "split-the-moon-catalogue-reader",
"edits": [
"add-file-moon-catalogue",
"retarget-object-children-moon-catalogue",
"remove-import-prepare-body-moons-moon-catalogues",
"add-import-prepare-body-moons-moon-catalogue"
]
},
{
"id": "relocate-queued-loader-owners",
"edits": [
Expand Down
4 changes: 3 additions & 1 deletion docs/site-architecture/moves.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,5 +354,7 @@
"site/zoom-carry.characterization.test.mts": "site/world/zoom-carry.characterization.test.mts",
"site/zoom-scope.characterization.test.mts": "site/world/zoom-scope.characterization.test.mts",
"site/object-entry-types.mts": "site/directory/object-entry-types.mts",
"site/object-entry-types.test.mts": "site/directory/object-entry-types.test.mts"
"site/object-entry-types.test.mts": "site/directory/object-entry-types.test.mts",
"site/moon-catalogue.mts": "site/content/moon-catalogue.mts",
"site/moon-catalogue.test.mts": "site/content/moon-catalogue.test.mts"
}
24 changes: 24 additions & 0 deletions site/moon-catalogue.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import moonCatalogues from './source/moon-catalogues.json' with { type: 'json' };
import { sourceArray, sourceId, sourceObject, sourceText, sourceUnique } from '@cssearth/objects/sources';

/** Read names from the source catalogue without creating scene objects or invented positions. */
export function parseMoonCatalogue(input: unknown) {
const catalogue = sourceObject(input);
const moons = sourceArray(catalogue.moons, value => {
const moon = sourceObject(value);
return { id: sourceId(moon.id), name: sourceText(moon.name) };
});
sourceUnique(moons.map(moon => moon.id), 'moon identities');
const count = catalogue.count;
if (!Number.isInteger(count) || count !== moons.length) throw new TypeError('Moon catalogue count does not match its entries.');
return { moons };
}

const catalogues: Readonly<Record<string, ReturnType<typeof parseMoonCatalogue>>> = Object.fromEntries(
moonCatalogues.systems.map(system => [sourceId(system.id), parseMoonCatalogue(system)]));

/** The moons a host's source catalogue names, in the catalogue's order; none for a host without one. */
export const catalogueMoons = (hostId: string): readonly { readonly id: string; readonly name: string }[] => catalogues[hostId]?.moons ?? [];

/** Read the cached catalogue, preserving the distinction between an absent host and an empty catalogue. */
export const readMoonCatalogue = (hostId: string) => catalogues[hostId];
64 changes: 64 additions & 0 deletions site/moon-catalogue.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import moonCatalogues from './source/moon-catalogues.json' with { type: 'json' };
import { sourceArray, sourceId, sourceObject, sourceText, sourceUnique } from '@cssearth/objects/sources';
import { catalogueMoons, parseMoonCatalogue, readMoonCatalogue } from './moon-catalogue.mts';
import ts from 'typescript';

// The pre-extraction reader, retained only as an independent parity oracle.
function oldParseMoonCatalogue(input: unknown) {
const catalogue = sourceObject(input);
const moons = sourceArray(catalogue.moons, value => {
const moon = sourceObject(value);
return { id: sourceId(moon.id), name: sourceText(moon.name) };
});
sourceUnique(moons.map(moon => moon.id), 'moon identities');
const count = catalogue.count;
if (!Number.isInteger(count) || count !== moons.length) throw new TypeError('Moon catalogue count does not match its entries.');
return { moons };
}

test('the whole source catalogue and validation errors match the old reader', () => {
for (const system of moonCatalogues.systems) {
assert.equal(JSON.stringify(parseMoonCatalogue(system)), JSON.stringify(oldParseMoonCatalogue(system)), system.id);
assert.deepEqual(catalogueMoons(system.id), oldParseMoonCatalogue(system).moons);
assert.strictEqual(catalogueMoons(system.id), readMoonCatalogue(system.id)?.moons);
assert.strictEqual(readMoonCatalogue(system.id), readMoonCatalogue(system.id));
}
assert.equal(readMoonCatalogue('not-a-host'), undefined);
assert.deepEqual(catalogueMoons('not-a-host'), []);
for (const input of [null, {}, { count: 0, moons: [] }, { count: 1, moons: [] },
{ count: 1, moons: [{ id: 'Bad ID', name: 'Name' }] },
{ count: 2, moons: [{ id: 'moon', name: 'Moon' }, { id: 'moon', name: 'Moon' }] }]) {
const outcome = (parse: typeof parseMoonCatalogue) => {
try { return JSON.stringify(parse(input)); }
catch (error) { assert.ok(error instanceof Error); return `${error.name}: ${error.message}`; }
};
assert.equal(outcome(parseMoonCatalogue), outcome(oldParseMoonCatalogue));
}
});

/** Every module a file names, whether by import, re-export, dynamic import or inline import type, resolved against that file. */
function modulesNamedBy(file: URL): string[] {
const named: string[] = [];
const visit = (node: ts.Node): void => {
const literal = ts.isImportDeclaration(node) || ts.isExportDeclaration(node) ? node.moduleSpecifier
: ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword ? node.arguments[0]
: ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) ? node.argument.literal : undefined;
if (literal && ts.isStringLiteral(literal)) named.push(literal.text.startsWith('.') ? new URL(literal.text, file).pathname : literal.text);
ts.forEachChild(node, visit);
};
visit(ts.createSourceFile(file.pathname, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true));
return named;
}

test('production children read the shared reader and never name preparation; preparation has one reader', () => {
const children = modulesNamedBy(new URL('./object-children.mts', import.meta.url));
const reader = new URL('./moon-catalogue.mts', import.meta.url).pathname;
assert.ok(children.includes(reader), 'Children use the shared reader');
assert.ok(!children.includes(new URL('./prepare-body-moons.mts', import.meta.url).pathname), 'Children never import preparation');
const preparation = modulesNamedBy(new URL('./prepare-body-moons.mts', import.meta.url));
assert.ok(preparation.includes(reader), 'Preparation reads the shared catalogue');
assert.ok(!preparation.some(module => module.endsWith('/moon-catalogues.json') || module === '@cssearth/objects/sources'), 'Preparation must not re-parse a separate catalogue');
});
2 changes: 1 addition & 1 deletion site/object-children.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OBJECTS, ancestorsOf, type NavigableObject } from './objects.mts';
import { WORLD_HOST_ID } from './navigation/navigation-scope.mts';
import { catalogueMoons } from './prepare-body-moons.mts';
import { catalogueMoons } from './moon-catalogue.mts';
import { APPLICATION_WORLD_CONTEXT, APPLICATION_WORLD_FILE_OF } from './world-context-plan.mts';

const PARSEC_M = 3.085677581491367e16;
Expand Down
25 changes: 25 additions & 0 deletions site/prepare-body-moons.characterization.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,28 @@ test('catalogue hosts retain source order, and unknown hosts have no moons', ()
assert.deepEqual(catalogueMoons('not-a-host'), []);
assert.deepEqual(prepareBodyMoons('not-a-host'), []);
});

// Whole-catalogue parity uses the actual production registry and search order.
test('preparation preserves the old serialized results, ordering and named-label eligibility', async () => {
const { default: input } = await import('./source/moon-catalogues.json', { with: { type: 'json' } });
const { SEARCH_OBJECTS } = await import('./search/search-objects.mts');
const { systemObjectId } = await import('./navigation/system-address.mts');
const { readMoonCatalogue } = await import('./moon-catalogue.mts');
for (const hostId of [...input.systems.map(system => system.id), ...new Set(SEARCH_OBJECTS.map(object => object.id)), 'not-a-host']) {
const available = SEARCH_OBJECTS.filter(object => object.classification === 'satellite' && object.parent === systemObjectId(hostId));
const source = input.systems.find(system => system.id === hostId);
const byId = new Map(available.map(object => [object.id, object]));
const old = source ? source.moons.map(moon => ({ id: moon.id, name: moon.name, object: byId.get(moon.id) }))
: available.map(object => ({ id: object.id, name: object.name, object }));
assert.equal(JSON.stringify(prepareBodyMoons(hostId)), JSON.stringify(old), hostId);
if (source) assert.deepEqual(source.moons.filter(moon => hasProperMoonName(moon)),
source.moons.filter(moon => moon.name !== moon.provisionalDesignation));
}
// The default production call must observe the shared cache, not another parse.
const cached = readMoonCatalogue('mars')!;
const original = cached.moons;
try {
cached.moons = [...original].reverse();
assert.deepEqual(prepareBodyMoons('mars').map(moon => moon.id), cached.moons.map(moon => moon.id));
} finally { cached.moons = original; }
});
25 changes: 3 additions & 22 deletions site/prepare-body-moons.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SEARCH_OBJECTS } from './search/search-objects.mts';
import type { ObjectEntry } from './objects.mts';
import { systemObjectId } from './navigation/system-address.mts';
import moonCatalogues from './source/moon-catalogues.json' with { type: 'json' };
import { sourceArray, sourceId, sourceObject, sourceText, sourceUnique } from '@cssearth/objects/sources';
import { readMoonCatalogue } from './moon-catalogue.mts';
export { parseMoonCatalogue, catalogueMoons } from './moon-catalogue.mts';
import { labelEligible } from '@cssearth/renderer/labels/universe-label-policy.ts';

export interface MoonListEntry { id: string; name: string; object?: ObjectEntry; }
Expand All @@ -13,31 +13,12 @@ export function hasProperMoonName(moon: { name: string; provisionalDesignation:
return labelEligible({ named: moon.name !== moon.provisionalDesignation });
}

/** Read names from the source catalogue without creating scene objects or invented positions. */
export function parseMoonCatalogue(input: unknown) {
const catalogue = sourceObject(input);
const moons = sourceArray(catalogue.moons, value => {
const moon = sourceObject(value);
return { id: sourceId(moon.id), name: sourceText(moon.name) };
});
sourceUnique(moons.map(moon => moon.id), 'moon identities');
const count = catalogue.count;
if (!Number.isInteger(count) || count !== moons.length) throw new TypeError('Moon catalogue count does not match its entries.');
return { moons };
}

const catalogues: Readonly<Record<string, ReturnType<typeof parseMoonCatalogue>>> = Object.fromEntries(
moonCatalogues.systems.map(system => [sourceId(system.id), parseMoonCatalogue(system)]));

/** The moons a host's source catalogue names, in the catalogue's order; none for a host without one. */
export const catalogueMoons = (hostId: string): readonly { readonly id: string; readonly name: string }[] => catalogues[hostId]?.moons ?? [];

/** A host's moons: the moons inside its system in the object tree, nearest the Sun first, or, where its moon catalogue
* names them, in the catalogue's order with the moons that have no package yet. */
export function prepareBodyMoons(objectId: string): readonly MoonListEntry[] {
const system = systemObjectId(objectId);
const available = SEARCH_OBJECTS.filter(object => object.classification === 'satellite' && object.parent === system);
const catalogue = catalogues[objectId];
const catalogue = readMoonCatalogue(objectId);
if (!catalogue) return available.map(object => ({ id: object.id, name: object.name, object }));
const byId = new Map(available.map(object => [object.id, object]));
const knownIds = new Set(catalogue.moons.map(moon => moon.id));
Expand Down
Loading