Skip to content

Crawler split issue 30 - #33

Draft
nikitakarpei wants to merge 15 commits into
mainfrom
crawler-split-issue-30
Draft

Crawler split issue 30#33
nikitakarpei wants to merge 15 commits into
mainfrom
crawler-split-issue-30

Conversation

@nikitakarpei

Copy link
Copy Markdown
Owner

No description provided.

nikitakarpei and others added 15 commits August 21, 2026 16:37
Phase A of the crawler split (#30): contracts only, no behavior change
for existing consumers.

Move fetching, extraction, the content format graph, and page derivations
out of yacycrawler into libraries/pagescrape, with their tests and ADRs
0005, 0008, 0009 and 0010, renumbered 0001-0004 in the new module. The
crawler consumes the library; crawl/fetchedpage and the pagevisit fetcher
port become pagescrape/pagefetch.

Add ReachedPage, CRAWL_REACHED_PAGES and crawl.reachedpage to the crawl
contract. The crawler creates the stream and publishes every page it
fetched and did not dispose, alongside today's representation streams.
Nothing downstream reads it yet. ReachedPage carries only CanonicalURL:
the OrderID the plan named has no consumer and the subject is unrouted.

Two units come out of pagevisit, which had grown six collaborators:
fetchtiming, a pagefetch.Fetcher decorator that times the fetch, shaped
like redirectrecording; and reachedpagepublication, which canonicalizes
and publishes a reached page, shaped like disposal.Disposer. Unlike
Disposer it returns a failed publish instead of warning, because the
reached page is the crawler's primary output from here on.

The crawler e2e suite gains a case asserting a reached page arrives on
the new stream for a crawled fixture site. Its two stream-wait helpers,
which used different mechanisms and different timeouts, collapse into one.

Fix make arch-diagram: for_each_go moved to xargs -I{} but the recipe
still named $m, so every module overwrote arch-diagrams/.svg.

Arch-lint rules dropped or widened, in services/yacycrawler:

- new components fetchtiming, reachedpagepublication, reachedpagesjetstream
- new vendors pagescrapepagefetch, pagescrapecontentextraction and
  pagescrapecontentformatgraph, replacing readability, htmltomarkdown
  and xnet, which leave with the code
- pagevisit mayDependOn reachedpagepublication, canUse pagescrapepagefetch
- reachedpagepublication canUse canonicalurl; pagevisit no longer does
- reachedpagesjetstream canUse nats
- fetchtiming, recrawlrulesalwaysdue, recrawlrulesdueaftergrace and
  redirectrecording canUse pagescrapepagefetch
- pageabsorption canUse pagescrapecontentextraction, pagescrapepagefetch
- pagepublication, pagerepresentations{rwi,text,markdown} and
  representationpublishersjetstream canUse pagescrapecontentformatgraph
- progressobserversprometheus mayDependOn fetchtiming and
  reachedpagepublication
- commonComponents drops contentformatgraph; redirectrecording drops
  mayDependOn pagevisit

libraries/pagescrape gets its own ruleset, carrying the component edges
and the readability, htmltomarkdown and xnet vendors from the crawler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
corpusmarkdown consumes reached pages and derives markdown for itself
through pagescrape, so the crawler no longer publishes a markdown
representation. Plan phase B step 3.

Container extraction goes with it, in the same commit because the two
changes share pagescrape's scraper and the markdown intake loop; no
intermediate state of the two apart is green.

A reached page names one URL, but a container held many documents under
synthetic member URLs that no client can fetch. Keeping it would make
every consumer re-expand the same archive to rebuild the same
unaddressable corpus keys. Only HTML has a registered extractor, so an
archive had to hold HTML to yield anything at all.

One fetch now yields at most one document. contentextraction keeps its
media-type router and returns that document; the ContainerExpander port,
ContainerMember, the archive expander, and the nesting, document, and
member bounds are gone, as are the nesting-too-deep and
document-budget-exhausted disposal reasons that nothing can raise any
more. ExtractedContent and ExtractedDocument merge, pagescrape.Scrape
returns one page and whether it scraped one, and pageabsorption loses
its per-document fan-in.

pagescrape ADR 5 records the removal and supersedes the container half
of ADR 3; the routing half stands.

Arch-lint allows more in two places: pagescrape declares a root
component that may depend on every extraction and derivation component
and use canonicalurl, and corpusmarkdown adds the pagescrape,
pagescrape/contentformatgraph, and pagescrape/pagefetchers/http vendors
with markdownintake allowed to use pagescrape. It allows less by
dropping the containerexpandersarchive and pagerepresentationsmarkdown
components.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
corpustext consumes the crawler's reached pages, fetches each one through
the egress proxy, and derives readable text with pagescrape. The crawler's
text representation, PageTextRepresentation, and PageReference are deleted.

Both search indexes now take a searchdocument.Document, so they no longer
depend on the crawl contract; the intake maps the scraped page into the
document and stamps the time it fetched.

corpusrecall and firecrawlshim e2e still configured corpusmarkdown for the
retired crawled-page subject and never set the now-required proxy URL. Both
are fixed here.

Arch-lint widenings: corpustext gains the pagescrape, contentformatgraph,
and pagefetchers/http vendors, the pageintake -> scrapedpagedocument edge,
and pageintake's use of pagescrape and searchdocument. Narrowings: the
crawledpagedocument component becomes scrapedpagedocument, the search-index
components drop yacycrawlcontract, and yacycrawler drops
pagerepresentationstext.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
yacynode consumes the crawler's reached pages, fetches each one through the
egress proxy, and derives its reverse word index with pagescrape. The chunk
reassembly in crawlresults is deleted: with one message per reached page
there is nothing to reassemble, and the tokenizer and index builder move in
from the crawler as pagerwi.

The intake splits a page's postings by the same cap that configures posting
admission, so batching moves off the wire and into the consumer.

With RWI gone, the crawler publishes only the URL of every page it reached.
Deleted: pagepublication, pagerepresentations, representationpublishers, the
representation catalog and its YACYCRAWLER_PUBLISH_* / NATS_PAGE_* config,
the publication metrics, and PageRepresentationKind, PageRWIRepresentation,
PageRWIChunk, and CrawledPageStreamName/Subject from the contract.
pageabsorption now extracts, decides disposal, and returns discovered links.

The crawler's spec kept all-or-nothing publication requirements that the
last representation leaving made false, so they are rewritten here rather
than at the planned later step. ADR 0012 records its supersession in its
Status section and is otherwise left historical.

Arch-lint widenings: yacynode gains the pagescrape vendor, the poisonhalt
and pullintake serviceruntime paths, the pagerwi and reachedpageintake
components, and the reachedpageintake -> pagerwi edge. Narrowings:
crawlresults is gone, crawlbroker drops its project dependency, and
yacycrawler drops the pagepublication, pagerepresentationsrwi, and
representationpublishersjetstream components and the yacymodel vendor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
The node stored a reached page's postings in batches sized by the
admission cap, and derived URLMetadata.ByteSize from the text it
tokenized rather than from the document the origin served.

ScrapedPage now carries DocumentByteSize, taken from the fetched body,
so pagerwi reports the served document size while still building the
index from derived full text. corpustext and corpusmarkdown gain the
same fact.

reachedpageintake makes one urls.Receive and one postings.Receive per
page; postingBatchesOf, storeBatch, and PostingBatchCap are deleted.

That moves the too-large refusal. It lived in rwiadmission.Receive, the
receiver both the local intake and remote DHT peers pass through, so
leaving it there would refuse any page with more than 1000 distinct
words. It now sits in rwiingress, the endpoint an untrusted peer request
arrives at, which is the only caller it ever bounded. rwiadmission loses
Config.BatchCap, Receipt.TooLarge, and the branch; rwiingress.Mount
takes Config{PostingCap, Pause, Refusals} and answers ResultBusy with
the pause. The request_too_large metric label is unchanged, reported
through the same rwiadmission.RefusalObserver. node.go renames
postingAdmissionBatchCapacity to peerPostingTransferCapacity, its one
remaining reader.

Receipt.TooLarge had no reader left once the intake stopped batching, so
it is deleted rather than left as a dead field.

No arch-lint rule changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
Structure comes from this branch: page representations, chunking, and the
crawled page document stay deleted, and pagescrape stays a library. Canonical
URL typing comes from main: a URL is a value type again, not a string.

The type moves to libraries/canonicalurl. Main declared it in the crawl
contract, but phase A moved link discovery into pagescrape, which three
services share, so a library cannot depend on one service's contract. The
crawler contract now consumes the library.

pagerwi.Of no longer returns an error. A CanonicalURL always parses, so the
failure it reported cannot happen; the tests that built an invalid URL go with
it.

Arch-lint allows more: canonicalurl becomes a common vendor in yacycrawler,
yacynode, corpusmarkdown, corpusrecall, and visitcrawl, and a single edge in
the two contract modules and in pagescrape/contentextraction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
corpusmarkdown gains the read API that corpusrecall reaches into its object
store for today. MarkdownCorpus/RecallPage answers the markdown held for one
URL, or NOT_FOUND; it neither fetches the page nor orders a crawl.

The contract lives in the module that already owns the markdown vocabulary,
services/corpusmarkdown/contract (pagemarkdownstore), rather than in a module
of its own: the proto names the same corpus the bucket and object names do.

The server canonicalizes the requested URL at the edge, so MarkdownOf takes a
canonicalurl.CanonicalURL and the response's canonical_url reports what the
corpus was actually asked for instead of echoing the request. A URL that
cannot be canonicalized — the empty one included — is InvalidArgument.

CORPUSMARKDOWN_LISTEN_ADDR defaults to :8094; firecrawlshim already owns :8093.

Arch-lint: the contract module gains the grpc and protobuf vendors and
mayDependOn itself, matching corpusrecall/contract; in the service,
markdownrecallreceiversgrpc uses the pagemarkdownstore vendor, which is wider
than the generated-package-only vendor it replaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
The crawler answers what crawling last did with a URL: where it resolved
to, and whether it is disposed. Callers read the two key-value buckets
directly today, which ties every reader to the crawler's storage layout
and to broker credentials. CrawlOutcomes/ReadPage replaces both reads,
and the buckets become private to the crawler again.

The contract lives in the module that already owns the crawl vocabulary,
services/yacycrawler/contract, rather than in a module of its own.

ReadPageRequest carries url, not the canonical_url the plan named, and
the server canonicalizes at the edge as the markdown corpus does. A
caller no longer needs the canonicalization library to ask a question,
and canonical_url in the answer reports what was asked for instead of
echoing it. A URL that cannot be canonicalized is InvalidArgument.
resolved_url always carries a URL, the canonical one itself when no
redirect was recorded, so no caller needs a branch for an empty field.

disposal.Disposer passes the reason to DisposedPages.Record, which
stores it as the entry value; the reason was discarded after the metric
until now. The disposal mark is the bucket revision, so a caller that
captures it before it places an order can tell a new disposal from an
earlier one.

Both bucket-owning units gained a read side, so both took the name of
what they own: Recorder became DisposedPages and RedirectResolutions,
and their tests now write and read through those surfaces instead of
reaching into the bucket.

YACYCRAWLER_LISTEN_ADDR defaults to :8095, the next address free of
renderproxy, visitcrawl, corpusrecall, firecrawlshim, and corpusmarkdown.

ADR 13 records the gRPC decision against core-NATS request-reply, and
ADR 14 the traffic arithmetic behind one message per reached page.

Arch-lint rules this widens: the contract module gains the grpc and
protobuf vendors; the service gains a grpc vendor and the
crawloutcomereceiversgrpc component, and both it and disposedpagesjetstream
may now depend on disposal, whose reason and mark they carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
firecrawlshim now owns the order-and-wait loop: it reads what crawling
last did with a URL over CrawlOutcomes/ReadPage, places a crawl order on
NATS, then polls MarkdownCorpus/RecallPage for the resolved URL until the
corpus answers, crawling disposes of the page, or the recall limit runs
out. It no longer depends on corpusrecallapi.

The scrape response loses title and language with the text
representation, which no read API serves.

Arch-lint gains the markdownrecall and crawlorderplacersjetstream
components with the canonicalurl, corpusmarkdownapi, crawlerapi,
jetstream, uuid, and yacycrawlcontract vendors; the corpusrecallapi
vendor is dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
firecrawlshim now owns the recall loop, and corpusmarkdown serves the
markdown read API, so nothing is left for corpusrecall to do. The service,
its contract module, its e2e suite, its image build workflow, and its
proto and e2e entries in the Makefile go with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
Absorption had one caller, always immediately after a successful fetch,
so the two were stages of one procedure rather than two units. What is
left of absorption since derivation moved out of the crawler is extract,
read off links and refusal, name a disposal — private detail of a visit.

pagevisit now owns IndexingRefusal and PageExtractor, and its visitor
absorbs the fetched page itself. AbsorberSource, Absorber, and
AbsorptionOutcome are gone; the extraction failure that Absorb declared
but never returned goes with them.

Arch-lint drops the pageabsorption component; pagevisit gains its
pagescrapecontentextraction vendor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxeJbGijz5jUp9fRaLJpqQ
A reached page was the crawler's report of what it did. What travels on
that stream is a request that any interested corpus scrape a URL, so the
crawler's vocabulary must not stay stamped on it.

libraries/scraperequestcontract now holds ScrapeRequestsStreamName,
ScrapeRequestSubject, ScrapeRequest and its marshallers. yacycrawlcontract
keeps only OrdersStreamName and the order marshallers. The vocabulary went
the whole path: packages scraperequests, scraperequestpublication and
scraperequestintake, the metric yacycrawler_scrape_requests_published_total,
the env vars NATS_SCRAPE_REQUEST_SUBJECT and NATS_SCRAPE_REQUEST_DURABLE,
the subject scrape.request, and the service docs.

The stream is LimitsPolicy with DiscardOld now. It was a work queue, which
permits only non-overlapping consumers and removes each message on first
ack, so with three corpora attached NATS rejected the overlapping durables
and a page reached one corpus rather than all three. A crawler e2e case
reads one scrape request through two durables. DiscardNew went with the
work queue: nothing frees space under LimitsPolicy, so the stream would
fill and stall every publisher for good.

Arch-lint widening: a new scraperequestcontract vendor in four modules,
granted to markdownintake, pageintake, crawlbroker, nodeconfiguration,
scraperequestintake, and the crawler's commonVendors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No service creates SCRAPE_REQUESTS any more. The crawler and the shim
publish to it, three corpora read it, and none of them owns its
retention. The crawler gains SCRAPE_REQUEST_NATS_URL beside
CRAWL_NATS_URL, which it keeps for the crawl orders it does own; the
corpora and the node rename their CRAWL_NATS_URL to the same name.

The deployment example provisions the stream from a nats-box init
service that every publisher and consumer waits for, and its README
carries the retention window an operator now sizes.

test/e2eharness/scraperequeststream stands in for that operator in
every suite that drives a scrape request, and replaces the divergent
copy the searxng plugin suite carried.

test/scraperequestfanout is a new suite that publishes one scrape
request to two corpora and asserts both absorb it: corpusmarkdown
stores the markdown object, corpustext indexes the text. It lives
beside the harness because no single service owns the claim. The
Makefile's e2e module glob now covers test/*/go.mod, and a suite may
name its own directory through E2E_SUITE_DIR_<name>.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The service placed a crawl order with MaxDepth 0 and one seed, then
polled CrawlOutcomes/ReadPage and RecallPage until the markdown
appeared. That order was never a crawl, and the waiting machine around
it observed the result of a fetch the service could have made itself.

Making it scrape for itself was tried first: derive the markdown
in-process through pagescrape, answer from that, and publish a scrape
request so the corpora still gain the page. The publish is a
fire-and-forget side effect of serving an HTTP call, owned by no unit
in the service. The Firecrawl API is worth having, so it comes back
with a design of its own, and a unit that owns announcing a page.

Nothing else in the repo depended on it. The module, its Dockerfile,
its e2e suite, its docs, its build workflow, and its go.work, CI, and
Makefile entries all go with it.

It was the only external reader of CrawlOutcomes/ReadPage, so the
crawler can now shed the outcome API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing outside the crawler read a crawl outcome once firecrawlshim
went, and both KV buckets became write-only: the gRPC receiver was the
sole reader of redirectresolvers and disposedpages.

Deleted: internal/crawloutcomereceivers/grpc, contract/crawler/v1,
contract/crawloutcomesclienttest, internal/redirectresolvers,
internal/disposedpages, the two bucket contracts, the ensure and open
calls for both buckets, the gRPC listener, and YACYCRAWLER_LISTEN_ADDR
with its port. grpc and protobuf leave both modules, and make proto no
longer generates crawler/v1.

internal/crawl/redirectrecording goes whole: it was confirmed to have
no caller inside crawl logic, so with its bucket gone it recorded for
nobody.

disposal.Disposer goes the same way. With the persistence removed it
only forwarded a reason to the metric, so ordertraversal counts a
disposal through its own progress observer and disposal holds only
Reason. TraversalProgress gains PageDisposed; DisposalProgress goes.

The spec loses the resolve-and-disposal-answering requirement. Its
known limitation about the consumer outage window is already stated in
the deployment example's README, which is where an operator sizes the
retention it describes.

ADR 0013 is deleted rather than superseded, at the user's request.

Arch-lint drops four components (redirectrecording,
redirectresolversjetstream, disposedpagesjetstream,
crawloutcomereceiversgrpc) and the grpc vendor in the service, and the
grpc and protobuf vendors in the contract. No rule widens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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