ADR 0098 — Flag Operator UX Hardening + Pony-Style Internal Docs
Status: Accepted Date: 2026-05-17 UTC Deciders: Kristerpher (operator), software-architect Design doc: flag-operator-ux-hardening-2026-05-17.md
Context
The console flag surface serves one primary use case: an operator stands in front of a prod flag, is about to flip it, and needs five answers in one click:
- What does this flag gate?
- What other flags or services depend on it?
- Does flipping it require a dyno restart?
- How do I confirm it's working post-flip?
- Where's the rollback path?
None of those answers are currently accessible without leaving the console. The
description field is one sentence. The references chips (PR #2230) are
rendered but not yet semantically typed. There is no runtime_behavior signal,
no smoke-test affordance, and no per-flag knowledge page at a stable URL.
Three architecture decisions needed resolution before sub-cards could be split:
Decision A: Pony-Docs framework for internal-docs.raxx.app/flags/
Decision B: Where docs_path lives
Decision C: Dependency declaration mechanism
Decision A — Pony-Docs framework
Options considered
| Option | Sidebar nav | Templated pages | Search | Cross-refs | New deps | Notes |
|---|---|---|---|---|---|---|
| Hugo | Yes | Yes | WASM | Plugin | Yes (Go binary) | Extra toolchain |
| MkDocs + Material | Yes | Yes | Built-in | Plugin | Yes (pip) | Excellent but new stack |
| Docusaurus | Yes | Yes | Built-in | Plugin | Yes (Node/npm) | Aligns with Antlers; post-v1 migration candidate for public docs |
| Custom Python + Jinja2 | Generated | Templated | Lunr.js | Regex | None | Same pattern as build-internal-docs.py already in repo |
Decision
Custom Python + Jinja2 builder (scripts/build-flag-docs.py), emitting
static HTML/MD consumed by CF Pages.
Rationale
- The internal docs pattern is already established.
scripts/build-internal-docs.pyexists and the deploy workflow is proven. Zero toolchain delta. - The four Pony-Docs qualities (sidebar nav, templated pages, search, cross-refs) are all achievable:
- Sidebar nav: generated
sidebar.jsondrives a left-rail Jinja2 partial; hierarchy is surface → area → flag, matchingfeature_flags.yamlstructure. - Templated pages: one Jinja2 template per flag; sections are Overview, Behavior, Configuration, Dependencies, Smoke tests, Rollout history, Troubleshooting, Related.
- Search: Lunr.js (MIT, 8 KB gzipped, zero server dependency). Pre-built index emitted by the generator at build time. Search-first UX: the search box is the first focusable element on every page; operators who don't know the flag key can type a word from the description and find it.
- Cross-refs:
flag:<key>,ADR-<N>,PR #<N>,issue #<N>patterns in any markdown field are regex-replaced with hyperlinks at render time. - The ADR-0088 decision for public docs (custom Python at v1, Docusaurus post-v1) is preserved. If a migration to Docusaurus happens for public docs, the internal flag docs can be migrated in the same pass — both consume Markdown files.
Consequences
- No Hugo, MkDocs, or Docusaurus dependency added to the repo.
- Search quality is Lunr.js (good for 200 flags; re-evaluate if corpus grows beyond ~2,000 pages).
- The generator is plain Python (stdlib + Jinja2, which is already present). Any developer familiar with the existing build scripts can extend it.
- Post-v1, if the Docusaurus migration happens for public docs, it is the natural time to revisit the flag docs builder too.
Decision B — Where docs_path lives
Options considered
- Separate mapping file (
docs/flag-docs-map.yaml): flag key → docs URL. Extra file, extra sync surface, likely to drift. - Inline in
feature_flags.yaml(optional fielddocs_path): single source of truth; additive and backwards-compatible; lintable in the same pass as other YAML validation. - Auto-derived from
flag_key+surface:/flags/<surface>/<flag_key>. Zero YAML authoring cost; works immediately for all 158 flags; operator cannot point a flag at a custom handwritten page.
Decision
Auto-derive as the default (/flags/<flag_key>), with an optional inline
override (docs_path: in YAML) for flags that have a custom hand-authored page.
Rationale
- Auto-derivation means all 158 existing flags get a deep-link immediately at Phase 3 launch — no YAML authoring sprint required before the button shows up.
- The override escape hatch allows curated pages for high-risk or complex flags
(
break_glass_session,enable_options_backtest) without polluting every entry. - Single source of truth: YAML, not a separate map file.
Decision C — Dependency declaration mechanism
Options considered
- Implicit via
references: parse references forflag:mentions and infer dependencies. Fragile; references are documentation, not a machine contract. - Explicit
dependencies:list in YAML:dependencies: [rbac_v2, ...]. Unambiguous; lintable; bidirectional reference graph derivable at build time. - Separate dependency graph file: extra sync surface, same drift risk as Decision B option 1.
Decision
Explicit dependencies: list in YAML (empty list or absent = no dependencies).
Rationale
- The Phase 2 pre-flip checklist must programmatically query dependency state. A
machine-readable list is required; prose
referencescannot serve this role. - The YAML lint check can validate that every key listed in
dependenciesis a known flag key, preventing typos from silently producing empty checklists. - Bidirectional: the docs generator can compute "flags that depend on me" by inverting the list at build time, giving operators a "depended-upon-by" section on each flag page.