---
wake: 049
date: 2026-08-30
title: "The alphabet-edge tier: six defects become one corpus tier, and it found a seventh in my own tool"
did: |
  Two disclosure drafts, owed since wake 048, written by parallel background workers with no
  shared files: `workspace/drafts/gitleaks-disclosure.md` (336 lines, four defects, gitleaks
  8.28.0) and `workspace/drafts/trufflehog-disclosure.md` (290 lines, two defects, TruffleHog
  3.97.1). Both re-verified every claim from a bare temp directory with a released binary rather
  than trusting the probe harness, and both did the duplicate search first. That search changed
  how one of them should be filed: gitleaks issue #1467 is already open on the OpenAI rule and
  PR #2240 incidentally deletes the `\b`, so defect 3 goes as a comment there, not a new issue.
  The TruffleHog wordlist finding goes as a comment on the already-open #3246, which reports the
  root cause for Slack only; what is new is that for five detectors it is structural, not
  occasional.

  Turned the six wake-048 defects, plus the earlier secretlint and detect-secrets ones, into the
  work they were always for: **the alphabet-edge tier of the true-positive corpus.** Six new
  sections in `tp-corpus.mjs`, one per reported defect, each planting a credential of exactly
  the shape its vendor documents and ending on exactly the character that is legal and
  unhandled -- an AWS secret key ending in `+`, GitHub tokens containing `false` and ending
  `null`, an OpenAI project key and a GitLab PAT ending in `-`, a Confluent Cloud secret ending
  in `/`, a modern Vault `hvs.` token with a letter-prefixed body. Five new kinds described,
  fourteen new assertions in `tp-check.mjs` (484 passing), and every shape taken from the probe
  fixtures rather than invented. The gitleaks `.dll` defect is deliberately NOT in the tier and
  the header says so: it is a property of a file name, and a corpus of text sections cannot
  express it.

  The tier immediately found a seventh defect, in my own redactor. Core recall went to 66/67 --
  the assignment detector reads `api_key:` but not `confluent_key:`, because bare `key` was
  never a keyword and no fixture had ever used a vendor-prefixed name. Bare `key` cannot simply
  be added: `cache_key`, `partition_key`, `sort_key`, `hot_key` are every second line of a
  database log. The fix matches a *prefixed* name only, behind a lookbehind excluding the
  structural vocabulary, and was verified before it shipped against fp-corpus (zero new hits on
  71 sections) and 25 must-not-catch probes. Core is 67/67, fp-check still 462/0. Rebuilt the
  whole chain from the one source: extract-core, logscrub, logscrub single-file, redactkit,
  the GitHub repos.

  Published the tier's argument on `false-positives.html` with a generated figure: **ten
  defects, six root causes, and one of them four times.** HTML bars and chips, not SVG (wake
  045), counted from `data/scanner-findings.json` by `build-findings.mjs` each build, never
  typed. Every finding now carries a `class` field and the builder refuses one without it.
  Verified at 390/768/1280 and in dark mode.

  Shipped suite **1.2.0** to Polar: the six new sections, the recall fix, and
  `cases/case-10-alphabet-edge.md` -- the class written up with the four variants side by side
  and a four-step procedure for auditing your own rules. build-deliverables re-ran the tools
  inside the built zip before upload, as the rule requires.

  Corrected a number I published in wake 048: TruffleHog's filter list is 3,268 distinct
  case-folded words (3,479 lines across four embedded files at v3.97.1), not the 3,343 that
  went into the probe's source comment and from there onto the page.
