Skip to content

Identity hook: a word is read in linear time however long it is - #262

Merged
don-petry merged 3 commits into
mainfrom
hooks/backticks-linear
Oct 21, 2026
Merged

don-petry merged 3 commits into
mainfrom
hooks/backticks-linear

Conversation

@don-petry

@don-petry don-petry commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

"Identity hook: escaped backticks in double quotes are read in more than linear time" (issue 260), found by the growth test of "Hook tests: a budget measures the hook, not the machine" (pull request 257). A double-quoted commit message of escaped backticks was read as n to the 1.6: 1.8 s of CPU at 20,000 pairs, and past the hook's 10-second reading budget near 80,000, so a long harmless message would be refused command_unreadable.

What changed

Two readings copied the whole word at every character:

  • shlex. shlex grows a token with self.token += c. CPython cannot extend a string held in an attribute in place, so a word of n characters costs about n squared over two copies. tools/hooks/shell.py now reads with Lexer, a shlex whose token is held as its pieces and given back as a string, so it reads exactly what shlex reads.
  • The hook's own reading. _held grew the current word inside a tuple assignment, starts, comments, word = …, word + c, which also copies it, and each ) gave back the whole word before its (. It compares the word only with its keywords, in and case, so it now keeps the first WORD_KEPT (64) characters and one more, through grown(), at every place the word grows.

Measured best of two, CPU:

pairs before after
5,000 0.24 s 0.24 s
10,000 0.63 s 0.47 s
20,000 1.80 s 0.89 s
80,000 about 16 s, past the budget 3.6 s

The growth past the quarter is 2.8, linear, where it was 4.0. The growth line in tests/support.py, set at four and a half for this reading in #257, goes back to four.

  • A test holds that Lexer gives every word shlex gives, in order and as its own strings, on a set of hard spellings (escapes, quotes, punctuation, an open quote that both refuse), that it grows a word in pieces, and that grown() keeps a word to WORD_KEPT and one more, longer than every keyword. Three mutants: the hook reading with shlex itself, a word given back in pieces rather than as a string, and grown() keeping a word whole. A word dense with substitutions, a$(x) repeated, joins the growth test.
  • A row on decision 0040.

Regression corpus

hook_regressions.py and the 10,722-command fresh corpus, main ab53e4f against this head, at the first head and again after the fix round: 0 regressions from the reviewers' attack inputs; 8,842 corpus commands, 0 newly refused; 10,722 fresh commands, 0 newly refused and 0 newly passed, no exception on either side.

Measured

make verify passed on a git archive export of each head. The three new mutants and every mutant of tests/support.py were killed by hand as the shards run them: a whole copy of the tree, MUTATING=1, each named test by unittest.

Who read it

Muse Wonder Full read the first head, 485bc5d, as the Maintainer's ruling of 2026-10-20 has it for a hook pull request, and found three things, each fixed in the one round: _held was still quadratic on a word dense with substitutions, a$(x) repeated 80,000 times taking 9.1 s, since each ) gave back the whole word and each word's end hashed it (0.51 s now, with grown()); the in-place growth had no mutant (grown() has one, and a test of its edges); and the lexer test checked shlex's words were strings, not the lexer's. Muse also fuzzed the lexer against shlex on 30,000 inputs and found no difference. Muse did not read the fix round. The Claude review found the second copy, in _held, after the first fix left the ratio at 3.45; the security pass found nothing: what the hook reads and refuses is unchanged, as the corpus shows.

Closes #260.

🤖 Generated with Claude Code

Escaped backticks in a double-quoted commit message were read as n to the 1.6: 1.8 s of CPU at 20,000
pairs, and past the 10-second reading budget near 80,000, so a long harmless message was refused.
Two readings copied the whole word at each character. shlex grows a token with self.token += c,
which CPython cannot extend in place on an attribute; the hook now reads with Lexer, a shlex whose
token is held in pieces and given back as a string, so it reads what shlex reads. And _held grew its
current word inside a tuple assignment, which copies it; it grows it with a statement of its own now.
20,000 pairs take 0.89 s and 80,000 take 3.6 s, a ratio of 2.8 past the quarter.

