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
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Last updated: 2026-03-11
- The desktop app is scaffolded as `Tauri + Vite + React`, but initial verification keeps Rust packaging out of the default quickcheck path.
- The desktop shell uses an explicit Tauri CSP that only allows self-hosted assets, inline styles, Tauri IPC, and loopback development traffic.
- Mechanical gates focus on lint, typecheck, unit tests, coverage for Python, and documentation presence.
- Python quality gates also require 100% docstring coverage via `package.json` script `check:python-docstrings`, enforced with Ruff rules `D100` through `D107` across tracked packages, modules, classes, nested classes, functions, methods (including `__init__`), `services/analysis-engine` tests, and repo-owned Python scripts.
- Mechanical gates also enforce security document presence, plan `Security Notes`, and basic forbidden-pattern checks.
- Security context is part of architecture, not just implementation detail; docs and plans must record the trust boundary touched by risky changes.
- Supply-chain controls are part of the bootstrap architecture, not a release-afterthought.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
"check:security-gates": "python3 scripts/checks/security_gates.py",
"check:supply-chain": "python3 scripts/checks/verify_supply_chain.py",
"check:github-bootstrap": "python3 scripts/checks/verify_github_bootstrap_policy.py",
"check:python-docstrings": "sh -c 'cd services/analysis-engine && uv run ruff check src tests ../../scripts --select D100,D101,D102,D103,D104,D105,D106,D107'",
"ruff:check": "sh -c 'cd services/analysis-engine && uv run ruff check src tests'",
"ruff:format:check": "sh -c 'cd services/analysis-engine && uv run ruff format --check src tests'",
"lint": "npm run lint:workspaces && npm run check:docs && npm run check:security-notes && npm run check:security-gates && npm run check:supply-chain && npm run check:github-bootstrap && npm run ruff:check && npm run ruff:format:check",
"lint": "npm run lint:workspaces && npm run check:docs && npm run check:security-notes && npm run check:security-gates && npm run check:supply-chain && npm run check:github-bootstrap && npm run check:python-docstrings && npm run ruff:check && npm run ruff:format:check",
"typecheck": "npm run typecheck --workspaces --if-present && sh -c 'cd services/analysis-engine && uv run mypy src'",
"test": "npm run test --workspaces --if-present && sh -c 'cd services/analysis-engine && uv run pytest tests --cov=src/bandscope_analysis --cov-report=term-missing --cov-fail-under=100'",
"build": "npm run build --workspaces --if-present",
Expand Down
4 changes: 4 additions & 0 deletions scripts/checks/security_gates.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Scan repository workspace source files for disallowed security patterns."""

from pathlib import Path
import re
import sys
Expand Down Expand Up @@ -32,12 +34,14 @@


def should_scan(path: Path) -> bool:
"""Return whether a path should be scanned for security-pattern violations."""
return path.suffix in TARGET_EXTENSIONS and not any(
part in EXCLUDED_PARTS for part in path.parts
)


def main() -> int:
"""Return a failing exit code when a forbidden security pattern is found."""
violations: list[str] = []

for path in Path(".").rglob("*"):
Expand Down
3 changes: 3 additions & 0 deletions scripts/checks/verify_docs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Verify that required repository documentation files and references exist."""

from pathlib import Path
import sys

Expand Down Expand Up @@ -62,6 +64,7 @@


def main() -> int:
"""Return a failing exit code when required docs or references are missing."""
missing = [str(path) for path in REQUIRED_PATHS if not path.exists()]
if missing:
print("Missing required docs:")
Expand Down
3 changes: 3 additions & 0 deletions scripts/checks/verify_github_bootstrap_policy.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Verify that GitHub bootstrap policy docs are present and referenced."""

from pathlib import Path


Expand All @@ -16,6 +18,7 @@


def main() -> int:
"""Return a failing exit code when bootstrap policy docs drift out of sync."""
if not REQUIRED_PATH.exists():
print(f"Missing GitHub bootstrap policy: {REQUIRED_PATH}")
return 1
Expand Down
4 changes: 4 additions & 0 deletions scripts/checks/verify_security_notes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Verify that design-plan documents include a complete Security Notes section."""

