mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 04:38:55 +00:00
feat(view): the moon in its phase over the "Follow the Sun" background (#661)
At dawn, dusk and night the environment shows the moon in the top-left corner of the scene, behind the plan: computed in the card from the home coordinates and the browser clock (short Meeus series + topocentric parallax, within 1.4° / 1.8 pp of JPL Horizons), one designer image under a continuous phase mask with the lit side always on the left (owner 2026-09-29), a feathered terminator, 3°/3 % thresholds and the 2 s fade of the window rays. Everything but a small gate lives in the lazy moon-runtime chunk, with its own 30 s ticker. General settings: "Sun" becomes "Sun and Moon" with one switch, on for new installations (DEFAULT_CONFIG), off for existing ones. The initial View graph sat 728 B under its budget: the gate is paid for by moving fifteen dialog-only strings of General settings into the lazy settings dictionary (#459) and by one build fingerprint literal instead of three, so the graph ends 4 B above dev. Golden: two new moon scenes, and the two General settings help frames show «Sun and Moon»; the WSL artifact test fixture now models scenes whose first capture awaits acceptance. Issue: #661 User-Visible: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e515bdaab8
commit
c8c470ed57
@@ -734,6 +734,13 @@ export function assertBundleBudget(
|
||||
if (!manifest.lazyPdfFiles?.length) {
|
||||
throw new Error('bundle has no lazy PDF graph');
|
||||
}
|
||||
// #661 C7/AC8: the moon's math, art and template load only at night.
|
||||
if (!manifest.lazyMoonFiles?.length) {
|
||||
throw new Error('bundle has no lazy moon graph');
|
||||
}
|
||||
if (manifest.initialViewFiles.some((path) => manifest.lazyMoonFiles.includes(path))) {
|
||||
throw new Error('initial View graph overlaps lazy moon graph');
|
||||
}
|
||||
// #474: designer furniture artwork is lazy; a static import anywhere in the
|
||||
// View graph would pull ~10 KB gzip back into the initial graph silently.
|
||||
if (!manifest.lazyFurnitureArtFiles?.length) {
|
||||
@@ -806,6 +813,7 @@ export function assertBundleBudget(
|
||||
lazyIsometricGzipBytes: manifest.lazyIsometricGzipBytes,
|
||||
lazyFurnitureArtGzipBytes: manifest.lazyFurnitureArtGzipBytes,
|
||||
lazyPdfGzipBytes: manifest.lazyPdfGzipBytes,
|
||||
lazyMoonGzipBytes: manifest.lazyMoonGzipBytes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -840,6 +848,7 @@ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
||||
`lazy locale: ${result.lazyLocaleGzipBytes} B gzip`,
|
||||
`lazy isometric: ${result.lazyIsometricGzipBytes} B gzip`,
|
||||
`lazy PDF: ${result.lazyPdfGzipBytes} B gzip`,
|
||||
`lazy moon: ${result.lazyMoonGzipBytes} B gzip`,
|
||||
];
|
||||
for (const line of lines) console.log(line);
|
||||
const warning = lowHeadroomWarning(headroom);
|
||||
|
||||
@@ -11,6 +11,7 @@ const DE_RETRY_ASSET_TOKEN = '__HOUSEPLAN_DE_RETRY_ASSET__';
|
||||
const FR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FR_RETRY_ASSET__';
|
||||
const FURNITURE_ART_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FURNITURE_ART_RETRY_ASSET__';
|
||||
const PDF_RETRY_ASSET_TOKEN = '__HOUSEPLAN_PDF_RETRY_ASSET__';
|
||||
const MOON_RETRY_ASSET_TOKEN = '__HOUSEPLAN_MOON_RETRY_ASSET__';
|
||||
|
||||
/**
|
||||
* #627: ru/de/fr of the three lazy dictionary namespaces are one chunk per
|
||||
@@ -110,6 +111,9 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
? 'isometric'
|
||||
: modules.some((id) => id.endsWith('/src/pdf/pdf-export.ts'))
|
||||
? 'pdf'
|
||||
// #661: the moon's math, art and template — loaded only at night.
|
||||
: modules.some((id) => id.endsWith('/src/moon-runtime.ts'))
|
||||
? 'moon'
|
||||
// #474: designer furniture artwork — its own lazy chunk, shared by
|
||||
// the editor (static import) and the View runtime (dynamic).
|
||||
: modules.some((id) => id.endsWith('/src/furniture-plan-art.generated.ts'))
|
||||
@@ -153,6 +157,8 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
const furnitureArtRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'furniture-art');
|
||||
const pdfRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'pdf'
|
||||
|| path.includes('pdf-export-'));
|
||||
const moonRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'moon'
|
||||
|| path.includes('moon-runtime-'));
|
||||
// #627: namespace dictionaries are dynamic imports of LAZY chunks, never of
|
||||
// the initial graph, so they are found by their module, not as a root.
|
||||
const namespaceLocaleRoots = files.filter((file) => file._role === 'namespace-locale')
|
||||
@@ -170,6 +176,7 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
const lazyIsometric = graphFrom(isometricRoots);
|
||||
const lazyFurnitureArt = graphFrom(furnitureArtRoots);
|
||||
const lazyPdf = graphFrom(pdfRoots);
|
||||
const lazyMoon = graphFrom(moonRoots);
|
||||
const lazyNamespaceLocale = graphFrom(namespaceLocaleRoots);
|
||||
const sum = (paths) => [...paths]
|
||||
.reduce((total, path) => total + (byPath.get(path)?.gzipBytes || 0), 0);
|
||||
@@ -198,6 +205,8 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
lazyFurnitureArtGzipBytes: sum(lazyFurnitureArt),
|
||||
lazyPdfFiles: [...lazyPdf].sort(),
|
||||
lazyPdfGzipBytes: sum(lazyPdf),
|
||||
lazyMoonFiles: [...lazyMoon].sort(),
|
||||
lazyMoonGzipBytes: sum(lazyMoon),
|
||||
lazyNamespaceLocaleFiles: [...lazyNamespaceLocale].sort(),
|
||||
lazyNamespaceLocaleGzipBytes: sum(lazyNamespaceLocale),
|
||||
files: files.map(({ _role, ...file }) => file),
|
||||
@@ -257,6 +266,8 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith('/src/furniture-plan-art.generated.ts')));
|
||||
const pdf = chunks.find((chunk) => Object.keys(chunk.modules)
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith('/src/pdf/pdf-export.ts')));
|
||||
const moon = chunks.find((chunk) => Object.keys(chunk.modules)
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith('/src/moon-runtime.ts')));
|
||||
if (!editor) throw new Error('editor runtime chunk was not emitted');
|
||||
if (!onboarding) throw new Error('onboarding runtime chunk was not emitted');
|
||||
if (!isometric) throw new Error('isometric runtime chunk was not emitted');
|
||||
@@ -264,6 +275,7 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
if (!french) throw new Error('French locale chunk was not emitted');
|
||||
if (!furnitureArt) throw new Error('furniture artwork chunk was not emitted');
|
||||
if (!pdf) throw new Error('PDF export runtime chunk was not emitted');
|
||||
if (!moon) throw new Error('moon runtime chunk was not emitted');
|
||||
const namespaceLocales = NAMESPACE_LOCALE_CHUNKS.map((entry) => {
|
||||
const chunk = chunks.find((candidate) => Object.keys(candidate.modules)
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith(entry.module)));
|
||||
@@ -279,6 +291,7 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
let germanReplacements = 0;
|
||||
let frenchReplacements = 0;
|
||||
let pdfReplacements = 0;
|
||||
let moonReplacements = 0;
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.code.includes(EDITOR_RETRY_ASSET_TOKEN)) {
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), editor.fileName);
|
||||
@@ -322,6 +335,12 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
pdfReplacements += chunk.code.split(PDF_RETRY_ASSET_TOKEN).length - 1;
|
||||
chunk.code = chunk.code.replaceAll(PDF_RETRY_ASSET_TOKEN, asset);
|
||||
}
|
||||
if (chunk.code.includes(MOON_RETRY_ASSET_TOKEN)) {
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), moon.fileName);
|
||||
if (!asset.startsWith('.')) asset = `./${asset}`;
|
||||
moonReplacements += chunk.code.split(MOON_RETRY_ASSET_TOKEN).length - 1;
|
||||
chunk.code = chunk.code.replaceAll(MOON_RETRY_ASSET_TOKEN, asset);
|
||||
}
|
||||
for (const entry of namespaceLocales) {
|
||||
if (!chunk.code.includes(entry.token)) continue;
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), entry.chunk.fileName);
|
||||
@@ -332,9 +351,9 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
}
|
||||
if (editorReplacements !== 1 || onboardingReplacements !== 1 || isometricReplacements !== 1
|
||||
|| germanReplacements !== 1 || frenchReplacements !== 1 || furnitureArtReplacements !== 1
|
||||
|| pdfReplacements !== 1) {
|
||||
|| pdfReplacements !== 1 || moonReplacements !== 1) {
|
||||
throw new Error('lazy retry URL placeholder counts are '
|
||||
+ `${editorReplacements}/${onboardingReplacements}/${isometricReplacements}/${germanReplacements}/${frenchReplacements}/${furnitureArtReplacements}/${pdfReplacements}, expected 1/1/1/1/1/1/1`);
|
||||
+ `${editorReplacements}/${onboardingReplacements}/${isometricReplacements}/${germanReplacements}/${frenchReplacements}/${furnitureArtReplacements}/${pdfReplacements}/${moonReplacements}, expected 1/1/1/1/1/1/1/1`);
|
||||
}
|
||||
// #627: the same strict rule for every namespace × language token —
|
||||
// exactly one second-attempt URL each, never zero and never two.
|
||||
|
||||
@@ -342,7 +342,7 @@ export const CHECKS = {
|
||||
entries: ['test/*.test.mjs', 'scripts/no-new-any.mjs', 'scripts/no-new-private-writes.mjs', 'scripts/render-layout-read.mjs', 'scripts/bundle-budget.mjs',
|
||||
'scripts/fix-test-build.mjs', 'scripts/unused-locals-gate.mjs'],
|
||||
// demo/helpers/** — область no-new-private-writes (#629): гейт читает их текст.
|
||||
roots: [...BUILD_INPUTS, 'assets/furniture/**', 'test/**', 'tsconfig*.json', 'scripts/monolith-baseline.json', 'demo/smoke_*.mjs', 'demo/benchmark_*.mjs', 'demo/guard/*.mjs', 'demo/helpers/**', ...WORKFLOW],
|
||||
roots: [...BUILD_INPUTS, 'assets/furniture/**', 'assets/moon/**', 'test/**', 'tsconfig*.json', 'scripts/monolith-baseline.json', 'demo/smoke_*.mjs', 'demo/benchmark_*.mjs', 'demo/guard/*.mjs', 'demo/helpers/**', ...WORKFLOW],
|
||||
},
|
||||
changed_mutants: {
|
||||
entries: ['scripts/mutation-*.mjs', 'scripts/*-guard.mjs', 'test/*.test.mjs', 'demo/smoke_*.mjs', 'tests_backend/**/*.py'],
|
||||
|
||||
@@ -413,6 +413,10 @@
|
||||
"required": true,
|
||||
"type": "str"
|
||||
},
|
||||
"config.settings.moon": {
|
||||
"required": false,
|
||||
"type": "bool"
|
||||
},
|
||||
"config.settings.new_device_ids": {
|
||||
"required": false
|
||||
},
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// Designer moon pack → `src/moon-art.generated.ts` (#661, variant A).
|
||||
//
|
||||
// The pack is one full-moon SVG; the card applies the phase as a mask (C5).
|
||||
// The generator is the gate for what the designer's file may contain: a square
|
||||
// 512 viewBox with the documented disc, vector shapes and gradients only — no
|
||||
// filters, raster, fonts, scripts or external references — and at most 30 KB.
|
||||
// Ids are prefixed so the art can live in the card's shadow root next to
|
||||
// everything else. `--check` fails when the committed module has drifted.
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
export const MOON_PACK_DIR = path.join(ROOT, 'assets', 'moon', 'houseplan-1.0.0');
|
||||
const OUT = path.join(ROOT, 'src', 'moon-art.generated.ts');
|
||||
/** Brief to the designer: ≤ 30 KB before compression. */
|
||||
export const MOON_SVG_MAX_BYTES = 30 * 1024;
|
||||
const ALLOWED_TAGS = new Set([
|
||||
'svg', 'title', 'defs', 'g', 'path', 'circle', 'ellipse', 'mask', 'radialGradient', 'linearGradient', 'stop',
|
||||
]);
|
||||
const ID_PREFIX = 'hp-moon-';
|
||||
|
||||
const fail = (message) => { throw new Error(`Moon pack: ${message}`); };
|
||||
|
||||
/** pack.json: identity, licence and the disc geometry the mask is built for. */
|
||||
export function validateMoonPack(pack) {
|
||||
if (!pack || typeof pack !== 'object' || Array.isArray(pack)) fail('pack.json must be an object');
|
||||
if (pack.schema_version !== 1) fail('schema_version must be 1');
|
||||
if (pack.pack_id !== 'houseplan') fail('pack_id must be "houseplan"');
|
||||
if (typeof pack.pack_version !== 'string' || !/^\d+\.\d+\.\d+$/.test(pack.pack_version)) fail('pack_version must be semver');
|
||||
if (pack.license !== 'MIT') fail('license must be MIT');
|
||||
if (typeof pack.source_url !== 'string' || !pack.source_url.startsWith('https://')) fail('source_url must be https');
|
||||
if (pack.file !== 'moon.svg') fail('file must be moon.svg');
|
||||
if (JSON.stringify(pack.view_box) !== '[0,0,512,512]') fail('view_box must be [0, 0, 512, 512]');
|
||||
if (JSON.stringify(pack.disc) !== '{"cx":256,"cy":256,"r":240}') fail('disc must be centred at 256 with r 240');
|
||||
if (pack.mirror !== false) fail('mirror must be false (owner 2026-09-29)');
|
||||
if (pack.lit_side !== 'left') fail('lit_side must be "left" (owner 2026-09-29)');
|
||||
return pack;
|
||||
}
|
||||
|
||||
/** The SVG body the card inlines: validated, title dropped, ids prefixed. */
|
||||
export function moonArtFromSvg(source) {
|
||||
const text = String(source).replace(/^/, '').trim();
|
||||
if (Buffer.byteLength(text, 'utf8') > MOON_SVG_MAX_BYTES) fail(`moon.svg is larger than ${MOON_SVG_MAX_BYTES} bytes`);
|
||||
if (/<!DOCTYPE|<!ENTITY|<\?|<!--|<!\[CDATA/i.test(text)) fail('moon.svg contains active XML');
|
||||
if (/href\s*=|url\((?!#)|@import|base64|font-family|style\s*=/i.test(text)) fail('moon.svg references outside itself or carries styles');
|
||||
for (const [, name] of text.matchAll(/<\/?([A-Za-z][\w:-]*)/g)) {
|
||||
if (!ALLOWED_TAGS.has(name)) fail(`moon.svg contains <${name}>`);
|
||||
}
|
||||
const root = text.match(/^<svg\b([^>]*)>([\s\S]*)<\/svg>$/);
|
||||
if (!root) fail('moon.svg is not one SVG root');
|
||||
if (!/\bviewBox="0 0 512 512"/.test(root[1])) fail('moon.svg must have viewBox="0 0 512 512"');
|
||||
let body = root[2].replace(/<title>[\s\S]*?<\/title>/g, '');
|
||||
const ids = [...body.matchAll(/\bid="([^"]+)"/g)].map((match) => match[1]);
|
||||
if (new Set(ids).size !== ids.length) fail('moon.svg repeats an id');
|
||||
for (const [, ref] of body.matchAll(/url\(#([^)]+)\)/g)) {
|
||||
if (!ids.includes(ref)) fail(`moon.svg references a missing id: ${ref}`);
|
||||
}
|
||||
if (/[`\\]|\$\{/.test(body)) fail('moon.svg contains template-literal syntax');
|
||||
body = body
|
||||
.replace(/\bid="([^"]+)"/g, (_, id) => `id="${ID_PREFIX}${id}"`)
|
||||
.replace(/url\(#([^)]+)\)/g, (_, id) => `url(#${ID_PREFIX}${id})`);
|
||||
return body;
|
||||
}
|
||||
|
||||
export function generateMoonModule(packDir = MOON_PACK_DIR) {
|
||||
const pack = validateMoonPack(JSON.parse(fs.readFileSync(path.join(packDir, 'pack.json'), 'utf8')));
|
||||
const svg = fs.readFileSync(path.join(packDir, pack.file), 'utf8');
|
||||
const art = moonArtFromSvg(svg);
|
||||
const sha = createHash('sha256').update(svg).digest('hex');
|
||||
return [
|
||||
'/* Generated by scripts/generate-moon-assets.mjs. DO NOT EDIT. */',
|
||||
`// Designer pack assets/moon/${pack.pack_id}-${pack.pack_version} (#661). Lazy: imported only by`,
|
||||
'// moon-runtime.ts, never by the initial View graph (C7).',
|
||||
'// A static Lit template: no directive, no binding, nothing parsed at runtime but the art.',
|
||||
"import { svg } from 'lit';",
|
||||
`export const MOON_PACK = ${JSON.stringify(`${pack.pack_id}-${pack.pack_version}`)};`,
|
||||
`export const MOON_ART_SHA256 = ${JSON.stringify(sha)};`,
|
||||
`export const MOON_ART = svg\`${art}\`;`,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || '')) {
|
||||
const next = generateMoonModule();
|
||||
if (process.argv.includes('--check')) {
|
||||
const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : '';
|
||||
if (current !== next) {
|
||||
console.error('src/moon-art.generated.ts is stale: run node scripts/generate-moon-assets.mjs');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('moon art is up to date');
|
||||
} else {
|
||||
fs.writeFileSync(OUT, next);
|
||||
console.log(`wrote ${path.relative(ROOT, OUT)}`);
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,5 @@
|
||||
"hostRefs": 4880,
|
||||
"portPrivates": 94,
|
||||
"harnessPrivates": 100,
|
||||
"bundleBytes": 2583806
|
||||
"bundleBytes": 2616267
|
||||
}
|
||||
|
||||
@@ -5432,8 +5432,8 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'правка пакета не выбирает frontend и не запускает сверку generated-модулей (#671)',
|
||||
patches: [{
|
||||
file: 'scripts/check-inputs.mjs',
|
||||
find: " roots: [...BUILD_INPUTS, 'assets/furniture/**', 'test/**',",
|
||||
replace: " roots: [...BUILD_INPUTS, 'test/**', // mutant: furniture package is not a frontend input",
|
||||
find: " roots: [...BUILD_INPUTS, 'assets/furniture/**', 'assets/moon/**', 'test/**',",
|
||||
replace: " roots: [...BUILD_INPUTS, 'assets/moon/**', 'test/**', // mutant: furniture package is not a frontend input",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -6214,6 +6214,110 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' kind: \'device\', core, presentation,',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-threshold-off',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 AC2: the moon keeps the window rays\' 3° horizon; at 2.9° it is not shown',
|
||||
patches: [{
|
||||
file: 'src/moon.ts',
|
||||
find: " return phase !== 'day' && altitude >= MOON_ELEVATION_MIN && fraction >= MOON_MIN_ILLUMINATION;",
|
||||
replace: " return phase !== 'day' && altitude >= 0 && fraction >= MOON_MIN_ILLUMINATION;",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-new-moon-shown',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 AC2: below 3 % illumination (around new moon) there is no moon',
|
||||
patches: [{
|
||||
file: 'src/moon.ts',
|
||||
find: " return phase !== 'day' && altitude >= MOON_ELEVATION_MIN && fraction >= MOON_MIN_ILLUMINATION;",
|
||||
replace: " return phase !== 'day' && altitude >= MOON_ELEVATION_MIN;",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-day-visible',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 AC2: the day phase shows no moon, however high it stands',
|
||||
patches: [{
|
||||
file: 'src/moon.ts',
|
||||
find: " return phase !== 'day' && altitude >= MOON_ELEVATION_MIN && fraction >= MOON_MIN_ILLUMINATION;",
|
||||
replace: " return altitude >= MOON_ELEVATION_MIN && fraction >= MOON_MIN_ILLUMINATION;",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-parallax-off',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 AC1: without the topocentric parallax the altitude by the horizon is off by '
|
||||
+ 'up to 2° — the 2026-10-31 Moscow point leaves the 1.5° tolerance',
|
||||
patches: [{
|
||||
file: 'src/moon.ts',
|
||||
find: ' const altitude = geocentric - parallax * Math.cos(geocentric);',
|
||||
replace: ' const altitude = geocentric;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-lit-side-flips',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 AC3 (owner 2026-09-29): the lit side is always the left one, in both hemispheres',
|
||||
patches: [{
|
||||
file: 'src/moon.ts',
|
||||
find: ' return `M${c} 0A${c} ${c} 0 0 0 ${c} ${MOON_BOX}L${c} ${bottom}${terminator}L${c} 0Z`;',
|
||||
replace: ' return `M${c} 0A${c} ${c} 0 0 1 ${c} ${MOON_BOX}L${c} ${bottom}${terminator}L${c} 0Z`;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-tick-renders-every-time',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 AC4: a 30 s tick over an unchanged moon must not cost a render',
|
||||
patches: [{
|
||||
file: 'src/moon-runtime.ts',
|
||||
find: ' if (key === watch.key) return;\n',
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-lazy-gate-off',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 C7: the moon chunk is not loaded by day — the initial View graph pays only the gate',
|
||||
patches: [{
|
||||
file: 'src/moon-gate.ts',
|
||||
find: " if (!runtime && state.phase !== 'day' && Date.now() >= nextAttempt) {",
|
||||
replace: ' if (!runtime && Date.now() >= nextAttempt) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-over-paper',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#661" test/moon.test.mjs',
|
||||
because: '#661 C6 / owner decision 7: the moon covers only the background — it is the last child '
|
||||
+ 'of the environment and never stacks by itself; taken out and raised, it would paint over the plan',
|
||||
patches: [{
|
||||
file: 'src/day-cycle-render.ts',
|
||||
find: ' })}${moon}\n </div>`;',
|
||||
replace: ' })}\n </div>${moon}`;',
|
||||
}, {
|
||||
file: 'src/moon-runtime.ts',
|
||||
find: " + '.hp-moon{--hp-moon-box:min(200px,25cqmin);position:absolute;'",
|
||||
replace: " + '.hp-moon{--hp-moon-box:min(200px,25cqmin);position:absolute;z-index:2;'",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-validation-accepts-anything',
|
||||
guard: 'python3 -m pytest tests_backend/test_settings_moon.py -q -p no:cacheprovider',
|
||||
because: '#661 AC9: the backend accepts only a boolean moon',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/validation.py',
|
||||
find: ' vol.Optional("moon"): bool,\n',
|
||||
replace: ' vol.Optional("moon"): object,\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'wall-isolated-extra-discarded',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -27,6 +27,17 @@
|
||||
|
||||
/** @type {SmokeLink[]} */
|
||||
export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: [
|
||||
'moonLayer', 'renderMoon', 'moonTick', 'moonView', 'moonShownAt', 'moonPhasePath',
|
||||
'moonPosition', 'moonIllumination', 'moonFingerprint', 'renderDayCycleEnvironment',
|
||||
],
|
||||
smokes: ['smoke_moon.mjs', 'smoke_daycycle_layer_budget.mjs'],
|
||||
because: '#661: the moon is observed only as `.hp-moon` in the production bundle — the lazy '
|
||||
+ 'chunk arriving at night, the 30 s ticker and state updates carrying it through 3° with the '
|
||||
+ '2 s fade, the plan painting over it pixel for pixel, and no extra composited layer (CDP); '
|
||||
+ 'no smoke names the functions',
|
||||
},
|
||||
{
|
||||
symbols: ['memoIsoLightFloorRooms', 'IsoFirstFrameState', 'isoPaperContext'],
|
||||
smokes: ['smoke_iso_first_frame.mjs'],
|
||||
|
||||
Reference in New Issue
Block a user