Skip to content

Scan speed: repo-complexity gate + evidence-driven read set (halve runtime on complex repos) - #34

Closed
serenakeyitan wants to merge 4 commits into
mainfrom
feat/scan-complexity-gate
Closed

Scan speed: repo-complexity gate + evidence-driven read set (halve runtime on complex repos)#34
serenakeyitan wants to merge 4 commits into
mainfrom
feat/scan-complexity-gate

Conversation

@serenakeyitan

Copy link
Copy Markdown
Contributor

Refs #33.

Problem

The dominant scan cost is the agent reading+analyzing files (seconds each). The module greps are milliseconds and sweep the whole tree regardless of depth. But scan depth was decided by file COUNT alone — so a 400-file / 200k-LOC repo read all 200k lines under DEEP. The user asked to (a) add a complexity judgment at the very start of the run, (b) read only important files top-down / on-demand for complex repos, (c) aim to halve runtime.

Design (adversarially vetted)

A workflow pressure-tested the naive idea ("read only MUST_SCAN + grep-hits") and proved it unsafe — error-handling (swallowed catch, floating promise), auth (IDOR / guard-dominance dataflow), and performance are read-dependent verticals: their fatal-eligible findings need the agent to READ a handler/service body, not match a grep. Dropping those files would silently drop confirmed fatals. The final design keeps the speed win with guardrails:

  1. Complexity gate at 1.1bscan_depth = max(count_tier, loc_tier). New LOC axis (wc -l over the already-sorted source_list; thresholds 40k / 250k). Deterministic: max() of two closed-range total orders is one pure, monotone function (can only ever read more conservatively). A 400-file / 200k-LOC repo now tiers DEEP → SHALLOW; a 400-file / 20k-LOC repo stays DEEP.
  2. Expanded MUST_SCAN — added the read-dependent service surface (services/jobs/workers/tasks/lib/domain/usecases/processors/core) so handler+service bodies are always fully read even when the rest is sampled. This is the guardrail that makes tiering-down safe.
  3. Evidence-driven read set (every depth): MUST_SCAN ∪ grep-hits ∪ depth-scaled sample. Every grep-flagged file is promoted into the full-read set — the "follow the evidence / on-demand top-down" read. must + hits are never sampled out.
  4. Sample floors — DEEP all / SHALLOW 1-in-5 / MINIMAL 1-in-20 (was zero — a coverage improvement).
  5. Transparency — coverage line + ⚠️ Partial coverage warning whenever below DEEP. Cache/determinism intact (scan_depth already gates the key).

Net effect

~50% fewer file-reads on complex repos; normal/small repos unchanged; two pre-existing coverage gaps fixed as a side effect (MINIMAL zero-sample; grep-hits could be sampled out in SHALLOW).

Verification

  • Bash logic validated end-to-end on a fixture repo (LOC sum, max() tiering, MUST_SCAN dirs, two-phase read set).
  • Complexity tiering verified across 6 (count, LOC) cases (300/15k→DEEP, 400/200k→SHALLOW, 400/20k→DEEP, 3000/100k→SHALLOW, 400/300k→MINIMAL, 8000/50k→MINIMAL).
  • render-report.test.mjs 17/17 still pass (no collateral).

🤖 Generated with Claude Code

serenakeyitan and others added 2 commits July 7, 2026 20:19
…on complex repos)

The dominant scan cost is the agent reading+analyzing files (seconds each;
the module greps are ms and sweep the whole tree regardless). Depth was
decided by file COUNT alone, so a 400-file / 200k-LOC repo read all 200k
lines under DEEP. Add a complexity gate at the very start and read the
important + evidence-flagged files on demand (top-down) for complex repos.

STAGE 1 changes (SKILL.md only — no behavior for small/normal repos):
- 1.1b: depth = max(count_tier, loc_tier). New LOC axis (wc -l over the
  already-sorted source_list; thresholds 40k/250k). max() of two closed-range
  total orders is deterministic and monotone (can only read MORE
  conservatively). A 400-file/200k-LOC repo now tiers DEEP -> SHALLOW.
- Expand MUST_SCAN dirs to the READ-DEPENDENT service surface
  (services/jobs/workers/tasks/lib/domain/usecases/processors/core) so
  error-handling/auth/perf still fully read handler+service bodies even when
  the rest is sampled — the guardrail that makes tiering-down safe.
