PROPOSAL-3: storage that streams, measured shape, concurrency, bounded reasoning - #27
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
go.devwas 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 ofPROPOSAL-3.mdto 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.searchis a generated column, soto_tsvectorruns during theINSERT. Postgres's 1 MB tsvector ceiling was therefore not an indexing limit but a storage limit - the row could not exist at all. Oneleft(content, 300_000)removes it (_upgrade_v4). The page is stored and served whole; the newsectiontable keeps the tail searchable, so bounding the index does not trade a visible failure for an invisible one.Phases
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 documentclassify_shape/magnitude;sectiontable; federation completeness becomes the headlineHOST_CONCURRENCY. 1.30s to 0.38s at 4 workers, same pages in the same orderBudget(calls=12), 4 decision points, cached per template/host, off by defaultThe 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 atreeCorpus.magnitude- always0, so every escalation read "size unknown"Federation.complete- tested, never read; the headline came from the entry corpusEach is now wired with a grep-the-source assertion. Two of those assertions caught real defects within minutes of being written.
Found while building
pagebranch would have stayed unreachable in exactly the case it exists for._upgrade_v4's guard looked forleft(, but Postgres renders the reserved word as"left"(- it would have rebuilt a table and a GIN index on every process start, silently._federaterendered its own coverage note that had drifted into claiming coverage described "only the corpus that was crawled", long after selection began harvesting the others.apiis not an intent; unknown intents fall back to permissive, so the first version of a test passed while proving nothing.sectiontable 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-adkharvest stored 1,799 pages, complete, and the web UI said "that did not go through". The Claude Code provider raised on anyis_errorresult, anderror_max_turnsdiscarded 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, missingharvest_docsitself - is now derived fromforge_tools.TOOLS.Design docs moved to
Project Development/,README.mdstaying at the root. Moved withgit 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_DBexample 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 againstadk.dev: the same corpus is available in 1 request (llms-full.txt, 3.45 MB), 229 requests (.mdtwins, 100% of the index's links), or the 1,799 HTML fetches a real harvest actually made.Not done
harvest_jobs._JOBSis in-memory). The pages survive; the report of them does not.The spec also asked that
ISSUES.mdgain 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 skippedwith Postgres againstDocsForgeTest;611 passed, 59 skippedwithout. 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_DBmust be set or the 37 Postgres tests skip silently and the run looks greener than it is.