Skip to content

[DO NOT MERGE] CI timing for the redactor floor (see Consiliency/pmcp#234) - #307

Closed
ViperJuice wants to merge 38 commits into
mainfrom
wip/234-redactor-floor-code
Closed

ViperJuice wants to merge 38 commits into
mainfrom
wip/234-redactor-floor-code

Conversation

@ViperJuice

@ViperJuice ViperJuice commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Draft opened only to measure CI time for the reference implementation of plan revision 14 (#288) on GitHub runners. It will be closed without merging; the implementation lands through its own PR after the plan is approved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

ViperJuice and others added 30 commits September 23, 2026 09:42
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Rev 2: the board found three blocking defects in rev 1, all reproduced and
fixed with tests and mutation rows M18-M20 -- the payload bound was applied
after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation
back-up was bounded on the whole trailing run (a credential after a long path
kept its prefix; offset 0 kept a partial token), and quoted values stopped at
an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Rev 2: the board found three blocking defects in rev 1, all reproduced and
fixed with tests and mutation rows M18-M20 -- the payload bound was applied
after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation
back-up was bounded on the whole trailing run (a credential after a long path
kept its prefix; offset 0 kept a partial token), and quoted values stopped at
an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`).

Rev 3: two more board defects, both reproduced and fixed with tests and
mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a
value class that ate the closing quote (`{"password": "hunter2 Bearer x"}`
left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and
a truncation cut inside a quoted multi-word password leaked its first word
(128 of 1452 swept cuts). Keyed values are now redacted first, every looser
value class stops at quotes and brackets, and an unterminated quoted value
runs to the end of its line; the sweep is 0 of 1452 with coverage asserted.
mypy src/ is clean and in the verification.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Rev 2: the board found three blocking defects in rev 1, all reproduced and
fixed with tests and mutation rows M18-M20 -- the payload bound was applied
after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation
back-up was bounded on the whole trailing run (a credential after a long path
kept its prefix; offset 0 kept a partial token), and quoted values stopped at
an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`).

Rev 3: two more board defects, both reproduced and fixed with tests and
mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a
value class that ate the closing quote (`{"password": "hunter2 Bearer x"}`
left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and
a truncation cut inside a quoted multi-word password leaked its first word
(128 of 1452 swept cuts). Keyed values are now redacted first, every looser
value class stops at quotes and brackets, and an unterminated quoted value
runs to the end of its line; the sweep is 0 of 1452 with coverage asserted.
mypy src/ is clean and in the verification.

Rev 4: the architecture changes so both defect classes close by construction.
Every pass now reads the original text and returns spans; overlaps are merged
and all replacements land in one step, so no pass can consume a boundary a
later pass needs (the round-3 URL-eats-escape leak). process_output redacts
BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut
never lands inside a credential the redactor has not seen whole (the round-3
backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value
rule are gone. Property tests replace hand-picked cases: 3000 random printable
passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with
coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in
place) and M26 (truncate then redact) are red under the property tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Rev 2: the board found three blocking defects in rev 1, all reproduced and
fixed with tests and mutation rows M18-M20 -- the payload bound was applied
after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation
back-up was bounded on the whole trailing run (a credential after a long path
kept its prefix; offset 0 kept a partial token), and quoted values stopped at
an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`).

Rev 3: two more board defects, both reproduced and fixed with tests and
mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a
value class that ate the closing quote (`{"password": "hunter2 Bearer x"}`
left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and
a truncation cut inside a quoted multi-word password leaked its first word
(128 of 1452 swept cuts). Keyed values are now redacted first, every looser
value class stops at quotes and brackets, and an unterminated quoted value
runs to the end of its line; the sweep is 0 of 1452 with coverage asserted.
mypy src/ is clean and in the verification.

Rev 4: the architecture changes so both defect classes close by construction.
Every pass now reads the original text and returns spans; overlaps are merged
and all replacements land in one step, so no pass can consume a boundary a
later pass needs (the round-3 URL-eats-escape leak). process_output redacts
BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut
never lands inside a credential the redactor has not seen whole (the round-3
backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value
rule are gone. Property tests replace hand-picked cases: 3000 random printable
passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with
coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in
place) and M26 (truncate then redact) are red under the property tests.

Rev 5: key coverage as a property. The lead's own property test found keys
the redactor names but did not redact in every form (passwd/pwd via the policy
default only, quoted Authorization stopping at a space, private_key/
credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the
public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a
quoted Authorization value is redacted whole, a bare value may start with
punctuation, and an existing [REDACTED] is inert to every span. The new test
derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks);
M27 drops passwd from the strong set and the property goes red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Rev 2: the board found three blocking defects in rev 1, all reproduced and
fixed with tests and mutation rows M18-M20 -- the payload bound was applied
after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation
back-up was bounded on the whole trailing run (a credential after a long path
kept its prefix; offset 0 kept a partial token), and quoted values stopped at
an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`).

Rev 3: two more board defects, both reproduced and fixed with tests and
mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a
value class that ate the closing quote (`{"password": "hunter2 Bearer x"}`
left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and
a truncation cut inside a quoted multi-word password leaked its first word
(128 of 1452 swept cuts). Keyed values are now redacted first, every looser
value class stops at quotes and brackets, and an unterminated quoted value
runs to the end of its line; the sweep is 0 of 1452 with coverage asserted.
mypy src/ is clean and in the verification.

Rev 4: the architecture changes so both defect classes close by construction.
Every pass now reads the original text and returns spans; overlaps are merged
and all replacements land in one step, so no pass can consume a boundary a
later pass needs (the round-3 URL-eats-escape leak). process_output redacts
BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut
never lands inside a credential the redactor has not seen whole (the round-3
backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value
rule are gone. Property tests replace hand-picked cases: 3000 random printable
passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with
coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in
place) and M26 (truncate then redact) are red under the property tests.

Rev 5: key coverage as a property. The lead's own property test found keys
the redactor names but did not redact in every form (passwd/pwd via the policy
default only, quoted Authorization stopping at a space, private_key/
credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the
public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a
quoted Authorization value is redacted whole, a bare value may start with
punctuation, and an existing [REDACTED] is inert to every span. The new test
derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks);
M27 drops passwd from the strong set and the property goes red.

Rev 6: three rules inside the span design leaked, two of them regressions
against main, and are replaced. The URL pass yields component spans (userinfo,
secret query values, fragment) instead of a rewritten URL that swallowed every
keyed and shape span inside its query; a literal [REDACTED] in the input is a
span that merges with what touches it instead of a shield that disarmed the
rule next to it; and process_output keeps only the cap plus the tail of any
span that starts before it, so the window's far edge is never emitted.
`redact_auth_url` is byte-identical to main again. New properties: every span
the redactor collected is absent from the output (over the corpora and
generators for query strings and literal markers), the window edge never emits
an unredacted value, and a 42-input corpus of what main redacts still vanishes.
Authorization gains the plain-word gate and same-line separator; `==` is a
comparison. Mutation rows M28-M30, M32, M33.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Rev 2: the board found three blocking defects in rev 1, all reproduced and
fixed with tests and mutation rows M18-M20 -- the payload bound was applied
after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation
back-up was bounded on the whole trailing run (a credential after a long path
kept its prefix; offset 0 kept a partial token), and quoted values stopped at
an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`).

Rev 3: two more board defects, both reproduced and fixed with tests and
mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a
value class that ate the closing quote (`{"password": "hunter2 Bearer x"}`
left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and
a truncation cut inside a quoted multi-word password leaked its first word
(128 of 1452 swept cuts). Keyed values are now redacted first, every looser
value class stops at quotes and brackets, and an unterminated quoted value
runs to the end of its line; the sweep is 0 of 1452 with coverage asserted.
mypy src/ is clean and in the verification.

Rev 4: the architecture changes so both defect classes close by construction.
Every pass now reads the original text and returns spans; overlaps are merged
and all replacements land in one step, so no pass can consume a boundary a
later pass needs (the round-3 URL-eats-escape leak). process_output redacts
BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut
never lands inside a credential the redactor has not seen whole (the round-3
backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value
rule are gone. Property tests replace hand-picked cases: 3000 random printable
passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with
coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in
place) and M26 (truncate then redact) are red under the property tests.

Rev 5: key coverage as a property. The lead's own property test found keys
the redactor names but did not redact in every form (passwd/pwd via the policy
default only, quoted Authorization stopping at a space, private_key/
credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the
public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a
quoted Authorization value is redacted whole, a bare value may start with
punctuation, and an existing [REDACTED] is inert to every span. The new test
derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks);
M27 drops passwd from the strong set and the property goes red.

Rev 6: three rules inside the span design leaked, two of them regressions
against main, and are replaced. The URL pass yields component spans (userinfo,
secret query values, fragment) instead of a rewritten URL that swallowed every
keyed and shape span inside its query; a literal [REDACTED] in the input is a
span that merges with what touches it instead of a shield that disarmed the
rule next to it; and process_output keeps only the cap plus the tail of any
span that starts before it, so the window's far edge is never emitted.
`redact_auth_url` is byte-identical to main again. New properties: every span
the redactor collected is absent from the output (over the corpora and
generators for query strings and literal markers), the window edge never emits
an unredacted value, and a 42-input corpus of what main redacts still vanishes.
Authorization gains the plain-word gate and same-line separator; `==` is a
comparison. Mutation rows M28-M30, M32, M33. The CLI seats' supplementary
cases (userinfo plus a non-secret query key, an operator pattern in a URL
path, a window shrink from ordinary keyed values) are named tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Plan only: src/ and tests/test_auth.py are byte-identical to main @ 860636a.

The redactor is keyword-anchored, so it redacts the word after `token`,
`secret`, `session`, `password` and `bearer` in prose while letting keyword-less
credentials (AKIA…, ghp_…, xoxb-…, a token in a URL path) through. The plan
specifies separator-anchored keyword rules with the key as the identifier's last
segment, a value gate for whitespace-separated keywords and for `Bearer`, an
OAuth-only reading of `code`, a shape layer (vendor supplement, prefixed
tokens, class-transition scoring of opaque runs with a 256-char payload bound),
a padding-safe split in `redact_secrets`, and a truncation cut that never ends
inside a token. Both directions are acceptance criteria: a prose corpus that
must survive byte-identically and a 29-class credential corpus that must not.

The inherited spike was measured, not trusted: ten defects were found and fixed
(elicitation-URL path redaction, JSON-RPC `code`, `bearer token`, the `\bbearer`
boundary, `0x` addresses, image-sized base64, pod names, newline-spanning
separators, `param=value` after a keyword, base64-padding split). Its
tests/test_auth.py edit is dropped: both original fixtures pass unchanged.

Rev 2: the board found three blocking defects in rev 1, all reproduced and
fixed with tests and mutation rows M18-M20 -- the payload bound was applied
after path splitting (JPEG base64 `/9j/...` was destroyed), the truncation
back-up was bounded on the whole trailing run (a credential after a long path
kept its prefix; offset 0 kept a partial token), and quoted values stopped at
an escaped quote (`{"password": "a\"hunter2"}` left `hunter2"`).

Rev 3: two more board defects, both reproduced and fixed with tests and
mutation rows M21-M24 -- the Bearer pass ran before the keyword pass with a
value class that ate the closing quote (`{"password": "hunter2 Bearer x"}`
left `hunter2` visible; the Authorization rule missed a JSON-quoted key), and
a truncation cut inside a quoted multi-word password leaked its first word
(128 of 1452 swept cuts). Keyed values are now redacted first, every looser
value class stops at quotes and brackets, and an unterminated quoted value
runs to the end of its line; the sweep is 0 of 1452 with coverage asserted.
mypy src/ is clean and in the verification.

Rev 4: the architecture changes so both defect classes close by construction.
Every pass now reads the original text and returns spans; overlaps are merged
and all replacements land in one step, so no pass can consume a boundary a
later pass needs (the round-3 URL-eats-escape leak). process_output redacts
BEFORE it truncates, over a bounded window (max_bytes + 16 KiB), so the cut
never lands inside a credential the redactor has not seen whole (the round-3
backslash-cut leak); the trailing-run back-up and the end-of-line quoted-value
rule are gone. Property tests replace hand-picked cases: 3000 random printable
passwords x 5 keyed forms on both surfaces, a 23 713-cut truncation sweep with
coverage asserted, and 2000 generated prose lines. Mutation rows M25 (mutate in
place) and M26 (truncate then redact) are red under the property tests.

Rev 5: key coverage as a property. The lead's own property test found keys
the redactor names but did not redact in every form (passwd/pwd via the policy
default only, quoted Authorization stopping at a space, private_key/
credentials/auth uncovered). AUTH_DIAGNOSTIC_SECRET_KEYS is widened, the
public WEAK_SECRET_KEYS keeps plain words after auth/code/credential(s), a
quoted Authorization value is redacted whole, a bare value may start with
punctuation, and an existing [REDACTED] is inert to every span. The new test
derives its keys from the source (31 keys x 4 forms, 6158 strings, 0 leaks);
M27 drops passwd from the strong set and the property goes red.

Rev 6: three rules inside the span design leaked, two of them regressions
against main, and are replaced. The URL pass yields component spans (userinfo,
secret query values, fragment) instead of a rewritten URL that swallowed every
keyed and shape span inside its query; a literal [REDACTED] in the input is a
span that merges with what touches it instead of a shield that disarmed the
rule next to it; and process_output keeps only the cap plus the tail of any
span that starts before it, so the window's far edge is never emitted.
`redact_auth_url` is byte-identical to main again. New properties: every span
the redactor collected is absent from the output (over the corpora and
generators for query strings and literal markers), the window edge never emits
an unredacted value, and a 42-input corpus of what main redacts still vanishes.
Authorization gains the plain-word gate and same-line separator; `==` is a
comparison. Mutation rows M28-M30, M32, M33. The CLI seats' supplementary
cases (userinfo plus a non-secret query key, an operator pattern in a URL
path, a window shrink from ordinary keyed values) are named tests.

Rev 7: the differential against main is the never-worse test. The board
seat's 4000-row corpus is regenerated in-test and main @ 860636a's recorded
removals are a committed oracle fixture (beside this plan; the implementer
copies it to tests/fixtures/); a row where rev 7 keeps a piece main removed
fails unless it is in an accepted class decided from the row's own key,
separator and value (0 unaccepted; 226 row-surfaces in 5 classes; 1852 rows
better than main). Four regressions against main are fixed (a quote before
Bearer; httpie := and ==; percent-encoded query values; Ruby =>), the
serialisation layer is in scope -- structured results are redacted as
structure before json.dumps -- and M28 is a two-rule mutant red for its
stated reason. Mutation rows M34-M38.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G8iK4noj1iNbSTqJVFEX6
Work-in-progress snapshot for the move from claw to dev0. UNVERIFIED: not measured, reviewed or panel-approved. See HANDOFF-dev0.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gtj1apYGGFYzjYgL26KHKC
Reference code for plan revision 8 (PR #288). Not for merge:
the plan PR embeds this code; an executor applies it later.

- Keyed values in quotes are redacted INSIDE the quotes, so a redacted JSON
  document stays JSON and a dict result round-trips as a dict. The rev-8
  spike returned 120/400 dict corpus rows as str (main: 30); now 14, a
  strict subset of main's. JSON-valid corpus rows broken: 0/258 on both
  surfaces (main 7).
- New keyword-list pass: `"password": ["hunter2"]` (pretty-printed as a
  string) is redacted per element inside its quotes; it leaked on main.
- Oracle fixture regenerated from main 9ca081e (auth/policy unchanged since
  860636a); string/dict removals identical to the prior fixture; adds
  main's per-row result type (`dict_types`) so the dict differential also
  asserts the type is never worse.
- Tests for items 3 (depth bound fails closed) and 4 (operator pattern on a
  percent-encoded query value), each proven by a mutant.

Full suite -m 'not live': 4213 passed, 3 skipped, 25 deselected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8FnHrdkBnAGRWfDkbtUti
…uments (see #234)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8FnHrdkBnAGRWfDkbtUti
The keyed-list comment still called a list a stated residual (the new
_keyword_list_spans redacts it); the accepted-class docstring said rev 7;
`not inner` in _keyword_list_spans could never change output (an empty
element makes a zero-length span, which apply_redaction_spans drops); the
plan-folder copy of the oracle now matches the fixture byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…d found narrowed (see #234)

Board round on plan rev 8 (#288), claude seat, all
reproduced on main vs rev 8 before fixing:
- B1 a JSON literal after a quoted key stays JSON: null/true/false are
  kept; a number becomes the string "[REDACTED]" (dict stays a dict).
- B2 the Authorization bare value never starts on `[`/`{`; JSON literals
  after a quoted Authorization key get B1's rule.
- B3 `.` no longer blocks a key (`self.password =`); `:` still does (arn).
- B4 the camelCase qualifier may start with a capital (`AccessToken=`).
- B5 whitespace is `[^\S\r\n]` everywhere rev 8 enumerated `[ \t\xa0]`,
  on both surfaces (U+3000, U+2003, \v, \f ...).
- N1 suffixed keys (`password2`, `passwordHash`, `secret_value`) are
  secrets when the value is credential-shaped and not a URL/ARN; declared
  compound keys (`secret_access_key`) are exempt; the whitespace-separator
  rule gets the `_key`/`_id` and camelCase key shapes.
- N2 the keyed-list pass skips lists of objects.
- N3 after an UNQUOTED key, a quote whose content starts with `,:]}`
  closes the enclosing string and is not a value.
- N4 the key qualifier is bounded to 8 segments (`"a_"*33000`: 0.12 s).
- Policy defaults: value never ends on a backslash and never starts on a
  quote (the engine redacts quoted values inside the quotes on that
  surface), which also removes rev 8's opening-quote residual.

Seat's JSON fuzz (6000 docs): rev 9 breaks 6/6 (engine/policy), main
1005/1276; the only docs broken here and not on main carry escaped
quotes (JSON inside a string leaf, #290).
Generator widening, oracle regeneration and new tests: next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
grok, codex and gemini on plan rev 8 (#288); each
reproduced on main vs this tree before fixing:
- `==` then whitespace is a separator on the engine (main's `[\s:=]+`):
  `password == hunter2` is redacted; a non-credential-shaped value is a
  comparison (`if token == expected:` survives).
- A percent-encoded query KEY is decoded and the `key=value` pair asked
  of the engine (`?api%2Dkey=hunter2`).
- `Bearer` continues onto an unindented line only with a
  credential-shaped value that is not a flag/bullet (`Bearer\r\nhunter2`).
- The whitespace-rule and Authorization bare values never end on a
  backslash (a serialised leaf's `\"` stays escaped; dict stays dict).
- The keyed-list body is quote-aware (`["first]", "hunter2"]` -- leaked
  on main too) and honours the `code` qualifier exemption
  (`error_codes: [...]`).
- Dropped the `&` accepted-class arm: it read the output and could not
  fire (gemini).

Seat JSON fuzz: see commit message of the next plan revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…234)

After an unquoted key inside a string leaf (`{"a": "token: [x", "b": "]"}`),
the quote-aware list body scanned past the string's closing quote to a
`]` in a later value and redacted across the boundary: invalid JSON on
both surfaces where main left it valid (rev 8 did the same). The list
body is now a literal array -- quoted strings or JSON scalars separated by
commas, nothing bare -- so that text is not an array, while
`["first]", "hunter2"]` still is.

Seat JSON fuzz (6000 docs, seed 7): 4/4 broken (engine/policy), main
1005/1276, and 0 documents broken here that main keeps valid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…the whitespace rule (see #234)

Found by the widened differential (both pre-existing in rev 8; main keeps
them valid / redacted):
- A: process_output serialises with json.dumps, which spells non-ASCII
  whitespace ` `; the shape pass started a span at the `u` and left a
  lone backslash (invalid JSON, dict -> str: 54 key x whitespace cases).
  widen_over_escapes() gives a span that follows an escaping backslash the
  backslash too, on both surfaces. Sweep: 0 of 117 break.
- B: the whitespace-separator rule now takes the suffixed-key shape
  (`password2 abc123def456`, `passwordHash hunter2`, `secret_value\t...`);
  it already gates on a credential-shaped value, and now excludes URL/ARN
  values like the separator form. `secret-bearer hunter2` is redacted as
  on main; `secret-bearer failed` still survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
Defect C from the widened differential: `CLIENTSECRET=`, `dbpassword=`,
`Accesstoken\t…`, `mypassword=` leaked (main redacts all; rev 8 too) --
the camelCase qualifier needs a case boundary a case-folded key does not
have. Keys may now carry a prefix glued on with no boundary (`glued`,
<= 24 chars, never starting right after a backslash, so a JSON `\n`
escape's `n` is not a prefix). A glued key is a secret only when the
value is credential-shaped and not a URL/ARN; on the whitespace rule it
must also be a single-case word (`Ed25519PrivateKey X509Cert` is prose).
The `code` qualifier check sees the glued prefix (`errorcode=` stays).

The straddle guard also skips a quoted value after an unquoted key that
contains the OTHER quote character (`…password='x"], "k": "y'` runs across
a JSON string boundary); main never redacts a value that starts on a quote.

casing sweep: 0 leaks (main-equal); escape sweep 0/117; seat fuzz 4/4,
0 documents broken here that main keeps valid; "a_"*33000 < 0.3 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…see #234)

Regression tests, one per finding class of the board's second round (B1-B5,
N1-N4, G1, C2, C4, C6, C7, the policy defaults) and of round 3 (A: no span
starts inside a JSON escape; B: suffixed keys on the whitespace rule; C:
case-folded compound keys; the literal-array list body; the other-quote
straddle; the flag/bullet guard). Each is red on rev 8 (56d7f80) and green
here; the round-3 fixes are each pinned by a mutant that the tests kill.

The differential is widened along every axis the findings found: dotted,
PascalCase/camelCase, suffixed, glued and --flag keys; every str.isspace()
separator but CR/LF, alone and after `:`, plus spaced `==` and a quoted key
before a bare value; JSON literals and values starting on `,`/`}`; JSON
documents with mixed types, nested objects and lists of objects (8 000 rows,
800 dict rows; an axis test asserts each axis is present). The oracle is
regenerated from main @ 9ca081e (auth.py/policy.py unchanged since 860636a)
and now also records process_output's type over the committed JSON fuzz.
Every row worse than main falls in a class decided from its own key,
separator, value and wrap: 0 unaccepted.

The JSON fuzz (seed 7, 1 500 objects x 4 serialisations) is a property test:
wherever the input has no backslash-escaped quote (#290's scope, decided
from the input), the output parses on both surfaces, and process_output
returns a dict wherever main's did. N4 gets a 2 s timing guard.

One production change, authorised by the coordinator: the flag/bullet guard
skips only a `--` flag or a lone `-*#>` marker followed by whitespace, on the
whitespace rule, the keyed and Authorization continuations and the Bearer
next-line check. A marker glued to a value (`token -abc123def`,
`password:\n  -hunter22`) is part of it and is redacted, as on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…s (see #234)

A literal U+2028 in a parametrize list made the review tooling refuse the
plan bundle for every external seat ("brokered review input contains a
transport-active control character"). The whitespace list now uses
escapes, and a test fails on any literal Cc/Cf/Zl/Zp character (tab
aside) or U+0085 in auth.py, policy.py or this file; appending a literal
U+2028 to auth.py fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…sion test (see #234)

Every family below was worse than main on the rev-10 grammar audit. Each one
has a regression test that fails on rev 9 (60ca193) and passes here, and a
mutant of its production change that those tests kill. The families share the
keyword regexes, so they land together; each has its own section here.

F5/F6 (the maintainer's decision 1). process_output({"token": NaN}) came back
  as a str; {"max_tokens": 1024} became "[REDACTED]". Now every scalar
  json.dumps emits (null true false, numbers, NaN, +-Infinity) is left alone
  after a quoted key, as on main (_JSON_SCALAR_RE). The two test_b1 number
  rows now assert that. Numeric secrets move to #290.
F3. process_output({"t": "\u00a0password=hunter22x"}) kept the value,
  because the serialiser's \uXXXX (and \b, \f, control, non-ASCII) tail read
  as a glued prefix. Now every JSON escape is a key boundary: the engine
  keyword, whitespace and Bearer rules plus the secret/token policy defaults.
  Follow-up: an escaped space between Bearer/Authorization: and the value
  (Cookie: session=bearer \u2006(tok)) is part of the separator
  (_JSON_SPACE_ESCAPE).
F4. password:\n\nhunter22x, password:\rX, token\r\r\nX and password\n: X
  leaked. Now a line break is any \r/\n run, before or after the operator;
  the unindented-break gate is kept.
F2. X-Auth: Bearer hunter22x and token: Bearer abc123def456 leaked. The
  Bearer (?<![=:]) lookbehinds are gone: the lookahead and plain-word gate
  already keep token_type=Bearer expires_in=3600.
F9. The PEM rule was quadratic: 5.6 s on 264 KB of BEGIN lines. It now has a
  tempered body, and a timing guard holds it under 2 s.
F1. Database:Password=hunter22x leaked. A single : now starts a key; :: does
  not, and nothing inside an arn:/urn: name does (_RESOURCE_NAME_RE).
F8. C:\secret=, DOMAIN\password=, \password hunter22x and \0WM9qpSecret=X
  leaked. Now a backslash is a boundary. Only a JSON escape's tail (the u00a0
  of \u00a0, the n of \n) may never start a qualifier or glued prefix.
N1. password===X, password:: X and password=:X leaked. The operator is now a
  run of up to four :/=. A run holding == or :: is gated on a
  credential-shaped value.
N2. password_=X, password__=X and id_token__Xv=X leaked. A suffix may now
  hold joiner runs and a trailing joiner.
N5. password="hunter22x (unterminated) leaked. Now the run after the quote
  is the value; when it ends at a double quote it must be credential-shaped.
N6/N7. Bearer "x", Bearer (x), Authorization: [x] and Authorization: Bearer
  "x" leaked. Now the inside of a wrapped value is redacted and the wrapping
  kept, except after a quoted key.
N8. otp_code=abc123def456, mfa_code=... and verification_code=... leaked.
  Now code under a qualifier that is not a status name
  (_STATUS_CODE_QUALIFIERS) is a weak key, gated on a credential-shaped
  value.
N9. java -jar x.jar -password hunter22x leaked, as did _token X and ---secret
  X. The whitespace rule now takes any -/_ flag run and any number of joined
  segments. Its start is anchored, so that stays linear.
N4b. PGPassword abc123def456 and DBPassword X leaked. An acronym glued to a
  Titlecase key now counts on the whitespace rule. Ed25519PrivateKey
  X509Cert and random-case gby3zPassword x stay prose (the maintainer's
  decision 4).

Also: the false-positive guard list (71 strings) is pinned as a table of
exact engine, policy and dict-leaf outputs. Four entries changed from rev 9,
each commented: token::Type and secret::new() lose the engine false positive,
and Authorization: [required] / (none) now redact the inside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…see #234)

Revs 8 and 9 each passed a hand-listed differential with 0 unaccepted rows,
and a board then blocked each one on an axis the list never produced. The
differential is now derived from the oracle's grammar instead:
tests/_redaction_grammar.py samples main's keyword, Authorization and Bearer
rules, the policy keys, and every escape and scalar json.dumps emits. The
docstring lists every axis and the sampling density.

- Surfaces: 12 per text row (E and P on raw text and on four json.dumps
  spellings, process_output(str) and process_output(dict)), plus the result
  type. Each scalar row is checked through process_output(dict).
- Tier 1 (11 335 rows) runs in the default suite, in about 7 s.
- Tier 2, the full set (145 983 rows), runs under the new `slow` marker.
  It is excluded by default via addopts, following `live`, and runs one
  block per case.

The oracle is regenerated from main @ 9ca081e by regen_fixture.py. It is
deterministic (gzip mtime=0, sorted keys, base64 wrapped at 76) and carries a
fingerprint of each tier's corpus, so a corpus change without a new fixture
fails. The fixture and the plan copy are cmp-identical.

The accepted classes now implement the stated ones. Each is decided from the
row as written (pre, qual, name, suffix, sep, value) and the surface, never
from the output:
- N3: a glued prefix or suffix of more than 24 alphanumerics;
- N10: arn:/urn:;
- C3/C3a: D2 whitespace;
- N11;
- N4: the maintainer's decision 4;
- C4: == or :: with a plain value;
- C5: suffixed;
- C6: glued, any case;
- C7;
- C8: any unindented line break with a plain value;
- N6b: a quoted value on a serialised surface (#290,
  decision 3);
- C10: weak key; qualified code with a non-credential value;
- C11: Authorization/Bearer with a plain word, possibly wrapped;
- N4c: an inner key word straddling name and suffix.

Result: 0 unaccepted on both tiers, and on a second seed of tier 2.

The oracle is regenerated by tests/fixtures/regen_redaction_main_oracle.py
(now committed; it reproduces the fixture byte for byte). The hand-listed string differential and its word-list classifier are
removed. The structured-result differential and the JSON fuzz keep their
corpora and now use the grammar classifier. An axis test asserts every
claimed axis, escape, scalar and surface is produced. The no-control-
characters guard also covers the new module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…alifier axes (see #234)

This commit applies the coordinator's decisions on the rev-10 open items.

N8. key_code=abc123def456 kept a credential-shaped value that main
redacted, because `key` was on the status-qualifier list. `key`, `promo`,
`coupon` and `discount` are removed: none of them names a status. Those keys
are now weak keys and redact such a value, as on main. The 37 qualifiers
that remain are the new stated class C12 ("a `code` key qualified by a
diagnostic or descriptive word keeps its value"). It is decided from the
qualifier's last segment alone and listed explicitly in the grammar module
and in the plan. A test pins the class set equal to
_STATUS_CODE_QUALIFIERS.

Three new axes in the grammar-derived differential:
- a `code` qualifier axis: an exhaustive block of every diagnostic and 13
  credential qualifiers, plus sampling in the name-focus and mix rows;
- a wrap axis: bare, prose, or the pair in each position main's URL rule
  reads (path, query pair, query value, fragment);
- a URL block derived from main's URL rule and redact_auth_url: userinfo;
  hosts, including one whose port does not parse; query keys in and out of
  AUTH_SECRET_QUERY_KEYS, in any case, percent-encoded or not, with
  credential, plain, encoded or empty values; and the fragment.

A piece now counts as present when it appears as written or percent-decoded,
so main's re-encoding of what it keeps is not counted as a removal. The
N11 class reads the value together with whatever follows it in the row.

Found by the URL axis:
  process_output({"t": 'see https://:pw@h.example:99999?SID=%69abc"'})
came back as a str on rev 9 and on the previous rev-10 commit. Inside a
serialised leaf the URL runs up to the backslash of the closing \", and the
query-value span ate it. A URL now hands back a trailing backslash, as it
does trailing punctuation.

Tier 1 has 12 535 rows and tier 2, the full set, 166 183. Both have 0
unaccepted rows, as does tier 2 on a second seed. The slow tier's docstring
says how to run it (-m slow) and that CI does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HCiJU6dVVrNdcz461oN33
…(see #234)

main's sanitize_auth_diagnostic, redact_auth_url and redact_secrets, copied
from origin/main (1fb36f2) so the floor replay can be compared with them byte
for byte. Test-only: main's keyword rule is quadratic on joiner-rich runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
)

The redactor no longer re-implements main's rules. pmcp.redaction_floor
replays them -- URL, authorization, bearer, keyword, JWT, then the effective
policy patterns, each on the previous one's output as main ran them -- with
every character of the intermediate text tracking the input position it came
from. What main removed is known exactly, as spans over the input, and every
such span is applied unless a named suppression predicate drops it. Rev 10's
rules only add.

* Main's keyword rule is quadratic on joiner-rich runs (8 KB of 'a-' takes
  3 s on main), so it is replayed by main_keyword_matches, a linear matcher.
* redact_auth_url is called for real and aligned with its input per
  component; an alignment that does not reproduce main's output makes the
  whole URL one floor span.
* A floor span over a JSON document is applied inside the string it starts
  in; only syntax is kept.
* The predicates are the stated classes (C3 C3a C4 C5 C6 C7 C8 C10 C11 C12
  N3 N4 N10 N11), each decided from the match's own key, separator and
  value, plus four content-free syntax predicates (separator_syntax,
  policy_keyword_word, scheme_word, wrapper_syntax).
* DEFAULT_REDACTION_PATTERNS is main's list again; rev 10's forms are
  _ADDITIVE_DEFAULT_PATTERNS, applied beside each effective default.

Rev 10 tests that pinned a redaction main made and no stated class covers
(a flag or a bullet after a keyword, '-bearer <credential>', a policy value
across a JSON string boundary) now pin main's redaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
… time (see #234)

B3 of rev 10's board: the split re-stripped the rest of the match at every
position, which is quadratic in a run of ':=' (58 s on 264 KB) and reachable
from redact_secrets callers with no window (a task's status_message and raw
payload, _scrub_sensitive_text). 'A value after it' is now one comparison with
where the trailing separator run starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…, B1-B5 (see #234)

* Fidelity: the replay's text is main's output byte for byte (vendored main)
  on tier 1 (tier 2 slow), the JSON fuzz and random text, with no URL
  fallback; the linear keyword matcher yields the same match tuples as main's
  real regex on random fragments, adversarial joiner runs (main's regex in a
  worker under a per-input timeout) and the grammar tiers.
* Construction: on every surface of tier 1 (tier 2 slow), the board's
  multi-pair and wide-separator grids and the JSON fuzz, every unsuppressed
  floor span is removed and the JSON adjustment keeps syntax only; every
  piece main removes and this keeps is explained by a named predicate that
  fired on it. No hand classification.
* One case per suppression predicate: fires on its false positive, keeps
  none of a credential-shaped neighbour.
* B1-B5 of rev 10's board, red on e79d75f and green here, each with a
  mutant that turns it red; timing guards (66 KB, under 1 s, every surface).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…key reading (see #234)

* pmcp.keyword_matcher holds main's keyword rule in linear time with no
  dependency on the rest of pmcp (redact_keyword_values is a drop-in for
  main's re.sub), so it can be lifted into a fix for main on its own.
* The matcher scanned the value run after every separator before asking
  whether the run held a key, which re-scanned one long value run from every
  run inside it: 6 s on 66 KB of 'a='. It asks for the key first.
* A key of 11 000 'token-' has 11 000 readings: a key over 256 characters is
  read no way, so no key-reading predicate fires and main's redaction
  stands (30 s -> 0.6 s).
* Timing guards for both, and for 'a-b=' and 'password=b'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…t (see #234)

The board's URL probes (userinfo, fragments, a port main cannot parse and its
400-character arm, every query key main redacts, encoded keys) and the .env
and YAML multi-pair rows join the construction grid; the URL step's labels get
a direct test. The slow tier's adversarial matcher runs stop at 1 500
repetitions (main's regex takes seconds on each).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…budget (see #234)

A running regex cannot be cancelled, so the executor-based comparison waited
for main's quadratic regex on its longest inputs at shutdown and hung the
slow tier. The comparison now uses a pool it terminates, reports what it did
not reach as a TIMEOUT, and a test holds that to a 2 s budget on 66 KB of
'a-'. The slow tier's adversarial runs stop at 1 000 repetitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
ViperJuice and others added 8 commits September 26, 2026 20:16
…see #234)

Main's URL rewrite gives a bare query key an '=' ('?urn:x:tok.' becomes
'?urn%3Ax%3Atok=.'), which the piece metric reads as a removal of 'tok.'.
Part B of the construction test now checks main's own output for the piece
with characters inserted into it before asking the floor for a predicate,
and counts those apart. Found by the tier-2 mix block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ntial-shaped value (see #234)

The maintainer's decision after rev 11's report: a name=value token after a
keyword and whitespace is its own pair only when its value is not
credential-shaped (D2), and never directly after Bearer or Authorization,
whose value it is. 'x Bearer abcdef=SECRETPART' is redacted as main redacts
it; 'token expires_in=3600' and 'token code=404' stay prose. The predicate's
look-ahead grows from 8 to 256 characters so a long pair value is judged
whole.

Main's behaviour wins over an unapproved suppression, so four lines that
rev 10 kept because of N11-after-Bearer now read as main reads them:
'token_type=Bearer expires_in=3600', 'Bearer realm="api"', the
WWW-Authenticate challenge and 'scheme=Bearer scope=read' (the false-positive
table and test_bearer_is_a_scheme_not_a_word pin main's output). The prose
corpus drops those two shapes, and its generator never ends a clause on the
word 'bearer'. They can come back as a separately reviewed class.

Test: red on d3595a0, green here; restoring the old N11 kills it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
One keyword matcher: main's src/pmcp/keyword_matcher.py is taken as it is
(the floor's copy differed only in docstrings), and the floor calls its
public interface unchanged; tests/test_keyword_matcher.py passes untouched.

sanitize_auth_diagnostic stays span-based: main's sequential pipeline is what
pmcp.redaction_floor replays, so the floor's URL step now hands back trailing
punctuation with main's one-pass rstrip, and so does the additive URL rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
The floor module, the vendored-main helper and the floor tests now describe
pmcp.keyword_matcher as the matcher main runs, and the timeout test of the
regex comparison uses a stand-in reference instead of a long input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…ame, N3 glued only, short plain numbers (see #234)

* B-1: the closing wrapping of a Bearer/Authorization value is found by one
  right-to-left scan (a search anchored at the end restarted at every
  closer). B-1b: an unindented line break is one reverse search, in the
  floor's C8 and in the additive rules. B-1c: the predicates are asked once
  per part of a match, not once per span (one match can split around many
  of main's markers and each span rescanned the shared value).
* A generated timing sweep (tests/_redaction_shapes.py): every unit of every
  redactor regex, repeated after each trigger word and before each failing
  tail, 46 096 shapes, through every entry point; the default tier holds
  the board's shapes and the families it found at 16 KB -> 64 KB with a
  linear-growth assertion, the slow tier the whole sweep.
* B-2: N10 fires only on a key that is a segment of an arn:/urn: name, with
  its separator and value inside the name -- not on a key after the name
  across & , ; ( ) ? =.
* N-1: N3 counts only the run glued to the key word, never across joiners.
* N-2: a plain number is at most 10 decimal digits; longer runs and 0x hex
  follow main.
* N-4: every URL position is kept by a piece or labelled; what nothing
  covers (an empty, doubled or trailing '&') is labelled url.normal.

Each check is red on 3e49b95 and green here, with a mutant restoring rev
11's mechanism that turns it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
One case over 46 096 shapes ran past the 700 s per-test timeout in the slow
tier; each chunk now takes about 90 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…isected resource names, camelCase joins words; linearity on a work count (see #234)

* B-3: a marker the URL step synthesises stands for the input it replaced
  (a redacted query value) or for nothing (an added '='), not for the whole
  URL, and _Tracked.source emits each input character once, left to right.
  A later match over many such markers built a value of k x the URL.
* B-4: _in_resource_name answers with a binary search over the sorted,
  disjoint names instead of asking every name for every keyword match.
  widen_over_escapes reads backslash runs from one precomputed pass.
* N-5: N3 treats a case change as a joiner, so camelCase and SHOUTING
  multi-word keys are keys, as _/- joined ones are (N-1).
* T-1: linear time is asserted on a deterministic work count
  (pmcp.redaction_floor.WORK, test-only; every loop and string build adds
  what it iterates over or copies), at 16 KB -> 64 KB -> 256 KB on every
  entry point for every shape both boards found and the composition
  families. What a count cannot see, a regex's own backtracking, keeps a
  wall-clock check with a 64x bound over 16x the input (linear 16x,
  quadratic 256x). The remaining default-tier clock tests are smoke caps
  (10 s) or use the same ratio bound.
* tests/_redaction_shapes.py adds compositions derived from the loops: two
  leads, interleaved units, markers inside markers, nested JSON and header
  contexts (24 120 shapes); the slow tier screens all 70 216 on the work
  count and the regex-derived ones on the clock.

Each finding is red on b7b071d and green here, with a mutant restoring the
earlier mechanism.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
…rk charges; memory linearity; shapes inside atomised pieces (see #234)

* B-5 (B-3's class): a re-encoded URL component is one piece standing for
  all of itself, and every later match inside it copied the whole piece.
  Now a step spells out each input character once: a later match of the
  same step reads those characters as the intermediate text (_Tracked.source
  keeps a per-step frontier). floor_spans copies the removed text only for
  the syntax parts its predicates read, and applies a span over one input
  range once.
* Work is charged exactly where it is copied (source, the removed text).
* S-1: the 256-character windowed searches (_context, the glued tail, the
  resource-name segment) are backward walks; key readings are computed
  once per decision.
* A peak-memory (tracemalloc) linearity check beside the work count: 8 KB
  -> 128 KB in the default tier, 16 KB -> 256 KB in the slow tier.
* tests/_redaction_shapes.py adds repeats of each match-producing unit
  inside each context the replay atomises (a re-encoded query key or value,
  a re-cased host, the ValueError arm, a header value, a JSON string): 476
  shapes; on 4b5cac8 156 of them were super-linear, here none.
* The seat's repros join LINEAR_SHAPES; a mutant restoring rev 13's source
  is killed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT8D8tRmExyQve69DCWRik
@ViperJuice

Copy link
Copy Markdown
Collaborator Author

Closing without merging. This draft existed only to time CI for the replay-based design; plan revision 15 replaces that design with additive-only rules on top of main's redactor. See #288.

🤖 Generated with Claude Code

@ViperJuice ViperJuice closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant