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-09-03 | Receipts are noted and filed in a Telegram Mini App sheet listing every waiting receipt at once β chat has no text field, and a batch in chat costs one reply plus one tap per receipt | T-206 |
| 2026-09-03 | The Mini App authenticates on Telegram's signed initData alone (no cookie, no login), and a pendingId is never a capability β ownership is re-checked per receipt |
T-206 |
| 2026-09-03 | Confirm lives only in the sheet β the chat card keeps no button at all, which is what makes retiring the card flow possible; the cost is that approving even one receipt means opening the sheet | T-206 |
| 2026-09-03 | The chat card's lifecycle is retired only after the sheet is proven in production (phased), so there is never a moment with no working path | T-206 |
| 2026-09-03 | The Telegram receipt card is sent with no force_reply β the auto-opening reply box is given up so the card stays editable and can morph through its whole lifecycle, which Telegram forbids for ForceReply messages |
I-041 |
| 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-09-24 | Airwallex connects automatically once the authenticator unlock passes β the Connect button is no longer a step; the exception is right after an explicit Disconnect, which sticks until Connect is pressed | T-205 |
| 2026-09-24 | Choosing how a WOPC is paid is required at creation (Create Pending WOPC, Reimburse, and Airwallex's Pay-with-WOPC new): "pay automatically" runs through ONE server path that checks the code before the WOPC exists, then holds or sends; on the Airwallex page a new WOPC is created and paid in one step, replacing create-then-open-the-payment-drawer | T-184 |
| 2026-09-24 | OCBC pays a WOPC only to an OCBC account (bank code 035), by the same internal transfer the app already makes (ITSO, verified from live traffic); a payee at another bank is paid from Airwallex, and a WOPC can't be planned for OCBC to a non-OCBC payee |
T-184 |
| 2026-09-24 | The OCBC transfer remark for a WOPC is its display reference without # (ERL-WOPC/JC-2026-007) because OCBC's remark rejects # and \| (owner) β ONLY the OCBC remark changes; the WOPC number renders and stores as before, and auto-match already reads that form (verified against past OCBC history) |
T-184 |
| 2026-09-24 | Double-pay guard: before a WOPC is offered or paid, Airwallex's last 60 days of transfers are checked for one carrying its reference that hasn't failed/cancelled; if Airwallex can't be asked, paying is refused. Every WOPC payment is forced to carry the WOPC's own reference. (Extended same day: the books are also checked for a debit naming the WOPC that was never linked β any bank, last 60 days) Chosen over recording immediate payments in Firestore (a new use of the held-payment collection) | T-184 |
| 2026-09-24 | A payment held for a WOPC signature stores no recipient (transferRequest.beneficiary_id dropped); on release or retry the recipient is built from the WOPC's bank details as signed. Chosen over copying the bank details into the held payment (a second copy of the account number) and over keeping a saved Airwallex recipient |
T-184 |
| 2026-09-24 | A WOPC payment's Airwallex recipient is sent inside the payment, built on the server from the WOPC's stored bank details β nothing is saved in Airwallex and no address is sent; the browser sends only the WOPC reference. Chosen over matching saved Airwallex recipients by name (the drawer's old behaviour), letting the payer pick, and (same-day first version) finding/saving an Airwallex recipient by account number β the owner: the web app, not Airwallex, holds recipient information | T-184 |
| 2026-09-24 | OCBC payments to other people happen inside the EOP OCBC portal β never by sending the payer to OCBC's own online banking (owner). Rejected: showing a WOPC's details for payment by hand in OCBC's banking | T-184 |
| 2026-09-24 | OCBC FPS stays out of the app β OCBC charges HK$5 per FPS transfer to a non-OCBC account (HK$10 above HK$500,000; fee guide effective 18 Mar 2026). OCBC WOPC payments go by transfer to the payee's OCBC account (refined same day: the capture showed this is the existing ITSO internal transfer, not a separate ITOA; the OCBC screen's FPS option is disabled β it showed a false "submitted") |
T-184 |
| 2026-09-24 | A WOPC stores the ERL account it is to be paid from at transaction.bankAccountId β written at creation as the plan, overwritten with the account the money actually left from when the payment is linked; never a "paid" signal (only transaction.id is). Chosen over a separate payout.from section and over putting the payer's account in bank (the recipient's side) |
T-184 |
| 2026-09-24 | "No bank account on file" blocks a WOPC payee only where the WOPC still has to be paid (Bank Access "Pay with WOPC", Records "Create Pending WOPC"), not when recording a payment that already left the bank β blocking there would stop the books recording money already sent | T-184 |
| 2026-09-24 | Both bank pages' "Pay with WOPC" offers an existing unpaid WOPC or a new one β previously it could only create a new WOPC | T-184 |
| 2026-09-24 | People with no bank account on file are shown greyed out with a "No bank account on file" tag and cannot be chosen as a WOPC payee (and their Reimburse action is greyed out the same way) β chosen over hiding them, so the creator can see why | T-184 |
| 2026-09-24 | An unpaid WOPC that doesn't require a signature, set to pay automatically, is paid as soon as its creator confirms with their authenticator code; when a WOPC is set to pay automatically, the code is checked before the WOPC is created | T-184 |
| 2026-09-24 | A WOPC's creator chooses how it gets paid: automatically the moment it is signed (authorised with their authenticator code), or by pre-selecting which ERL account a person with Bank Access later pays it from by hand β chosen over a single Bank Access-side "Pay withβ¦" entry point | T-184 |
| 2026-09-24 | Paying automatically on signature is ERL's Airwallex only β every OCBC transfer needs a separate OneToken approval, so it can never be released automatically; OCBC stays available for paying a pre-assigned WOPC by hand (refined the same day: an earlier row excluded OCBC from T-184 entirely) | T-184 |
| 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 (Superseded in part 2026-09-24: a WOPC now STORES its pay-from account at transaction.bankAccountId β planned at creation, overwritten with the actual account when the payment is linked, since a plan made before any payment has nothing to derive from β T-184) |
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 |
| 2026-09-03 | A receipt in flight lives in the run, and the pending row is what the sheet reads β the row outlives dispatch instead of being deleted (owner rejected a new collection, and Inngest cannot be the sheet's source because a person's edits are never checkpointed); nine fields added, nothing removed, renamed or retyped, and accounting/receipts/entries still written only on Confirm |
T-206 |
| 2026-09-03 | The straightened image is parked in the existing receipt bucket under a receipts/_staging/ prefix, streamed through our own gated endpoint and deleted at every exit β chosen over pulling it from Inngest per thumbnail; accepted cost is a cropped copy on our disk before approval, which is what lets a person judge the crop before filing |
T-206 |
| 2026-09-03 | The worker's card is conditional on the chat handing it a message to drive β a dispatch naming no status message keeps the run out of the chat entirely, so runs already parked at cutover finish with their cards while new ones are silent | T-206 |
| 2026-09-03 | Two per-receipt chat notices survive the card's retirement: a photo that is not a receipt, and a failed re-run β both are rare and actionable, and in each case the row is cleared or unchanged so the sheet could not carry them | T-206 |
| 2026-09-03 | The receipt bot's chat surface is three notices and nothing else β a batch arrived, a batch is ready to check, and a daily expiry warning β with the note, company, reading, corrections and Confirm all in the Mini App; the per-receipt card is retired (owner: "I don't mind that the bot serves as a reminder, I'm also entirely fine with C", and the 78 rows stale since May are the evidence a shut app cannot chase anyone) | T-206 |
| 2026-09-03 | One message per BATCH is achieved with Inngest debounce keyed on the chat, not a remembered message_id β which is what withdrew the tenth field: two fresh messages at two moments say what one self-rewriting line would have said, and remember nothing |
T-206 |
| 2026-09-03 | The retired chat handlers (note force-reply, subsidiary picker, card callbacks) are left in place unreachable, not deleted β nothing sends those prompts, and until the sheet is proven in production restoring the chat path is one call rather than a revert | T-206 |
| 2026-09-04 | A short-lived claim on a receipt's printed reference (new collection accounting/receipts/refClaims) is how duplicates inside one upload batch are caught β a claim must exist before the receipt document does, so it cannot be a field on it; approved retrospectively by the owner after review ("let's keep this design as it's essential for the new implementations to work"), the original change having neither explained nor sought it |
T-207 |
| 2026-09-04 | Every receipt image is bounded to 2000 px for processing but archived at full size β the bound is what keeps the segmenter inside its budget and measurably improves the AI's reading of a faded thermal receipt, but it must not decide what the books keep; the untouched photograph is parked beside the working copy and filed on Confirm (HEIC excepted: archived as its full-resolution decode, since raw HEIC is a file no viewer opens) | T-207 |
| 2026-09-04 | An image that already meets the standard is passed through, not re-encoded β a Telegram photo is already a modest upright JPEG, and re-encoding it cost a generation of quality on the archived record for nothing; the exception is an image whose rotation lives only in its EXIF tag, which must be re-encoded upright or the workers that ignore EXIF read it sideways | T-207 |
| 2026-09-04 | Abandoned staging files are swept by a daily job, not a bucket lifecycle rule β the bucket has no lifecycle configuration, and a sweep that also checks the receipt's row is gone (treating an unreadable row as present) is reviewable and testable where a bucket rule is neither | T-207 |
| 2026-09-04 | The generative image redraw is retired β the display copy comes from the deterministic crop chain, which moves pixels and cannot alter a printed figure, so the verify re-OCR goes with it (3β7 Gemini calls per receipt β 1); RECEIPT_IMAGE_CLEANUP=1 is a one-variable rollback, not a permanent fork |
T-207 |
| 2026-09-05 | The segmenter runs on the NAS, not Cloud Run (per T-171's cost move) β made possible by points_per_batch 64 β 8, which cuts peak memory 3.7 GB β 1.06 GB with byte-identical crops; the image is built in CI and pulled, because the NAS cannot build torch |
T-207 |
| 2026-09-05 | Worker reachability is watched by an Inngest cron, not a NAS task β the NAS is one of the things being watched, and a watchdog on the box it watches cannot report that box going dark; a worker is "down" only after two consecutive misses because the tunnel 502s in bursts, and a newly configured worker that never answers is announced | T-208 |
| 2026-09-05 | The sheet's default company is ERL by name, not "the first switched-on one" β the two agree only because ERL is the only company wired up, and the derived version would have moved everyone's default the day a second one was switched on; the rule now lives in a client-safe module shared by the page, since the server module that holds the rest of the filing rules would drag Firestore Admin into the browser bundle | T-206 |
| 2026-09-05 | A fade-out in the sheet holds the element mounted for the fade's length (a Fade wrapper; screens switch on a lagging copy of the phase) β React removes an element the instant its condition is false, so without the hold nothing can fade away, only appear; filing a batch is deliberately not a screen change |
T-206 |
| 2026-09-05 | The company list opens in flow inside the row, not floating over it, and unavailable companies show a drawn red cross rather than an emoji β the row clips its overflow to keep its corners, so a floating menu is cut in half; an emoji's colour and shape are the phone's choice, two SVG strokes in the sheet's own red are not | T-206 |
| 2026-09-05 | A row in the sheet opens by growing, not by fading β the card animates to its full height and the content is revealed as the card makes room (owner: "I wanna see the tab expandING instead"); the height is unknown in advance, so it is the CSS grid trick (0fr β 1fr) rather than a measured pixel height, and an engine too old for it opens the row without motion rather than broken | T-206 |
| 2026-09-05 | The check-it panel carries no picture of its own; the thumbnail opens the receipt large and the flip between the person's photo and the straightened copy lives there β a tap on the photo flips, a tap anywhere else closes β so a check-it row is half the height and there is one place, not two, to look at the image | T-206 |
| 2026-09-05 | The sheet's category is a picker that scrolls inside itself, not nineteen chips β the same picker as "Files under", opened with the current choice in view; chips filled half the check-it panel and an in-flow list of nineteen would push Confirm off the screen | T-206 |
| 2026-09-05 | A receipt is read the moment it arrives, not when it is filed (owner: "Then build a new one") β the row shows the receipt's own date and the shop before the note is written, and the worker takes that reading instead of reading again; the cost is one reading per photo later discarded, and the path is best-effort so a failed early look leaves the receipt exactly as it was | T-206 |
| 2026-09-05 | A waiting row that cannot be filed says so in red and refuses File β not a receipt, unreadable, no date, or already on file β with Discard on the row; the rule is one client-safe module used by both the sheet and the submit endpoint, so the page cannot file what the server would refuse | T-206 |
| 2026-09-05 | Only a strong duplicate (shop + printed number) is flagged at arrival; a weak one still waits for the confirm-time warning, and the in-batch claim stays at File time β two identical purchases on one day collide on the weak key, and the claim needs a chosen company | T-206 |
| 2026-09-05 | The clash check runs on every change to the figures, not only at the first reading β at arrival, after each edit on the row, and again at File time β so a number the AI missed and a person typed in is checked the moment it is typed (owner: "make sure that the checking β¦ doesn't just check upon initial OCR") | T-206 |
| 2026-09-05 | A clash with a receipt on file reddens the number and the shop for correction rather than blocking with Discard alone β the AI may have misread them β with one explicit "it is a different receipt, file it anyway" for the shop that reuses numbers; a receipt that can never be filed is worse than a rare override | T-206 |
| 2026-09-05 | The date is required; the number and the time are not β their red is advisory with a placeholder, because many receipts print neither (owner: "No date, no submission" / "genuinely allow user to skip") | T-206 |
| 2026-09-05 | The figures are checked in the waiting row before File, so the check-it panel after straightening is about the picture β one line of what will be filed, the picture via the thumbnail, the decision β with the figures under "Check the details" and a new "use my photo instead" beside "straighten it again" | T-206 |
| 2026-09-05 | Every waiting row has its own File button; the button at the foot files everything ready at once β a receipt held back by a missing figure goes the moment it is fixed, not when the batch does (owner) | T-206 |
| 2026-09-05 | Two photos of one receipt in a batch are worked out from the waiting rows, never stored β same shop, same printed number, the books' own strong rule β and "Keep this one" removes the rest of the group as computed on the server; when one is discarded the other clears with no write, and no new field is needed | T-206 |
| 2026-09-05 | The amber "looks like one on file" is told from the red clash by the row's own number, not a stored kind β the strong match needs a usable number on this side and the weak match is only recorded when there is none, so one existing field carries both and the duplicateOf.kind question is withdrawn; amber is shown and never blocks (owner: "if the items are the same but receipt number is different, then no need to flag" β and when there is no number, a likeness, not a verdict) |
T-206 |
| 2026-09-05 | The sheet's worker light probes live and stores nothing β green when every configured worker answers, flashing red when any does not, the list a tap away, and the stages that need an offline worker wait (owner) β because a light and a gate need only "now"; the alerts that need memory stay with #1011 and its document question | T-206 |
| 2026-09-05 | The sheet's crop stage is gated on the cropper alone β the finder is an optional helper the crop skips when it does not answer, so a finder outage turns the light red (owner's rule: red when any worker is offline) but shuts nothing; the pipeline size cap measures the normalised working copy, not the raw download, because that is what rides through step state | T-206 |
| 2026-09-05 | The sheet's light is red only for a worker the chain needs; blinking yellow when only an optional one is down β the finder is optional because the cropper stands in for it; "optional" is a fact the health module states about a worker, not a list the sheet keeps (owner) | T-206 |
| 2026-09-05 | The receipt finder runs on the NAS at 4 prompts per batch and 100 prompts per image (SAM_POINTS_PER_SIDE=10), because 144 prompts measured 50β52 s per receipt there against the app's 60 s abort on the crop step and 100 measured 37β38 s with the same crop; it is started by a workflow that logs the NAS in with its own run token, since the login the NAS keeps for GHCR has expired |
T-206 |
| 2026-09-06 | The worker crops one receipt at a time in production and the finder gets 45 s of the step, because the finder holds one model and answers one request at a time (~32 s); two at once queued, timed out and retried into a busy finder β a batch is slower and never times out (owner: "none of my recent receipt submissions have been straightened") | T-206 |
| 2026-09-06 | The finder finds the paper on a 1024 px working copy and warps the full photo β SAM's encoder resizes to 1024 regardless, so the model sees the same picture and the output is the same picture; with 64 prompts (safe: the area filter discards under 5 % of the frame) and four threads a receipt takes 17 s on the NAS, from 50 (owner: "speed the process up safely with nothing missed") | T-206 |
| 2026-09-06 | A status write lives inside a step, never before one, and undefined is stripped before any books write β a bare write outside a step re-runs on every retry of a later step (it wrote "straightening" back onto confirmed receipts for seven hours), and one undefined field (summaryMessageId, absent for a receipt with no chat card) fails the whole document |
T-206 |
| 2026-09-14 | Drive: accounting folders move to decade 60 (62. Sales Invoices / 63. Incoming Payments / 64. Expenses); app accepts both layouts so the move needs no timed deploy |
T-211 |
| 2026-10-06 | The finder runs the model off its request loop, one crop at a time, so "are you there?" is answered at once mid-crop β a busy finder is not an offline one; and every waiting-row update drops empty values and reports a refused save, with the reading and "ready for your check" retrying instead of passing silently | T-206 |
| 2026-10-06 | The finder checks its own cut (paper must fill β₯ 90 % of it, else the next guess, else the photo unstraightened) and drops a shape that is the background around another β chosen over trusting the best-scoring shape, which on a plain wall was the wall itself; "Straighten it again" asks for the next guess rather than repeating a deterministic one | T-207 Β· T-206 |
| 2026-10-06 | One Drive file per receipt copy: each upload carries a Drive-side label (waiting-row id + photo/straightened) and updates its own file on a retry or re-file instead of adding one β chosen over a clean-up sweep after the fact, and over keying on the receipt id, which a failed run does not keep | T-206 |
| 2026-10-06 | The Records WOPC drawer shows the archived WOPC at its latest stage (signed and sealed, voided, or a print of the template if never signed), from the same address as Download and "Open in new window" β not a re-draw from the database record, which can never show the signature or chop | T-198 |