Skip to content

feat(core): add a property catalog and derive its discovery channels#16

Merged
nmrtist merged 1 commit into
mainfrom
feat/property-catalog-and-search
Jul 25, 2026
Merged

feat(core): add a property catalog and derive its discovery channels#16
nmrtist merged 1 commit into
mainfrom
feat/property-catalog-and-search

Conversation

@nmrtist

@nmrtist nmrtist commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

PlotX has a command catalog but no parameter catalog. Around 150 controls are
spread across separate windows with no shared registration, and the command
palette matches only a command's label — so no parameter is in the searchable set
at all. 2D contour settings were the extreme case: base, count and ratio were
hard-coded constants with no way to reach them.

This adds a parameter catalog where registering a parameter once yields its
description, addressing, reading, validation, and every way a user can find it:
search, a Ribbon and right-click landing point, and — where the parameter has a
direction — a canvas gesture. Contour settings are the first parameters to go
through it, so they are now adjustable, findable, and readable in the units they
actually mean.

Reviewing the result against a real spectrum turned up a crash that already
exists on main, so it is fixed here too — see Contour levels below.

What changed

Three-layer separation

  • plotx-core holds the semantic definition: identity, scope, value schema,
    access, capability-based applicability, default policy, tier, canonical label
    and aliases. The app crate holds presentation: localized label, aliases, and
    the route back to the control.
  • Each rule is declared once. Tier lives on the definition and presentation
    reads it; a numeric bound is a single declaration that a control's range and
    the writer's validation both consult, so a value the panel accepts cannot be
    one the writer rejects.
  • Real parameters stay in their strongly typed domain models. The catalog
    describes, addresses, reads and validates, then compiles an edit into a typed
    Action. There is no HashMap<PropertyId, PropertyValue> state store.

Addressing

  • A contour setting is addressed as a plot object plus a Series component —
    not as the dataset that happens to hold the data, and not as the field the
    series draws from. Two series of one object that read the same field are
    edited independently.
  • The component kind is checked against the definition before any document
    lookup, so a malformed address is rejected by identity rather than by
    accident.

One canonical home, several editors

  • The panel, the palette, the context menu and the canvas gesture reach the same
    planner and the same typed Action. The gesture passes a direction, not a
    value: the step is computed inside the planner against the same working copy
    and validated by the same write path, and a test asserts a stepped result is
    identical to the one a typed value produces.
  • Page and object activation also share one path, so opening a page or an object
    from the palette carries the selection and data focus with it. Object ids are
    page-local, so a selection left behind by a page switch could otherwise name a
    different object entirely.

Reads aggregate rather than pick a representative

  • A contour ladder keeps a positive and a negative half that share base, count
    and ratio, so even a single-target read is an aggregate. When the halves — or
    the selected series — disagree, the value reports Mixed, the control shows no
    number, and no resolved level is captioned beside it. An edit then applies to
    all of them, which the control says up front.
  • Cross-target disagreement and within-target disagreement compose, so neither
    can mask the other. A definition declares how many copies of the value a
    single target holds, so the interface can say which kind of disagreement it is
    without knowing what a contour is.
  • The panel renders against the same selection every other channel resolves,
    including the fallback to a page's active plot and the aggregate over several
    selected plots — so a setting the Ribbon offers is a setting the panel shows.

Writes are atomic and stay in scope

  • Values are validated against a working copy inside the plan; a rejected value
    returns before any action exists, so nothing lands. Every series of an object
    folds into one binding edit, and the whole set commits as one Composite Action
    with normal undo. Targets that cannot take the value are reported with a
    reason rather than dropped.
  • Resetting an encoding names the encoding it rebuilds, so resetting a contour
    leaves a heatmap or line overlaid on the same plot untouched. Resetting a
    single value re-derives it under the policy currently in force rather than the
    one the factory would have chosen, so a reset cannot advertise a value the
    current anchor would refuse.

