Gogentic Finance · internal engineering record

Sheet 6 of 6

Notes & Schedules

Supporting detail for the five preceding sheets: the module layout, the route register, how the test suite is distributed, what the deployment expects to find in its environment, and the points where the repository disagrees with itself.

Source files
149
API routes
27
Env variables
14
npm scripts
13
Open items
9
Exceptions
17
A

Module topology

149 TypeScript files under src/, 22,437 lines, in four layers over one database. The path alias @/* maps to ./src/* in both tsconfig.json and jest.config.ts.

Exhibit 12
Layered module topology under src Four layers over Postgres. The app router at the top holds fifteen page and layout files and twenty-seven API routes. Below it, fifty-nine component files plus contexts and hooks. Below that, twenty-nine library files holding the Mercury client, QuickBooks client, chat tools, metrics and queries. Below that, fifteen server files holding sync, patterns, actions and the database schema. src/app — ROUTING · 42 files · 4,873 lines (dashboard)/ — 6 pages + layout / · /cash-flow · /expenses /payroll · /transactions · /settings all server components, all force-dynamic api/…/route.ts — 27 files 18 production · 9 debug plus sign-in, sign-up, privacy, terms, error, loading and not-found src/components · contexts · hooks — PRESENTATION · 62 files · 7,510 lines components/chat chat-panel · chat-input message-list conversation-list markdown-renderer tool-status-indicator components/payroll payroll-kpis payroll-chart payroll-runs-table employee-payroll-table team-compensation components/dashboard kpi-grid · kpi-card charts-grid cash-flow-chart expense-breakdown-chart monthly-comparison-chart contexts · hooks chat-context use-chat use-smart-scroll state, streaming, abort components/{settings, layout, transactions, expenses, cash-flow} components/ui — 18 shadcn primitives · empty-states · error-boundary src/lib — DOMAIN & INTEGRATION · 29 files · 7,493 lines lib/mercury client · types · index MercuryClient, MercuryError offset pagination, 500/page not used by the live sync lib/quickbooks client · api · types encryption · sync team-compensation 549 lines in sync.ts alone lib/chat tools — 20 definitions tool-handlers — 2,127 lines handleToolCall · safeEvaluate the largest file in the repo lib/ai anthropic · categorize one of only two places a model id appears lib/metrics · lib/queries/{transactions, chat, payroll, quickbooks} lib/{format, auth, cron, crypto, markdown-utils, time-range, month-range, utils} src/server — DATA & ORCHESTRATION · 15 files · 2,535 lines server/sync.ts live — shadows the directory server/sync/ is unreachable server/patterns normalizeVendorName findPatternMatch · createPattern server/actions settings · chat · transactions categorize — 16 actions server/db schema · index · seed + 3 seed scripts

← Scroll to see the full diagram →

Four layers, one database. Dependencies run downward: nothing in src/lib imports from src/components, and only src/server/db opens a connection. Two boxes are highlighted because they are where the layering breaks down. lib/chat/tool-handlers.ts imports @/server/db directly and writes its own Drizzle aggregations rather than going through lib/queries — at 2,127 lines it is by some margin the largest file in the project. And server/sync.ts shadows the server/sync/ directory beside it, as set out on Sheet 3.
B

Register of routes

Twenty-seven route files: eighteen production, nine debug. Authentication is layered — src/middleware.ts protects everything by default and names eight public exceptions, and most protected routes then re-check the session for themselves.

Production routes

18 production routes
Route Verbs Guard Note
Conversation & agent
/api/chatPOSTauth()SSE stream; 1 MB body cap; 20 tools attached
/api/conversationsGET, POSTcurrentUser()Just-in-time users row creation
/api/conversations/[id]GET, PATCH, DELETE+ ownership404 then 403 checks
/api/conversations/[id]/messagesGET, POST+ ownershipNo route test of its own
Scheduled & public
/api/cron/syncGETBearerPublic in middleware; strict secret comparison in-route
/api/categorizePOSTBearer, optionalPublic; accepts a request with no Authorization header at all
/api/cron/healthGETnonePublic by design
/api/healthGETnonePublic by design
/api/webhooks/clerkPOSTSvixSignature-verified; handles user created / updated / deleted
Data & integrations
/api/syncGET, POSTauth()Manual trigger; body { type: 'full' | 'incremental' }
/api/metrics/expensesGETmiddleware?days; omitted means all time. No in-route check
/api/metrics/net-changeGETmiddleware?days. No in-route check
/api/transactions/[id]/categoryPATCHauth(){ categoryId, rememberPattern? }
/api/auth/quickbooksGETmiddlewareSets a 10-minute httpOnly qb_oauth_state cookie, then redirects
/api/auth/quickbooks/callbackGETauth() + stateCSRF state comparison; eight distinct error codes
/api/quickbooks/statusGETauth()Returns the realm identifier
/api/quickbooks/syncGET, POSTauth()Zod-validated date range; default 90 days, maximum 365
/api/quickbooks/disconnectDELETEauth()404 when no connection exists

Debug routes

Note — this surface is unauthenticated

src/middleware.ts lists /api/debug(.*) among its public matchers, with the comment // Debug endpoints (REMOVE IN PRODUCTION). No debug route file performs a check of its own. Four of the nine mutate data; one deletes every payroll row it can reach. Several exist specifically to print payroll internals, so their responses carry exactly the material this record is written to keep out.

9 debug routes
Route Verbs Writes Purpose
/api/debug/quickbooks-entriesGETtokens onlyDecrypts the stored tokens and dumps journal entries, chart of accounts and company info
/api/debug/quickbooks-purchasesGETtokens onlyContractor payment rollup by vendor
/api/debug/test-parserGETtokens onlyRuns parseGustoJournalEntry over live entries and prints the parse
/api/debug/payroll-line-itemsGETnoMost recent 50 line items, joined to employees
/api/debug/test-compensationGETnoPer-person compensation totals
/api/debug/setup-teamGET, POSTyesUpserts employee rows from a hard-coded roster
/api/debug/cleanup-employeesPOSTyesDeactivates the four parser-artefact names
/api/debug/update-from-gustoGET, POSTyesOverwrites period and headcount fields on matched runs
/api/debug/sync-all-payrollPOSTdestructive?clear=true deletes all payroll line items and all payroll runs, unconfirmed, then re-syncs

Specified but never built

PRD.md carries a route table written before implementation. Seven of its entries have no corresponding file. In most cases the capability exists — it simply arrived as a server action instead.

PRD routes with no implementation
SpecifiedWhat happened instead
/api/mercury/connectupdateMercuryApiKey() server action
/api/mercury/synctriggerManualSync() action, and POST /api/sync
/api/categoriesgetCategories / createCategory actions
/api/patternsFolded into updateTransactionCategory(…, rememberPattern)
/api/dashboard/metricsServer components call lib/metrics directly
/api/transactionsThe page calls getTransactions() directly
/api/auth/webhookBuilt as /api/webhooks/clerk; DEPLOY.md uses the second name

Public route matchers

Everything else is protected src/middleware.ts
const isPublicRoute = createRouteMatcher([
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/api/webhooks(.*)',
  '/api/cron(.*)',       // Cron jobs use CRON_SECRET auth
  '/api/health(.*)',     // Health checks should be public
  '/api/categorize(.*)', // AI categorization endpoint
  '/api/debug(.*)',      // Debug endpoints (REMOVE IN PRODUCTION)
  '/privacy',            // Privacy policy (public for QuickBooks)
  '/terms',              // Terms of service (public for QuickBooks)
]);

Comments are the source's own. The privacy and terms pages are public because Intuit requires a published policy before granting production OAuth access — which is also why those two pages exist in src/app/ at all.

Common response conventions

Status codes and their bodies
CodeBodyRaised when
200variesSuccess
201the created objectConversation and message creation
400{ error: 'Invalid request body' }, { error: 'Title is required' }Malformed JSON or a failed field check
401{ error: 'Unauthorized' }No Clerk session, or a failed bearer check
403{ error: 'Forbidden' }The row belongs to another user
404{ error: '… not found' }No such row
413{ error: 'Request too large' }/api/chat only, above 1 MB
500{ error: '<verb> failed', message }Any throw; a non-Error reports 'Unknown error'
C

Distribution of tests

1,088 test blocks across 50 files and 16,649 lines — 0.74 lines of test for every line of source. The distribution is heavily weighted toward the agent boundary and the markdown renderer, the two places where behaviour is defined by a long list of discrete cases rather than by a single control flow.

Exhibit 13
Test blocks by file, twelve largest A horizontal bar chart of the twelve test files with the most test blocks. The tool handlers file leads with 111, followed by the tools file with 78, the markdown renderer with 43, the conversation list with 34, and then a cluster in the low thirties. Together the twelve account for 514 of 1,088 blocks. TEST BLOCKS BY FILE — TWELVE LARGEST OF FIFTY 0 25 50 75 100 lib/chat/tool-handlers 111 lib/chat/tools 78 components/chat/markdown-renderer 43 components/chat/conversation-list 34 lib/quickbooks/client 33 lib/mercury/client 33 contexts/chat-context 33 components/settings/category-settings 32 lib/markdown-utils 31 lib/metrics/index 30 app/api/conversations/[id]/route 29 components/chat/chat-panel 27 Subtotal, twelve files 514 47.2% Remaining thirty-eight files 574 52.8%

← Scroll to see the full diagram →

Where the assurance sits. The two lib/chat files carry 189 blocks between them, 17.4% of the suite — a defensible allocation given that they define the entire surface a language model is permitted to touch. Counts are of it and test declarations, including those inside conditionally skipped describe blocks.

Where the assurance is not

Untested surface
AreaCoveredNote
API routes5 of 27Untested: the Clerk webhook, both OAuth legs, all QuickBooks routes, both metrics routes, the category PATCH, /api/sync, and all nine debug routes
Server actions1 of 4 filesOnly settings.ts; no tests for chat.ts, transactions.ts or categorize.ts
Payroll components0 of 6No __tests__/components/payroll/ directory
Payroll agent tools0 of 8Handlers exist; none is invoked by a test
The live sync0src/server/sync.ts has no test at all; the sync tests target the shadowed directory

The pattern is consistent: almost everything built in Phases 1 to 9 is covered, and almost nothing added in Phase 10 is. That matches the four unticked assurance items in TASKS.md.

D

Reconciliation of counts

CLAUDE.md requires that completed tasks record their test count in TASKS.md. That makes the file auditable: the recorded figures can be compared against the blocks actually present.

Recorded count against countable blocks
TASKS.md entry Recorded Counted Result
2.2 Mercury API client2733agrees — 6 are conditionally skipped
2.3 Transaction sync1313agrees
1.2 Auth flow77agrees
2.1 DB operations77agrees
8.1 Sync → categorize1010agrees — on a module that never runs
8.3 /api/categorize endpoint88agrees
10.3 QuickBooks sync2121agrees
10.1 QuickBooks OAuth + client7474agrees — client 33 + encryption 19 + types 22
Summary — chat tools2978differs — the file grew across Phases 9 and 10
Summary — chat tool handlers53111differs — same cause
3.2 + 8.2 Pattern learning2723differs — the two entries overlap in one file

Eight of eleven agree exactly. The three that differ are the entries whose files were later extended without the earlier line being revised — the same drift that left the tool count assertion at nineteen. Every figure recorded against a component that was finished and not revisited still agrees, which is why the figures on Sheet 1 are drawn from mechanical counts rather than from the summary table.

E

Environment

Fourteen variables are referenced. DEPLOY.md lists seven; the other seven are discoverable only by reading code. Names only appear below — no values.

Environment variables in use
Variable Required Consumed by, and behaviour when absent
Listed in DEPLOY.md
DATABASE_URLyessrc/server/db/index.ts asserts it non-null at module scope, so an unset value fails at import
MERCURY_API_KEYfallbackThe sync prefers the encrypted key in settings and falls back to this; also gates the Mercury integration suite
ANTHROPIC_API_KEYyesgetAnthropicClient() throws 'ANTHROPIC_API_KEY not configured'
CRON_SECRETyesverifyCronAuth returns false — scheduled endpoints close rather than open
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYyesClerk browser SDK
CLERK_SECRET_KEYyesClerk server SDK and clerkMiddleware
CLERK_WEBHOOK_SECRETyesSvix verification; the webhook route throws without it
Discoverable only from code
ENCRYPTION_KEYyessrc/lib/crypto.ts throws 'ENCRYPTION_KEY environment variable is not set'. Both the QuickBooks tokens and the stored Mercury key depend on it
QUICKBOOKS_CLIENT_IDyesNamed error from getQuickBooksConfig()
QUICKBOOKS_CLIENT_SECRETyesNamed error
QUICKBOOKS_REDIRECT_URIyesNamed error
QUICKBOOKS_ENVIRONMENTnoDefaults to sandbox — a live deployment that omits it talks to the sandbox host
SKIP_MERCURY_INTEGRATIONnoSet to 'true' to skip live Mercury calls even when a key is present
NODE_ENVnoThe error boundary renders the underlying message only when it is 'development'

Local development reads .env.local, loaded by dotenv from jest.env.ts, drizzle.config.ts, check-qb.js and the three files in scripts/. .gitignore excludes .env* wholesale.

Note — deployment checklist gap

DEPLOY.md was written at Phase 7 and lists seven variables. Phase 10 added five more — ENCRYPTION_KEY and the four QUICKBOOKS_* entries — without the checklist being updated. A deployment following it exactly would start, authenticate, and run the agent; QuickBooks would fail at connect time, and saving a Mercury key through the settings page would fail on encryption.

F

Scripts & the completion gate

Thirteen scripts. Five wrap drizzle-kit; the rest are the standard Next.js and Jest entry points.

Terminal — the declared scripts
$ npm run

Scripts available in gogenticcfo@0.1.0 via `npm run-script`:
  dev
    next dev
  build
    next build
  start
    next start
  lint
    eslint
  test
    jest
  test:watch
    jest --watch
  test:coverage
    jest --coverage
  typecheck
    tsc --noEmit
  db:generate
    drizzle-kit generate
  db:migrate
    drizzle-kit migrate
  db:push
    drizzle-kit push
  db:studio
    drizzle-kit studio
  db:seed
    npx tsx src/server/db/seed.ts

Script bodies are quoted verbatim from package.json. Because test is bare jest, npm test -- <path> forwards the path straight through, which is the form used in the terminal renderings on Sheets 3 and 5.

The Definition of Done

CLAUDE.md — six conditions
ConditionStanding
Feature implemented
Unit tests, 100% of new codeEnforced at 80% by jest.config.ts across branches, functions, lines and statements. Phase 10 did not meet it
npm test passesRecorded once, at Phase 8.3. It does not pass on the code as committed — see exception 1
Real implementations, not mocksPartly honoured. Four suites run against a live Postgres and clean up after themselves; the component and handler suites mock at the module boundary
npm run typechecktsc --noEmit under strict: true
npm run buildVerified at Phase 8.3, before the Phase 10 work landed

The four suites that talk to a real database are __tests__/server/db/seed.test.ts, __tests__/server/patterns/index.test.ts, __tests__/server/sync/transactions.test.ts and __tests__/lib/queries/chat.test.ts. Each carries a @jest-environment node docblock, opens its own Neon connection, and tears down its fixtures — the pattern tests delete every row whose pattern begins TEST_ in an afterAll.

“No skipped tests” is not quite true. Two describe blocks are skipped: the Mercury integration suite, conditionally, and an “API Integration Tests” block in __tests__/lib/ai/categorize.test.ts, unconditionally via describe.skip. The latter carries its own instructions for running it by hand — npm test -- --testEnvironment=node __tests__/lib/ai/categorize.test.ts — because the Anthropic SDK will not run under jsdom without an explicit browser flag. Both are documented rather than hidden.

G

Open items

Nine items in TASKS.md are unticked. They cluster in two places: the tail of the polish phase, and the tests for the most recent feature.

Incomplete tasks
RefItemKind
Phase 6 — polish
6.2Performance optimizationEngineering
6.2Mobile responsiveness QAEngineering
Phase 7 — deployment
7.1Set up production Neon databaseOperations
7.1Configure Clerk production keysOperations
7.1Deploy and smoke testOperations
Phase 10 — QuickBooks and payroll
10.2Write schema testsAssurance
10.4Write payroll page testsAssurance
10.5Write settings testsAssurance
10.6Write agent tool testsAssurance

All four assurance items are corroborated by the file listing rather than taken on trust. A fifth, unlisted, belongs with them: the debug routes carry DELETE THIS ENDPOINT BEFORE PRODUCTION! comments and a matching note in middleware.ts, but no task tracks their removal.

H

Register of exceptions

Points where the code contradicts the specification, the tests contradict the code, or the code contradicts itself. They are listed because a technical record that reports only agreement is not much of a record. Several are simply the ordinary residue of a system built quickly in phases; a few are worth acting on.

Exceptions noted
# Exception Effect
Correctness
1 tools.test.ts asserts toHaveLength(19); financialTools holds 20 The suite fails on the committed code. get_team_compensation was added without the assertion being updated
2 src/server/sync.ts shadows src/server/sync/index.ts under bundler resolution The directory version — the only one that triggers categorisation after a sync — is unreachable from production. Ten tests certify it anyway
3 The Runway card's subtitle reads “Based on net burn rate”, but getRunwayMonths() is called with no argument and its parameter defaults to false The figure shown is gross-burn runway, labelled as net
4 Burn rate is documented as “average monthly expenses over the last 3 months” in both the code comments and the agent tool description; the implementation takes a daily average over the data range and multiplies by 30 The two agree only when the data range happens to be three even months
5 The get_metrics cache key omits months Within a 60-second window, cash_flow_by_month for 3 months and for 12 months share one entry
Exposure
6 All nine /api/debug/* routes are public in middleware.ts and perform no check of their own Four mutate; one deletes all payroll rows on a query parameter; several print payroll and vendor detail. The source flags this itself in two places
7 /api/categorize accepts a request with no Authorization header Deliberate, per its comment — “allow either cron secret or no auth for manual triggers”. A wrong secret is still rejected
8 verifyCronAuth compares the bearer token with === Not a constant-time comparison
Schema
9 schema.ts declares no indexes at all Only primary keys and the three unique constraints are indexed. transactions.date, transactions.category_id, transactions.counterparty_name and payroll_runs.pay_date are all filtered or sorted on without one
10 seed.ts relies on onConflictDoNothing(), but categories.name has no unique constraint Re-running npm run db:seed duplicates all twelve rows
11 payroll_runs.total_deductions is assigned a literal 0 by the parser The column exists and is NOT NULL, and is always zero. Deductions are computed only per employee
12 The employment_type comment lists three values; five are in use Plain text, no check constraint, no enum — nothing enforces either set
13 quickbooks_connections.user_id is text with no foreign key and no unique constraint, though the code upserts one row per user Two identity conventions in one schema, and no referential integrity on the QuickBooks side
Documentation & duplication
14 PRD.md specifies cursor pagination; the client and the sync both use limit/offset. The cursor types survive, unreferenced Dead type surface; the specification is wrong about a shipped behaviour
15 Three totals for the test suite — 870 recorded as run, 1,056 in the summary table, 1,088 countable — and TASKS.md describes migration 0000 as creating 7 tables where the SQL creates 8 Documentation drift; the mechanical counts are used throughout this record
16 Currency formatting exists twice — src/lib/format.ts and a private formatCurrency inside tool-handlers.ts; the time-range option list exists three times The two formatters differ on sign handling. lib/time-range.ts is duplicated verbatim into two dashboard components
17 cronErrorResponse's 'Operation failed' label is never used; findOrCreateEmployee in quickbooks/sync.ts is defined but never called, its logic inlined at the call site Dead branches in otherwise live modules

Accessibility, as evidenced

TASKS.md 6.2 marks an accessibility audit complete. The component tests corroborate it — not as a claim, but as assertions that would fail if the attributes were removed.

Affordances asserted by tests
AffordanceWhere
aria-labelSend message, Chat message input, Toggle theme, Toggle AI Chat, Open navigation menu, New chat, Delete conversation, previous/next page, Go to page N, Clear all filters, Try again, Start date, End date
aria-current"page" on the current pagination button; "true" on the active conversation
role="status"Tool status indicator and every success message in settings
role="alert"Every error message in settings
aria-live="polite"Tool status indicator
aria-hidden="true"Decorative icons throughout, and the streaming cursor in the markdown renderer
Screen-reader text“Thinking…” inside the typing indicator
Focus stylingA focus-visible: variant asserted on conversation items

Two related items remain open — performance optimisation and mobile responsiveness QA — so the audit covered semantics rather than layout.

I

Basis of preparation

This record was prepared by reading the repository: 149 source files, three migrations, the committed configuration, the specification set, and a fifty-file test suite. Where a claim on these sheets could be checked against more than one of those, it was.

The basis markers exist because those sources do not always agree. A behaviour read from src/ is what the application does. A behaviour pinned by a test is what it was expected to do when the test was written — usually the same thing, and occasionally, as the register above records, not. A behaviour stated only in PRD.md is an intention, and at least seven of those intentions were met a different way or not at all. Where the sources disagree, this record follows the code.

Sources read
Artefact Extent What it establishes
src/149 files22,437 lines. Behaviour, constants, limits, prompts, route contracts, schema
__tests__/50 files16,649 lines. Expected behaviour, literal strings, formatting rules, error messages
drizzle/7 filesThe schema as applied, and the timestamp of each migration
package.json1 file13 scripts, 29 dependencies, 19 devDependencies, exact versions
vercel.json1 fileBoth cron schedules
PRD.md, TASKS.md629 linesIntended scope, completion state, recorded test counts, open items
CLAUDE.md, DEPLOY.md83 linesDefinition of Done, environment checklist, webhook configuration
Config & scripts11 filesJest, Drizzle, Next.js, TypeScript, ESLint, PostCSS, shadcn, plus four diagnostic scripts

Deliberately excluded

This is a private business repository, and parts of it hold live financial and personal data. The following were read only far enough to establish their shape, and are not reproduced anywhere in this record — not in summary, not in aggregate, and not as illustrative figures.

Material excluded from this record
LocationNature of the content
payrollexport/Two exported payroll reports, one originating from Gusto and one from QuickBooks, held as a spreadsheet and a CSV. Real payroll records
src/server/db/seed-employees.tsA hard-coded roster of real people with departments and employment classifications, plus a headcount summary
src/server/db/seed-contractors.tsNamed contractors with individual payment dates and amounts, per-person totals, and an aggregate spend figure
src/server/db/seed-payroll.tsFourteen payroll periods with gross, tax and net figures and per-employee splits
src/app/api/debug/setup-team/route.tsThe same roster again, in a module constant and in the file's own documentation comment
src/app/api/debug/update-from-gusto/route.tsFourteen pay dates with headcounts and gross totals, and two named employees in comments
src/app/api/chat/route.tsThe system prompt's opening section, describing the company, its product, its customer segments and its commercial model; and the worked figures in its example-response block
src/app/(dashboard)/payroll/page.tsxA mock-data fallback containing invented personal names and placeholder totals — excluded on the same principle even though it is fictional
check-qb.js outputRealm identifier, journal entries, account names and amounts printed when run against the live connection

Where a name does appear on these sheets it is one of three things: a package name, a well-known brand written into a test as a string-handling fixture, or a payroll line-item label such as Debit net pay that the parser mistakes for a person. No balance, revenue figure, payroll amount, hourly rate, headcount, client name, employee name, bank account identifier or QuickBooks realm identifier from the source system appears anywhere in this record.

Note — what the system prompt contains

SYSTEM_PROMPT in src/app/api/chat/route.ts runs to roughly eighty lines and is the most business-revealing artefact in the repository. Its structure can be described without quoting it: a role line, a section describing the company and its commercial model, a role definition, a tool-selection guide mapping six question shapes onto specific tools, a response-style section, a startup-metrics section, a rules section, a missing-data section, a worked example, and a list of prohibitions. Only the structure and the tool-selection behaviour are used on these sheets.