from pathlib import Path
import sys

Expand All @@ -15,6 +17,7 @@


def security_notes_section(content: str) -> str:
"""Extract the lowercased Security Notes section from a plan document."""
lowered = content.lower()
marker = SECURITY_NOTES_TEXT.lower()
start = lowered.find(marker)
Expand All @@ -34,6 +37,7 @@ def security_notes_section(content: str) -> str:


def main() -> int:
"""Return a failing exit code when Security Notes or required subsections are missing."""
missing: list[str] = []
for path in sorted(PLAN_DIR.glob("*.md")):
content = path.read_text(encoding="utf-8")
Expand Down
8 changes: 8 additions & 0 deletions scripts/checks/verify_supply_chain.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Verify that repository-controlled supply-chain controls stay in place."""

from pathlib import Path
import re

Expand Down Expand Up @@ -28,10 +30,12 @@


def verify_required_files() -> list[str]:
"""Return missing files required by the supply-chain baseline."""
return [str(path) for path in REQUIRED_FILES if not path.exists()]


def verify_pinned_actions() -> list[str]:
"""Return workflow actions that are not pinned to immutable SHAs."""
violations: list[str] = []
workflow_paths = sorted(Path(".github/workflows").glob("*.yml")) + sorted(
Path(".github/workflows").glob("*.yaml")
Expand All @@ -53,6 +57,7 @@ def verify_pinned_actions() -> list[str]:


def verify_dependabot_coverage() -> list[str]:
"""Return missing Dependabot ecosystems from the repo configuration."""
path = Path(".github/dependabot.yml")
if not path.exists():
return [f"missing file: {path}"]
Expand All @@ -65,13 +70,15 @@ def verify_dependabot_coverage() -> list[str]:


def read_workflow(path: Path, label: str, missing: list[str]) -> str:
"""Read a workflow file, recording a missing-file violation when absent."""
if not path.exists():
missing.append(f"missing file: {path}")
return ""
return path.read_text(encoding="utf-8")


def verify_workflow_coverage() -> list[str]:
"""Return workflow trigger and artifact coverage violations."""
missing: list[str] = []
ci = read_workflow(Path(".github/workflows/ci.yml"), "ci", missing)
for token in ["develop", "main", "pull_request", "push", "ci / build-and-test"]:
Expand Down Expand Up @@ -153,6 +160,7 @@ def verify_workflow_coverage() -> list[str]:


def main() -> int:
"""Return a failing exit code when supply-chain controls are incomplete."""
violations: list[str] = []
violations.extend(f"missing file: {item}" for item in verify_required_files())
violations.extend(verify_pinned_actions())
Expand Down
38 changes: 34 additions & 4 deletions scripts/release/package_desktop_artifact.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Package desktop build outputs into traceable release artifacts."""

from __future__ import annotations

from pathlib import Path
Expand All @@ -8,6 +10,7 @@


def sha256_file(path: Path) -> str:
"""Return the SHA-256 digest for a file."""
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
Expand All @@ -16,9 +19,16 @@ def sha256_file(path: Path) -> str:


def normalized_platform() -> str:
"""Return the normalized artifact platform label for the current environment."""
if artifact_platform := os.environ.get("BANDSCOPE_ARTIFACT_OS"):
return artifact_platform

target_triple = os.environ.get("BANDSCOPE_TARGET_TRIPLE", "")
if "windows" in target_triple:
return "windows"
if "apple-darwin" in target_triple:
return "macos"

system = platform.system().lower()
if system == "darwin":
return "macos"
Expand All @@ -27,9 +37,16 @@ def normalized_platform() -> str:


def normalized_architecture() -> str:
"""Return the normalized artifact architecture label for the current environment."""
if artifact_arch := os.environ.get("BANDSCOPE_ARTIFACT_ARCH"):
return artifact_arch

target_triple = os.environ.get("BANDSCOPE_TARGET_TRIPLE", "")
if target_triple.startswith(("x86_64", "amd64")):
return "amd64"
if target_triple.startswith(("aarch64", "arm64")):
return "arm64"