Contour levels

  • The level anchor now multiplies the larger of the estimated noise scale and a
    fraction of the field's peak. A homonuclear phase-sensitive plane carries t1
    ridges and solvent residual that scale with its strongest feature, far above
    the thermal sigma a robust estimator correctly measures — so a ladder anchored
    to sigma alone can sit entirely inside that structure. The peak fraction is
    calibrated at the knee where contours stop following the artefact floor, and
    the readout names which term is in force, so a floored level never reads as a
    multiple of sigma.
  • A field whose sigma clears the floor resolves exactly as it did before, which
    a test pins by resolving the same field with and without the floor.
  • Geometry is separately bounded by what the renderer can draw. When a ladder
    would exceed it, whole magnitudes are dropped from both signs together — never
    a partial trace — and the status line says how many, below which level, and
    what to raise the lowest level to in order to see them all.
  • Without these two changes, opening a 2048x8192 NOESY asks the renderer for 8.4
    million segments and a gigabyte of index buffer, which aborts the process.
    That default reached main earlier in this series; the plot it produces is
    empty until the moment it crashes.

Discovery derived from one registration

  • Search: the palette searches commands, parameters and resources together,
    matching identifier tokens, canonical labels and aliases, and locale terms. A
    hit with no applicable target is shown disabled with the reason, rather than
    navigating to a row that is not there.
  • Ribbon and context menu: parameter groups have a landing point that opens
    the panel, expands the section, scrolls and highlights briefly. Navigation is
    data — a route no panel declares is rejected rather than scrolling nowhere.
  • Canvas: bare +/- raise and lower the lowest contour level, multiplying
    or dividing by the ladder's own ratio so one press moves the floor by exactly
    one drawn rung at any magnitude. The level shown on the plot is read from the
    series the keys would edit, and several series that disagree say so rather
    than showing the first one's.
  • Adding a parameter to an existing group lights up every applicable channel
    with no second registration, which is asserted by registering a parameter that
    exists nowhere else and checking each channel picks it up. A brand-new group
    still needs one group entry — a label, icon and placement cannot be inferred —
    and a test fails the build if a section lacks one.

Nothing waits in silence

  • While an estimate or a geometry is being computed the status line says which,
    so a plot that is still empty is never unexplained. The messages name the work
    rather than the plot, since the jobs are content-addressed and shared across
    objects.
  • Deciding that a job is already in flight now happens before its input is
    materialized, so repeated rebuilds during the wait no longer copy the whole
    field each frame.

Panel budget

  • Panels render Essential by default with Advanced/Expert collapsed.
    Non-default values carry a modified marker with the default on hover and a
    one-click reset. A test fails the build when a section exceeds its Essential
    budget, naming the offending entries and asking for re-tiering rather than a
    higher limit. Only the lowest contour level is Essential.

Two contour defaults corrected

  • A fraction-of-range base is measured from each half's own floor, so it can no
    longer hand a signed magnitude to a field that has both signs. The result is
    unchanged on the single-signed fields the capability admits.
  • An unanchored base now follows the field's own peak instead of a fixed
    literal, which silently drew nothing whenever a field's peak was below one.

Compatibility

Pre-release: no backward compatibility is kept for older project files. The
project schema version is unchanged. The catalog stores no parameter values of
its own, so it adds nothing to the saved project. + and - were unbound;
existing zoom shortcuts use them only with a modifier.

Out of scope

Automation access to properties, and migrating the remaining parameters into the
catalog, are separate changes. The level readout is currently specific to
contours; a second steppable encoding would want it generalized. The noise-floor
fraction is calibrated against one real spectrum plus synthetics, and the
constant carries a note saying when to re-measure it.

Testing

cargo pr-check passes: formatting, the source-size limit, dependency policy,
Clippy with warnings denied, and the test suite on both the reference and
DataFusion backends. npm run build passes for the documentation.

Catalog consistency is enforced by tests: every user-visible parameter has a
presentation, every presentation points at a valid definition and reads its tier
rather than repeating it, stable ids are unique, every route resolves to a
declared section, every section has a group, aliases are reachable from the
search index, and at most one parameter claims the canvas gesture.

