Skip to content

feat: publish Relayfile ingest identity contracts - #254

Merged
khaliqgant merged 4 commits into
mainfrom
feat/relayfile-ingest-p0-contracts
Jul 25, 2026
Merged

feat: publish Relayfile ingest identity contracts#254
khaliqgant merged 4 commits into
mainfrom
feat/relayfile-ingest-p0-contracts

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 25, 2026

Copy link
Copy Markdown
Member

Outcome

Publishes the two adapter-owned contracts blocking the Relayfile ingest cut:

  1. Gmail canonical identity and migration policy.
  2. A typed inbound capability catalog plus deterministic logicalEventKey, including GitLab Hookdeck, with cross-runtime golden vectors.

No Cloud or relayfile-cloud code is changed. Consumers must wait for the adapter release and import these exports directly.

Gmail identity

  • Canonical provider: gmail
  • Canonical root: /gmail
  • Provider-config aliases: gmail, google-mail, google-mail-relay
  • Legacy provider/root: google-mail, /google-mail
  • New writes are canonical-only.
  • Legacy reads and digests remain supported.
  • Legacy deletion is forbidden during migration.
  • A full Gmail resync is required; retire /google-mail only after zero-reference reconciliation and an explicit cutover.
  • Gmail path mapping, config, and digest handling now derive from the published identity contract.

Inbound catalog and logical key

  • Each provider owns src/inbound.ts; adapter-core generates a normalized, version-hashed catalog rather than maintaining a static package map.
  • Published surface: INBOUND_CAPABILITY_CATALOG, INBOUND_CAPABILITY_CATALOG_VERSION, resolveInboundCapability, logicalEventKey, and exact golden vectors.
  • Catalog currently covers GitHub, GitLab, Gmail, HubSpot, Linear, Notion, and Slack Nango aliases plus GitLab Hookdeck.
  • Key hierarchy:
    1. adapter-declared immutable delivery header;
    2. Nango provider-config + connection + sync + model + window + cursor;
    3. Hookdeck delivery header;
    4. canonical semantic JSON;
    5. exact raw body only for unknown event kinds.
  • Exact raw-body SHA-256 is always returned as evidence.
  • Generic payload eventId fields are not implicitly promoted to transport identity.
  • GitLab Hookdeck prefers x-gitlab-event-uuid, then x-hookdeck-eventid.

RED-first evidence

  • Gmail tests initially failed because the identity export did not exist and /google-mail digest events were rejected.
  • Core inbound tests initially failed because the catalog/key export did not exist.
  • A later regression test proved generic payload eventId was incorrectly selected as delivery identity before the header-only fix.

Validation

  • npx turbo build typecheck test — 147/147 tasks successful.
  • @relayfile/adapter-core — 178/178 tests.
  • @relayfile/adapter-github — 377/377 tests.
  • Gmail — 14/14 tests.
  • GitLab — 89/89 tests.
  • Linear — 163/163 tests.
  • npm run catalog:check --workspace @relayfile/adapter-core — 8 capabilities, catalog version sha256:edcb9170384faeebebe1a4c535552d6c6e2e96c69df68373d588322080fd96b0.
  • npm pack --dry-run — adapter-core, Gmail, and GitLab package surfaces verified.
  • git diff --check — clean.

Release and downstream gates

Package versions are intentionally unchanged in this feature PR.

After approval and merge, @lead / khaliq must gate:

  1. Run the adapter publish workflow with patch bumps for core, github, gitlab, gmail, hubspot, linear, notion, and slack.
  2. Confirm npm publication and exact released versions.
  3. Only then bump adapter pins in Cloud and relayfile-cloud and replace their local maps with direct imports.
  4. Run Gmail canonical resync and prove zero legacy references before any explicit legacy-root retirement.

Do not merge, publish, deploy, or update downstream pins from this PR without that gate.

Explicitly deferred P0s

  • Neutral claim ledger.
  • Signed drain/watermark proof.
  • Gmail backfill completion signal.

Review in cubic

@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@khaliqgant, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e647e2f2-5c92-4ce0-ac5c-10e4131eaae0

📥 Commits

Reviewing files that changed from the base of the PR and between c6719b7 and 097640e.

📒 Files selected for processing (4)
  • packages/core/src/inbound/catalog-generator.ts
  • packages/core/tests/inbound/catalog-generator.test.ts
  • packages/gmail/src/path-mapper.test.ts
  • packages/gmail/src/path-mapper.ts
