Skip to content

Decision index

Every dated decision, one line each, pointing at the task that holds the full reasoning. This page is pointers only β€” never the reasoning itself, and never a conversation transcript.

Why this exists, and why it is not a transcript

The owner asked (2026-07-25) whether the working conversation should be stored on the board so decisions could be referenced later. The capture already existed β€” AGENTS.md requires every decision to be recorded in its task file with the owner's words quoted, dated and attributed. What was missing was findability, not capture. A transcript would have made things worse: it can never be updated when a decision is superseded, it preserves dead ends and corrections next to conclusions so a reader cannot tell which is current, and the board is a published site β€” task files are written knowing that, raw conversation is not.

So: when a decision changes, edit the line here and the task. A superseded row keeps its history via the task's own strike-through record (AGENTS.md), not by leaving a stale line on this page.

How to add a row

One line, newest at the top of its section: date Β· the decision in a single clause Β· [[T-NNN]]. If you cannot state it in one clause, it belongs in the task, not here.


People, profiles and naming

Date Decision Task
2026-07-31 A person's document name order is a stored preference (legal.legalNameOrder = givenFirst | surnameFirst), flipped from a Contacts drawer toggle; the stored director closingName/title are retired β€” the name derives from legal parts + this order (+ signsAs), the signature job line from isDirector T-196
2026-07-30 users/{uid} groups role / badges / subsidiary access / status under an access map T-188
2026-07-30 The invite-time abbreviation is reserved (claimed at send), not just checked β€” uniqueness spans individuals + pending invitations, released on revoke/expire T-188
2026-07-30 The preferred name keeps first + last, stored as a preferredName.{firstName,lastName} map (matching legalName/chineseName); a preferred/nickname is never allowed on a signable document β€” greeting-only, enforced in the naming validator T-188
2026-07-30 Name fields use a map named for the qualifier with plain part fields (legalName.firstName, not legalName.legalFirstName) β€” no requalifying prefix; a single-string name (preferredName) stays scalar T-188
2026-07-30 The Chinese name migrates from flat chineseLastName/chineseFirstName to a chineseName map, to match the legal name's shape T-195
2026-07-27 Badges are system-wide, the org chart is personnel-wide: a badge names an ability and never a person, the chart names a relationship and never grants an ability β€” a permitted action needs both T-192
2026-07-27 Seniority never widens what someone may do β€” the chart can only narrow which people a badge-granted ability is exercised on, so it can't become a back door into access T-192
2026-07-27 Approvals route to anyone ranked above the requester, not to one named line manager β€” the single approver is both a setup burden and a single point of failure T-190
2026-07-27 The RBAC user record and the Individual are combined in labour, not in storage: users/{uid} keeps access facts only, individuals/{id} becomes the only home of person-facts T-188
2026-07-27 Users may input a blank field on their own record but not change it once set β€” enforced server-side, amendments go through a bookkeeper T-188
2026-07-27 The inviter assigns the new user's abbreviation (and their names) at invite time β€” the id.slice(0,8) placeholder is retired; self-onboarding users still may not set it, because uniqueness needs directory-wide sight T-188
2026-07-27 A user may type their own nickname / preferred name and use it as a name token β€” a scoped exception to T-161's no-self-typed-name-parts rule T-188
2026-07-27 No preferred LAST name β€” the nickname is a single field; document renderings fall back to legal-name combinations T-188
2026-07-27 Legal information appears on a user's own profile read-only, after it has been entered into the system T-188

Expense documents, receipts and service invoices

Date Decision Task
2026-07-28 CJK glyph fidelity in the Gemini receipt redraw is handled by prompt-level fidelity clauses, chosen over a text-drift verification gate β€” residual mangling accepted since parsed data and the retained original never depend on the redraw I-037
2026-07-25 Orphaned company-card receipts alert the submitter and the bookkeeper via a new notification topic T-182
2026-07-25 An auto-match hook is mandatory at receipt submit time β€” the owner submits company-card receipts and knows the statement descriptor T-182
2026-07-25 Payment method is declared by the submitter: a Telegram /command on an already-submitted receipt, plus a per-receipt flag on the web Review step β€” chosen over a caption flag so a typo cannot mis-file silently T-182
2026-07-27 Receipts and Service Invoices are separate top-level tabs, each mounting only its own upload pipeline; the merged expense view is retired as a user-facing surface β€” the combined feed survives kind-less as a data layer only T-180
2026-07-25 Receipts are for reimbursements, Service Invoices are for charges already made on the company account β€” separate tabs, and the unified expense view stops being a front door T-180
2026-07-25 Service Invoices get their own pipeline: PDF only, no cropping, no substantiation note, auto-match only on an operator-entered pattern T-181

