docs(design): project types, slug-only identity, initial_state + unreviewed flag (§22 draft)

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>
This commit is contained in:
Ben Stull
2026-06-02 18:26:52 -07:00
parent 6f901e3e2c
commit 57b2fc5205
2 changed files with 317 additions and 54 deletions
+204 -41
View File
@@ -14,8 +14,9 @@
## 22. Projects: multiple corpora per deployment
A **deployment** hosts one or more **projects**. A project is a single
corpus: one content repository (§1) holding RFC entries under `rfcs/`, with
its own slug and `RFC-NNNN` namespaces, catalog, philosophy, branding,
corpus: one content repository (§1) holding entries under `rfcs/`, with
its own per-project slug namespace (the slug is the identity — §22.4), a
declared **type** (§22.4a), catalog, philosophy, branding,
member roster, and model universe. The deployment is the substrate the
projects share — one Gitea org, one bot, one account system, one inbox, one
running process — and the surface a visitor first lands on.
@@ -73,8 +74,11 @@ deployment:
projects:
- id: ohm # url-stable slug, unique within the deployment
name: Open Human Model
type: document # document | specification | bdd — immutable (§22.4a)
content_repo: ohm-content # repo under the deployment's Gitea org (§22.3)
visibility: gated # gated | public | unlisted (§22.5)
initial_state: super-draft # super-draft | active — landing state of a new
# entry; defaults from type (§22.4b)
enabled_models: [claude, gemini] # optional; falls back to deployment ENABLED_MODELS
theme: { accent: "#5b5bd6" } # optional per-project token overrides (§22.9)
```
@@ -94,14 +98,116 @@ about the bot, the §6 app-owned authorization, or the "app is the only
contribution surface" stance changes. There are no per-project Gitea orgs and
no per-project bot accounts.
### 22.4 Slug and ID namespaces are per-project
### 22.4 The slug namespace is per-project; the slug is the identity
An RFC's slug (§2) is unique **within its project**, not across the
deployment: `ohm` and `specs` may each have an `intro`. `RFC-NNNN` integer
IDs are assigned per project at graduation as `max(existing integer IDs in
this project) + 1` (§13, §2.3 amended) — each project numbers from `RFC-0001`
independently. A fully qualified RFC reference is therefore
`(project_id, slug)`, and after graduation `(project_id, RFC-NNNN)`.
An entry's slug (§2) is unique **within its project**, not across the
deployment: `ohm` and `specs` may each have an `intro`. The slug **is** the
identity — a fully qualified reference is `(project_id, slug)`, and nothing
more. There is no type prefix and **no per-project numeric ID**: within a
project the slug alone is unambiguous, and the project context (its `/p/<id>/`
URL prefix and chrome) carries everything the old `RFC-NNNN` label used to.
This retires the per-project numbering of earlier drafts. The §13 graduation
flip still happens — it moves an entry from proposal to graduated state and
into the content repo — but it no longer allocates a number; the
`max(integer IDs)+1` allocator (§2.3, the old `api_graduation.py` path) is
removed. The displayed *noun* around a slug ("RFC", "Spec", "Feature") is a
presentation concern driven by the project's type (§22.4a), not part of the
identity.
### 22.4a Project type
Every project declares a `type` in the registry (§22.2), chosen at creation
and **immutable**: one of `document`, `specification`, or `bdd`. Type does not
change the engine — every type uses the same content repo (§22.3), the same
propose→branch→PR→discuss→graduate lifecycle (§§913), the same threads,
flags, and chat. Type selects exactly three things:
1. the **entry frontmatter schema** the project validates entries against (§2);
2. the **terminology** the chrome uses for an entry (the §8.1 noun, catalog
labels);
3. the set of **type-specific surfaces** layered on top of the shared §7
catalog.
Type-specific behavior is implemented as a per-type module the framework
selects on `project.type`; the engine itself treats every entry as markdown +
frontmatter regardless of type. `type` is an **open set** in shape — a future
type is a new module plus a new allowed enum value, no schema rebuild — and
the three below are what is defined now. The type names and their behavior are
framework concepts (like role names), not deployment content: a deployment
picks which type each project is, but does not define or rename types.
- **`document`** — long-form normative prose (OHM: a model of principles and
definitions). Frontmatter is the §2 baseline (title, status,
owners/arbiters, tags). No type-specific surfaces. The §22.13 generated
default project is a `document` project, so the N=1 case is unchanged.
- **`specification`** — a versioned technical specification (this framework's
own `SPEC.md` is the archetype: numbered normative sections, upgrade steps).
Frontmatter adds spec metadata (`version`, lifecycle `status` of
draft/active/superseded, `supersedes`). **Type-specific surface — release
planning:** group entries/changes into versioned releases, carry a
changelog + §20-style upgrade-steps per release, and surface "what is in the
next release."
- **`bdd`** — behavior-driven feature specs: each entry states a feature as
Given/When/Then scenarios with acceptance criteria. Frontmatter adds feature
metadata and an optional link to the `specification` entries a feature
verifies. **Type-specific surface:** a scenario/acceptance view, and — where
a deployment runs a BDD project alongside a specification project — a
coverage view mapping features to the spec sections they exercise.
> **Draft note.** The shared-engine boundary is locked; the per-type
> *schemas and surfaces* above (notably the specification release-planning
> data model and whether BDD scenarios are free-form markdown or a parsed
> Given/When/Then structure) are first proposals, to be pinned in the
> type-surface slice (Part C, M5).
### 22.4b Initial state of a new entry
A project sets the **landing state** a new entry takes when its creating
idea-PR merges (§2.4) — the `initial_state` registry field, one of the §2.4
super-draft entry-states:
- **`super-draft`** (default for `document` and `specification`) — a new entry
lands as a super-draft and must be explicitly graduated (§13) to reach
`active`. This is today's flow: propose → super-draft → review → graduate.
- **`active`** (default for `bdd`) — a new entry lands `active` on the idea-PR
merge, with the **`unreviewed` flag set** (§22.4c). The §13 graduate gate is
not surfaced — the entry is already active — but because nothing reviewed
it, the flag marks it as not-yet-vetted until an owner clears it. A behavior
spec is captured fact, not a proposal under deliberation, so it skips the
review-then-promote ceremony; branches, PRs, and discussion work afterward
exactly as on any `active` entry.
The default comes from the project's **type** (§22.4a) — each type module
supplies it — but `initial_state` is an independent registry knob: a
deployment may run a `bdd` project with `super-draft` if it wants a review
gate, or (less commonly) a `document` project that auto-actives. It changes
only the *landing state and whether graduation is required*; the underlying
propose→branch→PR→discuss engine is unchanged (shared-engine rule, §22.4a).
The §22.13 default project keeps `super-draft`, preserving the N=1 flow.
### 22.4c The `unreviewed` flag
An `active` entry carries an **`unreviewed`** boolean, orthogonal to its
`state`. It records whether a human gate has vetted the entry:
- An entry that reaches `active` by the normal **graduate** path (§13) is
**never** flagged — the graduate action, taken by an owner/admin, *is* the
review.
- An entry that **skips straight to `active`** via `initial_state: active`
(§22.4b) lands with `unreviewed = true`, because nothing reviewed it.
A **project owner**`project_admin`, or a deployment `owner`/`admin` (§22.7)
— clears the flag with a **mark-reviewed** action (§17), the same authority
that graduates an entry. Clearing stamps `reviewed_at`/`reviewed_by` for
provenance, paralleling `graduated_at`/`graduated_by`. The flag is a property
of the entry (frontmatter, §2 amendment), so it is git-truth and survives a
cache rebuild, exactly like `state`.
The §7 catalog gains an **unreviewed filter** so an owner can find the entries
awaiting review; it is the natural worklist for the mark-reviewed action.
`unreviewed` only applies to `active` entries — a `super-draft` is pre-review
by definition, and `withdrawn` is out of scope.
### 22.5 Project visibility
@@ -237,8 +343,9 @@ single **default project** so it keeps running unchanged:
1. The migration generates a `projects` row from current config:
`META_REPO → content_repo`, `VITE_APP_NAME → name`, `visibility = public`
(preserving the deployment's current open-by-default posture), `id` a
slug derived from the deployment.
(preserving the deployment's current open-by-default posture),
`type = document` (every pre-multi-project corpus is a document corpus),
and `id` a slug derived from the deployment.
2. Every existing RFC-scoped row (§5 amendment list) is stamped with that
`project_id`.
3. Old corpus-root URLs (`/rfc/<slug>`, `/proposals/<n>`) 308-redirect to
@@ -262,34 +369,63 @@ Applied in place, in the established amendment-note style (cf. §1's
and app-owned-authorization paragraphs are unchanged and now read
org-wide.
- **§2 Meta schema / §2.3 IDs.** Slugs are unique within a project; entry
filenames are unchanged (per content repo). §2.3's `max+1` is scoped to the
project (§22.4).
filenames are unchanged (per content repo). §2.3's `RFC-NNNN` `max+1`
allocation is **removed** — the slug is the identity (§22.4); there is no
per-project number. The entry frontmatter schema becomes type-dependent
(§22.4a): `document` keeps today's fields, `specification` and `bdd` add
their type metadata. New `active`-entry fields: `unreviewed` (bool) and the
`reviewed_at`/`reviewed_by` provenance pair (§22.4c), paralleling
`graduated_at`/`graduated_by`.
- **§2.4 State machine.** The `(no entry) ─[idea-PR merged]→` transition now
targets the project's `initial_state` (§22.4b): `super-draft` as today, or
straight to `active` when the project (e.g. a `bdd` project) lands entries
there — in which case the entry is stamped `unreviewed` (§22.4c). A new
`active ─[mark-reviewed, owner/admin]→ active` self-transition clears the
flag. The rest of the machine is unchanged.
- **§5 Data model.** Add `project_id` to: `branch_visibility`,
`branch_contribute_grants`, `stars`, `threads`, `changes`, `watches`,
`notifications`, `rfc_invitations`, `rfc_collaborators`,
`contribution_requests`, `funder_consents`, the `*_seen` cursors, `actions`,
and the §4 cache tables `cached_rfcs` (PK → `(project_id, slug)`, `rfc_id`
unique per project), `cached_branches`, `cached_prs`,
`pr_resolution_branches`, `proposed_use_cases`. Add the new tables
`projects` and `project_members` (§22.2, §22.6). `users.role` is annotated
as deployment-scope (§22.7).
and the §4 cache tables `cached_rfcs` (PK → `(project_id, slug)`;
also mirrors the `unreviewed` frontmatter flag, §22.4c, so the §7 catalog
filter can query it without reading every entry file),
`cached_branches`, `cached_prs`, `pr_resolution_branches`,
`proposed_use_cases`. Add the new tables `projects` (carrying the immutable
`type`, §22.4a) and `project_members` (§22.2, §22.6). `users.role` is
annotated as deployment-scope (§22.7).
- **§6.1 Roles.** Add the §22.7 composition note: deployment roles are now
one of three tiers; a plain `contributor` has no implicit access to a
project until §22.6 grants a project role (subject to §22.5).
- **§6.3 Per-RFC delegated authority.** Note that `project_admin` (§22.6) is
the same delegation idea at project scope, sitting above per-RFC authority.
- **§7 Left pane.** The catalog is per-project, under `/p/<project>/`. The
deployment directory (§22.10) is a new surface above it.
- **§8.1 Breadcrumb.** Gains a leading project segment (§22.10).
- **§13.3 Graduation flip.** Operates on the project's content repo;
`RFC-NNNN` is allocated per project (§22.4).
deployment directory (§22.10) is a new surface above it. The catalog gains
an **unreviewed filter** (§22.4c) — the owner's worklist of `active` entries
that landed unreviewed.
- **§8.1 Breadcrumb.** Gains a leading project segment (§22.10). The entry is
named by its slug, not a number (§22.4); the noun shown around it ("RFC",
"Spec", "Feature") is the project type's label (§22.4a).
- **§13.3 Graduation flip.** Operates on the project's content repo. It flips
status and moves the entry, but **allocates no number** — the slug is the
identity throughout (§22.4); the old per-project `RFC-NNNN` allocation is
gone. Graduation is also **conditional on the project's `initial_state`
(§22.4b)**: a project that lands entries `active` has no super-draft phase,
so the graduate action is a no-op there and is not surfaced. For those
entries the **mark-reviewed** action (§22.4c) takes graduation's place as
the owner/admin vetting step — it clears `unreviewed` instead of flipping
state.
- **§14.1 Pre-login landing.** Splits into deployment landing (the directory,
§22.10) and per-project philosophy/deck (§14 under `/p/<project>/`).
Deployment name comes from `GET /api/deployment`, not `VITE_APP_NAME`.
- **§17 Backend surface.** RFC routes gain the `/p/<project>` /
`project_id` scoping; add `GET /api/deployment`, `GET /api/projects/:id`,
and the `project_members` management + request-to-join endpoints (§22.6,
§22.8).
`project_id` scoping; add `GET /api/deployment`, `GET /api/projects/:id`
(returns the project's `type`, §22.4a), and the `project_members`
management + request-to-join endpoints (§22.6, §22.8). Add a
**mark-reviewed** endpoint clearing an entry's `unreviewed` flag (§22.4c,
owner/admin), and an `unreviewed` filter param on the catalog list. Type-
specific surfaces (§22.4a) add their own routes, mounted only for projects of
the matching type — e.g. the `specification` release-planning endpoints and
the `bdd` scenario/coverage endpoints.
- **§18 Stack.** `ENABLED_MODELS` is the deployment fallback; per-project
`enabled_models` overrides it (§22.12).
- **§20 Versioning / surface.** `VITE_APP_NAME` deprecated in favor of the
@@ -309,11 +445,13 @@ Applied in place, in the established amendment-note style (cf. §1's
# Part C — Slicing plan
Six slices carry §22 and its amendments end-to-end. The ordering mirrors
Seven slices carry §22 and its amendments end-to-end. The ordering mirrors
DEV.md's original principle — foundations and the cache/permission spine
first, the surfaces that consume them after, hardening last. Each slice is
shippable: a deployment can stop at any slice boundary and still run (the
default project keeps the N=1 case whole throughout).
default project keeps the N=1 case whole throughout). The project `type`
(§22.4a) rides M3 (config) and M5 (its surfaces); M1M4 are type-agnostic
because the engine is.
**M1 — The project spine (schema + default-project migration).** *(landed)*
The `projects` and `project_members` tables; `project_id` threaded
@@ -341,24 +479,45 @@ rows beyond `default`); the `/p/<project>/` route prefix and the 308
redirects off the old corpus-root URLs; `GET /api/deployment` and `GET
/api/projects/:id`; the frontend cut from `VITE_APP_NAME` to runtime config,
per-project `theme` token overlay. The deployment directory at `/` and the
project switcher in deployment chrome. After M3 a deployment with two
registry projects is fully navigable — which makes this the slice that must
also land the deferred slug-keyed uniqueness/PK rebuilds (migration 025
header) before a second project can collide with the first.
project switcher in deployment chrome. This slice also adds the additive
`type` and `initial_state` columns to `projects` (a small migration — M1
shipped `projects` without them), mirrors both from the registry, returns
them on `GET /api/projects/:id`, and drives the entry-noun terminology off
`type` (§22.4a). It also teaches the shared creation path to honor
`initial_state` (§22.4b) — land a new entry `active` instead of `super-draft`,
stamping `unreviewed` and skipping the graduate gate when the project says so
— plus the `unreviewed` frontmatter fields (§22.4c) mirrored into
`cached_rfcs`, the owner/admin mark-reviewed action, and the §7 catalog
unreviewed filter that queries that cached column. But no type
*surfaces* yet; beyond their label, landing state, and review flag, all three
types still look the same here. After M3 a deployment with two registry projects is
fully navigable — which makes this the slice that must also land the deferred
slug-keyed uniqueness/PK rebuilds (migration 025 header) before a second
project can collide with the first.
**M4 — Per-project corpus surfaces.** The §7 catalog, §8 RFC view, §9/§10
proposal/PR flows, §13 graduation, and §14 philosophy all confirmed working
under project scope with per-project slug/ID namespaces (§22.4). Mostly
inherited from M1M3; this slice is the end-to-end pass that proves a second
project's full lifecycle (propose → super-draft → graduate → `RFC-0001` *in
that project*).
**M4 — Per-project corpus surfaces (the second-project acceptance pass).** The
§7 catalog, §8 entry view, §9/§10 proposal/PR flows, §13 graduation, and §14
philosophy all confirmed working under project scope with the per-project,
slug-only namespace (§22.4). This is mostly *inherited* from M1M3 — the work
is an end-to-end pass that proves a second `document` project's full lifecycle
(propose → super-draft → graduate, identified by slug *in that project*), not
new build. Naming it explicitly as the acceptance slice keeps scope that
belongs in M3 from leaking in.
**M5 — Membership lifecycle.** §22.8 invite (re-scoped §12 machinery) and
**M5 — Type modules + type-specific surfaces.** The per-type layer of §22.4a:
type-specific entry-frontmatter validation (`specification`/`bdd` metadata);
the `specification` **release-planning** surface; the `bdd` scenario/coverage
surface. Type-scoped routes mounted only for matching projects (§17). The
shared engine is untouched — this slice only adds the layers on top, so a
`document` project is unaffected and the M4 acceptance still holds. (The
per-type schema/surface details are the §22.4a draft note's open work.)
**M6 — Membership lifecycle.** §22.8 invite (re-scoped §12 machinery) and
request-to-join (re-scoped §28); the inbox surfacing of join requests; the
§22.11 cross-project inbox with `project_id` filtering and project-level
mute. The admin surface for managing a project's roster.
**M6 — Hardening + operator path.** Per-project `enabled_models` resolution
**M7 — Hardening + operator path.** Per-project `enabled_models` resolution
(§22.12) including funder/§6.6 intersection; the registry's place in
`docs/DEPLOYMENTS.md` and the flotilla operator tooling; end-to-end tests
spanning two projects with disjoint membership; the §20.4 changelog +
@@ -375,5 +534,9 @@ upgrade-steps block; the SPEC merge (Part A appended, Part B applied).
contract. With OHM becoming one project among several, the registry sits
*above* any single project's content repo, so a file inside one project's
repo is wrong — a dedicated repo is the right home.
- **Request-to-join vs. invite-only** — drafted with both (§22.8); M5 may
ship invite-only first and add request-to-join second if scope demands.
- **Request-to-join vs. invite-only** — drafted with both (§22.8); M6
(membership lifecycle) may ship invite-only first and add request-to-join
second if scope demands.
- **Per-type schemas and surfaces** — the §22.4a draft note's open work:
the `specification` release-planning data model and whether `bdd` scenarios
are free-form markdown or a parsed Given/When/Then structure. Pinned in M5.