Skip to content

PROPOSAL-3: storage that streams, measured shape, concurrency, bounded reasoning - #27

Merged
R204570 merged 4 commits into
mainfrom
proposal-3
Aug 31, 2026
Merged

PROPOSAL-3: storage that streams, measured shape, concurrency, bounded reasoning#27
R204570 merged 4 commits into
mainfrom
proposal-3

Conversation

@R204570

@R204570 R204570 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Why

go.dev was harvested for sixteen minutes, hit one 1.19 MB page, and stored none of the ~1,200 pages that had already extracted cleanly. This branch implements all four phases of PROPOSAL-3.md to make that impossible, then fixes what building it exposed.

The root cause was not what we assumed

We expected "large pages need splitting." It wasn't that.

page.search is a generated column, so to_tsvector runs during the INSERT. Postgres's 1 MB tsvector ceiling was therefore not an indexing limit but a storage limit - the row could not exist at all. One left(content, 300_000) removes it (_upgrade_v4). The page is stored and served whole; the new section table keeps the tail searchable, so bounding the index does not trade a visible failure for an invisible one.

Phases

Phase Change
1 - streaming storage store().writer() in both backends, save() delegates to it. Per page, not the batches of 50 the proposal named - a batch of 50 contradicts Invariant 16 in the same document
2 - shape + bounded index Probe to classify_shape / magnitude; section table; federation completeness becomes the headline
3 - concurrency Prefetch window, per-host pacing, HOST_CONCURRENCY. 1.30s to 0.38s at 4 workers, same pages in the same order
4 - bounded reasoning Budget(calls=12), 4 decision points, cached per template/host, off by default

The wiring debt

PROPOSAL-II shipped features that were built, tested, and never called. All had passing tests - passing tests proved behaviour, not reachability.

  • classify_shape - zero callers, so every corpus was a tree
  • Corpus.magnitude - always 0, so every escalation read "size unknown"
  • Federation.complete - tested, never read; the headline came from the entry corpus
  • the entry corpus could be reported "not requested" despite being stored

Each is now wired with a grep-the-source assertion. Two of those assertions caught real defects within minutes of being written.

Found while building

  • A corpus was compared against a median it was itself in - so the one giant document could never be six times it, and the page branch would have stayed unreachable in exactly the case it exists for.
  • _upgrade_v4's guard looked for left(, but Postgres renders the reserved word as "left"( - it would have rebuilt a table and a GIN index on every process start, silently.
  • _federate rendered its own coverage note that had drifted into claiming coverage described "only the corpus that was crawled", long after selection began harvesting the others.
  • api is not an intent; unknown intents fall back to permissive, so the first version of a test passed while proving nothing.
  • The section table went a whole phase populated with nothing reading it - the exact defect this branch exists to fix. search() now unions both indexes.

Also in this branch

A finished harvest was reported as a failure. A google-adk harvest stored 1,799 pages, complete, and the web UI said "that did not go through". The Claude Code provider raised on any is_error result, and error_max_turns discarded everything already streamed. Running out of turns is not failing - the store is the proof, since a page is durable before the next is stored. Now a notice; every other error still raises. The turn cap went 12 to 24, and the tool allowlist - drifted to 3 of 14, missing harvest_docs itself - is now derived from forge_tools.TOOLS.

Design docs moved to Project Development/, README.md staying at the root. Moved with git mv, so history follows.

The README was corrected against the source. It had drifted into contradicting itself - "Serving a purpose" described federation working while "Known limits" still called single-corpus harvesting the most serious known defect. Also fixed a real footgun: the DOCSFORGE_TEST_DB example pointed at the database holding real harvests, directly under a comment promising tests could never touch them.