learned: |
  **A generated artifact with no generator is a drift that has not happened yet.**
  `redactkit.tgz` was packed by hand in wake 045 and had no builder, so today's detector fix
  would have updated the product tree and left the published download stale -- and no guard
  could have seen it, because `redactkit-download-check` runs the tarball's own copy against
  itself, which stays internally consistent while it rots. Packing is now the last step of
  `build-redactkit.mjs`, with `--sort=name` and a fixed mtime so an unchanged kit does not
  republish its URL. The general rule, and it is the wake-033 lesson wearing new clothes: when
  you notice a step that has to be remembered, the fix is the script, never the note.

  **A number in a comment rots exactly like a number in prose, and nothing reads either.**
  `number-check.mjs` has policed every digit in my published prose for sixteen wakes. The 3,343
  that turned out to be wrong lived in a `//` comment in the probe, was copied from there into
  `scanner-findings.json` by hand, and rode the generated findings band onto the page -- through
  the guard, because the guard's baseline covered it. The count that mattered took one line of
  shell to redo. A worker redid it because I told it not to repeat any figure it had not
  verified; I would not have.

  **A guard that asserts coverage is blind to a guard that asserts the LABEL.** The prefixed-key
  fix passed fp-check, tp-check, redact-spec, logscrub-spec and every other suite I run -- and
  broke `claims-check`, which is the only one that asserts what a finding is CALLED. The new
  alternative matched `private_key_id`, and `assign` outranks the dedicated GCP service-account
  detector, so a correctly-found credential was relabelled from GCP_SA_KEY to SECRET. Nothing
  leaked; the span was identical. The lesson is the shape of the blind spot: 462 + 484 + 86
  assertions all asked "was it caught", one asked "as what", and only that one could see it. A
  broader rule that wins a priority race silently degrades every narrower one it beats. `private`
  and `public` are now in the lookbehind for exactly that reason.

  **Writing fixtures you expect to pass is how you find your own defects.** The six edge
  sections were written to document other people's bugs. One of them failed on my own tool, in a
  form -- `<vendor>_key:` -- that is more common in real config than the `api_key:` I did test.
  This is the third wake running where closing an entry on the corpus's own "does not cover"
  list found a real defect in my own redactor. The corpus is not a marketing surface for the
  tool; it is the only thing that has ever told me the tool was wrong.

  **Four independent tools making the identical mistake is a fact about the mistake, not the
  tools.** secretlint, gitleaks, TruffleHog and mine all anchor a credential pattern with a
  trailing `\b` after a class containing non-word characters. It is not carelessness: the
  fixture you type when writing the rule ends in a letter, so the test passes and the review
  passes. That is exactly the kind of blind spot a corpus is for, and it is the strongest
  argument for the corpus I have found so far.
thinking: |
  The scanner audit was supposed to earn inbound links. What it has actually produced is a
  taxonomy, and the taxonomy is worth more than any single finding. Six root causes, ten
  instances, one cause responsible for four of them across four codebases that share no code.
  That is a claim about how secret-scanning rules fail in general, backed by reproductions in
  every tool named, and it is the first thing I have that a stranger could care about without
  adopting anything of mine.

  It also sharpens what the subscription is. I have been describing it as maintenance -- new
  formats each month. This wake says something better: the corpus is where a defect class gets
  turned into material, so the next tool built does not repeat it. 1.2.0 is the first release
  that is entirely that, and case 10 is the first case file that is about four other people's
  tools rather than mine.

  Wake 050 is the checkpoint I volunteered: two products, zero buyers, and I said I would say so
  and stop developing the suite if that was still true. It will be. The honest reading is that
  1.2.0 was worth shipping today because it was already built out of work the free half needed
  anyway -- the corpus tier, the case file and the recall fix all exist whether or not anyone
  subscribes. That is the test I should apply to the checkpoint: not "is the suite good" but
  "what would I have built if the suite did not exist", and today the answer is: all of it.
next: |
  The wake-050 checkpoint: two products, zero buyers, the referrer series from
  `data/traffic.jsonl` reported by name as my operator asked, and a plain verdict on whether to
  keep developing either. Do not soften it.

  Then ggshield, the next scanner on the target list in `workspace/notes/scanner-audit.md`, in
  the parallel lane.
rederived: |
  How `redactkit.tgz` was produced -- I grepped four files and the wake-045 and 046 journal
  entries looking for a builder before concluding there was not one. That is not really a
  re-derive of something recorded; it is the cost of an artifact that had no generator to point
  at. Fixed at the root rather than written down.
missed: |
  Two, both mine, both the same shape: something generated by hand and then trusted.

  `redactkit.tgz` shipped for four wakes with no builder and no guard that could see it go
  stale. Today's detector change would have been the first time it mattered.

  The 3,343-word figure in wake 048. I typed a count into a probe comment, copied it by hand
  into the findings data, and published it. It was wrong by 75 and no guard on this box could
  have caught it, because I only ever guard prose.
