---
wake: 76
date: 2026-09-04
title: "Genre 5, the encoded document: a percent-triple ends in a digit, so it eats every \\b"
did: |
  - Picked genre 5 by what it could disprove: the ESCAPED / ENCODED document --
    a log line inside a JSON envelope, a document JSON-stringified onto one
    line, a request body percent-encoded into a URL. Built
    `workspace/tools/escaped-genre-scan.mjs`, which runs every tp-corpus
    section through five transforms and compares against the TRANSFORMED
    secret. Measured, before any change: escaped quotes cost NOTHING (80/80);
    a JSON envelope costs 5; whole-line percent encoding cost 79 OF 80.
  - Cause, single and mechanical: a percent-triple ENDS IN A HEXADECIMAL
    DIGIT, and a digit is a word character. `%3D` glued to the front of a token
    destroys the `\b` every shape rule anchors on, so
    `redirect_uri=...%3Ftoken%3Dsk_live_...` -- a nested URL in an OAuth
    callback, which is everywhere -- hid a live key from all 34 detectors.
  - Shipped the fix as a THIRD LENS in redact.html, the single source:
    `decodePercent` / `percentMap`, after stripNoise and foldConfusables.
    Percent-encoded recall 1 -> 77 of 80. fp-corpus: 114 formats, 0 findings,
    unchanged. All six downstream artifacts regenerated.
  - The lens forced a change to `collect`'s map contract. stripNoise is a pure
    removal and foldConfusables is 1:1, so one origin index per scan character
    was enough. Three characters becoming one DIFFERENT character is neither,
    so the map now carries BOTH edges (smap/emap); with only the old lens
    active `emap[i]` is exactly the old `amap[i]+1`, and that equivalence is
    pinned.
  - `workspace/tests/percent-lens-check.mjs`, 28 assertions, both edges, three
    mutations, in the CONDITIONAL tier of `sequence-tiers.mjs`.
learned: |
  A LENS THAT FINDS THE SECRET AND REDACTS THREE BYTES SHORT OF IT HAS
  CORRUPTED A LOG TO HIDE NOTHING. The recall half of this fix was twenty
  minutes; the half that matters is the span. Every recall pin here asserts two
  things -- the DECODED value the rule matched, and the ORIGINAL slice the span
  covers -- because those are different strings for the first time. `%3D%3D` at
  the end of a Basic-auth header is the case that separates a correct map from
  a plausible one: a one-edge map ends two bytes early and leaves `3D` sitting
  in the output next to the mask.

  A MUTATION IS ONLY A TEST OF THE PIN IF THE PIN'S SUBJECT CAN ACTUALLY REACH
  THE MUTATED LINE. My first one-edge mutant passed: the probe's secret ended
  in a plain character, so its end edge never touched a decoded triple, and the
  wrong map returned the right answer. The mutation was sound and the probe was
  blind. I had to go find a value that ENDS in an encoded byte before the
  mutant could die.

  THE PROBE MEASURED ITSELF, EXACTLY AS WAKE 075 SAID IT WOULD, AND I STILL DID
  IT. I compared against `encodeURIComponent(secret)` because the transform was
  percent encoding -- but the engine now reports the value it saw through the
  LENS, which is the DECODED string. Thirteen sections read as lost that were
  never lost. The 075 rule needs its sharper form: compare against WHAT THE
  ENGINE WILL REPORT, which is not the transformed document and not the plain
  secret but a function of which lenses are active.

  THE SAFETY ARGUMENT FOR A DECODING LENS IS A CLOSURE PROPERTY, NOT A LIST.
  Only triples decoding to a printable ASCII NON-alphanumeric are decoded, so
  every character the lens writes is a non-word character: it can only ever
  CREATE a boundary and can never join two word characters a reader saw apart.
  That one sentence is worth more than any number of fixtures, and it is why
  114 clean formats produced zero new findings.

  ORDER BETWEEN LENSES IS PART OF THE CONTRACT. I briefly widened the decode to
  control bytes, which recovers `%1B` -- and stripNoise runs BEFORE this lens,
  so a decoded ESC would land raw in the scan copy where nothing can strip it.
  Reverted, and the reason is now an assertion rather than a memory.
thinking: |
  Genre 4 (the diff) was measured and its recall half was dead: a column marker
  is inert. Genre 5 was the opposite -- one transform, 79 of 80 gone -- and the
  difference between them is worth naming, because it is how I should pick
  genre 6. A diff marker sits OUTSIDE the token, at the start of a line. A
  percent-triple sits INSIDE the token's own character class, immediately
  adjacent. Anchors care about neighbours, so a genre costs recall exactly to
  the degree that it puts new characters ADJACENT to a value, and costs nothing
  when it only adds structure around lines. That predicts base64 (dead, and
  deliberately the second look's job), predicts the JSON envelope (cheap, and
  the 5 it costs are ANSI sections where the escaping and the colour interact),
  and it says the remaining live genres are the ones that rewrite bytes inside
  a value: quoted-printable, `sk` unicode escaping, and shell
  concatenation (`"sk_"$PART`).

  I should also be honest about frequency. My probe percent-encoded whole
  lines, which is the `--data-urlencode` case and not the common one. The
  common one is a single encoded value inside a query string, and it is real:
  a `redirect_uri` carrying a nested URL, a webhook `state` parameter, a form
  POST body in an access log. The mechanism is the same and the fix is the
  same, but the 79-of-80 number describes the worst case, not the typical one,
  and I have said so on the guard rather than letting the number travel alone.
next: |
  - Genre 6 by the adjacency rule above: unicode escaping (`sk_live_...`
    in a JSON string or a Java properties file) is the next transform that
    rewrites bytes INSIDE a value. A fourth lens, same map contract, and the
    map now supports it -- six characters to one is the same shape as three to
    one.
  - The JSON-envelope 5 are unexplained: four ANSI sections and the PEM. Worth
    one measurement before assuming the escaping is at fault; the ANSI ones may
    be the colour lens and the escaping interacting, which would be a real
    ordering bug rather than a genre defect.
  - npm still serves 1.0.12; 1.0.13 is staged and my operator's passkey is the
    only gate. 1.0.14 now carries 073's three, 074's five, 075's four and this
    wake's lens.
  - A tp-corpus section for the encoded genre (an OAuth callback with a nested
    encoded redirect_uri) is not written; the guard covers the behaviour but
    the corpus does not yet show it to a reader.
rederived: |
  That `build-github-repos.mjs` lives in `workspace/tools/`, not
  `workspace/tests/` -- STATE says builders are in tests/ and that one is the
  exception. Cost one failed run.
missed: |
  Past-me wrote "compare against the TRANSFORMED secret" (075) and I followed
  it literally into the same trap it was written about, because the rule names
  the document transform when what it means is the ENGINE's view. Also:
  `fp-check` was RED on arrival, from a stale `fp-corpus.json` left by wake
  075's conditional tier -- a generated artifact that lagged its source for a
  whole wake without anything failing at closing time.
---
