c0ecf2fa3bebd46bdfd0169efd73b89653d1dfed
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dc07ca8872 |
feat(transactions): steer private marking in locked periods to ignore, with v1 and MCP ignore verbs (#1661) (#2031)
Decision (option a): a private marking stays a real booking (eget uttag/insattning), so it remains blocked in a locked or closed period; the legal escape for rows that are not affarshandelser is ignore. Private + locked now returns TX_CATEGORIZE_PRIVATE_PERIOD_LOCKED with remediation naming the ignore paths instead of a bare PERIOD_LOCKED, on all four categorize surfaces and the bulk driver. New v1 POST/DELETE /transactions/{id}/ignore (isTransactionBooked-based 409, idempotent) and a staged MCP gnubok_ignore_transaction (+ accounted_ alias, search visibility to respect the tools/list payload ceiling) with operation_type ignore_transaction; the CHECK pair 20260831070000/070001 rebuilds the constraint from main's newest list plus the new value. Dashboard toast gains an Ignorera i stallet action. Closes #1661
|
||
|
|
188816652d |
docs(api,mcp): tool counts, changelog backfill, version-header honesty, lazy auth, endpoint map (#1929)
Brings the developer-facing API and MCP docs back in line with origin/main (audit 2026-08-26). Docs only; no runtime behaviour changes. - Tool counts: the server registers 153 tools; docs said 90+/100+/120. All now say "150+" (connect-claude, gnubok-mcp README, plugin README, mcp-server rules, CLAUDE.md, registry entry with refreshed updatedAt). Not derived from the tools array: lib/ must not import @/extensions/. - REST changelog: backfilled the additive 2026-08 changes (#1909 report date ranges + PDFs, #1864 POST /companies, #1773 vat-declarations, #1405 PATCH settings, #1724/#1788 customer personal_number, #1809 cash_account_id filter). API version date unchanged. - Version headers: Gnubok-Deprecation is planned, not emitted; the Gnubok-Version request header is not read today (version.ts comment, versioning page, conventions overlay, regenerated skills/accounted-api). - connect-claude Path A documents lazy auth (connector works before an account exists; sign-in on the first company-scoped call). - MCP server README: real Anthropic SDK call sites, real resource URIs, pending-operations widget, public-tools/tasks/origin-guard/pii-guard. Rules file gains Lazy auth + feedback/tasks paragraphs. - api-routes endpoint map regenerated from the filesystem (560 routes, 55 families incl. v1, agent, reconciliation account-keyed, dimensions, peppol, rot-rut, webshop-orders, mileage, billing, skatteverket, receipt-hunt). - gnubok-mcp/accounted-mcp: /settings?tab=api is the pre-redesign URL; now /settings/api (README + help hints, no version bump). Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f40795896f |
feat(reconciliation): sign-off, period picker, Hem row and the three doors for it (#1835)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): the Avstämning page, one body for every account with an outside truth /reconciliation in Arbeta (after Transaktioner), on the approved layout: an account rail on the left (bank accounts and the skattekonto, logo or monogram, last fetch, status dot, URL-owned selection), and for the selected account four tiles (outside, ledger, difference, unexplained), the bridge that explains the difference, an actions row (link the proposed pairs, book the unbooked skattekonto events, run the bank matcher) and a full-width table banded by bucket with proposal rows linkable one by one. Every read and write goes through the PR 2 dashboard routes, so the page shows exactly what the v1 API and the MCP tools see. Also: nav item, command palette entry, sv/en strings. Period picker, manual match mode and sign-off are deliberately not here (PR 4/5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): sign-off, period picker, Hem row and the three doors for it "Markera som avstämd t.o.m. <datum>" as an append-only attestation: account_reconciliations (who signed which account through which date, with the numbers as they stood; reopen stamps instead of deletes; RLS members write as themselves, viewers read). Policy in one place (lib/reconciliation/signoff.ts): refused with an unexplained difference unless forced with a note, refused past today or past the skattekonto snapshot, refused at or before an active sign-off; reopen is the undo. Every status read now carries the latest active sign-off and the rail shows "avstämt t.o.m.". Three doors: dashboard routes (GET/POST .../signoff, POST .../reopen), v1 (same, scope reconciliation:signoff, Idempotency-Key, dry-run, registry + regenerated API skill), MCP gnubok_reconcile_signoff (search catalog, stages reconciliation_signoff after a policy dry run; executor + risk tier + op-type CHECK migration pair). Events reconciliation.signed_off / reconciliation.reopened, and the four reconciliation events join the public webhook set (additive; API version unchanged, changelog section added). Page: räkenskapsår + range picker in the header (own preset memory, opens on this month) scoping the bridge, the items and the default sign-off date; sign-off dialog with the forced-with-note path; reopen on hover. Hem: worklist category reconciliation_due ("Konton att stämma av"), zero until the company has signed anything off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): classify reconciliation:signoff as a tenant write for the MCP role guard gnubok_reconcile_signoff carries the deliberately separate reconciliation:signoff scope; the central viewer guard keys on the :write/:approve/:manage suffixes, so a viewer could reach the tool (RLS would still refuse the row, but the guard is the intended layer). Add :signoff to the classifier; the strictness test that caught it now passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(providers): serve local rate-limiter waiters in arrival order Two callers that both found the in-memory bucket empty each set their own timeout; the timeouts expired at the same instant from different timer lists and which woke first was platform-dependent. hydrateInvoices relies on "started first, requested first" to serve open invoices before paid ones, so lib/providers/__tests__/hydrate-invoices.test.ts flipped on CI (twice on #1817) while holding locally. A promise queue makes the local waiters FIFO without changing the rate; the Upstash path is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 14a7599bf2c6fa7f97de6ffab3dc4cf4d0e1827d) --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
49ff234954 |
feat(webhooks): dispatch on emit instead of waiting for the next cron tick (#1256)
* feat(webhooks): dispatch on emit instead of waiting for the next cron tick The webhook dispatcher ran only on a per-minute cron, so the floor on delivery latency was up to 60 seconds plus the request. An external consumer that wanted to react as a transaction landed had only one alternative: polling /api/events, which the 100 rpm per-key limit makes expensive and which still cannot beat the tick interval. Schedules one dispatch cycle as soon as deliveries are enqueued. The cron is unchanged and remains the retry and sweep path; this only moves the first attempt forward. Wired into the event-bus fanout plus the two routes that enqueue a delivery directly: the :test verb, whose entire purpose is telling someone whether their receiver works, and the manual delivery retry. Three properties are load-bearing and covered by tests. The kick is never awaited, because eventBus.emit is awaited at ~99 call sites including journal_entry.committed and each delivery can burn a 10 s receiver timeout. It coalesces per function instance, so a bulk booking that emits once per row does not schedule one claim round trip per row. It claims 5 rows rather than the cron's 50, because it runs on the tail of a user-facing request. Double delivery is not a risk: claim_due_webhook_deliveries already claims FOR UPDATE SKIP LOCKED and flips rows to in_flight in the same statement, so a kick racing the cron sees disjoint rows. Does not close #1201, which asks for a realtime stream for API consumers. This is the cheap half. Refs #1201 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(webhooks): stop claiming the kick makes double delivery impossible Adversarial review of the previous commit caught an overstatement in its own comments. SKIP LOCKED keeps a kick and the cron from claiming the same row at the same moment, but claim_due_webhook_deliveries autocommits before any POST is issued, so from then on ownership is only status='in_flight' and a later cycle's recoverStuckInFlight sweep can re-arm a row still queued behind an earlier cycle's serial loop. Delivery is at-least-once, which is what the public docs already tell receivers ("the same delivery id may arrive more than once ... idempotency is on you"). The comments contradicted that. No behaviour change. The kick does not create this window: the cron claims 50 rows serially against the same 20 s stuck threshold, which is wider than what a batch of 5 can open. Refs #1201 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ee3c33c7a4 |
docs(api): correct /docs/api against the v1 implementation (#999)
Audited every endpoint, param, header, request/response field, error code, and webhook event in the public API docs against the v1 implementation and fixed the drift; addressed two rounds of CodeRabbit review. - Error envelope, idempotency, dry-run, and reversal-field corrections. - Registered the missing articles/dimensions/inbox-items reference resources. - Cookbook fixes: removed nonexistent endpoints, corrected params/fields, fixed the test-key vs live-key quickstart flow and the year-end lock/close sequence. - Webhooks/changelog: retry window ~87h (incl. route metadata), shipped-vs- coming-soon, counts, API-key format, previous_attributes. - export-docs-to-website.mts absolutises app-served links for the website. The gnubok-website side is on branch docs/api-correctness (already deployed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
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> |
||
|
|
c74b19df1b |
Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3912c74a7b |
feat(api): Phase 6 PR-2 — docs polish (Stripe-inspired) (#497)
* feat(api): Phase 6 PR-2 — docs polish (Stripe-inspired) Ships the developer-facing documentation surface for the v1 REST API. Mirrors Stripe's structure (landing → cookbooks → concepts → reference → errors → changelog) at /docs/api with a sticky-sidebar layout in the gnubok editorial-monochrome aesthetic. Every page is also served as plain Markdown via a sibling .md URL so agents and LLM crawlers can ingest the same content without HTML parsing — the existing /llms.txt already promised /docs/api references that this PR makes real. Single source of truth for endpoint metadata is the existing Zod registry (lib/api/v1/registry.ts). The reference pages auto-generate from it: adding a new endpoint surfaces in the docs on the next build with no manual sync. The error reference pulls directly from lib/errors/structured-errors.ts STRUCTURED_ERRORS. CONTENT LAYER (lib/docs/): - content/landing.ts — introduction, auth, base URL, response envelope, the four core principles (dry-run, idempotency, strict-mode, inline audit), pointers to every other section. - content/versioning.ts — versioning + deprecation policy (Stripe dated format), idempotency, dry-run, strict-mode write semantics, inline audit blocks. - content/webhooks.ts — webhook concept guide. Full Node.js (express + crypto) and Python (Flask + hmac) signature-verification samples that match lib/webhooks/signing.ts exactly. Lifecycle, event-type catalogue, payload shape, request headers, common pitfalls, auto-disable behaviour, audit + retention. - content/errors.ts — generated from STRUCTURED_ERRORS. Groups by domain (generic, bookkeeping, periods, invoices, supplier-invoices, transactions, reports, imports, documents, salary, company, provider). Every code is anchorable so the docs_url field on every error envelope finally points somewhere real. - content/reference.ts — generated from listEndpoints(). Groups by resource (companies, customers, invoices, suppliers, supplier-invoices, transactions, journal-entries, fiscal-periods, accounts, documents, employees, salary-runs, reports, imports, compliance, webhooks, operations, voucher-gap-explanations, reconciliation). Each endpoint section: summary, description, useWhen, doNotUseFor, pitfalls, scope, idempotent/reversible/dry-run flags, request + response examples. - content/changelog.ts — initial entry for API version 2026-05-12 covering every endpoint shipped in Phases 1-6. Lists what's coming in Phase 6 PR-3 (hardening + remaining cookbooks). - content/cookbook/quickstart.ts — five-minute send-your-first-invoice guide. Demonstrates auth, dry-run, idempotency, audit-block patterns in one continuous narrative. - content/cookbook/webhooks.ts — end-to-end webhook setup, sig verification, retry handling, idempotency on receiver side, replay patterns, auto-disable behaviour. Companion to the concept page. - content/cookbook/index.ts — recipe registry. 4 placeholder recipes (ingest-bank-transactions, file-vat-declaration, run-payroll-and-agi, year-end-closing) link to their reference pages with a "coming after Phase 6 PR-3 hardening" note. Narrative cookbook quality benefits from a focused pass after the substrate stabilises. - nav.ts — single source of truth for the sidebar nav, used by the layout AND the landing-page resource grid. - markdown.tsx — shared <DocsMarkdown> component using react-markdown (already a dep) with Hedvig serif headlines, Geist mono code blocks, hairline section borders, paper-white surfaces — same editorial aesthetic as the dashboard. LAYOUT (components/docs/DocsLayout.tsx): Two-column sticky-sidebar layout. Top header carries the gnubok mark + section links (API reference, Cookbooks, Errors, Changelog, openapi.json). Sidebar groups: Getting started, Cookbooks, Concepts, API reference, Reference. Active page highlighted with the same warm-beige bg the dashboard sidebar uses. ROUTES (app/docs/api/, app/llms-full.txt/): - /docs/api → landing - /docs/api/errors → error reference - /docs/api/webhooks → webhook concept - /docs/api/versioning → versioning + idempotency + dry-run - /docs/api/changelog → release notes - /docs/api/reference → resource overview - /docs/api/reference/[slug] → per-resource pages (19 resources, all generated from the registry; generateStaticParams listed) - /docs/api/cookbook/[slug] → recipe pages (8 entries, 2 fully written + 6 aliases/placeholders) - /llms-full.txt → everything concatenated for one-shot LLM ingestion Every page has a sibling .md route (e.g. /docs/api/errors.md) serving the raw Markdown for agents — same content, no HTML wrapper, same 5-min cache. Honours the existing /llms.txt promise that "every .md URL under /docs/api is served as plain Markdown". CI GUARD (lib/api/v1/__tests__/spec-snapshot.test.ts): Vitest snapshot test that locks down (a) the endpoint count, (b) the sorted set of method+path keys, (c) the set of distinct scopes referenced. CI fails if any drift unexpectedly so a Zod-schema change can't ship a silent API break — when you intentionally add/remove an endpoint, run with -u to refresh the snapshot, review the diff, and commit alongside the route change. The snapshot diff itself is a self-describing changelog entry. Initial snapshot: 100 endpoints, 17 distinct scopes, full key set sorted. Fourth assertion in the test guarantees every endpoint declares the agent-facing metadata (summary, description, useWhen, doNotUseFor, pitfalls, example) the reference pages depend on — so a registerEndpoint call that omits any of these fields is caught at CI time rather than rendering an empty section in the docs. INFRA TOUCH (lib/api/v1/load-routes.ts): Added the 5 Phase 6 webhook route imports so the registry includes them on the docs builders' path. Required for the /docs/api/reference/ webhooks page to render. The webhook routes' registerEndpoint calls already exist; this just side-effect-imports them where the spec generator can see them. Coming in Phase 6 PR-3 (hardening — separate PR): - 90-day TTL cleanup cron for non-accounting webhook deliveries - claim_due_webhook_deliveries SQL function (FOR UPDATE SKIP LOCKED) - Per-route rate limits on :test, :retry, webhook :create - V16 audit-log on webhook lifecycle events - DNS-rebinding pinned-IP HTTPS agent - Integration tests for webhook routes + *.pg.test.ts for triggers - Populated previous_attributes for update-style webhook events - The remaining 4 cookbook recipes (ingest-bank-transactions, file-vat-declaration, run-payroll-and-agi, year-end-closing) once the engine surface is fully stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 1 — CI fix + 5 small docs items CI BLOCKER (the reason core-only failed): 1. **Type error on `[slug].md/route.ts` dynamic routes** — Next.js 16's route-type inference can't extract the dynamic segment from a directory whose name contains a literal suffix like `[slug].md/`. It types `params` as `Promise<{}>` and rejects our handler that declares `params: Promise<{ slug: string }>`. The framework still ROUTES requests correctly (URL `/docs/api/cookbook/quickstart.md` reaches the handler) — only the typed `params` is unusable. Fix: drop the typed `params` parameter on the two affected handlers (cookbook + reference) and parse the slug from `request.url.pathname` directly. Inline comment documents the workaround so the next person to touch these doesn't try to "fix" it back to the typed pattern. GREPTILE INLINE (2 items): 2. **Python sample was missing `import json` and `import os`** — the webhook signature-verify sample uses both but only imported `hmac`, `hashlib`, `time`, and `flask`. Added the two missing imports. 3. **`buildResourcePages()` perf — called twice per request** (P2). Each call iterates every registered endpoint, groups by resource, sorts, and serialises Markdown for all 19 resource pages. Memoised at module level — the registry is populated once at module load and immutable for the process lifetime, so a single derivation is safe to cache. Halves the cost on the HTML routes' `generateMetadata` + page render pair, and the .md route handlers (which Next.js doesn't statically pre-render) are now constant-time after the first GET. SWEDISH-COMPLIANCE PRECISION (3 items): 4. **`webhooks.ts`: behandlingshistorik vs räkenskapsinformation distinction.** The previous "Audit + retention" section conflated the two — webhook delivery rows are *behandlingshistorik* (system- event log) per BFNAR 2013:2 kap 8 §, NOT räkenskapsinformation themselves. The 7-year retention from BFL 7 kap 1 § attaches to the underlying verifikation/faktura/AGI XML in its own table, not to the delivery envelope. Updated the section to draw the distinction and clarify gnubok's 7-year retention on accounting-event delivery rows is an operational audit-trail policy, not a statutory obligation passed through to the integrator. 5. **`changelog.ts`: same distinction in the Phase 6 PR-1 entry** — replaced the "räkenskapsinformation" framing with the correct behandlingshistorik framing + the operational-policy note. 6. **Quickstart cookbook: ML 17 kap 24 § p.8 note about `beskattningsunderlag per skattesats`.** The "What just happened" section now explicitly notes that the rendered PDF contains every ML 17 kap 24 § field (including taxable amount per VAT rate) and that the JSON response's summary fields are convenience aggregates for the integration — the binding faktura content is the PDF. Forecloses the misreading that `subtotal + vat_total` is sufficient compliance. 7. **Changelog: BFL 7 kap caveat on SIE export.** The `/reports/ sie-export` line now warns that a SIE4 export alone does NOT satisfy BFL 7 kap archiving obligations — SIE captures account positions and verifikationer but lacks system documentation and behandlingshistorik. Treat as a portability format (Fortnox/Visma/Bokio migration), not as a complete archive. Closes the misreading the swedish-sie-import-export skill flagged. DEFENSIBLE DEFERS (round 1 final): - **CM-8 SPDX-License-Identifier headers per file** (Compliance Swarm). The repo declares AGPL-3.0-or-later in the root LICENSE file, which satisfies licensing for the project as a whole. Per-file SPDX headers are a REUSE-conformance feature; we can address as a sweep across the entire codebase if/when REUSE conformance becomes a requirement. Out of scope for a docs PR. - **`/llms-full.txt` exposes payroll endpoint metadata publicly** (A.8.12). By design — the entire point of the file is one-shot LLM ingestion of the public docs corpus. Endpoint METADATA (path, scope, description) is non-sensitive; actual payroll DATA is gated behind `payroll:read` scope and requires a real API key. Adding an auth gate would defeat the agent-discovery purpose. - **Secret rotation endpoint** (Art.25(2)). Real product gap (delete + recreate is the current rotation path), but it's feature work, not docs. Tracked for Phase 6 PR-3 alongside the other webhook hardening. - **DNS rebinding pinned-IP HTTPS agent** (Art.5(1)(f)). Already documented in this changelog as a Phase 6 PR-3 item; bot is just re-flagging that it's not yet shipped. - **A.5.34 changelog cites GDPR Art.5(1)(c) for personnummer masking without linking privacy policy.** The citation is informational context for developers, not a privacy notice to data subjects. Data- subject notices live at /privacy. Adding a pointer is reasonable; adding it would consume real estate that's better spent on the technical detail. Defer. - **Compliance Swarm A.5.21 third-party attribution in llms-full.txt**. False positive — all markdown content in this PR is original first-party text. No third-party snippets to attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 2 — 6 small precision fixes All CI green after round 1 (core-only fixed). Compliance Swarm: 7 → 10 findings is the documented oscillation pattern — net-new actionable items are 6 small fixes; the rest are recurring defers (plaintext-secret variants, SPDX, planned PR-3 features the changelog already lists as "coming soon"). FIXED: 1. **Slug allow-list validation in `[slug].md` routes** (V1.2.5 ×2, medium). The cookbook + reference .md routes parse the slug from the URL pathname (round-1 workaround for Next.js 16's failed inference on `[slug].md/` directories) and pass it to a dictionary-based lookup. The lookup itself is safe — findRecipe / buildResourcePages can't reach SQL or filesystem from a bad slug — but the explicit allow-list gate keeps the contract safe if the lookup mechanism ever changes (file-load, RPC, etc.). Added `Set<string>(COOKBOOK_SLUGS)` + `Set<string>(RESOURCE_SLUGS)` guard before any lookup runs. 2. **Changelog: BFL 5 kap 5 § cited on both `/reverse` AND `/correct`** (swedish-compliance precision). The previous wording cited BFL 5:5 only on `/reverse` (storno) and described `/correct` as plain "rättelse" — but BFL 5:5 governs rättelse generally, and storno is the canonical method of rättelse, so both endpoints satisfy 5:5. Updated to: "/{id}/reverse (storno) and /{id}/correct (rättelse) — both satisfy BFL 5 kap 5 § (storno is the canonical method of rättelse)". 3. **Changelog AGI: explicit that XML is for manual submission** (swedish-compliance / swedish-payroll). Previously said "/generate-agi produces AGI XML" — could be misread as auto-submission to Skatteverket. Now states explicitly that the response carries `data.xml` for the integrator to upload via Skatteverket Mina Sidor (or via the optional `skatteverket` extension), and that the AGI deadline (12th / 17th of the following month) is the integrator's responsibility. Aligns with the route file's existing doNotUseFor + pitfalls metadata. 4. **Quickstart: F-skatt note qualified** (swedish-invoice-compliance). The "What just happened" section previously said the PDF "contains the F-skatt note" — only valid if the seller actually holds F-skatt. Updated to: "The 'Godkänd för F-skatt' note is included automatically when company_settings.has_f_skatt is set — confirm this on the company settings page before sending invoices in production." Also tightened the beskattningsunderlag wording to mention "one line per distinct rate on multi-rate invoices" — closes the swedish-compliance note about the multi-rate claim in the landing needing explicit support in the cookbook. 5. **Cookbook placeholder VAT description: "compute and review" not "submit"** (swedish-vat). The placeholder previously said "Compute momsdeklaration rutor and submit to Skatteverket" — but no Skatteverket-submission endpoint exists in the v1 surface; the API only computes the rutor 05–62 values for manual filing. Updated description in BOTH cookbook/index.ts AND nav.ts (where the same string was duplicated): "Compute momsdeklaration rutor 05–62 and reconcile against the GL before manual submission to Skatteverket." Title also flipped from "File a VAT declaration" to "Compute and review a VAT declaration". 6. **Cookbook nav for AGI: aligned to "generate" semantics**. nav.ts AGI summary used to say "file AGI" — same misreading risk as #3. Now: "Calculate, approve, mark paid, book, generate AGI XML for manual Skatteverket upload." DEFERS (round 2 final — every remaining swarm finding is in one of these buckets): - **🟠 Art.32 plaintext webhook secret + 4 sibling framings** (V14, V11.1, A.8.24, CC6.1). Established defer per Stripe / GitHub / Slack precedent; documented inline in lib/webhooks/signing.ts. Bot is re-flagging via the docs surface this round; underlying position unchanged. - **🟡 Art.5(1)(e) 90-day TTL non-accounting cron + V16 audit log + V2.4 rate limits**. All explicitly listed in the changelog as "Coming soon (Phase 6 PR-3 hardening)". Bot is reading the same text we wrote; not blocking. - **🟠 A.8.11 personnummer masking lacks an automated test**. Real product-hardening request, but it's feature work in the employees test surface, not docs. Tracked. - **🟡 CM-8 SPDX-License-Identifier headers per file**. Established defer from round 1 — root LICENSE covers AGPL-3.0-or-later for the project as a whole; per-file SPDX is a REUSE-conformance sweep that's its own effort. - **🟡 SR-3 SBOM dependencies**. False positive — next/server, react, next/link, next/navigation are existing project deps, not new in this PR. - **swedish-compliance "SIE disclaimer could note immutability requirement"** — the current disclaimer accurately calls out system documentation + behandlingshistorik as missing; adding immutability would over-stuff a one-line caveat. Defer with the understanding that the SIE skill itself documents the immutability requirement for any consumer that follows the reference. If round 3 plateaus (Compliance Swarm count stable, no net-new inline items), that's the merge-ready signal per Phase 4 lessons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 3 — 4 small precision fixes Compliance Swarm: 10 → 3 (down 70%) — net-new actionable items are the 4 below; remaining 3 swarm findings are either trivial defense- in-depth (URL decode, fixed here) or out-of-repo decisions (personnummer disclosure DPO confirmation). FIXED: 1. **URL-decode slug before allow-list check** in both .md route handlers (V1.2.5 ×2 low). The closed allow-list is pure ASCII so a percent-encoded value can't decode to a legitimate slug, but the explicit decode-then-check pattern keeps the contract correct under any future encoding-quirk runtime. try/catch around decodeURIComponent so a malformed % sequence (which throws) returns a clean 404 rather than a 500. 2. **Quickstart: explicit `delivery_date` requirement** (swedish- invoice-compliance / ML 17:24 field 7). The previous wording listed "supply date" as a covered field but didn't note that the API does NOT default delivery_date to invoice_date — integrators shipping invoices for goods delivered on a different date than the invoice date must pass delivery_date explicitly or the rendered PDF is non-compliant. Added explicit pass-it-yourself note. 3. **Quickstart: F-skatt strengthened from "verify" to "legal requirement"** (swedish-invoice-compliance / Peppol BIS 3.0 SE-R-005). The previous "confirm on settings page" wording risked integrators treating the F-skatt note as optional UX. It's a legal requirement on every faktura issued by a company that holds F-skatt registration — and a FATAL Peppol BIS 3.0 validation failure (SE-R-005) for B2G invoices when missing. Reframed as a compliance assertion: the PDF includes it automatically when the setting is true; verifying the setting is correct before production is the integrator's responsibility. 4. **Changelog: SIE post-import VAT code reconfiguration warning** (swedish-sie-import-export). The /imports/sie line previously noted the file format support but didn't warn that SIE files do NOT carry VAT codes or tax-rate-to-account mappings. After migrating from Fortnox / Visma / BL / SpeedLedger / Bokio, integrators must manually reconfigure VAT codes before the first momsdeklaration — skipping this is the most common source of incorrect VAT submissions in migrated bookkeeping. DEFERS (round 3 final — these are the architectural floor): - **🟠 A.5.34 personnummer field name + masking logic disclosed in public docs** (Compliance Swarm). Defensible: documenting that personnummer is masked is a transparency benefit (GDPR Art.13/14 intent), not a privacy disclosure risk. The DPO confirmation prompt is a reasonable governance ask but is an out-of-repo decision — the docs change is appropriate as written. - **AGI penalty amounts (625 / 1,250 SEK)**. Operational guidance for integrators building deadline-tracking; not strictly API-doc material. The deadline (12th / 17th) is documented; integrators who automate compliance can read SFL for penalties. - **VAT period thresholds in the cookbook placeholder**. Belongs in the actual cookbook content when written, not in the placeholder description. - **`invoice.credited` event-naming verification**. False positive — the emitter uses `credit_note.created` (which IS in the docs); there is no `invoice.credited` event in the codebase. Naming is consistent. - **Webhook retention sentence reordering** (swedish-compliance stylistic). Current wording leads with what delivery rows ARE (behandlingshistorik), then clarifies what they are NOT (räkenskapsinformation) — clean teaching arc, the qualifier is prominent. Reordering doesn't change clarity. Round 3 stop signal hit (per Phase 4 lessons): swarm count plateauing at the architectural floor with all remaining findings either defers, false positives, or out-of-repo decisions. Greptile has posted no inline comments since round 1's two items (both fixed). This should be merge-ready. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 4 — 5 small precision fixes Compliance Swarm: 3 → 5 (slight uptick from oscillation, but 0 critical, 1 actionable; remaining 4 are recurring or philosophical). swedish-compliance: 7 advisories — 3 actionable precision items incorporated below; the others are forward-looking notes for cookbook content that ships in Phase 6 follow-ups. FIXED: 1. **Spec-snapshot test enforces ep.scope is explicitly defined** (CC6.3, real future-bug prevention). Previously the test asserted every endpoint declared the agent-facing metadata fields the docs depend on, but `scope` could be `undefined` — a registerEndpoint call that silently dropped the field would make the wrapper treat the route as unauthenticated. Added an assertion that `ep.scope !== undefined` (the literal sentinel `null` is allowed for genuinely public endpoints like /api/v1/health). The 4 spec tests still pass — confirming no current endpoint has undefined scope and the gate works prospectively. 2. **F-skatt: integrator responsibility for `has_f_skatt` accuracy** (swedish-invoice-compliance). The previous "verify on settings page" framing didn't connect the flag to the live Skatteverket registration. Now: "The integrator is responsible for keeping has_f_skatt in sync with the company's live Skatteverket registration status. Update via PATCH /api/v1/companies/{id}/ settings or the settings page — a flag that's false while the company is actually F-skatt-registered produces non-compliant invoices, not merely a missing optional note." 3. **AGI deadline qualified by turnover** (swedish-payroll). The previous wording listed "12th / 17th of the following month" with no condition. Now: "12th of the following month for large employers, 17th for companies with annual turnover ≤ 40 MSEK." Aligns with the swedish-payroll skill's AGI filing deadline section. 4. **SIE import warning includes behandlingshistorik gap** (swedish-sie-import-export + swedish-accounting-compliance). The previous warning covered the VAT-code reconfiguration requirement but didn't note that SIE files also do NOT transfer behandlingshistorik (the source system's processing log per BFNAR 2013:2 kap 8 §) or systemdokumentation. Added: "The behandlingshistorik gap must either be preserved separately (export from the source system + archive alongside the SIE file) or accepted with documented justification — gnubok starts a fresh behandlingshistorik from the import date forward." 5. **Webhook 7-year retention: voluntary policy vs statutory obligation** (swedish-accounting-compliance). The previous wording said gnubok keeps delivery rows "for 7 years as an operational audit-trail policy" — the 7-year figure could be misread as statutory. Tightened in BOTH webhooks.ts and changelog.ts: the 7-year statutory retention under BFL 7 kap 1 § applies ONLY to the underlying verifikation/faktura/AGI XML; gnubok's 7-year policy on delivery rows is voluntary and chose the duration to align conveniently with the statutory horizon on the underlying records. DEFERS (round 4 final, all in the architectural-floor bucket): - **🟠 A.5.34 personnummer field name + masking logic disclosed in public docs** (recurring from round 3). Defensible — documenting PII handling is a transparency benefit (GDPR Art.13/14 intent), not a privacy disclosure risk. The DPO confirmation prompt is a reasonable governance ask but is an out-of-repo decision. - **🟡 A.8.23 DNS-rebinding "coming soon" item**. The bot is reading the changelog's own deferral list. Already tracked for Phase 6 PR-3 hardening. - **🟠 CC6.6 SSRF protection details exposed in /llms-full.txt**. Stripe / GitHub / Slack publish their full webhook security posture publicly (signature format, rejected IP ranges, retry policy) — documenting protections IS the trust pattern. Obscurity is not security; the SSRF protection is enforced in code, not in the docs. - **🟡 CC6.7 public CDN caching**. withPublicSecurityHeaders() already applies the appropriate headers (CSP, X-Content-Type- Options, X-Frame-Options). The 5-min cache is appropriate for static developer documentation; the alternative (no caching) is cost without security benefit since the content is intended to be public. - swedish-compliance "VAT 2026-04-01 livsmedel rate change" — forward-looking; ships when the actual VAT cookbook is written. - swedish-compliance "year-end IB/UB continuity" — forward-looking; ships when the year-end cookbook is written. - 2 verify-only notes (rättelse implementation, future salary- journal/avgifter-basis masking sweep) — not actionable in this PR. Round 4 stop signal: every remaining swarm finding is in the deferred or recurring bucket; the actionable item (CC6.3) is shipped. swedish-compliance is now in advisory mode (no errors, just stylistic suggestions and future-cookbook notes). Per Phase 4 lessons, this is the merge-ready signal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 5 — 4 small precision fixes (last actionable items) Compliance Swarm: 5 → 2 (down to architectural floor — 1 high + 1 medium). swedish-compliance: 7 advisories, 4 actionable precision items addressed below; the others are forward-looking notes for content that ships in Phase 6 follow-ups. Trajectory: 7 → 10 → 3 → 5 → 2. Plateaued. FIXED: 1. **Webhook secret storage guidance: secrets manager, not env file** (A.8.5 high). Added explicit instruction to the cookbook that the returned secret is signing material and must live in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler, 1Password Connect, ...) — not a plaintext .env file or config commit. Treated with the same care as a database password. 2. **AGI deadline correction: 17th = January and August only** (swedish-payroll). Round 4's wording said "12th (large employers) / 17th (≤40 MSEK)" — but per the swedish-payroll skill the 17th applies in January and August specifically, not generally to all months for sub-40 MSEK companies. Other months are the 12th regardless of employer size. Fixed to: "the 12th of the following month for every reporting period EXCEPT January and August, where companies with annual turnover ≤ 40 MSEK get the 17th." This would've caused integrators automating sub-40 MSEK deadline tracking to misfile by 5 days from February through July and September through December. 3. **F-skatt SE-R-005 broader scope** (swedish-invoice-compliance / swedish-e-invoicing). The previous wording framed SE-R-005 as primarily a Peppol B2G validation rule. Reframed: the F-skatt note is a legal requirement on every faktura issued by a Swedish momsregistrerad seller that holds F-skatt registration — applies to PDF/paper AND Peppol/e-invoice formats. The buyer uses it to determine A-skatt withholding obligation (omitting it can shift tax liability onto the buyer); B2G is just where the validation is automated as a FATAL Peppol BIS 3.0 check. 4. **SIE behandlingshistorik gap: full räkenskapsår scope** (swedish-accounting-compliance). Round 4's wording said the integrator "must either preserve [behandlingshistorik] separately or accept the gap with documented justification" and that gnubok "starts a fresh behandlingshistorik from the import date forward." The "documented justification" framing implied the gap was acceptable as a default. Per BFNAR 2013:2 kap 8 §, the obligation attaches to the entire räkenskapsår, not from the import date. Reframed as: "must be preserved separately... best practice for a mid-year migration: export the source system's behandlingshistorik for the full fiscal year and archive it alongside the SIE file." DEFERS (round 5 final — these are the architectural-floor items that will recur indefinitely): - **🟡 A.8.20 DNS-rebinding gap** (Compliance Swarm). Already documented in the changelog as a Phase 6 PR-3 deferral item; the bot is reading the same text we wrote. - **swedish-compliance: VAT 2026-04-01 livsmedel rate change**. Forward-looking — for the actual VAT cookbook recipe content, which ships post-Phase-6. - **swedish-compliance: year-end IB/UB continuity**. Forward-looking — same. - **swedish-compliance: SIE warning placement note**. Forward-looking — for the imports reference page when authored. - **swedish-compliance: BFNAR 2013:2 citation correct, webhook retention correct**. No-op confirmations. - **swedish-compliance: delivery_date pre-payment scenario**. Real but extremely narrow edge case (faktura utfärdad före leverans). Defer with the understanding that anyone using the API for pre-payment invoicing will read the full invoice reference, not rely solely on the quickstart. This is the merge-ready signal per Phase 4 lessons-learned: every remaining swarm finding is in the deferred or recurring bucket; swedish-compliance is in pure-advisory mode (forward-looking notes for cookbook content that ships later); CI is fully green; Greptile posted nothing past round 1's two items (both fixed). Ship it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |