Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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 presence for tracked modules, classes, and functions under `services/analysis-engine`, its 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",
Comment on lines +21 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐ŸŒ Web query:

What do Ruff pydocstyle rules D100-D107 cover? Do they apply only to public modules/classes/functions/methods, or also private underscore-prefixed ones?

๐Ÿ’ก Result:

Ruffโ€™s pydocstyle D100โ€“D107 rules are the โ€œmissing docstringโ€ checks:

Do they apply to private (underscore-prefixed) definitions?

For D100โ€“D104 and D106, they apply only to public constructs. In pydocstyle (which Ruff derives these from), a construct is public only if its parent is public and its name does not start with _ or __ (with the special case that names like __init__.py are treated as public for this purpose). Also, if __all__ is defined, only names listed there are considered public. (pydocstyle.readthedocs.io)

So, e.g. _foo() / _Bar / _baz.py wonโ€™t trigger D1xx โ€œmissing docstringโ€ errors just because they lack docstrings. (pydocstyle.readthedocs.io)

D105 is different: it targets magic methods (dunders), not โ€œpublic vs privateโ€ by underscore prefix. (pydocstyle.readthedocs.io)

Citations:


D100-D107 ๊ทœ์น™๋งŒ์œผ๋กœ๋Š” "100% docstring coverage"๋ฅผ ์‹ค์ œ๋กœ ๊ฐ•์ œํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์„ ํƒํ•œ Ruff ๊ทœ์น™์€ public ์‹ฌ๋ณผ๋งŒ์„ ๋Œ€์ƒ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ _helper() ๊ฐ™์€ private ํ•จ์ˆ˜๋‚˜ _PrivateClass ๊ฐ™์€ private ํด๋ž˜์Šค๋Š” docstring ๊ฒ€์‚ฌ ๋Œ€์ƒ์—์„œ ์ œ์™ธ๋˜์–ด ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. PR์˜ ๋ชฉํ‘œ์™€ ARCHITECTURE.md์—์„œ ๋ช…์‹œํ•œ "100% docstring coverage" ์ •์ฑ…์„ ์ผ์น˜์‹œํ‚ค๋ ค๋ฉด, ์ •์ฑ… ๋ฌธ๊ตฌ๋ฅผ public ๋ฒ”์œ„๋กœ ๋ช…ํ™•ํžˆ ์ขํžˆ๊ฑฐ๋‚˜ private ์‹ฌ๋ณผ๊นŒ์ง€ ํฌํ•จํ•˜๋Š” ์ถ”๊ฐ€ ๊ฒ€์‚ฌ๋ฅผ ๋„์ž…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

"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 tracked 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 plan files are missing security notes."""
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
18 changes: 16 additions & 2 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,6 +19,7 @@ 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

Expand All @@ -27,6 +31,7 @@ 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

Expand All @@ -40,6 +45,7 @@ def normalized_architecture() -> str:


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()
Expand All @@ -53,17 +59,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
Comment on lines 61 to 76

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

BANDSCOPE_TARGET_TRIPLE๊ฐ€ ํŒจํ‚ค์ง• ๊ฒฐ๊ณผ์˜ ๋‹จ์ผ ๊ธฐ์ค€์ด ์•„๋‹™๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ๋Š” target triple๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ ๊ฒฝ๋กœ์™€ ํ™•์žฅ์ž๋ฅผ ์ผ๋ถ€ ๋ฎ์–ด์“ฐ์ง€๋งŒ, artifact_identity()์™€ manifest๋Š” ์—ฌ์ „ํžˆ ํ˜ธ์ŠคํŠธ ๊ธฐ๋ฐ˜ normalized_platform() / normalized_architecture()๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ Linux ํ˜ธ์ŠคํŠธ์—์„œ x86_64-pc-windows-msvc๋งŒ ์„ค์ •ํ•˜๋ฉด .exe๋ฅผ ํŒจํ‚ค์ง•ํ•˜๋ฉด์„œ๋„ ์‚ฐ์ถœ๋ฌผ ์ด๋ฆ„์€ bandscope-linux-amd64-...๋กœ ๊ธฐ๋ก๋˜๊ณ , ๋ฐ˜๋Œ€๋กœ Windows ํ˜ธ์ŠคํŠธ์—์„œ Linux triple์„ ์“ฐ๋ฉด ์—ฌ๊ธฐ์„œ๋Š” ์—ฌ์ „ํžˆ .exe ๊ฒฝ๋กœ๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. ํ”Œ๋žซํผ/์•„ํ‚คํ…์ฒ˜๋ฅผ target triple์—์„œ ํ•œ ๋ฒˆ๋งŒ ํŒŒ์ƒํ•ด์„œ expected_binary_path()์™€ artifact_identity()๊ฐ€ ๊ฐ™์ด ์“ฐ๋„๋ก ๋งž์ถฐ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release/package_desktop_artifact.py` around lines 61 - 76,
expected_binary_path() currently uses BANDSCOPE_TARGET_TRIPLE to choose a binary
name/path while artifact_identity() and manifest generation still use
host-derived normalized_platform()/normalized_architecture(), causing mismatched
artifact names; update code so the target triple is parsed once into
platform/architecture values and both expected_binary_path() and
artifact_identity() consume those derived values (e.g., add a helper that parses
BANDSCOPE_TARGET_TRIPLE into target_platform and target_arch, fall back to
normalized_platform()/normalized_architecture() when unset, then use those
values inside expected_binary_path() to pick binary_name and path and in
artifact_identity() to build the artifact name) ensuring both functions
reference the same canonical target info.



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
32 changes: 32 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 @@ -27,6 +30,7 @@ def test_release_packaging_includes_architecture_in_artifact_identity(


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 +51,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 +90,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