Modernize the Tier-1 stack to the three-tier app and add browser coverage for
the §22.4a metadata UI, closing the E2E gap deferred across SLICE-3/4/5:
- seed-gitea.sh: create a REGISTRY_REPO with projects.yaml + a faceted named
collection (.collection.yaml fields: priority enum + tags) seeded with three
metadata-bearing entries; register content+registry webhooks; self-guarding
(skip if a prior token still works) so a dependency-triggered re-run can't
remint and invalidate the backend's token.
- .env.tier1: REGISTRY_REPO/DEFAULT_PROJECT_ID; disable OTC cooldown + lift the
per-IP auth limiter for the single-IP test runner.
- docker-compose: pin backend image; backend-seed inserts a granted owner the
OTC path can sign in as (write paths need contributor+).
- Makefile: two-phase tier1-up (seed to completion, then create backend so it
reads the populated token env); robust down; e2e-fresh = down+up+e2e (the
canonical run, since the edit/bulk specs mutate the seeded corpus).
- metadata.spec.js: SLICE-3 faceted filter (anon), SLICE-4 edit panel (owner),
SLICE-5 bulk bar (owner). 4 passed against a fresh stack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add RATELIMIT_OTC_REQUEST_MAX / RATELIMIT_VERIFY_MAX / RATELIMIT_CHECK_MAX
(default to the existing secure values; non-positive/unparseable → default).
Lets a test/PPE stack that drives the auth endpoints repeatedly from one IP
lift the budget; production leaves them unset. Mirrors the existing
OTC_REQUEST_COOLDOWN_SECONDS knob.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
useCollectionId read useParams().collectionId, but the Catalog renders at
/p/:projectId/* — outside the c/:collectionId route — so it always fell back
to the default collection. The faceted filter (SLICE-3) and bulk action bar
(SLICE-5) therefore never scoped to a named (fields-bearing) collection in the
deployed app; the Catalog unit test had mocked useCollectionId, hiding it.
Resolve the /c/<id>/ segment from the pathname when the route param isn't in
scope. Caught by the new Playwright metadata suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Code-review follow-ups:
- Validate the raw submitted value before apply_values coerces it, in both
the single-edit and bulk endpoints — a scalar set onto a tags field now
rejects (422 / rejected) instead of silently char-splitting into a list.
- Catalog prunes bulk selections to the currently-visible (filtered) entries
after each list fetch, so the bulk bar can't act on rows the user has
filtered out of view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Faceted catalog rows are selectable for contributors; a sticky bulk bar
(BulkActionBar) drives set/add/remove gestures from the collection fields
schema, calls bulkEntryMeta (one commit), toasts applied/skipped counts,
and re-fetches. Non-contributors and legacy (no-fields) collections see
no selection (INV-5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MetadataFieldsPanel renders one control per declared field (enum→select,
tags→chips, text→input), read-only without contribute access, saving changed
values via saveEntryMeta (direct sidecar commit). Wired into the canonical
main view, gated on the collection declaring fields (INV-5). saveEntryMeta
API client added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST .../collections/{cid}/migrate — now safe to ship since all write paths
are sidecar-aware. Idempotent, one commit per collection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Direct-commit to the sidecar (D7), contributor+ gated (INV-4), schema-validated
at the write boundary, lazy-migrates a legacy entry, re-ingests. GET RFC now
returns the per-entry meta mapping and a can_edit_meta capability.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
graduate, claim, retire/unretire, _read_meta_entry, mark_entry_reviewed,
body extract/wrap (api_branches + api_prs replay) now dual-read and write
metadata to the sidecar via write_entry_files + bot.commit_entry_files/
open_entry_pr — a migrated body-only .md no longer crashes entry.parse or
re-grows frontmatter; legacy entries lazy-migrate on first metadata write.
Existing tests updated to assert the sidecar (INV-2 clean docs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apply_values, EntryGitState, read_entry_from_git, write_entry_files, sidecar_path_for.
Plus the SLICE-4 implementation plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collections declare a `fields:` schema in `.collection.yaml`; entries carry
typed metadata (enum/tags/text). New central `metadata_schema` module parses
the schema leniently (INV-3) and validates entry values — advisory at read
(corpus mirror flags violations as `metadata_malformed` without hard-failing),
the enforcement point for the write boundary (edit endpoints land SLICE-4/5).
- app/metadata_schema.py: parse_fields (lenient/normalizing) + validate
- registry.parse_collection_manifest reads `fields:` into collection config
- collections.get_collection unpacks `fields`; served by the collection API
- cache._refresh_collection_corpus validates each entry advisory-only
Non-breaking, opt-in: no `fields:` → unchanged (INV-5); default `document`
collection declares none (N=1 unchanged). No DB migration — schema rides in
collections.config_json. SLICE-2 of
docs/design/2026-06-06-configurable-collection-metadata.md §7.2.
Backend suite green (601 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§22.4a SLICE-1 of docs/design/2026-06-06-configurable-collection-metadata.md
(§7.2). Entry metadata can live in a per-entry `<slug>.meta.yaml` sidecar with
the `.md` kept as pure prose (INV-2). Additive and non-breaking — with no
sidecars present every corpus stays on the legacy frontmatter path,
byte-identical (N=1 unchanged).
- Dual-read (app/metadata.py `read_entry`) — sidecar-else-legacy-frontmatter,
identical records (INV-6); unknown/forward-compat keys ride along through
parse→serialize and migration (INV-7, `Entry.extra`). A degenerate sidecar
(malformed/empty/slug-less) never drops the entry — slug backstopped from the
filename stem, flagged not lost (INV-3).
- Migration tool (`metadata.migrate_collection`) — idempotent, one ChangeFiles
commit per collection (new `gitea.change_files`). Tested as a function; its
Owner-gated operator trigger is DEFERRED to SLICE-4 (write paths must become
sidecar-aware first — see the design's SLICE-4 note + INV-8). No production
trigger ships here, so no corpus is rewritten.
- Malformed flag — migration 033 adds `cached_rfcs.metadata_malformed`
(additive); the corpus mirror derives it; catalog list + entry-detail APIs
surface `metadata_malformed`.
- INV-7 at graduation — graduation now carries `Entry.extra` through the rebuild
instead of dropping forward-compat keys.
Gate: backend 575 passed (28 new: test_metadata / _migration / _cache +
graduation extra-preservation). Frontend untouched. CHANGELOG 0.47.0 +
upgrade-steps; VERSION + frontend/package.json -> 0.47.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reframes binding SPEC.md §22.4a so a collection's entry metadata schema is
collection-configured — a `fields:` schema in `.collection.yaml` plus per-entry
`<slug>.meta.yaml` sidecars — rather than a frontmatter schema hard-wired to the
collection's `type`. Item 3's type-specific surfaces (release planning;
bdd scenario/coverage views) are deferred to a future design; bdd coverage is
recorded as a future `ref`-field surface rendered as hyperlinks (no cross-
collection corpus fusion). `type` still selects terminology (entry noun,
v0.45.0) + default initial_state/review posture (§22.4b-c).
SLICE-0 of docs/design/2026-06-06-configurable-collection-metadata.md (§7.2);
supersedes the per-type-surfaces draft (D11). Doc-only: per-type frontmatter
validation was never implemented, so no operator action, no schema/behavior
change. Sidecar storage + validation + UI arrive in SLICE-1+.
- SPEC.md §22.4a reframed; document/specification/bdd bullets updated; metadata
amendment blockquote added.
- SPEC.md §2 and §22 forward-pointer blockquotes: "type-dependent frontmatter
schema" -> "collection-configured, not type-driven (§22.4a, as amended)".
- CHANGELOG 0.46.2; VERSION + frontend/package.json -> 0.46.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The example comment referenced 'ohm-rfc-app-flotilla overlay set' — the retired
per-app shim. flotilla-core is the operator CLI for all deployments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Solution Design for making the left pane a universal git-directory tree:
host existing documentation repos as path-addressed corpora, full
governance lifecycle per file, dual-mode (structure/flat) pane replacing
the §7 Catalog, zero-migration onboarding ("no record = active").
Discovery output of ohm session 0081.0. Conforms to handbook §3.3
Solution Design standard. Sliced SLICE-1..4 in §7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying the three-tier series onto OHM crash-looped on migration 029:
`NOT NULL constraint failed: cached_branches__new.collection_id`, then a UNIQUE
collision. Root cause: the v0.39.0 default→ohm re-stamp updated
cached_rfcs.project_id but NOT the entry-satellite tables, so ~1.3k
cached_branches rows were stranded at project_id='default' — which 029's
per-project collection backfill can't map (NULL), some of which also duplicate
freshly-re-mirrored 'ohm' rows (UNIQUE), and some of which reference entries
that no longer exist.
Fix — a repair prologue at the top of 029 (no schema change):
- drop stale rows that duplicate an already-correctly-stamped row (keep the
fresh copy) for the branch-keyed tables;
- re-derive each satellite's project_id from its entry (cached_rfcs, by slug);
- drop rows whose entry no longer exists (stale cache, rebuildable from gitea).
A no-op on clean/fresh deployments (empty or consistent satellites).
Validated against a snapshot of the live OHM DB: 029→032 apply cleanly, zero
NULL collection_id, FK check clean, watches/RFCs/branch_visibility preserved,
cached_branches 1397 → 1291 (−67 no-RFC, −39 dups). Fresh-install path
unchanged: existing 029 suite green + a new regression test for the
stale/dup/orphan shape. Full backend suite 547 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §1 Business Context holds the whole business lens (1.1–1.9), solution-agnostic
- §2 Solution Proposal introduces the chosen approach (and justifies build over
a manual alternative); may be non-software in general
- Renumber product/engineering lenses to §§3–7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Executive Summary → value-only (no solution)
- New §4 Pain Points (PP-1..PP-7)
- Business Outcomes → §5, restated as business outcomes (adoption/diversity),
not solution outputs
- Business Use Cases rewritten as solution-agnostic actor-goal scenarios
- Renumber per the Solution Design template revision
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-type frontmatter schemas (item 1) and type-specific surfaces (item 3:
specification release-planning; bdd scenario/coverage views) were flagged at
v0.45.0 as wanting a discovery/spec pass first (they lacked BDD scenarios in
Part C). This is that pass — a design doc specifying the schemas, the surfaces,
their data model / API / frontend shape, BDD-style acceptance scenarios, and a
three-slice delivery plan (S7a schemas, S7b releases, S7c bdd surfaces), all
additive and engine-preserving per §22.4a. Doc-only; no code, no version bump.
- docs/design/2026-06-06-per-type-surfaces.md — the new spec pass.
- three-tier design doc S6 bullet: forward-pointer + S6 shipped/spec'd status.
Leaves implementation to the S7a–S7c coding slices; S7a (schemas) is unblocked,
S7b/S7c carry open product questions for the operator/discovery to settle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>