Skip to content

Surface eleven properties MATLAB models, and pin the rule that hid them - #32

Closed
ww-mw wants to merge 6 commits into
mainfrom
schema-parity-gaps
Closed

ww-mw wants to merge 6 commits into
mainfrom
schema-parity-gaps

Conversation

@ww-mw

@ww-mw ww-mw commented Sep 17, 2026

Copy link
Copy Markdown
Member

A parity pass against the MATLAB Data Explorer app found eleven properties MATLAB
models that this package either dropped on the floor or never declared. Full parity is
not the goal — the app is a different product — so this is scoped to the schema and
data model only. No UI change, and the Min>Max warning channel is deliberately out.

What was unreachable, and why

Three of the gaps turned out to be the same defect, not three bugs:

schemaBridge.resolvePropForKey prefers a layout atom over the class's own schema
descriptor. The atoms read node fields, not the descriptor's sourcePath, and nothing
checked that the node class actually declares the field. When it does not, the value is
lost twice: the row renders blank, and BaseNode.toPIObject still adds the key to
shownKeys, so buildOtherRows suppresses the raw source property from "Other" as well.
Silent — the row is present and merely empty.

Changes

  • Simulink.ValueTypeMin, Max, Unit reachable in both panes, including the
    serialized-key asymmetry MATLAB has: ValueType writes Unit, while Simulink.Parameter
    and Simulink.Signal write DocUnits.
  • Simulink.BusElement — the real / Fixed display defaults MATLAB shows. A silent
    element still gains no key on save.
  • Simulink.EnumTypeIsTunableInCode, last in Code Generation.
  • Simulink.LookupTable / Simulink.Breakpoint — the flat properties MATLAB models.
    The 17-value StorageClass override replaces the shared 6 rather than widening it,
    so Custom is refused on a LookupTable while still accepted on a Simulink.Parameter.
  • Simulink.ConfigSet / Simulink.ConfigSetRefDescription, and SourceName on
    the ref.
  • _rejectUnknownEnumeral and the Min/Max normalizer move up to DataNode; a duplicate
    copy in ParameterNode is gone.

The guard

test/schemaLayoutResolvable.test.ts pins the rule between the two paths a layout key
resolves through, rather than re-testing each class. It enumerates every (class, layout key)
pair, probes each with two sentinels so the row must change rather than merely contain
text, and fails any row that reads a node field the built instance does not have. Coverage
is asserted, not assumed — 20+ classes, 182+ pairs, 100+ probe cases, and the atom table is
compared by object identity so it cannot go stale in either direction.

