> ## Documentation Index
> Fetch the complete documentation index at: https://docs.latwoodtech.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Feature truth

# Feature Truth — the machine-computed feature ledger

> **Status:** Canonical · **Introduced:** 2026-07-10 · **Owner:** Factory platform
> Engine: [`scripts/feature-truth/feature-truth.mjs`](../../scripts/feature-truth/feature-truth.mjs) ·
> Reusable workflow: [`.github/workflows/_feature-truth.yml`](../../.github/workflows/_feature-truth.yml) ·
> Schema: [`features.schema.json`](./features.schema.json)

## The law

**Humans author intent; machines author status.** No file in any repo may contain a
hand-typed "Shipped". A feature's status is *computed* by joining signals across
surfaces (worker routes, web callers, nav, tests, telemetry). The only status-shaped
field a human writes is `expect:` — a *claim* the machine then verifies. A claim the
signals can't support is a CI failure, not a docs bug.

This mechanizes Phase 3 of the [Wingspan Method](../playbooks/wingspan-method.md)
(BUILT / WIRED / MISSING) so the recon is continuous instead of a manual LLM pass.

## Registry layering — which file owns which question

| File                                               | Question it answers                                                                           | Authored by                                                            |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `docs/service-registry.yml` (Factory)              | Which workers/services exist, where do they live, who consumes them?                          | human                                                                  |
| `features.yml` (each repo root)                    | What product capabilities is this app *supposed* to have, and what is each expected to reach? | human (intent fields only)                                             |
| `docs/_generated/feature-truth.json` (each repo)   | What rung has each feature *actually* reached?                                                | **machine only**                                                       |
| `docs/FEATURES.md` (each repo)                     | Human-readable view of the above                                                              | **machine only** (generated between markers)                           |
| `feature-registry.yml` (each repo root, schema v1) | Marketing/dashboard view for latwoodtech.com/platform/                                        | human labels; **status overridden by feature-truth.json when present** |
| `docs/capabilities/*.yml` (Factory)                | Which routes may the supervisor call, with what blast radius?                                 | human                                                                  |

Any other feature inventory in a repo (e.g. a hand-maintained `FEATURE_REGISTRY.md`
status column) is **subordinate to `feature-truth.json`** and should be converted or
retired. One intent source per repo: `features.yml`.

## The status ladder

Rungs are ordinal; a feature holds the highest rung whose signals (and all below) hold.

| Rung        | Meaning                          | Signal                                                                                                                                                                                                                          |
| ----------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `planned`   | Declared, no code                | — (default)                                                                                                                                                                                                                     |
| `built`     | Backend/module exists            | every declared route resolves in worker source (mount-aware match), or all declared `sources` files exist                                                                                                                       |
| `wired`     | A caller exists                  | route path found in web source (exact, param-cut, or base-composed prefix — callers often build `'/api/x' + '/y'`); pages-only features: page implementation found                                                              |
| `reachable` | A user can find it               | a declared page appears in nav files **or** has an inbound link from any other page (real apps reach pages through flows, not just global nav); API-only features inherit `wired` and are flagged `api_only`                    |
| `shipped`   | Reachable **and proven by test** | `@feature F-ID` tag in a test/spec or declared `tests` exist (canonical), **or** a spec references the feature's route/page (inferred — bootstraps repos pre-tag-adoption); if per-feature test results are present, 0 failures |
| `alive`     | Shipped **and used**             | ≥1 of the feature's `analytics_events` fired in the last 30 d (`feature-usage.json`)                                                                                                                                            |

### The `surface` field — who calls it

Not every feature has a first-party web caller *by design*. `surface:` declares the
caller class so the ladder stays honest without lying about wiring:

* `web` (default) — first-party web caller expected; full signal chain applies.
* `partner` — the caller is external (Stripe webhooks, OAuth IdP consumed by another
  app, crawler-facing SEO routes). `wired`/`reachable` inherit `built`.
* `internal` / `ops` — no caller expected (crons, queue pipelines, DO internals,
  admin/ops tooling). `wired`/`reachable` inherit `built`.

Misusing `surface` to hide a genuinely dark web feature is the one way to cheat this
system — reviewers should treat `surface:` changes on existing features with the same
suspicion as a deleted test.

Failure states (computed alongside the rung):

* **`dark`** — built but not wired. The classic wingspan delta; latent capability.
* **`zombie`** — shipped, usage data present, zero events in 30 d. Built product meeting no demand.

## Hard gates vs advisory (CI semantics)

`feature-truth.mjs --check` **hard-fails** on exactly three things:

1. **Overclaim** — `expect` is higher than the computed rung. The claim is a lie; fix the wiring or lower the claim.
2. **Orphan route file** — a file under `route_files` dirs that no feature claims in `sources` and no exemption covers. This is what forces registration at PR time and prevents duplicate re-builds of unregistered capability.
3. **Schema violations** — unparseable YAML, duplicate IDs, unknown `expect` values.