Project Development/llmsfinder.md - a new proposal, not implemented here. Measured against adk.dev: the same corpus is available in 1 request (llms-full.txt, 3.45 MB), 229 requests (.md twins, 100% of the index's links), or the 1,799 HTML fetches a real harvest actually made.

Not done

  • P1 - the spec wants an interrupted harvest to leave 60% readable. Only "previous version intact" is true. This is a contradiction inside the spec: blue/green invisibility is what Invariant 17 requires. Needs a human decision.
  • P2 - job progress does not survive a process kill (harvest_jobs._JOBS is in-memory). The pages survive; the report of them does not.
  • R1 is still open - the identity gate confirms the name, not the project. Reasoning can veto a wrong admission but is off by default, so the default path is unchanged. Still the top defect.

The spec also asked that ISSUES.md gain no entries. That is broken deliberately - a criterion met by not recording a known gap is met dishonestly. It closed 12 and opened 2.

Testing

648 passed, 22 skipped with Postgres against DocsForgeTest; 611 passed, 59 skipped without. Phase 4's criterion - "with reasoning off, behaves exactly as Phase 3" - holds checkably: all 611 Phase-3 tests pass unchanged, none mentions reasoning.

CI note: DOCSFORGE_TEST_DB must be set or the 37 Postgres tests skip silently and the run looks greener than it is.

R204570 and others added 4 commits August 31, 2026 14:51
Ten markdown files sat at the repository root alongside the code: three
proposals, an audit, two findings write-ups, the issue list, and the design,
product and suggestions notes. They are the record of how the project was
reasoned about, not part of its surface, and at root they made it harder to see
what the repository actually ships.

README.md stays at the root, because GitHub renders it as the repository
landing page and moving it would leave the project with no front door.

Moved with `git mv`, so `git log --follow` still reaches each file's history.
No code opens any of these paths — every reference in the Python sources is a
prose mention in a docstring (`ISSUES.md` R1 and similar), which reads the same
wherever the file lives. PROPOSAL.md's link to AUDIT.md is relative and both
moved together, so it still resolves. README's three mentions are updated.

643 passed, 22 skipped, unchanged by the move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQKNEFbskMmKsBnCbVeWCq
A tool whose pitch is calibrated confidence cannot be selective about its own,
and the README had drifted badly enough to contradict itself: "Serving a
purpose" described federation working while "Known limits" still called
harvesting one corpus per technology the most serious known defect.

Corrected claims, each checked against the source rather than from memory:

- "Nothing checks HTTP status before storing a page" was simply false —
  Fetcher.html raises on 4xx/5xx. The real remaining gap is the soft 404, a
  genuine 200 whose body reads "Page not found", which is what decision point 4
  exists for. Rewritten to say that.
- "An unrecognised page template can store navigation as documentation ... falls
  through to the page body" is inverted, not fixed: pick_main now refuses such a
  page rather than storing its navigation. That is the right default and it
  costs a real page whenever an API reference is mostly method links, so the
  limit is restated in the direction it now points.
- "A technology documented across several sites is harvested as one" is gone.
  Federation discovers, admits, classifies and selects corpora, and the coverage
  roll-up is the headline.
- "PROPOSAL-II.md is the plan for the first two. None of it is built yet" has
  been false since 24 August. Both proposals are implemented; ISSUES.md is now
  named as the live list of what is still open.

Added: an interrupted harvest leaving nothing readable, which is a deliberate
choice rather than an oversight and worth stating as one.

A real footgun in the Tests section: the DOCSFORGE_TEST_DB example pointed at a
database named DocsForge — the one holding real harvests — directly under a
comment promising tests "can never touch real harvests". These tests create and
drop technologies. Now DocsForgeTest, with the reason spelled out.

Test counts were stale (536) and my first correction was wrong arithmetic:
offline is 606 passed / 59 skipped, so 37 tests are Postgres-gated, not 22. The
22 that skip either way need network or API keys. Verified by running both ways.

Also documents DOCSFORGE_REASONING, which had no entry at all, and adds the two
user-visible properties from PROPOSAL-3: durability per page, and concurrent
fetching under a per-host cap.

643 passed, 22 skipped with Postgres; 606 passed, 59 skipped without.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQKNEFbskMmKsBnCbVeWCq
A google-adk harvest through the Claude Code provider stored 1,799 pages,
complete, expected matching stored — and the web UI said "that did not go
through". Three defects, one visible symptom.

The turn cap was treated as a failure. When the CLI stops at --max-turns it
emits a result event with is_error set and subtype error_max_turns, and the
provider raised on any is_error. Raising discards everything already streamed:
every tool call, every result, and the entire successful harvest. Running out
of room to talk is not the same as failing, and the store is the proof — a page
is durable before the next is stored, so the pages outlived the conversation
that fetched them. error_max_turns is now a notice saying what was stored and
that asking again will not re-crawl it. Every other error still raises, and a
test pins that so tolerating the cap does not become shrugging at everything.

The non-zero exit had to be tolerated with it. The CLI exits non-zero when it
stops at the cap, and the exit-code check at the end of stream() would have
raised anyway, undoing the fix entirely.

The cap was set for chatting, not harvesting. Learning a technology means
resolving the name, probing the entry point, harvesting, then reading back what
landed; the real run spent nine calls just finding the entry point. Twelve
turns to twenty-four. `timeout` is still the backstop on a runaway turn.

The tool allowlist had drifted to three of fourteen. learn_technology,
harvest_docs and every knowledge-base tool were missing from --allowedTools —
harmless while the CLI approves MCP servers it was handed a config for, and a
trap on any version that does not. It is now derived from forge_tools.TOOLS,
the same list mcp_server builds from, which also means it respects
DOCSFORGE_ALLOW_DELETE for free: with deletion off, forget_documentation is not
in TOOLS and cannot be pre-approved here.

The test for that allowlist asserted the drifted three were correct, which is
how a hardcoded list stays wrong. It now asserts derivation.

The new tests were checked by reverting the fix: three of five fail without it.
A test that passes either way is the defect this project keeps rediscovering.

648 passed, 22 skipped with Postgres; 611 passed, 59 skipped without.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQKNEFbskMmKsBnCbVeWCq
Every proposal so far has been about crawling better. This one is about the
case where crawling is the wrong verb, because the publisher already did the
work and put the answer at a known URL.

Measured against adk.dev on 31 August 2026: llms-full.txt is 3,451,848
characters, llms.txt is a 17 KB index of 229 links of which 229 end in .md, and
those .md twins return text/markdown. So the same corpus is available in one
request, or 229, or the 1,799 HTML fetches a real harvest of the same product
actually made. Doing 1,799 lossy conversions to reproduce a file that could
have been downloaded is the most expensive mistake the system currently makes.

The proposal is a priority ladder: full dump, then a Markdown manifest, then an
HTML manifest, then the generator manifest and sitemap and crawl that already
exist. Rungs 1, 4, 5 and 6 are built; the two llms.txt manifest rungs are not,
and they are the point.

Three findings worth recording separately from the request:

llms-full.txt is already preferred over llms.txt (_fuller_dump, DUMP_SIBLINGS),
so one of the three things asked for is done.

handle_llms_txt splits dumps over 60,000 characters for a reason its own
comment states — a single huge page ranks as one search hit. That reason was
retired by PROPOSAL-3 Phase 2: the index is bounded and the tail is indexed
section by section, so the artifact no longer has to be fragmented to be
searchable. Splitting is now a workaround for a limitation that no longer
exists, and it invents 4,000 URLs nobody can visit.

Storing whole has two consequences the proposal refuses to leave implicit,
because shipping the principle without them would be a regression wearing a
principle: search returns one row per page, so a one-page corpus returns one
hit however many sections matched; and read_knowledge_base caps at 60,000
characters, so reading back 3.45 MB silently omits 98% of it. Both are read-path
work, both are in Phase 1, and the second is exactly the undisclosed subset this
project exists to refuse.

Also states what it will not do: no model calls, no trusting llms.txt about
identity, no rewriting the dump, and no abandoning the crawl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQKNEFbskMmKsBnCbVeWCq
@R204570 R204570 self-assigned this Aug 31, 2026
@R204570
R204570 merged commit 7db6942 into main Aug 31, 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