Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 92 additions & 93 deletions docs/contact-attribution.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,92 @@
# Contact import attribution

Contacts retain separate records of creation, submissions, declared import sources,
provider ingestion and subsequent contact or relationship edits. Relationship owner
remains the person responsible for the work. Changing ownership does not rewrite
who submitted the contact.

For example, if one member uploads another member's list, the authenticated uploader
is the submitter and the other member is an explicitly **declared source**. The source
declaration is not proof of original authorship. When a member's assistant submits
contacts through verified OAuth MCP, the member, transport, verified client ID and
grant ID come from authentication. Caller-supplied actor claims cannot override them.

Open **Details → Import attribution** in a contact record or inspector to read the original
recorded creator visible in the selected product, most recent recorded editor and
paginated contribution history.
**Record import source** opens an inline form for a reviewed declaration on a
known contact. People
filters distinguish **Submitted by**, **Declared source**, coverage and multiple
contributors independently of current ownership. Multiple contributors means more
than one distinct recorded actor or declared source across visible submissions.
Edits do not count as imports. Filters respect the selected product. Archived
contact records retain readable attribution, with new declarations disabled.

Existing contacts without recorded provenance remain **Unknown**. The migrations
add tables and constraints without rewriting people, relationships, notes, owners
or versions. Historical owner and creation timestamps are insufficient to infer an
importer. Deactivated members retain their recorded names; they lose permission to
submit new data. Names come from the stored member profile; immutable member IDs
retain identity if a profile is renamed. This is recorded history from this feature
onward, not a complete reconstruction of older activity.

## MCP and HTTP import workflow

1. Call `create_contact_import_batch` with `organizationId`, `productId`, a readable
`label`, `sourceKind` (`file`, `agent` or `manual`), and a stable `submissionKey`.
Optionally pass `sourceMemberId` for a current member with access to the product.
2. Create each new contact with `create_person` and
`importSource: {batchId, sourceRecordId}`. Keep each source row ID stable. Exact
retries return the same contact and relationship; changed payloads for an already
recorded source row return `IMPORT_SOURCE_CONFLICT`.
3. For a confirmed existing contact already related to the product, call
`record_contact_import` with its current `version`, `batchId` and `sourceRecordId`.
This appends provenance without replacing its original creator or owner. Exact
retries remain safe after later edits. Do not automatically merge contacts by name.
4. Inspect `get_contact_attribution` with a person ID and optional product ID. It
returns `creator`, `lastEditor`, `items`, `nextOffset` and coverage. Pass the next
offset to read more history. `list_records` for people supports `submittedBy`,
`sourceMemberId`, and `attribution: recorded | unknown | shared`.

The shared operation registry maps these to `/api/crm`: POST `import-batch`, POST
`contact-import`, GET `contact-attribution`. Both transports enforce the same
membership, grants, product, version and retry rules. A batch belongs to its
authenticated submitter; another member cannot reuse it. Stable keys identify
retries, not authorization. The source form retains retry keys for unchanged
declarations after a partial failure. Correcting the batch label, kind or declared source creates a new batch;
existing batches remain immutable. No general CSV parser or new bulk upload UI is
included; agents can submit parsed rows using these operations.

## Provider sources and privacy

Materialized provider messages and meetings record the source connection owner and
provider. Background sync records a system event rather than attributing each event
to a human submitter. An explicit link records the authenticated linking actor,
including MCP identity when applicable. Duplicate provider receipts add no duplicate
contribution. Pending unmatched items become contributions when linked to a contact.

Provider source details and their filter identifiers remain private to the account
owner, even when a conversation has separately been shared. Attribution records
contain no message body, credential or external account handle. Product-shared
manual declarations are visible to authorized product members.

This feature does not change sequence approval, provider consent or sending gates.
Sender and approver remain recorded by existing outreach and delivery operations;
this contact panel is not a unified outbound activity log. Attribution is append-only
through the business API; database administration remains able to alter stored data.

## Migration and validation

Apply generated migrations `0023` and `0024` before running this application version.
They introduce import batches, contributions and same-tenant/product constraints.
Production rollout still requires reviewing the target and backup policy. Tests cover
populated upgrades, retry collisions, actor spoofing, shared submissions, revoked
membership, private accounts, product isolation, MCP provider links and the UI flow.

