ARCHITECTURE.md
Language Markdown8.5 KB167 lines
# ArchitectureMeridian Display Co. is a fictional custom print and display shop. Thisrepository is the operations board its small team runs on, rebuilt as aportfolio piece: the company, the people, the clients and every piece of dataare invented. The engineering is real.## What it doesThe board reads six sources and turns them into one page per person, plus adistilled payload that a model turns into written morning briefings:| Source | What it holds | Client ||---|---|---|| Plinth | orders and product lines, the shop's own order backend | `packages/plinth-client` || Bitrix24 | CRM leads and tasks | `packages/bitrix-client` || Ninety | EOS to-dos, company-wide | `packages/ninety-client` || Google Sheets | the content tracker, read from its public CSV export | `packages/sheets-client` || Fathom | meeting action items (never transcripts) | `packages/fathom-client` || Gmail | the shared role mailboxes, read and reply | `packages/gmail-client` |```config/ rosters, stage taxonomies, topic buckets, onboardingplans, access grants: the human-edited knowledgeapps/dashboard/ collect -> view-model -> surfaces -> dashboard.htmlapps/worker/ Cloudflare Worker: cron sync, serves the pages, /inboxapps/worker/migrations/ D1 schema: the end-of-day note and the private listanalysis/brief.py the only place a model is called````collect.ts` decides what the numbers **are**. `view-model.ts` decides what they**look like**. Keep that split.The Worker sits behind Cloudflare Access. It verifies the Access assertion,maps the verified address to a roster person, and renders only what thatperson may see. Pages are self-contained (`default-src 'none'`), which is whythe fonts are embedded as data URIs.## The prime invariant**Zero, null, and broken must never look the same.**- `0` means "we looked, there is nothing": free capacity.- `null` means "no source covers this": it renders as "not tracked", never as 0.- A failed source renders as **failed**, with its error, never as 0.Every panel, tile, count and payload field obeys this. If a change makes anabsent number render as a zero, the change is wrong, however convenient.## RulesEach of these came from a real bug. The comments in the code say what each oneprevents at that spot; the code refers to these by number.1. **Never state a cause you cannot observe.** An auth error handler once said atoken had "most likely expired" about a token with months left, and thefix it recommended would have invalidated a working credential. Report whathappened, list candidate causes, do not pick one.2. **Do not assert a default and forget it.** Unknown lead stages oncedefaulted to "ours" so they would get noticed. Nothing reported them, so mostof the pipeline silently became work the team owed. A default that stands infor knowledge must be surfaced every run.3. **Prefer the source's own semantics over a hand-maintained list.** Bitrixexposes `STATUS_SEMANTIC_ID` (P/S/F). A local stage table always lags the CRM.4. **Be strict about the fields decisions are made from, permissive about therest.** A schema that was strict about a field nothing reads discarded areal share of live records. On records you do not control, unread fields are`.nullable().optional()`.5. **Truncated and labelled beats absent.** A capped list returns what it hasplus `truncated: true`, and the caller says so. Never throw away fetched datato guard against a hypothetical.6. **Changing what a number MEANS invalidates its history.** `history.ts` has`DEFS_VERSION`; bump it, or the next run reports a swing that never happened.7. **`node:fs` must not be reachable from the Worker.** A static import resolveseven behind `import type` and breaks the Worker build. Node-only IO lives in`*-file.ts`; everything the Worker imports stays runtime-neutral.8. **A tool that fails silently is worse than one that fails loudly, includingyour own tools.** Two raw control bytes in a source file made `grep` classifyit as binary and print nothing for it, with no error. Control characters insource are written as escapes, and `source-guards.test.ts` enforces thatacross the tree.9. **A number and the list under it must be able to account for each other.**A card once counted to-dos in its header but built its list from orders andtasks only, so it read "13" and then "nothing on this board". Where a surfacedeliberately withholds a row, it says how many and why.10. **Take the most accurate source available; flag only when there is no goodone.** If a better source exists, use it. If none does but the source isadequate, use it. Only when every source is flawed is it worth flagging, ora flag stops meaning anything. This does not loosen the prime invariant: asource that failed still renders as failed.11. **A source you cannot date is not a source you can call fresh.** The contenttracker is read from a public CSV export that sends no `Last-Modified`, andit is the one input a person types by hand. Its pill says `read`, never`live`, and its note says staleness is not tracked.12. **The board owns a small set of state and one outbound write, and all of itfails safe.** There is no write-back to Plinth, Bitrix, Ninety or the tracker.Gmail is the one exception: a reply or a new message can be sent from theInbox tab. A send cannot be undone, so it clears a higher bar: an explicitper-person send grant that is not the read grant, a confirmation page, anonce against double submission, a recipient cap that refuses rather thantrims, and an audit record written whether the send succeeds or fails.Replies derive their recipients on the server from the original message.Attachments cross the confirmation step in the form, never at rest in KV,and every download is served as `application/octet-stream`. Nothing thatidentifies the person behind a shared mailbox goes into the outgoing mail.Every other piece of board state (sender filters, read marks, notes,signatures, replied marks) has a safe fallback and is reversible from thepage that wrote it.13. **An access list is not a default you can guess, and a grant that cannot bereached is worse than one that is missing.** Every permission defaults tofalse: getting less than you should is a bug report, getting more is anincident. Every grant is an explicit human-written line, never a loop overthe roster. Grants keyed on a role mailbox follow the mailbox when it changeshands, which is correct only while address and person match, so re-read themat every handover. A send grant without a read grant is inert, so a testasserts every sender is also a reader.14. **State that is a series belongs in a table, and a store that is absent isnot a store that is empty.** The end-of-day note is a row a day, read back asa date range, so it lives in D1 rather than as one KV blob. The binding isoptional, and four states get four different sentences: not set up, a storethat refused, a day with no note, and a note. The reader and author lists arehand-written and derived from nothing, because the day they diverge from anyrole is the day a derivation would have been wrong.15. **A store with one reader is the one place a grant list would be wrong.** Theprivate task list has exactly one reader, its author, so there is no grant towrite. Every statement is scoped `AND person = ?` with the name taken from theverified identity, never from the request. It is filed under the roster name,not the address, so the next holder of a role mailbox cannot read the lastone's list. Its count never joins the shared workload number, and it isstamped into the page at serve time, never written into cached pages.## Verification```bashpnpm installpnpm test # every TypeScript suite, then the Python suitepnpm typecheckpnpm dashboard --fixtures # renders from invented data, no credentials```## Testing conventions- Fixtures are the contract: real structure, invented values.- Tests assert **properties**, not wording.- When a test comes from a bug, its comment states the failure it prevents,not the history of how it was found.## CredentialsProcess environment only. There is no `.env` reader and there must not be one:a credentials file in the working tree can be read by any tool or agent thattouches the repo, and a secret that has been read has to be rotated.`credentials.example.sh` is the template for a shell profile.