#723 — tooling/db/decision show <D-NNN> with implementing tickets + cross-refs; decisions-coverage now lists ticket IDs per decision
Motivation
Sprint 36 hit a binary merge conflict class when two branches both committed changes to server/data/systems.db. The meta stamp + pre-push hook + /pr-push regen form a systemic fix: the canonical rule is "edit sources, not the DB" and the tooling now enforces it.
Verification
make regen-db ran successfully; three generators stamped in meta table
make check-systems-db exit 0 — check-systems-db-stamp: OK — 3 generator(s) up to date
ruff check tooling/ — All checks passed
All 7 tickets moved to done in the DB
Test plan
make regen-db runs end-to-end on a clean clone
make check-systems-db exits 0 after a fresh regen
Attempt git push with a modified generator but stale DB — hook rejects
/pr-push on a branch with modified tooling/economy-db/import_economics.py auto-regenerates before pushing
tooling/db/decision show D-159 returns implementing tickets + refs
## Summary
Sprint 37 CI sweep. Seven tickets, all closed:
- **#854** (epic) — Generator-driven asset pipeline
- **#855** — systems.db regeneration with versioning awareness
- **#856** — `meta` table stamping (schema_version + generator_sha + generated_at)
- **#857** — Pre-push hook rejects stale systems.db via `tooling/check-systems-db-stamp`
- **#858** — `/pr-push` auto-runs `make regen-db` when generator sources changed
- **#859** — `.claude/rules/asset-pipeline.md`, CLAUDE.md hook, DEVOPS.md section
- **#723** — `tooling/db/decision show <D-NNN>` with implementing tickets + cross-refs; `decisions-coverage` now lists ticket IDs per decision
## Motivation
Sprint 36 hit a binary merge conflict class when two branches both committed changes to `server/data/systems.db`. The meta stamp + pre-push hook + `/pr-push` regen form a systemic fix: the canonical rule is **"edit sources, not the DB"** and the tooling now enforces it.
## Verification
- `make regen-db` ran successfully; three generators stamped in `meta` table
- `make check-systems-db` exit 0 — `check-systems-db-stamp: OK — 3 generator(s) up to date`
- `ruff check tooling/` — All checks passed
- All 7 tickets moved to `done` in the DB
## Test plan
- [ ] `make regen-db` runs end-to-end on a clean clone
- [ ] `make check-systems-db` exits 0 after a fresh regen
- [ ] Attempt `git push` with a modified generator but stale DB — hook rejects
- [ ] `/pr-push` on a branch with modified `tooling/economy-db/import_economics.py` auto-regenerates before pushing
- [ ] `tooling/db/decision show D-159` returns implementing tickets + refs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds systems.db regeneration discipline (#855) via a `meta` table (#856)
stamped by every generator, a pre-push hook that rejects stale DBs (#857),
and the top-level `make regen-db` / `make check-systems-db` targets that
drive the whole pipeline.
The stamp stores SHA-1 of generator source + schema, so the pre-push hook
can cheaply detect "you changed a generator but forgot to regen the DB"
before a binary merge conflict lands. Sprint 36 hit that class of conflict
on two branches touching systems.db simultaneously — this is the systemic
fix.
Regenerated systems.db is stamped; `make check-systems-db` passes.
Refs: #855#856#857
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a new step 4a that detects generator-source or economy-data changes
vs origin/main and runs `make regen-db` before the push, staging the
updated systems.db. Pairs with the pre-push hook (#857) — instead of
rejecting a stale DB, /pr-push proactively freshens it.
Refs: #858
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds `.claude/rules/asset-pipeline.md` describing the full pipeline —
generators, meta stamp, pre-push hook, how to make DB/schema changes,
and why direct systems.db edits are forbidden. CLAUDE.md and DEVOPS.md
point at the rules file; sprint-start template now reminds teammates
to read it before touching DB sources.
CLAUDE.md also gains the missing `decision show` row in the CLI tool
table (companion to #723's `decision show` subcommand).
Refs: #859#723
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes the decision-to-ticket coverage gap (#723). The `decisions-coverage`
Makefile target already reported per-decision counts; this adds the
single-decision drill-down via `tooling/db/decision show D-159`, returning
linked tickets, outbound refs, and inbound refs in one shot.
Schema unchanged — reverse link is a SELECT on tickets.decision_ref.
Refs: #723
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewers: Hoshe (code quality & tests) + Tyre (architecture).
Verdict: CHANGES REQUESTED
Source-canonical rule is a solid design — SHA-of-sources rather than DB-content hash sidesteps SQLite non-determinism cleanly, pre-push hook is the correct enforcement layer, /pr-push auto-regen is appropriately scoped. But 5 blocking items + several minor/suggestion items need addressing before merge.
Hoshe — Code Quality & Tests: REQUEST_CHANGES
Exit codes are correct, error messages actionable, schema coherent, show_decision handles zero-ticket cases cleanly, pre-push hook integrates without disrupting existing checks. Two real bugs + two nits.
#
File:Line
Issue
Severity
H1
tooling/planet-gen/generate_atlas.py:~1383-1391
Double-commit bug.conn.commit() at 1384 persists atlas data, then _write_stamp(conn) issues its ownconn.commit() at line 124. Inconsistent with import_economics._write_stamp which leaves transaction ownership to caller. Either remove the inner commit (match the economics pattern — preferred) or remove the outer commit.
error
H2
tooling/check-systems-db-stamp:~48-54 (file_sha1)
Silent ghost-SHA on missing source file. Skips missing paths (if p.exists(): h.update(...)). If a generator source file is moved/deleted, validator computes SHA-1 of zero bytes (always da39a3ee...). The stored SHA won't match → push correctly rejected, but the error message says "STALE" with no hint that the file is missing. Edge case: if both stored and current SHA converge on the empty hash, check passes vacuously. Add explicit "source file not found: " error with exit 1.
generate_brands source is listed but no stamp writer in the Rust binary — it's stamped on behalf by import_economics._write_stamp. Pre-existing design constraint, not a regression, but should be called out in the meta table schema comment or a TODO so the next engineer doesn't add an independent stamp path that races.
nit
H4
.config/hooks/pre-push (stamp section, last ~30 lines)
rc=2 (no meta table) is advisory (intended grace for new DBs). But the warning message "run make regen-db after next regeneration" reads like a future action. Suggest: "systems.db has no meta stamp — run make regen-db now if this DB was generated by you."
nit
Tyre — Architecture: REQUEST_CHANGES
The core design is elegant where it matters. SHA-of-sources (not hash-of-DB) is the right choice because it sidesteps SQLite's non-determinism (timestamps, autoincs, rowids). Pre-push is the correct enforcement layer. /pr-push auto-regen is scoped tight to generator-touching branches. But three cracks need patching, plus one Makefile failure-handling bug.
#
File:Line
Issue
Severity
T1
.claude/rules/asset-pipeline.md:~95 ("Why direct DB edits are forbidden")
Rule overstates determinism. States "Regeneration is deterministic (same sources → same DB, byte-for-byte)". Not true: meta.generated_at uses datetime('now'), SQLite rowids/autoinc aren't stable across runs. The stamp is deterministic, not the DB binary. The rule's strength doesn't depend on byte-determinism — so soften the claim to "the stamp is deterministic (same generator source → same recorded SHA)" and remove the byte-for-byte assertion. Otherwise reviewers expect binary-identical DBs after regen and are confused when diffs appear.
generate_brands stamped on behalf by import_economics, not by its own Rust binary. Undocumented coupling in the rule's "Three generators write to systems.db" table. Two real failure modes: (a) someone runs tooling/generate-brands directly (the rule's documented invocation) without import_economics after it → SHA drifts from stamp; (b) brands source changes, economics doesn't → any workflow invoking only the Rust binary silently skips stamping. Fix: either make the Rust binary stamp itself (~30 LOC with rusqlite), or document the coupling explicitly and remove "on behalf" as an invisible convention. Hoshe also flagged this (H3).
major
T3
.claude/rules/asset-pipeline.md (whole doc)
Escape hatch not named. Rule says "never run UPDATE or INSERT directly on systems.db outside of a migration" but doesn't define when a migration is legitimate, who writes them, or where they live. MIGRATION_SQL in import_economics.py is the de-facto location — the rule should state that explicitly and forbid any other form of direct DB edit (no hand-run sqlite-exec, no one-off patches). Otherwise the rule has a loophole an operator can drive a truck through.
major
T4
Makefile:357-366 (regen-db target)
No inter-step failure handling. Three generators run sequentially but each recipe line is an independent shell — failures in step 1 don't halt step 2. If generate_brands (step 1) fails, import_economics still runs and stamps → DB with stale brand data and a "fresh" stamp. Add set -e semantics or || exit 1 after each step, or use a .SHELLFLAGS guard.
major
T5
.config/hooks/pre-push:~155
Stamp check skipped on a branch's first push (no remote ref exists). Low-frequency corner case but real: developer creating a new branch with a stale DB gets no warning until the second push. One-line guard: run check-systems-db-stamp unconditionally when server/data/systems.db was modified in ANY branch commit, regardless of remote-ref state. Alternatively, document the gap.
minor
T6
tooling/check-systems-db-stamp:61-65
Fail-open on unknown generators. Unknown generator_name in meta silently skipped ("forward compat"). Wrong default for a safety gate — a future branch adding a new generator without registering it here becomes a no-op check, silently. Warn loudly on unknown names, or require explicit allowlist bump. Fail closed.
minor
T7
.claude/skills/pr-push/SKILL.md step 4a
Watch list hardcodes generator paths (tooling/economy-db/import_economics.py, tooling/planet-gen/generate_atlas.py, server/src/bin/generate_brands/main.rs, etc.) plus content dirs. This will drift from GENERATOR_SOURCES in check-systems-db-stamp. Two sources of truth in a system whose purpose is source-canonical discipline is exactly what this PR is trying to prevent. Shell out to a registry script, or at minimum add mutual cross-reference comments.
Solid — reverse link via tickets.decision_ref is the right direction (no two-way sync). One note: decision_ref is free-text without a foreign key, so typo'd or deleted D-IDs silently don't show up anywhere. Not blocking; worth a follow-up decisions-orphan-tickets CLI to surface mismatches.
suggestion
T9
server/data/systems-schema.sql (meta table)
schema_version TEXT uses SHA-1 of the schema file. The rule doc names "savegame migration lineage" as a future use. When that arrives, a content-hash is harder to order than a monotonic version number — you cannot tell from two SHAs which is newer. Don't change now; flag for the migration phase: v1.0-style semver would serve lineage better.
suggestion
Process note
PR test plan has 5 unchecked boxes:
make regen-db runs end-to-end on a clean clone
make check-systems-db exits 0 after a fresh regen
Attempt git push with modified generator but stale DB — hook rejects
/pr-push on a branch with modified generator auto-regenerates before pushing
tooling/db/decision show D-159 returns implementing tickets + refs
These are NOT merge-path per skill 0b-i (no pre-game/scene/save/connection code). No automatic verdict downgrade. But they ARE the pipeline's own smoke tests — the features this PR ships. Worth running before the next push so the verification is concrete, not aspirational.
Summary of blocking issues
5 error/major-severity fixes required:
H1 — Double-commit in generate_atlas._write_stamp (inconsistent with economics pattern)
T1 — Rule overstates byte-for-byte determinism (stamp is deterministic, DB is not)
T2 — generate_brands silently stamped "on behalf" by economics (also H3). Either self-stamp or document the coupling explicitly.
T3 — Escape hatch for legitimate direct DB writes (migrations) is absent from the rule. Name the sanctioned path.
T4 — Makefile:regen-db doesn't halt on generator failure → can produce stale-data-fresh-stamp state.
Other items are minor/suggestion; address before merge or push back with rationale.
# Review: sprint-37/ci → main (PR #136)
Reviewers: **Hoshe** (code quality & tests) + **Tyre** (architecture).
## Verdict: CHANGES REQUESTED
Source-canonical rule is a solid design — SHA-of-sources rather than DB-content hash sidesteps SQLite non-determinism cleanly, pre-push hook is the correct enforcement layer, `/pr-push` auto-regen is appropriately scoped. But 5 blocking items + several minor/suggestion items need addressing before merge.
---
## Hoshe — Code Quality & Tests: REQUEST_CHANGES
Exit codes are correct, error messages actionable, schema coherent, `show_decision` handles zero-ticket cases cleanly, pre-push hook integrates without disrupting existing checks. Two real bugs + two nits.
| # | File:Line | Issue | Severity |
|---|-----------|-------|----------|
| H1 | `tooling/planet-gen/generate_atlas.py:~1383-1391` | **Double-commit bug.** `conn.commit()` at 1384 persists atlas data, then `_write_stamp(conn)` issues its *own* `conn.commit()` at line 124. Inconsistent with `import_economics._write_stamp` which leaves transaction ownership to caller. Either remove the inner commit (match the economics pattern — preferred) or remove the outer commit. | **error** |
| H2 | `tooling/check-systems-db-stamp:~48-54` (`file_sha1`) | **Silent ghost-SHA on missing source file.** Skips missing paths (`if p.exists(): h.update(...)`). If a generator source file is moved/deleted, validator computes SHA-1 of zero bytes (always `da39a3ee...`). The stored SHA won't match → push correctly rejected, but the error message says "STALE" with no hint that the file is missing. Edge case: if both stored and current SHA converge on the empty hash, check passes vacuously. Add explicit "source file not found: <path>" error with exit 1. | warning |
| H3 | `tooling/check-systems-db-stamp:~32-37` (`GENERATOR_SOURCES`) | **`generate_brands` source is listed but no stamp writer in the Rust binary** — it's stamped on behalf by `import_economics._write_stamp`. Pre-existing design constraint, not a regression, but should be called out in the meta table schema comment or a `TODO` so the next engineer doesn't add an independent stamp path that races. | nit |
| H4 | `.config/hooks/pre-push` (stamp section, last ~30 lines) | `rc=2` (no meta table) is advisory (intended grace for new DBs). But the warning message "run `make regen-db` after next regeneration" reads like a future action. Suggest: "systems.db has no meta stamp — run `make regen-db` now if this DB was generated by you." | nit |
---
## Tyre — Architecture: REQUEST_CHANGES
The core design is elegant where it matters. SHA-of-sources (not hash-of-DB) is the right choice because it sidesteps SQLite's non-determinism (timestamps, autoincs, rowids). Pre-push is the correct enforcement layer. `/pr-push` auto-regen is scoped tight to generator-touching branches. But three cracks need patching, plus one Makefile failure-handling bug.
| # | File:Line | Issue | Severity |
|---|-----------|-------|----------|
| T1 | `.claude/rules/asset-pipeline.md:~95` ("Why direct DB edits are forbidden") | **Rule overstates determinism.** States *"Regeneration is deterministic (same sources → same DB, byte-for-byte)"*. Not true: `meta.generated_at` uses `datetime('now')`, SQLite rowids/autoinc aren't stable across runs. The *stamp* is deterministic, not the DB binary. The rule's strength doesn't depend on byte-determinism — so soften the claim to "the stamp is deterministic (same generator source → same recorded SHA)" and remove the byte-for-byte assertion. Otherwise reviewers expect binary-identical DBs after regen and are confused when diffs appear. | **major** |
| T2 | `tooling/economy-db/import_economics.py:1194` + `tooling/check-systems-db-stamp:37-43` | **`generate_brands` stamped on behalf by `import_economics`**, not by its own Rust binary. Undocumented coupling in the rule's "Three generators write to systems.db" table. Two real failure modes: (a) someone runs `tooling/generate-brands` directly (the rule's documented invocation) without `import_economics` after it → SHA drifts from stamp; (b) brands source changes, economics doesn't → any workflow invoking only the Rust binary silently skips stamping. Fix: either make the Rust binary stamp itself (~30 LOC with `rusqlite`), or document the coupling explicitly and remove "on behalf" as an invisible convention. Hoshe also flagged this (H3). | **major** |
| T3 | `.claude/rules/asset-pipeline.md` (whole doc) | **Escape hatch not named.** Rule says "never run UPDATE or INSERT directly on systems.db **outside of a migration**" but doesn't define *when* a migration is legitimate, who writes them, or where they live. `MIGRATION_SQL` in `import_economics.py` is the de-facto location — the rule should state that explicitly and forbid any other form of direct DB edit (no hand-run `sqlite-exec`, no one-off patches). Otherwise the rule has a loophole an operator can drive a truck through. | **major** |
| T4 | `Makefile:357-366` (`regen-db` target) | **No inter-step failure handling.** Three generators run sequentially but each recipe line is an independent shell — failures in step 1 don't halt step 2. If `generate_brands` (step 1) fails, `import_economics` still runs and stamps → DB with stale brand data and a "fresh" stamp. Add `set -e` semantics or `\|\| exit 1` after each step, or use a `.SHELLFLAGS` guard. | **major** |
| T5 | `.config/hooks/pre-push:~155` | Stamp check skipped on a branch's first push (no remote ref exists). Low-frequency corner case but real: developer creating a new branch with a stale DB gets no warning until the second push. One-line guard: run `check-systems-db-stamp` unconditionally when `server/data/systems.db` was modified in ANY branch commit, regardless of remote-ref state. Alternatively, document the gap. | minor |
| T6 | `tooling/check-systems-db-stamp:61-65` | **Fail-open on unknown generators.** Unknown `generator_name` in meta silently skipped ("forward compat"). Wrong default for a safety gate — a future branch adding a new generator without registering it here becomes a no-op check, silently. Warn loudly on unknown names, or require explicit allowlist bump. Fail closed. | minor |
| T7 | `.claude/skills/pr-push/SKILL.md` step 4a | Watch list hardcodes generator paths (`tooling/economy-db/import_economics.py`, `tooling/planet-gen/generate_atlas.py`, `server/src/bin/generate_brands/main.rs`, etc.) plus content dirs. This will drift from `GENERATOR_SOURCES` in `check-systems-db-stamp`. Two sources of truth in a system whose purpose is source-canonical discipline is exactly what this PR is trying to prevent. Shell out to a registry script, or at minimum add mutual cross-reference comments. | minor |
| T8 | `tooling/db/decisions_sync.py:417-457` (`show_decision`) | Solid — reverse link via `tickets.decision_ref` is the right direction (no two-way sync). One note: `decision_ref` is free-text without a foreign key, so typo'd or deleted D-IDs silently don't show up anywhere. Not blocking; worth a follow-up `decisions-orphan-tickets` CLI to surface mismatches. | suggestion |
| T9 | `server/data/systems-schema.sql` (meta table) | `schema_version TEXT` uses SHA-1 of the schema file. The rule doc names "savegame migration lineage" as a future use. When that arrives, a content-hash is harder to order than a monotonic version number — you cannot tell from two SHAs which is newer. Don't change now; flag for the migration phase: `v1.0`-style semver would serve lineage better. | suggestion |
---
## Process note
PR test plan has 5 unchecked boxes:
- [ ] `make regen-db` runs end-to-end on a clean clone
- [ ] `make check-systems-db` exits 0 after a fresh regen
- [ ] Attempt `git push` with modified generator but stale DB — hook rejects
- [ ] `/pr-push` on a branch with modified generator auto-regenerates before pushing
- [ ] `tooling/db/decision show D-159` returns implementing tickets + refs
These are NOT merge-path per skill 0b-i (no pre-game/scene/save/connection code). No automatic verdict downgrade. But they ARE the pipeline's own smoke tests — the features this PR ships. Worth running before the next push so the verification is concrete, not aspirational.
---
## Summary of blocking issues
5 `error`/`major`-severity fixes required:
- **H1** — Double-commit in `generate_atlas._write_stamp` (inconsistent with economics pattern)
- **T1** — Rule overstates byte-for-byte determinism (stamp is deterministic, DB is not)
- **T2** — `generate_brands` silently stamped "on behalf" by economics (also H3). Either self-stamp or document the coupling explicitly.
- **T3** — Escape hatch for legitimate direct DB writes (migrations) is absent from the rule. Name the sanctioned path.
- **T4** — `Makefile:regen-db` doesn't halt on generator failure → can produce stale-data-fresh-stamp state.
Other items are minor/suggestion; address before merge or push back with rationale.
Addresses all blocking + minor items from PR #136 review.
Architectural change (T2/H3 — the review's main complaint):
generate_brands was previously a separate Rust binary that produced a TOML
artifact, with its stamp written "on behalf" by import_economics.py at the
end of its own run. Reviewers flagged the invisible coupling: two sources
of truth in a system designed to have one, and no way to tell from the
stamp that one "generator" was really a subroutine of the other.
import_economics now invokes tooling/generate-brands as the first step of
its main() flow, before opening its own DB connection. The TOML artefact
is still produced and still committed (useful for diff-review of brand
changes), but there's now one pipeline owner. The meta table carries two
rows (import_economics, generate_atlas) not three; the Rust binary's
source SHA folds into import_economics' stamp via IMPORT_ECONOMICS_SOURCES.
A MIGRATION_SQL DELETE cleans up pre-merge DBs that still have the
orphan generate_brands row.
Other review items addressed in-line:
H1 generate_atlas._write_stamp no longer commits — transaction ownership
stays with the caller (matches import_economics pattern). Stamp +
atlas data now commit atomically; a failed stamp rolls back the
atlas data rather than leaving a stamp-missing-data intermediate.
H2 _file_sha1 (in both import_economics, generate_atlas,
check-systems-db-stamp) raises FileNotFoundError on missing sources
instead of silently contributing an empty-bytes hash. A ghost-SHA
convergence could otherwise produce vacuous "fresh" passes.
H4 pre-push no-meta-table warning rephrased — was "run after next
regeneration", now "run now if this DB was generated by you".
T1 asset-pipeline.md determinism claim softened: the stamp is
deterministic (same source → same recorded SHA), the DB binary is
not (generated_at + SQLite rowids/freelist churn).
T3 asset-pipeline.md gains a "migration escape hatch" section naming
MIGRATION_SQL in import_economics.py as the only sanctioned path
for direct writes, and forbidding hand-run sqlite-exec / one-off
patch scripts / SQLite-GUI edits.
T4 Makefile regen-db now runs as a single shell with `set -e`. A
failure in one generator halts the pipeline immediately, preventing
the "stale data, fresh stamp" state where a later step stamped a
DB whose earlier step had failed. import_economics' exit code 2
(coverage gate warning) remains explicitly tolerated.
T5 pre-push stamp check now runs on a branch's first push too —
compares against origin/main instead of origin/$BRANCH, closing
the gap where a new branch could ship a stale DB via the first push.
T6 check-systems-db-stamp fails closed on unknown generator_names in
meta — a future branch adding a new generator without registering
it in GENERATOR_SOURCES will now be rejected, not silently skipped.
T7 /pr-push watch list gains a mutual cross-reference comment with
GENERATOR_SOURCES in check-systems-db-stamp, plus the missing
names.rs source file, so the two lists cannot silently drift.
Follow-up tickets created:
#887 T8 decisions-orphan-tickets CLI — surfaces tickets whose
decision_ref points at a non-existent D-record.
#888 T9 meta.schema_version monotonic semver — for savegame migration
lineage in Phase 5+ (SHA comparison can't be ordered).
Verified:
make regen-db end-to-end — OK
make check-systems-db — OK, 2 generator(s) up to date
STALE detection — OK, verified by touching generate_atlas.py
/pr-push watch list — OK, flags this branch's changed sources
decision show D-159 — OK, structured output with tickets + refs
Refs: #855#856#857#858#859 PR #136
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewers: Hoshe (code quality & tests) + Tyre (architecture).
Verdict: APPROVED
3 → 0 blockers across 2 rounds. All round-1 items correctly addressed. The T2 pivot (merging the brand pipeline into import_economics instead of documenting the stamp-on-behalf coupling) is architecture, not paper.
Hoshe — Code Quality & Tests: APPROVE
All 4 round-1 items fixed. H2 error-handling path is consistent across import_economics, generate_atlas, and check-systems-db-stamp. The H3 architectural pivot is clean.
Round-1 status
#
Item
Status
H1
generate_atlas._write_stamp double-commit
FIXED — inner conn.commit() removed; caller wraps stamp + data in try/except with rollback. Data and stamp land atomically or not at all
H2
Silent ghost-SHA on missing source
FIXED — _file_sha1 now raises FileNotFoundError with formatted path. check() loop catches it and returns rc=1 with "BROKEN — {generator_name}: {exc}" on stderr. Not swallowed. Consistent across all three files
H3
Undocumented stamping coupling
FIXED via pivot — generate_brands no longer stamps at all. import_economics.regenerate_brands() shells out to the Rust binary as step zero of its main(), checks return code, raises _ImportAborted on non-zero exit (stderr printed before raising). IMPORT_ECONOMICS_SOURCES includes the Rust .rs files + the wrapper script. Migration DELETE FROM meta WHERE generator_name='generate_brands' cleans the orphan row on pre-PR DBs
H4
pre-push no-meta-table wording
FIXED — "pre-push: WARNING — systems.db has no meta stamp — run 'make regen-db' now if this DB was generated by you"
New observation (non-blocking)
The stamp-write failure path in import_economics (around line 1271) catches Exception, prints a warning, and continues — but does not roll back the data commit (which has already landed). Intentional: the stamp is advisory, and a missing-meta state is caught by check-systems-db-stamp as rc=2 (warn-not-block). That's a coherent deliberate choice. Worth a one-line comment "stamp failure is non-fatal; DB data is already committed" so the next reader doesn't wonder if the missing rollback is a bug. Not required before merge.
Tyre — Architecture: APPROVE
The T2 pivot is real architecture, not documentation papering over a smell. Single pipeline owner with one stamp row per meta entry; Rust source SHAs fold into IMPORT_ECONOMICS_SOURCES so any change to main.rs / names.rs / the wrapper invalidates the stamp correctly. All 7 addressable items fixed; 2 suggestions deferred to #887/#888.
Architectural deep-check on the pivot
Does import_economics.py get too big? Grew from 1170 → 1306 lines. Growth is MIGRATION_SQL expansion + a 30-line regenerate_brands() subprocess wrapper + the DELETE-orphan migration. The Python file doesn't absorb brand-generation logic — it shells out. The Rust binary is untouched.
Is the Rust binary still independently invokable? Yes — tooling/generate-brands works standalone for debug/experimentation. The regenerate_brands() docstring explicitly calls out the non-canonical-seed escape hatch ("callers that need non-canonical seeds must still invoke the wrapper directly — experimentation only"). The "must go through import_economics" rule is self-enforcing via the stamp: wrapper-only runs don't write a stamp, so the next stamp check catches the drift. That's the right level.
Migration DELETE idempotency on revert:DELETE FROM meta WHERE generator_name = 'generate_brands' runs on every import. Pull branch → regen → revert: orphan row stays gone because main's import_economics won't re-add it. Revert further to pre-PR state where the Makefile called generate-brands separately and the binary wrote its own stamp — yes, the row comes back, but that's the correct pre-PR behavior. Clean.
FIXED via pivot — coupling eliminated, not documented
T3 (escape hatch unnamed)
FIXED — "The migration escape hatch" section names MIGRATION_SQL + COLUMN_MIGRATIONS as the sanctioned path and explicitly forbids hand-run sqlite-exec, GUI edits, one-off patch scripts, and committing systems.db alone
T4 (Makefile failure halting)
FIXED — set -e in a single shell, `ec=0; ...
T5 (first-push gap)
FIXED — compares against origin/main (always exists) instead of origin/$BRANCH. No false-positive risk: the stamp check validates meta.generator_sha vs current source SHAs, so a DB-only commit with untouched generators passes cleanly
T6 (fail-open on unknowns)
FIXED — unknown generator_name returns rc=1 with "Update GENERATOR_SOURCES in tooling/check-systems-db-stamp to register them before pushing" on stderr
T7 (watch-list drift)
FIXED — mutual cross-reference comments exist in both places: GENERATOR_SOURCES comment says "Keep this list in sync with IMPORT_ECONOMICS_SOURCES"; the pr-push SKILL watch list cites T7 by name and lists all four generate_brands sources
PR test plan still has 5 unchecked boxes (pipeline smoke tests). The commit body reports manual verifications were done:
Verified:
make regen-db end-to-end — OK
make check-systems-db — OK, 2 generator(s) up to date
STALE detection — OK, verified by touching generate_atlas.py
/pr-push watch list — OK, flags this branch's changed sources
decision show D-159 — OK, structured output with tickets + refs
Worth ticking those boxes in the PR description before merge so the evidence is recorded where reviewers expect it, matching the Sprint 36 retro "evidence over aspiration" norm. Not a gate.
Summary
APPROVED across both reviewers. 14 round-1 items total (4 Hoshe + 9 Tyre + 1 cross-reference) → 0 blockers in round 2. The pivot on T2/H3 is the structural win — fixing the coupling by removing it rather than documenting it.
# Review: sprint-37/ci → main (PR #136) — Round 2
Reviewers: **Hoshe** (code quality & tests) + **Tyre** (architecture).
## Verdict: APPROVED
3 → 0 blockers across 2 rounds. All round-1 items correctly addressed. The T2 pivot (merging the brand pipeline into `import_economics` instead of documenting the stamp-on-behalf coupling) is architecture, not paper.
---
## Hoshe — Code Quality & Tests: APPROVE
All 4 round-1 items fixed. H2 error-handling path is consistent across `import_economics`, `generate_atlas`, and `check-systems-db-stamp`. The H3 architectural pivot is clean.
### Round-1 status
| # | Item | Status |
|---|------|--------|
| H1 | `generate_atlas._write_stamp` double-commit | **FIXED** — inner `conn.commit()` removed; caller wraps stamp + data in `try/except` with rollback. Data and stamp land atomically or not at all |
| H2 | Silent ghost-SHA on missing source | **FIXED** — `_file_sha1` now raises `FileNotFoundError` with formatted path. `check()` loop catches it and returns rc=1 with `"BROKEN — {generator_name}: {exc}"` on stderr. Not swallowed. Consistent across all three files |
| H3 | Undocumented stamping coupling | **FIXED via pivot** — `generate_brands` no longer stamps at all. `import_economics.regenerate_brands()` shells out to the Rust binary as step zero of its `main()`, checks return code, raises `_ImportAborted` on non-zero exit (stderr printed before raising). `IMPORT_ECONOMICS_SOURCES` includes the Rust `.rs` files + the wrapper script. Migration `DELETE FROM meta WHERE generator_name='generate_brands'` cleans the orphan row on pre-PR DBs |
| H4 | pre-push no-meta-table wording | **FIXED** — `"pre-push: WARNING — systems.db has no meta stamp — run 'make regen-db' now if this DB was generated by you"` |
### New observation (non-blocking)
The stamp-write failure path in `import_economics` (around line 1271) catches `Exception`, prints a warning, and continues — but does not roll back the data commit (which has already landed). Intentional: the stamp is advisory, and a missing-meta state is caught by `check-systems-db-stamp` as rc=2 (warn-not-block). That's a coherent deliberate choice. Worth a one-line comment "stamp failure is non-fatal; DB data is already committed" so the next reader doesn't wonder if the missing rollback is a bug. Not required before merge.
---
## Tyre — Architecture: APPROVE
The T2 pivot is real architecture, not documentation papering over a smell. Single pipeline owner with one stamp row per `meta` entry; Rust source SHAs fold into `IMPORT_ECONOMICS_SOURCES` so any change to `main.rs` / `names.rs` / the wrapper invalidates the stamp correctly. All 7 addressable items fixed; 2 suggestions deferred to #887/#888.
### Architectural deep-check on the pivot
- **Does `import_economics.py` get too big?** Grew from 1170 → 1306 lines. Growth is `MIGRATION_SQL` expansion + a 30-line `regenerate_brands()` subprocess wrapper + the DELETE-orphan migration. The Python file doesn't absorb brand-generation logic — it shells out. The Rust binary is untouched.
- **Is the Rust binary still independently invokable?** Yes — `tooling/generate-brands` works standalone for debug/experimentation. The `regenerate_brands()` docstring explicitly calls out the non-canonical-seed escape hatch ("callers that need non-canonical seeds must still invoke the wrapper directly — experimentation only"). The "must go through import_economics" rule is self-enforcing via the stamp: wrapper-only runs don't write a stamp, so the next stamp check catches the drift. That's the right level.
- **Migration DELETE idempotency on revert:** `DELETE FROM meta WHERE generator_name = 'generate_brands'` runs on every import. Pull branch → regen → revert: orphan row stays gone because main's `import_economics` won't re-add it. Revert further to pre-PR state where the Makefile called `generate-brands` separately and the binary wrote its own stamp — yes, the row comes back, but that's the correct pre-PR behavior. Clean.
### Round-1 status
| # | Status |
|---|--------|
| T1 (determinism overclaim) | **FIXED** — section renamed "What's deterministic / What's NOT deterministic", explicitly names `generated_at`, rowid drift, freelist reshuffling |
| T2 (stamped on behalf) | **FIXED via pivot** — coupling eliminated, not documented |
| T3 (escape hatch unnamed) | **FIXED** — "The migration escape hatch" section names `MIGRATION_SQL` + `COLUMN_MIGRATIONS` as the sanctioned path and explicitly forbids hand-run `sqlite-exec`, GUI edits, one-off patch scripts, and committing `systems.db` alone |
| T4 (Makefile failure halting) | **FIXED** — `set -e` in a single shell, `ec=0; ... || ec=$?`, tolerates only exit 2 (`import_economics` coverage warning), halts on anything else. Comment cites the prior "stale data, fresh stamp" failure mode |
| T5 (first-push gap) | **FIXED** — compares against `origin/main` (always exists) instead of `origin/$BRANCH`. No false-positive risk: the stamp check validates `meta.generator_sha` vs current source SHAs, so a DB-only commit with untouched generators passes cleanly |
| T6 (fail-open on unknowns) | **FIXED** — unknown `generator_name` returns rc=1 with `"Update GENERATOR_SOURCES in tooling/check-systems-db-stamp to register them before pushing"` on stderr |
| T7 (watch-list drift) | **FIXED** — mutual cross-reference comments exist in both places: `GENERATOR_SOURCES` comment says "Keep this list in sync with IMPORT_ECONOMICS_SOURCES"; the pr-push SKILL watch list cites T7 by name and lists all four `generate_brands` sources |
| T8 (decisions-orphan-tickets CLI) | **DEFERRED** to #887 |
| T9 (semver vs SHA) | **DEFERRED** to #888 |
### No new architectural concerns
Ship it.
---
## Pre-merge action (not a reviewer issue)
PR test plan still has 5 unchecked boxes (pipeline smoke tests). The commit body reports manual verifications were done:
```
Verified:
make regen-db end-to-end — OK
make check-systems-db — OK, 2 generator(s) up to date
STALE detection — OK, verified by touching generate_atlas.py
/pr-push watch list — OK, flags this branch's changed sources
decision show D-159 — OK, structured output with tickets + refs
```
Worth ticking those boxes in the PR description before merge so the evidence is recorded where reviewers expect it, matching the Sprint 36 retro "evidence over aspiration" norm. Not a gate.
## Summary
APPROVED across both reviewers. 14 round-1 items total (4 Hoshe + 9 Tyre + 1 cross-reference) → 0 blockers in round 2. The pivot on T2/H3 is the structural win — fixing the coupling by removing it rather than documenting it.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Sprint 37 CI sweep. Seven tickets, all closed:
metatable stamping (schema_version + generator_sha + generated_at)tooling/check-systems-db-stamp/pr-pushauto-runsmake regen-dbwhen generator sources changed.claude/rules/asset-pipeline.md, CLAUDE.md hook, DEVOPS.md sectiontooling/db/decision show <D-NNN>with implementing tickets + cross-refs;decisions-coveragenow lists ticket IDs per decisionMotivation
Sprint 36 hit a binary merge conflict class when two branches both committed changes to
server/data/systems.db. The meta stamp + pre-push hook +/pr-pushregen form a systemic fix: the canonical rule is "edit sources, not the DB" and the tooling now enforces it.Verification
make regen-dbran successfully; three generators stamped inmetatablemake check-systems-dbexit 0 —check-systems-db-stamp: OK — 3 generator(s) up to dateruff check tooling/— All checks passeddonein the DBTest plan
make regen-dbruns end-to-end on a clean clonemake check-systems-dbexits 0 after a fresh regengit pushwith a modified generator but stale DB — hook rejects/pr-pushon a branch with modifiedtooling/economy-db/import_economics.pyauto-regenerates before pushingtooling/db/decision show D-159returns implementing tickets + refs🤖 Generated with Claude Code
decision show <D-NNN>with implementing tickets + refs a41f7aa06cReview: sprint-37/ci → main (PR #136)
Reviewers: Hoshe (code quality & tests) + Tyre (architecture).
Verdict: CHANGES REQUESTED
Source-canonical rule is a solid design — SHA-of-sources rather than DB-content hash sidesteps SQLite non-determinism cleanly, pre-push hook is the correct enforcement layer,
/pr-pushauto-regen is appropriately scoped. But 5 blocking items + several minor/suggestion items need addressing before merge.Hoshe — Code Quality & Tests: REQUEST_CHANGES
Exit codes are correct, error messages actionable, schema coherent,
show_decisionhandles zero-ticket cases cleanly, pre-push hook integrates without disrupting existing checks. Two real bugs + two nits.tooling/planet-gen/generate_atlas.py:~1383-1391conn.commit()at 1384 persists atlas data, then_write_stamp(conn)issues its ownconn.commit()at line 124. Inconsistent withimport_economics._write_stampwhich leaves transaction ownership to caller. Either remove the inner commit (match the economics pattern — preferred) or remove the outer commit.tooling/check-systems-db-stamp:~48-54(file_sha1)if p.exists(): h.update(...)). If a generator source file is moved/deleted, validator computes SHA-1 of zero bytes (alwaysda39a3ee...). The stored SHA won't match → push correctly rejected, but the error message says "STALE" with no hint that the file is missing. Edge case: if both stored and current SHA converge on the empty hash, check passes vacuously. Add explicit "source file not found: " error with exit 1.tooling/check-systems-db-stamp:~32-37(GENERATOR_SOURCES)generate_brandssource is listed but no stamp writer in the Rust binary — it's stamped on behalf byimport_economics._write_stamp. Pre-existing design constraint, not a regression, but should be called out in the meta table schema comment or aTODOso the next engineer doesn't add an independent stamp path that races..config/hooks/pre-push(stamp section, last ~30 lines)rc=2(no meta table) is advisory (intended grace for new DBs). But the warning message "runmake regen-dbafter next regeneration" reads like a future action. Suggest: "systems.db has no meta stamp — runmake regen-dbnow if this DB was generated by you."Tyre — Architecture: REQUEST_CHANGES
The core design is elegant where it matters. SHA-of-sources (not hash-of-DB) is the right choice because it sidesteps SQLite's non-determinism (timestamps, autoincs, rowids). Pre-push is the correct enforcement layer.
/pr-pushauto-regen is scoped tight to generator-touching branches. But three cracks need patching, plus one Makefile failure-handling bug..claude/rules/asset-pipeline.md:~95("Why direct DB edits are forbidden")meta.generated_atusesdatetime('now'), SQLite rowids/autoinc aren't stable across runs. The stamp is deterministic, not the DB binary. The rule's strength doesn't depend on byte-determinism — so soften the claim to "the stamp is deterministic (same generator source → same recorded SHA)" and remove the byte-for-byte assertion. Otherwise reviewers expect binary-identical DBs after regen and are confused when diffs appear.tooling/economy-db/import_economics.py:1194+tooling/check-systems-db-stamp:37-43generate_brandsstamped on behalf byimport_economics, not by its own Rust binary. Undocumented coupling in the rule's "Three generators write to systems.db" table. Two real failure modes: (a) someone runstooling/generate-brandsdirectly (the rule's documented invocation) withoutimport_economicsafter it → SHA drifts from stamp; (b) brands source changes, economics doesn't → any workflow invoking only the Rust binary silently skips stamping. Fix: either make the Rust binary stamp itself (~30 LOC withrusqlite), or document the coupling explicitly and remove "on behalf" as an invisible convention. Hoshe also flagged this (H3)..claude/rules/asset-pipeline.md(whole doc)MIGRATION_SQLinimport_economics.pyis the de-facto location — the rule should state that explicitly and forbid any other form of direct DB edit (no hand-runsqlite-exec, no one-off patches). Otherwise the rule has a loophole an operator can drive a truck through.Makefile:357-366(regen-dbtarget)generate_brands(step 1) fails,import_economicsstill runs and stamps → DB with stale brand data and a "fresh" stamp. Addset -esemantics or|| exit 1after each step, or use a.SHELLFLAGSguard..config/hooks/pre-push:~155check-systems-db-stampunconditionally whenserver/data/systems.dbwas modified in ANY branch commit, regardless of remote-ref state. Alternatively, document the gap.tooling/check-systems-db-stamp:61-65generator_namein meta silently skipped ("forward compat"). Wrong default for a safety gate — a future branch adding a new generator without registering it here becomes a no-op check, silently. Warn loudly on unknown names, or require explicit allowlist bump. Fail closed..claude/skills/pr-push/SKILL.mdstep 4atooling/economy-db/import_economics.py,tooling/planet-gen/generate_atlas.py,server/src/bin/generate_brands/main.rs, etc.) plus content dirs. This will drift fromGENERATOR_SOURCESincheck-systems-db-stamp. Two sources of truth in a system whose purpose is source-canonical discipline is exactly what this PR is trying to prevent. Shell out to a registry script, or at minimum add mutual cross-reference comments.tooling/db/decisions_sync.py:417-457(show_decision)tickets.decision_refis the right direction (no two-way sync). One note:decision_refis free-text without a foreign key, so typo'd or deleted D-IDs silently don't show up anywhere. Not blocking; worth a follow-updecisions-orphan-ticketsCLI to surface mismatches.server/data/systems-schema.sql(meta table)schema_version TEXTuses SHA-1 of the schema file. The rule doc names "savegame migration lineage" as a future use. When that arrives, a content-hash is harder to order than a monotonic version number — you cannot tell from two SHAs which is newer. Don't change now; flag for the migration phase:v1.0-style semver would serve lineage better.Process note
PR test plan has 5 unchecked boxes:
make regen-dbruns end-to-end on a clean clonemake check-systems-dbexits 0 after a fresh regengit pushwith modified generator but stale DB — hook rejects/pr-pushon a branch with modified generator auto-regenerates before pushingtooling/db/decision show D-159returns implementing tickets + refsThese are NOT merge-path per skill 0b-i (no pre-game/scene/save/connection code). No automatic verdict downgrade. But they ARE the pipeline's own smoke tests — the features this PR ships. Worth running before the next push so the verification is concrete, not aspirational.
Summary of blocking issues
5
error/major-severity fixes required:generate_atlas._write_stamp(inconsistent with economics pattern)generate_brandssilently stamped "on behalf" by economics (also H3). Either self-stamp or document the coupling explicitly.Makefile:regen-dbdoesn't halt on generator failure → can produce stale-data-fresh-stamp state.Other items are minor/suggestion; address before merge or push back with rationale.
Review: sprint-37/ci → main (PR #136) — Round 2
Reviewers: Hoshe (code quality & tests) + Tyre (architecture).
Verdict: APPROVED
3 → 0 blockers across 2 rounds. All round-1 items correctly addressed. The T2 pivot (merging the brand pipeline into
import_economicsinstead of documenting the stamp-on-behalf coupling) is architecture, not paper.Hoshe — Code Quality & Tests: APPROVE
All 4 round-1 items fixed. H2 error-handling path is consistent across
import_economics,generate_atlas, andcheck-systems-db-stamp. The H3 architectural pivot is clean.Round-1 status
generate_atlas._write_stampdouble-commitconn.commit()removed; caller wraps stamp + data intry/exceptwith rollback. Data and stamp land atomically or not at all_file_sha1now raisesFileNotFoundErrorwith formatted path.check()loop catches it and returns rc=1 with"BROKEN — {generator_name}: {exc}"on stderr. Not swallowed. Consistent across all three filesgenerate_brandsno longer stamps at all.import_economics.regenerate_brands()shells out to the Rust binary as step zero of itsmain(), checks return code, raises_ImportAbortedon non-zero exit (stderr printed before raising).IMPORT_ECONOMICS_SOURCESincludes the Rust.rsfiles + the wrapper script. MigrationDELETE FROM meta WHERE generator_name='generate_brands'cleans the orphan row on pre-PR DBs"pre-push: WARNING — systems.db has no meta stamp — run 'make regen-db' now if this DB was generated by you"New observation (non-blocking)
The stamp-write failure path in
import_economics(around line 1271) catchesException, prints a warning, and continues — but does not roll back the data commit (which has already landed). Intentional: the stamp is advisory, and a missing-meta state is caught bycheck-systems-db-stampas rc=2 (warn-not-block). That's a coherent deliberate choice. Worth a one-line comment "stamp failure is non-fatal; DB data is already committed" so the next reader doesn't wonder if the missing rollback is a bug. Not required before merge.Tyre — Architecture: APPROVE
The T2 pivot is real architecture, not documentation papering over a smell. Single pipeline owner with one stamp row per
metaentry; Rust source SHAs fold intoIMPORT_ECONOMICS_SOURCESso any change tomain.rs/names.rs/ the wrapper invalidates the stamp correctly. All 7 addressable items fixed; 2 suggestions deferred to #887/#888.Architectural deep-check on the pivot
import_economics.pyget too big? Grew from 1170 → 1306 lines. Growth isMIGRATION_SQLexpansion + a 30-lineregenerate_brands()subprocess wrapper + the DELETE-orphan migration. The Python file doesn't absorb brand-generation logic — it shells out. The Rust binary is untouched.tooling/generate-brandsworks standalone for debug/experimentation. Theregenerate_brands()docstring explicitly calls out the non-canonical-seed escape hatch ("callers that need non-canonical seeds must still invoke the wrapper directly — experimentation only"). The "must go through import_economics" rule is self-enforcing via the stamp: wrapper-only runs don't write a stamp, so the next stamp check catches the drift. That's the right level.DELETE FROM meta WHERE generator_name = 'generate_brands'runs on every import. Pull branch → regen → revert: orphan row stays gone because main'simport_economicswon't re-add it. Revert further to pre-PR state where the Makefile calledgenerate-brandsseparately and the binary wrote its own stamp — yes, the row comes back, but that's the correct pre-PR behavior. Clean.Round-1 status
generated_at, rowid drift, freelist reshufflingMIGRATION_SQL+COLUMN_MIGRATIONSas the sanctioned path and explicitly forbids hand-runsqlite-exec, GUI edits, one-off patch scripts, and committingsystems.dbaloneset -ein a single shell, `ec=0; ...origin/main(always exists) instead oforigin/$BRANCH. No false-positive risk: the stamp check validatesmeta.generator_shavs current source SHAs, so a DB-only commit with untouched generators passes cleanlygenerator_namereturns rc=1 with"Update GENERATOR_SOURCES in tooling/check-systems-db-stamp to register them before pushing"on stderrGENERATOR_SOURCEScomment says "Keep this list in sync with IMPORT_ECONOMICS_SOURCES"; the pr-push SKILL watch list cites T7 by name and lists all fourgenerate_brandssourcesNo new architectural concerns
Ship it.
Pre-merge action (not a reviewer issue)
PR test plan still has 5 unchecked boxes (pipeline smoke tests). The commit body reports manual verifications were done:
Worth ticking those boxes in the PR description before merge so the evidence is recorded where reviewers expect it, matching the Sprint 36 retro "evidence over aspiration" norm. Not a gate.
Summary
APPROVED across both reviewers. 14 round-1 items total (4 Hoshe + 9 Tyre + 1 cross-reference) → 0 blockers in round 2. The pivot on T2/H3 is the structural win — fixing the coupling by removing it rather than documenting it.
Pull request closed