PolicyEngine / PolicyEngine/chronicle

Facts-only store: type source-published projections as facts, enforce period contracts at consumption

Open
#71 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
0
Forks
1
Avg merge
11h 48m
Merged PRs (30d)
45

Description

What

Keep Ledger a facts-only store, and make the schema enforce it. The boundary is who asserted the value, not "level vs projection":

  • Facts — immutable, as-published claims with lineage to source cells. This includes sources' own projections: "CBO's January 2026 baseline projects individual income tax receipts of $X in 2027" is a source-backed claim with provenance, exactly like an SOI observation. Type these with an explicit assertion field (observation vs source_projection) so consumers can distinguish measured outcomes from publisher forecasts (CBO baselines, BFP outlooks, SSA trustees tables, TPC/JCT scores).
  • PolicyEngine-computed values — aged, uprated, or forecast levels we derive (e.g., SOI TY2022 aged to TY2025 under CBO factors) — never enter the store. They are regenerable build artifacts, not ledger entries, and they live in Populace as a named, versioned aging implementation that consumes growth-factor facts from Ledger and emits its own lineage.

What Ledger contributes instead of projection objects:

  1. Rigorous period semantics on every fact — the period a value refers to, distinct from the release vintage/label. This covers BE-SILC lagged incomes and heterogeneous Statbel/BFP reference periods without any projection machinery.
  2. Period-contract enforcement at consumption — resolving a profile target at a period other than the fact's reference period hard-fails unless the consumer passes an explicit, named alignment declaration (model id + version + parameters). Ledger records the declaration in diagnostics; it never computes the aligned number.
  3. Basis-aware diagnostics — resolution rows carry fact_period, requested_period, and the declared alignment, so downstream diagnostics distinguish "missed a published fact" from "missed an aged level."

Why this framing (not facts + projections in one schema)

  • Thesis stays clean. Thesis resolves forecasts against Ledger facts as official observations. If the store contains PolicyEngine-computed projections, a forecast can end up scored against partly-model output — circular. A facts-only ledger is a model-free resolution substrate.
  • Append-only stays meaningful. Facts never churn; PE-computed projections churn every CBO update and every aging-model version bump. Storing them alongside facts turns an auditable ledger into a store of volatile derivations wearing provenance costumes.
  • It's the existing boundary. docs/architecture.md and AGENTS.md already assign aging to Populace and forbid "derived facts whose source is Ledger itself." The gap is schema + enforcement, not policy.
  • The populace#212 lesson, re-read: the failure wasn't that aging lived in Populace — it's that un-aged consumption was silent. Calibration was exact against SOI TY2022/23 levels applied at 2024 while simulated 2025 aggregates ran ~6–10% under current-year projections. The fix is making that impossible to do silently, which is a consumption-contract property, not a projection-object property.
  • The shared-implementation argument dissolves. Thesis doesn't want PE-computed aging (its forecasts live in brier; its resolutions need raw facts). Validation comparators (TPC ~$130B FY26, JCT scores) are source-published → facts with assertion: source_projection. The only consumer of PE-computed aged values is Populace calibration, so that's where the code belongs (PolicyEngine/populace#116 is the concrete case).

Scope

  • Schema: assertion: observation | source_projection on aggregate facts (key-stable: only non-default values enter canonical key payloads); explicit period-coverage metadata (reference period start/end, basis, source period label, accounting basis) as non-identity provenance — absorbing the design from #50.
  • Consumer contract: resolution API that enforces the period contract and emits basis-aware diagnostics; consumer rows expose assertion explicitly.
  • Governance: boundary checks reject PE-computed values in facts; AGENTS.md/governance doc updated; ADR recording the decision.
  • Populace: versioned aging library consuming growth-factor facts via the consumer artifact (#61), declaring alignments at resolution time (tracked populace-side; see populace#116, populace#212).
  • Geography vintage translation (populace#205) follows the same pattern: a declared consumer-side transform over facts, never an edit to them.

Acceptance

  • CBO/BFP/TPC-style source projections representable as facts with assertion: source_projection, full lineage, byte-stable keys for all existing facts.
  • Resolving a dollar target at a period ≠ the fact's reference period without a declared alignment raises; with a declaration, resolution rows carry basis + alignment metadata end to end.
  • Facts remain byte-identical before/after; no object in the store carries a PolicyEngine-computed value.
  • Diagnostics distinguish "missed a published fact" from "missed an aged level."

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with docs/architecture.md and AGENTS.md, then inspect the existing aggregate-fact schema and resolution path described in the issue. Use the acceptance criteria to define completion: source projections are representable with lineage, period mismatches require declared alignments, diagnostics expose basis metadata, and existing facts remain byte-identical.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design, data, database, documentation
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.