diff --git a/.tickets/tex-nak8.md b/.tickets/tex-nak8.md index 1aedb36..17656aa 100644 --- a/.tickets/tex-nak8.md +++ b/.tickets/tex-nak8.md @@ -35,3 +35,10 @@ Episode 000003 adversarial projection found one further current-v2 gap: use_focu **2026-08-01T23:52:53Z** Episode 000003 now projects its exact 10,133,930-byte two-source prefix set as 212 source events: 210 readable resident turns plus two structural roots, with zero unsupported events, zero nonconforming lines, and zero unsupported_* diagnostics. Focused test 26/26; full 212 pass/1 scale skip; lint and production build pass. Lync upstream ownership is tracked as lyn-cnew. + +**2026-08-02T08:21:50Z** + +Removed Textile's now-redundant Behold v2 presenter overlay. Textile now consumes +the checksum-pinned Lync package from canonical commit `6e0734d` directly. Exact +episode 000014 projects all 692 source events with no unsupported or unclaimed +event; Textile retains only its reader, grouping, and storage responsibilities. diff --git a/.tickets/tex-q3ad.md b/.tickets/tex-q3ad.md new file mode 100644 index 0000000..36397f4 --- /dev/null +++ b/.tickets/tex-q3ad.md @@ -0,0 +1,30 @@ +--- +id: tex-q3ad +status: closed +deps: [] +links: [] +created: 2026-08-02T10:15:37Z +type: task +priority: 0 +assignee: deepfates +external-ref: beh-wo5b +tags: [behold, lync, reader] +--- +# Read Behold null cognition as a private-life event + +Consume Lync's additive behold.entity-cognition-turn.v1 presenter so Textile shows admitted perception plus the resident's explicit no-bodily-action choice without fabricating an action or outcome. + +## Acceptance Criteria + +The checksum-pinned Lync candidate is updated, raw and indexed resident reading use the shared presenter, a focused Textile regression retains source identity and absent consequence fields, and full verify passes. + + +## Notes + +**2026-08-02T10:15:42Z** + +Vendored exact Lync 860aa549... archive dad3cff0..., updated provenance/docs, and focused raw reader regression passes 28/28. Pending full Textile verify. + +**2026-08-02T10:16:18Z** + +Full Textile verify passes: lint, 228 tests plus one opt-in scale skip, and both production builds. The installed physical package is the exact checksum-pinned Lync candidate. diff --git a/.tickets/tex-wrif.md b/.tickets/tex-wrif.md index 53cbaae..649277c 100644 --- a/.tickets/tex-wrif.md +++ b/.tickets/tex-wrif.md @@ -53,3 +53,9 @@ Principal independent review on 2026-08-01: code and ownership design are accept **2026-08-02T03:03:00Z** 2026-08-02 Behold episode 000003 ordinary browser check against current main: exact manifest is 2 sources / 20,556,307 bytes / digest 05307f02... The Stories drawer pointer Import Archive input accepts only one file; choosing a ZIP of the exact manifest + two Lync sources routes to the Twitter archive parser and fails for missing data/manifest.js. The intended hidden multiple input could not be opened through the keyboard/gamepad Import Archive action in the in-app browser, even though current source has the action. Therefore this exact retained source set is not yet claimed readable through the ordinary UI. Single-file historical Textile reading remains real. Candidate diagnosis is front-door multi-file activation/archive dispatch, distinct from the indexed-union scale work; reproduce before changing it. + +2026-08-02 exact browser correction on current main: the ordinary visible multi-file picker does admit the Behold manifest plus both canonical resident files, but the first episode-000012 attempt failed because WebKit supplied a 1,114,112-byte native Blob chunk to Lync's explicitly 1,048,576-byte indexed admission boundary. The browser File adapter now splits arbitrary native chunks into at-most-1-MiB pieces without changing, copying, or weakening the authenticated prefix. A focused regression supplies the observed oversized shape. The repaired visible local-only reader then opened exact episode 000014: 56,638,047 authenticated bytes, 692 source events, 690 readable turns, two structural roots, all presented, zero nonconforming events. Navigating the real Lark life exposed the 1h23m wall-clock gap after turn 228 rather than presenting stop/resume as one scene. Full verify is 227 pass / one opt-in scale skip plus lint and production builds. This crosses the browser front-door defect and current 56 MB checkpoint; the ticket remains open for its stated representative retained six-hour-scale browser exercise rather than promoting the existing synthetic projector gate. + +**2026-08-02T11:17:02Z** + +2026-08-02 pinned Lync candidate 0a511b6 adds truthful place_held_against_focus presentation. The exact episode-000019 ordered-prefix front door imported 68,444,160 authenticated bytes: 871 source events, 869 readable, two structural, zero unsupported diagnostics, 12,278 explicit source-only/withheld diagnostics. Full Textile verify passes (228 pass, one opt-in scale skip, lint, production builds). This repairs presentation completeness but does not close the ticket's retained six-hour browser exercise. diff --git a/README.md b/README.md index 8c923c9..5c1d3e4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > **Status:** private application. Version 0.1.0 is the first coherent source > release candidate for the raw-Lync corpus workflow and UI; it is not an npm > package. Textile supports `@deepfates/lync >=0.3.0 <0.5.0`; this checkout pins -> the exact packed Lync 0.4.3 indexed-union candidate documented in +> the exact packed Lync 0.4.3 indexed-union and presentation candidate documented in > `vendor/LYNC-PROVENANCE.md` pending an owner-authorized registry release. Textile is a tactile multiverse reader for branching stories, conversations, @@ -120,6 +120,10 @@ O(events + edges + public presentation text), not O(source bytes). The ordinary multi-file picker reports resident/source/readable/structural/diagnostic counts and rejects changed, truncated, conflicting, dangling, or cyclic input. Existing single-file raw imports keep their eager, lossless archive/export behavior. +Source event times remain attached to their readable turns; when adjacent turns +are at least five minutes apart, the story surface marks the elapsed interval so +a stopped or otherwise discontinuous life is not presented as one continuous +scene. The marker describes time only and does not invent an episode boundary. Readable events above one MiB of text use an explicit bounded reader with FIRST/PREV/NEXT/LAST 65,536-character windows. This is presentation @@ -127,10 +131,11 @@ virtualization, not truncation: the exact complete source string remains in the story model, full-tree JSON, and kept-context export paths. Textile reads raw corpus events through Lync's non-mutating, kind/profile-aware -presentation contract. The same contract is consumed by Splice and Curare; its -checksum-pinned local candidate keeps this private app installable before Lync -0.4 publication. For Behold `org.behold.inhabitant.v1`, it presents the safe -resident perception/action/outcome allowlist, preserves the original source +presentation contract. The same contract is consumed by Splice and Curare; the +checksum-pinned Lync package keeps this private app installable before Lync 0.4 +publication. For Behold `org.behold.inhabitant.v1` and `.v2`, it presents the safe +resident perception/action/outcome allowlist plus explicit non-action cognition, +preserves the original source events for full-tree export, and fails closed for unknown nested shapes. See [the Behold resident Lync presentation boundary](docs/behold-resident-lync-presentation.md). diff --git a/bun.lock b/bun.lock index 74b2761..105aff2 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "name": "textile", "dependencies": { "@ax-llm/ax": "^14.0.39", - "@deepfates/lync": "file:vendor/deepfates-lync-0.4.3-indexed-union.tgz", + "@deepfates/lync": "file:vendor/deepfates-lync-0.4.3-0a511b6.tgz", "@tailwindcss/postcss": "^4.1.17", "@types/d3-flextree": "^2.1.4", "@types/express": "^4.17.21", @@ -250,7 +250,7 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@deepfates/lync": ["@deepfates/lync@vendor/deepfates-lync-0.4.3-indexed-union.tgz", { "bin": { "lync": "bin/lync.js" } }], + "@deepfates/lync": ["@deepfates/lync@vendor/deepfates-lync-0.4.3-0a511b6.tgz", { "bin": { "lync": "bin/lync.js" } }], "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], diff --git a/client/interface/components/StoryText.tsx b/client/interface/components/StoryText.tsx index 0e50620..a2e6e50 100644 --- a/client/interface/components/StoryText.tsx +++ b/client/interface/components/StoryText.tsx @@ -23,6 +23,17 @@ interface StoryTextProps { } const STORY_PATH_WINDOW_SIZE = 48; +const STORY_TIME_SEAM_MS = 5 * 60 * 1000; + +function elapsedTimeLabel(elapsedMs: number): string { + const totalMinutes = Math.floor(elapsedMs / 60_000); + const days = Math.floor(totalMinutes / (24 * 60)); + const hours = Math.floor((totalMinutes % (24 * 60)) / 60); + const minutes = totalMinutes % 60; + if (days > 0) return `${days}d${hours > 0 ? ` ${hours}h` : ""} later`; + if (hours > 0) return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""} later`; + return `${minutes}m later`; +} interface StoryNodeProseProps { text: string; @@ -215,6 +226,14 @@ export function StoryText({ {visiblePath.map((segment, visibleIndex) => { const index = windowStart + visibleIndex; const previous = currentPath[index - 1]; + const elapsedMs = + previous?.createdAt !== undefined && segment.createdAt !== undefined + ? segment.createdAt - previous.createdAt + : null; + const timeSeam = + showTurnBoundaries && elapsedMs !== null && elapsedMs >= STORY_TIME_SEAM_MS + ? elapsedTimeLabel(elapsedMs) + : null; const seam = !showTurnBoundaries && previous ? storySeam(previous.text, segment.text) @@ -253,6 +272,14 @@ export function StoryText({ data-node-id={segment.id} data-source-text-length={segment.text.length} > + {timeSeam ? ( + + {timeSeam} + + ) : null} {seam} { expect(render([human, model], 1)).not.toContain("story-turn-boundary"); }); + it("makes a material source-time discontinuity visible without inventing an episode", () => { + const before: StoryNode = { + ...unknown, + id: "resident-228", + text: "Stop worrying.", + sourceId: "source-228", + createdAt: Date.parse("2026-08-02T06:05:27.978Z"), + }; + const after: StoryNode = { + ...unknown, + id: "resident-229", + text: "Read the first private-life turn.", + sourceId: "source-229", + createdAt: Date.parse("2026-08-02T07:29:17.641Z"), + }; + const html = render([before, after], 1); + expect(html).toContain("1h 23m later"); + expect(html).toContain( + 'title="2026-08-02T06:05:27.978Z → 2026-08-02T07:29:17.641Z"', + ); + + const continuous = render( + [before, { ...after, createdAt: before.createdAt! + 4 * 60_000 }], + 1, + ); + expect(continuous).not.toContain("story-time-seam"); + }); + it("presents a multi-megabyte turn as an explicit bounded reader window", () => { const large: StoryNode = { ...unknown, diff --git a/client/interface/lync/__tests__/orderedLyncSourceSet.test.ts b/client/interface/lync/__tests__/orderedLyncSourceSet.test.ts index e4fcb4e..a2a453e 100644 --- a/client/interface/lync/__tests__/orderedLyncSourceSet.test.ts +++ b/client/interface/lync/__tests__/orderedLyncSourceSet.test.ts @@ -141,6 +141,37 @@ describe("Behold ordered Lync source sets", () => { .rejects.toThrow(/read-only/); }); + it("normalizes browser-native Blob chunks to Lync's indexed admission bound", async () => { + const bytes = new Uint8Array(1024 * 1024 + 65_536).fill(7); + const file = { + name: "wide-browser-chunk.lync", + size: bytes.byteLength, + slice: () => ({ + stream: () => + new ReadableStream({ + start(controller) { + controller.enqueue(bytes); + controller.close(); + }, + }), + arrayBuffer: async () => bytes.slice().buffer, + }), + } as unknown as File; + const binding = source( + 0, + "OxfordWide", + "/canonical/OxfordWide/wide-browser-chunk.lync", + bytes, + "org.behold.inhabitant.v2", + ); + const chunks: Uint8Array[] = []; + for await (const chunk of orderedLyncFileSource({ binding, file }).stream()) { + chunks.push(chunk); + } + expect(chunks.map((chunk) => chunk.byteLength)).toEqual([1024 * 1024, 65_536]); + expect(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))).toEqual(Buffer.from(bytes)); + }); + it("fails closed on union conflicts and source changes between index and projection", async () => { const lines = aster.toString("utf8").trimEnd().split("\n"); const conflicting = JSON.parse(lines[1]!); diff --git a/client/interface/lync/__tests__/rawLync.test.ts b/client/interface/lync/__tests__/rawLync.test.ts index 5d989a6..2d0bc61 100644 --- a/client/interface/lync/__tests__/rawLync.test.ts +++ b/client/interface/lync/__tests__/rawLync.test.ts @@ -590,6 +590,45 @@ describe("raw .lync projection", () => { .toEqual(raw.trim().split("\n").map((line) => JSON.parse(line))); }); + it("reads resident null cognition without fabricating an action outcome", () => { + const lines = readFileSync(oxfordResidentV2FixtureUrl, "utf8") + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + const event = lines[1]; + const cognition = event.payload.payload; + cognition.protocol = "behold.entity-cognition-turn.v1"; + cognition.id = "OxfordCedar:cognition:7"; + cognition.utterance = { + assistant: { role: "assistant", content: '{"action":null,"arguments":{}}' }, + }; + delete cognition.action; + delete cognition.outcome; + delete cognition.nextObservation; + + const projection = projectRawLyncFile( + `${lines.map(JSON.stringify).join("\n")}\n`, + "resident-cognition.lync", + ); + expect(projection.readableEventCount).toBe(1); + expect(projection.unsupportedEventCount).toBe(0); + const beat = projection.snapshot.turns.find( + (turn) => turn.meta.sourceKind === "lync/turn", + ); + expect(beat?.payload.text).toContain("OxfordCedar · cognition 7"); + expect(beat?.payload.text).toContain("OxfordCedar chose no bodily action."); + expect(beat?.payload.text).not.toContain("unknown outcome"); + expect(beat?.meta.sourcePresentationSections?.map((section) => section.role)).toEqual([ + "perception", + "action", + ]); + const storedTurn = (beat?.meta.sourceEvent?.payload as { + payload?: Record; + })?.payload; + expect(storedTurn?.action).toBeUndefined(); + expect(storedTurn?.outcome).toBeUndefined(); + }); + it("distinguishes resident chat input dispatch from independently observed delivery", () => { for (const actionName of ["chat", "whisper"] as const) { const events = readV2FixtureEvents(); diff --git a/client/interface/lync/indexedRawLync.ts b/client/interface/lync/indexedRawLync.ts index f15350d..edbf58c 100644 --- a/client/interface/lync/indexedRawLync.ts +++ b/client/interface/lync/indexedRawLync.ts @@ -26,6 +26,11 @@ export interface IndexedRawLyncProjection extends RawLyncProjection { ownership: OrderedLyncReviewOwnership; } +// Match Lync's default indexed-union admission bound. Browser Blob streams do +// not promise a particular chunk size and WebKit can hand us a chunk slightly +// larger than one MiB, so the File adapter—not the corpus—owns normalizing it. +const INDEXED_LYNC_STREAM_CHUNK_BYTES = 1024 * 1024; + /** Browser File adapter that exposes only the manifest-authenticated prefix. */ export function orderedLyncFileSource(source: OrderedLyncSourceFile): ReReadableLyncSource { const { binding, file } = source; @@ -46,8 +51,14 @@ export function orderedLyncFileSource(source: OrderedLyncSourceFile): ReReadable break; } const chunk = result.value.subarray(0, remaining); - supplied += chunk.byteLength; - yield chunk; + for (let start = 0; start < chunk.byteLength; start += INDEXED_LYNC_STREAM_CHUNK_BYTES) { + const bounded = chunk.subarray( + start, + Math.min(chunk.byteLength, start + INDEXED_LYNC_STREAM_CHUNK_BYTES), + ); + supplied += bounded.byteLength; + yield bounded; + } if (supplied === binding.endOffset) { await reader.cancel(); break; diff --git a/client/interface/lync/rawLyncPresentation.ts b/client/interface/lync/rawLyncPresentation.ts index 4ebc69a..a491a82 100644 --- a/client/interface/lync/rawLyncPresentation.ts +++ b/client/interface/lync/rawLyncPresentation.ts @@ -1,13 +1,14 @@ import type { LyncEventBody } from "@deepfates/lync/events"; import { - BEHOLD_INHABITANT_PROFILE, presentLyncEvent, - resolveLyncPresentationProfiles, type LyncPresentation, type LyncPresentationContext, -} from "../../../vendor/lync-presentation/index.js"; +} from "@deepfates/lync/presentation"; -export { BEHOLD_INHABITANT_PROFILE, resolveLyncPresentationProfiles }; +export { + BEHOLD_INHABITANT_PROFILE, + resolveLyncPresentationProfiles, +} from "@deepfates/lync/presentation"; export type { LyncPresentation as RawLyncPresentation, @@ -16,7 +17,7 @@ export type { LyncPresentationRole as RawLyncPresentationRole, LyncPresentationSection as RawLyncPresentationSection, LyncPresentationSource as RawLyncPresentationSource, -} from "../../../vendor/lync-presentation/index.js"; +} from "@deepfates/lync/presentation"; export type RawLyncPresentationContext = LyncPresentationContext; diff --git a/client/interface/lync/rawLyncPresentationTypes.ts b/client/interface/lync/rawLyncPresentationTypes.ts index 3b928ea..3fdda11 100644 --- a/client/interface/lync/rawLyncPresentationTypes.ts +++ b/client/interface/lync/rawLyncPresentationTypes.ts @@ -5,7 +5,7 @@ export type { LyncPresentationRole as RawLyncPresentationRole, LyncPresentationSection as RawLyncPresentationSection, LyncPresentationSource as RawLyncPresentationSource, -} from "../../../vendor/lync-presentation/index.js"; +} from "@deepfates/lync/presentation"; /** * Compact on-Loom form for a presentation section. When a section is exactly @@ -14,9 +14,9 @@ export type { * expands this marker back to the public Lync presentation shape. */ export type StoredRawLyncPresentationSection = - | import("../../../vendor/lync-presentation/index.js").LyncPresentationSection + | import("@deepfates/lync/presentation").LyncPresentationSection | { - role: import("../../../vendor/lync-presentation/index.js").LyncPresentationRole; + role: import("@deepfates/lync/presentation").LyncPresentationRole; sourcePaths: string[]; sameAsTurnText: true; }; diff --git a/client/interface/lync/storyLoom.ts b/client/interface/lync/storyLoom.ts index a54e5f6..98e4c99 100644 --- a/client/interface/lync/storyLoom.ts +++ b/client/interface/lync/storyLoom.ts @@ -602,9 +602,13 @@ function turnToStoryNode( return { id: turn.id, text, - // Archive/portable artifacts need their source time for an honest reopen. - // Keep legacy story projections byte-for-byte shaped as before. - ...(meta?.archiveSource || meta?.portableTurnId ? { createdAt: turn.createdAt } : {}), + // Imported artifacts need their source time for honest reading. In + // particular, an indexed resident life may cross a stop/resume boundary + // while remaining one causal thread; dropping its event times makes that + // discontinuity look like one uninterrupted scene. + ...(meta?.archiveSource || meta?.portableTurnId || meta?.sourceId + ? { createdAt: turn.createdAt } + : {}), continuations: [], origin: originFromMeta(meta), actor: meta?.author, diff --git a/client/styles/terminal.css b/client/styles/terminal.css index 3517345..8cff8fb 100644 --- a/client/styles/terminal.css +++ b/client/styles/terminal.css @@ -395,6 +395,14 @@ html, body { margin-top: 0.65rem; } +.story-text .story-time-seam { + display: block; + margin: 0.9rem 0 0.45rem; + color: var(--theme-muted); + font-size: 0.82em; + opacity: 0.82; +} + .story-text .story-path-window-notice { display: block; margin: 0.35rem 0 0.65rem; diff --git a/docs/behold-resident-lync-presentation.md b/docs/behold-resident-lync-presentation.md index 00c3ce7..0bad9cd 100644 --- a/docs/behold-resident-lync-presentation.md +++ b/docs/behold-resident-lync-presentation.md @@ -1,11 +1,11 @@ # Behold resident Lync presentation Textile presents one declared Behold life profile without changing the source -Lync. The domain contract is owned by Behold at commit -`a95491b55da6bb716b858ff88e8cfa7e7429b6f0` in -`docs/RESIDENT_LYNC_PRESENTATION.md`; Lync implements the exact presenter at -commit `fee7adc14f0038d9d0be51bd16b930659ffae1f8`, and this file records -Textile's consumption and the boundary actually exercised here. +Lync. The domain contract is owned by Behold in +`docs/RESIDENT_LYNC_PRESENTATION.md`; Lync implements the presenter consumed +directly here through `@deepfates/lync/presentation`. The exact local package is +from Lync commit `860aa549727bdec7fda2ba1571c0a6be1c787aa8` and is checksum-bound in +`vendor/LYNC-PROVENANCE.md`. ## Dispatch @@ -13,7 +13,8 @@ Lync's presenter runs only for a causally inherited Loom profile of `org.behold.inhabitant.v1` or `org.behold.inhabitant.v2`. The root must declare `behold.entity-loom.v1`; each turn must declare both `behold.entity-turn-link.v1` on the outer `lync/turn` payload and -`behold.entity-turn.v1` on its nested EntityTurn. The turn's body and action +`behold.entity-turn.v1` or `behold.entity-cognition-turn.v1` on its nested +private-life event. The event's body and action profiles must both be exactly `minecraft-human-semantic-v1`. There is no recursive text search and no generic fallback after this profile @@ -22,7 +23,8 @@ action input remains unsupported or receives a named source-path diagnostic. ## Derived reading -Textile can derive these sections from the ratified allowlist: +Lync derives these sections from the ratified allowlist; Textile groups and +renders the returned public sections: - perceptions before and after the action: HUD condition and inventory, player-list/chat/event information, visible entity semantics, focus, and @@ -31,6 +33,10 @@ Textile can derive these sections from the ratified allowlist: - action name, known action-specific input, source, and kind; and - success/failure event plus known action-specific outcome evidence. +An explicit cognition event presents its admitted perception and “chose no +bodily action.” It has no action, outcome, or terminal observation; the reader +rejects a cognition record that invents any of those fields. + Provider-private reasoning, request/response bodies, transport evidence, controller state, absolute coordinates, stable hidden identifiers, navigation conclusions, and arbitrary nested strings are never promoted to prose. @@ -64,6 +70,9 @@ navigates both source ids in MAP, performs the ordinary full-tree JSON export, and proves that the exported `sourceEvent` records reproduce those fixture bytes exactly. +The exact Behold episode 000014 ordered source set exercises the direct package +boundary across 692 source events with no unsupported or unclaimed event. + This shared presentation contract does not change Keep/export meaning, Backspace editing, MAP focus, or Textile's first-parent reading path. Typed additional-parent/pointer navigation and contextual export remain separate diff --git a/docs/splice-raw-lync-presentations.md b/docs/splice-raw-lync-presentations.md index fe27c23..f50fe0b 100644 --- a/docs/splice-raw-lync-presentations.md +++ b/docs/splice-raw-lync-presentations.md @@ -1,6 +1,6 @@ # Splice raw-Lync presentation matrix -Ground truth: Lync `fee7adc14f0038d9d0be51bd16b930659ffae1f8`, +Ground truth: Lync `860aa549727bdec7fda2ba1571c0a6be1c787aa8`, Splice `ea2ecb0c408590fef7fabe51335765a61831ab5e`, their presentation/import tests, and Splice's `splice lync archive|glowfic|ocr|tweet-embed` writers, source adapters, and diff --git a/package.json b/package.json index ed62a10..7ac2013 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ax-llm/ax": "^14.0.39", - "@deepfates/lync": "file:vendor/deepfates-lync-0.4.3-indexed-union.tgz", + "@deepfates/lync": "file:vendor/deepfates-lync-0.4.3-0a511b6.tgz", "@tailwindcss/postcss": "^4.1.17", "@types/d3-flextree": "^2.1.4", "@types/express": "^4.17.21", diff --git a/vendor/LYNC-PROVENANCE.md b/vendor/LYNC-PROVENANCE.md index 4325d5e..612e14b 100644 --- a/vendor/LYNC-PROVENANCE.md +++ b/vendor/LYNC-PROVENANCE.md @@ -1,14 +1,15 @@ # Vendored Lync package -`deepfates-lync-0.4.3-indexed-union.tgz` was produced with `pnpm pack` +`deepfates-lync-0.4.3-0a511b6.tgz` was produced with `pnpm pack` from the clean `@deepfates/lync` checkout at commit -`e99bce30fa00fd833f527506d799d09ccfde89e6`. +`0a511b6f918acf87800bf433f34311ee47a9c48b`. - Declared package version: `0.4.3` -- Archive SHA-256: `4295c1e0c7ad368f1b913a755853b733f3bd7a47d10d7cc7bba305e7a534b4a3` -- Required unpublished surface: `@deepfates/lync/indexed-union` +- Archive SHA-256: `4de871ce1117cff040fedc069944c3705e314168e01f151ae6d27239f6d1b745` +- Required unpublished surfaces: `@deepfates/lync/indexed-union` and + `@deepfates/lync/presentation` This exact local package keeps Textile reproducible before an owner-approved 0.4 registry release. It is a dependency boundary, not a fork: union parsing, -conflict adjudication, topology, suppression, and source-byte verification -remain owned by Lync. +conflict adjudication, topology, suppression, source-byte verification, and +domain presentation remain owned by Lync. diff --git a/vendor/deepfates-lync-0.4.3-0a511b6.tgz b/vendor/deepfates-lync-0.4.3-0a511b6.tgz new file mode 100644 index 0000000..79716da Binary files /dev/null and b/vendor/deepfates-lync-0.4.3-0a511b6.tgz differ diff --git a/vendor/deepfates-lync-0.4.3-indexed-union.tgz b/vendor/deepfates-lync-0.4.3-indexed-union.tgz deleted file mode 100644 index 17c1a4d..0000000 Binary files a/vendor/deepfates-lync-0.4.3-indexed-union.tgz and /dev/null differ diff --git a/vendor/lync-presentation/PROVENANCE.md b/vendor/lync-presentation/PROVENANCE.md deleted file mode 100644 index 8418ec5..0000000 --- a/vendor/lync-presentation/PROVENANCE.md +++ /dev/null @@ -1,45 +0,0 @@ -# Vendored Lync presentation candidate - -These files come from the immutable local package artifact -`/Users/deepfates/Hacking/data/artifacts/lync/deepfates-lync-0.4.2.tgz`, -SHA-256 -`c83b01766b73656a334d49b28e54056472e2b171a981560ded642524fed3aba8`. -The archive declares `@deepfates/lync` version `0.4.2` and was packed from -`deepfates/lync` commit -`0ec1b37db5759c1f8f12a47394a0300925a5bfad`. - -Vendored file digests are: - -- `index.js` SHA-256 `3471960558021f9ffb9a885d9f56ba1cc20de07a301041fd542952fdecea22c3` -- `index.d.ts` SHA-256 `d7f523d246181fce6d99e60e7c36021ecda74ae83e8b8847a74e4ac260cc5a33` -- `presenters/behold-inhabitant.js` SHA-256 `b8af885b3fd80da8ea65a2b01be9473f525520d8a25c80c2d840ed4a3a1a716c` - -The corresponding archive members have SHA-256 -`a8411061032cd81b96646c77dfbf629acc6cb02ffebf353aa4287734b6c95d1b` -(`dist/presentation.js`), -`dceac3a543c799f5be84a31eab80ac1ae712a0c974f9535925d32da1f3d5842b` -(`dist/presentation.d.ts`), and -`e027474eacb6f484ed8c4242a4af622b1879f0cd0e6700504abd772ecf2904e7` -(`dist/presenters/behold-inhabitant.js`). `index.js` differs only by omission of -its source-map trailer. The vendored Behold presenter also carries Textile's -bounded `tex-nak8` reader overlay for four current resident lifecycle event -shapes, the current world-event wait result, truthful chat/whisper input -dispatch settlement, focused-use failure presentation, and Lync commit -`ddace87`'s distinction between received private whispers and public chat; all -other unknown shapes still fail closed. `index.d.ts` keeps Textile's published-0.3-compatible -`@deepfates/lync/events` type import and declares the two exported profile -constants directly. - -Lync owns the API, dispatch, presentation pacts, and tests. This exact compiled -copy keeps the private application independently installable while its lock -remains on published Lync 0.3. It has no runtime dependencies and performs no -filesystem, network, DOM, clock, or storage I/O. Source-map trailers and map -files are omitted. - -Regenerate by verifying and extracting that exact package artifact, copying -`dist/presentation.js` and `dist/presenters/behold-inhabitant.js` with the same -relative layout, omitting their source-map trailers, then applying the bounded -reader overlay and declaration-only import adaptation described above. Replace -this candidate with `@deepfates/lync/presentation` after an owner-authorized -Lync release contains the same pact; do not add unrelated consumer-local -presentation heuristics. diff --git a/vendor/lync-presentation/index.d.ts b/vendor/lync-presentation/index.d.ts deleted file mode 100644 index b2c35e4..0000000 --- a/vendor/lync-presentation/index.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { LyncEventBody } from "@deepfates/lync/events"; -export declare const BEHOLD_INHABITANT_PROFILE = "org.behold.inhabitant.v1"; -export declare const BEHOLD_INHABITANT_PROFILE_V2 = "org.behold.inhabitant.v2"; -export type LyncPresentationKind = "content" | "structure"; -export type LyncPresentationRole = "content" | "structure" | "perception" | "utterance" | "action" | "outcome"; -export interface LyncPresentationSection { - role: LyncPresentationRole; - text: string; - /** Exact JSON paths in the source event used for this section. */ - sourcePaths: string[]; -} -export interface LyncPresentationDiagnostic { - code: string; - sourcePath: string; -} -export interface LyncPresentationSource { - id: string; - parents: string[]; - author: { - actor: string; - via?: string; - }; - kind: string; -} -/** A non-mutating readable projection over one exact source event. */ -export interface LyncPresentation { - text: string; - kind: LyncPresentationKind; - contract: string; - source: LyncPresentationSource; - sections: LyncPresentationSection[]; - diagnostics: LyncPresentationDiagnostic[]; -} -export interface LyncPresentationContext { - /** Exact profile inherited from a causal loom root, when unambiguous. */ - loomProfile?: string; -} -export type LyncPresentationResult = { - status: "presented"; - presentation: LyncPresentation; -} | { - status: "unsupported"; - contract: string; - diagnostics: LyncPresentationDiagnostic[]; -} | { - status: "unclaimed"; -}; -/** - * Resolve one source event through exact profile, exact kind, then the small - * generic text/message pact. A claimed profile or kind never falls through - * when its payload is malformed: callers receive an explicit unsupported - * decision instead of plausible prose from an unrelated nested field. - */ -export declare function presentLyncEvent(event: LyncEventBody, context?: LyncPresentationContext): LyncPresentationResult; -/** - * Resolve the one profile inherited through an event's causal parents. - * Parent order and event identities are untouched. Conflicting inherited - * profiles deliberately resolve to no profile rather than choosing one. - */ -export declare function resolveLyncPresentationProfiles(events: Iterable): Map; -/** Convert source HTML to inert, readable plain text without executing it. */ -export declare function htmlToPlainText(html: string | null): string | null; diff --git a/vendor/lync-presentation/index.js b/vendor/lync-presentation/index.js deleted file mode 100644 index e3b996a..0000000 --- a/vendor/lync-presentation/index.js +++ /dev/null @@ -1,338 +0,0 @@ -import { BEHOLD_INHABITANT_PROFILE, BEHOLD_INHABITANT_PROFILE_V2, presentBeholdInhabitantEvent, presentBeholdInhabitantEventV2, } from "./presenters/behold-inhabitant.js"; -export { BEHOLD_INHABITANT_PROFILE, BEHOLD_INHABITANT_PROFILE_V2, } from "./presenters/behold-inhabitant.js"; -const splicePresenters = { - "twitter/tweet": { - contract: "splice/twitter-archive", - present: (event) => content(event, firstString(event.payload, ["full_text", "fullText", "text"]), "splice/twitter-archive", firstStringPath(event.payload, ["full_text", "fullText", "text"])), - }, - "twitter/like": { - contract: "splice/twitter-archive", - present: (event) => content(event, firstString(event.payload, ["full_text", "fullText", "text"]), "splice/twitter-archive", firstStringPath(event.payload, ["full_text", "fullText", "text"])), - }, - "bluesky/post": { - contract: "splice/bluesky-archive", - present: (event) => { - const nested = nestedString(event.payload, ["record", "text"]); - return content(event, nested ?? stringField(event.payload, "text"), "splice/bluesky-archive", nested !== null ? "payload.record.text" : fieldPath(event.payload, "text")); - }, - }, - "glowfic/post": { - contract: "splice/glowfic-json", - present: (event) => content(event, htmlToPlainText(stringField(event.payload, "content")), "splice/glowfic-json", fieldPath(event.payload, "content")), - }, - "twitter/tweet-embed": { - contract: "splice/twitter-embed-cache", - present: (event) => content(event, htmlToPlainText(nestedString(event.payload, ["embed", "html"])), "splice/twitter-embed-cache", nestedString(event.payload, ["embed", "html"]) !== null - ? "payload.embed.html" - : null), - }, - "ocr/page": { - contract: "splice/ocr-text-import", - present: (event) => content(event, stringField(event.payload, "text"), "splice/ocr-text-import", fieldPath(event.payload, "text")), - }, - "ocr/document": { - contract: "splice/ocr-text-import", - present: (event) => content(event, stringField(event.payload, "text"), "splice/ocr-text-import", fieldPath(event.payload, "text")), - }, - "glowfic/thread": { - contract: "splice/glowfic-json", - present: glowficThread, - }, - "ocr/set": { - contract: "splice/ocr-text-import", - present: ocrSet, - }, -}; -/** - * Resolve one source event through exact profile, exact kind, then the small - * generic text/message pact. A claimed profile or kind never falls through - * when its payload is malformed: callers receive an explicit unsupported - * decision instead of plausible prose from an unrelated nested field. - */ -export function presentLyncEvent(event, context = {}) { - if (context.loomProfile === BEHOLD_INHABITANT_PROFILE) { - const presentation = presentBeholdInhabitantEvent(event); - return presentation - ? normalizePresentation(event, presentation) - : unsupported("org.behold.presentation.inhabitant-turn.v1", "unsupported_profile_event"); - } - if (context.loomProfile === BEHOLD_INHABITANT_PROFILE_V2) { - const presentation = presentBeholdInhabitantEventV2(event); - return presentation - ? normalizePresentation(event, presentation) - : unsupported("org.behold.presentation.inhabitant-turn.v2", "unsupported_profile_event"); - } - const known = splicePresenters[event.kind]; - if (known) { - const presentation = known.present(event); - return presentation - ? normalizePresentation(event, presentation) - : unsupported(known.contract, "malformed_known_kind"); - } - if (event.kind === "lync/pointer") { - const presentation = lyncPointer(event); - return presentation - ? normalizePresentation(event, presentation) - : unsupported("lync/pointer", "malformed_known_kind"); - } - const generic = genericPresentation(event); - return generic ? normalizePresentation(event, generic) : { status: "unclaimed" }; -} -/** - * Resolve the one profile inherited through an event's causal parents. - * Parent order and event identities are untouched. Conflicting inherited - * profiles deliberately resolve to no profile rather than choosing one. - */ -export function resolveLyncPresentationProfiles(events) { - const list = [...events]; - const byId = new Map(list.map((event) => [event.id, event])); - const resolved = new Map(); - const visiting = new Set(); - const profileFor = (event) => { - const cached = resolved.get(event.id); - if (cached !== undefined) - return cached; - if (visiting.has(event.id)) - return null; - visiting.add(event.id); - let profile = null; - if (event.kind === "lync/loom") { - const meta = recordField(event.payload, "meta"); - profile = typeof meta?.profile === "string" ? meta.profile : null; - } - else { - const inherited = new Set(); - for (const parentId of event.parents) { - const parent = byId.get(parentId); - if (!parent) - continue; - const parentProfile = profileFor(parent); - if (parentProfile) - inherited.add(parentProfile); - } - if (inherited.size === 1) - profile = [...inherited][0] ?? null; - } - visiting.delete(event.id); - resolved.set(event.id, profile); - return profile; - }; - const result = new Map(); - for (const event of list) { - const profile = profileFor(event); - if (profile) - result.set(event.id, profile); - } - return result; -} -function normalizePresentation(event, presentation) { - return { - status: "presented", - presentation: { - ...presentation, - source: presentation.source ?? presentationSource(event), - sections: presentation.sections ?? [], - diagnostics: presentation.diagnostics ?? [], - }, - }; -} -function unsupported(contract, code) { - return { - status: "unsupported", - contract, - diagnostics: [{ code, sourcePath: "payload" }], - }; -} -function content(event, text, contract, sourcePath) { - if (text === null || sourcePath === null || text.length === 0) - return null; - return presentation(event, text, "content", contract, "content", [sourcePath]); -} -function presentation(event, text, kind, contract, role, sourcePaths) { - return { - text, - kind, - contract, - source: presentationSource(event), - sections: [{ role, text, sourcePaths }], - diagnostics: [], - }; -} -function presentationSource(event) { - return { - id: event.id, - parents: [...event.parents], - author: { - actor: event.author.actor, - ...(typeof event.author.via === "string" ? { via: event.author.via } : {}), - }, - kind: event.kind, - }; -} -function glowficThread(event) { - const payload = event.payload; - const title = stringField(payload, "title"); - const id = stringField(payload, "id"); - const authors = stringArray(payload.authors); - const source = stringField(payload, "url"); - const lines = [ - `Glowfic thread: ${title ?? id ?? "untitled"}`, - id && title ? `Thread ${id}` : null, - authors.length ? `Authors: ${authors.join(", ")}` : null, - source ? `Source: ${source}` : null, - ].filter((line) => line !== null); - const paths = [ - title !== null ? "payload.title" : null, - id !== null ? "payload.id" : null, - authors.length ? "payload.authors" : null, - source !== null ? "payload.url" : null, - ].filter((path) => path !== null); - return presentation(event, lines.join("\n"), "structure", "splice/glowfic-json", "structure", paths); -} -function ocrSet(event) { - const payload = event.payload; - const locator = stringField(payload, "locator") ?? "unnamed set"; - const pages = integerField(payload, "pages"); - const documents = Array.isArray(payload.documents) ? payload.documents.length : null; - const range = recordField(payload, "page_range"); - const min = range ? integerField(range, "min") : null; - const max = range ? integerField(range, "max") : null; - const counts = [ - pages === null ? null : `${pages} ${pages === 1 ? "page" : "pages"}`, - documents === null ? null : `${documents} ${documents === 1 ? "document" : "documents"}`, - ].filter((value) => value !== null); - const lines = [ - `OCR set: ${locator}`, - counts.length ? counts.join(" · ") : null, - min === null || max === null ? null : `Page range: ${min}–${max}`, - ].filter((line) => line !== null); - const paths = [ - stringField(payload, "locator") !== null ? "payload.locator" : null, - pages !== null ? "payload.pages" : null, - documents !== null ? "payload.documents" : null, - min !== null || max !== null ? "payload.page_range" : null, - ].filter((path) => path !== null); - return presentation(event, lines.join("\n"), "structure", "splice/ocr-text-import", "structure", paths); -} -function lyncPointer(event) { - const name = stringField(event.payload, "name"); - const target = stringField(event.payload, "target"); - if (!name || !target) - return null; - return presentation(event, `Lync pointer: ${name}\nTarget: ${target}`, "structure", "lync/pointer", "structure", ["payload.name", "payload.target"]); -} -function genericPresentation(event) { - const payload = event.payload; - for (const field of ["text", "full_text", "fullText", "message"]) { - const direct = stringField(payload, field); - if (direct !== null) { - return content(event, direct, "lync/generic-text-v1", `payload.${field}`); - } - } - const message = recordField(payload, "message"); - if (!message) - return null; - const messageText = stringField(message, "content"); - if (messageText !== null) { - return content(event, messageText, "lync/generic-message-v1", "payload.message.content"); - } - const blocks = message.content; - if (!Array.isArray(blocks)) - return null; - const paths = []; - const text = blocks - .map((block, index) => { - const record = recordValue(block); - const value = record ? stringField(record, "text") : null; - if (value !== null) - paths.push(`payload.message.content[${index}].text`); - return value ?? ""; - }) - .filter(Boolean) - .join(""); - return text - ? presentation(event, text, "content", "lync/generic-message-blocks-v1", "content", paths) - : null; -} -/** Convert source HTML to inert, readable plain text without executing it. */ -export function htmlToPlainText(html) { - if (html === null) - return null; - const withoutExecutable = html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, ""); - const withBreaks = withoutExecutable - .replace(//gi, "\n") - .replace(/<\/(p|div|blockquote|li|h[1-6])\s*>/gi, "\n"); - const decoded = decodeHtmlEntities(withBreaks.replace(/<[^>]*>/g, "")); - const normalized = decoded - .replace(/\r\n?/g, "\n") - .replace(/[\t ]+\n/g, "\n") - .replace(/\n[\t ]+/g, "\n") - .replace(/[\t ]{2,}/g, " ") - .replace(/\n{3,}/g, "\n\n") - .trim(); - return normalized || null; -} -function decodeHtmlEntities(value) { - const named = { - amp: "&", apos: "'", gt: ">", hellip: "…", ldquo: "“", lsquo: "‘", - lt: "<", mdash: "—", nbsp: " ", ndash: "–", quot: '"', rdquo: "”", rsquo: "’", - }; - return value.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);/gi, (match, entity) => { - if (entity[0] === "#") { - const base = entity[1]?.toLowerCase() === "x" ? 16 : 10; - const digits = base === 16 ? entity.slice(2) : entity.slice(1); - const codePoint = Number.parseInt(digits, base); - if (Number.isSafeInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff) { - return String.fromCodePoint(codePoint); - } - return match; - } - return named[entity.toLowerCase()] ?? match; - }); -} -function firstString(payload, fields) { - for (const field of fields) { - const value = stringField(payload, field); - if (value !== null) - return value; - } - return null; -} -function firstStringPath(payload, fields) { - for (const field of fields) { - if (stringField(payload, field) !== null) - return `payload.${field}`; - } - return null; -} -function fieldPath(payload, field) { - return stringField(payload, field) !== null ? `payload.${field}` : null; -} -function nestedString(payload, path) { - let value = payload; - for (const key of path) { - const record = recordValue(value); - if (!record) - return null; - value = record[key]; - } - return typeof value === "string" ? value : null; -} -function stringField(payload, field) { - return typeof payload[field] === "string" ? payload[field] : null; -} -function integerField(payload, field) { - const value = payload[field]; - return Number.isInteger(value) ? value : null; -} -function stringArray(value) { - return Array.isArray(value) ? value.filter((item) => typeof item === "string") : []; -} -function recordField(payload, field) { - return recordValue(payload[field]); -} -function recordValue(value) { - return value !== null && typeof value === "object" && !Array.isArray(value) - ? value - : null; -} diff --git a/vendor/lync-presentation/presenters/behold-inhabitant.js b/vendor/lync-presentation/presenters/behold-inhabitant.js deleted file mode 100644 index d98129c..0000000 --- a/vendor/lync-presentation/presenters/behold-inhabitant.js +++ /dev/null @@ -1,911 +0,0 @@ -export const BEHOLD_INHABITANT_PROFILE = "org.behold.inhabitant.v1"; -export const BEHOLD_INHABITANT_PROFILE_V2 = "org.behold.inhabitant.v2"; -const V1 = { - profile: BEHOLD_INHABITANT_PROFILE, - contract: "org.behold.presentation.inhabitant-turn.v1", - version: 1, -}; -const V2 = { - profile: BEHOLD_INHABITANT_PROFILE_V2, - contract: "org.behold.presentation.inhabitant-turn.v2", - version: 2, -}; -const HUMAN_PROFILE = "minecraft-human-semantic-v1"; -const OBSERVATION_PROTOCOL = "behold.minecraft-human-semantic-observation.v1"; -/** - * Behold owns the meaning of this exact profile. Lync projects only the - * allowlisted paths in Behold's v1 presentation pact; it never searches an - * opaque payload for plausible prose. - */ -export function presentBeholdInhabitantEvent(event) { - return presentBeholdEvent(event, V1); -} -export function presentBeholdInhabitantEventV2(event) { - return presentBeholdEvent(event, V2); -} -function presentBeholdEvent(event, profile) { - if (event.kind === "lync/loom") - return presentResidentLoom(event, profile); - if (event.kind === "lync/turn") - return presentResidentTurn(event, profile); - return null; -} -function presentResidentLoom(event, profile) { - const meta = recordField(event.payload, "meta"); - if (stringField(meta, "protocol") !== "behold.entity-loom.v1" || - stringField(meta, "profile") !== profile.profile) { - return null; - } - const entityId = stringField(meta, "entityId"); - const circleId = stringField(meta, "circleId"); - if (!entityId) - return null; - const lines = [ - `Behold resident life: ${entityId}`, - circleId ? `World circle: ${circleId}` : null, - `Profile: ${profile.profile}`, - ].filter((line) => line !== null); - const section = { - role: "structure", - text: lines.join("\n"), - sourcePaths: ["payload.meta"], - }; - return { - text: section.text, - kind: "structure", - contract: profile.contract, - source: presentationSource(event), - sections: [section], - diagnostics: [], - }; -} -function presentResidentTurn(event, profile) { - const link = recordField(event.payload, "meta"); - const turn = recordField(event.payload, "payload"); - if (!link || - !turn || - stringField(link, "protocol") !== "behold.entity-turn-link.v1" || - stringField(turn, "protocol") !== "behold.entity-turn.v1") { - return null; - } - const profiles = recordField(turn, "profiles"); - if (stringField(profiles, "body") !== HUMAN_PROFILE || - stringField(profiles, "actions") !== HUMAN_PROFILE) { - return null; - } - const entityId = stringField(turn, "entityId"); - const sequence = integerField(turn, "sequence"); - const model = stringField(turn, "model"); - if (!entityId || sequence === null || !model) - return null; - const diagnostics = []; - const sections = []; - const observation = presentObservation(turn.observation, entityId, "payload.payload.observation", diagnostics, profile); - if (observation) - sections.push(observation); - const utterance = presentUtterance(turn.utterance); - if (utterance) - sections.push(utterance); - sections.push(presentAction(turn.action, entityId, diagnostics, profile)); - sections.push(presentOutcome(turn.action, turn.outcome, diagnostics, profile)); - const nextObservation = presentObservation(turn.nextObservation, entityId, "payload.payload.nextObservation", diagnostics, profile); - if (nextObservation) - sections.push(nextObservation); - const release = recordField(turn, "experimentRelease"); - const releaseId = stringField(release, "releaseId"); - const observedOrder = integerField(release, "residentObservedOrder"); - const structure = [ - `${entityId} · turn ${sequence}`, - `Model: ${model}`, - `Profiles: ${stringField(profiles, "policy") ?? "unknown"} · ${HUMAN_PROFILE} · ${stringField(profiles, "safety") ?? "unknown"}`, - releaseId ? `Release: ${releaseId}` : null, - observedOrder === null ? null : `Resident-observed order: ${observedOrder}`, - ].filter((line) => line !== null); - return { - text: [structure.join("\n"), ...sections.map(sectionText)].join("\n\n"), - kind: "content", - contract: profile.contract, - source: presentationSource(event), - sections, - diagnostics, - }; -} -function presentationSource(event) { - return { - id: event.id, - parents: [...event.parents], - author: { - actor: event.author.actor, - ...(typeof event.author.via === "string" - ? { via: event.author.via } - : {}), - }, - kind: event.kind, - }; -} -function presentObservation(value, entityId, sourcePath, diagnostics, profile) { - const observation = recordValue(value); - if (!observation || stringField(observation, "protocol") !== OBSERVATION_PROTOCOL) { - diagnostics.push({ code: "unsupported_observation_protocol", sourcePath }); - return null; - } - const bodyContract = recordField(observation, "bodyContract"); - if (stringField(bodyContract, "profile") !== HUMAN_PROFILE) { - diagnostics.push({ - code: "unsupported_observation_body_profile", - sourcePath, - }); - return null; - } - const lines = []; - const self = recordField(observation, "self"); - const condition = recordField(self, "condition"); - const conditionParts = [ - numberField(condition, "health") === null - ? null - : `health ${numberField(condition, "health")}`, - numberField(condition, "food") === null - ? null - : `food ${numberField(condition, "food")}`, - numberField(condition, "breathBubbles") === null - ? null - : `breath ${numberField(condition, "breathBubbles")}`, - typeof condition?.sleeping === "boolean" - ? condition.sleeping - ? "sleeping" - : "awake" - : null, - stringField(condition, "dimension"), - stringField(condition, "daylight"), - ].filter((part) => part !== null); - if (conditionParts.length) - lines.push(`${entityId}'s condition: ${conditionParts.join(" · ")}.`); - const inventory = arrayField(self, "inventory").flatMap(presentInventoryItem); - if (inventory.length) - lines.push(`Inventory: ${inventory.join(", ")}.`); - else if (Array.isArray(self?.inventory)) - lines.push("Inventory: empty."); - const heldItem = presentInventoryItem(self?.heldItem); - if (heldItem.length) - lines.push(`Held item: ${heldItem.join(", ")}.`); - if (observation.eventWindow !== undefined) { - diagnostics.push({ - code: "source_only_event_window", - sourcePath: `${sourcePath}.eventWindow`, - }); - } - if (self?.pose !== undefined) { - diagnostics.push({ - code: "source_only_pose", - sourcePath: `${sourcePath}.self.pose`, - }); - } - const scene = recordField(observation, "scene"); - const social = recordField(scene, "social"); - const players = stringArray(social?.playersOnline); - if (players.length) - lines.push(`Players online: ${players.join(", ")}.`); - const entities = arrayField(scene, "entities").flatMap((item, index) => { - const entity = recordValue(item); - const name = stringField(entity, "name"); - const kind = stringField(entity, "kind"); - if (!name && !kind) - return []; - if (entity?.reference !== undefined) { - diagnostics.push({ - code: "withheld_observation_local_reference", - sourcePath: `${sourcePath}.scene.entities[${index}].reference`, - }); - } - const details = [ - kind, - stringField(entity, "proximity"), - stringField(entity, "relativeDirection"), - stringField(entity, "visibility"), - ].filter((detail) => detail !== null); - return [ - `Saw ${name ?? kind}${details.length ? ` (${details.join(", ")})` : ""}.`, - ]; - }); - lines.push(...entities); - const focus = recordField(scene, "focus"); - if (focus) { - const focusParts = [ - stringField(focus, "name"), - stringField(focus, "kind"), - stringField(focus, "material"), - stringField(focus, "proximity"), - stringField(focus, "relativeDirection"), - stringField(focus, "visibility"), - ].filter((part) => part !== null); - if (focusParts.length) - lines.push(`Focus: ${focusParts.join(" · ")}.`); - if (focus.reference !== undefined) { - diagnostics.push({ - code: "withheld_observation_local_reference", - sourcePath: `${sourcePath}.scene.focus.reference`, - }); - } - } - const terrain = recordField(scene, "terrain"); - const visualField = recordField(terrain, "visualField"); - if (stringField(visualField, "protocol") === "behold.visual-field.v1" && - visualField?.available === true) { - const materials = arrayField(visualField, "materialLegend").flatMap((item) => { - const name = stringField(recordValue(item), "name"); - return name ? [humanize(name)] : []; - }); - const depths = arrayField(visualField, "depthLegend").flatMap((item) => { - const label = stringField(recordValue(item), "label"); - return label ? [label] : []; - }); - if (materials.length) - lines.push(`Visible terrain: ${unique(materials).join(", ")}.`); - if (depths.length) - lines.push(`Visual depth bands: ${unique(depths).join(", ")}.`); - if (visualField.materialRows !== undefined || - visualField.depthRows !== undefined) { - diagnostics.push({ - code: "source_only_visual_field_encoding", - sourcePath: `${sourcePath}.scene.terrain.visualField`, - }); - } - } - const eventLines = arrayField(observation, "events").flatMap((item, index) => { - const event = recordValue(item); - const type = stringField(event, "type"); - const data = recordField(event, "data"); - if (type === "spawned") - return ["Spawned into the world."]; - if (type === "condition_changed") - return ["HUD condition changed."]; - if (type === "entity_became_visible") { - const name = stringField(data, "name") ?? stringField(data, "kind") ?? "An entity"; - const proximity = stringField(data, "proximity"); - return [`${name} became visible${proximity ? ` ${proximity}` : ""}.`]; - } - if (type === "entity_left_view") { - const name = stringField(data, "name") ?? stringField(data, "kind") ?? "An entity"; - if (data?.lastSeenDistance !== undefined) { - diagnostics.push({ - code: "withheld_incidental_numeric_estimate", - sourcePath: `${sourcePath}.events[${index}].data.lastSeenDistance`, - }); - } - return [`${name} left the current view.`]; - } - if (type === "chat_received") { - const from = stringField(data, "from"); - const text = stringField(data, "text"); - if (profile.version === 2 && stringField(data, "channel") === "private") { - diagnoseSourceOnlyFields(data, new Set(["from", "text", "channel", "addressed"]), `${sourcePath}.events[${index}].data`, diagnostics); - return text - ? [`Private whisper${from ? ` from ${from}` : ""}: ${text}`] - : []; - } - return text - ? [`Public chat${from ? ` from ${from}` : ""}: ${text}`] - : []; - } - if (profile.version === 2 && type === "sound_heard") { - diagnoseObservationEventEnvelope(event, `${sourcePath}.events[${index}]`, diagnostics); - return presentSoundHeard(data, `${sourcePath}.events[${index}]`, diagnostics); - } - if (profile.version === 2 && type === "sound_sequence_heard") { - diagnoseObservationEventEnvelope(event, `${sourcePath}.events[${index}]`, diagnostics); - return presentSoundSequence(data, `${sourcePath}.events[${index}]`, diagnostics); - } - if (profile.version === 2 && type === "time_passed") { - diagnoseObservationEventEnvelope(event, `${sourcePath}.events[${index}]`, diagnostics); - diagnoseSourceOnlyFields(data, new Set(["elapsedMs"]), `${sourcePath}.events[${index}].data`, diagnostics); - const elapsedMs = integerField(data, "elapsedMs"); - return elapsedMs !== null && elapsedMs >= 0 - ? [`Time passed: ${elapsedMs} ms.`] - : unsupportedObservationEvent(`${sourcePath}.events[${index}]`, diagnostics); - } - if (profile.version === 2 && - [ - "action_failed", - "controller_suspended", - "cancellation_requested", - "day_phase_changed", - "died", - "self_hurt", - "visible_entity_hurt", - "visible_block_changed", - ].includes(type ?? "")) { - return presentResidentLifecycleEvent(type ?? "", event, data, `${sourcePath}.events[${index}]`, diagnostics); - } - diagnostics.push({ - code: "unsupported_observation_event", - sourcePath: `${sourcePath}.events[${index}]`, - }); - return []; - }); - lines.push(...eventLines); - if (lines.length === 0) { - diagnostics.push({ code: "empty_safe_observation_projection", sourcePath }); - return null; - } - return { - role: "perception", - text: lines.join(" "), - sourcePaths: [sourcePath], - }; -} -function presentUtterance(value) { - const assistant = recordField(recordValue(value), "assistant"); - const content = stringField(assistant, "content")?.trim(); - if (!content) - return null; - return { - role: "utterance", - text: content, - sourcePaths: ["payload.payload.utterance.assistant.content"], - }; -} -function presentAction(value, entityId, diagnostics, profile) { - const action = recordValue(value); - const name = stringField(action, "name") ?? "unknown action"; - const source = stringField(action, "source") ?? "unknown source"; - const kind = stringField(action, "kind") ?? "unknown kind"; - const input = recordField(action, "input"); - let description = null; - const paths = [ - "payload.payload.action.name", - "payload.payload.action.source", - "payload.payload.action.kind", - ]; - if (action?.id !== undefined) { - diagnostics.push({ - code: "source_only_action_identity", - sourcePath: "payload.payload.action.id", - }); - } - if (action?.toolCallId !== undefined) { - diagnostics.push({ - code: "source_only_tool_call_identity", - sourcePath: "payload.payload.action.toolCallId", - }); - } - if (name === "look_direction") { - const horizontal = stringField(input, "horizontal"); - const vertical = stringField(input, "vertical"); - if (horizontal) { - description = `${entityId} looked ${horizontal}${vertical ? `, ${vertical}` : ""}.`; - paths.push("payload.payload.action.input.horizontal"); - if (vertical) - paths.push("payload.payload.action.input.vertical"); - } - } - else if (name === "move_controls") { - const direction = stringField(input, "direction"); - const durationMs = integerField(input, "durationMs"); - if (direction) { - description = `${entityId} held ${direction}${durationMs === null ? "" : ` for ${durationMs} ms`}.`; - paths.push("payload.payload.action.input.direction"); - if (durationMs !== null) - paths.push("payload.payload.action.input.durationMs"); - } - } - else if (name === "chat") { - const text = stringField(input, "text"); - if (text) { - description = `${entityId} sent public chat: ${text}`; - paths.push("payload.payload.action.input.text"); - } - } - else if (name === "wait_for_event") { - const reason = stringField(input, "reason"); - description = `${entityId} waited for another event${reason ? `: ${reason}` : "."}`; - if (reason) - paths.push("payload.payload.action.input.reason"); - } - else if (profile.version === 2 && - ["attack_focused_entity", "dig_focused_block", "use_focused_block"].includes(name)) { - diagnoseSourceOnlyFields(action, new Set(["id", "name", "input", "kind", "toolCallId", "source"]), "payload.payload.action", diagnostics, new Set(), "source_only_action_field"); - diagnoseSourceOnlyFields(input, new Set(), "payload.payload.action.input", diagnostics, new Set(), "source_only_action_input_field"); - if (input) { - description = name === "attack_focused_entity" - ? `${entityId} attempted one attack at the focused entity.` - : name === "dig_focused_block" - ? `${entityId} attempted to dig the focused block.` - : `${entityId} attempted to use the focused block.`; - } - } - else if (profile.version === 2 && name === "whisper") { - diagnoseSourceOnlyFields(action, new Set(["id", "name", "input", "kind", "toolCallId", "source"]), "payload.payload.action", diagnostics, new Set(), "source_only_action_field"); - diagnoseSourceOnlyFields(input, new Set(["username", "text"]), "payload.payload.action.input", diagnostics, new Set(), "source_only_action_input_field"); - const username = stringField(input, "username")?.trim(); - const text = stringField(input, "text")?.trim(); - if (username && text) { - description = `${entityId} whispered to ${username}: ${text}`; - paths.push("payload.payload.action.input.username", "payload.payload.action.input.text"); - } - } - if (!description) { - description = `${entityId} recorded ${name}; its input has no safe v${profile.version} Textile presenter.`; - diagnostics.push({ - code: "unsupported_action_input", - sourcePath: "payload.payload.action.input", - }); - } - return { - role: "action", - text: `[${source} · ${kind}] ${description}`, - sourcePaths: paths, - }; -} -function presentOutcome(actionValue, outcomeValue, diagnostics, profile) { - const action = recordValue(actionValue); - const outcome = recordValue(outcomeValue); - const actionName = stringField(action, "name") ?? "action"; - const eventType = stringField(outcome, "eventType") ?? "unknown outcome"; - const ok = typeof outcome?.ok === "boolean" ? outcome.ok : null; - const result = recordField(outcome, "result"); - const paths = [ - "payload.payload.outcome.ok", - "payload.payload.outcome.eventType", - ]; - let detail = null; - let v2ShownResultKeys = null; - if (actionName === "look_direction") { - const orientation = recordField(result, "orientation"); - const facing = stringField(orientation, "facing"); - const vertical = stringField(orientation, "vertical"); - if (facing) { - detail = ` The body confirmed facing ${facing}${vertical ? ` and ${vertical}` : ""}.`; - paths.push("payload.payload.outcome.result.orientation.facing"); - if (vertical) - paths.push("payload.payload.outcome.result.orientation.vertical"); - } - } - else if (actionName === "move_controls" && - typeof result?.bodyMoved === "boolean") { - detail = result.bodyMoved ? " The body moved." : " The body did not move."; - paths.push("payload.payload.outcome.result.bodyMoved"); - } - else if (profile.version === 2 && actionName === "chat" && result) { - const resultOk = typeof result.ok === "boolean" ? result.ok : null; - const status = stringField(result, "status")?.trim(); - const error = stringField(result, "error")?.trim(); - if (resultOk === true && (!status || status === "chat_input_dispatched")) { - detail = " The public chat input was submitted; recipient delivery was not independently confirmed here."; - v2ShownResultKeys = new Set(["ok", "status", "message"]); - paths.push("payload.payload.outcome.result.ok"); - if (status) - paths.push("payload.payload.outcome.result.status"); - } - else if (resultOk === false && error) { - detail = ` The public chat input was rejected: ${humanize(error)}.`; - v2ShownResultKeys = new Set(["ok", "error", "maximumCharacters", "providedCharacters"]); - paths.push("payload.payload.outcome.result.ok", "payload.payload.outcome.result.error"); - } - } - else if (actionName === "wait_for_event" && - typeof result?.sawPeerChat === "boolean") { - detail = result.sawPeerChat - ? " The resident observed peer chat." - : " No peer chat was observed."; - paths.push("payload.payload.outcome.result.sawPeerChat"); - } - else if (profile.version === 2 && - actionName === "wait_for_event" && - result?.ok === true && - result?.status === "waiting_for_world_event") { - detail = " The resident is waiting for a world event."; - v2ShownResultKeys = new Set(["ok", "status"]); - paths.push("payload.payload.outcome.result.ok", "payload.payload.outcome.result.status"); - } - else if (profile.version === 2 && actionName === "whisper" && result) { - diagnoseSourceOnlyFields(outcome, new Set(["ok", "eventType", "result"]), "payload.payload.outcome", diagnostics, new Set(), "source_only_outcome_field"); - const resultOk = typeof result.ok === "boolean" ? result.ok : null; - const status = stringField(result, "status")?.trim(); - const error = stringField(result, "error")?.trim(); - if (resultOk === true && (!status || status === "whisper_input_dispatched")) { - detail = " The private whisper input was submitted; recipient delivery was not independently confirmed here."; - v2ShownResultKeys = new Set(["ok", "status", "message"]); - paths.push("payload.payload.outcome.result.ok"); - if (status) - paths.push("payload.payload.outcome.result.status"); - } - else if (resultOk === false && error) { - detail = ` The private whisper input was rejected: ${humanize(error)}.`; - v2ShownResultKeys = new Set(["ok", "error", "maximumCharacters", "providedCharacters"]); - paths.push("payload.payload.outcome.result.ok", "payload.payload.outcome.result.error"); - } - } - else if (profile.version === 2 && - result && - ["attack_focused_entity", "dig_focused_block"].includes(actionName)) { - diagnoseSourceOnlyFields(outcome, new Set(["ok", "eventType", "result", "error", "cancellation"]), "payload.payload.outcome", diagnostics, new Set(["error", "cancellation"]), "source_only_outcome_field"); - const focused = presentFocusedActionResult(actionName, result, paths, diagnostics); - detail = focused.detail; - v2ShownResultKeys = focused.shownResultKeys; - } - if (profile.version === 2 && - result && - detail === null && - eventType === "action_failed" && - result.ok === false) { - const error = stringField(result, "error")?.trim(); - if (error) { - detail = ` The bodily attempt failed: ${humanize(error)}.`; - v2ShownResultKeys = new Set(["ok", "error"]); - paths.push("payload.payload.outcome.result.ok", "payload.payload.outcome.result.error"); - } - } - if (result && detail === null) { - diagnostics.push({ - code: "unsupported_outcome_result", - sourcePath: "payload.payload.outcome.result", - }); - } - if (result) { - const shownResultKeys = v2ShownResultKeys ?? (actionName === "look_direction" - ? new Set(["orientation"]) - : actionName === "move_controls" - ? new Set(["bodyMoved"]) - : actionName === "wait_for_event" - ? new Set(["sawPeerChat"]) - : new Set()); - for (const key of Object.keys(result)) { - if (!shownResultKeys.has(key)) { - diagnostics.push({ - code: "source_only_outcome_field", - sourcePath: `payload.payload.outcome.result.${key}`, - }); - } - } - } - const terminal = outcomeTerminal(ok, eventType); - return { - role: "outcome", - text: `${actionName} ${terminal} (${eventType}).${detail ?? ""}`, - sourcePaths: paths, - }; -} -function presentResidentLifecycleEvent(type, event, data, eventPath, diagnostics) { - diagnoseObservationEventEnvelope(event, eventPath, diagnostics); - if (type === "day_phase_changed") { - diagnoseSourceOnlyFields(data, new Set(["previous", "current"]), `${eventPath}.data`, diagnostics); - const previous = stringField(data, "previous")?.trim(); - const current = stringField(data, "current")?.trim(); - return previous && current - ? [`Day phase changed: ${humanize(previous)} → ${humanize(current)}.`] - : unsupportedObservationEvent(eventPath, diagnostics); - } - if (type === "self_hurt") { - diagnoseSourceOnlyFields(data, new Set(["name", "kind", "proximity"]), `${eventPath}.data`, diagnostics); - const name = stringField(data, "name")?.trim(); - const kind = stringField(data, "kind")?.trim(); - const proximity = stringField(data, "proximity")?.trim(); - if (!name && !kind) - return unsupportedObservationEvent(eventPath, diagnostics); - const subject = name ?? humanize(kind ?? "resident"); - const details = [kind, proximity].filter((item) => Boolean(item)); - return [ - `${subject} was hurt${details.length ? ` (${details.join(", ")})` : ""}.`, - ]; - } - if (type === "visible_entity_hurt") { - diagnoseSourceOnlyFields(data, new Set(["name", "kind", "proximity"]), `${eventPath}.data`, diagnostics); - const name = stringField(data, "name")?.trim(); - const kind = stringField(data, "kind")?.trim(); - const proximity = stringField(data, "proximity")?.trim(); - if (!name && !kind) - return unsupportedObservationEvent(eventPath, diagnostics); - const subject = name ?? humanize(kind ?? "entity"); - const details = [kind, proximity].filter((item) => Boolean(item)); - return [ - `${subject} was seen taking damage${details.length ? ` (${details.join(", ")})` : ""}.`, - ]; - } - if (type === "died") { - diagnoseSourceOnlyFields(data, new Set(), `${eventPath}.data`, diagnostics); - return ["The resident died."]; - } - if (type === "visible_block_changed") { - diagnoseSourceOnlyFields(data, new Set(["before", "after"]), `${eventPath}.data`, diagnostics); - const before = stringField(data, "before")?.trim(); - const after = stringField(data, "after")?.trim(); - return before && after - ? [`Visible block changed: ${humanize(before)} → ${humanize(after)}.`] - : unsupportedObservationEvent(eventPath, diagnostics); - } - if (type === "action_failed") { - diagnoseSourceOnlyFields(data, new Set([ - "intent", - "authorization", - "result", - "error", - "cancellation", - "failureKind", - ]), `${eventPath}.data`, diagnostics, new Set(["authorization", "result", "cancellation", "failureKind"])); - const intent = recordField(data, "intent"); - diagnoseIntentFields(intent, `${eventPath}.data.intent`, diagnostics); - const tool = stringField(intent, "tool")?.trim(); - const error = stringField(data, "error")?.trim(); - if (!error) - return unsupportedObservationEvent(eventPath, diagnostics); - return [ - `Action failed${tool ? `: ${humanize(tool)}` : ""} (${humanize(error)}).`, - ]; - } - if (type === "controller_suspended") { - diagnoseSourceOnlyFields(data, new Set(["reason", "activeIntent"]), `${eventPath}.data`, diagnostics); - const activeIntent = recordField(data, "activeIntent"); - diagnoseIntentFields(activeIntent, `${eventPath}.data.activeIntent`, diagnostics); - const reason = stringField(data, "reason")?.trim(); - const tool = stringField(activeIntent, "tool")?.trim(); - if (!reason) - return unsupportedObservationEvent(eventPath, diagnostics); - return [ - `Controller suspended (${humanize(reason)})${tool ? ` during ${humanize(tool)}` : ""}.`, - ]; - } - if (type === "cancellation_requested") { - diagnoseSourceOnlyFields(data, new Set(["intent", "requestedBy", "reason"]), `${eventPath}.data`, diagnostics); - const intent = recordField(data, "intent"); - diagnoseIntentFields(intent, `${eventPath}.data.intent`, diagnostics); - const requestedBy = recordField(data, "requestedBy"); - diagnoseSourceOnlyFields(requestedBy, new Set(["source", "tool"]), `${eventPath}.data.requestedBy`, diagnostics); - const tool = stringField(intent, "tool")?.trim(); - const requesterSource = stringField(requestedBy, "source")?.trim(); - const requesterTool = stringField(requestedBy, "tool")?.trim(); - const reason = stringField(data, "reason")?.trim(); - if (!reason) - return unsupportedObservationEvent(eventPath, diagnostics); - const requester = [requesterSource, requesterTool] - .filter((item) => Boolean(item)) - .map(humanize) - .join(" "); - return [ - `Cancellation requested${tool ? ` for ${humanize(tool)}` : ""}${requester ? ` by ${requester}` : ""} (${humanize(reason)}).`, - ]; - } - return unsupportedObservationEvent(eventPath, diagnostics); -} -function diagnoseIntentFields(intent, sourcePath, diagnostics) { - diagnoseSourceOnlyFields(intent, new Set(["source", "tool", "input", "observationSequence", "enqueuedAt"]), sourcePath, diagnostics, new Set(["source", "input", "observationSequence", "enqueuedAt"])); -} -function presentFocusedActionResult(actionName, result, paths, diagnostics) { - const shownResultKeys = new Set(); - const details = []; - const resultOk = typeof result.ok === "boolean" ? result.ok : null; - if (resultOk !== null) { - shownResultKeys.add("ok"); - paths.push("payload.payload.outcome.result.ok"); - } - const error = stringField(result, "error")?.trim(); - if (error) { - shownResultKeys.add("error"); - paths.push("payload.payload.outcome.result.error"); - details.push(`Body report: ${humanize(error)}.`); - } - if (actionName === "attack_focused_entity") { - const status = stringField(result, "status")?.trim(); - const confirmation = stringField(result, "confirmation")?.trim(); - if (resultOk === true && status && confirmation) { - shownResultKeys.add("status"); - shownResultKeys.add("confirmation"); - paths.push("payload.payload.outcome.result.status", "payload.payload.outcome.result.confirmation"); - details.push(`Body confirmation: ${humanize(status)} (${confirmation}).`); - } - } - else { - const changes = presentMaterialChanges(result, "changes", "Change evidence", paths, diagnostics); - const attempted = presentMaterialChanges(result, "attemptedChanges", "Attempted change", paths, diagnostics); - if (changes.length) - shownResultKeys.add("changes"); - if (attempted.length) - shownResultKeys.add("attemptedChanges"); - details.push(...changes, ...attempted); - } - return { - detail: details.length ? ` ${details.join(" ")}` : null, - shownResultKeys, - }; -} -function presentMaterialChanges(result, field, label, paths, diagnostics) { - const raw = result[field]; - if (raw === undefined) - return []; - if (!Array.isArray(raw)) { - diagnostics.push({ - code: "unsupported_outcome_change", - sourcePath: `payload.payload.outcome.result.${field}`, - }); - return []; - } - return raw.flatMap((item, index) => { - const change = recordValue(item); - const changePath = `payload.payload.outcome.result.${field}[${index}]`; - diagnoseSourceOnlyFields(change, new Set([ - "verb", - "position", - "before", - "after", - "verified", - "observed", - "confirmation", - "context", - ]), changePath, diagnostics, new Set(["position", "context"]), "source_only_outcome_field"); - const confirmation = recordField(change, "confirmation"); - diagnoseSourceOnlyFields(confirmation, new Set([ - "source", - "observedAt", - "dimension", - "position", - "before", - "after", - "beforeStateId", - "afterStateId", - ]), `${changePath}.confirmation`, diagnostics, new Set([ - "observedAt", - "dimension", - "position", - "before", - "after", - "beforeStateId", - "afterStateId", - ]), "source_only_outcome_field"); - const verb = stringField(change, "verb")?.trim(); - const before = stringField(change, "before")?.trim(); - const after = stringField(change, "after")?.trim(); - const verified = typeof change?.verified === "boolean" ? change.verified : null; - const observed = typeof change?.observed === "boolean" ? change.observed : null; - const confirmationSource = stringField(confirmation, "source")?.trim(); - if (!verb || !before || !after || verified === null || observed === null) { - diagnostics.push({ code: "unsupported_outcome_change", sourcePath: changePath }); - return []; - } - paths.push(`${changePath}.verb`, `${changePath}.before`, `${changePath}.after`, `${changePath}.verified`, `${changePath}.observed`); - if (confirmationSource) - paths.push(`${changePath}.confirmation.source`); - return [ - `${label}: ${humanize(verb)} ${humanize(before)} → ${humanize(after)}; verified ${verified ? "yes" : "no"}; observed ${observed ? "yes" : "no"}; confirmation ${confirmationSource ?? "none"}.`, - ]; - }); -} -function presentSoundHeard(data, eventPath, diagnostics) { - diagnoseSourceOnlyFields(data, new Set(["sound", "distanceBand", "relativeDirection", "volume", "pitch"]), `${eventPath}.data`, diagnostics, new Set(["volume", "pitch"])); - const sound = stringField(data, "sound")?.trim(); - const distanceBand = stringField(data, "distanceBand")?.trim(); - const relativeDirection = stringField(data, "relativeDirection")?.trim(); - if (!sound || !distanceBand || !relativeDirection) { - return unsupportedObservationEvent(eventPath, diagnostics); - } - return [ - `Heard ${sound} (${distanceBand}, ${relativeDirection}).`, - ]; -} -function diagnoseObservationEventEnvelope(event, eventPath, diagnostics) { - diagnoseSourceOnlyFields(event, new Set(["sequence", "type", "salience", "source", "isNew", "data"]), eventPath, diagnostics); -} -function presentSoundSequence(data, eventPath, diagnostics) { - diagnoseSourceOnlyFields(data, new Set([ - "compaction", - "fromSequence", - "throughSequence", - "omittedIndividualEvents", - "occurrences", - ]), `${eventPath}.data`, diagnostics); - if (stringField(data, "compaction") !== "behold.sound-sequence.v1") { - return unsupportedObservationEvent(eventPath, diagnostics); - } - const occurrences = arrayField(data, "occurrences"); - if (occurrences.length === 0) { - return unsupportedObservationEvent(eventPath, diagnostics); - } - let total = 0; - const descriptions = []; - for (let index = 0; index < occurrences.length; index += 1) { - const occurrence = recordValue(occurrences[index]); - const occurrencePath = `${eventPath}.data.occurrences[${index}]`; - diagnoseSourceOnlyFields(occurrence, new Set([ - "fromSequence", - "throughSequence", - "count", - "firstAt", - "lastAt", - "salience", - "data", - ]), occurrencePath, diagnostics); - const count = integerField(occurrence, "count"); - const sound = recordField(occurrence, "data"); - diagnoseSourceOnlyFields(sound, new Set(["sound", "distanceBand", "relativeDirection", "volume", "pitch"]), `${occurrencePath}.data`, diagnostics, new Set(["volume", "pitch"])); - const name = stringField(sound, "sound")?.trim(); - const distanceBand = stringField(sound, "distanceBand")?.trim(); - const relativeDirection = stringField(sound, "relativeDirection")?.trim(); - if (count === null || - count < 1 || - !name || - !distanceBand || - !relativeDirection) { - return unsupportedObservationEvent(eventPath, diagnostics); - } - total += count; - descriptions.push(`${count} × ${name} (${distanceBand}, ${relativeDirection})`); - } - return [`Heard ${total} sounds: ${descriptions.join("; ")}.`]; -} -function unsupportedObservationEvent(eventPath, diagnostics) { - diagnostics.push({ - code: "unsupported_observation_event", - sourcePath: eventPath, - }); - return []; -} -function diagnoseSourceOnlyFields(value, allowed, sourcePath, diagnostics, explicitlySourceOnly = new Set(), code = "source_only_observation_event_field") { - if (!value) - return; - for (const key of Object.keys(value)) { - if (!allowed.has(key) || explicitlySourceOnly.has(key)) { - diagnostics.push({ - code, - sourcePath: `${sourcePath}.${key}`, - }); - } - } -} -function outcomeTerminal(ok, eventType) { - if (ok === true) - return "succeeded"; - if (/reject/i.test(eventType)) - return "was rejected"; - if (/cancel/i.test(eventType)) - return "was cancelled"; - if (/provider.*fail|fail.*provider/i.test(eventType)) { - return "ended with a provider failure"; - } - return ok === false ? "failed" : "ended"; -} -function sectionText(section) { - const label = section.role === "perception" ? "PERCEPTION" : section.role.toUpperCase(); - return `${label}\n${section.text}`; -} -function presentInventoryItem(value) { - if (typeof value === "string") - return [humanize(value)]; - const item = recordValue(value); - const name = stringField(item, "name") ?? stringField(item, "kind"); - if (!name) - return []; - const count = integerField(item, "count"); - return [`${humanize(name)}${count === null ? "" : ` ×${count}`}`]; -} -function unique(values) { - return [...new Set(values)]; -} -function humanize(value) { - return value.replaceAll("_", " "); -} -function recordField(value, field) { - return recordValue(value?.[field]); -} -function recordValue(value) { - return value !== null && typeof value === "object" && !Array.isArray(value) - ? value - : null; -} -function stringField(value, field) { - return typeof value?.[field] === "string" ? value[field] : null; -} -function integerField(value, field) { - const item = value?.[field]; - return Number.isSafeInteger(item) ? item : null; -} -function numberField(value, field) { - const item = value?.[field]; - return typeof item === "number" && Number.isFinite(item) ? item : null; -} -function arrayField(value, field) { - const item = value?.[field]; - return Array.isArray(item) ? item : []; -} -function stringArray(value) { - return Array.isArray(value) - ? value.filter((item) => typeof item === "string") - : []; -}