📝 Walkthrough

Walkthrough

This PR adds a shared inbound capability contract, provider declarations, catalog generation and validation commands, deterministic logical event key computation, golden vectors, and Gmail canonical identity migration support.

Changes

Inbound capability platform

Layer / File(s) Summary
Inbound contracts and public surfaces
packages/core/src/inbound/types.ts, packages/core/src/index.ts, AGENTS.md, docs/inbound-capability-contract.md
Defines inbound capability schemas, logical-key strategies, typed factories, public exports, catalog guidance, and the documented contract.
Adapter inbound declarations
packages/{github,gitlab,gmail,hubspot,linear,notion,slack}/src/inbound.ts, packages/*/package.json, packages/*/src/index.ts
Adds provider-specific inboundCapabilities declarations and publishes their inbound subpath exports.
Catalog generation and validation
packages/core/src/inbound/catalog-generator.ts, packages/core/src/cli.ts, packages/core/package.json, packages/core/tests/inbound/*
Discovers and validates adapter declarations, generates versioned JSON and TypeScript artifacts, and adds inbound generate and inbound check commands.
Logical event key computation
packages/core/src/inbound/logical-event-key.ts, packages/core/src/inbound/golden-vectors.ts, packages/core/src/inbound/logical-event-key.test.ts
Computes deterministic keys across delivery, sync-page, semantic-payload, and raw-body strategies, with structured Nango identity errors and golden-vector coverage.
Gmail identity migration integration
packages/gmail/src/identity.ts, packages/gmail/src/digest.ts, packages/gmail/src/path-mapper.ts, packages/gmail/src/*test.ts
Defines canonical and legacy Gmail identities and applies them to inbound declarations, path mapping, digest behavior, and migration tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Adapter
  participant Catalog
  participant KeyResolver
  Adapter->>Catalog: publish inboundCapabilities
  Catalog->>Catalog: normalize and validate declarations
  Catalog-->>KeyResolver: provide catalog and catalogVersion
  KeyResolver->>KeyResolver: resolve capability and compute logicalEventKey
Loading

Poem

A rabbit hops through catalogs bright,
Hashing events by moonlit light.
Gmail roots both old and new,
Providers declare what they do.
“Hop hop!” says Bun, “the keys stay true!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: publishing Relayfile ingest identity contracts.
Description check ✅ Passed The description is detailed and directly matches the inbound identity and catalog changes in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/relayfile-ingest-p0-contracts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2ed055d8e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/inbound/logical-event-key.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/gmail/src/path-mapper.ts (1)

54-65: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

parseRelayfilePath doesn't recognize the legacy /google-mail root.

RELAYFILE_ROOT (line 61 check) only reflects the canonical GMAIL_PATH_ROOT, so paths under the legacy root (/google-mail/...) fall through to resource: 'unknown' instead of 'object'. This contradicts the migration policy declared in identity.ts (legacyReads: 'supported') and is inconsistent with digest.ts, which was just updated to match against GMAIL_PATH_ROOTS (canonical + legacy) for the same migration.

🐛 Proposed fix
-import { GMAIL_PATH_ROOT, GMAIL_PROVIDER_ID } from "./identity.js";
+import { GMAIL_PATH_ROOTS, GMAIL_PROVIDER_ID } from "./identity.js";
 
-export const RELAYFILE_ROOT = GMAIL_PATH_ROOT;
+export const RELAYFILE_ROOT = GMAIL_PATH_ROOTS[0];
@@
-  if (segments[0] === RELAYFILE_ROOT.slice(1)) {
+  if (GMAIL_PATH_ROOTS.some((root) => segments[0] === root.slice(1))) {
     return { resource: 'object', id: segments.at(-1) ?? null, segments };
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gmail/src/path-mapper.ts` around lines 54 - 65, Update
parseRelayfilePath to recognize both canonical and legacy object roots by
matching the first segment against the existing GMAIL_PATH_ROOTS collection,
rather than only RELAYFILE_ROOT. Preserve the current lifecycle matching and
unknown-path behavior, and return resource: 'object' with the existing ID and
segments for either supported root.
🧹 Nitpick comments (3)
packages/gmail/src/path-mapper.ts (3)

4-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use RELAYFILE_ROOT instead of re-hardcoding "/gmail".

RELAYFILE_ROOT was just introduced from GMAIL_PATH_ROOT on line 3 specifically to centralize the root, but OBJECT_RESOURCE_PATH/LIFECYCLE_RESOURCE_PATH still hardcode the literal "/gmail". If the canonical root ever changes, these constants will silently drift out of sync.

♻️ Proposed fix
 export const RELAYFILE_ROOT = GMAIL_PATH_ROOT;
-export const OBJECT_RESOURCE_PATH = "/gmail/{account}/threads";
-export const LIFECYCLE_RESOURCE_PATH = "/gmail/watches";
+export const OBJECT_RESOURCE_PATH = `${RELAYFILE_ROOT}/{account}/threads`;
+export const LIFECYCLE_RESOURCE_PATH = `${RELAYFILE_ROOT}/watches`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gmail/src/path-mapper.ts` around lines 4 - 5, Update
OBJECT_RESOURCE_PATH and LIFECYCLE_RESOURCE_PATH to build their paths from the
existing RELAYFILE_ROOT constant instead of hardcoding "/gmail", while
preserving the current account and watches suffixes.

41-41: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Canonical thread filename omits the required slug segment.

Per the repo's canonical record contract, flat records must be named <slug>__<id>.json, but this builds the thread path as just encodePathSegment(input.threadId ?? id) + '.json' — no slug component. Please confirm whether Gmail threads are intentionally excepted from the slug requirement, or whether this needs a slugifyAlias-derived prefix.

As per coding guidelines, "Canonical record names must use <slug>__<id>: flat records are <slug>__<id>.json... Slugs must be ASCII, lowercase, hyphen-separated, truncated to 80 characters at a word boundary, and produced through slugifyAlias".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gmail/src/path-mapper.ts` at line 41, Update the Gmail case in the
path-mapping logic to construct the thread filename as the canonical
`<slug>__<id>.json` form, deriving the slug through `slugifyAlias` and applying
its standard ASCII, lowercase, hyphenated, truncated behavior. Preserve the
existing account path and thread ID selection while ensuring the slug prefix is
included before the encoded ID.

Source: Coding guidelines


32-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead switch branches for unrelated providers, all hardcoding "/gmail".

PROVIDER_SLUG is derived from GMAIL_PROVIDER_ID and is always "gmail" in this package, yet the switch carries cases for google-drive, gcs, sharepoint, onedrive, azure-blob, dropbox, s3, box, postgres, and redis — none reachable, and each one still hardcodes the "/gmail" prefix regardless of its own case label. Because PROVIDER_SLUG is typed as plain string (line 6) rather than the literal type of GMAIL_PROVIDER_ID, TypeScript can't flag these as unreachable. This looks like leftover boilerplate from a shared template; collapsing it to the single relevant case would remove confusing dead code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gmail/src/path-mapper.ts` around lines 32 - 48, The
toObjectRelayfilePath switch contains unreachable provider branches and
hardcodes the Gmail prefix for unrelated providers. Since PROVIDER_SLUG is
always Gmail, collapse the switch to the single Gmail path construction,
preserving its current account, threadId, identifier fallbacks, encoding, and
“.json” suffix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/inbound/catalog-generator.ts`:
- Around line 74-79: Extend validateCatalog to enforce a one-to-one relationship
between providerId and pathRoot across all capability declarations: capabilities
sharing a providerId must use the same pathRoot, and distinct providerIds must
not reuse one. Ensure validation runs before catalog generation and reports
conflicting declarations. Update generateInboundCapabilityCatalog to use the
validated consistent providerId rather than assuming
declarations[0]!.providerId.

---

Outside diff comments:
In `@packages/gmail/src/path-mapper.ts`:
- Around line 54-65: Update parseRelayfilePath to recognize both canonical and
legacy object roots by matching the first segment against the existing
GMAIL_PATH_ROOTS collection, rather than only RELAYFILE_ROOT. Preserve the
current lifecycle matching and unknown-path behavior, and return resource:
'object' with the existing ID and segments for either supported root.

---

Nitpick comments:
In `@packages/gmail/src/path-mapper.ts`:
- Around line 4-5: Update OBJECT_RESOURCE_PATH and LIFECYCLE_RESOURCE_PATH to
build their paths from the existing RELAYFILE_ROOT constant instead of
hardcoding "/gmail", while preserving the current account and watches suffixes.
- Line 41: Update the Gmail case in the path-mapping logic to construct the
thread filename as the canonical `<slug>__<id>.json` form, deriving the slug
through `slugifyAlias` and applying its standard ASCII, lowercase, hyphenated,
truncated behavior. Preserve the existing account path and thread ID selection
while ensuring the slug prefix is included before the encoded ID.
- Around line 32-48: The toObjectRelayfilePath switch contains unreachable
provider branches and hardcodes the Gmail prefix for unrelated providers. Since
PROVIDER_SLUG is always Gmail, collapse the switch to the single Gmail path
construction, preserving its current account, threadId, identifier fallbacks,
encoding, and “.json” suffix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cd4c106-3bd4-4d55-83c1-f327673d737d

📥 Commits

Reviewing files that changed from the base of the PR and between d5dcad5 and c6719b7.

⛔ Files ignored due to path filters (2)
  • packages/core/src/inbound/catalog.generated.json is excluded by !**/*.generated.*
  • packages/core/src/inbound/catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (41)
  • AGENTS.md
  • CHANGELOG.md
  • docs/inbound-capability-contract.md
  • packages/core/package.json
  • packages/core/src/cli.ts
  • packages/core/src/inbound/catalog-generator.ts
  • packages/core/src/inbound/golden-vectors.ts
  • packages/core/src/inbound/index.ts
  • packages/core/src/inbound/logical-event-key.test.ts
  • packages/core/src/inbound/logical-event-key.ts
  • packages/core/src/inbound/types.ts
  • packages/core/src/index.ts
  • packages/core/tests/inbound/catalog-generator.test.ts
  • packages/core/tsconfig.json
  • packages/github/package.json
  • packages/github/src/inbound.ts
  • packages/github/src/index.ts
  • packages/gitlab/package.json
  • packages/gitlab/src/inbound.ts
  • packages/gitlab/src/index.ts
  • packages/gmail/package.json
  • packages/gmail/src/config.ts
  • packages/gmail/src/digest.test.ts
  • packages/gmail/src/digest.ts
  • packages/gmail/src/identity.test.ts
  • packages/gmail/src/identity.ts
  • packages/gmail/src/inbound.ts
  • packages/gmail/src/index.ts
  • packages/gmail/src/path-mapper.ts
  • packages/hubspot/package.json
  • packages/hubspot/src/inbound.ts
  • packages/hubspot/src/index.ts
  • packages/linear/package.json
  • packages/linear/src/inbound.ts
  • packages/linear/src/index.ts
  • packages/notion/package.json
  • packages/notion/src/inbound.ts
  • packages/notion/src/index.ts
  • packages/slack/package.json
  • packages/slack/src/inbound.ts
  • packages/slack/src/index.ts

