Skip to content

v0.9.1: engineering-quality, architecture, and infrastructure follow-ups - #51

Merged
andremun merged 32 commits into
masterfrom
0.9.1/development
Aug 21, 2026
Merged

v0.9.1: engineering-quality, architecture, and infrastructure follow-ups#51
andremun merged 32 commits into
masterfrom
0.9.1/development

Conversation

@andremun

Copy link
Copy Markdown
Owner

Summary

Implements the v0.9.1 milestone: engineering-quality, architecture, and infrastructure follow-ups to v0.9.0, deliberately scoped to no algorithmic changes and no new user-facing options. Full details, organized as New functionality / Better engineering / Bug fixes, are in the new RELEASE_NOTES.md v0.9.1 section added by this PR (the existing v0.9.0 section is untouched).

Highlights:

17 milestone issues are closed on GitHub already (each closing comment cites the commit that fixed it) — this PR is what actually lands that code on master. Two items were deliberately deferred, not included here: #30 (PILOT projection-orientation canonicalisation) and #50 (CLOISTER's 2D-only convex hull for 3D projections), both algorithmic changes out of this milestone's scope, left for a future v0.9.2. #35 (cross-repo Python-port fixture export tool) was closed as not planned — that concern belongs in the pyInstanceSpace repo, not here.

Also bumps Contents.m/CITATION.cff to version 0.9.1, and fixes CITATION.cff's license field (was an unrecognized SPDX identifier, causing v0.9.0's Zenodo archival to silently fail) to use license-url instead.

