Skip to content

fix: resolve instance types from the type each instance declares - #184

Merged
ehennestad merged 19 commits into
mainfrom
resolve-instance-types-from-at-type
Sep 11, 2026
Merged

ehennestad merged 19 commits into
mainfrom
resolve-instance-types-from-at-type

Conversation

@ehennestad

@ehennestad ehennestad commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Background

All controlled instances (openMINDS_instances) are stored in a folder hierarchy organized by types. In this toolbox, the type of an instance was taken from the name of the folder the instance is stored in. However, folder names are pluralized type names, so a hand-maintained plural-to-singular map was needed to recover the type. Upstream (openMINDS_instances) renames those folders whenever a type is renamed, so a map must be kept up to date.

Problem

Currently this map is outdated, it still mapped brainAtlases and commonCoordinateSpaces, renamed to anatomicalAtlases and commonCoordinateFrameworks in v5.0, and had no entry for accessibilities at all. Loading the InstanceLibrary warned five times and left 115 instances of five types untyped. parseInstanceIRI failed on the same instances, because it read the type from the second-to-last IRI segment.

Solution

Every instance document declares its own type (via the @type field), which no rename can invalidate. The type is now read from @type and the module from the type itself. All instances in a folder share one type, so one document per folder types the whole library. This removes getPluralToSingularTypeMap and SandsInstanceFolders, and carries no mapping in their place.

Of the 118 IRI segments in the library, 115 name their type in the singular and the Types enumeration matches them directly. The three that are plural — accessibilities, contentTypes, licenses — are resolved through the instance library, which indexes the segments the instance documents use.

Related

Versions

Two version problems surfaced from making types resolve against the model. Instances are typed against the model version that was on the path when the table was read, so selectModelVersion now rebuilds the library in place, and only one that already exists, since creating one would download the instance repository as a side effect of selecting a version. And the library version, a public property fixed at "latest" that nothing ever set, now follows the model version and is read-only: reading one version's instances against another version's types left 3132 instances untyped, against none when they match. Model versions 1 and 2 predate the type names the library is written against, and no version of the library can stand in for them, so that is reported rather than left looking like an empty library.

Selecting a model version no longer fails when the library is absent or incomplete for that version. The library is a separate, downloaded resource, and not being able to read it is not a reason for a change to the search path to fail; it leaves empty tables that keep their columns, and says why.

Performance

Resolving a type turned out to cost 15.5 ms. fromAtType looked up the base IRI of both known namespaces on every call, and each lookup validated its literal argument by listing the installed model versions from disk. Both are now resolved once. Reading the library drops from 4.81 s to 1.69 s, below the 2.46 s the folder-name version took, and deserialization pays the same cost per node of every document read.

before after
warnings on load 5 none
untyped instances 115 0
types resolved 114 118
fromAtType × 191 2.93 s 0.025 s
library build 2.46 s 1.69 s

Where the library lives

Re-reading the library on a version change exposed that its location could be relative. The location is built under userpath, which is empty wherever MATLAB's default user folder does not exist, including every GitHub Actions runner. A relative location stops naming the library as soon as anything changes the working directory, which the test suite does through WorkingFolderFixture; the versions detected at construction still listed a version whose files no longer resolved. The location is now resolved to an absolute path when it is set, and getSingleton resolves the location a caller asks for the same way, so one folder named two ways is no longer two libraries that rebuild each other on every call.

The folders under the user folder were constant properties. MATLAB evaluates those once, when the class is first loaded, and validating an argument to openminds.startup already loads it, so nothing inside the toolbox could set a user folder ahead of them. They now resolve when first asked for and are kept for the session, so they neither freeze too early nor move the library out from under a reader when userpath changes later. When MATLAB has no user folder the toolbox uses the temporary folder, not the working directory, and warns once, naming the folder and how to choose another. A runner has a better folder to offer; knowing which runner we are on is not the toolbox's business, so a task under tools sets that before the tests run, and nothing shipped inspects CI environment variables.

Tests

InstanceLibraryTest is new. testEveryInstanceIsTyped is the one that matters: it fails on any untyped row, which is what the v5.0 rename produced and what nothing caught. It also covers the version coupling, the rebuild on a version change, the report for a model version without instances, and the location surviving a change of working directory. PathsTest is new and covers the resolved-once folders. ValidatorsTest and EnumerationTest gain the plural-segment cases and a guard against the base IRIs being resolved per call again.

Notes

ModelVersionFixture documents that class definitions are not reloaded while instances of them are held in memory. A test asserting that matching the versions leaves nothing untyped therefore passes alone and fails in a full suite, so the test checks that the selected version's instances are the ones read, and leaves the reloading to MATLAB.

