Web Market Discovery — convergence and boundaries
Driftless had two information surfaces: the governed warehouse (supplier discovery, opportunity discovery, market history) and targeted Web Evidence, which verifies a claim about an entity someone already named. Neither could answer the question a commercial user actually asks first:Vendo estructuras metálicas. ¿Quién está construyendo plantas en Nuevo León?Nobody has named the organization yet. There is no tender, because the project is private. There is no award, because nothing was procured. The signal exists only as something a publisher printed. That is the gap
discover_web_market_signals
closes, and this document is the boundary around it.
The invariant this change is for
One implementation of the Parallel Search transport, shared by every capability that needs it. Before,radar/adapters/parallel.adapter.ts implemented both
DiscoveryProviderPort (FindAll — exhaustive discovery) and
EvidenceWebProviderPort (Search). It was registered in two Nest modules, and
Nest providers are module-scoped, so there were two objects and two circuit
breakers over one upstream: targeted verification could keep calling a provider
the Radar had already declared down, and vice versa.
The Search half was extracted, not rewritten:
research-providers/adapters/parallel-search.adapter.ts. The Radar’s FindAll
adapter keeps only the verbs that start a discovery run, and the Radar’s own
WebResearchGateway now injects the shared instance through
WEB_RESEARCH_PROVIDER instead of re-binding the class.
FindAll keeps its own breaker on purpose. A discovery run is minutes long and
priced per match; a search is one cheap synchronous call. One shared breaker
would let a discovery outage open the circuit on verification — availability
coupling between two capabilities that share nothing but a vendor.
What the two web capabilities share, and what they do not
Shared, in exactly one place each:
Separate, because the semantics are genuinely different:
Budget and stop conditions
Two successful provider calls per run, shared. Not two each. The run’sResearchWebSurface owns the ceiling; both capabilities reserve from it, and a
refused reservation is still recorded in the audit with its own platform id.
A discovery protocol spends at most:
- one discovery search, and
- one grouped verification call over the top ≤5 candidates.
- the brief is invalid → refuse before spending;
- the run’s web budget is spent → stop, audited;
- the provider failed → stop, audited, warehouse untouched;
- no candidate cleared extraction → stop with an honest zero;
target_resultsreached → verification covers the top slice;- verification finished → done.
WEB_SEARCH_MAX_USD_PER_CALL (0.05 USD), so cost is bounded — but the run does
not yet accumulate spend across calls. The extension point is
EvidenceWebUsage.costUsd, already returned by the adapter and already recorded
per attempt; accumulating it into a per-run monetary ceiling is a small change in
ResearchWebSurface and deliberately not made here rather than inventing an
accounting that nothing consumes.
Deletion ledger
What is NOT reachable from this capability
Enforced mechanically, not by review:- FindAll / exhaustive discovery — the Search class does not have the verbs;
research-providers.architecture.spec.tssweepsfindall,/v1beta,match_limit,createrun,streamrun,draftfrombriefacross the whole layer, adapters included. - Contact enrichment / people search — swept in the same spec plus
market-research.architecture.spec.ts; the brief refuses people vocabulary, and a planned query naming a person is dropped rather than repaired. - A second provider, a second HTTP surface, a second runner, a second synthesis — no controller, no entity, no repository in either layer, and the audit tool vocabulary is closed to the warehouse operations plus exactly two web capabilities.
- A model-authored URL, id or date — every identifying field is re-derived from the platform’s own ledger, and a candidate citing an id the platform never issued is dropped before it can be rendered.
Contracts
Verification, unchanged and backwards-compatible:additionalProperties: false. There is no provider, processor, mode, raw query
list, host allowlist, output schema, enrichment flag, people field, warehouse row
or SQL — every one of those is either a vendor concept or an unbounded-spend
concept, and the platform owns all of them.
A brief that cannot be answered honestly is refused recoverably:
Deduplication and independence
Merging is conservative and never fuzzy. Two candidates merge only when they share a verified domain (the host’s label matches the organization name) or the same normalized name AND the same stated geography.- “ABC Industrial” in Monterrey and in Puebla stay two candidates.
- “Grupo Brisas” and “Grupo Hotelero Brisas” stay two candidates.
- Two articles about the same project from the same company consolidate, keeping all the evidence, and record the alternate spellings and the merge rule.
verified when a new origin says something
compatible.
Comparing this against Parallel’s Task API, later
Not implemented, and deliberately not integrated on intuition. When we run it:- Arm A — this workflow over Search: bounded planning, one discovery call, extraction under a strict schema, dedupe, ranking, one verification call.
- Arm B — the Task API on the same broad question, same objective, same output criteria.
- if the Search workflow is close enough in quality at materially lower latency and cost, keep it and do not add a second provider surface;
- if the Task API materially improves depth and coverage on broad questions, adopt it as an explicit deep-research escalation behind its own budget and approval — never as a silent replacement for this protocol.
See also
docs/market-data/web-market-discovery-live-eval.md— the runbook. Nothing in it has been run.docs/market-data/web-evidence-live-smoke.md— the verification smoke test.docs/market-data/commercial-intelligence-v1.md— the harness this capability composes into.