## Fictional demo screenshots

The local review uses fictional members and contacts. It shows a declared source
without replacing an unknown historical creator, plus the matching People filter.

![Contact history](assets/screenshots/contact-attribution-history.jpg)

![Declared-source filter](assets/screenshots/contact-attribution-filters.jpg)
# Contact import attribution

Contacts retain separate records of creation, submissions, declared import sources,
provider ingestion and subsequent contact or relationship edits. Relationship owner
remains the person responsible for the work. Changing ownership does not rewrite
who submitted the contact.

For example, if one member uploads another member's list, the authenticated uploader
is the submitter and the other member is an explicitly **declared source**. The source
declaration is not proof of original authorship. When a member's assistant submits
contacts through verified OAuth MCP, the member, transport, verified client ID and
grant ID come from authentication. Caller-supplied actor claims cannot override them.

Open **Details → Import attribution** in a contact record or inspector to read the original
recorded creator visible in the selected product, most recent recorded editor and
paginated contribution history.
**Record import source** opens an inline form for a reviewed declaration on a
known contact. People
filters distinguish **Submitted by**, **Declared source**, coverage and multiple
contributors independently of current ownership. Multiple contributors means more
than one distinct recorded actor or declared source across visible submissions.
Edits do not count as imports. Filters respect the selected product. Archived
contact records retain readable attribution, with new declarations disabled.

Existing contacts without recorded provenance remain **Unknown**. The migrations
add tables and constraints without rewriting people, relationships, notes, owners
or versions. Historical owner and creation timestamps are insufficient to infer an
importer. Deactivated members retain their recorded names; they lose permission to
submit new data. Names come from the stored member profile; immutable member IDs
retain identity if a profile is renamed. This is recorded history from this feature
onward, not a complete reconstruction of older activity.

## MCP and HTTP import workflow

1. Call `create_contact_import_batch` with `organizationId`, `productId`, a readable
`label`, `sourceKind` (`file`, `agent` or `manual`), and a stable `submissionKey`.
Optionally pass `sourceMemberId` for a current member with access to the product.
2. Create each new contact with `create_person` and
`importSource: {batchId, sourceRecordId}`. Keep each source row ID stable. Exact
retries return the same contact and relationship; changed payloads for an already
recorded source row return `IMPORT_SOURCE_CONFLICT`.
3. For a confirmed existing contact already related to the product, call
`record_contact_import` with its current `version`, `batchId` and `sourceRecordId`.
This appends provenance without replacing its original creator or owner. Exact
retries remain safe after later edits. Do not automatically merge contacts by name.
4. Inspect `get_contact_attribution` with a person ID and optional product ID. It
returns `creator`, `lastEditor`, `items`, `nextOffset` and coverage. Pass the next
offset to read more history. `list_records` for people supports `submittedBy`,
`sourceMemberId`, and `attribution: recorded | unknown | shared`.

The shared operation registry maps these to `/api/crm`: POST `import-batch`, POST
`contact-import`, GET `contact-attribution`. Both transports enforce the same
membership, grants, product, version and retry rules. A batch belongs to its
authenticated submitter; another member cannot reuse it. Stable keys identify
retries, not authorization. The source form retains retry keys for unchanged
declarations after a partial failure. Correcting the batch label, kind or declared source creates a new batch;
existing batches remain immutable. Reviewed CSV imports with preview, column mapping, and duplicate review are documented in [CSV imports](csv-imports.md); agents can also submit parsed rows using these operations.

## Provider sources and privacy

Materialized provider messages and meetings record the source connection owner and
provider. Background sync records a system event rather than attributing each event
to a human submitter. An explicit link records the authenticated linking actor,
including MCP identity when applicable. Duplicate provider receipts add no duplicate
contribution. Pending unmatched items become contributions when linked to a contact.

Provider source details and their filter identifiers remain private to the account
owner, even when a conversation has separately been shared. Attribution records
contain no message body, credential or external account handle. Product-shared
manual declarations are visible to authorized product members.

This feature does not change sequence approval, provider consent or sending gates.
Sender and approver remain recorded by existing outreach and delivery operations;
this contact panel is not a unified outbound activity log. Attribution is append-only
through the business API; database administration remains able to alter stored data.

## Migration and validation

