Commit Graph

5 Commits

Author SHA1 Message Date
2f862a6c8b Name the ledger's lines the way everything else names them
The walk read from tag and note, and hardcoded the word "Baseline" for the
baseline load -- so a segment called 03 - New Orders in the pivot, in the
bridge and on the Baseline page read as "Baseline" in the one place you go
to check a number before changing it. label comes first now, the same
precedence pf_segment uses, in the ledger and the bridge alike. logMeta did
not carry label at all, which is why neither could reach it.

The immovable rows split one line per segment. Combined, "01 - YTD Sales ·
02 - Open Orders" said 1.6m was untouchable without saying how much of it was
billed and how much was booked -- different things a forecaster treats
differently. The FINAL badge also gains the space it was missing, having
rendered as "02 - Open Ordersfinal".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:38:37 -04:00
7738b904bf Delete the client-side ordering expressions
With the prefix stored in pf.log.label there is nothing left to compute, so
all of it goes: ORDER_EXPR_NAMES, buildOrderExpression and its ExprTK
printable-ASCII-per-byte guard, SYNTHETIC_SEGMENTS and its 98/99 ordinals,
syncOrderExpressions with its two self-issued fetches, the dbgOrder tracing,
and the three places it had to be re-applied because restore() replaces
`expressions` wholesale.

What remains is a list of the names it used to manage, stripped by
cleanLayout so a layout saved under the old scheme does not keep ordering by
a rule nothing updates. The strip goes before the axis filter: dropping them
from `expressions` is what makes the existing ok() reject them on every axis,
which restore() requires -- an expression the pivot is using cannot vanish
from underneath it.

Net 250 lines out. The Baseline page's reorder buttons no longer feed
anything and go next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 22:44:07 -04:00
0fdb08291d Carry the pivot's expressions into the bridge's own view
The bridge builds its own view from the pivot's filters, and those filters
can name pf_bucket_ord or pf_segment_ord — columns that exist only as
expressions. A view created without them cannot resolve the column, so the
bridge failed outright as soon as anyone filtered on an ordering column.

The per-slice path is left alone: its filters are built from col_meta names,
so they only ever reference real columns. Said so in place, since the
asymmetry otherwise reads as an oversight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:38:03 -04:00
5acac2738a Build the bridge on buckets, and let it start from a comparison basis
The bridge filtered on exclude_iters, so it dropped every reference row --
including Open Orders, which is loaded as reference precisely so nothing
adjusts it and is still part of the forecast. Membership is pf_bucket now,
which is the axis that answers this question; pf_iter answers a different
one.

What you compare against depends on what you are building: an AOP is built
off a prior period, a forecast off an update to the AOP. So the basis is
chosen rather than assumed, and the walk stays exact either way because

    Forecast - Basis = (Forecast loads - Basis) + adjustments

The opening step is that first term, every tagged adjustment explains the
rest, and the bars sum to the endpoint by construction. No residual to
explain away -- which is what a bridge from prior year to forecast would
otherwise be, since the adjustments describe movement from the forecast's
own loads and not from last year.

With no basis it is the composition instead: the forecast's loads as the
opening anchor, then the adjustments. The picker only appears once something
carries a bucket, so a version that has not been labelled behaves as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 01:46:40 -04:00
55814ee0d5 Multi-slice operations, tagged bridge, and a reworked adjustment panel
Forecast operations could only act on one clicked row at a time, and the
panel that drove them separated the numbers you were reading from the
inputs that changed them. This reworks both, and adds initiative tags so
a version's history can be read as a bridge.

Operations
- Accept `slices` (array) alongside the legacy single `slice`, with
  apply_mode 'prorate' (one pool) or 'each' (independent per slice).
- buildWhereAny() ORs the slices into one predicate. A union of slices
  cannot be flattened into per-column IN lists without over-selecting,
  and the result is parenthesised so the appended exclude clause does not
  bind wrong.
- resolveIncrs() now resolves each measure independently: target, percent
  or change amount per measure, so a target on value and a percent on
  units can be submitted together. Replaces the single global `mode`.
- target_basis chooses what a target measures against: only the rows an
  operation can write, or everything the pivot shows for the slice.
  Excluded iters are visible in the grid but immovable, so a target set
  against the visible total previously overshot by their contribution.

Two latent bugs surfaced by the above, both pre-existing:
- A slice naming no filterable column reduced to TRUE and applied the
  operation to the entire version. Now rejected on all three operations.
- Prorating across a pool that nets to ~zero multiplies each row's share
  by an exploding factor, sending rows to extreme opposite values to hit
  the target. Refused when the net falls below 1% of gross.

Tags and the bridge
- pf.log gains a nullable `tag`, written by a follow-up UPDATE rather
  than through the generated SQL: those templates are stored per source
  in pf.sql, so a {{tag}} token would strand any source that had not
  re-run "Generate SQL".
- Tag is editable after the fact in the change log, with completion from
  tags already used on the source. PATCH branches on whether a field was
  sent, so a tag can be cleared as well as set.
- BridgeView renders the walk from baseline to current as a waterfall,
  one step per tag, scoped to the selection, the pivot's filters, or the
  whole version. Computed from the loaded Perspective table so the
  figures always reconcile with what is on screen; overlapping slices are
  deduped by pf_id to match the OR semantics operations use.
- Colour is a polarity job, so it uses the validated diverging pair
  (blue/red, CVD dE 21.6) with neutral anchors, not categorical hues.
  Every bar is directly labelled and a table view is available.

Panel
- Extracted to OperationPanel; the scale form is one continuous ledger:
  baseline, each adjustment, current, then New value / Change / % change
  as three interchangeable editable rows. Typing in any one derives the
  others, which removes the target/delta/percent mode toggle entirely.
- Dockable bottom, right, or floating (drag to move, grip to resize), and
  closable via header, Esc, or the toolbar. Placement persists.
- Controls no longer stretch to the dock width, and text contrast now
  clears WCAG AA against white throughout.

Also: the status bar names the physical table writes land in, with live
row counts; and the pivot's expand depth is re-applied when the tab
regains focus, since Perspective rebuilds its view on redraw and a
ROLLUP view with no depth set renders fully expanded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1TQiBYZbbWWkMNoCtUd8M
2026-09-11 23:30:56 -04:00