labkit.app is the App-facing runtime contract. Apps declare product meaning;
the private runtime owns native MATLAB components, event serialization,
transactions, project documents, portable sources, resources, and cleanup.
Each App-owned definition.m is the single immutable product contract, and
each entrypoint delegates to it:
function varargout = labkit_Example_app(varargin)
[varargout{1:nargout}] = example.definition().launch(varargin{:});
endfunction app = definition()
app = labkit.app.Definition( ...
Entrypoint="labkit_Example_app", ...
AppId="examples.example", ...
Title="Example", ...
Family="Examples", ...
AppVersion="1.0.0", ...
Updated="2026-07-19", ...
Requirements=labkit.contract.requirements("app", ">=2 <3"), ...
Workbench=example.workbench.buildLayout(), ...
ProjectSchema=example.projectSpec(), ...
CreateSession=@example.createSession, ...
PresentWorkbench=@example.workbench.present);
endRequired Definition arguments are product metadata, requirements, and one
labkit.app.layout.workbench value. Optional callbacks are:
| Argument | Signature | Purpose |
|---|---|---|
CreateSession |
session = callback(project,callbackContext) |
Rebuild transient App data from durable project state. |
PresentWorkbench |
view = callback(applicationState) |
Return the App-owned fragment of the complete visible snapshot. |
OnStart |
applicationState = callback(applicationState,callbackContext) |
Perform a real post-first-commit request or resource initialization. |
BuildSyntheticSample |
sample = callback(callbackContext) |
Build clean-room debug input when the App supports it. |
Ordinary default state needs no startup callback. Exact syntax and errors are in the generated public API reference.
launch("requirements") and launch("version") answer metadata without
creating a figure. launch() constructs the private native adapter and shows
the App.
+workbench/buildLayout.m returns a tree composed from
labkit.app.layout.* values. Layout IDs are stable semantic identifiers and
must be globally unique.
function layout = buildLayout()
controls = { ...
labkit.app.layout.section("parameters", "Parameters", { ...
labkit.app.layout.field("gain", ...
Label="Gain", Kind="numeric", ...
Bind="project.parameters.gain"), ...
labkit.app.layout.button("exportResult", ...
"Export", @example.resultFiles.exportResult, ...
Tooltip="Export the current analyzed result.")})};
workspace = labkit.app.layout.workspace( ...
labkit.app.layout.plotArea( ...
"previewPlot", @example.previewPlot.draw));
layout = labkit.app.layout.workbench( ...
controls, Workspace=workspace);
endLayout controls own direct callbacks. Plot areas own direct renderers. There is no handler, renderer, command, or capability registry for Apps to maintain. Definition compiles the immutable graph once and validates callback roles, renderer roles, IDs, bindings, and view capabilities before UI mutation.
Complex Apps keep the top-level workbench readable by composing
capability-owned layoutSection, workspaceTable, or workspacePlot
functions in user order.
Runtime state always has two App-owned buckets:
applicationState.project
applicationState.sessionproject is durable, validated meaning. session is transient and
reconstructible. A callback receives the previous complete state and returns a
candidate complete state. The runtime:
- serializes the event;
- invokes the direct callback;
- validates project and session shape;
- builds and validates the complete view snapshot;
- reconciles native components;
- publishes state and view together.
Failure rolls back both state and presentation and clears event-scoped resources. Apps do not implement busy flags, callback queues, readiness timers, or figure close guards.
Runtime enters its non-reentrant busy state before invoking a callback. New
button, field, table, file-list, workspace, and managed-interaction input is
ignored until that transaction finishes. Visible feedback is delayed briefly:
short callbacks therefore leave the pointer, title, and enabled appearance
untouched, while longer callbacks show the action's BusyMessage (or its
button label), switch to the busy pointer, and freeze mutable controls. The
committed Snapshot restores the final enabled state. User-facing log messages
emitted while that feedback is visible replace the current stage text, so an
App can report real named stages through its existing diagnostic timeline
without owning a second progress window.
Use direct Bind="project...." or Bind="session...." paths for ordinary
fields, ranges, sliders, file sources, and selection. Bound controls need no
callback or presenter operation unless the App has additional derived meaning.
labkit.app.layout.fileList owns file/folder selection, portable source
records, removal, clearing, and optional selection binding:
labkit.app.layout.fileList("sources", ...
Filters=["*.csv", "CSV files"], ...
Bind="project.inputs.sources", ...
SelectionBind="session.selection.sources", ...
SourceRole="measurement", ...
SourceIdPrefix="source")The App does not mirror those UI lifecycle actions with callbacks. Runtime
updates the bound source records and invokes CreateSession after source
changes:
function session = createSession(project, callbackContext)
paths = callbackContext.resolveSourcePaths(project.inputs.sources);
session = struct("measurements", example.sourceFiles.read(paths));
endPortable source records are opaque. Resolve their paths only at IO boundaries.
Saved projects store portable references and use runtime relinking. A project
Schema declares each durable source location with project-relative
SourceBindings, such as "inputs.sources"; an explicit empty list means the
project has no sources. Omitted bindings retain layout-derived inference for
older external App definitions, while built-in Apps use explicit declarations
so persistence does not depend on UI layout.
Callbacks are attached only where an App owns real behavior:
- button:
state = callback(state,callbackContext) - field, range, slider, workspace page, or interaction change:
state = callback(state,value,callbackContext) - table edit:
state = callback(state,labkit.app.event.TableCellEdit,callbackContext) - table selection:
state = callback(state,labkit.app.event.TableCellSelection,callbackContext)
Name the boundary values explicitly and delegate domain work through narrow inputs:
function applicationState = replaceGroupValue( ...
applicationState, cellEdit, callbackContext)
arguments
applicationState (1,1) struct
cellEdit (1,1) labkit.app.event.TableCellEdit
callbackContext (1,1) labkit.app.CallbackContext
end
groups = applicationState.project.groups;
applicationState.project.groups = ...
example.groupData.replaceValue(groups, ...
cellEdit.Row, cellEdit.Column, cellEdit.NewValue);
endDo not pass the complete state or callback context into calculation code that only needs groups and one edited value.
Runtime starts from layout defaults, bindings, file state, log text, and
status text. PresentWorkbench returns only derived App-owned operations:
function view = present(applicationState)
view = labkit.app.view.Snapshot();
view = view.include(example.previewPlot.present( ...
applicationState.session.measurements, ...
applicationState.project.parameters));
view = view.enabled("exportResult", ...
~isempty(applicationState.session.measurements));
endThe combined snapshot must cover every semantic target exactly as its layout
capabilities require. Snapshot.include composes feature-owned fragments
without opening a generic property-patch schema.
Plot presentation passes a prepared model to the renderer declared by its plot area:
view = view.renderPlot("previewPlot", model);function draw(axesById, model)
ax = axesById.main;
cla(ax);
plot(ax, model.x, model.y);
endRenderers own drawing and viewport policy, not workflow decisions or project mutation. Display-only graphics disable hit testing. Managed interaction specs own editable gestures and event-scoped resources.
For a multi-row plot dashboard, place multiple plot areas in one workspace
page. Page content is arranged vertically, while each plot area independently
chooses single, horizontal pair, or vertical stack. Two paired plot areas
therefore form a 2-by-2 dashboard without App-owned native containers.
ColumnWidths={'1x', 90} gives a pair a flexible main plot and a fixed-width
scale or histogram; RowHeights provides the analogous control for a stack.
top = labkit.app.layout.plotArea("topPlots", @drawTop, ...
Layout="pair", AxisIds=["image" "profile"]);
bottom = labkit.app.layout.plotArea("bottomPlots", @drawBottom, ...
Layout="pair", AxisIds=["result" "scale"], ...
ColumnWidths={'1x', 90});
workspace = labkit.app.layout.workspace(Title="Plots");
workspace = workspace.page("plots", "Plots", {top, bottom});
workspace = workspace.initialPage("plots");Declare managed gestures statically on their plot area and provide their current value in the snapshot:
crop = labkit.app.interaction.rectangle( ...
"cropRegion", @example.cropGeometry.moveCrop);
plot = labkit.app.layout.plotArea( ...
"previewPlot", @example.previewPlot.draw, Interactions={crop});view = view.rectangle( ...
"cropRegion", project.annotations.crop, ImageSize=size(image));Named contracts also cover anchor paths, paired anchors, fixed point slots,
transient region selection, intervals, and scale references. Apps never author
Kind, Targets, Event, or Options transport structs.
Renderer mechanics such as complete clears, empty-state messages, fitting,
fixed-aspect canvases, and axes-relative annotation placement live under
labkit.app.plot.*. Apps own when those operations occur, user wording, and
whether a semantic change should preserve or fit the viewport.
labkit.app.CallbackContext is sealed and exposes specifically named runtime
operations for dialogs, status and diagnostics, portable sources, project
documents, result packages, render surfaces, and managed resources. It does
not expose figures, component registries, queues, lifecycle handles, or a
nested service bag.
Use callbackContext.inform(message,title) for successful or neutral
information; it presents the native information icon. Reserve
callbackContext.alert(message,title) for a blocking problem; it presents the
native error icon. Keeping these operations distinct prevents completed INFO
outcomes from inheriting failure styling.
Use context methods only at a callback or reconstruction boundary. Pure readers, calculations, result builders, and render-model builders accept ordinary explicit values.
callbackContext.chooseOption(prompt, choices, ...) owns ordinary native
confirmation choices. Title controls the dialog title, DefaultChoice
selects the Enter-key action, and CancelChoice is returned when the user
dismisses the dialog. All three named choices must be members of the declared
nonempty unique choice row. File and folder methods remain separate because
they return paths and use platform file choosers. Successful input and output
choices are remembered separately across App windows. A valid App-supplied
start path takes precedence; cancellation or an invalid path does not replace
the last successful folder.
An App-specific project button may choose a MAT file and return
callbackContext.restoreProjectDocument(filepath). The context prepares the
same migrated, relinked project/session candidate used by the framework Load
State menu; the active callback transaction still owns validation, native
presentation, rollback, document metadata, and title publication.
callbackContext.newProjectDocument() similarly returns the schema's fresh
project/session state and publishes a new unsaved document identity only when
that callback transaction commits.
Every ordinary App launch starts one sanitized session event stream and durable journal. Launch arguments do not select a debug mode, change startup behavior, or generate sample data. Runtime automatically records lifecycle, callback, transaction, dialog, project, source, result, and failure boundaries with correlated operation IDs.
App callbacks add domain events through
callbackContext.log(severity,eventName,message,Name=Value). Use info for
useful progress and completed user actions, warning for recoverable
conditions, and error or critical for failures. Stable event names and
structured allowlisted attributes support diagnosis; messages remain concise
and safe for display. Pass caught exceptions through the dedicated
Exception option instead of copying stack, path, identifier, or scientific
data into free text.
The App's Tools > Diagnostics menu opens the live session viewer and exports a diagnostic bundle from the same session history. Each viewer title names the App that owns the session. Its single Level selector has three modes: Full TRACE displays every retained record, DEBUG hides trace-only stages, and User shows user-audience INFO and higher events. Full TRACE is the default view; it does not manufacture detail that was not captured. The Action filter groups a top-level user or lifecycle action with its nested callback, presentation, dialog, resource, and transaction records.
Runtime initially captures DEBUG and higher records to bound ordinary-session cost. The first ERROR or CRITICAL event automatically enables TRACE for later activity. The viewer also provides an explicit Enable TRACE / Disable TRACE control when a user needs detailed capture before an error. TRACE adds callback state-update and validation stages, App/runtime presentation stages, native presentation commit, and post-failure rollback cleanup; DEBUG retains operation start and terminal boundaries. Enabling TRACE never reconstructs earlier detail.
Export Diagnostic Bundle writes directly to ignored
artifacts/diagnostics/ with a generated App-specific, timestamped, unique ZIP
name. Every bundle contains complete sensitive events, attributes, exception
messages, stack locations, and App state. Complete bundle (exact MAT)
writes app-state.mat unchanged. Complete bundle (compact synthetic MAT)
writes app-state-compact.mat: Runtime recursively reviews state containers
and replaces supported numeric, logical, character, or string leaves larger
than 1 MiB with deterministic compressible placeholders of the same class and
dimensions. It preserves smaller parameters, annotations, results, and caches.
bundle-report.json names structural state paths and sizes for every
replacement without retaining the replaced values; it also lists oversized
unsupported leaf types that had to remain exact. Compact state is diagnostic
evidence, not scientifically valid input. Both modes may contain sensitive
paths, filenames, scientific values, and decoded data; neither is a privacy
filter. Compact is the default; exact remains an explicit choice. If the
session records any ERROR or CRITICAL event, Runtime automatically writes a
compact bundle after the App closes, including the completed close lifecycle
event. A clean session does not create a bundle on close.
If ZIP staging or publication fails, Runtime writes a generated complete-event
text fallback beside that ZIP. Only when automatic output cannot be written
does it ask for another location, with the generated fallback filename already
filled in. Text cannot represent either MAT state and says so explicitly. The
success or fallback alert reports the complete destination path.
Journal degradation remains visible in the surviving in-memory stream; logging
failures never alter callback transaction semantics or scientific results.
A callback exception is recorded as an ERROR with failed operation result,
rollback disposition, safe exception identifier, and sanitized function stack.
Runtime close is also an instrumented lifecycle operation. Resource and native adapter cleanup continue independently; a cleanup exception is retained and persisted before the journal closes, then returned to the caller. Diagnostics cannot manufacture evidence for a native event that never entered Runtime, an exception swallowed by App code without logging, or a MATLAB process that hangs or terminates before a terminal event. In those cases the last retained DEBUG boundary and durable journal state are the available evidence.
An App that declares BuildSyntheticSample exposes Tools > Developer
Tools > Generate Synthetic Inputs.... The action writes an anonymous,
validated labkit.app.synthetic.Pack and synthetic-input-pack.json into a
new folder beneath the selected destination. Generation does not load the
pack, mutate the open project, or suppress OnStart; every App launch follows
the same clean startup path. Users deliberately import the generated files
through the App's ordinary controls.
labkit.app.project.Schema owns current project creation, validation, and
ordered version migration. Runtime owns the project envelope, atomic save,
restore, recovery, and relinking loop.
After a document is saved or accepted from restore, Runtime fingerprints the on-disk file. Saving again to the same path is rejected if another program has changed that file; Save As remains available. This prevents a stale App window from silently overwriting external edits without changing the project payload format.
Every save writes exactly one labkitProject variable using the current App
payload version. A restore accepts an older payload only when the App's current
schema declares one Migrate(project, fromVersion) callback; Runtime invokes
it once for each missing version in order and then validates the current
payload. A payload newer than the running App is rejected rather than guessed
at.
An App may declare an exact legacy MAT variable name in LegacyImports when
real user files still require a one-way import. That callback converts the
legacy value directly to the current project and optional resume state. It is
read-only: current saves never write the legacy variable, and Runtime contains
no App ID, field-shape, or filename heuristics.
These readers are supported data contracts while their Apps declare and test them. They are not an excuse for duplicate live state fields or old runtime APIs. Removing a supported payload migration or importer is an explicit breaking saved-data decision; adding one requires App-owned persistence evidence plus a runtime restore test for the framework mechanism.
labkit.app.result.File and labkit.app.result.Package describe App-owned
outputs. CallbackContext.writeResultPackage writes through the runtime so
source and project provenance remain consistent.
Resources have event, interaction, document, or application scope. Replacing the same scope and ID is idempotent; the runtime cleans every surviving resource on scope end or close.
Use focused contract tests for Definition, layout, callbacks, snapshots, project schema, and runtime transactions. Add downstream App tests for changed behavior and a bounded hidden-GUI test for native wiring. Automated hidden GUI tests do not prove dialog quality, pointer feel, scientific validity, or a complete interactive workflow.
See Testing and Build A Complete App.