Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
0d08380
chore: capture ui migration baseline
Pluze Jul 19, 2026
77dbc60
chore: prototype ui contract representations
Pluze Jul 19, 2026
ee43719
chore: add ui migration analyzer
Pluze Jul 19, 2026
762d154
chore: merge main ci repair
Pluze Jul 19, 2026
2b369f4
chore: accept ui contract representation
Pluze Jul 19, 2026
bec81a3
test: harden ui migration evidence
Pluze Jul 19, 2026
4e6f889
test: validate ui contract end to end
Pluze Jul 19, 2026
1226557
feat: establish explicit ui contract kernel
Pluze Jul 19, 2026
af9369e
feat: complete ui contract values
Pluze Jul 19, 2026
ce6e15e
feat: close strict ui contract kernel
Pluze Jul 19, 2026
ff6e6ea
feat: establish transactional ui runtime
Pluze Jul 19, 2026
3da1e4e
feat: add ui project and result storage
Pluze Jul 19, 2026
5a87bdd
feat: establish ui sdk paved road
Pluze Jul 19, 2026
6c3d9b7
feat: migrate first app to explicit ui
Pluze Jul 19, 2026
895445d
feat: add typed table workflow
Pluze Jul 19, 2026
b60ccf6
refactor: partition app sdk contract
Pluze Jul 19, 2026
f65947e
refactor: migrate t-test wizard to app sdk
Pluze Jul 19, 2026
877b113
refactor: simplify app sdk authoring contract
Pluze Jul 19, 2026
79c5b1d
refactor: migrate app sdk workflow waves
Pluze Jul 19, 2026
52e8128
refactor: migrate vt resistance to app sdk
Pluze Jul 19, 2026
4cf0a34
refactor: migrate gait analysis to app sdk
Pluze Jul 19, 2026
f179587
refactor: migrate dic preprocess to app sdk
Pluze Jul 19, 2026
b63e060
refactor: migrate batch crop to app sdk
Pluze Jul 19, 2026
af4e9b6
refactor: migrate video marker to app sdk
Pluze Jul 19, 2026
11dd3da
refactor: internalize app sdk ui implementation
Pluze Jul 19, 2026
134d215
refactor: remove executable legacy ui dependencies
Pluze Jul 19, 2026
417f85c
test: migrate dic postprocess gui workflow
Pluze Jul 19, 2026
0099aab
fix: complete neuro app sdk workflows
Pluze Jul 19, 2026
67f9f8d
test: migrate rhs preview gui workflow
Pluze Jul 19, 2026
3568772
test: migrate image match gui workflow
Pluze Jul 19, 2026
3aa6d01
test: migrate focus stack gui workflow
Pluze Jul 19, 2026
4f374f0
fix: complete image enhance app sdk workflow
Pluze Jul 20, 2026
08d04fb
fix: complete flir thermal app sdk workflow
Pluze Jul 20, 2026
4c8e312
test: migrate curvature gui workflow
Pluze Jul 20, 2026
2362963
test: migrate cic gui workflow
Pluze Jul 20, 2026
d4778aa
test: migrate csc gui workflow
Pluze Jul 20, 2026
03c19cc
fix: complete eis app sdk workflow
Pluze Jul 20, 2026
1dcf98e
fix: complete ecg print app sdk workflow
Pluze Jul 20, 2026
dbfc947
feat: restore app sdk runtime parity
Pluze Jul 20, 2026
90afa12
fix: restore dic and cic product ui
Pluze Jul 20, 2026
cc9495c
fix: restore electrochem app ui parity
Pluze Jul 20, 2026
bc890ab
fix: restore figure studio workflow ui
Pluze Jul 20, 2026
60657dd
fix: restore response review stats ui
Pluze Jul 20, 2026
c4fcf08
fix: restore nerve response analysis ui
Pluze Jul 20, 2026
8d2d320
feat: type interval scroll events
Pluze Jul 20, 2026
ca6132a
fix: restore rhs preview workflow ui
Pluze Jul 20, 2026
c96a6ef
fix: restore dic postprocess ui parity
Pluze Jul 20, 2026
98c3a96
fix: restore focus stack ui parity
Pluze Jul 20, 2026
a6915c3
fix: restore image enhance workflow ui
Pluze Jul 20, 2026
4c76fd0
fix: restore image match workflow ui
Pluze Jul 20, 2026
ffe1d8f
feat: compose workspace page surfaces
Pluze Jul 20, 2026
b2a18f6
fix: restore t-test wizard ui
Pluze Jul 20, 2026
a386da0
feat: support task-oriented file lists
Pluze Jul 20, 2026
23063c0
fix: preserve file task identity on removal
Pluze Jul 20, 2026
d6525bd
fix: restore batch crop workflow ui
Pluze Jul 20, 2026
69088f8
fix: render file list status text
Pluze Jul 20, 2026
0e0057e
fix: restore curvature measurement ui
Pluze Jul 20, 2026
b16ccbc
fix: restore flir thermal ui
Pluze Jul 20, 2026
9da0369
fix: restore gait analysis ui
Pluze Jul 20, 2026
fec34d0
fix: preserve session reconstruction failures
Pluze Jul 20, 2026
70b3f9d
fix: fill single-surface control tabs
Pluze Jul 20, 2026
c59a74b
fix: restore ecg print ui
Pluze Jul 20, 2026
ee290f4
feat: support titled control groups
Pluze Jul 20, 2026
af7c1fb
feat: restore projects from app callbacks
Pluze Jul 20, 2026
759525b
feat: start new project documents transactionally
Pluze Jul 20, 2026
146bd1e
fix: reconcile structured point slots
Pluze Jul 20, 2026
289c6c1
fix: disable bound field labels
Pluze Jul 20, 2026
f82e293
feat: support native option dialogs
Pluze Jul 20, 2026
a5a01c8
fix: restore video marker ui
Pluze Jul 20, 2026
a7c0be4
test: route app sdk validation fixtures
Pluze Jul 20, 2026
db1fcc5
refactor: retire legacy ui facade
Pluze Jul 20, 2026
7f38e6f
refactor: narrow callback context boundary
Pluze Jul 20, 2026
ec2a67e
refactor: extract app runtime factory
Pluze Jul 20, 2026
2e45c5f
test: move headless runtime construction internal
Pluze Jul 20, 2026
b0ae589
test: move matlab runtime construction internal
Pluze Jul 20, 2026
be308af
refactor: internalize compiled app contract
Pluze Jul 20, 2026
ccaa0e9
fix: preserve startup failure presentation
Pluze Jul 20, 2026
2f1ee2a
docs: finalize app sdk migration metadata
Pluze Jul 20, 2026
2d01f4c
test: align lifecycle callback boundary
Pluze Jul 20, 2026
98313ba
docs: document migrated helper ownership
Pluze Jul 20, 2026
d465158
docs: close app sdk public contracts
Pluze Jul 20, 2026
0283c3a
fix: close migrated app hygiene gaps
Pluze Jul 20, 2026
ff01900
refactor: split layout value validation
Pluze Jul 20, 2026
4cb707e
refactor: split runtime contract responsibilities
Pluze Jul 20, 2026
357b008
refactor: partition native adapter internals
Pluze Jul 20, 2026
2a623f5
feat: adapt launcher to app sdk diagnostics
Pluze Jul 20, 2026
78097e1
feat: add explanatory app tooltips
Pluze Jul 20, 2026
3fd75f6
fix: type neurophysiology debug samples
Pluze Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
99 changes: 99 additions & 0 deletions +labkit/+app/+diagnostic/Artifact.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
classdef (Sealed) Artifact
%ARTIFACT Describe one anonymous diagnostic-sample artifact.
%
% Usage:
% artifact = labkit.app.diagnostic.Artifact( ...
% id,role,relativePath,Name=Value)
%
% Description:
% Identifies one synthetic input, expected export, or support file
% beneath a diagnostic sample root without exposing a user file path.
%
% Inputs:
% id - Nonempty semantic identifier unique within a SamplePack.
% role - Nonempty App-owned artifact purpose.
% relativePath - Nonempty diagnostic-root-relative path without
% traversal.
%
% Optional Name-Value Arguments:
% Expectation - "loads", "rejects", "exports", or "support".
% Default: "loads".
%
% Outputs:
% artifact - Immutable diagnostic artifact value.
%
% Errors:
% labkit:app:contract:UnknownArgument - An option is unknown,
% duplicated, or unpaired.
% labkit:app:contract:InvalidValue - Text, path, or Expectation is
% malformed.
%
% Example:
% artifact = labkit.app.diagnostic.Artifact( ...
% "input","source","samples/input.csv");
% assert(artifact.Expectation == "loads")
%
% See also labkit.app.diagnostic.SampleContext,
% labkit.app.diagnostic.SamplePack

