ADR-133 — Product-to-repos mapping (products.yaml)
Status: Proposed Date: 2026-05-05
Context
A "product" in the gyrum fleet is the user-facing thing — warp, ai-research, distill, devtools, dark-factory itself. A user thinks "I use warp"; they do not think "I use the warp backend at gyrum-labs/warp and the warp web UI at gyrum-labs/warp/web/". Several products span more than one repo: ai-research is the backend at gyrum-labs/ai-research and the frontend at gyrum-labs/ai-frontend; warp is one repo today but with backend at the root and frontend rooted at web/.
EPIC warp#1358 commits the fleet to a per-product user-facing /changes page rendered into every product's frontend at build time. Sister ticket warp#1359 (C0.1, ADR-changelog-schema, this tranche) locks the per-PR .changelog/entries.jsonl shape; sister ticket warp#1361 (C0.3, ADR-pr-title-format, this tranche) locks the [ID:warp#NNN]: title contract that ties each entry back to the warp ticket. The frontend aggregator that renders /changes for product ai-research therefore needs to know that ai-research ships from two repos and pull both repos' .changelog/entries.jsonl at build time. Without a stable mapping, every product's frontend re-derives membership from chat memory or operator instinct — exactly the failure mode the structured-changelog stack exists to remove.
The mapping lives at fleet level, not project level, because membership is a fleet-coordination concern (distill is one product across one repo today and may grow to two; ai-research is two repos and may grow to three when a worker repo lands; the addition is a cross-repo PR, not a per-repo edit). gyrum-catalog is the sister manifest at the per-project level — it captures per-repo capability and stack metadata; products.yaml here is its peer at the per-product level. The two are coordinated by repo URL: a repo named in products.yaml resolves to a manifest entry in gyrum-catalog. They are not merged into one file because their consumers are different: gyrum-catalog is consumed by capability resolvers and provisioning playbooks; products.yaml is consumed by the build-time /changes aggregator and (later) the operator-facing fleet feed at /operate/changes.
Decision
products.yaml is the canonical product-to-repos mapping for the fleet. The schema is:
# dark-factory/products.yaml
# Canonical fleet-level product-to-repos mapping. See ADR-133.
# Sister: gyrum-catalog (per-project manifest peer).
products:
<product-id>:
repos: # Required. Ordered list; first entry is the
- <owner>/<repo> # product's "primary" repo for tie-breaking.
frontend_root: <path> # Required. Where the user-facing frontend
# lives. Path relative to the FIRST repo, or
# "<owner>/<repo>" when the frontend is its
# own repo. The /changes aggregator builds
# from this root.
display_name: <string> # Optional. Defaults to <product-id>.
# The string shown in user-facing UI.
homepage_url: <url> # Optional. The product's public homepage.
# Used for cross-product links in /changes.
Resolution rule (build-time aggregator). When frontend X builds its /changes page, it:
- Reads
products.yamlfromhttps://raw.githubusercontent.com/gyrum-labs/dark-factory/<pinned-sha>/products.yaml— pinned to a specific commit SHA, nevermain, to prevent build-time drift when the fleet roster changes mid-build. - Identifies its own product entry by matching its repo URL against any
repos:list. Exactly one match is expected; zero matches is a build-time error (the frontend's repo must be enrolled inproducts.yamlbefore its/changespage can build); multiple matches is a config error. - For each repo in the matched product's
repos:list, fetcheshttps://raw.githubusercontent.com/<owner>/<repo>/<pinned-sha>/.changelog/entries.jsonl— again pinned to a SHA, recorded in the frontend's lockfile alongside its other build-time fetches. The pinned SHA per sibling repo is the latestmainSHA at the moment the frontend's build starts, captured by the build orchestrator and frozen for the duration of the build. - Merges the entries, filters to
user_facing: true(per the sisterADR-changelog-schemalanding under warp#1359), renders the timeline grouped by date.
Fail-soft behaviour. When a sibling repo's .changelog/entries.jsonl is unreachable at build time (404, 5xx, network timeout), the aggregator:
- Emits a build warning containing the missing repo URL, the resolved SHA, and the failure mode.
- Falls back to the last-known-good cache stored in the frontend's repo at
.changelog/cache/<owner>-<repo>.jsonl, refreshed on every successful build. - Continues the build with the cached entries marked stale-since-
<timestamp>in the rendered output's metadata block (not the user-visible body). - Does not break the build. A
/changespage that is one sibling repo behind is strictly better than no/changespage at all; an outage inai-research's API repo must not blockai-research's frontend from deploying a UI fix.
If the cache is also missing (first build, or cache was deleted), the aggregator emits an error-level build warning and renders /changes with only the entries it could fetch. The warning escalates to an observer ticket per ADR-110 (observe-and-file) when the same sibling-repo fetch fails on three consecutive builds — the observer's job is to file warp rather than to gate the build.
Add/remove product process. Adding or removing a product is a PR against products.yaml with the standard 3-persona review (per ADR-115, principle-aware reviewers). The reviewers verify (a) every repo named in the new entry exists and is in gyrum-labs/, (b) the product-id is unique, (c) frontend_root resolves to a real path or repo, (d) no other product claims any of the same repos. A validator script gyrum-validate-products (devtools, follow-up ticket — sister to this ADR) enforces (a-d) at PR-author time.
Initial roster. The initial products.yaml enrolled alongside this ADR captures the five products visible in EPIC warp#1358's session-log audit:
products:
warp:
repos: [gyrum-labs/warp]
frontend_root: web/
display_name: warp
homepage_url: https://warp.gyrum.ai
ai-research:
repos: [gyrum-labs/ai-research, gyrum-labs/ai-frontend]
frontend_root: gyrum-labs/ai-frontend
display_name: ai-research
distill:
repos: [gyrum-labs/distill-gyrum-ai]
display_name: distill
devtools:
repos: [gyrum-labs/devtools]
display_name: devtools
dark-factory:
repos: [gyrum-labs/dark-factory]
display_name: dark-factory
Subsequent products (e.g. heed, brief, ramp, vie per ADR-125) enrol via the add/remove PR process above as their first user-facing surface ships.
Consequences
What becomes easier. The frontend /changes aggregator gets a stable, machine-readable contract to build against — it does not need to read CLAUDE.md or guess membership from repo names. Cross-repo aggregation is a directory lookup, not a fan-out search. Adding a new repo to an existing product (e.g. an ai-worker repo joining ai-research) is a single-file PR; the frontend picks it up at next build. The operator-facing /operate/changes aggregator (Phase 3, deferred per EPIC warp#1358) inherits the same map for free.
What becomes harder. Every new repo that ships to users now needs an explicit products.yaml entry before its frontend can render /changes. This is friction at product-launch time but a structural improvement: today a new product can ship invisibly, with users discovering changes only through Slack or word-of-mouth; under this ADR, the act of enrolment is the signal that a product is user-visible.
What we sign up to operate. The products.yaml file in dark-factory becomes a load-bearing config — every product frontend's build depends on it. The fail-soft cache behaviour means a dark-factory outage does not break product builds (cached entries continue to render), but a malformed products.yaml (yaml parse error, schema violation) merged to main does. Mitigation: gyrum-validate-products runs as a dark-factory pre-commit hook and as a structural check on every dark-factory PR; the 3-persona review (per ADR-115) catches semantic errors that syntax validation misses.
Drift risk. The mapping can drift from reality if a repo joins a product but the PR forgets the products.yaml update. Mitigation: gyrum-review-pr fetches products.yaml at PR-review time and warns when a PR adds a .changelog/ directory to a repo that is not enrolled — the observer pattern (ADR-110) files a warp ticket rather than blocking the build, but the warning is visible in every reviewer's feedback. The same warning fires when a PR removes the last .changelog/ directory from a repo that is still enrolled, prompting the operator to remove the entry.
Build-time coupling. Every product frontend's build now fetches from raw.githubusercontent.com for every sibling repo's entries.jsonl. For the initial roster (max 2 sibling repos per product), this is two HTTP fetches per build — negligible. The fail-soft cache caps the worst case at "build proceeds with cached entries", so a sustained raw.githubusercontent.com outage degrades freshness, not deploy-ability.
Commit-pinning rationale. Pinning to a SHA rather than main prevents the failure mode where two builds running concurrently see different entries.jsonl snapshots — important when the frontend build itself is part of the merge that adds a new entry. The lockfile entry per sibling repo is one line; the build orchestrator already resolves SHAs for git submodules and reuses the same machinery.
Alternatives considered
Per-repo
product.yamlinstead of fleet-levelproducts.yaml. Each repo declares its own product membership in a local file. Lost: cross-repo consistency. Two repos can claim the same product-id, or the same product-id can mean different things in two different repos, with no central authority to reject the conflict. The fleet-level mapping is the only place where "exactly one product owns this repo" is enforceable.Derive product membership from
gyrum-catalog. The per-project manifest already captures repo metadata; extend it with aproduct:field. Lost: separation of concerns.gyrum-catalogis consumed by capability resolvers and provisioning playbooks; mixing user-product membership into it couples two unrelated change cadences. Products are added/removed at human-decision pace (~quarterly); capabilities are added/removed at deploy pace (~daily). Sister files with cross-references serve both consumers cleanly.Hard-code product membership in each frontend's build script. The
ai-frontendbuild hard-codes "I amai-researchand my sibling isgyrum-labs/ai-research". Lost: a single file change that adds a new repo to an existing product becomes a per-frontend code edit. The whole reason/changesexists is to remove "you have to know where to look" friction; hard-coding reintroduces it for the build itself.Latest-
mainfetch instead of commit-pinned. Simpler — no lockfile entry, no SHA capture. Lost: build-time drift. Two concurrent builds readingmainsee different roster snapshots; a frontend build that started before aproducts.yamlPR merged but finished after sees the post-merge state, which may include sibling-repo URLs whose.changelog/does not yet exist. SHA-pinning is an extra ~10 lines in the build orchestrator and removes an entire class of intermittent-failure bug.
Supersedes: none — extends EPIC warp#1358; sister to warp#1359 (C0.1, changelog schema ADR, this tranche) and warp#1361 (C0.3, PR title format ADR, this tranche). Superseded by: {{leave blank until a later ADR reverses this one}}