From 934dcb08ade1f6eb86eeeb94d55638d15a671bfc Mon Sep 17 00:00:00 2001 From: Pluze Zhu Date: Fri, 5 Jun 2026 16:30:59 -0500 Subject: [PATCH] docs: converge governance guidance --- +labkit/AGENTS.md | 12 +- .agents/skills/labkit-app-builder/SKILL.md | 27 ++-- .agents/skills/labkit-boundary-guard/SKILL.md | 18 +-- .agents/skills/labkit-test-planner/SKILL.md | 31 +--- AGENTS.md | 21 +-- README.md | 19 +-- apps/AGENTS.md | 17 +-- docs/README.md | 15 +- docs/apps.md | 16 +- docs/architecture.md | 33 ++++- docs/testing.md | 3 +- docs/ui.md | 8 +- tests/AGENTS.md | 3 +- .../project/ProjectDebtGuardrailTest.m | 140 ++++++++++++++++++ .../ProjectDocumentationGuardrailTest.m | 97 +++++++++++- 15 files changed, 334 insertions(+), 126 deletions(-) diff --git a/+labkit/AGENTS.md b/+labkit/AGENTS.md index 6375150..d1006b9 100644 --- a/+labkit/AGENTS.md +++ b/+labkit/AGENTS.md @@ -34,11 +34,7 @@ ## Validation Routing -- Always run `buildtool testProject` for package boundary or public surface changes. -- DTA changes: also run `buildtool testLabkitDta`; add - `buildtool testAppsElectrochem` when app-facing behavior may be affected. -- Biosignal changes: also run `buildtool testLabkitBiosignal`; add - `buildtool testAppsWearableGui` when app-facing behavior may be affected. -- UI changes: also run `buildtool testLabkitUi`; add - `buildtool testLabkitUiGui testAppsGui` for layout, launch, callback, or app - shell changes. +Package boundary or public surface changes should include project guardrails. +Add the focused DTA, biosignal, or UI task for the touched facade, and add +downstream app-family tasks when the app-facing contract may be affected. Use +`docs/testing.md` for exact task names and GUI/non-GUI pairings. diff --git a/.agents/skills/labkit-app-builder/SKILL.md b/.agents/skills/labkit-app-builder/SKILL.md index e7aa027..b4b8490 100644 --- a/.agents/skills/labkit-app-builder/SKILL.md +++ b/.agents/skills/labkit-app-builder/SKILL.md @@ -14,6 +14,8 @@ The target shape is: - one launchable app entry point under `apps//` - reusable UI/data boilerplate behind `labkit.ui`, `labkit.dta`, or `labkit.biosignal` - domain formulas, plot choices, result fields, export schemas, and workflow wording owned by the app +- app-owned helper packages under `apps///+/...` + when the app needs extracted production helpers - synthetic tests for core calculations and export contracts ## Required Read Order @@ -109,24 +111,19 @@ Build the app in this order: 3. Store state in one app struct; avoid globals, base workspace state, and hidden local paths. 4. Rebuild the user workflow around stable controls, previews, summaries, and exports; do not reproduce command-line debug staging. 5. Move GUI-free calculations below the app `end` as app-local functions. -6. Add narrow internal test handlers only for app-owned GUI-free helpers or explicit structural diagnostics. -7. Render prepared data through `labkit.ui` helpers; keep analysis out of UI helpers. -8. Add export builders before CSV/PNG writing so output contracts can be tested. -9. Add focused tests with synthetic fixtures or minimal generated data. -10. Update human docs for user-facing behavior and scoped `AGENTS.md` only when rules change. +6. Extract production helpers into an app-owned package when the app is too + large for a readable single entry point. +7. Do not add new `private/` runners, `*Workflow.m` string-dispatch adapters, + fixed `+app` package names, or app-local public helper packages. +8. Render prepared data through `labkit.ui` helpers; keep analysis out of UI helpers. +9. Add export builders before CSV/PNG writing so output contracts can be tested. +10. Add focused tests with synthetic fixtures or minimal generated data. +11. Update human docs for user-facing behavior and scoped `AGENTS.md` only when rules change. ## Validation -Use `labkit-test-planner` to choose build tasks. Common checks: - -```bash -buildtool testProject -buildtool testAppsElectrochem -buildtool testAppsDicGui -buildtool testAppsImageMeasurementGui -buildtool testAppsWearableGui -buildtool testLabkitUiGui testAppsGui -``` +Use `labkit-test-planner` to choose source-aligned validation. It should route +to `docs/testing.md` for exact build-task names and GUI/non-GUI pairings. For reusable facade changes, also use `labkit-boundary-guard`. diff --git a/.agents/skills/labkit-boundary-guard/SKILL.md b/.agents/skills/labkit-boundary-guard/SKILL.md index f1cf6bb..f78c0d7 100644 --- a/.agents/skills/labkit-boundary-guard/SKILL.md +++ b/.agents/skills/labkit-boundary-guard/SKILL.md @@ -39,20 +39,10 @@ For UI boundary work, prefer `labkit.ui.app.createShell`, `labkit.ui.app.dispatc ## Validation -Always run or recommend: - -```bash -buildtool testProject -``` - -Add focused tasks by touched boundary: - -```bash -buildtool testLabkitDta testAppsElectrochem -buildtool testLabkitBiosignal testAppsWearableGui -buildtool testLabkitUiGui testAppsGui -buildtool testAppsElectrochem -``` +Run or recommend project guardrails for package-boundary and public-surface +changes. Add focused DTA, biosignal, UI, or app-family validation when that +boundary is touched, and use `docs/testing.md` for exact task names and +pairings. If MATLAB is unavailable, report that clearly and do not claim tests passed. diff --git a/.agents/skills/labkit-test-planner/SKILL.md b/.agents/skills/labkit-test-planner/SKILL.md index a7d9853..ead2da4 100644 --- a/.agents/skills/labkit-test-planner/SKILL.md +++ b/.agents/skills/labkit-test-planner/SKILL.md @@ -18,39 +18,24 @@ Choose visible, source-aligned validation without overstating coverage. ## Task Routing -Use the smallest set that covers the touched boundary: +Use the smallest source-aligned validation set that covers the touched +boundary. `docs/testing.md` owns the canonical build-task names, wrappers, and +command examples. ```text project startup, architecture, package surface, sample-data hygiene labkit/dta DTA parser, facade, session, item, pulse behavior labkit/biosignal biosignal import, processing, ECG peaks, segments, measurements -labkit/ui reusable UI helpers; use testLabkitUiGui for layout/callback/shell/debug checks +labkit/ui reusable UI helpers; include GUI coverage for layout/callback/shell/debug checks apps/electrochem electrochem app-owned calculations, exports, layout -apps/dic DIC app layout; usually testAppsDicGui +apps/dic DIC app layout apps/image_measurement image measurement calculations, exports, layout -apps/wearable wearable app layout; usually testAppsWearableGui +apps/wearable wearable app layout apps/smoke cross-app noninteractive launch checks ``` -Pair reusable changes with downstream apps when the app-facing contract could be affected: - -```bash -buildtool testLabkitDta testAppsElectrochem -buildtool testLabkitBiosignal testAppsWearableGui -buildtool testLabkitUiGui testAppsGui -``` - -Use the default non-GUI build task for broad changes: - -```bash -buildtool test -``` - -On Windows, use: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run_matlab_tests.ps1 testProject -``` +Pair reusable changes with downstream apps when the app-facing contract could +be affected. Use the default non-GUI task for broad changes. ## GUI Claims diff --git a/AGENTS.md b/AGENTS.md index 092b33b..c0ccea2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,23 +80,10 @@ When using local lab files to reproduce a bug: Run relevant automated checks after executable MATLAB, test, fixture, package, or validation-rule changes. Use focused checks during iteration and the default non-GUI build task for broad changes. -Common commands: - -```bash -buildtool testProject -buildtool testLabkitDta -buildtool testLabkitBiosignal -buildtool testLabkitUiGui testAppsGui -buildtool test -``` - -On Windows PowerShell: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run_matlab_tests.ps1 testProject -matlab -batch "buildtool checkProject" -matlab -batch "buildtool packageDryRun" -``` +Use `docs/testing.md` as the canonical command matrix for build tasks, wrapper +behavior, CI scope, fixture expectations, and GUI validation limits. Scoped +`AGENTS.md` files should only route by ownership and should not duplicate the +full task list. Interactive GUI workflows are checked manually by the user. Do not run interactive GUI workflows in MATLAB `-batch` mode. If MATLAB cannot run, report the blocker and do not claim tests passed. diff --git a/README.md b/README.md index 2e31d8d..3eeaea3 100644 --- a/README.md +++ b/README.md @@ -94,23 +94,8 @@ The local scripts are optional wrappers around build tasks: .\scripts\run_matlab_tests.ps1 test ``` -Focused checks are available during development: - -```bash -buildtool checkStyle -buildtool checkProject -buildtool packageDryRun -buildtool testLabkitDta -buildtool testLabkitBiosignal -buildtool testAppsWearableGui -buildtool testLabkitUiGui testAppsGui -``` - -Script arguments are build task names; selector flags such as `--suite`, -`--test`, and `--gui` are not supported. GitHub Actions runs shell-wrapper, -quality, unit, and integration jobs on pushes and pull requests to `main`; -manual and scheduled runs add coverage, GUI structural, and non-blocking -gesture jobs. +See `docs/testing.md` for the focused build-task matrix, wrapper details, CI +scope, and GUI validation limits. ## Repository Layout diff --git a/apps/AGENTS.md b/apps/AGENTS.md index bf0c308..013b8ca 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -29,6 +29,9 @@ Apps are first-class deliverables. Do not treat them as examples for a hidden pl - Callback-heavy migrated apps should move app-owned production code into these package components instead of adding new `private/` runners or string-dispatch workflow adapters. +- Do not add new `*Workflow.m` files or app-owned `+core/dispatch.m` string + routers. The existing electrochemistry dispatch files are temporary migration + debt documented in `docs/architecture.md`. - When a public app file grows large, prefer moving GUI-free app-owned calculations, export builders, formatting utilities, deterministic image/signal transforms, and focused control construction into `apps///+/...`. - Do not add new `apps//private/` helpers unless the helper is genuinely shared by multiple apps in that family and the user approves that family-level boundary. - Keep the public app entry point responsible for GUI state, callbacks, user alerts, app workflow order, debug launch routing, and user-facing log wording. @@ -42,12 +45,8 @@ Apps are first-class deliverables. Do not treat them as examples for a hidden pl ## Validation Routing -- Electrochem app change: `buildtool testAppsElectrochem`; use - `buildtool testAppsElectrochemGui` for layout, launch, or callback wiring. -- DIC app change: `buildtool testAppsDicGui`. -- Image measurement app change: `buildtool testAppsImageMeasurement`; use - `buildtool testAppsImageMeasurementGui` for layout, launch, or callback wiring. -- Wearable app change: `buildtool testAppsWearableGui`; add - `buildtool testLabkitBiosignal` when the biosignal facade contract may be - affected. -- App entrypoint or boundary changes also run `buildtool testProject`. +Route validation by the touched app family and whether the change affects pure +logic/export behavior, layout/callback wiring, or app-entrypoint boundaries. +Use `docs/testing.md` for exact task names and pairings. App entrypoint, +ownership-boundary, fixture, or validation-rule changes should include the +project guardrail task. diff --git a/docs/README.md b/docs/README.md index 7b1180f..24d0721 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,14 +2,25 @@ These docs are written for people who run, maintain, or extend LabKit. Start with the page that matches the work in front of you; you do not need to read the whole documentation set for everyday app use. +## Documentation Responsibilities + +| Source | Owns | +| --- | --- | +| `../README.md` | Project overview, app launch list, and the default validation entry point. | +| `*.md` component docs | Human-readable behavior, architecture, public APIs, and maintenance contracts. | +| `testing.md` | The canonical build-task matrix, wrapper behavior, CI scope, fixture expectations, and GUI validation limits. | +| `../AGENTS.md` and scoped `AGENTS.md` files | Future execution rules for agent work, ownership red lines, and routing rules. | +| `../.agents/skills/` | Task procedures for boundary checks, app building, and validation planning. | +| `../tests/integration/project/` guardrails | Automated checks that keep package boundaries, documentation ownership, debt inventory, and sample hygiene from drifting. | + ## I Want To Run An App -- `../README.md`: project overview, startup command, app list, and basic test commands. +- `../README.md`: project overview, startup command, app list, and default validation entry point. - `apps.md`: current app families, what each app does, expected inputs, and typical outputs. ## I Maintain An App -- `apps.md`: app ownership, current app notes, new-app checklist, and validation guidance. +- `apps.md`: app ownership, current app notes, and new-app checklist. - `ui.md`: shared GUI shell, tabs, panels, axes, and reusable UI helper contracts. - `testing.md`: focused app and GUI structural build-task commands. diff --git a/docs/apps.md b/docs/apps.md index 378d9b3..7925451 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -65,7 +65,7 @@ The app owns: Every public app entry point should preserve its launch name, route debug launch requests through `labkit.ui.app.dispatchRequest`, build the GUI with `labkit.ui.app.createShell`, and keep visible debug trace wired into the Log tab during debug launches. Image apps with drawing, scale bars, ROI, or preview scroll should pass a `labkit.ui.tool.createRuntime` result into reusable tools instead of owning figure pointer callbacks directly. -Move code into `+labkit` only when it is reusable without app vocabulary, testable independently, and useful beyond one workflow. When a documented UI tool owns app-neutral interaction mechanics, the app should consume that tool and keep workflow meaning, summaries, and exports app-local. +When a documented UI tool owns app-neutral interaction mechanics, the app should consume that tool and keep workflow meaning, summaries, and exports app-local. `docs/architecture.md` owns the reusable-library extraction rule and temporary debt inventory. ## App File Shape @@ -120,7 +120,8 @@ tests. Tests should call the app-owned package function that owns the behavior. Use `apps//private/` only for helpers that are genuinely shared by multiple apps in that family and are not ready for a reusable `+labkit` facade. Existing DIC and wearable `private/` runners are migration debt, not the -preferred app structure. +preferred app structure. Electrochemistry `+core/dispatch.m` routers are also +temporary migration debt; do not add that routing layer to new app work. ## New App Checklist @@ -144,15 +145,8 @@ Start from the closest existing app, reduce it to the needed workflow, and prese ## Validation Pure app calculations, export table construction, and plotting helpers belong -in app-family build tasks. Use the GUI tasks for noninteractive launch/layout -checks: - -```bash -buildtool testAppsElectrochem testAppsElectrochemGui -buildtool testAppsDicGui -buildtool testAppsImageMeasurement testAppsImageMeasurementGui -buildtool testAppsWearableGui -``` +in app-family build tasks. Use GUI tasks for noninteractive launch/layout +checks. See `docs/testing.md` for the canonical task names and pairings. Interactive file selection, drawing, visual inspection, and full workflow feel are validated manually in MATLAB app windows. diff --git a/docs/architecture.md b/docs/architecture.md index d6f5a5f..fcd5f3d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -94,6 +94,37 @@ app-owned package shape. Older DIC and wearable apps may still contain `private/` runners while they are being migrated; do not copy that shape into new app work. +## Current Temporary Debt Inventory + +This inventory is a narrow exception list, not a preferred design. It may +shrink during future migrations, but it should not grow. + +Allowed app `private/` debt: + +```text +apps/dic/private/ +apps/wearable/private/ +``` + +Exit condition: migrate the remaining DIC and wearable app bodies/helpers into +app-owned packages under their owning app folders, with public entry points +owning GUI state, callbacks, debug launch routing, and user-facing log wording. + +Allowed electrochemistry string-dispatch debt: + +```text +apps/electrochem/chrono_overlay/+chrono_overlay/+core/dispatch.m +apps/electrochem/cic/+cic/+core/dispatch.m +apps/electrochem/csc/+csc/+core/dispatch.m +apps/electrochem/eis/+eis/+core/dispatch.m +apps/electrochem/vt_resistance/+vt_resistance/+core/dispatch.m +``` + +Exit condition: replace these app-owned `+core/dispatch.m` string routers with +direct package functions or component-local implementation helpers. New apps +and new migrations should not add `private` runners, `*Workflow.m` adapters, or +additional `+core/dispatch.m` routing layers. + ## Library Extraction Rule A helper may move into `+labkit` only when it satisfies all of these: @@ -123,7 +154,7 @@ Private helpers may keep shorter comments, but should still identify expected ca The default automated validation boundary is the non-GUI MATLAB build task: project architecture checks, `labkit` facade/parser checks, and pure app analysis/export checks. GitHub Actions runs that task on pushes and pull requests to `main`. -GUI launch/layout checks live in source-aligned build tasks such as `testLabkitUiGui` and `testAppsGui`. Interactive GUI workflows are validated manually in MATLAB app windows. +GUI launch/layout checks live in source-aligned build tasks such as `testLabkitUiGui` and `testAppsGui`. Interactive GUI workflows are validated manually in MATLAB app windows. See `docs/testing.md` for the canonical validation matrix. ## Current Package Surface diff --git a/docs/testing.md b/docs/testing.md index 24e63e0..3995bc2 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -93,7 +93,8 @@ runLabKitTests("Suites", "labkit/dta", "ListOnly", true) ``` Use direct `runLabKitTests(...)` calls only for local diagnosis. Build tasks -remain the official entry points for CI, PR validation, and handoff commands. +remain the official entry points for CI, PR validation, and local validation +commands. ## Validation Levels diff --git a/docs/ui.md b/docs/ui.md index 98f921a..f9dc152 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -163,11 +163,7 @@ All `setX(value)` style APIs should no-op when the requested value is already cu ## Validation -Reusable UI contracts are covered by: - -```bash -buildtool testLabkitUi testLabkitUiGui -buildtool testProject -``` +Reusable UI contracts are covered by the source-aligned UI and project build +tasks listed in `docs/testing.md`. Automated GUI tests validate launch, layout, callback wiring, and trace plumbing. Full interactive drawing, file selection, visual inspection, and workflow feel still require manual MATLAB GUI validation. diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 2a72a95..a6350f9 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -30,7 +30,8 @@ Tests mirror source ownership. Do not create a parallel runner framework unless - Keep fixtures synthetic and minimal. - Never copy raw local lab files, real filenames, timestamps, absolute paths, subject names, device IDs, or proprietary metadata into tracked files. - Parser regressions should preserve only structural format details required for coverage. -- Run `buildtool testProject` after fixture, hygiene, architecture, or test-layout changes. +- Run the project guardrail task after fixture, hygiene, architecture, or + test-layout changes. Use `docs/testing.md` for the exact command. ## Documentation Sync diff --git a/tests/integration/project/ProjectDebtGuardrailTest.m b/tests/integration/project/ProjectDebtGuardrailTest.m index 1ff8c64..2ed8d8a 100644 --- a/tests/integration/project/ProjectDebtGuardrailTest.m +++ b/tests/integration/project/ProjectDebtGuardrailTest.m @@ -56,6 +56,53 @@ function oldRunnerDependenciesAreRemoved(testCase) fprintf('Old runner dependency inventory: %d files.\n', numel(dependencyFiles)); end + + function appPrivateRunnerDebtDoesNotGrow(testCase) + root = setupLabKitTestPath(); + expectedDirs = [ ... + "apps/dic/private", ... + "apps/wearable/private"]; + actualDirs = collectAppPrivateDirs(root); + unexpectedDirs = setdiff(actualDirs, expectedDirs); + testCase.verifyTrue(isempty(unexpectedDirs), ... + ['expected-debt: new app private helper directories are not allowed. Files: ' ... + strjoin(cellstr(unexpectedDirs), ', ')]); + + expectedFiles = expectedAppPrivateDebtFiles(); + actualFiles = collectAppPrivateMFiles(root); + unexpectedFiles = setdiff(actualFiles, expectedFiles); + testCase.verifyTrue(isempty(unexpectedFiles), ... + ['expected-debt: app private helper debt grew. Files: ' ... + strjoin(cellstr(unexpectedFiles), ', ')]); + + fprintf('App private helper debt inventory: %d files in %d directories.\n', ... + numel(actualFiles), numel(actualDirs)); + end + + function appWorkflowDispatchDebtDoesNotGrow(testCase) + root = setupLabKitTestPath(); + workflowFiles = collectRelativeFiles(root, ... + fullfile(root, 'apps', '**', '*Workflow.m')); + testCase.verifyTrue(isempty(workflowFiles), ... + ['String-dispatch workflow adapters should not be reintroduced. Files: ' ... + strjoin(cellstr(workflowFiles), ', ')]); + + expectedDispatchFiles = [ ... + "apps/electrochem/chrono_overlay/+chrono_overlay/+core/dispatch.m", ... + "apps/electrochem/cic/+cic/+core/dispatch.m", ... + "apps/electrochem/csc/+csc/+core/dispatch.m", ... + "apps/electrochem/eis/+eis/+core/dispatch.m", ... + "apps/electrochem/vt_resistance/+vt_resistance/+core/dispatch.m"]; + dispatchFiles = collectRelativeFiles(root, ... + fullfile(root, 'apps', '**', '+core', 'dispatch.m')); + unexpectedDispatchFiles = setdiff(dispatchFiles, expectedDispatchFiles); + testCase.verifyTrue(isempty(unexpectedDispatchFiles), ... + ['expected-debt: app-owned +core/dispatch.m debt grew. Files: ' ... + strjoin(cellstr(unexpectedDispatchFiles), ', ')]); + + fprintf('Workflow dispatch debt inventory: %d Workflow files, %d +core dispatch files.\n', ... + numel(workflowFiles), numel(dispatchFiles)); + end end end @@ -98,6 +145,99 @@ function oldRunnerDependenciesAreRemoved(testCase) end end +function dirs = collectAppPrivateDirs(root) + dirs = collectPrivateDirs(fullfile(root, 'apps'), root); +end + +function dirs = collectPrivateDirs(folder, root) + dirs = strings(1, 0); + if ~isfolder(folder) + return; + end + + entries = dir(folder); + for k = 1:numel(entries) + entry = entries(k); + if ~entry.isdir || any(strcmp(entry.name, {'.', '..'})) + continue; + end + + child = fullfile(entry.folder, entry.name); + if strcmp(entry.name, 'private') + dirs(end+1) = string(relativePath(root, ... + child)); %#ok + else + dirs = [dirs, collectPrivateDirs(child, root)]; %#ok + end + end + dirs = unique(dirs); +end + +function files = collectAppPrivateMFiles(root) + files = collectRelativeFiles(root, fullfile(root, 'apps', '**', 'private', '*.m')); +end + +function files = collectRelativeFiles(root, pattern) + entries = dir(pattern); + files = strings(1, 0); + for k = 1:numel(entries) + if ~entries(k).isdir + files(end+1) = string(relativePath(root, ... + fullfile(entries(k).folder, entries(k).name))); %#ok + end + end + files = unique(files); +end + +function files = expectedAppPrivateDebtFiles() + files = [ ... + "apps/dic/private/alignMovingToReference.m", ... + "apps/dic/private/autoAlignMovingToReference.m", ... + "apps/dic/private/axesImageSize.m", ... + "apps/dic/private/boundaryMaskImage.m", ... + "apps/dic/private/catmullRomPoint.m", ... + "apps/dic/private/chooseImageFile.m", ... + "apps/dic/private/clamp01.m", ... + "apps/dic/private/clampLimits.m", ... + "apps/dic/private/colorbarLevelsTable.m", ... + "apps/dic/private/cropSelectionSummary.m", ... + "apps/dic/private/cropSummary.m", ... + "apps/dic/private/defaultSquareRect.m", ... + "apps/dic/private/deleteIfValid.m", ... + "apps/dic/private/displayPath.m", ... + "apps/dic/private/enhanceReferenceImage.m", ... + "apps/dic/private/ensureRgb.m", ... + "apps/dic/private/exportOverlayFigure.m", ... + "apps/dic/private/exportStrainColorbar.m", ... + "apps/dic/private/extendStrainMapToRoi.m", ... + "apps/dic/private/imageHeightWidth.m", ... + "apps/dic/private/imageMask.m", ... + "apps/dic/private/insideImageBounds.m", ... + "apps/dic/private/loadNcorrStrain.m", ... + "apps/dic/private/makeFalseColorOverlay.m", ... + "apps/dic/private/makeStrainOverlay.m", ... + "apps/dic/private/maskBoundaryCurve.m", ... + "apps/dic/private/maskFromCurve.m", ... + "apps/dic/private/maskRgb.m", ... + "apps/dic/private/nanSafeStats.m", ... + "apps/dic/private/normalizeGray.m", ... + "apps/dic/private/runDICPreprocessApp.m", ... + "apps/dic/private/showImage.m", ... + "apps/dic/private/squareRectInsideImage.m", ... + "apps/dic/private/strainToRgb.m", ... + "apps/dic/private/strainValidMask.m", ... + "apps/dic/private/summarizeStrain.m", ... + "apps/dic/private/summaryMaskForStrain.m", ... + "apps/dic/private/summaryTableData.m", ... + "apps/dic/private/tagFromPath.m", ... + "apps/dic/private/ternary.m", ... + "apps/dic/private/transformMatrix.m", ... + "apps/dic/private/transformSummary.m", ... + "apps/dic/private/wrapIndex.m", ... + "apps/dic/private/zoomAxesAtPoint.m", ... + "apps/wearable/private/runECGPrintApp.m"]; +end + function assertExpectedDebt(testCase, actualFiles, expectedMax, label) testCase.verifyTrue(numel(actualFiles) <= expectedMax, ... sprintf('%s. Current count %d exceeds expected debt %d. Files: %s', ... diff --git a/tests/integration/project/ProjectDocumentationGuardrailTest.m b/tests/integration/project/ProjectDocumentationGuardrailTest.m index 5524edd..7d20cb1 100644 --- a/tests/integration/project/ProjectDocumentationGuardrailTest.m +++ b/tests/integration/project/ProjectDocumentationGuardrailTest.m @@ -1,7 +1,60 @@ classdef ProjectDocumentationGuardrailTest < matlab.unittest.TestCase - %PROJECTDOCUMENTATIONGUARDRAILTEST Public/private helper comment checks. + %PROJECTDOCUMENTATIONGUARDRAILTEST Documentation ownership and contract checks. methods (Test, TestTags = {'Integration', 'Style'}) + function humanDocsDoNotContainAgentOnlyWorkflowMandates(testCase) + root = setupLabKitTestPath(); + files = collectHumanDocFiles(root); + forbidden = [ ... + "Codex", ... + "agent-only", ... + "git handoff", ... + "dedicated development branch", ... + "force-push", ... + "Conventional Commits", ... + "commit hash", ... + "branch deletion", ... + "current turn", ... + "final response"]; + + leaks = strings(1, 0); + for k = 1:numel(files) + content = lower(string(fileread(files(k)))); + for iWord = 1:numel(forbidden) + if contains(content, lower(forbidden(iWord))) + leaks(end+1) = relativePath(root, files(k)) + ... + " -> " + forbidden(iWord); %#ok + end + end + end + + testCase.verifyTrue(isempty(leaks), ... + ['Human docs should not contain agent-only workflow mandates: ' ... + strjoin(cellstr(leaks), ', ')]); + end + + function testingDocOwnsBuildTaskCommandMatrix(testCase) + root = setupLabKitTestPath(); + canonical = fullfile(root, "docs", "testing.md"); + canonicalTasks = extractBuildtoolTaskNames(fileread(canonical)); + testCase.verifyGreaterThan(numel(canonicalTasks), 5, ... + 'docs/testing.md should remain the canonical build-task matrix.'); + + files = collectGuidanceFilesExceptTesting(root); + duplicates = strings(1, 0); + for k = 1:numel(files) + tasks = extractBuildtoolTaskNames(fileread(files(k))); + if numel(tasks) > 1 + duplicates(end+1) = relativePath(root, files(k)) + ... + " -> " + strjoin(tasks, " "); %#ok + end + end + + testCase.verifyTrue(isempty(duplicates), ... + ['Only docs/testing.md should maintain a build-task command matrix: ' ... + strjoin(cellstr(duplicates), ', ')]); + end + function publicLibraryFunctionsDocumentAppFacingContracts(testCase) root = setupLabKitTestPath(); publicFiles = collectPublicLibraryFiles(root); @@ -73,6 +126,48 @@ function appOwnedPackageHelpersDocumentImplementationContracts(testCase) end end +function files = collectHumanDocFiles(root) + files = string(fullfile(root, "README.md")); + entries = dir(fullfile(root, "docs", "*.md")); + for k = 1:numel(entries) + files(end+1) = string(fullfile(entries(k).folder, entries(k).name)); %#ok + end +end + +function files = collectGuidanceFilesExceptTesting(root) + files = [ ... + string(fullfile(root, "README.md")), ... + string(fullfile(root, "AGENTS.md")), ... + string(fullfile(root, "apps", "AGENTS.md")), ... + string(fullfile(root, "tests", "AGENTS.md")), ... + string(fullfile(root, "+labkit", "AGENTS.md"))]; + + docEntries = dir(fullfile(root, "docs", "*.md")); + for k = 1:numel(docEntries) + filepath = string(fullfile(docEntries(k).folder, docEntries(k).name)); + if endsWith(filepath, fullfile("docs", "testing.md")) + continue; + end + files(end+1) = filepath; %#ok + end + + skillEntries = dir(fullfile(root, ".agents", "skills", "*", "SKILL.md")); + for k = 1:numel(skillEntries) + files(end+1) = string(fullfile(skillEntries(k).folder, skillEntries(k).name)); %#ok + end +end + +function tasks = extractBuildtoolTaskNames(content) + tokens = regexp(char(content), ... + 'buildtool[ \t]+([A-Za-z][A-Za-z0-9_]*(?:[ \t]+[A-Za-z][A-Za-z0-9_]*)*)', ... + 'tokens'); + tasks = strings(1, 0); + for k = 1:numel(tokens) + tasks = [tasks, split(string(tokens{k}{1})).']; %#ok + end + tasks = unique(tasks(strlength(tasks) > 0), 'stable'); +end + function files = collectPublicLibraryFiles(root) allFiles = dir(fullfile(root, '+labkit', '**', '*.m')); files = strings(1, 0);