Farpost · Data Modeling
$ Not Every Fact Goes Stale at the Same Rate
Not every fact about a building goes out of date at the same speed. Foundation documentation is still probably accurate a decade later; a mechanical inspection is not. Farpost's building records track that directly, instead of flattening every fact into one "last updated" timestamp for the whole record.
Each category — foundation, electrical, plumbing, roof, mechanical, and others — has its own half-life, in months, and its own independent staleness clock, so a building can have fresh foundation data and stale mechanical data at the same time, and get renotified about only the part that's actually gone stale.
async def upsert_fact_staleness(self, slug: str, category: str, visit_date: date):
"""Resets the category's staleness clock to the new visit_date."""
building = await self.find_by_slug(slug)
half_life_months = FACT_STALENESS_HALF_LIFE_MONTHS[category]
building.fact_staleness[category] = FactStaleness(
last_documented_at=visit_date,
half_life_months=half_life_months,
next_stale_at=_add_months(visit_date, half_life_months),
notified_at=None,
)
await building.save()
return building
async def get_due_for_fact_decay(self) -> list[Building]:
"""Categories are a small fixed set, so this is a plain $or across each known
key rather than an aggregation pipeline."""
today = date.today()
conditions = [
{
f"fact_staleness.{category}.next_stale_at": {"$lt": today},
f"fact_staleness.{category}.notified_at": None,
}
for category in FACT_STALENESS_HALF_LIFE_MONTHS
]
return await Building.find({"$or": conditions}).to_list()The fix
Recording a visit resets that specific category's clock — next_stale_at is set to the visit date plus that category's own half-life (120 months for foundation down to 36 for mechanical). Finding buildings due for renotification is a plain $or across each of the small, fixed set of categories, checking each one's next_stale_at and notified_at independently — deliberately not an aggregation pipeline, since the category list is small and fixed rather than something that grows without bound.
Why this matters
Modeling "how fast does this specific kind of information actually decay" instead of collapsing it into one blanket timestamp is a more nuanced, more accurate model of the real problem — and choosing the plain query over a fancier aggregation pipeline, because the actual shape of the data didn't call for one, is its own kind of judgment: knowing when the simpler tool is the correct one, not just the easier one.
