Skip to main content

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 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 (BUILT / WIRED / MISSING) so the recon is continuous instead of a manual LLM pass.

Registry layering — which file owns which question

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.

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. Overclaimexpect 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

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:
  • 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 — 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 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)

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.