Skip to content

RFC: first-party company resolution and provider-pinned People Search sessions #261

Description

@sicko7947

mise ~/.config/mise/config.toml tools: gh@2.98.0

What problem does this solve?

Treg's routed point lookups are a strong fit for waterfall routing, but identity resolution and paginated search need stricter semantics than "try providers until one returns something".

The current surface exposes the pieces, but consumers still have to compose provider-specific behavior:

  • treg.companies.enrich returns one enriched company, while crustdata.companies.identify is the provider-specific route that preserves ranked candidates and confidence. Flattening ambiguous identity directly into enrichment risks enriching the wrong legal/entity record (shared domains and similar names are common in large organizations and corporate groups).
  • treg.people.search can waterfall weak/missed results and optionally merge provider-native rows. Its output contract can contain next_cursor, but the request contract has no cursor/session input and routing is planned anew per call. That is useful for top-N discovery, but it is not an exhaustive directory contract: count, cursor shape, ordering, row identity, and supported filters differ by provider.
  • A provider can safely change before any rows have been emitted. It cannot safely change between page 1 and page 2 without producing inconsistent totals, duplicate/missing rows, or an invalid cursor.
  • people.decision_makers exists today only as lusha.x.decision-makers, despite being a consumer-level capability (rank relevant contacts, then reveal only selected ones).

The key ownership boundary should be:

  • Treg owns provider selection, accepted-identifier adaptation, normalized candidate/search schemas, deduplication, provenance, cost accounting, idempotency, and cursor/session integrity.
  • The caller owns the ranking policy, budget, confidence threshold, permission to reveal paid contact data, and final acceptance/user choice.

This keeps provider orchestration in one place without letting the router silently make a application-critical identity decision.

Relevant current sources:

Proposed solution

1. Add a first-class treg.companies.resolve capability

Keep canonical identity resolution separate from enrichment.

Suggested request:

{
  "domain": "example.com",
  "name": "Example Company",
  "linkedin_url": "https://www.linkedin.com/company/example-company",
  "country": "US"
}

Treg should:

  1. Derive and rank identifier specificity (linkedin_url/provider id > registrable domain > name plus geography).
  2. Send each child only one identifier variant that the child accepts. A provider that accepts domains or names, but not both, must never receive both collections in one request.
  3. Return normalized ranked candidates rather than silently collapsing ambiguity:
{
  "candidates": [
    {
      "entity_id": "treg_company_...",
      "name": "Example Company",
      "domain": "example.com",
      "linkedin_url": "https://www.linkedin.com/company/example-company",
      "confidence": 0.98,
      "confidence_band": "high",
      "matched_on": ["linkedin_url", "domain"],
      "evidence": [],
      "provider_refs": []
    }
  ],
  "resolution": "unique"
}

resolution should distinguish unique, ambiguous, and no_match. A low-confidence or tied result must remain ambiguous. Treg should normalize and rank the evidence; the caller chooses its acceptance threshold.

2. Treat ranked discovery and exhaustive search as different products

Keep the existing routed treg.people.search behavior as a ranked discovery surface:

  • top-N candidates for a brief;
  • waterfall on miss/weak hit;
  • optional cross-provider merge;
  • normalized stable row ids and explicit per-row provenance;
  • no claim that count is an exhaustive company headcount.

For cross-provider ranking, preserve each child's source_rank/source_score. If Treg re-ranks the merged set, return a separate relevance_score, score factors, and ranking_model_version; provider ranks are not directly comparable and should not be silently treated as one scale.

Add a session-scoped exhaustive surface, for example:

POST /people.search.sessions
POST /people.search.sessions/{session_id}/pages

The create response should include:

{
  "session_id": "...",
  "query_fingerprint": "...",
  "total": 250,
  "total_relation": "exact",
  "people": [],
  "next_cursor": "opaque-treg-cursor",
  "has_more": true,
  "served_by": "icypeas.people.search",
  "expires_at": "...",
  "charged_micro": 0
}

