ADR-018: .gyrum/product.yml as the product registry
Status: Proposed Date: 2026-04-21
Context
The factory has ~30+ repos under gyrum-labs/ (see platform-gap-audit §1.2). Today, "what is a product?" is answered by convention — the subdomain guesses a name, the repo name guesses the product, the operator reconstructs the rest by eye. The Platform UI lands on per-repo pages because repos are the only unit the tooling reliably sees.
The product-concept doc proposes making products first-class. For that to work, the tooling needs a single, authoritative source of product membership. Three options surfaced:
- Convention on repo name. Infer
productfrom a<slug>-gyrum-<tld>pattern. Zero files to maintain; zero reliability.ai-frontendhas never matched the pattern, and multi-repo products (buzzy-frontend,buzzy-backend) produce the right slug by accident only until one is renamed. - Central registry file in
dark-factory(e.g.docs/products.yml). One file to grep; contention on every product change; synchronisation with the repo itself drifts silently (a repo can be archived without the central file knowing). - Per-repo manifest. Each repo declares its product membership. Zero drift between a repo's state and its claimed product (delete the repo, the claim disappears). Discoverable with one org-scan. Extensible.
The first two also fail a practical test: we want a committer to see their repo's product binding inside the repo, without leaving it, because the binding affects the deploy path, the dashboard folder, and the retirement procedure.
Naming: .gyrum/ rather than a repo-root file (gyrum.yml, product.yml) because the factory will accrete more per-repo metadata (hook config, compliance overrides, RBAC hints) and a directory prevents those from polluting the repo root one file at a time.
Related ADRs: ADR-019 (deploy order depends on the role enum defined here), ADR-020 (shared-infra dependencies declared via this file's depends).
Decision
Every repo that belongs to a product carries a .gyrum/product.yml file at its root. The schema defined in product-concept.md §2 is authoritative.
The schema is closed — every field is listed in §2.2 of the product-concept doc. Unknown fields are a validation error. Extending the schema requires an ADR that supersedes or amends this one. "The schema is extensible by any PR" is explicitly rejected: the factory depends on the schema being mechanically readable by tooling written in Go, TypeScript, and shell, each of which assumes a known shape.
Platform tooling (ai-frontend, devtools, Grafana provisioning) reads .gyrum/product.yml as the single source of truth for product membership. No other mechanism (repo names, central registry, GitHub topics) is load-bearing for the same question.
A gyrum-validate-product CLI in devtools validates the file on pre-push. A missing file is not an error — repos without a manifest are legacy and render in the Platform UI's fallback list. An invalid file is an error.
Libraries and tooling repos (gyrum-go, gyrum-ui, devtools) do not carry a manifest. They are listed in devtools' LIBRARY_REPOS constant so the validator treats their absent manifest as deliberate, not as drift.
Consequences
What becomes easier. The Platform UI gets a mechanical answer to "what repos are in Buzzy?" without name-based guessing. gyrum-new-product (future) knows exactly what to write into each scaffolded repo. Deploy-order tooling (ADR-019) keys on role from this file. The subdomain-takeover sweep (platform-gap-audit T2-M) knows which slugs are live.
What becomes harder. Adding a new field to the schema is an ADR, not a PR. That friction is deliberate — a field that tooling doesn't parse is a field that drifts in freeform into hand-written YAML and eventually breaks a sweeper nobody is watching. The cost is ADR latency for a real extension; the benefit is schema stability across Go, TypeScript, and shell consumers.
What we sign up to operate. The validator in devtools. The LIBRARY_REPOS list (today implicit — becomes explicit as part of this). The migration PRs that add the manifest to every live product's repos (§9.1 of the product-concept doc). Quarterly, during platform review (T4-A): reconcile the live product list against the registry.
What we accept as risk. The manifest can lie. A repo whose product.yml claims role: backend but whose code is a worker produces confused platform-UI output. The validator is a type check, not a semantic one. Mitigation is code review + the platform-UI surface showing enough detail that a misclassified repo is visible. The long-term fix (out of scope for this ADR) is pairing the manifest with a runtime probe (health endpoint returns {role} and we cross-check).
Alternatives considered
- Central
docs/products.ymlin dark-factory. Rejected per the Context section. Drift between the claim and the repo state is the killer. - GitHub repository topics for product membership. Rejected. Topics are GitHub-API-only, not present in a clone, which makes local tooling (pre-push hook, offline validator) either slower or broken. Also topic-based metadata is flat — we need a nested shape (
depends,deploy_order). - Encode product membership in a README badge. Rejected. Unparseable by tools without HTML parsing; drifts silently when READMEs are edited.
- Skip the manifest; derive everything from deploy history. Rejected. A brand-new product hasn't deployed yet; the Platform UI should know it exists. The manifest is also the thing the scaffolder writes, which bootstraps the rest.
- Use
package.json/go.modfor the same role. Rejected. Those serve the compiler; gyrum metadata doesn't belong in them, and Go modules don't cover non-Go repos. - Inline into repo description. Rejected for the same reasons as topics plus the UI on GitHub is worse.
Supersedes: none Superseded by: