§22.13 step 1: default-project-id re-stamp (v0.39.0)

projects.restamp_default_project(config): at startup after the registry mirror,
renames project_id from the M1 bootstrap 'default' to the configured
DEFAULT_PROJECT_ID across every project-scoped table (discovered by column) and
drops the stale 'default' projects row, so a deployment's original corpus lands
at a meaningful /p/<id>/ and 'default' is never a public URL. FK off for the
rename (parent+children move together) + foreign_key_check backstop. Idempotent;
no-op unless DEFAULT_PROJECT_ID is set to a non-'default' value.

test_restamp_default_project.py (3 tests). 450 backend green.

This is the last framework piece for OHM's clean /p/ohm/ cutover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-04 07:45:10 -07:00
parent 508a8cb6d0
commit 33c67ccc09
6 changed files with 170 additions and 2 deletions
+30
View File
@@ -23,6 +23,36 @@ skip versions are the composition of each intervening adjacent
release's steps in order — no A-to-B path is pre-computed beyond release's steps in order — no A-to-B path is pre-computed beyond
that. that.
## 0.39.0 — 2026-06-04
**Minor — §22.13 step 1: the default-project-id re-stamp. A deployment can
move its original corpus off the bootstrap `default` id onto a meaningful slug
(e.g. `ohm`) so it lands at `/p/<id>/` and `default` is never a public URL.
No-op unless `DEFAULT_PROJECT_ID` is set to a non-`default` value.**
Added:
- **`projects.restamp_default_project(config)`** — at startup, after the
registry mirror, if `DEFAULT_PROJECT_ID` resolves to a non-`default` id and
bootstrap-stamped rows still exist, it renames `project_id` from `default` to
the configured id across **every** project-scoped table (discovered by
column, so it stays correct as the schema grows) and drops the stale
`default` `projects` row (its data has moved to the configured row the
registry mirror created). The rename runs with FK enforcement off — parent
and child rows move together, so the composite FKs stay consistent — with a
`foreign_key_check` backstop before commit. Idempotent.
- **Tests:** `test_restamp_default_project.py` — data + composite-FK children
move to the new id, the stale row is dropped, FK integrity holds, the second
call is a no-op, and an unset `DEFAULT_PROJECT_ID` leaves `default` in place.
450 backend green.
Upgrade steps:
1. **MAY** set `DEFAULT_PROJECT_ID=<slug>` in the backend overlay and add the
matching project (same `id`) to `projects.yaml`. On the next deploy the
re-stamp moves the original corpus onto `<slug>` once; `default` URLs never
become public. Leave it unset to keep the `default` id (no change).
## 0.38.0 — 2026-06-04 ## 0.38.0 — 2026-06-04
**Minor — §22 M3-backend Plan B (write path, propose): a new entry can be **Minor — §22 M3-backend Plan B (write path, propose): a new entry can be
+1 -1
View File
@@ -1 +1 @@
0.38.0 0.39.0
+5
View File
@@ -112,6 +112,11 @@ async def lifespan(app: FastAPI):
raise RuntimeError( raise RuntimeError(
f"registry mirror failed at startup ({config.registry_repo_full}/projects.yaml): {e}" f"registry mirror failed at startup ({config.registry_repo_full}/projects.yaml): {e}"
) from e ) from e
# §22.13 step 1: re-stamp the M1 bootstrap 'default' project id to the
# deployment's configured id (DEFAULT_PROJECT_ID) once the registry row
# exists, so the original corpus lands at a meaningful /p/<id>/ and
# 'default' is never a public URL. Idempotent no-op once done.
projects.restamp_default_project(config)
if projects.default_content_repo(config) is None: if projects.default_content_repo(config) is None:
raise RuntimeError( raise RuntimeError(
f"registry does not describe the default project " f"registry does not describe the default project "
+69
View File
@@ -7,9 +7,13 @@ the registry mirror (`registry.refresh_registry`) is authoritative.
""" """
from __future__ import annotations from __future__ import annotations
import logging
from . import db from . import db
from .config import Config from .config import Config
log = logging.getLogger(__name__)
DEFAULT_PROJECT_ID = "default" DEFAULT_PROJECT_ID = "default"
@@ -20,6 +24,71 @@ def resolved_default_id(config: Config) -> str:
return config.default_project_id.strip() or DEFAULT_PROJECT_ID return config.default_project_id.strip() or DEFAULT_PROJECT_ID
def restamp_default_project(config: Config) -> None:
"""§22.13 step 1 — one-time rename of the M1 bootstrap project id
(DEFAULT_PROJECT_ID = 'default') to the deployment's configured default id
(the DEFAULT_PROJECT_ID env var, e.g. 'ohm'), so the deployment's original
corpus lands at a meaningful `/p/<id>/` and `default` is never a public URL.
Renames `project_id` across every project-scoped table (discovered by
column, so it stays correct as the schema grows), then drops the stale
bootstrap `projects` row (its data has moved to the configured row, which
the registry mirror already created). Idempotent and a no-op when the
configured id is still 'default' or no bootstrap rows remain. Runs at
startup after the registry mirror, with FK enforcement off for the rename
(the composite FKs are kept consistent because parent and child rows are
renamed together) and a foreign_key_check backstop before commit.
"""
target = resolved_default_id(config)
if target == DEFAULT_PROJECT_ID:
return
conn = db.conn()
has_rows = conn.execute(
"SELECT 1 FROM cached_rfcs WHERE project_id = ? LIMIT 1", (DEFAULT_PROJECT_ID,)
).fetchone()
stale_proj = conn.execute(
"SELECT 1 FROM projects WHERE id = ? LIMIT 1", (DEFAULT_PROJECT_ID,)
).fetchone()
if not has_rows and not stale_proj:
return
if conn.execute("SELECT 1 FROM projects WHERE id = ? LIMIT 1", (target,)).fetchone() is None:
log.warning("restamp: target project %r not in registry yet; skipping", target)
return
tables = [r["name"] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")]
pid_tables = [
t for t in tables
if any(c["name"] == "project_id" for c in conn.execute(f"PRAGMA table_info({t})"))
]
conn.execute("PRAGMA foreign_keys = OFF")
try:
conn.execute("BEGIN")
for t in pid_tables:
conn.execute(
f"UPDATE {t} SET project_id = ? WHERE project_id = ?",
(target, DEFAULT_PROJECT_ID),
)
# The bootstrap row's data has moved to the configured (registry) row.
conn.execute("DELETE FROM projects WHERE id = ?", (DEFAULT_PROJECT_ID,))
violations = conn.execute("PRAGMA foreign_key_check").fetchall()
if violations:
conn.execute("ROLLBACK")
raise RuntimeError(
f"restamp left foreign-key violations: {[tuple(v) for v in violations]}"
)
conn.execute("COMMIT")
except Exception:
try:
conn.execute("ROLLBACK")
except Exception:
pass
raise
finally:
conn.execute("PRAGMA foreign_keys = ON")
log.info("restamp: renamed bootstrap project %r -> %r across %d tables",
DEFAULT_PROJECT_ID, target, len(pid_tables))
def default_content_repo(config: Config) -> str | None: def default_content_repo(config: Config) -> str | None:
"""The content repo the single-corpus mirror reads, from the default """The content repo the single-corpus mirror reads, from the default
project's row (filled by the registry mirror). Replaces the retired project's row (filled by the registry mirror). Replaces the retired
@@ -0,0 +1,64 @@
"""§22.13 step 1 — the bootstrap-id re-stamp: 'default' → the configured
DEFAULT_PROJECT_ID across every project-scoped table, with the composite FKs
kept intact and the stale 'default' projects row dropped. Idempotent."""
from __future__ import annotations
import tempfile
from pathlib import Path
import app.db as db
from app import projects
class _Cfg:
def __init__(self, path, default_id):
self.database_path = path
self.default_project_id = default_id
def _setup(monkeypatch, default_id="ohm"):
path = str(Path(tempfile.mkdtemp()) / "t.db")
cfg = _Cfg(path, default_id)
db.run_migrations(cfg)
monkeypatch.setattr(db, "_CONN", db.connect(path))
conn = db.conn()
# M1 bootstrap row + a registry-mirrored 'ohm' row coexist pre-restamp.
conn.execute("INSERT OR IGNORE INTO projects (id,name,type,content_repo,visibility,initial_state) "
"VALUES ('default','Bootstrap','document','ohm-content','public','super-draft')")
conn.execute("INSERT OR IGNORE INTO projects (id,name,type,content_repo,visibility,initial_state) "
"VALUES ('ohm','Open Human Model','document','ohm-content','public','super-draft')")
conn.execute("INSERT INTO users (id,gitea_login,display_name,role) VALUES (1,'a','A','contributor')")
# default-stamped data with a composite-FK child
conn.execute("INSERT INTO cached_rfcs (slug,title,state,project_id) VALUES ('human','Human','active','default')")
conn.execute("INSERT INTO rfc_collaborators (rfc_slug,user_id,role_in_rfc,project_id) "
"VALUES ('human',1,'contributor','default')")
conn.execute("INSERT INTO stars (user_id,rfc_slug,project_id) VALUES (1,'human','default')")
return cfg, conn
def test_restamp_moves_data_and_drops_bootstrap_row(monkeypatch):
cfg, conn = _setup(monkeypatch, default_id="ohm")
projects.restamp_default_project(cfg)
assert conn.execute("SELECT COUNT(*) c FROM cached_rfcs WHERE project_id='default'").fetchone()["c"] == 0
assert conn.execute("SELECT project_id FROM cached_rfcs WHERE slug='human'").fetchone()["project_id"] == "ohm"
assert conn.execute("SELECT project_id FROM rfc_collaborators WHERE rfc_slug='human'").fetchone()["project_id"] == "ohm"
assert conn.execute("SELECT project_id FROM stars WHERE rfc_slug='human'").fetchone()["project_id"] == "ohm"
# stale bootstrap projects row removed; 'ohm' remains
assert conn.execute("SELECT 1 FROM projects WHERE id='default'").fetchone() is None
assert conn.execute("SELECT 1 FROM projects WHERE id='ohm'").fetchone() is not None
# FK integrity intact after the rename
assert conn.execute("PRAGMA foreign_key_check").fetchall() == []
def test_restamp_is_idempotent(monkeypatch):
cfg, conn = _setup(monkeypatch, default_id="ohm")
projects.restamp_default_project(cfg)
projects.restamp_default_project(cfg) # second call: no rows left → no-op
assert conn.execute("SELECT COUNT(*) c FROM cached_rfcs WHERE project_id='ohm'").fetchone()["c"] == 1
def test_restamp_noop_when_default_id_unchanged(monkeypatch):
cfg, conn = _setup(monkeypatch, default_id="") # resolves to 'default'
projects.restamp_default_project(cfg)
# nothing renamed; bootstrap data + row still present
assert conn.execute("SELECT project_id FROM cached_rfcs WHERE slug='human'").fetchone()["project_id"] == "default"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "rfc-app-frontend", "name": "rfc-app-frontend",
"private": true, "private": true,
"version": "0.38.0", "version": "0.39.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",