Comment thread packages/core/src/inbound/catalog-generator.ts
@khaliqgant

Copy link
Copy Markdown
Member Author

CodeRabbit outside-diff/nit triage for head 840107aa236ebd641d97db7e03c68c911a35555b:

  • Fixed the real legacy-read gap: parseRelayfilePath now recognizes both /gmail and /google-mail for object and lifecycle paths, with RED-first regression coverage. Canonical writes remain /gmail only.
  • Centralized resource paths on RELAYFILE_ROOT and removed the unreachable non-Gmail switch branches without changing emitted Gmail paths.
  • Did not apply the slug__id filename suggestion. threadId.json is the already-shipped Gmail canonical/writeback layout; renaming it here would be a breaking mount migration requiring compatibility aliases and an explicit versioned policy, not a safe nit for this identity cut. That naming debt should be handled separately if desired.
  • The generic 80% docstring warning is not a repository gate and does not identify a functional defect; no mass docstring churn added.

Targeted exact-worktree validation before push: core build + 181/181 core tests + inbound catalog check; Gmail build/typecheck + 16/16 tests.

@khaliqgant

Copy link
Copy Markdown
Member Author

Follow-up proof on exact head 097640e73078e27a22b531988c13529e67f093b5: added the requested RED-first mixed-provider package regression and routed generation through the exported package validator. The test proves one adapter package cannot declare gmail and google-mail even when their roots are otherwise globally non-colliding. Core is 182/182 locally; exact-head full CI run 30176352052 is GREEN. CodeRabbit itself marked r3651095583 “✅ Addressed in commit 840107a”; the thread was resolved only after that code landed.

@khaliqgant
khaliqgant merged commit c29865c into main Jul 25, 2026
3 checks passed
@khaliqgant
khaliqgant deleted the feat/relayfile-ingest-p0-contracts branch July 25, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant