Map sources to one shape
Yesterday's copy holds GitHub issues in GitHub's shape. Before a second source arrives, you give each concept one shape, so that tools and rules are written once for every source.
Study first
- Keep one internal model per concept. The mapping from each provider lives in code, and the provider's record stays whole in a
rawfield. A field that only one provider has stays inrawuntil a second provider needs it, and then it joins the shape. - A changed shape is a migration. WorkBot's convention is that a create statement stays as it was on the day it was written, and the columns a later day needs are appended as idempotent statements. A step that must run once guards itself on what it finds, in a
doblock such asdo $$ ... 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 comes in upper or lower case, and only the delivery carries
html_url. SofromIssuereads both, and a test holds the 2 results equal.
Build
- Write
src/lib/unified.tswith theTicketandContacttypes and a mapping per source.fromIssuemaps a GitHub issue, andfromLoginmaps the people on tickets.fromRequestmaps a request, whose team is already set, so no rule routes it. - In the same file,
fromDocumentmaps a document to a contact. It takes the vendor of an invoice, the From of an email, and the writer of a letter with the fields its form carries. A job posting maps to nothing. - Add
upsertTicketandupsertContact, which run the rules and record what changed inapplied. Invoices are the exception, becausesaveDocumentruns their rules. - Key a document contact by 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.
- Add the vendor's email and phone to the invoice schema, both nullable, so WorkBot can reach a sender. The extraction eval (Day 17) compares only the fields each case names, so its score does not move.
- Write the migration in
src/lib/schema.ts, which appends 4 columns to Day 27's table. A block then re-keys the table's rows under their source and drops the issue number, once. - Write tests that map the 2 issue shapes equal, map a request with its team, and map a document of each kind, or not. Test that a contact seen twice is one row, and that a ticket is stored with the team the rules gave it. Test that a version older than the row's is dropped.
Done when
find_ticketandfind_contactread the one shape only.- The same issue, listed or delivered, makes one row.

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


