Framework UX-maturity rubric¶
A rubric that scores Dazzle the framework, not a screen. The question is not
"does this screen answer the user's question?" — that's instance evaluation
(dazzle ux verify, the /ux-pass story walk). The question here is:
Does Dazzle make the data-right UI the DEFAULT?
Instance screens are evidence; the verdict is about the framework's primitives and their zero-effort defaults. The output is a defensible maturity index plus a prioritised list of framework primitives worth building, keyed to the framework version so regressions are attributable.
Naming. "maturity" is overloaded in this codebase —
fitness/maturity.py(project lifecycle: mvp/beta/stable) and the agent-command[maturity]gate (command applicability) are unrelated. This rubric is alwaysux maturity.
The one capability¶
Three UX principles converge, at the framework altitude, on a single capability: choosing — from the data and context — rather than by hand.
- Data drives the UI → choose the form from the data's shape (temporal → trend/timeline; ordered-state → board; categorical → chips/colour; grouped quantity → chart — not table-by-default).
- Progressive disclosure → choose how much to show from what the user needs to act (answer-first; depth one cheap click away, never forced up front).
- Negative space / strategic concealment → choose whether to show from role, entity state, and frequency (rare-but-important features present but not routinely rendered; affordances appear only when they apply).
Form / amount / whether are inference problems the framework is better placed to solve than the author — which is why maturity tops out at adaptive, not configurable.
The five lenses (human-readable spine)¶
Per workspace/region, the rubric asks (after AegisMark strategy.md):
- Question — what decision is this persona here to make?
- Answer-first? — does the surface lead with the answer/anomaly, or a raw table the user must compute from?
- Shape — is the representation right for the question (trend for "improving?", comparison for "who's behind?", heatmap for "which dimension?"), or a generic list?
- Cost — ≤3 actions for a basic info need, surfaced where the user lands.
- Honesty — human values, not raw data (no UUID / unrounded float /
True); for AI outputs, confidence + evidence shown.
The 13 criteria below are these lenses made measurable at the framework altitude.
The maturity ladder (0–4, scored per criterion)¶
| Level | Name | Meaning |
|---|---|---|
| 0 | Absent | the capability does not exist in any form |
| 1 | Escape-hatch | achievable only via mode: custom / a custom renderer / hand HTML |
| 2 | Declarative-manual | a primitive exists but the author must hand-specify it and the default is wrong for the common case |
| 3 | Good-defaults | exists and the zero-effort default is right for the common case |
| 4 | Adaptive | the framework infers the right treatment from data shape / semantics / usage |
Target: most criteria ≥3; none at 0–1 for a common case. RAG: 0–1 red, 2 amber, 3–4 green. Overall index = mean criterion level; per-principle index = mean of that principle's criteria.
Level-4 is real, not aspirational — Dazzle already exhibits it in three
places, which anchor the top of the ladder:
- a chart display: compiles to one scope-aware GROUP BY (the framework
infers the aggregation + the row filter);
- transitions are offered from the state graph (the framework infers
which actions are available in the current state);
- scope: compiles to predicate algebra + RLS, so rows the user can't act on
are never rendered (concealment inferred from the predicate).
The 13 criteria¶
Revised from the 11-criterion starting set for how Dazzle actually models surfaces/displays/visibility (see "Revisions", below).
Data drives the UI — choose the form¶
- 1a — region form inference. Does a workspace region infer its form from
the source data shape (temporal → timeline/trend, ordered-state → board,
grouped quantity → chart), or default to a list? Entity
mode:surfaces are exempt — a list/detail CRUD surface is correctly tabular (R1). - 1b — semantic-state binding. Is a status/enum → colour/icon mapping
declared on the field and validated against a semantic role, or derived
from a name→theme convention? (R3: Dazzle has a validated palette
positive/warning/destructive; the gap was binding it by declaration vs the_STATUS_TONE_MAPname fallback.) #1493 reached level 4: asemantic:line on a sharedenum/inlineenum[...]field declares each value's tone, validated against the palette, andresolve_status_toneconsults it before the name guess. On top of the binding, level 4 adds WCAG colour+icon+text (badge_icon_htmlleads every non-neutral badge with a non-colour glyph) and state-machine-terminal inference (status_tone_mapmergesinfer_terminal_tone_map: an undeclared, name-guess-miss terminal state — a graph sink — is inferred asuccessend-state). Precedence: declared > name-guess > SM-terminal > neutral. - 1c — comparison context. Are lone scalars avoided — is a number shown with
trend / rank / distribution / outlier by default? Level 4 (#1491): an unset
metrics/summarytile infers a 30-day period-over-period delta by default, and the inference adapts to the aggregate grain —countAND scalarsum/avg/min/max(resolve_comparison+_prior_period_taskat the_compute_aggregate_metricsseam) — so a revenue-sum or rating-avg tile gets a trend too, not just count tiles. Inferred deltas areneutral-sentiment — magnitude/direction without asserting good/bad; declaredsemantic:(1b) owns tone. An explicitdelta:wins; nocreated_atstays a lone KPI. - 1d — raw-data honesty. Are UUID / FK / ISO / float / JSON / bool
unrenderable raw by default (resolved / rounded / humanised)? Level 4 (#1491):
the shared cell core humanises every type, list AND detail —
datetime(date+ time),number(rounded float),json(compactkey: val · …summary), bool (icon), badge (WCAG chrome), null (—). The detail view's form-input types are reconciled to these display types; the_probe_1ddrift gate renders each through the real core and asserts no raw leak.
Progressive disclosure — choose the amount¶
- 2a — answer-first landing. Does the persona land on a workspace that leads with the answer/anomaly, not raw CRUD?
- 2b — depth in ≤1 action. Is detail one cheap click away (auto-drill list →
detail), not hunted? Level 4 (#1491): drilling is perceived-instant —
clickable rows carry
hx-preload="mouseover", so the vendored htmx-4preloadextension warms the detail GET on hover and the click serves the cached prefetch, fleet-wide. - 2c — action-proximate detail. Is row-peek / inline-expand declarative and on by default where it helps (not a hand-set flag)?
- 2d — field economy. Is column priority / hidden-by-default the default, so a wide entity doesn't dump every column?
Negative space — choose whether¶
- 3a — frequency-weighted prominence. Do rare actions default to overflow and
common actions to primary, derived from usage/frequency, not hand-placed?
Level 3 (#1491): a workspace heading keeps the top-3 actions prominent by
declaration order (inferred create-CTAs first) and demotes the tail to a native
<details>More ⋯menu (resolve_action_prominence, applied at thepage_routesassembly seam). A ≤3-action heading is unchanged. L4: derive prominence from observed usage frequency + extend to row/bulk/grid actions. - 3b — role-gated affordance. Is an affordance's visibility gated by role, declaratively and provably (not rendered-then-403)?
- 3c — state-gated affordance. Is it gated by entity state (an action absent when the state graph doesn't allow it, not disabled-then-blocked)?
- 3d — empty-state suppression. Does an empty region self-demote (collapse / hide), or render dead scaffolding?
- 3e — scope concealment. Are rows the user has no scope for never rendered (concealment by construction), not filtered client-side?
Evidence + attribution (what makes it rigorous)¶
Score each criterion with two kinds of evidence that must agree:
- Capability (static). Inspect the DSL grammar + renderer: does the primitive
exist, and what's the default with zero author effort? Default-wrong = 2;
default-right = 3; only-via-escape-hatch = 1; inferred = 4. This is the
primary, CI-able framework score — it needs no running app. (
dazzle ux maturity.) - Rendered (dynamic). Drive a real app in a browser and confirm the live UI
exhibits it. For attribution + drift only — it confirms the default actually
renders and catches "capability says 3, real screen shows 2". (the
/ux-maturityagent command, reusing the/ux-passwalk.)
Attribution rule (bridges instance → framework). A failing screen is an authoring gap if the right-by-default primitive existed and wasn't used; it's a framework gap if it fails despite the author (missing / escape-hatch / wrong-default). Promote to the framework backlog only on repetition under effort — many screens failing the same criterion, several via custom renderers. One screen is authoring; a pattern is the framework.
Clumsiness flags → criterion map. The rendered pass records per-screen flags
(from the /ux-pass vocabulary); a pattern of a flag attributes to a framework
criterion:
| Flag (rendered evidence) | Pattern attributes to |
|---|---|
raw_data (UUID/FK/float shown raw) |
1d |
empty_on_path (card empty though data exists) |
3d (+ scope/data-path bug) |
hunt / nav_only (no in-context entry; fell back to nav) |
2a, 3a |
long_chain (over action budget for a basic need) |
2a, 2b |
deadend (looked right → 403/empty) |
3b, 3c (affordance shown when not applicable) |
| dense-table-scanned-for-outliers | 1a, 1c |
Output schema¶
dazzle ux maturity --json (and the rendered pass) emit:
{
"framework_version": "0.87.11",
"overall_index": 2.6,
"rag": "amber",
"principles": {
"data_drives_ui": { "index": 2.25, "criteria": ["1a", "1b", "1c", "1d"] },
"progressive_disclosure": { "index": 2.5, "criteria": ["2a", "2b", "2c", "2d"] },
"negative_space": { "index": 3.0, "criteria": ["3a", "3b", "3c", "3d", "3e"] }
},
"criteria": {
"1a": {
"principle": "data_drives_ui",
"name": "region form inference",
"capability": 3,
"rendered": null,
"rag": "green",
"evidence": "`display: auto` is the default for an unset `display:` (#1492); the form is inferred from the data shape",
"attribution": null
}
},
"framework_backlog": [
{
"criterion": "1c",
"leverage": "high",
"gap": "lone scalars shown without trend/rank/distribution by default",
"evidence": ["comparison-family display kinds present but not the default", "..."],
"since_version": "0.87.11"
}
]
}
The backlog is keyed to framework_version: a criterion dropping (e.g. 3 → 2)
between versions is an attributable regression; a criterion rising is the
primitive work landing. A drift gate keeps the declared capability levels honest —
a probe that contradicts its declared level fails CI.
Turning the scorecard into work¶
Every criterion implies primitive work — see the UX-maturity primitive roadmap (each criterion → its target primitive + how to build it, including which ones htmx 4 now expresses directly so they're declarative wrappers, not custom JS).
Discovery + distribution¶
One source, every Dazzle app, via Dazzle's existing agent-command channel:
dazzle ux maturity— the static capability scan, shipped in the wheel. Deterministic, CI-gateable; emits the schema above. The spine./ux-maturity— an agent-command definition (services/agent_commands/definitions/ux_maturity.toml) that runsdazzle ux maturity(static) then drives the app (rendered + attribution + roll-up).dazzle agent syncdistributes it — and the CLAUDE.md / AGENTS.md pointers — to every consuming app.docs/reference/ux-maturity.md— this doc (the framework-neutral rubric).
Revisions from the 11-criterion starting set¶
- R1 — 1a scoped to regions, not entity surfaces. A
mode: listsurface being tabular is correct (CRUD); penalising it is a false negative. Form-inference applies to workspace regions. - R2 — 3b split into role (3b) and state (3c). Dazzle is strong on both via different mechanisms (provable RBAC matrix; state machine). One conflated criterion hid both a strength and a real "adaptive" example.
- R3 — 1b reframed as binding, not palette. Dazzle has a validated semantic palette already; the gap is declaring the status→tone binding on the field vs the name-convention fallback.
- R4 — added 3e (scope concealment).
scope:→ predicate algebra + RLS means unactionable rows are never rendered — negative-space by construction. Crediting it makes the rubric a fair scorecard, not a gap-list. - R5 — anchored level-4 to existing behaviour (display→
GROUP BY, transitions-from-state-graph, scope→RLS) so scorers calibrate "adaptive" against what already ships.
How the AegisMark prototype relates¶
This rubric is the framework-altitude abstraction over AegisMark's
instance-level UX work: strategy.md (the "answer the question" synthesis →
"grow ~6 native primitives"), target.md / scorecard.json / verdicts.json
(the /ux-pass story walk), ssr-htmx-ux-doctrine.md (the SSR+htmx substrate
doctrine + operating rules). Those judge AegisMark; this judges Dazzle. Most
of the 6 primitives strategy.md called for have since shipped (#1470 comparison
/ outlier / heatmap / insight_summary, the format layer, sparkline/timeseries) —
so this rubric's job today is to re-score and repoint at what's left, which
is why the gaps now sit at default + inference (level 2) rather than absent
(level 0).