Source expansion — data rights, and which providers are selected
Phase 6 requires that source expansion happen behind capability ports and that LinkedIn/social sources get a recorded data-rights review before implementation. This is that record. It also states which providers are selected today, which is: none beyond the web-document executor already wired. Nothing here is a legal opinion. It records (a) what this codebase does and refuses to do, which is verifiable from the code and enforced by tests, and (b) which questions a provider must answer before it can be selected, with the answers marked NOT DETERMINED where no contract has been read in this repository. A NOT DETERMINED row is a decision waiting for a human, not a gap to be filled in by guessing.The three capabilities, and why they are separate
The first two share the web-document port because their contract is “what a page
said”. Firmographics are not documents — they are typed assertions carrying a
licence — so routing them through the document port would store a vendor’s
structured claim as if a page had published it, and lose the licensing metadata
that decides whether the value may be displayed at all.
Enrichment runs only after identity resolution. The port has no name-only
input shape:
ResolvedOrganization requires an entity id or a stated domain, and
resolvedOnly() drops anything else at the seam. Cold discovery through a
firmographics provider is unbounded spend against an unbounded set, and it is not
in this slice.
What the platform stores today
Verifiable fromlibs/db/src/entities/investigation-candidate.entity.ts and the
migration that created it:
- organization name, domain, geography;
- a signal type and summary, an announcement date, a commercial-relevance note;
- evidence ids and an independent-source count;
- a score and its ranking reasons.
PEOPLE_VOCABULARY in apps/api/src/research-providers/web-market-discovery.contract.ts
rejects contact and role vocabulary — including the word linkedin — as query
material in both capabilities. research-providers.architecture.spec.ts pins
that this is the only place in the layer that knows the word.
That is the current answer to “personal-data minimization”: the schema cannot
hold personal data, so minimization is structural rather than a policy somebody
has to remember.
The questions every source must answer before selection
Per Phase 6, and to be answered per field, not per vendor:- Contractual right to collect and retain each field. Which fields does the agreement cover, and for how long?
- Derived commercial profiles. Do the terms permit building and selling a derived profile from the data, or only internal use?
- Personal-data minimization and retention. Is any field personal data? If so, what is the lawful basis, and what is the retention limit?
- Deletion and export obligations. On a subject request or contract termination, what must be deleted, and within what window? Can we satisfy it for values already copied into a Collection?
- Geography-specific restrictions. Where may the data be processed and stored, and which jurisdictions restrict this category of data?
- Show / export / promote. May a value be displayed in the product? Exported to CSV? Written into a Collection record, which leaves the investigation’s lifecycle behind?
displayableFields(), exportableFields() and withinRetention() apply it at
the seam, so every consumer inherits the rule instead of re-deciding it.
licensingViolations() refuses a field claiming rights broader than the adapter
declared: a per-field licence may narrow the contract, never widen it, and a
retention limit with no observation time is a contract breach rather than a
degraded field — there would be no clock to measure the window from.
Decisions
D1 — LinkedIn and social sources: not implemented, and not storable
Decision: no LinkedIn or social personal data is collected, stored, displayed, exported or promoted. The first source expansion favours company-level facts and public announcements, per Phase 6. This is a deny, so it needs no external answer: refusing to store a category of data requires no contract. The corresponding allow would need every question above answered, and would additionally trip the queue’s global stop condition — “exposing raw LinkedIn personal data or implementing scraping without a documented legal/data-rights decision” — so it cannot be taken without an explicit human decision recorded here first.D2 — Bright Data (and unlocker/scraping networks): NOT SELECTED
Phase 6 says “if selected, implement it as an adapter behind the existing evidence-web provider port”. It is not selected, for a reason that is architectural before it is legal: the seam it would sit behind already exists and is proven by the shared conformance suite, so selecting a provider is a configuration act — and it is the wrong week to spend the review budget on it. The blocking facts, stated as they are:
A scraping-network adapter is implementing scraping, which the queue’s global
stop conditions place behind a documented legal decision. That decision has not
been made, so the adapter is not written. This is a documented stop, not an
omission, and it does not block the rest of Phase 6: the point of the phase is
the seam, and the seam is complete and tested without a second vendor.
What is ready for the day it is selected, so that day is small:
EvidenceWebProviderPortis provider-neutral and already carries a per-vendorWebContentLicense(mayStoreArtifact/mayDisplayExcerpt/mayRedistributeplus a note written for a lawyer). A vendor whose terms have not been read answersfalse, false, falseand the router will not persist from it.- The shared conformance suite
(
apps/api/src/research-providers/ports/web-provider.conformance.ts) gives a new adapter its ten cases: normalized documents, provenance, timeout/partial, rate limit and breaker, duplicate collapse, credential absence, secret redaction, disallowed egress, prompt-injection text, and a capability declaration that matches what actually comes back. - The composition root binds one token to one adapter instance, so a second executor is a binding change and nothing else.
D3 — Structured firmographics (Crunchbase-like): NOT SELECTED, port shipped
InvestigationEnrichmentPort is defined, the licensing/retention/confidence
metadata travels with every value, and the enrichment step runs only on resolved
organizations. No vendor is configured, and that is bound explicitly:
NullEnrichmentAdapter declares zero field keys and a licence that grants
nothing, so the runner skips the step with capability_unavailable and the
investigation lands partial — a declared hole rather than a silent one.
Provider neutrality, enforced
research-providers.architecture.spec.ts— vendor names and wire vocabulary appear nowhere outsideadapters/; the source-expansion vendor set (unlocker networks, firmographics providers) is in the token list before any of them is wired, which is when a name is cheapest to keep out.investigations.architecture.spec.ts— the same rule for the investigation surface, where the quarantine is*.adapter.ts; this layer may not importresearch-providers/adapters/; and an adapter id may not name a vendor either, because it is read in logs and copied into dashboards.sanitizeEventDetaildrops adapter ids and internal cost from the curated activity stream by key, whatever their type. Provider choice is not a product-visible fact, and an internal cost is not a price.
