Solution Design for a generic per-collection metadata system — typed fields
(enum/tags/text) declared in .collection.yaml, per-entry values in a clean
<slug>.meta.yaml sidecar, schema-derived faceted left-pane filtering, and
single/bulk direct-commit tag/untag. Restores the retired BDD Release Planner's
corpus-annotation half inside the framework; release planning stays downstream.
Output of discovery session OHM-0079.0. Proposed as §23; flag for reconciliation
with §22 S6 type-modules at the SPEC merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the global-Owner create-project action and the role-aware deployment
directory, completing slice S5 of the three-tier refactor (release v0.44.0,
minor/non-breaking — no migration). Per
docs/design/2026-06-05-three-tier-projects-collections.md Part E (S5),
Part C.3 (C3.1–C3.2).
Backend:
- auth.can_create_project — global-Owner gate (deployment owner/admin or an
explicit scope_type='global' Owner grant).
- bot.create_project — provision the Gitea content repo (seed README so main
exists), read+append+commit projects.yaml in the registry repo, audit-log
(create_project). The bot stays the only git writer (§1).
- POST /api/projects (api_deployment) — global-Owner gated; validates the id
(slug, not 'default'), name, type, visibility, content_repo; commits via the
bot, then re-mirrors the registry so projects + default collection rows flow
from git (§22.2). GET /api/deployment gains viewer.can_create_project and
default_project_readable. make_router now takes gitea + bot.
Frontend:
- api.createProject; DeploymentProvider surfaces viewer + defaultProjectReadable
+ refresh; DeploymentLanding redirects into the default only when readable
(gated/absent default falls through to the directory, no 404 bounce).
- Directory.jsx role-aware empty states (C3.1 "Create your first project" CTA;
C3.2 "Nothing has been shared with you yet") + Owner-only "New project"
control + CreateProjectModal.
Tests: backend test_create_project_vertical.py (vertical + gates + the
deployment empty-state signals); frontend Directory.test.jsx empty-state cases.
Also: ignore the session-local .superpowers/ tooling dir.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump VERSION + frontend/package.json to 0.43.0, add the CHANGELOG entry
(minor, non-breaking — additive endpoints + UI, no migration, no change to
existing authz outcomes), and mark Part E slice S4 shipped in the design doc.
Completes @S4 (C2.1–C2.7 invitation + C3.3–C3.5 role-aware empty states).
Next: S5 (in-app create-project + the global directory, C3.1–C3.2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement slice S3 of the §22 three-tier refactor: the four-layer
most-permissive scope-role resolver (§B.2) over {owner, contributor}
grants at {global, project, collection}, with the §22.5 visibility gate
enforced at the collection grain.
- migration 030: memberships.scope_type += 'global' (the global RFC
Contributor tier; sentinel scope_id '*').
- auth.effective_scope_role folds global → project → collection,
most-permissive, no negative override; can_read_collection /
can_contribute_in_collection / is_collection_superuser /
can_create_collection gate reads, writes, admin, and create.
- collection-grain visibility: a gated collection is hidden from the
public (404, omitted from the directory) yet visible+listed for a
scope-role holder; a collection may be set only as strict or stricter
than its project (public < unlisted < gated), validated at create and
clamped at the mirror.
- entry-scoped authority (mark-reviewed, graduate, branch read/contribute,
PR/discussion/contribution moderation) re-pointed from the project grain
to the entry's collection.
- create-collection authority widened to a project/global-scope grant
holder (§B.1), not only a deployment owner/admin.
Keystone reconciliation (session 0076): a plain granted account is a
granted *account*, not a write-everywhere global role; the implicit-public
write baseline is grandfathered onto the migration-seeded `default`
collection only, so the N=1 deployment loses no capability. Reinterprets
§B.1/§B.3 literally — flagged for the SPEC merge (S6).
Completes @S3 (C1.1–C1.8). Tests: test_s3_scope_roles_vertical.py (8 C.1
scenarios + visibility/strictness), test_migration_030_global_scope.py.
Full backend suite 493 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan for slice S2 of the three-tier (deployment→project→collection) refactor.
Completes acceptance @S2 (C3.6). See
docs/design/2026-06-05-three-tier-projects-collections.md Part E.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per operator (session 0072): every slice must end in a usable deployment and
declare which Part C scenarios it makes pass. Tag all 23 Gherkin scenarios with
@S<n> (the slice that completes them) and re-cut Part E from layer-by-layer
(N1-N6) to usable increments (S1-S6) with a slice->scenario index table. S1
bundles the coupled migration 029 + threading + redirect as one right-sized
first session; S2 second collection; S3 role enforcement; S4 invitation;
S5 create-project + directory; S6 types + SPEC merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Operator chose (session 0072) to add the collection grain beneath today's
project: projects table stays the group tier (keeps content_repo), a new
collections table holds the per-corpus fields, entries re-key to
(collection_id, slug), one default collection per project on migrate, breaking
/p/<project>/e/<slug> -> /p/<project>/c/<collection>/e/<slug> with 308s.
Re-sloted slices N1-N6. Correction banners updated from pending -> decided.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-checked code vs the stale memory: migration 028 (slug PK -> (project_id,
slug)), v0.35.0 /p/<project>/ routing, and v0.37/0.38 per-project read+propose
are all shipped to main. The 'fold into not-yet-shipped Plan B + M3-frontend'
premise is false. Neutralize the wrong claims in §0/§A.3 and flag Part E's
sequencing as pending re-decision; structural model (Parts A-D) unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Splits the original §22 two-tier model (deployment → project=corpus) into
three tiers (deployment → project → RFC collection). Project owns one
content repo; collections are typed subfolders declared by .collection.yaml
manifests (git-truth). Reconciles the accumulated role vocabulary onto one
{owner, contributor} enum attached at {global, project, collection}, with
downward additive inheritance and no negative override. Adds BDD scenarios
(Part C) for role usage, invitation, and empty states. Re-slots the roadmap
to fold the tier into the not-yet-shipped Plan B (mig 028) + M3-frontend.
Session 0072 (spec). Revises docs/design/multi-project-spec.md §22.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Builds the three-tier authorization resolver on M1's schema spine: the
most-permissive union of deployment role (§6.1), project role (§22.6), and
per-RFC authority (§6.3/§12), with the §22.5 visibility gate subtractive on
top (§22.7). Pure app-layer — no migration (M1 shipped the tables), no
behavior change on the public default project. Verifiable on the single
default project by flipping its visibility and granting/revoking roles.
- app/auth.py: the §22.7 resolver primitives — project_visibility,
project_member_role, project_of_rfc, is_project_superuser, can_read_project,
effective write/discuss standing (can_contribute_in_project /
can_discuss_in_project), require_project_readable, visible_project_ids.
The three per-RFC capability helpers (can_discuss_rfc / can_contribute_to_rfc
/ can_invite_to_rfc) now compose all three tiers, so the ~20 call sites
inherit M2 unchanged.
- Read pass: the §22.5 visibility gate (404 to non-members) threaded into
every RFC-resolution helper across api.py / api_branches / api_prs /
api_graduation / api_discussion / api_contributions / api_invitations, plus
the catalog + proposals listings filtered by visible_project_ids.
- Write pass: the deep gates (branch read/contribute/owner, PR merge/withdraw/
edit, graduation, discussion resolve) fold in project_admin via
is_project_superuser; the "any-contributor" branch mode routes through
can_contribute_in_project; propose + claim gate on project contribute
standing. Deployment-level surfaces (admin idea-PR merge/decline, account
notification-mute) stay deployment-scoped.
- Operator decisions: implicit-on-public (a granted deployment contributor
keeps its pre-M2 write baseline on a public project, no membership row, so
the N=1 case stays whole) and preserve-curation (that baseline does not
override per-RFC owner curation — only an explicit project grant or a
deployment owner/admin does), keeping the v0.16.0 per-RFC invite contract
intact on public.
- tests: test_multi_project_authz_vertical.py — 9 vertical assertions on the
resolver tiers, public-unchanged regression, the gated 404 read gate, the
gated contribution gate, union + subtractive visibility, and revocation.
Full suite 390 passed.
- docs: mark Part C M2 "(landed)" with the two operator decisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Default project id: config-derived slug (DEFAULT_PROJECT_ID > slug of
deployment name > 'default'); M1's 'default' bootstrap is re-stamped in M3
before any /p/ URL is public, so it's meaningful (/p/ohm/) and never renamed
live. §22.13.
- Entry-noun in URLs: generic /p/<project>/e/<slug> for every type; the noun
(RFC/Spec/Feature) is a type-driven UI label, not in the path. §22.10.
- Existing RFC-NNNN: kept as a frozen, read-only legacy display label in
frontmatter id (preserves citations); never used for lookup, never assigned
to new entries. §22.4, §2.3 amended.
Threads through §22.4/22.10/22.13, the §2.3 amendment, and the M3 slice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the §22 multi-project draft along four axes decided this session:
- Project type — each project declares document | specification | bdd
(immutable, registry-set). One shared propose→branch→PR→graduate engine;
type only layers entry schema, terminology, and type-specific surfaces
(spec release-planning, BDD scenarios/coverage). §22.4a.
- Slug-only identity — an entry is (project_id, slug); the per-project
RFC-NNNN numbering and its allocator are retired. §22.4, §2.3/§13 amended.
- initial_state — per-project landing state for a new entry: super-draft
(document/spec default) or active (bdd default). §22.4b.
- unreviewed flag — entries that skip straight to active land unreviewed;
graduation implies review so the normal path is never flagged. Owner
mark-reviewed clears it; §7 catalog gains an unreviewed filter; mirrored
into cached_rfcs. §22.4c.
Slicing plan re-sliced six→seven: type/initial_state/unreviewed config ride
M3, type surfaces become M5, membership→M6, hardening→M7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Design for hosting N projects (corpora) per deployment, with today's
single-corpus deployment as the N=1 case. Captures the locked decisions
(git registry, gated-by-default visibility, per-project RFC numbering),
the three-tier role model (deployment / project / per-RFC), and the
forced runtime-branding shift off VITE_APP_NAME.
- multi-project.md: rationale, decisions, operator (flotilla) integration.
- multi-project-spec.md: draft binding §22, in-place amendments to
§§1,2,5,6,7,8,13,14,17,18,20, and a six-slice (M1-M6) build plan.
Registry lives in a deployment-side repo the framework reads via a new
REGISTRY_REPO env var (META_REPO's multi-project successor); confirmed
against the ohm-rfc-app-flotilla spec — no operator-tooling change needed.
Not yet merged into SPEC.md or versioned; folds in when M1 lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>