Categories and the chart of accounts

Date Decision Task
2026-07-27 Postal expenses get a postage category mapped to existing 6300 β€” chosen over a dedicated "6310 Postage & Courier" account; the chart of accounts is untouched T-189

Reimbursement payments and the WOPC

Date Decision Task
2026-08-04 The company funding account is derived, never stored (Option A): an Airwallex-originated payout can only draw from the org's wallet in the transfer currency (the API names no source account), and a manually-paid payout's account is captured at match time from bankTx.bankAccountId β€” the channel decides, so a stored "intended source" field would duplicate a derivable fact T-199
2026-08-04 A 2110 reimbursement payout and its WOPC point at each other with references only: the tx carries gl['2110'].wopcRef (the ONLY field β€” the T-193 references-not-copies contract) and the WOPC carries transaction.id β€” the same pair the 5050 pipeline stores. The payout links the pre-created WOPC, never creates one (updateWOPCMatched, not createWOPC); the T-192 auto-match guard is lifted T-193
2026-08-01 A WOPC's whole state collapses into one honest status enum β€” draft Β· standBy Β· awaitingSignature Β· processing Β· paid Β· error Β· voided Β· deleted β€” retiring the misleading active/pending_transaction and the separate signed/void/deleted side-fields; a reimbursement can finally read paid (design signed off, build phased) T-199
2026-08-01 The reimbursement payout transaction.id moves onto the WOPC (one payout per WOPC β€” a WOPC is never split; receipts keep their wopc.ref backlink), so WOPC status reflects the receipts β€” refines the 2026-07-28 "option 2" receipt-only link (build Phase 2, rewires the derived 2110 journals) T-199
2026-08-01 No paidAt/paidVia marker β€” the linked transaction.id already carries the paid date and the payment kind; the owner rejected redundant fields in favour of the honest status T-199
2026-08-01 A paid WOPC is terminal β€” it can never be voided; there is no clawback, a refund would be a new transaction/document, never a void of the paid WOPC T-199
2026-08-01 Signing a WOPC auto-fires its payout (no confirm beat) β€” safe only behind an idempotency interlock (a WOPC-derived key + the processing lock), which is why T-185's can't-pay-twice becomes mandatory rather than optional T-199
2026-07-31 WOPC signing authority is not limited to directors β€” a director, or any person the directors authorise, may sign; granting it gets its own permission (wopc_signers:manage) and the signer picker offers {directors + authorised signers} βˆ’ payee. The mark is system.authorizedSignature {authorized, grantedBy, grantedAt}. (Claimed as T-195, renumbered T-196 on merge.) T-196
2026-07-30 A reimbursement WOPC line prints its category's documentation label ("F&B", "Out-Job Consumables") via the shared categoryLabelFor map, not the stored slug β€” the snapshot carries the words the document prints T-194
2026-07-30 A reimbursement's recipient block sits at the paginated height on every page (one-pager or multi-pager) so the address lands in the same No.5 window-envelope window regardless of page count β€” the measured single-page position sat too high T-194
2026-07-30 A paginated reimbursement's first page drops its receipt table two lines and centres the opening between the recipient and the intro T-194
2026-07-30 A reimbursement one-pager shifts everything below the recipient down two lines (the salutation was cramped against the lowered recipient), which lowers its capacity from nine rows to eight; the one-pager measures capacity directly against the footer (maxReimbursementSingleRows) since its closing follows the payment block, not the anchored position T-194
2026-07-30 A reimbursement's closing follows the payment block β€” a line below the bank block on an unsigned one-pager, pulled tight to the bank block on a SIGNED one-pager (the taller signed closing would else hit the footer after the two-line shift), and three lines below on a busy paginated last page β€” instead of the template's bottom anchor; contractor fees keep the anchor T-194
2026-07-30 Reimbursement WOPC document polish (post-deploy): a middle page holds 20 one-line rows (whole page, no recipient/total/closing β€” REIMBURSEMENT_CAP_MIDDLE = 20 vs contractor 15) and centres that list between the page-number line and the footer; the amount column pins the "$" to a fixed left column with figures right-aligned; and the total-row "(HKD)" no longer wraps T-194
2026-07-29 Reimbursement issuance gains a bank-account picker β€” the operator confirms which account (pre-selected when one on file, an explicit pick required when several); the client only ever sees masked accounts + an identifier, and the server re-resolves the full number T-194
2026-07-29 The reimbursement preview shows a real peeked reference number (non-reserving, minted only at issue β€” the 5050 mechanism), retiring the β€’β€’β€’ placeholder; no gap risk T-194
2026-07-29 WOPC recipient line spacing loosened 1.0 β†’ 1.2 (ADDR_PITCH 11 β†’ 13.2) β€” an owner-directed departure from the measured template, applies to every WOPC T-194
2026-07-27 The abbreviation stays bookkeeper-owned β€” users can self-serve their legal name and bank accounts but never their reference code, which must be unique across everyone the company pays T-183
2026-07-27 One definition of how a bank account is stored: the Contacts editor's inline normalisation was extracted so self-service and bookkeeper writes produce identical documents T-183
2026-07-27 A Payment Confirmation is refused when the payee's abbreviation is still the sign-up placeholder, not merely when it is blank β€” it keys payees/{abbr}, the directory and cross-issuance (corrected: since #664 the reference number is ERL-WOPC/{YYYY}-{NNN} and never prints it β€” T-188 finding) T-183
2026-07-27 Payment readiness is derived from the Contacts record on every read, never stored β€” no paymentReady flag to go stale or backfill T-183
2026-07-25 The transaction's GL code is the discriminator β€” 6xxx means the company paid the merchant, 2110 means it repaid an employee β€” so no settled-payout marker field is needed T-178
2026-07-25 Receipt and transaction each answer their own question: the receipt owns "was I repaid, by which payment", the transaction owns "what kind of payment am I" β€” no duplicated fact, so nothing can disagree T-178
2026-07-25 Document-link information moves under gl; flat document-root fields are avoided, and unmatching a transaction must wipe GL information with it T-178
2026-07-25 The WOPC table keeps its original single rule under the header row, not the full grid the measured template draws β€” a deliberate departure from the template T-178
2026-07-25 Contractor tables render at full type size wherever the page allows, stepping down only for a crowded one-pager T-178
2026-07-25 WOPC pagination is a fixed-count rule β€” 9 / 10Β·10 / 10Β·15Β·10 β€” not a content-height engine (corrected 2026-07-30: the reimbursement one-pager is now 8, not 9 β€” the single-page body shift for the window envelope cost a row; the 10Β·10 / 10Β·15Β·10 multi-page shape is unchanged β€” T-194) T-178
2026-07-25 Non-related-party WOPCs close unsigned with the company-level "For and on behalf of Establish Records Limited"; only related parties are signed and sealed T-178
2026-07-25 The closing's job title comes from Individual.system.position β€” no new field on the RBAC user profile, which stays an access record T-178
2026-07-25 Reimbursement WOPCs are sequenced in the shared ERL-WOPC series, resolved via email β†’ Individual β†’ abbreviation using the directory's existing payee write-through T-178
2026-07-28 Receipts settle automatically at the moment the co-director signs β€” the sign-completion hook settles every backlinked receipt through the same core the manual settle uses; the payment queued behind the signature releases at the same moment (existing purpose-agnostic queue) T-178
2026-07-28 The backlink is the reimbursement discriminator β€” a WOPC with receipts naming it is a reimbursement document; the proposed stored WOPC.purpose field is withdrawn as unnecessary T-178
2026-07-28 When a released payment syncs back, auto-match assigns GL 2110 (not contractor fees) to a reimbursement WOPC's payout β€” confirmed automatic by the owner over manual or propose-only ("2=A"), mirroring how fee payouts have matched since T-168 T-178
2026-07-28 A reimbursement payout transaction records nothing about the receipts it repaid β€” the link lives only on the receipt (settlement.bankTxId); the transaction owns only its GL code (2110). The payout unassign reopens its receipts by query. Owner chose "option 2" over storing gl.receiptIds (Refined 2026-08-04: the payout tx now also carries the DOCUMENT reference β€” gl['2110'].wopcRef, T-193 β€” still nothing about receipts) T-178
2026-07-28 A transaction stores its own facts and a reference, not a copy of detail the attached document owns β€” a WOPC-matched tx reads payee/contractor/bank/project detail from the WOPC, not from denormalised gl[code] copies that go stale and are lossy for multi-project WOPCs (owner: "if it's stored elsewhere and can be referenced elsewhere, drop it from the tx") T-191
2026-07-28 A 2110 reimbursement transaction's gl['2110'] node stores only wopcRef β€” no denormalised payee/contractor/bank/project copies; the WOPC is the single source (the T-191 references-not-copies contract, applied to T-178's new 2110 write path) T-192
2026-07-28 Receipt metadata's twelve loose scalars regroup into review / accounting / settlement / matching / wopc maps β€” fields that answer the same question live in the same map; the document ROOT stays untouched (shared document-store shape) T-178
2026-07-28 Processed receipts are read-only: once confirmed, no editing, AI re-run or deletion β€” server-refused and buttons hidden; a receipt that entered the books cannot drift from what was posted T-178
2026-07-28 A reimbursement WOPC to a director goes through the same another-director signing pipeline as fee WOPCs β€” the payee decides the procedure, the document type only decides the content; non-director reimbursements stay unsigned T-178
2026-07-28 For a director's reimbursement the signature gates the settlement β€” receipts cannot settle until the co-director signs; the books deliberately wait for the control (chosen over settle-now-sign-later) T-178
2026-07-28 The 2110 settle leg (DR 2110 / CR bank) is owned by the payout transaction when one is linked β€” settling assigns GL 2110 to that transaction; the receipt owns the leg only for unlinked settles. Exactly one owner per settle, in every data state T-178
2026-07-28 Unassigning a reimbursement payout resets its receipts β€” removing GL 2110 from a transaction wipes its document links and reopens the receipts it settled, so the books and the receipts can never disagree T-178
~~2026-07-24~~ ~~GL 2110 is excluded from the Match-Transaction dropdown β€” the payable is derived from receipt data and a direct match would double-post~~ Superseded 2026-07-28: with transaction ownership a direct 2110 match posts once, so the dropdown allows it again T-178

