Skip to content

Commit d70c260

Browse files
authored
feat: add app version metadata guardrails (#24)
1 parent cb39ef4 commit d70c260

72 files changed

Lines changed: 892 additions & 170 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

+labkit/+ui/+app/appVersionTitle.m

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
function titleText = appVersionTitle(baseTitle, info)
2+
%APPVERSIONTITLE Format an app figure title with version metadata.
3+
%
4+
% App-facing contract:
5+
% titleText = labkit.ui.app.appVersionTitle(baseTitle, info)
6+
%
7+
% Inputs:
8+
% baseTitle - scalar text already used as the figure title.
9+
% info - app version struct with version and updated scalar text fields.
10+
%
11+
% Outputs:
12+
% titleText - string in the form "<baseTitle> v<version> (<updated>)".
13+
14+
baseTitle = normalizeTitle(baseTitle, info);
15+
version = textField(info, 'version');
16+
updated = textField(info, 'updated');
17+
titleText = baseTitle + " v" + version + " (" + updated + ")";
18+
end
19+
20+
function baseTitle = normalizeTitle(value, info)
21+
if nargin > 0 && (ischar(value) || (isstring(value) && isscalar(value)))
22+
baseTitle = strtrim(string(value));
23+
else
24+
baseTitle = "";
25+
end
26+
if strlength(baseTitle) == 0 && isstruct(info) && isfield(info, 'displayName')
27+
baseTitle = textField(info, 'displayName');
28+
end
29+
if strlength(baseTitle) == 0 && isstruct(info) && isfield(info, 'name')
30+
baseTitle = textField(info, 'name');
31+
end
32+
if strlength(baseTitle) == 0
33+
error('labkit:ui:app:InvalidVersionInfo', ...
34+
'App version title requires a base title, displayName, or name.');
35+
end
36+
end
37+
38+
function text = textField(info, fieldName)
39+
if ~isstruct(info) || ~isscalar(info) || ~isfield(info, fieldName)
40+
error('labkit:ui:app:InvalidVersionInfo', ...
41+
'App version info must include scalar text field "%s".', fieldName);
42+
end
43+
value = info.(fieldName);
44+
if ~(ischar(value) || (isstring(value) && isscalar(value)))
45+
error('labkit:ui:app:InvalidVersionInfo', ...
46+
'App version info field "%s" must be scalar text.', fieldName);
47+
end
48+
text = strtrim(string(value));
49+
if strlength(text) == 0
50+
error('labkit:ui:app:InvalidVersionInfo', ...
51+
'App version info field "%s" cannot be empty.', fieldName);
52+
end
53+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
function titleText = applyVersionTitle(fig, info)
2+
%APPLYVERSIONTITLE Apply app version metadata to a figure title.
3+
%
4+
% App-facing contract:
5+
% titleText = labkit.ui.app.applyVersionTitle(fig, info)
6+
%
7+
% Inputs:
8+
% fig - scalar app figure handle with a Name property.
9+
% info - app version struct with version and updated scalar text fields.
10+
%
11+
% Outputs:
12+
% titleText - title written to fig.Name.
13+
14+
if isempty(fig) || ~isscalar(fig) || ~isvalid(fig) || ~isprop(fig, 'Name')
15+
error('labkit:ui:app:InvalidFigure', ...
16+
'Version title can only be applied to a valid scalar figure.');
17+
end
18+
titleText = labkit.ui.app.appVersionTitle(string(fig.Name), info);
19+
fig.Name = char(titleText);
20+
end

+labkit/+ui/+app/dispatchRequest.m

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,23 @@
66
% "labkit_Example_app", varargin, nargout);
77
% [handled, outputs, debug] = labkit.ui.app.dispatchRequest( ...
88
% "labkit_Example_app", varargin, nargout, "Requirements", req);
9+
% [handled, outputs, debug] = labkit.ui.app.dispatchRequest( ...
10+
% "labkit_Example_app", varargin, nargout, ...
11+
% "Requirements", req, "Version", info);
912
%
1013
% Inputs:
1114
% appName - app entry-point name used to build app-scoped error IDs.
1215
% args - input argument cell from the app entry point.
1316
% nout - requested output count from the app entry point.
1417
% Name-value options:
1518
% Requirements - optional struct returned by app requirements().
19+
% Version - optional struct returned by app version().
1620
%
1721
% Outputs:
18-
% handled - true only when the lightweight "requirements" request is
19-
% consumed; false for normal and debug launches.
20-
% outputs - one-cell output containing the requirements struct for the
21-
% "requirements" request; empty otherwise.
22+
% handled - true only when a lightweight request such as "requirements" or
23+
% "version" is consumed; false for normal and debug launches.
24+
% outputs - one-cell output containing the requested app struct for handled
25+
% lightweight requests; empty otherwise.
2226
% debugContext - disabled for normal launches; enabled for "debug" launches.
2327
% Debug launch requests do not consume app launch. Public app debug
2428
% launches write a trace log under artifacts/debug so the last event is
@@ -54,6 +58,20 @@
5458
return;
5559
end
5660

