docs: the tool-kind validation guards and the shape that was invisible
Written from the diff, per ai-meta's change-documentation rule.
validate_tool_kinds read tool: as a mapping only. serde_yaml's Value::get("kind") on a
Sequence returns None, and the recursion cannot recover it because each sequence item
is a {name, kind, …} mapping with no tool key of its own. So for the sequence form the
walker collected nothing: the guard ran, returned successfully, and validated an empty
set.
Measured against the e2e corpus (166 files carrying a tool: key): 59 files had kinds it
could not see, roughly 219 of 1,178. Shape distribution — 20 sequence-only, 109
mapping-only, 37 using BOTH in one file, so the mixed case is ordinary. Hand-verified
on test_vars_block.yaml: five list-form blocks each carrying kind: python, extracted
zero.
Also records that the probe's raw after-figure is 3 rather than 0, and that all three
are ground-truth over-counting on text inside string literals — so nobody later treats
3 as a residual gap to chase.
docs: the catalog listing guards, the partial-result failure mode, and Hazard 4
Written from the diff rather than from the central log, per ai-meta's
change-documentation rule.
Adds a `db/queries/catalog.rs` guards subsection and documents the failure mode it
prevents, which is the part worth keeping: /api/catalog/list filtered `kind`
case-sensitively while the column holds BOTH spellings, so a filter returned a
PARTIAL set rather than an empty one. Measured on prod across 1,527 rows:
'Playbook' 875, 'playbook' 650, 'mcp' 2 — so "playbook" returned 650 of 1,525
playbooks and "Playbook" returned 875.
Neither returned zero, which is why it survived four months. An empty result gets
reported as a bug; a 43%-complete one gets believed. Nothing in tests/ mentioned
resource_type (0 of 27 files), and the pre-existing shape guard asserted
page.contains("kind = $1") — it had pinned the buggy predicate as correct while
passing Some("Playbook") to it.
Both halves are recorded because each alone is still wrong: predicate-only makes a
caller sending Playbook match nothing, parameter-only still misses the 875 legacy
rows written before f0301e86 lowercased registration and did not backfill. Also
notes that an index on kind, if ever added, must be ON noetl.catalog (LOWER(kind)).
Hazard 4 is new and distinct from Hazard 1: a guard that reads its whole file with
include_str! counts the needle inside its OWN assertion messages. Mine reported 4
uses where the code has 2 and failed with the fix correctly in place — a false
FAILURE, which invites relaxing a correct assertion until it agrees with the
miscount. The fix slices with rfind rather than Hazard 1's split(..).next(), because
this file has an earlier #[cfg(test)] with real code after it, and asserts the slice
is plausibly large before using it.
wiki(static-guards): three pinned-label guards, and a stale claim about CI
noetl/ai-meta#415, noetl/server#493.
## Corrects a stale claim this page was built on
It said no cargo test runs in CI on any NoETL Rust repo, citing noetl/ai-meta#232.
That stopped being true: #232 closed on 2026-10-02 and test.yml runs
`cargo test --all-targets` on every pull request here, so these guards gate merges.
Rewritten with the better reason they are tests rather than CI scripts: a guard beside
the thing it guards moves with it, while a script reading src/ from outside drifts
silently the moment a file is renamed.
## The three new guards
Each uses a total match, so a new variant fails the BUILD rather than quietly adding
an unpinned label, and each asserts the REVERSE direction too -- nothing pinned that
no caller can produce.
That reverse assertion found a live bug on its first run: engine_unavailable was
pinned in EMBEDDED_READ_OUTCOMES and reachable by NO caller, because the verify
handler returned early without touching the metric. The series was registered, read 0
for ever, and told anyone watching that the embedded engine was always available.
## Hazard 3 added: a mutation battery where no mutant was applied
The battery for these guards reported 5 of 5 FIRES on its first run and was
worthless -- the helper never forwarded its arguments, so nothing was mutated and the
non-zero exits came from E0308 after a sed changed an array's length but not its
contents. Records the three things a battery here must do: assert the mutation
applied, count compile errors separately and require 0, and establish a green
baseline one filter at a time (cargo test --lib a b c is a usage error, not three
tests, and a red baseline makes every mutant look caught).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(server): record what every static guard prevents, and the two scanning hazards
New page static-guards.md, cross-linked from Home and the sidebar.
Written alongside noetl/server#490, which reviewed the four
split("#[cfg(test)]") uses in handlers/ehdb_eventlog_mirror.rs. Two are
sound — their assertions are contains(..), so truncation can only make
them stricter — and were deliberately left unchanged. Two compare counts
within the truncated slice and could false-pass on a site hidden below
the cut; in handlers/events.rs that invisible region is 67% of the file,
and two past direct-write defects were in exactly that file.
The page exists because a guard whose purpose is unrecorded is a guard
someone deletes as noise. It records, per guard, the defect that already
happened at least once, plus the two hazards that produce confident wrong
zeros: the truncation idiom and a test filter that matches nothing and
exits 0.
Refs noetl/ai-meta#361
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>