Working rules (apply to every agent)

Date Decision Where
2026-07-27 Firestore documents group related fields into maps β€” flat layouts are avoided, and a new field is proposed under a parent naming what kind of fact it is AGENTS.md
2026-07-25 Firestore structure changes need the owner's approval first β€” whole document current-vs-after, new field under its immediate parent, plus the existing fields considered and rejected AGENTS.md
2026-07-25 Decisions are indexed here, recorded in full in the task β€” no conversation transcripts on the board this page
2026-07-28 Phase wrap-up reports are written for the owner as an end-user: what the app can now do in plain words first, function names only as pointers after, and every touched Firestore document's structure before β†’ after (or an explicit "no structure change") AGENTS.md
2026-07-25 A "decision" is a choice that constrains future work β€” it closed off a real alternative, sets a convention, touches data shape/money/outbound documents, or would annoy you if silently reversed AGENTS.md
2026-07-31 T-195 person-record dual-read is flat-authoritative β€” the legacy flat field wins while it exists; the new grouped map is read only once the contract deletes the flat field (chosen over map-first so a lagging map can never win on the auth path) T-195
2026-07-31 T-195 person-record migration executed in production β€” users/{uid} + individuals/{id} moved to the approved grouped-map layout; old flat fields removed, and every writer persists the grouped shape natively (the reconcile mirror is deleted β€” owner: "no adaptor"); only the free-text organization Profile box awaits an owner ruling T-195
2026-08-06 Dependency vulnerabilities are cleared by targeted semver upgrades, never npm audit fix --force β€” majors that need real testing (googleapis, puppeteer+chromium, eslint 9, storybook 9) are deferred to their own tasks rather than swept in behind a security fix T-201
2026-08-06 sharp is pinned tree-wide via overrides, not just as a direct dependency β€” next optionally pins its own sharp ^0.34.x for image optimization, and without the override that second, vulnerable copy is the one a self-hosted/NAS deploy actually executes T-201
2026-08-06 vite is pinned forward to ~8.1.5 by override rather than re-resolved β€” dropping the nested lockfile entry let it fall back to vite 6, which cleared the advisory but broke vitest's config loader; ~ not ^ keeps it off vite 8.2's rolldown bundler swap T-201
2026-08-10 A vulnerable shared transitive is fixed once by override, not by bumping each parent β€” overrides.uuid = ^11.1.1 retired 11 moderates in one line, where npm's per-parent "fixes" were downgrades of @google-cloud/storage and exceljs; capped at ^11 because uuid v12 drops CommonJS T-202
2026-08-10 Storybook was kept and upgraded 8 β†’ 10 knowing it clears no alert β€” image-size has no patched release at any version and Storybook still depends on it at 10.5.7, so the 7 dev-only alerts are permanent while Storybook is a dependency; chosen over removing Storybook (owner's call, tooling kept) T-202
2026-08-10 The repo now effectively requires Node β‰₯ 20.19 / 22.12 β€” Storybook 10 refuses to start below it, and npm silently skips engine-gated optional native bindings on an older Node, which is what made vitest and Storybook appear broken rather than under-installed T-202