- Read set redefined (holds at every depth): MUST_SCAN UNION grep-hits UNION
  a depth-scaled deterministic sample of the remainder. Grep-hit files are
  promoted into the full-read set at every depth (evidence-driven / on-demand
  "top-down" read); must + hits are NEVER sampled out.
- Sample floors: DEEP all / SHALLOW 1-in-5 / MINIMAL 1-in-20 (was zero) —
  MINIMAL no longer reads the tail blind.
- Transparency: coverage line + "Partial coverage" warning whenever below
  DEEP. Determinism + cache intact (scan_depth already gates the key; hits
  are a pure function of content the WT fingerprint already covers).

Vetted by an adversarial workflow: the naive "MUST_SCAN + grep-hit only"
version would silently drop read-dependent confirmed fatals (swallowed
catch, IDOR dataflow) living in service/worker files — hence guardrail (2)
and the "never sample below the flagged set" invariant. Bash logic
validated end-to-end on a fixture repo; complexity tiering verified across
6 (count, LOC) cases; render-report.test.mjs 17/17 still pass.

Refs #33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…urity filenames)

Live before/after test on SillyTavern (358 files / 193k LOC, DEEP→SHALLOW)
caught two holes the file-list math + design review missed:

1. Backend routes in `src/endpoints/` were NOT in MUST_SCAN — only 10 of 47
   route files were read (rest sampled). `endpoints` wasn't in the dir regex.
   Added endpoints/resolvers/graphql/rpc/views/functions (Django views, GraphQL
   resolvers, serverless functions, the endpoints convention). Now 47/47 read.
2. Security files named by FUNCTION at the top of src/ (recover-password.js,
   users.js, private-request-filter.js [SSRF], request-proxy.js) were dropped —
   dir matching misses them. Added a security-noun BASENAME rule to MUST_SCAN.

After both fixes on SillyTavern: 367→203 files read (45% fewer, still ~half),
with 0 grep-hit files dropped and 0 route/read-dependent-dir files dropped —
every backend route, every security-named file, and every grep-flagged file is
fully read; only util/parser/vector-provider/frontend-UI boilerplate is sampled.

This is why the live test mattered: the design was sound but the concrete
dir/filename lists had real holes only a real repo exposed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@serenakeyitan

Copy link
Copy Markdown
Contributor Author

Live before/after verification — SillyTavern (358 files / 193k LOC, real complex app)

Ran the actual old-vs-new read-set computation on a real 3rd-party app that tiers DEEP → SHALLOW. This surfaced two real coverage gaps the design/file-math missed (now fixed in ca13d75):

