Skip to content

Question (owner, 2026-06-15)

Can the accounting page's LOGIC + ROUTING produce ACCURATE information for the Working Paper Pack? (Separate matter, NOT audited here: whether the TYPES of documents in the pack are sufficient.)

Verdict: NO — not reliably. material-risks.

Foundation is SOUND (journals correctly DERIVED on the fly, never stored; invoices/projects via canonical builders; PAID + related-party key off the gl-allocation map / payee directory; CSV is a faithful pass-through). But logic+routing have confirmed defects that ship WRONG numbers — and most are SILENT because the pack's own integrity flags (bs/tb.isBalanced, socie/cfs.isReconciled) cannot detect them (a commingled or wrong-period set still balances). Numbers are correct today largely BY ACCIDENT — ERL is currently the only subsidiary with material data. Method: 65-agent workflow (trace → adversarial verify → synth → completeness critic), 41 confirmed findings. Entry point pages/api/accounting/working-paper-pack.ts.

CRITICAL

  • Subsidiary commingling. TB/P&L/BS/CFS/SOCIE + AR-aging are generated with NO subsidiaryId (working-paper-pack.ts:163-177 → reports.server.ts:319-334 calls canonical journals with zero args, cached under fixed key 'report-journals:all'). The precise leak (critic) = the coaching generators (generateCoachingIssued/PaymentEntriesServer hardcode subsidiary='mel', included whenever subsidiaryId falsy) — production invoices are already stamped 'erl'. So a live MEL coaching invoice leaks straight into the "ERL" TB/P&L/BS/cash/equity, and journals.csv (ERL-only) cannot foot to the statements.
  • Cash-basis revenue collapses to ~zero (critic, arguably critical). On basis=cash, cashBasisFilter strips ISSUED (the only DR-AR/CR-Revenue leg); the surviving PAID leg credits AR (1100), not a 4000 revenue account → cash-basis P&L revenue ≈ only directly-categorized income (interest); ALL invoice + coaching revenue vanishes and BS AR goes large-negative. derivedJournals.server.ts:373-388 / reports.server.ts:207-216,463.

HIGH

  • Comparative period wrong for ERL's irregular calendar. Equal-length "ends 1ms before start" derivation (working-paper-pack.ts:144-148) is meaningless for P1(~16mo)/stub(~3mo)/Mar-year; every flow-statement (P&L/CFS/SOCIE) comparative column for the periods ERL files NOW is fabricated. (TB/BS prior via priorAsOf cumulative cutoff are OK.) Fix: resolve via entityCalendar/basisPeriod, no comparative for first period. Launcher also defaults to a hard-coded Apr–Mar FY (WorkingPaperPackLauncher.tsx:45-50).
  • SOCIE isReconciled is a tautology (reports.server.ts:862 vs :896 are byte-identical) → always YES; the pack's only equity tie-out can never fail. Fix: compare to BS.totalEquity like CFS does.
  • CATEGORIZED journals gate GL code on stored transaction.status (derivedJournals.server.ts:498) → drop billing-linked GCP/Workspace expense legs whose status drifted ('unmatched' w/ gl code set — the default 2026-01-14→06-14, the bug fixed in bea88e3d but NEVER backfilled) → pre-fix packs understate expense. Fix: use tx.accountCode directly (gl map is robust); backfill persistRecomputedStatus; re-issue affected packs.
  • AR-aging cannot tie to BS AR. Counts cleared invoices at full face value + no as-of cutoff (reports.server.ts:1677,1690), payment map gated on transaction.status Firestore where-query (invoicePaymentStatus.ts:188 — drift-window paying txns dropped → invoice shows unpaid at full face), bucket bug (current always 0), unconditionally merges tebs-mel coaching (:1699). Worst single schedule.
  • No period-close snapshot (working-paper-pack.ts regenerates from live mutable data) → closed-period packs not reproducible "as filed"; post-close edits silently change a regenerated pack. Fix: freeze a TB/P&L/BS/… snapshot (or hash) onto PeriodCloseRecord at close.
  • Cash-basis SOCIE/CFS use a narrower filter (reports.server.ts:747-750, :1012-1015 strip only ISSUED) than shared cashBasisFilter (5 events) → the 6 statements disagree on the same period's profit; surviving depreciation makes cfs.isReconciled unreliable.
  • ISSUED vs PAID population asymmetry (critic) — a paid-but-never-"issued" invoice gets AR credited with NO revenue recognized → understated revenue, negative AR, TB still balances.

