From fff46492caf4e1ed4dcdc53132222342009439b0 Mon Sep 17 00:00:00 2001 From: Nibu Date: Mon, 2 Nov 2026 17:09:42 +0000 Subject: [PATCH 1/2] scene: Phacelle bound says measured-not-proved; neutral creature draws nothing world.ts already carries Libby's 7.0 (CultLib PR #1 990521b3); the comment now states the 6.63 sup was measured, not proved. creatures.ts follows creature-projection-v2: a neutral creature publishes no sizeBucket, and one that arrives is ignored. creatureScale returns null (draw nothing) for neutral, absent or unknown buckets; it never defaults to medium. NEUTRAL_SCALE is removed. Co-Authored-By: Claude Sonnet 5.5 --- src/app/features/scene/creatures.test.ts | 18 +++++++++++------- src/app/features/scene/creatures.ts | 20 ++++++++++---------- src/app/features/scene/world.ts | 3 ++- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/app/features/scene/creatures.test.ts b/src/app/features/scene/creatures.test.ts index 8e59175b..011f19e3 100644 --- a/src/app/features/scene/creatures.test.ts +++ b/src/app/features/scene/creatures.test.ts @@ -10,14 +10,18 @@ describe('creature scale', () => { ); }); - it('is one fixed size for every neutral creature, whatever its sizeBucket', () => { - buckets.forEach((sizeBucket) => - expect(creatureScale({ sizeBucket, neutral: true })).toBe(NEUTRAL_SCALE) - ); + it('draws nothing for a neutral creature, whatever sizeBucket it was sent', () => { + expect(creatureScale({ neutral: true })).toBeNull(); + buckets.forEach((sizeBucket) => expect(creatureScale({ sizeBucket, neutral: true })).toBeNull()); }); - it('is only visible as an override where the bucket would have said something else', () => { - expect(BUCKET_SCALE.small).not.toBe(NEUTRAL_SCALE); - expect(BUCKET_SCALE.large).not.toBe(NEUTRAL_SCALE); + it('does not crash or default to medium when a non-neutral creature has no sizeBucket', () => { + expect(creatureScale({ neutral: false })).toBeNull(); + expect(creatureScale({ sizeBucket: undefined, neutral: false })).not.toBe(BUCKET_SCALE.medium); + }); + + it('draws nothing for an unknown bucket, including inherited property names', () => { + expect(creatureScale({ sizeBucket: 'huge' as SizeBucket, neutral: false })).toBeNull(); + expect(creatureScale({ sizeBucket: 'toString' as SizeBucket, neutral: false })).toBeNull(); }); }); diff --git a/src/app/features/scene/creatures.ts b/src/app/features/scene/creatures.ts index 5026d2e2..b67a423c 100644 --- a/src/app/features/scene/creatures.ts +++ b/src/app/features/scene/creatures.ts @@ -3,10 +3,12 @@ // Props contract: heimdall-body:follow_up:ratatoskr-creature-projection-v2 (against GameCult/Bifrost; supersedes heimdall-body/ratatoskr-creature-projection). // Nothing here is binding until Bifrost's steward cuts that contract. // -// Shape: {id, estate, sizeBucket, neutral, activity}. +// Shape: {id, estate, neutral, activity} plus sizeBucket when neutral is false. // Invariants: // - Exact vote weights stay in Bifrost; Ratatoskr only ever sees a bucket. -// - neutral=true overrides any bucket, and no size is drawn. +// - neutral=true means Bifrost publishes no sizeBucket, no kind and no energy. A received value is +// ignored, and a neutral creature is not drawn. +// - A missing or unknown sizeBucket is not drawn either; no size is ever defaulted. // - activity comes from the Heimdall session. // Consent: humans get a coarse bucket by default plus a typed opt-out; Minds and Personas may show finer buckets. // Humans have three coarse buckets. The scales below (0.7 / 1 / 1.4) are placeholders, not fixed. @@ -16,8 +18,8 @@ export type SizeBucket = 'small' | 'medium' | 'large'; export interface Creature { id: string; estate: string; - sizeBucket: SizeBucket; - /** A neutral creature (no estate allegiance) is drawn at one fixed size whatever its bucket. */ + /** Absent when neutral. Read it only through creatureScale. */ + sizeBucket?: SizeBucket; neutral: boolean; activity: 'idle' | 'active'; } @@ -28,10 +30,8 @@ export const BUCKET_SCALE: Readonly> = { large: 1.4, }; -/** Neutral creatures are medium sized, not necessarily their bucket's size. */ -export const NEUTRAL_SCALE = BUCKET_SCALE.medium; - -/** Neutral overrides sizeBucket. */ -export function creatureScale(c: Pick): number { - return c.neutral ? NEUTRAL_SCALE : BUCKET_SCALE[c.sizeBucket]; +/** The draw scale, or null when nothing is drawn: a neutral creature, or a bucket that is absent or unknown. */ +export function creatureScale(c: Pick): number | null { + if (c.neutral || c.sizeBucket === undefined) return null; + return Object.hasOwn(BUCKET_SCALE, c.sizeBucket) ? BUCKET_SCALE[c.sizeBucket] : null; } diff --git a/src/app/features/scene/world.ts b/src/app/features/scene/world.ts index ba9aec53..ba366a6e 100644 --- a/src/app/features/scene/world.ts +++ b/src/app/features/scene/world.ts @@ -24,7 +24,8 @@ export const WORLD = { erodeSide: [1.7, 0, 1] as const, erodeNorm: 0.5, // Bound on |grad| of the phasor's cos component for the side and norm above. Libby measured a - // sup of 6.63 at |side| = 1.5, n = 0 (CultLib PR #1 head 990521b3); 7.0 adds margin. The + // sup of 6.63 at |side| = 1.5, n = 0 (CultLib PR #1 head 990521b3); 7.0 adds margin. + // The sup was measured, not proved. The // shader passes roughness n = 0 as a literal, so the doc envelope max(10, 1.25|side|)/(1-n) // needs no 1/(1-n) factor here; if n ever becomes nonzero, divide this bound by (1 - n). // |value| <= 1 holds by construction (the output is a normalized phasor). -- 2.47.3 From 5eb0a63caa254824dbddc6c0e3319c35bf3a65de Mon Sep 17 00:00:00 2001 From: Nibu Date: Tue, 3 Nov 2026 08:59:54 +0000 Subject: [PATCH 2/2] scene: ES2016-safe hasOwnProperty, tighten undefined-bucket assertion, reflow bound comment Co-Authored-By: Claude Sonnet 5.5 --- src/app/features/scene/creatures.test.ts | 4 ++-- src/app/features/scene/creatures.ts | 2 +- src/app/features/scene/world.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/features/scene/creatures.test.ts b/src/app/features/scene/creatures.test.ts index 011f19e3..938c217f 100644 --- a/src/app/features/scene/creatures.test.ts +++ b/src/app/features/scene/creatures.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { BUCKET_SCALE, creatureScale, NEUTRAL_SCALE, SizeBucket } from './creatures'; +import { BUCKET_SCALE, creatureScale, SizeBucket } from './creatures'; const buckets: SizeBucket[] = ['small', 'medium', 'large']; @@ -17,7 +17,7 @@ describe('creature scale', () => { it('does not crash or default to medium when a non-neutral creature has no sizeBucket', () => { expect(creatureScale({ neutral: false })).toBeNull(); - expect(creatureScale({ sizeBucket: undefined, neutral: false })).not.toBe(BUCKET_SCALE.medium); + expect(creatureScale({ sizeBucket: undefined, neutral: false })).toBeNull(); }); it('draws nothing for an unknown bucket, including inherited property names', () => { diff --git a/src/app/features/scene/creatures.ts b/src/app/features/scene/creatures.ts index b67a423c..a0b2bd29 100644 --- a/src/app/features/scene/creatures.ts +++ b/src/app/features/scene/creatures.ts @@ -33,5 +33,5 @@ export const BUCKET_SCALE: Readonly> = { /** The draw scale, or null when nothing is drawn: a neutral creature, or a bucket that is absent or unknown. */ export function creatureScale(c: Pick): number | null { if (c.neutral || c.sizeBucket === undefined) return null; - return Object.hasOwn(BUCKET_SCALE, c.sizeBucket) ? BUCKET_SCALE[c.sizeBucket] : null; + return Object.prototype.hasOwnProperty.call(BUCKET_SCALE, c.sizeBucket) ? BUCKET_SCALE[c.sizeBucket] : null; } diff --git a/src/app/features/scene/world.ts b/src/app/features/scene/world.ts index ba366a6e..e9594a68 100644 --- a/src/app/features/scene/world.ts +++ b/src/app/features/scene/world.ts @@ -25,9 +25,9 @@ export const WORLD = { erodeNorm: 0.5, // Bound on |grad| of the phasor's cos component for the side and norm above. Libby measured a // sup of 6.63 at |side| = 1.5, n = 0 (CultLib PR #1 head 990521b3); 7.0 adds margin. - // The sup was measured, not proved. The - // shader passes roughness n = 0 as a literal, so the doc envelope max(10, 1.25|side|)/(1-n) - // needs no 1/(1-n) factor here; if n ever becomes nonzero, divide this bound by (1 - n). + // The sup was measured, not proved. The shader passes roughness n = 0 as a literal, so the doc + // envelope max(10, 1.25|side|)/(1-n) needs no 1/(1-n) factor here; if n ever becomes nonzero, + // divide this bound by (1 - n). // |value| <= 1 holds by construction (the output is a normalized phasor). // Typed number, not the literal 7, so callers can build variants such as { ...WORLD, phacelleGradBound: 0 }. phacelleGradBound: 7.0 as number, -- 2.47.3