uro-core 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uro_core/__init__.py +8 -0
- uro_core/_distill_core.py +256 -0
- uro_core/adapters/__init__.py +1 -0
- uro_core/adapters/postgres/__init__.py +1 -0
- uro_core/adapters/postgres/migrations/.gitkeep +0 -0
- uro_core/adapters/postgres/migrations/001_init.sql +46 -0
- uro_core/adapters/postgres/migrations/002_llm_calls.sql +16 -0
- uro_core/adapters/postgres/migrations/003_projections.sql +35 -0
- uro_core/adapters/postgres/migrations/004_embeddings.sql +23 -0
- uro_core/adapters/postgres/migrations/005_timeline_branching.sql +56 -0
- uro_core/adapters/postgres/migrations/006_branch_name_unique.sql +9 -0
- uro_core/adapters/postgres/migrations/007_pc_binding.sql +14 -0
- uro_core/adapters/postgres/migrations/008_sheets.sql +13 -0
- uro_core/adapters/postgres/migrations/009_items.sql +13 -0
- uro_core/adapters/postgres/migrations/010_factions_edges.sql +27 -0
- uro_core/adapters/postgres/migrations/011_threads.sql +11 -0
- uro_core/adapters/postgres/migrations/012_actor_status.sql +5 -0
- uro_core/adapters/postgres/migrations/013_campaign_ruleset.sql +6 -0
- uro_core/adapters/postgres/migrations/014_memory_index_commit_idx.sql +7 -0
- uro_core/adapters/postgres/migrations/015_counters.sql +16 -0
- uro_core/adapters/postgres/migrations/016_campaign_seed.sql +12 -0
- uro_core/adapters/postgres/migrations/017_participant_notes.sql +21 -0
- uro_core/adapters/postgres/migrations/018_session_tokens.sql +19 -0
- uro_core/adapters/postgres/migrations/019_claim_created_day.sql +6 -0
- uro_core/adapters/postgres/projector.py +409 -0
- uro_core/adapters/postgres/store.py +1759 -0
- uro_core/authored.py +48 -0
- uro_core/chronicler.py +86 -0
- uro_core/domain/__init__.py +1 -0
- uro_core/domain/events.py +992 -0
- uro_core/domain/hashing.py +20 -0
- uro_core/domain/ids.py +8 -0
- uro_core/engines/__init__.py +1 -0
- uro_core/engines/actor.py +84 -0
- uro_core/engines/history.py +96 -0
- uro_core/engines/probe.py +149 -0
- uro_core/engines/rules.py +279 -0
- uro_core/engines/rules_gauntlet.py +420 -0
- uro_core/errors.py +39 -0
- uro_core/export.py +146 -0
- uro_core/memory/__init__.py +1 -0
- uro_core/metering.py +24 -0
- uro_core/pipeline/__init__.py +1 -0
- uro_core/pipeline/encounter.py +90 -0
- uro_core/pipeline/engine.py +852 -0
- uro_core/pipeline/extraction.py +208 -0
- uro_core/pipeline/mechanics.py +51 -0
- uro_core/pipeline/plan.py +192 -0
- uro_core/pipeline/prompts.py +48 -0
- uro_core/pipeline/recall.py +274 -0
- uro_core/ports/__init__.py +1 -0
- uro_core/ports/event_store.py +151 -0
- uro_core/ports/participant.py +41 -0
- uro_core/ports/projections.py +157 -0
- uro_core/ports/tokens.py +33 -0
- uro_core/ports/vector.py +29 -0
- uro_core/prompts/__init__.py +1 -0
- uro_core/prompts/extractor.system.j2 +36 -0
- uro_core/prompts/narrator.system.j2 +7 -0
- uro_core/prompts/planner.system.j2 +12 -0
- uro_core/providers/__init__.py +4 -0
- uro_core/providers/adapters/__init__.py +4 -0
- uro_core/providers/adapters/anthropic.py +146 -0
- uro_core/providers/adapters/openai_compat.py +132 -0
- uro_core/providers/adapters/stub.py +57 -0
- uro_core/providers/base.py +48 -0
- uro_core/providers/router.py +66 -0
- uro_core/py.typed +0 -0
- uro_core/rulesets/__init__.py +4 -0
- uro_core/rulesets/base.py +202 -0
- uro_core/rulesets/registry.py +58 -0
- uro_core/rulesets/rng.py +42 -0
- uro_core/rulesets/uro_basic/__init__.py +8 -0
- uro_core/rulesets/uro_basic/ruleset.py +328 -0
- uro_core/rulesets/uro_pbta/__init__.py +11 -0
- uro_core/rulesets/uro_pbta/ruleset.py +278 -0
- uro_core/session.py +268 -0
- uro_core/timeline/__init__.py +1 -0
- uro_core/timeline/models.py +140 -0
- uro_core/worldpack/__init__.py +1 -0
- uro_core/worldpack/backfill.py +73 -0
- uro_core/worldpack/importer.py +64 -0
- uro_core/worldpack/models.py +116 -0
- uro_core/worldpack/parse.py +130 -0
- uro_core/worldpack/rules.py +593 -0
- uro_core/worldpack/sufficiency.py +103 -0
- uro_core-0.2.0.dist-info/METADATA +65 -0
- uro_core-0.2.0.dist-info/RECORD +90 -0
- uro_core-0.2.0.dist-info/WHEEL +4 -0
- uro_core-0.2.0.dist-info/licenses/LICENSE +21 -0
uro_core/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Uro Engine core library. Architecture: docs/01-architecture.md."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("uro-core")
|
|
7
|
+
except PackageNotFoundError: # pragma: no cover - source checkout without an install
|
|
8
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Outcome distillation core (D-25/D-32/D-41) — the parameterized engine behind Chronicler mode.
|
|
2
|
+
|
|
3
|
+
INTERNAL (underscore module): the outcome-bundle models + the single `_distill` body, parameterized
|
|
4
|
+
by a `protect` predicate that decides which actors are shielded from a bundle's canon-minting. Two
|
|
5
|
+
callers inject it:
|
|
6
|
+
|
|
7
|
+
- `uro_core.chronicler.distill_outcome*` → `protect=_is_protected` — the UNTRUSTED path (an external
|
|
8
|
+
game reporting over the network). The full D-32 ceiling holds: a protected actor can't be killed,
|
|
9
|
+
looted, or conscripted as a witness by a bundle.
|
|
10
|
+
- `uro_core.authored.distill_authored_outcome` → `protect=_never_protected` — the TRUSTED,
|
|
11
|
+
in-process
|
|
12
|
+
path (a Posture-A embedder that already holds root via `store.append_beat`). It reuses the
|
|
13
|
+
distillation services (witness-rumor cascade, casualty→death, loot) with NO ceiling.
|
|
14
|
+
|
|
15
|
+
**Trust is which MODULE a caller imports, never a flag on the wire (D-41, the outcome twin of D-37's
|
|
16
|
+
`plan=`).** `uro_server` is import-linter-forbidden from importing this module / `uro_core.authored`
|
|
17
|
+
directly, so the wire layer cannot even NAME the lethal path — the untrusted endpoint only ever
|
|
18
|
+
reaches `chronicler` (ceiling on). The guarantee against the wire is structural + CI-enforced; the
|
|
19
|
+
residual (a deliberate in-core edit importing `_distill` with a false predicate) is not
|
|
20
|
+
language-preventable and is backstopped by a behavioral server test + review (same posture as D-37).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Awaitable, Callable
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
|
|
28
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
29
|
+
|
|
30
|
+
from uro_core.domain.events import (
|
|
31
|
+
DomainEvent,
|
|
32
|
+
actor_died,
|
|
33
|
+
claim_recorded,
|
|
34
|
+
external_cause,
|
|
35
|
+
item_transferred,
|
|
36
|
+
)
|
|
37
|
+
from uro_core.engines.actor import propagate_belief
|
|
38
|
+
from uro_core.ports.projections import ProjectionQueries
|
|
39
|
+
|
|
40
|
+
_MAX_LIST = 64 # cap on feats / casualties / loot per bundle (anti-abuse; docs/13, OQ-12)
|
|
41
|
+
_MAX_WITNESSES = 256
|
|
42
|
+
_SUPPORTED_BUNDLE_VERSIONS = frozenset({1}) # schema-v1 pin (D-41); a v2 bundle is rejected LOUDLY
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Feat(BaseModel):
|
|
46
|
+
model_config = ConfigDict(extra="forbid") # D-41: an unknown field (e.g. a forged trust)→400
|
|
47
|
+
actor: str # the actor who performed the deed
|
|
48
|
+
description: str # "a lone wizard split the warband's champion in two"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LootTransfer(BaseModel):
|
|
52
|
+
model_config = ConfigDict(extra="forbid")
|
|
53
|
+
item_id: str
|
|
54
|
+
from_ref: str = ""
|
|
55
|
+
to_ref: str = ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OutcomeBundle(BaseModel):
|
|
59
|
+
"""Outcome bundle v1 (docs/05, D-25) — what a game reports after resolving an encounter in its
|
|
60
|
+
own domain. List sizes are capped (D-32 anti-abuse) and `extra='forbid'` + a version pin (D-41)
|
|
61
|
+
make a forged `{trust:...}` field or an unknown v a loud 400, not a silently-dropped extra."""
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(extra="forbid")
|
|
64
|
+
v: int = 1
|
|
65
|
+
encounter_id: str
|
|
66
|
+
participants: list[str] = Field(default_factory=list, max_length=_MAX_WITNESSES)
|
|
67
|
+
witnesses: list[str] = Field(
|
|
68
|
+
default_factory=list, max_length=_MAX_WITNESSES
|
|
69
|
+
) # SURVIVORS who saw it
|
|
70
|
+
casualties: list[str] = Field(default_factory=list, max_length=_MAX_LIST)
|
|
71
|
+
feats: list[Feat] = Field(default_factory=list, max_length=_MAX_LIST)
|
|
72
|
+
loot: list[LootTransfer] = Field(default_factory=list, max_length=_MAX_LIST)
|
|
73
|
+
duration_rounds: int = 0
|
|
74
|
+
|
|
75
|
+
@field_validator("v")
|
|
76
|
+
@classmethod
|
|
77
|
+
def _pin_version(cls, v: int) -> int:
|
|
78
|
+
if v not in _SUPPORTED_BUNDLE_VERSIONS:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"OutcomeBundle v={v} unsupported; this engine supports "
|
|
81
|
+
f"{sorted(_SUPPORTED_BUNDLE_VERSIONS)}"
|
|
82
|
+
)
|
|
83
|
+
return v
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# A protection predicate: (store, branch_id, ref) → is this actor shielded from bundle canon?
|
|
87
|
+
Protect = Callable[[ProjectionQueries, str, str], Awaitable[bool]]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _is_protected(store: ProjectionQueries, branch_id: str, ref: str) -> bool:
|
|
91
|
+
"""A PC or a T2+ named actor is protected canon — an UNTRUSTED bundle may not kill/loot/
|
|
92
|
+
first-hand-witness it (D-32). The predicate the network path injects."""
|
|
93
|
+
if await store.is_pc(branch_id, ref):
|
|
94
|
+
return True
|
|
95
|
+
actor = await store.get_actor(branch_id, ref)
|
|
96
|
+
return actor is not None and actor.tier >= 2
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def _never_protected(store: ProjectionQueries, branch_id: str, ref: str) -> bool:
|
|
100
|
+
"""No actor is shielded — the TRUSTED, in-process path (D-41). A Posture-A embedder holds root
|
|
101
|
+
via `append_beat` anyway, so `authored.py` reuses distillation with the ceiling off."""
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ReceiptEntry(BaseModel):
|
|
106
|
+
"""One line of an ingestion receipt (docs/18 B6): what distillation did with a bundle ref, so a
|
|
107
|
+
consumer learns whether its report was applied, downgraded to a rumor, or dropped — and why
|
|
108
|
+
(Ironwake rows 1-2, Seventh G-22). Purely informational; it commits nothing."""
|
|
109
|
+
|
|
110
|
+
kind: str # feat | witness | casualty | loot
|
|
111
|
+
ref: str # the actor / item ref the entry is about
|
|
112
|
+
disposition: str # applied | downgraded | dropped
|
|
113
|
+
reason: str = ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class DistillResult:
|
|
118
|
+
"""The events to commit + the per-ref ingestion receipt (docs/18 B6)."""
|
|
119
|
+
|
|
120
|
+
events: list[DomainEvent] = field(default_factory=list)
|
|
121
|
+
receipt: list[ReceiptEntry] = field(default_factory=list)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def _distill(
|
|
125
|
+
store: ProjectionQueries, branch_id: str, bundle: OutcomeBundle, *, protect: Protect
|
|
126
|
+
) -> DistillResult:
|
|
127
|
+
"""Distill an outcome bundle into committable events (emitter E) + the witness rumor cascade +
|
|
128
|
+
a per-ref receipt. `protect` decides which actors are shielded from canon: `_is_protected` (the
|
|
129
|
+
untrusted D-32 ceiling) or `_never_protected` (a trusted embedder). Every protection site —
|
|
130
|
+
casualty-death, witness-conscript, loot from_ref, loot to_ref — routes through it, so the two
|
|
131
|
+
tiers differ ONLY in that predicate. Out-of-scope / nonexistent effects are dropped regardless
|
|
132
|
+
of tier (scope + existence are not trust-relaxable — a bundle still only touches its declared
|
|
133
|
+
cast). Deterministic ids (encounter_id, index) → idempotent replay."""
|
|
134
|
+
cause = external_cause(bundle.encounter_id)
|
|
135
|
+
participants = set(bundle.participants)
|
|
136
|
+
events: list[DomainEvent] = []
|
|
137
|
+
receipt: list[ReceiptEntry] = []
|
|
138
|
+
|
|
139
|
+
def note(kind: str, ref: str, disposition: str, reason: str = "") -> None:
|
|
140
|
+
receipt.append(ReceiptEntry(kind=kind, ref=ref, disposition=disposition, reason=reason))
|
|
141
|
+
|
|
142
|
+
async def resolve(ref: str) -> str | None:
|
|
143
|
+
"""Entity-resolve a bundle ref to a KNOWN actor id (never mint one — a bundle cannot create
|
|
144
|
+
actors), or None. Pass the RAW name to find_actor_by_name (it canonicalizes internally):
|
|
145
|
+
passing a pre-canonicalized value would defeat its exact-name tiebreak and could attribute a
|
|
146
|
+
feat to a different duplicate than the extractor resolves to (P8-P1)."""
|
|
147
|
+
if not ref:
|
|
148
|
+
return None
|
|
149
|
+
if await store.get_actor(branch_id, ref) is not None:
|
|
150
|
+
return ref
|
|
151
|
+
match = await store.find_actor_by_name(branch_id, ref)
|
|
152
|
+
return match.actor_id if match is not None else None
|
|
153
|
+
|
|
154
|
+
# --- feats → testimony + a witness rumor cascade (deterministic ids → idempotent replay) ---
|
|
155
|
+
for i, feat in enumerate(bundle.feats):
|
|
156
|
+
actor = await resolve(feat.actor)
|
|
157
|
+
if actor is None or actor not in participants:
|
|
158
|
+
note("feat", feat.actor, "dropped", "actor unknown or not a declared participant")
|
|
159
|
+
continue # a feat is about a DECLARED combatant; skip out-of-cast / unknown attribution
|
|
160
|
+
note("feat", actor, "applied", "recorded as truth=unknown testimony")
|
|
161
|
+
claim_id = f"c:{bundle.encounter_id}:feat:{i}"
|
|
162
|
+
events.append(
|
|
163
|
+
claim_recorded(
|
|
164
|
+
claim_id=claim_id,
|
|
165
|
+
statement=feat.description,
|
|
166
|
+
subject_refs=[actor],
|
|
167
|
+
truth="unknown", # testimony, not canon — witnesses believe it, the world doesn't
|
|
168
|
+
origin="external",
|
|
169
|
+
caused_by=cause,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
# Witnesses must be real, in the declared cast, alive (not a casualty), and (untrusted) not
|
|
173
|
+
# protected — an external game can't conscript a PC / named canon figure as its eyewitness.
|
|
174
|
+
witnesses: list[str] = []
|
|
175
|
+
for w in bundle.witnesses:
|
|
176
|
+
if w not in participants:
|
|
177
|
+
note("witness", w, "dropped", "not a declared participant")
|
|
178
|
+
elif w in bundle.casualties:
|
|
179
|
+
note("witness", w, "dropped", "reported as a casualty")
|
|
180
|
+
elif await store.get_actor(branch_id, w) is None:
|
|
181
|
+
note("witness", w, "dropped", "unknown actor")
|
|
182
|
+
elif await protect(store, branch_id, w):
|
|
183
|
+
note("witness", w, "dropped", "protected (PC or T2+) — can't be conscripted")
|
|
184
|
+
else:
|
|
185
|
+
witnesses.append(w)
|
|
186
|
+
note("witness", w, "applied", "carries the rumor")
|
|
187
|
+
events.extend(
|
|
188
|
+
await propagate_belief(store, branch_id, claim_id=claim_id, witnesses=witnesses)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# --- casualties: an in-cast, unprotected combatant dies; an out-of-cast one DROPS (scope, like
|
|
192
|
+
# feats/loot — D-41, Ironwake row-7); an in-cast PROTECTED one downgrades to testimony ---
|
|
193
|
+
for casualty in bundle.casualties:
|
|
194
|
+
victim = await store.get_actor(branch_id, casualty)
|
|
195
|
+
if victim is None or victim.status == "dead":
|
|
196
|
+
note("casualty", casualty, "dropped", "unknown actor or already dead")
|
|
197
|
+
continue
|
|
198
|
+
if casualty not in participants:
|
|
199
|
+
note("casualty", casualty, "dropped", "not a declared participant")
|
|
200
|
+
continue # out-of-cast: dropped, not rumored (was a public rumor leak on the wire)
|
|
201
|
+
if not await protect(store, branch_id, casualty):
|
|
202
|
+
note("casualty", casualty, "applied", "committed as a death")
|
|
203
|
+
events.append(
|
|
204
|
+
actor_died(actor_id=casualty, cause="fell in the battle", caused_by=cause)
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
note(
|
|
208
|
+
"casualty",
|
|
209
|
+
casualty,
|
|
210
|
+
"downgraded",
|
|
211
|
+
"protected canon — a rumored fall, not a committed death (untrusted path)",
|
|
212
|
+
)
|
|
213
|
+
# protected canon on the UNTRUSTED path — the world hears a rumor of the fall, it does
|
|
214
|
+
# not become a committed death (a protected death needs Uro's own mechanics, or the
|
|
215
|
+
# trusted authored path). On the trusted path `protect` is false, so this is dead.
|
|
216
|
+
events.append(
|
|
217
|
+
claim_recorded(
|
|
218
|
+
claim_id=f"c:{bundle.encounter_id}:fell:{casualty}",
|
|
219
|
+
statement=f"{victim.name} is said to have fallen at {bundle.encounter_id}.",
|
|
220
|
+
subject_refs=[casualty],
|
|
221
|
+
truth="unknown",
|
|
222
|
+
origin="external",
|
|
223
|
+
caused_by=cause,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# --- loot: only a real item, actually held by an in-cast, unprotected loser, moves to an
|
|
228
|
+
# in-cast, unprotected recipient (D-41: both ends route through `protect`) ---
|
|
229
|
+
for loot in bundle.loot:
|
|
230
|
+
item = await store.get_item(branch_id, loot.item_id)
|
|
231
|
+
if item is None or item.get("owner_ref") != loot.from_ref:
|
|
232
|
+
note("loot", loot.item_id, "dropped", "nonexistent item or from_ref is not the owner")
|
|
233
|
+
continue # nonexistent item, or from_ref is not the current owner (forged transfer)
|
|
234
|
+
if loot.from_ref not in participants or loot.to_ref not in participants:
|
|
235
|
+
note("loot", loot.item_id, "dropped", "from_ref or to_ref not a declared participant")
|
|
236
|
+
continue # both sides must be declared combatants (scope)
|
|
237
|
+
if loot.to_ref in bundle.casualties:
|
|
238
|
+
note("loot", loot.item_id, "dropped", "recipient reported as fallen")
|
|
239
|
+
continue # a recipient reported as fallen can't carry loot off the field
|
|
240
|
+
if await protect(store, branch_id, loot.from_ref):
|
|
241
|
+
note("loot", loot.item_id, "dropped", "owner is protected (PC or T2+)")
|
|
242
|
+
continue # a PC's / named actor's gear is not looted out-of-band by an external game
|
|
243
|
+
if await protect(store, branch_id, loot.to_ref):
|
|
244
|
+
note("loot", loot.item_id, "dropped", "recipient is protected (PC or T2+)")
|
|
245
|
+
continue # nor can a bundle mint canon making a PC/named actor the RECIPIENT (D-41)
|
|
246
|
+
note("loot", loot.item_id, "applied", f"transferred {loot.from_ref} → {loot.to_ref}")
|
|
247
|
+
events.append(
|
|
248
|
+
item_transferred(
|
|
249
|
+
item_id=loot.item_id,
|
|
250
|
+
from_ref=loot.from_ref,
|
|
251
|
+
to_ref=loot.to_ref,
|
|
252
|
+
caused_by=cause,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return DistillResult(events=events, receipt=receipt)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Port implementations: Postgres, pgvector, in-proc bus. Governing doc: 07."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Postgres adapter: event store, projections, SQL migrations (docs/14)."""
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- Phase 0 timeline core (docs/03, 07). Append-only events + hash-chained commits.
|
|
2
|
+
-- Projections, snapshots, edges, pgvector, and the full event catalog arrive later.
|
|
3
|
+
|
|
4
|
+
CREATE TABLE worlds (
|
|
5
|
+
world_id TEXT PRIMARY KEY,
|
|
6
|
+
name TEXT NOT NULL,
|
|
7
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE TABLE commits (
|
|
11
|
+
commit_id TEXT PRIMARY KEY,
|
|
12
|
+
world_id TEXT NOT NULL REFERENCES worlds(world_id),
|
|
13
|
+
parent_id TEXT REFERENCES commits(commit_id), -- NULL = world genesis
|
|
14
|
+
commit_hash TEXT NOT NULL,
|
|
15
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
16
|
+
);
|
|
17
|
+
CREATE INDEX commits_parent_idx ON commits(parent_id);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE branches (
|
|
20
|
+
branch_id TEXT PRIMARY KEY,
|
|
21
|
+
world_id TEXT NOT NULL REFERENCES worlds(world_id),
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
head_commit TEXT REFERENCES commits(commit_id),
|
|
24
|
+
forked_from TEXT REFERENCES commits(commit_id)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- append-only; NEVER updated or deleted. Canonical event types: docs/12.
|
|
28
|
+
CREATE TABLE events (
|
|
29
|
+
event_id TEXT PRIMARY KEY,
|
|
30
|
+
commit_id TEXT NOT NULL REFERENCES commits(commit_id),
|
|
31
|
+
seq INT NOT NULL,
|
|
32
|
+
event_type TEXT NOT NULL,
|
|
33
|
+
entity_refs TEXT[] NOT NULL DEFAULT '{}',
|
|
34
|
+
world_time JSONB NOT NULL,
|
|
35
|
+
caused_by JSONB NOT NULL,
|
|
36
|
+
payload JSONB NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX events_commit_idx ON events(commit_id);
|
|
39
|
+
CREATE INDEX events_type_idx ON events(event_type);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE campaigns (
|
|
42
|
+
campaign_id TEXT PRIMARY KEY,
|
|
43
|
+
world_id TEXT NOT NULL REFERENCES worlds(world_id),
|
|
44
|
+
branch_id TEXT NOT NULL REFERENCES branches(branch_id),
|
|
45
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
46
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Usage metering (docs/07, D-14, docs/01 rule 5). Stage-tagged LLM calls.
|
|
2
|
+
-- Operational and PRUNABLE — never world truth, never on the timeline.
|
|
3
|
+
-- Added as a forward-only migration (docs/14): 001 is already applied, so metering
|
|
4
|
+
-- arrives in its own file rather than editing the released migration.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE llm_calls (
|
|
7
|
+
call_id TEXT PRIMARY KEY,
|
|
8
|
+
stage_tag TEXT NOT NULL,
|
|
9
|
+
model TEXT,
|
|
10
|
+
prompt_hash TEXT NOT NULL,
|
|
11
|
+
tokens_in INT,
|
|
12
|
+
tokens_out INT,
|
|
13
|
+
latency_ms INT NOT NULL,
|
|
14
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
15
|
+
);
|
|
16
|
+
CREATE INDEX llm_calls_stage_idx ON llm_calls(stage_tag);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- Phase 1 projections (docs/02, 03, 07). Rebuildable read-models DERIVED from the
|
|
2
|
+
-- event log — never written directly, only by the projector, in the same transaction
|
|
3
|
+
-- as the commit that produced the events. Keyed by (branch_id, entity_id).
|
|
4
|
+
-- Phase 1 has a single branch (main); copy-on-fork arrives in Phase 2.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE proj_actors (
|
|
7
|
+
branch_id TEXT NOT NULL,
|
|
8
|
+
actor_id TEXT NOT NULL,
|
|
9
|
+
name TEXT NOT NULL,
|
|
10
|
+
tier INT NOT NULL,
|
|
11
|
+
role TEXT NOT NULL DEFAULT '',
|
|
12
|
+
aliases TEXT[] NOT NULL DEFAULT '{}',
|
|
13
|
+
PRIMARY KEY (branch_id, actor_id)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE proj_claims (
|
|
17
|
+
branch_id TEXT NOT NULL,
|
|
18
|
+
claim_id TEXT NOT NULL,
|
|
19
|
+
statement TEXT NOT NULL,
|
|
20
|
+
subject_refs TEXT[] NOT NULL DEFAULT '{}',
|
|
21
|
+
truth TEXT NOT NULL, -- true | false | unknown (engine ground truth)
|
|
22
|
+
origin TEXT NOT NULL DEFAULT '',
|
|
23
|
+
PRIMARY KEY (branch_id, claim_id)
|
|
24
|
+
);
|
|
25
|
+
CREATE INDEX proj_claims_subject_idx ON proj_claims USING GIN (subject_refs);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE proj_beliefs (
|
|
28
|
+
branch_id TEXT NOT NULL,
|
|
29
|
+
actor_id TEXT NOT NULL,
|
|
30
|
+
claim_id TEXT NOT NULL,
|
|
31
|
+
confidence DOUBLE PRECISION NOT NULL, -- how strongly the actor holds the claim
|
|
32
|
+
learned_from TEXT,
|
|
33
|
+
PRIMARY KEY (branch_id, actor_id, claim_id)
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX proj_beliefs_actor_idx ON proj_beliefs (branch_id, actor_id);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Phase 1 semantic memory (docs/04, 07). pgvector-backed.
|
|
2
|
+
-- Vectors live once, keyed by content hash (dedup); per-branch membership rows point
|
|
3
|
+
-- at them and carry the recallable text. Copy-on-fork of memberships is a later increment.
|
|
4
|
+
-- Unbounded `vector` (dimension set by the deployment's embedder) + exact search for now;
|
|
5
|
+
-- an HNSW index with a pinned dimension arrives when scale demands it.
|
|
6
|
+
|
|
7
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
8
|
+
|
|
9
|
+
CREATE TABLE embeddings (
|
|
10
|
+
content_hash TEXT PRIMARY KEY,
|
|
11
|
+
vector vector NOT NULL
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE memory_index (
|
|
15
|
+
memory_id TEXT PRIMARY KEY,
|
|
16
|
+
branch_id TEXT NOT NULL,
|
|
17
|
+
content_hash TEXT NOT NULL REFERENCES embeddings(content_hash),
|
|
18
|
+
kind TEXT NOT NULL, -- 'beat' | 'synopsis' | 'journal' | …
|
|
19
|
+
text TEXT NOT NULL, -- the recallable snippet
|
|
20
|
+
entity_refs TEXT[] NOT NULL DEFAULT '{}',
|
|
21
|
+
commit_id TEXT NOT NULL
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX memory_index_branch_idx ON memory_index(branch_id);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- Phase 2 timeline substrate (docs/03, 07). Markers, snapshots, and the places
|
|
2
|
+
-- projection — the machinery for branch-from-any-commit and materialization.
|
|
3
|
+
--
|
|
4
|
+
-- `depth` = generation from genesis (genesis = 0, each child = parent + 1). Commits
|
|
5
|
+
-- form a tree (one parent each; branches share prefixes), so depth is well-defined.
|
|
6
|
+
-- It gives cheap "snapshot every N commits", ordered replay, and git-log numbering
|
|
7
|
+
-- without walking ancestry per beat. Backfilled here for any pre-Phase-2 commits.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE commits ADD COLUMN depth INT NOT NULL DEFAULT 0;
|
|
10
|
+
|
|
11
|
+
WITH RECURSIVE gen AS (
|
|
12
|
+
SELECT commit_id, 0 AS depth FROM commits WHERE parent_id IS NULL
|
|
13
|
+
UNION ALL
|
|
14
|
+
SELECT c.commit_id, gen.depth + 1
|
|
15
|
+
FROM commits c JOIN gen ON c.parent_id = gen.commit_id
|
|
16
|
+
)
|
|
17
|
+
UPDATE commits SET depth = gen.depth FROM gen WHERE commits.commit_id = gen.commit_id;
|
|
18
|
+
|
|
19
|
+
CREATE INDEX commits_depth_idx ON commits(depth);
|
|
20
|
+
|
|
21
|
+
-- Markers: named, immutable refs to a commit (campaign-a-end, "the meteor falls").
|
|
22
|
+
-- Refs, not events (docs/12 rule 2) — creating one never touches the log.
|
|
23
|
+
CREATE TABLE markers (
|
|
24
|
+
marker_id TEXT PRIMARY KEY,
|
|
25
|
+
world_id TEXT NOT NULL REFERENCES worlds(world_id),
|
|
26
|
+
name TEXT NOT NULL,
|
|
27
|
+
commit_id TEXT NOT NULL REFERENCES commits(commit_id),
|
|
28
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
29
|
+
UNIQUE (world_id, name)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Snapshots: full materialized projection state at a commit, serialized every N
|
|
33
|
+
-- commits and at markers. Keyed by commit_id — state-at-a-commit is branch-independent
|
|
34
|
+
-- (a commit's ancestry is immutable), so a snapshot serves any branch that passes
|
|
35
|
+
-- through it. Materializing commit X = nearest ancestor snapshot ≤ X + replay forward;
|
|
36
|
+
-- branching is therefore O(replay window), not O(history). state_hash makes each blob
|
|
37
|
+
-- tamper-evident on its own (docs/07, the export-pack trust anchor).
|
|
38
|
+
CREATE TABLE snapshots (
|
|
39
|
+
commit_id TEXT PRIMARY KEY REFERENCES commits(commit_id),
|
|
40
|
+
state_hash TEXT NOT NULL,
|
|
41
|
+
state JSONB NOT NULL,
|
|
42
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
-- Places projection (docs/02). Deferred in Phase 1 ("places/edges when needed");
|
|
46
|
+
-- the meteor test needs it — PlaceDestroyed(Vel) is simply true on the aftermath
|
|
47
|
+
-- branch and false on a what-if forked before the strike.
|
|
48
|
+
CREATE TABLE proj_places (
|
|
49
|
+
branch_id TEXT NOT NULL,
|
|
50
|
+
place_id TEXT NOT NULL,
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
kind TEXT NOT NULL DEFAULT 'site', -- region | settlement | site (docs/02)
|
|
53
|
+
status TEXT NOT NULL DEFAULT 'active', -- active | destroyed
|
|
54
|
+
description TEXT NOT NULL DEFAULT '',
|
|
55
|
+
PRIMARY KEY (branch_id, place_id)
|
|
56
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Phase 2 review fix (inc 2.1): branch names are unique per world.
|
|
2
|
+
--
|
|
3
|
+
-- Markers got UNIQUE(world_id, name) in 005; branches were left unguarded, so
|
|
4
|
+
-- `uro branch fork --name main` could mint a SECOND branch named 'main' — and the
|
|
5
|
+
-- world-main lookups (get_world / get_world_by_name) resolve via an unordered
|
|
6
|
+
-- `JOIN branches ... AND b.name='main'`, which then returns an arbitrary one and
|
|
7
|
+
-- silently targets the wrong timeline. Git's branch namespace is unique per repo;
|
|
8
|
+
-- Uro's is unique per world. Structural guarantee, not policy (the repo norm).
|
|
9
|
+
ALTER TABLE branches ADD CONSTRAINT branches_world_name_uniq UNIQUE (world_id, name);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Phase 2 inc 2.2: PC-binding projection (docs/02, 12). "Is this actor a PC?" is
|
|
2
|
+
-- answered per-branch by the campaign's PCBound/PCReleased history — NOT a global flag,
|
|
3
|
+
-- because the same actor_id is a PC on the fork where the player continues and an
|
|
4
|
+
-- ordinary NPC on a sibling fork (the meteor test's retired wizard). `active` flips to
|
|
5
|
+
-- false on PCReleased (campaign end); the row stays as history ("was a PC in campaign A").
|
|
6
|
+
CREATE TABLE proj_pcs (
|
|
7
|
+
branch_id TEXT NOT NULL,
|
|
8
|
+
campaign_id TEXT NOT NULL,
|
|
9
|
+
actor_id TEXT NOT NULL,
|
|
10
|
+
participant_id TEXT NOT NULL,
|
|
11
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
12
|
+
PRIMARY KEY (branch_id, campaign_id, actor_id)
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX proj_pcs_active_idx ON proj_pcs (branch_id, actor_id) WHERE active;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Phase 3 inc 3.2: character-sheet projection (docs/06, 12). The store keeps the sheet
|
|
2
|
+
-- opaquely as JSONB; the pipeline reads it via the shared port Sheet (a d20-shaped contract
|
|
3
|
+
-- for now, OQ-13) and the ruleset owns its semantics. Written by the projector on
|
|
4
|
+
-- SheetUpdated (emitter R S),
|
|
5
|
+
-- carried per-branch and rebuilt on replay like every other projection. The mechanics gate
|
|
6
|
+
-- reads a PC/NPC sheet here to build a CheckRequest.
|
|
7
|
+
CREATE TABLE proj_sheets (
|
|
8
|
+
branch_id TEXT NOT NULL,
|
|
9
|
+
actor_id TEXT NOT NULL,
|
|
10
|
+
ruleset_id TEXT NOT NULL DEFAULT '',
|
|
11
|
+
sheet JSONB NOT NULL,
|
|
12
|
+
PRIMARY KEY (branch_id, actor_id)
|
|
13
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Phase 3 inc 3.3: items projection (docs/02, 12). Minimal ownership tracking so a lost
|
|
2
|
+
-- fight's loot (ItemTransferred) and a starting weapon (ItemCreated) persist into later
|
|
3
|
+
-- free-roam. Written by the projector on ItemCreated/ItemTransferred; HP damage lives in
|
|
4
|
+
-- proj_sheets (ActorDamaged mutates the sheet's hp), so no separate condition table yet.
|
|
5
|
+
CREATE TABLE proj_items (
|
|
6
|
+
branch_id TEXT NOT NULL,
|
|
7
|
+
item_id TEXT NOT NULL,
|
|
8
|
+
name TEXT NOT NULL DEFAULT '',
|
|
9
|
+
kind TEXT NOT NULL DEFAULT '',
|
|
10
|
+
owner_ref TEXT NOT NULL DEFAULT '',
|
|
11
|
+
PRIMARY KEY (branch_id, item_id)
|
|
12
|
+
);
|
|
13
|
+
CREATE INDEX proj_items_owner_idx ON proj_items (branch_id, owner_ref);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Phase 4 inc 4.2: factions + the graph edge table (docs/02, 07). Deferred since Phase 1
|
|
2
|
+
-- ("places/edges when needed") — World seeding needs them: dynasties are factions and wars
|
|
3
|
+
-- are `at_war_with` edges, so seed 42 vs 43 differ here on identical geography.
|
|
4
|
+
--
|
|
5
|
+
-- Edges are the whole graph story for MVP. The PoC keeps CURRENT-STATE edges keyed by
|
|
6
|
+
-- (src, rel_type, dst); the per-commit temporal validity (valid_from/valid_to for point-in-time
|
|
7
|
+
-- reads, docs/07) is deferred — copy-on-fork already materializes current state per branch.
|
|
8
|
+
CREATE TABLE proj_factions (
|
|
9
|
+
branch_id TEXT NOT NULL,
|
|
10
|
+
faction_id TEXT NOT NULL,
|
|
11
|
+
name TEXT NOT NULL,
|
|
12
|
+
kind TEXT NOT NULL DEFAULT 'faction', -- faction | religion (docs/02)
|
|
13
|
+
description TEXT NOT NULL DEFAULT '',
|
|
14
|
+
PRIMARY KEY (branch_id, faction_id)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE proj_edges (
|
|
18
|
+
branch_id TEXT NOT NULL,
|
|
19
|
+
src TEXT NOT NULL,
|
|
20
|
+
rel_type TEXT NOT NULL, -- member_of | located_in | at_war_with | rules | owns | knows | …
|
|
21
|
+
dst TEXT NOT NULL,
|
|
22
|
+
weight DOUBLE PRECISION NOT NULL DEFAULT 1.0,
|
|
23
|
+
attrs JSONB NOT NULL DEFAULT '{}',
|
|
24
|
+
PRIMARY KEY (branch_id, src, rel_type, dst)
|
|
25
|
+
);
|
|
26
|
+
CREATE INDEX proj_edges_src_idx ON proj_edges (branch_id, src);
|
|
27
|
+
CREATE INDEX proj_edges_dst_idx ON proj_edges (branch_id, dst);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Phase 4 review fix: threads become real state. World-pack conflict seeds (authored) and AI
|
|
2
|
+
-- backfill (provenance=ai_backfill) now commit as ThreadCreated events at import (emitter S),
|
|
3
|
+
-- so the machine's inventions are reviewable committed state — not a discarded in-memory model.
|
|
4
|
+
CREATE TABLE proj_threads (
|
|
5
|
+
branch_id TEXT NOT NULL,
|
|
6
|
+
thread_id TEXT NOT NULL,
|
|
7
|
+
stakes TEXT NOT NULL,
|
|
8
|
+
state TEXT NOT NULL DEFAULT 'dormant',
|
|
9
|
+
provenance TEXT NOT NULL DEFAULT 'author', -- author | ai_backfill (docs/09)
|
|
10
|
+
PRIMARY KEY (branch_id, thread_id)
|
|
11
|
+
);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Cross-phase review fix (P1+P3+P5): ActorDied previously only zeroed a character sheet's hp, so
|
|
2
|
+
-- a sheet-less casualty (a History-seeded NPC, or a Chronicler-reported death) left NO queryable
|
|
3
|
+
-- death trace — recall still surfaced it as alive/present. A lifecycle column on proj_actors gives
|
|
4
|
+
-- death a projection trace independent of the ruleset sheet.
|
|
5
|
+
ALTER TABLE proj_actors ADD COLUMN status TEXT NOT NULL DEFAULT 'alive'; -- alive | dead
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Phase 6 inc 6.3 (D-30): pin the bound ruleset on the campaign, so a later `play`/fork rebinds
|
|
2
|
+
-- the SAME ruleset (a d20 campaign resolves uro_basic; a PbtA campaign resolves uro_pbta). Before
|
|
3
|
+
-- this, only the CampaignStarted EVENT carried ruleset_id (and no version) — the play path ignored
|
|
4
|
+
-- it and hard-bound uro_basic. These columns make the binding queryable at get_campaign time.
|
|
5
|
+
ALTER TABLE campaigns ADD COLUMN ruleset_id TEXT NOT NULL DEFAULT '';
|
|
6
|
+
ALTER TABLE campaigns ADD COLUMN ruleset_version TEXT NOT NULL DEFAULT '';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Gap-report fix (Hollowloop G-10, found by a fork-per-loop game at 500 forks): the fork hot path
|
|
2
|
+
-- `_copy_memory` filters `memory_index WHERE commit_id = ANY(...)`, but the only index on the table
|
|
3
|
+
-- was on `branch_id` (migration 004) — so every fork did a Seq Scan over ALL of memory_index, which
|
|
4
|
+
-- holds one row per beat of EVERY world in the database. EXPLAIN ANALYZE showed ~17k rows discarded
|
|
5
|
+
-- per fork (30-60% of fork time), and the cost grows with total beats ever played, not with the
|
|
6
|
+
-- world being forked. One index removes the scan.
|
|
7
|
+
CREATE INDEX IF NOT EXISTS memory_index_commit_idx ON memory_index(commit_id);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Computation Layer INC-C1 (docs/19, D-34): pack-authored numeric state as engine-owned, event-
|
|
2
|
+
-- sourced integer counters. A CounterChanged event UPSERTs a row here; the projector is the sole
|
|
3
|
+
-- writer, and because proj_counters joins projector._HANDLERS + _SNAPSHOT_TABLES it forks/replays/
|
|
4
|
+
-- snapshots/exports by construction (the fix for shadow-state that did not ride fork_branch —
|
|
5
|
+
-- Sable G-1/G-10). value is BIGINT (integer only — no float, for cross-platform arithmetic
|
|
6
|
+
-- determinism). created_day is preserved on UPSERT (updated_day moves) so claim/counter age math
|
|
7
|
+
-- works. Branch-scoped like every projection → per-branch isolation + copy-on-fork inherited.
|
|
8
|
+
CREATE TABLE proj_counters (
|
|
9
|
+
branch_id TEXT NOT NULL,
|
|
10
|
+
scope_ref TEXT NOT NULL, -- the entity the counter hangs on (a faction/thread/place id)
|
|
11
|
+
key TEXT NOT NULL, -- the counter name (e.g. "tension", "heat", "influence")
|
|
12
|
+
value BIGINT NOT NULL DEFAULT 0,
|
|
13
|
+
created_day INT NOT NULL DEFAULT 0,
|
|
14
|
+
updated_day INT NOT NULL DEFAULT 0,
|
|
15
|
+
PRIMARY KEY (branch_id, scope_ref, key)
|
|
16
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- G-3 (docs/18): make a played-through campaign's mechanics REPRODUCIBLE run-to-run. Before this,
|
|
2
|
+
-- Engine._beat_rng hashed (campaign_id : head_commit) — BOTH freshly random per run (new_id()) — so
|
|
3
|
+
-- the same intents produced different d20/2d6 rolls every run (even on a REPLAY of the same log), and
|
|
4
|
+
-- a "guaranteed-loss" fight would occasionally flip (a flaky gate, Seventh Vault G-3). The fix reseeds
|
|
5
|
+
-- _beat_rng from the campaign's persisted seed + the commit DEPTH (a deterministic generation count),
|
|
6
|
+
-- so the RNG STREAM is a pure function of (seed, depth) — a fight then replays identically given an
|
|
7
|
+
-- identical event log up to it (which is what makes the deterministic-provider CI gate reproducible;
|
|
8
|
+
-- live-LLM runs can still vary the log). The seed lives on CampaignStarted (payload) already;
|
|
9
|
+
-- denormalize it here (like ruleset_id) so get_campaign can load it without replaying the event.
|
|
10
|
+
-- BIGINT: the seed is a 48-bit-ish integer; DEFAULT 0 keeps every pre-existing campaign valid
|
|
11
|
+
-- (seed 0 is a fine, fully-deterministic default — tests pin it, `uro campaign new --seed` sets it).
|
|
12
|
+
ALTER TABLE campaigns ADD COLUMN seed BIGINT NOT NULL DEFAULT 0;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- B8 (docs/18, D-36): participant-scoped memory — a player's out-of-world meta-knowledge that
|
|
2
|
+
-- SURVIVES A FORK (time-loop / roguelike / NG+). Keyed on (participant_id, world_ref) — NOT
|
|
3
|
+
-- branch_id — so it is deliberately OUTSIDE the branch axis: it is NOT a proj_* projection, is NOT
|
|
4
|
+
-- in the projector's _HANDLERS/_SNAPSHOT_TABLES, and is never touched by fork_branch's copy step, so
|
|
5
|
+
-- a fork neither copies nor resets it (fork-immunity is structural). It is also NOT an event and has
|
|
6
|
+
-- no DIRECT path into proj_claims/proj_beliefs — nothing wires it into the extractor, planner, or
|
|
7
|
+
-- belief-propagation, so it cannot DIRECTLY become canon or an NPC belief (structural for direct
|
|
8
|
+
-- wiring). It surfaces only in the narrator prompt (labelled "the world does NOT know this"); a note
|
|
9
|
+
-- the narrator ECHOES into prose could then be re-extracted like any narrator output — that residual
|
|
10
|
+
-- is fenced by narrator-tier trust (by-policy), the same fence every narrator input already has (13).
|
|
11
|
+
-- world_ref is the SCOPE knob: the world_id gives per-(world x participant) scope (the default; a
|
|
12
|
+
-- within-world fork keeps the same world_id, so it survives); a sentinel could later give global.
|
|
13
|
+
CREATE TABLE participant_notes (
|
|
14
|
+
participant_id TEXT NOT NULL,
|
|
15
|
+
world_ref TEXT NOT NULL, -- the world_id (scope); a fork stays in-world so notes persist
|
|
16
|
+
key TEXT NOT NULL, -- caller-supplied or sha256(text): the dedup key across loops
|
|
17
|
+
text TEXT NOT NULL,
|
|
18
|
+
pinned BOOLEAN NOT NULL DEFAULT FALSE, -- always-surface vs entity-triggered
|
|
19
|
+
entity_refs TEXT[] NOT NULL DEFAULT '{}', -- e.g. {name:vault} — surfaces when mentioned
|
|
20
|
+
PRIMARY KEY (participant_id, world_ref, key)
|
|
21
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- B10 (docs/18, D-39): runtime session-token management. A durable, hashed, revocable token
|
|
2
|
+
-- registry so a player added to a RUNNING server (bind_pc / uro campaign join) can AUTHENTICATE
|
|
3
|
+
-- without a server restart (the --token argv map is frozen at launch). Like participant_notes
|
|
4
|
+
-- (017), this is deliberately OFF THE BRANCH AXIS: it is NOT a proj_* projection, is NOT in the
|
|
5
|
+
-- projector's _HANDLERS/_SNAPSHOT_TABLES, and is never touched by fork_branch / import_world — a
|
|
6
|
+
-- token authenticates a HUMAN to the server, independent of which what-if branch they explore, so a
|
|
7
|
+
-- fork must neither copy nor reset it (structural, mirrored by a fork-exclusion test). It is also
|
|
8
|
+
-- NOT event-sourced (D-31/D-39 kept: no session/turn/token state enters the log — nothing new
|
|
9
|
+
-- rides fork_branch). Only sha256(token) is stored; the plaintext is returned once at mint and
|
|
10
|
+
-- never at rest. Durability is REQUIRED: an in-process-only registry would lose runtime-minted
|
|
11
|
+
-- tokens on restart while proj_pcs survives, permanently locking out every runtime-added player.
|
|
12
|
+
CREATE TABLE session_tokens (
|
|
13
|
+
token_hash TEXT NOT NULL PRIMARY KEY, -- sha256(token); plaintext never stored
|
|
14
|
+
participant_id TEXT NOT NULL,
|
|
15
|
+
campaign_id TEXT NOT NULL,
|
|
16
|
+
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
17
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
18
|
+
);
|
|
19
|
+
CREATE INDEX session_tokens_participant_idx ON session_tokens (participant_id, campaign_id);
|