Case 404-PAP: the hardcoded drift

How a lost afternoon under an anti-PAP drift was fixed in 8 lines — and turned into a system

Jesús Pérez
Adding a content type that 'should have been just NCL' cost ~88 minutes, 8 builds and 8 false leads: a hardcoded match shadowed the routes registry (anti-PAP). The fix was 8 lines. But what the case left behind — a howto, a mode, a proof, a series — is the real evidence of what ontoref is for.
Case 404-PAP: the hardcoded drift

🕵️ Show the full case file → 📋 Session protocol →

Case file · Code Homicide Dept.

It was a quiet afternoon. Then <span class='mono'>/recursos</span> walked in and showed nothing. Nothing at all. And I, naive, thought it would be quick.

Case No. 404-PAPClassification: ANTI-PAPStatus: SOLVED
Show glossary
PAP
Project's Architecture Principles — the rules and patterns holding the project's architecture up: the single source of truth all code must respect.
anti-PAP
Code written against those rules: a stub that re-lists by hand what routes.ncl already declares, duplicating and contradicting the source of truth.

The protocol to declare, version and verify this → ontoref.dev

The double ledger — what it cost, and what it left

A case isn’t judged only by what it costs. It’s judged by what it leaves. From a hope-less low —eight false leads, six fingerprints, the whole afternoon— we found our bearings, and everything that was travelling on the same journey clicked into place at once:

What the crime cost

  • Pure origin hunt (framework only) ~88 <i>min</i>
  • Release builds launched 8
  • Server restarts ~15+
  • Build fingerprints coexisting 6
  • Files of <i>someone else's</i> code read (2 repos) ~20+
  • Full <span class='mono'>cargo clean</span> runs (867&nbsp;MB gone) 2
  • False leads before the right one 8
  • Size of the final fix 8 <i>lines</i>

What the case left

  • Registry-driven fix PAP-compliant
  • Knowledge content-kind-howto
  • Mechanism generate-expediente mode
  • Evidence proof (positioning)
  • Series + hub /case-files
  • Framework unblocked

That’s the credible depth: the afternoon of pain didn’t end in a patch — it ended in a declared system. Ontology, modes and positioning weren’t “added” — they were travelling together, and the case made them converge.

The point of abandonment — what the table doesn't show

There’s a point in every drift that no cost table records: the one where you stop feeling fear and start wanting to quit. Bugs become ghosts —they appear, vanish, come back with a different face—; everything stalls and you don’t know why; there’s no urgency left, only fatigue. “It’s too much. It can’t be solved.” That’s the real moment, the one not measured in builds — the one that kills projects in silence.

With ontoref that point shouldn’t arrive. Not because there are no bugs, but because it doesn’t leave you alone with them: the dao (ondaod) forces you to name the tension instead of collapsing or abandoning it; the modes hand you the next step when you no longer know what it is; the ADRs close what’s already decided so you don’t re-litigate it at two in the morning. The drift doesn’t sink you because the map holds you up.

The suspects — the false leads

The content in site/r“I wasn't in sync.”minor accomplice
filename ≠ id“The body came back empty, but I'm not the 404 guy.”other crime
The {# … #} comment“I just slipped in as text. Innocent.”solid alibi
Staleness & fingerprints“With 6 fingerprints, how could you not doubt the binary?”false lead
SITE_PUBLIC_PATH / symlink“The dir didn't exist. Symlink for nothing.”false lead
generated.rsvec!["content"]“I'm the fallback. I don't even run.”decoy
RBAC“A deny from me is a 302, darling. A 404 isn't my job.”302 alibi
build_page_generator“That's Leptos. In htmx-ssr I don't even show up.”wrong jurisdiction

The weapon — The weapon · a hardcoded match (pre-existing, anti-PAP)

let kind = match base {
    "blog" => Some("blog"),
    "activities" | "actividades" => Some("activities"),
    // … every kind by hand … except resources …
    _ => None,   // ← /recursos landed here → 404
};

Route registered. Content-type enabled. The API returned the items. The content, present. And still, 404 — because a stub decided the truth against the source of truth.

The turn — PAP-compliant · adding a kind is just NCL again

let kind = load_routes_config().routes.iter().find_map(|r| {
    if !r.enabled { return None; }
    let seg = r.path.trim_start_matches('/').split('/').next().unwrap_or("");
    (seg == base).then(|| r.content_type.clone()).flatten()
});

/recursos, /proyectos, /dominios — all of them, bilingual aliases included — resolved from the single source. Adding a kind is just NCL again.

The verdict

We didn’t go to the beach because we chose badly: we went because the map had an unmarked drift. The four things ontoref instruments are the four that were missing:

Hardcoded stub shadowing the registryPAP + anti-patterns → grep-able
Knowledge lost, rediscovered through painqa/howto → content-kind-howto
Building on the invalid without knowingreview against invariants
Assuming the unassumable, believing the unbelievabledeclared state → 3 levels

Case law — what enforces the lesson today

  • The anti-pattern this case promised — the hand-written list that shadows the registry — finally exists as a typed constraint, and it was SEEN TO FAIL against the tree before anything was fixedadr:002/hardcoded-kind-list-shadows-registry
  • And the eight lines went home: rustelo's two templates resolve the kind from the registry, not from a hand-written listcontract:templates/*/crates/server/src/shell/htmx.rs#render_content_or_grid
  • That match arms are an optimisation and not the source of truth — queryable instead of rediscovered the hard wayqa:rustelo-static-page-howto

Declared debt: One thing remains, and it is what decides whether the ones above bite at all: nothing runs the constraint. Rustelo's justfile calls no ADR validator — 23 constraints across 8 ADRs, zero run by recipe or CI — so the rule that now forbids this defect is as written, and as silent, as ADR-002 was for months. A just recipe in rustelo's chain that runs them and reports per constraint would settle it. And two more come with it, ownerless: the instance's cure is still uncommitted (git status M), and templates/ is unversioned — a git clean takes the backport and nobody finds out. Case 8/0 tells this whole story.

A ten-line drift, unmarked, cost us an afternoon to escape — and was fixed in eight. That differential is the ROI. Ontoref is the map’s signposting.

Glosario

Was this useful? Rate it
Got something to add? Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.
· reads

We use cookies to help this site function, understand service usage, and support marketing efforts. Cookie Policy for more info.