application capa

application-party-discovery

Sprint 3.69 Phase 1 item 3 — business-discovery composition module: Google Places search -> contact/domain enrichment -> human-reviewed staging, never auto-writing an Organization.

Sprint 3.69 Phase 1 item 3 — business-discovery composition module: Google Places search -> contact/domain enrichment -> human-reviewed staging, never auto-writing an Organization.

Tierapplication
Roleunclassified (baselined)
Pathcrates/application/party-discovery
Edition2024
Targetsapplication_party_discovery, pg_forge_party_discovery
Public items38 across 8 modules
Tests24

What it is for

application-party-discovery — business-discovery composition module.

Sprint 3.69 Phase 1 item 3 (docs/planning/PARTY-DOSSIER-VAULT-ROADMAP.md). Composes four already-real crates (infrastructure-adapters-google-places, infrastructure-web-contact-extractor, infrastructure-web-domain-intel, domain-competitive-intel) into a search-and-enrich pipeline, staged for human review (discovery_run/discovery_candidate — Gate 1.5, unanimous). A candidate is NEVER an Organization until a human explicitly imports it via api::discovery_admin (item 4).

Money note: infrastructure_adapters_google_places::GooglePlacesClient::search_text makes a real, billed call. Nothing in this crate calls it automatically — pipeline::discover takes an already-constructed GooglePlacesClient the caller owns, and this crate's own test suite never constructs a real one. Use infrastructure_adapters_google_places::GooglePlacesClient::dry_run_estimate (pure, local, no network) to show the operator a cost estimate BEFORE calling discover — Gate 1.5 open question 5 (cost surfacing), still open at the UI layer.

Capabilities

PartyDiscoveryModule

application-party-discovery — business-discovery composition module.

Item
pub struct PartyDiscoveryModule
PartyDiscoveryModule :: fn new() -> Self
PartyDiscoveryModule :: fn name(& self) -> & 'static str
PartyDiscoveryModule :: fn version(& self) -> & 'static str
PartyDiscoveryModule :: fn migrations(& self) -> Vec <MigrationSet>
PartyDiscoveryModule :: async fn migrate_pending(& self, pool : & PgPool) -> std::result::Result <u32, MigrationError>
PartyDiscoveryModule :: fn permission_codes(& self) -> Vec <String>

api (other)

HTTP API routers for application-party-discovery.

Item
fn admin_router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>,

api::discovery_admin (other)

Discovery-candidate admin REST router — the browse/review/decide half

Item
pub const MAX_PAGE_SIZE: u32
pub const DEFAULT_PAGE_SIZE: u32
fn to_dto(c : & DiscoveryCandidate) -> DiscoveryCandidateDto
fn organization_from_candidate(c : & DiscoveryCandidate) -> Organization
fn clamp_paging(page : Option <u32>, page_size : Option <u32>) ->(u32, u32)
fn page_offset(page : u32, page_size : u32) -> i64
fn router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>,

ListCandidatesQuery

Discovery-candidate admin REST router — the browse/review/decide half

Item
pub struct ListCandidatesQuery

TenantQuery

Discovery-candidate admin REST router — the browse/review/decide half

Item
pub struct TenantQuery

dedup (other)

Entity resolution — Gate 1.5's precedence table (`docs/userstories/

Item
fn normalize_domain(website_or_domain : & str) -> Option <String>
fn find_suspected_duplicate(existing : & DiscoveryCandidate, place_id : & str, domain : Option <& str>,) -> Option <Uuid>

DiscoveryError

Error type for application-party-discovery.

Item
pub enum DiscoveryError

CandidateStatus

Discovery staging models — discovery_runs/discovery_candidates.

Item
pub enum CandidateStatus
CandidateStatus :: fn as_str(& self) -> & 'static str
CandidateStatus :: fn parse(s : & str) -> Self

DiscoveryCandidate

Discovery staging models — discovery_runs/discovery_candidates.

Item
pub struct DiscoveryCandidate

DiscoveryRun

Discovery staging models — discovery_runs/discovery_candidates.

Item
pub struct DiscoveryRun
DiscoveryRun :: fn new(tenant_id : Uuid, query_text : impl Into <String>) -> Self

pipeline (other)

The composition pipeline: Places search -> per-result contact/domain

Item
async fn discover(pool : & PgPool, tenant_id : Uuid, places_client : & GooglePlacesClient, contact_extractor : & ContactExtractor, domain_analyzer : & DomainIntelAnalyzer, http : & SafeHttpClient, request : & TextSearchRequest,) -> Result <Vec <DiscoveryCandidate>, DiscoveryError>

repo (other)

Database access for discovery_runs/discovery_candidates. Every

