One discipline · three doors contextkeeping.com machinereadyknowledge.com answerecon.com

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."
The approvals queue with a candidate's reject reason-code menu open, listing R1 through R6, and a Why? (required) prompt
The reject flow in the gate console. "Why?" is required — the menu will not let a rejection pass without a code. Capture-source links are masked.

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:

A candidate card titled Focus on High-Impact Tasks with reason R2 selected and a confirmation button reading Sure? (R2, never re-raised)
Rejection is consequential, so the console makes you mean it. "Sure? (R2, never re-raised)" — a duplicate rejected with R2 will not be proposed 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 metrics view: 195 candidates created, 124 promoted, 58 rejected; rejections broken down by reason code with a diagnosis line under each; the curation card showing 152 validated, 8 draft, 15 archived, all within the 2-day gate SLA
The reason as telemetry, live. 195 candidates → 124 promoted · 58 rejected, each rejection coded, each code carrying its diagnosis ("R1 → capture trigger fires too eagerly — raise its threshold"). Note the honest line: "uncoded ×3 — logged before reason codes existed." The right-hand card is where the next build log picks up: refusals feed the Capture loop.

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.