mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

ARCHITECTURE.md

Language Markdown8.5 KB167 lines

docs/ARCHITECTURE.md

  1. # Architecture
  2. Meridian Display Co. is a fictional custom print and display shop. This
  3. repository is the operations board its small team runs on, rebuilt as a
  4. portfolio piece: the company, the people, the clients and every piece of data
  5. are invented. The engineering is real.
  6. ## What it does
  7. The board reads six sources and turns them into one page per person, plus a
  8. distilled payload that a model turns into written morning briefings:
  9. | Source | What it holds | Client |
  10. |---|---|---|
  11. | Plinth | orders and product lines, the shop's own order backend | `packages/plinth-client` |
  12. | Bitrix24 | CRM leads and tasks | `packages/bitrix-client` |
  13. | Ninety | EOS to-dos, company-wide | `packages/ninety-client` |
  14. | Google Sheets | the content tracker, read from its public CSV export | `packages/sheets-client` |
  15. | Fathom | meeting action items (never transcripts) | `packages/fathom-client` |
  16. | Gmail | the shared role mailboxes, read and reply | `packages/gmail-client` |
  17. ```
  18. config/ rosters, stage taxonomies, topic buckets, onboarding
  19. plans, access grants: the human-edited knowledge
  20. apps/dashboard/ collect -> view-model -> surfaces -> dashboard.html
  21. apps/worker/ Cloudflare Worker: cron sync, serves the pages, /inbox
  22. apps/worker/migrations/ D1 schema: the end-of-day note and the private list
  23. analysis/brief.py the only place a model is called
  24. ```
  25. `collect.ts` decides what the numbers **are**. `view-model.ts` decides what they
  26. **look like**. Keep that split.
  27. The Worker sits behind Cloudflare Access. It verifies the Access assertion,
  28. maps the verified address to a roster person, and renders only what that
  29. person may see. Pages are self-contained (`default-src 'none'`), which is why
  30. the fonts are embedded as data URIs.
  31. ## The prime invariant
  32. **Zero, null, and broken must never look the same.**
  33. - `0` means "we looked, there is nothing": free capacity.
  34. - `null` means "no source covers this": it renders as "not tracked", never as 0.
  35. - A failed source renders as **failed**, with its error, never as 0.
  36. Every panel, tile, count and payload field obeys this. If a change makes an
  37. absent number render as a zero, the change is wrong, however convenient.
  38. ## Rules
  39. Each of these came from a real bug. The comments in the code say what each one
  40. prevents at that spot; the code refers to these by number.
  41. 1. **Never state a cause you cannot observe.** An auth error handler once said a
  42. token had "most likely expired" about a token with months left, and the
  43. fix it recommended would have invalidated a working credential. Report what
  44. happened, list candidate causes, do not pick one.
  45. 2. **Do not assert a default and forget it.** Unknown lead stages once
  46. defaulted to "ours" so they would get noticed. Nothing reported them, so most
  47. of the pipeline silently became work the team owed. A default that stands in
  48. for knowledge must be surfaced every run.
  49. 3. **Prefer the source's own semantics over a hand-maintained list.** Bitrix
  50. exposes `STATUS_SEMANTIC_ID` (P/S/F). A local stage table always lags the CRM.
  51. 4. **Be strict about the fields decisions are made from, permissive about the
  52. rest.** A schema that was strict about a field nothing reads discarded a
  53. real share of live records. On records you do not control, unread fields are
  54. `.nullable().optional()`.
  55. 5. **Truncated and labelled beats absent.** A capped list returns what it has
  56. plus `truncated: true`, and the caller says so. Never throw away fetched data
  57. to guard against a hypothetical.
  58. 6. **Changing what a number MEANS invalidates its history.** `history.ts` has
  59. `DEFS_VERSION`; bump it, or the next run reports a swing that never happened.
  60. 7. **`node:fs` must not be reachable from the Worker.** A static import resolves
  61. even behind `import type` and breaks the Worker build. Node-only IO lives in
  62. `*-file.ts`; everything the Worker imports stays runtime-neutral.
  63. 8. **A tool that fails silently is worse than one that fails loudly, including
  64. your own tools.** Two raw control bytes in a source file made `grep` classify
  65. it as binary and print nothing for it, with no error. Control characters in
  66. source are written as escapes, and `source-guards.test.ts` enforces that
  67. across the tree.
  68. 9. **A number and the list under it must be able to account for each other.**
  69. A card once counted to-dos in its header but built its list from orders and
  70. tasks only, so it read "13" and then "nothing on this board". Where a surface
  71. deliberately withholds a row, it says how many and why.
  72. 10. **Take the most accurate source available; flag only when there is no good
  73. one.** If a better source exists, use it. If none does but the source is
  74. adequate, use it. Only when every source is flawed is it worth flagging, or
  75. a flag stops meaning anything. This does not loosen the prime invariant: a
  76. source that failed still renders as failed.
  77. 11. **A source you cannot date is not a source you can call fresh.** The content
  78. tracker is read from a public CSV export that sends no `Last-Modified`, and
  79. it is the one input a person types by hand. Its pill says `read`, never
  80. `live`, and its note says staleness is not tracked.
  81. 12. **The board owns a small set of state and one outbound write, and all of it
  82. fails safe.** There is no write-back to Plinth, Bitrix, Ninety or the tracker.
  83. Gmail is the one exception: a reply or a new message can be sent from the
  84. Inbox tab. A send cannot be undone, so it clears a higher bar: an explicit
  85. per-person send grant that is not the read grant, a confirmation page, a
  86. nonce against double submission, a recipient cap that refuses rather than
  87. trims, and an audit record written whether the send succeeds or fails.
  88. Replies derive their recipients on the server from the original message.
  89. Attachments cross the confirmation step in the form, never at rest in KV,
  90. and every download is served as `application/octet-stream`. Nothing that
  91. identifies the person behind a shared mailbox goes into the outgoing mail.
  92. Every other piece of board state (sender filters, read marks, notes,
  93. signatures, replied marks) has a safe fallback and is reversible from the
  94. page that wrote it.
  95. 13. **An access list is not a default you can guess, and a grant that cannot be
  96. reached is worse than one that is missing.** Every permission defaults to
  97. false: getting less than you should is a bug report, getting more is an
  98. incident. Every grant is an explicit human-written line, never a loop over
  99. the roster. Grants keyed on a role mailbox follow the mailbox when it changes
  100. hands, which is correct only while address and person match, so re-read them
  101. at every handover. A send grant without a read grant is inert, so a test
  102. asserts every sender is also a reader.
  103. 14. **State that is a series belongs in a table, and a store that is absent is
  104. not a store that is empty.** The end-of-day note is a row a day, read back as
  105. a date range, so it lives in D1 rather than as one KV blob. The binding is
  106. optional, and four states get four different sentences: not set up, a store
  107. that refused, a day with no note, and a note. The reader and author lists are
  108. hand-written and derived from nothing, because the day they diverge from any
  109. role is the day a derivation would have been wrong.
  110. 15. **A store with one reader is the one place a grant list would be wrong.** The
  111. private task list has exactly one reader, its author, so there is no grant to
  112. write. Every statement is scoped `AND person = ?` with the name taken from the
  113. verified identity, never from the request. It is filed under the roster name,
  114. not the address, so the next holder of a role mailbox cannot read the last
  115. one's list. Its count never joins the shared workload number, and it is
  116. stamped into the page at serve time, never written into cached pages.
  117. ## Verification
  118. ```bash
  119. pnpm install
  120. pnpm test # every TypeScript suite, then the Python suite
  121. pnpm typecheck
  122. pnpm dashboard --fixtures # renders from invented data, no credentials
  123. ```
  124. ## Testing conventions
  125. - Fixtures are the contract: real structure, invented values.
  126. - Tests assert **properties**, not wording.
  127. - When a test comes from a bug, its comment states the failure it prevents,
  128. not the history of how it was found.
  129. ## Credentials
  130. Process environment only. There is no `.env` reader and there must not be one:
  131. a credentials file in the working tree can be read by any tool or agent that
  132. touches the repo, and a secret that has been read has to be rotated.
  133. `credentials.example.sh` is the template for a shell profile.