Skip to content

fix: the scanner, rules loader and reading fixes, and the name guard reads escaped spellings - #733

Merged
REPPL merged 69 commits into
mainfrom
integ/land-9
Sep 28, 2026
Merged

REPPL merged 69 commits into
mainfrom
integ/land-9

Conversation

@REPPL

@REPPL REPPL commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Integration branch 9 lands four reviewed lanes as one change: the scanner cluster (drainS1 and drainS3), the rules loader (drainS2) and the reading bugs (drainRd), with one window recalibration at the merged tip.

drainS1 (scanner identity). The redactors read the JSON escape layers of a transcript line as decoded views, so a secret, a home path or a real name written straight after a \n, \t or \" escape, or spelled with the \/ solidus escape, is a finding and is redacted before it is stored. A Windows home (C:\Users\<name>, typed or JSON-escaped) is a third-party home path, and a short name in author metadata is kept on bytes. The Windows allowlist entries Default and All also cover the POSIX /Users root, so a third-party macOS account literally named Default or all raises no home_path_other; the resolution of the Windows-home record says so.

drainS3 (scanner siblings). The caller's own home is caught in its escaped spellings by the backstop sweep, the privacy-hygiene rule and the harness-leak lint read the same decoded spellings, a UTF-16 byte view catches a value written in UTF-16, and the private name guard in the pre-commit hook reads the decoded spellings of staged text. The EXIF and PDF-string residues are deferred on their records.

drainS2 (rules loader). The home directory is never a session's repo root, the foreign-uid refusal says what it still reads at the working directory, and a bundled guardrail entry that an override withholds is named on stderr on every load.

drainRd (reading bugs). A CRLF document redacts like its LF twin wherever the excluded section sits, a nested mapping is refused behind every block indicator, the escaped-key refusal states only what it knows, and the bare reading render lists only the parked runs still awaiting an outcome. The lane carries drainM1's commits, which integration branch 7 lands first.

drainS3 fix round (name guard decoding). The pre-commit name guard reads a name hidden behind escape-spelled backslashes (\u005c, %5C) up to three layers deep, the same depth the scanner decodes, and a test holds the two bounds equal. It decodes a very long staged line in linear time, so a commit carrying one no longer stalls for minutes. The decode block is byte-identical in the repository hook and the hook abcd ahoy scaffolds.