Item
async fn create_discovery_run(pool : & PgPool, run : & DiscoveryRun,) -> Result <DiscoveryRun, DiscoveryError>
async fn find_discovery_run_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid,) -> Result <Option <DiscoveryRun>, DiscoveryError>
async fn list_candidates_for_run(pool : & PgPool, tenant_id : Uuid, discovery_run_id : Uuid,) -> Result <Vec <DiscoveryCandidate>, DiscoveryError>
async fn find_candidates_by_domain_for_tenant(pool : & PgPool, tenant_id : Uuid, domain : & str,) -> Result <Vec <DiscoveryCandidate>, DiscoveryError>
async fn upsert_candidate_for_tenant(pool : & PgPool, tenant_id : Uuid, candidate : & DiscoveryCandidate,) -> Result <DiscoveryCandidate, DiscoveryError>
async fn list_candidates_page_for_tenant(pool : & PgPool, tenant_id : Uuid, status : Option <CandidateStatus>, discovery_run_id : Option <Uuid>, limit : i64, offset : i64,) -> Result <(Vec <DiscoveryCandidate>, i64), DiscoveryError>
async fn find_candidate_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid,) -> Result <Option <DiscoveryCandidate>, DiscoveryError>
async fn mark_candidate_imported_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid, organization_id : Uuid,) -> Result <DiscoveryCandidate, DiscoveryError>
async fn mark_candidate_rejected_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid,) -> Result <DiscoveryCandidate, DiscoveryError>
async fn mark_candidate_duplicate_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid, duplicate_of_candidate_id : Uuid,) -> Result <DiscoveryCandidate, DiscoveryError>

scoring (other)

Candidate scoring — decoupled from the discovery pipeline.

Item
fn score_candidate(candidate : & DiscoveryCandidate) -> Option <EntityClassification>

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

application_party_discovery

flowchart TD
  n_application_party_discovery["application_party_discovery"]
  n_application_party_discovery --> n_api["api"]
  n_api --> n_api__discovery_admin["discovery_admin"]
  n_application_party_discovery --> n_dedup["dedup"]
  n_application_party_discovery --> n_error["error"]
  n_application_party_discovery --> n_models["models"]
  n_application_party_discovery --> n_pipeline["pipeline"]
  n_application_party_discovery --> n_repo["repo"]
  n_application_party_discovery --> n_scoring["scoring"]

Public surface

`crate root`

ItemWhat it is
pub struct PartyDiscoveryModule
PartyDiscoveryModule :: fn new() -> Self
PartyDiscoveryModule :: fn name(& self) -> & 'static str
PartyDiscoveryModule :: fn version(& self) -> & 'static str
PartyDiscoveryModule :: fn migrations(& self) -> Vec <MigrationSet>
PartyDiscoveryModule :: async fn migrate_pending(& self, pool : & PgPool) -> std::result::Result <u32, MigrationError>
PartyDiscoveryModule :: fn permission_codes(& self) -> Vec <String>

`api`

ItemWhat it is
fn admin_router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>,Admin router

`api::discovery_admin`

ItemWhat it is
pub const MAX_PAGE_SIZE: u32
pub const DEFAULT_PAGE_SIZE: u32
pub struct ListCandidatesQuery
pub struct TenantQuery
fn to_dto(c : & DiscoveryCandidate) -> DiscoveryCandidateDtoProject a candidate onto the wire DTO.
fn organization_from_candidate(c : & DiscoveryCandidate) -> OrganizationBuild the Organization a candidate imports as — pure, unit-tested without a database.
fn clamp_paging(page : Option <u32>, page_size : Option <u32>) ->(u32, u32)Clamp caller-supplied paging into a served range.
fn page_offset(page : u32, page_size : u32) -> i64
fn router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>,Discovery admin router

`dedup`

ItemWhat it is
fn normalize_domain(website_or_domain : & str) -> Option <String>Lowercased registrable-ish domain (scheme/path/query stripped, www. prefix stripped) for a website URL or bare domain string
fn find_suspected_duplicate(existing : & DiscoveryCandidate, place_id : & str, domain : Option <& str>,) -> Option <Uuid>Given a new candidate's (place_id, domain) and the tenant's existing candidates, return the id of an existing candidate this one is a probable duplicate of — or None if there's no strong match

`error`

ItemWhat it is
pub enum DiscoveryError

`models`

