33c67ccc09
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>
121 lines
5.0 KiB
Python
121 lines
5.0 KiB
Python
"""Project registry — the §22 multi-project layer.
|
|
|
|
A deployment hosts one or more projects (§22.1). The git registry mirror
|
|
that lets a deployment declare projects lands in M3 and drives this module.
|
|
`seed_default_project` (the §22.13 META_REPO backfill) is retired in M3;
|
|
the registry mirror (`registry.refresh_registry`) is authoritative.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from . import db
|
|
from .config import Config
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
DEFAULT_PROJECT_ID = "default"
|
|
|
|
|
|
def resolved_default_id(config: Config) -> str:
|
|
"""The id of the deployment's bootstrap/default project. Plan A: always
|
|
'default' (the re-stamp to a config slug rides Plan B). The config knob is
|
|
read here so Plan B can flip the resolution without touching call sites."""
|
|
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:
|
|
"""The content repo the single-corpus mirror reads, from the default
|
|
project's row (filled by the registry mirror). Replaces the retired
|
|
META_REPO. None until the registry mirror has run."""
|
|
row = db.conn().execute(
|
|
"SELECT content_repo FROM projects WHERE id = ?",
|
|
(resolved_default_id(config),),
|
|
).fetchone()
|
|
return row["content_repo"] if row and row["content_repo"] else None
|
|
|
|
|
|
def content_repo(project_id: str) -> str | None:
|
|
"""The content repo for a specific project (§22.3). None if unknown/unset.
|
|
The per-project successor to `default_content_repo` for the write path."""
|
|
row = db.conn().execute(
|
|
"SELECT content_repo FROM projects WHERE id = ?", (project_id,)
|
|
).fetchone()
|
|
return row["content_repo"] if row and row["content_repo"] else None
|
|
|
|
|
|
def project_initial_state(project_id: str) -> str:
|
|
"""§22.4b landing state for new entries in a project. Defaults to
|
|
'super-draft' for an unknown/unset row (the safe, today's-flow default)."""
|
|
row = db.conn().execute(
|
|
"SELECT initial_state FROM projects WHERE id = ?", (project_id,)
|
|
).fetchone()
|
|
if row is None or not row["initial_state"]:
|
|
return "super-draft"
|
|
return row["initial_state"]
|