ditdash U-I7/I8/I9: preferences, the wipe-and-compare harness, usage + statements views #8

Merged
jsutter merged 3 commits from ditdash-u-i789 into main 2026-10-11 23:33:16 +00:00
Owner

I7: the PREF_KEYS-whitelisted per-user store, PUT /api/v1/me, the /me panel. I8: the wipe-and-compare CI harness (all_routes + /me x 3 profiles, byte-identical modulo preferences; the falsifiability instrument) — the live SC-1 run stays deferred. I9: the usage/statement views (matrix extension, drill-through, shadow operator-only, no charge math, the U-G4 format gap). Carry-overs landed: the dark-leg contrast test, the forge-mode fail-closed test, Page::Config in all_routes. Critic: PASS (0C/0W). Dispatcher-run suite + clippy green.

I7: the PREF_KEYS-whitelisted per-user store, PUT /api/v1/me, the /me panel. I8: the wipe-and-compare CI harness (all_routes + /me x 3 profiles, byte-identical modulo preferences; the falsifiability instrument) — the live SC-1 run stays deferred. I9: the usage/statement views (matrix extension, drill-through, shadow operator-only, no charge math, the U-G4 format gap). Carry-overs landed: the dark-leg contrast test, the forge-mode fail-closed test, Page::Config in all_routes. Critic: PASS (0C/0W). Dispatcher-run suite + clippy green.
The store: src/prefs.rs — one JSON document per forward-auth identity
under state_root()/preferences/<username>.json (0600, ≤64 KiB), the
frozen PREF_KEYS whitelist (saved_views, pinned, default_range,
default_msp), the identity→path guard, and a corrupt-file path that
renders defaults with a note (a preference file never breaks a page).

The write path: PUT /api/v1/me accepts {preferences: {...}} only — any
other top-level key refuses with the key named and the store key derives
from the verified headers identity, never the body; the /me panel's form
posts through the same prefs::save routine (post/redirect/get). Saved
views validate against U-I5a's view-state vocabulary at save time (an
unparseable view refuses with the reason) and render as links only when
they still parse.

The /me resource: the page gains the preferences panel (saved views,
pinned rows, defaults, the form) between prefs compare markers; the
envelope carries data.preferences, so /me?view=json and /api/v1/me twin
the page exactly. The openapi document gains the additive put operation.

tests/preferences.rs: whitelist refusal, cross-user isolation (a body
can never redirect the write), corrupt-file resilience, form
round-trip, unsafe-username refusal, twin parity, and the FR-27 probe
at unit scale (clearing the store resets only preferences — /hosts and
/clients byte-identical across it).

Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash
tests/wipe.rs: build the full fixture world (bundle + history +
collected statements, fixture config repo, seeded per-profile
preferences, the pinned no-series metrics stub), render every route in
all_routes() + /me and their twins as operator@msp, viewer@msp, and
viewer@tenant — then delete the WHOLE state root, refresh exactly once
(the cooldown + cached-dir reuse land the same bytes; the rebuilt cache
asserts one run dir), re-render, and compare. Byte-identical modulo the
/me preferences block (the one allowed difference: it must equal the
seed before and the defaults after), with every other divergence
failing on a named JSON pointer or byte context. The rebuilt state root
is asserted against the disposable whitelist (bundle-cache,
config-cache, preferences); a read never resurrects the store. The
harness's instruments carry their own falsifiability test.

tests/common/mod.rs: the world's shared wiring — the loopback
no-series Prometheus stub (the configured path, deterministic across
both captures) and the preference-store seed.

I6-critic carry-overs riding this batch:
- Page::Config joins all_routes() so the wipe sweep walks /config (the
  introspect round-trip walk skips it — its read model lives in
  configrepo.rs and its only view state is the raw toggle; the
  configpage suite still owns its round trips).
- a_dark_below_floor_palette_refuses_with_the_dark_leg_named: the
  solver iterates both modes — the dark leg refuses with its pair named
  while the light leg stays clean.
- a_forge_mode_client_without_the_write_token_refuses_saves: a
  Forge-mode client with only the read credential has no write path —
  write_configured() is false and a save past every content guard
  refuses 503 naming DITDASH_CONFIG_WRITE_TOKEN.

Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash
The pages: `/usage` (the msp-level per-client index — current-period
summary + period-close status, each row linking its statement page) and
`/clients/{c}/statements/{period}` (`current` or a `YYYY-MM` tag: the
statement facts, the priced lines VERBATIM, each line's drill to its
usage records with their windows + `source` refs, the close state, the
v1 not-metered row for llm.tokens, and the export linkage). Render-only:
no charge math runs in ditdash — the envelope's amounts render as
carried, the shadow filter drops rows for roles that cannot read
BillingShadow and never re-rates anything.

The data: the collected `client show` surface (U-G3's collector line —
the statement content inside the show envelope: lines carrying
`usage_record_ids` plus the referenced usage records, freeze/close
state). Until it lands, every page renders its named data-absence state;
fixture tests pin BOTH states (acme's full world, globex's
usage-without-statements variant). Readers are additive-tolerant (no
deny_unknown_fields; quantity/consumed_quantity, ref/source and the
period naming fallbacks) — an unknown future field parses without
refusal (rings alignment).

The policy: viewer@client(c) gains Class::Statement + Class::Usage
within its client scope (the design §23.3 client-viewer halves) — the
golden matrix and policy_matrix.rs extend in the same commit; the
msp-level index stays out of a client grant's reach, cross-client
probes answer the indistinguishable 404, and shadow lines never reach
any viewer (page or twin).

The panels: the client detail's usage/statements fill from the
collected surface (period rows linking their pages); the app + tenant
pages gain their #usage sections from the rows' summaries (no new
surfaces). The statement twin's `?format=focus|csv` answers the named
U-G4 gap — the wiring point the serializers replace (a probe, never a
panic); an unknown format refuses 400 naming the accepted set; the
openapi document declares the parameter.

tests/billing_views.rs (13 tests): render-as-role, FR-29's drill walk,
shadow absence on page + twin for both viewer classes, frozen +
provisional close fixtures, the format branch, both data-absence
states, the panel fills, the matrix extension, openapi, and the
usage/statement digests. tests/pages2.rs's client assertion moves to
the reworded data-absence line; the wipe world (tests/wipe.rs) carries
the client-show surfaces so the U-I8 sweep walks the new pages; the
page digests refresh via the suites' mechanism (the nav gains Usage;
app/tenant/client pages gain their sections).

Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
deepnet/ditdash!8
No description provided.