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
🕵️ 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.
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 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.rs → vec!["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 registry | PAP + anti-patterns → grep-able |
| Knowledge lost, rediscovered through pain | qa/howto → content-kind-howto |
| Building on the invalid without knowing | review against invariants |
| Assuming the unassumable, believing the unbelievable | declared 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 fixed
adr: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 list
contract: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 way
qa: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
Sin coincidencias.