Part 1
One shape, one mapping per source
Study first
- One internal model per concept, with the mapping from each provider in code and the provider's record kept whole in a
rawfield. A field only one provider has stays inrawuntil a second provider needs it; then it joins the shape. - A changed shape is a migration. The convention here: the create statement stays as the day it was written, the columns a later day needs are appended as idempotent statements, and a step that must run once guards itself on what it finds (
do $$ ... if exists (select 1 from information_schema.columns ...) ... $$). - GitHub's list response and its webhook payload differ: labels come as names or as objects, the state in upper or lower case, and only the delivery carries
html_url.fromIssuereads both, and a test holds the 2 results equal.
Build
src/lib/unified.ts: theTicketandContacttypes;fromIssue,fromRequest(a request's team is already set, so no rule routes it),fromDocumentfor the vendor of an invoice, the From of an email, the writer of a letter with the fields a form carries, and nothing for a job posting;fromLoginfor the people on tickets;upsertTicketandupsertContact, which run the rules and record what changed inapplied. Invoices are the exception:saveDocumentruns their rules.- A document contact's key is its email, lower-cased, when it has one, so a sender seen twice is one row; on conflict the upsert fills the fields the row lacks from the new sighting and keeps the rest. The id carries the source, so a login and a sender never merge.
- The invoice schema gains the vendor's email and phone, nullable, so a sender can be reached; the extraction eval compares only the fields each case names, so it does not move.
- The migration in
src/lib/schema.ts: 4 columns appended to Day 27's table, and the block that re-keys its rows under their source and drops the issue number, once. - Tests: the 2 issue shapes map equal; a request maps with its team; a document of each kind maps, or does not; a contact seen twice; a ticket stored with the team the rules gave it; a version older than the row's is dropped.
Done when
find_ticketandfind_contactread the one shape only, and the same issue arriving listed or delivered makes one row.

Technical notes
- A ticket's id is
source#key:github#164,workbot#<uuid>. Day 27 keyed by issue number under the repository's name; the block in the schema rewrites those rows once. - GitHub's own API omits an empty
labelsorassigneeswhere a webhook sends[], and its read of an issue gives label names where a delivery gives objects; the delivery schema takes both. - The ticket upsert accepts a version only when its
updated_atis not older than the row's: an older delivery loses to a newer pull, and the same version twice changes nothing.


