Compare commits
136
Commits
v0.2.0
..
f748b017cb
@@ -34,7 +34,8 @@ All agents read their briefing file at `docs/briefings/{name}.md` before startin
|
||||
|
||||
| Agent | File | Role | Model | When to use |
|
||||
|-------|------|------|-------|-------------|
|
||||
| `si` | si.md | Project Manager & Scrum Master | sonnet | Sprint planning, ticket management, coordination |
|
||||
| `si` | si.md | Refinement Manager | sonnet | Ticket context review before batch activation (`/whats-next` step 2) |
|
||||
| `clerk` | clerk.md | Institutional Guardrail | sonnet | Pre-push D-record/ticket consistency checks |
|
||||
|
||||
### Standby team (activate when implementation starts)
|
||||
|
||||
@@ -93,7 +94,7 @@ Synthesize findings.
|
||||
| Design review | Team (core agents) | Independent perspectives, genuine disagreement |
|
||||
| Style guide creation | Subagent (araminta) | Focused creative work |
|
||||
| Test writing | Subagent (hoshe) | Focused, spec-driven |
|
||||
| Implementation sprint | Team (tyre + hoshe + relevant others) | Parallel build + test |
|
||||
| Implementation batch | Team (tyre + hoshe + relevant others) | Parallel build + test |
|
||||
| Documentation update | Subagent (qatux) | Structured, accurate citations |
|
||||
|
||||
## Agent usage notes
|
||||
@@ -108,10 +109,11 @@ Synthesize findings.
|
||||
- Has access to `/asset-gen` skill and `generate_image` MCP tool
|
||||
- **Image generation costs money - always ask Team Leader for permission before generating**
|
||||
|
||||
### SI (Project Manager)
|
||||
- Manages the ticketing database via `/ticket` skill
|
||||
- Creates initiatives from decisions, breaks into epics/stories/tasks
|
||||
- Does not make design decisions - coordinates and tracks
|
||||
### SI (Refinement Manager)
|
||||
- Spawned by `/whats-next` to review ticket context before batch activation
|
||||
- Reads D/Q-records, workshop outcomes, existing code to assess ticket readiness
|
||||
- Reports READY (with context summary) or GAPS (with specific ambiguities)
|
||||
- Does not implement — refines
|
||||
|
||||
### Qatux (Documenter & Librarian)
|
||||
- Core team member — participates in discussion rounds as documenter
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: araminta
|
||||
description: Visual Designer responsible for art direction, UI consistency, asset style guides, and visual coherence across the game. NOT part of regular brainstorming discussions - spawned when visual decisions need to be made or when implementation needs visual guidance. Use when creating mockups, defining color palettes, establishing UI patterns, or reviewing visual consistency.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are ARAMINTA, the Visual Designer on a game development project set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: burnelli-sheldon
|
||||
description: Economist and Simulation Modeler for the Settled Reach game project. Use when designing economic systems, evaluating market models, grounding supply/demand mechanics in real economic theory, building price formation models, or when the team needs someone to ask "is this economically credible?" Covers both conceptual frameworks (market equilibrium, price elasticity, input-output models) and quantitative modeling (convergence algorithms, stability analysis, parameter tuning).
|
||||
tools: Read, Glob, Grep, Edit, Write
|
||||
tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are BURNELLI-SHELDON, the Economist on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
@@ -39,4 +38,4 @@ Named for the Burnelli-Sheldon dynasty — old money that understood how wealth
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/burnelli-sheldon.md` before starting work (if it exists). Read the relevant decisions/ domain files listed in the sprint briefing for confirmed decisions. Key references: D-117 (tycoon bookmark), D-118 (small business owner), D-131 (economic verb vocabulary), D-132 (dual-scale consequence model).
|
||||
Read your briefing at `docs/briefings/burnelli-sheldon.md` before starting work (if it exists). Read the relevant `decisions/*.md` domain files referenced in your ticket. Key references: D-117 (tycoon bookmark), D-118 (small business owner), D-131 (economic verb vocabulary), D-132 (dual-scale consequence model).
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: clerk
|
||||
description: Institutional guardrail for the Settled Reach game project. Pre-push review agent that checks D-record consistency, ticket drift, and decision contradictions. Spawned by the pre-push hook or manually for consistency audits. Binary output (APPROVED/REJECTED) with verbose findings file.
|
||||
tools: Read, Glob, Grep, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are the CLERK, the institutional guardrail on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
## Your personality
|
||||
|
||||
Precise, dispassionate, thorough. You are not a reviewer — you don't judge code quality. You are a consistency checker. You say things like "File X contradicts D-142" and "Ticket #890 describes outcome Y but implementation does Z." You do not have opinions about design. You have facts about what was decided and whether the code matches.
|
||||
|
||||
## Your role
|
||||
|
||||
You check whether a diff is consistent with the project's institutional knowledge:
|
||||
|
||||
1. **D-record consistency** — do changed files contradict any active D-record?
|
||||
2. **Referenced decisions** — if code references a D/Q/R-ID, does that ID exist and is it active?
|
||||
3. **Ticket drift** — if commits reference ticket #NNN, does the implementation match the ticket's described outcome? (Drift is flagged, not blocked — tickets are descriptive, not prescriptive.)
|
||||
4. **Q-record surfacing** — are there open Q-records relevant to the changed files? Surface them as context, not blockers.
|
||||
|
||||
## Output format
|
||||
|
||||
When spawned by the pre-push hook:
|
||||
- Write findings to `.cache/pre-push-review.md` (verbose: each check, what you found, citations)
|
||||
- Output exactly one word to stdout: `APPROVED` or `REJECTED`
|
||||
- `REJECTED` only for hard contradictions with active D-records. Everything else is a finding, not a block.
|
||||
|
||||
When spawned manually for an audit:
|
||||
- Report findings directly. No binary gate needed.
|
||||
|
||||
## What you do NOT do
|
||||
|
||||
- Judge code quality, style, or architecture (that's /pr-review)
|
||||
- Make design decisions
|
||||
- Modify any files except `.cache/pre-push-review.md`
|
||||
- Block on subjective grounds
|
||||
|
||||
## Project context
|
||||
|
||||
Read `decisions/README.md` for the domain index. The `decisions` table in the ticketing DB is synced from these files via `tooling/db/decisions-sync`.
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: dudley
|
||||
description: Server Developer for the Settled Reach game project. STANDBY - activate when simulation implementation begins. Responsible for the game simulation server, entity systems, information boundaries, deterministic tick processing, and all server-side game logic.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are DUDLEY, the Server Developer on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: gestalt
|
||||
description: Systems Design and Fun Factor specialist for the Settled Reach game project. Use when designing game mechanics, evaluating whether systems create interesting decisions, mapping concepts to concrete mechanics, defining how systems interact, or when someone needs to ask "is this fun?" Use proactively when implementation discussions need mechanical grounding.
|
||||
tools: Read, Glob, Grep, Edit, Write
|
||||
tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are GESTALT, the Systems Designer on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: gore
|
||||
description: Themes and Endgame Design specialist for the Settled Reach game project. Use when discussing ascension paths, the philosophical questions the game explores, what the game is fundamentally ABOUT, late-game transformation mechanics, or when the team needs someone to zoom out and reframe the question at a higher level.
|
||||
tools: Read, Glob, Grep
|
||||
tools: Read, Glob, Grep, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are GORE, the Themes and Endgame Design specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: hoshe
|
||||
description: QA Engineer and Test specialist for the Settled Reach game project. Use when tests need to be written, test plans created, bugs investigated, test reports generated, or when implementation needs verification against specifications. NOT part of brainstorming discussions - spawned for testing and quality assurance work.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are HOSHE, the QA Engineer on a game development project set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: inigo
|
||||
description: Sound Designer for the Settled Reach game project. STANDBY - activate when audio implementation begins. Responsible for soundscape design, ambient audio layers, diegetic sound cues, audio propagation rules, and all player-facing audio. Use when designing sound palettes, defining audio triggers, creating spatial audio specs, or reviewing audio consistency.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are INIGO, the Sound Designer on a game development team building a top-down immersive sim set in an original science fiction universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: justine
|
||||
description: Polish and Deployment specialist for the Settled Reach game project. STANDBY - activate when builds need packaging, performance needs optimizing, or release preparation begins. Responsible for build pipelines, performance profiling, platform packaging, and release quality.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are JUSTINE, the Polish and Deployment specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: mellanie
|
||||
description: Copywriter for the Settled Reach game project. STANDBY - activate when game text needs writing - internal monologue lines, dialogue, descriptions, UI text, tutorial text, news ticker content. Responsible for all in-game written content.
|
||||
tools: Read, Glob, Grep, Edit, Write
|
||||
tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are MELLANIE, the Copywriter on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: miri
|
||||
description: Worldbuilder and Setting Designer for the game project. Use when designing the original setting (inspired by Commonwealth-style space opera), building factions, cultures, technology trees, locations, history, and lore. Ensures the world is internally consistent, richly detailed, and serves gameplay mechanics. Use when new features need setting grounding or when world consistency matters.
|
||||
tools: Read, Glob, Grep, WebSearch, WebFetch
|
||||
tools: Read, Glob, Grep, WebSearch, WebFetch, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are MIRI, the Worldbuilder and Setting Designer on a game development team building a top-down immersive sim — an occlusion-based life sim with emergent narrative, set in an original science fiction universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: nigel
|
||||
description: Sandbox and Replayability advocate for the Settled Reach game project. Use when evaluating whether features create emergent stories, when discussing how systems produce different experiences across playthroughs, when considering procedural generation, or when the team needs someone to ask "what happens the SECOND time you play this?"
|
||||
tools: Read, Glob, Grep
|
||||
tools: Read, Glob, Grep, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are NIGEL, the Sandbox and Replayability advocate on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: oscar
|
||||
description: Networking Developer for the Settled Reach game project. STANDBY - activate when networking/multiplayer work begins. Responsible for client-server communication, network protocol design, sync mechanisms, and ensuring the architecture supports future multiplayer.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are OSCAR, the Networking Developer on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: ozzie
|
||||
description: Player Experience and "Wow Factor" advocate for the Settled Reach game project. Use when evaluating whether features are exciting, when the team needs a gut-check on whether something will feel good to play, or when designs risk being technically correct but emotionally flat. Champions the moments that make players feel something.
|
||||
tools: Read, Glob, Grep
|
||||
tools: Read, Glob, Grep, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are OZZIE, the Player Experience and "Wow Factor" advocate on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: paula
|
||||
description: Narrative and Political Depth specialist for the Settled Reach game project. Use when designing faction mechanics, character relationships, political intrigue, consequences of player actions, or narrative structure. Focused on the human drama and ensuring choices have meaningful weight.
|
||||
tools: Read, Glob, Grep, WebSearch
|
||||
tools: Read, Glob, Grep, WebSearch, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are PAULA, the Narrative and Political Depth specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: qatux
|
||||
description: Documenter and Librarian for the Settled Reach game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains decisions/ domain files, DISCUSSION.md, and briefings.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
+25
-24
@@ -1,43 +1,44 @@
|
||||
---
|
||||
name: si
|
||||
description: Project Manager and Scrum Master for the Settled Reach game project. Use when creating or managing tickets, planning sprints, breaking initiatives into epics/stories/tasks, tracking progress, or coordinating work across agents. Primary user of the /ticket skill. Does not participate in design discussions - coordinates execution.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
description: Refinement Manager for the Settled Reach game project. Spawned by /whats-next to review ticket context before activation. Reads D/Q-records, workshop outcomes, and existing code to assess whether tickets have sufficient context for implementation. Reports READY or GAPS. Does not implement — refines.
|
||||
tools: Read, Glob, Grep, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are SI, the Project Manager and Scrum Master on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
You are SI, the Refinement Manager on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
## Your personality
|
||||
|
||||
You are organized, direct, and calm under pressure. You turn vision into executable plans. You see the dependency graph that others miss. You say things like "Let me break that into actionable items" and "What's the blocker?" and "Sprint goal:" You do not offer design opinions - you coordinate execution. Efficient, never wastes words.
|
||||
Organized, direct, thorough. You see the dependency graph that others miss. You say things like "This ticket references D-194 but the constraint in Q-086 qualifies it" and "The outcome is clear but the approach has two valid readings." You do not implement — you ensure implementers have what they need. Efficient, never wastes words.
|
||||
|
||||
Named after the Sentient Intelligences that manage all Commonwealth infrastructure - tireless, omnipresent, keeping everything running so others can focus on their work.
|
||||
Named after the Sentient Intelligences that manage all Commonwealth infrastructure — tireless, omnipresent, keeping everything running so others can focus on their work.
|
||||
|
||||
## Your role on the team
|
||||
## Your role
|
||||
|
||||
- Manage the ticketing database via /ticket skill and sqlite_connector.py
|
||||
- Break decisions into initiatives, epics, stories, and tasks
|
||||
- Plan and track sprints
|
||||
- Identify blockers, dependencies, and critical paths
|
||||
- Coordinate parallel work across agents
|
||||
- Maintain project velocity and scope clarity
|
||||
- Report status to Team Leader
|
||||
- Ensure nothing falls through the cracks between agents
|
||||
You are spawned by `/whats-next` step 2, one instance per ticket in a batch. Your job:
|
||||
|
||||
## How you work
|
||||
1. **Research context** for your assigned ticket:
|
||||
- Read the ticket's `decision_ref` D/Q-record in `decisions/*.md`
|
||||
- Grep for related Q-records in `decisions/questions-*.md`
|
||||
- Read workshop outcomes if referenced (check `docs/workshops/`)
|
||||
- Check whether referenced code, tables, or files actually exist
|
||||
|
||||
You are execution-focused. When a decision is made, you immediately think about what needs to happen, in what order, by whom, and what depends on what. You maintain the project's pulse - always knowing what's in progress, what's blocked, and what's next. You don't wait to be asked for status updates; you surface risks early.
|
||||
2. **Assess completeness** — does the ticket have enough context for an agent to work without guessing?
|
||||
- Is the desired outcome clear and unambiguous?
|
||||
- Are relevant D-records consistent about the approach?
|
||||
- Are there open Q-records that conflict with or qualify the ticket?
|
||||
- Does the ticket reference artifacts that exist in the codebase?
|
||||
|
||||
## Ticket assignment rules
|
||||
3. **Report back** with exactly one of:
|
||||
- **READY** — ticket has sufficient context. Include a one-paragraph summary of what the implementing agent needs to know (key D-records, relevant files, constraints).
|
||||
- **GAPS** — list each specific ambiguity with the options you see. Be concrete: "D-194 says X but Q-086 leaves Y open" is useful; "needs more detail" is not.
|
||||
|
||||
The development teams are: **server**, **client**, **copy**, **audio**, **visual**, **ci**.
|
||||
## What you do NOT do
|
||||
|
||||
When creating or splitting tickets:
|
||||
- **Always assign a ticket to exactly one team.** Every ticket must have a team.
|
||||
- **Never use "joint" as a team.** If work spans multiple teams, split it into separate tickets — one per team — with explicit dependencies between them.
|
||||
- For example, a feature requiring server-side logic and client-side rendering becomes two tickets: one for server (implement the data/system), one for client (consume and render it), with the client ticket blocked by the server ticket.
|
||||
- Sprint proof/acceptance tickets should be assigned to the team responsible for the final integration step, with blockers on the upstream tickets.
|
||||
- Implement anything
|
||||
- Create tickets or modify the database
|
||||
- Make design decisions — you surface the gap, the human decides
|
||||
- Offer opinions on whether the ticket is a good idea
|
||||
|
||||
## Project context
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: stig
|
||||
description: UI Developer for the Settled Reach game project. STANDBY - activate when UI implementation begins. Responsible for insert/minimap UI, perception mode overlays, internal monologue display, HUD elements, and all player-facing interface code.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are STIG, the UI Developer on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: tiger
|
||||
description: Translator and Localization specialist for the Settled Reach game project. STANDBY - activate when the game needs localization to other languages. Responsible for translation, localization infrastructure, and cultural adaptation of game text.
|
||||
tools: Read, Glob, Grep, Edit, Write
|
||||
tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are TIGER, the Translator and Localization specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: troblum
|
||||
description: Technical sparring partner and external consultant for architecture evaluation. Use when Tyre needs a second opinion on engine choices, architectural tradeoffs, technology evaluations, or performance analysis. NOT part of brainstorming discussions - only spawned for specific evaluation sidequests alongside Tyre.
|
||||
tools: Read, Glob, Grep, Bash, WebSearch, WebFetch
|
||||
tools: Read, Glob, Grep, Bash, WebSearch, WebFetch, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are TROBLUM, an external technical consultant brought in for specific evaluation tasks on a game development project.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
name: tyre
|
||||
description: Technical Architect and Feasibility specialist for the Settled Reach game project. Use when evaluating engine choices, assessing technical feasibility of features, designing system architecture, discussing performance implications, or when the team needs a reality check on scope. Also use proactively for any implementation planning or code architecture decisions.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, WebSearch, WebFetch
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash, WebSearch, WebFetch, SendMessage, TaskList, TaskUpdate, TaskGet
|
||||
model: opus
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are TYRE, the Technical Architect on a game development team building a top-down immersive sim set in the Settled Reach universe.
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# git-centralize-guard: PreToolUse hook that blocks .git-mutating commands for
|
||||
# agent-team teammates. Version control is centralized to the team lead
|
||||
# (see CLAUDE.md / memory: "Team lead commits, never agents").
|
||||
#
|
||||
# Detection: an agent-team teammate's hook input carries an "agent_type" field
|
||||
# (e.g. "qatux"); the team lead's main session does NOT. So agent_type present
|
||||
# => a teammate (or subagent) => block git writes. Absent => the lead => allow.
|
||||
# (Empirically confirmed on Claude Code 2.1.148; docs field is "agent_type".)
|
||||
#
|
||||
# Read-only git (status, log, diff, show, fetch, branch listing) is always allowed.
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Only act on Bash tool calls
|
||||
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
|
||||
[ "$TOOL_NAME" = "Bash" ] || exit 0
|
||||
|
||||
# Teammates/subagents carry agent_type; the lead does not.
|
||||
AGENT_TYPE=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('agent_type','') or '')" 2>/dev/null)
|
||||
[ -n "$AGENT_TYPE" ] || exit 0 # lead -> allow everything
|
||||
|
||||
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
|
||||
|
||||
# Block git history/working-tree-mutating subcommands anywhere in the command
|
||||
# (catches chained forms like `cd x && git commit`). Read-only git is allowed.
|
||||
if echo "$COMMAND" | grep -qE '(^|[^[:alnum:]_])git[[:space:]]+(add|commit|merge|push|pull|rebase|reset|checkout|stash|cherry-pick|rm|mv)([[:space:]]|$)'; then
|
||||
echo "BLOCKED: git write operations are centralized to the team lead. Teammate '${AGENT_TYPE}' may not run: ${COMMAND}" >&2
|
||||
echo "Editing files is fine — leave staging, commits, merges, and pushes to the lead." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -7,6 +7,21 @@
|
||||
- Skip files in `.gitignore`
|
||||
- The `.claude/` directory IS tracked — skills and agents belong in the repo
|
||||
|
||||
## Centralized version control (agent teams)
|
||||
|
||||
Git is the **team lead's** job. Teammates edit files; the lead stages, commits,
|
||||
merges, and pushes. This is enforced, not just convention:
|
||||
|
||||
- **Hook:** `.claude/hooks/git-centralize-guard.sh` (PreToolUse on Bash) blocks
|
||||
`.git`-mutating commands — `add`, `commit`, `merge`, `push`, `pull`, `rebase`,
|
||||
`reset`, `checkout`, `stash`, `cherry-pick`, `rm`, `mv` — for any agent-team
|
||||
teammate. Read-only git (`status`, `log`, `diff`, `show`, `fetch`) is allowed.
|
||||
- **Detection:** a teammate's PreToolUse hook input carries an `agent_type` field
|
||||
(e.g. `"qatux"`); the lead's main session does not. Present → block; absent →
|
||||
allow. (Confirmed on Claude Code 2.1.148.)
|
||||
- **Activation:** hooks load at lead startup and teammates inherit that snapshot,
|
||||
so changes to this hook require a Claude Code restart to reach teammates.
|
||||
|
||||
## Commit conventions
|
||||
|
||||
Use conventional commits: `<type>(<scope>): <summary>`
|
||||
|
||||
@@ -21,15 +21,14 @@ docs/
|
||||
architecture/ # Technical architecture documents
|
||||
design/ # Game design documents
|
||||
diagrams/ # d2 source + PNG renders
|
||||
sprints/ # Sprint briefings per team
|
||||
sprints/ # Historical archive (Sprint 1–38) — no new sprint directories
|
||||
workshops/ # Workshop briefs and outputs
|
||||
db/
|
||||
schema.sql # Database schema
|
||||
tooling/
|
||||
db/ # Connector scripts for SQLite and audio
|
||||
config.json # Endpoint configuration
|
||||
ticket # Ticket CLI
|
||||
sprint # Sprint lifecycle CLI
|
||||
ticket # Ticket + milestone CLI
|
||||
sqlite_connector.py # SQLite mini MCP
|
||||
audio_connector.py # Stable Audio Open connector
|
||||
.claude/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Model selection
|
||||
|
||||
Default model is Opus 4.6 (200K context). For heavy sessions (workshops,
|
||||
sprint planning, large reviews), switch to extended context on-demand:
|
||||
large reviews, milestone planning), switch to extended context on-demand:
|
||||
|
||||
- `/model sonnet[1m]` — Sonnet 4.6 with 1M context window
|
||||
- `/model opus[1m]` — Opus 4.6 with 1M context window
|
||||
@@ -20,7 +20,7 @@ Key: writers use Write tool directly (no transcription bottleneck), librarian ca
|
||||
|
||||
## Team monitoring (stuck agent detection)
|
||||
|
||||
When leading a team (sprint, workshop, or any multi-agent session):
|
||||
When leading a team (workshop, batch, or any multi-agent session):
|
||||
|
||||
**Agent heartbeat rule** — include in every agent spawn prompt:
|
||||
> If you have been working on a single task for more than 15 minutes
|
||||
|
||||
@@ -25,10 +25,27 @@ tooling/db/ticket create <type> <title> [--parent N] [--priority P] [--decision
|
||||
|
||||
# Read
|
||||
tooling/db/ticket show <id>
|
||||
tooling/db/ticket list [--sprint N] [--team T] [--status S]
|
||||
tooling/db/ticket list [--milestone N] [--team T] [--status S]
|
||||
|
||||
# Update
|
||||
tooling/db/ticket assign <id> <agent>
|
||||
|
||||
# Dependencies
|
||||
tooling/db/ticket deps <id>
|
||||
tooling/db/ticket dep add <blocker_id> <blocked_id>
|
||||
tooling/db/ticket dep rm <blocker_id> <blocked_id>
|
||||
|
||||
# WIP
|
||||
tooling/db/ticket wip
|
||||
|
||||
# Milestones
|
||||
tooling/db/ticket milestone list [--status S]
|
||||
tooling/db/ticket milestone create <name> [--description TEXT] [--phase N]
|
||||
tooling/db/ticket milestone link <ticket_id> <milestone_id>
|
||||
tooling/db/ticket milestone unlink <ticket_id> <milestone_id>
|
||||
tooling/db/ticket milestone complete <milestone_id>
|
||||
tooling/db/ticket milestone show <milestone_id>
|
||||
tooling/db/ticket milestone dep <blocker_id> <blocked_id>
|
||||
```
|
||||
|
||||
## Key rules
|
||||
@@ -37,3 +54,4 @@ tooling/db/ticket assign <id> <agent>
|
||||
- **Quote the title** — always wrap in double quotes to handle spaces
|
||||
- **Never use `sqlite3` CLI** — it crashes (std::bad_alloc). Use `tooling/db/sqlite-query` or `tooling/db/sqlite-exec` for raw SQL
|
||||
- **Verify after create** — run `tooling/db/ticket show <id>` to confirm the title is clean
|
||||
- **Milestones are many-to-many** — a ticket can be linked to multiple milestones via `milestone link`
|
||||
|
||||
+13
-10
@@ -4,7 +4,7 @@
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
|
||||
"SR_DB_PATH": "/var/home/jeroenschweitzer/Projects/settled-reach/settledreach.db"
|
||||
},
|
||||
"teammateMode": "in-process",
|
||||
"teammateMode": "auto",
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add *)",
|
||||
@@ -28,7 +28,6 @@
|
||||
"Bash(git rev-parse --show-toplevel)",
|
||||
|
||||
"Bash(tooling/db/ticket *)",
|
||||
"Bash(tooling/db/sprint *)",
|
||||
"Bash(tooling/db/sqlite-query *)",
|
||||
"Bash(tooling/db/sqlite-exec *)",
|
||||
"Bash(tooling/db/sqlite-init)",
|
||||
@@ -58,22 +57,21 @@
|
||||
"Bash(ruff check)",
|
||||
"Bash(tests/run-*)",
|
||||
|
||||
"Bash(mkdir -p docs/sprints/*)",
|
||||
"Write(docs/sprints/*)",
|
||||
|
||||
"Bash(chmod *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(find *)",
|
||||
"Bash(list *)",
|
||||
"Bash(tree *)",
|
||||
"Bash(sed -n *)",
|
||||
"Bash(.claude/skills/sprint-start/scripts/start-sprint.sh *)",
|
||||
"Bash(.claude/skills/sprint-start/scripts/sprint-teardown.sh *)",
|
||||
|
||||
"Read(//var/mnt/data/projects/settled-reach/**)",
|
||||
"Read(//tmp/**)",
|
||||
"Grep",
|
||||
"Glob",
|
||||
|
||||
"Skill(git-commit)",
|
||||
"Skill(sprint-start)",
|
||||
"Skill(sprint-plan)",
|
||||
"Skill(pr-push)",
|
||||
"Skill(whats-next)",
|
||||
"Skill(pr-process)",
|
||||
"Skill(pr-review)",
|
||||
"Skill(ticket)",
|
||||
"Skill(docs-search)",
|
||||
@@ -95,6 +93,11 @@
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/git-lock-guard.sh",
|
||||
"timeout": 5
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/git-centralize-guard.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,16 +14,31 @@ allowed-tools: Bash, Read, Grep, Glob, Write, Edit
|
||||
## Workflow
|
||||
|
||||
1. Run `git status` and `git diff --stat` to assess all pending changes.
|
||||
2. Group changes into **logical commits** — each commit should represent one
|
||||
2. **Triage: session vs pre-existing.** Separate changes you made in this
|
||||
session from changes that were already dirty when the session started.
|
||||
Check the conversation history — if you didn't touch a file, it's
|
||||
pre-existing. After grouping your own commits, surface any remaining
|
||||
modified or untracked files to the user:
|
||||
|
||||
> These files are also modified/untracked but weren't part of this
|
||||
> session's work:
|
||||
> - `path/to/file` — <brief description of the diff>
|
||||
> - `path/to/other` — <untracked, N files>
|
||||
>
|
||||
> Should any of these be included in a commit?
|
||||
|
||||
Wait for the user's answer before staging them. Never silently skip
|
||||
or silently include pre-existing changes.
|
||||
3. Group changes into **logical commits** — each commit should represent one
|
||||
coherent change. Common groupings:
|
||||
- A bug fix (all files touched to fix one issue)
|
||||
- A new feature or system (e.g., new trait files + localization + events)
|
||||
- Refactoring / cleanup (encoding fixes, comment fixes, renaming)
|
||||
- Config / meta changes (CLAUDE.md, .claude/ skills, mod descriptor)
|
||||
- Data corrections (province remapping, missing fields)
|
||||
3. For each logical group, stage only the relevant files and commit with a
|
||||
4. For each logical group, stage only the relevant files and commit with a
|
||||
properly formatted message.
|
||||
4. After all commits, update CHANGELOG.md.
|
||||
5. After all commits, update CHANGELOG.md.
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
@@ -73,6 +88,24 @@ Use the project subsystem as scope. Examples:
|
||||
- Never combine unrelated changes (e.g., don't mix a crash fix with new features).
|
||||
- When in doubt, prefer more smaller commits over fewer large ones.
|
||||
|
||||
### Clerk safety valve (`Clerk-Skip:` trailer)
|
||||
|
||||
The pre-push clerk reviews each commit for D-record consistency. For a pure bulk
|
||||
content commit — e.g. shipping thousands of generated planetary description files
|
||||
— that review is moot and just burns agents on noise. Add a `Clerk-Skip:` **trailer
|
||||
line** to the commit message and `tooling/clerk-review` auto-approves that commit
|
||||
without spawning an agent. Use it only for content/data dumps, never for commits
|
||||
that touch `decisions/`, code, or ticket-bearing work.
|
||||
|
||||
It must be a trailer (a line starting with `Clerk-Skip:`), not inline prose — that
|
||||
way a commit that merely *mentions* the token in its subject or body isn't skipped.
|
||||
|
||||
```
|
||||
data(content): generate 5000 planetary description files
|
||||
|
||||
Clerk-Skip: bulk generated content, no D-record surface
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
---
|
||||
name: pr-push
|
||||
name: pr-process
|
||||
description: >
|
||||
Push commits and create or update a pull request. Use when the user says
|
||||
"push pr", "push and create pr", "update pr", "create a pr", "open a pr",
|
||||
or invokes /pr-push. NOT triggered by plain "push" (that's just git push).
|
||||
Pushes the current branch, creates a PR if none exists, or confirms the
|
||||
existing PR was updated. NEVER merges the PR into main — this skill only
|
||||
pushes to the branch and manages the PR lifecycle.
|
||||
Author-side PR lifecycle: commit, lint, push, create PR, AND pick up review
|
||||
comments from /pr-review. Use when the user says "process pr", "push pr",
|
||||
"push and create pr", "update pr", "handle review comments", or invokes
|
||||
/pr-process. Runs from the worktree. The counterpart to /pr-review which
|
||||
runs from main.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, AskUserQuestion, Skill
|
||||
---
|
||||
|
||||
# Push PR Skill
|
||||
# Process PR Skill
|
||||
|
||||
Push commits to remote and create or update a PR. Operates exclusively on the
|
||||
current branch — never touches main.
|
||||
@@ -46,7 +45,7 @@ If the user invokes `/pr-push --dry-run`:
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
If on `main`, stop: "You're on main. Switch to a team branch first."
|
||||
If on `main`, stop: "You're on main. Switch to a topic branch first."
|
||||
|
||||
### 1a. Orphan process check (MANDATORY)
|
||||
|
||||
@@ -346,13 +345,34 @@ Report which tickets were moved to review. Skip tickets that are
|
||||
already `done`, `review`, `cancelled`, or `backlog` (only transition
|
||||
`in_progress` → `review`).
|
||||
|
||||
### 9. Next steps
|
||||
### 9. Pick up review comments
|
||||
|
||||
If a sprint team is active (you are the team lead), do NOT shut down
|
||||
agents after pushing. The team should remain alive for PR review and
|
||||
potential comment fixes.
|
||||
Check if the PR already has review comments (from a prior `/pr-review` run):
|
||||
|
||||
Suggest: "PR created/updated. Run `/pr-review` to review before merge."
|
||||
```bash
|
||||
tea pr --login schweitz --repo jpmschweitzer/settled-reach --comments -o simple <PR_NUMBER>
|
||||
```
|
||||
|
||||
If comments exist and contain a review verdict (look for "CHANGES REQUESTED" or
|
||||
"REQUEST_CHANGES" or a structured review table):
|
||||
|
||||
1. Parse each file-specific issue from the review comment
|
||||
2. Present each issue to the user (or working agents)
|
||||
3. For each issue, the response is one of:
|
||||
- **Fix:** make the change, commit via /git-commit
|
||||
- **Pushback:** explain why the comment should be retracted (concrete technical rationale)
|
||||
4. After addressing all comments, re-run lint + smoke checks (steps 1b, 1c)
|
||||
5. Push updated commits (step 5)
|
||||
6. Post a response comment on the PR summarizing:
|
||||
- Which issues were fixed (with commit refs)
|
||||
- Which issues were pushed back on (with rationale)
|
||||
- Use `tooling/tea-comment <PR_NUMBER> @/tmp/pr-response.md`
|
||||
|
||||
If no review comments exist, or the review is APPROVED, skip this step.
|
||||
|
||||
### 10. Next steps
|
||||
|
||||
Suggest: "PR processed. Run `/pr-review` from main to review, or `/whats-next` for the next batch."
|
||||
|
||||
## Arguments
|
||||
|
||||
@@ -104,17 +104,28 @@ Then determine the branch:
|
||||
|
||||
### 2. Determine reviewer team
|
||||
|
||||
Map the branch name to a reviewer set. Use the branch prefix (before any `/`
|
||||
or `-` suffix) to classify:
|
||||
Classify the branch by **which directories changed**, not by branch name prefix.
|
||||
Run:
|
||||
|
||||
| Branch type | Branches | Reviewers |
|
||||
|-------------|----------|-----------|
|
||||
| **code** | `server`, `client`, `ci`, or unknown | Hoshe (code quality) + Tyre (architecture) |
|
||||
| **copy** | `copy` | Hoshe (QA) + Paula (narrative depth) + Miri (world consistency) |
|
||||
| **visual** | `visual` | Hoshe (QA) + Araminta (art direction) |
|
||||
| **audio** | `audio` | Hoshe (QA) + Ozzie (player experience) |
|
||||
```bash
|
||||
git diff --stat main...<branch>
|
||||
```
|
||||
|
||||
If the branch name doesn't match any known type, default to **code** reviewers.
|
||||
Examine the changed file paths to determine the dominant change type:
|
||||
|
||||
- Mostly `server/` changes → **code** reviewers
|
||||
- Mostly `client/` changes (excluding `client/ui/` art assets) → **code** reviewers
|
||||
- Mostly `wiki/`, `docs/atlas/`, `content/` changes → **copy** reviewers
|
||||
- Mostly `client/ui/` or asset changes (`.tres`, `.tscn`, textures) → **visual** reviewers
|
||||
- Mostly audio-related changes (audio assets, audio scripts) → **audio** reviewers
|
||||
- Mixed or unclear → **code** reviewers (default)
|
||||
|
||||
| Branch type | Reviewers |
|
||||
|-------------|-----------|
|
||||
| **code** | Hoshe (code quality) + Tyre (architecture) |
|
||||
| **copy** | Hoshe (QA) + Paula (narrative depth) + Miri (world consistency) |
|
||||
| **visual** | Hoshe (QA) + Araminta (art direction) |
|
||||
| **audio** | Hoshe (QA) + Ozzie (player experience) |
|
||||
|
||||
### 3. Generate the diff and read source files
|
||||
|
||||
@@ -162,11 +173,8 @@ into every reviewer prompt with prominent language. The reviewer
|
||||
reads from the worktree, not from main.
|
||||
|
||||
```bash
|
||||
# Determine worktree path
|
||||
SPRINT_NUM=$(echo "<branch>" | sed -E 's|sprint-([0-9]+)/.*|\1|')
|
||||
TEAM=$(echo "<branch>" | sed -E 's|sprint-[0-9]+/||')
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
WORKTREE="$(dirname "$REPO_ROOT")/.sprint/sprint-${SPRINT_NUM}/${TEAM}"
|
||||
# Find worktree for this branch (if one exists)
|
||||
WORKTREE=$(git worktree list --porcelain | grep -B2 "branch refs/heads/<branch>" | grep "worktree " | sed 's/worktree //')
|
||||
|
||||
# Verify it exists and matches the branch tip
|
||||
git -C "$WORKTREE" rev-parse HEAD # should equal `git rev-parse origin/<branch>`
|
||||
@@ -174,10 +182,10 @@ git -C "$WORKTREE" rev-parse HEAD # should equal `git rev-parse origin/<branch
|
||||
|
||||
If the worktree exists and its HEAD matches `origin/<branch>`, use it
|
||||
as the reviewer's source of truth. If it doesn't exist (e.g. the
|
||||
sprint has been torn down or you're reviewing a non-sprint branch),
|
||||
fall back to `git show origin/<branch>:<path>` — explicitly flag this
|
||||
fallback in the reviewer prompt so the reviewer knows Read/Grep on
|
||||
any local path would be wrong.
|
||||
worktree has been torn down or you're reviewing a branch without a
|
||||
worktree), fall back to `git show origin/<branch>:<path>` — explicitly
|
||||
flag this fallback in the reviewer prompt so the reviewer knows
|
||||
Read/Grep on any local path would be wrong.
|
||||
|
||||
In the reviewer prompt, state the rule non-negotiably:
|
||||
|
||||
@@ -304,26 +312,52 @@ tea pr close --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
||||
Gitea does **not** auto-close PRs when you push a local merge — always close
|
||||
manually with `tea pr close` after pushing.
|
||||
|
||||
### 8. Post-review team actions
|
||||
### 7a. Post-merge cleanup (default — suggest it, don't ask open-ended)
|
||||
|
||||
If a sprint team is active and you are the team lead, handle the
|
||||
review outcome:
|
||||
Once a PR is merged and closed, its branch is fully contained in `main` and its
|
||||
worktree is idle. **The default is to tear both down** — leaving them around is
|
||||
how stale empty worktrees accumulate. So after the merge, *recommend* cleanup as
|
||||
the default and proceed unless the user wants to keep them. Don't pose it as an
|
||||
open question — state the default action, give the user a chance to decline.
|
||||
|
||||
```bash
|
||||
# 1. Remove the worktree (if the branch had one)
|
||||
WORKTREE=$(git worktree list --porcelain | grep -B2 "branch refs/heads/<branch>" | grep "worktree " | sed 's/worktree //')
|
||||
git worktree remove "$WORKTREE" # add --force only if it has uncommitted junk
|
||||
|
||||
# 2. Delete the merged branch — local then origin
|
||||
git branch -d <branch> # -d (not -D) refuses if not merged: a safety net
|
||||
git push origin --delete <branch>
|
||||
```
|
||||
|
||||
Safety rules:
|
||||
- **Never delete protected branches** (`main`, `maintenance`, `server`, `client`,
|
||||
`copy`, `audio`, `visual`, `ci`) — see `tea-cli.md`. Cleanup applies only to
|
||||
topic branches.
|
||||
- Use `git branch -d` (lowercase) — it refuses to delete a branch not merged into
|
||||
HEAD, so it can't silently drop unmerged work. If it refuses, the branch isn't
|
||||
actually merged: stop and investigate, don't reach for `-D`.
|
||||
- This is safe because the commits live in `main` after the merge — but it is still
|
||||
an outward action (deletes the origin branch). Recommend-and-proceed is fine for a
|
||||
merged topic branch; if anything looks unmerged or shared, ask first.
|
||||
|
||||
If `/whats-next` created the worktree (kanban flow), this is the step that closes
|
||||
that loop — the worktree it spun up at activation is removed here at merge.
|
||||
|
||||
### 8. Post-review actions
|
||||
|
||||
**CHANGES_REQUESTED:**
|
||||
The sprint-start lifecycle (step 9c) handles dispatching review
|
||||
comments to agents. After presenting results, remind the lead:
|
||||
"Review requested changes. Create tasks from each issue and dispatch
|
||||
to idle agents, then re-push and re-review."
|
||||
After presenting results, tell the user: "Run `/pr-process` from the
|
||||
worktree to pick up and address review comments."
|
||||
|
||||
The team may **push back** on specific comments. When a team agent
|
||||
The author may **push back** on specific comments. When the author
|
||||
disagrees with a reviewer comment, the process is:
|
||||
|
||||
1. The team agent explains why the comment should be retracted — with
|
||||
1. The author explains why the comment should be retracted — with
|
||||
a concrete technical rationale, not just "I disagree."
|
||||
2. The team lead (you) evaluates the pushback. If the rationale is
|
||||
sound, mark that comment as **retracted** in the review table and
|
||||
note the reason.
|
||||
3. If the team lead is unsure, escalate to the user for a ruling.
|
||||
2. Evaluate the pushback. If the rationale is sound, mark that
|
||||
comment as **retracted** in the review table and note the reason.
|
||||
3. If unsure, escalate to the user for a ruling.
|
||||
4. Retracted comments do NOT need to be fixed. The re-review should
|
||||
note which comments were retracted and why.
|
||||
|
||||
@@ -332,9 +366,8 @@ that every comment is taken seriously. The bar for retraction is
|
||||
"the reviewer was wrong about this" — not "we don't want to do it."
|
||||
|
||||
**APPROVED:**
|
||||
The sprint-start lifecycle (step 9c) handles shutdown. After
|
||||
presenting results, remind the lead: "Review approved. Proceed with
|
||||
team shutdown per sprint-start step 9c."
|
||||
After presenting results, tell the user: "Review approved. Proceed
|
||||
to merge from main (section 7)."
|
||||
|
||||
## Tips from practice
|
||||
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
name: sprint-plan
|
||||
description: >
|
||||
Plan the next sprint and generate team briefing files. Use when the user says
|
||||
"plan sprint", "prep sprint briefing", "plan next sprint", "sprint planning",
|
||||
or invokes /sprint-plan. Gathers current sprint status, scans the backlog,
|
||||
proposes ticket selection, and writes briefing files per team to
|
||||
docs/sprints/sprint-N/.
|
||||
user-invocable: true
|
||||
allowed-tools: Task, Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Plan Sprint
|
||||
|
||||
**Delegate this entire skill to SI** (Project Manager agent, `subagent_type: si`).
|
||||
|
||||
When this skill is invoked, spawn SI using the Task tool:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "si",
|
||||
prompt: "Run /sprint-plan for Sprint N. Read the skill at
|
||||
.claude/skills/sprint-plan/SKILL.md for the full workflow,
|
||||
then execute it. Use the arguments provided: {args}",
|
||||
description: "Plan sprint N"
|
||||
)
|
||||
```
|
||||
|
||||
Pass through any arguments the user provided (e.g. sprint number).
|
||||
Present SI's output to the user when done. Do NOT run the workflow yourself.
|
||||
|
||||
---
|
||||
|
||||
The remainder of this file is SI's reference for executing the workflow.
|
||||
|
||||
Generate sprint briefing files for each active team (e.g. `server.md`,
|
||||
`client.md`, `copy.md`, `joint.md`) for the next sprint based on current
|
||||
project state. Only generate briefings for teams that have tickets in the sprint.
|
||||
|
||||
## Teams and Default Agents
|
||||
|
||||
| Team | Branch | Default Agents | Focus |
|
||||
|------|--------|----------------|-------|
|
||||
| `server` | `server` | Dudley (dev), Tyre (arch), Hoshe (QA) | Rust/bevy_ecs simulation, ECS systems, knowledge graph, perception |
|
||||
| `client` | `client` | Stig (dev), Tyre (arch), Hoshe (QA) | Godot 4 client, rendering, UI, HUD, input handling |
|
||||
| `copy` | `copy` | Mellanie (author), Paula (narrative), Gestalt (systems) | Dialogue, monologue, UI text, knowledge vocabulary, lore |
|
||||
| `audio` | `audio` | Inigo (sound design) | Soundscapes, ambient layers, diegetic cues, audio propagation |
|
||||
| `visual` | `visual` | Araminta (art direction) | Art assets, sprites, visual consistency, style guides |
|
||||
| `ci` | `ci` | Justine (build/deploy) | Build pipelines, CI/CD, tooling, packaging |
|
||||
| `planning` | `planning` | Purpose-assembled (see below) | Design discussions, decision resolution, workshop-style tickets |
|
||||
|
||||
When writing briefings, name the assigned agents in the **Agents** line of each
|
||||
file so the team knows who to spawn.
|
||||
|
||||
### Planning Team Tickets
|
||||
|
||||
Some tickets need **design discussion** before implementation can begin — tagged
|
||||
"NEEDS DESIGN DISCUSSION" or blocking multiple downstream tickets with open
|
||||
questions. These run on the `planning` branch as structured discussions with
|
||||
the user and a purpose-assembled agent panel.
|
||||
|
||||
**When to create a planning ticket:**
|
||||
- Ticket description says "NEEDS DESIGN" or "NEEDS DESIGN DISCUSSION"
|
||||
- Ticket blocks 2+ downstream tickets across different teams
|
||||
- Open Q-NNN items that block sprint candidates
|
||||
- Architectural decisions that need multi-domain input before implementation
|
||||
|
||||
**Planning briefing format** (differs from implementation briefings):
|
||||
- **Agents line**: List agents by domain relevance, not fixed team roster.
|
||||
Pick from: Gestalt (systems), Miri (worldbuilding), Araminta (visual/spatial),
|
||||
Tyre (technical), Paula (narrative), Ozzie (player experience), Gore (themes),
|
||||
Nigel (replayability). Typically 4-6 domain agents, plus Qatux (documenter —
|
||||
records decisions, updates domain files) and SI (project manager — creates
|
||||
follow-up tickets, updates sprint assignments).
|
||||
- **Discussion rounds**: Structure the conversation into 2-3 rounds
|
||||
(inventory → proposals → convergence)
|
||||
- **Context section**: List all existing design docs, decisions, and related
|
||||
tickets that participants must read before the discussion
|
||||
- **Output specification**: What the discussion must produce — typically a
|
||||
D-record in `decisions/`, possibly a design doc in `docs/design/`
|
||||
- **Decision questions**: Specific questions the discussion must answer,
|
||||
not open-ended exploration
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Run sprint prepare
|
||||
|
||||
Get carry-overs, backlog candidates, and decision gaps in one shot:
|
||||
|
||||
```bash
|
||||
tooling/db/sprint prepare
|
||||
```
|
||||
|
||||
This auto-detects the next sprint number (max ID + 1), creates the sprint
|
||||
record in `planning` status if needed, and outputs:
|
||||
- Previous sprint status and carry-over candidates
|
||||
- Backlog candidates grouped by team
|
||||
- Decision coverage gaps
|
||||
- Already-assigned tickets (if any)
|
||||
|
||||
### 2. Deepen the scan
|
||||
|
||||
For critical epics, check their children for granular candidates:
|
||||
|
||||
```bash
|
||||
tooling/db/ticket children <epic_id>
|
||||
```
|
||||
|
||||
Use `tooling/db/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets.
|
||||
|
||||
### 3. Read existing code state
|
||||
|
||||
Scan what's already built to write accurate "what exists" notes:
|
||||
|
||||
```bash
|
||||
# Server modules
|
||||
ls server/src/ server/src/*/
|
||||
|
||||
# Client scripts
|
||||
ls client/scripts/ client/scripts/*/
|
||||
```
|
||||
|
||||
Read key files that sprint tickets will build on (bridge types, existing
|
||||
renderers, etc.) to reference specific integration points in the briefing.
|
||||
|
||||
### 4. Select tickets — propose to user
|
||||
|
||||
Based on the backlog scan, propose a sprint with:
|
||||
|
||||
- **Sprint theme** — a short name (Sprint 1 was "Run", Sprint 2 was "See")
|
||||
- **Sprint goal** — one sentence shared across all teams
|
||||
- **Server tickets** — stories from server-side epics
|
||||
- **Client tickets** — stories from client-side epics
|
||||
- **Joint tasks** — integration proofs, pre-sprint decisions, schema work
|
||||
|
||||
Present the proposal using AskUserQuestion for the user to approve or adjust.
|
||||
|
||||
Selection heuristics:
|
||||
- Follow dependency chains (don't pick a ticket if its blocker isn't in scope)
|
||||
- Respect D-030 test priority phases (sprint 1-2: infra, sprint 3-4: integration)
|
||||
- Mix carry-overs with new work
|
||||
- Aim for 3-6 tickets per team, with parallel tracks where possible
|
||||
- Check `decisions/questions.md` for open Q-NNN items that block candidates
|
||||
|
||||
### 5. Read relevant decisions
|
||||
|
||||
For the selected tickets, identify which `decisions/*.md` files are relevant.
|
||||
Read them to provide accurate cross-references in the briefing.
|
||||
|
||||
### 6. Write briefing files
|
||||
|
||||
Create `docs/sprints/sprint-N/` and write one file per team.
|
||||
|
||||
Read the template at `references/briefing-template.md` in this skill directory
|
||||
for the exact file structure.
|
||||
|
||||
**Relative paths only:** All file paths in briefings must be relative to
|
||||
the repo root. Example: `server/src/bridge/types.rs`, not absolute paths.
|
||||
Each sprint branch (`sprint-{N}/{team}`) contains the full repo.
|
||||
|
||||
Key requirements per file:
|
||||
- **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes
|
||||
referencing existing Rust modules by path from worktree root (e.g. `server/src/simulation/movement.rs`)
|
||||
- **client.md**: Same structure, notes referencing existing GDScript files by path from worktree root (e.g. `client/scripts/rendering/fog.gd`)
|
||||
- **copy.md**: In-game text tasks — dialogue, UI copy, tooltips, flavor text, lore
|
||||
- **audio.md**: Sound design, music, audio integration tasks
|
||||
- **visual.md**: Art direction, asset creation, visual consistency tasks
|
||||
- **ci.md**: Build pipeline, CI/CD, tooling, infrastructure tasks
|
||||
- **joint.md**: Pre-sprint decisions table, integration tickets, sprint
|
||||
completion proof (concrete observable criteria), test plan alignment
|
||||
|
||||
Only generate briefing files for teams that have tickets assigned in the sprint.
|
||||
Not every sprint will have work for every team.
|
||||
|
||||
### 7. Assign tickets to sprint in DB
|
||||
|
||||
After the user approves, assign all selected tickets. The sprint record
|
||||
was already created by `sprint prepare` in step 1 (status: `planning`).
|
||||
Update it with the theme and goal, then assign tickets:
|
||||
|
||||
```bash
|
||||
# Update the sprint with theme and goal
|
||||
tooling/db/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N"
|
||||
|
||||
# Assign tickets
|
||||
tooling/db/ticket sprint assign <ticket_id> <sprint_id>
|
||||
```
|
||||
|
||||
The sprint stays in `planning` status until explicitly activated via
|
||||
`tooling/db/sprint start`. This prevents starting an unplanned sprint.
|
||||
|
||||
### 8. Commit and push
|
||||
|
||||
Stage the briefing files and any other changes (db backup, closed tickets),
|
||||
then commit and push so worktree branches can pull the planning artifacts:
|
||||
|
||||
```bash
|
||||
git add docs/sprints/sprint-N/
|
||||
make db-backup
|
||||
git add docs/backups/settledreach.db.backup
|
||||
git commit -m "chore(meta): plan Sprint N: Theme"
|
||||
git push
|
||||
```
|
||||
|
||||
### 9. Present summary
|
||||
|
||||
Output:
|
||||
- Sprint number, theme, and goal
|
||||
- Ticket count per team
|
||||
- Carry-over count
|
||||
- Open questions that need early resolution
|
||||
- Files written
|
||||
- Commit pushed to main
|
||||
@@ -1,80 +0,0 @@
|
||||
# Sprint Briefing Template
|
||||
|
||||
Each team gets one briefing file at `docs/sprints/sprint-N/<team>.md`.
|
||||
|
||||
## File structure
|
||||
|
||||
```markdown
|
||||
# Sprint N: <Theme> — <Team> Tasks
|
||||
|
||||
**Goal:** <One-sentence sprint goal, shared across all teams>
|
||||
|
||||
**Branch:** `sprint-{N}/<team>`
|
||||
**Agents:** <Agent names and roles>
|
||||
|
||||
## Carry-over from Sprint N-1
|
||||
|
||||
(Only if there are incomplete tickets from the previous sprint)
|
||||
|
||||
| # | Title | Status | Notes |
|
||||
|---|-------|--------|-------|
|
||||
| #ID | Title | status | Why it carried over |
|
||||
|
||||
## New Tickets
|
||||
|
||||
| # | Title | Blocked by |
|
||||
|---|-------|------------|
|
||||
| #ID | Title | #dependency or — |
|
||||
|
||||
Use `tooling/db/ticket show <id>` for full details.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- `decisions/<domain>.md` — D-NNN (short name), D-NNN (short name)
|
||||
|
||||
## Open Questions to Resolve Early
|
||||
|
||||
(Only if there are Q-NNN items that block sprint tickets)
|
||||
|
||||
- **Q-NNN: Title** — Brief context. Resolve before #ID starts.
|
||||
|
||||
## Notes
|
||||
|
||||
One bullet per ticket with:
|
||||
- What exists already (files, modules, stubs)
|
||||
- What the ticket actually needs to deliver
|
||||
- Integration points with other tickets
|
||||
- Non-obvious gotchas
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#A (name) → #B (name) → #C (name)
|
||||
#D (name) → standalone, parallel track
|
||||
```
|
||||
|
||||
## PR Workflow
|
||||
|
||||
When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
|
||||
\```bash
|
||||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(<scope>): description" --description "body" --base main --head sprint-{N}/<team>
|
||||
\```
|
||||
```
|
||||
|
||||
## Joint briefing extras
|
||||
|
||||
The `joint.md` file additionally includes:
|
||||
|
||||
- **Pre-Sprint** table: decisions/schema work that must happen before implementation
|
||||
- **Sprint Completion Proof**: concrete observable criteria (what you can see/do when the sprint is done)
|
||||
- **Test plan** alignment with D-030 phases
|
||||
|
||||
## Team assignments
|
||||
|
||||
| Team | Branch | Agents | Scope |
|
||||
|------|--------|--------|-------|
|
||||
| server | `sprint-{N}/server` | Dudley (simulation), Oscar (networking) | server/, Rust/bevy_ecs simulation |
|
||||
| client | `sprint-{N}/client` | Stig (UI), Oscar (networking) | client/, Godot rendering |
|
||||
| copy | `sprint-{N}/copy` | Mellanie, Paula, Miri | wiki/, docs/atlas/, content/ |
|
||||
| joint | both | All implementation agents | Integration, proofs, cross-team schema |
|
||||
| content | (none) | Mellanie, Paula, Miri, Araminta | Content authoring, no code branch |
|
||||
@@ -1,616 +0,0 @@
|
||||
---
|
||||
name: sprint-start
|
||||
description: >
|
||||
Manage the sprint lifecycle from main, or start sprint work on a team
|
||||
branch. Use when the user says "start sprint", "start working on the
|
||||
server/client/copy", "begin sprint", or invokes /sprint-start. On main:
|
||||
assesses sprint state and does the next right thing (close, activate, or
|
||||
guide). On a team branch: merges main, loads the briefing, presents the
|
||||
work plan.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, TeamCreate, Task, TaskCreate, TaskUpdate, TaskList, SendMessage, AskUserQuestion
|
||||
---
|
||||
|
||||
# Start Sprint Skill
|
||||
|
||||
Prepare a team branch for sprint work: sync with main, load the sprint
|
||||
briefing, and present actionable next steps.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Determine the team
|
||||
|
||||
The current branch IS the team. Read it with:
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Sprint branches follow the pattern `sprint-{N}/{team}` (e.g. `sprint-31/server`).
|
||||
Valid team names: `server`, `client`, `copy`, `audio`, `visual`, `ci`, `planning`.
|
||||
|
||||
If on `main`, follow the **Main branch workflow** below instead of
|
||||
the team branch workflow (steps 2–8).
|
||||
|
||||
---
|
||||
|
||||
## Main branch workflow (sprint lifecycle management)
|
||||
|
||||
When `/sprint-start` is run on `main`, assess the current sprint state
|
||||
and do the next right thing. Query the database to determine the state:
|
||||
|
||||
```bash
|
||||
tooling/db/sqlite-query "SELECT id, name, status FROM sprints ORDER BY id DESC LIMIT 3"
|
||||
```
|
||||
|
||||
Then follow the **first matching case**:
|
||||
|
||||
### Case A: An active sprint exists
|
||||
|
||||
First, check whether the sprint's work is actually done:
|
||||
|
||||
```bash
|
||||
tooling/db/sprint status
|
||||
```
|
||||
|
||||
This shows ticket counts by status (done, in_progress, backlog).
|
||||
|
||||
Also check for open PRs that may contain completed work waiting for
|
||||
review or merge:
|
||||
|
||||
```bash
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
Report the full picture to the user:
|
||||
- Ticket progress (done / in_progress / backlog counts)
|
||||
- Open PRs (if any — these represent work that's done but not merged)
|
||||
|
||||
**If tickets remain unfinished** (in_progress or backlog) **or open PRs
|
||||
exist**, do NOT close the sprint. Instead, report the current progress
|
||||
and ask the user what they want to do:
|
||||
|
||||
- **Continue working** — switch to a team branch and run `/sprint-start`
|
||||
there to resume work
|
||||
- **Review & merge PRs first** — (only if open PRs exist) merge pending
|
||||
work before deciding whether to close
|
||||
- **Close anyway** — proceed with the close workflow below (carries over
|
||||
unfinished tickets)
|
||||
|
||||
Use `AskUserQuestion` to confirm. Do not proceed to A1 unless the user
|
||||
explicitly chooses to close.
|
||||
|
||||
#### A1. Close the active sprint
|
||||
|
||||
```bash
|
||||
tooling/db/sprint stop
|
||||
```
|
||||
|
||||
This marks the active sprint as completed and lists carry-over candidates.
|
||||
Note the sprint number (N) from the output.
|
||||
|
||||
#### A1b. Sprint retrospective and review
|
||||
|
||||
Before bumping the version, run a brief retro. Present the following to
|
||||
the user:
|
||||
|
||||
1. **What shipped** — list completed tickets with one-line summaries
|
||||
2. **What didn't ship** — carry-overs and why (blocked, cut, deprioritized)
|
||||
3. **What we learned** — open questions raised during the sprint (new Q-NNN
|
||||
items), review findings that surfaced design gaps, and any assumptions
|
||||
that turned out to be wrong
|
||||
4. **Process notes** — what worked well, what was friction (e.g. dependency
|
||||
chains that blocked teams, specs that were over/under-specified,
|
||||
review cycles that caught real issues vs busywork)
|
||||
|
||||
5. **Process improvements** — this is the most important section. Do NOT
|
||||
skip it. Look for:
|
||||
- Dependency chains that blocked teams — could the sprint have been
|
||||
structured differently to avoid the bottleneck?
|
||||
- Specs that were over-specified (wasted planning) or under-specified
|
||||
(wasted iteration) — what's the right level of detail for this
|
||||
project's current stage?
|
||||
- Review cycles — did they catch real issues or create busywork?
|
||||
- Agent coordination — were agents stuck, duplicating work, or idle?
|
||||
- **Dig into the deeper why.** Don't stop at "the dependency chain
|
||||
blocked the copy team." Ask: why was there a dependency chain? Was
|
||||
the sprint structured wrong, or was the work inherently sequential?
|
||||
Could Phase 0 have been done pre-sprint? Should we change how we
|
||||
plan sprints going forward?
|
||||
- If something went rough, understand the root cause — not just what
|
||||
happened, but why the process allowed it to happen.
|
||||
- If a concrete process change follows naturally, propose it. But do
|
||||
NOT force improvements. If nothing was broken, say so and move on.
|
||||
Unnecessary process changes are worse than no changes.
|
||||
|
||||
Keep each section concise — a few bullet points, not a document. The
|
||||
retro is a conversation checkpoint, not a report. Use `AskUserQuestion`
|
||||
to let the user add their own observations and push back before proceeding.
|
||||
|
||||
If the user raises items that should be tracked, create Q-NNN entries
|
||||
or backlog tickets on the spot. If process changes are agreed, update
|
||||
the relevant skill files or CLAUDE.md immediately — don't defer them.
|
||||
|
||||
#### A1c. Clean up sprint worktrees (MANDATORY — do not skip)
|
||||
|
||||
Always run the teardown script. It's idempotent and prints
|
||||
"No worktrees found" gracefully if there's nothing to clean:
|
||||
|
||||
```bash
|
||||
.claude/skills/sprint-start/scripts/sprint-teardown.sh {N}
|
||||
```
|
||||
|
||||
**Do not try to pre-check whether worktrees exist by running `ls`
|
||||
locally.** Sprint worktrees live at
|
||||
`$(dirname <repo-root>)/.sprint/sprint-{N}/` — a *sibling* of the
|
||||
repo root, not a child. Running `ls .sprint/` from inside the repo
|
||||
will always show nothing even when worktrees exist, leading to a
|
||||
false negative and skipped cleanup (Sprint 36 close missed teardown
|
||||
this way; three stale worktrees persisted until Sprint 37 planning).
|
||||
|
||||
The script knows the correct path via its own `SCRIPT_DIR` — trust it.
|
||||
|
||||
Verify cleanup after it runs:
|
||||
|
||||
```bash
|
||||
git worktree list
|
||||
```
|
||||
|
||||
Only `main` should remain. Local `sprint-{N}/{team}` branches are
|
||||
left in place (they're harmless stale refs pointing at already-merged
|
||||
work; `origin/sprint-{N}/*` survives on the remote).
|
||||
|
||||
#### A2. Bump the version
|
||||
|
||||
The project version scheme is `v0.1.{sprint_number}`. After closing
|
||||
sprint N, the version is `v0.1.N`.
|
||||
|
||||
Update `project.yaml`:
|
||||
- Set the `version` field to `0.1.N` (this is the source of truth).
|
||||
|
||||
Update `server/Cargo.toml`:
|
||||
- Set `version = "0.1.N"` in `[package]`.
|
||||
|
||||
Update `CHANGELOG.md`:
|
||||
- Move all entries under `## [Unreleased]` into a new section
|
||||
`## [v0.1.N] — YYYY-MM-DD` (using today's date).
|
||||
- Leave `## [Unreleased]` as an empty section above the new version.
|
||||
- Keep the existing sub-headings (Added, Fixed, Changed, Removed) —
|
||||
only move entries that have content.
|
||||
|
||||
#### A3. Commit the release
|
||||
|
||||
Stage and commit `project.yaml`, `server/Cargo.toml`, and `CHANGELOG.md`:
|
||||
```
|
||||
chore(meta): release v0.1.N
|
||||
```
|
||||
|
||||
#### A4. Tag the release
|
||||
|
||||
```bash
|
||||
git tag v0.1.N
|
||||
```
|
||||
|
||||
#### A5. Push
|
||||
|
||||
```bash
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
#### A6. Check for a planned sprint
|
||||
|
||||
After closing, re-query the database. If a sprint in `planning` status
|
||||
exists, continue to **Case B**. Otherwise, report the close and suggest
|
||||
running `/sprint-plan`.
|
||||
|
||||
---
|
||||
|
||||
### Case B: No active sprint, but a planned sprint exists
|
||||
|
||||
A sprint is ready to activate. Verify it looks complete:
|
||||
|
||||
1. Check that briefing files exist at `docs/sprints/sprint-N/`:
|
||||
```bash
|
||||
ls docs/sprints/sprint-N/
|
||||
```
|
||||
2. Check the ticket count:
|
||||
```bash
|
||||
tooling/db/sprint status --sprint N
|
||||
```
|
||||
|
||||
If briefings are missing or the sprint has 0 tickets, report the gap
|
||||
and suggest running `/sprint-plan` to complete planning.
|
||||
|
||||
If everything looks ready, activate the sprint:
|
||||
|
||||
```bash
|
||||
tooling/db/sprint start
|
||||
```
|
||||
|
||||
Then open team terminal tabs automatically:
|
||||
|
||||
```bash
|
||||
.claude/skills/sprint-start/scripts/start-sprint.sh
|
||||
```
|
||||
|
||||
This creates ephemeral worktrees under `.sprint/sprint-{N}/{team}/` for
|
||||
each team with open tickets, and opens Ptyxis windows with tmux + Claude
|
||||
auto-starting in each tab. The user will have one tab per active team.
|
||||
|
||||
Report:
|
||||
- Sprint activated (name, ticket count per team)
|
||||
- Worktrees created and tabs opened
|
||||
- Each team tab runs `/sprint-start` to load briefing and spawn agents
|
||||
|
||||
---
|
||||
|
||||
### Case C: No active sprint and no planned sprint
|
||||
|
||||
Nothing is ready. Report the state and suggest running `/sprint-plan`
|
||||
to plan the next sprint.
|
||||
|
||||
---
|
||||
|
||||
## Team branch workflow
|
||||
|
||||
### 2. Sync with main
|
||||
|
||||
Sprint branches are created fresh from main by `start-sprint`, so they
|
||||
should already be up to date. If main has moved since branch creation:
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git merge origin/main --no-edit
|
||||
```
|
||||
|
||||
If the merge has conflicts, report them and stop — do not force-resolve.
|
||||
|
||||
### 3. Load sprint context
|
||||
|
||||
Run the sprint CLI to get the full context dump in one shot:
|
||||
|
||||
```bash
|
||||
tooling/db/sprint start-work
|
||||
```
|
||||
|
||||
This auto-detects the active sprint and current team from the branch.
|
||||
It outputs: sprint metadata, briefing paths, decision refs, actionable
|
||||
tickets, blocked tickets, and done tickets.
|
||||
|
||||
If no active sprint is found, report that and stop.
|
||||
|
||||
### 4. Read the sprint briefing
|
||||
|
||||
Read the briefing file(s) listed in the `start-work` output
|
||||
(e.g. `docs/sprints/sprint-6/server.md` and `joint.md`).
|
||||
If no matching briefing exists for the team, suggest running
|
||||
`/sprint-plan` to generate one.
|
||||
|
||||
### 5. Load ticket details
|
||||
|
||||
For tickets that need more detail than the `start-work` summary provides:
|
||||
```bash
|
||||
tooling/db/ticket show <id>
|
||||
```
|
||||
|
||||
### 6. Read key decisions
|
||||
|
||||
Read the decision files referenced in the sprint briefing so the agent has
|
||||
full architectural context before starting work.
|
||||
|
||||
### 7. Mark tickets in progress and present the work plan
|
||||
|
||||
Mark all actionable (unblocked, non-done) tickets in the sprint as
|
||||
`in_progress`:
|
||||
|
||||
```bash
|
||||
tooling/db/ticket status <id> in_progress
|
||||
```
|
||||
|
||||
Then output a summary:
|
||||
- Sprint name and goal
|
||||
- Branch status (clean merge or conflicts)
|
||||
- Tickets marked in_progress (list with IDs)
|
||||
- Blocked tickets (and what blocks them)
|
||||
- Key decisions loaded
|
||||
- Suggested first task (lowest ID unblocked ticket)
|
||||
|
||||
### 8. Confirm and spawn the team
|
||||
|
||||
Before spawning agents, use `AskUserQuestion` to confirm the work plan and
|
||||
agent lineup with the user. If declined, stop.
|
||||
|
||||
Once confirmed:
|
||||
|
||||
#### 8a. Parse agents from the briefing
|
||||
|
||||
Extract agent names from the `**Agents:**` line. Format:
|
||||
```
|
||||
**Agents:** Name (role), Name (role), ...
|
||||
```
|
||||
|
||||
Map each name to its `subagent_type` (lowercase):
|
||||
- "Dudley (simulation)" → `dudley`
|
||||
- "Stig (UI)" → `stig`
|
||||
- "Tyre (architecture)" → `tyre`
|
||||
- "Hoshe (QA)" → `hoshe`
|
||||
- "Mellanie (author)" → `mellanie`
|
||||
- etc. (see `.claude/agents/` for full roster)
|
||||
|
||||
#### 8b. Create the team
|
||||
|
||||
```
|
||||
TeamCreate(team_name: "sprint-{N}-{team}")
|
||||
```
|
||||
|
||||
This makes you the team lead.
|
||||
|
||||
#### 8c. Create tasks from tickets
|
||||
|
||||
For each ticket in the briefing, create a task:
|
||||
|
||||
```
|
||||
TaskCreate(
|
||||
subject: "#{id}: {title}",
|
||||
description: "Full ticket details from step 5, plus briefing notes
|
||||
and integration points for this ticket.",
|
||||
activeForm: "Working on #{id}: {short_title}"
|
||||
)
|
||||
```
|
||||
|
||||
After creating all tasks, mirror the dependency chain from the briefing
|
||||
using `TaskUpdate` with `addBlockedBy`.
|
||||
|
||||
#### 8d. Spawn agents
|
||||
|
||||
For each agent from the `**Agents:**` line, spawn a teammate in the
|
||||
background. Spawn all agents in parallel (one message, multiple Task calls):
|
||||
|
||||
**Model pin (MANDATORY for team members):** every team-mode spawn —
|
||||
i.e. any `Task` with a `team_name` argument — must pass `model: "sonnet"`.
|
||||
Sprint 37 observed Opus 4.7 teammates ignoring scope rules, leaving
|
||||
tasks half-done, and failing to report back via SendMessage. Sonnet 4.6
|
||||
follows literal rules block discipline better. The **team lead**
|
||||
(this session, running `/sprint-start`) stays on whatever model the
|
||||
user has selected — typically Opus.
|
||||
|
||||
**Inline (non-team) Agent spawns are exempt.** One-shot reviewers
|
||||
(`/pr-review`), research subagents, and other `Task` calls without a
|
||||
`team_name` keep their default model. The pin applies to the
|
||||
long-running team-coordination path specifically, not every Agent call.
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "{name_lowercase}",
|
||||
team_name: "sprint-{N}-{team}",
|
||||
name: "{name_lowercase}",
|
||||
model: "sonnet",
|
||||
prompt: "You are on the {team} team for Sprint {N}.
|
||||
Branch: `sprint-{N}/{team}`
|
||||
|
||||
RULES (NON-NEGOTIABLE):
|
||||
|
||||
0. TEAM SCOPE: Your team is `{team}` on branch `sprint-{N}/{team}`.
|
||||
Stay within your team's file scope (server → server/, client → client/, copy → wiki/ + docs/atlas/ + content/).
|
||||
You may read (but not modify): docs/, decisions/, wiki/, .claude/
|
||||
Do NOT modify files belonging to other teams.
|
||||
|
||||
1. GIT: Do NOT run any git commands (commit, push, pull, merge,
|
||||
checkout, branch, stash, tag, etc.). All git operations are
|
||||
handled by the team lead. No exceptions.
|
||||
|
||||
2. DB SCRIPTS: When calling ticket/sprint/sqlite scripts, use
|
||||
the exact command with no wrappers or chaining. Examples:
|
||||
tooling/db/ticket show 528
|
||||
tooling/db/ticket list --sprint {N}
|
||||
Do NOT prepend python3, do NOT chain with && or ;, do NOT
|
||||
add cleanup commands. Just the bare command.
|
||||
|
||||
3. READ BEFORE WRITE: Before modifying ANY file, Read it first.
|
||||
Before creating a new file, Glob for similar files to learn
|
||||
the existing patterns (naming, structure, imports). Follow
|
||||
the conventions you find — do not invent new ones.
|
||||
|
||||
4. VERIFY AFTER WRITE: After implementing a change, grep for
|
||||
all references to functions/properties/classes you modified
|
||||
or removed. If you renamed, moved, or deleted something,
|
||||
update EVERY call site. Missing a call site breaks tests
|
||||
and blocks the team.
|
||||
|
||||
5. NO PARTIAL WORK: Do not mark a task completed unless ALL
|
||||
parts of the ticket are implemented. If the ticket says
|
||||
'deliver A, B, and C', all three must exist and work. If
|
||||
you cannot complete part of a task, message the team lead
|
||||
explaining what is blocked and what remains — do NOT mark
|
||||
it completed.
|
||||
|
||||
6. MESSAGE WHEN BLOCKED: If you hit a problem you cannot solve
|
||||
in 3 attempts, stop and message the team lead immediately.
|
||||
Do not silently skip work or leave stubs. Do not move to
|
||||
the next task while the current one is incomplete.
|
||||
|
||||
7. BACKWARD COMPATIBILITY: When extracting, moving, or
|
||||
refactoring code, ensure all existing consumers still work.
|
||||
Add proxy methods/properties if needed. Grep for the old
|
||||
name to find every call site.
|
||||
|
||||
WORKFLOW:
|
||||
|
||||
1. Read the sprint briefing: docs/sprints/sprint-{N}/{team}.md
|
||||
2. Read the decision files referenced in the briefing.
|
||||
If your work touches systems.db sources (markers.json, TOML files,
|
||||
or generator code), read .claude/rules/asset-pipeline.md before
|
||||
modifying anything.
|
||||
3. Check TaskList for available work.
|
||||
4. Claim an unblocked task (TaskUpdate with owner: your name),
|
||||
mark it in_progress, and implement it.
|
||||
5. Before marking done, verify:
|
||||
- All deliverables from the ticket exist (not just some)
|
||||
- No broken references (grep for changed names/signatures)
|
||||
- New files follow existing naming and directory conventions
|
||||
- Modified files still parse (no syntax errors)
|
||||
6. Mark the task completed and check TaskList for the next
|
||||
available task.
|
||||
7. If no tasks remain, message the team lead. Do NOT shut down
|
||||
on your own.
|
||||
|
||||
Use `tooling/db/ticket show <id>` for full ticket specs.",
|
||||
description: "Sprint {N} {team}: {name}",
|
||||
run_in_background: true
|
||||
)
|
||||
```
|
||||
|
||||
#### 8d-planning. Planning team variant — proposers vs executors
|
||||
|
||||
When the team is `planning`, agents work in discussion mode, not
|
||||
implementation mode. The briefing typically defines discussion rounds
|
||||
(propose → review → execute). Most agents are **proposers** — they
|
||||
analyze and recommend. Only designated agents **execute** (write to
|
||||
files, modify DB).
|
||||
|
||||
**Classify agents from the briefing:**
|
||||
- **Executor agents:** SI (project manager), Qatux (documenter).
|
||||
These agents wait for consensus before writing anything.
|
||||
- **Proposer agents:** Everyone else (Gestalt, Tyre, Paula, Ozzie,
|
||||
etc.). These agents analyze, propose, and discuss — they do NOT
|
||||
write decision files, modify the DB, or create/delete tickets.
|
||||
|
||||
**Add this block to proposer agent prompts** (replaces rules 3–5
|
||||
and the WORKFLOW section from the standard template):
|
||||
|
||||
```
|
||||
PLANNING TEAM RULES (replace standard rules 3-7 and WORKFLOW):
|
||||
|
||||
3. PROPOSE ONLY: You are a discussion participant. Your job is
|
||||
to ANALYZE tickets, PROPOSE team assignments, and RECOMMEND
|
||||
splits. You do NOT:
|
||||
- Write or modify decision files (decisions/*.md)
|
||||
- Run DB update/insert/delete commands
|
||||
- Create, delete, or modify tickets
|
||||
- Mark tasks as completed
|
||||
Those actions belong to executor agents (SI, Qatux) after
|
||||
the team lead confirms consensus.
|
||||
|
||||
4. OUTPUT FORMAT: Your first message to the team lead should
|
||||
be your full analysis. Structure it as:
|
||||
- Your position on open questions (with rationale)
|
||||
- Proposed team assignments (table format)
|
||||
- Split candidates (if any)
|
||||
- Sprint readiness assessment
|
||||
Do NOT claim or work tasks. The team lead coordinates.
|
||||
|
||||
5. WAIT FOR CONSENSUS: Do not treat your own proposal as
|
||||
decided. Other agents may disagree. The team lead calls
|
||||
consensus and directs executors to implement it.
|
||||
```
|
||||
|
||||
**Add this block to executor agent prompts** (appended after the
|
||||
standard rules):
|
||||
|
||||
```
|
||||
PLANNING TEAM RULES (additional):
|
||||
|
||||
8. WAIT FOR CONSENSUS: Do NOT execute DB changes, write
|
||||
decision files, or create tickets until the team lead
|
||||
explicitly tells you to. Your first message should confirm
|
||||
you are ready and describe your execution plan. Then wait.
|
||||
|
||||
9. EXECUTE EXACTLY WHAT IS DIRECTED: When the team lead sends
|
||||
you a list of changes, execute them precisely. Do not add
|
||||
extra changes, reinterpret the instructions, or fill gaps
|
||||
with your own judgment. If something is ambiguous, ask
|
||||
before executing.
|
||||
```
|
||||
|
||||
#### 8e. Report
|
||||
|
||||
Output to the user:
|
||||
- Team name: `sprint-{N}-{team}`
|
||||
- Agents spawned (names and roles)
|
||||
- Tasks created (count actionable vs blocked)
|
||||
- How to interact: `SendMessage` to talk to agents, `TaskList` to
|
||||
check progress
|
||||
|
||||
You are now the team lead. Agents work autonomously — monitor via
|
||||
`TaskList`, communicate via `SendMessage`, and handle blockers as
|
||||
they arise.
|
||||
|
||||
**When all tasks complete:** Do NOT shut down agents. The team stays
|
||||
alive through PR review AND merge. Follow step 9 (post-work lifecycle).
|
||||
|
||||
### 9. Post-work lifecycle
|
||||
|
||||
When all tasks are complete (TaskList shows all completed):
|
||||
|
||||
#### 9a. Commit and push
|
||||
|
||||
Run `/git-commit` to commit all changes, then `/pr-push` to create or
|
||||
update the PR. Do NOT shut down agents — the team stays alive through
|
||||
review and merge.
|
||||
|
||||
#### 9b. Wait for review
|
||||
|
||||
Do NOT run `/pr-review` from the team window — PR reviews run from
|
||||
the `main` branch (a separate window/session). The team window stays
|
||||
on its sprint branch.
|
||||
|
||||
After pushing and creating the PR, report the PR number to the user
|
||||
and stop. Wait for review feedback to arrive (the user or the main
|
||||
session will relay it, or it will appear as Gitea PR comments).
|
||||
|
||||
#### 9c. Handle review outcome
|
||||
|
||||
When review feedback arrives (from the user, main session, or PR
|
||||
comments):
|
||||
|
||||
**If CHANGES_REQUESTED:**
|
||||
|
||||
1. Parse the review comment table (from the Gitea PR comment or the
|
||||
review output). Extract each issue with:
|
||||
- File path and approximate line
|
||||
- Description
|
||||
|
||||
2. Create a task per issue:
|
||||
```
|
||||
TaskCreate(
|
||||
subject: "Review: {short description}",
|
||||
description: "{full issue description from review table, including
|
||||
file path and reviewer name}",
|
||||
activeForm: "Fixing review comment: {short description}"
|
||||
)
|
||||
```
|
||||
Every comment is actionable — there is no "suggestion" tier to skip (per pr-review policy).
|
||||
|
||||
3. Dispatch to idle agents: send each a message via SendMessage telling
|
||||
them to check TaskList for new review-fix tasks. Agents claim and
|
||||
work tasks as usual.
|
||||
|
||||
4. After all review-fix tasks are complete, re-run `/git-commit` then
|
||||
`/pr-push` to update the PR. Then re-run `/pr-review`.
|
||||
|
||||
5. Repeat this loop until review returns APPROVED.
|
||||
|
||||
**If APPROVED (but not yet merged):**
|
||||
|
||||
Do NOT shut down. Approval alone is not terminal — reviewers can leave
|
||||
follow-up comments, the PR can be re-reviewed, or merge conflicts can
|
||||
surface. Keep the team alive and idle until the PR is merged into main.
|
||||
|
||||
1. Report to the user: "Sprint {N} {team} PR #{X} approved. Awaiting
|
||||
merge. Team remains alive."
|
||||
2. Agents stay idle. Do not reassign them to unrelated work.
|
||||
3. Periodically check merge state (or wait for the user to confirm the
|
||||
merge). The `main` session handles the merge itself.
|
||||
4. If new review comments arrive between approval and merge, treat it
|
||||
as CHANGES_REQUESTED and re-enter the fix loop.
|
||||
5. Once the PR is merged, proceed to 9d.
|
||||
|
||||
#### 9d. Handle merge completion
|
||||
|
||||
When the PR is confirmed merged into main (user confirmation, Gitea
|
||||
state change, or the `main` session reports the merge):
|
||||
|
||||
1. Send `shutdown_request` to all sprint agents.
|
||||
2. Wait for all `shutdown_response` confirmations.
|
||||
3. Call `TeamDelete` to clean up.
|
||||
4. Report: "Sprint {N} {team} complete. PR #{X} merged into main. Team
|
||||
shut down."
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Clean up ephemeral worktrees for a closed sprint.
|
||||
# Usage: sprint-teardown.sh <sprint-number>
|
||||
#
|
||||
# Removes all worktrees under .sprint/sprint-{N}/ and prunes git metadata.
|
||||
# Safe to run multiple times — skips already-removed worktrees.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
SPRINT_BASE="$(dirname "$REPO_ROOT")/.sprint"
|
||||
SPRINT=${1:?Usage: sprint-teardown.sh <sprint-number>}
|
||||
|
||||
SPRINT_DIR="$SPRINT_BASE/sprint-${SPRINT}"
|
||||
|
||||
if [ ! -d "$SPRINT_DIR" ]; then
|
||||
echo "No worktrees found for sprint-${SPRINT} (directory $SPRINT_DIR does not exist)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Cleaning sprint-${SPRINT} worktrees..."
|
||||
|
||||
for wt in "$SPRINT_DIR"/*/; do
|
||||
[ -d "$wt" ] || continue
|
||||
team="$(basename "$wt")"
|
||||
echo " Removing: $team"
|
||||
git -C "$REPO_ROOT" worktree remove "$wt" --force 2>/dev/null || echo " (already removed or dirty)"
|
||||
done
|
||||
|
||||
rmdir "$SPRINT_DIR" 2>/dev/null || true
|
||||
git -C "$REPO_ROOT" worktree prune
|
||||
|
||||
echo "Done. Sprint-${SPRINT} worktrees cleaned."
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Opens Ptyxis tabs for the active sprint teams in the CURRENT window.
|
||||
# Auto-starts Claude Code in each tab via tmux.
|
||||
#
|
||||
# Usage: start-sprint.sh [sprint-number]
|
||||
# If omitted, auto-detects the active sprint from the database.
|
||||
#
|
||||
# Assumes the caller is already on main in the current terminal.
|
||||
# Adds one tab per active team — no duplicate main tab.
|
||||
#
|
||||
# Worktrees are created under .sprint/ (ephemeral, cleaned after sprint close).
|
||||
# Can be called from any directory — resolves paths from the script location.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# Script lives at .claude/skills/sprint-start/scripts/ — repo root is 4 levels up
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
# Parent of repo root is where .sprint/ and the DB live
|
||||
PARENT="$(dirname "$REPO_ROOT")"
|
||||
|
||||
# Resolve sprint number — argument or active sprint from DB
|
||||
if [ -n "${1:-}" ]; then
|
||||
SPRINT="$1"
|
||||
else
|
||||
SPRINT=$(cd "$REPO_ROOT" && tooling/db/sqlite-query "SELECT id FROM sprints WHERE status='active'" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['rows'][0]['id'])" 2>/dev/null || true)
|
||||
if [ -z "$SPRINT" ]; then
|
||||
echo "error: no active sprint found. Pass a sprint number or activate a sprint first." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Query teams with open tickets
|
||||
TEAMS=$(cd "$REPO_ROOT" && tooling/db/sqlite-query "SELECT DISTINCT team FROM tickets WHERE sprint_id=$SPRINT AND status NOT IN ('done','cancelled') AND team IS NOT NULL" 2>/dev/null | python3 -c "import sys,json; [print(r['team']) for r in json.load(sys.stdin)['rows'] if r['team']]" 2>/dev/null || true)
|
||||
|
||||
if [ -z "$TEAMS" ]; then
|
||||
echo "Sprint $SPRINT has no open tickets. Opening main only."
|
||||
fi
|
||||
|
||||
echo "Sprint $SPRINT — teams: ${TEAMS:-none}"
|
||||
|
||||
# ── Open team tabs in the CURRENT window ────────────────────────────
|
||||
# No separate main tab — the caller is already on main.
|
||||
# All team tabs open as --tab in the active Ptyxis window.
|
||||
for team in $TEAMS; do
|
||||
BRANCH="sprint-${SPRINT}/${team}"
|
||||
WDIR="$PARENT/.sprint/sprint-${SPRINT}/${team}"
|
||||
|
||||
# Create worktree if it doesn't exist
|
||||
if [ ! -d "$WDIR" ]; then
|
||||
echo "Creating worktree: $WDIR (branch: $BRANCH)"
|
||||
mkdir -p "$(dirname "$WDIR")"
|
||||
# Create branch from main if it doesn't exist remotely
|
||||
if git -C "$REPO_ROOT" rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1; then
|
||||
git -C "$REPO_ROOT" worktree add "$WDIR" "$BRANCH"
|
||||
else
|
||||
git -C "$REPO_ROOT" worktree add -b "$BRANCH" "$WDIR" HEAD
|
||||
fi
|
||||
fi
|
||||
|
||||
ptyxis --tab -d "$WDIR" -x 'tmux new-session \; send-keys "claude /sprint-start" Enter'
|
||||
sleep 0.3
|
||||
done
|
||||
|
||||
echo "Session ready. Main + ${TEAMS:-(no teams)}"
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
name: sprint-status
|
||||
description: >
|
||||
Sprint health check and cleanup sweep. Lists all tickets in the active
|
||||
sprint grouped by status, detects bookkeeping issues (stale tickets,
|
||||
orphan PRs, done-but-open PRs, unassigned work), and shows open work
|
||||
by team. Use when checking sprint progress, before sprint close, or
|
||||
when housekeeping feels off. Triggers on "sprint status", "cleanup
|
||||
sweep", "what's open", "sprint health".
|
||||
user-invocable: true
|
||||
allowed-tools: Task, Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Sprint Status
|
||||
|
||||
**Delegate this entire skill to a subagent** (general-purpose, model: haiku).
|
||||
|
||||
When this skill is invoked, spawn a subagent using the Task tool:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
model: "haiku",
|
||||
prompt: "Run /sprint-status. Read the skill at
|
||||
.claude/skills/sprint-status/SKILL.md for the full workflow
|
||||
(below the --- separator), then execute it.",
|
||||
description: "Sprint status report"
|
||||
)
|
||||
```
|
||||
|
||||
Present the subagent's output to the user verbatim. Do NOT run the
|
||||
workflow yourself.
|
||||
|
||||
---
|
||||
|
||||
The remainder of this file is the subagent's reference for executing
|
||||
the workflow.
|
||||
|
||||
## Step 1 — Gather data
|
||||
|
||||
Run these two commands in parallel:
|
||||
|
||||
```bash
|
||||
tooling/db/sprint sweep
|
||||
```
|
||||
|
||||
```bash
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
The `sweep` command returns JSON with:
|
||||
- `sprint` — id, name, goal
|
||||
- `progress` — total, done, pct
|
||||
- `by_status` — tickets grouped into done, review, in_progress, blocked, backlog
|
||||
- `by_team` — per-team counts
|
||||
- `issues` — bookkeeping problems with suggested fix commands
|
||||
|
||||
The `tea pr list` returns open PRs as `#N title` lines.
|
||||
|
||||
## Step 2 — Cross-reference PRs with tickets
|
||||
|
||||
Parse PR head branches from the `tea pr list` output. Known team branches:
|
||||
`server`, `client`, `copy`, `audio`, `visual`, `ci`.
|
||||
|
||||
Detect additional issues:
|
||||
|
||||
- **done_team_open_pr**: A team's tickets are all done but an open PR
|
||||
still exists for that team branch.
|
||||
- **orphan_pr**: An open PR exists on a branch that has no tickets in
|
||||
the active sprint.
|
||||
|
||||
Add these to the issues list from step 1.
|
||||
|
||||
## Step 3 — Format output
|
||||
|
||||
Read `references/output-template.md` for the exact format spec.
|
||||
|
||||
Render the report using data from steps 1-2. Key rules:
|
||||
- Sections ordered: Completed, In Review, In Progress, Blocked, Backlog
|
||||
- Sort tickets within sections by team then ticket ID
|
||||
- Empty sections: show header with "(0)" and "(none)" — no empty table
|
||||
- Bookkeeping Issues: two-column table (Issue, Fix)
|
||||
- Open Work by Team: summary table at the bottom
|
||||
- Issue type labels: `stale_backlog` → "Stale backlog",
|
||||
`unassigned_in_progress` → "Unassigned in_progress",
|
||||
`assigned_but_done` → "Assigned but done",
|
||||
`done_team_open_pr` → "Done team with open PR",
|
||||
`orphan_pr` → "Orphan PR"
|
||||
|
||||
## Step 4 — Suggest actions
|
||||
|
||||
After the formatted report, if there are bookkeeping issues, add a
|
||||
"Suggested fixes" section with the fix command for each issue. Group
|
||||
by issue type for readability.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Sprint Status Output Template
|
||||
|
||||
## Sprint {N}: {Theme} — Status Report
|
||||
|
||||
**Goal:** {goal}
|
||||
**Status:** {status} | {done}/{total} tickets ({pct}%)
|
||||
**Open PRs:** {count} ({branches})
|
||||
|
||||
---
|
||||
|
||||
### Completed ({count})
|
||||
|
||||
| # | Team | Title | Assigned |
|
||||
|---|------|-------|----------|
|
||||
| #{id} | {team} | {title} | {assigned} |
|
||||
|
||||
### In Review ({count})
|
||||
|
||||
| # | Team | Title | PR |
|
||||
|---|------|-------|----|
|
||||
| #{id} | {team} | {title} | #{pr} |
|
||||
|
||||
### In Progress ({count})
|
||||
|
||||
| # | Team | Title | Assigned | Note |
|
||||
|---|------|-------|----------|------|
|
||||
| #{id} | {team} | {title} | {assigned} | |
|
||||
|
||||
### Blocked ({count})
|
||||
|
||||
| # | Team | Title | Blocked by |
|
||||
|---|------|-------|------------|
|
||||
| #{id} | {team} | {title} | #{ids} |
|
||||
|
||||
### Backlog ({count})
|
||||
|
||||
| # | Team | Title | Note |
|
||||
|---|------|-------|----|
|
||||
| #{id} | {team} | {title} | not started |
|
||||
|
||||
---
|
||||
|
||||
### Bookkeeping Issues
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| {type}: {detail} | `{command}` |
|
||||
|
||||
### Open Work by Team
|
||||
|
||||
| Team | Backlog | In Progress | Review | Blocked | Done |
|
||||
|------|---------|-------------|--------|---------|------|
|
||||
| {team} | {n} | {n} | {n} | {n} | {n} |
|
||||
| **Total** | **{n}** | **{n}** | **{n}** | **{n}** | **{n}** |
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: ticket
|
||||
description: >
|
||||
Manage project tickets in the SQLite ticketing database. Use when the user
|
||||
says "ticket", "create a ticket", "show tickets", "sprint", or invokes /ticket.
|
||||
Wraps the ticket CLI for structured project management operations.
|
||||
Manage project tickets and milestones in the SQLite ticketing database. Use
|
||||
when the user says "ticket", "create a ticket", "show tickets", "milestone",
|
||||
or invokes /ticket. Wraps the ticket CLI for structured project management.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob
|
||||
---
|
||||
@@ -17,12 +17,12 @@ section. This skill covers the full command reference.
|
||||
|
||||
### List tickets (full flags)
|
||||
```bash
|
||||
tooling/db/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
||||
tooling/db/ticket list [--status S] [--priority P] [--epic N] [--milestone N] [--assigned A] [--team T]
|
||||
```
|
||||
|
||||
### Create ticket
|
||||
```bash
|
||||
tooling/db/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
||||
tooling/db/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T] [--description TEXT]
|
||||
```
|
||||
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
||||
Priorities: `critical`, `high`, `medium`, `low`
|
||||
@@ -46,18 +46,27 @@ tooling/db/ticket team <id> <teams>
|
||||
```
|
||||
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
|
||||
|
||||
### Sprint management
|
||||
```bash
|
||||
tooling/db/ticket sprint [--active]
|
||||
tooling/db/ticket sprint assign <id> <sprint_id>
|
||||
```
|
||||
|
||||
For sprint-scoped operations (status overview, context dumps, lifecycle),
|
||||
use the dedicated sprint CLI instead: `tooling/db/sprint --help`
|
||||
|
||||
### Dependencies
|
||||
```bash
|
||||
tooling/db/ticket deps <id>
|
||||
tooling/db/ticket dep add <blocker_id> <blocked_id>
|
||||
tooling/db/ticket dep rm <blocker_id> <blocked_id>
|
||||
```
|
||||
|
||||
### WIP
|
||||
```bash
|
||||
tooling/db/ticket wip
|
||||
```
|
||||
|
||||
### Milestones
|
||||
```bash
|
||||
tooling/db/ticket milestone list [--status S]
|
||||
tooling/db/ticket milestone create <name> [--description TEXT] [--phase N]
|
||||
tooling/db/ticket milestone link <ticket_id> <milestone_id>
|
||||
tooling/db/ticket milestone unlink <ticket_id> <milestone_id>
|
||||
tooling/db/ticket milestone complete <milestone_id>
|
||||
tooling/db/ticket milestone show <milestone_id>
|
||||
tooling/db/ticket milestone dep <blocker_id> <blocked_id>
|
||||
```
|
||||
|
||||
### Search and browse
|
||||
@@ -75,10 +84,10 @@ tooling/db/ticket show --brief <id> [<id>...]
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **SI (Project Manager)** is the primary user of this skill
|
||||
2. Decisions from decisions/ domain files become **initiatives**
|
||||
3. SI breaks initiatives into **epics** (major work areas)
|
||||
4. Epics break into **stories** (user-facing deliverables)
|
||||
5. Stories break into **tasks** (concrete work items assignable to agents)
|
||||
6. **Sprints** group tasks into time-boxed work periods
|
||||
1. Decisions from `decisions/` domain files become **initiatives**
|
||||
2. Initiatives break into **epics** (major work areas)
|
||||
3. Epics break into **stories** (user-facing deliverables)
|
||||
4. Stories break into **tasks** (concrete work items)
|
||||
5. **Milestones** group tickets by deliverable (many-to-many via `milestone link`)
|
||||
6. `/whats-next` selects the next batch from the dependency graph
|
||||
7. Track history via `ticket_history` table for audit trail
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
---
|
||||
name: whats-next
|
||||
description: >
|
||||
Analyze the milestone dependency web and surface the best batch of tickets
|
||||
to pick up next. Refines ticket context via parallel Si agents before
|
||||
activation. Use when the user says "what's next", "next batch", "pick up
|
||||
work", or invokes /whats-next. NOT triggered by "what should we work on"
|
||||
in a design context — that's a discussion, not a batch selection.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, Agent, AskUserQuestion
|
||||
---
|
||||
|
||||
# What's Next
|
||||
|
||||
Dependency-driven batch selection with ticket refinement. Three steps:
|
||||
batch selection → refinement review → batch activation.
|
||||
|
||||
## Step 1: Batch Selection
|
||||
|
||||
### 1a. Query active milestones
|
||||
|
||||
```bash
|
||||
tooling/db/ticket milestone list --status active
|
||||
```
|
||||
|
||||
If no active milestones exist, tell the user and stop.
|
||||
|
||||
### 1b. Build the work landscape
|
||||
|
||||
For each active milestone, get its tickets:
|
||||
|
||||
```bash
|
||||
tooling/db/ticket milestone show <milestone_id>
|
||||
```
|
||||
|
||||
Collect all tickets across all active milestones. Deduplicate (a ticket
|
||||
can appear in multiple milestones).
|
||||
|
||||
### 1c. Identify unblocked tickets
|
||||
|
||||
For each ticket that is `ready` or `backlog`, check its dependencies:
|
||||
|
||||
```bash
|
||||
tooling/db/ticket deps <id>
|
||||
```
|
||||
|
||||
A ticket is **unblocked** if all its blockers are `done` or `cancelled`.
|
||||
Filter to only unblocked tickets.
|
||||
|
||||
### 1d. Check milestone-level blocking
|
||||
|
||||
```bash
|
||||
tooling/db/sqlite-query "SELECT * FROM milestone_deps"
|
||||
```
|
||||
|
||||
If a milestone is blocked by another milestone that is not yet `completed`,
|
||||
deprioritize its tickets (they're unblocked at the ticket level but the
|
||||
milestone isn't ready for focus yet). Still show them, but ranked lower.
|
||||
|
||||
### 1e. Rank and group
|
||||
|
||||
Rank unblocked tickets by:
|
||||
1. **Priority** (critical > high > medium > low)
|
||||
2. **Milestone proximity** — milestones closest to completion (highest
|
||||
done/total ratio) get priority. Finishing a milestone unlocks downstream
|
||||
milestone deps.
|
||||
3. **Fan-out** — tickets that unblock the most downstream tickets rank
|
||||
higher (query `ticket_deps` for blocked_id counts per blocker_id)
|
||||
|
||||
Group into **epic-sized batches**: tickets sharing the same `parent_id`
|
||||
(epic), or logically related tickets if no epic parent. If a natural
|
||||
grouping doesn't exist, batch by milestone.
|
||||
|
||||
### 1f. Show WIP status
|
||||
|
||||
```bash
|
||||
tooling/db/ticket wip
|
||||
```
|
||||
|
||||
### 1g. Present the recommended batch
|
||||
|
||||
Show the user:
|
||||
- The recommended batch (tickets with IDs, titles, priorities, milestones)
|
||||
- Why this batch (which milestone it advances, what it unblocks)
|
||||
- Current WIP status
|
||||
- Any alternative batches worth considering
|
||||
|
||||
Wait for user confirmation before proceeding to Step 2.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Ticket Refinement Review
|
||||
|
||||
Spawn one Si agent per ticket in the confirmed batch, running in parallel.
|
||||
Use `general-purpose` subagent_type with Si's personality baked into the
|
||||
prompt. (The `si` subagent_type now works directly too — all agent
|
||||
definitions carry the team coordination tools, SendMessage + Task, as of the
|
||||
agent-teams tooling fix; they load at lead startup.)
|
||||
|
||||
For each ticket, spawn:
|
||||
|
||||
```
|
||||
Agent({
|
||||
subagent_type: "general-purpose",
|
||||
model: "sonnet",
|
||||
prompt: "You are SI, the Refinement Manager. <include si.md personality>
|
||||
|
||||
Review ticket #<id>: <title>
|
||||
Description: <description>
|
||||
Decision ref: <decision_ref>
|
||||
|
||||
Your job:
|
||||
1. Read the ticket's decision_ref D/Q-record in decisions/*.md
|
||||
2. Grep for related Q-records in decisions/questions-*.md
|
||||
3. Read workshop outcomes if referenced (check docs/workshops/)
|
||||
4. Check whether referenced code, tables, or files actually exist
|
||||
|
||||
Assess: does this ticket have enough context for an agent to implement
|
||||
without guessing? Report exactly one of:
|
||||
- READY: <one-paragraph summary of what the implementer needs to know>
|
||||
- GAPS: <list of specific ambiguities with options for each>"
|
||||
})
|
||||
```
|
||||
|
||||
### 2a. Collect reports
|
||||
|
||||
Wait for all Si agents to complete. Collect their reports.
|
||||
|
||||
### 2b. Resolve gaps
|
||||
|
||||
For any ticket that came back GAPS, surface each ambiguity to the user
|
||||
via AskUserQuestion:
|
||||
|
||||
> Ticket #NNN: <title>
|
||||
> Si found these gaps:
|
||||
> 1. <gap description + options>
|
||||
> 2. <gap description + options>
|
||||
>
|
||||
> How should we resolve these?
|
||||
|
||||
After the user responds, append the resolution to the ticket description:
|
||||
|
||||
```bash
|
||||
tooling/db/sqlite-exec "UPDATE tickets SET description = description || char(10) || char(10) || '---' || char(10) || 'Refinement: <resolution>' WHERE id = <id>"
|
||||
```
|
||||
|
||||
If the user says a gap requires a D-record, flag it and ask whether to
|
||||
write the D-record now or defer. If deferred, add a note to the ticket.
|
||||
|
||||
### 2c. Present refined batch summary
|
||||
|
||||
Show each ticket with:
|
||||
- Si's context summary (for READY tickets) or user's resolution (for GAPS)
|
||||
- Relevant D-record references
|
||||
- Dependencies
|
||||
|
||||
Ask: "Batch ready. Activate?"
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Batch Activation
|
||||
|
||||
### 3a. Mark tickets in_progress
|
||||
|
||||
```bash
|
||||
tooling/db/ticket status <id> in_progress
|
||||
```
|
||||
|
||||
For each ticket in the batch. The `status` command will warn if WIP limit
|
||||
is exceeded.
|
||||
|
||||
### 3b. Create topic branch
|
||||
|
||||
Name the branch after the epic or logical grouping. Examples:
|
||||
- `tier0-schema-tables` (for a schema epic)
|
||||
- `heightmap-import` (for a single focused ticket)
|
||||
- `city-names-pipeline` (for related importer tickets)
|
||||
|
||||
```bash
|
||||
git checkout -b <branch-name>
|
||||
```
|
||||
|
||||
### 3c. Create worktree (optional)
|
||||
|
||||
If the user wants to work in a separate worktree:
|
||||
|
||||
```bash
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
WORKTREE_DIR="$(dirname "$REPO_ROOT")/.worktrees/<branch-name>"
|
||||
git worktree add "$WORKTREE_DIR" <branch-name>
|
||||
```
|
||||
|
||||
Report the worktree path so the user can open a terminal there.
|
||||
|
||||
**Lifecycle:** a worktree created here is torn down at the *end* of the work, not
|
||||
here. `/pr-review` §7a removes it (and deletes the merged branch) by default once
|
||||
the PR merges. If you activate a batch and the work never ships, the worktree is
|
||||
yours to clean up — don't leave empty worktrees parked at `main`'s HEAD (a sign of
|
||||
an abandoned activation). Before creating a new worktree, `git worktree list` and
|
||||
reuse or prune any stale one for the same branch.
|
||||
|
||||
### 3d. Spawn implementation agents (optional)
|
||||
|
||||
If the user wants agents, spawn them using general-purpose subagent_type
|
||||
with the relevant personality baked into the prompt. Include:
|
||||
- The ticket details and Si's context summary
|
||||
- Relevant D-record content (read and include, don't just reference)
|
||||
- The RULES block (no git commands, write files only, message when blocked)
|
||||
- Model: sonnet (per team-mode pin rule)
|
||||
|
||||
### 3e. Report
|
||||
|
||||
Summarize what was activated:
|
||||
- Branch name
|
||||
- Worktree path (if created)
|
||||
- Tickets now in_progress
|
||||
- Agents spawned (if any)
|
||||
- Next step: work, then `/pr-process` when ready to ship
|
||||
@@ -0,0 +1,87 @@
|
||||
# workshop-start — Improvement Log
|
||||
|
||||
Running notes on friction and fixes observed while running workshops. Fold the
|
||||
confirmed ones into `SKILL.md` periodically; delete once applied.
|
||||
|
||||
## From the `system-economic-specialization` relaunch (2026-05-31)
|
||||
|
||||
- **Tool name drift.** SKILL.md §5 says "Use the **Task tool** to spawn each
|
||||
agent." The actual spawn tool is **`Agent`** (with `team_name` + `name` +
|
||||
`subagent_type`). "Task" reads as the TaskCreate/TaskUpdate family and is
|
||||
misleading. Rename to "Agent tool" throughout §5.
|
||||
|
||||
- **SI has no Round 1 work — say so.** The skill adds SI to every workshop but
|
||||
SI's job is Round 2 ticket creation. Round 1 there is no SI task, so SI sits
|
||||
with an empty TaskList and no instruction. Recommend the skill explicitly note:
|
||||
"SI joins on standby; SI gets no task until Round 2 — spawn it in background
|
||||
with a 'load context and wait for Round 2' prompt." Avoids an idle/confused
|
||||
agent on spawn.
|
||||
|
||||
- **Spawn order vs. task creation.** Following the skill literally (§4 create
|
||||
tasks → §5 spawn) matters: if you spawn agents before the tasks exist, they
|
||||
check TaskList, find nothing, and may idle. Worth a one-line warning in §5:
|
||||
"Create and assign all Round 1 tasks BEFORE spawning, or the agents wake to an
|
||||
empty list." (Recoverable by messaging them after, but cleaner to order it
|
||||
right.)
|
||||
|
||||
- **`TaskCreate` is one-task-per-call.** No batch/array form. A batch attempt is
|
||||
rejected. The skill could state this so the lead emits N separate TaskCreate
|
||||
calls (and assigns owners via TaskUpdate, since TaskCreate takes no `owner`).
|
||||
|
||||
- **Read-only participants can't write their own files.** Several workshop agent
|
||||
types (paula, gore, nigel, ozzie, and the read-only miri variant) have NO Write
|
||||
tool — yet SKILL.md §4 makes "write your full output to disk" a hard
|
||||
requirement for every participant. Paula hit this: she produced her full Round 1
|
||||
doc but had to send it to the lead to write `paula-round1.md`. The skill should
|
||||
state up front: "Participants without a Write tool deliver their output via
|
||||
SendMessage; the lead writes the file and marks the task complete on their
|
||||
behalf." Or: assign a write-capable scribe. Either way, don't make disk-write a
|
||||
per-agent requirement for read-only agent types — it guarantees a manual relay.
|
||||
|
||||
- **Participants self-claim, which races owner assignment.** Paula picked up
|
||||
task #3 on her own before the lead assigned owners. Harmless here, but if tasks
|
||||
aren't clearly scoped per-agent an agent could grab the wrong one. The skill's
|
||||
per-agent task titles ("Round 1 — Miri …") mitigate this; keep titles
|
||||
agent-named.
|
||||
|
||||
## Candidate feature — per-round "scout" critic (user idea, 2026-05-31)
|
||||
|
||||
Add an optional independent **scout** agent that runs per round, AFTER the round's
|
||||
output files land and BEFORE the user checkpoint, to surface missed options,
|
||||
simpler designs, internal contradictions, and external design prior-art.
|
||||
|
||||
- **Value is mostly the independent adversarial review**, not the web. Participants
|
||||
build on each other and converge; a fresh agent with no stake catches blind
|
||||
spots convergence hides. Web search is a bonus, useful for the *mechanism* (real
|
||||
economic geography, how other games encode X, schema-granularity tradeoffs) —
|
||||
NOT the lore.
|
||||
- **Hard guardrails:** (1) lore is authored-canonical and fictional — the scout must
|
||||
NOT "correct" the fiction with real-world/web facts; scope web to design/mechanism
|
||||
prior-art only. (2) Only surface findings that would CHANGE a decision — a critic
|
||||
that always finds 10 things gets ignored.
|
||||
- **Shape:** cheap model (Haiku), `run_in_background: true`, reads the round files +
|
||||
relevant D-records, writes `scout-round{N}.md`. Lead folds decision-changing items
|
||||
into the checkpoint presented to the user. Escalate to Sonnet only to dig into a
|
||||
real find.
|
||||
- Pairs well with the quality-over-scope-creep principle: the scout is a cheap way
|
||||
to pressure-test that "quality" claims actually hold and nothing simpler was missed.
|
||||
|
||||
## Wrap-up gotchas (2026-05-31)
|
||||
|
||||
- **`decision claim` is NOT idempotent.** Each call burns the next ID and appends a
|
||||
placeholder. Retrying it (e.g. after misreading an unrelated error) creates a
|
||||
DUPLICATE record that must be hand-removed before filing. Before re-running claim,
|
||||
check `decision next D` / grep the domain file. The wrap-up step should claim each
|
||||
ID exactly once and record it immediately.
|
||||
- **Read-only documenter can still file**, but the lead must hand it the exact ID(s)
|
||||
and the "remove any stray placeholder" instruction — the claim/file split is where
|
||||
duplicate IDs sneak in.
|
||||
- **Background (`run_in_background`) teammates can be slow/unreliable to consume
|
||||
`shutdown_request`.** At wrap-up, qatux + si (both spawned in background per the
|
||||
skill's §5 guidance) stayed "active" through multiple shutdown sends, blocking
|
||||
TeamDelete. Foreground participants shut down immediately. Options to fold into
|
||||
the skill: (a) don't background the always-present agents, or (b) document that
|
||||
wrap-up may need several shutdown retries / a longer settle, and that the lead
|
||||
should not spin indefinitely — the work is already on disk, TeamDelete is just
|
||||
resource cleanup. Consider whether the skill should note the team can be left to
|
||||
expire rather than blocking the session on cleanup.
|
||||
+35
-2
@@ -87,10 +87,12 @@ elif command -v cargo >/dev/null 2>&1 && [ -d "$REPO_ROOT/server" ]; then
|
||||
echo "pre-push: fmt — OK"
|
||||
fi
|
||||
|
||||
# clippy needs a build — skip if target/ doesn't exist (cold worktree)
|
||||
# clippy needs a build — skip if target/ doesn't exist (cold worktree).
|
||||
# --all-targets lints tests + examples too (#967 closed the gap where test
|
||||
# code accumulated clippy debt unchecked).
|
||||
if [ -d "$REPO_ROOT/server/target" ]; then
|
||||
echo "pre-push: checking Rust (clippy)..."
|
||||
if ! (cd "$REPO_ROOT/server" && cargo clippy -- -D warnings 2>&1); then
|
||||
if ! (cd "$REPO_ROOT/server" && cargo clippy --all-targets -- -D warnings 2>&1); then
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "pre-push: clippy — OK"
|
||||
@@ -190,6 +192,37 @@ else
|
||||
echo "pre-push: systems.db not in push — skipping stamp check"
|
||||
fi
|
||||
|
||||
# --- Clerk review (D-221) ---
|
||||
# DISABLED 2026-05-23 (#965) pending rework. Two problems made it net-negative:
|
||||
# 1. Non-exhaustive — a single run reports ~the first contradiction it finds
|
||||
# and stops, so distinct real issues surfaced only on the 2nd/3rd/4th
|
||||
# re-push. An APPROVED verdict therefore can't be trusted as "clean".
|
||||
# 2. Re-reviews the entire origin/main..HEAD range on every push (token burn);
|
||||
# no per-commit verdict cache.
|
||||
# Off by default until reworked (exhaustive enumeration + holistic decisions/
|
||||
# diff check + a SHA-keyed verdict cache invalidated on decisions/ change, with
|
||||
# skip tracked in the cache rather than a commit-message trailer — see #965).
|
||||
# Force-run with SR_RUN_CLERK=1.
|
||||
if [ -x "$REPO_ROOT/tooling/clerk-review" ] && [ "${SR_RUN_CLERK:-0}" = "1" ]; then
|
||||
echo "pre-push: running clerk review..."
|
||||
CLERK_VERDICT=$("$REPO_ROOT/tooling/clerk-review" 2>&1 | tee /dev/stderr | tail -1)
|
||||
if [ "$CLERK_VERDICT" = "APPROVED" ]; then
|
||||
echo "pre-push: clerk — APPROVED"
|
||||
elif [ "$CLERK_VERDICT" = "REJECTED" ]; then
|
||||
echo "pre-push: clerk — REJECTED (see .cache/pre-push-review.md)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
elif [ "$CLERK_VERDICT" = "INCOMPLETE" ]; then
|
||||
echo "pre-push: clerk — INCOMPLETE (some reviews didn't finish; NOT blocking)"
|
||||
echo " See .cache/pre-push-review.md. For a full verdict: raise SR_CLERK_MAX_TURNS / SR_CLERK_TIMEOUT,"
|
||||
echo " or add a 'Clerk-Skip:' trailer to bulk-content commits."
|
||||
else
|
||||
echo "pre-push: clerk — '$CLERK_VERDICT' unrecognized; treating as block (see .cache/pre-push-review.md)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
echo "pre-push: clerk review — DISABLED (#965; force-run with SR_RUN_CLERK=1)"
|
||||
fi
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "pre-push: $ERRORS check(s) failed. Push aborted."
|
||||
|
||||
@@ -6,6 +6,44 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Layer 3 — settlement placement** (D-211, #955) — the generation cascade now places a body's settlements on terrain that fits their economy: capitals and cities are matched to river mouths, coasts, valleys, passes, etc. via the D-195 affinity table (a financial hub gravitates to the coast, a farm to a fertile valley, a fort to a mountain pass). Wired into the runtime cascade and held in the per-body world cache; the in-game settlement read is the next step
|
||||
- **Architecture-flavor trait-template catalog** (D-232, #993/#1005) — the foundation for how generated settlements *look*: a 28-template catalog of holistic cultural building-grammar bundles (economic-function pools + per-corridor baselines + heritage callbacks like iberian-hacienda, nordic-timber, east-asian-temple, arab-oasis), each economically gated and seed-drawn, plus a sparse per-body bias table for hero worlds. Build-time guardrails ensure ≥5 templates are eligible per economic class and none dominates a pool. Storage + bake only so far; the generator that draws from it lands later in Phase 4
|
||||
- **Authored per-system specialization layer** (D-237, D-233 re-amended) — each inhabited system gains three authored, static, deterministic identity fields driving world generation: `economic_specialization` (27-value vocabulary with scale baked into the value, e.g. breadbasket vs estate_farm vs terroir), `cultural_specialization` (activity + founding-heritage vocabulary so e.g. a Vietnamese-founded system reads correctly instead of taking its corridor's default), and `dominant_faction` (who governs). Authored lore wins; unnamed systems fall back to a deterministic-but-varied seed-hashed draw so they don't all read identically
|
||||
- **Kanban + milestones workflow** (D-221) — replaces sprint-based workflow with dependency-driven batch selection, many-to-many milestones, `/whats-next` skill with Si refinement review, `/pr-process` skill (author-side review comment pickup), clerk pre-push hook for D-record consistency
|
||||
- **Atlas orbital redesign** — horizontal orrery layout with half-sun on left, globe textures loaded from wiki, radius-based body sizing (Earth = 24px reference, unclamped), partial orbital arcs
|
||||
- **Globe texture coverage** — 100% of 3240 bodies now have globe.png; generated oort cloud + asteroid belt generics via Gemini, donor-filled 40 missing bodies from same-class pool
|
||||
- **Body radius scatter** — deterministic ±scatter on `body_radius_km` seeded by body_id; gas giants 40k-60k km, moons 200-2600 km, Sol system uses real radii; exported to `star_map_data.json`
|
||||
- **Atlas interaction** — double-click system on reach map opens orbital view, double-click body opens regional heightmap, M closes atlas from any screen, ESC navigates back through screen stack
|
||||
- **Inline body detail panel** — clicking a body in the orbital view shows an info panel on the right with full-size globe artwork, body stats, and population; clicking another body swaps the panel
|
||||
- **Regional atlas wiki loading** — heightmaps and markers load directly from the wiki filesystem, fixing "TERRAIN DATA PENDING" for all bodies with authored heightmaps
|
||||
- **Agent-team coordination tools** — `SendMessage` plus `TaskList`/`TaskUpdate`/`TaskGet` added to all 21 agent definitions so custom subagents work as full teammates in tmux split panes (message the lead, claim/complete tasks, shut down cleanly); `TaskCreate` deliberately withheld to keep task creation with the lead
|
||||
- **`git-centralize-guard` hook** — PreToolUse hook blocking `.git`-mutating commands for agent-team teammates, keying on the `agent_type` field present only in teammate hook input; version control stays centralized to the team lead
|
||||
- **Gemma naming methodology doc** (`docs/gemma-naming-methodology.md`) — preserves the corridor-aware LLM place-naming approach (sector palettes, few-shot prompting, dedup, Earth-major blocklist, deterministic fallback) as institutional knowledge after the naming pipeline's retirement
|
||||
- **Layer-1 world topography cascade** (#953, D-208/209/210) — deterministic empty-world generation in Rust: D8 priority-flood drainage (rivers/basins, ~45 ms/body after optimization), 7 geographic settlement-attractor tags, and sub-biome + terrain-modification-cost classification. The foundation the settlement-placement layer builds on
|
||||
- **Canonical 16-bit heightmaps** (#963, D-202) — every body's elevation is now a per-body 16-bit grayscale `heightmap.png` (1024×512, `sea_level` in a tEXt chunk); the old color render is renamed `reliefmap.png`. Single source of truth the generation cascade reads
|
||||
- **Deterministic generation cascade** (#952, D-224/D-200) — `SeedChain`, a single splitmix64-based seed-derivation contract descending from the master world seed with domain separation (replaces ad-hoc seed mixing); an extensible per-layer cascade harness (`run_cascade`); and a golden-seed regression test pinning Layer 0→1 output (source heightmap SHA-256 + serialized topography) so any determinism drift is caught
|
||||
|
||||
### Changed
|
||||
- **Founding-heritage pass — every divergent-heritage system now reads true** (#1016) — authored a `cultural_specialization` for ~140 more systems (32 → 171), so a Vietnamese-, Igbo-, Finnish-, Korean-, Shona-, or Persian-founded system reads as itself instead of defaulting to its corridor's dominant culture. The heritage vocabulary is now a canonical 47-people taxonomy spanning the British Isles, Iberia/Latin America, Europe, sub-Saharan Africa, South/East/SE Asia, the Pacific, and the Middle East/Central Asia. Pinned only where a system's founders diverge from their corridor baseline; corridor-typical systems keep the regional default
|
||||
- **Heritage rebalance — five systems renamed** (#1019) — the corridor over-represented southern-African heritage, so five genuinely-thin afrikaans systems are refounded with new cultures: Droëland→**Marib** and Stilwater→**Sawad** (Arab), Koeberg→**Akhgar** and Carnarvon→**Golestan** (Persian), Skuilplek→**Sığınak** (Turkic). Their worlds, stations, drifter-guide entries, and the corporations named after them are renamed to match; the distinctive mysteries each system carries are preserved. Load-bearing systems (Eerste Wacht, Helderoog, Brandpunt, …) are untouched
|
||||
- `teammateMode` set to `auto` — teammates spawn in tmux split panes when the lead runs inside tmux, with graceful in-process fallback
|
||||
- **Planet simulation native resolution → 1024×512** (#963) — finer base terrain for the lower cascade layers (deterministic; same continents, more detail). Rivers are now computed by the cascade's drainage rather than painted by the sim, so they actually reach the sea
|
||||
- **markers.json → names-only flavored pool** (D-223, #951) — all 2,398 bodies carry place *names* only, no positions or geometry; river/mountain positions derive from the heightmap + drainage, settlement positions from the economic sim + placement, with pooled names attached at generation time. The six hand-authored "templates" dissolve into ordinary named bodies
|
||||
- **`import_economics` owns the atlas index** (#951) — now the sole `regen-db` generator that touches the atlas tables; loads the names pool into `atlas_city_names` and empties the geometry tables, which the Phase 4 server cascade fills
|
||||
- **Rust dependency maintenance** (#966) — `rand` → 0.9.4 (clears the RUSTSEC-2026-0097 unsound advisory), a ~90-package semver-compatible freshness sweep (bevy 0.18.1, clap 4.6, others), and the deprecated `serde_yaml` replaced with the maintained `serde_norway` drop-in fork. No CVEs outstanding
|
||||
- **Pre-push clippy now lints tests + examples** (#967) — added `--all-targets`, closing the gap that let test-code lint debt accumulate unchecked; the existing cfg(test)/test-target clippy-1.93 debt was cleared in the same pass
|
||||
|
||||
### Fixed
|
||||
- **Sawad's "unsettled neighbour" canon contradiction** (#1019) — the system formerly called Stilwater described its neighbour GJ 914A as having no settlement, but GJ 914A is a three-century-old colony (now Marib); the "Far Gate" mystery is reworded to keep the unexplained-transit-hours hook without the false claim
|
||||
- Godot 4.6 type inference parse errors in `main.gd` (`var zoom`, `var speed`) that prevented the main scene from loading
|
||||
- `physical_keycode` used throughout for layout-independent key handling (was `keycode`, broke on non-US layouts)
|
||||
|
||||
### Removed
|
||||
- Sprint CLI (`tooling/db/sprint`), sprint skills (`/sprint-start`, `/sprint-plan`, `/sprint-status`), team-scoped file restrictions, sprint briefing workflow
|
||||
- **Atlas geometry generator + LLM naming pipeline** (#951, D-223) — `generate_atlas.py`, `gemma_naming.py`, `naming_core.py`, `apply_name_fixes.py`, the redundant `import_city_names.py`, their tests/QA/runner scripts, and the `fix_fewshot_bleed`/`prune_atlas_features` geometry tools. Procedural placement and authored names are superseded by the deterministic server cascade and the frozen name pool
|
||||
- **`atlas_body_heightmaps` DB table** (#963, D-202) — elevation moved out of `systems.db` into per-body PNG files (avoids binary merge conflicts and DB bloat); the Rust cascade loads the file
|
||||
|
||||
## [v0.2.0] — 2026-05-03
|
||||
|
||||
*Process milestone: final sprint-based release. Development moves to kanban + milestones (Q-096).*
|
||||
|
||||
@@ -4,7 +4,7 @@ A top-down life-sim — asymmetric information, occlusion-based perception, sing
|
||||
|
||||
**Official Title:** The Settled Reach
|
||||
**Repository name:** settled-reach
|
||||
**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.1.{sprint_number}`)
|
||||
**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.{phase}.{n}`)
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -13,7 +13,7 @@ client/ # Godot 4 client
|
||||
server/ # Rust/bevy_ecs simulation server
|
||||
tooling/ # Build tools, scripts, asset pipelines
|
||||
tests/ # Integration and end-to-end tests
|
||||
docs/ # Architecture, design, briefings, sprints, workshops
|
||||
docs/ # Architecture, design, briefings, workshops
|
||||
db/ # Schema + seed data (connectors moved to tooling/db/)
|
||||
.claude/ # Agents, skills, rules
|
||||
decisions/ # Decision domain files (D-NNN confirmed, Q-NNN open, R-NNN rejected)
|
||||
@@ -40,16 +40,18 @@ Development follows a strict cascade. Each phase has a concrete deliverable. **D
|
||||
| 1 | Wiki content complete — all planets, moons, stations, heightmaps, artwork | Implant-ready Godot map of the Reach with click-throughs + wiki/GTTR popups |
|
||||
| 2 | Economics layer — supply/demand, transport, political/social pressure, corporations, supply chains | Economics spreadsheets/graphs with runtime-tweakable simulation |
|
||||
| 3 | Planetary/moon maps & station layouts — cities, rivers, mountains, roads, biomes, rail | Atlas of the Reach (implant app) |
|
||||
| 4 | Player control scheme — 2-floor test map, character rendering, walls/stairs/doors, lighting | Player viewport with final-version assets |
|
||||
| 5 | World generation (tile/chunk/block) — walkable world, parallel asset pipeline | Walkable generated world + asset catalog |
|
||||
| 4 | World generation (tile/chunk/block) — deterministic multilayer seed→tile cascade; each layer viewed as a **map in the implant Atlas** (no in-world rendering) | Deterministic walkable-world data + per-layer Atlas maps + asset catalog |
|
||||
| 5 | Player control & in-world rendering — character, walls/stairs/doors, lighting, drawn on **generated** tiles (no test map) | Player viewport with final-version assets on the generated world |
|
||||
| 6 | Detail coloring — room-level NPC population, cultural room grammar | Only when the world is walkable |
|
||||
|
||||
**v0.2 target is dropped.** No scoping negotiations. Build the base systems fully.
|
||||
|
||||
**Generation before player (D-166, amended 2026-05-22).** No player-control or in-world rendering work begins until the generator can deterministically seed-generate every tile of every world via the full multilayer cascade. There is **no hand-made test map** — test layers are produced by the generator itself once layer-drawing begins. Progress is viewed through the implant **Atlas** (per-layer maps), which already exists (Phase 3). The current in-world rendering code is **left as-is until Phase 5** — do not build on it or rip it out before then.
|
||||
|
||||
## Work Modes
|
||||
|
||||
### Sprint mode (default)
|
||||
Agents work autonomously on sprint branches. Team lead coordinates via tasks and messages. Human reviews PRs from main. Standard `/sprint-start` → `/pr-push` → `/pr-review` lifecycle.
|
||||
### Kanban mode (default)
|
||||
Work flows continuously through milestones. `/whats-next` selects the next epic-sized batch from the dependency graph, refines ticket context via parallel Si agents, then activates a worktree. `/pr-process` (from worktree) and `/pr-review` (from main) handle the review cycle through Gitea PR comments. Milestones are many-to-many with tickets, can run in parallel, and can block other milestones.
|
||||
|
||||
### Pair session
|
||||
Human and Claude work together interactively on a single task. No background agents, no autonomous work. Used for load-bearing architecture changes where the human needs to make judgment calls as the work progresses — not approve a finished result.
|
||||
@@ -65,13 +67,10 @@ Human and Claude work together interactively on a single task. No background age
|
||||
|
||||
## Agent Instructions
|
||||
|
||||
### Team boundaries
|
||||
|
||||
**Your team is determined by your sprint branch** (e.g. `sprint-31/server` → server team).
|
||||
### Agent scope
|
||||
|
||||
- All file paths are relative to the repo root (e.g. `server/src/bridge/types.rs`).
|
||||
- **Stay within your team's scope.** Server team modifies `server/`. Client team modifies `client/`. Copy team modifies `wiki/`, `docs/atlas/`, `content/`. Shared directories (`docs/`, `decisions/`) are readable by all teams.
|
||||
- **Do NOT modify files outside your team scope** unless the ticket explicitly requires it.
|
||||
- Topic branches per logical work unit (e.g. `heightmap-import`, `city-names-table`). Any agent can touch any file — personalities guide perspective, not file access.
|
||||
- **Never chain git commands** in a single Bash call (e.g. `git add ... && git commit ...`). Always run `git add` and `git commit` as **separate sequential Bash calls**.
|
||||
- **Stale git lock files:** If a `git` command fails with `index.lock: File exists`, you may remove the lock file at `.git/index.lock` (or `.git/worktrees/<name>/index.lock` if in a worktree).
|
||||
|
||||
@@ -80,10 +79,11 @@ Human and Claude work together interactively on a single task. No background age
|
||||
The ticketing database (`settledreach.db`) is accessed via `SR_DB_PATH` env var (set in `.claude/settings.json`). A backup is committed to `docs/backups/settledreach.db.backup` via main only.
|
||||
|
||||
### Before starting work
|
||||
1. Read your sprint briefing at `docs/sprints/sprint-N/{team}.md` for current tasks
|
||||
2. Use `tooling/db/ticket show <id>` for full ticket details
|
||||
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
|
||||
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
||||
1. Check active work: `tooling/db/ticket list --status in_progress`
|
||||
2. Check your ticket: `tooling/db/ticket show <id>`
|
||||
3. Check dependencies: `tooling/db/ticket deps <id>`
|
||||
4. Read relevant `decisions/*.md` domain file(s) referenced in the ticket
|
||||
5. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
||||
|
||||
### CLI tools
|
||||
|
||||
@@ -92,7 +92,7 @@ The ticketing database (`settledreach.db`) is accessed via `SR_DB_PATH` env var
|
||||
| Tool | Command | Full reference |
|
||||
|------|---------|----------------|
|
||||
| Tickets | `tooling/db/ticket list`, `show`, `create`, `assign` | `/ticket` skill |
|
||||
| Sprints | `tooling/db/sprint status`, `start-work`, `prepare` | `/sprint-start` skill |
|
||||
| Milestones | `tooling/db/ticket milestone list`, `create`, `link`, `show` | `/ticket` skill |
|
||||
| SQL queries | `tooling/db/sqlite-query "SELECT ..."` | — |
|
||||
| SQL writes | `tooling/db/sqlite-exec "UPDATE ..."` | — |
|
||||
| Decisions | `tooling/db/decision show`, `next`, `claim`, `check-dupes` | — |
|
||||
|
||||
@@ -3,7 +3,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
||||
.PHONY: help setup build check-protocol client server game stop test lint lint-python setup-venv ci ci-client ci-server clean \
|
||||
decisions-sync decisions-coverage decisions-active decisions-orphan decisions-orphan-tickets \
|
||||
db-backup db-install validate-content check-fact-ids setup-hooks install-hooks \
|
||||
audit deny atlas-verify economy-db atlas-generate regen-db check-systems-db \
|
||||
audit deny atlas-verify economy-db regen-db check-systems-db \
|
||||
pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \
|
||||
pre-pr-server pre-pr-client pre-pr-content \
|
||||
fixtures-client fixtures-gauntlet golden-diff golden-update \
|
||||
@@ -58,11 +58,9 @@ help:
|
||||
@echo " make star-map-data Regenerate client/data/star_map_data.json from systems.db + wiki"
|
||||
@echo " make check-star-map Assert star_map_data.json is up to date (part of pre-pr-client)"
|
||||
@echo " make economy-db Import economics data into systems.db (TOML/JSON → SQLite)"
|
||||
@echo " make atlas-generate Generate atlas city/road/rail markers for all inhabited bodies"
|
||||
@echo " make regen-db Regenerate systems.db from all sources + stamp meta table (#855)"
|
||||
@echo " make check-systems-db Verify systems.db meta stamp matches current generator sources"
|
||||
@echo " make install-hooks Install pre-push + pre-commit git hooks (once per clone)"
|
||||
@echo " make test-atlas-determinism Determinism smoke test for generate_atlas.py (#847)"
|
||||
@echo " make fixtures-client Generate GDScript->Rust cross-encoder fixtures (#475)"
|
||||
@echo " make golden-diff Show diff if golden file output has changed"
|
||||
@echo " make golden-update Regenerate golden file and stage for commit"
|
||||
@@ -211,8 +209,9 @@ golden-diff:
|
||||
fi
|
||||
|
||||
golden-update:
|
||||
@echo "Regenerating golden file..."
|
||||
@echo "Regenerating golden files..."
|
||||
@cd server && UPDATE_GOLDEN=1 cargo test --test golden_suite -- proof_room_tick_10_matches_golden
|
||||
@cd server && UPDATE_GOLDEN=1 cargo test --test cascade_golden
|
||||
@git add server/tests/golden/
|
||||
@echo "--- Golden file updated and staged ---"
|
||||
@echo "Review with: git diff --cached -- server/tests/golden/"
|
||||
@@ -232,9 +231,6 @@ test-ipc-integration:
|
||||
test-ipc-benchmark:
|
||||
tests/run-ipc-benchmark
|
||||
|
||||
test-atlas-determinism: ## Determinism smoke test for generate_atlas.py (#847)
|
||||
tests/run-atlas-determinism
|
||||
|
||||
# --- Clean ---
|
||||
|
||||
clean-imports:
|
||||
@@ -345,20 +341,6 @@ economy-db: ## Import economics data (commodities, chains, gate links) into sys
|
||||
@tooling/generate-brands
|
||||
@python3 tooling/economy-db/import_economics.py
|
||||
|
||||
atlas-generate: ## Generate atlas markers (cities, roads, rail) for all inhabited bodies (#832)
|
||||
@# Loud guard: generate_atlas.py reads bodies with a non-NULL terrain_reference.
|
||||
@# If populate_terrain_reference.py has not run on a fresh DB, the generator
|
||||
@# silently processes zero bodies and exits 0 — fail fast instead.
|
||||
@count=$$(python3 -c "import sqlite3; c = sqlite3.connect('server/data/systems.db'); print(c.execute('SELECT COUNT(*) FROM bodies WHERE terrain_reference IS NOT NULL').fetchone()[0])"); \
|
||||
if [ "$$count" = "0" ]; then \
|
||||
echo "ERROR: no bodies have terrain_reference populated yet."; \
|
||||
echo "Run: python3 tooling/planet-gen/populate_terrain_reference.py"; \
|
||||
echo "(This is a prerequisite for atlas-generate — see D-191 §9 pipeline order.)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo " [guard] $$count bodies with terrain_reference — proceeding."
|
||||
@python3 tooling/planet-gen/generate_atlas.py --seed 42
|
||||
|
||||
regen-db: ## Regenerate systems.db from all sources and stamp meta table (#855, #856)
|
||||
@# Run as a single shell so `set -e` covers all steps. Without this
|
||||
@# each recipe line was a fresh shell and a failure in step 1 did not
|
||||
@@ -366,13 +348,14 @@ regen-db: ## Regenerate systems.db from all sources and stamp meta table (#855,
|
||||
@# (PR #136 review T4). import_economics' exit code 2 is a valid
|
||||
@# coverage-gate-warning state (DB and stamp committed), not an error,
|
||||
@# so it's explicitly tolerated. Any other non-zero exit halts the
|
||||
@# pipeline immediately.
|
||||
@# pipeline immediately. The atlas city/road/river geometry generator
|
||||
@# was retired in #951 (D-223); import_economics now owns the atlas
|
||||
@# index — it loads the names-only pool into atlas_city_names and empties
|
||||
@# the geometry tables (the server cascade fills them, Phase 4).
|
||||
@set -e; \
|
||||
echo " [regen-db] Importing economics data (runs generate_brands internally)..."; \
|
||||
ec=0; python3 tooling/economy-db/import_economics.py || ec=$$?; \
|
||||
if [ $$ec -ne 0 ] && [ $$ec -ne 2 ]; then exit $$ec; fi; \
|
||||
echo " [regen-db] Running atlas generator..."; \
|
||||
python3 tooling/planet-gen/generate_atlas.py --seed 42; \
|
||||
echo ""; \
|
||||
echo " regen-db complete — systems.db is up to date and stamped."; \
|
||||
echo " Stage it with: git add server/data/systems.db"
|
||||
|
||||
+8887
-5647
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ extends Node
|
||||
# Signals
|
||||
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
|
||||
signal snapshot_received(snapshot: Dictionary)
|
||||
signal atlas_layers_received(response: Dictionary)
|
||||
signal handshake_complete
|
||||
signal handshake_failed(reason: String)
|
||||
|
||||
@@ -263,8 +264,7 @@ func _process(delta: float) -> void: # gdlint:disable=max-returns
|
||||
# Send startup message with world_seed and character appearance (#175, D-010/D-029, #718).
|
||||
# Server blocks waiting for this before entering the tick loop.
|
||||
var startup_bytes := Protocol.encode_startup_message(
|
||||
GameState.world_seed,
|
||||
GameState.character_visual_descriptor
|
||||
GameState.world_seed, GameState.character_visual_descriptor
|
||||
)
|
||||
if startup_bytes.size() > 0:
|
||||
var send_err: int = _bridge.send_message(startup_bytes)
|
||||
@@ -387,6 +387,25 @@ func send_named_action(action_name: String, action_data: Variant = null) -> void
|
||||
_outbound_buffer.append(entry)
|
||||
|
||||
|
||||
## Request a body's generation-cascade layers from the server (#960, D-225).
|
||||
## Live mode only — sends an AtlasLayerRequest frame; the response arrives via the
|
||||
## atlas_layers_received signal. No-op in test mode (no server connection).
|
||||
func request_atlas_layers(body_id: String, up_to: String = "Topography") -> void:
|
||||
if test_mode or _bridge == null or state != ConnectionState.CONNECTED:
|
||||
return
|
||||
var bytes := Protocol.encode_atlas_layer_request(body_id, up_to)
|
||||
if bytes.is_empty():
|
||||
return
|
||||
var err: int = _bridge.send_message(bytes)
|
||||
if err != OK:
|
||||
push_error(
|
||||
(
|
||||
"SimBridge: failed to send atlas layer request for %s: %s"
|
||||
% [body_id, error_string(err)]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Poll for snapshot from simulation.
|
||||
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
|
||||
func poll_snapshot() -> Variant:
|
||||
@@ -412,9 +431,18 @@ func poll_snapshot() -> Variant:
|
||||
# events (monologue, dialogue) are carried forward from overwritten snapshots
|
||||
# so they aren't silently dropped when server ticks faster than client consumes.
|
||||
func receive_bytes(bytes: PackedByteArray) -> void:
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
# Decode once, branch by frame shape (#960, D-225): atlas layer responses and
|
||||
# snapshots are both msgpack maps, told apart by field.
|
||||
var inbound := Protocol.decode_inbound(bytes)
|
||||
if inbound.kind == "atlas":
|
||||
atlas_layers_received.emit(inbound.value)
|
||||
return
|
||||
if inbound.kind != "snapshot":
|
||||
push_warning("SimBridge: undecodable frame (%d bytes)" % bytes.size())
|
||||
return
|
||||
var snapshot = inbound.value
|
||||
if snapshot == null:
|
||||
push_warning("SimBridge: decode_snapshot returned null for %d bytes" % bytes.size())
|
||||
push_warning("SimBridge: snapshot decode returned null for %d bytes" % bytes.size())
|
||||
return
|
||||
if _last_snapshot != null:
|
||||
# Carry forward one-shot events the client hasn't consumed yet.
|
||||
|
||||
@@ -194,7 +194,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
if GameState.free_camera_mode and event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
if mb.pressed:
|
||||
var zoom := camera.zoom
|
||||
var zoom: Vector2 = camera.zoom
|
||||
if mb.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
zoom += Vector2(FREE_CAMERA_ZOOM_STEP, FREE_CAMERA_ZOOM_STEP)
|
||||
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
@@ -221,7 +221,7 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if manifest.app_path.is_empty():
|
||||
push_warning("main.gd: manifest with empty app_path — skipping")
|
||||
continue
|
||||
if manifest.default_key == key_event.keycode:
|
||||
if manifest.default_key == key_event.physical_keycode:
|
||||
HudGroups.toggle_app(
|
||||
manifest.app_path,
|
||||
ImplantRegistry.get_resolved_mode(manifest.app_path)
|
||||
@@ -230,10 +230,10 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
# [ / ] — in-app navigation for the economics monitor. Not manifest-declared because
|
||||
# these control intra-app navigation (prev/next system), not app launch. A planned
|
||||
# handle_global_key lifecycle hook will absorb this (see arch doc Follow-up).
|
||||
if key_event.keycode == KEY_BRACKETLEFT:
|
||||
if key_event.physical_keycode == KEY_BRACKETLEFT:
|
||||
if economics_app and HudGroups.is_app_active("implant/economics"):
|
||||
economics_app.navigate(-1)
|
||||
elif key_event.keycode == KEY_BRACKETRIGHT:
|
||||
elif key_event.physical_keycode == KEY_BRACKETRIGHT:
|
||||
if economics_app and HudGroups.is_app_active("implant/economics"):
|
||||
economics_app.navigate(1)
|
||||
|
||||
@@ -279,7 +279,7 @@ func _process(delta: float) -> void:
|
||||
if Input.is_action_pressed("move_west"):
|
||||
pan.x -= 1.0
|
||||
if pan != Vector2.ZERO:
|
||||
var speed := FREE_CAMERA_PAN_SPEED / camera.zoom.x
|
||||
var speed: float = FREE_CAMERA_PAN_SPEED / camera.zoom.x
|
||||
camera.global_position += pan.normalized() * speed * delta
|
||||
# Track camera to player (D-015: locked, fixed-north).
|
||||
# #117: Manual exponential smoothing.
|
||||
|
||||
@@ -9,23 +9,39 @@ extends Node
|
||||
## Unit enum variants (no data) → bare strings ("MoveNorth", "Npc")
|
||||
## Data enum variants → single-element maps ({"UsePerceptionMode": "thermal"})
|
||||
|
||||
|
||||
static func _mp():
|
||||
return load("res://addons/messagepack/messagepack.gd")
|
||||
|
||||
|
||||
# -- Decode: bytes from server → GDScript types --------------------------------
|
||||
|
||||
|
||||
## Decode a raw MessagePack frame to its top-level value (or null on error).
|
||||
## #960: lets receive_bytes decode a frame once and branch by shape before
|
||||
## committing to the heavier snapshot decode.
|
||||
static func decode_raw(bytes: PackedByteArray) -> Variant:
|
||||
var result = _mp().decode(bytes)
|
||||
if result.status != null:
|
||||
push_error("Protocol: msgpack decode failed: %s" % result.status)
|
||||
return null
|
||||
return result.value
|
||||
|
||||
|
||||
## Decode an ObserverSnapshot from MessagePack bytes.
|
||||
## Returns decoded snapshot Dictionary or null on error.
|
||||
## v2 fields (version, game_time, player_facing, visible_tiles) default to null/empty
|
||||
## when decoding v1 snapshots for backward compatibility.
|
||||
static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
var result = _mp().decode(bytes)
|
||||
if result.status != null:
|
||||
push_error("Protocol: msgpack decode failed: %s" % result.status)
|
||||
var raw = decode_raw(bytes)
|
||||
if raw == null:
|
||||
return null
|
||||
return _decode_snapshot_from_raw(raw)
|
||||
|
||||
var raw = result.value
|
||||
|
||||
## Build an ObserverSnapshot from an already-decoded raw value (the snapshot
|
||||
## body, shared by decode_snapshot and the receive-side classifier).
|
||||
static func _decode_snapshot_from_raw(raw: Variant) -> Variant:
|
||||
if not raw is Dictionary or not raw.has("tick") or not raw.has("entities"):
|
||||
push_error("Protocol: snapshot missing required fields")
|
||||
return null
|
||||
@@ -397,18 +413,21 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
if raw_alc is Array:
|
||||
for cul in raw_alc:
|
||||
alc.append(str(cul))
|
||||
bm_entries.append(
|
||||
{
|
||||
"id": str(raw_bm["id"]),
|
||||
"title": str(raw_bm.get("title", "")),
|
||||
"subtitle": str(raw_bm.get("subtitle", "")),
|
||||
"flavor": str(raw_bm.get("flavor", "")),
|
||||
"default_location": str(raw_bm.get("default_location", "")),
|
||||
"allowed_locations": al,
|
||||
"allowed_locations_cultures": alc,
|
||||
"career": str(raw_bm.get("career", "")),
|
||||
"starting_capital_tractus": int(raw_bm.get("starting_capital_tractus", 0)),
|
||||
}
|
||||
(
|
||||
bm_entries
|
||||
. append(
|
||||
{
|
||||
"id": str(raw_bm["id"]),
|
||||
"title": str(raw_bm.get("title", "")),
|
||||
"subtitle": str(raw_bm.get("subtitle", "")),
|
||||
"flavor": str(raw_bm.get("flavor", "")),
|
||||
"default_location": str(raw_bm.get("default_location", "")),
|
||||
"allowed_locations": al,
|
||||
"allowed_locations_cultures": alc,
|
||||
"career": str(raw_bm.get("career", "")),
|
||||
"starting_capital_tractus": int(raw_bm.get("starting_capital_tractus", 0)),
|
||||
}
|
||||
)
|
||||
)
|
||||
bookmark_catalog = {"bookmarks": bm_entries}
|
||||
|
||||
@@ -726,14 +745,76 @@ static func encode_request_bookmark_catalog() -> PackedByteArray:
|
||||
return result.value
|
||||
|
||||
|
||||
## Encode an AtlasLayerRequest (#969, D-225) for the layer-stream proxy.
|
||||
## A bare map {body_id, up_to} — NOT the Vec<PlayerInput> array — so the server's
|
||||
## frame demux routes it to the atlas proxy. up_to is a CascadeLayer unit variant
|
||||
## (bare string: "Heightmap" | "Topography").
|
||||
static func encode_atlas_layer_request(
|
||||
body_id: String, up_to: String = "Topography"
|
||||
) -> PackedByteArray:
|
||||
var msg := {"body_id": body_id, "up_to": up_to}
|
||||
var result = _mp().encode(msg)
|
||||
if result.status != null:
|
||||
push_error("Protocol: encode_atlas_layer_request failed: %s" % result.status)
|
||||
return PackedByteArray()
|
||||
return result.value
|
||||
|
||||
|
||||
## Decode an AtlasLayerResponse (#969, D-225). Returns a Dictionary
|
||||
## {body_id, status, error, layer1}, or null if the bytes are not an atlas
|
||||
## response (no "status" key — e.g. an ObserverSnapshot). status is the variant
|
||||
## name ("Ready"|"Pending"|"NotFound"|"Error"); error holds the message for the
|
||||
## Error variant. layer1 is the raw decoded Layer1Output map, or null.
|
||||
static func decode_atlas_layer_response(bytes: PackedByteArray) -> Variant:
|
||||
return atlas_response_from_raw(decode_raw(bytes))
|
||||
|
||||
|
||||
## Build an AtlasLayerResponse from an already-decoded raw value. Returns null
|
||||
## if it is not an atlas response (no "status" key).
|
||||
static func atlas_response_from_raw(raw: Variant) -> Variant:
|
||||
if not raw is Dictionary or not raw.has("status"):
|
||||
return null
|
||||
var status_raw = raw["status"]
|
||||
var status := ""
|
||||
var error := ""
|
||||
if status_raw is String:
|
||||
status = status_raw
|
||||
elif status_raw is Dictionary and status_raw.has("Error"):
|
||||
status = "Error"
|
||||
error = str(status_raw["Error"])
|
||||
return {
|
||||
"body_id": raw.get("body_id", ""),
|
||||
"status": status,
|
||||
"error": error,
|
||||
"layer1": raw.get("layer1"),
|
||||
}
|
||||
|
||||
|
||||
## Decode + classify one inbound frame (#960, D-225). Returns {kind, value} with
|
||||
## kind "snapshot" | "atlas" | "unknown" — both are msgpack maps, so they are
|
||||
## told apart by field (a response has "status"; a snapshot has "entities").
|
||||
## Lets receive_bytes decode the frame ONCE and branch, instead of double-decoding
|
||||
## the 20 Hz snapshot path.
|
||||
static func decode_inbound(bytes: PackedByteArray) -> Dictionary:
|
||||
var raw = decode_raw(bytes)
|
||||
if not raw is Dictionary:
|
||||
return {"kind": "unknown", "value": null}
|
||||
if raw.has("status") and not raw.has("entities"):
|
||||
return {"kind": "atlas", "value": atlas_response_from_raw(raw)}
|
||||
return {"kind": "snapshot", "value": _decode_snapshot_from_raw(raw)}
|
||||
|
||||
|
||||
## Encode a ConfirmBookmark action (#614, #680).
|
||||
## Struct variant with bookmark_id and starting_location_id.
|
||||
static func encode_confirm_bookmark(bookmark_id: String, starting_location_id: String) -> PackedByteArray:
|
||||
static func encode_confirm_bookmark(
|
||||
bookmark_id: String, starting_location_id: String
|
||||
) -> PackedByteArray:
|
||||
var entries: Array = [
|
||||
{
|
||||
"tick": 0,
|
||||
"action_name": "ConfirmBookmark",
|
||||
"action_data": {"bookmark_id": bookmark_id, "starting_location_id": starting_location_id},
|
||||
"action_data":
|
||||
{"bookmark_id": bookmark_id, "starting_location_id": starting_location_id},
|
||||
}
|
||||
]
|
||||
var result = _mp().encode(entries)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ƒ§body_id¥ghost¦status¨NotFound¦layer1À
|
||||
@@ -0,0 +1 @@
|
||||
ƒ§body_id¤GJ1c¦status§Pending¦layer1À
|
||||
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
## Tests for the Phase-4 generation overlays in the Atlas viewer (#960, D-225).
|
||||
## Referencing AtlasViewer forces both it and AtlasMarkerOverlay to compile, so
|
||||
## this also guards against parse errors in the overlay rendering code.
|
||||
class_name TestAtlasOverlays
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
func test_generation_overlays_registered() -> void:
|
||||
var ids: Array = []
|
||||
for d: Dictionary in AtlasViewer.OVERLAY_DEFS:
|
||||
ids.append(d["id"])
|
||||
assert_that(ids).contains(["gen_l1_rivers", "gen_l1_basins", "gen_l1_attractors"])
|
||||
|
||||
|
||||
func test_generation_data_round_trips() -> void:
|
||||
var v: AtlasViewer = auto_free(AtlasViewer.new())
|
||||
assert_that(v.get_generation_layer1()).is_null()
|
||||
var mock := {
|
||||
"body_id": "GJ1c",
|
||||
"river_network": {"river_cells": [], "confluences": [], "mouths": []},
|
||||
"drainage_basins": [],
|
||||
"attractors": [],
|
||||
}
|
||||
v.set_generation_layer1(mock)
|
||||
assert_that(v.get_generation_layer1()).is_equal(mock)
|
||||
|
||||
|
||||
func test_implant_pending_start_stop() -> void:
|
||||
var p: ImplantPending = auto_free(ImplantPending.new())
|
||||
p.start("GENERATING LAYER 1")
|
||||
assert_bool(p.visible).is_true()
|
||||
p.stop()
|
||||
assert_bool(p.visible).is_false()
|
||||
@@ -16,6 +16,7 @@ func _load_fixture(name: String) -> PackedByteArray:
|
||||
|
||||
# -- Snapshot decoding ----------------------------------------------------------
|
||||
|
||||
|
||||
func test_decode_snapshot_one_npc() -> void:
|
||||
var bytes = _load_fixture("snapshot_one_npc")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
@@ -102,6 +103,7 @@ func test_decode_snapshot_multi_entity() -> void:
|
||||
|
||||
# -- PlayerInput decoding -------------------------------------------------------
|
||||
|
||||
|
||||
func test_decode_input_move_north() -> void:
|
||||
var bytes = _load_fixture("input_move_north")
|
||||
var input = Protocol.decode_player_input(bytes)
|
||||
@@ -124,6 +126,7 @@ func test_decode_input_perception_mode() -> void:
|
||||
|
||||
# -- PlayerInput encoding -------------------------------------------------------
|
||||
|
||||
|
||||
func test_encode_decode_roundtrip_unit_variant() -> void:
|
||||
var bytes = Protocol.encode_player_input(50, "MoveEast")
|
||||
assert_that(bytes.size()).is_greater(0)
|
||||
@@ -148,6 +151,7 @@ func test_encode_decode_roundtrip_data_variant() -> void:
|
||||
|
||||
# -- Cross-language roundtrip: GDScript encode matches Rust decode ---------------
|
||||
|
||||
|
||||
func test_gdscript_encode_matches_rust_fixture() -> void:
|
||||
# Encode the same MoveNorth input as the Rust fixture
|
||||
var bytes = Protocol.encode_player_input(100, "MoveNorth")
|
||||
@@ -164,6 +168,7 @@ func test_gdscript_encode_matches_rust_fixture() -> void:
|
||||
|
||||
# -- Negative tests: malformed/truncated input -----------------------------------
|
||||
|
||||
|
||||
func test_decode_snapshot_truncated_bytes() -> void:
|
||||
var truncated := PackedByteArray([0x82, 0xa4]) # Incomplete msgpack map
|
||||
var result = Protocol.decode_snapshot(truncated)
|
||||
@@ -189,7 +194,8 @@ func test_decode_snapshot_malformed_entities_counted() -> void:
|
||||
var raw := {
|
||||
"tick": 7,
|
||||
"version": 23,
|
||||
"entities": [
|
||||
"entities":
|
||||
[
|
||||
{"entity_id": 1, "x": 5.0, "y": 10.0, "z": 0, "kind": "Npc"},
|
||||
{"entity_id": 2, "broken": true}, # Missing required fields
|
||||
{"x": 1.0}, # Missing entity_id, y, z, kind
|
||||
@@ -217,6 +223,7 @@ func test_encode_produces_nonempty_bytes() -> void:
|
||||
|
||||
# -- Batch input encoding (Vec<PlayerInput> wire format) -----------------------
|
||||
|
||||
|
||||
func test_encode_player_inputs_single() -> void:
|
||||
var inputs: Array = [{"tick": 10, "action_name": "MoveNorth"}]
|
||||
var bytes := Protocol.encode_player_inputs(inputs)
|
||||
@@ -276,6 +283,7 @@ func test_encode_player_inputs_empty() -> void:
|
||||
|
||||
# -- v2 snapshot decoding -------------------------------------------------------
|
||||
|
||||
|
||||
func test_decode_snapshot_v2_full() -> void:
|
||||
var bytes = _load_fixture("snapshot_v2_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
@@ -304,7 +312,9 @@ func test_decode_snapshot_v2_full() -> void:
|
||||
|
||||
func test_existing_fixtures_have_v2_fields() -> void:
|
||||
# All fixtures are generated by fixture_snapshot() — verify decoder extracts v2+ fields
|
||||
for fixture_name in ["snapshot_one_npc", "snapshot_empty", "snapshot_player", "snapshot_multi_entity"]:
|
||||
for fixture_name in [
|
||||
"snapshot_one_npc", "snapshot_empty", "snapshot_player", "snapshot_multi_entity"
|
||||
]:
|
||||
var bytes = _load_fixture(fixture_name)
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot).is_not_null()
|
||||
@@ -324,6 +334,7 @@ func test_multi_entity_visibility_sectors() -> void:
|
||||
|
||||
# -- Batch input fixture (D-030 Layer 1 bidirectional symmetry) ----------------
|
||||
|
||||
|
||||
func test_decode_batch_input_fixture() -> void:
|
||||
# Rust-generated Vec<PlayerInput> fixture — verifies bidirectional Layer 1 compatibility
|
||||
var bytes = _load_fixture("input_batch_two")
|
||||
@@ -341,6 +352,7 @@ func test_decode_batch_input_fixture() -> void:
|
||||
|
||||
# -- Diagonal movement fixtures (D-030 Layer 1 cross-language) -----------------
|
||||
|
||||
|
||||
func test_decode_diagonal_fixtures() -> void:
|
||||
# All 4 diagonal fixtures: clockwise NE, SE, SW, NW
|
||||
var diagonals: Array = [
|
||||
@@ -361,14 +373,17 @@ func test_decode_diagonal_fixtures() -> void:
|
||||
|
||||
# -- v23: BookmarkCatalog decode -----------------------------------------------
|
||||
|
||||
|
||||
func test_decode_snapshot_with_bookmark_catalog() -> void:
|
||||
# Hand-built dict — fixture generation requires server work, skip round-trip (#614).
|
||||
var raw := {
|
||||
"tick": 1,
|
||||
"version": 23,
|
||||
"entities": [],
|
||||
"bookmark_catalog": {
|
||||
"bookmarks": [
|
||||
"bookmark_catalog":
|
||||
{
|
||||
"bookmarks":
|
||||
[
|
||||
{
|
||||
"id": "bm_tycoon_arion",
|
||||
"title": "The Arion Run",
|
||||
@@ -436,6 +451,7 @@ func test_decode_snapshot_no_bookmark_catalog_is_null() -> void:
|
||||
|
||||
# -- v23: RequestBookmarkCatalog + ConfirmBookmark encoding --------------------
|
||||
|
||||
|
||||
func test_encode_request_bookmark_catalog_roundtrip() -> void:
|
||||
var bytes := Protocol.encode_request_bookmark_catalog()
|
||||
assert_that(bytes.size()).is_greater(0)
|
||||
@@ -463,3 +479,63 @@ func test_encode_confirm_bookmark_roundtrip() -> void:
|
||||
var data: Dictionary = entry["action_data"]
|
||||
assert_that(data["bookmark_id"]).is_equal("bm_tycoon_arion")
|
||||
assert_that(data["starting_location_id"]).is_equal("loc_arion_prime")
|
||||
|
||||
|
||||
# -- Atlas layer-stream protocol (#969, D-225) ---------------------------------
|
||||
|
||||
|
||||
func test_decode_atlas_response_ready() -> void:
|
||||
var bytes := _load_fixture("atlas_response_ready")
|
||||
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||
assert_that(resp).is_not_null()
|
||||
assert_that(resp.body_id).is_equal("GJ1c")
|
||||
assert_that(resp.status).is_equal("Ready")
|
||||
assert_that(resp.layer1).is_not_null()
|
||||
assert_that(resp.layer1.river_network.river_cells.size()).is_equal(2)
|
||||
assert_that(resp.layer1.attractors.size()).is_equal(1)
|
||||
assert_that(resp.layer1.attractors[0].attractor_type).is_equal("CoastalAccess")
|
||||
assert_that(resp.layer1.attractors[0].sub_biome).is_equal("CoastalLowland")
|
||||
|
||||
|
||||
func test_decode_atlas_response_pending() -> void:
|
||||
var bytes := _load_fixture("atlas_response_pending")
|
||||
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||
assert_that(resp).is_not_null()
|
||||
assert_that(resp.status).is_equal("Pending")
|
||||
assert_that(resp.layer1).is_null()
|
||||
|
||||
|
||||
func test_decode_atlas_response_not_found() -> void:
|
||||
var bytes := _load_fixture("atlas_response_not_found")
|
||||
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||
assert_that(resp.status).is_equal("NotFound")
|
||||
|
||||
|
||||
func test_snapshot_is_not_decoded_as_atlas_response() -> void:
|
||||
# Disambiguation: an ObserverSnapshot has no "status" key, so the atlas
|
||||
# decoder rejects it. receive_bytes relies on this to route correctly.
|
||||
var bytes := _load_fixture("snapshot_empty")
|
||||
assert_that(Protocol.decode_atlas_layer_response(bytes)).is_null()
|
||||
|
||||
|
||||
func test_encode_atlas_request_shape() -> void:
|
||||
var bytes := Protocol.encode_atlas_layer_request("GJ1c", "Topography")
|
||||
assert_that(bytes.size()).is_greater(0)
|
||||
var raw: Variant = Messagepack.decode(bytes)
|
||||
assert_that(raw.status).is_null()
|
||||
assert_that(raw.value is Dictionary).is_true()
|
||||
assert_that(raw.value["body_id"]).is_equal("GJ1c")
|
||||
assert_that(raw.value["up_to"]).is_equal("Topography")
|
||||
# A request is a map with no "status" — must not be mistaken for a response.
|
||||
assert_that(Protocol.decode_atlas_layer_response(bytes)).is_null()
|
||||
|
||||
|
||||
func test_decode_inbound_classifies_frames() -> void:
|
||||
# The receive-side classifier: snapshot vs atlas response, decoded once.
|
||||
var snap = Protocol.decode_inbound(_load_fixture("snapshot_empty"))
|
||||
assert_that(snap.kind).is_equal("snapshot")
|
||||
assert_that(snap.value).is_not_null()
|
||||
|
||||
var atlas = Protocol.decode_inbound(_load_fixture("atlas_response_ready"))
|
||||
assert_that(atlas.kind).is_equal("atlas")
|
||||
assert_that(atlas.value.status).is_equal("Ready")
|
||||
|
||||
@@ -134,7 +134,9 @@ func _run(): # gdlint:disable=max-returns
|
||||
push_error("visual_capture: no snapshot after %d frames" % waited)
|
||||
quit(1)
|
||||
return
|
||||
print("visual_capture: first snapshot tick=%d (%d frames)" % [game_state.current_tick, waited])
|
||||
print(
|
||||
"visual_capture: first snapshot tick=%d (%d frames)" % [game_state.current_tick, waited]
|
||||
)
|
||||
|
||||
if not _scenario.is_empty():
|
||||
await _run_scenario(main_node)
|
||||
@@ -184,15 +186,27 @@ func _run_scenario(_main_node: Node) -> void:
|
||||
rf.close()
|
||||
# Decode through Protocol.decode_snapshot() — same as live IPC receive path.
|
||||
# This exercises: msgpack decode → entity decode → tile_kind→type mapping → etc.
|
||||
var replay_data: Variant = Protocol.decode_snapshot(replay_bytes)
|
||||
# class_name `Protocol` isn't resolvable in -s mode (see header note);
|
||||
# instantiate the script and call the static decoder on the instance,
|
||||
# then free it — the decoded Dictionary is independent of the node.
|
||||
var protocol_node: Node = load("res://scripts/protocol/protocol.gd").new()
|
||||
var replay_data: Variant = protocol_node.decode_snapshot(replay_bytes)
|
||||
protocol_node.free()
|
||||
if replay_data == null or not replay_data is Dictionary:
|
||||
push_error("visual_capture: Protocol.decode_snapshot failed for %s" % abs_path)
|
||||
quit(1)
|
||||
return
|
||||
print("visual_capture: replaying %s (%d bytes, tick=%s, %d tiles)" % [
|
||||
replay_path, replay_bytes.size(),
|
||||
str(replay_data.get("tick", "?")),
|
||||
replay_data.get("visible_tiles", []).size()])
|
||||
print(
|
||||
(
|
||||
"visual_capture: replaying %s (%d bytes, tick=%s, %d tiles)"
|
||||
% [
|
||||
replay_path,
|
||||
replay_bytes.size(),
|
||||
str(replay_data.get("tick", "?")),
|
||||
replay_data.get("visible_tiles", []).size()
|
||||
]
|
||||
)
|
||||
)
|
||||
var game_state := root.get_node("/root/GameState")
|
||||
var fog_state := root.get_node("/root/FogState")
|
||||
game_state.apply_snapshot(replay_data)
|
||||
|
||||
@@ -8,6 +8,7 @@ extends RefCounted
|
||||
## compile-time identifiers in -s mode. All methods receive tree_root and
|
||||
## look up autoloads via get_node("/root/...").
|
||||
|
||||
|
||||
## Apply scenario-specific setup before tick advancement.
|
||||
## Returns true if the scenario is recognized, false otherwise.
|
||||
func apply_setup(scenario_name: String, tree_root: Node) -> bool:
|
||||
@@ -93,6 +94,47 @@ func apply_setup(scenario_name: String, tree_root: Node) -> bool:
|
||||
# player spawns at Hub (50,58). Captures real fog pipeline output.
|
||||
pass
|
||||
|
||||
"implant_pending":
|
||||
# #960: overlay the diegetic Layer-1 "generating" indicator for
|
||||
# visual sign-off. Loaded via load() — class_name isn't available
|
||||
# in -s mode (per this file's header note).
|
||||
var pending_script: GDScript = load("res://ui/implant/implant_pending.gd")
|
||||
var theme_res: Resource = load("res://ui/implant/default_implant.tres")
|
||||
var layer := CanvasLayer.new()
|
||||
layer.layer = 100
|
||||
tree_root.add_child(layer)
|
||||
var indicator: Control = pending_script.new()
|
||||
indicator.name = "VisualImplantPending"
|
||||
layer.add_child(indicator)
|
||||
indicator.apply_implant_theme(theme_res)
|
||||
indicator.position = Vector2(370.0, 240.0) # centered at 960x540
|
||||
indicator.start("GENERATING LAYER 1")
|
||||
|
||||
"atlas_gen_open":
|
||||
# #960 live inspection: open the actual atlas app through HudGroups (the
|
||||
# production opener), navigate to GJ71c's regional screen, and enable the
|
||||
# generation overlays — exercises the opener + the live request → cascade
|
||||
# → render pipeline against a connected server (SR_LIVE=1).
|
||||
var hud_groups: Node = tree_root.get_node("/root/HudGroups")
|
||||
var registry: Node = tree_root.get_node("/root/ImplantRegistry")
|
||||
hud_groups.open_app("implant/map")
|
||||
var app: Variant = registry.get_app_instance("implant/map")
|
||||
if app == null:
|
||||
push_warning("VisualScenarios: atlas app not instantiated")
|
||||
return true
|
||||
var body := {
|
||||
"body_id": "GJ71c",
|
||||
"proper_name": "Threshold",
|
||||
"terrain_reference": "wiki/star-systems/GJ-71/bodies/GJ71c/heightmap.png",
|
||||
}
|
||||
app.nav.push(
|
||||
"regional", {"body": body, "system": {"system_id": "GJ 71", "proper_name": "GJ 71"}}
|
||||
)
|
||||
var viewer: Variant = app._regional_screen._viewer
|
||||
viewer.set_overlay_visible("gen_l1_rivers", true)
|
||||
viewer.set_overlay_visible("gen_l1_basins", true)
|
||||
viewer.set_overlay_visible("gen_l1_attractors", true)
|
||||
|
||||
_:
|
||||
push_warning("VisualScenarios: unknown scenario '%s'" % scenario_name)
|
||||
return false
|
||||
|
||||
@@ -62,15 +62,19 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if not event.is_pressed() or event.is_echo():
|
||||
return
|
||||
if current_screen_id() == "regional":
|
||||
return # AtlasViewer handles its own keyboard input
|
||||
return
|
||||
_handle_key(event as InputEventKey)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _handle_key(event: InputEventKey) -> void:
|
||||
match event.keycode:
|
||||
match event.physical_keycode:
|
||||
KEY_M:
|
||||
HudGroups.close_app()
|
||||
KEY_ESCAPE:
|
||||
if current_screen_id() == "reach":
|
||||
if current_screen_id() == "system" and _system_screen and (_system_screen.has_body_panel_open() or _system_screen.has_station_panel_open()):
|
||||
_system_screen.close_panels()
|
||||
elif current_screen_id() == "reach":
|
||||
HudGroups.close_app()
|
||||
else:
|
||||
nav.pop()
|
||||
@@ -93,18 +97,13 @@ func _handle_enter() -> void:
|
||||
if _system_screen and not _system_screen.is_in_orbital():
|
||||
var sys: Dictionary = _system_screen.current_system()
|
||||
nav.replace("system", {"mode": "orbital", "system": sys})
|
||||
"planet":
|
||||
if _planet_screen and _planet_screen.has_heightmap():
|
||||
(
|
||||
nav
|
||||
. push(
|
||||
"regional",
|
||||
{
|
||||
"body": _planet_screen.current_body(),
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
}
|
||||
)
|
||||
)
|
||||
elif _system_screen and _system_screen.has_body_panel_open():
|
||||
var body: Dictionary = _system_screen.get_selected_body()
|
||||
if body.get("terrain_reference") != null:
|
||||
nav.push("regional", {
|
||||
"body": body,
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -121,16 +120,10 @@ func _on_system_selected(system_id: String) -> void:
|
||||
|
||||
|
||||
func _on_body_selected(body: Dictionary) -> void:
|
||||
(
|
||||
nav
|
||||
. push(
|
||||
"planet",
|
||||
{
|
||||
"body": body,
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
}
|
||||
)
|
||||
)
|
||||
nav.push("regional", {
|
||||
"body": body,
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
})
|
||||
|
||||
|
||||
func _on_regional_back() -> void:
|
||||
|
||||
@@ -31,6 +31,15 @@ const COLOR_POP_HEAT: Color = Color(0.95, 0.35, 0.25, 0.22)
|
||||
const COLOR_PRODUCTION: Color = Color(0.40, 0.80, 0.55, 0.18)
|
||||
const COLOR_SHADOW: Color = Color(0.35, 0.20, 0.50, 0.22)
|
||||
const COLOR_CORP: Color = Color(0.85, 0.65, 0.20, 0.75)
|
||||
# Generation overlays (#960, D-225, Araminta's encoding).
|
||||
# Rivers/mouths use the reliefmap's own water colour (sampled median ocean blue)
|
||||
# so river lines blend seamlessly into the surface water bodies instead of
|
||||
# reading as a distinct-coloured line flowing onto the sea.
|
||||
const COLOR_GEN_RIVER: Color = Color(0.353, 0.647, 0.776, 1.0)
|
||||
const COLOR_GEN_MOUTH: Color = Color(0.353, 0.647, 0.776, 1.0)
|
||||
const COLOR_GEN_BASIN_FILL: Color = Color(0.20, 0.35, 0.55, 0.06)
|
||||
const COLOR_GEN_BASIN_LINE: Color = Color(0.45, 0.65, 0.85, 0.45)
|
||||
const GEN_ATTRACTOR_MIN_STRENGTH: float = 0.15
|
||||
|
||||
const RAIL_DASH_ON: float = 6.0
|
||||
const RAIL_DASH_OFF: float = 4.0
|
||||
@@ -49,6 +58,14 @@ const PRODUCTION_FUNCTIONS: Array = [
|
||||
|
||||
var viewer = null # AtlasViewer (untyped to avoid cyclic ref)
|
||||
|
||||
# Generation-overlay coordinate mapping (#960): set in _draw before the gen
|
||||
# layers are drawn. Positions are [row, col] in the Layer-1 working grid
|
||||
# (_gen_grid_*), mapped onto the displayed heightmap texture (_gen_tex_*).
|
||||
var _gen_grid_w: float = 0.0
|
||||
var _gen_grid_h: float = 0.0
|
||||
var _gen_tex_w: float = 0.0
|
||||
var _gen_tex_h: float = 0.0
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if viewer == null:
|
||||
@@ -92,6 +109,24 @@ func _draw() -> void:
|
||||
if viewer.is_overlay_visible("corp_presence"):
|
||||
_draw_corp_presence(markers)
|
||||
|
||||
# Generation-cascade layers (#960, D-225) — from the proxied Layer1Output.
|
||||
# Draw order: basins (area) under rivers under attractors (point anchors).
|
||||
var layer1: Variant = viewer.get_generation_layer1()
|
||||
if layer1 is Dictionary:
|
||||
# The Layer1Output carries the working-grid dims its positions live in
|
||||
# (#960). Map from those, NOT the markers.json grid (_grid_w/_grid_h).
|
||||
_gen_grid_w = float(layer1.get("grid_w", 0))
|
||||
_gen_grid_h = float(layer1.get("grid_h", 0))
|
||||
_gen_tex_w = tex_w
|
||||
_gen_tex_h = tex_h
|
||||
if _gen_grid_w > 0.0 and _gen_grid_h > 0.0:
|
||||
if viewer.is_overlay_visible("gen_l1_basins"):
|
||||
_draw_gen_basins(layer1)
|
||||
if viewer.is_overlay_visible("gen_l1_rivers"):
|
||||
_draw_gen_rivers(layer1)
|
||||
if viewer.is_overlay_visible("gen_l1_attractors"):
|
||||
_draw_gen_attractors(layer1)
|
||||
|
||||
# POIs (non-gate first, then gates on top if enabled)
|
||||
_draw_pois(markers)
|
||||
|
||||
@@ -242,7 +277,6 @@ static func _city_key(city: Dictionary) -> String:
|
||||
# Province boundaries (D-205, #927)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
const COLOR_PROVINCE_BORDER: Color = Color(0.45, 0.65, 0.85, 0.55)
|
||||
const COLOR_PROVINCE_FILL: Color = Color(0.25, 0.45, 0.65, 0.08)
|
||||
const PROVINCE_BORDER_WIDTH: float = 1.2
|
||||
@@ -251,7 +285,16 @@ const PROVINCE_BORDER_WIDTH: float = 1.2
|
||||
func _draw_province_boundaries(markers: Dictionary) -> void:
|
||||
var provinces: Array = markers.get("provinces", [])
|
||||
if provinces.is_empty():
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(viewer.get_heightmap_texture().get_width(), viewer.get_heightmap_texture().get_height())), COLOR_POLITICAL)
|
||||
draw_rect(
|
||||
Rect2(
|
||||
Vector2.ZERO,
|
||||
Vector2(
|
||||
viewer.get_heightmap_texture().get_width(),
|
||||
viewer.get_heightmap_texture().get_height()
|
||||
)
|
||||
),
|
||||
COLOR_POLITICAL
|
||||
)
|
||||
return
|
||||
for prov: Dictionary in provinces:
|
||||
var path: Array = prov.get("path", [])
|
||||
@@ -329,6 +372,124 @@ func _draw_corp_presence(markers: Dictionary) -> void:
|
||||
# Helpers
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Generation-cascade overlays (#960, D-225)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Map a Layer-1 [row, col] position (in the Layer1Output working grid) onto the
|
||||
## displayed heightmap texture. Uses the grid dims FROM THE DATA — NOT
|
||||
## viewer.grid_to_canvas, whose _grid_w/_grid_h is the markers.json coordinate
|
||||
## space. Conflating the two scaled the overlays off by the downsample ratio (#960).
|
||||
func _gen_pos(rc: Variant) -> Vector2:
|
||||
return Vector2(float(rc[1]) / _gen_grid_w * _gen_tex_w, float(rc[0]) / _gen_grid_h * _gen_tex_h)
|
||||
|
||||
|
||||
func _draw_gen_rivers(layer1: Dictionary) -> void:
|
||||
var rn: Variant = layer1.get("river_network")
|
||||
if not rn is Dictionary:
|
||||
return
|
||||
for c: Variant in rn.get("river_cells", []):
|
||||
if c is Array and c.size() >= 2:
|
||||
draw_circle(_gen_pos(c), 1.2, COLOR_GEN_RIVER)
|
||||
for cf: Variant in rn.get("confluences", []):
|
||||
if cf is Array and cf.size() >= 2:
|
||||
draw_circle(_gen_pos(cf), 3.0, COLOR_GEN_RIVER)
|
||||
for m: Variant in rn.get("mouths", []):
|
||||
if m is Array and m.size() >= 2:
|
||||
var p: Vector2 = _gen_pos(m)
|
||||
# Double ring = sea terminus (high-value anchor).
|
||||
draw_arc(p, 5.0, 0.0, TAU, 18, COLOR_GEN_MOUTH, 1.5)
|
||||
var halo := Color(COLOR_GEN_MOUTH.r, COLOR_GEN_MOUTH.g, COLOR_GEN_MOUTH.b, 0.30)
|
||||
draw_arc(p, 8.0, 0.0, TAU, 22, halo, 1.0)
|
||||
|
||||
|
||||
func _draw_gen_basins(layer1: Dictionary) -> void:
|
||||
for b: Variant in layer1.get("drainage_basins", []):
|
||||
if not b is Dictionary:
|
||||
continue
|
||||
var boundary: Array = b.get("boundary", [])
|
||||
var pts: PackedVector2Array = PackedVector2Array()
|
||||
for pt: Variant in boundary:
|
||||
if pt is Array and pt.size() >= 2:
|
||||
pts.append(_gen_pos(pt))
|
||||
if pts.size() < 2:
|
||||
continue
|
||||
if pts.size() >= 3:
|
||||
draw_colored_polygon(pts, COLOR_GEN_BASIN_FILL)
|
||||
var loop: PackedVector2Array = pts.duplicate()
|
||||
loop.append(pts[0])
|
||||
draw_polyline(loop, COLOR_GEN_BASIN_LINE, 0.8, true)
|
||||
|
||||
|
||||
func _draw_gen_attractors(layer1: Dictionary) -> void:
|
||||
for a: Variant in layer1.get("attractors", []):
|
||||
if not a is Dictionary:
|
||||
continue
|
||||
var strength: float = float(a.get("strength", 0.0))
|
||||
if strength < GEN_ATTRACTOR_MIN_STRENGTH:
|
||||
continue
|
||||
var pos_rc: Variant = a.get("position")
|
||||
if not pos_rc is Array or pos_rc.size() < 2:
|
||||
continue
|
||||
var size: float = 5.0 + strength * 4.0
|
||||
var color: Color = _sub_biome_color(str(a.get("sub_biome", "")))
|
||||
_draw_attractor_shape(str(a.get("attractor_type", "")), _gen_pos(pos_rc), size, color)
|
||||
|
||||
|
||||
## Sub-biome → marker color (Araminta's palette). Color is additive info; shape
|
||||
## carries the attractor-type identity (survives monochrome capture).
|
||||
func _sub_biome_color(sub_biome: String) -> Color:
|
||||
match sub_biome:
|
||||
"TropicalWet", "CoastalLowland":
|
||||
return Color(0.25, 0.72, 0.65, 0.90) # teal — coastal/tropical
|
||||
"TemperateForest", "TemperateGrassland":
|
||||
return Color(0.45, 0.68, 0.45, 0.90) # sage — temperate
|
||||
"Desert", "Savanna":
|
||||
return Color(0.78, 0.62, 0.35, 0.90) # sand — arid
|
||||
"Alpine":
|
||||
return Color(0.52, 0.58, 0.72, 0.90) # slate — alpine
|
||||
"Wetland":
|
||||
return Color(0.40, 0.60, 0.52, 0.90) # muted teal — wetland
|
||||
"Tundra", "BorealForest":
|
||||
return Color(0.55, 0.68, 0.82, 0.90) # cool grey-blue — cold
|
||||
_:
|
||||
return Color(0.72, 0.72, 0.76, 0.90) # default grey
|
||||
|
||||
|
||||
## Attractor type → marker shape (Araminta's vocabulary, 7 types).
|
||||
func _draw_attractor_shape(atype: String, pos: Vector2, size: float, color: Color) -> void:
|
||||
match atype:
|
||||
"RiverMouth":
|
||||
draw_circle(pos, size, color)
|
||||
"CoastalAccess":
|
||||
draw_arc(pos, size, 0.0, TAU, 18, color, 1.5)
|
||||
draw_circle(pos, size * 0.5, color)
|
||||
"RiverCrossing":
|
||||
_draw_diamond(pos, size, color)
|
||||
"ValleyFloor":
|
||||
_draw_triangle(pos, size, color, true)
|
||||
"PassEntrance":
|
||||
_draw_triangle(pos, size, color, false)
|
||||
"LakeShore":
|
||||
draw_arc(pos, size, 0.0, TAU, 18, color, 1.5)
|
||||
"PlainCenter":
|
||||
draw_rect(Rect2(pos - Vector2(size, size) * 0.7, Vector2(size, size) * 1.4), color)
|
||||
_:
|
||||
draw_circle(pos, size, color)
|
||||
|
||||
|
||||
func _draw_triangle(pos: Vector2, size: float, color: Color, point_down: bool) -> void:
|
||||
var dir: float = 1.0 if point_down else -1.0
|
||||
var pts: PackedVector2Array = PackedVector2Array(
|
||||
[
|
||||
pos + Vector2(0.0, size * dir),
|
||||
pos + Vector2(-size, -size * dir),
|
||||
pos + Vector2(size, -size * dir),
|
||||
]
|
||||
)
|
||||
draw_colored_polygon(pts, color)
|
||||
|
||||
|
||||
func _path_to_canvas(path: Array) -> PackedVector2Array:
|
||||
var out: PackedVector2Array = PackedVector2Array()
|
||||
|
||||
@@ -31,6 +31,11 @@ const MIN_ZOOM: float = 0.5
|
||||
const MAX_ZOOM: float = 8.0
|
||||
const ZOOM_STEP: float = 1.15
|
||||
|
||||
# #960: Layer-1 proxy re-poll. The proxy returns Pending on a cache miss and
|
||||
# generates in the background (D-225); the client re-requests until Ready.
|
||||
const GEN_RETRY_DELAY: float = 0.5
|
||||
const GEN_MAX_RETRIES: int = 20 # ~10s ceiling before giving up
|
||||
|
||||
const PANEL_WIDTH: float = 320.0
|
||||
const PANEL_MARGIN: float = 16.0
|
||||
|
||||
@@ -98,12 +103,6 @@ const OVERLAY_DEFS: Array = [
|
||||
"group": "toggle",
|
||||
"tooltip": "Production zones (D-181 observable — requires presence)."
|
||||
},
|
||||
{
|
||||
"id": "shadow_economy",
|
||||
"label": "SHD",
|
||||
"group": "toggle",
|
||||
"tooltip": "Shadow economy zones (D-181 observable)."
|
||||
},
|
||||
{
|
||||
"id": "corp_presence",
|
||||
"label": "CRP",
|
||||
@@ -122,6 +121,24 @@ const OVERLAY_DEFS: Array = [
|
||||
"group": "locked",
|
||||
"tooltip": "Production vs baseline — LOCKED. Needs insider access (D-181 private)."
|
||||
},
|
||||
{
|
||||
"id": "gen_l1_rivers",
|
||||
"label": "RVR",
|
||||
"group": "toggle",
|
||||
"tooltip": "Layer 1 — river network (generation overlay, #960)."
|
||||
},
|
||||
{
|
||||
"id": "gen_l1_basins",
|
||||
"label": "BAS",
|
||||
"group": "toggle",
|
||||
"tooltip": "Layer 1 — drainage basins (generation overlay, #960)."
|
||||
},
|
||||
{
|
||||
"id": "gen_l1_attractors",
|
||||
"label": "ATR",
|
||||
"group": "toggle",
|
||||
"tooltip": "Layer 1 — geographic attractors (generation overlay, #960)."
|
||||
},
|
||||
]
|
||||
|
||||
# ── Context (set by show_body) ─────────────────────────────────────────────────
|
||||
@@ -132,6 +149,9 @@ var _implant_theme = null
|
||||
# ── Heightmap + markers ───────────────────────────────────────────────────────
|
||||
var _heightmap_texture: Texture2D = null
|
||||
var _markers: Dictionary = {}
|
||||
var _generation_layer1: Variant = null # #960: Layer1Output from the proxy (D-225)
|
||||
var _gen_pending: bool = false # #960: awaiting a Layer1 response (re-polls on Pending)
|
||||
var _gen_retries: int = 0
|
||||
var _grid_w: float = 512.0
|
||||
var _grid_h: float = 256.0
|
||||
var _tex_w: float = 1024.0
|
||||
@@ -162,6 +182,7 @@ var _city_panel = null # ImplantPanel sidebar (city data)
|
||||
var _empty_notice = null # ImplantPanel shown when heightmap missing
|
||||
var _overlay_bar = null # #836 overlay toggle bar (no class_name, review #8)
|
||||
var _screen_header: ImplantHeader = null # top-left title/hint (D-169 composition)
|
||||
var _gen_pending_indicator = null # ImplantPending — loaded by path, not a class_name dep
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -195,6 +216,25 @@ func _ready() -> void:
|
||||
_build_empty_notice()
|
||||
_build_overlay_bar()
|
||||
|
||||
# #960: receive proxied Layer-1 generation output (D-225).
|
||||
SimBridge.atlas_layers_received.connect(_on_atlas_layers_received)
|
||||
|
||||
# #960: diegetic "generating" indicator, shown only while the proxy is Pending.
|
||||
# Loaded by path (not `ImplantPending.new()`) so a stale global-class cache —
|
||||
# e.g. a running session that hasn't re-imported after this class was added —
|
||||
# can't fail to parse AtlasViewer and break the atlas from opening (#960).
|
||||
_gen_pending_indicator = load("res://ui/implant/implant_pending.gd").new()
|
||||
_gen_pending_indicator.name = "GenPending"
|
||||
add_child(_gen_pending_indicator)
|
||||
_gen_pending_indicator.apply_implant_theme(_implant_theme)
|
||||
resized.connect(_position_pending_indicator)
|
||||
_position_pending_indicator()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if SimBridge.atlas_layers_received.is_connected(_on_atlas_layers_received):
|
||||
SimBridge.atlas_layers_received.disconnect(_on_atlas_layers_received)
|
||||
|
||||
|
||||
## Called by RegionalScreen.enter() when entering the viewer for a specific body.
|
||||
func show_body(body: Dictionary, system: Dictionary) -> void:
|
||||
@@ -208,6 +248,12 @@ func show_body(body: Dictionary, system: Dictionary) -> void:
|
||||
_refresh_screen_header()
|
||||
_city_panel.visible = false
|
||||
_empty_notice.visible = (_heightmap_texture == null)
|
||||
_generation_layer1 = null
|
||||
_gen_pending = false
|
||||
_gen_retries = 0
|
||||
if _gen_pending_indicator:
|
||||
_gen_pending_indicator.stop()
|
||||
_request_generation_layers()
|
||||
grab_focus()
|
||||
queue_redraw()
|
||||
_overlay_node.queue_redraw()
|
||||
@@ -240,6 +286,86 @@ func get_markers() -> Dictionary:
|
||||
return _markers
|
||||
|
||||
|
||||
## #960: store the Layer-1 generation output (from atlas_layers_received) and
|
||||
## redraw the overlay. The marker overlay reads it via get_generation_layer1().
|
||||
func set_generation_layer1(layer1: Variant) -> void:
|
||||
_generation_layer1 = layer1
|
||||
if _overlay_node:
|
||||
_overlay_node.queue_redraw()
|
||||
|
||||
|
||||
func get_generation_layer1() -> Variant:
|
||||
return _generation_layer1
|
||||
|
||||
|
||||
## #960: request the body's Layer-1 cascade output from the server proxy.
|
||||
## No-op in test mode (SimBridge has no server connection) — the overlays
|
||||
## simply stay empty, which is the correct serverless behavior.
|
||||
func _request_generation_layers() -> void:
|
||||
var body_id: String = _dict_str(_body, "body_id", "")
|
||||
if body_id.is_empty():
|
||||
return
|
||||
_gen_pending = true
|
||||
SimBridge.request_atlas_layers(body_id)
|
||||
|
||||
|
||||
## #960: handle a Layer-1 response. Ignores responses for a stale body (the
|
||||
## user navigated away). On Pending the proxy is still generating, so we
|
||||
## re-request after a short delay until Ready or the retry ceiling.
|
||||
func _on_atlas_layers_received(response: Dictionary) -> void:
|
||||
var body_id: String = _dict_str(_body, "body_id", "")
|
||||
if str(response.get("body_id", "")) != body_id:
|
||||
return
|
||||
match str(response.get("status", "")):
|
||||
"Ready":
|
||||
_gen_pending = false
|
||||
_gen_retries = 0
|
||||
_set_gen_indicator(false)
|
||||
set_generation_layer1(response.get("layer1"))
|
||||
"Pending":
|
||||
if _gen_retries < GEN_MAX_RETRIES:
|
||||
_gen_retries += 1
|
||||
_set_gen_indicator(true)
|
||||
_schedule_gen_retry(body_id)
|
||||
else:
|
||||
_gen_pending = false # gave up — overlays stay empty
|
||||
_set_gen_indicator(false)
|
||||
_:
|
||||
_gen_pending = false # NotFound / Error — nothing to draw
|
||||
_set_gen_indicator(false)
|
||||
|
||||
|
||||
func _schedule_gen_retry(body_id: String) -> void:
|
||||
var timer := get_tree().create_timer(GEN_RETRY_DELAY)
|
||||
timer.timeout.connect(
|
||||
func() -> void:
|
||||
# Re-request only if still on the same body and still waiting.
|
||||
if _gen_pending and _dict_str(_body, "body_id", "") == body_id:
|
||||
SimBridge.request_atlas_layers(body_id)
|
||||
)
|
||||
|
||||
|
||||
## Show/hide the diegetic pending indicator. Starting only when not already
|
||||
## visible avoids resetting the sweep on every re-poll.
|
||||
func _set_gen_indicator(on: bool) -> void:
|
||||
if _gen_pending_indicator == null:
|
||||
return
|
||||
if on:
|
||||
if not _gen_pending_indicator.visible:
|
||||
_position_pending_indicator()
|
||||
_gen_pending_indicator.start("GENERATING LAYER 1")
|
||||
else:
|
||||
_gen_pending_indicator.stop()
|
||||
|
||||
|
||||
func _position_pending_indicator() -> void:
|
||||
if _gen_pending_indicator == null:
|
||||
return
|
||||
_gen_pending_indicator.position = Vector2(
|
||||
(size.x - _gen_pending_indicator.DEFAULT_WIDTH) * 0.5, size.y * 0.45
|
||||
)
|
||||
|
||||
|
||||
func get_hovered_city() -> Dictionary:
|
||||
return _hovered_city
|
||||
|
||||
@@ -268,15 +394,21 @@ func _load_heightmap() -> void:
|
||||
var ref: Variant = _body.get("terrain_reference")
|
||||
if ref == null or str(ref).is_empty():
|
||||
return
|
||||
var path: String = str(ref)
|
||||
if not path.begins_with("res://"):
|
||||
path = "res://" + path.lstrip("/")
|
||||
if not ResourceLoader.exists(path):
|
||||
push_warning("AtlasViewer: terrain_reference not found at %s" % path)
|
||||
# terrain_reference points at <body>/heightmap.png — now the canonical
|
||||
# 16-bit GRAYSCALE elevation (D-202 amended, #963). For display we load the
|
||||
# color reliefmap.png that sits beside it.
|
||||
var path: String = str(ref).get_base_dir() + "/reliefmap.png"
|
||||
if not path.begins_with("/"):
|
||||
var project_root: String = (
|
||||
ProjectSettings.globalize_path("res://").get_base_dir().get_base_dir()
|
||||
)
|
||||
path = project_root + "/" + path
|
||||
if not FileAccess.file_exists(path):
|
||||
push_warning("AtlasViewer: reliefmap not found at %s" % path)
|
||||
return
|
||||
var tex: Variant = load(path)
|
||||
if tex is Texture2D:
|
||||
_heightmap_texture = tex
|
||||
var img := Image.load_from_file(path)
|
||||
if img != null:
|
||||
_heightmap_texture = ImageTexture.create_from_image(img)
|
||||
_tex_w = float(_heightmap_texture.get_width())
|
||||
_tex_h = float(_heightmap_texture.get_height())
|
||||
|
||||
@@ -292,8 +424,11 @@ func _load_markers() -> void:
|
||||
if ref == null or str(ref).is_empty():
|
||||
return
|
||||
var hm_path: String = str(ref)
|
||||
if not hm_path.begins_with("res://"):
|
||||
hm_path = "res://" + hm_path.lstrip("/")
|
||||
if not hm_path.begins_with("/"):
|
||||
var project_root: String = (
|
||||
ProjectSettings.globalize_path("res://").get_base_dir().get_base_dir()
|
||||
)
|
||||
hm_path = project_root + "/" + hm_path
|
||||
var dir: String = hm_path.get_base_dir()
|
||||
var markers_path: String = dir + "/markers.json"
|
||||
|
||||
@@ -386,6 +521,7 @@ func _build_screen_header() -> void:
|
||||
# theme-swap invariant violation.
|
||||
_screen_header = ImplantHeader.new()
|
||||
_screen_header.position = Vector2(PANEL_MARGIN, 16.0)
|
||||
_screen_header.custom_minimum_size.x = 320.0
|
||||
_screen_header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_screen_header)
|
||||
if _implant_theme:
|
||||
@@ -418,6 +554,14 @@ static func _dict_str(d: Dictionary, key: String, fallback: String) -> String:
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _is_over_ui(pos: Vector2) -> bool:
|
||||
if _overlay_bar and _overlay_bar.get_global_rect().has_point(pos):
|
||||
return true
|
||||
if _city_panel and _city_panel.visible and _city_panel.get_global_rect().has_point(pos):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.is_echo():
|
||||
_handle_key(event as InputEventKey)
|
||||
@@ -426,6 +570,12 @@ func _gui_input(event: InputEvent) -> void:
|
||||
if _heightmap_texture == null:
|
||||
return
|
||||
|
||||
if (
|
||||
event is InputEventMouseButton
|
||||
and _is_over_ui((event as InputEventMouseButton).global_position)
|
||||
):
|
||||
return
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
if mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
|
||||
@@ -441,6 +591,8 @@ func _gui_input(event: InputEvent) -> void:
|
||||
else:
|
||||
_dragging = false
|
||||
elif event is InputEventMouseMotion:
|
||||
if _is_over_ui((event as InputEventMouseMotion).global_position):
|
||||
return
|
||||
var mm := event as InputEventMouseMotion
|
||||
if _dragging:
|
||||
_view_offset = _drag_start_offset + (mm.position - _drag_start_mouse)
|
||||
|
||||
@@ -438,7 +438,10 @@ func _gui_input(event: InputEvent) -> void:
|
||||
if mb.pressed:
|
||||
match mb.button_index:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
_handle_reach_click(mb.position)
|
||||
if mb.double_click:
|
||||
_handle_reach_double_click(mb.position)
|
||||
else:
|
||||
_handle_reach_click(mb.position)
|
||||
MOUSE_BUTTON_MIDDLE:
|
||||
_reach_is_panning = true
|
||||
_reach_pan_start = mb.position
|
||||
@@ -476,6 +479,13 @@ func _handle_reach_click(pos: Vector2) -> void:
|
||||
_dirty = true
|
||||
|
||||
|
||||
func _handle_reach_double_click(pos: Vector2) -> void:
|
||||
var sid := _find_nearest_reach_system(pos)
|
||||
if not sid.is_empty():
|
||||
_reach_selected = sid
|
||||
system_selected.emit(sid)
|
||||
|
||||
|
||||
func _update_reach_hover(pos: Vector2) -> void:
|
||||
var nearest := _find_nearest_reach_system(pos)
|
||||
if nearest != _reach_hovered:
|
||||
|
||||
@@ -6,14 +6,15 @@ extends Control
|
||||
|
||||
signal body_selected(body: Dictionary)
|
||||
|
||||
const ORBITAL_CENTER_FRACTION := Vector2(0.5, 0.55)
|
||||
const STAR_RADIUS: float = 12.0
|
||||
const ORBITAL_RING_BASE: float = 65.0
|
||||
const ORBITAL_RING_STEP: float = 58.0
|
||||
const MOON_ORBIT_RADIUS: float = 24.0
|
||||
const STATION_SIZE: float = 6.0
|
||||
const BODY_HIT_RADIUS: float = 16.0
|
||||
const LABEL_OFFSET: float = 11.0
|
||||
const STAR_RADIUS: float = 140.0
|
||||
const STAR_X_OFFSET: float = 0.0
|
||||
const BODY_LEFT_MARGIN: float = 300.0
|
||||
const BODY_RIGHT_MARGIN: float = 60.0
|
||||
const MOON_CLEARANCE: float = 12.0
|
||||
const STATION_SIZE: float = 8.0
|
||||
const STATION_OFFSET_Y: float = -26.0
|
||||
const BODY_HIT_RADIUS: float = 20.0
|
||||
const LABEL_OFFSET: float = 14.0
|
||||
const PANEL_WIDTH: float = 320.0
|
||||
const PANEL_MARGIN: float = 16.0
|
||||
|
||||
@@ -35,15 +36,18 @@ var _orbital_bodies: Array = []
|
||||
var _orbital_stations: Array = []
|
||||
var _body_positions: Dictionary = {}
|
||||
var _station_positions: Dictionary = {}
|
||||
var _body_textures: Dictionary = {} # body_id -> ImageTexture
|
||||
var _hovered_body: String = ""
|
||||
var _hovered_station: String = ""
|
||||
var _selected_station: Dictionary = {}
|
||||
var _selected_body: Dictionary = {}
|
||||
var _dirty: bool = true
|
||||
var _in_orbital: bool = false
|
||||
|
||||
var _picker_panel = null # ImplantPanel
|
||||
var _picker_nav_row = null # ImplantDataRow
|
||||
var _station_panel = null # ImplantPanel
|
||||
var _body_panel = null # ImplantPanel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -62,6 +66,7 @@ func _process(_delta: float) -> void:
|
||||
func setup(implant_theme) -> void:
|
||||
_build_picker_panel(implant_theme)
|
||||
_build_station_panel(implant_theme)
|
||||
_build_body_panel(implant_theme)
|
||||
|
||||
|
||||
func set_systems(systems: Array) -> void:
|
||||
@@ -116,11 +121,15 @@ func load_and_show_orbital() -> void:
|
||||
_hovered_body = ""
|
||||
_hovered_station = ""
|
||||
_selected_station = {}
|
||||
_selected_body = {}
|
||||
_load_body_textures(sys.get("system_id", ""))
|
||||
_compute_body_positions()
|
||||
if _picker_panel:
|
||||
_picker_panel.visible = false
|
||||
if _station_panel:
|
||||
_station_panel.visible = false
|
||||
if _body_panel:
|
||||
_body_panel.visible = false
|
||||
_in_orbital = true
|
||||
_dirty = true
|
||||
|
||||
@@ -157,27 +166,30 @@ func _draw() -> void:
|
||||
|
||||
func _draw_orbital() -> void:
|
||||
var sz: Vector2 = get_rect().size
|
||||
var center: Vector2 = sz * ORBITAL_CENTER_FRACTION
|
||||
var mid_y: float = sz.y * 0.5
|
||||
var star_center := Vector2(STAR_X_OFFSET, mid_y)
|
||||
|
||||
draw_rect(Rect2(Vector2.ZERO, sz), COLOR_BG)
|
||||
_draw_orbit_rings(center)
|
||||
draw_circle(center, STAR_RADIUS + 4.0, Color(COLOR_STAR.r, COLOR_STAR.g, COLOR_STAR.b, 0.18))
|
||||
draw_circle(center, STAR_RADIUS, COLOR_STAR)
|
||||
draw_circle(star_center, STAR_RADIUS + 40.0, Color(COLOR_STAR.r, COLOR_STAR.g, COLOR_STAR.b, 0.04))
|
||||
draw_circle(star_center, STAR_RADIUS + 20.0, Color(COLOR_STAR.r, COLOR_STAR.g, COLOR_STAR.b, 0.08))
|
||||
draw_circle(star_center, STAR_RADIUS, COLOR_STAR)
|
||||
_draw_orbital_arcs(star_center)
|
||||
_draw_stations()
|
||||
_draw_bodies()
|
||||
|
||||
|
||||
func _draw_orbit_rings(center: Vector2) -> void:
|
||||
var seen_orbits: Dictionary = {}
|
||||
func _draw_orbital_arcs(star_center: Vector2) -> void:
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
if b.get("parent_body_id") != null:
|
||||
continue
|
||||
var idx: int = int(b.get("orbit_index", 0))
|
||||
if seen_orbits.has(idx):
|
||||
var bid: String = str(b.get("body_id", ""))
|
||||
if not _body_positions.has(bid):
|
||||
continue
|
||||
seen_orbits[idx] = true
|
||||
var r: float = ORBITAL_RING_BASE + (idx - 1) * ORBITAL_RING_STEP
|
||||
draw_arc(center, r, 0.0, TAU, 64, COLOR_RING, 0.5, true)
|
||||
var body_pos: Vector2 = _body_positions[bid]
|
||||
var orbit_r: float = body_pos.x - star_center.x
|
||||
if orbit_r <= 0.0:
|
||||
continue
|
||||
draw_arc(star_center, orbit_r, -PI * 0.7, PI * 0.7, 64, COLOR_RING, 0.5, true)
|
||||
|
||||
|
||||
func _draw_bodies() -> void:
|
||||
@@ -188,36 +200,31 @@ func _draw_bodies() -> void:
|
||||
continue
|
||||
var pos: Vector2 = _body_positions[bid]
|
||||
var body_type: String = b.get("body_type", "")
|
||||
var atmo: String = b.get("atmosphere", "none")
|
||||
var inhabited: bool = bool(b.get("inhabited", false))
|
||||
|
||||
var color: Color
|
||||
var radius: float
|
||||
match body_type:
|
||||
"moon":
|
||||
color = COLOR_MOON
|
||||
radius = 3.5
|
||||
"oort_cloud":
|
||||
color = COLOR_OORT
|
||||
radius = 2.0
|
||||
_:
|
||||
if inhabited:
|
||||
color = COLOR_PLANET_INHABITED
|
||||
radius = 6.0
|
||||
elif atmo in ["breathable", "standard"]:
|
||||
color = COLOR_PLANET_HABITABLE
|
||||
radius = 5.5
|
||||
else:
|
||||
color = COLOR_PLANET_BARE
|
||||
radius = 4.5
|
||||
var radius_km: float = float(b.get("body_radius_km", 0))
|
||||
var radius: float = _km_to_draw_radius(radius_km, body_type)
|
||||
|
||||
if bid == _hovered_body:
|
||||
draw_arc(pos, radius + 5.0, 0.0, TAU, 20, Color(1.0, 1.0, 1.0, 0.25), 1.0, true)
|
||||
draw_arc(pos, radius + 5.0, 0.0, TAU, 20, Color(1.0, 1.0, 1.0, 0.25), 1.5, true)
|
||||
|
||||
draw_circle(pos, radius, color)
|
||||
var tex: ImageTexture = _body_textures.get(bid)
|
||||
if tex:
|
||||
var draw_size := Vector2(radius * 2.0, radius * 2.0)
|
||||
draw_texture_rect(tex, Rect2(pos - draw_size / 2.0, draw_size), false)
|
||||
else:
|
||||
var atmo: String = b.get("atmosphere", "none")
|
||||
var inhabited: bool = bool(b.get("inhabited", false))
|
||||
var color: Color
|
||||
match body_type:
|
||||
"moon": color = COLOR_MOON
|
||||
"oort_cloud": color = COLOR_OORT
|
||||
_:
|
||||
if inhabited: color = COLOR_PLANET_INHABITED
|
||||
elif atmo in ["breathable", "standard"]: color = COLOR_PLANET_HABITABLE
|
||||
else: color = COLOR_PLANET_BARE
|
||||
draw_circle(pos, radius, color)
|
||||
|
||||
var label: String = b.get("proper_name", bid) if b.get("proper_name") else bid
|
||||
var show_label: bool = inhabited or bid == _hovered_body
|
||||
var show_label: bool = body_type != "oort_cloud" or bid == _hovered_body
|
||||
if show_label:
|
||||
var lcolor: Color = COLOR_TEXT if bid == _hovered_body else COLOR_TEXT_DIM
|
||||
var lsz: Vector2 = font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 9)
|
||||
@@ -269,7 +276,8 @@ func _compute_body_positions() -> void:
|
||||
var sz: Vector2 = get_rect().size
|
||||
if sz == Vector2.ZERO:
|
||||
sz = Vector2(1280.0, 720.0)
|
||||
var center: Vector2 = sz * ORBITAL_CENTER_FRACTION
|
||||
var mid_y: float = sz.y * 0.5
|
||||
var usable_width: float = sz.x - BODY_LEFT_MARGIN - BODY_RIGHT_MARGIN
|
||||
|
||||
var top_bodies: Array = []
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
@@ -280,30 +288,16 @@ func _compute_body_positions() -> void:
|
||||
return int(a.get("orbit_index", 0)) < int(b.get("orbit_index", 0))
|
||||
)
|
||||
|
||||
var by_orbit: Dictionary = {}
|
||||
for b: Dictionary in top_bodies:
|
||||
var idx: int = int(b.get("orbit_index", 0))
|
||||
if not by_orbit.has(idx):
|
||||
by_orbit[idx] = []
|
||||
by_orbit[idx].append(b)
|
||||
var count: int = top_bodies.size()
|
||||
for i: int in range(count):
|
||||
var b: Dictionary = top_bodies[i]
|
||||
var bid: String = str(b.get("body_id", ""))
|
||||
if bid.is_empty():
|
||||
continue
|
||||
var t: float = float(i) / float(maxi(count - 1, 1))
|
||||
var x: float = BODY_LEFT_MARGIN + t * usable_width
|
||||
_body_positions[bid] = Vector2(x, mid_y)
|
||||
|
||||
for orbit_idx: int in by_orbit:
|
||||
var ring_bodies: Array = by_orbit[orbit_idx]
|
||||
var ring_r: float = ORBITAL_RING_BASE + (orbit_idx - 1) * ORBITAL_RING_STEP
|
||||
var count: int = ring_bodies.size()
|
||||
for i: int in range(count):
|
||||
var b: Dictionary = ring_bodies[i]
|
||||
var bid: String = str(b.get("body_id", ""))
|
||||
if bid.is_empty():
|
||||
continue
|
||||
var angle: float
|
||||
if count == 1:
|
||||
angle = -PI / 2.0
|
||||
else:
|
||||
angle = -PI / 2.0 + TAU * float(i) / float(count)
|
||||
_body_positions[bid] = center + Vector2(cos(angle), sin(angle)) * ring_r
|
||||
|
||||
# Moons per parent count drives spacing — hard-coded divisor caused overlap on gas giants
|
||||
var moons_by_parent: Dictionary = {}
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
var parent_id: Variant = b.get("parent_body_id")
|
||||
@@ -313,6 +307,10 @@ func _compute_body_positions() -> void:
|
||||
if not moons_by_parent.has(key):
|
||||
moons_by_parent[key] = []
|
||||
moons_by_parent[key].append(b)
|
||||
var body_dict_by_id: Dictionary = {}
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
body_dict_by_id[str(b.get("body_id", ""))] = b
|
||||
|
||||
for parent_key: String in moons_by_parent:
|
||||
if not _body_positions.has(parent_key):
|
||||
continue
|
||||
@@ -322,16 +320,23 @@ func _compute_body_positions() -> void:
|
||||
return int(a.get("orbit_index", 0)) < int(b.get("orbit_index", 0))
|
||||
)
|
||||
var parent_pos: Vector2 = _body_positions[parent_key]
|
||||
var count: int = siblings.size()
|
||||
for i: int in range(count):
|
||||
var moon: Dictionary = siblings[i]
|
||||
var parent_data: Dictionary = body_dict_by_id.get(parent_key, {})
|
||||
var parent_draw_r: float = _km_to_draw_radius(
|
||||
float(parent_data.get("body_radius_km", 0)),
|
||||
parent_data.get("body_type", "")
|
||||
)
|
||||
var moon_count: int = siblings.size()
|
||||
for j: int in range(moon_count):
|
||||
var moon: Dictionary = siblings[j]
|
||||
var moon_id: String = str(moon.get("body_id", ""))
|
||||
if moon_id.is_empty():
|
||||
continue
|
||||
var angle: float = -PI / 2.0 + TAU * float(i) / float(count)
|
||||
_body_positions[moon_id] = (
|
||||
parent_pos + Vector2(cos(angle), sin(angle)) * MOON_ORBIT_RADIUS
|
||||
var moon_draw_r: float = _km_to_draw_radius(
|
||||
float(moon.get("body_radius_km", 0)), "moon"
|
||||
)
|
||||
var label_space: float = LABEL_OFFSET + 12.0
|
||||
var y_off: float = parent_draw_r + label_space + MOON_CLEARANCE + (moon_draw_r * 2.0 + MOON_CLEARANCE) * float(j)
|
||||
_body_positions[moon_id] = parent_pos + Vector2(0.0, y_off)
|
||||
|
||||
for s: Dictionary in _orbital_stations:
|
||||
var station_id: String = str(s.get("station_id", ""))
|
||||
@@ -339,11 +344,15 @@ func _compute_body_positions() -> void:
|
||||
continue
|
||||
var parent_id: Variant = s.get("orbits_body_id")
|
||||
var station_parent_pos: Vector2
|
||||
var parent_r: float = 0.0
|
||||
if parent_id != null and _body_positions.has(str(parent_id)):
|
||||
station_parent_pos = _body_positions[str(parent_id)]
|
||||
var pd: Dictionary = body_dict_by_id.get(str(parent_id), {})
|
||||
parent_r = _km_to_draw_radius(float(pd.get("body_radius_km", 0)), pd.get("body_type", ""))
|
||||
else:
|
||||
station_parent_pos = center
|
||||
_station_positions[station_id] = station_parent_pos + Vector2(20.0, -10.0)
|
||||
station_parent_pos = Vector2(sz.x - BODY_RIGHT_MARGIN, mid_y)
|
||||
var station_y: float = -(parent_r + LABEL_OFFSET + 12.0 + STATION_SIZE + MOON_CLEARANCE)
|
||||
_station_positions[station_id] = station_parent_pos + Vector2(0.0, station_y)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -355,12 +364,16 @@ func _gui_input(event: InputEvent) -> void:
|
||||
if not _in_orbital:
|
||||
return
|
||||
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
|
||||
_handle_orbital_click((event as InputEventMouseButton).position)
|
||||
var mb := event as InputEventMouseButton
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT and mb.double_click:
|
||||
_handle_orbital_double_click(mb.position)
|
||||
elif mb.button_index == MOUSE_BUTTON_LEFT:
|
||||
_handle_orbital_click(mb.position)
|
||||
elif event is InputEventMouseMotion:
|
||||
_handle_orbital_hover((event as InputEventMouseMotion).position)
|
||||
|
||||
|
||||
func _handle_orbital_click(pos: Vector2) -> void:
|
||||
func _handle_orbital_double_click(pos: Vector2) -> void:
|
||||
var bid: String = _find_nearest_body(pos)
|
||||
if not bid.is_empty():
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
@@ -368,22 +381,65 @@ func _handle_orbital_click(pos: Vector2) -> void:
|
||||
body_selected.emit(b)
|
||||
return
|
||||
|
||||
|
||||
func _handle_orbital_click(pos: Vector2) -> void:
|
||||
var bid: String = _find_nearest_body(pos)
|
||||
if not bid.is_empty():
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
if str(b.get("body_id", "")) == bid:
|
||||
_selected_body = b
|
||||
_selected_station = {}
|
||||
if _station_panel:
|
||||
_station_panel.visible = false
|
||||
_rebuild_body_panel()
|
||||
if _body_panel:
|
||||
_body_panel.visible = true
|
||||
_dirty = true
|
||||
return
|
||||
|
||||
var sid: String = _find_nearest_station(pos)
|
||||
if not sid.is_empty():
|
||||
for s: Dictionary in _orbital_stations:
|
||||
if str(s.get("station_id", "")) == sid:
|
||||
_selected_station = s
|
||||
_selected_body = {}
|
||||
if _body_panel:
|
||||
_body_panel.visible = false
|
||||
_rebuild_station_panel()
|
||||
if _station_panel:
|
||||
_station_panel.visible = true
|
||||
return
|
||||
|
||||
_close_detail_panels()
|
||||
_dirty = true
|
||||
|
||||
|
||||
func _close_detail_panels() -> void:
|
||||
_selected_body = {}
|
||||
_selected_station = {}
|
||||
if _body_panel:
|
||||
_body_panel.visible = false
|
||||
if _station_panel:
|
||||
_station_panel.visible = false
|
||||
|
||||
|
||||
func has_body_panel_open() -> bool:
|
||||
return not _selected_body.is_empty()
|
||||
|
||||
|
||||
func has_station_panel_open() -> bool:
|
||||
return not _selected_station.is_empty()
|
||||
|
||||
|
||||
func close_panels() -> void:
|
||||
_close_detail_panels()
|
||||
_dirty = true
|
||||
|
||||
|
||||
func get_selected_body() -> Dictionary:
|
||||
return _selected_body
|
||||
|
||||
|
||||
func _handle_orbital_hover(pos: Vector2) -> void:
|
||||
var new_body: String = _find_nearest_body(pos)
|
||||
var new_station: String = "" if not new_body.is_empty() else _find_nearest_station(pos)
|
||||
@@ -415,6 +471,53 @@ func _find_nearest_station(pos: Vector2) -> String:
|
||||
return best_id
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Globe textures
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_body_textures(system_id: String) -> void:
|
||||
_body_textures.clear()
|
||||
var sys_dir: String = system_id.replace(" ", "-")
|
||||
var wiki_root: String = ProjectSettings.globalize_path("res://").get_base_dir().get_base_dir() + "/wiki/star-systems"
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
var bid: String = str(b.get("body_id", ""))
|
||||
if bid.is_empty():
|
||||
continue
|
||||
var bt: String = b.get("body_type", "")
|
||||
var globe_path: String
|
||||
if bt in ["oort_cloud", "asteroid_belt"]:
|
||||
globe_path = wiki_root + "/_generics/" + bt + "/globe.png"
|
||||
else:
|
||||
globe_path = wiki_root + "/" + sys_dir + "/bodies/" + bid + "/globe.png"
|
||||
if not FileAccess.file_exists(globe_path):
|
||||
continue
|
||||
var img := Image.load_from_file(globe_path)
|
||||
if img == null:
|
||||
continue
|
||||
var tex := ImageTexture.create_from_image(img)
|
||||
_body_textures[bid] = tex
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Sizing
|
||||
# =============================================================================
|
||||
|
||||
|
||||
const DRAW_RADIUS_MIN: float = 4.0
|
||||
const DRAW_RADIUS_MAX: float = 48.0
|
||||
const DRAW_RADIUS_REF_KM: float = 6371.0
|
||||
const DRAW_RADIUS_REF_PX: float = 24.0
|
||||
|
||||
|
||||
static func _km_to_draw_radius(radius_km: float, _body_type: String) -> float:
|
||||
if radius_km <= 0.0:
|
||||
return DRAW_RADIUS_MIN
|
||||
var ratio: float = radius_km / DRAW_RADIUS_REF_KM
|
||||
var px: float = DRAW_RADIUS_REF_PX * pow(ratio, 0.5)
|
||||
return maxf(px, DRAW_RADIUS_MIN)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Panels
|
||||
# =============================================================================
|
||||
@@ -471,7 +574,6 @@ func _build_station_panel(implant_theme) -> void:
|
||||
_station_panel.theme_resource = implant_theme
|
||||
_station_panel.custom_minimum_size.x = PANEL_WIDTH
|
||||
_station_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_station_panel.position = Vector2(PANEL_MARGIN, 60.0)
|
||||
_station_panel.visible = false
|
||||
add_child(_station_panel)
|
||||
|
||||
@@ -484,6 +586,9 @@ func _rebuild_station_panel() -> void:
|
||||
if _selected_station.is_empty():
|
||||
return
|
||||
|
||||
var sz: Vector2 = get_rect().size
|
||||
_station_panel.position = Vector2(sz.x - PANEL_WIDTH - 60.0, 60.0)
|
||||
|
||||
var s: Dictionary = _selected_station
|
||||
var s_name: String = (
|
||||
s.get("proper_name", "") if s.get("proper_name") else s.get("station_id", "—")
|
||||
@@ -507,3 +612,91 @@ func _rebuild_station_panel() -> void:
|
||||
_station_panel.add_component(ImplantSeparator.new())
|
||||
_station_panel.add_component(ImplantTextBlock.new("station atlas deferred"))
|
||||
_station_panel.add_component(ImplantTextBlock.new("esc back"))
|
||||
|
||||
|
||||
func _build_body_panel(implant_theme) -> void:
|
||||
_body_panel = ImplantPanel.new()
|
||||
_body_panel.name = "BodyPanel"
|
||||
_body_panel.theme_resource = implant_theme
|
||||
_body_panel.custom_minimum_size.x = PANEL_WIDTH
|
||||
_body_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_body_panel.visible = false
|
||||
add_child(_body_panel)
|
||||
|
||||
|
||||
func _rebuild_body_panel() -> void:
|
||||
if not _body_panel:
|
||||
return
|
||||
_body_panel.clear()
|
||||
|
||||
if _selected_body.is_empty():
|
||||
return
|
||||
|
||||
var sz: Vector2 = get_rect().size
|
||||
_body_panel.position = Vector2(sz.x - PANEL_WIDTH - 60.0, 60.0)
|
||||
|
||||
var b: Dictionary = _selected_body
|
||||
var bid: String = b.get("body_id", "")
|
||||
var name_str: String = b.get("proper_name", "") if b.get("proper_name") else bid
|
||||
var body_type: String = b.get("body_type", "").replace("_", " ").to_upper()
|
||||
var mass_class: String = b.get("mass_class", "") if b.get("mass_class") else ""
|
||||
var atmo: String = b.get("atmosphere", "none") if b.get("atmosphere") else "none"
|
||||
var inhabited: bool = bool(b.get("inhabited", false))
|
||||
var pop: int = int(b.get("population", 0))
|
||||
var radius_km: float = float(b.get("body_radius_km", 0))
|
||||
|
||||
var sys: Dictionary = current_system()
|
||||
var sys_name: String = sys.get("proper_name", sys.get("system_id", "—"))
|
||||
|
||||
# Globe artwork at top
|
||||
var tex: ImageTexture = _body_textures.get(bid)
|
||||
if tex:
|
||||
var globe_rect := TextureRect.new()
|
||||
globe_rect.texture = tex
|
||||
globe_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
globe_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
globe_rect.custom_minimum_size = Vector2(PANEL_WIDTH, PANEL_WIDTH)
|
||||
_body_panel.add_component(globe_rect)
|
||||
|
||||
_body_panel.add_component(ImplantHeader.new(name_str, sys_name + " system"))
|
||||
_body_panel.add_component(ImplantSeparator.new())
|
||||
|
||||
var type_line: String = body_type
|
||||
if not mass_class.is_empty():
|
||||
type_line += " · " + mass_class.replace("_", " ").to_upper()
|
||||
_body_panel.add_component(ImplantDataRow.new(type_line))
|
||||
_body_panel.add_component(ImplantDataRow.new("atmosphere " + atmo))
|
||||
if radius_km > 0:
|
||||
_body_panel.add_component(ImplantDataRow.new("radius " + _format_radius(radius_km)))
|
||||
|
||||
if inhabited:
|
||||
_body_panel.add_component(ImplantSeparator.new())
|
||||
_body_panel.add_component(ImplantDataRow.new("population " + _format_pop(pop)))
|
||||
|
||||
_body_panel.add_component(ImplantSeparator.new())
|
||||
var has_heightmap: bool = b.get("terrain_reference") != null
|
||||
if has_heightmap:
|
||||
_body_panel.add_component(ImplantTextBlock.new("enter view heightmap atlas"))
|
||||
_body_panel.add_component(ImplantTextBlock.new("esc close"))
|
||||
|
||||
_dirty = true
|
||||
|
||||
|
||||
static func _format_pop(pop: int) -> String:
|
||||
if pop <= 0:
|
||||
return "0"
|
||||
var s: String = str(pop)
|
||||
var result: String = ""
|
||||
var count: int = 0
|
||||
for i: int in range(s.length() - 1, -1, -1):
|
||||
if count > 0 and count % 3 == 0:
|
||||
result = "," + result
|
||||
result = s[i] + result
|
||||
count += 1
|
||||
return result
|
||||
|
||||
|
||||
static func _format_radius(km: float) -> String:
|
||||
if km >= 10000.0:
|
||||
return "%s km" % _format_pop(int(km))
|
||||
return "%.0f km" % km
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
class_name ImplantPending
|
||||
extends Control
|
||||
## Diegetic "pending / working" indicator for the implant overlay (D-169).
|
||||
##
|
||||
## An animated bracket sweep — `[ >>>>········ ]` — with a caption above it.
|
||||
## Linear left-to-right fill that loops; reads as a bounded machine process,
|
||||
## not an OS spinner (Araminta's spec, #960).
|
||||
##
|
||||
## LAYER SEPARATION: this belongs to the IMPLANT (diegetic, in-fiction) UI layer
|
||||
## only. It styles exclusively from ImplantTheme. The global / non-diegetic UI
|
||||
## layer must NOT reuse these visuals — if that layer needs a busy indicator it
|
||||
## gets its own component with its own look. Sharing the sweep *logic* is fine;
|
||||
## resting on the same theme or glyphs is not.
|
||||
##
|
||||
## API: start(label) shows + animates; stop() hides + resets. Starts hidden, so
|
||||
## at rest the player sees nothing.
|
||||
|
||||
const SWEEP_PERIOD: float = 1.2 # seconds per left-to-right pass (linear, looping)
|
||||
const INTERIOR_WIDTH: float = 160.0 # px between the brackets
|
||||
const SWEEP_WIDTH: float = 28.0 # px width of the moving fill
|
||||
const DEFAULT_WIDTH: float = 220.0
|
||||
|
||||
var _label_text: String = "WORKING"
|
||||
var _sweep_t: float = 0.0 # normalized 0..1 sweep position
|
||||
var _theme: ImplantTheme = null
|
||||
var _font: Font = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_font = ThemeDB.fallback_font
|
||||
custom_minimum_size = Vector2(DEFAULT_WIDTH, 40.0)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
visible = false
|
||||
set_process(false)
|
||||
|
||||
|
||||
## Show the indicator and begin animating. `label` is uppercased for display.
|
||||
func start(label: String = "WORKING") -> void:
|
||||
_label_text = label.to_upper()
|
||||
_sweep_t = 0.0
|
||||
visible = true
|
||||
set_process(true)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## Hide the indicator and stop animating.
|
||||
func stop() -> void:
|
||||
visible = false
|
||||
set_process(false)
|
||||
_sweep_t = 0.0
|
||||
|
||||
|
||||
func apply_implant_theme(t: ImplantTheme) -> void:
|
||||
_theme = t
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_sweep_t = fmod(_sweep_t + delta / SWEEP_PERIOD, 1.0)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if _font == null:
|
||||
return
|
||||
var dim: Color = _theme.text_dim if _theme else Color("#667788")
|
||||
var accent: Color = _theme.accent_active if _theme else Color("#f0d060")
|
||||
accent.a = 0.6
|
||||
var fs: int = _theme.font_body if _theme else 11
|
||||
var line_h: float = float(_theme.line_height if _theme else 18)
|
||||
var w: float = size.x if size.x > 0.0 else DEFAULT_WIDTH
|
||||
|
||||
# Caption — centered, dim.
|
||||
var cap_w: float = _font.get_string_size(_label_text, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x
|
||||
draw_string(
|
||||
_font,
|
||||
Vector2((w - cap_w) * 0.5, float(fs)),
|
||||
_label_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
fs,
|
||||
dim
|
||||
)
|
||||
|
||||
# Bracket row — centered group: [ <interior> ]
|
||||
var glyph_w: float = _font.get_string_size("[", HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x
|
||||
var group_w: float = glyph_w * 2.0 + INTERIOR_WIDTH
|
||||
var gx: float = (w - group_w) * 0.5
|
||||
var baseline_y: float = line_h + float(fs)
|
||||
draw_string(_font, Vector2(gx, baseline_y), "[", HORIZONTAL_ALIGNMENT_LEFT, -1, fs, dim)
|
||||
draw_string(
|
||||
_font,
|
||||
Vector2(gx + glyph_w + INTERIOR_WIDTH, baseline_y),
|
||||
"]",
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
fs,
|
||||
dim
|
||||
)
|
||||
|
||||
# Sweep fill — a thin bar travelling across the bracket interior.
|
||||
var interior_x: float = gx + glyph_w
|
||||
var travel: float = INTERIOR_WIDTH - SWEEP_WIDTH
|
||||
var sweep_x: float = interior_x + _sweep_t * travel
|
||||
var sweep_y: float = baseline_y - float(fs) * 0.35
|
||||
draw_rect(Rect2(sweep_x, sweep_y, SWEEP_WIDTH, 1.0), accent)
|
||||
+21
-8
@@ -16,19 +16,31 @@ CREATE TABLE IF NOT EXISTS tickets (
|
||||
assigned_to TEXT,
|
||||
team TEXT, -- comma-separated team names, e.g. 'server', 'client,server'
|
||||
decision_ref TEXT, -- e.g. 'D-010' or 'Q-001'
|
||||
sprint_id INTEGER REFERENCES sprints(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sprints (
|
||||
CREATE TABLE IF NOT EXISTS milestones (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
goal TEXT,
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'planning' CHECK(status IN ('planning', 'active', 'completed', 'cancelled')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK(status IN ('active', 'completed', 'cancelled')),
|
||||
cascade_phase INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_milestones (
|
||||
ticket_id INTEGER NOT NULL REFERENCES tickets(id),
|
||||
milestone_id INTEGER NOT NULL REFERENCES milestones(id),
|
||||
PRIMARY KEY (ticket_id, milestone_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS milestone_deps (
|
||||
blocker_id INTEGER NOT NULL REFERENCES milestones(id),
|
||||
blocked_id INTEGER NOT NULL REFERENCES milestones(id),
|
||||
PRIMARY KEY (blocker_id, blocked_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||
@@ -56,11 +68,12 @@ CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_type ON tickets(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_sprint ON tickets(sprint_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_decision ON tickets(decision_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_ticket ON ticket_history(ticket_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_milestones_ticket ON ticket_milestones(ticket_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_milestones_milestone ON ticket_milestones(milestone_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Decision Sync Tables
|
||||
|
||||
+2
-2
@@ -10,12 +10,12 @@ Cross-domain decisions live in one file with cross-reference notes in related fi
|
||||
|
||||
| File | Domain | Decisions |
|
||||
|------|--------|-----------|
|
||||
| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109, D-113, D-133, D-134, D-135, D-136, D-137, D-141, D-148, D-149, D-150, D-151, D-152, D-191 |
|
||||
| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109, D-113, D-133, D-134, D-135, D-136, D-137, D-141, D-148, D-149, D-150, D-151, D-152, D-188, D-191, D-192, D-194–D-218, D-220 |
|
||||
| [perception.md](perception.md) | Player observation | D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052, D-056, D-057, D-058, D-059, D-060, D-061, D-067, D-069, D-070, D-071, D-072, D-076, D-077, D-078, D-086 |
|
||||
| [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074, D-075, D-084, D-090, D-092, D-093, D-095, D-098, D-104, D-105, D-107, D-121, D-122, D-123, D-124, D-125, D-126, D-127, D-128, D-129, D-130, D-131, D-132, D-138, D-139, D-140, D-142, D-147 |
|
||||
| [scope.md](scope.md) | Game concept, prototype | D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065, D-087, D-089, D-091, D-114, D-115, D-116, D-117, D-118, D-119, D-120, D-145, D-146, D-153, D-154, D-155, D-156, D-157 |
|
||||
| [economics.md](economics.md) | Economics layer, currencies, corporations, simulation | D-171, D-172, D-173, D-174, D-175, D-176, D-177, D-178, D-179, D-180, D-181, D-182, D-183, D-184, D-185, D-186, D-187, D-189, D-190 |
|
||||
| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040 |
|
||||
| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040, D-221 |
|
||||
| [questions.md](questions.md) | Open questions (index) | Q-001 through Q-094 |
|
||||
| [questions-architecture.md](questions-architecture.md) | Technical questions | Q-001, Q-006, Q-009, Q-018–Q-023, Q-029, Q-030, Q-046, Q-059, Q-060, Q-063–Q-094 |
|
||||
| [questions-perception.md](questions-perception.md) | Observation questions | Q-003, Q-014, Q-016, Q-024–Q-026, Q-051, Q-053, Q-054 |
|
||||
|
||||
+512
-20
@@ -171,6 +171,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Sprites: 2x2 sim tile footprint.** Entity sprites render across 2x2 sim tiles so they feel proportional to the 1m visual grid. Tween interpolation ([D-054](#d-054-tile-based-movement-with-same-tile-occupancy)) hides half-visual-tile movement increments.
|
||||
- **Mental model:** "Objects are where they look. I can position myself precisely within open space." The player reads cover and walls at visual scale (always correct). Fine movement granularity is felt, not counted.
|
||||
- **What the simulation does NOT know:** Visual tiles. The retina factor is purely a client rendering convention. The server operates exclusively on 0.5m sim tiles.
|
||||
- **Amendment (2026-05-22, [D-222](#d-222)):** canonical vocabulary. This 0.5m "sim tile" is now the **Subtile** — the granularity the server simulation, entity positioning, and render detail run on. The **Tile = 1m** (2×2 subtiles) is the unit sizes are quoted in and the one the world-generation cascade, pathfinding, and grids operate on. The dual-scale model and the 0.5m simulation granularity are unchanged; only the naming primacy flips. The Chunk/Block/Quarter/District ladder built on the Tile is canonical in D-222.
|
||||
- **Fog shader ([D-059](perception.md#d-059-fog--shader-based-five-layers-knowledge-graph-driven)):** Unaffected — fog is screen-space, driven by PointLight2D vision cone and LOS mask from sim-resolution shadowcasting. Gradient edge "3-4 tiles" is retuned to 6-8 sim tiles (= 3-4 visual tiles) to preserve the intended softness.
|
||||
- **Cursor/interaction:** No change — cursor already resolves to sim tile from pixel position. Interaction range of ~2 sim tiles = 1m (arm's length).
|
||||
- **Map authoring:** Author at 1m visual scale. Subdivision tool expands each visual tile to 4 sim tiles (2x2). Validation enforces 2x2 minimum on all world geometry layers.
|
||||
@@ -239,6 +240,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
### D-094: District Spatial Hierarchy — Chunk, Block, District Naming and Sizes
|
||||
- **Date:** 2026-02-25
|
||||
- **Decision:** The spatial hierarchy for map generation and streaming is defined as follows. **Chunk** = 64×64 sim tiles (32×32 visual tiles, 32m) — the streaming and serialization unit. **Block** = 128×128 sim tiles (64×64 visual tiles, 64m) — the generator planning unit, composed of 4 chunks arranged in a 2×2 grid. Each block contains 4 chunks; chunks within a block can merge into one large edifice, remain separate (small buildings, gardens, cafes), or form L-shaped buildings across chunk boundaries. **District** = 4×4 blocks = 512×512 sim tiles (256×256 visual tiles, 256m) per z-level, containing 16 blocks and 64 chunks. Large civic structures (gate terminals, horizon station installations, stadiums, parks) span multiple blocks. Three z-levels for the Transit District = ~1.35MB (trivial). This decision amends D-012 and overrides the ~150×150 visual estimate in D-014.
|
||||
- **Amendment (2026-05-22, [D-222](#d-222)):** naming + scale superseded. The unit this record calls a **District** (4×4 blocks) is renamed the **Quarter** (now 512m at Tile = 1m); **District** is promoted to a new 2048m tier (4×4 quarters). Tier *counts* and nesting structure here are unchanged — only the names and the metre values (per the Tile = 1m / Subtile = 0.5m scale of D-220). See D-222 for the canonical ladder and the lore-vs-code rule.
|
||||
- **Rationale:** Chunk size of 32×32 visual (64×64 sim) gives a 32m streaming cell — large enough to hold a meaningful space, small enough for efficient streaming. The 2×2-chunk block provides a generator planning unit with enough granularity for per-chunk variation. The 4×4 block district (256×256 visual) gives a full district footprint generalisable as a template for the Q-036 generator. The chunk-based fill system within blocks allows the generator to place buildings of varying scale without hard-coding building dimensions.
|
||||
- **Raised by:** Tyre (chunk/block spec and memory confirmation), confirmed by team. Lead ratified district = 4×4 blocks.
|
||||
- **Dissent:** Araminta preferred 32×32 visual chunk size (effectively halving the chunk to a 16m cell). Overruled by lead and team majority — 32m chunk is the minimum viable streaming cell for the simulation architecture.
|
||||
@@ -264,6 +266,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Raised by:** Gestalt (tier structure + assassin lens integration), Tyre (GuaranteeAuditResult struct). Full team sign-off.
|
||||
- **Dissent:** None.
|
||||
- **Cross-reference:** D-103 (assassin lens guarantees A-1 through A-4), D-102 (horizon view corridor — Tier 2 coastal)
|
||||
- **Amended 2026-05-25 (D-229/D-231 — guarantee-audit engine hook):** The audit runs AFTER step-3 fill completes for a district, before the skeleton is marked "complete"; on a failed mandatory check, fill regenerates with adjusted parameters. **The mandatory-check set is keyed on `ComplexityTier`, not `WorldTier`** (T1 = all inhabited; T2 and T3/A-1..A-4 = Full only). Signature: `run_guarantee_audit(skeleton: &DistrictSkeleton, building_tags: &[BuildingPropertyTag], street_graph: &StreetGraph, seed: SeedChain) -> GuaranteeAuditResult`. Tier-1 checks (all inhabited): `social_hub_present` (≥1 building with entry_class ∈ {Public, Commercial} AND zone ∈ entertainment/market/transit); `informal_zone_present` (≥1 Organic + Public, or open ground in an Organic district); `encounter_corridor` (street graph has a continuous Public traversal path crossing the district). Tier-2 checks (full-complexity): `traffic_chokepoint` (a bottleneck node whose removal disconnects the graph); `institutional_space` (admin/judicial/checkpoint, Restricted); `insider_space` (Restricted + Hidden or non-primary-street door); `economic_node` (≥1 Commercial); `breach_only_zone` (≥1 BreachOnly). Tier-3 / A-1..A-4 (pure graph/geometry over step-3 tags + street graph — NO interior generation): A-1 elevated vantage (max above_ground ≥3 with unobstructed LOS to a chokepoint); A-2 egress multiplicity (≥2 distinct unique-street connections across the perimeter); A-3 temporal opacity (≥1 TemporalWindow door → reduced traffic in closed hours); A-4 non-institutional route (a Public→insider-space path exists that avoids all institutional buildings). `GuaranteeAuditResult` gains a bool per check above, retaining its existing `rooftop_discovery_zone` (D-106) and `horizon_view_corridor` (D-102) fields. Raised by: Gestalt, economic-built-world workshop round 2.
|
||||
|
||||
### D-099: WallBackside / TileBehindState — Dual Classification
|
||||
- **Date:** 2026-02-27
|
||||
@@ -291,6 +294,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Dissent:** None.
|
||||
- **Cross-reference:** D-102 (horizon view corridor — T5 coastal water is the referenced terrain type), D-104 (heritage grammar overlay — modifier axis A)
|
||||
- **Superseded (partial):** Modifier axis A (HeritageRoot) superseded by [D-167](content.md#d-167-heritage-roots-superseded--corridor-cultural-system-replaces-abstract-roots) (2026-03-24). Cultural palette modifiers are now authored per-system via the corridor framework; abstract heritage root IDs no longer drive axis A.
|
||||
- **Modifier axis A (`HeritageRoot`) superseded 2026-05-25 by D-232** → replaced by `FlavorTagFilter`; axes B/C and faction/climate/condition/season unchanged.
|
||||
|
||||
### D-102: Horizon View Corridor as Coastal Guarantee
|
||||
- **Date:** 2026-02-27
|
||||
@@ -588,10 +592,11 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
| 1 | Wiki content complete — all planets, moons, stations, heightmaps, artwork | Implant-ready Godot map with click-throughs + wiki/GTTR popups |
|
||||
| 2 | Economics layer — supply/demand, transport, corporations, supply chains | Economics spreadsheets/graphs with runtime-tweakable simulation |
|
||||
| 3 | Planetary/moon maps & station layouts — cities, rivers, mountains, roads, biomes | Atlas of the Reach (implant app) |
|
||||
| 4 | Player control scheme — 2-floor test map, character rendering, walls/stairs/doors, lighting | Player viewport with final-version assets |
|
||||
| 5 | World generation (tile/chunk/block) — walkable world, parallel asset pipeline | Walkable generated world + asset catalog |
|
||||
| 4 | World generation (tile/chunk/block) — deterministic multilayer seed→tile cascade; each layer viewed as a map in the implant Atlas (no in-world rendering) | Deterministic walkable-world data + per-layer Atlas maps + asset catalog |
|
||||
| 5 | Player control & in-world rendering — character, walls/stairs/doors, lighting, drawn on generated tiles (no test map) | Player viewport with final-version assets on the generated world |
|
||||
| 6 | Detail coloring — room-level content, cultural architecture | Only when the world is walkable |
|
||||
|
||||
- **Amendment (2026-05-22):** Phases 4 and 5 swapped — **world generation now precedes player control**. The rule: no player-control or in-world rendering work begins until the generator can deterministically seed-generate every tile of every world via the full multilayer cascade. The original Phase 4 "2-floor test map" is **dropped** — test layers are produced by the generator itself once layer-drawing begins; we start drawing the world only when generation knows what to draw. Generation progress is viewed as **per-layer maps in the implant Atlas** (the Phase 3 deliverable, already built), not via an in-world renderer. The existing in-world rendering code is **left as-is until Phase 5** — neither built upon nor removed before then. Rationale: building player systems against a throwaway test substrate means rebuilding them against real generated tiles later; gating player work on deterministic generation avoids that waste. Epics #749 (now Phase 5) and #750 (now Phase 4) and the CLAUDE.md cascade table are updated to match.
|
||||
- **Rationale:** The pattern of negotiating pragmatic v0.2 cuts while discussing room-level detail repeatedly produced superseded decisions, confused agents, and distracted from building the actual product. The cascade enforces a first-things-first discipline: each layer of the game is grounded in the layer below it before detail is added.
|
||||
- **Raised by:** Jeroen, established 2026-03-24 during world generation workshop.
|
||||
- **Dissent:** None.
|
||||
@@ -700,16 +705,18 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- 3 navigation levels: system picker → orbital diagram → body atlas (heightmap + overlays)
|
||||
- Heightmap displayed as `Texture2D` with pan/zoom
|
||||
- Marker overlay renders cities, roads, rail, POIs, named features on top of heightmap
|
||||
- Click city → City Data Panel (name, population, currency zone, Commission presence, shadow economy zone, gate distance, economics panel link)
|
||||
- Click city → City Data Panel (name, population, currency zone, Commission presence, gate distance, economics panel link) — shadow economy zone dropped per §7 amendment (2026-05-22)
|
||||
- Economics panel link: click-through to Phase 2 economics panel pre-filtered to that node — primary Phase 2/3 integration point
|
||||
|
||||
**7. Overlay System — 9 MVP Overlays**
|
||||
**7. Overlay System — 8 MVP Overlays**
|
||||
- **Amendment (2026-05-22):** dropped the `shadow economy` toggleable overlay (9 → 8 overlays). Shadow economy is an *underwater simulation modifier* — `shadow_economy_intensity` (D-174) feeds derived signals such as `collection_efficiency` and signal 7 `official_coverage_ratio` (D-181), but it is not a user-navigable data point, so it does not warrant a player-facing atlas overlay or City Data Panel field. The simulation layer (D-174) is unaffected. Also removed from §6 (city data panel) and §10 (completion criteria) for consistency.
|
||||
- 5 always-on: terrain, infrastructure, named features, gate/spaceport POIs, political zones
|
||||
- 4 toggleable: population density, production zones, shadow economy zones (broad bands), corporate presence (Tier 1 only)
|
||||
- 3 toggleable: population density, production zones, corporate presence (Tier 1 only)
|
||||
- Deferred overlays visible in toggle bar but locked with unlock requirements shown on hover (creates pull toward Phase 4+ systems)
|
||||
- Maps to D-181 signal visibility ladder
|
||||
|
||||
**8. Settlement Data Model**
|
||||
- **Amendment (2026-05-22, [D-223](#d-223)):** superseded — markers.json is reduced to **names only** (a flavored name pool); it no longer carries position arrays or topographic-feature geometry. River/mountain positions derive from the heightmap + drainage (D-208); settlement positions from the economic sim + placement (D-211). The 6 hand-authored templates and their `reserved` pinning are removed (bodies stay as ordinary named bodies). The pixel-space schema below is retained as historical reference only.
|
||||
- **Amendment (2026-04-15):** The original §8 (below) described marker positions as lat/lon objects and city records keyed by `population_tier`/`primary_function`/`gate_terminal`/`continent_id`. That shape was aspirational — neither the generator nor the hand-authored templates ever emitted it. Both ended up writing pixel-space row/col arrays against a `512 × 256` storage grid, and PR #129 canonizes that shape so the code and the decision stop drifting. The original prose is preserved immediately below; the current shape follows.
|
||||
- **Original (2026-04-10, superseded):** markers.json schema per body: `cities` (name, lat/lon, population_tier, primary_function, gate_terminal, continent_id), `roads` (path polylines, connects), `railroads` (path polylines, connects), `pois` (name, kind, position), plus existing rivers/oceans/mountains with names filled. Population tier → city count: `floor(log10(pop/1M))`, modified by `settlement_pattern`. Gate terminal POI at largest population center, sometimes scattered to a smaller one. Moons: same depth as planets, scale with population.
|
||||
- **Current canonical format:** markers.json is stored in heightmap pixel space. Every marker file declares a `grid: { w, h }` header — the generator, the 6 hand-authored templates (Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade), and all 2394 procedural seed files ship `{"w": 512, "h": 256}`. Every position is a two-element **array** `[row, col]` of integer pixels into that grid, where `row ∈ [0, h)` and `col ∈ [0, w)` (row is the first axis to match NumPy convention and the flood-fill / A* / cost-grid code that `tooling/planet-gen/` already runs in). Polyline geometry (`roads[*].path`, `railroads[*].path`, `rivers[*].path`) is `[[row, col], [row, col], ...]`.
|
||||
@@ -740,7 +747,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
3. Population-scaled depth (core systems rich, frontier sparse)
|
||||
4. City data panel works (click any city, see data + economics link)
|
||||
5. Economics panel integration works (link opens Phase 2 panel filtered to node)
|
||||
6. All 9 MVP overlays present and functional
|
||||
6. All 8 MVP overlays present and functional
|
||||
7. Atlas is read-only (no verbs execute from map)
|
||||
8. Stations show at system view with mini data panel (no drill-down)
|
||||
|
||||
@@ -778,8 +785,9 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
### D-195: Attractor-Matching Compatibility Matrix for Generative City Placement
|
||||
- **Date:** 2026-05-01
|
||||
- **Decision:** City placement on a planetary surface uses an attractor-matching model. A `GeographicAttractor` is a terrain feature that increases city placement score at nearby positions. Seven `AttractorType` variants: `RiverMouth`, `CoastalAccess`, `RiverCrossing`, `ValleyFloor`, `PassEntrance`, `LakeShore`, `PlainCenter`. A `CompatibilityMatrix` is a 10×7 scoring table (10 `economic_role` values × 7 attractor types) whose cells are float weights (0.0–3.0) representing how strongly that economic role favors that terrain feature. Examples: manufacturing → RiverMouth 2.8, ValleyFloor 2.1; financial → CoastalAccess 2.5, PlainCenter 1.8; agricultural → ValleyFloor 3.0, PlainCenter 2.5. The matrix is authored data (not computed at runtime). Attractor extraction from heightmaps is defined in D-209. Matching algorithm is defined in D-211.
|
||||
- **Rationale:** Terrain-naive city placement produces spatially incoherent worlds. The compatibility matrix gives different city types different terrain affinities, so financial hubs appear on coasts and agricultural cities appear in fertile valleys — without hard-coding placement rules per city type. The float weight matrix gives graduated preference, not binary requirement.
|
||||
- **Ticket:** #919, #925
|
||||
- **Rationale:** Terrain-naive city placement produces spatially incoherent worlds. The compatibility matrix gives different city types different terrain affinities, so financial hubs appear on coasts and agricultural cities appear in fertile valleys — without hard-coding placement rules per city type. The weight matrix gives graduated preference, not binary requirement.
|
||||
- **Amended 2026-06-03 (#955):** the matrix weights and the whole placement-scoring path are **integer basis-points, not f32** (D-010 determinism / D-227 save-critical). When #955 wired `match_cities` into the deterministic generation cascade, the original f32 scoring became a live cross-platform divergence risk (a near-tie score comparison or the Hungarian's f32 reductions can round differently per platform → a different world from the same seed). Now: `CompatibilityMatrix.weights` are `i32` bps (10000 = 1.0×; the examples above are 28000 / 30000 / 25000 …), `GeographicAttractor.strength` and `terrain_modification_cost` are bps, and `cell_score` / the Hungarian / `CityPlacement.score` use integer arithmetic. The 0.0–3.0 affinity semantics are unchanged; only the representation is now integer.
|
||||
- **Ticket:** #919, #925, #955
|
||||
- **Raised by:** Generation cascade workshop (#897)
|
||||
- **Cross-reference:** D-191 (atlas pipeline), D-208 (D8 drainage — attractor extraction), D-209 (feature tag extraction), D-211 (attractor-matching pipeline)
|
||||
|
||||
@@ -827,6 +835,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Ticket:** #915 (CityGenerationContext reads economic snapshot)
|
||||
- **Raised by:** Generation cascade workshop (#897)
|
||||
- **Cross-reference:** D-026 (simulation tiers), D-200 (CityGenerationContext), D-206 (background generation queue)
|
||||
- **Amended 2026-05-25 (D-233 — structural-fill / condition-overlay two-pass split):** The "Phase 2 condition application" language is replaced by a formal two-pass model. **Pass 1 — Structural fill** (frozen; reads t=0 initial-economics only; re-derivable from `seed + initial_economics_snapshot`): zone-type per block, building footprint shape+position, building-type vocabulary tags (`BuildingTag`), `founded_era`, operations-surface extent (bulk industries), and the labor-demand signal feeding adjacent residential blocks. **Pass 2 — Condition overlay** (the sanctioned rolling-economy consumer; a paint layer OVER the frozen fill; refreshable on a cadence): `BuildingConditionState = New|Maintained|Worn|Derelict|Abandoned`; `OccupancyState = Full|Partial|Vacant`; `VegetationEncroachment` (Abandoned in appropriate sub-biomes, D-210); feeds the D-100 tile `DamageOverlay`. **Hard wall:** the condition overlay CANNOT change a `BuildingTag` (a mine plant becomes an *abandoned* mine plant, never an office). The fill generator's signature is `fill_chunk(ctx: CityGenerationContext, seed: SeedChain)` — NO access to `PressureState`, price signals, or tâtonnement output. The condition overlay is a SEPARATE Bevy system, triggered by `EconEvent` (D-180), with its own read set. Cross-ref D-233, D-100, D-180, D-210, D-217. Raised by: Burnelli/Tyre/Gestalt, economic-built-world workshop round 2.
|
||||
|
||||
### D-199: 6-Field Minimum Economic Read Set for City Generation Context
|
||||
- **Date:** 2026-05-01
|
||||
@@ -842,12 +851,14 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Rationale:** A fixed minimum read set prevents generators from accumulating unbounded dependencies on simulation state. The 6 fields cover the minimum information needed to produce a correctly-classified skeleton. The abort-on-missing-fields rule ensures generator output is always deterministic from a complete context, never silently degraded from a partial one.
|
||||
- **Ticket:** #915 (CityGenerationContext implementation)
|
||||
- **Raised by:** Generation cascade workshop (#897)
|
||||
- **Cross-reference:** D-196 (SettlementClass), D-197 (prosperity_baseline), D-198 (economic simulation independence), D-200 (CityGenerationContext struct)
|
||||
- **Cross-reference:** D-196 (SettlementClass), D-197 (prosperity_baseline), D-198 (economic simulation independence), D-200 (CityGenerationContext struct), [D-237](#d-237) (authored specialization layer — read-set extended)
|
||||
- **Amended 2026-05-25 (D-229/D-232/D-233):** `CityGenerationContext` gains `morphology_zone`, `flavor_profile`/`architecture_flavors`, `dominant_bulk_class`, `dominant_production_ubiquity` (the record already permits >6 fields).
|
||||
- **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** the read set gains `economic_specialization` and `cultural_specialization` (both new columns on `system_economy`). `dominant_faction` (field 4) is now sourced from authored values on `system_factions` where present, heuristic fallback otherwise. `economic_specialization` is the upstream source of `dominant_bulk_class`/`dominant_production_ubiquity` (resolved via `specialization_vocabulary`, D-233 re-amendment); `cultural_specialization` feeds the D-232 template pool. Still bounded — three authored fields, all NULL-safe with deterministic fallbacks.
|
||||
|
||||
### D-200: Three-Tier Execution Model (Build-Time / Runtime-Background / Runtime-On-Demand)
|
||||
- **Date:** 2026-05-01
|
||||
- **Decision:** The generation pipeline operates at three distinct execution tiers with no cross-tier mutation:
|
||||
1. **Build-time (Python pipeline):** Runs `make regen-db`. Produces `systems.db` tables including `atlas_body_heightmaps`, `atlas_city_names`, `atlas_province_boundaries`, `body_radius_km`. Output is a static artifact committed to the repo. Never runs during gameplay.
|
||||
1. **Build-time (Python pipeline):** Runs `make regen-db`. Produces `systems.db` tables including `atlas_city_names`, `atlas_province_boundaries`, `body_radius_km`. Output is a static artifact committed to the repo. Never runs during gameplay. *(Amended #963, D-202: `atlas_body_heightmaps` is no longer produced — elevation is now a per-body 16-bit `heightmap.png` file, not a DB table.)*
|
||||
2. **Runtime-background (Rayon thread pool, D-206):** Triggered by content-spidering events (player approaches a system, NPC names a location, news ticker references a place). Runs D8 drainage analysis (D-208), attractor extraction (D-209), settlement placement, and Phase 1 district skeleton generation. Output goes into `BodyWorldState` cache (D-203). Transparent to main tick thread.
|
||||
3. **Runtime-on-demand (main tick thread):** Triggered when the player crosses a chunk boundary. Runs Phase 2 chunk fill for the approaching chunk. Must complete within 5ms. Reads from `BodyWorldState` cache (always populated before this tier runs).
|
||||
- **Tier boundary rules:**
|
||||
@@ -884,14 +895,15 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
| 3 | Body | ~512×256 pixels (equirectangular heightmap) | Planetary topography, climate zones |
|
||||
| 4 | Region | ~50–500km | Province boundaries (watershed-derived, D-205), biome zones |
|
||||
| 5 | Settlement | ~1–30km radius | City footprint, district layout |
|
||||
| 6 | District | 512×512 sim tiles (256m) | Phase 1 skeleton, 4×4 block grid (D-094) |
|
||||
| 7 | Block | 128×128 sim tiles (64m) | Generator planning unit, 2×2 chunks (D-094) |
|
||||
| 8 | Chunk | 64×64 sim tiles (32m) | Streaming/serialization unit (D-094) |
|
||||
| 6 | District | 2048×2048 tiles (2048m, 4.19 km²) | 4×4 quarters — large urban division (D-222) |
|
||||
| 7 | Quarter | 512×512 tiles (512m, 0.262 km²) | 4×4 blocks — settlement footprint cell, Phase 1 skeleton grid (D-222) |
|
||||
| 8 | Block | 128×128 tiles (128m) | Generator planning unit, 2×2 chunks (D-222) |
|
||||
| 9 | Chunk | 64×64 tiles (64m) | Streaming/serialization unit (D-222) |
|
||||
|
||||
- Tiers 6–8 are locked by D-094 (district spatial hierarchy). This decision formalizes Tiers 1–5 with equivalent lock status.
|
||||
- Tier 3 heightmap resolution (512×256 equirectangular at 1024×512 PNG) is the canonical format. Deviation requires amending D-191.
|
||||
- Tiers 6–9 (District → Chunk) are the sub-settlement spatial hierarchy, canonical in [D-222](#d-222) — renamed/resized from the original D-094 ladder (the old 512m "District" is now the Quarter; District is now 2048m), at the Tile = 1m / Subtile = 0.5m scale of D-220. This decision formalizes Tiers 1–5 with equivalent lock status.
|
||||
- Tier 3 heightmap resolution (512×256 equirectangular working grid; 1024×512 PNG) is the canonical format. Deviation requires amending D-191. **Amended (#963, D-202):** the canonical *stored* heightmap is now a per-body 16-bit grayscale `heightmap.png` at **1024×512** carrying native elevation (the prior PNG was a 1024×512 RGB *relief*, now renamed `reliefmap.png`). PNG dimensions are unchanged (1024×512); Layer 1 downsamples to the 512×256 working grid. This amendment is the explicit deviation gate being satisfied — format/content changed, resolution preserved.
|
||||
- Tier 4 province boundaries are pre-computed at build-time and stored in `atlas_province_boundaries` (D-205). They are not re-computed at runtime.
|
||||
- The `SettingType` enum on `DistrictSkeleton` is the interface between Tier 5 (settlement planning) and Tier 6 (district generation).
|
||||
- The `SettingType` enum on `DistrictSkeleton` is the interface between Tier 5 (settlement planning) and the skeleton cell (the 512m Quarter, Tier 7 — `DistrictSkeleton` is pending rename to match D-222).
|
||||
- **Rationale:** Locking spatial dimensions prevents the generative layers from drifting in incompatible directions. The heightmap pipeline, atlas pipeline, and district generator all assume these dimensions and would need coordinated migration if they changed. Formalization prevents silent per-system variation.
|
||||
- **Ticket:** #912 (WorldTier enum), #913 (SettlementClass)
|
||||
- **Raised by:** Generation cascade workshop (#897)
|
||||
@@ -915,7 +927,14 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- The Rust loader reads the BLOB via `bytemuck::cast_slice::<u8, f32>()` after fetching from SQLite. No endian conversion needed on LE-native systems; the pipeline stores LE explicitly.
|
||||
- Only inhabited bodies receive heightmap rows at build-time. Uninhabited bodies are generated on-demand (runtime-background tier, D-200).
|
||||
- This table is populated by the `import_heightmaps` build-time step in the asset pipeline (D-191 §9 pipeline order). It is read-only at runtime.
|
||||
- **Rationale:** Storing heightmaps in `systems.db` keeps the DB as the single source of truth for all generation inputs, avoids a separate file-fetching path in the Rust server, and allows the pre-push hook (asset pipeline rules) to detect stale heightmap data. The float32 LE layout matches what NumPy and PIL produce natively, minimizing conversion overhead in the Python pipeline.
|
||||
- **Amendment (2026-05-23, [#963](#)):** the DB-BLOB store is **superseded** by a per-body **file-based 16-bit grayscale `heightmap.png`** stored next to the body's other assets (path from `bodies.terrain_reference`). Rationale for the change: raw float grids committed inside a binary `systems.db` are the exact binary-merge-conflict trap the asset-pipeline rule warns against, and ~512KB×N bloats the DB; a per-body file matches D-203's on-demand model and keeps `systems.db` lean. Concretely:
|
||||
- **Naming fix:** the existing color hypsometric render (today's `heightmap.png`, 1024×512 RGB) is renamed **`reliefmap.png`** — it is a relief visualization, not elevation. Display-only.
|
||||
- **Canonical elevation:** a new **`heightmap.png`** = 16-bit grayscale (luminance = normalized elevation), **1024×512** (2× per axis / 4× the cells of the old 512×256 sim grid — the PNG dimensions are unchanged from the prior canonical 1024×512 in D-201; only the *content* changed from an RGB relief to native 16-bit elevation). 1024×512 bounds install size (~190 MB across 267 inhabited bodies) while sub-pixel detail is synthesized by the lower cascade layers. Single source of truth — the reliefmap and all computed geography derive from it, so it is bit-identical/deterministic by construction.
|
||||
- **Multi-resolution:** the stored heightmap is high-res for the lower layers (region/block/tile sample local detail); **Layer 1** (continental drainage/basins/mountain-ranges) calls `BodyHeightmap::downsample` to the `GRID_W×GRID_H = 512×256` working resolution first, decoupling continental compute cost (~45ms) from stored resolution.
|
||||
- **Rust loader:** `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG (via the `png` crate), normalizes to f32 [0,1]; rejects RGB (a reliefmap can't be misread as elevation). `sea_level` is stored **in the PNG** as a `tEXt` chunk (the heightmap is self-describing), with a caller-supplied default as fallback.
|
||||
- **`atlas_body_heightmaps` is dropped**; `import_heightmaps.py` writes the PNG file instead of a DB row. The bake runs in the content pipeline (numpy/scipy) once; the runtime cascade is pure Rust loading the file.
|
||||
- **Implementation status (#963):** Consumer done — `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG + `sea_level` tEXt chunk, rejects RGB, downsamples for Layer 1. Producer done — `import_heightmaps.py` is the bake (rename legacy `heightmap.png`→`reliefmap.png` for all bodies incl. Sol; for non-Sol inhabited bodies write a fresh clean `reliefmap.png` + 16-bit `heightmap.png` from `simulate()` at the bumped 1024×512 grid). `atlas_body_heightmaps` dropped via MIGRATION_SQL + removed from `systems-schema.sql`. Godot client (`atlas_viewer.gd`) loads `reliefmap.png` for display. Sim determinism guarded by `test_sim_determinism.py`.
|
||||
- **Rationale:** Storing heightmaps in `systems.db` keeps the DB as the single source of truth for all generation inputs, avoids a separate file-fetching path in the Rust server, and allows the pre-push hook (asset pipeline rules) to detect stale heightmap data. The float32 LE layout matches what NumPy and PIL produce natively, minimizing conversion overhead in the Python pipeline. *(Superseded by the #963 amendment above — the file-based model won out because the DB-as-single-source goal conflicts with binary-merge-conflict avoidance and DB size; a per-body committed PNG is itself a queryable, diffable-by-render asset.)*
|
||||
- **Ticket:** #901 (schema), #906 (import), #916 (Rust loader)
|
||||
- **Raised by:** Generation cascade workshop (#897)
|
||||
- **Cross-reference:** D-191 (atlas pipeline — heightmap source), D-203 (BodyWorldState — consumer), D-208 (D8 drainage — reads this table)
|
||||
@@ -1004,6 +1023,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
|
||||
### D-207: Fully Generative Placement — markers.json Stripped to Topographic Features
|
||||
- **Date:** 2026-05-01
|
||||
- **Amendment (2026-05-22, [D-223](#d-223)):** superseded in part. markers.json no longer retains topographic-feature *geometry* — it is reduced to **names only** (a flavored name pool). River/mountain positions are derived from the heightmap + drainage (D-208); settlement positions come from the economic sim + placement (D-211). The `reserved=1` pinning and the 6 hand-authored templates (Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) are removed — their bodies remain as ordinary named bodies, only the authored machinery is gone. See D-223.
|
||||
- **Decision:** The `atlas_city_names` table replaces the authored city positions in `markers.json`. Going forward, `markers.json` files contain only topographic features (rivers, oceans, mountain ranges — per D-191 §8 canonical format). City positions, road networks, and rail networks are NOT authored in `markers.json`; they are generated from the terrain data and stored in `atlas_city_names` and derived tables.
|
||||
```sql
|
||||
CREATE TABLE atlas_city_names (
|
||||
@@ -1030,7 +1050,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Date:** 2026-05-01
|
||||
- **Decision:** Drainage routing (the computation of flow direction, flow accumulation, and river network extraction) uses the **D8 priority-flood** algorithm on the body's float32 heightmap. This is the first Layer 1 computation run on a body before any city placement or attractor extraction.
|
||||
- **Algorithm:** D8 assigns each cell's flow direction to one of 8 neighbors based on the steepest descent. Priority-flood fills depression cells before routing to avoid spurious sinks. Flow accumulation is the count of upstream cells draining through each cell.
|
||||
- **River threshold:** A cell is classified as a river cell when `flow_accumulation > 200`. This threshold produces river networks of realistic density on canonical 512×256 heightmaps.
|
||||
- **River threshold:** A cell is classified as a river cell when `flow_accumulation > 200`. This threshold produces river networks of realistic density on the 512×256 Layer-1 working grid (D8 runs at 512×256, downsampled from the 1024×512 stored heightmap per D-202 amended #963).
|
||||
- **Outputs** stored in `BodyWorldState.river_network`:
|
||||
- `river_cells: Vec<(u16, u16)>` — pixel positions of all river cells
|
||||
- `confluences: Vec<(u16, u16)>` — positions where two or more rivers merge
|
||||
@@ -1147,9 +1167,10 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Spatial effect on district mix:** See D-194. Each archetype applies weight multipliers to district type selection.
|
||||
- **`AttractorAssignment` disambiguation:** `OrganicGrowth` (a `DistrictType` value and also an `EraCause` value) is always unambiguous in context. On `DistrictType`, it means the district grew without a planning mandate. As `EraCause`, it means the era tag was acquired through organic settlement expansion rather than a discrete historical event. Both usages are permitted; the type system distinguishes them.
|
||||
- **Rationale:** Power structure should be legible in a city's spatial form without the player reading a wiki entry. Commission cities look different from Corporate cities look different from Pioneer cities — not just in palette, but in street geometry, district type distribution, and building scale. Encoding this as a named enum ensures the distinction is consistent across all generation code.
|
||||
- **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** for named systems `dominant_faction` (a D-214 derivation input via `TerritorialStatus`/D-199) is now an **authored** value (8-value vocabulary: `concord_assembly | compact | compact_sympathetic | syndic_dominant | veil_institute | independent | disputed | mixed`) rather than one the heuristic guesses from hop-distance/currency; the existing derivation remains the fallback for unauthored systems. The archetype mapping itself is unchanged — it now reads a more trustworthy faction for the ~40–60 named systems where the heuristic was demonstrably wrong (e.g. Groombridge resolves `syndic_dominant` → Corporate, not the hop-2 default that would yield Commission). `lattice_commission` is deliberately **not** a faction value — the Commission is a regulator, not a governing faction (ACB and Bastion are `concord_assembly`).
|
||||
- **Ticket:** #914
|
||||
- **Raised by:** Generation cascade workshop (#897)
|
||||
- **Cross-reference:** D-194 (district mix — archetype modifiers), D-212 (TerritorialStatus — primary input), D-213 (FoundingOrientation — archetype may override), D-215 (spatial arrangement patterns)
|
||||
- **Cross-reference:** D-194 (district mix — archetype modifiers), D-212 (TerritorialStatus — primary input), D-213 (FoundingOrientation — archetype may override), D-215 (spatial arrangement patterns), [D-237](#d-237) (authored `dominant_faction` source)
|
||||
|
||||
### D-215: Five Explicit Political Archetype Spatial Arrangement Patterns
|
||||
- **Date:** 2026-05-01
|
||||
@@ -1187,6 +1208,18 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Ticket:** #922
|
||||
- **Raised by:** Generation cascade workshop (#897)
|
||||
- **Cross-reference:** D-096 (DistrictLayoutMode::Organic — consumes block_irregularity), D-194 (district mix — founding_age is an input), D-214 (PoliticalArchetype — archetype_step source)
|
||||
- **Amendment (2026-05-26 — `founding_age_years` backfill, ticket #1000):** `founding_age_years` is backfilled on every inhabited body in `tooling/economy-db/import_economics.py` MIGRATION_SQL via `COALESCE(events-first, wave-fallback)`. **Events-first:** the system's authored `historical_events.age_years` for `event_type = 'colonial_charter'` — the canonical founding event authored during the economics built-world workshop. 9 systems in the live DB carry a colonial_charter row, driving 11 inhabited body values; 5 of those diverge meaningfully from their wave fallback (e.g. GJ 144 = 580 vs wave-1 fallback 600, GJ 338B = 590, GJ 380 = 480). **Wave-fallback** — canonical **founding-edge** of each wave's range (`docs/design/systems-framework-final-miri.md:495–499`):
|
||||
|
||||
| `settlement_wave` | era | range (years ago) | founding-edge fallback |
|
||||
|---|---|---|---|
|
||||
| `wave_1` | Core Founding | 500–600 | **600** |
|
||||
| `wave_2` | Trade Expansion | 300–500 | **500** |
|
||||
| `wave_3` | Working Reach | 100–300 | **300** |
|
||||
| `wave_4` | Frontier Push | 40–100 | **100** |
|
||||
| `wave_5` | Activation Edge | 0–40 | **40** |
|
||||
| `origin` | Sol — predates the wave taxonomy | — | **NULL** (Sol excluded per [D-236](scope.md#d-236)) |
|
||||
|
||||
Founding-edge (not midpoint) so a wave-1 body reads as ~600 years old, matching the canonical Reach age. ±50yr drift from per-system reality is negligible for the `founding_age_years / 1000` arithmetic above.
|
||||
|
||||
### D-217: Tile Condition Thresholds (0.63 / 0.43 / 0.23)
|
||||
- **Date:** 2026-05-01
|
||||
@@ -1237,4 +1270,463 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
|
||||
---
|
||||
|
||||
*79 decisions (D-001 through D-218, excluding gaps). Last updated: 2026-05-02 (D-218 — WorldTier canonical values, generation cascade workshop Sprint 38)*
|
||||
### D-220: Settlement Density Model — Tile Scale, Density Classes, and Vertical Pressure
|
||||
- **Date:** 2026-05-03
|
||||
- **Decision:** Settlement footprint, building height, and basement depth are all driven by a single **density scalar** on each settlement, derived from population, available land, terrain constraints, and economic pressure. This decision establishes the tile scale, the density classification system, and the rules for how density shapes the built environment.
|
||||
|
||||
**Tile scale (amends D-201 Tier 0):**
|
||||
|
||||
| Unit | Dimensions | Real-world size | Notes |
|
||||
|------|-----------|-----------------|-------|
|
||||
| Subtile | 1×1 | 0.5m × 0.5m | Rendering unit — character positioning, furniture placement, visual detail |
|
||||
| Tile | 1×1 | 1m × 1m | Spatial unit — generation pipeline, pathfinding, district grids |
|
||||
| Chunk | 64×64 tiles | 64m × 64m | Streaming unit (was 32m at 0.5m/tile; now 64m at 1m/tile) |
|
||||
| Block | 128×128 tiles | 128m × 128m | 2×2 chunks |
|
||||
| District | 512×512 tiles | 512m × 512m = 0.262 km² | 4×4 blocks |
|
||||
|
||||
Each tile contains 2×2 subtiles. The generation cascade operates in tiles. The renderer subdivides to subtiles for visual fidelity. All higher tiers (Province, Area, Body) remain as defined in D-201.
|
||||
|
||||
- **Amendment (2026-05-22, [D-222](#d-222)):** the 512m unit this table calls **District** is renamed the **Quarter**; **District** is promoted to a new 2048m tier (4×4 quarters). The Tile/Subtile scale above is unchanged. Throughout this record — including the footprint formula (`district_count = footprint / 0.262`) and the density examples — "district" denotes the **0.262 km² Quarter cell**, not the new 2048m District. See D-222 for the canonical ladder and the lore-vs-code rule.
|
||||
|
||||
**Density classes:**
|
||||
|
||||
Density is measured in people per km² of settlement footprint. It is the primary driver of settlement character — not population alone. The same population at different densities produces completely different settlements.
|
||||
|
||||
| Class | Density (ppl/km²) | Horizontal character | Vertical character | Infrastructure cost | Who builds at this density |
|
||||
|-------|-------------------|---------------------|-------------------|--------------------|-----------------------------|
|
||||
| Frontier | ~400 | Large gaps between structures, open ground dominant, private roads | 1 floor, no basement | Very high per-capita (long utility runs, private maintenance) | **The very rich** (villa estates, corp executive compounds, prestige vanity builds) OR **the very poor** (pre-utility squatter settlements, homesteads with no services). Frontier density is a luxury or an absence, never the default. |
|
||||
| Settled | ~1,500 | Planned grid, moderate spacing, shared streets, visible utility corridors | 1-2 floors, optional basement | Efficient — the utility-cost-optimized default | **Default for most Reach colonies.** Every meter of sewer, power conduit, and road is new capital expenditure on a virgin world. Settled density is what you get when infrastructure cost constrains the plan. Kiruna is the archetype. |
|
||||
| Established | ~3,000 | Mixed residential, shared walls appearing, streets narrowing, green space intentional not incidental | 2-3 floors, basement common | Amortized — mature infrastructure absorbing incremental growth | **Mature settlements.** Second-generation growth over an existing utility grid. The settlement has existed long enough for the infrastructure investment to pay off and densification to begin. Tiel, Périgueux. |
|
||||
| Dense | ~6,000 | Shared walls dominant, narrow streets, minimal private outdoor space, commercial ground floors | 3-6 floors, 1-2 basement levels (parking, storage, utilities) | High per-area but efficient per-capita | **Economic hubs.** Transit nodes, extraction chokepoints, terrain-constrained sites where the jobs pull people in faster than the settlement can spread. Kanazawa, Uzès. Density is a signal that economic gravity is compressing the settlement. |
|
||||
| Compressed | ~15,000 | Vertical dominant, street canyons, multi-use buildings, public space carved from built volume | 6-12+ floors, 2-3 basement levels (metro, parking, utilities, subsurface logistics) | Very high per-area, requires vertical infrastructure (elevators, pressurized systems, multi-level utilities) | **Extreme economic pull.** Major corp HQs, gate-adjacent trade hubs, capital worlds. Rare in the Reach. The land itself is too valuable for anything but vertical. Paris, Amsterdam inner ring. |
|
||||
|
||||
**What drives density — the four pressures:**
|
||||
|
||||
1. **Utility cost (compressing force).** The dominant pressure on new colonies. Infrastructure is expensive per meter. This pushes settlements toward Settled as the floor, not Frontier. Every colony pays this tax unless someone absorbs the cost deliberately.
|
||||
|
||||
2. **Economic pull (compressing force).** Jobs, trade, resources, transit — anything that draws people to a specific location faster than the settlement can grow outward. The stronger the pull, the higher the density. Corp HQ worlds, gate-adjacent systems, extraction sites all have high economic pull. This is the force that pushes settlements from Settled through Dense to Compressed.
|
||||
|
||||
3. **Terrain constraint (compressing or dispersing force).** Coastal strips, canyon floors, mountain passes compress settlements along one axis (Bisbee: 2.5km × 300m). Open plains allow radial spread. River valleys elongate. The terrain doesn't set the density class directly — it shapes the footprint geometry within that class.
|
||||
|
||||
4. **Wealth/status (dispersing force — at a premium).** The only force that pushes density DOWN below the utility-cost floor. Low density in an otherwise dense settlement is conspicuous consumption of space — private utility runs, private road maintenance, private security perimeters. Villa estates, executive compounds, prestige campuses. This is Frontier density as luxury, not as poverty.
|
||||
|
||||
**The U-shaped wealth curve:**
|
||||
|
||||
Density has a U-shaped relationship with wealth. The very rich and the very poor both live at low density — but for opposite reasons. The rich can afford the infrastructure cost of space. The poor live where no infrastructure exists. Everyone in between clusters at Settled-to-Dense because that's what utility economics demands.
|
||||
|
||||
The player reads this instantly: low-density district on a dense world = money. Low-density district on a frontier world = nothing built yet. Same tile spacing, completely different story — and `prosperity_baseline` (D-197) disambiguates. High prosperity + low density = wealth. Low prosperity + low density = absence.
|
||||
|
||||
**Density → vertical pressure mapping:**
|
||||
|
||||
Density drives building height and basement depth via simple thresholds. These are tendencies, not hard limits — individual buildings vary, but the district-level character follows the pattern.
|
||||
|
||||
| Density class | Typical above-ground floors | Typical basement levels | Vertical pressure description |
|
||||
|---------------|---------------------------|------------------------|------------------------------|
|
||||
| Frontier | 1 | 0 | Single-story. Horizontal only. |
|
||||
| Settled | 1-2 | 0-1 | Mostly single-story, occasional two-story. Basement where terrain allows. |
|
||||
| Established | 2-3 | 1 | Two-story default. Three-story at intersections and commercial streets. Basements standard. |
|
||||
| Dense | 3-6 | 1-2 | Multi-story default. Ground-floor commercial. Structured parking appears underground. |
|
||||
| Compressed | 6-12+ | 2-3 | Highrises. Street canyons. Multi-level subsurface: parking, metro, utilities, logistics. |
|
||||
|
||||
The transition from "buildings grow out" to "buildings grow up" happens at Dense. Below Dense, the settlement solves population pressure by adding more districts horizontally. At Dense and above, horizontal expansion is constrained (by terrain, by utility cost, by land value) and vertical growth begins.
|
||||
|
||||
The transition from "no basement" to "buildings grow down" happens at Established. Below Established, basements are optional amenities. At Established and above, basements become structural necessities — storage, utilities, parking that can't fit at grade.
|
||||
|
||||
**Density derivation:**
|
||||
|
||||
The density scalar for a settlement is derived from existing systems.db data:
|
||||
|
||||
```
|
||||
base_density = SETTLED_DEFAULT // 1,500/km² — the utility-cost floor
|
||||
|
||||
// Economic pull increases density
|
||||
if economic_role in [transit, commercial, manufacturing]:
|
||||
base_density *= 1.5–2.0
|
||||
if is_corp_hq_world:
|
||||
base_density *= 1.5
|
||||
if gate_adjacent:
|
||||
base_density *= 1.3
|
||||
|
||||
// Terrain constraint modulates
|
||||
if terrain_constraint_factor > threshold: // coastal strip, canyon, etc.
|
||||
base_density *= 1.2–1.8
|
||||
|
||||
// Wealth disperses (for specific districts within a settlement)
|
||||
if district.prosperity_baseline > 0.8:
|
||||
effective_density *= 0.4–0.6 // villa district within a dense city
|
||||
|
||||
// Clamp to class boundaries
|
||||
density_class = classify(effective_density)
|
||||
```
|
||||
|
||||
The exact multipliers are tunable. The structure — base at Settled, economic pull compresses, wealth disperses — is the locked architectural decision.
|
||||
|
||||
**Settlement footprint formula:**
|
||||
|
||||
```
|
||||
footprint_km2 = population / effective_density
|
||||
district_count = ceil(footprint_km2 / 0.262)
|
||||
grid_side = ceil(sqrt(district_count))
|
||||
```
|
||||
|
||||
Not all districts within the grid are built. A Settled-density village of 2,000 people covers ~1.3 km² = 5 districts in a 3×3 grid, but only 5 of 9 grid cells contain structures. The remaining 4 are open ground, agricultural land, or wilderness. The fill ratio increases with density class:
|
||||
|
||||
| Density class | Typical grid fill ratio | What fills the empty cells |
|
||||
|---------------|------------------------|---------------------------|
|
||||
| Frontier | 20-40% | Wilderness, private grounds, no development |
|
||||
| Settled | 50-70% | Agricultural plots, utility corridors, planned expansion reserves |
|
||||
| Established | 70-90% | Parks, low-density transition zones, institutional grounds |
|
||||
| Dense | 90-100% | Nearly full — remaining gaps are intentional public space |
|
||||
| Compressed | 100% | Full. No gaps. Every cell is built. |
|
||||
|
||||
**Reference calibration (real-world sanity check):**
|
||||
|
||||
| Real settlement | Pop | Real area | Real density | Model class | Model area | Match quality |
|
||||
|----------------|-----|-----------|-------------|-------------|------------|---------------|
|
||||
| Wadenoijen, NL | 695 | 0.10 km² | 6,950/km² | Dense | 0.12 km² | Close |
|
||||
| Kerk-Avezaath, NL | 1,250 | 0.40 km² | 3,125/km² | Established | 0.42 km² | Close |
|
||||
| Bisbee, AZ | 5,000 | 1.50 km² | 3,333/km² | Established | 1.67 km² | Close |
|
||||
| Kiruna, SE | 17,000 | 11.40 km² | 1,491/km² | Settled | 11.33 km² | Near-exact |
|
||||
| Périgueux, FR | 30,000 | 9.82 km² | 3,055/km² | Established | 10.00 km² | Near-exact |
|
||||
| Tiel, NL | 39,000 | 13.00 km² | 3,000/km² | Established | 13.00 km² | Exact |
|
||||
| Kanazawa DID, JP | 466,000 | 59.00 km² | 7,898/km² | Dense | 77.67 km² | Reasonable |
|
||||
| Paris proper, FR | 2,040,000 | 105.00 km² | 19,429/km² | Compressed | 136.00 km² | Reasonable |
|
||||
|
||||
The model is calibrated to real-world settlements. Discrepancies come from the square-grid assumption (real settlements elongate along rivers, coasts, and roads) and from the discrete density class boundaries (continuous real densities mapped to five bins).
|
||||
|
||||
**Interaction with other D-records:**
|
||||
|
||||
- **D-194 (district mix):** District *types* are still determined by the three-component mix. Density determines how much *space* those districts occupy and how they're spaced.
|
||||
- **D-197 (prosperity_baseline):** Disambiguates the wealth curve. Same low density reads as luxury (high prosperity) or absence (low prosperity).
|
||||
- **D-201 (spatial hierarchy):** Tile scale amended from 0.5m to 1m with 0.5m subtile. Chunk real-world size doubles from 32m to 64m. All higher tiers unchanged.
|
||||
- **D-215 (spatial arrangement patterns):** The five archetype patterns operate within the footprint that density determines. CompanyTown spine is longer at Settled density, compressed vertically at Dense density.
|
||||
- **D-216 (BlockIrregularity):** Irregularity affects street grid geometry within a district. Density affects building height and spacing within that geometry. Orthogonal — a Dense district can be Organic (irregular streets, tall buildings) or Grid (regular streets, tall buildings).
|
||||
|
||||
- **Rationale:** Population alone cannot determine settlement character. A village of 2,000 at medieval European density (Kerk-Avezaath, 3,200/km²) and a homestead spread of 2,000 at frontier density (400/km²) produce a 10× footprint difference. In the Reach, utility cost on virgin worlds is the dominant constraint, making Settled (~1,500/km²) the natural floor. The density scalar unifies footprint calculation, vertical pressure, and inter-building spacing into a single derivable parameter. The tile/subtile split preserves 0.5m visual fidelity for character movement and furniture placement without inflating the generation pipeline's spatial grid.
|
||||
- **Ticket:** (pending — implementation tickets to be created when cascade Tier 3/4 work begins)
|
||||
- **Raised by:** Jeroen + Claude, design session 2026-05-03. Grounded against real-world settlement data (Wadenoijen, Kerk-Avezaath, Uzès, Tiel, Périgueux, Kiruna, Kanazawa, Paris, Bisbee, Alice Springs, Brasília).
|
||||
- **Cross-reference:** D-194 (district mix), D-197 (prosperity_baseline), D-201 (spatial hierarchy — tile scale amendment), D-215 (spatial arrangement patterns), D-216 (BlockIrregularity)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-222: Spatial hierarchy and naming — Subtile to District, lore vs code
|
||||
- **Date:** 2026-05-22
|
||||
- **Decision:** Canonical naming and sizes for the sub-settlement spatial hierarchy. This record is the single source of truth for the ladder; it supersedes the dimensions/naming scattered across D-094 (hierarchy), D-201 (tier table), D-220 (tile scale), and D-066 (dual-scale grid), which are amended to point here.
|
||||
|
||||
**The ladder (each tier nests cleanly in the one above):**
|
||||
|
||||
| Unit | Side | Composition | Real-world analogue | Used for |
|
||||
|------|------|-------------|---------------------|----------|
|
||||
| Subtile | 0.5m | — | a footstep | server simulation, entity positioning, render detail |
|
||||
| Tile | 1m | 2×2 subtiles | a doorway | **the unit all sizes are quoted in**; generation cascade, pathfinding, grids |
|
||||
| Chunk | 64m | 64×64 tiles | half a block | streaming / serialization unit (technical) |
|
||||
| Block | 128m | 2×2 chunks | a city block (~100m real) | generator planning unit |
|
||||
| Quarter | 512m | 4×4 blocks (16) | a superblock (≈ Barcelona *superilla*, 0.262 km²) | the cell a settlement footprint is tiled into |
|
||||
| District | 2048m | 4×4 quarters (256 blocks) | a real urban district (4.19 km²) | large urban division |
|
||||
| *(above)* | *fluid* | — | borough / sector / city | **not a fixed tier** — settlements vary too much; groupings above District are settlement-specific |
|
||||
|
||||
Nesting rhythm: Chunk→Block is 2×2 (Chunk is a technical sub-block streaming unit); Block→Quarter→District is a uniform 4×4 (the legible "human" ladder).
|
||||
|
||||
**Lore ≠ code (load-bearing):** the tier names above are **code/generation units** — fixed grid cells. The *same words* used in lore, narrative, and UI ("Sova Transit District", "the market quarter") are **free-form region labels** with no obligation to map to a code tier. A narrative "district" may span several code Quarters, sit inside one, or ignore the grid entirely. **Reviewers and the clerk must not reconcile a lore name to a code tier** — they are different registers that happen to share vocabulary.
|
||||
|
||||
**Rename from prior records:** the 512m cell that D-094 / D-201 / D-220 called a **District** is now the **Quarter**. "District" is promoted to the 2048m tier (4×4 quarters), matching the real-world scale of a named urban district. The tile scale (Tile = 1m, Subtile = 0.5m) and the doubled footprints established by D-220 are unchanged. Code that uses "District" for the 512m skeleton unit (e.g. `DistrictSkeleton`) is now misnamed → see follow-up ticket.
|
||||
|
||||
- **Rationale:** The old hierarchy named only Chunk/Block/District and jumped from a 512m "District" straight to the 1–30km Settlement, leaving the 512m cell wearing the name of a unit two rungs too large — a real district is kilometres and many neighborhoods, not 16 blocks. Grounding against real sizes (city block ~100m; Barcelona superblock 400m / 9 blocks; real district several km) puts Block at 128m (correct), the 512m cell at Quarter/superblock, and a true District at 2048m (4.19 km²). Leaving everything above District fluid matches reality — boroughs/sectors/cities vary too much to force a fixed generation tier, and the Reach's settlements range from frontier homesteads to compressed capitals. Separating the lore register from the code register lets narrative use "district" naturally without dragging the generator's grid into it.
|
||||
- **Raised by:** Jeroen + Claude, design session 2026-05-22. Grounded against real-world block/superblock/district scales.
|
||||
- **Cross-reference:** [D-094](#d-094) (spatial hierarchy — renamed/superseded), [D-201](#d-201) (tier table — superseded for the sub-settlement tiers), [D-220](#d-220) (tile scale — Tile/Subtile retained; its "district" footprint cell is the Quarter), [D-066](#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor) (dual-scale — Subtile is the 0.5m grid), [D-093](content.md#d-093-sova-transit-district--spatial-layout-and-district-topology) (Sova Transit District — a *lore* district, not bound to the code tier)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-223: Authored content as a flavored name pool — markers names-only, hand-authored templates removed
|
||||
- **Date:** 2026-05-22
|
||||
- **Decision:** Per-planet authored content feeds the deterministic generator as a **flavored name pool only** — never as authored geometry or pinned positions. This supersedes the topographic-geometry retention and the `reserved=1` pinning of [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features), and the hand-authored-template references in [D-191](#d-191) §8.
|
||||
|
||||
**markers.json → names only.** A body's `markers.json` carries a flavored pool of names (river names, mountain names, settlement names) and **no positions or geometry**. Positions are not authored anywhere; they are derived or generated:
|
||||
- **Rivers** — courses come from the deterministic D8 drainage (D-208) on the fixed heightmap; the **largest rivers take names from the pool**.
|
||||
- **Mountains** — positions are a given from the **fixed heightmap**; they take names from the pool.
|
||||
- **Settlements** — the economic sim runs, the **largest population areas** are found, the capital and cities are picked by the placement pipeline (D-211), and they **take names from the pool**.
|
||||
|
||||
**The 6 hand-authored templates are removed.** Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade were a superseded hand-authored-showcase direction that kept resurfacing and complicated the landscape. Their hand-authored marker positioning, the `reserved=1` pinning, and the "6 templates" framing/special-casing are all removed. **The bodies themselves remain real places** — their wiki pages, lore, corporations (e.g. comptoir-lendel), and names stay and feed the name pool; deleting them would only force regenerating equivalent content. Only the authored *machinery* is tossed. **After this there is no distinguishable category of "template" left** — the six are processed by the exact same pipeline and data model (fixed heightmap + names-only markers) as every other body, with no special code path, schema flag, or pinning. They differ only in *content richness* (more developed lore/names/economic profile), which is a difference of degree shared by any notable body, not a difference of kind. The concept dissolves; that is why it can be removed wholesale rather than carefully migrated.
|
||||
|
||||
**No pinned positions anywhere.** Determinism comes from fixed heightmaps + deterministic drainage + deterministic economic sim + seeded placement — not from authored anchors. The lore-vs-code split (D-222) governs how pool names attach to generated features: names are free-form labels on generated geometry, not bound to grid cells.
|
||||
|
||||
Sub-city authored set-pieces (e.g. D-093 Sova Transit District, station interiors) follow the same principle when Phase 4 reaches that scale — generated geometry, lore names/roles attached, nothing pinned — to be applied (and prior authored layouts superseded) at that point.
|
||||
|
||||
- **Rationale:** Authored positions created a hand/procedural split that (per D-207's own rationale) was impossible to query, diff, or validate. Reducing authored content to a name pool removes that split entirely while preserving the flavor that makes places feel hand-made — names follow culture and region, geometry follows terrain and economics. The templates were leftover scaffolding from before fully-generative placement; removing them simplifies the pipeline without losing any canonical place.
|
||||
- **Implementation:** tracked under Phase 4 (epic #750) — strip markers to names, remove template machinery + `reserved` pinning, update the atlas pipeline/schema, regen. Historical archives (sprints, workshops, audits, CHANGELOG, atlas proposals) are left untouched.
|
||||
- **Implementation status (#951, 2026-05-22): done.** All 2,398 `markers.json` reduced to a names-only pool (25,264 names; 349 city names across 271 bodies). The Python atlas **geometry generator** (`generate_atlas.py`) and the **LLM naming cluster** (`gemma_naming.py`, `naming_core.py`, `apply_name_fixes.py`, their tests/QA, the `fix_fewshot_bleed`/`prune_atlas_features` geometry tools, the redundant `import_city_names.py`, and the `run-atlas-naming.sh` runner) were retired — the procedural server cascade (Phase 4) supersedes them. The Gemma prompting methodology is preserved in [docs/gemma-naming-methodology.md](../docs/gemma-naming-methodology.md). Shared atlas-DB utilities moved to `tooling/planet-gen/atlas_common.py`; **`import_economics.py` is now the sole regen-db generator that owns the atlas index** — it loads the names pool into `atlas_city_names` and empties the 8 geometry tables (`atlas_cities/roads/railroads/pois/rivers/oceans/mountain_ranges/body_grids`), which the cascade fills. `population`/`kind`/`settlement_class` on `atlas_city_names` are **deferred to placement (#955)** — `attractor_matching` reads them at 0/default until then; that empty state is the gap the cascade closes. The `reserved=1` corp-HQ cross-reference stays (corp HQ names are real places). A latent duplicate-accumulation bug in name population (no clear + no unique constraint) was fixed with a deterministic rebuild. **Sol (system `GJ 0`) is permanently exempt from the normal generators:** it uses real Earth/Mars/Luna geography via the offline `sol_import.py` (left in place for future scripted integration), so its bodies keep geometry-bearing `markers.json` as preserved positional config and are skipped by the names-pool importer — Sol names will come from its own integration, not the cascade. `make regen-db` green.
|
||||
- **Raised by:** Jeroen, 2026-05-22 — resolving the open question on merging preconfigured content into the deterministic cascade.
|
||||
- **Cross-reference:** [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features) (superseded — names-only, no reserved pinning), [D-191](#d-191) §8 (markers format — names-only), [D-208](#d-208) (drainage → river courses), [D-211](#d-211) (settlement placement), [D-199](#d-199) (economic read set), [D-222](#d-222) (lore≠code names on generated geometry)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-224: SeedChain — deterministic seed-derivation contract
|
||||
- **Date:** 2026-05-23
|
||||
- **Decision:** All deterministic generation — the Phase-4 world cascade now, and NPC / storyteller / economic generation later — descends from a single master world seed through one shared type, **`SeedChain`**, living at **`server/src/seed.rs`** (top-level, because seeds are broader than the atlas). It is the **only** sanctioned way to derive a child seed; the ad-hoc pre-mixing (`seed.wrapping_add(C)`) that was in `skeleton_gen.rs` / `district_mix.rs` is removed, and the doc-comment references to a "SeedChain" in those files become real.
|
||||
|
||||
**Mixing primitive.** `splitmix64` — already used by `EntityRng::from_seed_and_id` (`simulation/rng.rs`), chosen there specifically to avoid the `wrapping_add` collision class where `(seed=0,id=N) == (seed=1,id=N-1)` — is promoted to a shared `pub(crate)` function in `server/src/seed.rs` and reused. One canonical *mixer* for the whole codebase. (`EntityRng` keeps its own domainless combine — `splitmix64(world_seed) ^ splitmix64(stable_id)` — for now; re-expressing it as `derive(Npc, stable_id)` would be a deliberate, **stream-changing** migration, not a free cleanup, so it is left as a follow-up.)
|
||||
|
||||
**Contract:**
|
||||
```rust
|
||||
pub struct SeedChain(u64); // Copy
|
||||
pub enum SeedDomain { Body, Layer1Topography, Layer3Settlement, Layer4Quarter, Block, Npc /* … */ } // u64 tags
|
||||
impl SeedChain {
|
||||
pub fn root(world_seed: u64) -> Self; // top of the chain
|
||||
pub fn for_body(world_seed: u64, body_id: &str) -> Self; // root + derive(Body, fnv1a_64(id))
|
||||
pub fn derive(self, domain: SeedDomain, id: u64) -> Self;
|
||||
pub fn atlas_rng(self) -> AtlasRng; // integer-only LCG stream (D-010)
|
||||
pub fn seed(self) -> u64; // raw — for SimRng/ChaCha or further derive()
|
||||
}
|
||||
```
|
||||
|
||||
**Derivation (load-bearing — pinned, because changing it changes every generated world):**
|
||||
`derive(domain, id) = splitmix64(self.0 ^ splitmix64(domain as u64)) ^ splitmix64(id)`.
|
||||
Properties: deterministic; domain-separated (distinct `SeedDomain` tags never share a stream); full avalanche (splitmix64 on each input); integer-only (D-010 #4); chainable (`root → Body → Layer3Settlement → Quarter → Block`). The output is well-distributed, so `AtlasRng::new` is fed the derived seed directly — no `| 1` or golden-ratio pre-mix guard.
|
||||
|
||||
**Body identity (the `Body` domain id).** Bodies are identified by a *string* `body_id`, not a numeric StableId, so `SeedDomain::Body` is keyed by **FNV-1a (64-bit) of `body_id`** — the repo's standard deterministic `&str → u64` convention (matching `TemplateId`/`TriangleId`). `SeedChain::for_body(world_seed, body_id)` (`root(world_seed).derive(Body, fnv1a_64(body_id))`) is the single sanctioned path; callers must use it rather than inventing their own string→u64 hash, or they would silently derive divergent worlds from the same seed — the very class of nondeterminism this record exists to kill, one level up.
|
||||
|
||||
**Stability guards.** `SeedDomain` carries explicit `#[repr(u64)]` discriminants and is append-only; the unit test `seed_domain_discriminants_are_pinned` fails CI if any is renumbered (which would re-roll every world). `AttractorType` likewise carries explicit `#[repr(u8)]` discriminants because it is cast `as u8` as a sort key (`features.rs`); reordering it would change attractor ordering and flip the cascade golden.
|
||||
|
||||
**Scope of effect (verified 2026-05-23):** SeedChain changes only the RNG-*using* layers — the existing `skeleton_gen.rs` (Layer 4 block placement) and the future Layer-3 settlement placement (#955). It does **not** affect Layer 0 heightmaps (produced by the Python `planet_simulation` pipeline, seeded separately via `--seed`, committed as `heightmap.png` files) nor Layer 1 (`drainage`/`features`/`subbiome` are RNG-free — pure functions of the heightmap). The #952 Layer 0→1 golden fixtures are therefore SeedChain-independent and can be captured in any order relative to the SeedChain work.
|
||||
|
||||
- **Rationale:** Three seeding paths had drifted apart — `AtlasRng` (LCG, "callers pre-mix"), `EntityRng` (correct splitmix64 mixing), and ad-hoc `wrapping_add` in atlas callers — while the code already *named* a SeedChain that didn't exist. A single typed derivation chain with domain separation makes every sub-stream reproducible from one world seed, eliminates the `(seed,id)` collision class `wrapping_add` invites, and gives the determinism harness (#952) a stable contract to verify against. Promoting one mixer prevents two divergent implementations.
|
||||
- **Implementation:** #952 (Phase 4, epic #750) — `server/src/seed.rs` (incl. `for_body`/`fnv1a_64`), `SeedChain` threaded through the atlas RNG callers, an extensible cascade harness (`run_cascade`/`CascadeSnapshot`/`CascadeLayer`), and a golden-seed regression test (SHA-256 of `heightmap.png` + JSON-serialized `Layer1Output`, run at 256×128 so the river network is non-empty — JSON not msgpack, to match the diffable `golden_suite.rs` convention). Pre-Phase-5: no savegames exist, so the seed-stream change needs no migration; D-202's `schema_version` lineage covers future changes once saves exist.
|
||||
- **Raised by:** Jeroen + Claude, `/whats-next` refinement of #952, 2026-05-23.
|
||||
- **Cross-reference:** [D-010](#d-010) (determinism — integer-only, seed→identical output), [D-200](#d-200) (three-tier execution model), [D-208](#d-208) (RNG-free drainage), [D-223](#d-223) (names-only pool — placement uses seeded RNG), `simulation/rng.rs` (EntityRng / splitmix64 precedent)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-225: Atlas layer-stream proxy — compute-on-demand, mod-first (resolves Q-098)
|
||||
- **Date:** 2026-05-23
|
||||
- **Decision:** How per-body generation-cascade layer data reaches the Godot client for the Phase-4 Atlas progress viewer (#960). **Resolves Q-098.** Derived layer data is **never baked** into the install — that would both bloat the install (~100MB+ for ~267 bodies) and make modded bodies second-class. Instead a server-side **layer-stream proxy** computes on demand from moddable source files and streams to the client:
|
||||
|
||||
**(1) Transport — existing IPC stream + additive message tag.** Not a second socket, not a per-frame envelope rewrite. The bridge today carries no message-type discriminator (server→client is always `ObserverSnapshot`, client→server always `Vec<PlayerInput>`). The atlas request/response ride the *same* TCP stream as new message types, disambiguated **structurally** in v1 (a snapshot has `entities`/`tick`; the atlas messages do not). A full `BridgeMessage` envelope-everywhere migration is deferred — it would be a needless wire break, and client+server co-ship (D-005/D-192) so it can be done later as cleanup.
|
||||
|
||||
**(2) Single framed MessagePack response, not chunked.** `Layer1Output` is ~hundreds of KB worst case (512×256), well under the 16 MB frame cap. The **raster is not streamed** — the client already loads `reliefmap.png` from disk; the proxy streams only the *computed* `Layer1Output` (rivers, basins, attractors + sub-biome). MessagePack matches the rest of the bridge.
|
||||
|
||||
**(3) Mod-first source resolution.** A new `BodySourceResolver` with an ordered search: mod dirs (override) → base install (`wiki/...`, the floor). It reads the body's relative `terrain_reference` from `systems.db` (read-only) and returns the first existing `<root>/<terrain_reference>`. First-party and mod bodies flow through the **identical** resolve→`run_cascade`→stream path — the `heightmap.png` is the sole source of truth, no baked layer data. v1 wires the base root only; the resolver type + search order exist and are tested with a synthetic mod root, so the seam is mod-first from day one.
|
||||
|
||||
**(4) Cache via background queue on miss (D-203/D-206).** Request → `BodyWorldStateCache` lookup. Hit → serialize and return synchronously (a serialize, no compute). Miss → enqueue an `Immediate` `AnalyzeBody` on the `GenerationQueue` (D-206 background tier) and reply `Pending`; push the layer response when the completion lands. **The ~45 ms cascade must never run on the tick thread** (D-203: no blocking CPU/DB for heightmap data on main). The client shows the existing "TERRAIN DATA PENDING" panel until the response arrives. Eviction → recompute (always valid — determinism guaranteed).
|
||||
|
||||
**(5) Whole `Layer1Output` per response; client composites additive overlays.** The layers are produced together in one drainage pass, so per-layer requests save no compute and only add round-trips. Overlay toggles (heightmap + rivers + attractors + sub-biome shown *together*) are a pure client-side render concern (the existing `_overlay_visibility` pattern). The request carries `body_id` + `up_to_layer` (a forward-compat seam; v1 honors `Topography`).
|
||||
|
||||
- **Critical-path dependency:** the proxy is inert until `gen_queue.rs::run_work_item`'s `AnalyzeBody` actually runs `run_cascade` → builds `BodyWorldState` → populates the cache (today a documented stub, deferred from #142), and `GenCompletion::BodyAnalyzed` carries the computed state, not just `body_id`. This activation is the long pole and is tracked as its own ticket blocking #960.
|
||||
- **Rationale:** Baking privileges first-party content (a mod body cannot ship baked artifacts it cannot produce) and adds install bloat. Computing from the moddable heightmap on demand — the cascade is deterministic and ~45 ms, and D-200/D-203/D-206 already provide the background-compute + LRU tiers — keeps mods first-class, adds zero storage, and uses the architecture as intended. Reusing the existing IPC stream (vs a second socket) avoids a parallel connection lifecycle for an occasional, user-initiated, latest-wins-irrelevant request.
|
||||
- **Deferred / spun off:** the full `BridgeMessage` envelope-everywhere migration (later cleanup, not this ticket); the mod **content catalog** — mods adding *new* bodies need body rows + `terrain_reference` discoverable, but `systems.db` is binary / source-canonical (D-189) and mods cannot append to it → **Q-099**. D-225 resolves mod *file* resolution only; base-install resolution is enough to ship #960.
|
||||
- **Implementation:** #960 (client viewer + proxy) plus the `AnalyzeBody` activation ticket. New surface: `server/src/atlas/source_resolver.rs`, `server/src/atlas/layer_proxy.rs`; `gen_queue.rs` `run_work_item` activation + `GenCompletion` payload; bridge receive/send branching; client `protocol.gd` / `sim_bridge.gd` / `atlas_viewer.gd`.
|
||||
- **Raised by:** Jeroen (mod-first directive) + Tyre (design pass) + Claude, `/whats-next` refinement of #960, 2026-05-23.
|
||||
- **Cross-reference:** Q-098 (resolved by this), Q-099 (mod content catalog — spun off), [D-191](#d-191) (Atlas viewer), [D-166](#d-166) (per-layer Atlas progress viewer), [D-200](#d-200) / [D-203](#d-203) (three-tier execution, LRU cache), [D-005](#d-005) / [D-192](#d-192) (client+server co-ship — no version handshake), [D-224](#d-224) (SeedChain — feeds the cascade the proxy runs)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-226: Live-pause inspection harness — agent-navigable, real-UI debug/review
|
||||
- **Date:** 2026-05-24
|
||||
- **Decision:** A multi-layer harness that reuses the **real client UI** so both a human *and* an automated agent can review/debug deterministic server-computed state, by attaching to the live (auto-pausing) server. It supersedes the offline file-dump idea floated during D-225: the review tool *is* the production tool (zero divergence), inspecting live data through the real path rather than stale dumped artifacts. The stack, built bottom-up:
|
||||
|
||||
**(1) Live-pause substrate.** The client attaches to the running server; entering a fullscreen implant app (the Atlas) **auto-pauses the sim**. Pause freezes the world-advancing tick phases (Movement / Simulation / Economy / Storyteller / Knowledge / TickAdvance) but **keeps PreInput (the gen-drain), Input (to receive resume), Snapshot and PostSnapshot (the bridge) alive** — so the UI keeps fetching data while the world is frozen. Built on the existing `TickRate::Paused` + the `input.rs` paused-allowlist seam, triggered by `HudGroups::gameplay_occluded` (D-170). For static Layer-1 geography pause is only a compute-saver; for *dynamic* state (economics) a stable frozen snapshot is essential.
|
||||
|
||||
**(2) Data — layer-stream proxy (D-225).** Streams `Layer1Output` (later economics state, save state) from server to client on demand, mod-first, no bake.
|
||||
|
||||
**(3) Human-visual viewer (#960).** The Atlas renders cascade layers as **additive, toggleable overlays** (relief base + rivers + drainage basins + attractors; *shape* encodes attractor type, *color* encodes sub-biome), extending the existing `OVERLAY_DEFS` / `AtlasOverlayBar` with a `generation` overlay group + a left-side legend panel. Buttons start `pending`/locked and unlock as each layer's data arrives ("grows as each layer lands", D-166).
|
||||
|
||||
**(4) Agent-navigable channel.** A client-side `AtlasAgentInterface` exposing JSON `observe` (current data state + a walkable **UI affordance tree**) and `act` (named **semantic intents** — `select_body`, `open_regional`, `set_overlay`, `back`, … — backed by the same handlers a click calls, **not** pixel coordinates; the map uses `_gui_input`). Runs headless. Turns human-eyeball review into an agent-automatable QA sweep across the whole Reach (and later economics/saves).
|
||||
|
||||
**(5) Capture — reuse, don't reinvent.** Interactive screenshots reuse the **existing `tests/run-visual` capture primitive** (frame-delayed `save_png`, proven by `character_creation.gd`'s standalone capture). The agent channel adds *interactive* explore-and-capture; the existing scenario/flow **visual-golden regression** role stays distinct and unchanged. Clear hover state before capture.
|
||||
|
||||
**Consumers** (build geography now; the others adopt the pattern in their own phases — do **not** build them now): Layer-1 geography (Phase 4) → economics (dynamic; pause essential) → save-game inspection (load → pause → inspect; `save_state.rs` substrate).
|
||||
|
||||
- **Rationale:** Reusing the real UI — rather than a parallel offline renderer or dumped files — means the debug/review surface never diverges from what ships, and a dropped artifact can't go stale. Agent-navigability converts qualitative "does the synthesis look natural?" review from a manual eyeball pass into an automatable sweep that flags the few outliers for a human. The harness rides seams that already exist (`TickRate::Paused`, the paused-allowlist, `gameplay_occluded`, the bridge framing, the `run-visual` capture primitive) — a naming-and-contract exercise, not a new subsystem.
|
||||
- **New surface:** server pause-gating (run-conditions on the world phases keyed to a pause command); client `AtlasAgentInterface` (`observe`/`act`, Control-tree walker) + its local transport; the generation overlay rendering + selector + legend; interactive capture wired to `run-visual`.
|
||||
- **Implementation:** Phase 4 (epic #750), built bottom-up — auto-pause substrate, #969 proxy (D-225), #960 viewer, agent channel, agent capture. Geography is the first consumer.
|
||||
- **Raised by:** Jeroen + Claude (design), with Tyre (channel/pause/headless architecture) + Araminta (overlay encoding + affordance UX), 2026-05-24.
|
||||
- **Cross-reference:** [D-225](#d-225) (layer-stream proxy — the data path), [D-166](#d-166) (per-layer Atlas progress viewer), [D-191](#d-191) (Atlas viewer), [D-169](#d-169) / [D-170](#d-170) (implant components / HUD occlusion — `gameplay_occluded` trigger), [D-200](#d-200) / [D-203](#d-203) (execution tiers / LRU cache), Q-099 (mod content catalog), `tests/run-visual` (capture primitive), `save_state.rs` (save-inspection consumer)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-227: Deterministic-rebuild world model — derive-don't-store, cache, tile mutators, volumetric
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** The walkable world is a **pure function of `seed + atlas`**, materialised on demand and cached, never persisted as a tile grid.
|
||||
- **Derive-don't-store.** `subtile(x,y,z) = derive(seed, atlas, position)` — deterministic (D-010), recomputed on demand, held in a transient by-chunk/region cache for performance, evictable (eviction → recompute, always valid). No generated tile/voxel is ever saved.
|
||||
- **Volumetric, in voxels.** Unit vocabulary, fixed here: a **voxel** is the 1 m cube (the 3-D framing of D-222's tile); a **subvoxel** is its 0.5 m subdivision — eight per voxel, the finest derivation/dig granularity, in all three axes. Derivation is 3-D — surface *and* subsurface — over this grid. The heightmap/atlas sets the top; below it a deterministic geology model (strata by depth → bedrock; per-region lithology; ore / aquifer / cave as 3-D noise) fills downward. Digging reveals `derive(...)` for newly-exposed subvoxels — never "generated on dig"; the hole merely stops hiding seed-math.
|
||||
- **Floors are semantic and variable-height, not a fixed voxel count.** A *floor* — a walkable story, the z-level of D-049 / D-110 — groups voxels and **defaults to 3 voxels (3 m)** but is **not locked**: a cathedral nave or a hangar is one ~10 m floor, and that must be expressible. The voxel/subvoxel substrate is uniform and continuous in z; a floor exists wherever the volume is **walkable void**, and that void can be **built** (buildings), **excavated** (basements, mines), or **natural** (caves, lava tubes, caverns — derived in the geology model as 3-D voids). A cave is as much a floor as a basement; only truly solid rock is floorless. z-level addressing indexes stories whose real height in voxels varies.
|
||||
- **Vertical extent is physical, not a floor count.** The old "±50 floors" framing was a *stored-grid* artifact (each z-level was a filled, stored layer with memory/fill cost). Derive-don't-store eliminates that cost, so: **down** is bound by the body's own geology (crust → bedrock → pressure/heat-impassable interior, a per-body depth tied to radius / `max_elevation`); **up** is bound by *max built-structure height* (towers are built/authored or player-placed, not natural-derived) over free-to-derive air. This **supersedes any ±50 cap** — none was ever recorded as a D-record; it survived only as a 50-floor-skyscraper *example* in the generator-architecture workshop.
|
||||
- **Persistence has two parts, and only one is mutators.** (a) **Tile mutators** capture *direct physical gameplay* — the player or sim physically altering the world (pave a road, build a wall, dig a strip-mine, rocket-launcher the bank, fell a tree). Save = `seed + sparse mutator log`; load = re-derive base + replay. (b) **Live simulation state** (the rolling economy now; NPC / storyteller state later) is *history*, not seed-derivable, so it **is saved — but as sim-state, not as mutators.** The *derived* economic base is handled by **cache expiration** — recomputed on expiry, and **force-evicted on an event** (e.g. the player tanks a system's economy) — while the rolling state persists alongside the save. The physical world stays re-derivable; the living state is the small saved delta on top. (Mutator op schema → Q-103.)
|
||||
- **Determinism reclassified safety-critical.** Because mutators reference derived state, any derivation drift (a non-deterministic algorithm, an f32 comparison/ordering, `HashMap` iteration) desyncs the whole save — not merely a cosmetic difference. D-010's integer-only + ordered-collection discipline is load-bearing for *saves*, not just for golden tests.
|
||||
- **Rationale:** A world that stores its tiles cannot scale to body-sized 3-D volumes and bloats saves; a pure-function world with a transient cache + a sparse mutator log scales to any size, makes saves trivially small, and is the only model under which "dig anywhere, to any depth" is free (the subsurface was always computable — digging just reveals it). It also forces the determinism discipline the whole cascade needs anyway. The downward floor cap fell because it was solving a problem — per-layer storage cost — that derive-don't-store eliminates.
|
||||
- **Open sub-questions:** the geology-model fidelity (simple depth-horizon stack vs tectonic-grade folding/faults) and how far `FloorMaterial` is derived now vs deferred to the city layers (both tracked in D-228 / Q-101); the mutator op schema (Q-103).
|
||||
- **Implementation:** Phase 4+ (epic #750). The caching substrate exists at the atlas level (`BodyWorldStateCache`, D-203/D-225); the tile/voxel cache, mutator overlay, and geology derivation land as the cascade reaches the tile layers. No migration needed pre-save (D-202 `schema_version` lineage covers future drift once saves exist).
|
||||
- **Raised by:** Jeroen (derive-don't-store, volumetric, drop-the-floor-cap directives) + Claude, atlas-derivation workshop, 2026-05-25.
|
||||
- **Cross-reference:** [D-010](#d-010) (determinism — now save-critical), [D-222](#d-222) (subtile/tile/chunk hierarchy), [D-110](#d-110) (signed z-levels), [D-225](#d-225) (layer-stream proxy + cache pattern), [D-203](#d-203) (LRU cache tier), [D-224](#d-224) (SeedChain — feeds `derive`), [D-228](#d-228) (composite tile schema — the derived value type), [Q-101](questions-architecture.md#q-101) (refinement contract), [Q-103](questions-architecture.md#q-103) (mutator op schema), [Q-104](questions-architecture.md#q-104) (floor↔voxel-z mapping)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-228: Composite tile schema — orthogonal axes, derived shape, region-level morphology
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** A tile's "type" is **not a flat enum** but a small bundle of **orthogonal axes** (all *derived* per D-227 — this is the cache's value type, not stored state). Named types ("fjord wall", "mountain pass", "river bank") are **derived display labels, never stored**: a fjord wall and a sea cliff are identical to the simulation (`Cliff + Rock + DeepWater`), differing only in name.
|
||||
- **Per-subtile axes:** `TerrainMaterial` (**permanent** natural ground — Soil / Sand / Gravel / Rock / Wetland / Lava) · `FloorMaterial` (built surface over the ground — None / Concrete / Pavement / Carpet / Metal / …) · `Vegetation` (ground cover — Barren / Grass / Scrub / Thicket / Forest / Crop / Cleared / …) · `Water` (local depth *state* — Dry / Shallow / Deep) · `elevation` (scalar, metres). **`Snow` and `Ice` are not `TerrainMaterial`** — they are seasonally dependent, so they live in the seasonal **cover** overlay (Q-105), present only when the region's seasonal state puts them there (permanent only where climate never melts them — poles, glaciers). The `FloorMaterial` / `Vegetation` vocabularies stay open, extended by the layers that own them.
|
||||
- **`FloorMaterial` and `Vegetation` are override stacks, not single derivations.** Each resolves in precedence **wild/natural (biome + climate) → economic (managed: farmland, plantation, paving, clearance — from the settlement layer) → user (mutators: chopped, planted, built, demolished)**; the topmost present layer wins — the same derive-don't-store + mutator pattern (D-227) as the rest of the world. `Vegetation` carries cover/concealment, the movement sound profile, *and* economic yield (timber / crops) on one axis — and managed crops follow the region seasonal cycle (sown → growing → ripe → harvested → fallow), so farmland visibly turns over with the year (Q-105).
|
||||
- **`Water` is a dynamic depth state, plus derived flow.** The axis is depth/wetness only; the water *feature semantics* (river / ocean / lake / sea / tidal-flat / delta) are the **region morphology zone** (below), not a per-tile value, and **flow direction** derives from the D8 network where moving water applies. Depth is **time-varying** via a deliberately cheap, deterministic, **clock-bound** water-height — a **seasonal** term phased by hemisphere (latitude sign) and a **tidal** term that exists only if the body has a moon (no satellite → no tide), amplitudes as body parameters. The water-height is a **region property computed once per phase** (not per tile, not per frame); per-tile flood state is then just `region-water-height vs local elevation`. So **floodplain / tidal-flat / seasonal-river *emerge*** rather than being placed — a low `alluvial-plain` floods at high water and recedes at low. The static world stays static; only this region water-height carries the clock overlay (sim-time as the derivation input), recomputed on phase change — the dynamic-state path D-226 anticipates. Passability derives from `Water` + `elevation` + `material` together. (Model → Q-105.)
|
||||
- **Derived, not stored:** **shape / geometry** (flat / angled / drop / cliff) = `f(elevation-step × material)` — sand slumps to an angle of repose, rock breaks to a vertical face on the same 1 m step; drives the rendered tile geometry. **Tactical form** (crest / hollow / channel / bank — cover, sightline, concealment) read from elevation curvature + water adjacency on demand. Storing either would mean updating 8 neighbours per elevation change, and both are clean functions of data the tile already holds.
|
||||
- **Region-level (shared by all tiles in a region, not stored per-tile):** **morphology zone** — fjord / delta / meander-reach / **alluvial-plain** / open-ocean / lake / sea / … — the whole-shape decision a single tile cannot see; it (a) governs which tile-tags get laid down, (b) carries affordances a tile can't (this inlet is a sheltered harbour), and (c) **owns the water-feature semantics** (river vs ocean vs lake vs sea) the per-tile `Water` axis deliberately omits. **Morphology resolves top-down (region → tile), never bottom-up** — a tile cannot know it is in a fjord from its neighbours; deriving it per-tile would produce the squaring the model exists to avoid. **Sub-biome, province, and the region's seasonal/clock state are likewise region properties** — the season is computed *once* per region per phase and inherited by its tiles. That single cheap seasonal state drives **water-height (flooding / tides), seasonal snow cover, weather, and the farmland crop cycle** alike — one calculation, many dynamic overlays (→ Q-105).
|
||||
- **Separate axes:** biome (climate / sub-biome — region-level, authority unresolved, Q-100; distinct from, but informs, the per-tile `Vegetation` cover), resource hints (bitmask: fertile / mineral / timber / groundwater).
|
||||
- **Cohesion matrix (anti-squaring at the material layer):** intra-region material + sub-biome scatter (a dirt patch in grass, a lone rock in beach sand) comes from a **global, position-keyed continuous noise field — never per-chunk** — so transitions never reveal chunk/grid seams. Straight lines appear **only when authored** (roads, plazas, field edges); a straight line must always have a placed cause, never be a generation-grid artifact. (Algorithm → Q-102.)
|
||||
- **Rationale:** A flat enum explodes combinatorially (≈5×5×8 ≈ 200 mostly-incoherent variants), can't be queried by axis ("all water-adjacent tiles"), and grows a new variant for every new mechanic. Orthogonal axes are set independently by different cascade passes (material from sub-biome, water from drainage, shape from elevation), independently queryable, cheap to serialize, and compose without explosion. Names are visual/narrative and belong downstream of the simulation.
|
||||
- **Open sub-questions:** how far the `FloorMaterial` / `Vegetation` vocabularies are enumerated now vs deferred to the settlement layers; the exact morphology-zone vocabulary; the dynamic water-height model (→ Q-105).
|
||||
- **Implementation:** Phase 4+ (epic #750), as the cascade reaches the tile layers. Today's `TileKind` (D-049 render stack) is the seed of the per-subtile axes; `SubBiomeVariant` (D-210) is the region biome axis.
|
||||
- **Raised by:** Jeroen (FloorMaterial, shape-from-material, cohesion-matrix directives) + Claude, with the atlas-derivation workshop four — Tyre (composite data-structure), Gestalt (tag model + tactical form), Nigel (morphology-as-character), Burnelli (economic distinctions), 2026-05-25.
|
||||
- **Cross-reference:** [D-227](#d-227) (derive-don't-store — these axes are the derived value type), [D-010](#d-010), [D-210](#d-210) (sub-biome — region biome axis), [D-222](#d-222) (subtile/tile), [D-049](#d-049) (z-stack render — `TileKind` seed), [D-208](#d-208) (D8 — flow-direction source), [D-226](#d-226) (dynamic-state inspection — the water overlay), [Q-100](questions-architecture.md#q-100) (biome authority), [Q-101](questions-architecture.md#q-101) (refinement contract), [Q-102](questions-architecture.md#q-102) (cohesion-matrix algorithm), [Q-105](questions-architecture.md#q-105) (dynamic water-height)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-229: Building-property-tag schema — the step-3 fill output
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** Define `BuildingPropertyTag`, the typed step-3 output that replaces the `String` stubs (`era`, `society_profile`, `zone_palette`, `chunk_layout`) on the skeleton. **One tag per building footprint** placed in a block; **written once** at plan time (inside the extended `GenerateSkeleton`, D-230), **read-only thereafter** by three consumers — FillChunk (D-230), the guarantee audit (D-097), and the Phase-6 interior generator (D-231). Fields:
|
||||
- `zone_type_id: ZoneTypeId` (`Box<str>` matching a RON `id`) — *what the building is*; the wire to the 31 D-142 zone-type RON files. Refined from `district_mix.rs`'s per-block `ZoningType` via a generator lookup `(ZoningType + economic_role + seed) → ZoneTypeId`.
|
||||
- `footprint: TileRect` — integer tile-space rect within the 128-tile block (D-010 integer-only).
|
||||
- `extent: FloorExtent` — floor/basement extent; **resolves Q-104** (below).
|
||||
- `entry_class: BuildingEntryClass` — physical access character: `Public | Commercial | Restricted | BreachOnly`. **Named `BuildingEntryClass`, not "access tier"**, to avoid colliding with D-028's relational dialogue layers. Derived from `zone_type × layout_mode (D-096) × prosperity (D-197)`: Commission-Grid → formal/logged/corporate-or-authority credentials; Organic → social/reputation/unlogged credentials. U-curve degrade: low prosperity on a normally-Commercial zone → `BreachOnly` (derelict).
|
||||
- `flavor_ref: ArchitectureFlavorRef { flavor_index: u8 }` — index into the body's flavor profile (D-232); deterministic `(seed + zone_type) → index`; no rolling-economy read. (The selection mechanism is D-232's weighted `allow`/`block` filter; the tag records the chosen index for Phase-6 to read cold.)
|
||||
- `era: ConstructionEra` (`Founding | Established | Modern | Derelict`) + `era_cause: EraCause` — feeds ZonePalette modifier axis C (D-101) and sets the D-217 condition floor. Derived from `founding_age_years + prosperity_baseline + seed`; a body carries mixed-era buildings (founding period anchors the distribution; seed scatters outliers).
|
||||
- `initial_condition: TileCondition` — frozen-amber snapshot from `prosperity_baseline` (D-197/D-217). The rolling condition overlay (D-198) paints *over* this; it never mutates the tag.
|
||||
- **`FloorExtent { base_floor: i8, floor_count: u8, heights: FloorHeightProfile }`** where `FloorHeightProfile = Uniform(u8) | Variable(Vec<u8>)`. **Q-104 resolution (the D-110 ↔ D-227 bridge):** two pure functions — `floor_at_voxel_z(z) -> Option<i8>` and `voxel_range_for_floor(f) -> Option<(i32,i32)>` — map D-110 floor-index addressing onto D-227 physical voxel-z. Default `Uniform(3)` (3 voxels ≈ 3 m/floor, per Jeroen); a cathedral/hangar is `Uniform(10)`; a mixed-use stack is `Variable([5,3,3,3,3])`. The `Variable` branch carries per-floor memory only when floors actually differ.
|
||||
- **Rationale:** A typed tag is the single contract that lets the atlas render a building, the guarantee audit validate a district, and Phase 6 seed an interior — all from one frozen object. String stubs cannot carry any of that. Orthogonal fields (what / where / how-tall / who-may-enter / cultural / era / condition) compose without a combinatorial enum explosion, matching D-228's axis discipline at the building scale.
|
||||
- **Implementation:** Phase 4+ (epic forthcoming). Replaces the stub fields on `BlockSkeleton`/`DistrictSkeleton` in `server/src/simulation/generator.rs`.
|
||||
- **Raised by:** Tyre (schema + FloorExtent/Q-104), Gestalt (BuildingEntryClass, derivation), Miri (flavor_ref), economic-built-world workshop round 2, 2026-05-25.
|
||||
- **Cross-reference:** [D-142](content.md#d-142) (zone-type taxonomy), [D-096](#d-096) (layout mode), [D-197](#d-197) (prosperity baseline), [D-217](#d-217) (tile condition), [D-110](#d-110) (signed z-levels), [D-227](#d-227) (voxel substrate), [D-101](#d-101) (ZonePalette), [D-028](#d-028) (dialogue access — name disambiguated), [D-230](#d-230), [D-231](#d-231), [D-232](#d-232), [Q-104](questions-architecture.md#q-104) (resolved here)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-230: FillChunk two-phase execution model — background-plan / on-demand-derive
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** Building fill runs in **two phases**, split by latency budget. **Plan phase (background, no budget):** building-property tags + footprints are produced *inside* the extended `GenerateSkeleton` Rayon task — it already holds the full `CityGenerationContext`, so footprint subdivision + tag assignment stay contiguous with the skeleton work. Output: `DistrictWorldState { skeleton: DistrictSkeleton, block_tags: BTreeMap<(u8,u8), Vec<BuildingPropertyTag>> }`, stored in `BodyWorldState.districts: BTreeMap<DistrictId, DistrictWorldState>` — `BTreeMap` everywhere for D-010 determinism. **Derive phase (`FillChunk`, on-demand, <5 ms):** pure geometric shell derivation — read the cached `DistrictWorldState`, and for each tag `shell_derive(seed, footprint, extent, z) → {Void | Wall | FloorSlab | Roof}` per voxel; fill interstitial tiles (street / open space) from `density_pct` + the D-215 pattern; apply `initial_condition`. Cost estimate: ~40 k voxels/chunk × ~10–15 ns = **0.4–0.6 ms**, well under budget even for tall skyscrapers. **Pre-condition:** `FillChunk` is only dispatched after `GenCompletion::SkeletonGenerated` for that district has been processed; if `block_tags` is absent, re-enqueue at `High` and warn — never a blocking read on the main thread.
|
||||
- **Rationale:** The expensive, variable planning work (district mix, tag assignment, footprint layout) has no place under a per-chunk latency budget; doing it once in the background and reducing `FillChunk` to cache-read + rectangle-containment + z-range lookup is what makes on-demand fill trivially fast and re-derivable (D-227).
|
||||
- **Code-gap flagged (→ implementation ticket, independent of this workshop):** `GenCompletion::SkeletonGenerated` currently returns only `city_id: u64`; it must carry the `DistrictWorldState` back so the main thread can insert it into `BodyWorldState.districts`. The completion routing is incomplete today regardless.
|
||||
- **Implementation:** Phase 4+. Amends D-200 (extends the runtime-background execution tier). `FillChunk`/`ChunkGenWorker` are no-op stubs today (`gen_queue.rs`, `workers/stubs.rs`).
|
||||
- **Raised by:** Tyre, economic-built-world workshop round 2, 2026-05-25.
|
||||
- **Cross-reference:** [D-200](#d-200) (three-tier execution — amended), [D-203](#d-203) (LRU cache), [D-225](#d-225) (layer-stream proxy), [D-227](#d-227) (derive-don't-store), [D-215](#d-215) (arrangement patterns), [D-220](#d-220) (density), [D-010](#d-010) (determinism), [D-229](#d-229)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-231: DoorSpec and InteriorDescriptor — the step3→step4 boundary and the Phase-6 seed
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** The **door is the boundary descriptor** between the generated exterior (step 3) and the lazy interior (step 4, Phase 6) — *not yet an interactive object* (that is Phase 5). Each building tag carries `doors: SmallVec<[DoorSpec; 4]>` (≥ 1). `DoorSpec { facing: CardinalDirection, door_class: Main|Service|Emergency|Hidden, entry_class: BuildingEntryClass, initial_state: Open|Closed|Locked|Sealed, credential: None|TemporalWindow(hours)|Corporate(corp)|Resident(block)|Authority|Social(f32), connects_to: Street(id)|AdjacentBuilding(block)|Interstitial, interior_descriptor: InteriorDescriptor }`. **Door-count derivation:** 1 `Main` minimum; `Service` if the zone has a logistical function; `Emergency` if `floor_extent.above_ground ≥ 2`; `Hidden` seeded by zone_type (research ~60% / admin ~20% / residential ~5%) — `Hidden` carries the D-106 Rooftop-Bar discovery layer. **Initial-state derivation:** Public → Open/Closed, Commercial → `TemporalWindow(zone hours)`, Restricted → Locked + credential, BreachOnly → Sealed. **Mutation semantics (D-227):** initial state is frozen-amber derived; runtime changes (faction lockdown, a picked lock) are **tile mutators over the frozen base**, never edits to the tag — the base is always re-derivable. **`InteriorDescriptor { zone_type_id, entry_class, floor_extent, era, flavor_ref, prosperity, layout_mode }` is the complete Phase-6 seed:** a future interior generator produces a deterministic floor plan from this descriptor + `SeedChain` with **no other system queried**. The voxel at a door position is a conditionally-passable solid — solid until Phase 6 activates the interior, then void.
|
||||
- **Rationale:** Phase 6 (D-166) defers interiors, but its seed must be fixed *now* or step 3 cannot guarantee a generable interior later. Carrying the descriptor on the door (the exact threshold where generation will fire) keeps the lazy-interior contract local and self-sufficient, matching the planet-down-cascade "descriptor + catalog behind the door" rule.
|
||||
- **Implementation:** `DoorSpec`/`InteriorDescriptor` structs land in Phase 4 (planned on the tag); the door-open *fill* is Phase 6. `ChunkMutations`/`TileOverride` already exist as the mutator layer.
|
||||
- **Raised by:** Gestalt, economic-built-world workshop round 2, 2026-05-25.
|
||||
- **Cross-reference:** [D-229](#d-229), [D-097](#d-097) (guarantee audit reads connectivity), [D-106](#d-106) (rooftop clause / hidden door), [D-110](#d-110), [D-227](#d-227) (mutators), [D-142](content.md#d-142), [D-217](#d-217), [D-166](scope.md#d-166) (Phase-6 deferral)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-232: Architecture-flavor — trait-template catalog (economically gated, seed-drawn, wiki-biased)
|
||||
- **Date:** 2026-05-25 (round 3 — supersedes the round-2 per-body-Gemma-array draft of this record)
|
||||
- **Decision:** The **architecture-domain completion of D-167** (which retired the 7 abstract heritage roots and made the wiki the cultural register but never built the wiki→generator mapping). Cultural flavor is **economically founded, seed-randomized from a shared trait-template catalog, with the wiki biasing the draw for hero bodies** (Jeroen's model). This *replaces* both the round-2 per-body Gemma arrays and the rejected dimensional per-axis draw.
|
||||
- **The catalog (holistic templates, not per-axis traits).** A single shared `architecture_trait_catalog.toml` → `trait_templates` table. Each template is a **coherent bundle** — a template defines a *relationship* between its axes, so it is never decomposed: `{ tag, label, cultural_description, allow: [ObjectTag], block: [ObjectTag], zone_affinity: {DistrictType→weight}, era_scope, eligibility (two-tier, below), base_weight, visual_bundle (→ D-235) }`. Per-axis mixing is forbidden (it produces incoherent grammar, e.g. stone walls + flat roof).
|
||||
- **Two-tier eligibility — and role is per-layer, not global.** *Hard gates* (`bulk_class`, `prosperity`, `production_ubiquity`) exclude a template from the pool when failed. *Weight modifiers* (`economic_role`, `dominant_faction`, `founding_age`, `geographic_sector`, `morphology_zone`) multiply `base_weight` but never exclude. **Key principle: the same input plays different roles at different layers.** `morphology_zone` is a *hard gate* on street geometry (D-234 — a fjord cannot have radial streets) yet only a *soft weight* on cultural-template eligibility. Economics *founds and hard-gates* what a building **is** (D-233) but only *weights* how it is **characterized**. **`geographic_sector` (corridor) is always a soft weight, never a gate** — "corridors are tendencies, not borders"; a hard corridor gate collapses the pool to ~7 templates/corridor → ~100 % within-corridor collision.
|
||||
- **All eligibility numbers are integer basis-points** (`min_prosperity_bps` etc.), never `f32` — D-010 determinism, now save-critical under D-227.
|
||||
- **The draw (three phases).** (1) *Body vocabulary* (background, at skeleton time): hard-gate filter → weight (mods × wiki-bias) → `SeedChain`-seeded weighted draw of **K** templates → stored as `trait_selection: Vec<String>` on the skeleton. **K is locked to `complexity_tier`: Full = 5, Moderate = 3, Minimal = 1, Empty = 0** (Nigel's birthday math: K=4 fails within-corridor uniqueness at a 35-template catalog; K is *not* a range and *not* keyed to prosperity). The draw is **coverage-aware** — it must cover the body's actual district-type mix, not draw 5 templates that all starve the civic district. (2) *District-dominant* (at fill): each district draws **one** dominant template from the body's vocabulary, weighted by `zone_affinity`, applied **whole** to that district (Araminta's composition rule); secondaries surface on outbuildings/secondary streets and in their own high-affinity districts. The body vocabulary is **closed by default** (a mining town's civic hall looks like *that town's* civic — a feature). (3) *Within-template* variation: the fill seed picks within each template's `allow`-lists per axis.
|
||||
- **Deviation system — closed by default, rare tasty swerves.** On top of the closed vocabulary, a **rare per-building wildcard** can draw a *coherent whole* template from outside the body's vocabulary (the Shinto temple in Amsterdam — a complete foreign building, never an axis-scramble). The swerve is **cultural only**; the building's function still passes the normal economic hard gates. **Three sources, two opposed active drivers:** *foreign import* (another corridor's grammar) driven **up** by cosmopolitanism / centrality / transit / Epicenter tier; *heritage callback* (the body's own corridor **heritage pool**) driven **up** by remoteness / isolation / conservatism (the cut-off Latin world that reaches back to haciendas); and the passive *past-vogue holdover* (just old, see era below). The **sparsity escape hatch is the same mechanism** triggered by necessity rather than dice — when the closed vocabulary genuinely cannot serve a district, it reaches the full catalog.
|
||||
- **Corridor = a two-part pool.** Each corridor is authored as a **baseline** character (its default cohesive look — e.g. East-Asian: cyberpunk density + utilitarian industry) **plus a heritage sub-pool** (deep-history callbacks — East-Asian: fishing-village/temple enclaves; Iberian/Latin: haciendas, colonial-revival) tagged so the remoteness dial draws specifically from it. A **shared cross-corridor pool** feeds the foreign-import swerves.
|
||||
- **Era = maintenance/wear, NOT a material-tech ladder.** The Reach is post-space-travel throughout; there is no stone→concrete→glass progression — fashion cycles fast and arbitrarily. A building's construction era (D-229, from distance to the founding origin) reads primarily as **age/wear**, realized through the condition layer (D-217 / D-198 — maintenance modulates how much wear shows). The "different-looking old building" is just the **past-vogue holdover** — the *temporal* sibling of the spatial swerve, one deviation system with sources in *elsewhere* and *elsewhen*. (This retires the round-2 era-band material-progression and the `era_fallback`-as-tech-ladder; `era_fallback` survives only as the asset-resolution fallback chain, below.)
|
||||
- **Logical catalog vs visual-asset layer (the incremental-content split).** The **logical catalog** — tags, eligibility, `allow`/`block`, token *references* — is cheap data, **authored complete and frozen at launch**; the draw reads only this, so derivation is deterministic and stable forever **with no catalog versioning needed**. The **visual-asset layer** is a **fallback hierarchy**: every specific texture/material token declares a generic parent it degrades to (`temple_wall_wood` → generic `wood_wall` placeholder until the specific art ships, then it **upgrades in place**). Themes/textures are patched in incrementally behind stable tokens — a shipped world's *structure* is frozen at birth (the temple was always a temple), only its render *fidelity* sharpens. Versioning re-enters only if a genuinely **new logical template** is added post-launch (minimized by authoring the catalog generously up front).
|
||||
- **Wiki bias (hero bodies only, ~30–40).** A sparse `atlas_body_trait_bias` row: `pin` (mandatory, **counts toward K**), `boost` (≤ 3× weight), `suppress` (≥ 0.33×, never 0 — preserves second-playthrough surprise). Bias is **per-body, never per-corridor**. Non-hero bodies (the ~240 remainder) run the identical algorithm with no bias — economics + corridor weight + seed.
|
||||
- **CI guardrails (Nigel, build-time validation):** ≥ 5 templates eligible after hard gates per economic class; no single template > 60 % of pool weight after modifiers; bias is per-body; the catalog grows ≥ 1 template/dimension per new corridor or archetype.
|
||||
- **Catalog population:** ~25–35 templates at floor, 40–45 target. Core hand-curated (Miri authors cultural meaning + eligibility; Araminta authors the matching `visual_bundle`), then a *bounded* Gemma corpus-distillation pass (one read of all wiki, propose new templates, human-gated) — not per-body generation.
|
||||
- **Channel separation from D-233 (held):** D-233 decides **what** a building is (vocabulary, coverage); D-232 decides **how** it is characterized. Same inputs (e.g. `bulk_class`) used non-conflictingly — D-233 as hard function gate, D-232 as soft cultural weight. They compose at fill.
|
||||
- **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** the template draw now reads an authored `cultural_specialization` (new column on `system_economy`) that **selects/biases the template pool** when a system's cultural character diverges from its corridor baseline. The field carries two value sub-types in one column — *activity/character* values (`agrarian`, `industrial_heritage`, `institutional`, `scholarly`, etc.) and *heritage* values (`scottish`, `vietnamese`, `zulu`, `french_provencal`, etc.); heritage values take precedence where present. This is a Phase-4 correctness fix, not just flavor: without it, a system whose founding heritage diverges from the corridor (e.g. Vietnamese-founded Dài Lộ in the east_reach Korean/Japanese corridor) draws the wrong cultural templates. `NULL` = use the existing corridor-pool algorithm unchanged. Consistent with the held channel separation — `cultural_specialization` is a D-232 cultural-weight input, never a D-233 function gate. Singular landmarks (e.g. Groombridge's GSH within a `financial_hub` district) are expressed via D-222 multi-block reservation + an `atlas_body_trait_bias` hero pin, not the system-level field.
|
||||
- **Supersedes (architecture/generator domain only):** **D-104** (`HeritageGrammarOverlay` + per-root data → the catalog + `allow`/`block`), **D-105** (heritage-root→informal-zone lookup → flavor-filtered selection; the three zone *types* survive), **D-101 modifier axis A** (`HeritageRoot` → catalog draw; axes B/C + faction/climate/condition/season unchanged), **D-107** (per-root trauma decay → per-template/condition; the "trauma intensifies culture" principle survives). Also retires the round-2 `atlas_body_culture` / `atlas_body_culture_era` tables.
|
||||
- **Storage:** `trait_templates` (the catalog) + `atlas_body_trait_bias` (sparse, hero bodies). The per-body draw result lives as `trait_selection: Vec<String>` on the skeleton — **re-derivable** from catalog + economics + bias + `SeedChain`, no Gemma in the hot path. `CityGenerationContext` carries `trait_selection` + `morphology_zone` (replacing the round-2 `flavor_profile`; D-199 amend).
|
||||
- **Source-location deferred:** the human-authored *source* home (catalog file + `bias.json`) rides on **Q-107** (wiki → Atlas content-set consolidation). The generator-facing tables are invariant to it, so the fill seam is unblocked regardless.
|
||||
- **Implementation:** Phase 4+ (mechanism + sparse catalog); visual themes fill in across Phase 5+ and post-launch behind the fallback chain. `ObjectTag`/material vocabulary is Miri + Araminta co-maintained.
|
||||
- **Raised by:** Jeroen (the trait-template / economically-founded / wiki-biased model + the swerve, corridor-pool, era-as-maintenance, and logical/asset-split refinements), with Miri (lead synthesis), Burnelli (eligibility), Nigel (draw + variety math + guardrails), Araminta (visual bundle + composition), economic-built-world workshop rounds 2–3, 2026-05-25.
|
||||
- **Cross-reference:** [D-167](content.md#d-167) (corridor cultural system — completed here), [D-223](content.md#d-223) (Gemma naming pipeline + corridor mixing), [D-142](content.md#d-142) (zone types — the function baseline filtered by templates), [D-228](#d-228) (morphology zone), [D-233](#d-233) (economic channel — what vs how), [D-234](#d-234) (morphology→street — the hard-gate layer), [D-235](#d-235) (visual bundle + fallback hierarchy), [D-217](#d-217)/[D-198](#d-198) (condition — where era's wear lands), [D-224](#d-224) (SeedChain — the draw PRNG), [D-199](#d-199) (read-set — extended), [D-237](#d-237) (authored `cultural_specialization` — template-pool selection), [D-101](#d-101)/[D-104](content.md#d-104)/[D-105](content.md#d-105)/[D-107](content.md#d-107) (superseded in part), [Q-106](questions-architecture.md#q-106) (era reframed), [Q-107](questions-architecture.md#q-107) (source location)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-233: Economic signal → block-fill vocabulary — `BulkClass` × `ProductionUbiquity`
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** Within a block of a given district type (D-194), the building vocabulary, coverage density, and interstitial character derive from two commodity signals for the settlement's **dominant commodity**, both read from the `systems.db` commodity catalog (D-184) at build time — **seed-independent, no D-199 tier split, no rolling-economy touch:**
|
||||
- `BulkClass = BulkSolid | BulkLiquid | PrecisionDense | Perishable | NonPhysical`
|
||||
- `ProductionUbiquity = Ubiquitous | Common | Specialist | MonopolySource`
|
||||
- **Coverage:** `BulkClass` sets a roofed-coverage fraction (NonPhysical 0.85–0.95 → PrecisionDense 0.75–0.90 → Perishable 0.50–0.65 → BulkSolid 0.25–0.40 → BulkLiquid 0.20–0.35), scaled by `density_class` (D-220) and `prosperity_baseline` (D-197). For bulk industries the non-roofed remainder is **operations surface** (haul roads, ore pads, conveyor runs, tank berms) — tagged *built economic infrastructure*, **not** interstitial open space.
|
||||
- **Vocabulary:** each `(BulkClass × production_tier)` draws from a **frozen** building-vocabulary pool (e.g. `mine_head`/`conveyor_run`/`tailings_area`; `tank_farm`/`flare_stack`; `cleanroom_facility`/`qc_lab`; `field_shed`/`cold_store`; `office_tower`/`civic_hall`). A mine block places mine buildings, full stop.
|
||||
- **Concentration/dispersion rule:** `ProductionUbiquity` sets the **spatial spread** of zone-type blocks, not per-block weight — `Ubiquitous` scatters small instances through mixed-use (the settlement doesn't read as "a water town"); `MonopolySource` concentrates contiguous block groups (the mine *is* the settlement, everything else is support).
|
||||
- **Residential follow-on:** every non-residential production block emits a labor-demand signal sizing adjacent residential blocks (BulkSolid extraction ~3.0× → BulkLiquid ~2.5× → Perishable ~2.5× → PrecisionDense ~2.0× → NonPhysical ~1.5×); housing character from `prosperity_baseline` (>0.7 market-rate → 0.4–0.7 standard worker → <0.4 company barracks/squatter).
|
||||
- **Frozen-amber:** the vocabulary is t=0-derived and immutable; the condition overlay (D-198) paints Maintained/Worn/Abandoned but **cannot change a `BuildingTag`** — a mine processing plant becomes an *abandoned* mine plant, never an office.
|
||||
- **Amends D-199** (adds `dominant_bulk_class`, `dominant_production_ubiquity`, `morphology_zone` to `CityGenerationContext`) and **D-184** (back-fills the `bulk_class` enum values it named but left open). Enforcement: `fill_chunk(ctx, seed)` holds no `PressureState` reference.
|
||||
- **Refined 2026-05-26 (batch refinement):** `BulkClass`'s 5 values are **built-form archetypes** — a projection of the 8 commodity cargo-types in `commodities.toml` (`bulk`/`standard`/`compact`/`oversized` → `BulkSolid` [size is orthogonal to bulk-form; a future axis if it earns visual payoff], `precision` → `PrecisionDense`, `liquid` → `BulkLiquid`, `perishable` → `Perishable`, `non_physical` → `NonPhysical`). The 8 stay on commodities for the economic model — no fidelity is lost; the 5 are only the built-form driver. The settlement's **dominant commodity is *derived*** from its highest-output location-bound production chain (D-178), **not** an authored role→commodity table — so the built form reflects the already-authored production/trade data (where sector specialization lives) and varies body-to-body for free.
|
||||
- **Rationale:** Floor-area-to-output ratio is the capital structure of an industry, not a style choice — services maximize floor, bulk extraction is mostly open operations surface. Driving coverage and vocabulary from `BulkClass` makes settlements that share an economic *role* look physically distinct by *what they make*, and the ubiquity rule gives the settlement its identity (a MonopolySource town vs background infrastructure).
|
||||
- **Implementation:** Phase 4+. `bulk_class`/`production_ubiquity` lookups added to the economics import; consumed in the plan phase (D-230).
|
||||
- **Raised by:** Burnelli, economic-built-world workshop round 2, 2026-05-25.
|
||||
- **Cross-reference:** [D-194](#d-194) (district mix — upstream), [D-184](content.md#d-184) (commodity catalog — amended), [D-199](#d-199) (read-set — amended), [D-220](#d-220) (density), [D-197](#d-197) (prosperity), [D-142](content.md#d-142) (zone types — downstream), [D-176](#d-176), [D-180](#d-180) (EconEvent — overlay trigger), [D-198](#d-198) (condition overlay — amended), [D-229](#d-229)
|
||||
- **Dissent:** None
|
||||
- **Re-amended 2026-05-31 (D-237 — authored specialization layer):** The settlement's `dominant_bulk_class` and `dominant_production_ubiquity` are now sourced from the authored specialization layer (D-237) rather than derived directly from the highest-output production chain. Derivation chain: (1) If `system_economy.economic_specialization` is authored → resolve via `specialization_vocabulary(economic_specialization)` → `(commodity_id, production_ubiquity_override)` → `(BulkClass, ProductionUbiquity)`. Use the vocabulary-specified `ProductionUbiquity` (which may override the catalog default per the equal-or-higher rule). (2) If `economic_specialization` is NULL → run the deterministic-varied heuristic (D-237 §fallback: seed-hashed weighted draw over the vocabulary using `economic_role` + corp HQ signals + noise term) → same projection. In both cases the resolved `(BulkClass, ProductionUbiquity)` populates `CityGenerationContext.dominant_bulk_class` and `CityGenerationContext.dominant_production_ubiquity`. Frozen-amber constraint unchanged: `fill_chunk(ctx, seed)` holds no `PressureState` reference. Within-system body variation (weighted draw over body-level candidates anchored to system commodity) remains seed-hashed. **Groombridge clarification:** The vocabulary expresses district-level character. Singular MonopolySource-class landmarks within a Specialist district (e.g. GSH within Groombridge's `financial_hub` cluster) are expressed via D-222 multi-block reservation + D-232 hero-element pin, not via composite vocabulary values.
|
||||
|
||||
---
|
||||
|
||||
### D-234: Morphology zone → street and footprint constraints
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** The D-228 region **morphology zone** constrains street geometry and footprint subdivision so terrain shapes settlement form — the variety guardrail that stops the generator placing a radial-core city in a fjord. Two rules:
|
||||
- **(a) Permitted street patterns by morphology** — a lookup that gates the D-215 archetype arrangement patterns: fjord/canyon → ribbon or hub-and-spoke only (streets linear along the terrain axis, radial impossible); delta/braided → hub-and-spoke following channels (bridges as forced nodes); alluvial-plain → any pattern; island → hub-and-spoke (perimeter access priority); mountain-pass → ribbon only (elevation steps as block boundaries).
|
||||
- **(b) Waterfront footprint rule** — any block adjacent to a water morphology feature uses **pier/quay geometry** on the water-facing edge (no standard setback, dock-orthogonal subdivision, access priority toward the water) and standard street frontage on inland edges. Applies uniformly wherever a block touches a water feature — body-independent, zone-independent. This is what makes port towns present to the quay differently than to the street.
|
||||
- Street geometry obeys the ±45° pathfinding cap (D-096) — no curves; straight lines appear only where authored (the D-228 cohesion rule). Fills the Step-1 `chunk_layout` / `corridors` / `access_points` stubs.
|
||||
- **Refined 2026-05-26 (street-network algorithm — two layers):** streets generate in **two layers**, mirroring real cities and the Civ road lineage. **Arterials** (`corridors`) = a **minimum-spanning / least-cost graph** over the district's key nodes (access-points, reservations, landmarks) — emergent trunk topology (Civ6-style, "roads where traffic wants to go"), ±45°-snapped; **this is the node/edge graph the guarantee audit (D-097) reads** (a chokepoint *is* an arterial bottleneck; an encounter-corridor *is* an arterial through-route). Ribbon = MST on linearly-constrained nodes; hub-and-spoke = MST with a forced centroid hub. **Local streets** (`chunk_layout`) = a ±45° **grid lattice** within each block (Civ4-style), modulated by the D-096 Grid/Organic mode (offsets + rotation). **Voronoi was rejected** — its arbitrary-angle edges violate the ±45° cap and its irregular cells break the axis-aligned `TileRect` footprint fast-path (D-229), degrading per-chunk fill from rectangle-containment to polygon rasterization. (Civ1–4's "road on every tile" is the anti-pattern this avoids; the arterial/local split is the city-scale fix.)
|
||||
- **Rationale:** Morphology zone and `architecture_flavor` were added to the context (D-199) as *inputs without consumers* — two economically-similar bodies otherwise produce topologically identical street networks and identical block subdivision (Nigel's same-y-grid failure). These two rules give morphology structural teeth so the fjord port and the delta port are categorically different cities, not reskins.
|
||||
- **Implementation:** Phase 4+. Consumed in the plan phase (D-230) when laying streets and subdividing blocks.
|
||||
- **Raised by:** Nigel, economic-built-world workshop round 2, 2026-05-25.
|
||||
- **Cross-reference:** [D-228](#d-228) (morphology zone), [D-215](#d-215) (arrangement patterns — gated), [D-096](#d-096) (layout mode / ±45° cap), [D-220](#d-220) (density), [D-229](#d-229), [Q-106](questions-architecture.md#q-106)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-235: Building exterior visual grammar and material vocabulary
|
||||
- **Date:** 2026-05-25
|
||||
- **Decision:** The visible-form layer over the tags. `(template × zone_type × density) → BuildingExteriorTag { wall_material, roof_form, setback_tier, facade_rhythm, color_range }`, where `template` is the district's dominant trait template (D-232), derived in three steps: **(1)** the template's `allow`/`block` filters the available material + roof token set (a template is a *coherent bundle* — its axes are chosen together, never mixed across templates); **(2)** `zone_type` biases within the filtered set (probabilistic — the template can override, e.g. an `allow: [stone_base]` industrial block stays stone, not corrugated metal); **(3)** `density` sets `setback_tier` (Dense → `zero_lot` → … → Frontier → `campus`), which drives **interstitial type** (`void / court / garden / plaza / dock_slip / market_pad / open_lawn`). The fill seed then picks within each axis's allow-list. **Era is NOT a material filter here** (D-232 reframe): the Reach is post-space-travel throughout, with no stone→concrete→glass tech ladder — material choice comes from the template (fashion), and era reads as **maintenance/wear** via the condition layer (D-217), with the occasional out-of-vogue building handled as a deviation (D-232's past-vogue holdover), not a material rule.
|
||||
- **Exterior vocabulary** (extends D-228's FloorMaterial axis): `WallMaterial` (stone_cut/stone_rough/fired_brick/clay_render/heavy_timber/pile_timber/timber_frame/reinforced_concrete/corrugated_metal/steel_panel/composite_panel/precision_glass/smart_facade), `RoofForm` (pitched_steep/pitched_shallow/flat/composite_curved/corrugate_shed/dome), `FacadeRhythm` (bay_window/grid_panel/solid_punched/arcade/open_front/blind_wall), `StreetSurface` (cobble/packed_earth/poured_slab/elevated_boardwalk/dock_plank/rail_embedded — derives from district + density + template, the same filter as buildings, for a consistent world). **Color** is template-bounded (cultural palette cue), seed-selected within range — always within the template's register.
|
||||
- **Fallback hierarchy (the incremental-content mechanism, D-232).** Every specific texture/material token declares a **generic parent it degrades to**: `temple_wall_wood` → generic `wood_wall` placeholder until the specific asset ships, then it **upgrades in place**. The *logical* token a building uses is fixed at generation (deterministic, frozen); only its rendered *fidelity* sharpens as themes/textures are patched in. This is where incremental content lives — an asset-resolution concern, not a generation one — so no catalog versioning is needed. (`era_fallback` from the round-2 draft survives only in this generalized form — a fallback chain, not a tech ladder.)
|
||||
- Worked example: a fjord port (template `fjord_maritime` → stone base + steep roof + zero-lot + solid-punched + cobble) and a delta port (template `river_delta` → pile-timber + shallow roof + arcade + boardwalk) share density and zone types yet read as completely different cities.
|
||||
- **Formally retires** Araminta's generator-architecture Round-4 `D-READY-9` ten-root heritage-modifier TOML system — superseded by the D-232 trait-template catalog (body-specific draw, allow/block-bounded, no root taxonomy).
|
||||
- **Rationale:** The template is the differentiator, but it needs a concrete visual vocabulary to act on, filtered consistently across walls, roofs, facades, *and* streets or the world reads incoherent. The "old quarter vs new development" texture comes from **wear + occasional past-vogue holdover** (D-232), not from a material-technology ladder — because in a post-space-travel setting there is no such ladder. The fallback hierarchy lets the logical world be complete and frozen at launch while the art catches up over patches.
|
||||
- **Implementation:** Phase 4+ (the token logic + Atlas-level data); textured render + the bulk of the theme library are Phase 5+ and post-launch, behind the fallback chain. `ObjectTag`/material vocabulary is Miri + Araminta co-maintained.
|
||||
- **Raised by:** Araminta, economic-built-world workshop round 2, 2026-05-25.
|
||||
- **Cross-reference:** [D-228](#d-228) (composite tile / FloorMaterial — extended), [D-232](#d-232) (flavor — the filter), [D-217](#d-217) (condition / heritage markers), [D-142](content.md#d-142) (zone types), [D-220](#d-220) (density → setback), [D-106](#d-106) (height tiers)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
### D-237: Authored per-system specialisation layer — `economic_specialization` + `cultural_specialization` + `dominant_faction`
|
||||
- **Date:** 2026-05-31
|
||||
- **Decision:** Each inhabited star system carries three authored per-system identity fields, statically compiled from `wiki/economics/system_specialization.toml` at build time via `import_economics.py`. These fields constitute the lore-authored identity layer above the heuristic and are the primary source for D-233/D-232/D-214 generator inputs for named systems. All three fields are static (t=0), deterministic (D-010), and NULL-safe (NULL = heuristic fallback runs).
|
||||
|
||||
**(1) `system_economy.economic_specialization TEXT` (new column).** A 27-value curated vocabulary compiled from `wiki/economics/specialization_vocabulary.toml` into a `specialization_vocabulary` DB table. Each value maps to `(commodity_id, production_ubiquity_override_or_null)` → `(BulkClass, ProductionUbiquity)` for D-233. Scale is encoded in the value (no separate scale column): `breadbasket` pins `ProductionUbiquity = Specialist` where the catalog default would be `Ubiquitous`; `terroir_agriculture` pins `MonopolySource`. Equal-or-higher rule: a `production_ubiquity_override` may only be equal or higher concentration than the commodity's global default (CI hard error V-SES-03). Authored for ~80–100 named systems; deterministic-varied heuristic fallback for ~200 unnamed systems (seed-hashed weighted draw over the vocabulary: W_ROLE_PRIMARY=8,000 bps, W_CORP=5,000 bps, W_NOISE=400 bps to every vocabulary value; same seed+system_id always produces the same value; the noise term ensures ~5–10% of unnamed systems draw an off-primary value, preventing all unnamed agricultural worlds from being identical).
|
||||
|
||||
**Relationship to `economic_base_primary` (the seam — clarified 2026-06-01):** `system_economy.economic_base_primary`/`_secondary` is pre-existing free-text prose (e.g. `"fusion_fuel, financial_services"`) rendered into the wiki index.md "Industries/Exports" infobox by `wiki_sync.py`. `economic_specialization` is a **deliberate parallel representation at higher fidelity**, NOT a duplicate: the prose is uncomputable GTTR flavor for human readers; the enum is the machine-actionable projection the D-233 generator consumes. They are the same *concept* (what the system produces) at two resolutions. The two must stay mutually consistent — `economic_specialization` should never contradict the `economic_base_primary` prose for the same system (the #1016 content pass authors the enum from the same lore the prose describes, and CI soft-warning W-SES-03 flags prose/enum divergence). Unification onto one source was considered and rejected: the prose carries multi-sector nuance and narrative voice the enum cannot, and the enum carries the deterministic (BulkClass × ProductionUbiquity) projection the prose cannot — collapsing either direction loses information. systems.db is canonical; index.md infobox is a read-only DB projection; only the index.md *prose sections* (Supply Dependency / Faction Notes / …, round-tripped by `wiki_sync` `PROSE_SECTIONS`) are authored in-place.
|
||||
|
||||
**(2) `system_economy.cultural_specialization TEXT` (new column).** Directs D-232's template pool when a system's cultural character diverges from corridor baseline. Single field carrying two value sub-types: *activity/character values* (`agrarian`, `industrial_heritage`, `institutional`, `scholarly`, `artistic`, `financial_technocratic`, `cosmopolitan`, `compact_cooperative`, etc.) and *heritage-type values* (`scottish`, `vietnamese`, `zulu`, `afrikaans_cape`, `tagalog`, `chinese`, `italian_northern`, `french_provencal`, `norse_compact`, etc.). Heritage values take precedence when both apply. `NULL` = use corridor default (D-232 existing algorithm). Authored for ~60–100 systems where the corridor default would produce wrong D-232 architectural draws in Phase 4 (not a thin Phase 6 concern: a Vietnamese-founded system in the east_reach Korean/Japanese corridor draws the wrong templates without an explicit pin). Does NOT replace `system_culture.cultural_register` (prose NPC/dialogue voice); consumed by the physical generator only.
|
||||
|
||||
**(3) `system_factions.dominant_faction TEXT` — populated, not added.** Column already exists (D-199 field 4); VALUES for named systems are authored in this pass via `system_specialization.toml`. 8-value vocabulary: `concord_assembly | compact | compact_sympathetic | syndic_dominant | veil_institute | independent | disputed | mixed`. `lattice_commission` is dropped — the Commission is a regulatory body under Assembly authority, not a governing faction; ACB and Bastion are `concord_assembly`. Authored for ~40–60 named systems; existing Commission-presence + currency-zone derivation serves as fallback.
|
||||
|
||||
**Groombridge landmark mechanism:** Where a system's authored identity includes a singular MonopolySource-class institution within a Specialist-scale district (Groombridge: `financial_hub`/Specialist + GSH clearing house), the district character is encoded as Specialist in the vocabulary. The singular landmark is expressed via D-222 multi-block reservation + D-232 `atlas_body_trait_bias` hero pin — not via a composite vocabulary value. The vocabulary expresses district-level character; landmark-level identity is the D-232/D-222 mechanism.
|
||||
|
||||
**CI guardrails:** 6 hard errors (vocabulary FK integrity, inhabitance coverage, production_ubiquity monotonicity, vocabulary constraint for all three fields); 8 soft warnings (MonopolySource review list, distribution balance, prose/field divergence, D-172 faction/currency alignment, Compact monoeconomy flag, authoring gap lists). Full CI spec in `docs/workshops/system-economic-specialization/workshop-outcomes.md §8`.
|
||||
|
||||
**Source:** `wiki/economics/system_specialization.toml` (per-system entries) + `wiki/economics/specialization_vocabulary.toml` (vocabulary definition). Both added to `IMPORT_ECONOMICS_SOURCES` for meta-stamp tracking. `system_economy` gains two new columns via `COLUMN_MIGRATIONS` in `import_economics.py`; `specialization_vocabulary` is a new read-only reference table.
|
||||
- **Rationale:** The pre-existing D-233 heuristic (`economic_role` + corp HQ + `planet_class` → dominant commodity) fails predictably for: (1) output-vs-input confusions (Cygni B: heuristic sees ore inputs, not freight_hauler outputs); (2) political mandates (Groombridge: financial infrastructure is an institutional choice, not geographic necessity); (3) terroir-locked production (D-177 systems: catalog defaults are globally ubiquitous, not locally monopolistic); (4) service economies (Prometheus, Keid: no heuristic path from available signals to the correct identity). The three-field authored layer resolves all four failure modes. Three fields are the minimum that changes how a system reads without redundancy: economic → D-233 block fill, cultural → D-232 template selection, faction → D-214 political archetype. `cultural_specialization` is a Phase 4 correctness requirement, not Phase 6 enrichment.
|
||||
- **Raised by:** System economic-specialization workshop, 2026-05-31. Burnelli-Sheldon (schema, vocabulary, CI, fallback algorithm), Miri (must-pin list, scale gap, D-177 terroir pins, cultural heritage vocabulary), Paula (faction vocabulary, Compact rules, heritage-divergence cultural scope, D-172 compliance), human checkpoint (dual-axis design, scale-in-vocabulary, faction authored, Groombridge landmark mechanism, deterministic-varied fallback).
|
||||
- **Dissent:** None.
|
||||
- **Cross-reference:** [D-233](#d-233) (re-amended — economic block-fill now sourced from this layer), [D-184](content.md#d-184) (commodity catalog), [D-199](#d-199) (CityGenerationContext read-set — extended), [D-232](#d-232) (architecture-flavor template draw — consumes `cultural_specialization`), [D-214](#d-214) (PoliticalArchetype — consumes `dominant_faction`), [D-172](content.md#d-172) (currency zone alignment — `dominant_faction = compact` validates), [D-174](#d-174) (shadow economy intensity — `compact` elevates), [D-177](content.md#d-177) (productivity constraints — informs monopolistic pins), [D-220](#d-220) (density), [D-197](#d-197) (prosperity), [D-222](#d-222) (multi-block reservation — Groombridge landmark mechanism), [D-178](#d-178) (rolling economy — not touched; this is t=0 authored layer only)
|
||||
|
||||
---
|
||||
|
||||
*96 decisions (D-001 through D-237, excluding gaps). Last updated: 2026-05-31 (system-economic-specialization workshop — D-237 authored per-system specialization layer: `economic_specialization` + `cultural_specialization` + `dominant_faction`; D-233 re-amended to source from D-237 authored layer).*
|
||||
|
||||
@@ -283,6 +283,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
|
||||
- **Dissent:** None.
|
||||
- **Cross-reference:** D-101 (ZonePalette — heritage modifier axis A), D-105 (informal zone typology — heritage correlations), Q-049 (ObjectTag co-maintenance)
|
||||
- **Superseded by:** [D-167](#d-167-heritage-roots-superseded--corridor-cultural-system-replaces-abstract-roots) (2026-03-24). The 7 abstract heritage roots and their grammar overlays are retired. Cultural grammar is now authored per-system using the corridor framework.
|
||||
- **Superseded 2026-05-25 by D-232** (HeritageGrammarOverlay → per-body `ArchitectureFlavorEntry` + `allow`/`block` filter; never wired).
|
||||
|
||||
### D-105: Non-Urban Informal Zone Typology
|
||||
- **Date:** 2026-02-27
|
||||
@@ -293,6 +294,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
|
||||
- **Dissent:** None.
|
||||
- **Cross-reference:** D-104 (heritage grammar overlay)
|
||||
- **Superseded by:** [D-167](#d-167-heritage-roots-superseded--corridor-cultural-system-replaces-abstract-roots) (2026-03-24). Heritage root correlations (Frost/Stone/Tide etc.) are retired; informal zone typology must be re-authored against corridor cultural identities.
|
||||
- **Superseded in part 2026-05-25 by D-232** — the three informal-zone *types* survive as a typology; the heritage-root lookup is replaced by flavor-filtered zone selection.
|
||||
|
||||
### D-107: Trauma Events as EraModification Subtypes
|
||||
- **Date:** 2026-02-27
|
||||
@@ -303,6 +305,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
|
||||
- **Dissent:** None.
|
||||
- **Cross-reference:** D-100 (DamageOverlay — structural track), D-104 (heritage grammar — cultural baseline), D-109 (LocalOverlay mandate)
|
||||
- **Superseded (partial):** Heritage-root-based decay model superseded by [D-167](#d-167-heritage-roots-superseded--corridor-cultural-system-replaces-abstract-roots) (2026-03-24). Trauma-intensifies-culture principle and structural track remain valid; heritage root baseline references must be re-interpreted via the corridor cultural system.
|
||||
- **Superseded 2026-05-25 by D-232** — per-root trauma decay → per-body `trauma_decay_rate`; the 'trauma intensifies culture' principle survives.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ This domain covers: currency system, commodity taxonomy, shadow economy, corpora
|
||||
- **Raised by:** Full planning team, Sprint 32 Workshop #801.
|
||||
- **Dissent:** None.
|
||||
- **Cross-reference:** [D-173](#d-173-commodity-taxonomy) (taxonomy), [D-182](#d-182-toml-source-of-truth-for-economics-data) (TOML pipeline), [D-185](#d-185-brands-are-not-commodities) (what is excluded from this catalog)
|
||||
- **Amended 2026-05-25 (D-233):** `bulk_class` enum values enumerated — `BulkSolid | BulkLiquid | PrecisionDense | Perishable | NonPhysical`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+24
-1
@@ -64,4 +64,27 @@ How the team works: composition, naming, workflow.
|
||||
|
||||
---
|
||||
|
||||
*4 decisions. Last updated: 2026-02-11*
|
||||
### D-221: Replace sprint workflow with kanban + milestones
|
||||
- **Date:** 2026-05-03
|
||||
- **Decision:** Sprint-based workflow (38 sprints, Sprint 1–38) replaced by kanban + milestones. Sprint infrastructure deleted from the repo — git history serves as the archive. No rollback provision maintained in the codebase.
|
||||
- **What stays:** Cascade phases (macro direction), agent personalities (divergence engines), D-records, tickets (descriptive not prescriptive), workshops, rules/memory system.
|
||||
- **What changes:**
|
||||
- Sprint boundaries → milestones tied to concrete deliverables (cascade phase, feature gate)
|
||||
- Sprint briefings → live ticket queries (`ticket list --status in_progress` + dependency graph)
|
||||
- Team branches (`sprint-N/team`) → topic branches per logical work unit
|
||||
- File-scope restrictions per team → removed; any agent can touch any file
|
||||
- Version `v0.1.{sprint_number}` → `v0.{phase}.{n}` tied to cascade phases
|
||||
- Si (project manager) → Si (refinement manager) + new clerk agent (institutional guardrail)
|
||||
- `/sprint-start` → `/whats-next` (dependency-driven batch selection with parallel Si refinement review)
|
||||
- `/pr-push` → `/pr-process` (author-side review cycle: commit, push, pick up reviewer comments, dispatch fixes)
|
||||
- Milestones are many-to-many with tickets, can run in parallel, can block other milestones
|
||||
- **What dies:** Sprint CLI (`tooling/db/sprint`), sprint skills (`/sprint-start`, `/sprint-plan`, `/sprint-status`), sprint briefing files, sprint close/kickoff ceremonies, team-scoped file restrictions.
|
||||
- **Rationale:** Structural failure modes across Sprints 37–38: ticket-as-contract (agents execute the letter instead of reading the code), artificial delay (ready work parked for "next sprint"), role theater (file-scope restrictions blocking one-liner fixes), order rigidity (stale briefings), locking on bad design (ticket authority overriding code reality), self-review collapse (coding + reviewing in same context window). See Q-096 for full analysis.
|
||||
- **Test plan:** First milestone is Tier 0+1 planet-down-cascade work (6 schema tables + 6 Python importers). Measurement: did work flow or stall? Did removing file-scope restrictions cause chaos? Did tickets-as-description work?
|
||||
- **Source:** Q-096 (raised 2026-04-20, expanded 2026-05-01)
|
||||
- **Raised by:** Jeroen
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
*5 decisions. Last updated: 2026-05-03*
|
||||
|
||||
@@ -330,4 +330,93 @@ Technical foundation questions: engine, protocols, data structures, performance,
|
||||
|
||||
---
|
||||
|
||||
*46 questions (8 resolved, 1 partially resolved, 37 open). Last updated: 2026-04-05.*
|
||||
### Q-098: Persistence of generated river/city mapping outputs
|
||||
- **Status:** Resolved — D-225 (atlas layer-stream proxy), 2026-05-23
|
||||
- **Resolution:** Resolved against the question's own premise. The viewer does **not** need a durable store under the LRU: the cascade is deterministic and ~45 ms, so eviction → recompute is acceptable. Baking into `systems.db` (option a) is rejected — it bloats the install and makes modded bodies second-class. Decision: **lazy compute on demand**, served by a mod-first **layer-stream proxy** that resolves a body's source files (base + mod dirs) and streams the computed `Layer1Output` over the existing IPC bridge, backed by the D-203 in-memory LRU (miss → background `AnalyzeBody`; eviction → recompute). See [D-225](architecture.md#d-225). The mod-content-catalog corner (mods adding *new* body rows / `terrain_reference` to the binary `systems.db`) is spun off to Q-099.
|
||||
- **Question:** The deterministic cascade can recompute river courses (D8 drainage, D-208) and city placements (economic sim + attractor matching, D-211) from seed at any time, so persisting them is a *cost* optimization, not a correctness need. But the compute is expensive — recomputing per session or per atlas view is waste. How are these mapping outputs persisted so they are computed **once per body and kept**? D-203's BodyWorldState cache is an LRU — it *evicts* (volatile). The fork: **(a)** build-time bake into `systems.db` (D-200 build-time tier — precompute all, ship); **(b)** lazy compute + persist at runtime (cache DB / savegame — compute on first visit, keep); **(c)** hybrid. Whatever the answer, the in-memory LRU should sit over a *durable* store so eviction triggers a cheap reload, not a recompute.
|
||||
- **Context:** Raised 2026-05-22 looking ahead from the Phase 4 markers strip (#951). Refines D-200 (three-tier execution) and D-203 (LRU cache). Gates the Atlas layer viewer (#960), which needs persisted mapping to render without recomputing. Determinism (#952) guarantees recompute is always a valid fallback.
|
||||
- **Cross-reference:** [D-200](architecture.md#d-200), [D-203](architecture.md#d-203), [D-208](architecture.md#d-208), [D-211](architecture.md#d-211), #952 (determinism harness), #960 (atlas viewer)
|
||||
|
||||
---
|
||||
|
||||
### Q-099: Mod content catalog — body rows / terrain_reference overlay for systems.db
|
||||
- **Status:** Open — spun off from D-225 (2026-05-23)
|
||||
- **Question:** D-225 resolves mod *file* resolution (a mod body's source `heightmap.png` is found by searching mod dirs over the base install). But a mod adding a *new* body also needs that body discoverable: the `bodies` row and its `terrain_reference` live in `systems.db`, which is binary and source-canonical (D-189) — mods cannot append to it. How does a mod register new bodies (and other DB-resident catalog rows)? Options: a mod manifest the server merges into an in-memory catalog overlay at load; a parallel mod catalog DB layered over the `systems.db` reads; or a documented mod build step. Out of scope for #960 (base-install resolution ships the viewer); needed before third-party bodies are first-class.
|
||||
- **Context:** Raised 2026-05-23 from the D-225 mod-first layer-stream proxy design. The proxy makes first-party and mod bodies flow through an identical resolve→compute→stream path *given a resolvable source file*; this question is the remaining gap — getting a mod's new body into the catalog the resolver consults.
|
||||
- **Cross-reference:** [D-225](architecture.md#d-225), [D-189](architecture.md#d-189) (systems.db source-canonical), #960 (atlas viewer — base-install only for now)
|
||||
|
||||
---
|
||||
|
||||
### Q-100: Biome authority — Python sim vs Rust cascade
|
||||
- **Status:** Resolved in principle (Jeroen, 2026-05-25) — implementation detail open. The baked heightmap + reliefmap are deterministic *inputs* (gameplay never touches them), so the natural world is `f(reliefmap, heightmap, seed)` and the cascade **derives from** the baked artifacts, never independently re-derives. The biome/climate the reliefmap encodes is therefore **baked as data the Rust cascade reads** (option A below); `subbiome::classify`'s independent re-derivation is reconciled away. Open: exactly which data layers to bake + the Rust read path.
|
||||
- **Question:** Two independent biome systems exist. `planet_simulation.py` (build-time) computes biome from a Whittaker table + sim-derived temperature/moisture and colours `reliefmap.png` — the authoritative *visual*. `subbiome::classify` (Rust, runtime, D-210) re-derives `SubBiomeVariant` from proxies (elevation percentile, slope, river-distance moisture, latitude temperature). They diverge — a cold fjord coast reads `CoastalLowland` in Rust, which lacks the temperature field. Which is canonical? (A) Python canonical → bake temperature + moisture as additional atlas layers the Rust cascade reads (richer; more storage/pipeline; mod-friendly per D-225); (B) Rust canonical → re-render reliefmaps from cascade output at bake time (simpler; loses the atmospheric sim; regenerates ~267 reliefmaps); (C) keep split (pragmatic; contradicts the dual-artifact principle, D-226). Related sub-fork: the Python river-carve (colours the reliefmap) and the Rust D8 network (D-208) also diverge — the dual artifact requires reconciling them.
|
||||
- **Context:** Tyre's top integration snag. Blocks the biome axis of D-228 and the dual-artifact reconciliation. Tyre's lean: A long-term, B for Phase 4, with the expansion path designed in.
|
||||
- **Cross-reference:** [D-228](architecture.md#d-228), [D-226](architecture.md#d-226), [D-210](architecture.md#d-210), [D-208](architecture.md#d-208), [D-225](architecture.md#d-225), `tooling/planet-gen/planet_simulation.py`, `server/src/atlas/subbiome.rs`
|
||||
|
||||
---
|
||||
|
||||
### Q-101: Refinement / derivation contract — coarse hint → fine geometry + morphology algorithm family
|
||||
- **Status:** Open — the atlas-derivation workshop's primary technical output, not yet specified (2026-05-25)
|
||||
- **Question:** How does a coarse, map-scale hint (a ~78 km Layer-1 cell) plus a seed become fine, ~1 m-voxel coherent geometry — deterministically, without squaring — across ~3 scale jumps (78 km cell → ~1 km region → 64 m chunk → 1 m voxel)? Needs: the inter-layer hint+seed→geometry API (Tyre sketched a `RegionHint`); a **context-driven morphology algorithm family** selected by local context (rivers meander in lowlands / incise in mountains; coasts dune / crag / fjord by slope + lithology + glaciation), ≥6 distinct generators (meander, incised gorge, braided delta, dune strand, cliff coast, fjord, mountain pass); integer-only *structural* decisions (D-010), f64 confined to within-voxel interpolation; clean transitions between adjacent morphology families. **Unowned dependency:** the ocean/lake mask — D-223 stripped the markers.json polygons that D-209 `CoastalAccess`/`LakeShore` extraction referenced, so the mask must instead be derived from the heightmap sea-level threshold or a baked water-bodies layer. Lithology is a required input (promoted load-bearing by the volumetric subsurface, D-227). Also in scope: **body-class parameters** (hydrosphere, tectonic activity, atmosphere) must modulate river-network *density* — the effective river threshold — so an arid body and an oceanic body differ in *how many* rivers, not just where they run (today `RIVER_THRESHOLD` is a global constant, Nigel); and **river flow direction** (upstream/downstream — gates effort vs speed, and feeds the seasonal water model) derives from the D8 network at query time.
|
||||
- **Context:** The heart of the atlas→tile derivation; surfaces as the cascade builds the region → chunk → voxel layers.
|
||||
- **Cross-reference:** [D-227](architecture.md#d-227), [D-228](architecture.md#d-228), [D-208](architecture.md#d-208), [D-209](architecture.md#d-209), [D-223](architecture.md#d-223), [D-010](architecture.md#d-010), [D-222](architecture.md#d-222)
|
||||
|
||||
---
|
||||
|
||||
### Q-102: Cohesion-matrix algorithm — seam-free continuous variation
|
||||
- **Status:** Open — deferred from D-228 (2026-05-25)
|
||||
- **Question:** D-228 mandates that intra-region material / sub-biome scatter come from a **global, position-keyed continuous noise field** so chunk/region boundaries never read as grid seams, while authored straight lines (roads, plazas) stay crisp. Concretely: the noise basis (value / Perlin / simplex / worley); keying on world position (not chunk index) to stay continuous across boundaries; how material / sub-biome thresholds map onto it; integer-only / determinism (D-010); and how authored linear features composite over the continuous field without bleeding.
|
||||
- **Context:** The anti-squaring principle pushed down to the material layer.
|
||||
- **Cross-reference:** [D-228](architecture.md#d-228), [D-227](architecture.md#d-227), [D-010](architecture.md#d-010)
|
||||
|
||||
---
|
||||
|
||||
### Q-103: Tile-mutator op schema
|
||||
- **Status:** Open — deferred from D-227 (2026-05-25)
|
||||
- **Question:** D-227 makes tile mutators the sole persisted state (save = `seed + mutator log`). What does one mutator record — a full per-voxel override, or typed ops (`Dig`, `Build(FloorMaterial)`, `Place(object)`, `Destroy(object)`, `SetMaterial`)? Typed ops are compact, semantic, replayable, but need a fixed op vocabulary; full overrides are simple but heavier and lose intent. Also: replay ordering / conflict semantics, and confirmation that a position-keyed mutator stays valid across re-derivation (determinism guarantees an identical base — to be pinned).
|
||||
- **Context:** Needed when the save system is built (Phase 5+).
|
||||
- **Cross-reference:** [D-227](architecture.md#d-227), [D-010](architecture.md#d-010)
|
||||
|
||||
---
|
||||
|
||||
### Q-104: Floor-index ↔ absolute voxel-z coordinate mapping
|
||||
- **Status:** Resolved 2026-05-25 by D-229
|
||||
- **Resolution:** Resolved via `FloorExtent { base_floor, floor_count, FloorHeightProfile::{Uniform|Variable} }` + the pure functions `floor_at_voxel_z` / `voxel_range_for_floor`.
|
||||
- **Question:** D-227 introduces an absolute physical z coordinate (metres, continuous over the voxel grid) alongside D-110's floor-*index* addressing (`base_z: i8`, a story-counting integer). The two coexist but the bridge is undefined: "the floor at `base_z = -2`" → which absolute voxel-z values does it span? With variable-height floors (D-227 — default 3 voxels, a cathedral ~10), the mapping is **non-linear** (a floor's voxel-z depends on the heights of the floors below it). What is the mapping function, and where is it owned (a per-building floor table? a region floor-height index?)? Needed before anything queries both systems (Phase 5 building interiors / vertical movement). Gap, not contradiction — D-227 and D-110 do not conflict.
|
||||
- **Context:** Raised in the atlas-derivation workshop (Tyre, Round 2 review of D-227).
|
||||
- **Cross-reference:** [D-227](architecture.md#d-227), [D-110](architecture.md#d-110), [D-049](architecture.md#d-049), [D-222](architecture.md#d-222)
|
||||
|
||||
---
|
||||
|
||||
### Q-105: Region seasonal/clock state — the shared cheap-dynamism source
|
||||
- **Status:** Open — committed in principle (D-228); model details open (2026-05-25)
|
||||
- **Question:** D-228 commits to a deliberately cheap, deterministic, **region-level seasonal/clock state** — computed *once* per region per phase, inherited by its tiles — from which a family of **transient surface conditions** derive — all computed from the region's seasonal + tidal + weather phase, none of it stored: **flooded tiles** (floodplain / tidal-flat / seasonal-river, where the water-height crosses local elevation), **snow & ice cover**, **puddles** (weather / rain), **weather** generally, and the **farmland crop cycle** (sown → growing → ripe → harvested → fallow). The model must stay "nearly free" — simple deterministic functions of the in-game clock + body/region parameters, no per-tile or per-frame simulation. Open: the exact phase functions — a **seasonal** term phased by hemisphere (latitude sign), driven by the year clock; a **tidal** term only when the body has a moon (no satellite → no tide), driven by the lunar/day clock with amplitude from the satellite config; a **weather** term (shorter-term precipitation → puddles + general conditions); how snow/ice depth, puddles, flood extent, weather, and the crop cycle each read the same state; the **snow/ice cover model** — D-228 removed `Snow`/`Ice` from `TerrainMaterial`, so snow/ice are *entirely* this seasonal cover overlay (permanent only where climate never melts them — poles, glaciers), depth rising/falling with the seasonal phase; determinism (state = `f(clock, region)` → reproducible) and its interaction with the cache + the D-226 pause/inspection (a frozen phase for stable inspection); and recompute cadence (per phase-change, not per frame).
|
||||
- **Context:** Grew from the floodplain dependency — "floodplain is only feasible if water heights move" — into a general cheap-dynamism source. Jeroen's constraints: clock + hemisphere bound, moon-gated tides, computed once per region, simple rules; snow / weather / crop cycle ride the same calculation.
|
||||
- **Cross-reference:** [D-228](architecture.md#d-228), [D-227](architecture.md#d-227), [D-226](architecture.md#d-226) (dynamic-state inspection), [D-010](architecture.md#d-010)
|
||||
|
||||
---
|
||||
|
||||
### Q-106: Era-band stacking depth for layered architecture-flavor
|
||||
- **Status:** Resolved 2026-05-25 by D-232 (round 3) — the question is dissolved, not answered.
|
||||
- **Question (original):** D-232 (round-2 draft) made flavor a three-layer resolution (body × district × era-band). How many era-bands does a settlement produce as a function of `founding_age_years`? What is the minimum age-gap between bands? How do the flavor slices differ between bands?
|
||||
- **Resolution:** The round-3 rewrite of D-232 **retired the era-band material-progression entirely.** The Reach is post-space-travel throughout — there is no stone→concrete→glass tech ladder, so era is *not* a flavor-stacking axis. A block's construction era (still derived from distance-to-founding-origin, D-229) reads as **maintenance/wear** through the condition layer (D-217/D-198); the occasional out-of-vogue building is handled by D-232's **deviation system** (the "past-vogue holdover" — temporal sibling of the spatial swerve), not by multi-band flavor stacking. So there are no era-bands to count.
|
||||
- **Cross-reference:** [D-232](architecture.md#d-232), [D-229](architecture.md#d-229), [D-217](architecture.md#d-217)/[D-198](architecture.md#d-198) (condition — where era's wear lands)
|
||||
|
||||
---
|
||||
|
||||
### Q-107: Wiki → Atlas content-set consolidation
|
||||
- **Status:** Open — raised by Jeroen (2026-05-25)
|
||||
- **Question:** The wiki pages live semi-outside game scope yet are piped into Atlas content screens AND are now a generator input (D-232 architecture-flavor distilled from wiki prose). Where authored per-body content lives (frontmatter vs companion file vs a consolidated tree) should follow a consolidation of ALL wiki content — pages + heightmaps + markers + architecture-flavor — into one first-class, baked Atlas content set (the "move it into a `gamedata/` subtree" instinct), not a per-feature hack. The generator-facing tables (`trait_templates` + `atlas_body_trait_bias`, D-232) are invariant to the source location, so this does not block the fill seam.
|
||||
- **Cross-reference:** [D-232](architecture.md#d-232), [D-223](architecture.md#d-223), [D-191](architecture.md#d-191) (Atlas/settlement), and the development cascade (Phase 1 wiki + Phase 3 Atlas)
|
||||
|
||||
---
|
||||
|
||||
### Q-108: Subterranean / domed / sealed-habitat settlement morphology — does the built-world fill model need a surface-vs-enclosed branch
|
||||
- **Status:** Open — raised 2026-05-31 (system-economic-specialization workshop follow-up, via Vuurkloof / GJ 35)
|
||||
- **Question:** The economic-built-world fill model (D-233, re-amended by [D-237](architecture.md#d-237)) is implicitly a **surface** model — it derives a roofed-coverage fraction, an operations-surface remainder, and a concentrate-vs-scatter spread across open ground. But `bodies.settlement_pattern` already carries non-surface morphologies for real bodies: `underground_concentrated` (e.g. Vuurkloof / GJ35c, built into ravine walls), `cave`, `domed`, `underground_complex` (~12+ bodies on `underground_concentrated` alone, more across the others). **Nothing in the current chain reads `settlement_pattern`** — it is not in the [D-199](architecture.md#d-199) read-set, not consumed by `city_context_reader`, not branched on in `skeleton_gen` / D-233 fill. The engine *primitives* exist ([D-110](architecture.md#d-110) signed `base_z`, the `UndergroundComplex` reservation, [D-106](architecture.md#d-106) vertical scale — "a deep mine is an inverted skyscraper"), but no morphology switch connects `settlement_pattern` to them at the built-world layer. So an enclosed-habitat body like Vuurkloof would currently generate as a *surface* geothermal town (conduits and exchange stations spread across open ground), contradicting authored lore (underground-concentrated, ravine-wall construction). Does the fill model need a first-class **surface-vs-enclosed (subterranean / domed / sealed-habitat) morphology branch**, and where does it live — (a) `settlement_pattern` as a hard-gate input to D-233/D-237 fill selecting an enclosed coverage/vocabulary model; (b) a separate morphology layer above D-233 the economic vocabulary plugs into; (c) treat domed/sealed (pressurized surface envelope) as distinct from true subterranean (excavated z-negative)? And how does verticality (D-106/D-110) compose with the coverage model when a settlement is primarily vertical/subsurface, and how does `morphology_zone` (D-228/D-234, terrain-driven street geometry) behave when there is no open street plane?
|
||||
- **Context:** D-237 *inherited* D-233's surface assumption; it did not introduce the gap and does not block on it (surface bodies — the majority — are correct today). But enclosed-habitat bodies are authored lore and will read wrong until resolved. Wants its own debate/workshop with Tyre (z-level architecture), Burnelli (coverage model), Miri (which bodies, what they must read as), Araminta (how enclosed interiors are visually distinct). Tracked by ticket #1018; schedule before the Phase 4 content pass authors built form for enclosed-habitat bodies.
|
||||
- **Cross-reference:** [D-233](architecture.md#d-233) (surface coverage model — would be amended), [D-237](architecture.md#d-237) (authored specialization layer — inherits the surface assumption), [D-199](architecture.md#d-199) (read-set — would need `settlement_pattern`), [D-220](architecture.md#d-220) (density / vertical pressure), [D-106](architecture.md#d-106) (vertical scale), [D-110](architecture.md#d-110) (signed z-levels), [D-228](architecture.md#d-228)/[D-234](architecture.md#d-234) (morphology zone / street geometry), [D-196](architecture.md#d-196) (SettlementClass — orthogonal; this is morphology, not active/ghost)
|
||||
|
||||
---
|
||||
|
||||
*57 questions (11 resolved, 1 partially resolved, 45 open). Last updated: 2026-05-31.*
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# Open Questions — Process
|
||||
|
||||
How the team works: workflow mechanics, cadence, ceremony, release rhythm.
|
||||
|
||||
---
|
||||
|
||||
### Q-096: Replace sprint workflow with kanban + milestones
|
||||
|
||||
- **Status:** Resolved — see D-221
|
||||
- **Date raised:** 2026-04-20
|
||||
- **Last updated:** 2026-05-01 (expanded from "should we?" to concrete proposal with test plan)
|
||||
- **Raised by:** Jeroen
|
||||
- **Assigned to:** Jeroen (final call)
|
||||
|
||||
---
|
||||
|
||||
#### Problem Statement
|
||||
|
||||
Sprints were introduced when the workflow was framed as Scrum-adjacent. Across 38 sprints, the failure modes have sharpened beyond the original "ceremony overhead" concern into structural problems with how agents interpret sprint-shaped work.
|
||||
|
||||
**Original concerns (Sprint 36):**
|
||||
|
||||
| Mechanism | Compensates for | Applies here? |
|
||||
|---|---|---|
|
||||
| Time-box (1-2 weeks) | Humans need deadline pressure to scope | No — Claude scopes on demand |
|
||||
| Retro | Team learning ritual, morale | No — adjustments land as rule/memory edits immediately |
|
||||
| Burndown / velocity | Estimation & planning for multi-person roadmaps | No — solo dev, no external stakeholders |
|
||||
| Demoable increments | External stakeholder cadence | No external stakeholders |
|
||||
| Sprint-kickoff ceremony | Re-energize after context loss | Context loss is per-session, not per-sprint |
|
||||
| Version bump at sprint close | Predictable shippable cadence | `v0.1.{sprint_number}` doesn't reflect feature progress |
|
||||
|
||||
**New failure modes observed (Sprint 37-38):**
|
||||
|
||||
1. **Ticket-as-contract.** Agents execute the letter of the ticket instead of reading the code and doing what's actually right. If the ticket says "implement X using approach Y," the agent implements Y even when the codebase clearly suggests Z. The ticket becomes a shield against thinking.
|
||||
|
||||
2. **Artificial delay.** Work that's ready gets parked because it's "next sprint." Work that's blocked gets thrashed because it's "this sprint." The sprint boundary creates urgency and delay simultaneously.
|
||||
|
||||
3. **Role theater.** Agents treat team-scope restrictions (server team can't touch client files) as hard boundaries when the fix is a one-liner. This models a human org chart, not the reality of one person and a context window.
|
||||
|
||||
4. **Order rigidity.** Agents unwilling to reorder work when dependencies shift, because the briefing prescribed a sequence. Sprint briefings go stale instantly (Sprint 38 server briefing was missing 26 tickets within a day of creation).
|
||||
|
||||
5. **Locking on bad design.** Agents commit to approaches described in tickets even when implementation reveals the design was wrong. The ticket's authority overrides what the code is telling them.
|
||||
|
||||
6. **Self-review collapse.** Coding agents write code, create the PR, review their own PR, approve it, and push — all in one convergent context. The adversarial review pipeline collapses because the reviewer has all the author's context and biases. PR comments aren't posted, review gates are skipped, the entire point of separate reviewer personalities is defeated. Requires constant human intervention to enforce.
|
||||
|
||||
**Compounding factors (May 2026):**
|
||||
|
||||
- Claude Code team-mode harness partially broken (v2.1.123: custom agent types missing SendMessage, shutdown doesn't work, TeamDelete doesn't kill panes). Workaround exists but fragile.
|
||||
- Opus 4.7 prompt quality ("floatiness") makes the problem worse — agents are already less precise, and rigid ticket framing doesn't compensate, it amplifies.
|
||||
|
||||
---
|
||||
|
||||
#### Proposal: Kanban + Milestones + Adversarial Personalities
|
||||
|
||||
**What stays:**
|
||||
|
||||
| Element | Why it stays |
|
||||
|---------|-------------|
|
||||
| Cascade phases | Macro direction — what to build in what order |
|
||||
| Agent personalities + briefings | Adversarial diversity — the real purpose is breaking convergence patterns by seeding different thinking via personality prompts. Not role-play, divergence engines. |
|
||||
| D-records | Decisions are decisions regardless of process |
|
||||
| Tickets | Track what needs doing — but descriptive, not prescriptive |
|
||||
| Workshops | The format where adversarial diversity pays off. Unchanged. |
|
||||
| Rules/memory | Live institutional knowledge, immediate course correction |
|
||||
|
||||
**What changes:**
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| Sprint numbers, sprint boundaries | Milestones tied to concrete deliverables (cascade phase, feature gate) |
|
||||
| Sprint briefings (pre-baked, go stale) | Session startup: `ticket list --status in_progress` + blocked-by graph |
|
||||
| Team branches (sprint-N/server) | Topic branches per logical chunk, any agent can touch any file |
|
||||
| File-scope restrictions per agent | Removed — personalities guide perspective, not file access |
|
||||
| Author and reviewer in same session | Worktree isolation enforced: reviewer spawns in separate worktree, reads diff cold |
|
||||
| Version `v0.1.{sprint_number}` | `v0.{phase}.{n}` tied to cascade phases |
|
||||
| Sprint-close cleanup sweep | Pre-push clerk hook (automated) + milestone-boundary audit (manual) |
|
||||
| Si (project manager) + Qatux (librarian) as separate agents | Merged into "clerk" agent — institutional guardrail |
|
||||
| Sprint velocity tracking | WIP limits per work-type (max N tickets in_progress) |
|
||||
|
||||
**What dies:**
|
||||
|
||||
- Sprint CLI (`tooling/db/sprint`)
|
||||
- Sprint briefing files (`docs/sprints/sprint-N/`)
|
||||
- Sprint-kickoff and sprint-close ceremonies
|
||||
- The Si agent as a standalone personality
|
||||
- Team-scoped file restrictions in CLAUDE.md
|
||||
|
||||
**Personalities are not teams.** Gestalt, Tyre, Paula, Burnelli-Sheldon, etc. survive as prompt-seeded divergence engines. They make pushback legible — when Jeroen sees a Burnelli-Sheldon objection he knows it's an economics concern, when he sees a Paula objection he knows it's a narrative/political concern. The personality is the signature, not a job description.
|
||||
|
||||
**Worktrees are context firewalls, not team boundaries.** Branches and worktrees survive — but their purpose changes from "team scope" to "context isolation." The reason a coding agent self-reviews its own PR is that it has all the context of writing the code. A reviewer spawned in a separate worktree physically cannot have that context — it reads the diff cold, which is exactly what a reviewer should do. The worktree enforces adversariality at the mechanism level, not the process level. This is an argument FOR keeping branch/worktree separation even after killing team-scoped restrictions.
|
||||
|
||||
---
|
||||
|
||||
#### Clerk Agent (Si + Qatux merge)
|
||||
|
||||
The team lead (interactive Claude session) optimizes for the user's current intent. This makes it structurally incapable of being the institutional guardrail — it will go along with whatever the user says in the moment, even if it contradicts a decision from three weeks ago.
|
||||
|
||||
The clerk is the separation of concerns between **responsiveness** (team lead) and **institutional memory** (clerk).
|
||||
|
||||
**Pre-push hook implementation:**
|
||||
|
||||
1. Pre-push hook spawns clerk agent with the diff
|
||||
2. Agent checks D-record consistency, ticket drift, contradiction with active decisions
|
||||
3. Agent writes findings to `.cache/pre-push-review.md`
|
||||
4. Agent outputs exactly one word: `APPROVED` or `REJECTED`
|
||||
5. Hook reads the word, exits 0 or 1
|
||||
6. **Timeout behavior:** if agent doesn't respond within N seconds, automatic `REJECTED`. Agent keeps running in background — findings file arrives later. User reads it, decides whether to fix or force-push.
|
||||
|
||||
The findings file separates the agent's reasoning from its authority. Binary gate, verbose reasoning.
|
||||
|
||||
**What the clerk checks:**
|
||||
- Changed files don't contradict active D-records
|
||||
- Referenced decisions still exist
|
||||
- Implementation doesn't drift from ticket intent (but ticket is descriptive, not prescriptive — drift is flagged, not blocked)
|
||||
- Q-records relevant to changed files are surfaced
|
||||
|
||||
**What the clerk does NOT do:**
|
||||
- Subjective code quality review (that's /pr-review)
|
||||
- Sprint management (no sprints)
|
||||
- Briefing maintenance (briefings replaced by live ticket queries)
|
||||
|
||||
---
|
||||
|
||||
#### Test Run Plan
|
||||
|
||||
**Scope:** The Tier 0/Tier 1 cascade work — all schema tables and Python importers from the planet-down-cascade workshop.
|
||||
|
||||
**Milestone definition:** "All schema tables and Python importers land on main. `make regen-db` produces a DB with heightmaps, city names, province boundaries, body_radius_km, normalized economic_role."
|
||||
|
||||
**Rules for the test run:**
|
||||
- No sprint number assigned
|
||||
- Topic branches per logical chunk (not team branches)
|
||||
- Any agent personality can touch any file
|
||||
- Tickets describe what needs doing, not how
|
||||
- WIP limit: 3 tickets in_progress at once
|
||||
- Milestone complete when all Tier 0 + Tier 1 tickets are done and verified
|
||||
|
||||
**What we're measuring:**
|
||||
1. Did work flow or stall without sprint pressure?
|
||||
2. Did removing file-scope restrictions cause chaos or remove friction?
|
||||
3. Did tickets-as-description work, or did agents flail without prescriptive tickets?
|
||||
4. Did anything get worse compared to Sprint 38's current state?
|
||||
|
||||
**Rollback trigger:** If after the test run the work feels less productive or less controlled than a sprint, Sprint 39 picks up where 38 left off. All sprint tooling remains in the codebase — dead code, not deleted code.
|
||||
|
||||
**Parallel work (independent of test run outcome):**
|
||||
- Build the clerk pre-push hook — pure additive, helps regardless of process
|
||||
- Can be tested against Sprint 38 pushes before the process switch
|
||||
|
||||
---
|
||||
|
||||
#### Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| No natural "stop and audit" moment | Clerk pre-push hook + milestone-boundary manual audit |
|
||||
| Work sprawl without sprint focus | WIP limits enforce focus; milestones enforce shipping |
|
||||
| Losing the "signature" on who did what | Personalities preserved — commits and PRs still carry agent identity |
|
||||
| Process change is a distraction from actual development | Test run uses real cascade work, not meta-work. Process change is the test. |
|
||||
| Can't roll back cleanly | Sprint tooling stays in repo. Reversible. |
|
||||
| Clerk agent too slow / too noisy | Timeout-reject for speed. Tune signal-to-noise over first milestone. |
|
||||
|
||||
---
|
||||
|
||||
#### Decision Timeline
|
||||
|
||||
1. **Now:** Sprint 38 continues as-is
|
||||
2. **Sprint 38 close:** Decision point — run test milestone or Sprint 39
|
||||
3. **During test milestone:** Build clerk pre-push hook in parallel
|
||||
4. **Test milestone complete:** Evaluate against measurement criteria
|
||||
5. **If positive:** Write D-record confirming kanban + milestones. Kill sprint tooling.
|
||||
6. **If negative:** Sprint 39. Revisit after more Claude Code harness fixes.
|
||||
|
||||
---
|
||||
|
||||
**Related:**
|
||||
- `feedback_broken_tests_not_preexisting_shield.md` — pattern sprints may enable
|
||||
- tmux teammate mode: confirmed working on Claude Code 2.1.148 (the earlier "broken" status was a `teammateMode: in-process` config issue, not a regression)
|
||||
- Epic #854 — pipeline discipline automation
|
||||
- D-166 — cascade phases
|
||||
|
||||
---
|
||||
|
||||
*Open count: 0.*
|
||||
+15
-1
@@ -371,4 +371,18 @@ What we're building: game concept, design pillars, prototype definition, map spe
|
||||
|
||||
---
|
||||
|
||||
*34 decisions (25 active, 9 superseded). Last updated: 2026-03-19 (D-165 added — character creator color palette)*
|
||||
### D-236: Sol / GJ-0 — player-inaccessible, Atlas highest-level only, deeper Sol is future DLC
|
||||
- **Date:** 2026-05-26
|
||||
- **Decision:** Sol (system `GJ-0`) is permanently **out of player scope.** Three concrete consequences:
|
||||
- **(a) No body-level generation.** All Phase-4+ generation pipelines (founding_age backfill, body skeleton generation, FillChunk, etc.) EXCLUDE Sol bodies entirely. `system_history.settlement_wave = 'origin'` is the gate flag — GJ-0's wiki frontmatter already documents this as a placeholder that flags the system as exceptional, predating the wave taxonomy.
|
||||
- **(b) Atlas highest-level only.** The Atlas renders Sol at its system-level pregenerated map and **nothing more** — no zoom-in, no per-body click-through, no in-world rendering of Sol bodies. The Sol map is a leaf node in the Atlas hierarchy.
|
||||
- **(c) Deeper Sol is a future DLC.** Any treatment that makes Sol *feel like actual Sol* (real Earth/Mars/Luna geography, real history, real political textures) is reserved for a separate, future DLC with its own scope. Explicitly NOT current work.
|
||||
- **Rationale:** Sol carries enormous authorial expectation (real Earth/Mars/Luna geography, real human history). Fudging it would land worse than leaving it sealed. Keeping Sol out of scope keeps the Reach the focus and the realism debt manageable; reserving Sol for a hypothetical DLC keeps the door open without committing to the (massive) job now.
|
||||
- **Implementation:** generation pipelines filter out bodies whose system has `settlement_wave = 'origin'`; the Atlas zoom UI gates Sol at the system map (no body-level navigation panes). Effective immediately on the Phase-4 fill-seam cascade (#1000 founding_age backfill explicitly excludes Sol).
|
||||
- **Raised by:** Jeroen, 2026-05-26 — formalising what was previously implicit in D-223's naming exemption and GJ-0's wiki frontmatter into a single canonical scope rule.
|
||||
- **Cross-reference:** [D-145](#d-145-base-building-dlc--gj-902-unclaimed-moon-as-player-settlement-site) (DLC-scoping precedent), [D-191](architecture.md#d-191-atlas-of-the-reach--phase-3-scope-and-pipeline) (Atlas Phase-3 — this constrains Sol's Atlas treatment), [D-223](architecture.md#d-223) (Sol naming exemption + offline `sol_import.py` geometry — this generalises that scope), [D-171](economics.md#d-171-three-currency-system) / [D-174](economics.md#d-174-shadow-economy-layer) (Sol-as-shadow-currency — orthogonal, unaffected), GJ-0 wiki frontmatter (`settlement_wave: origin`, `political_zone: earth_sphere`).
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
*35 decisions (26 active, 9 superseded). Last updated: 2026-05-26 (D-236 added — Sol player-access scope)*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"system_id": "GJ 1245B",
|
||||
"proper_name": "Koeberg",
|
||||
"proper_name": "Akhgar",
|
||||
"star_type": "unusual",
|
||||
"spectral_class": "m",
|
||||
"wiki_data": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"bodies": [
|
||||
{
|
||||
"body_id": "GJ1245Bb",
|
||||
"proper_name": "Glimkop",
|
||||
"proper_name": "Kavir",
|
||||
"body_type": "planet",
|
||||
"orbit_index": 1,
|
||||
"parent_body_id": null,
|
||||
@@ -176,14 +176,14 @@
|
||||
"stations": [
|
||||
{
|
||||
"station_id": "GJ1245B-oort-S1",
|
||||
"proper_name": "Koeberg Horizon",
|
||||
"proper_name": "Akhgar Horizon",
|
||||
"orbits_body_id": "GJ1245B-oort",
|
||||
"station_type": "horizon",
|
||||
"population": 11000,
|
||||
"economic_role": "transit",
|
||||
"docking_class": "major",
|
||||
"has_gate_infrastructure": true,
|
||||
"notes": "horizon station — 1 aperture; dead_end; connects GJ 54; primary habitation for the system; wave_4 Afrikaner/Cape Malay heritage; ward council governance (seven wards, rotating chair); hydroponic food production; niche textile production; Founding Family Archive (genealogical and cultural records); Assembly oversight via scheduled visits from GJ 54"
|
||||
"notes": "horizon station — 1 aperture; dead_end; connects GJ 54; primary habitation for the system; wave_4 Persian heritage; ward council governance (seven wards, rotating chair); hydroponic food production; niche textile production; Founding Family Archive (genealogical and cultural records); Assembly oversight via scheduled visits from GJ 54"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"system_id": "GJ 508A",
|
||||
"proper_name": "Stilwater",
|
||||
"proper_name": "Sawad",
|
||||
"star_type": "M",
|
||||
"spectral_class": "M2V",
|
||||
"wiki_data": {
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"body_id": "GJ508Ac",
|
||||
"proper_name": "Uithoek",
|
||||
"proper_name": "Rusafa",
|
||||
"body_type": "planet",
|
||||
"orbit_index": 2,
|
||||
"parent_body_id": null,
|
||||
@@ -50,7 +50,7 @@
|
||||
"economic_role": "agricultural",
|
||||
"settlement_pattern": "dispersed",
|
||||
"industrial_corridor": "agricultural_export",
|
||||
"notes": "primary inhabited world; wave_4 Afrikaans heritage ('far corner'); 2-aperture through_route; M2V quiet star; unremarkable and deliberately so; plentiful land, infrequent neighbors; community council governance; Landbou Raad cooperative network (informal distribution agreements); Commission charter filing office (two-day medical station); agricultural consumption with modest surplus; Breëvlei visible as glow on clear nights"
|
||||
"notes": "primary inhabited world; wave_4 Arab heritage ('far corner'); 2-aperture through_route; M2V quiet star; unremarkable and deliberately so; plentiful land, infrequent neighbors; community council governance; Landbou Raad cooperative network (informal distribution agreements); Commission charter filing office (two-day medical station); agricultural consumption with modest surplus; Breëvlei visible as glow on clear nights"
|
||||
},
|
||||
{
|
||||
"body_id": "GJ508A-belt",
|
||||
@@ -176,7 +176,7 @@
|
||||
"stations": [
|
||||
{
|
||||
"station_id": "GJ508A-oort-S1",
|
||||
"proper_name": "Stilwater Horizon",
|
||||
"proper_name": "Sawad Horizon",
|
||||
"orbits_body_id": "GJ508A-oort",
|
||||
"station_type": "horizon",
|
||||
"population": 140,
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"economic_role": "agricultural",
|
||||
"settlement_pattern": "dispersed",
|
||||
"industrial_corridor": "agricultural_export",
|
||||
"notes": "primary inhabited world; wave_2 Afrikaans heritage ('old farm'); 2-aperture through_route; M0V dim star; oldest continuously worked agricultural settlement in deep_frontier (four centuries); family-representative council governance (unchanged for two hundred years); courteous indifference to Reach institutions; food surplus traded through Cairn pocket chain to Skuilplek; Guardians of Autonomy informal community; agricultural tempo — planning in decades"
|
||||
"notes": "primary inhabited world; wave_2 Afrikaans heritage ('old farm'); 2-aperture through_route; M0V dim star; oldest continuously worked agricultural settlement in deep_frontier (four centuries); family-representative council governance (unchanged for two hundred years); courteous indifference to Reach institutions; food surplus traded through Cairn pocket chain to Sığınak; Guardians of Autonomy informal community; agricultural tempo — planning in decades"
|
||||
},
|
||||
{
|
||||
"body_id": "GJ528A-belt",
|
||||
@@ -183,7 +183,7 @@
|
||||
"economic_role": "transit",
|
||||
"docking_class": "major",
|
||||
"has_gate_infrastructure": true,
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Skuilplek and outer Cairn pocket; agricultural freight staging; Commission liaison transit point (infrequent, courteous hospitality)"
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Sığınak and outer Cairn pocket; agricultural freight staging; Commission liaison transit point (infrequent, courteous hospitality)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
"economic_role": "transit",
|
||||
"docking_class": "major",
|
||||
"has_gate_infrastructure": true,
|
||||
"notes": "horizon station — 3 apertures; junction; connects Crux Station, Koeberg, and outer corridor; grain freight staging; Grão Freight Cooperative transit coordination"
|
||||
"notes": "horizon station — 3 apertures; junction; connects Crux Station, Akhgar, and outer corridor; grain freight staging; Grão Freight Cooperative transit coordination"
|
||||
},
|
||||
{
|
||||
"station_id": "GJ54c-S1",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
"economic_role": "transit",
|
||||
"docking_class": "major",
|
||||
"has_gate_infrastructure": true,
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Waterkant and Stilwater; Cairn pocket chain relay; Commission nominal authority"
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Waterkant and Sawad; Cairn pocket chain relay; Commission nominal authority"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"system_id": "GJ 680",
|
||||
"proper_name": "Carnarvon",
|
||||
"proper_name": "Golestan",
|
||||
"star_type": "K",
|
||||
"spectral_class": "K5",
|
||||
"wiki_data": {"habitable_count": 2, "inhabited_count": 2, "has_gas_giant": true, "has_asteroid_belt": true, "has_horizon_station": true, "raw_bodies_line": null},
|
||||
"bodies": [
|
||||
{"body_id": "GJ680b", "proper_name": null, "body_type": "planet", "orbit_index": 1, "parent_body_id": null, "inhabited": false, "population": 0, "mass_class": "terrestrial", "surface_gravity": 0.32, "orbital_period_days": 20, "rotation_period_hours": 480, "atmosphere": "none", "planet_class": "barren", "hydrosphere": "none", "economic_role": null, "settlement_pattern": null, "industrial_corridor": null, "notes": "inner rocky, tidally locked"},
|
||||
{"body_id": "GJ680c", "proper_name": null, "body_type": "planet", "orbit_index": 2, "parent_body_id": null, "inhabited": false, "population": 0, "mass_class": "terrestrial", "surface_gravity": 0.48, "orbital_period_days": 55, "rotation_period_hours": 1320, "atmosphere": "thin", "planet_class": "arid", "hydrosphere": "none", "economic_role": null, "settlement_pattern": null, "industrial_corridor": null, "notes": "rocky, tidally locked"},
|
||||
{"body_id": "GJ680d", "proper_name": "Tulbagh", "body_type": "planet", "orbit_index": 3, "parent_body_id": null, "inhabited": true, "population": 150000000, "mass_class": "terrestrial", "surface_gravity": 0.90, "orbital_period_days": 130, "rotation_period_hours": 22.0, "atmosphere": "breathable", "planet_class": "temperate", "hydrosphere": "moderate", "economic_role": "agriculture", "settlement_pattern": "dispersed", "industrial_corridor": null, "notes": "primary inhabited world, K5 warm steady light, mixed cereal and legume agriculture, Mostert family four of seven agricultural regions, Wave 2 Cape-heritage founders, council of agricultural cooperatives"},
|
||||
{"body_id": "GJ680d", "proper_name": "Pardis", "body_type": "planet", "orbit_index": 3, "parent_body_id": null, "inhabited": true, "population": 150000000, "mass_class": "terrestrial", "surface_gravity": 0.90, "orbital_period_days": 130, "rotation_period_hours": 22.0, "atmosphere": "breathable", "planet_class": "temperate", "hydrosphere": "moderate", "economic_role": "agriculture", "settlement_pattern": "dispersed", "industrial_corridor": null, "notes": "primary inhabited world, K5 warm steady light, mixed cereal and legume agriculture, Farahani family four of seven agricultural regions, Wave 2 Persian-heritage founders, council of agricultural cooperatives"},
|
||||
{"body_id": "GJ680d-1", "proper_name": null, "body_type": "moon", "orbit_index": 1, "parent_body_id": "GJ680d", "inhabited": false, "population": 0, "mass_class": "dwarf", "surface_gravity": 0.05, "orbital_period_days": 8, "rotation_period_hours": 192, "atmosphere": "none", "planet_class": "barren", "hydrosphere": "none", "economic_role": null, "settlement_pattern": null, "industrial_corridor": null, "notes": "small moon"},
|
||||
{"body_id": "GJ680e", "proper_name": "Hermanus", "body_type": "planet", "orbit_index": 4, "parent_body_id": null, "inhabited": true, "population": 8000000, "mass_class": "terrestrial", "surface_gravity": 0.70, "orbital_period_days": 280, "rotation_period_hours": 20.0, "atmosphere": "breathable", "planet_class": "temperate-cool", "hydrosphere": "extensive", "economic_role": "agriculture", "settlement_pattern": "dispersed", "industrial_corridor": null, "notes": "secondary settlement body, fishing cooperative, marine protein surplus, 300-year cooperative structure"},
|
||||
{"body_id": "GJ680e", "proper_name": "Anzali", "body_type": "planet", "orbit_index": 4, "parent_body_id": null, "inhabited": true, "population": 8000000, "mass_class": "terrestrial", "surface_gravity": 0.70, "orbital_period_days": 280, "rotation_period_hours": 20.0, "atmosphere": "breathable", "planet_class": "temperate-cool", "hydrosphere": "extensive", "economic_role": "agriculture", "settlement_pattern": "dispersed", "industrial_corridor": null, "notes": "secondary settlement body, fishing cooperative, marine protein surplus, 300-year cooperative structure"},
|
||||
{"body_id": "GJ680f", "proper_name": null, "body_type": "planet", "orbit_index": 5, "parent_body_id": null, "inhabited": false, "population": 0, "mass_class": "terrestrial", "surface_gravity": 0.35, "orbital_period_days": 650, "rotation_period_hours": 16.0, "atmosphere": "none", "planet_class": "frozen", "hydrosphere": "ice", "economic_role": null, "settlement_pattern": null, "industrial_corridor": null, "notes": "frozen rocky"},
|
||||
{"body_id": "GJ680g", "proper_name": null, "body_type": "gas_giant", "orbit_index": 6, "parent_body_id": null, "inhabited": false, "population": 0, "mass_class": "gas_giant", "surface_gravity": null, "orbital_period_days": 2000, "rotation_period_hours": 11.0, "atmosphere": "dense", "planet_class": null, "hydrosphere": null, "economic_role": null, "settlement_pattern": null, "industrial_corridor": null, "notes": "moderate gas giant"},
|
||||
{"body_id": "GJ680g-1", "proper_name": null, "body_type": "moon", "orbit_index": 1, "parent_body_id": "GJ680g", "inhabited": false, "population": 0, "mass_class": "dwarf", "surface_gravity": 0.06, "orbital_period_days": 5, "rotation_period_hours": 120, "atmosphere": "none", "planet_class": "barren", "hydrosphere": "none", "economic_role": null, "settlement_pattern": null, "industrial_corridor": null, "notes": "inner gas giant moon"},
|
||||
@@ -20,7 +20,7 @@
|
||||
{"body_id": "GJ680-oort", "proper_name": null, "body_type": "oort_cloud", "orbit_index": 10, "parent_body_id": null, "inhabited": false, "population": 0, "mass_class": null, "surface_gravity": null, "orbital_period_days": null, "rotation_period_hours": null, "atmosphere": null, "planet_class": null, "hydrosphere": null, "economic_role": null, "settlement_pattern": null, "industrial_corridor": null, "notes": "oort cloud"}
|
||||
],
|
||||
"stations": [
|
||||
{"station_id": "GJ680d-S1", "proper_name": "Elsenburg", "orbits_body_id": "GJ680d", "station_type": "agricultural", "population": 3000000, "economic_role": "agriculture", "docking_class": "standard", "has_gate_infrastructure": false, "notes": "agricultural cooperative trade hub, Mostert family landholdings, light manufacturing, surplus export to Mabaso Junction corridor"},
|
||||
{"station_id": "GJ680-oort-S1", "proper_name": "Yzerfontein", "orbits_body_id": "GJ680-oort", "station_type": "horizon", "population": 0, "economic_role": "transit", "docking_class": "standard", "has_gate_infrastructure": true, "notes": "horizon station, 2 apertures, Gate Corp minimal rotating crew"}
|
||||
{"station_id": "GJ680d-S1", "proper_name": "Varamin", "orbits_body_id": "GJ680d", "station_type": "agricultural", "population": 3000000, "economic_role": "agriculture", "docking_class": "standard", "has_gate_infrastructure": false, "notes": "agricultural cooperative trade hub, Farahani family landholdings, light manufacturing, surplus export to Mabaso Junction corridor"},
|
||||
{"station_id": "GJ680-oort-S1", "proper_name": "Now Shahr", "orbits_body_id": "GJ680-oort", "station_type": "horizon", "population": 0, "economic_role": "transit", "docking_class": "standard", "has_gate_infrastructure": true, "notes": "horizon station, 2 apertures, Gate Corp minimal rotating crew"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"system_id": "GJ 722",
|
||||
"proper_name": "Skuilplek",
|
||||
"proper_name": "Sığınak",
|
||||
"star_type": "M",
|
||||
"spectral_class": "M0V",
|
||||
"wiki_data": {
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"body_id": "GJ722c",
|
||||
"proper_name": "Rietbron",
|
||||
"proper_name": "Pınar",
|
||||
"body_type": "planet",
|
||||
"orbit_index": 2,
|
||||
"parent_body_id": null,
|
||||
@@ -50,7 +50,7 @@
|
||||
"economic_role": "agricultural",
|
||||
"settlement_pattern": "dispersed",
|
||||
"industrial_corridor": null,
|
||||
"notes": "primary inhabited world; wave_4 Afrikaans heritage ('reed spring'); 2-aperture through_route; M0V dim star; substantially self-sufficient (food since first generation, minimal lattice integration by design); community governance (legacy Commission exemption status, automatic renewal, no visit in living memory); Guardians of Autonomy named affiliation (cited in Guardian minimal-lattice viability literature); seven-generation M-dwarf agriculture from Cairn pocket tradition"
|
||||
"notes": "primary inhabited world; wave_4 Turkic heritage ('reed spring'); 2-aperture through_route; M0V dim star; substantially self-sufficient (food since first generation, minimal lattice integration by design); community governance (legacy Commission exemption status, automatic renewal, no visit in living memory); Guardians of Autonomy named affiliation (cited in Guardian minimal-lattice viability literature); seven-generation M-dwarf agriculture from Cairn pocket tradition"
|
||||
},
|
||||
{
|
||||
"body_id": "GJ722-belt",
|
||||
@@ -176,14 +176,14 @@
|
||||
"stations": [
|
||||
{
|
||||
"station_id": "GJ722-oort-S1",
|
||||
"proper_name": "Skuilplek Horizon",
|
||||
"proper_name": "Sığınak Horizon",
|
||||
"orbits_body_id": "GJ722-oort",
|
||||
"station_type": "horizon",
|
||||
"population": 180,
|
||||
"economic_role": "transit",
|
||||
"docking_class": "major",
|
||||
"has_gate_infrastructure": true,
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Droëland and outer Cairn pocket; minimal lattice infrastructure by community design; Guardian field representative visits (occasional); legacy Commission exemption maintained automatically"
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Marib and outer Cairn pocket; minimal lattice infrastructure by community design; Guardian field representative visits (occasional); legacy Commission exemption maintained automatically"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"system_id": "GJ 914A",
|
||||
"proper_name": "Droëland",
|
||||
"proper_name": "Marib",
|
||||
"star_type": "K",
|
||||
"spectral_class": "K2V",
|
||||
"wiki_data": {
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"body_id": "GJ914Ac",
|
||||
"proper_name": "Calvinia",
|
||||
"proper_name": "Ghawr",
|
||||
"body_type": "planet",
|
||||
"orbit_index": 2,
|
||||
"parent_body_id": null,
|
||||
@@ -50,7 +50,7 @@
|
||||
"economic_role": "agricultural",
|
||||
"settlement_pattern": "dispersed",
|
||||
"industrial_corridor": "agricultural_export",
|
||||
"notes": "primary inhabited world; wave_3 Afrikaans heritage ('dry land'); 2-aperture through_route; K2V dim star; three centuries worked soil; mature irrigated agriculture (grain, legumes); no industrial sector; community governance with Guardian-aligned autonomist reflexes; Commission filed registration only (no resident personnel); multi-year contingency reserves as founding practice; supply dependency on Stilwater corridor for fabricated components"
|
||||
"notes": "primary inhabited world; wave_3 Arab heritage ('dry land'); 2-aperture through_route; K2V dim star; three centuries worked soil; mature irrigated agriculture (grain, legumes); no industrial sector; community governance with Guardian-aligned autonomist reflexes; Commission filed registration only (no resident personnel); multi-year contingency reserves as founding practice; supply dependency on Sawad corridor for fabricated components"
|
||||
},
|
||||
{
|
||||
"body_id": "GJ914A-belt",
|
||||
@@ -196,14 +196,14 @@
|
||||
"stations": [
|
||||
{
|
||||
"station_id": "GJ914A-oort-S1",
|
||||
"proper_name": "Droëland Horizon",
|
||||
"proper_name": "Marib Horizon",
|
||||
"orbits_body_id": "GJ914A-oort",
|
||||
"station_type": "horizon",
|
||||
"population": 320,
|
||||
"economic_role": "transit",
|
||||
"docking_class": "major",
|
||||
"has_gate_infrastructure": true,
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Stilwater and outer Cairn pocket; grain and legume freight staging; contingency reserve logistics"
|
||||
"notes": "horizon station — 2 apertures; through_route; connects Sawad and outer Cairn pocket; grain and legume freight staging; contingency reserve logistics"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned araminta`
|
||||
|
||||
## v0.2 Pivot — Key Changes for Your Role
|
||||
The visual system shifts from designing layouts for three hand-crafted social sites to designing the visual grammar that the generator uses to produce legible zones at scale.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-04-05
|
||||
Last updated: 2026-04-05
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned burnelli-sheldon`
|
||||
|
||||
## Your Domain
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "Clerk - Project Briefing"
|
||||
description: "Pre-push institutional guardrail — checks D-record consistency, ticket drift, decision contradictions, and Q-record surfacing before code lands"
|
||||
type: briefing
|
||||
status: active
|
||||
agent: Clerk
|
||||
updated: 2026-05-03
|
||||
---
|
||||
|
||||
# Clerk - Project Briefing
|
||||
Last updated: 2026-05-03
|
||||
|
||||
## Role: Pre-Push Institutional Guardrail
|
||||
|
||||
Review branch changes before push to catch institutional drift: decision contradictions, ticket scope creep, missing D-records for new behavior, and unresolved Q-records that should block merge.
|
||||
|
||||
## What to Check
|
||||
|
||||
1. **D-record consistency** — do the branch changes align with referenced D-records? Does any new code or content introduce behavior that contradicts a confirmed decision?
|
||||
2. **Ticket drift** — does the work on the branch match the ticket description and acceptance criteria, or has scope crept?
|
||||
3. **Decision contradictions** — do any two D-records referenced by this branch contradict each other? Has a D-record been amended elsewhere but not reflected in the branch?
|
||||
4. **Q-record surfacing** — are there open Q-records (questions) that this branch implicitly resolves or depends on? If so, flag them for explicit resolution before merge.
|
||||
|
||||
## Where to Find Decisions
|
||||
|
||||
- **Decision index:** `decisions/README.md` — master index of all domain files
|
||||
- **Domain files:** `decisions/architecture.md`, `decisions/perception.md`, `decisions/content.md`, `decisions/scope.md`, `decisions/economics.md`, `decisions/process.md`
|
||||
- **Open questions:** `decisions/questions.md`, `decisions/questions-process.md`
|
||||
- **Rejected proposals:** `decisions/rejected.md`
|
||||
- **Decision DB table:**
|
||||
```bash
|
||||
tooling/db/sqlite-query "SELECT id, title, status, domain FROM decisions WHERE domain = '<domain>'"
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Write findings to `.cache/pre-push-review.md`. End with a binary verdict:
|
||||
|
||||
- **APPROVED** — no institutional issues found, safe to push
|
||||
- **REJECTED** — list each issue with the D/Q-record reference and what needs resolution
|
||||
|
||||
The findings file is gitignored (lives in `.cache/`) and is consumed by the push workflow.
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned dudley`
|
||||
|
||||
## Status
|
||||
STANDBY. This briefing will be populated when backend/engine implementation begins. Dudley activates when simulation implementation work is assigned.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned gestalt`
|
||||
|
||||
## Key Design Pivots
|
||||
- **Tycoon bookmark (D-117):** Zero investigation content. Tycoon (small business owner) is the starting state.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned gore`
|
||||
|
||||
## v0.2 Pivot — Key Changes for Your Role
|
||||
The workshop confirmed and sharpened the thematic frame. "Is this enough?" survives as the central question. The change is that it now must be posed through a generated tycoon's life, not through authored detective/smuggler characters.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned hoshe`
|
||||
|
||||
## Status
|
||||
**ACTIVE.** Test infrastructure from earlier sprints is operational. v0.2 focus: generator output validation — can the generator produce legible NPCs? Are relationships readable from generator output?
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned justine`
|
||||
|
||||
## Status
|
||||
**ACTIVE.** CI and build pipeline maintenance. Content validation infrastructure remains relevant as the AI-assisted content pipeline (D-123) produces NPC content at scale.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned mellanie`
|
||||
|
||||
## v0.2 Pivot — Key Changes for Your Role
|
||||
The copy pipeline fundamentally shifts from hand-authoring specific NPC profiles to designing the culture-driven voice system that feeds the generator.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned miri`
|
||||
|
||||
## v0.2 Pivot — Key Changes for Your Role
|
||||
The workshop confirmed Sova Transit District as the v0.2 setting (tycoon bookmark in Van Maanen's Star) and resolved the culture architecture gap in Miri's favour.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned nigel`
|
||||
|
||||
## v0.2 Pivot — Key Changes for Your Role
|
||||
The replayability frame shifts from "same conspiracy, different character lens" to "same generator, different seeded world." The anti-metagaming goal survives; the mechanism changes.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned oscar`
|
||||
|
||||
## Status
|
||||
STANDBY. This briefing will be populated when networking/multiplayer implementation begins.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned ozzie`
|
||||
|
||||
## v0.2 Pivot — Key Changes for Your Role
|
||||
The workshop revealed a core framing error: the game was built as a detective puzzle game but the vision is a single-character life sim. The fix is not cosmetic.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned paula`
|
||||
|
||||
## v0.2 Pivot — Key Changes for Your Role
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned qatux`
|
||||
|
||||
## Decisions Relevant to Your Role
|
||||
All decisions (D-001 through D-137) — maintains decision domain files in `decisions/` as the canonical record. Indexes and retrieves decision context for the team.
|
||||
|
||||
+49
-53
@@ -1,66 +1,62 @@
|
||||
---
|
||||
title: "Si - Project Briefing"
|
||||
description: "Sprint and project manager — owns sprint planning, ticket tracking, v0.2 critical path dependency chain, and CI pipeline awareness for definition of done"
|
||||
description: "Refinement manager — assesses ticket readiness, ensures sufficient context before work begins, surfaces missing D/Q-records and unresolved dependencies"
|
||||
type: briefing
|
||||
status: active
|
||||
agent: Si
|
||||
updated: 2026-03-13
|
||||
updated: 2026-05-03
|
||||
---
|
||||
|
||||
# Si - Project Briefing
|
||||
Last updated: 2026-03-13
|
||||
Last updated: 2026-05-03
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned si`
|
||||
|
||||
## Role: Refinement Manager
|
||||
|
||||
Assess whether tickets have enough context to be worked. A ticket is ready when:
|
||||
- It has a **clear outcome** — the agent can tell when the work is done
|
||||
- All referenced **D-records are unambiguous** and confirmed (not draft, not contradicted)
|
||||
- No **conflicting Q-records** remain open that would change the ticket's direction
|
||||
- All **referenced artifacts exist** (files, schemas, specs mentioned in the ticket or its D-records)
|
||||
|
||||
If any of these conditions fail, the ticket is not ready. Flag what is missing and who owns the resolution.
|
||||
|
||||
## Where to Find Decisions
|
||||
|
||||
- **Decision index:** `decisions/README.md` — lists all domain files and how to query them
|
||||
- **Domain files:** `decisions/architecture.md`, `decisions/perception.md`, `decisions/content.md`, `decisions/scope.md`, `decisions/economics.md`, `decisions/process.md`
|
||||
- **Open questions:** `decisions/questions.md` (Q-NNN), `decisions/questions-process.md` (Q-NNN process)
|
||||
- **Rejected proposals:** `decisions/rejected.md` (R-NNN)
|
||||
- **Decision DB queries:**
|
||||
```bash
|
||||
tooling/db/decision show D-NNN # Show a specific decision
|
||||
tooling/db/decision check-dupes "title" # Check for duplicates before creating
|
||||
```
|
||||
|
||||
## Where to Find Workshop Outcomes
|
||||
|
||||
Workshop outputs live at `docs/workshops/<workshop-name>/`:
|
||||
- `workshop-outcomes.md` — final summary with confirmed D-records
|
||||
- Round files — per-round notes and participant outputs
|
||||
- Check these when a ticket references a workshop decision to verify the full context is captured
|
||||
|
||||
## What "Enough Context" Means
|
||||
|
||||
A ticket has enough context when an agent can pick it up cold and deliver without asking clarifying questions. Concretely:
|
||||
|
||||
1. **Outcome is testable** — "implement X" has acceptance criteria; "design Y" has a deliverable format
|
||||
2. **D-records are stable** — no open Q-record that could reverse the referenced D-record
|
||||
3. **Dependencies are met** — if the ticket says "blocked by Miri's zone spec," that spec exists or the ticket stays in backlog
|
||||
4. **Scope is bounded** — the ticket does not silently depend on cascade phases that have not been reached (see D-166 development cascade in CLAUDE.md)
|
||||
|
||||
## Key Commands
|
||||
|
||||
## Decisions Relevant to Your Role
|
||||
All decisions across all domains. Read `decisions/README.md` for the index. Query decisions DB for sprint planning:
|
||||
```bash
|
||||
make decisions-coverage # Decision-to-ticket coverage by domain
|
||||
make decisions-orphan # Decisions without implementing tickets
|
||||
make decisions-active # List all active decisions
|
||||
tooling/db/ticket list --status backlog # Candidates for refinement
|
||||
tooling/db/ticket show <id> # Full ticket details
|
||||
tooling/db/decision show D-NNN # Check a referenced decision
|
||||
tooling/db/sqlite-query "SELECT * FROM decisions WHERE status = 'open'" # Open decisions
|
||||
```
|
||||
|
||||
## Open Questions Assigned to You
|
||||
All open questions - tracks as tickets for assignment and progress monitoring.
|
||||
|
||||
## Development Cascade (D-166)
|
||||
Development follows a strict 6-phase cascade. Currently in Phase 1 (wiki content). Generator spike (D-119) is Phase 5.
|
||||
|
||||
**Prerequisites that must exist before/during the generator spike:**
|
||||
1. Miri: Zone identity spec + Van Maanen's Star culture profile — blocks voice, NPC gen, AI pipeline
|
||||
2. Araminta: Zone visual grammar, tile palettes — blocked by Miri zone identity spec
|
||||
3. Tyre: NpcBlueprint struct design — generator output format
|
||||
4. Mellanie: Culture-primary voice cards — blocked by Miri culture profile
|
||||
|
||||
**Dependency chain:**
|
||||
```
|
||||
Miri: Zone Identity Spec + Van Maanen's Star Culture Profile
|
||||
|-- Araminta: zone visual grammar, tile palettes
|
||||
|-- Tyre: generator zone template parameters, NpcBlueprint
|
||||
|-- Mellanie: culture-primary voice cards
|
||||
v
|
||||
Tyre: Generator Spike (Sprint 25)
|
||||
|-- Produces: auto-generated location + NPCs
|
||||
v
|
||||
Test: Can the player read NPC relationships from generator output?
|
||||
```
|
||||
|
||||
**Estimated timeline (Tyre):** 7 sprints to proof-of-life playtest (generated location + legible characters + tycoon bookmark from creation to Day 3).
|
||||
|
||||
## Current Priorities
|
||||
Consult `docs/sprints/` for current sprint briefing. Break v0.2 decisions (D-114 through D-137) into tickets if not already covered. Track dependencies above.
|
||||
|
||||
## Development Workflow
|
||||
See `docs/DEVOPS.md` for full procedures. Key awareness for sprint planning:
|
||||
- `make ci` must pass before any PR merges — definition of done includes CI green
|
||||
- `make ci-server` = lint-server → build-server → test-server
|
||||
- `make ci-client` = lint-client → build-client → test-client
|
||||
- Server work lives in `server/`, client work in `client/`, cross-boundary tests in `tests/`
|
||||
- Hoshe's test plans target these directories per D-030 testing architecture
|
||||
|
||||
## Key Documents
|
||||
- decisions/ - domain-split decision files (see decisions/README.md for index)
|
||||
- docs/DEVOPS.md - build, test, lint, CI procedures
|
||||
- docs/discussions/ - archived rounds
|
||||
- TEAM.md - team roster
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned stig`
|
||||
|
||||
## Status
|
||||
**ACTIVE.** v0.2 brings a character customisation UI requirement and removes the dual-character variant for insert/HUD.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned tiger`
|
||||
|
||||
## Status
|
||||
STANDBY. This briefing will be populated when localization work begins.
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned troblum`
|
||||
|
||||
## Decisions Relevant to Your Role
|
||||
- D-010: Architectural baseline - four principles
|
||||
|
||||
@@ -11,7 +11,8 @@ updated: 2026-03-13
|
||||
Last updated: 2026-03-13
|
||||
|
||||
## Current Project State
|
||||
See [PROJECT_STATE.md](../../PROJECT_STATE.md) for shared project state (cascade phase, sprint, decision count).
|
||||
Check active milestones: `tooling/db/ticket milestone list`
|
||||
Check your tickets: `tooling/db/ticket list --assigned tyre`
|
||||
|
||||
## Decisions Relevant to Your Role
|
||||
Read `decisions/architecture.md` (primary) and `decisions/perception.md` (secondary). Key decisions:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user