Apply generated migrations `0023` and `0024` before running this application version.
They introduce import batches, contributions and same-tenant/product constraints.
Production rollout still requires reviewing the target and backup policy. Tests cover
populated upgrades, retry collisions, actor spoofing, shared submissions, revoked
membership, private accounts, product isolation, MCP provider links and the UI flow.

## Fictional demo screenshots

The local review uses fictional members and contacts. It shows a declared source
without replacing an unknown historical creator, plus the matching People filter.

![Contact history](assets/screenshots/contact-attribution-history.jpg)

![Declared-source filter](assets/screenshots/contact-attribution-filters.jpg)
44 changes: 44 additions & 0 deletions docs/csv-imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Reviewed CSV Imports

Gravity supports reviewed CSV imports for contacts and companies with column mapping, duplicate detection, and final summaries, matching Slice 2 of the roadmap.

## Overview & Policy

- **No silent merges**: Rows whose person email or company domain already exists within the organization (or duplicates within the same file) are highlighted for review. They are never silently overwritten or merged. Attempting an import without skipping duplicates explicitly rejects with `PERSON_EXISTS` or `COMPANY_EXISTS`.
- **Product relationship**: Every imported contact must be bound to a selected product with a defined purpose (`buyer` or `partner`).
- **Attribution & Provenance**: Imports create a batch in `contact_import_batches` with source kind `file` (or `agent` via MCP) and record submission attribution.
- **Atomic transactions**: Imports run inside a database transaction with organization locks. If `skipDuplicates` is false and a duplicate is encountered, the entire batch rolls back.

## UI Workflow

1. Navigate to **People** (`/people`) or **Companies** (`/companies`).
2. Click **Import CSV** in the view toolbar, or use the command menu (<kbd>Ctrl</kbd>+<kbd>K</kbd> / <kbd>⌘</kbd>+<kbd>K</kbd>) $\rightarrow$ **Import contacts and companies from CSV**.
3. Select the target **Product relationship** and **Relationship type** (Buyer or Partner).
4. Upload or drop a `.csv` file:
- Headers are parsed in the client using RFC 4180 parsing.
- Column mappings are auto-detected (e.g. `Name`, `Email`, `Title`, `Phone`, `LinkedIn`, `Company`, `Domain`, `Summary / Notes`). You can adjust mappings or set unwanted columns to "Do not import".
5. Click **Preview import**:
- The preview runs `preview_csv_import` against the server without writing any database records.
- Summarizes total rows, valid rows, duplicates for review, and invalid rows.
- Review duplicate conflicts: displays the existing record name, email/domain, and reason why it cannot be merged silently.
- Review validation errors (e.g. missing required person name, malformed emails or domains).
6. Click **Import valid contacts**:
- Executes `import_contacts_csv` with `skipDuplicates: true`.
- Displays the **Import summary** card with counts of people created, companies created, and duplicates skipped.

## MCP & Operations Catalog

Both browser clients and AI assistants execute the exact same reviewed flow via `packages/operations/catalog.ts`:

- **`preview_csv_import`** (POST `/api/crm`, operation `csv-import-preview`):
- Parameters: `organizationId`, `productId`, `csvText`, `columnMapping` (optional), `purpose` (`buyer` | `partner`).
- Returns: summary counts, detected mapping, valid row previews, duplicate rows with collision reasons, and invalid rows with error messages.
- **`import_contacts_csv`** (POST `/api/crm`, operation `csv-import`):
- Parameters: `organizationId`, `productId`, `csvText`, `columnMapping` (optional), `purpose`, `skipDuplicates` (`boolean`), `rowIndices` (optional), `label` (optional).
- Returns: summary with `importedCount`, `peopleCreated`, `companiesCreated`, `relationshipsCreated`, `duplicateCount`, and `invalidCount`.

## Fictional Fixture & Test Suite

The test suite exercises several hundred rows from `tests/fixtures/fictional-contacts.csv` containing exclusively fictional data:
- `tests/csv-import.test.ts`: Validates RFC 4180 parsing, column detection, 271-row preview, duplicate collision detection, rejection on duplicates, atomic transactional rollback, provenance attribution recording, product permissions, and cross-organization isolation.
- `tests/csv-import-ui.test.tsx`: Exercises toolbar launch, file selection, column mapping, preview tabs, duplicate review, and import completion in jsdom.
Loading
Loading