Merge branch 'docs/failure-handling-handbook'

Composes failure-handling.md (input contract discipline,
failure-propagation discipline) and adds a frontmatter convention
(title, collections) to both handbooks, fixing a collection-tag
drift found in the process.
This commit is contained in:
2026-08-22 17:05:48 +09:00
3 changed files with 123 additions and 18 deletions
+51 -14
View File
@@ -28,28 +28,65 @@ a retrieval store, not in a skill.
One markdown file per topic at the repo root. Each file should stand as a single admittable
document — the same shape a human vouches for at KB admission time.
## Ingesting a handbook
Each file opens with a small flat YAML frontmatter block — `title` and `collections` only, no nested
structure, so it stays greppable without a YAML dependency (same reasoning `index.py` itself gives
for hand-parsing `corpora.yaml`):
Markdown needs no conversion and no extraction verifier — admit it straight from this repo:
```bash
index.py admit ~/dev/handbooks/git-operations.md --corpus engineering \
--vouch mikkeli --title 'Git operations' --collection dev-practice \
--version "$(git -C ~/dev/handbooks rev-parse --short HEAD)"
```yaml
---
title: Git operations
collections: [handbooks, dev-practice]
---
```
Without `--title` the document is titled by its filename, which is what a retrieval hit then shows.
`--collection` is a flat cluster tag; getting it wrong costs recall and nothing else.
`title` and `collections` are the two ingest-time values that are actually about the document, so
they live with it instead of being retyped by hand at admission time — that's what let `--collection
dev-practice` and an actual admission of `handbooks` drift apart on 2026-08-22, silently, until it was
noticed by inspection rather than by anything catching it.
⚠ **`--version` is this repo's commit, and it is the one that matters.** Re-admitting replaces a
document outright with no version history, so the pin is the only record of which revision a chunk
reflects.
`version` is deliberately **not** a frontmatter field — a file can't contain the hash of the commit
that writes it, the same self-reference problem any `__COMMIT__` build stamp runs into. It stays a
value computed at admission time, same as before, just scoped to the file rather than the whole repo.
⚠ **Editing a file here does not update the corpus.** Re-admission is deliberate, because the vouch
is a claim about a specific version of a document.
## Ingesting a handbook
Markdown needs no conversion and no extraction verifier — admit it straight from this repo, reading
`title`/`collections` from the frontmatter instead of retyping them:
```bash
F=~/dev/handbooks/git-operations.md
TITLE=$(sed -n '/^---$/,/^---$/{/^title:/s/^title:[[:space:]]*//p}' "$F")
COLLECTIONS=$(sed -n '/^---$/,/^---$/{/^collections:/s/^collections:[[:space:]]*//p}' "$F" \
| tr -d '[]' | tr ',' '\n' | sed 's/^ *//;s/ *$//')
VERSION=$(git -C ~/dev/handbooks log -1 --format=%h -- "$F")
index.py admit "$F" --corpus engineering --vouch mikkeli \
--title "$TITLE" --version "$VERSION" \
$(for c in $COLLECTIONS; do printf -- '--collection %s ' "$c"; done)
```
⚠ **`--version` is the last commit that touched *this file*, not the repo's `HEAD`.** A whole-repo
`HEAD` pin would bump every handbook's recorded version on any unrelated edit, even one that never
touched this document — the per-file form is what actually answers "which revision does this chunk
reflect."
⚠ **Re-admitting replaces a document outright, with no version history kept.** The `--version` pin is
the only record of which revision a chunk reflects, which is also why editing a file here does not by
itself update the corpus — re-admission is deliberate, a fresh claim about a specific version of a
document, not something that should happen automatically on every commit.
## Status
Started 2026-08-21. First handbook, `git-operations.md`, composed 2026-08-21 – 2026-08-22: branching,
committing, merging vs. rebasing, reverting and undoing, remotes and collaboration, tags and releases.
Confirmed ingestible 2026-08-22 — 30 chunks, heading paths intact, retrieved correctly.
Second handbook, `failure-handling.md`, composed 2026-08-22: input contract discipline, failure-
propagation discipline. Environment/verification parity was scoped out of it deliberately — a
different axis (how confidence is established, not how code behaves), queued as a handbook of its
own rather than folded in.
Frontmatter (`title`, `collections`) added to both 2026-08-22, after `--collection dev-practice` in
this file's own recipe was found to have drifted from `handbooks`, the value actually used at
admission — the two ingest-time values that describe the document now live with it instead of being
retyped by hand.
+67 -4
View File
@@ -1,7 +1,11 @@
---
title: Failure handling
collections: [handbooks, dev-practice]
---
# Failure handling
> Composed 2026-08-22. General practice, not tied to any one project or language. Input contract
> discipline is composed; failure-propagation discipline is still outline.
> Composed 2026-08-22. General practice, not tied to any one project or language.
## Scope
@@ -63,6 +67,65 @@ or worse in production, is what leaves it open.
## Failure-propagation discipline
*(Not yet composed.)*
**Never substitute a plausible-looking artifact for a failed operation.** When something can't
complete its contract, the correct output is a signal that it didn't — not a value that resembles
success closely enough to pass unnoticed. A blank result where real output should be, a guard that
reports "fine" because it couldn't read what it was checking, a stale cached value returned as if it
were fresh: each of these is a failure wearing success's clothes, and the cost is always paid later,
by whoever trusted the output.
*(Handbook in progress — sections above stand as citable units; the rest is outline.)*
**A check that can't verify must refuse — and "couldn't verify" must be a different state than
"verified, and it's fine."** Collapsing both into one boolean is the mechanism behind the failure
above: a function that returns `true` for both "the input passed validation" and "validation itself
couldn't run" cannot be trusted by anything downstream, because the caller has no way to tell which
one happened. Whatever the check returns, its failure-to-run and its pass result need to be
distinguishable — a third state, a different return type, an exception path, anything that doesn't
require the caller to assume good faith on the checker's behalf.
**Retry, abort, and surface are three different responses to a failure, and picking the wrong one is
a fail-loudly violation on its own.**
- **Retry** only for failures that are plausibly transient and where repeating the operation doesn't
change its meaning (a network read, not a payment). Bound it — a fixed number of attempts or a
timeout, never unconditional — and don't let a bounded retry become an unbounded one by accident (a
retry nested inside another retry, a caller that itself retries a function that already retries
internally).
- **Abort** when the failure indicates a broken invariant or a state that's unsafe to continue from —
retrying or asking would just compound whatever's already wrong.
- **Surface** — to a caller, a human, or an error return — when the system doesn't have enough
information to pick correctly on its own. This is usually the right default when nothing above
clearly applies: asking is cheaper than guessing wrong.
**A retry that eventually succeeds is still a failure that happened, and needs to be recorded as one,
not discarded because the second attempt worked.** The caller downstream can get the successful
result — that's the point of retrying — but "the first attempt failed" is real information: it's how
a dependency's slow slide toward actually breaking becomes visible before it breaks outright, instead
of only being visible in the moment it finally does. A retry policy that only speaks up once retries
are exhausted throws that signal away for free.
**Pick one failure-signaling convention per boundary, and don't mix it with another.** Exceptions, a
`Result`-shaped return, a sentinel value, an error code — any of these work, but only if every caller
at that boundary can rely on which one is in play. Mixing them within the same boundary is its own
failure mode: a caller checking a sentinel value for a function that actually raises, or catching an
exception type a function never throws, proceeds past the real failure exactly as if it hadn't
checked at all — the check existed, and did nothing.
**Anti-patterns, all variations on the same shape:**
- A swallowed exception (`except: pass`, an empty `catch` block) — the failure existed for one
instruction and then didn't.
- `continue` on a caught exception inside a loop — one bad item disappears with no record it was ever
there, and the loop reports success for everything it actually attempted.
- A fallback default that isn't marked as one — returning `0`, `null`, an empty collection, or a
stale cached value in place of a failed real result, with nothing distinguishing it from a genuine
`0`, `null`, empty collection, or fresh value.
- A comment explaining away a shortcut instead of the code being fixed — `# TODO: handle this
properly` sitting next to the exact silent failure it describes, left in place.
- An unbounded retry — functionally identical to abort, except it costs resources first and never
actually informs anyone that it gave up.
The same discipline shows up in test coverage: a suite that only exercises the happy path hasn't
tested the code, it's tested the narrative about the code.
*(Handbook composed 2026-08-22. Both sections above stand as citable units — a chunk boundary, not
just a heading.)*
+5
View File
@@ -1,3 +1,8 @@
---
title: Git operations
collections: [handbooks, dev-practice]
---
# Git operations
> Composed 2026-08-21 – 2026-08-22. General practice, not tied to any one project's Gitea/GitHub