Model versions v1.0 and v2.0 cannot load openminds.enum.Types at all: their generated enumerations name classes that were never generated, AtlasTerminology for v1.0 and URL for v2.0. Untouched here. Whether those versions should ship at all is an open question, so no issue is filed for the enumerations themselves.

Three pre-existing bugs found on the way are filed separately and not fixed here: #185 (getSingleton rejects a missing library folder before the class can download it), #186 (getControlledInstance builds folder names by pluralizing, and gets accessibilities wrong), #187 (downloadRepository ignores whether the copy succeeded).

Closes #84.

🤖 Generated with Claude Code

ehennestad and others added 4 commits September 10, 2026 13:05
The openMINDS type of an instance was taken from the name of the folder
the instance is stored in. Folder names are pluralized type names, so a
hand-maintained plural to singular map was needed to recover the type.
Upstream renames those folders whenever a type is renamed, which the map
cannot follow: it still mapped brainAtlases and commonCoordinateSpaces,
renamed to anatomicalAtlases and commonCoordinateFrameworks in v5.0, and
had no entry for accessibilities at all. Loading the library warned five
times and left 115 instances of five types untyped.

Every instance document declares its own type, which no rename can
invalidate, so the type is now read from "@type" and the module from the
type itself. All instances in a folder share one type, so one document
per folder is enough to type the whole library.

Nearly every instance IRI names its type in the singular, which the Types
enumeration matches directly. The few that name it in the plural are
resolved through the instance library, which indexes the IRI segments the
instance documents use. openMINDS publishes no plural to singular
mapping, and this carries none.

Selecting another model version now rebuilds the library, since instances
are typed against the version that was on the path when the table was
built. An instance whose type the active model does not declare is listed
without a type and reported once, rather than failing the load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Instances are typed against the model version that was on the search path
when the library was read, so selecting another version leaves the table
naming types the active model may not declare.

selectModelVersion now tells the instance library which version it
selected, and the library rebuilds itself in place so that code holding a
reference to it sees the new table. Only a library that already exists is
rebuilt: creating one here would download the instance repository as a
side effect of selecting a version, which openminds.startup does on every
session.

The version is passed in rather than read back from the search path,
because that lookup caches its result for a second and would still name
the previous version. It is formatted the way openminds.version reports
it, so the check in getSingleton recognizes the library as current instead
of rebuilding it a second time. That check stays as the safety net for a
version change that does not go through selectModelVersion, since the
active version is derived from the search path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fromAtType checked the namespace of an @type by calling
openminds.constant.BaseIRI for both known namespaces on every call. That
function declares its argument as a VersionNumber validated by
mustBeValidModelVersion, so each call listed the installed model versions
from disk and built the version objects to compare against, for arguments
that are the literals "v1" and "v4".

Resolving 191 types spent 2.93 s there, against 0.05 s to read the
documents the types came from. The base IRI a version maps to is fixed and
does not depend on which model version is active, so both are now resolved
once. The same 191 types resolve in 0.025 s, and reading the instance
library drops from 4.81 s to 1.69 s.

fromAtType runs once per node of every document read, so deserialization
paid the same cost per node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The instance library publishes one set of instances per model version, but
the version to read was a public property fixed at "latest" that nothing
ever set. Selecting model version v3.0 therefore read the latest
instances, and reading one version's instances against another version's
types is exactly what leaves instances untyped: 3132 of them, against none
when the versions match.

LibraryVersion now follows the model version and is read-only. Selecting a
model version picks the matching instances, and the two can no longer
drift apart.

The library publishes no instances for model versions 1 and 2, which
predate the type names it is written against, and no other version of the
library can stand in for them. That is now reported, and the library is
left with tables that keep their columns so that listing instances answers
with none rather than erroring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (R2022a)

849 tests   845 ✅  3m 45s ⏱️
 25 suites    4 💤
  1 files      0 ❌

Results for commit f21575b.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (R2026a)

849 tests   847 ✅  3m 58s ⏱️
 25 suites    2 💤
  1 files      0 ❌

Results for commit f21575b.

♻️ This comment has been updated with latest results.

…library

Selecting a model version rebuilds the instance library, and listing the
instances of a version that holds none raised an error. That error
propagated out of selectModelVersion, so a model version could no longer
be selected at all when the library was incomplete, which is how the
library reaches a CI runner that has just downloaded it. Tests that only
switch model versions failed on it.

