Build a fixture that can witness what a tree changes
The stress fixture is sized from production and exists to weigh bytes, so it leaves every column it does not weigh at its default. Checked against a freshly built one, that is exactly the set phase 14 has to migrate: state_before and world_state_before NULL on all 600 rows, no scenario and so no RPG layer, no adventure scripts, both cursors 0, and a hundred retry histories whose two attempts carry byte-identical text with variant_index pinned at 0. That last one matters most. "Which attempt is live?" is the question SP4's migration answers when it decides which sibling node becomes the head of a turn, and against that fixture the question had no observable answer -- a migration that picked wrong would have looked correct. --rich fills in those columns and no others. A real RPG scenario read from the seed data rather than invented, with a world state played forward so hp declines and flags flip; monotonic per-action state snapshots, so a rollback that does not happen reads as a wrong number instead of as nothing; a gold script, story cards, non-zero cursors, pinned and forgotten memories; retry attempts with distinct texts, counts of two and three, and a live attempt that is often not the last written. Plus a second adventure, because a branch clause that forgot its adventure still looks right on a database holding one. The invariant SP4 reads -- text mirrors variants[variant_index] -- is asserted at build time rather than assumed. The plain fixture is untouched and re-measured unchanged at 1.8 kB, so the egress ceilings stay comparable. All six shapes run clean on --rich, including the turn path with the RPG layer and script pipeline now live. 283 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7
This commit is contained in:
committed by
Parth
co-authored by
Claude Opus 5
parent
1940e222a7
commit
5c1bcf7305
@@ -130,13 +130,14 @@ else:
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from app import auth, limits, memorybank, models, security
|
from app import auth, limits, memorybank, models, security, seed, worldstate
|
||||||
from app.database import Base, SessionLocal, engine, get_db
|
from app.database import Base, SessionLocal, engine, get_db
|
||||||
from app.main import app
|
from app.main import app
|
||||||
from app.providers import PromptParts
|
from app.providers import PromptParts
|
||||||
@@ -214,6 +215,213 @@ class FakeEmbeddings:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------- rich fixture
|
||||||
|
|
||||||
|
# The correctness fixture, as against the scale one.
|
||||||
|
#
|
||||||
|
# The default fixture is sized from production and exists to weigh bytes, so it
|
||||||
|
# leaves every column it does not weigh at its default. That makes it a poor
|
||||||
|
# witness for anything *semantic* — and phase 14 replaces the storage model
|
||||||
|
# underneath all of it. Measured on a freshly built default fixture, the
|
||||||
|
# columns a story tree has to migrate correctly look like this:
|
||||||
|
#
|
||||||
|
# state_before / world_state_before NULL on all 600 rows. SP4 moves these
|
||||||
|
# from *before* to *after* snapshots, and
|
||||||
|
# a migration over NULLs proves nothing.
|
||||||
|
# scenario_id / world_state absent. No RPG layer, so the cooldown
|
||||||
|
# clock SP5 must not advance never runs.
|
||||||
|
# adventure_scripts none. script_state rollback is exactly
|
||||||
|
# what a branch switch reuses.
|
||||||
|
# memory_cursor / summary_cursor both 0. SP3 replaces the cursors.
|
||||||
|
# variants two attempts of byte-identical text,
|
||||||
|
# variant_index always 0 — so "which
|
||||||
|
# attempt is live?", the one question
|
||||||
|
# SP4's migration has to answer, has no
|
||||||
|
# observable answer.
|
||||||
|
#
|
||||||
|
# --rich fills in exactly those and changes nothing else, so the measuring
|
||||||
|
# fixture's numbers stay comparable run to run. It is a *correctness* fixture:
|
||||||
|
# prefer it small (--rich --actions 30), because what it is for is variety per
|
||||||
|
# row, not rows.
|
||||||
|
|
||||||
|
RICH_SUMMARY = (
|
||||||
|
"You tracked the bandits to a camp above the ford, freed Gwen from the "
|
||||||
|
"quartermaster's cage, and struck a bargain: the iron key for safe passage "
|
||||||
|
"through the tunnels. The alarm has not yet been raised."
|
||||||
|
)
|
||||||
|
|
||||||
|
RICH_CARDS = [
|
||||||
|
("character", "Gwen", "Gwen, ranger, her",
|
||||||
|
"A loyal ranger who owes you her life and says so rarely."),
|
||||||
|
("location", "The Ford", "ford, river, crossing",
|
||||||
|
"A shallow crossing overlooked by the bandit camp."),
|
||||||
|
("item", "Iron Key", "iron key, key",
|
||||||
|
"Taken from the quartermaster. Opens something below the camp."),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ten gold a turn, so a state snapshot that failed to roll back reads as a
|
||||||
|
# wrong total rather than as nothing. Same shape the retry tests use.
|
||||||
|
RICH_SCRIPT = """
|
||||||
|
const modifier = (text) => {
|
||||||
|
state.gold = (state.gold || 0) + 10;
|
||||||
|
return { text };
|
||||||
|
};
|
||||||
|
modifier(text);
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def rich_stat_schema() -> dict:
|
||||||
|
"""The demo RPG schema, read from the seed data rather than invented here.
|
||||||
|
|
||||||
|
Using the real one means the fixture exercises bands, cooldowns,
|
||||||
|
max_delta_per_turn and NPC stat blocks as they are actually shaped — an
|
||||||
|
invented schema would drift from the thing it is standing in for.
|
||||||
|
"""
|
||||||
|
path = seed.SEED_DIR / "04-rpg-world-state.json"
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))["stat_schema"]
|
||||||
|
|
||||||
|
|
||||||
|
def rich_script_state(turn: int) -> dict:
|
||||||
|
"""The scoreboard as of `turn`. Monotonic, so any snapshot identifies the
|
||||||
|
turn it was taken at — which is what makes a bad rollback visible."""
|
||||||
|
return {"gold": turn * 10, "turn": turn}
|
||||||
|
|
||||||
|
|
||||||
|
def rich_world_state(schema: dict, turn: int) -> dict:
|
||||||
|
"""A live world state that has actually been played to `turn`.
|
||||||
|
|
||||||
|
`instantiate` gives the initial picture; a fixture whose every row holds
|
||||||
|
that same picture cannot tell a restored snapshot from an unrestored one.
|
||||||
|
So hp declines, mana drains and a flag flips partway through.
|
||||||
|
"""
|
||||||
|
ws = worldstate.instantiate(schema)
|
||||||
|
ws["player"]["hp"] = max(20, 100 - turn)
|
||||||
|
ws["player"]["mana"] = max(0, 30 - turn // 2)
|
||||||
|
ws["world"]["day"] = 1 + turn // 20
|
||||||
|
ws["flags"]["alarm_raised"] = turn > 12
|
||||||
|
ws["flags"]["player_hidden"] = turn <= 12
|
||||||
|
ws["_meta"]["last_changed"] = {"player.hp": turn}
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
def rich_variants(rng: random.Random, index: int) -> tuple[list, int]:
|
||||||
|
"""Distinguishable retry attempts, and which one is live.
|
||||||
|
|
||||||
|
Every attempt in the default fixture carries the same text with
|
||||||
|
variant_index pinned at 0. That is the one thing SP4 has to get right and
|
||||||
|
the one thing that fixture cannot witness, so here the texts differ, the
|
||||||
|
counts differ, and the live one is often not the last.
|
||||||
|
"""
|
||||||
|
count = 3 if index % 12 == 1 else 2
|
||||||
|
entries = [
|
||||||
|
{
|
||||||
|
"text": f"[attempt {n + 1} of {count} at turn {index}] {prose(rng, 240)}",
|
||||||
|
"reasoning": None,
|
||||||
|
"script_state": rich_script_state(index),
|
||||||
|
"created_at": f"2026-01-01T00:{index % 60:02d}:{n:02d}",
|
||||||
|
}
|
||||||
|
for n in range(count)
|
||||||
|
]
|
||||||
|
# Deliberately not always the newest: a player who retried twice and then
|
||||||
|
# went back to the first take is the case that breaks a migration which
|
||||||
|
# assumes the live attempt is the last one written.
|
||||||
|
live = 0 if index % 18 == 1 else count - 1
|
||||||
|
return entries, live
|
||||||
|
|
||||||
|
|
||||||
|
def add_rich_extras(db, args, rng: random.Random, user, adventure) -> None:
|
||||||
|
"""Everything --rich adds beside the actions themselves.
|
||||||
|
|
||||||
|
Called with the adventure already flushed, before the actions are written,
|
||||||
|
because the actions need the schema to snapshot a world state from.
|
||||||
|
"""
|
||||||
|
schema = rich_stat_schema()
|
||||||
|
scenario = models.Scenario(
|
||||||
|
user_id=user.id,
|
||||||
|
title="Stress (RPG)",
|
||||||
|
prompt="You crouch at the ford, watching the bandit camp.",
|
||||||
|
stat_schema=schema,
|
||||||
|
)
|
||||||
|
db.add(scenario)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
adventure.scenario_id = scenario.id
|
||||||
|
adventure.world_state = rich_world_state(schema, args.actions)
|
||||||
|
adventure.script_state = rich_script_state(args.actions)
|
||||||
|
# The post-turn passes SP3 rewrites only do anything when summarization is
|
||||||
|
# on and the cursors are somewhere other than the start.
|
||||||
|
adventure.auto_summarize = True
|
||||||
|
adventure.story_summary = RICH_SUMMARY
|
||||||
|
adventure.memory_cursor = max(0, args.actions - 8)
|
||||||
|
adventure.summary_cursor = max(0, args.actions - 20)
|
||||||
|
|
||||||
|
for kind, name, keys, entry in RICH_CARDS:
|
||||||
|
db.add(models.StoryCard(
|
||||||
|
adventure_id=adventure.id, type=kind, name=name, keys=keys, entry=entry,
|
||||||
|
))
|
||||||
|
|
||||||
|
db.add(models.AdventureScript(
|
||||||
|
adventure_id=adventure.id, position=0, enabled=True,
|
||||||
|
name="Gold", description="Ten gold a turn.", output_js=RICH_SCRIPT,
|
||||||
|
))
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def add_second_adventure(db, rng: random.Random, user) -> int:
|
||||||
|
"""A short second adventure, so 'does this leak across adventures?' is a
|
||||||
|
question the fixture can answer. A tree scopes every read by branch, and a
|
||||||
|
branch clause that forgot its adventure would still look right on a
|
||||||
|
database holding exactly one."""
|
||||||
|
other = models.Adventure(
|
||||||
|
user_id=user.id, title="Stress (second)", script_state={},
|
||||||
|
memory_bank_enabled=True, auto_summarize=False,
|
||||||
|
)
|
||||||
|
db.add(other)
|
||||||
|
db.flush()
|
||||||
|
for i in range(6):
|
||||||
|
db.add(models.Action(
|
||||||
|
adventure_id=other.id, index=i,
|
||||||
|
type="ai" if i % 2 else "do",
|
||||||
|
text=f"[second adventure] turn {i}. {prose(rng, 200)}",
|
||||||
|
state_before=rich_script_state(i),
|
||||||
|
))
|
||||||
|
memory = models.Memory(
|
||||||
|
adventure_id=other.id,
|
||||||
|
text="This memory belongs to the second adventure and must never be "
|
||||||
|
"retrieved for the first.",
|
||||||
|
source_start=0, source_end=5,
|
||||||
|
)
|
||||||
|
memorybank.set_vector(memory, [rng.uniform(-1.0, 1.0) for _ in range(EMBEDDING_DIMS)])
|
||||||
|
db.add(memory)
|
||||||
|
return other.id
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_live_variant_invariant(db, adventure_id: int) -> None:
|
||||||
|
"""`text` mirrors the live attempt, on every retried row.
|
||||||
|
|
||||||
|
Checked here rather than trusted, because SP4's migration reads exactly
|
||||||
|
this to decide which sibling node becomes the head of the turn. A fixture
|
||||||
|
that quietly violated it would let a wrong migration look right.
|
||||||
|
"""
|
||||||
|
rows = (
|
||||||
|
db.query(models.Action)
|
||||||
|
.filter(models.Action.adventure_id == adventure_id,
|
||||||
|
models.Action.variant_count > 0)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for action in rows:
|
||||||
|
entries = action.variants or []
|
||||||
|
live = entries[action.variant_index]["text"]
|
||||||
|
if live != action.text:
|
||||||
|
sys.exit(
|
||||||
|
f"fixture is inconsistent: action index {action.index} has text "
|
||||||
|
f"that is not its live attempt (variant_index="
|
||||||
|
f"{action.variant_index} of {len(entries)})."
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
sys.exit("--rich built no retried actions; raise --actions above 6.")
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------- fixture
|
# ------------------------------------------------------------------- fixture
|
||||||
|
|
||||||
|
|
||||||
@@ -254,24 +462,47 @@ def build_fixture(args, rng: random.Random) -> tuple[int, int]:
|
|||||||
db.add(adventure)
|
db.add(adventure)
|
||||||
db.flush()
|
db.flush()
|
||||||
|
|
||||||
|
schema = add_rich_extras(db, args, rng, user, adventure) if args.rich else None
|
||||||
|
|
||||||
for i in range(args.actions):
|
for i in range(args.actions):
|
||||||
is_ai = bool(i % 2)
|
is_ai = bool(i % 2)
|
||||||
|
retried = is_ai and i % 6 == 1
|
||||||
|
if args.rich:
|
||||||
|
# Distinguishable attempts, and a live one that is often not
|
||||||
|
# the last written. `text` must mirror the live attempt — that
|
||||||
|
# invariant is what SP4's migration reads to decide which
|
||||||
|
# sibling becomes the head.
|
||||||
|
entries, live = rich_variants(rng, i) if retried else ([], 0)
|
||||||
|
body = entries[live]["text"] if retried else (
|
||||||
|
f"[turn {i}] {NARRATION}" if is_ai else f"[turn {i}] {PLAYER_INPUT}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
entries, live = (
|
||||||
|
[{"text": NARRATION, "reasoning": None, "script_state": {},
|
||||||
|
"created_at": "2026-01-01T00:00:00"} for _ in range(2)]
|
||||||
|
if retried else []
|
||||||
|
), 0
|
||||||
|
body = NARRATION if is_ai else PLAYER_INPUT
|
||||||
db.add(models.Action(
|
db.add(models.Action(
|
||||||
adventure_id=adventure.id,
|
adventure_id=adventure.id,
|
||||||
index=i,
|
index=i,
|
||||||
type="ai" if is_ai else "do",
|
type="ai" if is_ai else "do",
|
||||||
text=NARRATION if is_ai else PLAYER_INPUT,
|
text=body,
|
||||||
context_snapshot={"system": SNAPSHOT_SYSTEM, "story": SNAPSHOT_STORY},
|
context_snapshot={"system": SNAPSHOT_SYSTEM, "story": SNAPSHOT_STORY},
|
||||||
world_delta={"delta": {"player.hp": -3},
|
world_delta={"delta": {"player.hp": -3},
|
||||||
"applied": [{"path": "player.hp", "old": 88, "new": 85}]},
|
"applied": [{"path": "player.hp", "old": 88, "new": 85}]},
|
||||||
# Every third AI turn was retried once, so the retry history is
|
# Every third AI turn was retried once, so the retry history is
|
||||||
# carrying weight a list response must not pay for.
|
# carrying weight a list response must not pay for.
|
||||||
variants=(
|
variants=entries or None,
|
||||||
[{"text": NARRATION, "reasoning": None, "script_state": {},
|
variant_count=len(entries),
|
||||||
"created_at": "2026-01-01T00:00:00"} for _ in range(2)]
|
variant_index=live,
|
||||||
if is_ai and i % 6 == 1 else None
|
# NULL unless --rich. These are the columns a story tree turns
|
||||||
|
# from *before* pictures into *after* ones, so a fixture that
|
||||||
|
# leaves them unset cannot witness that change.
|
||||||
|
state_before=rich_script_state(i) if args.rich else None,
|
||||||
|
world_state_before=(
|
||||||
|
rich_world_state(schema, i) if args.rich else None
|
||||||
),
|
),
|
||||||
variant_count=2 if is_ai and i % 6 == 1 else 0,
|
|
||||||
))
|
))
|
||||||
|
|
||||||
for i in range(args.memories):
|
for i in range(args.memories):
|
||||||
@@ -280,6 +511,11 @@ def build_fixture(args, rng: random.Random) -> tuple[int, int]:
|
|||||||
text=f"{MEMORY_TEXT} ({i})",
|
text=f"{MEMORY_TEXT} ({i})",
|
||||||
source_start=i * memorybank.MEMORY_INTERVAL,
|
source_start=i * memorybank.MEMORY_INTERVAL,
|
||||||
source_end=i * memorybank.MEMORY_INTERVAL + memorybank.MEMORY_INTERVAL - 1,
|
source_end=i * memorybank.MEMORY_INTERVAL + memorybank.MEMORY_INTERVAL - 1,
|
||||||
|
# A bank in play is not uniformly active: some memories are
|
||||||
|
# pinned (always retrieved) and some evicted but kept for the
|
||||||
|
# UI. Both states have to survive being re-attached to nodes.
|
||||||
|
pinned=bool(args.rich and i % 9 == 0),
|
||||||
|
forgotten=bool(args.rich and i % 11 == 5),
|
||||||
)
|
)
|
||||||
# Through the same door the app uses, so the fixture cannot end up
|
# Through the same door the app uses, so the fixture cannot end up
|
||||||
# storing vectors in a shape production never produces.
|
# storing vectors in a shape production never produces.
|
||||||
@@ -288,6 +524,10 @@ def build_fixture(args, rng: random.Random) -> tuple[int, int]:
|
|||||||
)
|
)
|
||||||
db.add(memory)
|
db.add(memory)
|
||||||
|
|
||||||
|
if args.rich:
|
||||||
|
add_second_adventure(db, rng, user)
|
||||||
|
_assert_live_variant_invariant(db, adventure.id)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return adventure.id, user.id
|
return adventure.id, user.id
|
||||||
finally:
|
finally:
|
||||||
@@ -453,6 +693,16 @@ def parse_args(argv=None):
|
|||||||
p.add_argument("--no-embeddings", action="store_true",
|
p.add_argument("--no-embeddings", action="store_true",
|
||||||
help="unset the embedding model — reproduces the round-two "
|
help="unset the embedding model — reproduces the round-two "
|
||||||
"blind spot, where the bank's cost is invisible")
|
"blind spot, where the bank's cost is invisible")
|
||||||
|
p.add_argument("--rich", action="store_true",
|
||||||
|
help="populate the columns the measuring fixture leaves at "
|
||||||
|
"their defaults: state_before/world_state_before, an "
|
||||||
|
"RPG scenario and live world state, adventure scripts, "
|
||||||
|
"non-zero memory/summary cursors, pinned and forgotten "
|
||||||
|
"memories, distinguishable retry attempts, and a second "
|
||||||
|
"adventure. A correctness fixture rather than a scale "
|
||||||
|
"one — prefer it small (--rich --actions 30). Changes "
|
||||||
|
"what the shapes cost, so do not compare a --rich run "
|
||||||
|
"against a plain one")
|
||||||
# Read at import time by _early_keep as well — the database location has
|
# Read at import time by _early_keep as well — the database location has
|
||||||
# to be settled before app.database loads. Declared here so it appears in
|
# to be settled before app.database loads. Declared here so it appears in
|
||||||
# --help and an unknown spelling is still rejected.
|
# --help and an unknown spelling is still rejected.
|
||||||
@@ -487,6 +737,10 @@ def main(argv=None) -> int:
|
|||||||
f"(+{args.snapshot_bytes // 1024} kB snapshot, deferred) · "
|
f"(+{args.snapshot_bytes // 1024} kB snapshot, deferred) · "
|
||||||
f"{args.memories} memories × {EMBEDDING_DIMS} dims · "
|
f"{args.memories} memories × {EMBEDDING_DIMS} dims · "
|
||||||
f"capacity {args.capacity}")
|
f"capacity {args.capacity}")
|
||||||
|
if args.rich:
|
||||||
|
print("RICH: correctness fixture — RPG scenario, per-action state "
|
||||||
|
"snapshots, scripts, cursors, distinguishable attempts, a second "
|
||||||
|
"adventure. Byte figures below are NOT comparable to a plain run.")
|
||||||
if args.no_embeddings:
|
if args.no_embeddings:
|
||||||
print("WARNING: embedding model unset — memory retrieval will return "
|
print("WARNING: embedding model unset — memory retrieval will return "
|
||||||
"early and the bank's cost will not appear below.")
|
"early and the bank's cost will not appear below.")
|
||||||
|
|||||||
@@ -178,6 +178,42 @@ A pre-migration (**schema 45**) fixture is needed too, built by a script rather
|
|||||||
committed as a binary — but its only consumer is the SP1 migration test, so it lands
|
committed as a binary — but its only consumer is the SP1 migration test, so it lands
|
||||||
there.
|
there.
|
||||||
|
|
||||||
|
#### `--rich`: a correctness fixture beside the scale one
|
||||||
|
|
||||||
|
The measuring fixture is sized from production and leaves every column it does not weigh
|
||||||
|
at its default. Checked against a freshly built one, that is exactly the set of columns a
|
||||||
|
tree has to migrate: `state_before`/`world_state_before` NULL on all 600 rows, no
|
||||||
|
scenario and so no RPG layer, no adventure scripts, both cursors 0, and 100 retry
|
||||||
|
histories whose two attempts carry **byte-identical text with `variant_index` always 0** —
|
||||||
|
so "which attempt is live?", the one question SP4's migration answers, had no observable
|
||||||
|
answer.
|
||||||
|
|
||||||
|
`tools.stress_session --rich` fills in precisely those and nothing else:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd backend
|
||||||
|
.venv/Scripts/python.exe -m tools.stress_session --rich --actions 30 --memories 12
|
||||||
|
```
|
||||||
|
|
||||||
|
An RPG scenario built from the real seed schema with a world state played forward (hp
|
||||||
|
100 → 71, flags flipping partway); per-action `state_before`/`world_state_before`
|
||||||
|
snapshots that are monotonic, so a bad rollback reads as a wrong number rather than as
|
||||||
|
nothing; a gold script on the adventure; story cards; non-zero memory and summary
|
||||||
|
cursors with a real `story_summary`; pinned and forgotten memories; retry attempts with
|
||||||
|
distinct texts, counts of 2 *and* 3, and a live attempt that is **often not the last one
|
||||||
|
written**; and a second adventure, so "does this leak across adventures?" is answerable —
|
||||||
|
a branch clause that forgot its adventure would still look correct on a database holding
|
||||||
|
exactly one.
|
||||||
|
|
||||||
|
It is a correctness fixture, so prefer it small: what it is for is variety per row, not
|
||||||
|
rows. **Its byte figures are not comparable to a plain run** and it does not replace the
|
||||||
|
scale fixture — the plain one still holds the egress ceilings, and was re-measured
|
||||||
|
unchanged (1.8 kB, actions 1.7 kB) after `--rich` was added.
|
||||||
|
|
||||||
|
The invariant `text == variants[variant_index]["text"]` holds on every retried row, and
|
||||||
|
is asserted when the fixture is built. SP4's migration reads exactly that to decide which
|
||||||
|
sibling becomes the head.
|
||||||
|
|
||||||
### SP1 — Schema and migration *(no behaviour change)*
|
### SP1 — Schema and migration *(no behaviour change)*
|
||||||
|
|
||||||
| File | Change |
|
| File | Change |
|
||||||
|
|||||||
@@ -357,6 +357,22 @@ cd ../frontend && AIDND_API_PORT=8010 npm run dev # → localhost:5173
|
|||||||
Everything in it is synthetic and no real adventure is read. `*.db` is gitignored, so
|
Everything in it is synthetic and no real adventure is read. `*.db` is gitignored, so
|
||||||
the fixture never lands in a commit.
|
the fixture never lands in a commit.
|
||||||
|
|
||||||
|
**A fixture to check correctness against, rather than bytes.** The measuring fixture
|
||||||
|
leaves every column it does not weigh at its default, which turns out to be exactly the
|
||||||
|
set a story tree has to migrate — `state_before`/`world_state_before` NULL on all 600
|
||||||
|
rows, no RPG scenario, no adventure scripts, both cursors 0, and retry attempts whose
|
||||||
|
text is byte-identical with `variant_index` always 0. `--rich` fills in those and only
|
||||||
|
those:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd backend
|
||||||
|
.venv/Scripts/python.exe -m tools.stress_session --rich --actions 30 --memories 12
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer it small — it exists for variety per row, not for rows. **Its byte figures are
|
||||||
|
not comparable to a plain run**, and it does not replace the scale fixture, which still
|
||||||
|
holds the egress ceilings.
|
||||||
|
|
||||||
On Windows the report's box-drawing characters crash the default cp1252 console;
|
On Windows the report's box-drawing characters crash the default cp1252 console;
|
||||||
prefix with `PYTHONIOENCODING=utf-8`.
|
prefix with `PYTHONIOENCODING=utf-8`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user