MEDIUM

  • TB/BS isBalanced computed only over in-COA accounts (reports.server.ts:386-390) → journal lines to an inactive/missing GL code silently dropped; balance check can stay green. (critic)
  • GL 3900 synthetic Net-Income row can collide with a manual 3900 posting → duplicate equity rows, flags stay green.
  • PP&E s.17 movement note doesn't foot for mid-period DISPOSALS (notes.server.ts:242-249; critic corrected the root cause — acquisitions DO foot); no note-footing flag.
  • Fixed-assets register↔BS reconciliation never run (withReconciliation not passed, working-paper-pack.ts:180)
  • the two depreciation month-count paths genuinely diverge ~1 month/asset.
  • Missing/invalid posting dates fall back to TODAY (derivedJournals.server.ts:146-169) → silently mis-periodized/dropped for historical packs.
  • Reimbursement + depreciation generators hardcoded to tebs-erl → a non-ERL pack loses those schedules. (critic)
  • ISSUED revenue gate reads stored paymentStatus (Elephant #5) not transactions (nil impact today, real asymmetry).

LOW

  • UTC-vs-local date drift on non-UTC hosts → comparative label one day off / wrong-FY notes (prod Vercel=UTC, no effect).

Cross-cutting theme

The integrity flags are NOT a safety net here: balanced sets stay balanced under commingling, wrong period, dropped legs, and status drift; SOCIE's check is a no-op. Any fix MUST add integrity assertions that detect the SILENT modes (per-entry subsidiary == requested entity; TB revenue == journals.csv revenue; AR-aging total == BS AR; cfs.netIncome == pnl.netProfit on cash basis; entries dated == generation day count).

Remediation priority

1) subsidiary-scope the 5 primaries + AR-aging (+ subsidiary-key the report-journals cache) · 2) cash-basis revenue routing · 3) comparative via entityCalendar · 4) SOCIE tie-out to BS · 5) CATEGORIZED status-gate → accountCode + backfill · 6) AR-aging reconciliation · 7) period-close snapshot.

Owner clarifications (2026-06-15) — re-scopes some findings

  • 1 NOT a bug: MEL is a brand, coaching is legally conducted THROUGH ERL, so ERL statutory accounts

    SHOULD include coaching. Real residual = INTERNAL INCONSISTENCY: statements include coaching but journals.csv + some schedules are ERL-only → won't foot. Fix flips to "include coaching consistently EVERYWHERE + label cover as ERL incl. coaching-through-MEL." AR-aging MEL-merge is therefore CORRECT.
  • 3 periods are INTENTIONAL: HK 18-month rule (Sep 3 2024→Mar 31 2026 = 19mo > 18) forced the split

    into P1 (~16mo) + 3mo stub. Periods fine; the COMPARATIVE arithmetic is the bug. ERL's FIRST period must emit NO comparative.
  • 5 no WPP has ever been issued/used → no recall needed; code fix + backfill still required so the

    NEXT pack is correct.
  • 7 confirmed by owner as a true issue (period lock does NOT prevent it: non-tx inputs + reopen +

    derivation-code changes all drift a regenerated closed-period pack).

NEW (2026-06-15) — Inspect-period modal misfiles transactions by timezone (owner-reported, CONFIRMED)

