Plan to lift the 高考志愿 application out of the generic
reflection-gamerepo into its own well-structured codebase, with a safe old→new transition. Plan only — no code moved yet. Grounded in an actual dependency scan ofmeno-sh/reflection-game@main.
0 · TL;DR
- gaokao is ~3.7k LOC (Python, excl. mini-program); it touches the generic engine in only 3 small seams. It is ~95 % self-contained → separation is low-risk.
- The live ranked engine (
ranked.py) has zero direct dependency on the generic code; the coupling enters only via the scene generator (trajectory.py/demo.py) and the legacyinteractive.py. - Strategy: extract → run old & new in parallel → verify byte/contract parity on golden replays → cut over. The old codebase keeps serving live inference until the new one is verified.
- Target: a
core/engine well under the 5k-LOC soft cap (estimated ~2.0–2.5k), plus one folder per functionality.
1 · Dependency analysis — how coupled is it?
The app lives in reflection-game/gaokao/. Its only ties to the generic engine:
| Seam | Generic module (LOC) | Imported by | What’s actually used |
|---|---|---|---|
| Scene-JSON parse/validate | core.emergent (400) | trajectory.py, interactive.py | parse_emergent_json, validate_emergent_payload, EmergentValidationError |
| State machine | core.state_manager (301) | trajectory.py, interactive.py | StateManager (imported; appears near-unused — no method calls found) |
| Value dimensions | human_modeling.personality (292) | profile.py, voice_profile.py, compare.py | the DIMENSIONS constant |
ranked.py(the live engine) imports onlygaokao.*(sources/prompts/profile) → no generic coupling.- Generic code referenced ≈ 993 LOC across 3 files, but only a handful of functions/one constant are used → the extractable surface is a few hundred LOC + the
DIMENSIONSdata. - Everything else gaokao needs (WTE/ranked, grounding, resolve, profile, prompts, scenes & dimensions data, web server) is already inside
gaokao/.
Verdict: separation = copy 3 small seams into the new core/, then drop the rest of reflection-game (the TRPG plot/, arcs, etc.). No deep entanglement.
2 · How the gaokao game plays out (core flow)
intake (4 pages: 专业选择 / 纠结+宏观思考 / 价值权重+去向 / 确定度) →
rank_start (build 画像 + first scene) →
per-stage loop: WTE / unified_turn picks the next value-dimension (VOI-style) and generates one scene — top vs contender, with grounding-backed takes → user reorders / per-stage AI chat / elicits confidence → rank_next … (until cap or “看结论”) →
rank_end → rank_report (reflective report).
Cross-cutting: grounding layer (阳光高考 facts: major_grounding/uni_employment), resolve (专业@学校 → canonical), profile (load-bearing 画像, incrementally updated each scene).
3 · Proposed structure of the new repo
gaokao-engine/ (new standalone repo)
core/ minimal engine — target < 5k LOC
ranked.py wte.py world-turning engine: dimension selection + scene gen (unified_turn)
profile.py human model: load-bearing 画像 + incremental update
grounding.py resolve.py grounding (阳光高考 facts) + 专业/学校 resolution
prompts.py sources.py
emergent.py ← extracted from core.emergent (scene-JSON parse/validate)
dimensions.py ← the DIMENSIONS list (from human_modeling.personality)
human_modeling/ variants of human models (as reflection-game did)
serving/ web.py the real-time HTTP inference server (kept OUT of core/)
data/ grounding/employment/scenes/dimensions JSON (the factual layers)
developer/ tooling: build_grounding, enrich, enumerate, export, eval/
prompts/ prompts.json (+ defaults)
miniprogram/ the WeChat MP (or split to its own repo — your call)
tests/ golden-replay parity tests
LOC budget: runtime ≈ 2.9k now; move the web server (~1.0k → serving/) and tooling (~0.75k → developer/) out, and core/ lands at ≈ 2.0–2.5k LOC — comfortably under the 5k soft cap, small enough for direct oversight.
4 · Transition plan (the delicate part)
Old keeps serving live until new is verified; then cut over. Your three requirements:
4.1 Preserve running data. The live game persists via the box listener (session_log.jsonl over the cloudflared tunnel, GAOKAO_LOG_SINK). The new engine writes to the same sink with the same event schema (start/stage/chat/end/report) → no data migration; both append to the same log. Freeze the event schema as a doc first. In-flight sessions (sid → state, used by rank_revisit) keep the same persisted format so a cutover never strands a live game.
4.2 New engine bug-free. (a) Golden-transcript replay — capture N real sessions’ full request/response traces from the OLD engine, replay the same inputs through the NEW engine, assert parity (pin the model; compare structure/fields, not exact LLM prose). (b) Contract tests on every endpoint against API.md. (c) Shadow run — mirror a copy of live traffic to the new engine (no user-facing effect), diff outputs until clean.
4.3 Experience exactly the same. The new engine serves the identical API.md contract, so the web + MP frontends are unchanged (just repoint the backend URL). Byte-diff the served rank_page.html and the API responses on the golden set. Cut over only after shadow parity is clean; keep the old deployable hot for instant rollback for N days.
4.5 雨来 contract-parity (hard gate before cutover). 雨来’s platform integrates against our API, so before flipping the yulai deploy to the new repo, verify — as an explicit acceptance checklist:
- Every endpoint 雨来 uses (
resolve_options,rank_start,rank_next,rank_reorder,rank_chat,rank_end,rank_report,rank_revisit,voice_profile) returns byte/shape-identical responses old-vs-new on the golden set. - Auth unchanged: the new yulai deploy carries the same
GAOKAO_API_KEYS(雨来’s existing key) → 401 without / 200 with, exactly as today. - Same URL:
yulai.gaokao.meno.shcut to the new repo (same Render service swaps repo, or DNS → new deploy) → 雨来 changes nothing on their side. - Rollback ready: old yulai deploy kept hot for instant revert. Only flip 雨来’s deploy after all four are green. (雨来 sees zero change.)
4.4 Sequencing. ① extract the 3 seams + assemble core/ (no behavior change) → ② stand up new serving/ on the same data + sink → ③ golden-replay + shadow-run vs the live old engine → ④ cut over at a low-traffic window, old stays as hot rollback → ⑤ decommission old.
5 · Open decisions for you
- Repo name + does the mini-program live in the same repo or its own?
StateManager: it looks near-unused in ranked mode — confirm during extraction whether we drop it entirely (further shrinkscore/).- Web server in
core/orserving/? I proposeserving/, keepingcore/pure engine (better for your <5k oversight goal). - Go order: do you want me to start with the dependency-freeze + extraction (step ①) as a no-risk first PR (new repo, old untouched), then we gate steps ②–⑤ on your review?