61+
if request == "version"
62+
if nout > 1
63+
error(errorId(appName, 'TooManyOutputs'), ...
64+
'%s version request returns at most one output.', appName);
65+
end
66+
if numel(args) > 1
67+
error(errorId(appName, 'UnsupportedInput'), ...
68+
'%s version request does not accept options.', appName);
69+
end
70+
handled = true;
71+
outputs = {options.Version};
72+
return;
73+
end
74+
5775
if isDebugRequest(request)
5876
if nout > 2
5977
error(errorId(appName, 'TooManyOutputs'), ...
@@ -79,7 +97,7 @@
7997
end
8098

8199
function options = parseOptions(varargin)
82-
options = struct('Requirements', []);
100+
options = struct('Requirements', [], 'Version', []);
83101
if isempty(varargin)
84102
return;
85103
end
@@ -92,6 +110,8 @@
92110
switch name
93111
case "Requirements"
94112
options.Requirements = varargin{k + 1};
113+
case "Version"
114+
options.Version = varargin{k + 1};
95115
otherwise
96116
error('labkit:ui:app:InvalidDispatchOptions', ...
97117
'Unsupported dispatch option "%s".', name);

+labkit/+ui/version.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
% compatible contract ranges implemented by this code, contract status,
1313
% and a short maintainer note.
1414

15-
info = labkit.contract.versionInfo("ui", "2.1.0", ">=2.0 <3", ...
16-
"stable", "UI 2.x app/spec/view/tool/diag contract with safe output prompts.");
15+
info = labkit.contract.versionInfo("ui", "2.2.0", ">=2.0 <3", ...
16+
"stable", "UI 2.x app/spec/view/tool/diag contract with app version title support.");
1717
end

+labkit/AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
- `labkit.contract` owns only MATLAB-native facade contract structs, simple range
2727
checks, and app requirement assertions. It must stay domain-neutral and must
2828
not become a package manager, plugin registry, or app metadata store.
29+
- App version display belongs to app-owned `version.m` files plus
30+
`labkit.ui.app` title formatting; do not move app metadata into
31+
`labkit.contract` or a central registry.
32+
- When app-facing facade code changes under `+labkit/+ui`, `+dta`, `+rhs`, or
33+
`+biosignal`, update the owning facade `version()` contract in the same
34+
change. Facade versions use `X.Y.Z` semantic format and must only increase.
2935
- Reusable UI tools may own domain-neutral interaction workflows such as image scale-bar controls, reference editing, unit normalization, and overlay placement. Keep those tools independent from app result schemas, scientific formulas, file formats, and workflow wording.
3036
- App-facing UI APIs live under `labkit.ui.app.*`, `labkit.ui.spec.*`, `labkit.ui.view.*`, `labkit.ui.tool.*`, and `labkit.ui.diag.*`. Do not reintroduce flat `labkit.ui.*` helper files.
3137
- Preview-axis tools that need pointer, drag, scroll, or hit-test ownership must use `labkit.ui.tool.createRuntime` sessions instead of each helper managing figure/axes callbacks independently.

.agents/skills/labkit-app-builder/SKILL.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,17 @@ results, exports, and usage text should be specific to the new app.
118118

119119
Keep app discovery source-based through `apps/**/labkit_*_app.m`; app
120120
manifests, registries, per-app build tasks, and governance apps are outside
121-
the current app model.
121+
the current app model. App-owned `version.m` files provide visible version and
122+
update-date metadata without becoming dependency manifests.
122123

123124
## Implementation Pattern
124125

125126
Build the app in this order:
126127

127-
1. Add or update the public app entry point as a thin dispatch wrapper, then
128-
create the GUI from package-root `run.m` with `labkit.ui.app.create` and
129-
`labkit.ui.spec.*`.
128+
1. Add or update app-local `requirements.m` and `version.m`, then keep the
129+
public app entry point as a thin dispatch wrapper. It should pass both
130+
structs to `labkit.ui.app.dispatchRequest`, create the GUI from package-root
131+
`run.m`, and apply the app version title to the returned figure.
130132
2. Put the data-only spec in `+<app_slug>/+ui/buildSpec.m`; package-root
131133
`run.m` should create callback handles, call
132134
`<app_slug>.ui.buildSpec(...)`, then call `labkit.ui.app.create(...)`.

.agents/skills/labkit-boundary-guard/SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ If this is not proven, keep the code app-local.
4848
For UI boundary work, prefer `labkit.ui.app.create`,
4949
`labkit.ui.spec.*`, named `labkit.ui.view.*` helpers,
5050
`labkit.ui.app.dispatchRequest`, `labkit.ui.diag.createContext`, and
51-
`labkit.ui.tool.createRuntime`. Keep primitive builders private; do not expose
51+
`labkit.ui.tool.createRuntime`. App version metadata stays in app-owned
52+
`version.m` files; reusable UI may format or apply that title, but
53+
`labkit.contract` should not become an app metadata registry. Keep primitive
54+
builders private; do not expose
5255
public `labkit.ui.spec.button`, `dropdown`, `slider`, `listbox`, `table`,
5356
`axes`, or similar MATLAB primitive constructors. Do not reintroduce
5457
`createShell` or legacy `view.section/form/panel/axes/draw/update/place` APIs.

apps/AGENTS.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ Apps are first-class deliverables. Do not treat them as examples for a hidden pl
2121
- Use `labkit.ui.app.create` with `labkit.ui.spec.*` for app GUIs. Do not
2222
reintroduce the removed `labkit.ui.app.createShell` or legacy view helpers.
2323
- Use `labkit.ui.app.dispatchRequest` for debug launch routing and `labkit.ui.diag.createContext` only when an app has an app-specific nonstandard request path.
24-
- Each public app entrypoint should call an app package `requirements.m` and
25-
pass it to `labkit.ui.app.dispatchRequest`. The only lightweight non-GUI
26-
request is `"requirements"`.
24+
- Each public app entrypoint should call app package `requirements.m` and
25+
`version.m`, pass both structs to `labkit.ui.app.dispatchRequest`, and apply
26+
the version title after `run.m` returns the figure. Lightweight non-GUI
27+
requests are `"requirements"` and `"version"`.
28+
- When app source, app-owned package code, or app-facing behavior changes,
29+
update that app's `version.m` version metadata in the same change. App
30+
versions use `X.Y.Z` semantic format and must only increase.
2731
- Debug launches should attach the Log tab text area, emit a startup trace line, and instrument high-level component callbacks after controls are built.
2832
- Apps with custom preview scroll, drawing, ROI, scale-bar, or other axes interaction should create a `labkit.ui.tool.createRuntime` and pass that runtime into reusable tools. Do not set preview-tool `WindowScrollWheelFcn`, `WindowButtonMotionFcn`, `WindowButtonUpFcn`, or axes `ButtonDownFcn` directly in app code.
2933
- DTA-backed apps use `labkit.dta.*` for discovery, loading, sessions, pulse detection, and parsed curve/table access.

apps/dic/dic_postprocess/+dic_postprocess/requirements.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33

44
function req = requirements()
55

6-
req = labkit.contract.requirements("ui", ">=2.1 <3");
6+
req = labkit.contract.requirements("ui", ">=2.2 <3");
77
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
%VERSION App version metadata for labkit_DICPostprocess_app.
2+
% Expected caller: labkit_DICPostprocess_app and launcher/catalog guardrails.
3+
% Inputs: none.
4+
% Outputs: struct with name, displayName, family, version, and updated fields.
5+
% Side effects: none.
6+
function info = version()
7+
info = struct( ...
8+
"name", "labkit_DICPostprocess_app", ...
9+
"displayName", "DIC Postprocess", ...
10+
"family", "DIC", ...
11+
"version", "1.0.0", ...
12+
"updated", "2026-06-23");
13+
end

0 commit comments

Comments
 (0)