machine = platform.machine().lower()
if machine in {"x86_64", "amd64"}:
return "amd64"
Expand All @@ -39,10 +56,15 @@ def normalized_architecture() -> str:
return machine


def resolved_artifact_target() -> tuple[str, str]:
"""Return the normalized platform and architecture for the current artifact target."""
return normalized_platform(), normalized_architecture()


def artifact_identity() -> dict[str, str]:
"""Build the archive and manifest names for the current artifact target."""
git_sha = os.environ.get("GITHUB_SHA", "local")[:12]
target_platform = normalized_platform()
target_arch = normalized_architecture()
target_platform, target_arch = resolved_artifact_target()
suffix = f"bandscope-{target_platform}-{target_arch}-{git_sha}"
return {
"platform": target_platform,
Expand All @@ -53,17 +75,25 @@ def artifact_identity() -> dict[str, str]:


def expected_binary_path(repo_root: Path) -> Path:
system = normalized_platform()
"""Return the expected desktop binary path for the selected target triple."""
target_triple = os.environ.get("BANDSCOPE_TARGET_TRIPLE")
if target_triple and "windows" in target_triple:
system = "windows"
elif target_triple and "apple-darwin" in target_triple:
system = "macos"
else:
system = normalized_platform()
binary_name = (
"bandscope-desktop.exe" if system == "windows" else "bandscope-desktop"
)
target_root = repo_root / "apps" / "desktop" / "src-tauri" / "target"
if target_triple := os.environ.get("BANDSCOPE_TARGET_TRIPLE"):
if target_triple:
target_root = target_root / target_triple
return target_root / "release" / binary_name


def main() -> int:
"""Package the desktop binary, frontend assets, and metadata into a zip archive."""
repo_root = Path(__file__).resolve().parents[2]
binary_path = expected_binary_path(repo_root)
frontend_dist = repo_root / "apps" / "desktop" / "dist"
Expand Down
2 changes: 2 additions & 0 deletions services/analysis-engine/src/bandscope_analysis/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@


class HealthReport(TypedDict):
"""Typed health payload returned by the analysis-engine bootstrap API."""

service: Literal["bandscope-analysis"]
status: Literal["ready"]
pipeline_stages: list[Literal["decode", "draft", "separate", "persist"]]
Expand Down
3 changes: 3 additions & 0 deletions services/analysis-engine/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Shared pytest helpers for analysis-engine and harness verification tests."""

from __future__ import annotations

from importlib.util import module_from_spec, spec_from_file_location
Expand All @@ -6,6 +8,7 @@


def load_module(relative_path: str, module_name: str) -> ModuleType:
"""Load a repository Python module from a path outside the package root."""
repo_root = Path(__file__).resolve().parents[3]
module_path = repo_root / relative_path
spec = spec_from_file_location(module_name, module_path)
Expand Down
3 changes: 3 additions & 0 deletions services/analysis-engine/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Tests for the public analysis-engine API helpers."""

from bandscope_analysis.api import get_analysis_status


def test_get_analysis_status_returns_health_payload() -> None:
"""Ensure the API helper returns the expected bootstrap status payload."""
assert get_analysis_status() == {
"service": "bandscope-analysis",
"status": "ready",
Expand Down
3 changes: 3 additions & 0 deletions services/analysis-engine/tests/test_health.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Tests for the analysis-engine health helpers."""

from bandscope_analysis.health import build_health_report


def test_build_health_report_exposes_bootstrap_defaults() -> None:
"""Ensure the bootstrap health payload exposes the expected default stages."""
assert build_health_report() == {
"service": "bandscope-analysis",
"status": "ready",
Expand Down
58 changes: 58 additions & 0 deletions services/analysis-engine/tests/test_release_packaging.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Tests for desktop release packaging helpers and artifact metadata."""

from __future__ import annotations

from pathlib import Path
Expand All @@ -8,6 +10,7 @@
def test_release_packaging_includes_architecture_in_artifact_identity(
monkeypatch,
) -> None:
"""Ensure artifact names encode the selected platform and architecture."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py", "package_desktop_artifact"
)
Expand All @@ -26,7 +29,34 @@ def test_release_packaging_includes_architecture_in_artifact_identity(
}


def test_release_packaging_derives_artifact_identity_from_target_triple(
monkeypatch,
) -> None:
"""Ensure target triples drive archive naming when explicit artifact env vars are absent."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py",
"package_desktop_artifact_identity_target",
)

monkeypatch.setenv("GITHUB_SHA", "fedcba9876543210")
monkeypatch.delenv("BANDSCOPE_ARTIFACT_OS", raising=False)
monkeypatch.delenv("BANDSCOPE_ARTIFACT_ARCH", raising=False)
monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "x86_64-pc-windows-msvc")
monkeypatch.setattr(packaging.platform, "system", lambda: "Darwin")
monkeypatch.setattr(packaging.platform, "machine", lambda: "arm64")

