Repository navigation
Binary nested objects in Other, a cleared envelope property, VariantControl Description, model-reference ids, and a dist-freshness CI gate - #123
Merged
Conversation
…, and an object array as its summary
C-032 of the 2026-10-15 unfixed sweep, decided in the maintainer's group E ruling (2026-10-16,
theme 2, "follow MATLAB", option (a)).
A binary dictionary spells a nested object as an object array of one element,
`{_array_class, _dimensions: [1, 1], _elements: [{_properties}]}`, where a .mat and a text
dictionary spell it `{_object_class, _properties}`. piOther.asNestedObject recognised only the
second, so a LookupTable's Table and Breakpoints each showed as one empty row in a binary
dictionary (`Other.Table: ''`), where the other venues list `Other.Table.Value =
[1+2i, 3+4i, 5+6i]` and `Other.Breakpoints.Value = [1, 2, 3]`. An object array of more than one
element, in any venue, showed `''` too. MATLAB's display holds the same object in every venue
(`Table: [1×1 Simulink.lookuptable.Table]`) and an array as `[1×2 Class]`.
Fix, in piOther.ts alone: asObjectArray claims `{_array_class, _elements}` when the class is a
non-empty string, the `_array_type` is not Struct, Cell or String, and every element is a plain
object with a `_properties` bag and no `_enumeration` (an enumeration keeps today's display).
One element in a 1x1 shape (or no `_dimensions`) is that object: asNestedObject returns it, with
the element's own `_object_class` winning over the array's, as DataNode.elementClassOf reads one,
and buildOtherRows flattens it one level, unchanged. Any other claimed array is its
summaryForm, `<1x2 Simulink.lookuptable.Breakpoint>`, `<0x0 C>`, at any depth; a one-element
object one level further down is its `[Class]`, as before. The scope said "not Struct or Cell";
String is excluded too, because a string array is never an object array and an empty one would
pass the element check vacuously.
Over the 76 fixtures under test/fixtures (4,216 nodes, 95 with Other rows), 7 rows go, 47 come
and 14 change, and every row that goes or changes was empty: the binary LookupTable's two empty
rows become its 18 sub-rows; a binary or text one-element bus or interface's empty
`Elements_internal` row (and a binary ValueType's `be`) lists its element's properties, as the
same one-element bus out of a .mat already did; and 14 multi-element `Elements_internal` and
`Arguments` rows, in .mat, text and binary, go from `''` to `<3x1 Simulink.BusElement>` and the
like.
Tests: test/piOther.test.ts, a new describe. A binary 1x1 Table flattens to the same rows as
the `{_object_class}` spelling; a 1x2 and a 3x1 array, an unshaped pair, a 0x0 and a 1x1x2 are
their summary; an element's own class names it, an empty one falls back to the array's, and its
sub-rows are its own; one level further down a 1x1 is `[Class]` and a 1x3 its summary. Each of
those four fails before the change (each row read `''`). A control describe passes before and
after: Struct, Cell and String envelopes carrying an `_array_class`, enumeration values of one
and two elements, a non-object element and an empty class all still show `''`. Ten mutants of
the new arms (each exclusion, the 1x1 check, the class rule both ways, the summary) are each
killed.
Existing test changed, where it pinned exactly the behaviour this removes:
complexMcosFixtures.test.ts "a LookupTable with a complex Table presents as it does
everywhere" held complex_binary.sldd to `Other.Table: ''`; the binary branch is deleted, so that
venue is held to the same Other.Table.Value and Other.Breakpoints.Value as the others (fails
before: `expected undefined to be '[1+2i, 3+4i, 5+6i]'`). DESIGN.md's known-limitation bullet
for this is deleted, and piOther.ts's header names the new form.
dist regenerated (rm -rf dist && npm run build): only piOther's three files change.
npm run verify: 7934 passed, 26 skipped.
…aves a cleared one
C-199 of the 2026-10-15 unfixed sweep, decided in the maintainer's group E ruling (2026-10-16,
theme 2, "follow MATLAB", option (a)).
Under a saveobj envelope DataNode._mergeProps dropped every empty override, because an empty
there is usually the default a node substitutes for a property it cannot see inside the
envelope. A real clear was dropped with it. Set a binary dictionary's VariantVariable
Specification to 'B' and save: defect 46 writes 'B' into the envelope and as a sibling <P>.
Reopen, clear it and save again: both copies still said 'B', and both this reader and MATLAB
(R2027a 27.1.0.3409468, measured) reopened it as 'B'. The same loss hit a VariantVariable this
package created, in a text dictionary as well (createDefault gives it an envelope in both
formats), and VariantBank's Value in both formats.
Fix, in _mergeProps: an empty override ('', null or undefined) is still dropped when the
stored bag has no own sibling for the key, because that is a default. When a sibling is there,
this package wrote it, since MATLAB never writes one beside an envelope, so the empty is a
clear. The sibling is deleted, because MATLAB writes none and this reader then reads the
node's empty default. The envelope's field is written as [], the 0x0 double. MATLAB writes
that for a default VariantVariable, for one set to '' and for one cleared after being 'B', and
it reads back as an empty char (both measured). A field the envelope does not declare leaves
the envelope untouched, as an edit does. The check uses hasOwnProperty, not `in`. After the
clear, cases.sldd's aVariant is MATLAB's own bytes again. The doc comment, the
SimulinkObjectNode header and test/parity/matlab/DESIGN.md (defect 46) state the sibling rule.
Tests, all new:
- test/binaryWriteBackGate.test.ts, a C-199 describe on MATLAB's cases.sldd. aVariant set to
'B', saved, reopened, cleared and saved again is written byte for byte as MATLAB wrote it,
with one Specification tag, the 0x0 double. It reopens as ''. On _mergeProps directly, each
of '', null and undefined drops the sibling and writes [] into a copy of the envelope,
leaving the parse state alone. These three fail before the change because the old value
stays ('B' twice in the file, 'B' on reopen, the sibling still present). Three controls
pass before and after: with no sibling an empty override writes nothing and returns the same
envelope; a non-empty edit over a saved one still goes to both places; and the text
artifact, which has no envelope here, already reopened a clear as cleared.
- test/newEntryEnvelopeBothFormats.test.ts, a C-199 describe. A new VariantVariable
(Specification) and a new VariantBank (Value), each in binary and text, set to 'x', saved,
reopened, cleared and saved: the entry holds the envelope alone, a declared field holds the
0x0 double, and it reopens as ''. On _mergeProps, VariantBank's undeclared Value drops its
sibling and returns the same envelope object. All five fail before the change, each with
the old 'x' still written.
No existing test changes. Mutants of the change (sibling kept, '' written instead of [], the
sibling check removed, null left out) are each killed.
dist regenerated (rm -rf dist && npm run build): DataNode and SimulinkObjectNode only.
…ne to save
F2-4 of the 2026-10-15 unfixed sweep, decided in the maintainer's group E ruling (2026-10-16,
theme 4, recommendation (c)). Decision D11 (2026-10-13) says an entry-level Description edit on
a class whose file has no Description is refused.
A VariantControl has no Description. MATLAB writes none for it (the binary params.sldd's
MyVarCtrl holds Value, ActivationTime and ValueType), its schema (classes/variantControl.json)
has none, and VariantControlNode._serializedOverrides writes back the Value alone. But the
node did not override descriptionEditable, so it inherited BaseNode's true. The cell and the
Property Inspector offered an editor, setProperty('Description', ...) returned true, the row
showed the text, and the save dropped it.
Fix: VariantControlNode.descriptionEditable is false. The row's _descriptionEditable and
getPropInfo's Description field are now not editable. setProperty passes a non-Value property
to DataNode.setProperty, whose existing guard refuses it ("A <kind> has no Description in a
dictionary ..."), with the node unchanged and no undo step. vscode reads _descriptionEditable
from the row, so it needs no change. Simulink.VariantControl.md says so.
The ObjectNode half of F2-4 (AUTOSAR.Parameter and other classes core has no node for) is not
in this change. It waits for the maintainer's ruling on whether "has a Description" is decided
per entry or per class.
Tests, all new, in test/editsTheFormatCannotKeep.test.ts ("a Description the entry has
nowhere to put"):
- On VariantControlNode.createDefault: descriptionEditable, the row's _descriptionEditable and
getPropInfo(PropDescription).editable are false. setProperty('Description') returns the
refusal and changes nothing, serializeValue and serializeXml do not hold the text, and a
Value edit is still taken.
- Through the session, on MATLAB's params.sldd (text and binary): editProperty on MyVarCtrl's
Description returns { error: true }, canUndo() is false, the row's Description stays '', and
the saved file is the same as before.
All three failed before the change: descriptionEditable was true, and editProperty returned
true. No existing test changed.
dist regenerated (rm -rf dist && npm run build): only dist/datamodel/node/data/VariantControlNode.*.
…e block or the path F6-1 of the 2026-10-15 unfixed sweep, decided in the maintainer's group E ruling (2026-10-16, theme 4, recommendation (a)). A model that references plant.slx from two Model blocks has two reference rows, both named plant.slx, since the name is the file the link follows. ModelReferenceNode inherited BaseNode.id (parent id + '/' + name), so both rows had the id <model>/references/plant.slx. The session index is keyed by id, so it kept only the second, and findNodeById could never return the first. A host keys rows and remembered selections by id too, so it could not tell the two apart. DataSourceNode had the same problem: its name is the basename of the recorded path, so a model whose sources are a/signals.mat and b/signals.mat had two rows with the id <model>/dataSources/signals.mat. Fix, after ModelBlockNode.id (which keys a block on its SID for the same reason): - ModelReferenceNode.id is parent id + '/' + name + '@' + blockPath. A block path is unique per Model block. - DataSourceNode.id is parent id + '/' + name + '@' + fullPath (the path the model recorded). name, displayName, the Name column and the Value cell's linkTarget are unchanged, so resolveLink and DataModel's resolution order (both read the name) behave as before. Nothing in core parses a node id. These ids are public and they change for every model, not only for models with duplicates. For example, at the reference fixtures: - mdlcases.mdl/references/mdl_child.mdl is now mdlcases.mdl/references/mdl_child.mdl@$bdroot/Child - mdlcases.mdl/dataSources/mdlparams.sldd is now mdlcases.mdl/dataSources/mdlparams.sldd@mdlparams.sldd - model_with_refs.slx/references/plant.slx is now model_with_refs.slx/references/plant.slx@ctrl/plant A host that keeps selection or expansion by node id loses it once for these rows when it updates. ModelReferenceNode.md and DataSourceNode.md describe the new id. New tests in test/referenceIdentity.test.ts. Each model is built by hand and goes through the real parser and ingest: - A .slx whose graphicalInterface.json references plant from top/A and top/B: the two reference nodes have distinct ids (spelled as above), findNodeById returns each one with its own blockPath, and flatten() has no duplicate id. Name and link target are still plant.slx. - The same for a classic .mdl with two ModelReference records ($bdroot/A, $bdroot/B). - A .slx whose ExternalDataSourceSettings.xml names a/signals.mat and b/signals.mat, and one with Windows paths: two distinct ids, each findable, both rows still named signals.mat with link target signals.mat. - Ingesting the same bytes again gives the same four ids. All five failed before the change, each because the two rows had the same id. Existing tests changed: only the three that looked up these rows by the old id string, which is the id the ruling removes. They now find the row by name and look up its id: - test/resolveLink.test.ts, "follows a data-source target ..." and "follows a model-reference target ..." (new helper modelEntry). - test/usedByColumn.test.ts, "gives no cell to the nodes the column means nothing for" (new helper idOf). What each test asserts is unchanged. dist regenerated (rm -rf dist && npm run build): only DataSourceNode.* and ModelReferenceNode.* change.
dist/ is committed because consumers install this package from git, so the committed dist/ is what they run. Until now nothing checked it against src/: verify's build overwrites dist/ in the runner and compares it with nothing, so a commit that changed src/ without rebuilding passed CI and could be tagged and released with the old code in it. ci.yml and release.yml now run one more step after verify, "dist/ is the build of src/". It deletes dist/, builds it again, prints any file that differs, and fails if a tracked output changed or was not rebuilt (git diff --exit-code) or if the build made an output nobody committed (an untracked file under dist/). It deletes first because tsc never deletes an output, so an incremental build would leave the output of a deleted source in place and pass. In release.yml it runs before Create GitHub Release, so a tag with a stale dist/ never becomes a Release. The .gitignore note credited verify with this check; it now names the step. test/ciDistGate.test.ts reads both workflows and checks that the step is there, after verify (and before the Release is created), with the same script in both, and with no if:, continue-on-error: or shell: that could skip it or ignore it. It then runs each workflow's own script under bash -e in a throwaway git repository whose build, like tsc, never deletes. The script passes on a dist/ that matches and fails, naming the file, when src/ changed without a rebuild, when dist/ keeps the output of a deleted source, and when a new output was never committed. Before adding the gate, a clean build of main (8e33408) from a fresh clone, on a case-sensitive volume with the runner's Node 22.23.3, reproduced the committed dist/ byte for byte: 568 files, no difference, no untracked file. CI's own pack line on Linux at that commit (571 files, 833.3 kB) matches the local one. No source change, so dist/ is unchanged.
Follow-up to C-199 (dd03991), from PR #123's correctness review (non-blocking). dd03991 reads "the stored bag has a sibling for this key" as "this package wrote the value, so an empty override is a user clear". That is false when the sibling is itself empty beside a real envelope value: envelope Specification 'B', sibling <P Name="Specification" Class="char"/>. The node read '' from the sibling, so its '' is no change. Edit only Description and save, and the envelope was rewritten as Class="double" Dimension="0*0", losing 'B'; the base before dd03991 kept it. Fix, in DataNode._mergeProps: an empty override is a clear only when the stored sibling is non-empty. An empty override over an empty sibling ('', null or undefined) is dropped as a default, so the entry is written as it was read. The doc comment states the rule. No writer leaves that shape today: MATLAB writes no sibling beside an envelope, and from v1.0.0 this package writes no empty one there (the review traced v0.1.7, which does, and found it writes the envelope as Name="undefined", which is not read as an envelope). Test, new, in test/binaryWriteBackGate.test.ts's C-199 describe: cases.sldd's aVariant set to 'B' and saved (envelope and sibling both 'B'), the sibling alone blanked, reopened (reads ''), Description edited and saved. The two Specification tags must be written as read: 'B' in the envelope and the empty sibling. Before the change it fails with one tag, Class="double" Dimension="0*0". The existing C-199 tests still pass, so a real clear (sibling 'B' or 'x') still deletes the sibling and writes the 0x0 double. dist regenerated (rm -rf dist && npm run build): DataNode only.
ww-mw
added a commit
that referenced
this pull request
Oct 16, 2026
main's #123 reached main after this branch's last green CI, and the merge ref failed 4 tests. Neither failure is a defect in either side: each test pinned something the other PR changed on purpose. - test/newEntryEnvelopeBothFormats.test.ts (from #123, C-199). Three cases set a Value on a new Simulink.VariantBank to exercise a field the saveobj envelope does not declare. C-168 removes exactly that Value (MATLAB's class has none), so the set is now refused. The cases move to the one vehicle a new entry still has for that shape: Simulink.VariantConfigurations' Value, written by createDefault as a sibling beside an envelope that does not declare it, in both formats. The assertions are unchanged; the undeclared case also states its premise (the envelope's _fields has no Value), so a vehicle that stops being one fails rather than passing vacuously. That Value is unmeasured against MATLAB and may be invented too; the comment says so. With #123's _mergeProps clear reverted (src only), all five C-199 cases here fail with the old sibling still written. - test/signalEnumProps.test.ts (this branch, C-030). The Property Inspector's Other group for sigC in a binary dictionary now lists LoggingInfo's seven properties, because #123's C-032 flattens a binary 1x1 object one level as the same object out of a .mat. The text dictionary's LoggingInfo has no properties, so it stays one row. The expectation follows #123 per format. With C-030's src reverted the case still fails (at its first assertion, the PI row's edit); with C-032's piOther change reverted it fails at the updated line. No src change; dist rebuilt clean after the merge with no difference.
This was referenced Oct 16, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the maintainer's 2026-10-16 group E ruling.
Five items from the 2026-10-15 unfixed sweep, one commit each. The ruling, from the agenda's "DECIDED 2026-10-16, group E (the backlog)" block:
C-032 and C-199 fall under theme 2 ("follow MATLAB, and measure it first … where it is unmeasured"). F2-4, F6-1 and C-003 fall under themes 3, 4, 6 and 7 ("take each item's recommendation").
Each commit regenerates
dist/withrm -rf dist && npm run buildand commits the result.npm run verifywas green at each commit.Gate at the head (
1b3aeac; after the guard31dca0c: verify 7971 passed / 26 skipped, CI green)rm -rf dist && npm run build: the tree stays clean, with 0 entries ingit status --porcelain --untracked-files=all.npm run verify: rc 0.check:pack(571 files, 836.3 kB),check:leakandcheck:browserare all OK.C-032: the Other group shows a binary dictionary's nested object (
b64fdaf)Ruling: theme 2, follow MATLAB. Recommendation, taken: "(a). Flip the pin in complexMcosFixtures to the same Other.Table.Value and Other.Breakpoints.Value expectations the other venues use." Option (a) reads: "Unwrap only a one-element
_elements… and show a multi-element one as[1xN Class]."What was wrong. In a binary dictionary, a LookupTable's Table and Breakpoints each showed in the Property Inspector's Other group as one empty row (
Other.Table: ''). The.matand text venues listOther.Table.Value = [1+2i, 3+4i, 5+6i]andOther.Breakpoints.Value = [1, 2, 3]. An object array of more than one element showed''in every venue. MATLAB shows the same object in every venue ([1×1 Simulink.lookuptable.Table]), and an array as[1×2 Class].Root cause. The binary reader spells a nested object as an object array of one element:
{_array_class, _dimensions: [1, 1], _elements: [{_properties}]}.piOther.asNestedObjectrecognised only the{_object_class, _properties}spelling, so the binary form fell through toformatOther, which returned''.Fix (only
src/datamodel/node/piOther.tschanges):asObjectArrayclaims{_array_class, _elements}when all of these hold:_array_typeis not Struct, Cell or String;_propertiesbag and no_enumeration._dimensions) is that object, and it flattens one level as before. The element's own_object_classwins over_array_class, asDataNode.elementClassOfreads it.<1x2 Simulink.lookuptable.Breakpoint>.DESIGN.md's known-limitation bullet is deleted.Deviations, with their reasons. First, the scope said "not Struct or Cell". String is excluded too, because a string array is never an object array, and an empty one would pass the element check vacuously. Second, option (a) writes the summary as
[1xN Class]. The code writes<1xN Class>, which is this repo's spelling of a summary (summaryForm,DisplayConvention.ts). The commit and test comments say the same.Tests.
test/piOther.test.tshas a new describe block:{_object_class}spelling.[Class]and a 1x3 is its summary.All four behaviour cases failed before the change, each because the row read
''. A controls case passes before and after: Struct, Cell and String envelopes, enumerations, a non-object element and an empty class all still show''. Ten mutants of the new code were each killed.Existing test changed:
test/complexMcosFixtures.test.ts, "a LookupTable with a complex Table presents as it does everywhere". Its binary branch pinnedOther.Table: ''forcomplex_binary.sldd, which is exactly the behaviour the ruling removes. That branch is deleted, so binary is held to the sameOther.Table.Value/Other.Breakpoints.Valueas the other venues. Before the fix it fails withexpected undefined to be '[1+2i, 3+4i, 5+6i]'.Fixture census (76 fixtures, 4,216 nodes): 7 rows go, 47 appear and 14 change. Every row that goes or changes was
''before.Elements_internalrow now lists its element's properties, in binary and text alike (.matalready did).''to<Nx1 Simulink.BusElement>and similar.No vscode test pins any of these rows.
Left out: nothing.
C-199: a cleared property under a saveobj envelope is saved as MATLAB saves one (
dd03991)Ruling: theme 2, follow MATLAB, measured first. Recommendation, taken: "(a), after one MATLAB read-back of an envelope whose Specification was written as an empty char. Defer (c) to the defect-40 work." Option (a) reads: "drop the empty only when the stored bag has no sibling for that key. When a sibling exists (a value this package wrote earlier), write the empty to the sibling and into the envelope."
What was wrong. Take a binary dictionary's VariantVariable:
'B'and save. Defect 46 writes'B'into the envelope and also as a sibling<P>.'B'.This reader reopened the entry as
'B', and so did MATLAB (R2027a 27.1.0.3409468, measured). The same loss hit:createDefaultgives it an envelope in both formats;Root cause. Under an envelope,
DataNode._mergePropsskipped every empty override ('',null,undefined). Usually an empty there is a default the node substitutes for a property it cannot see inside the envelope, but a real clear was skipped along with it.MATLAB measurements (Bmain R2027a):
Class="double" Dimension="0*0") in three cases: a default VariantVariable, one set to'', and one cleared after being'B'.So following MATLAB means writing
[], not''.Fix, in
_mergeProps. When the override is empty and the stored bag has no own sibling for the key (hasOwnProperty), it is still dropped, because it is a default. When a sibling is there, this package wrote it, since MATLAB never writes one beside an envelope, so the empty is a clear:[], the 0x0 double;After the clear,
cases.sldd's aVariant holds MATLAB's own bytes again. The doc comment, theSimulinkObjectNodeheader andtest/parity/matlab/DESIGN.md(defect 46) state the rule.Tests (all new):
test/binaryWriteBackGate.test.ts, a C-199 describe on MATLAB'scases.sldd:'B', then save, reopen, clear and save: the result is byte for byte MATLAB's own aVariant, and it reopens as'';_mergePropswith each of'',nullandundefineddrops the sibling and writes[]into a copy of the envelope.These failed before the change:
'B'was in the file twice, the entry reopened as'B', and the sibling was still present. Three controls pass before and after:test/newEntryEnvelopeBothFormats.test.ts, a C-199 describe:'x', then save, reopen, clear and save. Each ends as the envelope alone, with[]in a declared field, and reopens as''.All five failed before the change, each with the old
'x'still written. Across the two files: 8 failed and 54 passed before the change. Four mutants were each killed: sibling kept,''written instead of[], the sibling check removed, andnullleft out.Existing tests changed: none. The
binaryWriteBackGate.test.tscontrol for an envelope default still passes unchanged.Left out: option (c), which decodes the envelope into node properties, is deferred to the defect-40 work, as the recommendation says.
Empty-sibling guard, after review. C-199: an empty sibling beside a real envelope value is not read as a clear (
31dca0c).What was wrong.
dd03991treated any own sibling as a sign that the value was the package's own, so an empty override over it counted as a clear. That is wrong when the sibling is itself empty and the envelope holds a real value: envelope Specification 'B' and sibling<P Name="Specification" Class="char"/>. The node reads''from the sibling, so writing''back changes nothing. Even so, editing only Description and saving rewrote the envelope asClass="double" Dimension="0*0", and 'B' was lost. Beforedd03991, 'B' was kept.Guard. In
DataNode._mergeProps, an empty override counts as a clear only when the stored sibling is non-empty. If the override and the sibling are both empty ('',nullorundefined), the override is dropped as a default, and the entry is saved as it was read. The doc comment states the rule.No real input reaches this today. MATLAB writes no sibling beside an envelope. From v1.0.0, this package writes no empty sibling there. v0.1.7 does write one, but it writes the envelope as
Name="undefined", which is not read as an envelope (traced by the correctness review).Test. New, in the C-199 describe of
test/binaryWriteBackGate.test.ts:cases.sldd's aVariant to 'B' and save it. Both the envelope and the sibling now hold 'B'.''.Fail-before. Without the guard, the only failure is the new test: 1 failed, 6 passed in the C-199 describe. It received one tag,
<P Name="Specification" Class="double" Dimension="0*0"/>, where it expected[<P … Class="char">B</P>, <P … Class="char"/>].After. The new test passes. The 11 existing C-199 tests still pass: 6 in
binaryWriteBackGate.test.tsand 5 innewEntryEnvelopeBothFormats.test.ts. So a real clear (sibling 'B' or 'x') still deletes the sibling and writes the 0x0 double.npm run verifypasses: 240 files, 7971 passed, 26 skipped. dist is a clean rebuild (rm -rf dist && npm run build), and only DataNode changed.F2-4: a Simulink.VariantControl refuses a Description edit (
21ae8a1)Ruling: themes 3, 4, 6 and 7, take the recommendation. Recommendation, taken: "(c) now, since it is FIX-grade under D11. Then (b) once the maintainer rules whether 'has a Description' is decided per entry or per class." Option (c) reads: "Land VariantControlNode's refusal now (D11 names Simulink.VariantControl, and neither its doc's property table nor its schema has a Description) … Decide ObjectNode separately."
What was wrong. A VariantControl has no Description:
params.sldd's MyVarCtrl holds only Value, ActivationTime and ValueType._serializedOverrideswrites back only Value.Even so, the cell and the Property Inspector offered an editor,
setProperty('Description', …)returned true, and the row showed the text. The save then dropped it.Root cause.
VariantControlNodedid not overridedescriptionEditable, so it inherited BaseNode'strue.Fix.
VariantControlNode.descriptionEditablereturnsfalse.DataNode.setProperty's existing guard then refuses the edit ("… has no Description …"), leaves the node unchanged and adds no undo step. The row's_descriptionEditableandgetPropInfo's Description field are no longer editable. vscode reads_descriptionEditablefrom the row, so it needs no change.Simulink.VariantControl.mddocuments this.Tests (all new), in
test/editsTheFormatCannotKeep.test.ts:createDefault:descriptionEditable,_descriptionEditableandgetPropInfo(Description).editableare all false;setProperty('Description')returns the refusal and changes nothing;serializeValuenorserializeXmlholds the text;params.sldd, text and binary:editPropertyon MyVarCtrl's Description returns{ error: true };canUndo()is false;'';All three failed before the change. The unit case failed with
expected true to be false. Both session cases failed withexpected true to match object { error: true }, because the edit was accepted.Existing tests changed: none.
Left out:
test/fixtures/display/dispcat_text.sldd,fi_vec. It is an ObjectNode that still accepts a Description edit:setProperty('Description', …)returns true and the row shows the text. Saving then drops it, because its bag has no Description. Both candidate answers to the per-entry vs per-class question would refuse this edit.F6-1: model references and data sources get one id per row (
fbb53f6)Ruling: themes 3, 4, 6 and 7, take the recommendation. Recommendation, taken: "(a), keyed on blockPath, mirroring ModelBlockNode's blockKey precedent. Apply the same rule to DataSourceNode using its recorded path, and land it alongside dex-core's G14 so the parity record moves with it."
What was wrong. A model that references
plant.slxfrom two Model blocks has two reference rows, both namedplant.slx, and both had the id<model>/references/plant.slx. The session index is keyed by id, so it kept only the second row, andfindNodeByIdcould never return the first. A host that keys rows and selection by id could not tell the two apart.DataSourceNodehad the same collision for two sources with one basename, such asa/signals.matandb/signals.mat.Root cause. Neither class overrides
id, so each inheritedBaseNode.id(parent id +/+ name), and the name is the referenced file or the basename of the source path.Fix, after
ModelBlockNode.id's precedent:ModelReferenceNode.idis parent id +/+ name +@+ blockPath.DataSourceNode.idis parent id +/+ name +@+ the recorded path.The name, displayName, Name column and
linkTargetare unchanged, soresolveLinkand the resolution order behave as before. Nothing in core parses a node id.These public ids change for every model, not only for models with duplicates. For example,
mdlcases.mdl/references/mdl_child.mdlbecomesmdlcases.mdl/references/mdl_child.mdl@$bdroot/Child. A host that keeps selection or expansion by node id loses it once for these rows when it updates.ModelReferenceNode.mdandDataSourceNode.mddescribe the new ids.Tests (all new), in
test/referenceIdentity.test.ts. Each test builds a model by hand and runs it through the real parser andingest:.slxreferencing plant fromtop/Aandtop/B: the two rows have distinct ids, spelled exactly;findNodeByIdreturns each row with its own blockPath;flatten()has no duplicate id; the name and link target are stillplant.slx..mdlwith two ModelReference records..slxwhose ExternalDataSourceSettings namesa/signals.matandb/signals.mat.All five failed before the change, each because the two rows had one id. Each override is pinned on its own: removing only the ModelReferenceNode override fails tests 1, 2 and 5, and removing only the DataSourceNode one fails tests 3, 4 and 5.
Existing tests changed: three. Each looked a row up by its old id string, which is the id the ruling removes. Each now finds the row by name and looks up its id, and what each one asserts is unchanged. They are the three the scope predicted:
test/resolveLink.test.ts: "follows a data-source target …" and "follows a model-reference target across the .slx/.mdl extension guess" (helpermodelEntry).test/usedByColumn.test.ts: "gives no cell to the nodes the column means nothing for" (helperidOf).Left out:
C-003: CI fails when the committed dist is not a clean build of src (
1b3aeac)Ruling: themes 3, 4, 6 and 7, take the recommendation. Recommendation, taken: "Use the first option, with the clean rebuild. First confirm once, by hand, that a clean ubuntu build at origin/main reproduces the committed dist byte for byte, so the gate does not start red." The first option reads: "ci.yml and release.yml: after verify,
rm -rf dist && npm run build && git diff --exit-code -- dist && test -z "$(git status --porcelain --untracked-files=all -- dist)"."What was wrong.
dist/is committed, and it is what a consumer installing from git runs. Nothing checked it againstsrc/. A commit that changedsrc/without rebuilding passed CI and could be tagged and released with the old code in it.Root cause. Verify's build overwrites
dist/in the runner and compares it with nothing.Precondition, checked first. At
main(8e33408), a fresh clone was built on a case-sensitive volume with the runner's Node 22.23.3:npm ci, thenrm -rf dist && npm run build. The result matched the committeddist/byte for byte: 568 = 568 files, no diff, no untracked file and no absolute path. CI's Linux pack line at that commit (571 files, 833.3 kB) matches the local one. There is no Linux host here, so this PR's own CI run is the first byte-level check on Linux.Fix.
ci.yml(after verify) andrelease.yml(after Verify, before Create GitHub Release) each get the same step, "dist/ is the build of src/":dist/first because tsc never deletes an output. Without that, the output of a deleted source would stay and pass.git statusline names every differing file in the log. This is the scope's optional print step..gitignore's note credited verify with this check. It now names the step.Tests (all new):
test/ciDistGate.test.ts, 17 tests.{name, run}, so noif:,continue-on-error:orshell:can skip it or hide a failure;release.ymlbefore Create GitHub Release;bash -ein a throwaway git repo whose build, like tsc, never deletes. The script passes on a matchingdist/. It fails, naming the file, on each of: a stalesrc/, an orphaned output, and a new output that was never committed.Before the change, 16 tests failed and 1 passed (the reader self-check). Every failure was "has exactly one step that rebuilds dist/ … got +0". Ten mutants were each caught. Two examples:
rm -rf dist_old, which keeps every needle, reds only the orphan case;test -zline reds only the uncommitted-output case.Existing tests changed: none.
Cost: about 9 s more test time, because the behaviour cases spawn npm and git, plus one tsc run per CI job.
Left out: nothing. If this PR's Linux CI reds at the new step, the merged
dist/is stale, which is a real finding and not a gate defect.Not in this PR