Behavioural tests cover addressing a contour setting by series and rejecting the
object, dataset and field addresses; the source field not acting as the
component; component shape being rejected before any document lookup; several
series of one object folding into a single action; a rejected value aborting the
whole commit; a stepped value landing exactly where a typed value would; an
asymmetric ladder reading as Mixed until a write converges it; a ladder with no
negative half reading as uniform; both directions of cross-target and
within-target aggregation reaching the panel; a page activation not carrying a
stale selection onto the new page; an encoding reset leaving a stacked encoding
untouched; a reset re-deriving under the anchor in force; a mixed row carrying no
resolved level; a field whose sigma clears the floor resolving identically with
and without it; both halves sharing one floor; a dense ladder being bounded and
reporting what it dropped; and a pending estimate or build naming itself.

The derived-data invariants established for contour geometry still hold and are
extended: repeated readouts against warm caches queue no estimates, queue no
geometry, materialize no field payload, and run no marching squares on the
calling thread — and rebuilding while a job is in flight adds no materialization
at all.

PlotX had a command catalog but no parameter catalog. Around 150 controls were
scattered across separate windows with no shared registration, and the command
palette matched only a command's label, so no parameter was in the searchable
set at all. Contour settings were the extreme case: base, count and ratio were
hard-coded constants with no way to reach them.

Introduce a catalog where registering a parameter once yields its description,
addressing, reading, validation, and every way a user can find it. A definition
carries identity, scope, schema, capability-based applicability, default policy
and panel tier; presentation carries localized labels and the route back to the
control. Each rule is declared once: tier lives on the definition and
presentation reads it, and a numeric bound is a single declaration that a
control's range and the writer's validation both consult.

Real parameters stay in their strongly typed domain models. The catalog
describes, addresses, reads and validates, then compiles an edit into a typed
Action; there is no `HashMap<PropertyId, PropertyValue>` state store. A contour
setting is addressed as a plot object plus a `SeriesId` component, not as the
dataset that happens to hold the data, and not as the field the series draws
from.

The panel, the palette, the context menu and the canvas gesture reach the same
planner and the same typed Action. The gesture passes a direction rather than a
value, so the step is computed and validated where a typed value would be. Page
and object activation likewise share one path, so navigating from the palette
cannot leave a selection pointing at a page-local id that now names a different
object.

Reads aggregate instead of electing a representative. A contour ladder keeps a
positive and a negative half that share base, count and ratio, so even a
single-target read is an aggregate: when the halves or the selected series
disagree, the value is mixed, the control shows no number, and no resolved level
is captioned beside it. Writes are validated against a working copy and folded
into one atomic Composite Action, so a rejected value lands nowhere; a reset
stays inside the encoding it names and re-derives the value under the policy in
force rather than under the one the factory would have chosen.

Contour levels are anchored to a noise floor rather than to thermal noise alone.
A homonuclear phase-sensitive plane carries t1 ridges and solvent residual that
scale with its strongest feature, far above the thermal sigma a robust estimator
measures: on a 2048x8192 NOESY the whole 5-sigma ladder topped out at 0.125% of
peak, entirely inside that structure, and asked the renderer for 8.4 million
segments — a gigabyte of index buffer, which is a crash rather than a plot. The
anchor now multiplies the larger of the estimated scale and a fraction of the
field's peak, calibrated at the knee where contours stop following the artefact
floor, and the readout names which term is in force. A field whose sigma clears
the floor resolves exactly as before. Geometry is additionally bounded by what
the renderer can draw, dropping whole magnitudes from both signs together and
saying which and why, so no field can take the process down.

Discovery derives from that single registration: search over commands,
parameters and resources; a Ribbon and context-menu landing point that opens,
expands, scrolls and highlights by declared route; and bare `+`/`-` on the
canvas, which multiplies or divides the lowest level by the ladder's own ratio
so one press moves it by exactly one drawn rung at any magnitude. A hit with no
applicable target is disabled with a reason instead of navigating nowhere, and
the level shown on the plot is read from the series the gesture would edit.
While an estimate or a geometry is being computed the status line says so, so a
plot that is still empty is never silent about why.
@nmrtist
nmrtist merged commit 0e60998 into main Jul 25, 2026
11 checks passed
@nmrtist
nmrtist deleted the feat/property-catalog-and-search branch July 25, 2026 15:36
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant