Composed website-content ingester: fetch (infrastructure-fetcher -- SSRF-hardened, rate-limited, sha256-deduped, preserve-before-parse) -> decode/extract/chunk (infrastructure-html-text) -> transactional index (infrastructure-knowledge-index), with stored-artifact identity in the report for replay/re-extract without refetch. Non-goals (v1): crawling/link-following, JS rendering, robots.txt policy, PDF/media ingestion -- HTML only.
| Tier | infrastructure |
| Role | unclassified (baselined) |
| Path | crates/infrastructure/web-ingest |
| Edition | 2021 |
| Targets | infrastructure_web_ingest, e2e_db, pipeline_offline, support |
| Public items | 10 across 2 modules |
| Tests | 21 |
What it is for
infrastructure-web-ingest -- composed website-content ingester.
Thin orchestration over five existing layer crates: infrastructure-fetcher (fetch, SSRF-hardened, rate-limited, sha256-deduped, content-addressed storage), infrastructure-html-text (decode/extract/chunk), and infrastructure-knowledge-index (transactional index write). See pipeline::ingest_url for the composed entry point and docs/userstories/sprint-3.24-web-ingest.md for the full design + Gate 1.5 consensus log.
Non-goals (v1)
is a different mechanism (frontier, politeness, dedup-at-scale) layered ON TOP of this, not inside it.
- JS rendering.
infrastructure-fetcher'sBridgeExecutorexists for
sites that need a real browser, but this crate does not select it -- FetchHints::html_page()'s ExecutorPreference::Auto may still promote to it on a bot-challenge retry, but no code here targets rendering.
- `robots.txt` policy. This is an operator-directed single fetch
- Classification / keyword extraction. Extension seams for later, not
v1 scope.
- Scheduled refresh. Every call is a fresh, explicit operator action.
- PDF / non-HTML media.
text/htmlandapplication/xhtml+xmlonly
-- anything else is WebIngestError::NotHtml.
Concurrency contract
Two concurrent ingest_url calls for the SAME (tenant, corpus, source_ref) are last-write-wins: IndexService::index_doc's per-doc advisory lock (keyed by (tenant, corpus, source_ref)) plus its single transaction guarantee no torn write -- one call's full doc+chunks commit atomically before or after the other's, never interleaved. Neither call is told it lost the race; whichever committed last is what search sees until the next ingest. This is a deliberate v1 simplification (Gate 1.5 round 2, unanimous): a fetch-generation recheck is DEFERRED until a scheduled/concurrent ingester actually exists to need it. Self-healing: the next ingest_url call re-establishes the current state regardless of what a prior race left behind.
Capabilities
pipeline (other)
The composed pipeline: fetch -> decode/extract/chunk -> index.
| Item |
|---|
pub const SOURCE_KIND_WEBPAGE: & str |
pub const DEFAULT_BYTE_CAP: u64 |
async fn ingest_url <E : KnowledgeEmbedder + ? Sized>(fetcher : & Fetcher, storage : & dyn StorageBackend, pool : & PgPool, embedder : & E, cfg : & WebIngestConfig, url : & Url,) -> Result <WebIngestReport, WebIngestError> |
WebIngestConfig
The composed pipeline: fetch -> decode/extract/chunk -> index.
| Item |
|---|
pub struct WebIngestConfig |
WebIngestConfig :: fn default() -> Self |
WebIngestError
The composed pipeline: fetch -> decode/extract/chunk -> index.
| Item |
|---|
pub enum WebIngestError |
WebIngestReport
The composed pipeline: fetch -> decode/extract/chunk -> index.
| Item |
|---|
pub struct WebIngestReport |
TitleSource
Naming a fetched document (B-079).
| Item |
|---|
pub enum TitleSource |
TitleSource :: const fn as_str(self) -> & 'static str |
fn resolve_title(title : Option <& str>, og_title : Option <& str>, first_h1 : Option <& str>, final_url : & Url,) ->(String, TitleSource) |
How to use it
No examples/ target and no doctest in this crate's rustdoc. The tests listed under Verification are the closest executable usage.
Module structure
infrastructure_web_ingest
pipelinetitle
flowchart TD n_infrastructure_web_ingest["infrastructure_web_ingest"] n_infrastructure_web_ingest --> n_pipeline["pipeline"] n_infrastructure_web_ingest --> n_title["title"]
Public surface
`pipeline`
| Item | What it is |
|---|---|
pub const SOURCE_KIND_WEBPAGE: & str | The source_kind stamped on every doc this pipeline indexes. |
pub const DEFAULT_BYTE_CAP: u64 | Default page byte cap (Gate 1.5.6 input limits): 8 MiB |
pub struct WebIngestConfig | Pipeline configuration |
WebIngestConfig :: fn default() -> Self | — |
pub enum WebIngestError | Ingest-pipeline failures |
pub struct WebIngestReport | What one ingest_url call did |
async fn ingest_url <E : KnowledgeEmbedder + ? Sized>(fetcher : & Fetcher, storage : & dyn StorageBackend, pool : & PgPool, embedder : & E, cfg : & WebIngestConfig, url : & Url,) -> Result <WebIngestReport, WebIngestError> | Fetch, decode, extract, chunk, and index one URL |
`title`
| Item | What it is |
|---|---|
pub enum TitleSource | Which rule produced the stored title |
TitleSource :: const fn as_str(self) -> & 'static str | The snake_case wire name, for front_matter and CLI output. |
fn resolve_title(title : Option <& str>, og_title : Option <& str>, first_h1 : Option <& str>, final_url : & Url,) ->(String, TitleSource) | Choose the stored title: <title> -> og:title -> first <h1> -> URL segment |
Re-exports. Exported here, defined elsewhere.
| Export | Defined in |
|---|---|
{ingest_url,WebIngestConfig,WebIngestError,WebIngestReport,DEFAULT_BYTE_CAP,SOURCE_KIND_WEBPAGE,} | pipeline::{ingest_url,WebIngestConfig,WebIngestError,WebIngestReport,DEFAULT_BYTE_CAP,SOURCE_KIND_WEBPAGE,} |
{resolve_title,TitleSource} | title::{resolve_title,TitleSource} |
Boundary
Depends on no other workspace tier.
Shares tier infrastructure with 82 other crates: infrastructure-acquire, infrastructure-adapters-google-calendar, infrastructure-adapters-google-gmail, infrastructure-adapters-google-places, infrastructure-adapters-google-trends, infrastructure-adapters-shodan, infrastructure-adapters-yelp, infrastructure-agent, … (82 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | infrastructure |
| Architectural role (taxonomy) | unclassified (baselined) |
| Location | crates/infrastructure/web-ingest |
| Vocabulary in force (lexicon) | current |
Dependencies
Runtime, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `infrastructure-embeddings` | infrastructure | no | always |
| `infrastructure-fetcher` | infrastructure | no | always |
| `infrastructure-html-text` | infrastructure | no | always |
| `infrastructure-knowledge-index` | infrastructure | no | always |
| `infrastructure-storage` | infrastructure | no | always |
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
chrono | ^0.4 | serde | no | always |
mime | ^0.3 | — | no | always |
serde | ^1 | derive, derive | no | always |
serde_json | ^1 | — | no | always |
sqlx | ^0.8 | runtime-tokio, postgres, chrono, uuid, json | no | always |
thiserror | ^2 | — | no | always |
url | ^2 | — | no | always |
uuid | ^1 | v4, v7, serde, js, v4, serde | no | always |
Development, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `infrastructure-fetcher` | infrastructure | no | always |
| `operations-rate-limit` | operations | no | always |
Development, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
async-trait | ^0.1 | — | no | always |
flate2 | ^1 | — | no | always |
http | ^1 | — | no | always |
tempfile | ^3 | — | no | always |
tokio | ^1 | full, macros, rt-multi-thread | no | always |
wiremock | ^0.6 | — | no | always |
Build. None.
Depended on by. 1 workspace crate.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR n_application_wiki["application-wiki"] -->|uses| SELF SELF["infrastructure-web-ingest"] SELF -->|development| n_infrastructure_fetcher["infrastructure-fetcher"] SELF -->|development| n_operations_rate_limit["operations-rate-limit"] SELF -->|runtime| n_infrastructure_embeddings["infrastructure-embeddings"] SELF -->|runtime| n_infrastructure_fetcher["infrastructure-fetcher"] SELF -->|runtime| n_infrastructure_html_text["infrastructure-html-text"] SELF -->|runtime| n_infrastructure_knowledge_index["infrastructure-knowledge-index"] SELF -->|runtime| n_infrastructure_storage["infrastructure-storage"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| lib | infrastructure_web_ingest | `src/lib.rs` |
| test | e2e_db | `tests/e2e_db.rs` |
| test | pipeline_offline | `tests/pipeline_offline.rs` |
| test | support | `tests/support.rs` |
Error model
| Error type | Named by |
|---|---|
WebIngestError | ingest_url |
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | yes |
| async runtime | none detected |
| database access | yes |
| network I/O | none detected |
| unsafe code | none detected |
| environment variables | none detected |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
No environment variable is read with a literal name anywhere in this crate. A variable whose key is computed at run time cannot be listed here, and is not claimed to be absent.
Related capabilities
1 workspace crate depends on this one: application-wiki.
Verification
| Kind | Count |
|---|---|
| Unit tests | 13 |
| Integration tests | 8 |
| Examples | 0 |
| Doctests | 0 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
pipeline | 6 | 0 | 5 |
title | 2 | 0 | 0 |
What the tests establish, by name:
happy_path_then_dedup_writes_nothing_new—tests/e2e_db.rssource_ref_and_source_kind_match_the_federate_ledger_precedent—tests/e2e_db.rsbyte_cap_applies_to_wire_bytes_not_decompressed_bytes—tests/pipeline_offline.rscap_exceeded_surfaces_as_a_fetch_error—tests/pipeline_offline.rsempty_extraction_when_the_page_is_all_chrome—tests/pipeline_offline.rsnon_html_content_type_is_rejected_before_storage—tests/pipeline_offline.rsreport_serde_round_trips_via_a_real_happy_path_shape—tests/pipeline_offline.rsssrf_blocked_host_surfaces_the_underlying_error—tests/pipeline_offline.rsdefault_config_matches_documented_defaults—src/pipeline.rsis_acceptable_html_mime_accepts_html_and_xhtml_only—src/pipeline.rsreport_round_trips_through_json—src/pipeline.rsstrip_identity_is_a_no_op_for_a_clean_url—src/pipeline.rsstrip_identity_removes_userinfo_and_fragment_keeps_query—src/pipeline.rsblank_inputs_are_treated_as_absent—src/title.rsfirst_h1_is_third—src/title.rshost_names_a_root_document—src/title.rsog_title_is_second—src/title.rstitle_element_wins_when_present—src/title.rstitle_source_serializes_snake_case_and_matches_as_str—src/title.rstrailing_slash_and_query_do_not_hide_the_segment—src/title.rsurl_last_path_segment_is_the_final_fallback—src/title.rs
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 9 | 10 |
Public modules with a //! block | 2 | 2 |
pie showData
title Public items with rustdoc
"Documented" : 9
"No rustdoc detected" : 1
Metrics
| Metric | Value |
|---|---|
| Rust source files | 3 |
| Source lines | 671 |
| Code lines | 390 |
| Public API items | 10 |
| Public modules | 2 |
| Tests | 21 |
| Examples | 0 |
| Cargo features | 0 |
| Direct runtime dependencies | 13 |
| Workspace reverse dependencies | 1 |
pie showData
title Public API by kind
"constant" : 2
"enum" : 2
"function" : 2
"method" : 2
"struct" : 2
pie showData
title Rust source composition
"Code" : 390
"Blank or comment" : 281
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.