soundscape: neutral carries no size; cite grammar head 0091d01
NeutralFrame drops size alongside kind and energy; the reader ignores a size a producer sends on a neutral frame, and a live frame without a valid size is malformed. The human kind subset (Idle/Working/Speaking) stays PROVISIONAL, still proposed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
190e2e724b
commit
5e79a1853a
2 changed files with 24 additions and 12 deletions
|
|
@ -1,7 +1,8 @@
|
|||
// The activity frame of Aqua's soundscape grammar (AquaSynth docs/soundscape-grammar at 5e2eaa9,
|
||||
// section 2). The Heimdall session produces it; this file only reads it. Shape follows
|
||||
// The activity frame of Aqua's soundscape grammar (AquaSynth docs/soundscape-grammar, grammar head
|
||||
// 0091d01, section 2). The Heimdall session produces it; this file only reads it. Shape follows
|
||||
// heimdall-body:follow_up:ratatoskr-creature-projection-v2 as the grammar relays it. The human
|
||||
// subset below is PROVISIONAL until Bifrost's steward cuts the list.
|
||||
// kind subset (Idle/Working/Speaking) below is PROVISIONAL: still proposed, not ruled, until
|
||||
// Bifrost's steward cuts the list.
|
||||
|
||||
export type Estate = 'human' | 'mind' | 'persona';
|
||||
export type Kind = 'Idle' | 'Reading' | 'Thinking' | 'Working' | 'Speaking' | 'Waiting' | 'Alert';
|
||||
|
|
@ -12,20 +13,21 @@ export type Override = 'None' | 'Muted';
|
|||
interface FrameBase {
|
||||
mind: string;
|
||||
estate: Estate;
|
||||
size: Size;
|
||||
override: Override;
|
||||
seq: number;
|
||||
/** Producer observation time, whole seconds (grammar: 1 s resolution). */
|
||||
at: number;
|
||||
}
|
||||
/** O1: a neutral frame carries no kind and no energy; it is not an Idle and nothing reads one. */
|
||||
/** O1: a neutral frame carries no size, kind or energy; it is not an Idle and nothing reads one. */
|
||||
export interface NeutralFrame extends FrameBase {
|
||||
neutral: true;
|
||||
size?: undefined;
|
||||
kind?: undefined;
|
||||
energy?: undefined;
|
||||
}
|
||||
export interface LiveFrame extends FrameBase {
|
||||
neutral: false;
|
||||
size: Size;
|
||||
kind: Kind;
|
||||
energy: Energy;
|
||||
}
|
||||
|
|
@ -64,7 +66,7 @@ const clampEnergy = (estate: Estate, energy: Energy): Energy =>
|
|||
/**
|
||||
* Read an untrusted frame. Only the grammar's fields survive: any other key (a weight, a vote, a
|
||||
* room id) is dropped, so a consumer cannot be steered by data the grammar does not define (A3).
|
||||
* `neutral` is read first: a neutral frame yields no kind and no energy even if the producer sent
|
||||
* `neutral` is read first: a neutral frame yields no size, kind or energy even if the producer sent
|
||||
* them (O1). Returns undefined for anything malformed; the caller treats that as no frame (A4).
|
||||
*/
|
||||
export const readFrame = (raw: unknown): ActivityFrame | undefined => {
|
||||
|
|
@ -72,18 +74,18 @@ export const readFrame = (raw: unknown): ActivityFrame | undefined => {
|
|||
const r = raw as Record<string, unknown>;
|
||||
if (typeof r.neutral !== 'boolean') return undefined;
|
||||
const estate = oneOf(ESTATES, r.estate);
|
||||
const size = oneOf(SIZES, r.size);
|
||||
const override =
|
||||
r.override === undefined ? 'None' : oneOf(['None', 'Muted'] as const, r.override);
|
||||
if (!estate || !size || !override) return undefined;
|
||||
if (!estate || !override) return undefined;
|
||||
if (typeof r.mind !== 'string' || r.mind === '') return undefined;
|
||||
if (!Number.isInteger(r.seq) || !Number.isFinite(r.at)) return undefined;
|
||||
const base = { mind: r.mind, estate, size, override, seq: r.seq as number, at: r.at as number };
|
||||
const base = { mind: r.mind, estate, override, seq: r.seq as number, at: r.at as number };
|
||||
if (r.neutral) return { ...base, neutral: true };
|
||||
const kind = clampKind(estate, r.kind);
|
||||
const energy = oneOf(ENERGIES, r.energy);
|
||||
if (!kind || !energy) return undefined;
|
||||
return { ...base, neutral: false, kind, energy: clampEnergy(estate, energy) };
|
||||
const size = oneOf(SIZES, r.size);
|
||||
if (!kind || !energy || !size) return undefined;
|
||||
return { ...base, neutral: false, size, kind, energy: clampEnergy(estate, energy) };
|
||||
};
|
||||
|
||||
/** Latest frame per mind (S1: consumers drop seq <= last). */
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ describe('neutral overrides everything', () => {
|
|||
});
|
||||
|
||||
describe('neutral is read first', () => {
|
||||
it('yields no kind and no energy, even when the producer sent them', () => {
|
||||
it('yields no size, kind or energy, even when the producer sent them', () => {
|
||||
(['human', 'mind', 'persona'] as const).forEach((estate) => {
|
||||
const f = readFrame({
|
||||
...frame({ estate, kind: 'Speaking', energy: 'High' }),
|
||||
|
|
@ -57,6 +57,8 @@ describe('neutral is read first', () => {
|
|||
});
|
||||
expect(f).toBeDefined();
|
||||
expect(f?.neutral).toBe(true);
|
||||
expect(f?.size).toBeUndefined();
|
||||
expect(Object.keys(f ?? {})).not.toContain('size');
|
||||
expect(f?.kind).toBeUndefined();
|
||||
expect(f?.energy).toBeUndefined();
|
||||
expect(Object.keys(f ?? {})).not.toContain('kind');
|
||||
|
|
@ -221,3 +223,11 @@ describe('socket defaults and limits', () => {
|
|||
expect(t.offer({ ...frame(), kind: 'Dancing' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('neutral carries no size', () => {
|
||||
it('reads without a size, and a live frame without one is malformed', () => {
|
||||
const raw = frame();
|
||||
expect(readFrame({ ...without(raw, 'size', 'kind', 'energy'), neutral: true })).toBeDefined();
|
||||
expect(readFrame(without(raw, 'size'))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue