Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .agents/skills/labkit-test-planner/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Tests are owner-first behavioral specifications beneath `tests/specs/`; no
caller infers test paths, stage tags, suite roots, ranges, or runner options.

1. Begin with `labkittest.explain(SOURCE_FILE)` for a production change.
2. Add evidence with `labkittest.createSpec(SOURCE_FILE, Name=...)`. Specify
2. Add evidence with `labkittest.createSpec(SOURCE_FILE, Name=..., Reason=...)`.
Reason starts with `Regression`, `Invariant`, or `Compatibility`; specify
`Contract` only when `explain` reports multiple author-owned boundaries.
3. Iterate with `labkittest.run(Owner=..., Contract=...)`, or use
`labkittest.run(File=...)` for the catalog's complete bounded closure.
Expand Down Expand Up @@ -51,8 +52,9 @@ startup, licensing, reporting, progress, and failure-diagnosis costs.
- `buildtool isolated` runs every `Env:path-isolated` identity.
- `buildtool coverage` adds Cobertura XML and HTML coverage to headless.
- `buildtool changedFast` maps local Git paths to exact evidence closures. A
framework, build, policy, or unknown path safely widens to every automated
environment.
framework, build, or policy path selects explicit system evidence;
documentation is owned by `docsCheck`; an unknown path fails planning until
it has a declared validation owner or explicit ignore reason.
- Hidden GUI proves declared structural wiring, not native dialogs, visual
quality, pointer feel, real-data suitability, or scientific validity.

Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,39 @@ jobs:
artifacts/debug/**
artifacts/gui/**
artifacts/logs/**

docs-check:
name: Documentation check
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up clean MATLAB runtime
uses: matlab-actions/setup-matlab@v3
with:
release: ${{ env.MATLAB_RELEASE }}

- name: Verify generated documentation
uses: matlab-actions/run-build@v3
with:
tasks: docsCheck

ci-gate:
name: CI Gate
if: always()
needs:
- headless-platforms
- gui-platforms
- isolated-platforms
- docs-check
runs-on: ubuntu-latest
steps:
- name: Require all validation profiles
shell: bash
run: |
test '${{ needs.headless-platforms.result }}' = 'success'
test '${{ needs.gui-platforms.result }}' = 'success'
test '${{ needs.isolated-platforms.result }}' = 'success'
test '${{ needs.docs-check.result }}' = 'success'
42 changes: 28 additions & 14 deletions docs/development/maintain-and-release/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ spec without inventing a path, metadata, class shell, or runner command:
```matlab
labkittest.createSpec( ...
"apps/electrochem/cic/+cic/+analysisRun/computeCIC.m", ...
Contract="scientific", Name="PulseWindow")
Contract="scientific", Name="PulseWindow", ...
Reason="Regression: pulse-window bounds must remain stable.")
```

`Contract` is needed only when one source has more than one author-owned
boundary. The generated file contains the required `Contract:<name>` and
`Env:<name>` tags plus an intentional failing placeholder. Replace that
placeholder with a small behavioral proof. Never create a test by guessing a
folder, a suite range, a test tag, or a wrapper class.
boundary. `Reason` starts with `Regression`, `Invariant`, or `Compatibility`;
it records the durable behavior being protected, rather than a temporary
implementation detail. The generated file contains the required
`Contract:<name>` and `Env:<name>` tags plus an intentional failing placeholder.
Replace that placeholder with a small behavioral proof. Remove or revise a
specification when that stated behavior is intentionally retired. Never create
a test by guessing a folder, a suite range, a test tag, or a wrapper class.

During an edit, execute the narrow owner and contract that changed:

Expand Down Expand Up @@ -107,12 +111,19 @@ buildtool docsCheck
| `coverage` | Headless catalog with Cobertura XML and HTML coverage artifacts. |
| `docs` / `docsCheck` | Render or verify the generated documentation site. |

`changedFast` prints semantic reasons and exact identities. For ordinary App
and facade source it runs only the required contract closure. A framework,
Build, catalog, policy, or unknown path deliberately widens to every automated
environment: headless, hidden-GUI, and path-isolated. Broad selection is a
visible safety boundary, not a planner failure. Do not weaken that fallback to
make a route count look smaller.
`changedFast` prints whether its plan is `focused-local` or `full-profile`,
semantic reasons, exact identities, and any explicitly ignored paths. For
ordinary App and facade source it runs only the required contract closure.
Framework, Build, catalog, and repository-policy paths select explicit bounded
system evidence. Documentation and generated-site paths are explicitly ignored
because `docsCheck` owns their consistency. An unknown path is a planning error:
declare its production role or an explicit no-test reason; do not hide missing
ownership by widening the run.

Use `labkittest.explainChanged` to inspect that decision without executing
tests. It prints each changed path's classification, selected evidence, and any
manual boundary. A focused-local result is rapid author feedback, not merge
safety evidence; CI runs the full platform profiles.

Run focused behavior during iteration. Run `changedFast` once when the branch
is ready for review or direct-main integration. CI owns broad platform
Expand Down Expand Up @@ -148,9 +159,12 @@ migration, structural-GUI, or workflow proof.
## CI and Manual Evidence

Continuous Integration runs `headless`, `gui`, and `isolated` on Linux, macOS, and Windows
from a clean MATLAB runtime without optional Toolboxes. It uploads the catalog
artifacts even after failure. Coverage is an explicit report, not a duplicate
CI gate.
from a clean MATLAB runtime without optional Toolboxes. It also runs `docsCheck`
once, then reports one aggregate `CI Gate` result that depends on every required
profile. Configure repository branch protection to require `CI Gate`; the
workflow does not silently replace repository protection policy. It uploads the
catalog artifacts even after failure. Coverage is an explicit report, not a
duplicate CI gate.

Manual App validation remains required for native file dialogs, visual design,
pointer interaction, real-data suitability, and scientific interpretation.
Expand Down
2 changes: 1 addition & 1 deletion site/assets/search-index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion site/assets/search-index.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions site/development/maintain-and-release/testing.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ <h2 id="everyday-workflow">Everyday Workflow</h2>
<p><code>explain</code> prints the exact specification folders, contracts, environments, selected identities, and the reason for every selection. Add a new behavioral spec without inventing a path, metadata, class shell, or runner command:</p>
<pre><code class="language-matlab">labkittest.createSpec( ...
&quot;apps/electrochem/cic/+cic/+analysisRun/computeCIC.m&quot;, ...
Contract=&quot;scientific&quot;, Name=&quot;PulseWindow&quot;)</code></pre>
<p><code>Contract</code> is needed only when one source has more than one author-owned boundary. The generated file contains the required <code>Contract:&lt;name&gt;</code> and <code>Env:&lt;name&gt;</code> tags plus an intentional failing placeholder. Replace that placeholder with a small behavioral proof. Never create a test by guessing a folder, a suite range, a test tag, or a wrapper class.</p>
Contract=&quot;scientific&quot;, Name=&quot;PulseWindow&quot;, ...
Reason=&quot;Regression: pulse-window bounds must remain stable.&quot;)</code></pre>
<p><code>Contract</code> is needed only when one source has more than one author-owned boundary. <code>Reason</code> starts with <code>Regression</code>, <code>Invariant</code>, or <code>Compatibility</code>; it records the durable behavior being protected, rather than a temporary implementation detail. The generated file contains the required <code>Contract:&lt;name&gt;</code> and <code>Env:&lt;name&gt;</code> tags plus an intentional failing placeholder. Replace that placeholder with a small behavioral proof. Remove or revise a specification when that stated behavior is intentionally retired. Never create a test by guessing a folder, a suite range, a test tag, or a wrapper class.</p>
<p>During an edit, execute the narrow owner and contract that changed:</p>
<pre><code class="language-matlab">labkittest.run(Owner=&quot;apps/electrochem/cic/analysisRun&quot;, ...
Contract=&quot;scientific&quot;)</code></pre>
Expand All @@ -58,7 +59,8 @@ <h2 id="build-tasks">Build Tasks</h2>
buildtool docs
buildtool docsCheck</code></pre>
<div class="table-wrap"><table><thead><tr><th>Task</th><th>Purpose</th></tr></thead><tbody><tr><td><code>changedFast</code></td><td>Local final pre-commit/pre-push gate. Reads tracked and untracked working-tree paths; on a clean checkpoint it reads <code>HEAD^..HEAD</code>.</td></tr><tr><td><code>headless</code></td><td>Every headless catalog identity.</td></tr><tr><td><code>gui</code></td><td>Every hidden-GUI catalog identity.</td></tr><tr><td><code>isolated</code></td><td>Every path-isolated catalog identity.</td></tr><tr><td><code>coverage</code></td><td>Headless catalog with Cobertura XML and HTML coverage artifacts.</td></tr><tr><td><code>docs</code> / <code>docsCheck</code></td><td>Render or verify the generated documentation site.</td></tr></tbody></table></div>
<p><code>changedFast</code> prints semantic reasons and exact identities. For ordinary App and facade source it runs only the required contract closure. A framework, Build, catalog, policy, or unknown path deliberately widens to every automated environment: headless, hidden-GUI, and path-isolated. Broad selection is a visible safety boundary, not a planner failure. Do not weaken that fallback to make a route count look smaller.</p>
<p><code>changedFast</code> prints whether its plan is <code>focused-local</code> or <code>full-profile</code>, semantic reasons, exact identities, and any explicitly ignored paths. For ordinary App and facade source it runs only the required contract closure. Framework, Build, catalog, and repository-policy paths select explicit bounded system evidence. Documentation and generated-site paths are explicitly ignored because <code>docsCheck</code> owns their consistency. An unknown path is a planning error: declare its production role or an explicit no-test reason; do not hide missing ownership by widening the run.</p>
<p>Use <code>labkittest.explainChanged</code> to inspect that decision without executing tests. It prints each changed path's classification, selected evidence, and any manual boundary. A focused-local result is rapid author feedback, not merge safety evidence; CI runs the full platform profiles.</p>
<p>Run focused behavior during iteration. Run <code>changedFast</code> once when the branch is ready for review or direct-main integration. CI owns broad platform validation; do not repeatedly run broad tasks after each small edit.</p>
<h2 id="artifacts-and-failures">Artifacts and Failures</h2>
<p>Each run writes one folder beneath <code>artifacts/test-results/&lt;run-name&gt;/</code>:</p>
Expand All @@ -73,7 +75,7 @@ <h2 id="artifacts-and-failures">Artifacts and Failures</h2>
<p>After a failure, copy its exact class/method identity from the output and run the smallest owner/contract or exact source that proves the repair. A zero selection or missing-contract error is a test-authoring defect, never passing evidence.</p>
<p>When a mapped layout change leaves a non-automatable boundary, its plan can name a manual check. It is printed and recorded in <code>plan.json</code>, but it never makes an automated run pass. Manual checks are limited to native dialogs, pointer behavior, visual design, real-data suitability, and scientific interpretation; they cannot replace an automated calculation, state, export, migration, structural-GUI, or workflow proof.</p>
<h2 id="ci-and-manual-evidence">CI and Manual Evidence</h2>
<p>Continuous Integration runs <code>headless</code>, <code>gui</code>, and <code>isolated</code> on Linux, macOS, and Windows from a clean MATLAB runtime without optional Toolboxes. It uploads the catalog artifacts even after failure. Coverage is an explicit report, not a duplicate CI gate.</p>
<p>Continuous Integration runs <code>headless</code>, <code>gui</code>, and <code>isolated</code> on Linux, macOS, and Windows from a clean MATLAB runtime without optional Toolboxes. It also runs <code>docsCheck</code> once, then reports one aggregate <code>CI Gate</code> result that depends on every required profile. Configure repository branch protection to require <code>CI Gate</code>; the workflow does not silently replace repository protection policy. It uploads the catalog artifacts even after failure. Coverage is an explicit report, not a duplicate CI gate.</p>
<p>Manual App validation remains required for native file dialogs, visual design, pointer interaction, real-data suitability, and scientific interpretation. Use synthetic, minimal fixtures in automated tests. Never add real lab files, local paths, subject/device identifiers, timestamps, or proprietary metadata.</p>
<h2 id="maintainer-rules">Maintainer Rules</h2>
<ul>
Expand Down
125 changes: 125 additions & 0 deletions tests/+labkittest/classifyPath.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
function classification = classifyPath(file)
%CLASSIFYPATH Classify one repository path for LabKit validation planning.
% CLASSIFICATION = labkittest.classifyPath(FILE) returns a stable structural
% classification for one repository-relative path. Kind is "mapped" when
% the catalog owns automated evidence, "ignored" when a named non-test
% maintainer task owns the path, or "unknown" when the path has no declared
% validation ownership. Unknown is intentionally not a conservative fallback:
% callers must define the production role or explicitly classify the path.

file = normalizePath(file);
parts = split(file, "/");
classification = unknown(file, ...
"No validation ownership is declared for this repository path.");

if startsWith(file, "tests/specs/")
[folder, ~, ~] = fileparts(char(file));
owner = extractAfter(string(folder), "tests/specs/");
classification = mapped(file, "specification", owner, ...
"changed specification selects its physical owner");
return;
end
if startsWith(file, "tests/+labkittest/") || file == "buildfile.m" || ...
startsWith(file, ".github/")
classification = mapped(file, "validation-framework", "system/build", ...
"validation framework or CI policy behavior");
return;
end
if startsWith(file, ".agents/") || file == "AGENTS.md" || ...
file == "tests/AGENTS.md" || file == ".gitignore"
classification = mapped(file, "repository-policy", "system/repository", ...
"repository policy behavior");
return;
end
if startsWith(file, "docs/") || startsWith(file, "site/") || ...
startsWith(file, "tools/docs/")
classification = ignored(file, "documentation", ...
"documentation source or generated output; docsCheck owns consistency");
return;
end
if startsWith(file, "artifacts/") || startsWith(file, ".Trash/") || ...
startsWith(file, ".DS_Store")
classification = ignored(file, "generated-artifact", ...
"generated local artifact; no repository validation is required");
return;
end
if startsWith(file, "tests/+testfixtures/")
classification = mapped(file, "test-fixture", "system/build", ...
"cross-owner synthetic fixture behavior");
return;
end
if startsWith(file, "+labkit/") && numel(parts) >= 2 && startsWith(parts(2), "+")
area = erase(parts(2), "+");
if ismember(area, ["app", "biosignal", "contract", "dta", "image", "rhs", "thermal"])
classification = mapped(file, "framework-facade", "framework/" + area, ...
"direct public facade behavior");
return;
end
classification = unknown(file, "Unrecognized LabKit facade area: " + area + ".");
return;
end
if startsWith(file, "apps/") && numel(parts) == 4 && ...
startsWith(parts(4), "labkit_") && endsWith(parts(4), "_app.m")
classification = mapped(file, "app-launcher", "", ...
"public App launcher conformance");
return;
end
if startsWith(file, "apps/") && numel(parts) >= 5 && startsWith(parts(4), "+")
capability = "";
if numel(parts) >= 6 && startsWith(parts(5), "+")
capability = erase(parts(5), "+");
end
app = erase(parts(4), "+");
family = parts(2);
owner = "apps/" + family + "/" + app;
if numel(parts) == 5
name = lower(parts(5));
switch name
case "definition.m"
role = "app-definition";
case "projectspec.m"
role = "app-project";
case "createsession.m"
role = "app-session";
otherwise
role = "app-product-root";
end
classification = mapped(file, role, owner, ...
"App structural role with explicit evidence policy");
return;
end
if strlength(capability) == 0
classification = unknown(file, ...
"Recognized App " + app + " but no capability package is declared.");
return;
end
classification = mapped(file, "app-capability", owner + "/" + capability, ...
"App capability role with explicit evidence policy");
return;
end
end

function value = mapped(file, role, owner, reason)
value = valueFor(file, "mapped", role, owner, reason);
end

function value = ignored(file, role, reason)
value = valueFor(file, "ignored", role, "", reason);
end

function value = unknown(file, reason)
value = valueFor(file, "unknown", "", "", reason);
end

function value = valueFor(file, kind, role, owner, reason)
value = struct("Path", string(file), "Kind", string(kind), "Role", string(role), ...
"Owner", string(owner), "Reason", string(reason));
end

function value = normalizePath(value)
value = strip(replace(string(value), "\\", "/"));
if strlength(value) == 0 || startsWith(value, "/") || contains(value, "..") || contains(value, ":")
error("LabKit:TestLocation:InvalidPath", ...
"File must be a repository-relative path.");
end
end
Loading