ItemWhat it is
pub struct DiscoveryRunOne Places-search invocation.
DiscoveryRun :: fn new(tenant_id : Uuid, query_text : impl Into <String>) -> Self
pub enum CandidateStatusEntity-resolution status — Gate 1.5's precedence table (docs/userstories/ sprint-3.69.1-party-discovery.md §"Open questions" item 3): a candidate starts Pending; only a human reviewer moves it to Imported (linked_organization_id set), Rejected, or Duplicate (duplicate_of_candidate_id set) — the pipeline itself never auto-merges.
CandidateStatus :: fn as_str(& self) -> & 'static str
CandidateStatus :: fn parse(s : & str) -> Self
pub struct DiscoveryCandidateA discovered business, staged for human review

`pipeline`

ItemWhat it is
async fn discover(pool : & PgPool, tenant_id : Uuid, places_client : & GooglePlacesClient, contact_extractor : & ContactExtractor, domain_analyzer : & DomainIntelAnalyzer, http : & SafeHttpClient, request : & TextSearchRequest,) -> Result <Vec <DiscoveryCandidate>, DiscoveryError>Run one discovery: create a discovery_run row, call Places (billed — see the module doc), enrich each result (landing-page-only), and stage each as a discovery_candidate — applying Gate 1.5's entity-resolution precedence (exact Place ID = re-observation/upsert, same domain + different Place ID = flagged for human review, never auto-merged)

`repo`

ItemWhat it is
async fn create_discovery_run(pool : & PgPool, run : & DiscoveryRun,) -> Result <DiscoveryRun, DiscoveryError>Insert a discovery run
async fn find_discovery_run_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid,) -> Result <Option <DiscoveryRun>, DiscoveryError>Find a discovery run by id, tenant-scoped
async fn list_candidates_for_run(pool : & PgPool, tenant_id : Uuid, discovery_run_id : Uuid,) -> Result <Vec <DiscoveryCandidate>, DiscoveryError>All (non-paginated — a discovery run's candidate count is bounded by max_result_count on the Places request, never open-ended) candidates for a run, tenant-scoped
async fn find_candidates_by_domain_for_tenant(pool : & PgPool, tenant_id : Uuid, domain : & str,) -> Result <Vec <DiscoveryCandidate>, DiscoveryError>Candidates sharing domain, across ALL of the tenant's discovery runs — Gate 1.5's dedup concern spans runs ("re-discover the same business across multiple searches"), so the pipeline's duplicate check queries this instead of loading the whole tenant's candidate history into memory
async fn upsert_candidate_for_tenant(pool : & PgPool, tenant_id : Uuid, candidate : & DiscoveryCandidate,) -> Result <DiscoveryCandidate, DiscoveryError>Insert a NEW candidate, or update the existing row if (tenant_id, google_place_id) already exists (Gate 1.5: an exact Place ID match is the SAME listing re-observed, not a new candidate — see crate::dedup's module doc)
async fn list_candidates_page_for_tenant(pool : & PgPool, tenant_id : Uuid, status : Option <CandidateStatus>, discovery_run_id : Option <Uuid>, limit : i64, offset : i64,) -> Result <(Vec <DiscoveryCandidate>, i64), DiscoveryError>One page of a tenant's candidates, optionally filtered by status and/or discovery run — "browsable with various options" (operator, sprint-3.69 item 4)
async fn find_candidate_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid,) -> Result <Option <DiscoveryCandidate>, DiscoveryError>Find a candidate by id, tenant-scoped
async fn mark_candidate_imported_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid, organization_id : Uuid,) -> Result <DiscoveryCandidate, DiscoveryError>Move a Pending candidate to Imported, linking the newly-created Organization
async fn mark_candidate_rejected_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid,) -> Result <DiscoveryCandidate, DiscoveryError>Move a Pending candidate to Rejected
async fn mark_candidate_duplicate_for_tenant(pool : & PgPool, tenant_id : Uuid, id : Uuid, duplicate_of_candidate_id : Uuid,) -> Result <DiscoveryCandidate, DiscoveryError>Move a Pending candidate to Duplicate, recording which existing candidate it duplicates

`scoring`

ItemWhat it is
fn score_candidate(candidate : & DiscoveryCandidate) -> Option <EntityClassification>Score an already-staged candidate

Re-exports. Exported here, defined elsewhere.

ExportDefined in
DiscoveryErrorerror::DiscoveryError
discoverpipeline::discover
{CandidateStatus,DiscoveryCandidate,DiscoveryRun}models::{CandidateStatus,DiscoveryCandidate,DiscoveryRun}

Boundary

Reaches into domain, foundation, identity, infrastructure, platform.

Shares tier application with 120 other crates: application-agreements, application-ai, application-analytics, application-approvals, application-assessments, application-audit-log, application-auth, application-billing, … (120 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)application
Architectural role (taxonomy)unclassified (baselined)
Locationcrates/application/party-discovery
Vocabulary in force (lexicon)current