Required invariants:

  • select and pin one provider for the session;
  • obtain count and pages from the same provider/query snapshot;
  • make the Treg cursor opaque, signed, and bound to org + query fingerprint + provider + upstream cursor;
  • permit provider failover only before the first page is emitted;
  • after page 1, return restart_required rather than silently changing provider;
  • keep stable ordering and deduplicate by normalized LinkedIn URL/provider ids, with an explicit fallback key;
  • report total_relation as exact, estimated, or unknown instead of implying certainty;
  • expose ignored/unsupported filters before charging for rows.

The same session contract should be reusable for exhaustive companies.search.

3. Add routed treg.people.decision_makers

Expose a provider-neutral contract even while Lusha is the only child today. The request should take a resolved company identity plus the caller's relevance criteria:

{
  "company_entity_id": "treg_company_...",
  "departments": ["engineering", "product"],
  "seniorities": ["manager", "director"],
  "limit": 25
}

Return free previews with a normalized relevance score, score factors, seniority/department, stable person identity, and provider refs. Email/phone reveal stays a separate explicit paid action.

This is a better default than treating all employees as equally relevant or forcing every caller to know the Lusha-specific workflow.

4. Keep profile enrichment and contact reveal separate

These are good waterfall candidates because each request targets one already-resolved person/company:

  • treg.companies.enrich
  • treg.people.identity.resolve
  • treg.people.enrich
  • treg.people.email.find
  • treg.people.phone.find

Recommended guarantees:

  • accept a Treg resolved entity/person token where available;
  • never switch canonical entity while filling fields;
  • return field-level provenance and freshness when providers contribute different fields;
  • never overwrite a known non-null field with provider null;
  • keep email/phone reveal explicit, separately budgeted, and idempotent;
  • let people.identity.resolve mint/return the same stable person token accepted by profile and contact routes;
  • standardize _treg.tried, served_by, outcome, charged_micro, settled cost, and no-data/error classification across all five routes.

5. Standardize outcomes and cost controls

Machine-readable outcomes should include:

no_match
ambiguous
invalid_input
provider_unavailable
rate_limited
budget_exceeded
restart_required

Keep Idempotency-Key, X-Treg-Route-Max-Cost, prefer/exclude, Call ID, and route audit metadata. For search sessions, the budget should cover session creation and each page independently, while the provider pin remains unchanged.

Acceptance criteria

  1. A company request containing name + domain + exact LinkedIn URL returns ranked normalized candidates and does not send incompatible identifier families to a child.
  2. Shared/parent domains do not silently override a more specific LinkedIn/company identifier.
  3. A multi-page people query can return page 1 and page 2 with the same provider, stable total semantics, a valid opaque cursor, and zero cross-page duplicates.
  4. A provider failure before page 1 may fail over; a failure after page 1 returns restart_required and never silently mixes result sets.
  5. Ranked discovery may merge providers, but labels itself as discovery and exposes row provenance rather than presenting the result as an exhaustive count.
  6. Decision-maker previews are provider-neutral and free; contact reveal happens only through an explicit email/phone call.
  7. Company/person enrichment cannot change the resolved entity and reports field provenance/freshness.
  8. Misses, ambiguity, provider failure, budget refusal, and pagination restart are distinguishable without parsing provider text.

Alternatives considered

  • Let every consumer call provider-specific endpoints. This leaks credentials, pricing, cursor quirks, schemas, and fallback policy into every integration—the exact complexity routed capabilities should centralize.
  • Waterfall every page independently. This can change the underlying dataset mid-search and makes totals/cursors unreliable.
  • Merge all provider pages into one exhaustive list. Useful for ranked discovery, but it cannot honestly preserve a single exact total or stable pagination snapshot.
  • Automatically accept the top company candidate. Safe only when the result is uniquely high-confidence. Ambiguity must remain visible to the caller.

This can be delivered incrementally: companies.resolve and routed people.decision_makers first, then provider-pinned search sessions, then field-level provenance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions