Field notes · build log 02
The validation gate in action.
Build log 01 showed the record — a git repository where every gate action is a commit. This one shows what keeps that record honest: a schema gate that refuses malformed knowledge, a lifecycle that never resurrects the dead, and rejection codes that turn "no" into telemetry. Every exhibit is real; one demonstration is staged on a scratch copy and labeled as such.
What the gate refuses to see
The validator's docstring states its temperament, and it is the whole philosophy in one line:
"Strict by design: an unknown key or a missing section is a hard failure — governance the validator can't see doesn't exist."
Every object's front matter is checked against an exact key set — required keys present, unknown keys refused, enums enforced, the id matching both its pattern and its filename, the domain matching its directory. And the body is held to a per-type section shape, because the retrieval layer chunks on exactly these headings:
SECTIONS_BY_TYPE = {
"issue-resolution": ["Environment", "Issue", "Cause", "Resolution"],
"how-to": ["Goal", "Applies to", "Before you start", "Steps", "Verify",
"Troubleshooting"],
"faq": ["Question", "Answer"],
"known-error": ["Environment", "Symptom", "Root cause", "Workaround",
"Fix status"],
"note": ["Context", "Insight", "Implication"],
}
The first four shapes are KCS-informed article structures; note is a deliberate extension, and the code comment defends the judgment rather than hiding it:
"Forcing that shape into issue/resolution would be structure as religion; the framework asks for the minimum that stays machine-operable."
This is the house implementation of the same bet Machine-Ready Knowledge makes in public: structure machines can trust is checked, not hoped for.
The teeth, demonstrated
The store passes whole — this run is from the night of publication, and the count includes the eight drafts the capture triggers filed while build log 01 was being written:
$ python3 tools/validate.py 175/175 objects valid
A gate that only ever says yes proves nothing, so here it is saying no. This is a staged demonstration: I copied one object from the frozen reference corpus to a scratch directory outside the repo, planted three faults — an invented status: approved, an unknown reviewed_by: key, and ## Cause renamed to ## Root Cause — and ran the same validator:
$ python3 tools/validate.py /tmp/koe-gate-demo/store FAIL endpoint-support/KO-000003.md - unknown key: reviewed_by - status: must be one of ['archived', 'draft', 'validated'] - body: issue-resolution sections must open with ['Environment', 'Issue', 'Cause', 'Resolution'] in order (found ['Environment', 'Issue', 'Root Cause', 'Resolution', 'Notes']) 0/1 objects valid — 1 FAILED # exit 1
All three faults named, in human-readable sentences, with a non-zero exit — which is what lets the same script guard commits and the publish path unattended.
Drafts are invisible until a human says otherwise
Validation is one gate of a larger lifecycle, and the lifecycle tool states its own rules:
"draft → validated → archived. The gate refuses schema-invalid objects and never resurrects archived ones (let dead knowledge go — author a new object). publishable prints the exact set the AI layer may project … the only-validated-publishes guard. Everything else stays invisible to retrieval."
$ python3 tools/gate.py status | tail -1 draft 8 · validated 152 · archived 15 · publishable 152 $ python3 tools/gate.py publishable | wc -l 152
Eight drafts exist tonight and exactly none of them are answerable-from. That is the guard doing its job: knowledge the AI layer can cite is knowledge a human has validated, with no exceptions to remember.
Rejection is telemetry
Not everything the capture triggers propose deserves to become knowledge. Saying no is a first-class act with its own vocabulary — six reason codes, and the comment above them explains why an uncoded "no" is a waste:
REJECT_REASONS = {
"R1": "not knowledge-worthy — a one-off, no reuse potential",
"R2": "duplicate — covered by an existing object",
"R3": "inaccurate or unverifiable",
"R4": "policy or privacy risk",
"R5": "out of scope or wrong audience",
"R6": "insufficient demand evidence",
}
"The reason IS the telemetry: a spike in one code names the upstream fault (R1 = the trigger fires too eagerly, R6 = it fires without demand), so an uncoded rejection throws away the only signal that says which capture source to throttle."
Rejected candidates never touch the record. The repository's own ignore file states the design in two comments:
# Candidates are ephemeral — they only enter git history by being promoted # into store/ through the Curation Loop's gate. queue/* # Signals are operational telemetry, not knowledge. signals/*
The rejection itself lands in the signals log. Here is a real one — a duplicate stopped at the door:
{"ts": "2026-09-07T22:41:44Z", "signal": "candidate_rejected",
"reason": "R2", "reason_text": "duplicate — covered by an existing object",
"candidate_id": "CAND-20260905-040010", "origin": "reflect-loop",
"dedupe_key": "protect deep work time"}
That R2 has a history worth telling honestly. Over a month, different capture triggers proposed near-identical advice seven times — Focus on High-Impact Tasks, Dedicate Time to High-Impact Tasks, Protect Time for High-Impact Tasks, Prioritize High-Impact Tasks, Protect Deep Work Blocks (twice) — and the gate archived every duplicate, keeping one validated survivor (KO-000127, Protect High-Impact Time). The R2 above stopped the eighth at the door. And the morning this post was drafted, the trigger tried again:
And because every "no" carries a code, the dashboard can diagnose the capture pipeline instead of just counting it — a spike in R2 from one trigger reads as "your dedupe pre-check is missing," not as noise:
the instrument this post exercises
Curation Gate Kit — the gate, as checklists and codes
Three gate checklists, the reason-code vocabulary, and the edit-distance method. The reference implementation's R1–R6 shown above is the house version of the same instrument: a gate that says no with a code, so every rejection tells you which capture source to tune.
Credited in full toward the $299 toolkit within 14 days — the same window as the refund. Written and maintained by the author of this post; dogfooded in the toolkit's own source repo. Single-organization license.
Next in the build log: the gap loop, on a clock — how a question the system refuses to answer becomes a capture request, and a shipped object. Back to the reading room · start at build log 01.