Tier flow. Which tiers this crate's own edges cross.

flowchart LR
  n_application["application"] --> n_domain["domain"]
  n_application["application"] --> n_foundation["foundation"]
  n_application["application"] --> n_identity["identity"]
  n_application["application"] --> n_infrastructure["infrastructure"]
  n_application["application"] --> n_platform["platform"]

Dependencies

Runtime, in this workspace.

CrateTierOptionalOnly on
`application-core`applicationnoalways
`domain-competitive-intel`domainnoalways
`domain-contact`domainnoalways
`foundation-web-analysis`foundationnoalways
`identity-parties`identitynoalways
`infrastructure-adapters-google-places`infrastructurenoalways
`infrastructure-fetcher`infrastructurenoalways
`infrastructure-html-text`infrastructurenoalways
`infrastructure-web-contact-extractor`infrastructurenoalways
`infrastructure-web-domain-intel`infrastructurenoalways
`platform-dto`platformnoalways

Runtime, from outside the workspace.

CrateRequirementFeaturesOptionalOnly on
async-trait^0.1noalways
axum^0.7multipartnoalways
chrono^0.4serdenoalways
serde^1derivenoalways
serde_json^1noalways
sqlx^0.8runtime-tokio, postgres, chrono, uuid, jsonnoalways
thiserror^2noalways
tracing^0.1noalways
url^2noalways
uuid^1v4, v7, serde, jsnoalways

Development, in this workspace.

CrateTierOptionalOnly on
`application-engine`applicationnoalways
`application-parties`applicationnoalways

Development, from outside the workspace.

CrateRequirementFeaturesOptionalOnly on
tokio^1fullnoalways
tower^0.5noalways

Build. None.

Depended on by. Nothing in this workspace.

Signal flow — what reaches this crate, and what it reaches.

flowchart LR
  SELF["application-party-discovery"]
  SELF -->|development| n_application_engine["application-engine"]
  SELF -->|development| n_application_parties["application-parties"]
  SELF -->|runtime| n_application_core["application-core"]
  SELF -->|runtime| n_domain_competitive_intel["domain-competitive-intel"]
  SELF -->|runtime| n_domain_contact["domain-contact"]
  SELF -->|runtime| n_foundation_web_analysis["foundation-web-analysis"]
  SELF -->|runtime| n_identity_parties["identity-parties"]
  SELF -->|runtime| n_infrastructure_adapters_google_places["infrastructure-adapters-google-places"]
  SELF -->|runtime| n_infrastructure_fetcher["infrastructure-fetcher"]
  SELF -->|runtime| n_infrastructure_html_text["infrastructure-html-text"]
  SELF -->|runtime| n_infrastructure_web_contact_extractor["infrastructure-web-contact-extractor"]
  SELF -->|runtime| n_infrastructure_web_domain_intel["infrastructure-web-domain-intel"]
  SELF -->|runtime| n_platform_dto["platform-dto"]
  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

KindNameSource
libapplication_party_discovery`src/lib.rs`
testpg_forge_party_discovery`tests/pg_forge_party_discovery.rs`

Error model

Error typeNamed by
DiscoveryErrorcreate_discovery_run, discover, find_candidate_for_tenant, find_candidates_by_domain_for_tenant, find_discovery_run_for_tenant, list_candidates_for_run, … (11 total)

Operational characteristics

PropertyEvidence
async public surfaceyes
async runtimenone detected
database accessyes
network I/Oyes
unsafe codenone detected
environment variablesyes

No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.

Configuration

VariableRead in
CARGO_PKG_VERSIONsrc/lib.rs

No workspace crate depends on this one.

Verification

KindCount
Unit tests18
Integration tests6
Examples0
Doctests0

Evidence by module. How often each public module is named by something executable.

ModuleTestsExamplesConsumers
crate root100
api100
api::discovery_admin900
dedup200
error100
models300
pipeline100
repo1000
scoring100

What the tests establish, by name:

Documentation coverage

MeasureDocumentedTotal
Public items with rustdoc2238
Public modules with a //! block88
pie showData
    title Public items with rustdoc
    "Documented" : 22
    "No rustdoc detected" : 16

Metrics

MetricValue
Rust source files9
Source lines1553
Code lines1209
Public API items38
Public modules8
Tests24
Examples0
Cargo features0
Direct runtime dependencies21
Workspace reverse dependencies0
pie showData
    title Public API by kind
    "constant" : 2
    "enum" : 2
    "function" : 20
    "method" : 9
    "struct" : 5
pie showData
    title Rust source composition
    "Code" : 1209
    "Blank or comment" : 344

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.

Todas las application · Manual