Skip to content

Hold the refusal with tests, and make the document limits true - #5

Merged
coderloganli merged 4 commits into
mainfrom
fix-path-comparison
Aug 16, 2026
Merged

Hold the refusal with tests, and make the document limits true#5
coderloganli merged 4 commits into
mainfrom
fix-path-comparison

Conversation

@coderloganli

Copy link
Copy Markdown
Owner

Three commits, all on the same theme: the project says things about itself that nothing was checking.

The one refusal had no test. Nothing in the suite ran guard-write.mjs — the only hard enforcement in the product, and the thing README and the architecture both lead with. Cases 49-52 drive it as Claude Code does, a child process fed a PreToolUse event on stdin: the refusal itself, the documentation paths staying writable, and spelling a path differently not changing the answer.

That last one passes today, and the test says why it can: both sides of the comparison come from one string, since the guard hands context() the target's own directory. That is a property of the call site rather than of classify(), so a future caller passing a workspace from elsewhere would lose it silently.

§10 now says which way the refusal fails. It fails open — a crashed, timed-out or confused hook lets the write through, as does a ticket it cannot parse. That is intended, for the same reason the server's direction is the opposite: a hook failing closed would let one corrupt file in tickets/ freeze every repository in the project. The cost is stated plainly, because a refusal that fails open is not a guarantee. Case 52 holds the behaviour.

The document limits are now true. Six places said both top-level documents were about 200 lines; architecture.md was 456, because the only test enforcing a bound checked the other file. It is now 345, cut by removing duplication rather than content — the reasoning behind six decisions was narrated here and recorded in docs/adr/0002-0008, which is the drift ADRs exist to prevent.

Reaching 200 would mean dropping the stage table, the layout diagram or the refused-paths block, which are the parts most often read. So the bounds now differ and say so: product design 200, architecture 350, with case 53 holding both — and holding each document to stating the number it is held to, since a limit enforced in one place and stated in another drifts apart.

60/60 tests pass. Cases 52 and 53 were both verified to fail before being trusted.

🤖 Generated with Claude Code

https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy

coderloganli and others added 4 commits August 15, 2026 20:42
The refusal is the only hard enforcement in the product and had no test of its
own: nothing in the suite ran guard-write.mjs. Cases 49-52 drive it the way
Claude Code does, as a child process fed a PreToolUse event on stdin.

Case 50 holds the property the refusal rests on — that spelling a path
differently does not change the answer. It passes today because both sides of
the comparison come from one string: the guard hands context() the target's own
directory and compares the target against the workspace that call derived. That
is a property of the call site, not of classify(), so a future caller passing a
workspace from elsewhere would lose it silently. Hence the test.

Case 52 asserts that a ticket the guard cannot parse leaves the write
unrefused, and §10 now says so: the hook fails open where the server fails
closed. A hook failing closed would make a single corrupt file in tickets/
freeze every repository in the project, including the documents the harness
tells you to go and fix. The cost is that the refusal is not a guarantee, which
is the claim §2 already makes about circumvention, reached from the other side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
456 lines, for a document that tells every task to read two documents of about
200. The rule was stated three times in a file that broke it.

What went is duplication, not content. The reasoning behind six decisions was
narrated here and recorded again in docs/adr/0002-0008, which is the drift the
ADRs exist to prevent; each is now a sentence and a pointer. The tool table
repeated what every tool already tells the client about itself, and §11
repeated product.md's list of non-goals — what stays there is the three
exclusions that are architectural rather than product decisions.

456 to 338. Reaching 200 from here means dropping the stage table, the layout
diagram or the refused-paths block, which are the parts most often read.

Case 32 held two phrases this moved. The promise that nothing is deleted is the
product's, so it is now asserted against product.md, where it is made.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
Six places said both top-level documents were about 200 lines. One of them was
456, and the only test enforcing a bound checked the other document — which is
how it got there.

The two bounds now differ because the jobs do: product design at 200, since
what a product is for and will not do is prose that grows only by repeating
itself; architecture at 350, because how a system is built has reference
material — a layout, a stage table, the exact paths a rule names — that a
reader returns to and that sentences cannot replace.

Case 53 holds both, and holds each document to stating the number it is held
to, since a limit enforced in one place and stated in another drifts apart
silently. Verified it fails: ten lines of padding, and it names the file, the
count and the bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
Case 50 asserted that a backslash-separated path is refused, which is true on
Windows and wrong everywhere else: on Linux a backslash is an ordinary
character in a filename, so that path names a different file and allowing it is
correct. The case-insensitivity cases were already guarded; this one was not.

Found by the ubuntu job on its first run against these tests, which is what the
matrix is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
@coderloganli
coderloganli merged commit ff72d09 into main Aug 16, 2026
4 checks passed
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