Feature Truth — the machine-computed feature ledger
Status: Canonical · Introduced: 2026-07-10 · Owner: Factory platform Engine:scripts/feature-truth/feature-truth.mjs· Reusable workflow:.github/workflows/_feature-truth.yml· Schema: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 isexpect: — 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
(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 |
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/reachableinheritbuilt.internal/ops— no caller expected (crons, queue pipelines, DO internals, admin/ops tooling).wired/reachableinheritbuilt.
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:
- Overclaim —
expectis higher than the computed rung. The claim is a lie; fix the wiring or lower the claim. - Orphan route file — a file under
route_filesdirs that no feature claims insourcesand no exemption covers. This is what forces registration at PR time and prevents duplicate re-builds of unregistered capability. - Schema violations — unparseable YAML, duplicate IDs, unknown
expectvalues.
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
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:
- The tag alone satisfies the
shippedrung’s test signal. - For per-feature pass/fail (not just existence), add the Vitest reporter
scripts/feature-truth/vitest-feature-reporter.mjs— it writesdocs/_generated/feature-test-results.json, and the engine then requires the feature’s tagged tests to have 0 failures forshipped. - One test may prove several features; tag it with each.
Alive rung (telemetry integration)
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)
--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 afeatures.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.pyscores a Feature truth shadow dimension (presence offeatures.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 publishesdocs/_generated/feature-truth.json, dashboard feature status comes from computed rungs, never from hand-typedstatus: live.
Roadmap (adopted 2026-07-10)
- v1 (this kit) — grep-join signals, ladder, gates, Capricast pilot.
- Battery binding — reporter adoption in each repo’s vitest/playwright config.
- Alive rung — PostHog fetch scheduled per repo.
- OpenAPI emission — as repos adopt
chanfana/@hono/zod-openapi, the engine reads the emitted spec instead of grepping mounts (per-repo conformance sub-check). - Intent-conformance judge — Quincy-style rubric judge comparing each shipped
feature’s
storyagainst live behavior (curl + screenshot), weekly, advisory.