It was verified by reintroducing the bug in production code (commenting the Description
assignment out of ConfigSetNode's constructor), not only by its own self-check: 3 of its
14 tests fail, each naming the class, the key, and the mechanism.

Test results

4741 → 4856 passing, 26 pre-existing skips. No test deleted; no assertion loosened.
Three existing tests updated for real consequences of the new layout keys, one of them made
stricter. Typecheck, check:leak and build clean. dist/ is rebuilt and committed, as
this package ships prebuilt output for git-dependency consumers.

Not verified

The MATLAB re-open gate has not been run: the newly editable ValueType properties and
the two BusElement enums are verified in-process only (serialize → re-parse in this suite).
There is no recorded PASS that MATLAB itself loads a file written here and reads those
values back. The enums carry the risk — MATLAB writes real lower-case and Fixed
capitalized and refuses the other spelling of each. Recorded as open in the fidelity docs.

…s MATLAB shows

Simulink.ValueType's schema layout already listed min, max and unit, but the node
declared neither field. Those three keys resolve through schemaBridge's ATOM_BY_KEY
to atoms that read node FIELDS, so each row rendered blank — and toPIObject still
counted the key as shown, so the "Other" catch-all suppressed the raw property too.
A dictionary carrying "Unit": "m" had no way to display it in either pane.

ValueTypeNode now models the surface MATLAB models: Dimensions, Complexity,
DimensionsMode, Min, Max, Unit. Unit is read FIRST here and DocUnits second, the
opposite order from a Parameter or Signal, because MATLAB serializes a ValueType's
unit as Unit and those two as DocUnits; both spellings are still read so a file
written either way displays. No *_internal aliasing: a ValueType written with every
property non-default came back with flat keys only.

An absent Complexity or DimensionsMode means 'real' and 'Fixed', not the 'auto' a
Signal defaults to. That default is read on two independent paths — the table column
takes it from the schema descriptor, the Property Inspector from the node field — so
valueType.json overrides the shared dimensionsMode default per class and a test
asserts the two paths agree rather than asserting each alone. Simulink.BusElement had
the same pair defaulting to '' and showed blanks where MATLAB shows a value; its
write-back guards now compare against the default instead of testing truthiness,
which that change would otherwise have made always true.

_rejectUnknownEnumeral moves from BusElementNode, its only caller, up to DataNode
beside _setMinMax: ValueType's two enums need the same rule, and copying it would
have copied the '' -is-a-CLEAR licence, which is exactly the subtlety that rots out
of sync between two copies. _normalizeMinMax joins it there, replacing the identical
copies ParameterNode and BusElementNode each carried.

Also adds the schema descriptors the enum and lookup-table classes need next
(isTunableInCode, the three StructTypeInfo keys, SupportTunableSize and the
different-breakpoint-sizes flag), all measured off dictionaries MATLAB wrote.
…es and config sets

A parity check against the MATLAB Data Explorer app asked what each class has that we
do not model. Four answers, all measured off dictionaries MATLAB R2027a wrote rather
than read off adapter code.

Enums gain IsTunableInCode, which SLEnum.getCodegenPropertyNames appends under the
OpaqueEnum feature. We cannot read a feature flag out of a file, so the row is always
shown.

Simulink.LookupTable and Simulink.Breakpoint had almost nothing: a lookup table carried
only breakpointsSpecification and a breakpoint carried none at all. Both gain
CoderInfo.StorageClass, the three StructTypeInfo keys, and SupportTunableSize; the
lookup table also gains the different-breakpoint-sizes flag. StructTypeInfo is a nested
MATLAB object, which resolveSourcePath already traverses the way signal.json reaches
CoderInfo.CustomAttributes.DataScope.

StorageClass is shared with Simulink.Parameter and Simulink.Signal, and the two allowed
lists are not nested: a lookup table accepts seventeen values but NOT SimulinkGlobal or
Custom, both of which the shared six offer. So the two classes override `options` per
class rather than widening the shared descriptor — widening it would offer Custom on a
lookup table, and narrowing it would break Parameter and Signal. trySetSchemaProperty
validates select writes against that list, so it is an enforcement point and a test
pins both directions.

Config sets are narrowed to what a file actually carries. MATLAB's PI also shows
StartTime, StopTime and SystemTargetFile, and all three are real parameters, but a dump
of every era fixture found the modern path nests them in a heterogeneous
Simulink.ConfigComponent array no fixed sourcePath can address, values at their default
are not written at all, and one era carries none of them. Adding them would manufacture
permanently blank rows — the defect this work removes. So Simulink.ConfigSet gains
Description and Simulink.ConfigSetRef gains Description and SourceName.

Both config-set nodes had to gain a Description FIELD, not just a layout entry:
`description` is in schemaBridge's ATOM_BY_KEY, so the row reads the node field, and
both constructors dropped their props argument. A layout entry alone would have
reproduced the ValueType bug exactly. SourceName needs no field — one fixed sourcePath
covers both eras because SlxParser already normalizes WSVarName into SourceName before
a node is built.

The property-parity test now looks a nested MATLAB property up QUALIFIED by its parent
(StructTypeInfo.Name, not Name) before falling back to its leaf-name fold, so a
sub-property cannot be satisfied by some other class's row of the same name.
The three schema gaps this branch closed were three instances of one defect, and
the two rounds before this fixed the instances. `resolvePropForKey` prefers
`ATOM_BY_KEY` over a class's own schema descriptor, and those atoms read node
FIELDS rather than the descriptor's sourcePath — with nothing checking that the
node class declares the field. When it does not, the value is lost twice over:
the row renders blank, and `toPIObject` still adds the layout key to
`shownKeys`, so `buildOtherRows` suppresses the raw source property from
"Other" as well. A value the file carries goes unreachable in both panes, and
silently, because the row is present and merely empty.

Three assertions over the seam, not over the twenty classes:

- A sentinel sweep. For every (class, layout key) pair with a source location,
  build two real nodes carrying a different sentinel at that location, render
  the real Property Inspector, and require the row to show each sentinel and to
  DIFFER between them. The second half is what makes it an assertion about the
  source: a row wired to a constant passes a single-value test.
- A structural check on the built INSTANCE — a `field?: string` that is never
  assigned does not exist at runtime, and BaseNode declares `Description?:
  string` for every node, so checking the class would have called the broken
  ConfigSetNode correct.
- A self-check, because two hundred lines never observed to reject anything are
  not evidence that they would.

Coverage is asserted rather than assumed, since a sweep over an empty list
passes: 20 classes, 182 pairs, 85 computed-and-exempt, 100 probe cases, empty
skip list. The mirror of the module-private atom map is checked by object
identity, and the exemption list is derived from `readValue` rather than
trusted — so an atom that becomes field-backed falls INTO the sweep.

Verified by reintroducing the bug in ConfigSetNode rather than only on
synthetic input: 3 of 14 tests fail, each naming the class and the key.
…they do not

The fidelity docs for the classes round 2 touched still described them as
read-only pass-throughs whose inspector showed four rows. Each now names the
properties it surfaces, the group MATLAB puts them in, and — the part a reader
cannot get from the code — the measurements and the deliberate divergences.

Worth knowing from these, beyond the property tables:

- Storage Class is the ONE of the eleven new schema props with a write path at
  all. It is `projected`, so it is editable in the TABLE and a read-only label
  in the inspector, which `resolvePropForKey` forces for every schema-resolved
  row. The other ten are read-only projections of the source bag, so their
  "validation mirrored in code" sections say there is nothing to mirror rather
  than inventing a rule.
- The 17-value Storage Class list REPLACES the shared 6 rather than widening
  them, because `resolveRef` merges one level deep — and it is an enforcement
  point, not dropdown decoration: `Custom` is refused on a LookupTable and
  still accepted on a Simulink.Parameter.
- A Storage Class write can be in the list and still refused, because
  `writeSourcePath` never synthesizes a missing `CoderInfo` — which is the state
  `LookupTableNode.createDefault` leaves.
- `IsTunableInCode` cannot be gated on the file: the MATLAB feature switch that
  decides whether the property exists lives in the installation, not the
  `.sldd`. So the row is always shown, and `false` stays distinguishable from
  absent.
- Config-set `Description` is read from the node FIELD, not the descriptor's
  sourcePath, because `description` is an `ATOM_BY_KEY` key. That is the whole
  reason the constructors needed the assignment and not just a layout entry.
- ConfigSetRef's Source Name diverges from MATLAB on purpose, with the reasoning
  written down: MATLAB groups it under "Referenced Configuration" with two
  group-mates we cannot model from a file, so matching would mean a group of one.

Every doc states plainly that no MATLAB re-open gate was run for the properties
it describes. Uncomfortable to write down and worse to leave implied.
Core commits its prebuilt `dist/` because enterprise npm sets
`ignore-scripts=true`, which skips `prepare` when this package is installed as a
git dependency — so a consumer gets whatever is committed here, not a fresh
build. The `.json` files under `dist/datamodel/schema/` matter as much as the
`.js`: the class defs and prop registry are imported with `{ type: "json" }` and
tsc copies them to the output tree, so a stale `dist` would ship the old layouts
while the source showed the new ones.
Eleven properties MATLAB models became reachable, so this is a minor bump. The
release tag has to match this field, and the pin in data-explorer-vscode's
package.json cannot move until the tag exists.

package-lock.json is left alone deliberately: its `version` field has read
1.13.1 since the 1.3.0 bump, so every bump from 1.14.0 on has touched
package.json only. Matching that beats a drive-by lockfile edit — the field is
cosmetic for a git dependency, and a global version substitution in that file
has corrupted an unrelated package's entry before.
@ww-mw

ww-mw commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

Superseded by the same tree collapsed into a single commit: the repo ruleset allows only merge commits, so merging this branch would have carried its [wip] crash-recovery prefixes into main's history.

@ww-mw ww-mw closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant