fix: unify tax semantics across accountant and auditor tools - #69
Conversation
The accountant and auditor tool sets each carried their own field-alias chains, document-type matching, and income totals, and they disagreed. On a mixed six-document set, build_tax_summary reported $196,000 while cross_reference_income reported $65,000. Add functions/src/semantic/taxFields.ts as the single source of truth for document classification, extracted-field aliases, and income aggregation. Both tool sets now read from it, so totalIncome equals the sum of the reported sources by construction. Bugs fixed along the way: - auditTools parsed money with bare parseFloat, so "$120,000.00" became 0 and "1,234.56" became 1 (truncated at the comma). Now uses safeFloat. - check_audit_triggers read income as `income ?? gross_wages ?? total_income`, but the extractor writes `wages` for a W-2 — the high-income triggers never fired on a W-2 at all. - cross_reference_income omitted interest, dividends, and Schedule C income from totalDocumentedIncome. - The auditor's 1099-INT/1099-DIV matching did not strip separators, so "1099 INT" was silently skipped. Add a Vitest suite for functions/ (57 tests, offline — the Genkit tools are pure and never reach the model). tools.test.ts pins the cross-tool agreement invariant that would have caught the original drift. Reintroducing the two parsing bugs fails 22 of the 57 tests. Docs: document the Open Policy Agent evaluation for the audit trigger rules (evaluated, not adopted, with the conditions that would change the answer). Correct CLAUDE.md and TASKS.md, which still described the superseded Python architecture — the Flask backend is unreachable from the running app, and two backlog items were already done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SonarCloud flagged 10 issues, all in the new semantic module: - Prefer Number.isNaN / Number.parseFloat over the coercing globals. - Replace the /,|\$/ alternation with the /[,$]/ character class. - Six sites passed `unknown` to String(), which yields "[object Object]" for a non-scalar. Extracted data is model-generated JSON, so a nested object where a scalar was expected is possible and must not become a garbage value. Added an `asText` helper that returns "" for anything that is not a string, number, or boolean, and routed every coercion through it — safeFloat, pickString, normalizeDocType, and the document id/name/type reads in collectIncome and cross_reference_income. Three tests added for the new explicit behavior (60 total). The `upgrade` check has failed on every run since at least June — on schedule, on push to main, and on unrelated PRs — because create-pull-request rejects the detached HEAD that actions/checkout leaves on those events. This PR only tripped it by touching docs/**. Gate the PR-creating step to schedule and workflow_dispatch, and supply an explicit base. Opening a docs-upgrade PR as a side effect of someone else's docs edit was never the intent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Addressed in f87ca57. SonarCloud — all 10 issues fixed, none suppressed. All were in the new
Three tests added for the new explicit behavior — 60 passing.
Fixed by gating the PR-creating step to One thing I did not address: Sonar reports 0.0% Coverage on New Code. The 60 tests do cover this code, but the Sonar scan isn't configured to ingest a coverage report from 🤖 Addressed by Claude Code |
|
|
The three FOSSA checks are pre-existing failures on They fail on every recent commit on the default branch, including this PR's own base (
Going further back, the same three have been red on every commit through So this PR adds zero vulnerabilities and zero license issues. The single delta is one dependency-quality issue from the I have not tried to fix these. Clearing 64 vulnerabilities is separate work with its own risk, and it overlaps the 22 Dependabot alerts open on the default branch (13 high, 8 moderate, 1 low) — worth its own PR rather than being folded into a semantic-layer change. If the +1 is unwelcome, the alternative is Node 22's built-in 🤖 Addressed by Claude Code |



Summary
The accountant and auditor tool sets each carried their own field-alias chains, document-type matching, and income totals — and they disagreed. On a mixed six-document set,
build_tax_summaryreported $196,000 whilecross_reference_incomereported $65,000 for the same documents.This adds
functions/src/semantic/taxFields.tsas the single source of truth for document classification, extracted-field aliases, and income aggregation. Both tool sets now read from it, sototalIncomeequals the sum of the reported sources by construction.Net effect on the tool files is −79 lines.
Bugs fixed
auditToolsparsed money with bareparseFloat"$120,000.00"→0;"1,234.56"→1(truncated at the comma)check_audit_triggersread income asincome ?? gross_wages ?? total_incomewagesfor a W-2, so the high-income audit triggers never fired on a W-2cross_reference_incomebuilt its total from W-2 + 1099-NEC onlytotalDocumentedIncome"1099 INT"was silently skippedTests
functions/had no test framework. This adds Vitest — 57 tests, fully offline. The Genkit tools are invoked directly rather than mocked: their implementations are pure and never reach the model, so a dummy key invitest.config.mtsis sufficient.test/taxFields.test.ts— money parsing, alias resolution, document classification, income aggregationtest/tools.test.ts— the five real tools, including the cross-tool agreement invariant (accountant and auditor must report the same total income), plus a named regression case for each bug aboveVerified by mutation rather than by passing: reintroducing the two parsing bugs in
taxFields.tsfails 22 of the 57 tests.Docs
docs/open-policy-agent.md— evaluation of OPA/Rego for the audit trigger rules. Conclusion: evaluated, not adopted. Eight hardcoded constants do not justify a second language, anopabinary in CI, and WASM cold-start cost; most of OPA's value (bundle distribution, decision logs) targets cross-service authorization, not single-function heuristic scoring. The doc records the two conditions that would change the answer — rules branching by tax year / state / filing status, or a CPA rather than an engineer owning them — and notes that OPA is not the right fix for theApp.tsxrole check (that needs Firebase custom claims).CLAUDE.md/TASKS.md— both still described the superseded Python architecture.firebase.jsondeploys onlyfunctions/, and the frontend never referencesVITE_API_URL, the variable the Flask service is exposed under, sobackend/queue/,backend/embedding/,backend/tax_forms/and friends are unreachable from the running app. Two backlog items were also already done (the extraction pipeline ships inflows/extractor.ts;Dashboard.tsxalready reads filing status from the user profile).Note on the branch name
The branch was opened to explore Apache Ossie. Ossie's semantic-model spec is a genuine fit for this problem, but it is incubating at
v0.2.0.dev0with no stable SDK, so this lands the fix in plain TypeScript first. The module is shaped so an Ossie YAML model can drive it later; that option is recorded inTASKS.mdalongside the OPA one.🤖 Generated with Claude Code