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:
@@ -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
|
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.
|
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:
|
```yaml
|
||||||
|
---
|
||||||
```bash
|
title: Git operations
|
||||||
index.py admit ~/dev/handbooks/git-operations.md --corpus engineering \
|
collections: [handbooks, dev-practice]
|
||||||
--vouch mikkeli --title 'Git operations' --collection dev-practice \
|
---
|
||||||
--version "$(git -C ~/dev/handbooks rev-parse --short HEAD)"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Without `--title` the document is titled by its filename, which is what a retrieval hit then shows.
|
`title` and `collections` are the two ingest-time values that are actually about the document, so
|
||||||
`--collection` is a flat cluster tag; getting it wrong costs recall and nothing else.
|
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
|
`version` is deliberately **not** a frontmatter field — a file can't contain the hash of the commit
|
||||||
document outright with no version history, so the pin is the only record of which revision a chunk
|
that writes it, the same self-reference problem any `__COMMIT__` build stamp runs into. It stays a
|
||||||
reflects.
|
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
|
## Ingesting a handbook
|
||||||
is a claim about a specific version of a document.
|
|
||||||
|
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
|
## Status
|
||||||
|
|
||||||
Started 2026-08-21. First handbook, `git-operations.md`, composed 2026-08-21 – 2026-08-22: branching,
|
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.
|
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.
|
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
@@ -1,7 +1,11 @@
|
|||||||
|
---
|
||||||
|
title: Failure handling
|
||||||
|
collections: [handbooks, dev-practice]
|
||||||
|
---
|
||||||
|
|
||||||
# Failure handling
|
# Failure handling
|
||||||
|
|
||||||
> Composed 2026-08-22. General practice, not tied to any one project or language. Input contract
|
> Composed 2026-08-22. General practice, not tied to any one project or language.
|
||||||
> discipline is composed; failure-propagation discipline is still outline.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -63,6 +67,65 @@ or worse in production, is what leaves it open.
|
|||||||
|
|
||||||
## Failure-propagation discipline
|
## 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.)*
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Git operations
|
||||||
|
collections: [handbooks, dev-practice]
|
||||||
|
---
|
||||||
|
|
||||||
# Git operations
|
# Git operations
|
||||||
|
|
||||||
> Composed 2026-08-21 – 2026-08-22. General practice, not tied to any one project's Gitea/GitHub
|
> Composed 2026-08-21 – 2026-08-22. General practice, not tied to any one project's Gitea/GitHub
|
||||||
|
|||||||
Reference in New Issue
Block a user