Senior Application Developer

loc: Maynooth, ON K0L 2S0tel: 613-553-0960email: rgsamways@gmail.com
Robin Samways

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.

Feedback on this page