diff --git a/.claude/settings.json b/.claude/settings.json
index 8a49fc40..69715801 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,4 +1,16 @@
{
+ "permissions": {
+ "allow": [
+ "Bash(gh api repos/*/pulls/*/reviews*)",
+ "Bash(gh pr review *)",
+ "mcp__github__pull_request_review_write",
+ "mcp__github__add_comment_to_pending_review",
+ "mcp__github__add_reply_to_pull_request_comment",
+ "mcp__plugin_github_github__pull_request_review_write",
+ "mcp__plugin_github_github__add_comment_to_pending_review",
+ "mcp__plugin_github_github__add_reply_to_pull_request_comment"
+ ]
+ },
"enabledPlugins": {
"dotnet@dotnet-agent-skills": false,
"dotnet-diag@dotnet-agent-skills": true,
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
deleted file mode 100644
index 4a760032..00000000
--- a/.claude/settings.local.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "permissions": {
- "allow": [
- "mcp__jira__getAccessibleAtlassianResources",
- "mcp__jira__searchJiraIssuesUsingJql",
- "mcp__jira__getJiraIssue",
- "Bash(xargs:*)",
- "mcp__jira__createJiraIssue",
- "mcp__jira__editJiraIssue",
- "mcp__jira__getJiraProjectIssueTypesMetadata",
- "Monitor"
- ]
- }
-}
diff --git a/CLAUDE.md b/CLAUDE.md
index e7cefc87..dc0724dc 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -66,8 +66,9 @@ significantly change a design, create or update the relevant `_doc_*.md`:
A change is not complete until all of the following pass:
1. `scripts/validate-build` — clean build, zero warnings (`dotnet build /warnaserror` is the underlying command but the script also cleans first)
-2. `scripts/validate-tests` — all unit and headless E2E tests pass
-3. Affected `_doc_*.md` files are updated
+2. `scripts/validate-ml-build` — `mypy --strict` over `ml/pipeline` and `ml/test`, zero type errors
+3. `scripts/validate-tests` — all unit and headless E2E tests pass
+4. Affected `_doc_*.md` files are updated
## Accessibility
diff --git a/Directory.Build.props b/Directory.Build.props
index e90c7480..6166067d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,6 +16,10 @@
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e6fbf3c7..9f48f5eb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,6 +26,13 @@
+
+
diff --git a/ml/pipeline/__init__.py b/ml/pipeline/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ml/pyproject.toml b/ml/pyproject.toml
new file mode 100644
index 00000000..559a0d43
--- /dev/null
+++ b/ml/pyproject.toml
@@ -0,0 +1,6 @@
+# mypy --strict configuration (ADR-281). Scoped to ml/pipeline/ and ml/test/ only —
+# see scripts/validate-ml-build.sh/.cmd, which run `mypy --strict pipeline test` from
+# this directory so this config file is auto-discovered.
+[tool.mypy]
+files = ["pipeline", "test"]
+strict = true
diff --git a/ml/requirements.txt b/ml/requirements.txt
index 94dc4997..42cd6f63 100644
--- a/ml/requirements.txt
+++ b/ml/requirements.txt
@@ -2,3 +2,4 @@ numpy==2.5.0
pandas==3.0.3
tensorflow==2.21.0
dvc==3.67.1
+mypy==2.3.0
diff --git a/ml/test/__init__.py b/ml/test/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/scripts/validate-ml-build.cmd b/scripts/validate-ml-build.cmd
new file mode 100644
index 00000000..956e5e38
--- /dev/null
+++ b/scripts/validate-ml-build.cmd
@@ -0,0 +1,5 @@
+@echo off
+pushd %~dp0..\ml
+mypy --strict pipeline test
+if %ERRORLEVEL% neq 0 ( popd & exit /b %ERRORLEVEL% )
+popd
diff --git a/scripts/validate-ml-build.sh b/scripts/validate-ml-build.sh
new file mode 100755
index 00000000..44ed8bad
--- /dev/null
+++ b/scripts/validate-ml-build.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$SCRIPT_DIR/../ml"
+echo 'Running mypy --strict over ml/pipeline and ml/test...'
+mypy --strict pipeline test
diff --git a/scripts/validate.cmd b/scripts/validate.cmd
index 19ff6f50..74f3a48b 100644
--- a/scripts/validate.cmd
+++ b/scripts/validate.cmd
@@ -1,6 +1,6 @@
@echo off
echo Checking required tools...
-for %%T in (dotnet pwsh node python3 claude) do (
+for %%T in (dotnet pwsh node python3 mypy claude) do (
where %%T >nul 2>&1 || (
echo ERROR: Required tool '%%T' is not installed or not on PATH.
exit /b 1
@@ -8,5 +8,7 @@ for %%T in (dotnet pwsh node python3 claude) do (
)
call "%~dp0validate-build.cmd"
if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%
+call "%~dp0validate-ml-build.cmd"
+if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%
call "%~dp0validate-tests.cmd"
if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%
diff --git a/scripts/validate.sh b/scripts/validate.sh
index 384609b5..5849ce5a 100755
--- a/scripts/validate.sh
+++ b/scripts/validate.sh
@@ -15,7 +15,9 @@ check_tool dotnet
check_tool pwsh
check_tool node
check_tool python3
+check_tool mypy
check_tool claude
"$SCRIPT_DIR/validate-build.sh"
+"$SCRIPT_DIR/validate-ml-build.sh"
"$SCRIPT_DIR/validate-tests.sh"