monthBounds (lib/periodClose/months.ts:75) draws month boundaries in UTC (Aug = Aug1 00:00:00Z .. Aug31 23:59:59.999Z), but transactions carry HKT (UTC+8). A Sep-1 HKT tx in the first 8h (= Aug31 16:00-24:00Z) falls before the UTC cutoff → counted in Aug, while the table displays it in HKT as "Sep 1". Affects the Inspect modal AND the readiness check (same bounds). The WPP route's own date parsing (working-paper-pack.ts:111-112, server-local/UTC) has the SAME boundary issue → re-rate audit finding #13 UP from LOW: it misfiles REAL transactions across period edges in the pack, not just labels. Fix: compute month/period boundaries in Asia/Hong_Kong (UTC+8) consistently (monthBounds + WPP/report period filters).

Effort-mode guidance given: High for most fixes; Extra/Max for #2 cash-basis + #7 snapshot; Ultracode only for re-audits.

Log

  • 2026-06-15 created. 65-agent accuracy audit (trace→verify→synth→critic), verdict material-risks, 41 confirmed findings. Full transcript was in a tmp workflow output (ephemeral); key findings captured here. No code changed (assessment only). Relates to [[project_matching_redesign]], [[feedback_no_persisted_totals]], [[feedback_no_silent_fixes]] (integrity flags must DETECT, not mask).
  • 2026-06-15 owner reviewed: re-scoped #1 (coaching inclusion intended), #3 (periods intended), #5 (no packs issued), confirmed #7; added timezone misfiling bug (#8, Inspect modal + WPP). See sections above.
  • 2026-06-15 SHIPPED #1–#6 + #8 (owner greenlit, incl #2 cash-basis). Commit 847b94b8 → nightly → main (merge bc74d718). tsc clean; 18 eslint errors in touched files are all PRE-EXISTING any/unused debt (none on added lines; repo has ~9.7k, lint not gating). Per-fix: · #8 monthBounds + WPP parse → Asia/Hong_Kong (lib/time hkMonthBounds); fixes Inspect modal + readiness + pack. · #4 SOCIE isReconciled now ties closingTotal to generateBalanceSheetServer().totalEquity (independent). · #5 CATEGORIZED journal uses tx.accountCode (gl map), not tx.status → drifted GCP/Workspace expense no longer dropped. · #2 new applyCashBasis(): filter + re-route each invoice PAID AR-credit → revenue (tagged source.cashRevenueAccountCode = 4000 project / 4001 coaching at generation); applied to TB/P&L/BS AND SOCIE/CFS so all 6 agree on cash profit. · #6 buildPaymentMapFromTransactions: dropped status .where (gl map = SoT) + asOf cutoff; AR-aging reports OPEN balances of invoices issued ≤ asOf, excludes settled → ties to BS AR. Coaching: as-of cutoff added. · #3 comparative = preceding basis period via listBasisPeriods (resolveEntityCalendarServer); first period → none. · #1 journals.csv now entity-wide (no subsidiaryId) to foot to statements; cover states consolidation scope. REMAINING: status backfill (now OPTIONAL for WPP — reports are drift-robust; still wanted for UI status filters + per-invoice getInvoicePaymentStatus which is still status-gated); #7 FY-close hard snapshot (deferred, design agreed: hash per period-close + freeze at FY close); cash-basis fixture test (cfs.netIncome===pnl.netProfit); coaching partial-payment netting in AR-aging; AR-aging current/1-30 bucket-label cosmetic. Status → doing.
  • 2026-06-15 STATUS BACKFILL = verified NO-OP. Added backfillTransactionStatuses + admin endpoint (commit 60a84d81). Dry-run on prod (dev server, webhook auth): 176 scanned, 0 drift — byStored === byComputed {categorized:115, unmatched:26, matched:35}. The 5 historic GCP charges (incl. mjaAuF1u9moedF1NTmCB → 202501) confirmed status='categorized' (acct 6000, billing 202501-202505), so the modal symptom is genuinely resolved and there was nothing to write. Tool retained for future drift.
  • 2026-06-15 #7 SHIPPED (commit 78ef8294 → nightly → main merge 43fc2477). New lib/periodClose/snapshot.server.ts: per-month contentHash at close (drift fingerprint) + maybeFreezeFiscalYear (freezes TB/P&L/BS/CFS/SOCIE + comparative + journals hash when a close completes a basis period) + getFrozenSnapshotForPeriodEnd. WPP serves FROZEN primaries for a fully-closed FY (accrual); schedules+journals stay live; manifest.asFiled + cover note record provenance + journalsReproducible drift flag. DORMANT until the first FY fully closes (months closed only through 2025-03; P1→2025-12-31), so the WPP is unchanged today — low risk. tsc + eslint clean. REMAINING (per owner plan): cash-basis fixture test (cfs.netIncome===pnl.netProfit, socie net-income ===pnl.netProfit on cash) as the behavioral guard for #2/#4/#6; plus the earlier minor follow-ups.
  • 2026-06-15 T-043a DONE (sub-task; owner asked to name sub-tasks T-043a etc. going forward, commit a87cf909 → nightly → main merge 83bdb7cc). Extracted cashBasisFilter+applyCashBasis to pure lib/accounting/cashBasis.ts; added tests cashBasis (5) + snapshot (5) suites; fixed 2 stale basisPeriod tests. 16 tests green, tsc clean. The snapshot suite CAUGHT A REAL #7 BUG — basisPeriodMonths parsed the ISO start +08:00 then read UTC via monthOf, shifting a period starting on the 1st (2026-01-01 HKT→2025-12) and demanding an extra close before the FY freezes; fixed to slice YYYY-MM from the ISO string (also shipped in a87cf909). Decided UNIT-level (pure applyCashBasis) over full-generator integration (cross-statement cfs===pnl) — the latter needs heavy Firestore mocking
  • cache control; the unit test directly guards the re-route/filter, the highest-risk part of #2.
  • REMAINING sub-tasks (minor, owner to prioritize): T-043b coaching partial-payment netting in AR-aging (needs a coaching payment map) · T-043c AR-aging current/1-30 bucket-label cosmetic (touches summary type + CSV + UI) · T-043d launcher default-period can send a non-fiscal range (UX; route already emits no comparative for unmatched ranges). All low-priority.
  • 2026-06-15 T-043b/c/d DONE (commit f173892d → nightly → main merge 3911ffa7). · T-043b: buildCoachingPaymentMapFromTransactions (reads named gl.coachingInvoices allocs on tebs-erl bank txns, asOf-bounded); AR-aging coaching rows now net PARTIALS to open balance + mirror the ERL cutoff so they tie to the BS. (Turned out the UI builds its OWN '0-30' bucketSummary from per-invoice bucket, so the server summary keys feed only the WPP CSV — made #043c contained.) · T-043c: server AR-aging summary re-keyed current/'1-30' → '0-30' (matches getBucket + per-invoice bucket + UI); arAgingToCsv emits one '0-30' row. No UI change. · T-043d: launcher defaults the period picker to the entity's CURRENT basis period (listBasisPeriods/ERL_CALENDAR), not a hard-coded Apr–Mar year. Route still validates/resolves live calendar. tsc + eslint clean; full suite 218 passed / 9 pre-existing env-telegram failures (driveUploader, receiptProcessor — unrelated). Known gap (not a defect): AR-aging logic itself has no unit test (the critical #2 transform IS covered by cashBasis.test). T-043 substantively COMPLETE.
  • 2026-06-15 T-043e DONE (commit 38b2e76e → nightly → main merge 317caada) — closes the AR-aging unit-test gap. Extracted the per-invoice aging decision + bucketing into a pure, Firestore-free lib/accounting/arAging.ts (getAgingBucket, computeAgingRow = open balance + as-of cutoff + skip settled/future + payment status, addToAgingSummary, emptyAgingSummary). The ERL + coaching loops now share ONE implementation (removed duplicated inline logic + the local getBucket). 7 fixture tests (boundaries, future/settled/overpaid excluded, partial open-balance, aggregation). tsc+eslint clean, full suite 225 pass / 9 pre-existing env-telegram fails. T-043 fully complete — no open gaps.