artifact = packaging.artifact_identity()

assert artifact == {
"platform": "windows",
"arch": "amd64",
"archive_name": "bandscope-windows-amd64-fedcba987654.zip",
"manifest_name": "bandscope-windows-amd64-fedcba987654.manifest.txt",
}


def test_expected_binary_path_uses_target_triple_when_provided(monkeypatch, tmp_path: Path) -> None:
"""Ensure target triples redirect packaging to the expected Tauri output path."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py", "package_desktop_artifact_target"
)
Expand All @@ -47,7 +77,34 @@ def test_expected_binary_path_uses_target_triple_when_provided(monkeypatch, tmp_
)


def test_expected_binary_path_derives_windows_extension_from_target_triple(
monkeypatch, tmp_path: Path
) -> None:
"""Ensure Windows target triples select the .exe packaging path on non-Windows hosts."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py", "package_desktop_artifact_windows_target"
)

monkeypatch.delenv("BANDSCOPE_ARTIFACT_OS", raising=False)
monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "x86_64-pc-windows-msvc")
monkeypatch.setattr(packaging.platform, "system", lambda: "Darwin")

binary_path = packaging.expected_binary_path(tmp_path)

assert binary_path == (
tmp_path
/ "apps"
/ "desktop"
/ "src-tauri"
/ "target"
/ "x86_64-pc-windows-msvc"
/ "release"
/ "bandscope-desktop.exe"
)


def test_release_packaging_maps_darwin_to_macos(monkeypatch) -> None:
"""Ensure Darwin hosts map to the repository's canonical macOS label."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py", "package_desktop_artifact_platform"
)
Expand All @@ -59,6 +116,7 @@ def test_release_packaging_maps_darwin_to_macos(monkeypatch) -> None:


def test_release_packaging_main_writes_arch_specific_manifest(monkeypatch, tmp_path: Path) -> None:
"""Ensure the packaging entry point writes an architecture-aware manifest."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py", "package_desktop_artifact_main"
)
Expand Down
4 changes: 4 additions & 0 deletions services/analysis-engine/tests/test_supply_chain_policy.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Tests for repository supply-chain and workflow coverage checks."""

from __future__ import annotations

from pathlib import Path
Expand All @@ -6,6 +8,7 @@


def test_supply_chain_check_requires_multi_arch_runner_labels(monkeypatch, tmp_path: Path) -> None:
"""Ensure missing multi-arch workflow tokens are reported as violations."""
supply_chain = load_module("scripts/checks/verify_supply_chain.py", "verify_supply_chain")

workflow_dir = tmp_path / ".github" / "workflows"
Expand Down Expand Up @@ -50,6 +53,7 @@ def test_supply_chain_check_requires_multi_arch_runner_labels(monkeypatch, tmp_p


def test_supply_chain_check_accepts_repo_multi_arch_workflow(monkeypatch) -> None:
"""Ensure the checked-in multi-arch workflow satisfies the baseline policy."""
supply_chain = load_module("scripts/checks/verify_supply_chain.py", "verify_supply_chain_repo")
repo_root = Path(__file__).resolve().parents[3]

Expand Down
Loading