Commit Graph

8 Commits

Author SHA1 Message Date
Ben Stull 4e7410f90b fix(§22/G-15): make the branch/edit/body subsystem three-tier aware (v0.53.0)
§22 migrated the READ/catalog path to the three-tier (project/collection)
model but the WRITE/branch/body subsystem still hardcoded the default
project's default collection — resolving every meta-resident entry to the
default content repo at rfcs/<slug>.md, ignoring the entry's project (its own
content repo) and collection (a <subfolder>/rfcs/ prefix). An entry outside
the default collection rendered a blank canonical body and every edit/PR/
body-write path hit the wrong file.

- New single resolver: projects.content_repo_for_collection() +
  projects.entry_location(config, cid, slug) -> (org, repo, md_path)
  (collection -> project -> content_repo + subfolder; falls back to the
  default repo for a legacy/unknown collection).
- Every entry write/read path resolves via it: api_branches (body GET + all
  branch write paths), api_prs (pr-draft/open/merge/withdraw/review/
  resolution-branch), api_graduation (graduate/claim/retire/unretire +
  orchestrator + state-flip), api.py mark-reviewed + idea-PR merge/decline/
  withdraw + proposal preview, api_metadata. bot.open_metadata_pr and
  bot.mark_entry_reviewed gained a file_path param. refresh_meta_branches,
  the webhook corpus-refresh dispatch, and the hygiene branch-delete now
  span every project's content repo, not just the default.
- New additive collection-scoped body-read routes:
  GET /api/projects/{pid}/collections/{cid}/rfcs/{slug}/main and
  .../branches/{branch} disambiguate a slug across collections (G-5) and read
  the entry's own repo. Slug-only routes kept (now collection-aware via the
  cached row). Frontend getRFCMain/getBranch take optional pid+cid; RFCView
  threads them (mirrors the v0.52.1 entry-detail fix).

No migration, no config change. Existing default-collection entries
unaffected. Tests: backend resolver + collection-scoped branch/body + graduate-
in-subfolder write path; frontend api unit. backend 677 / frontend 60 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:39:46 -07:00
Ben Stull 620926b834 fix(§22): heal mig-029 vs registry-mirror collection-id divergence
On a deployment that already had >=2 projects when migration 029 ran, 029
seeds the default project's collection id as the project id (e.g. 'ohm'),
but the registry mirror expects 'default' -> it inserted a duplicate empty
'default' collection, orphaning the entries.

Adds projects.reconcile_default_collection_id() -- the collection-grain twin
of restamp_default_project -- run at startup BEFORE the mirror so it merges
onto the canonical 'default' collection instead of duplicating. Renames the
divergent collection id and cascades collection_id across all keyed tables
(FK-off atomic rename + foreign_key_check). Idempotent; no-op on fresh /
single-project / already-aligned deploys. Seam tests show the duplicate forms
without the fix and merges with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:54:52 -07:00
Ben Stull 9ca07a3f81 §22 S1: thread collection_id through backend + update tests (N=1 unchanged, 454 green)
- collections.py resolution helpers (default_collection_id, type, initial_state)
- registry mirror writes project grouping fields + default-collection corpus fields
- auth.project_of_rfc joins collections; project_member_role reads memberships
- cache/api_*/funder writers+readers re-keyed to collection_id (cached_prs + denormalised
  project_id tags unchanged); api_deployment reads type/initial_state from the default collection
- projects.py restamp detects bootstrap via collections; initial_state via collection
- tests updated to the three-tier schema; test_migration_028 retired (superseded by 029)
- add @S1 acceptance test (collection grain + N=1 serving)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:26:14 -07:00
Ben Stull 33c67ccc09 §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>
2026-06-04 07:45:10 -07:00
Ben Stull fec51bdbb6 §22 M3-backend Plan B (write path, propose): project-scoped propose (v0.38.0)
A new entry can be proposed into a specific project; it lands in that project's
content repo and shows under that project's proposals. A non-default project is
no longer read-only.

- api.py: POST /api/projects/{pid}/rfcs/propose (propose body extracted into a
  project-parameterized helper; unscoped /api/rfcs/propose kept as default
  compat). Slug uniqueness, idea-PR reservation, landing state, and the
  proposed_use_cases row scoped to the target project. GET
  /api/projects/{pid}/proposals.
- cache.py: refresh_meta_pulls loops every project's content_repo, stamping
  cached_prs.project_id; projects.content_repo(pid) helper.
- frontend: proposeRFC(projectId,…)/listProposals(projectId); ProposeModal
  takes projectId; App resolves current project from the /p/<id>/ URL; Catalog
  lists that project's proposals.
- tests: test_project_scoped_propose.py (lands scoped + gated 404). 447 backend
  + 11 Vitest green; clean build.

Known limitation: branch/PR/graduation edit flows + default-id re-stamp not yet
scoped (next slice). Per docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:02:19 -07:00
Ben Stull cecc6c0b41 feat(registry): hard-cut META_REPO -> REGISTRY_REPO; wire mirror into startup/webhook/sweep
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:16:51 -07:00
Ben Stull 1dab24eef0 Merge feat/multi-project (§22 M1+M2) into main
Integrates the multi-project work so far — the §22 design drafts, M1 (the
schema spine), and M2 (project-scoped authorization + the §22.7 resolver) —
on top of the v0.32.0 retire/§13 changes that landed on main meanwhile.

Integration decisions:
- Renumbered the M1 projects migration 025_projects.sql -> 026_projects.sql.
  v0.32.0 shipped 025_retired_state.sql, which rebuilds cached_rfcs and
  predates the project_id column; running projects *after* the retire rebuild
  is required so project_id survives on a fresh database (the runner applies
  *.sql in filename order). Comment/doc references bumped to match.
- Resolved the get_rfc conflict in api.py by composing both gates: compute the
  viewer once, apply the §22.5 visibility gate, then v0.32.0's §13.7
  retired-entry owner-only check.
- api_graduation.py / api_discussion.py auto-merged cleanly (M2's threaded
  viewer/visibility gate coexists with the new retire endpoints + retired
  state).

Full suite 401 passed on a fresh DB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 04:56:39 -07:00
Ben Stull 6f901e3e2c feat(projects): M1 — the multi-project schema spine (§22)
Lays the additive foundation for hosting N projects per deployment, with
today's single corpus as the N=1 case. No behavior change: the app runs
exactly as before, single project, with the spine underneath.

- migration 025: `projects` + `project_members` tables; seed the `default`
  project (visibility=public, preserving open-by-default); thread
  `project_id NOT NULL DEFAULT 'default'` onto all 19 slug-bearing tables,
  backfilling existing rows. Additive — no table rebuilds; the slug-keyed
  uniqueness/PK rework is enumerated in the migration header and deferred to
  the slice that activates project #2.
- app/projects.py: §22.13 startup backfill of the default project's
  content_repo from META_REPO (idempotent — never clobbers a value the
  future registry mirror sets).
- config: REGISTRY_REPO wired (optional through M1; consumed by the M3
  mirror), documented in .env.example as META_REPO's successor.
- tests: 6 vertical assertions on the spine (seed, backfill, column shape,
  role CHECK, idempotency, config). Full suite 381 passed.
- docs: align Part C M1/M3 boundaries with the landed code (registry mirror
  + redirect move to M3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:14:53 -07:00