Re-merge. Main moved after this branch was cut: integration branches 6, 7 and 8 landed (#730, #731, #732). The re-merge conflicted only on the reading window figures. Integration branch 8's git identity gate and this branch's name-guard decoder sit side by side in the repository hook, and both hold under their tests. Four records that drainM1 resolved landed with integration branch 7, so they are no longer listed here. Recalibrated at the re-merged tip, every window keeps at least 1% headroom and none moves: widening 1,280,867 tokens under 1,300,000, entailment 382,072 under 390,000, detection 1,289,903 under 1,310,000.

Reviews: drainS1 SHIP. drainS3 SHIP; its third fix round SHIP on verification, and its fourth fix round SHIP on verification. drainS2 SHIP; its fix round done. drainRd SHIP; its fix round done. Integration branch 9 SHIP.

Resolves: iss-174
Resolves: iss-2608301237450573
Resolves: iss-2608301421381157
Resolves: iss-2608311621412224
Resolves: iss-2609020219198779
Resolves: iss-2609090934372160
Resolves: iss-2609251522588539
Resolves: iss-2609251600019863
Resolves: iss-2609251639261103
Resolves: iss-2609251639263391
Resolves: iss-2609261647358395
Resolves: iss-2609261658553101
Resolves: iss-2609261659041553
Resolves: iss-2609261753285273
Resolves: iss-2609261753290536
Resolves: iss-2609261811321435
Resolves: iss-2609261827066511
Resolves: iss-2609261900095459
Resolves: iss-2609261905354450
Resolves: iss-2609261909101409
Resolves: iss-2609261909106167
Resolves: iss-2609261909108726
Resolves: iss-2609280944560197
Resolves: iss-2609280945018822
Refs: iss-2609020219265817
Refs: iss-2609261659051539
Refs: iss-2609261831352258
Refs: iss-96
Refs: iss-2609261536147903
Refs: iss-2609251455354719

Assisted-by: Claude:claude-opus-5-5

…home paths

The transcript store scans raw JSONL, and a token or home path written
beside a JSON string escape is invisible to the raw-line pass. Captured
before the fix, per the capture-first rule.

Refs: iss-2609261647358395
Assisted-by: Claude:claude-opus-5-5
The transcript store scans raw JSONL, and a JSON string escape changed
what the detectors saw without changing what the text says. A token
written after a \n or \t escape had the escape letter as a word byte
before it, so no leading-\b token pattern matched; a home path beside
a \n, \t or \" escape failed the home anchor's boundaries; a home
written with the solidus escape (\/ or /) had no '/' on the line;
and a non-ASCII name written as é matched no configured name.

Each line carrying an escape is now decoded one JSON layer at a time
(at most three, a layer that decodes nothing ends the walk) and every
layer is scanned as a view of the line, the way the percent-decode
pre-pass scans its decoded copy: each hit is mapped back to the raw
bytes it came from, so Redact masks the live spelling on disk. Every
layer is scanned, not only the last, because a literal backslash an
earlier layer uncovers is read by the next as an escape. The view scan
is shared with the percent pre-pass (viewFindings), and the new stage
charges the scan meter, with four linearity fixtures.

This changes the scan engine, not the canonical pattern set, so it
reaches every ScanText and ScanBundle consumer: the history transcript
store, the capture and memory redactors, scanner.CheckOutbound behind
`abcd lint outbound`, and the launch bundler and lifeboat packer. The
repolint privacy rule and the harness_leak lint rule run the patterns'
regexps directly and are not reached.

Refs: iss-2609261647358395, iss-2609251639263391
Assisted-by: Claude:claude-opus-5-5
home_path_other was POSIX-only, so a third party's home written as
<drive>:\Users\<name> (a WSL session, a pasted PowerShell transcript,
a Windows CI log) raised nothing, as typed or JSON-escaped.

genericHomeRe gains the Windows alternative, each separator a run of
backslashes so one JSON layer (doubled) or two (quadrupled) read the
same as the typed spelling. Its friends follow it: the trailing
boundary takes the backslash; the system-directory allowlist
recognises the Windows Users root and gains Default and the All Users
junction beside Public; the traversal walk reads a backslash run as ONE
separator, so an escaped separator inside a system root is not taken
for an empty traversal segment; and the home_path_other skip compares
the caller's home against the match with its separator runs collapsed,
so the caller's own escaped home is never reported as a third party's.

The caller's own home is home_path_self at any depth through the
JSON-escape views of the previous commit; abcd builds for darwin and
linux only, so the caller's home is never itself a Windows path.

The allowlist is shared with the repolint privacy rule, which applies
it to its own C:\Users branch; that rule's twin regexp is otherwise
unchanged.

Refs: iss-2609251639261103
Assisted-by: Claude:claude-opus-5-5
…ytes

The byte scan dropped every short single-token real_name as chance
noise, so a name the text scan hard-fails on shipped unreported in a
PDF /Author entry or any other author stamp. The existing shape test
pinned the defect itself, expecting /Author (Zedqx) to raise nothing.

Decision (recorded here, not in the record): the byte scan keeps a
short single token where a metadata key that names a person ends
within 96 bytes before it, and drops it everywhere else. The keys are
author, artist, creator and lastModifiedBy, which cover a PDF Info
/Author, XMP dc:creator, pdf:Author and tiff:Artist (pretty-printed
across lines, as XMP writers lay it out), PNG text Author and Artist,
and an OOXML or ODF dc:creator or cp:lastModifiedBy inside a decoded
zip. That is the anchored context the record names as closing it, and
it neither lowers the threshold nor accepts a false-positive rate: the
same token incidental to binary content, a hundred times over or past
the key's reach, still raises nothing, so the report is not flooded.
The threshold keeps counting bytes, now with the reason stated: a
chance collision needs that many specific bytes in a row. A key held
in binary structure (EXIF's Artist tag, a UTF-16 PDF string) is not
text in the bytes and stays out of reach.

Refs: iss-2609090934372160
Assisted-by: Claude:claude-opus-5-5
The residue iss-96 names (a bare value with no key name, a labelled
value under the entropy floor) is reached only by an always-on
entropy or charset detector, which the 2026-08-28 ruling rejected for
its redaction false-positive cost on transcript prose when it shipped
the opt-in external-scanner adapter. Building one here would re-open
that ruling, so nothing is built: the deferral is carried past v0.11.0
with the tradeoff named, and a dated re-check records that both pins
still hold after this lane's JSON-escape views.

Refs: iss-96
Assisted-by: Claude:claude-opus-5-5
Found by the sibling sweep and left for their own lanes: the repolint
privacy rule and the harness_leak rule read committed lines raw, so the
escaped spellings the scanner's views now read pass them; the literal
caller-home backstop reads no escaped spelling of the home; and a short
name in a binary EXIF Artist tag has no metadata key text beside it.

Refs: iss-2609261658553101, iss-2609261659041553, iss-2609261659051539
Assisted-by: Claude:claude-opus-5-5
The fix is c55ae5e: the scanner reads each line's JSON-escape layers
as decoded views mapped back to raw spans.

Resolves: iss-2609261647358395
Assisted-by: Claude:claude-opus-5-5
…eparator

The fix is c55ae5e: the JSON-escape views decode \/ and /, and
the resolution note records the decision on each other escape spelling
the scanner's input can carry.

Resolves: iss-2609251639263391
Assisted-by: Claude:claude-opus-5-5
…home path

The fix is 4013610: the one home matcher reads <drive>:\Users\<name>
at any escaping depth, with its boundary, allowlist and traversal walk.

Refs: iss-2609261658553101
Resolves: iss-2609251639261103
Assisted-by: Claude:claude-opus-5-5
… is kept on bytes

The fix is 7babc8d: a short single-token real name standing in a
person metadata field is kept by the byte scan; the binary-structured
residue (EXIF Artist, a UTF-16 PDF string) is captured on its own.

Refs: iss-2609261659051539
Resolves: iss-2609090934372160
Assisted-by: Claude:claude-opus-5-5
A home that is itself a git working tree (dotfiles in the home) made the
home the git toplevel for every non-repo directory beneath it, so the rules
root walk stopped at ~/.abcd and read it a second time as the REPO layer:
the home's guard.json, which has no user layer at all, and its config.json
over itself as the machine layer. Resolve now passes over the home in the
walk, and a toplevel that IS the home takes the non-repo route (cwd, no
walk) when nothing below it carries a .abcd. The user layer still reads
~/.abcd/rules.json, once, as the user layer.

Decision taken in the lane (the run A orchestrator's brief rules the
exclusion; the record's owed question was whether a home toplevel is a
legitimate repo-scope root): only the home itself is excluded, not every
ancestor of it. A toplevel that CONTAINS the home, the shape of a hermetic
harness that points HOME inside its checkout, stays the root, because it is
a repository git vouched for and its own .abcd is its own; the walk still
skips the home on the way up to it. A session whose working directory IS the
home keeps reading a .abcd there as cwd's, the working-directory read that
stays a posture question (DECISIONS.md, 2026-09-25).

Tests watched fail first: TestResolveRootNeverAdoptsTheHomeDirectory,
TestResolveRootNeverAdoptsTheHomeThroughTheMarker and
TestResolveRootNeverAdoptsTheHomeBeneathAnotherToplevel each resolved the
temp HOME as the root before the change.

Refs: iss-2609020219198779

Assisted-by: Claude:claude-opus-5-5
…epo root

Resolves: iss-2609020219198779
Assisted-by: Claude:claude-opus-5-5
The ownership refusal's note told the session that the refused root's
rules.json and guard.json were NOT read and that the bundled defaults stood
in. Resolve returns the working directory as the root on that refusal, so
a .abcd there is read: a session started at the refused root reads that
root's configuration while being told it did not, and so does one beneath
it that carries its own .abcd. 0434d47 corrected AGENTS.md and the marker
block ahoy writes; the note itself, the configuration chapter and the
install how-to still made the same claim.

The note now names which of the two happened: from a directory with no
.abcd the old wording stands, and where the working directory carries one
it says the refusal bounds the walk, not the working directory, and that
the .abcd there IS read. The posture change that would make that read
refuse too stays deferred as recorded (DECISIONS.md, 2026-09-25).

Test watched fail first: TestResolveRootRefusalSaysWhatItStillReads, both
subtests (the note said "NOT read" and "fall back to the bundled defaults"
while Load read the working directory's kill switch).

Refs: iss-2609251522588539

Assisted-by: Claude:claude-opus-5-5
…reads

Resolves: iss-2609251522588539
Assisted-by: Claude:claude-opus-5-5
A rules.json list replaces the bundled list wholesale, so a repo that
pinned PII's rules before iss-156 added the network-identifier rule kept
the old set with no notice: a stale guardrail set that looks current. The
merge stays per field (the documented behaviour a repo relies on to say a
rule in its own words); the loss is no longer silent. For the guardrail
domains, COMMITTING, LOAD and PII, Load compares every recall, alias and
rule list an override set against the list this binary bundles, and adds a
note naming each bundled entry left out and the file whose list is in
force. The hook and `abcd rules` print it on stderr, never in the injected
context or the --json document.

Decisions taken in the lane: the check covers the three guardrail domains
and not every bundled domain, because the others are conventions a
repository restates in its own words (this repository's own INTENTS and
ROADMAP overrides do exactly that), and a note on each deliberate
restatement would teach the reader to skip the one that matters. The
comparison is against the bundled list as shipped, not a hash recorded at
override time, so it needs no new schema field. Whether security-bearing
lists should union instead of replace, or take a replace-versus-extend
marker, stays the product thinker's question, recorded with itd-117's
finer-grained-merging follow-up.

Two front-door tests read `rules --json` through the combined output, and
the new stderr note broke their parse; they read stdout alone now, which is
what the JSON contract is.

Tests watched fail first: TestLoadNamesTheBundledSecurityRulesAnOverrideWithholds,
TestLoadNamesTheBundledRecallAnOverrideWithholds and
TestLoadNamesTheUserLayerThatWithholds (against a stub list, before
noteWithheld existed); TestRulesNamesAWithheldGuardrailOnStderr and
TestHookPromptRouterNamesAWithheldGuardrail on a scratch copy with the note
switched off.

Refs: iss-174, iss-156

Assisted-by: Claude:claude-opus-5-5
…ry load

Resolves: iss-174
Assisted-by: Claude:claude-opus-5-5
No mechanical close leaves rules reading as they do. Every CommonMark
construct that makes a rule body a heading has to be closed: ATX on a
continuation line and on the first line (`- # x` is a list item holding a
heading, which the record did not name), a setext underline, and an HTML
h1-h6 block. That takes either a code-safe rendering (a relative indent of
four or more, or a fence), which flattens every legitimately structured
multi-line rule wherever the block is rendered, or a fence-aware escaper
complete only by enumeration, which writes escapes into the raw text the
model reads. Both change how rules read, so the choice is the product
thinker's. The v0.10.0 grant lapsed at the v0.11.0 anchor; the renewal
quotes the question and the record carries the first-line finding.

Refs: iss-2609020219265817

Assisted-by: Claude:claude-opus-5-5
…l owed

Refs: iss-2609020219198779, iss-174, iss-2609251522588539, iss-2609020219265817

Assisted-by: Claude:claude-opus-5-5
…ings

The security review of the rules-loader lane found the home exclusion
comparing path strings, so a case-variant HOME is adopted as the repo
root, and the refusal note deciding "IS read" through a stat that
follows a symlinked .abcd the loader then refuses.

Refs: iss-2609261753285273, iss-2609261753290536

Assisted-by: Claude:claude-opus-5-5
The rules root resolver passes over the home in its walk and sends a
toplevel that is the home down the non-repo route, but it asked both
questions by comparing path strings. HOME is the caller's string and the
walk climbs the physical path git reports, and filepath.EvalSymlinks
keeps the caller's case, so a HOME spelled as a case variant of the
on-disk path on a case-insensitive volume was not recognised: the
version-controlled home became the repo root and its .abcd was read a
second time as the repo layer.

Both sites now ask one helper, homeMatcher, which stats the home once and
compares each candidate with os.SameFile. The home is still read through
userHomeDir, so the user layer and the declined directory stay the same
one; a home that is absent or cannot be stat'd matches nothing, as an
unset one did.

TestResolveRootNeverAdoptsTheHomeAtAnySpelling pins the four spellings
the review named (trailing slash, symlinked HOME, cwd through a symlinked
HOME, case variant) at both sites, with and without a ~/.abcd. Watched
fail first on a scratch copy of the unfixed tree: both case-variant
subtests failed (the home adopted as the root), the other six passed as
the review's probes had. The case-variant subtests skip, and say so,
where the test filesystem is case-sensitive.

Refs: iss-2609261753285273

Assisted-by: Claude:claude-opus-5-5
The foreign-owner refusal note says the working directory's .abcd "IS
read" when one is there, and decided that with os.Stat, which follows a
symlinked .abcd. readRepoLayer Lstat-refuses that shape, so in that edge
the note told the user a layer governs the session that the loader then
refuses. The check is Lstat plus IsDir, so the note and the loader give
the same answer.

TestResolveRootRefusalNeverSaysASymlinkedAbcdIsRead stages a refused
root with a symlinked .abcd at the working directory, proves the loader
refuses it, and requires the note to say NOT read. Watched fail first on
a scratch copy of the unfixed tree (the note said IS read).

Refs: iss-2609261753290536

Assisted-by: Claude:claude-opus-5-5
…identity

Resolves: iss-2609261753285273
Assisted-by: Claude:claude-opus-5-5
…ike the loader

Resolves: iss-2609261753290536
Assisted-by: Claude:claude-opus-5-5
SweepCallerHome and the user-segment rewrite behind SurvivingCallerHome
matched the home only as written, so the solidus escape, its / form
in either case, a second JSON layer and the percent-encoded separator all
passed the one stage the store-before-commit redactors keep independent
of the detector. Both now collect their spans through backstopSpans: the
text as written plus the decoded views of every line carrying a backslash
or a '%' (percentDecodeBounded and jsonEscapeLayers, the scanner's one
definition), with each view hit mapped back through its position map so
the rewrite covers whole escape units.

An occurrence straight after an odd backslash run is the tail of an
escape, so the raw reading leaves it to the view: judging it as written
read the escape's backslash as a boundary, swept "x\/root" under
HOME=/root, and rewriting from the '/' left a dangling "\~" that no JSON
reader accepts. The anchors run on the text the occurrence was found in,
so a control escape before a single-segment home is judged by its decoded
newline. Cost stays linear: TestSweepCallerHomeWorkIsLinear holds five
dense shapes to linearCostBar.

Refs: iss-2609261659041553
Assisted-by: Claude:claude-opus-5-5
… spellings

Resolves: iss-2609261659041553
Assisted-by: Claude:claude-opus-5-5
…e escapes

Refs: iss-2609261811321435
Assisted-by: Claude:claude-opus-5-5
The json_unicode_escaped_own_homes linearity fixture carried the literal
home path: its six-byte escapes had been folded back into slashes when it
was written, so TestScanLineWorkIsLinear held a plain path to the bar and
never the unicode-escape decode the fixture names. The line is now
assembled from escapeSeparators and uSolidus, which no tool can fold, and
TestMeterFixturesCarryTheSpellingTheyName pins both escaped fixtures to
the bytes their names promise.

Refs: iss-2609261811321435
Assisted-by: Claude:claude-opus-5-5
…capes

Resolves: iss-2609261811321435
Assisted-by: Claude:claude-opus-5-5
The redactor splits on "\n", so each line's carriage return is the first
half of the CRLF pair that ends it. When an excluded section is the last
one, the drop takes the newline after the last kept line and the join
left that line's carriage return behind alone. The verifier then refused
the document for a lone CR the source does not carry, so a CRLF record
whose excluded section came last could never be assembled.

The carriage return now goes with its newline, which is what an LF
document already loses at the same place: a CRLF document redacts to its
LF twin's text, line endings aside, wherever the section sits. A CR
ending the source's own last line had no newline to lose and is still
refused. Output bytes change only for documents that were refused
before, so the assembler version does not move.

Refs: iss-2609251600019863
Assisted-by: Claude:claude-opus-5-5
… the tail

Resolves: iss-2609251600019863
Assisted-by: Claude:claude-opus-5-5
The status render read a stage's commit marker through an unbounded
Lstat and a parked run's outcome through an os.Root, so the two
disagreed on a symlink: with the readings directory symlinked out of the
checkout, a parked run refused the render while a stage alone was
classified by a marker read outside the repository. Describe now opens
one root when anything is parked or staged and both probes read through
it.

Refs: iss-2609261905354450
Assisted-by: Claude:claude-opus-5-5
…efused

Resolves: iss-2609261900095459
Assisted-by: Claude:claude-opus-5-5
…run through one root

Resolves: iss-2609261905354450
Assisted-by: Claude:claude-opus-5-5
…uns, PDF hex

Three findings of the review of lane drainS3, captured before their fixes:
the private-banlist guard reads staged blobs only as written, so an
escaped spelling of a private name passes it; the UTF-16 byte view ends a
run at a surrogate pair, so a name after an emoji is missed; and a PDF hex
string, the hex half split from iss-2609261831352258, is never decoded.

Refs: iss-2609261909106167
Refs: iss-2609261909101409
Refs: iss-2609261909108726
Refs: iss-2609261831352258
Assisted-by: Claude:claude-opus-5-5
utf16TextRune refused every surrogate, so a character outside the Basic
Multilingual Plane, an emoji above all, ended a byte-order-marked run: a
name after an emoji in one string was cut off, and read not at all when
fewer than two units stood before the pair. utf16RuneAt decodes a valid
high-low pair as one character and lets the run continue; a lone
surrogate, a pair spelling a noncharacter and every unit the text rule
refuses still end it. TestUTF16RunContinuesPastAnAstralCharacter reads an
emoji, a space and a fake name behind either byte-order mark.

Refs: iss-2609261909101409
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609261909101409
Assisted-by: Claude:claude-opus-5-5
Past maxEscapeRunWalk the raw reading of the home backstop judges an
occurrence as written and spans the home alone; the escape unit is still
masked whole only because the JSON view of the same line spans it and
disjointSpans unions the two readings. The comment said the text as
written decides, which undersold where the correctness comes from.

Assisted-by: Claude:claude-opus-5-5
A PDF writer may spell a UTF-16 text string in hex, <FEFF005A...>, which
never puts the UTF-16 bytes in the file, so the byte scan's UTF-16 view
had nothing to read and the caller's name in such an /Author raised
nothing at any length. pdfHexView decodes the hex pairs of every string
between '<' and '>' (white space skipped, an odd last digit read as its
byte's high nibble, a '<<' dictionary opener passed over), maps each byte
to the raw offset of its first digit, and hands a byte-order-marked result
to utf16View; the findings are re-homed through both maps, so the
person-key rule judges a short name by the raw bytes before its first
digit. No escape grammar is involved: the octal-literal spelling stays
deferred on iss-2609261831352258. The walk reads each raw byte at most
twice; TestUTF16ViewWorkIsLinear gains the hex and astral shapes.
launch --dry-run on this tree is unchanged: 126 files, 1 finding, 0
hard fails.

Refs: iss-2609261909108726
Refs: iss-2609261831352258
Assisted-by: Claude:claude-opus-5-5
…ings

The hex half of iss-2609261831352258 is resolved; the record keeps the
octal-literal half, re-deferred past v0.11.0 with its reason narrowed to
the literal-string syntax that half needs.

Refs: iss-2609261831352258
Resolves: iss-2609261909108726
Assisted-by: Claude:claude-opus-5-5
The private-banlist guard matched every pattern against the staged bytes
as written, so a name spelled with JSON string escapes (a \u escape of
any letter, plain ASCII included, or an escaped solidus) or with
percent-encoding passed it, and a JSON transcript, export or fixture is
where such spellings live. Every staged line that holds such an escape
is now also decoded, into the two views the scanner reads beside the
text as written (the JSON escape layers, surrogate pairs joined, and
the percent view), and each pattern is matched against the decoded copy
as well as the text as written.

The decode stays in the hook, in awk and the shell's printf %b, rather
than in the abcd binary: no binary front door checks staged content
through the scanner's views, and the guard must hold before abcd is
built and in every clone the dispatcher runs it in, the scaffolded copy
included. Its reading is a deliberate superset (any backslash run before
an escape decodes as one escape, a chain of %25 layers decodes to the
byte it names), which can only refuse more. Every step fails closed and
names itself, and nothing decoded is printed. Applied to this
repository's hook and to the scaffolded template alike; the scaffold
cites no record id.

Measured on this machine (macOS awk): 30 MB of plain staged text costs
about as before (0.9s to 1.3s); 20 MB of JSON with escapes on every line
goes from 0.8s to about 5.5s, the awk decode being the cost.

Refs: iss-2609261909106167
Assisted-by: Claude:claude-opus-5-5
…ellings

The private-banlist guard and its scaffolded copy decode the JSON escape
layers and the percent view of every staged line that holds one, and
match each pattern against the decoded copy beside the text as written.

Resolves: iss-2609261909106167
Assisted-by: Claude:claude-opus-5-5
The scanner cluster: fix/drain-scanner-siblings contains
fix/drain-scanner-identity (drainS1, aaf4dd3), so its tip lands both.

Conflict: .githooks/pre-commit, the environment pin's `unset -f` list.
main added `go cd pwd trap` for the sources refresh; the lane added `awk`
for the name guard's decoder. Combined: both sets are pinned. The scaffold
copy (internal/core/ahoy/defaults/pre-commit) merged clean; the two copies
differ only where they differed at the base, plus comment wording.

Assisted-by: Claude:claude-opus-5-5
…61103

review-drainS1 (MINOR) found that the non-user home allowlist entries
Default and All, added for the Windows Users root, are shared with the
POSIX /Users root and the repolint privacy rule, so a third-party macOS
account literally named Default or all raises no home_path_other (the
WARN-level kind). The resolution text now says so. Checked against the
code: nonUserHomeSegments in scanner/network.go is one case-folded map,
isNonUserHomeMatch applies it under /Users/ and <drive>:\Users\, and
repolint's isUsersRoot does the same; neither word is a generic account
name, so the caller's own login under either is still local_username
(hard_fail) and the caller's home still home_path_self.

Amended rather than captured: the limit is a stated consequence of the
fix the record already describes, not a separate defect.

Refs: iss-2609251639261103
Assisted-by: Claude:claude-opus-5-5
The rules loader's security records: the home directory is never a
session's repo root, the foreign-uid refusal says what it still reads,
and a bundled guardrail an override withholds is named on every load.
iss-2609020219265817 stays deferred past v0.11.0 (ruling AN).

Clean merge: DECISIONS.md takes the lane's line as the last append;
commands.md auto-merged and `go generate ./internal/surface/cli`
regenerates it and surface.json with no drift.

Refs: iss-2609020219265817
Assisted-by: Claude:claude-opus-5-5
The reading bugs: a CRLF pair is dropped whole at the tail, a nested
mapping is refused behind every block indicator, the escaped-key refusal
states only what it knows, and the bare render lists only the parked
runs awaiting an outcome. The lane is based on drainM1 (a1c5dfc), so
drainM1's commits come with it; integ7 lands them first.

No textual conflicts; `go generate ./internal/surface/cli` regenerates
commands.md and surface.json with no drift.

Semantic conflict, fixed here: drainM1's b2fc7f3 rewrites the
release-gate manifest's _comment, which changes the manifest's sha256,
while main's fc345f9 pins receipt.example.json's manifestHash to the
committed manifest (TestReceiptExampleManifestHashIsTheCommittedManifests).
The example now carries the merged manifest's hash, sha256:0262c01b...;
the manifest itself is drainM1's text unchanged.

Assisted-by: Claude:claude-opus-5-5
…line decode cost

Two findings of the verify of fix3-drainS3: a backslash the guard decodes
from an escape is never read again as an escape, so a name one layer behind
it commits while the scanner reads it; and the awk decode is superlinear in
line length on macOS awk.

Refs: iss-2609280944560197, iss-2609280945018822

Assisted-by: Claude:claude-opus-5-5
Measured on a clean `git clone --no-local` of ecbaf85 with
`reading assemble --position P --target HEAD --dry-run --json`; each
window is ceil(tokens * 1.01 / 10000) * 10000.

- widening: 1,263,373 tokens (4,863,987 bytes); window stays 1,280,000.
- entailment: 376,646 tokens (1,450,090 bytes); window 380,000 -> 390,000,
  since 380,000 left under one per cent of headroom.
- detection: 1,272,409 tokens (4,898,775 bytes); window stays 1,290,000.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
…r time

A backslash the guard decoded from an escape (the unicode escape of
U+005C, or %5C) was never read again as an escape, so a banned name one
layer behind it committed while scanner.DecodedViews reads it on its
second layer. Each decoded layer is now read again for the escapes it
still holds, up to decode_layers, which the hook declares once and a
scanner test holds equal to maxJSONDecodeLayers (3). A %5C before a
JSON escape, which the scanner does not decode, is read here too.

The awk decode split every line on a regular expression, which makes
the split of the one true awk (macOS) quadratic in line length: one
19 MB line took 156 s to decode and 172 s to commit. The splits take
one-character strings, which POSIX, gawk and mawk read as the character
itself; the decoded output is byte-identical on a 4,000-line corpus of
escape edge cases and on the 19 MB fixtures, and the same line now
decodes in 2.5 s (commit 28 s at load 20).

Both hook copies carry the same decode block, and a test holds them to
the same bytes and every split to a one-character separator. The hook
comments and the banlist brief chapter say what the guard reads now.

Refs: iss-2609280944560197, iss-2609280945018822

Assisted-by: Claude:claude-opus-5-5
…line decode cost

Both findings of the verify of fix3-drainS3 are fixed in 5c5a859: the
guard reads each decoded layer again, as many layers as the scanner, and
its awk decode is linear in line length.

Resolves: iss-2609280944560197, iss-2609280945018822
Assisted-by: Claude:claude-opus-5-5
integ6, integ7 and integ8 landed on main after this branch's base. One
file conflicted: .abcd/config/reading-presets.json, whose three window
figures both sides re-measured; main's figures are taken by hunk and the
windows are re-measured at this tip in a following commit.

The pre-commit hook merged clean: main's itd-131 identity gate (author and
committer resolved in git's own order) and this branch's layered name-guard
decoder sit in different hunks of .githooks/pre-commit. main did not touch
the scaffold copy, so the two copies diverge exactly as they did on each
side. The release-gate manifest is unchanged by this merge and
receipt.example.json's manifestHash already equals its sha256.
commands.md and surface.json were regenerated with no drift.

Assisted-by: Claude:claude-opus-5-5
The name guard's decoder reads up to three escape layers (decode_layers=3,
pinned to the scanner's maxJSONDecodeLayers by a scanner test) and splits
each line on a one-character string, which is linear in the line length.
It merged clean: the decode block is byte-identical in both hook copies,
and main's identity gate in .githooks/pre-commit is untouched by it.

Refs: iss-2609280944560197, iss-2609280945018822

Assisted-by: Claude:claude-opus-5-5
Measured on a clean clone of 35483ff by dry-run assemble. Every window
keeps at least 1% headroom, so none moves: widening 1,280,867 tokens
(4,931,339 bytes) under 1,300,000; entailment 382,072 (1,470,980) under
390,000; detection 1,289,903 (4,966,127) under 1,310,000. The comparative
position is not measured this way, as its preset comment states.

Refs: iss-2609251455354719

Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 28, 2026 14:07
@REPPL
REPPL added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 18c83ca Sep 28, 2026
22 of 23 checks passed
@REPPL
REPPL deleted the integ/land-9 branch September 28, 2026 15:31
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