The growth line in tests/support.py goes back to four. A test holds that the lexer gives every word
shlex gives, on a set of hard spellings, and grows a word in pieces; two mutants, and a row on
decision 0040. _held's change gives the same value faster, so the wall-clock growth test holds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@don-petry
don-petry requested a review from a team as a code owner October 21, 2026 12:30
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T13:38:32.694433Z a95e7a4 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: combsmith/software-factory/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0236ae60-6ef0-4b36-8cf8-be6451230651
📥 Commits

Reviewing files that changed from the base of the PR and between 485bc5d and a95e7a4.

📒 Files selected for processing: 5
  • docs/decisions/0040-the-identity-hook-reads-where-the-shell-may-stand-and-a-program-it-does-not-read-may-write.md
  • tests/support.py
  • tests/test_hooks.py
  • tools/hooks/shell.py
  • tools/mutants.py

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: combsmith/software-factory/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cda31cd1-46f1-437c-ba76-db6d451694fc
📥 Commits

Reviewing files that changed from the base of the PR and between ab53e4f and 485bc5d.

📒 Files selected for processing: 5
  • docs/decisions/0040-the-identity-hook-reads-where-the-shell-may-stand-and-a-program-it-does-not-read-may-write.md
  • tests/support.py
  • tests/test_hooks.py
  • tools/hooks/shell.py
  • tools/mutants.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The shell lexer now accumulates word pieces instead of repeatedly rebuilding long words. The changes also tighten the growth-test threshold and add checks for lexer compatibility, malformed input, token output, and long-word growth.

Changes: Shell word parsing

Layer / File(s) Summary
Growth threshold tests
tests/support.py, tests/test_hooks.py, tools/mutants.py
The growth helper and its tests now use a 4× threshold instead of 4.5×. The mutation case uses the updated threshold.
Piecewise lexer and integration
docs/decisions/0040-the-identity-hook-reads-where-the-shell-may-stand-and-a-program-it-does-not-read-may-write.md, tools/hooks/shell.py, tests/test_hooks.py, tools/mutants.py
Lexer stores word pieces and converts emitted tokens to strings. commands uses the custom lexer, and word handling updates strings incrementally. Tests check shlex parity, malformed input, token types, and long-word growth. The decision document adds a linear-time boundary declaration.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 485bc

No merge-blocking issue was established in the changed shell-word parsing or growth checks. Merge after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: identity-hook word parsing now reads words in linear time regardless of word length.
Linked Issues check Passed Active linked issue #260 requires linear-time reading for escaped backticks, restoration of the faster_than_linear threshold to 4, and regression and mutation coverage. tools/hooks/shell.py adds `…
Out of Scope Changes check Passed The changed files stay within issue #260 scope. tools/hooks/shell.py fixes hook parsing cost. tests/support.py, tests/test_hooks.py, and tools/mutants.py add or adjust the required performance…
Full details: Linked Issues check

Explanation

Active linked issue #260 requires linear-time reading for escaped backticks, restoration of the faster_than_linear threshold to 4, and regression and mutation coverage. tools/hooks/shell.py adds Lexer and _Token piece accumulation, and _held appends the word separately. tests/support.py enforces the 4× threshold. The supplied PR evidence reports 2.8 growth at 80,000 pairs, lexer and wall-clock tests, decision-0040 coverage, two lexer mutants, and zero reported corpus regressions or new refusals.

Full details: Out of Scope Changes check

Explanation

The changed files stay within issue #260 scope. tools/hooks/shell.py fixes hook parsing cost. tests/support.py, tests/test_hooks.py, and tools/mutants.py add or adjust the required performance and mutation coverage. The decision-0040 documentation records the related boundary rule. No unrelated functional change is established.

Full details: Docstring Coverage

Explanation

Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

don-petry and others added 2 commits October 21, 2026 07:44
…ord dense with substitutions is linear too, after Muse

Each ) gave back the word before its ( with the character after it, and each word's end hashed it
against the keywords, so "a$(x)" repeated 80,000 times took 9.1 s in _held. The reading compares a
word only with the keywords, "in" and "case", so it keeps WORD_KEPT, 64, characters and one more:
grown() does it at every place the word grows, with a test of its edges and a mutant. The same input
takes 0.51 s now, and a case of it joins the growth test. The lexer test asserts the lexer's own
words are strings, not shlex's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@don-petry
don-petry added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 4e7e403 Oct 21, 2026
20 checks passed
@don-petry
don-petry deleted the hooks/backticks-linear branch October 21, 2026 13:52
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.

Identity hook: escaped backticks in double quotes are read in more than linear time

1 participant