Everything else is **advisory** (warnings, exit 0): underclaim (computed rung above
`expect` — raise your claim), stale `FEATURES.md`, missing optional signal files.
Generated-freshness must never hard-block (standing doctrine — see
`project_docs_antirot` history and DOCS\_TRUTH\_AND\_GUARDRAILS.md).

## features.yml (schema v2) — annotated example

```yaml theme={null}
version: 2
app: capricast                      # matches service-registry.yml id

scan:                               # where the engine looks for each signal
  worker_src: [apps/worker/src]     # route mounts
  web_src: [apps/web/src]           # callers + page implementations
  nav_files:                        # what counts as "findable"
    - apps/web/src/components/Navbar.tsx
    - apps/web/src/components/Footer.tsx
  test_dirs: [e2e, apps/worker/src, apps/web/src]

route_files: [apps/worker/src/routes]   # every file here must be claimed (orphan gate)
exempt_route_files:                      # infra/telemetry routes exempt from ownership
  - apps/worker/src/routes/health.ts

features:
  - id: F-CONF-01                   # stable, F-<DOMAIN>-NN
    title: Conference scheduling
    actor: creator
    story: As a creator, I can schedule a conference so fans can book time with me.
    expect: shipped                 # the human claim — the machine verifies it
    routes:
      - POST /api/conference/rooms  # METHOD /path — used for built + wired signals
    pages: [/studio/conference]     # used for wired + reachable signals
    sources:                        # files this feature claims (orphan gate + built fallback)
      - apps/worker/src/routes/conference.ts
    analytics_events: [conference_scheduled]   # Alive rung (PostHog)
    tests: [e2e/07-conference.spec.ts]          # optional explicit binding
    notes: RealtimeKit room component is RealtimeKitRoom, not ConferenceRoomPanel.
```

Intent fields (`title`, `actor`, `story`, `expect`, `notes`) are yours. Everything in
`docs/_generated/` and `docs/FEATURES.md` belongs to the engine.

## Test binding (battery integration)

Tag any test that proves a feature with `@feature F-ID` in its title or a comment:

```ts theme={null}
test('creator schedules a conference @feature F-CONF-01', async () => { … });
```

* The tag alone satisfies the `shipped` rung's test signal.
* For per-feature pass/fail (not just existence), add the Vitest reporter
  [`scripts/feature-truth/vitest-feature-reporter.mjs`](../../scripts/feature-truth/vitest-feature-reporter.mjs)
  — it writes `docs/_generated/feature-test-results.json`, and the engine then requires
  the feature's tagged tests to have **0 failures** for `shipped`.
* One test may prove several features; tag it with each.

## Alive rung (telemetry integration)

[`scripts/feature-truth/fetch-feature-usage.mjs`](../../scripts/feature-truth/fetch-feature-usage.mjs)
queries PostHog for every `analytics_events` name in `features.yml` (30-day window)
and writes `docs/_generated/feature-usage.json`. Run it on a schedule (not per-PR);
the engine merges it when present. Without it, features stop at `shipped` — absence
of telemetry data never fails a build.

## Consuming from CI (per app repo)

```yaml theme={null}
jobs:
  feature-truth:
    uses: Latimer-Woods-Tech/factory/.github/workflows/_feature-truth.yml@main
    with:
      app_id: capricast
```

The reusable workflow checks out the calling repo + Factory, runs the engine in
`--check` mode, and uploads `feature-truth.json` as an artifact. Regeneration of
`FEATURES.md`/`feature-truth.json` happens locally or on a scheduled job — CI only
verifies, so generated-file staleness can warn without blocking.

## Who registers a feature

Adding capability = adding a `features.yml` entry **in the same PR**. The orphan gate
enforces this mechanically: a new route file that no feature claims fails CI. The PR
template's checklist item ("feature registered or exemption added") is the human-facing
mirror of the same rule.

## Portfolio rollup

* `scripts/platform_conformance.py` scores a **Feature truth** shadow dimension
  (presence of `features.yml`, engine output freshness, zero overclaims/orphans).
  Shadow = advisory until Stage 4, like every other dimension.
* `.github/scripts/generate-platform-data.mjs` (trophy case feed for
  latwoodtech.com/platform/) **prefers computed truth**: when a repo publishes
  `docs/_generated/feature-truth.json`, dashboard feature status comes from computed
  rungs, never from hand-typed `status: live`.

## Roadmap (adopted 2026-07-10)

1. **v1 (this kit)** — grep-join signals, ladder, gates, Capricast pilot.
2. **Battery binding** — reporter adoption in each repo's vitest/playwright config.
3. **Alive rung** — PostHog fetch scheduled per repo.
4. **OpenAPI emission** — as repos adopt `chanfana`/`@hono/zod-openapi`, the engine
   reads the emitted spec instead of grepping mounts (per-repo conformance sub-check).
5. **Intent-conformance judge** — Quincy-style rubric judge comparing each shipped
   feature's `story` against live behavior (curl + screenshot), weekly, advisory.