Test plan

  • CI green on the head commit (227b799, run 32350513845) — example.m + the full matlab.unittest suite (tests/*.m), 40/40 cases passing.
  • New regression coverage added for this PR's own changes: ClassApiTest.testOnStageCallback (both build()'s full-pipeline and 'stages'-subset onStage modes, and explore()'s), plus existing RegressionTest coverage for the bug fixes listed above (seed reproducibility, CLOISTER non-mean-centred warning, required-field validation, TRACE good/best mismatch guard, PRELIM tie-breaking consistency, boundary display 2D/3D, traceAlphaBoundary multi-region).

Generated by Claude Code

claude added 25 commits August 19, 2026 11:49
…discarding them

Fixes #43. Both bydensity FILTER() call sites now thread isDissimilar/isVISA
into model.prelim/model.sifted alongside the existing unif output, rather than
discarding them via ~. Not consumed by the pipeline itself, kept for later
diagnostic inspection.
…ale subsetting

Fixes #41. Adds opts.pilot.seed/opts.sifted.seed (defaulting from
opts.general.seed, mirroring the existing opts.pythia.seed pattern), and
replaces all five rng('default') call sites with a seeded rng('twister')
using the appropriate seed. Previously these ignored opts.general.seed
entirely, so replication/variance studies with different configured seeds
got identical PILOT/SIFTED results.

Adds a regression test confirming different seeds change PILOT's BFGS
multi-start result, and that the same seed still reproduces bit-identically.
Fixes #36. SECURITY.md documents a private reporting channel (MATILDA
contact page / GitHub private advisories) instead of the public issue
tracker, plus a short scope note. CONTRIBUTING.md points to README's
existing setup instructions rather than duplicating them, describes the
example.m/test_integration.m pre-PR check, and records the repo's existing
code-style conventions (header/licence block, ISA: error identifiers,
centralised opts validation, comment-for-why-not-what). Both linked from
README's Contact section.
…erparameter deferred item

Fixes #33. Records, in RELEASE_NOTES.md's PYTHIA entry and CLAUDE.md's
deferred-items section, that the refactor plan's "verify fitcensemble
OptimizeHyperparameters support" item was resolved by architecture choice
(one shared Sobol/Bayesian tuning layer for every classifier, ensemble
included) rather than by the specific toolbox-compatibility check the plan
named -- so it doesn't get mistakenly re-opened as still pending.
…t normalized Y

Fixes #42. retrainLibsvmPythia was the only PYTHIA call site passing
model.data.Y (normalized) instead of model.data.Yraw, unlike both production
call sites in InstanceSpace.m. Avg_Perf_all_instances/Std_Perf_all_instances
in the resulting summary are pure functions of the Y passed in, so this
produced a visibly wrong (normalized-scale) summary table for any model
migrated through this path, not just wrong training data internally.

Adds a regression test comparing a retrained model's summary against a
freshly-built model's summary on the same data.
…n-centred data

Fixes #44. CLOISTER's sign(Xedge(i,j)) ~=/== sign(Xedge(i,k)) contradiction
check only means anything for mean-centred data. PRELIM only mean-centres
when both opts.auto.preproc and opts.norm.flag are true; with either off, a
naturally all-positive feature (counts, sizes) makes the sign check
degenerate for it with no indication anything went wrong. runCloister now
warns (ISA:InstanceSpace:cloisterNotMeanCentred) when either flag is false,
checked at the call site rather than inside CLOISTER.m itself so the
standalone function's signature/API is unchanged.

Adds a regression test confirming the warning fires with opts.norm.flag=false
and does not fire on the default (normalised) path.
Fixes #29. svmpredict.mexw64/svmtrain.mexw64 were precompiled binaries with
no corresponding source in the tree. Removed entirely (option (b) from the
issue) rather than kept-with-provenance, since LIBSVM is already fully
deprecated for new runs and ISAmigrateModel prefers retraining from scratch
whenever the original training data is available.

PYTHIA's eval mode now raises a clear ISA:PYTHIA:noLibsvm error, naming the
algorithm and pointing to the official LIBSVM project
(https://www.csie.ntu.edu.tw/~cjlin/libsvm/), instead of MATLAB's generic
undefined-function error, if it ever needs to dispatch to a legacy
LIBSVM-format classifier struct with svmpredict unavailable. ISAmigrateModel's
cannotRetrainPythia warning updated to match. Updated README/CLAUDE.md/
liveDemoIS.m/the matlab-toolkit skill reference, which all previously implied
the MEX-files were bundled in-repo.

Adds a regression test confirming the new error fires for a legacy LIBSVM
struct in eval mode.
Fixes #34. .github/workflows/tests.yml runs on every push (all branches)
and on pull requests targeting master, via matlab-actions/setup-matlab
(release: latest) + matlab-actions/run-command. Public-repo licensing
covers all four required toolboxes (Global Optimization, Optimization,
Parallel Computing, Statistics and Machine Learning) automatically, no
secrets needed.

example.m and test_integration.m both already abort with a nonzero exit on
any uncaught error (test_integration.m explicitly raises
ISA:test_integration:caseFailures on any case failure), so run-command's
exit-code-based failure detection needs no extra pass/fail parsing.
test/data/ outputs are uploaded as a workflow artifact on failure only, to
help debug without bloating storage on every green run.

Adds a Tests badge to README.
The first CI run (#34) failed immediately in PRELIM: "Unrecognized function
or variable 'boxcox'. boxcox requires Financial Toolbox." README/liveDemoIS.m
previously claimed the Financial Toolbox was NOT required -- wrong, and now
corrected with a live MATLAB R2026a run as evidence rather than assumption.

This also corrects part of the earlier #45 investigation, which guessed
boxcox() resolved to the Statistics and Machine Learning Toolbox.
…best counts match

Fixes #28. checkPrereq only confirmed the prerequisite STAGE completed, not
that the specific obj.model fields that stage dereferences are actually
present. Adds StageRequiredFields (a dotted-path field list per stage,
deliberately not a full dependency-injection system) and
checkRequiredFields, checked before dispatch in build()'s stage loop, so a
missing field on a hand-edited model.mat, an incomplete legacy migration, or
a model assembled outside the normal build() flow raises a clear
ISA:InstanceSpace:missingField naming the stage and field, instead of an
opaque crash deep inside PRELIM/SIFTED/PILOT/etc. No change to any
successful-path behaviour.

Also resolves the finding folded into this issue: TRACE.m's eval mode used
ngood/nbest as the real-vs-placeholder boundary for out.good/out.best
without ever checking they agree -- an implicit, never-asserted cross-file
convention (relies on InstanceSpace.m's evaluateTestSet reconciliation and
TRACE's own training-mode sizing staying in sync). Now asserts ngood==nbest
before use, raising ISA:TRACE:goodBestCountMismatch on a real mismatch
instead of silently producing misaligned footprints.

Adds regression tests for both: a corrupted/incomplete model triggering
checkRequiredFields, and a desynced trainedTrace.good/.best triggering the
new TRACE assertion.
…dual mode

Fixes #38, supersedes and closes #25/#37 (their scoped fixes are subsumed
here, not done separately).

Data ingestion previously had two independent, drifted implementations: the
CSV-read + opts.selvars filtering embedded in InstanceSpace.runPrelim
(build time), and a separate inline reimplementation in
InstanceSpace.evaluateTestSet (explore time). core/INIT.m is a new
standalone top-level function following the PYTHIA/TRACE nargin convention
-- INIT(rootdir, opts) trains (reads metadata.csv), INIT(rootdir, opts,
trainedModel) evaluates (reads metadata_test.csv, validates feature
set/order, reconciles algorithm columns). Both runPrelim and evaluateTestSet
now call INIT instead of maintaining their own copy. This half of the
refactor is a pure extraction: each mode's behaviour is preserved exactly
as it was, just no longer duplicated in two places that could silently
drift apart.

Design decision (the open question from #38's own description): PRELIM.m
itself gets the nargin train/eval dual mode, rather than folding its
Ybin/Ybest/P/beta computation into INIT. That computation (including
tie-breaking) doesn't depend on train-vs-eval at all -- it's a pure
function of Y and opts.perf.* -- so it now runs completely unconditionally,
shared automatically between both modes. Only the bound-clipping/Box-Cox+
Z-score normalisation actually differs (fit fresh vs apply trainedPrelim's
already-fit parameters), so only that part is nargin-branched. This is what
concretely closes #37: explore-time tie-breaking used to be a silent,
deterministic sort() with no tie-breaking logic at all, independently
drifted from PRELIM's own random tie-break for instances with more than one
best-performing algorithm. Sharing one code path makes that drift
structurally impossible going forward.

evaluateTestSet's signature changed from (model, datafile) to (model,
rootdir) so it can call INIT the same way runPrelim does; its sole caller
(explore()) already had testRootDir available directly.

Adds a regression test proving PRELIM's train and eval modes now break an
identical synthetic tie identically when seeded the same way -- the direct
evidence that both modes share code rather than merely producing similar
output. The full existing test_integration.m suite already exercises the
whole INIT+PRELIM eval path via exploreIS() on every case, so the extraction
itself is covered by the existing broad suite, not just the new targeted
test.

CLAUDE.md updated: INIT added to the core/ file list, the train/eval
convention note now includes PRELIM/INIT, and the #38 design decision is
recorded so it isn't re-litigated later without new evidence.
Fixes #39. Replaces the hand-rolled script (per-case try/catch, manual
pass/fail bookkeeping) with matlab.unittest.TestCase classes under tests/:

- PipelineOptionsTest: the 20-case option-coverage cell array is now a
  TestParameter (OptionCase), run by one parameterized test method instead
  of a manual loop -- matches the issue's "ideally parameterised, not a
  1:1 mechanical port" preference.
- ClassApiTest: the staged build()/explore()/save-load/invalidation
  sequence, kept as one test method since each step depends on state left
  by the previous one.
- MigrationTest: the full ISAmigrateModel legacy-migration table, split
  into independent test methods where the pre-#39 script's cases already
  were independent; a TestClassSetup builds one shared trained model for
  the LIBSVM-retraining/legacy-TRACE-recompute cases, replacing the old
  script's "reuse obj from class_api if available, else build one"
  workaround with a proper shared fixture.
- RegressionTest: the #41/#44/#28/#37+#38 targeted regressions, each its
  own test method.
- testDefaultOpts.m: the baseline opts struct, shared by all four classes
  (previously duplicated per test-class draft during this migration itself
  -- consolidated before commit rather than propagating four copies).

test_integration.m is now a thin runner: TestSuite.fromFolder('tests') +
TestRunner with CodeCoveragePlugin (Cobertura report, coverage.xml),
preserving the EOF:SUCCESS/EOF:ERROR sentinel exactly (buildIS.m/
exploreIS.m use the same convention independently of this file, per the
issue's own acceptance criteria). No changes needed to
.github/workflows/tests.yml's invocation (still `command: example,
test_integration`) -- only an added always-upload step for coverage.xml.

example.m is untouched, per the issue's explicit scope boundary.

CLAUDE.md's Test harness section updated to describe the new structure.
Run 32301539795 hung for ~2h45m inside setup-matlab's system-dependency
apt-get step (a runner-side network/mirror stall, unrelated to any code
change) before it had to be cancelled manually. Adds timeout-minutes so a
repeat fails fast and visibly instead of silently running toward GitHub's
6-hour default job timeout.
Fixes a real bug in the #39 migration, confirmed directly from CI logs
(run 32315024151): matlab.unittest does not run TestClassSetup/test
methods with the working directory test_integration.m was launched from --
observed as './test/data/...' resolving to 'tests/test/data/...' inside
every TestClassSetup, and PipelineOptionsTest's OptionCase TestParameter
(which reads metadata.csv at class-parse time) failing outright and
excluding the whole class from the suite.

Adds tests/testRepoRoot.m (fileparts(fileparts(mfilename('fullpath'))),
robust to caller cwd by construction) and uses it in all four test
classes instead of the relative literal. test_integration.m itself is
unaffected -- its own top-level code runs before TestSuite.fromFolder
shifts the working directory, matching example.m's already-working
relative-path usage in the same MATLAB session.
…test execution

Fixes the second #39 bug confirmed by CI (run 32316118276), one layer under
the path bug fixed in 6250a0f: buildIS.m/exploreIS.m/InstanceSpace.m live at
the repo root and were never formally on the MATLAB search path -- they only
resolved via MATLAB's implicit current-folder lookup, which broke the moment
matlab.unittest shifted the working directory away from the repo root during
test execution ("Undefined function 'buildIS'"/"'InstanceSpace' for input
arguments of type 'char'/'struct'"). core/output/utils were unaffected since
InstanceSpace.ensurePathSetup already puts them on the path explicitly,
independent of cwd.

test_integration.m now captures pwd (repo root, confirmed by example.m's
already-working relative-path usage earlier in the same MATLAB session) and
addpath()s it before the test suite runs, once, for the rest of the session.
Replaces the version-specific badge/DOI (10.5281/zenodo.4750845) with the
repo-linked badge (zenodo.org/badge/144672744.svg) pointing at
10.5281/zenodo.4484107, matching the DOI already used in the citation text
further down the file -- the two had pointed at different DOIs.
…ndary display

Fixes #31 and #32 (option (a): 2D-only, deferring CLOISTER's own 2D-only
convex hull to a separate follow-up -- see CLAUDE.md).

#31: traceAlphaBoundary (moved from scriptcsv.m into scriptfcn.m, for
testability alongside the other shared plotting helpers) used to build one
adjacency graph from boundaryFacets(poly)'s combined edge list -- every
disconnected region's edges flattened together -- and stop as soon as it ran
out of same-loop neighbours, silently returning only the first region traced
for a multi-region alpha shape. It now traces each boundaryFacets(poly,
regionID) region independently and concatenates them, NaN-separated (the
same multi-part-polyline convention polyshape.Vertices already uses).
Single-region shapes (the common case) are unaffected: no separator rows,
identical output to before. The Python-derived "retry with a re-optimised
alpha" pattern the issue proposed turned out not to apply here: scriptfcn.m's
plot() already renders multi-region alphaShapes correctly via MATLAB's
native plotting, so the bug was specifically in this hand-rolled CSV vertex
tracer, not in TRACE's alpha selection -- changing the alpha would have
changed the exported footprint's actual geometry to work around an
export-format limitation, and drifted from what the PNG output shows.

#32: CLOISTER's boundary was computed (model.cloist.Zedge/Zecorr) but never
rendered in automated output or exposed via plot(). Adds drawBoundary
(scriptfcn.m: grey instance scatter + red boundary outline), a
distribution_boundary.png in scriptpng.m (skipped when CLOISTER wasn't run,
e.g. an evaluateTestSet result, or when opts.pilot.dims==3), and an
InstanceSpace.plot('boundary') case mirroring the other views.
liveDemoIS.m's manual boundary-plotting cell now just calls
obj.plot('boundary'). Scoped to 2D only (option (a)): CLOISTER's own
Zedge/Zecorr computation is a 2D-only convex hull regardless of projection
dimensionality (core/CLOISTER.m's convhull(Z(:,1),Z(:,2))), a separate,
deliberately out-of-scope bug recorded in CLAUDE.md rather than fixed here,
since changing what CLOISTER computes is an algorithmic change outside
v0.9.1's stated scope. Both the plot() case and the PNG explicitly refuse a
3D projection (ISA:InstanceSpace:boundaryNot3D) rather than render an
inaccurate boundary.

Adds regression tests: a genuine two-cluster multi-region alphaShape
confirming traceAlphaBoundary now returns both regions (#31), and 2D
boundary-display + 3D-rejection coverage (#32).
Zenodo's GitHub integration failed to archive the v0.9.0 release
("Citation metadata load failed"): Zenodo validates CITATION.cff's license
field strictly against the official SPDX License List, and
LicenseRef-PolyForm-Noncommercial-1.0.0 is not on it (PolyForm
Noncommercial 1.0.0 is not an OSI-approved/SPDX-registered license). The
CFF spec's own documented mechanism for a non-SPDX license is license-url
instead of license, pointing at the licence text -- matching what every
other file header in this repo already cites.

Note for the next release: this only fixes future archival attempts: the
v0.9.0 release already failed to archive on Zenodo and will need a new
release (or Zenodo's "Sync now" re-request, if available) to pick this up.
…, #27)

build('onStage', @(stageName, model) ...) fires after each stage completes,
letting callers inspect intermediate results (e.g. PILOT's projection)
without manually breaking a run into several staged calls. explore(testRootDir,
'onStage', @(stageName, out) ...) does the same for the five conceptual
stages evaluateTestSet runs internally (prelim, sifted, pilot, pythia,
trace -- cloister is never recomputed at explore time). Both are purely
additive: omitting the callback changes nothing.
#30 (PILOT orientation canonicalisation) proposes a new algorithmic step and
opts toggle, out of v0.9.1's engineering-only scope -- deferred, comment
left on the issue. #35 (Python-port fixture export tool) closed as not
planned: cross-repo Python-port work belongs in that repo, not here. #45
(verify PRELIM's boxcox() call) closed as resolved -- confirmed it resolves
to the Financial Toolbox's boxcox.m, the same dependency CI already
installs for this call site, with no gap found alongside the existing
positive-shift guard.
#25-29, #31-34, #36, #38, #39, #41-44 closed with comments citing the
commit that fixed each. Milestone is now effectively down to the
not-yet-filed CLOISTER-3D-hull item and #30 (deferred to v0.9.2).
Was flagged as "not yet filed" since #32; user caught that it still wasn't.
Filed on GitHub, not milestone-assigned since it's an algorithmic change
out of v0.9.1 scope, same as #30.
Adds a new v0.9.1 section to RELEASE_NOTES.md (new functionality, better
engineering, bug fixes) covering everything since the v0.9.0 tag, without
touching the existing v0.9.0 section. Bumps Contents.m/CITATION.cff to
0.9.1. Updates README.md: onStage callback example, opts.pilot.seed/
opts.sifted.seed, opts.perf.epsilon's AbsPerf-dependent range, CLOISTER's
boundary-display output and non-mean-centred-data warning, and corrects
the test_integration.m/options.json section to describe the current
matlab.unittest-based tests/*.m structure instead of the pre-#39 script.
Same set of corrections applied to the Claude Code skill's
matlab-toolkit.md reference, plus its verified-against version bump.
Root cause of the CI failure on 905acdd: the test referenced
InstanceSpace.StageOrder directly from outside the class, which is
Access = private -- an access-denied runtime error (matlab.unittest
reports this as "Errored", not "Failed"), not a genuine test-coverage
gap. build()/explore() themselves are unaffected; only the test needed
its own hardcoded expected stage order, matching how the explore() half
of the same test already did it.
The previous version only exercised onStage's "run everything, inspect
along the way" mode (build() with no 'stages' filter), which is why it
needed to hardcode the full canonical stage list. It never exercised
onStage combined with build()'s existing 'stages' subset flexibility --
the arguably more useful combination, and the one #26's own docstring
example implies. Added a case that requests {'prelim','sifted','pilot'}
with onStage and asserts the callback fires only for that subset, in
that order -- no hardcoded full list needed, since the expectation is
just "echo back what was requested".
Copilot AI lite review requested due to automatic review settings August 20, 2026 09:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR lands the v0.9.1 milestone work on master, focusing on engineering-quality and infrastructure follow-ups to v0.9.0 (with explicit scope constraints: no algorithmic changes and no new user-facing options). It adds CI, modernizes the MATLAB test harness, consolidates duplicated ingestion logic into a shared train/eval entry point, and includes several targeted correctness and ergonomics improvements across the pipeline.

Changes:

  • Adds GitHub Actions CI and migrates the regression suite to matlab.unittest with code coverage output (coverage.xml).
  • Introduces shared train/eval ingestion (core/INIT.m) and extends PRELIM to a nargin-dispatched eval mode; adds per-stage onStage callbacks and required-field contract checks.
  • Fixes several confirmed regressions/edge cases (seed threading, LIBSVM migration retraining data scale, TRACE eval invariant checks) and updates documentation/release metadata for v0.9.1.

Reviewed changes

Copilot reviewed 31 out of 33 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
utils/ISAvalidateOpts.m Validates newly introduced pilot.seed / sifted.seed options.
utils/ISAmigrateModel.m Fixes LIBSVM-era retraining path to use Yraw; improves warning guidance.
utils/ISAdefaults.m Adds defaults for opts.pilot.seed and opts.sifted.seed.
core/INIT.m New shared ingestion function for build/explore paths (train/eval by nargin).
core/PRELIM.m Adds eval mode to apply trained preprocessing while sharing tie-breaking logic.
InstanceSpace.m Adds onStage callbacks, field-level contract checks, boundary plotting, INIT-based ingestion, and seed threading for small-scale subsetting.
core/PILOT.m Uses opts.seed instead of rng('default') for BFGS restarts.
core/PILOTviewpoint.m Uses opts.seed for viewpoint optimization restarts.
core/SIFTED.m Uses opts.seed instead of rng('default') for k-means/cluster steps.
core/TRACE.m Adds explicit eval-mode invariant guard for good/best count mismatch.
core/PYTHIA.m Adds explicit error when legacy LIBSVM struct is present but svmpredict is unavailable.
output/scriptpng.m Adds automated CLOISTER boundary PNG output for 2D projections.
output/scriptfcn.m Centralizes boundary drawing and alpha-shape boundary tracing utilities (multi-region fix).
output/scriptcsv.m Uses centralized boundary extraction; documents NaN-separated multi-region boundaries.
liveDemoIS.m Switches manual boundary plotting to obj.plot('boundary'); updates toolbox requirements text.
test_integration.m Becomes a matlab.unittest runner wiring in Cobertura coverage and preserving EOF sentinels.
tests/testRepoRoot.m Adds robust repo-root discovery for tests independent of cwd.
tests/testDefaultOpts.m Shared baseline opts for test suite.
tests/PipelineOptionsTest.m Parameterized option-surface coverage via matlab.unittest.
tests/ClassApiTest.m Class API staged build/save/load/explore coverage, plus onStage callback tests.
tests/MigrationTest.m Migration-table coverage + LIBSVM/Yraw regressions in matlab.unittest.
tests/RegressionTest.m Targeted regression tests for fixed issues (#28/#31/#32/#37/#38/#41/#44 etc.).
.github/workflows/tests.yml Adds CI workflow running example.m + test_integration.m and uploading artifacts.
README.md Adds CI badge; updates dependencies (Financial toolbox for boxcox), docs for tests and options, boundary, and LIBSVM status.
RELEASE_NOTES.md Adds v0.9.1 release notes section and preserves existing v0.9.0 notes.
SECURITY.md Adds security policy document.
CONTRIBUTING.md Adds contributor guide.
Contents.m Bumps version/date to 0.9.1.
CITATION.cff Bumps version/date; switches to license-url for non-SPDX license.
CLAUDE.md Updates repository operational notes consistent with v0.9.1 changes.
.claude/skills/instance-space-analysis/references/matlab-toolkit.md Updates skill reference doc for v0.9.1 defaults and behaviors.
Suppressed comments (1)

SECURITY.md:41

  • This bullet says the LIBSVM MEX binaries are bundled, but the PR removes them. Update this to avoid implying the repo ships binary artifacts.
- The bundled LIBSVM MEX binaries, used only for migrating pre-v1.7
  legacy models (see #29 for their provenance status).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread CONTRIBUTING.md Outdated
Comment thread SECURITY.md
Comment thread CONTRIBUTING.md Outdated
Comment thread core/INIT.m
Comment thread core/INIT.m
Comment thread test_integration.m Outdated
- CONTRIBUTING.md: CI now exists (#34), so drop the "no CI yet" note;
  PRELIM/INIT also implement the train/eval dual-mode convention now
  (#38), not just PYTHIA/TRACE.
- SECURITY.md: LIBSVM's MEX files are no longer bundled (#29, removed),
  not merely a bundled binary to be wary of -- reworded both mentions.
- core/INIT.m: revert featureCountMismatch/featureOrderMismatch back to
  their pre-#38 ISA:InstanceSpace:* identifiers. They'd drifted to
  ISA:INIT:* when this validation moved out of InstanceSpace.m into the
  new INIT.m, silently breaking any caller matching on the old
  identifier (this validation is reachable through explore(), a public
  entry point MATILDA's web platform calls).
- test_integration.m: derive repoRoot from mfilename('fullpath') instead
  of pwd, so it doesn't add the wrong directory to the path if launched
  from somewhere other than the repo root.
@andremun
andremun requested a balanced review from Copilot August 20, 2026 10:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 33 changed files in this pull request and generated no new comments.

Suppressed comments (16)

core/SIFTED.m:163

  • The configured SIFTED seed still does not control the CV partition or genetic algorithm: line 165 restores the caller RNG before both are created. This also leaves standalone SIFTED(...) calls without a locally defaulted seed failing on line 145. Keep the seeded state through GA completion with an onCleanup guard and provide the same standalone default used by PYTHIA.
rng(opts.seed, 'twister');
out.clust = bsxfun(@eq, kmeans(Xaux', opts.K, 'Distance', 'correlation', ...
                                              'MaxIter', opts.MaxIter, ...
                                              'Replicates', opts.Replicates, ...
                                              'Options', statset('UseParallel', nworkers~=0), ...

README.md:190

  • The migration path now explicitly requires model.data.Yraw, but this updated README still tells users that model.data.Y is sufficient. A model with only normalized Y will take the warning/skip path, so document the actual required field.
The toolkit selects one binary classifier per algorithm (good/not-good performance) from a registry of MATLAB-native classifiers, resolved via `ISAgetClassifierFcn`. It runs after PILOT, on the projected instance space, and does not depend on CLOISTER. **LIBSVM is deprecated for new runs**: `buildIS` never dispatches to it. `ISAmigrateModel` (see below) renames legacy field names on an old model (e.g. `.svm`/`.knn` → `.classifiers`), and additionally **retrains** any classifiers still in the legacy LIBSVM struct format (no `predict()` method, so they can't just be relabelled) using the current registry — `opts.pythia.classifier` if already set to a valid registry name, `'knn'` otherwise — provided the model still has `model.pilot.Z` and `model.data.Y`/`Ybin`/`Ybest`/`algolabels` to retrain from. If those fields are missing, migration proceeds but leaves the legacy classifiers in place with a warning; such a model can still be evaluated through `exploreIS`/`PYTHIA` eval mode (which dispatches to `svmpredict` when it detects a struct instead of a MATLAB classifier object), but this requires the LIBSVM MEX-files, which are **not bundled with this repository** (see the Installation Instructions section above) — obtain them from [the official LIBSVM project](https://www.csie.ntu.edu.tw/~cjlin/libsvm/) if you actually need to go down this path. Prefer retraining from scratch with `buildIS`, or a migration with the full training data available, to drop that dependency entirely.

.claude/skills/instance-space-analysis/references/matlab-toolkit.md:154

  • The operational reference uses prelim.norm, but users configure normalization through opts.norm.flag; prelim.norm is only an internal flattened field passed to PRELIM. Update this public-facing option name.
  `prelim.norm=false`, a warning (`ISA:InstanceSpace:cloisterNotMeanCentred`,

core/PILOT.m:206

  • This makes seed mandatory for direct PILOT(...) callers, even though this standalone API previously worked with the documented stage options and locally defaults other newly optional fields. ISAdefaults only protects calls routed through InstanceSpace; add a local seed fallback so existing standalone numerical calls do not fail with “Unrecognized field name 'seed'.”
            rng(opts.seed, 'twister');

core/PRELIM.m:207

  • This error used to be raised by public InstanceSpace.explore() as ISA:InstanceSpace:complexX. Moving the check into PRELIM and changing its identifier breaks callers that catch the established identifier, just like the feature-count/order identifiers already preserved in INIT. Keep the original identifier for this eval-only path.
    if ~isreal(X)
        error('ISA:PRELIM:complexX', ...
            'Feature matrix X is complex after normalisation. Check test data range vs training data.');

test_integration.m:75

  • The runner is still cwd-dependent despite deriving repoRoot: suite discovery, test-data creation, coverage source folders, and the report path remain relative. Calling this script from another directory therefore searches that caller’s tests/ (or fails) instead of this repository. Build all these paths from repoRoot.
suite = TestSuite.fromFolder('tests', 'IncludingSubfolders', true);

core/PILOTviewpoint.m:124

  • Direct PILOTviewpoint(...) calls are now required to supply a new opts.seed field, while this function locally defaults its other optional fields and does not run ISAdefaults. Preserve the standalone API by defaulting the seed locally when absent.
        rng(opts.seed, 'twister');

core/PRELIM.m:163

  • Moving explore-time clipping into PRELIM dropped the existing ISA:InstanceSpace:outOfDistribution warning that fired when more than 5% of test instances were clipped. That is a user-visible regression in explore() and removes the only indication that test data may lie outside the training distribution; retain the warning before applying the masks.
        himask = bsxfun(@gt, X, trainedPrelim.hibound);
        lomask = bsxfun(@lt, X, trainedPrelim.lobound);
        X = X.*~(himask | lomask) + bsxfun(@times, himask, trainedPrelim.hibound) + ...
                                    bsxfun(@times, lomask, trainedPrelim.lobound);

README.md:186

  • opts.prelim.norm is not a public option in this repository; the implemented warning checks opts.norm.flag. As written, users following this new guidance will set an ignored field and receive no warning. Use the actual option name.

This issue also appears on line 190 of the same file.

CLOISTER's correlation-contradiction filter assumes mean-centred feature data (its sign-based check is only meaningful when values span both positive and negative territory); if ```opts.prelim.norm = false```, a warning (```ISA:InstanceSpace:cloisterNotMeanCentred```) is raised, since a naturally all-positive feature would otherwise silently make the check degenerate for it.

RELEASE_NOTES.md:44

  • This release note names opts.prelim.norm, but the supported full options structure and the new guard use opts.norm.flag. Correcting the field name is necessary for the documented reproduction to work.
- **CLOISTER's correlation-contradiction filter silently degraded under non-mean-centred data.** Its sign-based contradiction check (`sign(Xedge(i,j)) ~=/== sign(Xedge(i,k))`) is only meaningful when feature values span both positive and negative territory — true by default (CLOISTER receives Box-Cox+Z-scored data), but not when `opts.prelim.norm=false`, a legitimate documented option. A naturally all-positive feature (counts, sizes) under that setting made the check degenerate with no indication anything was off. `InstanceSpace.m` now warns (`ISA:InstanceSpace:cloisterNotMeanCentred`) when this precondition is violated.

.claude/skills/instance-space-analysis/references/matlab-toolkit.md:6

  • The v0.9.1 documentation now correctly declares Financial Toolbox as required for boxcox(), but this operational reference’s updated prerequisite sentence still omits it. Add Financial Toolbox so the reference does not send users to an incomplete environment setup.

This issue also appears on line 154 of the same file.

v0.9.1). Requires **MATLAB R2025a or later**, with the Global
Optimization, Parallel Computing, Optimization, and Statistics and
Machine Learning toolboxes. Re-verify against the live repo if a detail

.github/workflows/tests.yml:23

  • CI currently tests latest, but the release and README promise support from MATLAB R2025a onward. Once latest advances, a green build no longer validates the minimum supported release and can also drift unexpectedly. Pin this job to R2025a (or add an R2025a matrix entry).
          release: latest

tests/MigrationTest.m:149

  • This regression test fails in a valid documented environment where a contributor has installed official LIBSVM: PYTHIA then calls svmpredict instead of raising ISA:PYTHIA:noLibsvm. Skip the negative-path assertion when the optional MEX-file is present so installing the supported external dependency does not break the suite.
            testCase.verifyError(@() PYTHIA(baseModel.pilot.Z, baseModel.data.Yraw, baseModel.data.Ybin, ...

RELEASE_NOTES.md:3

  • The release claims there are no new user-facing options, but this PR adds, validates, and documents opts.pilot.seed and opts.sifted.seed as independently configurable options. Reword the scope statement so it does not contradict the shipped API.
This release is an engineering-quality, architecture, and infrastructure follow-up to v0.9.0 — deliberately scoped to **no algorithmic changes and no new user-facing options** (see the `v0.9.1` GitHub milestone). What did land: two small, additive API surfaces (a per-stage inspection callback, and finally rendering a boundary CLOISTER had computed all along), a real architectural clean-up of data ingestion, and a batch of confirmed correctness fixes found via a full-repository audit against the project's own established conventions. It targets **MATLAB R2025a or later**.

InstanceSpace.m:369

  • This message points users to closed issue #32, while the unresolved 3D boundary work is explicitly tracked as #50 throughout this PR. Point to #50 so the actionable follow-up is discoverable.
                        error('ISA:InstanceSpace:boundaryNot3D', ...
                            ['The ''boundary'' view is 2D only: CLOISTER''s empirical bound is not ' ...
                             'yet computed for 3D projections (opts.pilot.dims==3). See issue #32.']);

RELEASE_NOTES.md:49

  • The previous v0.9.0 heading was replaced by the new v0.9.1 heading, but no heading was inserted before the retained v0.9.0 content below. As a result, the historical v0.9.0 changelog now appears to be part of v0.9.1. Restore its section heading after this separator.
---

…dings)

Fixed critical error first: my earlier RELEASE_NOTES.md edit had deleted
the "# ... v0.9.0" heading itself (used as the old_string anchor), making
the historical v0.9.0 changelog read as part of v0.9.1 -- restored.

Real correctness fixes:
- SIFTED.m: the seeded RNG state was restored right after kmeans, before
  cvpartition/ga() ran -- both silently ran under the caller's original
  RNG instead of opts.seed. Now held through GA completion via an
  onCleanup guard (PYTHIA's established pattern), not a manual restore.
- PILOT.m/SIFTED.m/PILOTviewpoint.m: opts.seed is now unconditionally
  read, but only ISAdefaults (i.e. calls routed through InstanceSpace)
  provides it -- a standalone call, which CLAUDE.md requires stay
  supported, would error with "Unrecognized field name 'seed'". Added
  the same local `if ~isfield(opts,'seed') opts.seed = 42; end` default
  PYTHIA already uses.
- PRELIM.m: restored two identifiers/behaviour dropped when this logic
  moved out of InstanceSpace.m -- ISA:PRELIM:complexX reverted to
  ISA:InstanceSpace:complexX (same backward-compat reasoning as the
  INIT.m identifiers fixed in the previous review round), and the
  >5%-clipped ISA:InstanceSpace:outOfDistribution warning restored to
  PRELIM's eval-mode bound-clipping (silently dropped, not carried over).
- test_integration.m: mkdir/TestSuite.fromFolder/coverage source
  folders/report path were still pwd-relative even after the previous
  round's repoRoot fix; now all built from repoRoot.
- .github/workflows/tests.yml: pinned release: R2025a instead of
  'latest', so CI keeps validating the documented minimum instead of
  silently drifting as new MATLAB versions ship.
- tests/MigrationTest.m: guarded testNoLibsvmRegression with
  assumeTrue(exist('svmpredict','file')==0) -- a contributor with LIBSVM
  actually installed would otherwise get a spurious failure.

Doc accuracy fixes: opts.prelim.norm corrected to the real
opts.auto.preproc/opts.norm.flag fields (README.md, RELEASE_NOTES.md,
matlab-toolkit.md -- opts.prelim.norm isn't a real field); README's
LIBSVM-migration paragraph corrected from model.data.Y to the actually-
required model.data.Yraw; matlab-toolkit.md's prerequisite sentence was
missing the Financial Toolbox; RELEASE_NOTES.md's "no new user-facing
options" claim softened to acknowledge opts.pilot.seed/opts.sifted.seed
exist (as a side effect of making the already-documented
opts.general.seed behave as promised, not a new capability); the
boundaryNot3D error pointed at closed issue #32 instead of the actual
open follow-up, #50.

Copy link
Copy Markdown
Owner Author

Addressed the second Copilot review round (16 suppressed-confidence findings) in a3aa2d4. All 16 checked directly against source before acting — full breakdown in the commit message. Highlights:

  • Caught a real self-inflicted bug first: my own earlier RELEASE_NOTES.md edit had deleted the "v0.9.0" heading itself, making the historical changelog read as if it were part of v0.9.1. Restored.
  • Genuine correctness fix: SIFTED.m was restoring the RNG right after kmeans, before cvpartition/ga() ran — both were silently running under the caller's original RNG instead of opts.seed. Now held through GA completion via an onCleanup guard, matching PYTHIA's existing pattern.
  • Standalone-callability regression: PILOT.m/SIFTED.m/PILOTviewpoint.m now unconditionally read opts.seed, but only ISAdefaults provides it — a direct standalone call (which CLAUDE.md requires stay supported) would've errored. Added the same local default PYTHIA.m already uses.
  • Two more backward-compat identifier reverts in PRELIM.m (ISA:PRELIM:complexXISA:InstanceSpace:complexX), plus a restored ISA:InstanceSpace:outOfDistribution warning that had been silently dropped when this logic moved out of InstanceSpace.m.
  • Documentation accuracy: opts.prelim.norm (not a real field) corrected to the actual opts.auto.preproc/opts.norm.flag gate across three files; README's LIBSVM-migration paragraph corrected from model.data.Y to the actually-required model.data.Yraw; a stale #32 issue reference pointed at the real open follow-up, #50.
  • CI robustness: pinned release: R2025a instead of latest; guarded MigrationTest.testNoLibsvmRegression against a contributor who actually has LIBSVM installed.

CI is running on a3aa2d4 now.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 33 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

InstanceSpace.m:832

  • PRELIM's shared tie-breaking now consumes randi, but explore() never seeds or restores the RNG. Consequently, evaluating the same model and test data twice can choose different P values—and therefore different TRACE best rescoring—based on unrelated prior RNG use. Seed this call from the frozen model seed while restoring the caller's RNG state.
            [out.data.X, out.data.Y, prelimOut] = PRELIM(out.data.X, out.data.Y, prelimOpts, model.prelim);

output/scriptfcn.m:501

  • A single alpha-shape region can contain multiple boundary cycles (for example, an outer ring plus a hole). In that case boundaryFacets(poly, r) returns disconnected edge loops, but this helper follows only the loop containing bf(1,1) (and then repeats it), silently omitting hole boundaries from the CSV. Trace every unvisited edge cycle within each region and NaN-separate those cycles too.
% correctly for a simple, single connected boundary -- traceAlphaBoundary
% calls this once per region rather than once for a whole (possibly
% multi-region) shape, which is what makes that assumption safe again.

.claude/skills/instance-space-analysis/references/matlab-toolkit.md:278

  • This operational reference says both paths raise boundaryNot3D, but scriptpng.m simply skips its boundary block when is3D; only InstanceSpace.plot('boundary') raises that error. Document the silent PNG omission separately so users do not expect a 3D build to fail.
other views. Both explicitly raise `ISA:InstanceSpace:boundaryNot3D`

tests/PipelineOptionsTest.m:17

  • The generated options are based on testDefaultOpts(), not a baseOpts() function in this file. This instruction currently sends contributors to a nonexistent helper.
% next run overwrites it. To change what gets tested, edit baseOpts()
% (this file) or the relevant OptionCase entry.

InstanceSpace.m:111

  • The SIFTED stage unconditionally dereferences obj.model.featsel.idx when updating the selected feature mapping, but this contract omits it. A reconstructed model can pass the new validation and still fail opaquely inside runSifted, which defeats the purpose of the field-level guard.

This issue also appears on line 832 of the same file.

            'sifted',   {{'data.X', 'data.Y', 'data.Ybin', 'data.featlabels'}}, ...

RELEASE_NOTES.md:19

  • Only plot('boundary') raises ISA:InstanceSpace:boundaryNot3D; scriptpng.m silently skips distribution_boundary.png for a 3D model. The release note should distinguish these behaviors rather than claiming both emit the error.
**CLOISTER boundary is now actually rendered, for 2D projections.** CLOISTER has always computed an empirical bound (`model.cloist.Zedge`/`Zecorr`) for the instance space, but nothing in the automated pipeline ever drew it — a three-year-old deferred item from the original refactor plan's design review that Phase 9 was supposed to close and didn't. Now: `scriptpng.m` writes a `distribution_boundary.png`, and `InstanceSpace.plot('boundary')` works like the other views. Both explicitly refuse a 3D projection (`ISA:InstanceSpace:boundaryNot3D`) rather than render a boundary that would silently be wrong in the third dimension — CLOISTER's own hull computation is still 2D-only regardless of projection dimensionality, tracked separately as a genuine algorithmic follow-up ([#50](https://github.com/andremun/InstanceSpace/issues/50), out of this release's scope).

Comment thread core/PRELIM.m
Real correctness fixes:
- InstanceSpace.m: runPrelim pruned data.Yraw/Y/Ybin/algolabels when an
  algorithm has no good instances, but left prelimOut.lambdaY/muY/sigmaY
  (per-algorithm, fit before pruning) untouched. PRELIM's eval mode
  derives modelalgos from numel(trainedPrelim.lambdaY), so this both
  over-counted modelalgos against the actually-reconciled eval-time Y
  (indexing past it, or misapplying a pruned algorithm's transform to an
  unrelated new algorithm's column) and, for any pruned algorithm that
  wasn't last, silently misaligned every surviving lambda/mu/sigma
  positioned after it. Now pruned with the same mask as data.algolabels.
  Regression test added directly against PRELIM (forcing real pruning
  through the full pipeline needs data engineered so one algorithm is
  never "good," which is fragile against the bundled reference dataset;
  simulating the post-prune trainedPrelim state is equivalent and
  deterministic), pruning the middle algorithm specifically since that's
  what exposes the positional-misalignment half of the bug.
- InstanceSpace.m: explore() shares PRELIM's tie-breaking code with
  build() (per #37/#38), including its randi() draw for ties, but never
  seeded the RNG before calling PRELIM -- two explore() calls on the
  identical model and test data could pick different tied algorithms
  depending on unrelated prior RNG use. Seeded from the frozen model's
  own opts.general.seed, restored via onCleanup (PYTHIA/SIFTED's
  existing pattern).
- InstanceSpace.m: StageRequiredFields for 'sifted' omitted featsel.idx,
  which runSifted() unconditionally read-then-writes
  (obj.model.featsel.idx = obj.model.featsel.idx(...)) -- a genuine
  prerequisite the #28 contract-validation work had missed. A model
  missing it crashed opaquely inside runSifted instead of via the
  intended clear error. Added to the contract; regression test extended.
- output/scriptfcn.m: traceOneRegion assumes a region's boundary is one
  closed cycle, but a region with a hole (numRegions counts connected
  point-set components, not boundary cycles) has two disconnected
  cycles -- outer ring and hole -- and the tracer only ever reaches the
  one containing its arbitrary starting vertex. Rather than attempt full
  multi-cycle tracing here (a real algorithmic addition, out of v0.9.1
  scope), it now warns (ISA:scriptfcn:boundaryHoleOmitted) instead of
  silently dropping the hole. Filed as #52 for the actual fix, tracked
  in CLAUDE.md alongside #50.

Doc accuracy fixes: RELEASE_NOTES.md/matlab-toolkit.md/CLAUDE.md
corrected -- scriptpng.m silently skips distribution_boundary.png for a
3D projection (no error, no warning); only plot('boundary') raises
ISA:InstanceSpace:boundaryNot3D. Previously described as if both
"explicitly refuse" 3D. tests/PipelineOptionsTest.m's stale baseOpts()
references (a function that doesn't exist in this file) corrected to
testDefaultOpts(), the actual shared-settings function.

Copy link
Copy Markdown
Owner Author

Addressed the third review round's 6 suppressed-confidence findings in 28223c9, alongside the posted core/PRELIM.m:176 finding (replied on that thread). All checked directly against source before acting:

  • explore()'s tie-break wasn't seeded: PRELIM's shared tie-breaking (randi) is reachable from explore() too, but nothing there seeded the RNG — two explore() calls on the identical model and test data could pick different tied algorithms depending on unrelated prior RNG use. Seeded from the frozen model's opts.general.seed, restored via onCleanup.
  • 'sifted''s field contract was missing featsel.idx: runSifted() unconditionally read-then-writes obj.model.featsel.idx, a genuine prerequisite the #28 contract validation had omitted. Added, with a regression test.
  • traceOneRegion can't handle a hole: a region with a hole (e.g. an annulus) has two disconnected boundary cycles under one numRegions count; the tracer only reaches the cycle it started on. Full multi-cycle tracing is a real algorithmic addition, so — same treatment as #50 — filed as #52 and made the failure visible (ISA:scriptfcn:boundaryHoleOmitted warning) instead of silent, rather than attempting the fix here.
  • Doc accuracy: RELEASE_NOTES.md/matlab-toolkit.md/CLAUDE.md all said scriptpng.m and plot('boundary') both "explicitly refuse" a 3D projection — actually scriptpng.m silently skips the file (no error, no warning), only plot('boundary') raises ISA:InstanceSpace:boundaryNot3D. Corrected across all three. tests/PipelineOptionsTest.m's comments referenced a nonexistent baseOpts(); corrected to the real testDefaultOpts().

CI is running on 28223c9 now.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 33 changed files in this pull request and generated no new comments.

Suppressed comments (1)

output/scriptfcn.m:525

  • The hole safeguard does not trigger as written. After the first boundary cycle closes, this traversal can revisit its starting vertex and keep repeating that cycle until order is full, so valid is all true even though the disconnected hole cycle was omitted. Exclude already visited neighbours so the loop breaks and the warning below can report the omitted vertices.
    nxt = adj(curr, adj(curr,:) ~= prev & adj(curr,:) ~= 0);

Copilot review round 4 on #51. traceOneRegion's walk only stopped on
isempty(nxt) (a dead end) -- it never checked for returning to the start
vertex. For a region with a hole (two disconnected boundary cycles),
excluding just `prev` still leaves a "next" vertex available once back
at the start, so the walk didn't halt when the outer cycle closed; it
just kept re-treading the same cycle's vertices until `order` filled up
completely. That left `valid` all true and the
ISA:scriptfcn:boundaryHoleOmitted warning added in the previous review
round -- specifically for this case -- silently never firing, with
duplicated outer-ring vertices in the output instead of the intended
"outer ring only, flagged" result.

Added a stop condition for nxt(1) == order(1) (back at the start).
No-op for the ordinary single-cycle case: the for loop's own range
(nRegionVerts) already ends before a closed cycle shorter than that
could ever reach this check, so nothing changes there.

Exposed traceOneRegion via scriptfcn's existing assignin mechanism
(matching footprintBoundary/traceAlphaBoundary/drawBoundary) and added a
regression test against a synthetic two-cycle edge list (a 4-vertex
outer square plus a disconnected 3-vertex inner triangle) rather than a
geometric alphaShape, for full control over the exact adjacency instead
of depending on alphaShape's own region/facet algorithm to reliably
reproduce a specific hole shape.

Copy link
Copy Markdown
Owner Author

Addressed the fourth review round's finding in 4c31755 — a real bug in my own previous fix, confirmed by tracing through the logic by hand:

traceOneRegion's walk only stopped on isempty(nxt) (a dead end), never on returning to its start vertex. For a region with a hole, once the outer cycle closed, excluding just prev still left a "next" vertex available (the very next step of the same cycle, going the same direction) — so the walk never actually halted, it just kept re-treading the outer cycle a second time until order filled up completely. That left valid all-true and the ISA:scriptfcn:boundaryHoleOmitted warning from the previous round — added specifically for this case — silently never firing, with duplicated outer-ring vertices in the output instead.

Fixed by adding a nxt(1) == order(1) stop condition. Verified by hand-tracing both cases: for the ordinary single-cycle path (no hole), the for loop's own range (nRegionVerts) already ends before a closed cycle that length could ever reach the new check, so nothing changes there. For a genuine two-cycle case, it now correctly stops right when the outer cycle closes.

Exposed traceOneRegion via scriptfcn's existing injection mechanism and added a direct regression test against a synthetic two-cycle edge list (a 4-vertex outer square + a disconnected 3-vertex inner triangle) — deliberately not a geometric alphaShape, for full control over the exact adjacency rather than depending on alphaShape's own region/facet algorithm to reproduce a specific hole shape reliably. CI is running on 4c31755 now.


Generated by Claude Code

@andremun
andremun requested a balanced review from Copilot August 20, 2026 19:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 33 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

output/scriptfcn.m:530

  • This traversal only excludes the immediately previous vertex. After closing the first boundary cycle it revisits the starting vertex and keeps filling order, so for a region with a hole valid can remain all true and the new omission warning never fires. Exclude already visited vertices so the loop stops when that cycle closes and the remaining cycle is detected.
    nxt = adj(curr, adj(curr,:) ~= prev & adj(curr,:) ~= 0);

output/scriptpng.m:234

  • Skipping this export for a 3D/no-CLOISTER run leaves any distribution_boundary.png from an earlier 2D build in the same output directory. That stale image then appears to belong to the current build even though it depicts the old 2D model; remove the existing file when the boundary is not applicable.
if isfield(container, 'cloist') && ~is3D
    clf;
    drawBoundary(container.pilot.Z, container.cloist.Zedge, 'CLOISTER empirical bound');
    exportgraphics(fig, [rootdir 'distribution_boundary.png']);
end

InstanceSpace.m:111

  • The SIFTED contract omits its conditional data_dense.X/Y/Ybin inputs. When model.prelim.bydensity is true, runSifted dereferences those fields unconditionally, so an incomplete loaded/edited model still passes this check and fails with the opaque missing-field error this validation is intended to prevent. Add those requirements conditionally for density mode.
            'sifted',   {{'data.X', 'data.Y', 'data.Ybin', 'data.featlabels', 'featsel.idx'}}, ...

output/scriptfcn.m:542

  • Use “vertex/vertices”; “vertice(s)” is not a valid singular or plural form.
        ['This region''s boundary has %d vertice(s) not reachable from its outer ring -- ' ...

tests/testRepoRoot.m:4

  • The framework name is matlab.unittest, not test.unittest.
% because MATLAB's current working directory during actual test.unittest

Comment thread core/TRACE.m Outdated
Real correctness fixes:
- core/TRACE.m: the #28 ngood==nbest guard only proved good/best agree
  with EACH OTHER, not that either still holds the true trained
  algorithm count -- if both were shortened by the same amount (e.g. the
  same buggy migration/edit), the check passed and the "lost" algorithms
  were silently replaced with empty placeholders instead of erroring.
  Cross-checked against trainedTrace.summary (cell(nalgos+1, 11),
  unconditionally built by both training-mode branches right after
  good/best), an independent field not derived from good/best's own
  sizes. Regression test extended with an equally-shortened case.
- InstanceSpace.m: runSifted()'s density-resubsetting branch
  unconditionally dereferences obj.model.data_dense.X/Y/Ybin once
  obj.model.prelim.bydensity is true, but that's conditional -- doesn't
  belong in StageRequiredFields' unconditional 'sifted' list (would
  wrongly demand it for every model). Guarded locally instead, matching
  this class's own stated convention for conditional-path fields.
  Regression test added building a genuinely density-subsetted model.
- output/scriptpng.m: rebuilding the same rootdir as 3D (or without
  CLOISTER) left behind a stale distribution_boundary.png from an
  earlier 2D build instead of removing it, so it looked like it belonged
  to the current result. Now deleted when the boundary isn't applicable.

Minor: "vertice(s)" grammar fix in the ISA:scriptfcn:boundaryHoleOmitted
warning; "test.unittest" typo fixed to "matlab.unittest" in
tests/testRepoRoot.m's comment.

Not changed: a suppressed comment claimed traceOneRegion (fixed last
round, 4c31755) still doesn't stop correctly on a hole. Hand-traced it
again against the actual committed code and confirmed the fix is
correct as committed -- this appears to be a stale finding not
accounting for 4c31755's already-applied nxt(1)==order(1) stop
condition, not a real remaining gap.

Copy link
Copy Markdown
Owner Author

Addressed the fifth review round's other findings in 1485d61, alongside the posted core/TRACE.m finding (replied on that thread):

  • scriptpng.m left stale artifacts: rebuilding the same rootdir as 3D (or without CLOISTER) skipped writing distribution_boundary.png but left a stale file from an earlier 2D build in place — it would look like it belonged to the current result. Now deleted when the boundary isn't applicable.
  • 'sifted''s density-mode fields: runSifted's density-resubsetting branch unconditionally dereferences obj.model.data_dense.X/Y/Ybin once bydensity is true, but that's conditional — doesn't belong in the unconditional 'sifted' contract (would wrongly demand it for every model). Guarded locally instead, with a regression test building a genuinely density-subsetted model.
  • Two typos: "vertice(s)" → proper singular/plural in the boundary-hole warning; "test.unittest" → "matlab.unittest" in testRepoRoot.m's comment.

On the output/scriptfcn.m:530 finding (claiming traceOneRegion still doesn't stop correctly on a hole): I re-verified by hand-tracing the actual committed code from 4c31755 against both a normal single-cycle case and a genuine two-cycle (hole) case, vertex by vertex. The nxt(1) == order(1) stop condition added last round does correctly halt the walk once the outer cycle closes, in both cases — I couldn't reproduce what this comment describes against the code as it's currently written. My best guess is this review pass didn't register the previous round's fix, since the fix landed in the same file this comment is anchored to. Didn't make a further change here since I can't identify what would actually need changing — happy to revisit if you or a future review can point to a specific input where the current code still breaks.

CI is running on 1485d61 now.


Generated by Claude Code

@andremun
andremun requested a balanced review from Copilot August 20, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

andremun pushed a commit that referenced this pull request Aug 21, 2026
Downloads badge is fully self-contained (shields.io reads the GitHub API
directly, no setup needed). The Codecov badge will show "unknown" until
two things happen: CI (which produces coverage.xml, currently only on
0.9.1/development pending PR #51's merge) actually uploads it to
Codecov, and the repo is linked on codecov.io with a CODECOV_TOKEN
secret added to the GitHub repo -- neither of which this commit does.
Feeds the Codecov badge added to README.md on docs/matlab-ci-testing-skill.
fail_ci_if_error: false so a Codecov outage or a not-yet-configured
CODECOV_TOKEN secret can't fail the actual test run -- pass/fail already
happened above this step. The token secret itself and linking the repo
on codecov.io are still needed on the codecov.io/GitHub side before the
badge shows real data; neither can be done from a commit.

Copy link
Copy Markdown
Owner Author

Added a small scope addition in 3ae57f7: uploads coverage.xml to Codecov (fail_ci_if_error: false, so it can't fail the actual test run). This feeds a Codecov coverage badge added to README.md on a separate branch (docs/matlab-ci-testing-skill, not part of this PR). Two things still need doing outside of any commit before the badge shows real data: linking this repo on codecov.io, and adding a CODECOV_TOKEN secret to the GitHub repo.


Generated by Claude Code

Filed after scoping a MATLAB-toolbox-style custom doc + GitHub Pages
reference site at the user's request (~29 pages: 22 function/class
reference pages, 7 conceptual/guide pages). Sized for v0.9.2 alongside
#30/#52, not v0.9.1, despite not being an algorithmic change -- it's a
genuinely new deliverable, not an engineering-quality fix.
@andremun
andremun merged commit da98d53 into master Aug 21, 2026
2 checks passed
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.

3 participants