Selecting a model version is a change to the search path. The instance
library is a separate resource that may be absent, incomplete, or not
published for the version, so all of those now leave the library with
empty tables that keep their columns, and report why, rather than stopping
the version from being selected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.43103% with 57 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.09%. Comparing base (f309e77) to head (f21575b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...minds/+internal/@InstanceLibrary/InstanceLibrary.m 79.51% 34 Missing ⚠️
code/+openminds/+internal/+setup/ensureUserpath.m 0.00% 13 Missing ⚠️
code/+openminds/+internal/+constants/Paths.m 56.25% 7 Missing ⚠️
...openminds/+internal/+utility/resolveAbsolutePath.m 66.66% 2 Missing ⚠️
...penminds/+internal/@InstanceLibrary/getSingleton.m 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #184      +/-   ##
==========================================
+ Coverage   79.56%   80.09%   +0.53%     
==========================================
  Files         422      423       +1     
  Lines        4252     4381     +129     
==========================================
+ Hits         3383     3509     +126     
- Misses        869      872       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ehennestad and others added 6 commits September 10, 2026 16:29
The library location is built under userpath, and userpath is empty when
its default folder does not exist, as on a fresh CI runner. The location
is then relative, and stops naming the library as soon as anything changes
the working directory, which the test suite does through
WorkingFolderFixture.

That went unnoticed while the library was read once, at construction,
before any test had changed folder. Reading it again on a model version
change moved the read after that point: the versions detected at
construction still listed the version, while its files no longer resolved,
so the library reported the version as holding no instances.

The location is now resolved against the working directory when it is set,
so it keeps naming the same folder however the working directory moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MATLAB's default user folder does not exist on a runner, so userpath is
empty and the folders the toolbox keeps under it come out relative. The
live script workflow already runs ensureUserpath for this reason; the test
workflow did not, and ran against an instance library placed relative to
the working directory.

It runs before anything else is put on the path, because those folders are
read from Constant properties and MATLAB evaluates those once per session:
reading one before userpath is set freezes the relative value for the rest
of the run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The toolbox keeps the schemas and instance library it downloads under
MATLAB's user folder, which does not exist on a runner. Two copies of the
remedy had grown apart: a script under tools that the live script workflow
ran, pointing userpath at RUNNER_TEMP, and a block inside setup.m pointing
it at the working directory.

The block in setup.m never had an effect: openminds.startup runs before it
and reads a path built under the user folder, and those are constant
properties, which MATLAB evaluates once when the class is first loaded.
Pointing userpath at the working directory was also what put a library of
thousands of files inside the checked out repository.

Both are replaced by openminds.internal.setup.ensureUserpath, which ships
with the toolbox and prefers the folder a GitHub Actions runner names for
this. setup.m does not call it, because setup.m is only run from a clone,
never from an installed toolbox. Nothing inside the toolbox can call it
either: the validation of an argument to openminds.startup already reads
one of those paths, so the call has to come before the toolbox is entered,
which is where the workflows now make it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library resolves its location against the working directory when it is
set, but getSingleton compared the location a caller asked for as given.
One folder named two ways then read as two libraries, so a relative
location, which is what an empty userpath produces, deleted and rebuilt
the library on every call and invalidated every handle held to it.

Both now resolve the location the same way, through one rule the class
owns rather than a rule the setter kept to itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The folders the toolbox downloads into sit under MATLAB's user folder,
and were declared as constant properties. MATLAB evaluates those once,
when the class is first loaded, which is before there is necessarily a
user folder to build them under: a runner whose user folder does not exist
leaves userpath empty and every folder built under it relative. Validating
an argument to openminds.startup already loads this class, so nothing
inside the toolbox could get ahead of it, and the remedy had to be run
from outside before MATLAB entered the toolbox at all.

They are resolved when they are first asked for instead, which is always
late enough because it is the moment the answer is needed, and gives
MATLAB a user folder if it has none. Resolving them on every call would
answer differently once userpath changed and move the instance library out
from under whoever was reading it, so the answer is kept for the session.
It is also kept because fullfile costs enough to notice on a lookup that
runs once per controlled instance read: resolved per call the instance
folder took 1012 us, against 8.5 us kept and 7.1 us as a constant.

GeneratedFolder stays constant. It sits inside the toolbox, which cannot
move while MATLAB is running, and it is read on the paths that run most.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r folder

MATLAB having no user folder left the toolbox choosing where to write tens
of megabytes, and it chose the working directory. That is as often as not
a repository or a project folder, and the choice was recorded as the user
folder for good, naming a directory that was only ever where MATLAB
happened to be standing. The temporary folder is used instead: also not a
folder anyone asked for, but a worse place to lose files is better than a
worse place to find them.

The choice is now said out loud, as a warning with an identifier rather
than a line of printed text, naming the folder and how to pick another. It
is said once, because the folders built on it are resolved once a session.

The folder is read back from MATLAB rather than kept as given: tempdir
ends in a separator and MATLAB normalizes what it stores, so the same
folder was named two ways on the first call and every call after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ehennestad and others added 2 commits September 10, 2026 20:28
The toolbox picked a user folder for MATLAB by asking whether it was
running on a GitHub Actions runner. Which runner we are on is not the
toolbox's business: it shipped code that inspects an environment variable
a user could set by accident, and behaviour that cannot be exercised from
inside the toolbox.

The toolbox now picks the temporary folder when MATLAB has no user folder,
and nothing else. A task under tools knows about the runner and sets the
folder the runner offers, before the toolbox picks its own; anywhere else
it does nothing and leaves the choice to the toolbox. testToolbox and the
live script workflow run that task, and the calls that reached into the
toolbox from the workflows to do the same are gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ectly

Three things a review of the branch found wrong in what the instance
library says and when it says it.

The warning for an instance type the model does not declare told the user
to set the library version to match the model version. The library version
follows the model version and cannot be set, so that advice could not be
taken; and it named the model version by reading it back from the search
path, which is cached for a second and is the reason the version is passed
in explicitly everywhere else. It now names the versions it was given, and
says what a mismatch between coupled versions means: the library is ahead
of the model, or the model classes in memory belong to a version selected
earlier in the session.

A document with no readable "@type", from a file that could not be opened
or one that was truncated, was skipped without a word and its folder left
untyped. Those are now collected and reported like an undeclared type.

The versions were recorded before the read that could fail, so a read
that failed part way left the version saying the table was current while
the table was the previous version's. Everything the read produces is now
assigned together, last, with the root folder and the versions passed down
to what needs them rather than read from an object not yet updated. That
also retires a dependent property that had no answer while the library
version was missing.

Also from the review: the setget mixin was inherited for a call that no
longer exists; a docstring described the folders under the user folder as
constant properties that had to be resolved ahead of time, which they no
longer are; the two header readers signalled "not found" differently; and
the test of whether a path is absolute duplicated the implementation under
test rather than checking it independently.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ehennestad and others added 2 commits September 11, 2026 04:41
Resolving a relative path against the working directory is not something
the instance library knows how to do better than anything else, and as a
private static method of it nothing else could use it. It is now
openminds.internal.utility.resolveAbsolutePath, beside the other path
helpers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The warning for an instance whose type could not be resolved said that
the model version did not declare the type. That is not what was checked,
and it is false in the case that raises the warning most: the classes in
memory belong to a version selected earlier in the session, and nothing
has let MATLAB read the selected version's classes yet. The types named
are then exactly those the two versions do not share, all of which the
selected version declares.

The warning now says what was checked, the model classes loaded in this
session, and gives the two things that can mean and what to do about the
second, rather than telling the user to restart MATLAB.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ehennestad and others added 4 commits September 11, 2026 10:15
On a runner whose default user folder does not exist, which is the case
this task exists for, MATLAB warns that it cannot locate the personal
folder while the user folder is being set. The warning is switched off
around setting it and restored afterwards.

The task also returns nothing when nothing is asked for, so that calling
it as a statement, as testToolbox does, does not print ans.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The comments added on this branch described mechanisms indirectly, and
some described the wrong problem: they said MATLAB has no user folder,
when the problem is that userpath is empty on Linux when $HOME/Documents
does not exist, as it is on CI runners.

Every comment added on the branch now states the mechanism first and
the consequence second. The Paths class header gives the two reasons
the user folder paths are cached static methods rather than Constant
properties. Both ensureUserpath functions say what they set, when, and
what they leave alone. The instance library comments name the one
second cache in openminds.version instead of calling it brief, and the
two causes an unresolved instance type can have. The test comments say
what is asserted and why.

The warning raised when userpath is unusable now says so, instead of
saying MATLAB has no user folder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A library folder that exists but holds no version folder is an
incomplete download that the recorded commit did not notice. Nothing
warned about it, so every controlled instance lookup silently found
nothing. resolveLibraryVersion now warns with
OPENMINDS:InstanceLibrary:NoVersionsFound in that case. A folder that
does not exist stays silent here, because the failed download has
already been reported.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Move createInstanceTable, resolveFolderInfo and createIRISegmentIndex
  out of the class into local functions. None of them read the object,
  and the private method block they sat in had no other members.
- Name the IRI segment index columns once, in
  iriSegmentIndexVariableNames, instead of repeating the literal.
- Spell out the staleness test in getSingleton as one named condition.
- Reword the comments in ensureUserpath, testToolbox and
  resolveFolderInfo that a reader had to reread.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ehennestad
ehennestad merged commit e571eb7 into main Sep 11, 2026
8 checks passed
@ehennestad
ehennestad deleted the resolve-instance-types-from-at-type branch September 11, 2026 13:21
@ehennestad ehennestad added the fixed Corrects a defect; listed under Fixed label Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fixed Corrects a defect; listed under Fixed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automate instance-folder type mapping and add regression check for warning-free InstanceLibrary instantiation

1 participant