8094f288fa1e8fbf47453fdcd90e66ea7f2dcd28
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ec27228a8e |
style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
01dbef4015 |
feat(dimensions): PR4 reports — dimension-filtered P&L + Resultat per projekt/kostnadsställe (#862)
* feat(dimensions): PR4 reports — dimension-filtered P&L everywhere + Resultat per projekt/kostnadsställe The Project P&L milestone of the dimensions plan (dev_docs §7 PR4). One choke point lights up everything: generateTrialBalance gains options.dimensions (SIE dim → code map) pushed down as jsonb containment (dimensions @>, served by idx_jel_dimensions_gin) on both line queries, with company-wide opening balances dropped when filtered (they cannot be dimension-scoped; P&L-safe by whitelist). Resultatrapport, resultaträkning, huvudbok, monthly-breakdown and the TB drill-down inherit the filter; the KPI route filters only its P&L-side inputs (income statement, months, expense composition) — never cash/VAT. New report lib/reports/dimension-pnl.ts — "Resultat per projekt/ kostnadsställe" (Fortnox Resultatrapport projekt): value-as-column matrix over one dimension with an explicit "(Utan dimension)" bucket computed as the residual against the same trial-balance pass resultatrapport uses, so every row and the Totalt column reconcile with the unfiltered resultatrapport by construction. Registered in REPORT_CATALOG (visible only when dimensions_enabled), slug-routed view + xlsx export. UI: DimensionFilter (dimension + value picker, persistent "Filtrerad — ej fullständig rapport" chip) mounts in FocusedReport for catalog entries flagged dimensions: true; huvudbok rows show line dim codes. Statutory exclusion pinned by TEST, not convention: lib/reports/__tests__/dimension-statutory-guard.test.ts fails if the filter parser leaks into balance sheet, balansrapport, kassaflöde, VAT, SIE or full-archive routes/generators, or if the catalog whitelist widens. MCP: new gnubok_get_dimension_pnl (reports:read); dimensions filter arg on get_trial_balance/get_income_statement/get_general_ledger with resolve-don't-select (names → registry codes, resolution echoes); query_journal totals fixed to aggregate the FULL match set (was silently slice-scoped while claiming otherwise) with an honest totals_scope field, plus group_by / group_by_dimension aggregation. Also: voucher-detail dim-6 badge now uses the registry name instead of the non-standard "PR" abbreviation (#859 review follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dimensions): address #862 review — export disclosure, prior-column suppression, period-label honesty, route hardening - Filtered XLSX/PDF exports now carry the partial-view disclosure past the file boundary (BFNAR 2013:2): filename suffix (-dim6-p001), a "Filtrerad … — ej fullständig rapport" row on every sheet, and a header note/title line in the PDFs. - Resultatrapport drops the prior-year column when a dimension filter is active — project codes are time-limited under K2/K3, so "this code last year" may be a different project (same rule as narrowed date ranges). - dimension-pnl no longer accepts fromDate: the matrix is cumulative from period_start by design (closing-balance semantics), and the period label now states exactly that instead of echoing a lower bound that was never applied. Routes/MCP tool updated to toDate-only. - dimension-pnl routes 404 on an unknown/foreign period id and cap dim_no to 4 digits (matching the MCP tool's PostgREST-path guard, which the generator now also enforces itself). - Statutory-guard test's generateTrialBalance call-site scan is paren-aware instead of a 300-char window; added fully-untagged and injection-guard test cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6629964780 |
fix(mcp): unblock query_journal text search across line + entry descriptions (#605)
* fix(mcp): unblock query_journal text search across line+entry descriptions `gnubok_query_journal` issued a single PostgREST `.or()` filter mixing a base-table column (line_description on journal_entry_lines) with a column on the embedded inner-joined resource (journal_entries.description). PostgREST's flat comma OR syntax cannot span base and embedded resources and returned 400 "failed to parse logic tree" on every free-text search, so the agent could not look up history (e.g. "har vi bokfört Google innan?") via the MCP tool. Refactor: pull the common filter chain into a `buildBaseQuery()` helper and issue two parallel `.ilike()` queries — one on `line_description`, one on `journal_entries.description` — then merge by line id, re-sort, and slice to `limit`. Same pattern as `lib/invoices/duplicate-payment-candidates.ts`, with the same rationale (LIKE-DSL injection risk + cross-embed unsupported). Drops the obsolete comma-stripping pre-processing on the search term (only needed because the value was being injected into PostgREST's OR DSL); LIKE wildcard escaping (`%` and `_`) is preserved. Adds four tests covering: merge across both legs, dedup of cross-leg overlap, correct columns + escaped pattern sent to `.ilike`, and LIKE-wildcard escape end-to-end. Existing amount-filter / accounts-cap / truncation tests unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): query_journal PR review — honest truncation, wider leg, error sanitisation Address PR #605 review (Greptile + compliance swarm). - truncated math: previously dbMatched = byLine.count + byEntry.count inflated the counter every time a row matched both legs (e.g. an entry whose line description AND header description both contain "Google"). total_lines was off by up to 2× and truncated was forced true even when every distinct match was already returned, sending the agent into unnecessary narrow-and-retry loops. dbMatched is now merged.size — the honest distinct-row count among what we fetched. A new legCapHit signal drives truncated when either leg's fetch window filled, so we never lie the other direction either. - per-leg fetch widened to 2 × limit. Previously each leg was capped at `limit` independently, so when one column was much more selective than the other (e.g. 150 line matches vs 5 entry matches), the merge could drop globally-ranked rows from the chronologically newer tail. The final slice still caps at `limit`; the wider per-leg window just gives the merge a better tail to choose from. - text input length cap (200 chars). Defence-in-depth against pathological inputs even though .ilike() parameterises the value (compliance A.8.28). - error sanitisation in the text-search path. Raw PostgREST messages can surface internal schema details (table names, constraint names) to the caller. We now log details server-side via the existing module logger and throw a generic message (compliance V16). - comma-stripping rationale documented inline so the next reviewer doesn't re-add it. The previous defence was needed because the value was injected into PostgREST's OR DSL where `,` is the separator; the parameterised .ilike() path treats `,` as a literal and stripping it would mangle real-comma searches (compliance V1.2.5). Adds 4 regression tests: overlap-doesn't-falsely-truncate, leg-cap-hit flags truncated, text>200 rejected, raw schema name never leaked. All 14 query_journal tests pass; 266/266 MCP suite green; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): query_journal PR review round 2 — schema guards, leg cap, audit log Address compliance swarm findings on commit 3dfab46c. - text input: add `maxLength: 200` to the JSON Schema so the limit is enforced at the boundary by the MCP runtime, not only inside execute(). Inline guard kept as belt-and-suspenders (ASVS V2.2, ISO 27001 A.8.28). - limit input: add `minimum: 1, maximum: 500` to the JSON Schema (matches the existing Math.min cap inside execute()) so the bound is visible to agents and enforced declaratively. - legLimit hard cap: `Math.min(limit * 2, 500)`. Previously legLimit scaled with limit unbounded, so a maxed-out limit triggered 2×1000-row fetches. Cap is independent of the caller-supplied limit (ASVS V2.3). - audit log: include `userId` alongside `companyId` in both log.warn payloads so failed-query events can be correlated to the actor for incident investigation (ASVS V16, SOC 2 CC7.2). - new test: assert both parallel ilike legs in the text-search path issue `.eq('journal_entries.company_id', companyId)` — defence-in-depth against a future refactor accidentally dropping tenant scoping from one leg (SOC 2 CC6.1). Not addressed (with rationale): - ASVS V1.2.5 / SOC 2 CC7.2 "log.warn body contains raw PostgREST error text". The swedish-accounting-compliance reviewer explicitly cleared this on the same commit ("the schema-leak is plugged in both paths"). The thrown error is generic; raw detail lives only in internal structured logs (Sentry, etc.) where it is needed for incident triage. Same pattern is used by 20+ other tools in this file; scrubbing only this one is inconsistent. Defer to a project-wide logger sanitiser. - Swedish reviewer's "reversed mixed with posted in status=all": false positive — `status` IS surfaced per line in the response shape (see the lines.map projection in server.ts). - Swedish reviewer's "debit XOR credit not enforced": insert-time concern, not a read-tool concern. Out of scope. Tests: 15 query_journal cases (4 new this round); 267 MCP suite; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4131db2894 |
chore: MCP intent-tools, BankID enrichment table, multi-tenant fixes (#402)
* chore: MCP intent-tools, BankID enrichment table, multi-tenant fixes MCP server gains six intent-shaped tools that collapse multi-call agent flows into one: vat_close_check, query_journal, auto_match_period, create_supplier_invoice_from_inbox, audit_package, year_end_readiness. Tools wired into TOOL_SCOPE_MAP and OPERATION_RISK_TIERS as appropriate (create_supplier_invoice_from_inbox at medium tier — reversible until approve, but stages a leverantörsskuld). BankID enrichment now persists to a dedicated bankid_enrichment table keyed by user_id. extension_data has been company-scoped (NOT NULL company_id) since the multi-tenant refactor, so every BankID signup has silently been failing the enrichment upsert. Select-company picker reads from the new table. delete_last_voucher (BFNAR 2013:2) needs to clear document_attachments.journal_entry_id before deleting the entry, but the new document immutability trigger blocks that UPDATE. Added the same gnubok.allow_delete transaction-scoped bypass pattern used by the journal-entry/line/retention triggers. pg-real tests cover the happy path, the unauthorized direct UPDATE, and the swap-to-different-entry attempt under the bypass flag. fiscal_periods.no_overlapping_fiscal_periods exclusion was scoped to user_id from before multi-tenant — rebound to company_id so the same user can have overlapping fiscal years across companies they own/are member of. Also adds scripts/seed-demo-account.ts for end-to-end demo seeding (two companies, full FY2025, active FY2026 with mixed state). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pr-402): address review feedback Migrations - Drop 20260506140000_document_journal_entry_immutability_delete_bypass.sql: redundant with 20260506140000_document_journal_entry_immutability_bypass.sql that landed on main while this branch was open. Both share the same gnubok.allow_delete pattern; main's version is what the DB actually has. - Rename 20260506150000_bankid_enrichment_table.sql → 20260506160000_bankid_enrichment_table.sql to clear the timestamp clash with 20260506150000_protect_document_journal_link.sql on main (Supabase branch preview was failing on schema_migrations PK collision). Tests - Drop the swap-under-flag test from delete-last-voucher.pg.test.ts: main's bypass returns NEW unconditionally when gnubok.allow_delete='true', so the swap is permitted. Drop the duplicate happy-path test (already covered by 'clears journal_entry_id on attached documents and deletes the voucher'). Keep the unauthorized-direct-UPDATE test. - Add bankid-enrichment.pg.test.ts covering the SELECT RLS policy: user reads own row, cannot read another user's row, INSERT denied for authenticated. gnubok_query_journal - amount_min/amount_max is applied post-fetch (PostgREST can't OR abs(debit) and abs(credit) cleanly), but PostgREST's count is computed pre-filter. Reporting that as total_lines mislead agents into paginating a tail that was already filtered out. When the amount filter is applied, anchor total_lines and truncated to the filtered set and surface db_matched_pre_amount_filter + amount_filter_applied_post_fetch separately. - Escape `_` in the free-text LIKE filter so a search for "2_441" doesn't match "2X441". VAT close check - Reverse-charge blocker no longer fires on ruta 30 (seller-side domestic omvänd skattskyldighet) — the seller books no VAT, the buyer does, so missing ruta 48 is expected. Now scoped to ruta 31/32 (EU acquisition) where the buyer must book both calculated output (2615) and matching ingående moms (2645). - High-value receipt threshold no longer reads journal_entries.total_amount (column doesn't exist; check silently never fired). Sums debits across the entry's lines, which equals the gross for ordinary purchase entries — comparing a gross figure against the BFL/ML 4 000 SEK threshold per ML 17 kap 26–28 §. seed-demo-account.ts - Require an explicit email argument; refuse to run with the previously hardcoded fallback that would silently target a real user. Ensure email is non-undefined for downstream typing. - Type the supabase fiscal_periods insert result locally so tsc no longer reports 'fp implicitly any' from the loose untyped client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): adjust fiscal-period-start-day pg test for per-company overlap The pg-real failure on PR #402 was a latent bug surfaced by this branch's fiscal_periods exclusion constraint flip from user_id to company_id (migration 20260506140100). The test was inserting periods that overlapped seedCompany's default 2026-01-01..2026-12-31 period; the previous constraint slipped past it because the test's INSERT didn't set user_id (NULL escapes the WITH = match), so two same-company overlapping periods silently coexisted. Now that the constraint correctly fires per company, pick years that don't overlap with the seeded 2026 period. The trigger's behavior under test (allow mid-month start when no earlier period exists, allow back-dated SIE imports, reject mid-month start when an earlier period exists) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(vat-close-check): correct reverse-charge/import blocker rutor Rutor 30/31/32 are the buyer's calculated utgående moms on reverse- charge purchases (domestic byggtjänster/electronics → 2614 → ruta 30; EU goods → 2624 → ruta 31; EU services → 2634 → ruta 32). The buyer must also book matching ingående moms (2647 inhemskt / 2645 utlandet → ruta 48). The previous fix removed ruta 30 on the basis that it was seller-side; that's incorrect — domestic-RC sellers book no VAT at all (they report only beskattningsunderlag on ruta 41), so 2614 only sees buyer-side entries. Restore ruta 30. Also extend the check to import rutor 60/61/62 (non-EU import VAT declared via momsdeklaration since 2015 — 2615/2625/2635). Same mechanic: importer books output VAT on these rutor and deducts the input side via ruta 48. SaaS-from-AWS / OpenAI / Vercel companies hit this path; without including 60/61/62 the blocker would silently miss their misbookings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): expose ruta 60/61/62 (import VAT) on the local VatReportResult The vat-close-check fix referenced vatReport.rutor.ruta60/61/62 but the MCP server's local VatReportResult type only carries ruta 05-49. Build broke on tsc. Extend the MCP server's slim VAT report to also project import VAT — 2615 → ruta 60 (25%), 2625 → ruta 61 (12%), 2635 → ruta 62 (6%) — and fold those into ruta 49 (att betala/återfå). Mirrors the BAS-to-Ruta mapping in lib/reports/vat-declaration.ts. Output schema and required list updated accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |