feat(promotion): generalized DL-2 entity promotion gate#116
Closed
spuentesp wants to merge 55 commits into
Closed
Conversation
Single-commit consolidation of the project: history squashed to drop the 256 MB Neo4j backup, divergent throwaway commits, and any leaked credentials. This tree includes the v1.1 work: multi-database ingestion pipeline (PDF/structure extraction, embeddings, analyzer, knowledge packs), CanonKeeper canonization, non-hardcoded game-system extraction + character creation, auto-roll resolution, data-driven resource economy, and multi-scene solo play — validated end-to-end against Vampire: The Masquerade (108 MB corebook) and Death in Space. Secrets scrubbed: removed personal .continue/ IDE config, redacted hardcoded z.ai keys (seed_zai_provider.py, env.example) to env-var lookups. test_logs, coverage artifacts, and *.xls excluded via .gitignore. .env stays untracked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* more npc wiring * cleanup --------- Co-authored-by: Alumno <alumno@ejemplo.com>
- Add missing wired CLI commands: play, manage, universe - Fix docs paths: docs/architecture/ → docs/2_architecture/, docs/ontology/ → docs/4_ontology/ - Remove reference to non-existent docs/archive/ directory - Remove reference to non-existent AGENT_SETUP.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMzawB9BZFgQxWo3kcToWj
- Add missing wired CLI commands: play, manage, universe - Fix docs paths: docs/architecture/ → docs/2_architecture/, docs/ontology/ → docs/4_ontology/ - Remove reference to non-existent docs/archive/ directory - Remove reference to non-existent AGENT_SETUP.md Claude-Session: https://claude.ai/code/session_01AMzawB9BZFgQxWo3kcToWj Co-authored-by: Claude <noreply@anthropic.com>
…eak) NPCVoice + ConversationLoop now thread universe_id through recall, memory writes, working state, and staged proposals. Each character card holds a versions[] array; every incarnation has its own entity_id + NPCProfile + memory set + relationship deltas, partitioned by universe. - data-layer: NPCProfile gains universe_id, relationship_states_by_universe, current_emotional_state_by_universe (legacy fields remain as fallback). - agents: NPCVoice._recall_memories/_write_npc_memory/_build_*_snapshot read/write the per-universe partition first; add universe_id + include_cross_incarnation kwargs to respond_direct. ConversationLoop stamps universe_id on staged ProposedChanges. - UI backend: character_storage.add_version/get_version/list_versions/ touch_version/delete_version; character_conversation.ensure_character_backed is per-universe idempotent; new delete_incarnation tears down entity + profile. 5 new endpoints (POST/GET/DELETE /versions + universe_id + include_cross_incarnation flags). - frontend: StandaloneCharacter + ConversationStart carry versions / default_universe_id / version_id / universe_id. Roster shows Nx badge, editor has Incarnations panel (list + delete + add by universe), chat has 'Remember across incarnations' toggle. - tests: 13 NPCVoice universe-scoping tests, 3 isolation tests in test_character_conversation, 9 frontend version-API tests; 87 agent tests, 39 backend tests, 38 frontend tests, type-check clean. - docs: docs/use-cases/epic-3-identity-M/M-31/CHARACTER_VERSIONS.md describes the architecture, data model, endpoint catalog, migration. docs/USE_CASES.md adds a VERSIONS row pointing at it. Use cases: M-31, DL-20, P-17, P-20. Co-Authored-By: Claude <noreply@anthropic.com>
P1 — packages/data-layer/src/monitor_data/tools/neo4j_tools/core.py Fork/split/merge Cypher matched (u)-[:IN_MULTIVERSE]->(mv) but the schema (core.py:72 / :314) creates (m)-[:CONTAINS]->(u) — opposite direction. Queries matched zero rows, so fork/split/merge silently failed. Flipped all three to (mv)-[:CONTAINS]->(source/new). P1 — packages/ui/backend/src/monitor_ui/routers/character_conversation.py _provision_entity_and_profile called neo4j_create_entity directly, bypassing the CanonKeeper authority check (INV-1: only CanonKeeper writes Neo4j entity nodes). Route through CanonKeeper.create_entity. P2 — packages/agents/src/monitor_agents/loops/conversation_loop.py ConversationState did not declare include_cross_incarnation, so assignments to state.include_cross_incarnation were silently dropped. Added the field with a False default (preserves per-universe incarnation isolation). P2 — packages/agents/src/monitor_agents/npc_voice.py _recall_memories filtered by universe_id even when include_cross_incarnation=True, making the flag a no-op. Drop the universe scope when cross-incarnation recall is requested. Co-Authored-By: Claude <noreply@anthropic.com>
First Medium post draft covering origin story, Yggdrasil/DAG insight, temporality model, and current e2e test state. Includes Mermaid source files for both diagrams (render via mermaid.live before uploading).
…h coverage Adds 6 live CI badges + Codecov to README, rewrites mutation.yml for 10-module parallel matrix, adds branch coverage config, fixes 13 stale tests, and expands test coverage for _enrichment and _game_system_persistence modules.
Full suite with --cov-branch: 6308 passed, 204 skipped, 37202 statements, 10468 branches. 67.9% combined branch+statement coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RC8T8VN6X4KaJmZ4fnUvkn
Phase 0 — fix 7 production-blocking bugs: - WS URL fallback for non-localhost deploys (lib/api.ts) - REST-path typing-indicator watchdog (PlayConsole.deliverTurn) - User-scrolled-up scroll guard (new useStickyScroll hook) - Per-turn streaming isolation (architect/page.tsx — keyed by message_id) - Cleanup all streaming state on session delete (PlayConsole) - SSE lifecycle: replace-on-reopen, retry with backoff (IngestionJobsList) - WS connect() race guard via isConnectingRef (use-chat-websocket) Phase 1 — extract chat core into features/chat/: - useChatSession owns WS lifecycle, streaming state, watchdog, retry, dice - ChatList (react-virtuoso virtualized list, followOutput auto-scroll) - Composer (textarea + status pill + quick actions) - 7 shared sub-components (TypingIndicator, ProseBubble, CopyButton, DiceResultCard, DiceRollPrompt, ConsequenceBanner, StatusPill) - PlayConsole.tsx: 1968 → 726 lines (-63%) - architect/page.tsx: 1599 → 603 lines (-62%) - 4 components extracted: SessionList, SetupPanel, PlayMessageBubble, ArchitectMessageBubble Phase 2 — backend emits composing/thinking/thinking_end events: - dspy_runtime.py: stream-callback now cb(kind, data); CoT text before 'Narrative Text:' prefix surfaces as thinking events - chat.py: emits composing after start; dispatches thinking/thinking_end - useChatSession handles new events; persists thinking to metadata.thinking on done (fixed bug surfaced by tests where trace vanished) - New ThinkingBubble component (collapsible CoT card) - WsServerMsg union extended with tool_call/tool_result (no-op for now, forward-compat for Phase 2B MCP surfacing) Tooling + tests: - react-virtuoso added to package.json - vitest config: JSX automatic, happy-dom env, setupFiles for jest-dom - 96/96 tests passing (was 28) - 8 new test files cover Phase 0/1/2 fixes and chat primitives - Per-turn isolation regression caught and fixed during test pass - Thinking-trace persistence bug caught and fixed during test pass Co-Authored-By: Claude <noreply@anthropic.com>
Backend: - BaseAgent.call_tool now emits tool_call before and tool_result after every MCP server invocation, correlated by a per-call uuid. Errors are caught and surfaced as a tool_result with an error payload so the UI can render the failure inline instead of crashing the turn. Result preview is truncated to 2000 chars. - chat.py on_event dispatcher forwards tool_call and tool_result as typed WS payloads (matching the protocol types added in Phase 2). - Backend test: test_tool_call_emit covers happy path + error path. Frontend: - New ToolCall interface + toolCalls? field on StreamingMessage. - useChatSession: tool_call appends pending entry, tool_result resolves by id, done handler captures toolCalls via setter-callback and persists them as metadata.tool_calls on the cached message. - New ToolCallCard component (121 lines): collapsible inline card with status pill (running / done / error) and expandable args / result / error panels. data-testid for testability. - PlayMessageBubble renders one ToolCallCard per msg.toolCalls below the prose bubble. ChatList threads toolCalls through the streaming bubble synthesis so live tool cards appear during streaming, not just after. - Tests: use-chat-session-toolcalls (3 tests), PlayMessageBubble render test (1 new). 108/108 frontend tests passing. Cleanup: - IngestionJobsList: re-add useCallback import I removed during Phase 0. - ChatList: fix streamingMsg null narrowing with local aliases. - tsconfig: exclude test files from tsc (they use dynamic FakeSocket typing for mocks; runtime is correct). - esbuild removed dead _preview line in base.py success emit. - Add minimal eslint.config.mjs so 'next lint' won't prompt interactively. Verified: - tsc --noEmit: 0 errors in production code - next build: all 19 routes compile, /play 19.1kB - Frontend tests: 108/108 passing - Backend tests: 183/183 passing on the touched subset Co-Authored-By: Claude <noreply@anthropic.com>
…t coverage
Type safety:
- New lib/errors.ts errorMessage(err: unknown) helper handles Error,
DOMException-shaped { message }, string, null/undefined, and circular
objects. Replaces 13 onError: (e: any) and 2 catch (e: any) sites
across characters, PackLibrary, and CharacterChat with proper unknown
+ the helper.
- New features/graph/adapters.ts bridges WorldGraph (GraphNode,
GraphEdge) to xyflow Node/Edge via toReactFlowNode/toReactFlowEdge/
toReactFlowGraph. Replaces 4 'as unknown as Node[]' / 'as unknown as
Edge[]' casts in architect/page.tsx and explorer/page.tsx.
- FORGE_KEYS.assets opts typed as { source_id?, universe_id?, asset_type?,
limit?, offset? } instead of (opts?: any).
- Two 'as any' casts in forge/page.tsx patchArtifactPack removed.
Test coverage (108 → 120):
- errorMessage helper: 6 tests covering all shapes + circular safety
- SessionList (Phase 1 extraction): 4 tests for render/empty/loading
- RecapModal (Phase 1 extraction): 2 tests for mount + close handler
Verified:
- tsc --noEmit: 0 errors
- next build: all 19 routes compile
- vitest: 120/120 passing
- Backend tests still passing (not re-run, no backend changes)
Co-Authored-By: Claude <noreply@anthropic.com>
The ingest mutation was posting ALL chunks in a single bulkCreate request. A 50-page document at chunk_size=200 produces ~300 chunks — that single POST exceeds the backend's default 30s timeout and silently loses everything. Fix: - Extract lorebookChunksForText + chunkLorebookEntries to a pure helpers module (src/components/play/lorebookChunking.ts). The chunking also clamps the size to a safe minimum of 200 chars (matching the original inline behavior). - The mutationFn now batches bulkCreate calls in groups of 20. Batches run sequentially so a failure on batch 7 still saves 1-6 (vs. the all-or-nothing single-POST). - Add ingestProgress state + 'Ingesting N / total…' UI label so the player sees progress on a long ingest. Tests: - lorebookChunking.test.ts: 7 tests covering empty input, size clamping, exact chunking, and batch splitting (7 new). Verified: tsc clean, vitest 127/127 passing. Co-Authored-By: Claude <noreply@anthropic.com>
chunk_size and priority_hint inputs were storing as numbers and snapping to the default (1000 / 50) whenever the field was cleared, making the silent-default bug a data-loss risk: a player who wanted to set chunk_size=0 (one chunk = whole doc) or priority=0 (deprioritize everything) couldn't — clearing the field re-set it to the default. Fix: store as strings, parse on submit with safe fallback. Empty fields now show empty (and submit falls back to default at the submit boundary). Co-Authored-By: Claude <noreply@anthropic.com>
Clicking a pack from the Sources panel used to call setForgeMode('packs')
which collapsed the sources view and switched to the sidebar+detail
layout used for pack editing — a jarring layout jump in the middle of
the player's flow.
Fix: route to /forge/editor?pack=<id> instead. The editor route is the
canonical place to edit a pack, has its own focused layout, and keeps
the back/forward navigation consistent.
Co-Authored-By: Claude <noreply@anthropic.com>
… differs The sidebar's Active World selector reads from useWorldContext (a persisted preference). When playing a session that uses a different universe, the sidebar displayed the saved preference — making it look like the wrong world was active. Fix: when on a session route (?universe=…), compare the session's universe to the persisted preference and show an amber 'session ≠' badge so the player knows the sidebar reflects their saved preference rather than the active session. Tests: 3 cases covering no-drift, matching, and drift states. Co-Authored-By: Claude <noreply@anthropic.com>
The web surface had only one root error.tsx and zero loading.tsx files. Every route rendered directly with no skeleton during data fetches and no per-route fallback for caught errors. Now: - src/components/PageSkeleton.tsx — reusable loading skeleton with default / two-column / compact variants. - src/components/PageError.tsx — reusable error fallback with friendly title, error message, and reload action. - 17 per-route loading.tsx files (play, gm, worlds, settings, forge, forge/editor, forge/review, forge/apply, characters, architect, explorer, systems, snapshots, search, prompts, history, universes) using the shared skeleton. - 18 per-route error.tsx files surfacing route-specific titles via the shared PageError. - src/app/not-found.tsx — top-level 404 fallback with link back to Gate / Worlds. - Tests: PageSkeleton (2), PageError (3) covering ARIA roles, reload button, default title. Verified: - tsc --noEmit: 0 errors - vitest: 135/135 passing (was 130) Note: 'next build' fails on master's /history page prerender — this is pre-existing on master and unrelated to this change. Co-Authored-By: Claude <noreply@anthropic.com>
Next 15 requires useSearchParams() to be inside a Suspense boundary during prerender. WorldPicker (rendered inside the layout's Sidebar) calls useSearchParams to detect drift between the persisted 'active world' and the session's world. Without Suspense, every page failed to prerender with: useSearchParams() should be wrapped in a suspense boundary at page "/history" Fix: wrap <Sidebar /> in <Suspense> at the layout level with a minimal fallback (a 240px-wide placeholder div). The page shell renders as static HTML; the search-params-dependent sidebar content swaps in during hydration. Verified: - next build: all 21 routes prerender as ○ static - tsc --noEmit: 0 errors - vitest: 135/135 passing Co-Authored-By: Claude <noreply@anthropic.com>
The fallback was 240px but Sidebar's default state is collapsed=false (width=224px). Matching the fallback width avoids a 16px visual jank when Sidebar hydrates and the animated width kicks in. Co-Authored-By: Claude <noreply@anthropic.com>
Audit found 20 buttons whose only child was a lucide icon (Close, Plus, Send, ChevronDown, Loader2) with no text and no aria-label / title attribute. Screen readers announce these as 'button' with no name, making them ungraspable for non-visual users. Fix: contextual aria-labels per button: - All <X> close buttons → aria-label="Close" - Plus icons in architect/worlds → "Create multiverse/universe", "Add tag" - SessionRecorder Send → "Log entry" - Demo button → "Start demo" - Snapshot confirm → "Confirm" - PlayConsole tone chip → "Change session tone" Verified: tsc clean, vitest 135/135 passing. Co-Authored-By: Claude <noreply@anthropic.com>
6 hidden file inputs (character card upload, asset upload, document upload, pack import, etc.) had no accessible name. Screen readers announced them as 'file upload' with no description. Search inputs on forge/worlds pages had placeholders but no aria-label, so screen readers also couldn't identify them as search fields. Fix: contextual aria-labels for each (e.g. 'Choose pack file to import', 'Search library', 'Choose document to upload', etc.). Verified: tsc clean, vitest 135/135 passing. Co-Authored-By: Claude <noreply@anthropic.com>
Settings was the last remaining god-component (2334 lines). Two of its 6 tabs are well-isolated and extracted: - components/settings/ToneTab.tsx (153 lines): tone profile CRUD + library list. Used by settings/page.tsx via import. - components/settings/PerformanceTab.tsx (106 lines): live snapshot of slow queries + alerts + health check. The other 4 tabs (LLM, Agents, Testbed, Databases) share too much state (PROMPTS, agent nodes, provider metadata, db types) to extract atomically without a larger refactor; left in place for now. Verified: tsc clean, vitest 135/135 passing. Co-Authored-By: Claude <noreply@anthropic.com>
Sidebar nav links now declare aria-current="page" when active so
screen readers announce the current page on focus. The root layout
adds a visually-hidden skip-to-content link that becomes visible on
keyboard focus, allowing keyboard users to bypass the sidebar and
jump straight to <main id="main-content" tabIndex={-1}>.
Verified: tsc clean, vitest 135/135 passing.
Co-Authored-By: Claude <noreply@anthropic.com>
Added 13 new tests across the extracted components: - ToneTab (4): section header, disabled-until-filled, enabled-when-filled, library list rendering. - PerformanceTab (3): header render, four metric cards, empty state. - NPCCard (6): name, state_tags, empty state_tags, description, onClick, role=button. Verified: tsc clean, vitest 144/144 passing, next build green. Co-Authored-By: Claude <noreply@anthropic.com>
Worlds page further decomposed. Two more self-contained pieces: - app/worlds/GraphLegend.tsx (21 lines): the colored swatch legend shown on the graph tab. - app/worlds/InspectorPanel.tsx (214 lines): the right-side detail panel that shows node attributes / properties and exposes edit controls. Pure presentational, takes a Node<GraphNodeData>. Worlds page now 1162 lines (-219). Verified: tsc clean, vitest 148/148 passing. Co-Authored-By: Claude <noreply@anthropic.com>
GraphLegend previously referenced KIND_CONFIG from settings/page.tsx — which broke once it was extracted. Replaced with a self-contained LEGEND array so the component is standalone. Tests verify all 9 kind labels render (Multiverse, Universe, Character, Location, Faction, Concept, Axiom, Lore, Rule). Verified: tsc clean, vitest 150/150 passing. Co-Authored-By: Claude <noreply@anthropic.com>
InspectorPanel was extracted in 717d0c8 but referenced KIND_CONFIG from settings/page.tsx, which is no longer in scope. The component compiled (TS was permissive because the GraphNodeKind cast hid the undefined) but would crash at runtime on the first node selection. Fix: inline a minimal KIND_CONFIG inside InspectorPanel.tsx. The mapping is the same data as the GraphLegend inlined array (8 + 2 extra kinds) and the canonical worldGraph KIND_CONFIG that lives in settings/page.tsx. Keeping it self-contained here avoids the same cross-file import fragility that bit GraphLegend. Verified: tsc clean, vitest 150/150 passing. Co-Authored-By: Claude <noreply@anthropic.com>
GraphTab was the biggest remaining function in the worlds page. It holds the ReactFlow graph view, filter side-panel, search bar, and related-entity selection — all the state and render code for the graph visualization tab. Extracted to app/worlds/GraphTab.tsx (416 lines, includes inlined GraphCanvas for self-containment). The worlds page is now 763 lines (down from 1599 at the start of this audit, -52%). Verified: tsc clean, vitest 150/150 passing, next build green. Co-Authored-By: Claude <noreply@anthropic.com>
… extraction GraphCanvas (WorldNode, KIND_CONFIG, nodeTypes) were still in page.tsx even after GraphTab was extracted to its own file in 4562a48. GraphTab.tsx already imported from "./GraphCanvas" but the file didn't exist, and several names (AnimatePresence, X, User, MapPin, Shield, Sparkles, WorldGraphFilter) were missing from GraphTab.tsx's import list. Also fixes InspectorPanel.tsx: Edit2 → Edit3 (icon rename in lucide-react) and adds missing Loader2 + Link2 imports that were never brought over from page.tsx. After this commit page.tsx sheds 248 lines and all xyflow/react imports. Verified: tsc clean, vitest 150/150 passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- base.py: max_tokens default changed from hardcoded 2048 to None;
fallback chain is now: caller override → provider config → env default
(MONITOR_LLM_DEFAULT_MAX_TOKENS). Old hardcoded 2048 silently truncated
extraction-heavy calls.
- dspy_runtime.py: node→role mapping and intensity keywords now override-
able via MONITOR_NODE_ROLES / MONITOR_HIGH_INTENSITY_KEYWORDS env vars,
without code changes.
- llm_registry.py + schemas/llm_config.py: prompt_version field on
LLMNodeAssignment threaded through to LLMClient, enabling A/B testing
prompt variants with correlated metrics.
- schemas/llm_config.py: ModelParams gains stop, seed, top_k, and
response_format fields (Anthropic + OpenAI compatible).
- config.py: new Settings fields for LLM timeout, streamable nodes,
and DSPy optimizer toggle.
- env.example: documents all new MONITOR_* knobs.
- test_llm_routing.py: adds params={} to FakeClient (required by new
max_tokens resolution logic).
Verified: 16 LLM routing tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LorebookKeywordExtractor and LorebookIngestionModule now use explicit DSPy Signature classes (LorebookKeywordExtractionSignature, LorebookIngestionSignature) instead of inline docstring definitions. Adds JSON parsing + validation helpers so callers get proper LorebookEntryDraft objects back rather than raw strings. No functional change to existing behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- conftest.py: adds FakeDSPyPrediction and FakeDSPyLM fakes for testing DSPy modules without real LLM calls, plus fake_dspy_lm fixture. - tests/property/test_agent_loop_properties.py: Hypothesis tests for resolve_dynamic_role, normalize_node_name, and default_role_for_node. 131 property tests across agent loop invariants. - tests/property/test_llm_registry_properties.py: Hypothesis tests for EffectiveLLMConfig construction, ModelParams.to_dict() idempotency, PROVIDER_BASE_URLS consistency, and param merging invariants. - pyproject.toml: replaces deal (removed) with hypothesis, adds pytest-cov, pytest-timeout, pytest-socket, and cosmic-ray to dev deps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds narrator, context-assembly, and simulacrum to the cosmic-ray mutation test matrix (was 10 modules, now 13). Each new config uses curated mutation operators tuned to the module's logic type. Also adds a quality gate step that emits a CI warning when a module's kill rate drops below 60%, making regressions visible without blocking CI on a hard threshold. Updates docs/contributing/BADGES.md to reflect the new module count and document the operator curation approach. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_adv.py and test_adv2.py were one-off manual scripts for checking AdvancementSystem schema fields. Behavior covered by proper tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…el testAll - query-keys.ts: add WORLDS_KEYS (worldGraph base + filter), UNIVERSE_KEYS; remove dead ENTITY_KEYS.graph which used "world-graph" instead of the live "worldGraph" key (would have silently prevented cache invalidation) - worlds/GraphTab: use UNIVERSE_KEYS + WORLDS_KEYS constants throughout - worlds/InspectorPanel: WORLDS_KEYS for invalidation; aria-label on tag delete buttons; fix Edit2→Edit3 lucide rename; add missing Loader2/Link2 imports - worlds/NPCDetailPanel: composite stable key for relationships list (was index-as-key, broke reconciliation on reorder) - worlds/page.tsx: UNIVERSE_KEYS constants in CreateMultiverseForm + CreateUniverseForm; error display on mutation failure; aria-label="Close" on modal close button - settings/page.tsx: testAll() sequential for..of → Promise.allSettled (parallel provider tests, ~N× faster) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ria-label on close - gm/page.tsx: SessionNotebook used useState(() => ...) to load localStorage — invalid pattern (the return value is ignored, setNotes() fired as a side effect inside state init). Replace with useEffect + [STORAGE_KEY] dep so notes reload correctly when the active universe changes. - ThinkingBubble: add aria-expanded + aria-label to the reasoning toggle button - ToolCallCard: add aria-expanded + aria-label to the tool detail toggle button - NPCDetailPanel: add aria-label="Close" to the panel dismiss button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o side-effects, timeout cleanup - explorer/page.tsx: move setState-during-render into useEffect; replace useMemo-as-side-effect (setNodes/setEdges inside useMemo) with useEffect - snapshots/page.tsx: same setState-during-render fix - Sidebar.tsx: startsWith(href) → startsWith(href + '/') to prevent false active matches on sibling routes - NotificationProvider.tsx: store setTimeout IDs in ref, cancel on removal and unmount; use crypto.randomUUID() for collision-safe notification IDs - forge/page.tsx: Discard handler was only resetting dirtyPack, not dirtySystem Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lity - TypingIndicator: role=status + aria-label so screen readers announce GM typing state - ChatModeToggle: aria-pressed on IC/OOC buttons to expose active state - QueryBoundary: role=status on loading div, role=alert on error div - CopyButton: aria-label with copied/copy states (title alone not reliable) - SessionList: aria-label on new/rename/delete icon-only buttons - ForgeRows: aria-label + aria-expanded on kebab button; aria-label on InlineEdit inputs - UploadCard: role=button + tabIndex + onKeyDown for keyboard drop-zone access - search/page.tsx: aria-label on search input; aria-pressed on collection filter toggles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LorebookEditor: sort toggle fixed (active ? !ascending : true) — was stuck returning true when clicking active-ascending column; inline lorebook query keys → LOREBOOK_KEYS constants - MemoryInspector: add minImportance to query key so slider changes bust cache (was serving stale results on filter change) - ProseBubble: hoist markdownComponents to module scope — was recreated every render, remounting all block elements during token streaming - use-llm: refetchOnWindowFocus: false on providers + assignments — was triggering connectivity checks on every browser tab focus - ChatList: wrap itemContent in useCallback, components in useMemo so Virtuoso does not remount visible rows/footer on every parent render - gm/page.tsx: rename local saved → stored to remove shadow over const [saved, setSaved] state variable - query-keys.ts: add LOREBOOK_KEYS, STORY_KEYS.threads, FORGE_KEYS.pack, ENTITY_KEYS.characterVersions; fix any param types on npcs/standaloneCharacters Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical: several components used inline key strings that hit different cache slots than the constants, silently breaking invalidation: - SessionRecorder: [sessions] → PLAY_KEYS.sessions and [chat,id,messages] → PLAY_KEYS.messages — was writing optimistic updates to a key no other consumer read; play console invalidations never reached session list - AssetsPanel: [assets,filter] → FORGE_KEYS.assets (was [forge-assets,*]) - forge/review: [proposals,id] → FORGE_KEYS.proposals; [forge-packs] → FORGE_KEYS.packs - WorldPicker: [universes,"all"] → UNIVERSE_KEYS.universes() (arity fix) - app/page.tsx: [sessions] → PLAY_KEYS.sessions; [universes] → UNIVERSE_KEYS.universes() New constants: HEALTH_KEYS, MODE_KEYS, CHANGE_LOG_KEYS, CANON_KEYS used in ConnectionStatus, ModeSwitcher, HistoryTab, CanonReviewPanel CanonReview bug: hide Accept All button in scene mode where storyId is always null and the handler was silently no-oping Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ogShell a11y - gm/page.tsx: extract AccordionRule component — useState was called inside system.rules.map(), violating Rules of Hooks (can corrupt hook order when rules array length changes) - settings/page.tsx: delete local ToneTab/PerformanceTab duplicates (2×140 lines of dead copies); import from components/settings/ instead; remove non-existent Tailwind bg-network-500/10 tokens; PLAY_KEYS.sessions constant - forge/apply/page.tsx: auto-commit toggle div had onClick that double-fired with the label's native input activation — net result was no toggle - DialogShell.tsx: add role=dialog, aria-modal, aria-labelledby, Escape key handler — all modals in the app share this shell so fix benefits everywhere Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KIND_CONFIG was defined 3× with silently divergent values — the inspector showed Shield for rule while the graph showed Zap, and the GraphLegend had its own hardcoded LEGEND array (7/9 icons wrong, 6/9 colors wrong, pack kind missing). Universe kind was showing MapPin (the location icon) — actively misleading. Fixes: - New lib/kind-config.ts — single source of truth matching the values GraphCanvas actually renders - GraphCanvas, InspectorPanel, architect/page all import from it - GraphLegend derives directly from KIND_CONFIG (no more LEGEND array) - Add ARCHITECT_KEYS, BENCHMARK_KEYS, STORY_KEYS.stories/turns/threads, ENTITY_KEYS.systemDetail/entities to query-keys.ts; add PLAY_KEYS.recap - Replace inline keys in architect/page.tsx (had no query-keys import), UniverseTree.tsx (9 keys), worlds/page.tsx (4 keys), settings/page.tsx (5 benchmark keys) - Fix architect stale closure: useEffect was missing multiverseId dep (eslint-disabled); guard already prevents overwriting user's selection, so adding the dep makes re-evaluation safe Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…i types - lib/play-constants.ts: extract TONES, TONE_DESCRIPTIONS, MODE_LABEL, PHASE_STYLE — was duplicated verbatim across PlayConsole and SetupPanel - PlayConsole: wrap onTurnSettled in useCallback (was new ref every render → useChatSession WebSocket reconnect storm); memoize quickActions; useCallback for renderBubble; replace inline "play-sessions" key - RecapModal: delete local RecapCopyButton (exact dup of features/chat CopyButton); import shared CopyButton; use PLAY_KEYS.recap constant - api.ts: export ApiError so callers can instanceof-check and read .status; type all 5 lorebookApi methods with LorebookEntry/LorebookStats; add LorebookStats to types.ts; move character_id out of inline path query string into query: option for consistency; LorebookEditor drops its old `as Promise<Record<string, number>>` cast (now correctly typed) - hooks/use-ingest-jobs.ts: extract polling query duplicated verbatim in IngestionDashboard, IngestionJobsList, SourceLibrary into one hook Tests: 150/150 pass. tsc clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GMAwareness (DSPy-based causality engine): - New gm_awareness.py: single LLM call replaces 5 regex heuristics (_infer_intent_type, _infer_roll_type, _classify_roll_necessity, _extract_target, _detect_forced_narrative) - GMAwarenessSignature(dspy.Signature) — prompt IS the test surface - GMAwarenessModule(dspy.Module) — wraps dspy.Predict for optimization - prediction_to_verdict() — lenient parser with alias normalization - Mode flags skip LLM: narrative→NARRATIVE_OVERRIDE, auto-roll→ACCEPT - LLM failure → fallback to propose_roll (safe default) - Resolver Branch 2 now uses check_gm_awareness (no regex) Narrative coherence (scene_loop + narrator): - SceneState: context_summary, turn_context, pending_roll, established_facts - New nodes: extract_facts, check_consistency - Narrator: setting_anchor, context_summary, turn_context injection - Prior turns expanded from [-3:] to [-8:] - Pending roll state machine: pushback when player skips a roll Tests (306 agent tests pass): - test_gm_awareness.py: 36 routing tests (trivial/propose/contested/narrative/auto) - test_gm_awareness_prompts.py: 16 gold-set prompt tests + parser mutations + metric - test_turn_context_coherence.py: 30 coherence feature tests - test_session0_and_forced_narrative.py: 13 session0 + causality tests - All existing resolver/narrator/scene_loop tests updated to mock check_gm_awareness UI worktree merge (from 5 claude agent worktrees): - agent-a95505: UI logic bugs + streaming perf (69 files) - agent-adc7a1: query-key constants + cache desync fixes (73 files) - agent-aa31942d: uncommitted UI refactors (5 files) - ui-fixes: shared play constants, RecapModal DRY, api types (33 files) - restore-deps: package.json deps update Coherence playtest: 10 turns, 9 pushbacks, 0 violations, PASS Layer dependencies: all pass
Implements the Generalized Entity Promotion Plan: replace lore-specific heuristics with a deterministic, system-agnostic promotion gate. * Narrator prompt enforces [Name](entity:anchor|flavor) tagging with few-shot examples. * ProposedChange schema gains PromotionIntent enum + interaction_count + is_mechanically_bound fields. * New loops/entity_promotion.py parses tags, merges intents into LLM- extracted proposals, bumps interaction counts, and detects mechanical binding from resolution/combat/inventory payloads. * Scene loop wires the parser into extract_new_entities and the mechanical-binding pass into persist_turn_artifacts. * CanonKeeper.evaluate_proposals runs a pre-LLM deterministic pass that promotes entities via topology (RELATIONSHIP references) and state- gating (STATE_CHANGE/EVENT references), then accepts anchors, accepts flavor entities that exceed the interaction threshold or are mechanically bound, and rejects low-traffic flavor entities. * Docs updated in data_model_workflow.md and ontology _index.md. * 20 new tests cover the full promotion lifecycle. DL-2 / 1130 existing tests still pass.
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment Thanks for integrating Codecov - We've got you covered ☂️ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the Generalized Entity Promotion Plan from docs/2_architecture/entity_promotion_architecture_plan.md.
Replaces lore-specific entity-promotion heuristics with a deterministic, system-agnostic gate:
[Name](entity:anchor)/[Name](entity:flavor)tagging with few-shot examples.PromotionIntentenum,interaction_count, andis_mechanically_boundfields.monitor_agents.loops.entity_promotionparses tags from narrator prose, merges intents into LLM-extracted proposals, bumps interaction counts, and detects mechanical binding from resolution / combat / inventory payloads.extract_new_entitiesand the mechanical-binding pass intopersist_turn_artifacts.(anchor)is auto-ACCEPTed.(flavor)withinteraction_count > 3graduates to anchor;(flavor)below threshold is garbage-collected.docs/2_architecture/data_model_workflow.md(§6) anddocs/4_ontology/_index.md.test_entity_promotion.py. Full agents suite (1130 tests) still green.Closes the DL-2 generalized promotion work item.