Register item 18 — Step-0 verification + build record (2026-08-20)
Read-only verification of "Connector rollout phases 2–3 (build, after the
above): Merchant Center + public Shopping; then LinkedIn / Amazon Ads / The
Trade Desk" before building, and the record of what was then built. Claims are
measured in-repo at file:line unless labeled otherwise. Companion to
docs/reports/ITEM15_STEP0_VERIFICATION_2026-08-20.md (whose projector this
item's KG leg extends) and the item-16 report.
Headline
The register line was stale in the same way items 15/16's were: every
phase-2 AND phase-3 adapter has been built and mounted since 2026-08-12
(direct_connectors.py — MerchantCenterAdapter, PublicShoppingAdapter,
LinkedInAdsAdapter, AmazonAdsAdapter, TradeDeskAdapter; module memory
records them live on revision service-marketing-connectors-00007-52f). What
was actually open was narrower and partly a defect list:
- TENANT-001 gap —
/api/v1/direct-connectors/{provider}/syncpassedreq.tenant_idstraight to ingestion (olddirect_connectors.py:441) while every sibling ingest door (ingest_batchmain.py:650, shopify sync:721, bucket sync:679) resolves the tenant against the verified caller. A mapped caller could ingest into a tenant it does not hold, on all eight providers. - Fetch-guard gap —
PublicShoppingAdapterfetched caller-suppliedoptions.urlswith no scheme/host validation: an authed caller could point the gateway at internal hosts (probe primitive) or ingest arbitrary content aspublic_source. - The KG lineage leg could not exist for ANY direct-pull provider — not
just phases 2–3. The adapters emitted provider-prefixed record_types
(
meta_ads_insight,google_merchant_center_product_reportviarecord_type=f"{provider}_{kind}") and raw-only payloads, while the item-26 projector maps(provider, record_type)keys againstpayload.normalized. Every direct-pull event would land in the canonical store and skip projection (unmapped:*/no_normalized_payload). The module's connected bar — "source→canonical→KG lineage proof passes" — was unreachable on this door. - Test depth: 3 in-service tests covered the eight adapters; no route, tenant, or normalization pins.
What was already right: the governed order itself. The sync route flows
fetched records through _ingest_records → service-canonical-ingestion
(main.py:300-322,1814-1820) — no side door, rule 1 honored.
What was built (this change set)
- TENANT-001 closed on the direct door:
build_routernow takesresolve_tenantand the sync route resolves BEFORE the provider pull (a refused caller costs no API quota); response exposes the resolvedtenant_id.main.pywires the realmizoki_contracts.resolve_tenant. - Public-feed fetch guard:
_validate_feed_url— https only, public DNS hosts only (metadata/localhost/*.internal/single-label refused, literal IPs must be global), ≤20 urls per sync. Scope stated honestly: literal targets only, no DNS re-resolution — the route staysverify_caller-gated. - Record contract converged with the bucket puller (item-15 shape):
record_typeis the CONCEPT (campaign_performance,product_report,feed_item,report_row) — provider stays insource_systemand provenance;payloadis{"raw": <provider-native row>, "normalized": <flat dict or {}>}. Per-provider normalizers added (tolerant numeric coercion; junk → absent key, never a crash; a non-urn LinkedIn pivot or a TTD row without campaign structure refuses to mint ids). - Projector vocabulary, phases 2–3 (
projector_kg.py):google_merchant_center:product_report→account_gmc_*PlatformAccount +product_gmc_*Product + CONTAINS;google_shopping_public:feed_item→product_gsp_{host}_{offer}Product (host-qualified so feeds cannot collide; no account parent by construction);linkedin_ads/amazon_adscampaign_performanceandthe_trade_desk:report_rowreuse the parameterized ad hierarchy (tokensli/amz/ttd).Productis live platform vocabulary (cell02 EKIS NodeType, market mappers), not a new type. Identity/config props only — metrics stay in the canonical event.ALL_ALLOWED_PROPERTY_KEYSextended in the same commit; still disjoint fromPERSON_TOKENS(tripwire test). - Direct meta/google now project through the EXISTING mappings — the convergence means the phase-1 direct door gained its KG leg for free.
- Tests:
tests/connectors/test_direct_connectors_governed.py(20 items — tenant both directions incl. refusal-before-pull, main.py wiring pin, record contract, url guard 13-case parametrize, normalizer tolerance) + 4 item-18 additions totests/connectors/test_kg_projection.pyincluding the cross-service contract test feeding all seven direct normalizers intoproject_envelope. Suites: tests/connectors 307 passed, gateway in-service 83 passed (pydantic 2.13.4 venv).
Deliberately NOT built
- GA4 direct rows stay unmapped (
report_row, normalized{}): the pull is session/transaction-grained (transactionIddimension) — a different concept from the mapped aggregateaudience_segment, and transaction linkage needs its own design pass. Named skip, never a guess. - TTD rows without campaign structure stay unmapped for the same reason.
- No scheduler/creds/terraform — operator surface, below.
Honest end condition
Build half of item 18 is code-complete and pinned; nothing here is
live-verified, because no phase-2/3 provider has credentials: every adapter
still reports configured: false and stays ready-to-connect (the module's
own bar). Remaining operator work per provider: install credentials/account
ids via Secret Manager (GOOGLE_MERCHANT_ID + content-API access,
PUBLIC_SHOPPING_FEED_URLS, LINKEDIN_*, AMAZON_ADS_*, TTD_*), map the
calling tenant (TENANT-001 registry), then run one bounded
/api/v1/direct-connectors/{provider}/sync and prove the
source→canonical→KG lineage on the projector sweep — that proof, not this
change, is what upgrades a provider's status.