The dual-write window ends (dev_docs/dimensions_implementation_plan.md PR9):
journal_entry_lines.cost_center/project are now GENERATED ALWAYS AS
(NULLIF(dimensions->>'1'/'6','')) STORED — divergence from the bag is
impossible by construction instead of by convention.
- migration 20260702230000: drift pre-flight (refuses cutover on
inconsistent data; prod verified 0 drift across 593k rows), column swap
(DROP metadata-only + one-rewrite ADD pair), and atomic redefinition of
the two SQL writers — retag_line_dimensions (SET dimensions only) and
bulk_book_transactions (INSERT names the bag only)
- TS writers stripped of the mirror spread: engine buildLineInserts
(covers create/update/reversal), storno-service (reversal + correction),
SIE import bulk insert, sandbox seed
- lineDimensionColumns() removed from dimension-resolver — nothing derives
mirrors in TypeScript anymore; normalizeLineDimensions + the deprecated
cost_center/project INPUT aliases stay (API contract, they normalize
into the bag); JournalEntryLine ROW type keeps the fields (generated
columns still SELECT)
- immutability carve-out unchanged BY DESIGN: its whole-row diff already
subtracts dimensions/cost_center/project on both sides, which is exactly
what makes it correct with generated columns (BEFORE-trigger NEW carries
not-yet-recomputed mirror values)
- audited every reader (v1 journal-entries, MCP query_journal filters +
group_by, rc-basis-gaps) — reads are untouched; no index, view, or
constraint referenced the TEXT columns, so DROP COLUMN cascades nothing
- new pg suite: generated derivation, explicit-mirror-write rejection,
draft-update recompute; existing retag/substrate/bulk-book suites
updated to bag-only writes (their mirror assertions now exercise the
generation expression)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Phase 3 of dev_docs/dimensions_implementation_plan.md. Companies with
dimensions_enabled=false see zero change; existing free-text API writers keep
working (validation is toggle-governed).
Engine (soft validation):
- validateEntryDimensions() in dimension-resolver: zero queries for untagged
entries; toggle off → passthrough; toggle on → one settings fetch + two
registry queries, rejects unknown dims/codes and archived values with
Swedish per-code messages (DimensionValidationError, 400, details.issues).
Wired into createDraftEntry + updateDraftEntry before any insert; reversal/
storno paths untouched (verbatim copies). Fails open on transient registry
errors — soft validation must never block bookkeeping.
MCP (agent write path):
- New tools: gnubok_list_dimensions, gnubok_list_dimension_values (fuse.js
fuzzy), gnubok_create_dimension_value (STAGED via pending_operations —
agents never silently mint reporting values; new op type + CHECK migration
+ executor with duplicate-idempotency).
- create_voucher/correct_entry: per-line dimensions bag + default_dimensions,
resolve-don't-select server-side (code OR natural-language name; exact →
fuzzy ≤0.30 with ≥0.15 runner-up margin; non-exact resolutions echoed with
confidence; ambiguous → ranked candidates, no auto-create).
- gnubok_get_agent_briefing gains a dimensions block (enabled, dims, top
values) — omitted when registry empty.
- TOOL_SCOPE_MAP entries; risk tier low for staged value creation.
UI:
- JournalEntryForm (manual voucher + TransactionBookingDialog embed): header
"+ Kostnadsställe/Projekt" progressive disclosure (gäller alla rader with
documented inheritance rule) + per-row tag popover + compact KS·PR badges;
gated on dimensions_enabled.
- Voucher detail: display-only dimension badges with registry-name resolution.
- EditDraftEntryDialog carries line dimensions so editing a draft no longer
strips tags.
categorize/bulk_book dims deferred to PR7 (needs the bulk_book RPC migration).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>