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
10 changes: 6 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,20 @@ jobs:
env:
CSSEARTH_ALLOW_MISSING_ASSETS: "1"
run: pnpm setup:assets
- name: Build the site with ASSET_ORIGIN set to a test origin
- name: Build the site with the production asset origin and reject metadata drift
env:
NODE_OPTIONS: --max-old-space-size=6144
CSSEARTH_ALLOW_MISSING_ASSETS: "1"
ASSET_ORIGIN: https://asset-origin-ci-check.example
run: pnpm build:deploy
ASSET_ORIGIN: https://earth-assets.lowpoly.cc
run: |
pnpm build:deploy
pnpm check:deploy-assets
- name: Check no page references a same-origin /scenes/ address
run: node tools/check-asset-origin-scenes.mts dist
- name: Check production navigation loads content-addressed assets
env:
CSSEARTH_TEST_ORIGIN: http://127.0.0.1:4212
CSSEARTH_ASSET_ORIGIN: https://asset-origin-ci-check.example
CSSEARTH_ASSET_ORIGIN: https://earth-assets.lowpoly.cc
PLAYWRIGHT_CHANNEL: chromium
run: |
pnpm exec playwright install --with-deps chromium
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"predev": "pnpm build:tools && pnpm prepare:object-json && pnpm prepare:environment-images && pnpm prepare:minimap && pnpm prepare:galaxy-field",
"prepare:catalog": "node tools/prepare-catalog.mts",
"prepare:checkout": "node tools/restore-source-inputs.mts && pnpm prepare:planets",
"prepare:deploy": "pnpm build:tools && pnpm setup:assets && pnpm prepare:object-json && pnpm prepare:environment-images && node tools/nebula/prepare.mts --if-missing && pnpm prepare:minimap && pnpm prepare:galaxy-field:data && pnpm prepare:deploy-catalogues",
"prepare:deploy": "pnpm build:tools && pnpm setup:assets && pnpm prepare:object-json && pnpm prepare:environment-images && node tools/nebula/prepare.mts --if-missing && pnpm prepare:minimap && pnpm prepare:deploy-catalogues",
"prepare:deploy-catalogues": "node tools/prepare-facilities.mts --catalog-only",
"prepare:earth-global": "node tools/objects/geographic-pages/operations/prepare-global-wmts.mts --object=earth && node tools/objects/geographic-pages/operations/integrate-global-wmts.mts --object=earth --latest",
"prepare:environment-images": "node tools/objects/dist/restore-environment-images.js",
Expand Down Expand Up @@ -102,6 +102,7 @@
"prepare:volume": "node tools/objects/dist/prepare-volume.js",
"prepare:wordmark": "node tools/prepare-wordmark-rail.mts",
"prepare:world-context": "node tools/objects/dist/prepare-spatial-context.js src/objects/sun/source/navigation/universe.json src/objects/sun/prepared/world-context.json && pnpm prepare:moon-labels",
"prepare:world-context:author": "node tools/objects/dist/prepare-spatial-context.js src/objects/sun/source/navigation/universe.json src/objects/sun/prepared/world-context.json --pin-references && pnpm prepare:moon-labels",
"pretest": "pnpm prepare:shell",
"preview": "node tools/preview.mts",
"preview:native-scroll": "node tools/experiments/native-scroll/run.mts",
Expand Down
3 changes: 2 additions & 1 deletion site/application-world-context.mts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function loadApplicationUniverse(): Promise<ApplicationUniverse> {
} } };
};
const volumeSet = resourceSet(applicationContext.volume.objectId), starSet = resourceSet(applicationContext.stars.objectId);
const backgroundPointSet = resourceSet('nearby-universe');
const [volume, pointAppearance] = await Promise.all([
loadPreparedCssVolume(volumeSet.descriptor, volumeSet.transport),
loadPreparedPointAppearance(starSet.descriptor, starSet.transport),
Expand Down Expand Up @@ -109,7 +110,7 @@ function loadApplicationUniverse(): Promise<ApplicationUniverse> {
const plannerSource = { contextUrl: APPLICATION_WORLD_CONTEXT_URL };
const catalogBank = { fadeStartDistanceM: galaxyPresentation.fadeStartDistanceM, fullDistanceM: galaxyPresentation.fullDistanceM,
clusters: { fadeStartDistanceM: clusterPresentation.fadeStartDistanceM, fullDistanceM: clusterPresentation.fullDistanceM } };
const universe = createPreparedUniverse({ environmentLinks: { 'milky-way': '/sun/?overview=milky-way' }, context: applicationContext, volume, pointAppearance, sprites, imageLayerBanks, loadImageLayer, volumeLensBanks, loadVolumeLens, backgroundPointSha256: parseObjectDescriptor(galaxyFieldDescriptor).prepared?.sha256, backgroundPointManifest: new URL('../src/objects/nearby-universe/prepared/points.json', import.meta.url).href, backgroundPointCloud: new URL('../src/objects/nearby-universe/prepared/cloud.webp', import.meta.url).href, annotationPriorities, annotationOpacities, plannerSource, catalogBank,
const universe = createPreparedUniverse({ environmentLinks: { 'milky-way': '/sun/?overview=milky-way' }, context: applicationContext, volume, pointAppearance, sprites, imageLayerBanks, loadImageLayer, volumeLensBanks, loadVolumeLens, backgroundPointSha256: parseObjectDescriptor(galaxyFieldDescriptor).prepared?.sha256, backgroundPointManifest: backgroundPointSet.resolve('prepared/points.json'), backgroundPointCloud: backgroundPointSet.resolve('prepared/cloud.webp'), annotationPriorities, annotationOpacities, plannerSource, catalogBank,
loadCatalog: async () => {
const { galaxies, clusters, nebulae } = await loadCatalogs();
return { payload: galaxies, galaxySample: galaxyDisplaySample, nebulae, ...catalogBank,
Expand Down
4 changes: 2 additions & 2 deletions src/objects/nearby-universe/runtime-assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
},
{
"filename": "provenance.json",
"bytes": 10363,
"sha256": "ee12abfd6a352def90493a13ade2c8297f6b75694240690980816e3482b582ea"
"bytes": 10354,
"sha256": "00dcf2448fb75c786fca834abb09d7bbdfc4dbd6cb9d235f456b0c65aad04a55"
},
{
"filename": "presentation.json",
Expand Down
21 changes: 10 additions & 11 deletions src/objects/nearby-universe/source/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,21 @@
"kind": "local",
"reason": "Project-authored processing configuration."
}
}
],
"generatedIntermediates": [
},
{
"id": "navigation-frame",
"kind": "generated-intermediate",
"path": "src/objects/sun/prepared/world-context.json",
"origin": "project-preparation",
"kind": "authored-document",
"path": "src/objects/sun/source/navigation/universe.json",
"origin": "project-authored",
"credit": "cssEarth contributors",
"acquisition": "Existing prepared shared world frame; only the frame identity and navigation epoch are read. No solar data supplies galaxy positions.",
"expectedSha256": "4330a638b653f9529d1089b0069353ba8669b7f447c055d7c8b929e1e75b7242",
"expectedBytes": 7536975,
"acquisition": "Authored shared navigation frame; only the frame identity and navigation epoch are read. No solar data supplies galaxy positions.",
"expectedSha256": "02a2fc7561bf3402bdda66f88d21108bc0fca01308eaf864ad108321039b4195",
"expectedBytes": 3396,
"sourceBinding": {
"kind": "local",
"reason": "Prepared application frame identity, not a galaxy measurement."
"reason": "Authored application frame identity, not a galaxy measurement."
}
}
]
],
"generatedIntermediates": []
}
23 changes: 22 additions & 1 deletion tools/body-additions.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { mkdtemp, mkdir, readFile, rm, writeFile, type FileHandle } from 'node:f
import { tmpdir } from 'node:os';
import { dirname, resolve } from 'node:path';
import test from 'node:test';
import { contextObjectModule, readCatalog, prepareCatalog } from './prepare-catalog.mts';
import { contextObjectAssetUrls, contextObjectModule, readCatalog, prepareCatalog } from './prepare-catalog.mts';
import { prepareBodyRecords } from '../packages/astronomy/tools/body-records.mts';
import { literalRecords } from '../packages/astronomy/tools/lib/write-record-sections.mts';
import type { PathLike } from 'node:fs';
Expand All @@ -25,6 +25,27 @@ test('context prepared resources stay external until their bank is selected', ()
'The source-only point bank stays excluded while its published resources remain external.');
});

test('asset-origin context resources come from inventories without local prepared bytes', async t => {
const root = await mkdtemp(resolve(tmpdir(), 'cssearth-context-assets-'));
t.after(() => rm(root, { recursive: true, force: true }));
await write(resolve(root, 'src/objects/nearby-universe/runtime-assets.json'), {
schema: 'cssnearby-universe-runtime-assets@1', resourceRoot: 'prepared', assets: [
{ filename: 'points.json', sha256: 'a'.repeat(64), bytes: 10 },
{ filename: 'cloud.webp', sha256: 'b'.repeat(64), bytes: 20 },
{ filename: 'datasets/preview.webp', sha256: 'c'.repeat(64), bytes: 30, location: 'public' },
],
});
const origin = 'https://assets.example.test';
const assets = await contextObjectAssetUrls([{ id: 'nearby-universe' }], root, origin);
assert.deepEqual(assets, {
'../src/objects/nearby-universe/prepared/cloud.webp': `${origin}/runtime-assets/${'b'.repeat(64)}/cloud.webp`,
'../src/objects/nearby-universe/prepared/points.json': `${origin}/runtime-assets/${'a'.repeat(64)}/points.json`,
});
const source = contextObjectModule([{ id: 'nearby-universe', type: 'galaxy-point-field' }], assets);
assert.match(source, /https:\/\/assets\.example\.test\/runtime-assets/u);
assert.doesNotMatch(source, /query: '\?url&no-inline'/u);
});

async function addBody(root: string, id: string, classification: string, parent = 'sun') {
await write(resolve(root, `src/objects/${id}/object.json`), {
schema: 'cssearth-object@1', id, type: 'test', properties: {
Expand Down
7 changes: 4 additions & 3 deletions tools/check-ci.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ test('the deploy consumes installed assets, rebuilds only catalogues and rejects
assert.match(workflow,/git diff --quiet -- src\/objects/);
assert.doesNotMatch(workflow,/ASSET_ORIGIN=https:\/\/earth-assets\.lowpoly\.cc pnpm build(?:\s|$)/);
assert.match(packageFile.scripts['prepare:deploy']??'',/node tools\/nebula\/prepare\.mts --if-missing/);
assert.match(packageFile.scripts['prepare:deploy']??'',/pnpm prepare:galaxy-field:data/);
assert.doesNotMatch(packageFile.scripts['prepare:deploy']??'',/pnpm prepare:galaxy-field:data/);
assert.match(packageFile.scripts['prepare:deploy']??'',/pnpm prepare:deploy-catalogues/);
assert.equal(packageFile.scripts['prepare:deploy-catalogues'],'node tools/prepare-facilities.mts --catalog-only');
assert.match(packageFile.scripts['setup:assets']??'',/node tools\/setup-volume-metadata\.mts/);
Expand All @@ -43,8 +43,9 @@ test('the deploy consumes installed assets, rebuilds only catalogues and rejects
test('the PR asset-origin check exercises the exact deploy build path',async()=>{
const workflow=await readFile(new URL('../.github/workflows/nightly.yml',import.meta.url),'utf8');
const steps=readCiSteps(workflow,'asset-origin-build');
const build=steps.find(step=>step.name==='Build the site with ASSET_ORIGIN set to a test origin');
assert.equal(build?.run.trim(),'pnpm build:deploy');
const build=steps.find(step=>step.name==='Build the site with the production asset origin and reject metadata drift');
assert.equal(build?.env.ASSET_ORIGIN,'https://earth-assets.lowpoly.cc');
assert.equal(build?.run.trim(),'pnpm build:deploy\npnpm check:deploy-assets');
});
test('--quick skips only the network and documentation steps, and refuses a job without them',async()=>{
const lint=readCiSteps(await readFile(new URL('../.github/workflows/universe.yml',import.meta.url),'utf8'),'lint');
Expand Down
2 changes: 1 addition & 1 deletion tools/check-deploy-assets.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { inventoriedAssets, inventoriedObjectIds, RUNTIME_ASSET_ORIGIN } from '.
const execFileAsync = promisify(execFile);
const textExtensions = new Set(['.css', '.html', '.js', '.json', '.map', '.svg', '.txt', '.xml']);
const escapedOrigin = RUNTIME_ASSET_ORIGIN.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
const runtimeAssetPattern = new RegExp(`${escapedOrigin}/runtime-assets/[a-f0-9]{64}/[a-zA-Z0-9._/-]+`, 'gu');
const runtimeAssetPattern = new RegExp(`${escapedOrigin}/runtime-assets/[a-f0-9]{64}/[a-zA-Z0-9._@/-]+`, 'gu');

export function runtimeAssetUrls(text: string): string[] {
return [...new Set(text.match(runtimeAssetPattern) ?? [])].sort();
Expand Down
2 changes: 1 addition & 1 deletion tools/check-deploy-assets.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import test from 'node:test';
import { runtimeAssetUrls, unknownRuntimeAssetUrls } from './check-deploy-assets.mts';

const hash = 'a'.repeat(64);
const known = `https://earth-assets.lowpoly.cc/runtime-assets/${hash}/datasets/preview.webp`;
const known = `https://earth-assets.lowpoly.cc/runtime-assets/${hash}/datasets/preview@2x.webp`;

test('deploy asset closure extracts unique R2 runtime URLs and rejects an uninventoried hash', () => {
assert.deepEqual(runtimeAssetUrls(`<img src="${known}"><script>const again='${known}'</script>`), [known]);
Expand Down
15 changes: 15 additions & 0 deletions tools/context-provenance.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile, readdir, mkdir, mkdtemp, rm, cp, copyFile, symlink } from 'node:fs/promises';
import { prepareContextProvenance } from './prepare-context-provenance.mts';
import { readPreparedContextProvenance } from './read-prepared-context-provenance.mts';
import { prepareVolumeProvenance } from './prepare-volume-provenance.mts';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
Expand All @@ -28,6 +29,15 @@ test('context provenance binds every declared output and installs one complete i
assert.ok(context.provenance.sources.some(s=>s.sourceBinding?.kind==='catalogued'));
}
});

test('deploy catalogue recovery reads prepared contexts without authoring intermediates', async () => {
const contexts = await readPreparedContextProvenance({ input: path => {
assert.doesNotMatch(path, /sun\/prepared\/world-context\.json/u);
return readFile(path);
} });
assert.deepEqual(contexts.map(context => context.id), ['galaxy-clusters', 'local-group', 'nearby-universe']);
assert.ok(contexts.every(context => context.outputs.length === 0));
});
test('changed prepared bytes and authored source documents are rejected', async () => {
for(const target of ['src/objects/local-group/prepared/manifest.json','src/objects/nearby-universe/source/preparation/field.json']) {
await assert.rejects(prepareContextProvenance({input:async path=>{
Expand Down Expand Up @@ -86,6 +96,11 @@ test('offline context recovery is independent of installed generated images and
await copyFile(`${base}/object.json`,join(root,base,'object.json'));
await copyFile(`${base}/prepared/manifest.json`,join(root,base,'prepared/manifest.json'));
}
await mkdir(join(root,'src/objects/sun/source/navigation'),{recursive:true});
await copyFile(
'src/objects/sun/source/navigation/universe.json',
join(root,'src/objects/sun/source/navigation/universe.json'),
);
const offline=await prepareContextProvenance({root,input:path=>readFile(path.startsWith('tools/')?path:join(root,path))});
assert.deepEqual(offline.map(c=>c.id),['galaxy-clusters','local-group','nearby-universe']);
const installed=await prepareContextProvenance();
Expand Down
2 changes: 1 addition & 1 deletion tools/galaxy-field/prepare-points.mts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const recipe = await readFieldRecipe();
const presentation=sourceObject(JSON.parse(await readFile('src/objects/nearby-universe/source/presentation.json','utf8')));
const appearance=Object.fromEntries(['pointExposure','pointReferenceDistanceMpc','cloudExposure','cloudMaximumOpacity'].map(key=>{const value=presentation[key];if(typeof value!=='number'||!Number.isFinite(value)||value<=0)throw new TypeError('Invalid field presentation');return [key,value];}));
const catalogue = await loadScientificCatalogue(recipe.maximumDistanceMpc, recipe.minimumDistanceMpc, recipe.hubbleKmSPerMpc);
const world = JSON.parse(await readFile('src/objects/sun/prepared/world-context.json', 'utf8'));
const world = JSON.parse(await readFile('src/objects/sun/source/navigation/universe.json', 'utf8'));
if (world.frame?.referenceFrame !== 'sun-icrf' || !Number.isFinite(world.frame.epochJdTt)) throw new TypeError('Invalid application world frame.');
const frame = parseDensityVolumeFrame({ referenceFrame: world.frame.referenceFrame, epochJdTt: world.frame.epochJdTt, originM: [0,0,0], localToReferenceXyzw: [0,0,0,1], metersPerUnit: 3.085677581491367e22, boundsUnits: { min: [-200,-200,-200], max: [200,200,200] } });
// Keep half the budget spread through the field; spend the rest on observed concentrations.
Expand Down
18 changes: 16 additions & 2 deletions tools/galaxy-field/recipe.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { parseFieldRecipe } from './recipe.mts';
import { sha256 } from '../../src/platform/sha256.mts';

const input: unknown = JSON.parse(await readFile(new URL('../../src/objects/nearby-universe/source/preparation/field.json', import.meta.url), 'utf8'));
const recipe = parseFieldRecipe(input);
Expand All @@ -22,6 +23,19 @@ test('cloud texture needs at least two pixels to normalize its radial coordinate
assert.equal(parseFieldRecipe({ schema: 'cssearth-galaxy-field-recipe@1', ...recipe, texture: { ...recipe.texture, size: 2 } }).texture.size, 2);
});

test('the galaxy field pins the authored frame contract, not rendered world geometry', async () => {
const manifest = JSON.parse(await readFile('src/objects/nearby-universe/source/manifest.json', 'utf8'));
const navigation = manifest.documents.find((entry: { id: string }) => entry.id === 'navigation-frame');
assert.equal(navigation.path, 'src/objects/sun/source/navigation/universe.json');
const frame = await readFile(navigation.path);
assert.equal(frame.length, navigation.expectedBytes);
assert.equal(sha256(frame), navigation.expectedSha256);
assert.equal(manifest.generatedIntermediates.length, 0);
const preparer = await readFile(new URL('./prepare-points.mts', import.meta.url), 'utf8');
assert.match(preparer, /sun\/source\/navigation\/universe\.json/u);
assert.doesNotMatch(preparer, /sun\/prepared\/world-context\.json/u);
});


test('a changed source manifest pin fails before any field output is replaced', async t => {
const root = await mkdtemp(resolve(tmpdir(), 'galaxy-field-pins-'));
Expand All @@ -48,8 +62,8 @@ test('the field bake replaces stale generated files and restores missing receipt
await cp(resolve(base,'source'),resolve(root,base,'source'),{recursive:true});
await mkdir(resolve(root,'.local/galaxy-field'),{recursive:true});
await cp('.local/galaxy-field/sources',resolve(root,'.local/galaxy-field/sources'),{recursive:true});
await mkdir(resolve(root,'src/objects/sun/prepared'),{recursive:true});
await cp('src/objects/sun/prepared/world-context.json',resolve(root,'src/objects/sun/prepared/world-context.json'));
await mkdir(resolve(root,'src/objects/sun/source/navigation'),{recursive:true});
await cp('src/objects/sun/source/navigation/universe.json',resolve(root,'src/objects/sun/source/navigation/universe.json'));
await writeFile(resolve(root,base,'prepared/points.json'),'stale generation');
await writeFile(resolve(root,base,'prepared/cloud.webp'),'stale generation');
const result=spawnSync(process.execPath,[fileURLToPath(new URL('./prepare-points.mts',import.meta.url))],{cwd:root,encoding:'utf8'});
Expand Down
Loading
Loading