gap found live fix
src/endpoints/*.js — only 10 of 47 backend routes read (endpoints wasn't in the MUST_SCAN dir regex) added endpoints/resolvers/graphql/rpc/views/functions47/47 read
security files named by function at top of src/ (recover-password.js, users.js, private-request-filter.js [SSRF], request-proxy.js) were sampled out added a security-noun basename rule → all promoted

After both fixes:

metric result
Files the agent reads (≈ runtime) 367 → 203 = 45% fewer (still ~halved)
grep-hit files dropped 0
route / read-dependent-dir files dropped 0
backend src/endpoints coverage 47/47
what IS dropped frontend public/scripts/* UI + tests/ + pure util/parser/vector-provider files

So on a genuinely complex repo the optimization reads ~half the files, and every backend route, every security-named file, and every mechanically-flagged file is still fully read — only boilerplate/UI is sampled. The disclosed residual (a swallow-and-continue in a hit-free non-security util file) is the same class SHALLOW already accepts, now warned via the partial-coverage line.

The report's arch summary must state what a project IS (stack, deploy shape),
which lives in README/docs — but .md was entirely outside the file universe
(not in EXT_RE). And for a prompt-native project (tdoc, Claude Code skills)
the real logic + risk lives in SKILL.md / *.prompt.md / .claude/ — a prompt
file can carry a hardcoded key OR an embedded injection, yet was never scanned.

Changes (all in MUST_SCAN — docs/prompts are read at EVERY depth; EXT_RE is
deliberately untouched so md never enters count/LOC/complexity-tier/sample —
a big docs/ or CHANGELOG must not inflate LOC and tier the scan DOWN):
- MUST_SCAN grep line 4: project-doc basenames (README/ARCHITECTURE/CONTRIBUTING/
  SECURITY/AGENTS/CLAUDE + docs/ architecture/deploy/design/setup files), off
  full_list.txt like manifests. Tight allow-list — CHANGELOG/generated/blog excluded.
- MUST_SCAN grep line 5: prompt/skill/agent files (SKILL.md, *.prompt.md,
  .claude/, agents/, prompts/, .cursorrules, copilot-instructions) as ATTACK
  SURFACE.
- secrets.md: prompt/skill/config files are secret-finding surface — a hardcoded
  key in SKILL.md is as fatal as one in db.ts.
- ai-integration.md: now ALSO applies to prompt-native repos with no SDK call —
  checks the prompt content for embedded injection / guardrail-override /
  dangerous-sink instructions.
- Doc/prompt read cap: first 2000 lines (a generated 10k-line README can't blow
  up the read); 10 MiB byte cap still the outer backstop.
- Coverage numerator X filtered to EXT_RE so read docs don't corrupt the
  "X of Y source files" ratio.

Validated on tdoc: README/SECURITY/CONTRIBUTING/docs-design + SKILL.md (x2)
captured; CHANGELOG/generated/blog/LICENSE correctly excluded.
render-report.test.mjs 17/17 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@serenakeyitan

Copy link
Copy Markdown
Contributor Author

Scope addition — read project docs + scan prompt/skill files as attack surface

.md was entirely outside the scan's file universe (not in EXT_RE), which broke two things:

  1. The arch summary must state what a project IS (stack, deploy shape) — that lives in README/docs, not code.
  2. For a prompt-native project (tdoc, Claude Code skills) the real logic + risk lives in SKILL.md / *.prompt.md / .claude/ — a prompt file can carry a hardcoded key OR an embedded injection, yet was never scanned.

Design (matches the complexity-gate philosophy — read via MUST_SCAN, NOT via EXT_RE):

  • MUST_SCAN grep line 4: project-doc basenames (README/ARCHITECTURE/CONTRIBUTING/SECURITY/AGENTS/CLAUDE + docs/ architecture/deploy/design/setup). Tight allow-list — CHANGELOG/generated/blog excluded.
  • MUST_SCAN grep line 5: prompt/skill/agent files as attack surface — read AND scanned.
  • secrets.md: prompt/config files are a secret-finding surface (a key in SKILL.md == a key in db.ts).
  • ai-integration.md: now also applies to prompt-native repos with no SDK call — checks prompt content for embedded injection / guardrail-override / dangerous-sink instructions.
  • Kept OUT of EXT_RE on purpose → docs/prompts never enter count/LOC/complexity-tier/sample (a big docs/ must not inflate LOC and tier the scan down, reading less code).
  • Doc/prompt read cap: 2000 lines (a generated 10k-line README can't blow up the read).
  • Coverage numerator X filtered to EXT_RE so read docs don't corrupt the "X of Y source files" ratio.

Validated on tdoc: README/SECURITY/CONTRIBUTING/docs-DESIGN + SKILL.md (×2) captured; CHANGELOG/generated/blog/LICENSE correctly excluded. render-report.test.mjs 17/17.

Measured DEEP-vs-SHALLOW read counts on 8 real repos: a ~30k-LOC project
like first-tree (134 files) was still fully read under the 40k threshold but
would save ~77% of file-reads if tiered to SHALLOW (few MUST_SCAN, many
sample-able leaves). 25k pulls the 25k–40k band (first-tree) into the fast
path while keeping genuinely small projects fully read (tdoc 9k, ellie 20k
stay DEEP — full read of a few dozen files is already fast, zero miss risk).
Kept ≥20k floor: below that, sampling's coverage (MUST_SCAN) is a smaller
share so the finding-miss risk rises faster than the time saved.

loc_tier: LOC ≤ 25000 → 0 (was 40000) · ≤ 250000 → 1 · else → 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@serenakeyitan
serenakeyitan deleted the feat/scan-complexity-gate branch July 9, 2026 01:36
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