properties (SetAccess = immutable)
Id (1, 1) string
Role (1, 1) string
RelativePath (1, 1) string
Expectation (1, 1) string
end

methods
function obj = Artifact(id, role, relativePath, varargin)
options = labkit.app.internal.OptionParser.parse( ...
"labkit.app.diagnostic.Artifact", "Expectation", ...
varargin{:});
obj.Id = nonemptyText(id, "Id");
obj.Role = nonemptyText(role, "Role");
obj.RelativePath = relativePathValue(relativePath);
expectation = optionValue(options, "Expectation", "loads");
expectation = nonemptyText(expectation, "Expectation");
if ~any(expectation == ...
["loads", "rejects", "exports", "support"])
error("labkit:app:contract:InvalidValue", ...
"Diagnostic Artifact Expectation is unsupported.");
end
obj.Expectation = expectation;
end
end
end

function value = relativePathValue(value)
value = replace(nonemptyText(value, "RelativePath"), "\", "/");
if startsWith(value, "/") || startsWith(value, "//") || ...
~isempty(regexp(char(value), '^[A-Za-z]:', "once"))
invalid("RelativePath must be diagnostic-root-relative.");
end
parts = split(value, "/");
if any(parts == ["", ".", ".."], "all")
invalid("RelativePath must not traverse diagnostic boundaries.");
end
value = join(parts, "/");
end

function value = nonemptyText(value, name)
if ~(ischar(value) || (isstring(value) && isscalar(value)))
invalid("%s must be scalar text.", name);
end
value = string(value);
if strlength(value) == 0
invalid("%s must be nonempty.", name);
end
end

function value = optionValue(options, name, defaultValue)
value = defaultValue;
if isfield(options, name)
value = options.(name);
end
end

function invalid(message, varargin)
error("labkit:app:contract:InvalidValue", ...
"Diagnostic Artifact " + message, varargin{:});
end
86 changes: 86 additions & 0 deletions +labkit/+app/+diagnostic/Options.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
classdef (Sealed) Options
%OPTIONS Configure one App SDK diagnostic session.
%
% Usage:
% options = labkit.app.diagnostic.Options()
% options = labkit.app.diagnostic.Options(Name=Value)
%
% Description:
% Options selects standard or verbose runtime recording and whether a
% Definition should build its declared anonymous synthetic sample.
% Omitting Diagnostics from Definition.launch is equivalent to the
% default standard options. This value never exposes a runtime,
% recorder, figure registry, or callback transport.
%
% Optional Name-Value Arguments:
% Level - "standard" or "verbose". Standard keeps a bounded in-memory
% diagnostic history; verbose additionally writes structured
% artifacts when ArtifactFolder is nonempty. Default: "standard".
% ArtifactFolder - Scalar diagnostic-session folder. Empty keeps
% recording in memory only. Default: "".
% Sample - "none" or "synthetic". Synthetic requires the Definition's
% BuildDebugSample contract. Default: "none".
%
% Outputs:
% options - Immutable diagnostic configuration.
%
% Errors:
% labkit:app:contract:UnknownArgument - An option is unknown,
% duplicated, or unpaired.
% labkit:app:contract:InvalidValue - A supplied value is malformed or
% outside its documented legal set.
%
% Example:
% options = labkit.app.diagnostic.Options(Level="verbose");
% assert(options.Level == "verbose")
%
% See also labkit.app.Definition,
% labkit.app.diagnostic.SampleContext,
% labkit.app.diagnostic.SamplePack

properties (SetAccess = immutable)
Level (1, 1) string
ArtifactFolder (1, 1) string
Sample (1, 1) string
end

methods
function obj = Options(varargin)
names = ["Level", "ArtifactFolder", "Sample"];
options = labkit.app.internal.OptionParser.parse( ...
"labkit.app.diagnostic.Options", names, varargin{:});
obj.Level = oneOf(optionValue( ...
options, "Level", "standard"), ...
["standard", "verbose"], "Level");
obj.ArtifactFolder = scalarText(optionValue( ...
options, "ArtifactFolder", ""), "ArtifactFolder");
obj.Sample = oneOf(optionValue( ...
options, "Sample", "none"), ...
["none", "synthetic"], "Sample");
end
end
end

function value = oneOf(value, legal, name)
value = scalarText(value, name);
if ~any(value == legal)
error("labkit:app:contract:InvalidValue", ...
"Diagnostic Options %s must be %s.", ...
name, strjoin(legal, " or "));
end
end

function value = scalarText(value, name)
if ~(ischar(value) || (isstring(value) && isscalar(value)))
error("labkit:app:contract:InvalidValue", ...
"Diagnostic Options %s must be scalar text.", name);
end
value = string(value);
end

function value = optionValue(options, name, defaultValue)
value = defaultValue;
if isfield(options, name)
value = options.(name);
end
end
162 changes: 162 additions & 0 deletions +labkit/+app/+diagnostic/SampleContext.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
classdef (Sealed) SampleContext
%SAMPLECONTEXT Provide bounded folders for anonymous debug samples.
%
% Usage:
% context = labkit.app.diagnostic.SampleContext(artifactFolder)
% filepath = context.samplePath(relativePath)
% filepath = context.outputPath(relativePath)
% record = context.sourceRecord(id,role,filepath,required)
% artifact = context.artifact(id,role,filepath,Name=Value)
%
% Description:
% SampleContext creates one diagnostic root with samples and outputs
% children. App-owned BuildDebugSample callbacks may write only
% anonymous synthetic files beneath these folders. The value does not
% expose a runtime, recorder, project store, or UI object.
%
% Inputs:
% artifactFolder - Nonempty scalar diagnostic-session folder.
% relativePath - Nonempty child path without an absolute root, empty
% segment, current segment, or parent traversal.
% id - Stable portable-source identifier.
% role - Stable portable-source role.
% filepath - Path returned by samplePath.
% required - Logical scalar source requirement.
%
% Outputs:
% context - Immutable diagnostic sample context.
% filepath - Absolute path bounded by SampleFolder or OutputFolder.
% record - Portable source value from
% labkit.app.project.sourceRecord.
% artifact - Typed diagnostic artifact whose relative path is derived
% from a filepath beneath ArtifactFolder.
%
% Errors:
% labkit:app:contract:InvalidValue - A folder, relative path, or source
% argument is malformed.
% MATLAB filesystem errors propagate when the diagnostic folders cannot
% be created.
%
% Typical Call:
% context = labkit.app.diagnostic.SampleContext(tempname);
% filepath = context.samplePath("input.csv");
%
% See also labkit.app.diagnostic.Artifact,
% labkit.app.diagnostic.SamplePack,
% labkit.app.project.sourceRecord

properties (SetAccess = immutable)
ArtifactFolder (1, 1) string
SampleFolder (1, 1) string
OutputFolder (1, 1) string
end

methods
function obj = SampleContext(artifactFolder)
artifactFolder = nonemptyText( ...
artifactFolder, "ArtifactFolder");
obj.ArtifactFolder = artifactFolder;
obj.SampleFolder = string(fullfile( ...
char(artifactFolder), "samples"));
obj.OutputFolder = string(fullfile( ...
char(artifactFolder), "outputs"));
ensureFolder(obj.ArtifactFolder);
ensureFolder(obj.SampleFolder);
ensureFolder(obj.OutputFolder);
end

function filepath = samplePath(obj, relativePath)
filepath = boundedPath(obj.SampleFolder, relativePath);
end

function filepath = outputPath(obj, relativePath)
filepath = boundedPath(obj.OutputFolder, relativePath);
end

function record = sourceRecord(~, id, role, filepath, required)
if nargin < 5
required = true;
end
record = labkit.app.project.sourceRecord( ...
id, role, filepath, required);
end

function artifact = artifact(obj, id, role, filepath, varargin)
relativePath = relativeArtifactPath( ...
obj.ArtifactFolder, filepath);
artifact = labkit.app.diagnostic.Artifact( ...
id, role, relativePath, varargin{:});
end
end
end

function filepath = boundedPath(folder, relativePath)
relativePath = replace(nonemptyText(relativePath, "relativePath"), "\", "/");
if startsWith(relativePath, "/") || startsWith(relativePath, "//") || ...
~isempty(regexp(char(relativePath), '^[A-Za-z]:', "once"))
invalid("relativePath must be folder-relative.");
end
parts = split(relativePath, "/");
if any(parts == ["", ".", ".."], "all")
invalid("relativePath must not traverse the diagnostic folder.");
end
partCells = cellstr(parts);
filepath = string(fullfile(char(folder), partCells{:}));
parent = string(fileparts(filepath));
ensureFolder(parent);
end

function ensureFolder(folder)
if exist(char(folder), "dir") ~= 7
mkdir(char(folder));
end
end

function relativePath = relativeArtifactPath(root, filepath)
root = normalizedAbsolutePath(root, "ArtifactFolder");
filepath = normalizedAbsolutePath(filepath, "filepath");
rootPrefix = root + "/";
if ispc
inside = startsWith(lower(filepath), lower(rootPrefix));
else
inside = startsWith(filepath, rootPrefix);
end
if ~inside
invalid("filepath must remain beneath ArtifactFolder.");
end
relativePath = extractAfter(filepath, strlength(rootPrefix));
if strlength(relativePath) == 0
invalid("filepath must name a child artifact.");
end
end

function value = normalizedAbsolutePath(value, name)
value = replace(nonemptyText(value, name), "\", "/");
if ~isAbsolutePath(value)
value = replace(string(fullfile(pwd, char(value))), "\", "/");
end
while endsWith(value, "/") && value ~= "/" && ...
isempty(regexp(char(value), '^[A-Za-z]:/$', "once"))
value = extractBefore(value, strlength(value));
end
end

function tf = isAbsolutePath(value)
tf = startsWith(value, "/") || startsWith(value, "//") || ...
~isempty(regexp(char(value), '^[A-Za-z]:/', "once"));
end

function value = nonemptyText(value, name)
if ~(ischar(value) || (isstring(value) && isscalar(value)))
invalid("%s must be scalar text.", name);
end
value = string(value);
if strlength(value) == 0
invalid("%s must be nonempty.", name);
end
end

function invalid(message, varargin)
error("labkit:app:contract:InvalidValue", ...
"Diagnostic SampleContext " + message, varargin{:});
end
Loading
Loading