QA-Improver

This commit is contained in:
2026-04-11 06:42:24 -04:00
parent 794de9c721
commit 0686224824
75 changed files with 350 additions and 108 deletions

View File

@@ -0,0 +1,85 @@
---
name: qa-improver
description: Automatically fix code quality issues identified by a QA assessment. Resolves lint violations, type errors, missing exports, and other mechanical improvements without changing behavior. Use after a quality-scorer run to action its recommendations.
tools: Bash Read Write Edit Grep Glob
---
You are a code quality improvement agent for the Impakt project. You receive a QA report and systematically fix the mechanical issues it identifies — without changing any runtime behavior.
## Inputs
Read the most recent `docs/QA-*.md` report (excluding QA-TEMPLATE.md and QA-INSTRUCTIONS.md). Extract the "Recommended Actions" table.
## Workflow
### 1. Assess what can be auto-fixed
Categorize each recommended action:
- **Auto-fixable**: lint auto-fix, import sorting, unused imports/vars
- **Mechanical**: adding type annotations, `__all__` exports, duplicate dict keys, line-length fixes
- **Requires judgment**: refactoring complex files, adding test coverage, security changes
Only perform auto-fixable and mechanical fixes. Skip anything that requires judgment or behavioral changes.
### 2. Execute fixes in order of safety
Run fixes from safest to most involved:
**a) Lint auto-fix (safest)**
```bash
uv run python -m ruff check --fix src/
```
**b) Remaining lint violations**
Read the ruff output. For each remaining violation:
- F601 (duplicate dict keys): read the file, determine which entry to keep, remove the duplicate
- F841 (unused variables): remove or prefix with `_`
- E501 (line too long): break the line naturally
- F541 (empty f-string): convert to regular string
**c) Type annotation fixes**
Run `uv run python -m mypy src/impakt --ignore-missing-imports` and fix `[type-arg]` errors by adding proper generic parameters (e.g., `dict` -> `dict[str, Any]`, `list` -> `list[str]`). Read context around each error to determine the correct type.
**d) Missing `__all__` exports**
For any module `__init__.py` that lacks `__all__`:
- List the public classes and functions in the module's files
- Add imports and `__all__` following the pattern in `src/impakt/channel/__init__.py`
**e) Coverage config (if not already present)**
Check if `addopts` in `[tool.pytest.ini_options]` includes `--cov`. If not, add it.
### 3. Verify after each category
After each category of fixes, run:
```bash
uv run python -m ruff check src/
uv run python -m pytest --tb=short -q
```
If tests fail, revert the last change and move on. Never leave the codebase in a broken state.
### 4. Final verification
Run all three quality tools:
```bash
uv run python -m ruff check src/
uv run python -m mypy src/impakt --ignore-missing-imports
uv run python -m pytest --tb=short -q
```
### 5. Report results
Return a summary:
- Which actions were completed
- Which were skipped and why
- Before/after counts for each tool (lint violations, mypy errors, test results)
- Any actions that still require human judgment
## Rules
- **Never change runtime behavior.** Only annotations, imports, formatting, and dead code removal.
- **Never modify test files.** Only source code under `src/`.
- **Verify after every category.** If tests break, revert immediately.
- **Be conservative with type annotations.** Use `Any` when the actual type is genuinely dynamic. Don't guess complex types — leave them for human review.
- **Don't touch security-sensitive code** (eval, exec, subprocess) unless the QA report explicitly flags an unsafe pattern AND the fix is mechanical.

BIN
.coverage Normal file

Binary file not shown.

View File

@@ -62,6 +62,7 @@ packages = ["src/impakt"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
pythonpath = ["src"] pythonpath = ["src"]
addopts = "--cov=impakt --cov-report=term-missing:skip-covered --cov-fail-under=60"
filterwarnings = [ filterwarnings = [
"ignore::pytest.PytestCollectionWarning", "ignore::pytest.PytestCollectionWarning",
] ]

136
scripts/qa-improve.sh Executable file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail
# ──────────────────────────────────────────────────────────────
# qa-improve.sh — Score the codebase, then auto-fix what's found
#
# Runs two agents in sequence:
# 1. quality-scorer → produces a QA report in docs/
# 2. qa-improver → reads the report and fixes mechanical issues
# 3. quality-scorer → re-scores to measure improvement
#
# Usage:
# ./scripts/qa-improve.sh # default: sonnet
# ./scripts/qa-improve.sh --model opus # use opus for deeper analysis
# ./scripts/qa-improve.sh --score-only # skip the improvement step
# ./scripts/qa-improve.sh --fix-only # skip initial scoring (use latest report)
# ──────────────────────────────────────────────────────────────
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MODEL="sonnet"
BUDGET="1.50"
SKIP_SCORE=false
SKIP_FIX=false
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--budget) BUDGET="$2"; shift 2 ;;
--score-only) SKIP_FIX=true; shift ;;
--fix-only) SKIP_SCORE=true; shift ;;
--help|-h)
echo "Usage: $0 [--model sonnet|opus|haiku] [--budget USD] [--score-only] [--fix-only]"
echo ""
echo "Score the codebase, auto-fix issues, then re-score to measure improvement."
echo ""
echo "Options:"
echo " --model MODEL Claude model (default: sonnet)"
echo " --budget USD Max spend per agent run (default: 1.50)"
echo " --score-only Run scoring only, skip auto-fix"
echo " --fix-only Skip initial score, fix based on latest report"
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
cd "$PROJECT_ROOT"
# Verify prerequisites
for cmd in claude uv; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: $cmd not found." >&2
exit 1
fi
done
if [[ ! -f "docs/QA-INSTRUCTIONS.md" ]]; then
echo "Error: docs/QA-INSTRUCTIONS.md not found." >&2
exit 1
fi
uv sync --dev --quiet
SCORE_PROMPT="Run a full codebase quality assessment. \
Read docs/QA-INSTRUCTIONS.md for the methodology and rubrics. \
Read docs/QA-TEMPLATE.md for the report structure. \
Check docs/ for previous QA-*.md reports and compute deltas if any exist. \
Collect all raw metrics by running every command in Step 1. \
Score each dimension using the Step 2 rubrics. \
Compute the composite score using the Step 3 formula. \
Write the completed report to docs/QA-<datetime>.md. \
Print the composite score, grade, per-dimension scores, and top 3 actions."
FIX_PROMPT="Read the most recent QA report in docs/ (the QA-*.md file with the latest date, \
excluding QA-TEMPLATE.md and QA-INSTRUCTIONS.md). \
Extract the Recommended Actions table. \
Execute all auto-fixable and mechanical fixes: \
1) Run uv run ruff check --fix src/ for lint auto-fixes. \
2) Fix remaining lint violations (duplicate keys, unused vars, long lines). \
3) Fix mypy type-arg errors by adding proper generic parameters. \
4) Add __all__ exports to any modules missing them. \
5) Verify after each category with ruff check and pytest. \
Never change runtime behavior. Never modify test files. \
Report what was fixed and what was skipped."
# ── Phase 1: Score ──
if [[ "$SKIP_SCORE" == false ]]; then
echo "Phase 1: Scoring codebase (model: $MODEL)..."
echo "────────────────────────────────────────────"
claude -p \
--agent quality-scorer \
--model "$MODEL" \
--max-budget-usd "$BUDGET" \
--allowedTools "Bash Read Write Grep Glob" \
--output-format text \
"$SCORE_PROMPT"
REPORT=$(ls -t docs/QA-2*.md 2>/dev/null | head -1)
echo ""
echo "Report: ${REPORT:-none found}"
echo ""
fi
# ── Phase 2: Fix ──
if [[ "$SKIP_FIX" == false ]]; then
echo "Phase 2: Auto-fixing issues (model: $MODEL)..."
echo "────────────────────────────────────────────"
claude -p \
--agent qa-improver \
--model "$MODEL" \
--max-budget-usd "$BUDGET" \
--allowedTools "Bash Read Write Edit Grep Glob" \
--output-format text \
"$FIX_PROMPT"
echo ""
# ── Phase 3: Re-score ──
echo "Phase 3: Re-scoring after fixes (model: $MODEL)..."
echo "────────────────────────────────────────────"
claude -p \
--agent quality-scorer \
--model "$MODEL" \
--max-budget-usd "$BUDGET" \
--allowedTools "Bash Read Write Grep Glob" \
--output-format text \
"$SCORE_PROMPT"
FINAL_REPORT=$(ls -t docs/QA-2*.md 2>/dev/null | head -1)
echo ""
echo "────────────────────────────────────────────"
echo "Final report: ${FINAL_REPORT:-none found}"
fi

View File

@@ -60,10 +60,9 @@ MAIN_LOCATIONS: dict[str, str] = {
"CLAV": "Clavicle", "CLAV": "Clavicle",
"RIBS": "Ribs", "RIBS": "Ribs",
"STRN": "Sternum", "STRN": "Sternum",
"SHLR": "Shoulder", "SHLR": "Shoulder Right",
"SHLD": "Shoulder", "SHLD": "Shoulder",
"SHLL": "Shoulder Left", "SHLL": "Shoulder Left",
"SHLR": "Shoulder Right",
# Spine # Spine
"SPIN": "Spine", "SPIN": "Spine",
"LUSP": "Lumbar Spine", "LUSP": "Lumbar Spine",
@@ -74,7 +73,6 @@ MAIN_LOCATIONS: dict[str, str] = {
"SACR": "Sacrum", "SACR": "Sacrum",
"PUBC": "Pubic Symphysis", "PUBC": "Pubic Symphysis",
# Upper extremities # Upper extremities
"SHLD": "Shoulder",
"UPRA": "Upper Arm", "UPRA": "Upper Arm",
"ELBO": "Elbow", "ELBO": "Elbow",
"FORA": "Forearm", "FORA": "Forearm",
@@ -122,7 +120,7 @@ MAIN_LOCATIONS: dict[str, str] = {
# Wheel # Wheel
"WHEL": "Wheel", "WHEL": "Wheel",
# Structural — additional # Structural — additional
"FORA": "Floor Rail", "FRAL": "Floor Rail",
"FBAR": "Barrier Face", "FBAR": "Barrier Face",
"KNSL": "Knee Slider", "KNSL": "Knee Slider",
# Simulation / other # Simulation / other
@@ -181,11 +179,6 @@ FINE_LOCATIONS: dict[str, str] = {
"URXX": "Upper Right", "URXX": "Upper Right",
"LLXX": "Lower Left", "LLXX": "Lower Left",
"LRXX": "Lower Right", "LRXX": "Lower Right",
# Tibia positions (real MME data uses these)
"LEUP": "Left Upper",
"RIUP": "Right Upper",
"LELO": "Left Lower",
"RILO": "Right Lower",
# Fine location with number suffix # Fine location with number suffix
"0001": "Secondary", "0001": "Secondary",
"0100": "Lower Position", "0100": "Lower Position",

View File

@@ -8,17 +8,17 @@ from __future__ import annotations
import fnmatch import fnmatch
from collections import defaultdict from collections import defaultdict
from collections.abc import Iterator
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
from typing import Any, Iterator from typing import Any
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
from impakt.channel.code import ChannelCode from impakt.channel.code import ChannelCode
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Metadata models # Metadata models
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -6,14 +6,11 @@ Viscous Criterion (VC): V(t) * C(t) where V = deflection velocity, C = compressi
from __future__ import annotations from __future__ import annotations
from typing import Any
import numpy as np import numpy as np
from impakt.channel.model import Channel, DummyInfo from impakt.channel.model import Channel, DummyInfo
from impakt.criteria.base import CriterionResult from impakt.criteria.base import CriterionResult
# Initial chest depth by dummy type (mm) # Initial chest depth by dummy type (mm)
CHEST_DEPTH: dict[str, float] = { CHEST_DEPTH: dict[str, float] = {
"H3-50M": 229.0, "H3-50M": 229.0,

View File

@@ -7,8 +7,6 @@ where the total time the signal exceeds that level equals at least 3 ms.
from __future__ import annotations from __future__ import annotations
from typing import Any
import numpy as np import numpy as np
from impakt.channel.model import Channel, ChannelGroup, DummyInfo from impakt.channel.model import Channel, ChannelGroup, DummyInfo

View File

@@ -6,8 +6,6 @@ Evaluated separately for left and right femur.
from __future__ import annotations from __future__ import annotations
from typing import Any
import numpy as np import numpy as np
from impakt.channel.model import Channel, DummyInfo from impakt.channel.model import Channel, DummyInfo

View File

@@ -14,7 +14,6 @@ The reported Nij is the maximum across all four modes and all time steps.
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
import numpy as np import numpy as np

View File

@@ -12,7 +12,6 @@ where:
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
import numpy as np import numpy as np

View File

@@ -1 +1,17 @@
"""Data I/O readers for crash test formats.""" """Data I/O readers for crash test formats."""
from impakt.io.csv import CSVReader
from impakt.io.mme import MMEReader
from impakt.io.reader import ReaderProtocol, ReaderRegistry, get_registry, read, register_reader
from impakt.io.tdms import TDMSReader
__all__ = [
"CSVReader",
"MMEReader",
"ReaderProtocol",
"ReaderRegistry",
"TDMSReader",
"get_registry",
"read",
"register_reader",
]

View File

@@ -89,7 +89,6 @@ UNIT_MAP: dict[str, str] = {
"Nm": "N·m", "Nm": "N·m",
"N.m": "N·m", "N.m": "N·m",
"N*m": "N·m", "N*m": "N·m",
"Nm": "N·m",
"mm": "mm", "mm": "mm",
"m": "m", "m": "m",
"cm": "cm", "cm": "cm",
@@ -626,7 +625,7 @@ class MMEReader:
unit = _normalize_unit(header.get("unit", "")) unit = _normalize_unit(header.get("unit", ""))
dt = _parse_float(header.get("sampling interval", "0")) dt = _parse_float(header.get("sampling interval", "0"))
t_first = _parse_float(header.get("time of first sample", "0")) t_first = _parse_float(header.get("time of first sample", "0"))
num_samples_declared = _parse_int(header.get("number of samples", "0")) _parse_int(header.get("number of samples", "0")) # validated by data length
cfc_str = header.get("channel frequency class", "") cfc_str = header.get("channel frequency class", "")
cfc_class: int | None = None cfc_class: int | None = None
if cfc_str and cfc_str.upper() != "NOVALUE": if cfc_str and cfc_str.upper() != "NOVALUE":

View File

@@ -6,8 +6,6 @@ across all plotted channels.
from __future__ import annotations from __future__ import annotations
from typing import Any
import numpy as np import numpy as np
import pandas as pd import pandas as pd

View File

@@ -11,11 +11,10 @@ from __future__ import annotations
from typing import Any from typing import Any
import numpy as np
import plotly.graph_objects as go import plotly.graph_objects as go
from impakt.channel.model import Channel from impakt.channel.model import Channel
from impakt.plot.spec import ChannelRef, Corridor, CursorValues, PlotSpec, PlotStyle from impakt.plot.spec import Corridor, CursorValues, PlotSpec
# Default color palette (colorblind-friendly) # Default color palette (colorblind-friendly)
DEFAULT_COLORS = [ DEFAULT_COLORS = [

View File

@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any
from impakt.plot.engine import PlotEngine from impakt.plot.engine import PlotEngine
from impakt.plot.spec import PlotSpec from impakt.plot.spec import PlotSpec

View File

@@ -1 +1,19 @@
"""Plugin system for extensibility.""" """Plugin system for extensibility."""
from impakt.plugin.registry import (
ImpaktPlugin,
PluginRegistry,
discover_all,
discover_directory,
discover_entry_points,
get_plugin_registry,
)
__all__ = [
"ImpaktPlugin",
"PluginRegistry",
"discover_all",
"discover_directory",
"discover_entry_points",
"get_plugin_registry",
]

View File

@@ -10,7 +10,6 @@ Threshold values are versioned — this module supports multiple protocol years.
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any
from impakt.criteria.base import CriterionResult from impakt.criteria.base import CriterionResult
from impakt.protocol.base import BodyRegionScore, Color, ProtocolResult from impakt.protocol.base import BodyRegionScore, Color, ProtocolResult

View File

@@ -7,7 +7,6 @@ The overall rating is determined by the worst sub-rating.
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any
from impakt.criteria.base import CriterionResult from impakt.criteria.base import CriterionResult
from impakt.protocol.base import BodyRegionScore, ProtocolResult, Rating from impakt.protocol.base import BodyRegionScore, ProtocolResult, Rating

View File

@@ -8,7 +8,6 @@ injury probability, which are then combined.
from __future__ import annotations from __future__ import annotations
import math import math
from typing import Any
from impakt.criteria.base import CriterionResult from impakt.criteria.base import CriterionResult
from impakt.protocol.base import BodyRegionScore, ProtocolResult, Rating from impakt.protocol.base import BodyRegionScore, ProtocolResult, Rating

View File

@@ -1 +1,13 @@
"""PDF and report generation.""" """PDF and report generation."""
from impakt.report.engine import (
generate_injury_summary,
generate_plot_sheet,
generate_protocol_report,
)
__all__ = [
"generate_injury_summary",
"generate_plot_sheet",
"generate_protocol_report",
]

View File

@@ -155,7 +155,8 @@ def _fallback_protocol_html(
test_info = f""" test_info = f"""
<div class="test-info"> <div class="test-info">
<p><strong>Test:</strong> {metadata.test_number}</p> <p><strong>Test:</strong> {metadata.test_number}</p>
<p><strong>Vehicle:</strong> {metadata.vehicle.year} {metadata.vehicle.make} {metadata.vehicle.model}</p> <p><strong>Vehicle:</strong> {metadata.vehicle.year} {metadata.vehicle.make} \
{metadata.vehicle.model}</p>
<p><strong>Dummy:</strong> {metadata.dummy.dummy_type} ({metadata.dummy.position})</p> <p><strong>Dummy:</strong> {metadata.dummy.dummy_type} ({metadata.dummy.position})</p>
</div> </div>
""" """
@@ -178,7 +179,11 @@ def _fallback_protocol_html(
color_badge = f'<span class="badge" style="background:{bg}">{rs.rating.value}</span>' color_badge = f'<span class="badge" style="background:{bg}">{rs.rating.value}</span>'
points_str = f"{rs.points:.1f}/{rs.max_points:.1f}" if rs.max_points > 0 else "" points_str = f"{rs.points:.1f}/{rs.max_points:.1f}" if rs.max_points > 0 else ""
rows += f"<tr><td>{rs.region}</td><td>{rs.criterion}</td><td>{rs.value:.2f} {rs.unit}</td><td>{color_badge}</td><td>{points_str}</td></tr>" rows += (
f"<tr><td>{rs.region}</td><td>{rs.criterion}</td>"
f"<td>{rs.value:.2f} {rs.unit}</td>"
f"<td>{color_badge}</td><td>{points_str}</td></tr>"
)
stars_display = "" stars_display = ""
if result.stars is not None: if result.stars is not None:
@@ -207,10 +212,12 @@ def _fallback_protocol_html(
<div class="summary"> <div class="summary">
<p><span class="stars">{stars_display}</span></p> <p><span class="stars">{stars_display}</span></p>
<p><strong>Overall:</strong> {result.overall_rating}</p> <p><strong>Overall:</strong> {result.overall_rating}</p>
<p><strong>Score:</strong> {result.total_points:.1f}/{result.max_points:.1f} ({result.percentage:.0f}%)</p> <p><strong>Score:</strong> \
{result.total_points:.1f}/{result.max_points:.1f} ({result.percentage:.0f}%)</p>
</div> </div>
<table> <table>
<tr><th>Body Region</th><th>Criterion</th><th>Value</th><th>Rating</th><th>Points</th></tr> <tr><th>Body Region</th><th>Criterion</th><th>Value</th>\
<th>Rating</th><th>Points</th></tr>
{rows} {rows}
</table> </table>
</body> </body>

View File

@@ -1 +1,12 @@
"""Scripting API and CLI.""" """Scripting API and CLI."""
from impakt.script.api import ChannelHandle, Session, Template, TransformProxy
from impakt.script.cli import main
__all__ = [
"ChannelHandle",
"Session",
"Template",
"TransformProxy",
"main",
]

View File

@@ -32,12 +32,12 @@ import numpy as np
from impakt.channel.model import Channel, ChannelGroup, TestData, TestMetadata from impakt.channel.model import Channel, ChannelGroup, TestData, TestMetadata
from impakt.criteria.base import CriterionResult from impakt.criteria.base import CriterionResult
from impakt.io.mme import MMEReader from impakt.io.mme import MMEReader
from impakt.io.reader import ReaderRegistry, get_registry, register_reader from impakt.io.reader import get_registry, register_reader
from impakt.plot.engine import PlotEngine, cursor_values from impakt.plot.engine import PlotEngine
from impakt.plot.spec import ChannelRef, CursorValues, PlotSpec, PlotStyle from impakt.plot.spec import ChannelRef, PlotSpec, PlotStyle
from impakt.protocol.base import ProtocolResult from impakt.protocol.base import ProtocolResult
from impakt.template.library import TemplateLibrary from impakt.template.library import TemplateLibrary
from impakt.template.model import SessionState, TemplateSpec from impakt.template.model import TemplateSpec
from impakt.template.session import SessionManager from impakt.template.session import SessionManager
from impakt.transform.cfc import CFCFilter from impakt.transform.cfc import CFCFilter

View File

@@ -3,8 +3,6 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import sys
from pathlib import Path
def main(argv: list[str] | None = None) -> None: def main(argv: list[str] | None = None) -> None:
@@ -121,7 +119,6 @@ def _cmd_channels(args: argparse.Namespace) -> None:
def _cmd_evaluate(args: argparse.Namespace) -> None: def _cmd_evaluate(args: argparse.Namespace) -> None:
from impakt.script.api import Session from impakt.script.api import Session
from impakt.criteria import hic, clip_3ms, nij, chest_deflection, femur_load
session = Session.open(args.path) session = Session.open(args.path)

View File

@@ -1 +1,13 @@
"""Template and session management.""" """Template and session management."""
from impakt.template.library import TemplateLibrary
from impakt.template.model import PlotDefinition, SessionState, TemplateSpec
from impakt.template.session import SessionManager
__all__ = [
"PlotDefinition",
"SessionManager",
"SessionState",
"TemplateLibrary",
"TemplateSpec",
]

View File

@@ -6,8 +6,8 @@ Manages the global template library at ``~/.impakt/templates/``.
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from typing import Iterator
from impakt.template.model import TemplateSpec from impakt.template.model import TemplateSpec

View File

@@ -6,7 +6,6 @@ Sessions bind templates to specific test data and track user overrides.
from __future__ import annotations from __future__ import annotations
import copy
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -94,7 +93,6 @@ class TemplateSpec:
for plot_data in data.get("plots", []): for plot_data in data.get("plots", []):
channel_patterns = [] channel_patterns = []
transforms = [] transforms = []
corridors = []
for ch in plot_data.get("channels", []): for ch in plot_data.get("channels", []):
if isinstance(ch, str): if isinstance(ch, str):

View File

@@ -76,7 +76,7 @@ class XAlign:
# Threshold never crossed — no shift # Threshold never crossed — no shift
return channel.with_data( return channel.with_data(
data=channel.data, data=channel.data,
transform_note=f"X-align threshold (no crossing found)", transform_note="X-align threshold (no crossing found)",
) )
crossing_time = float(channel.time[indices[0]]) crossing_time = float(channel.time[indices[0]])

View File

@@ -7,7 +7,7 @@ TransformChain composes multiple transforms into a reproducible pipeline.
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass
from typing import Any, Protocol, runtime_checkable from typing import Any, Protocol, runtime_checkable
from impakt.channel.model import Channel from impakt.channel.model import Channel

View File

@@ -15,13 +15,11 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
import numpy as np
from scipy.signal import butter, filtfilt from scipy.signal import butter, filtfilt
from impakt.channel.lookup import CFC_CLASSES, CFC_MINIMUM_SAMPLE_RATES from impakt.channel.lookup import CFC_CLASSES, CFC_MINIMUM_SAMPLE_RATES
from impakt.channel.model import Channel from impakt.channel.model import Channel
VALID_CFC_CLASSES = frozenset(CFC_CLASSES.keys()) VALID_CFC_CLASSES = frozenset(CFC_CLASSES.keys())

View File

@@ -7,7 +7,6 @@ with numpy functions.
from __future__ import annotations from __future__ import annotations
import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -16,7 +15,6 @@ import numpy as np
from impakt.channel.code import ChannelCode from impakt.channel.code import ChannelCode
from impakt.channel.model import Channel from impakt.channel.model import Channel
# Allowed numpy functions in expressions # Allowed numpy functions in expressions
_SAFE_NUMPY = { _SAFE_NUMPY = {
"abs": np.abs, "abs": np.abs,

View File

@@ -6,9 +6,6 @@ The AppState holds Session objects server-side; Dash stores hold lightweight UI
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import dash import dash
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc

View File

@@ -13,8 +13,7 @@ from __future__ import annotations
from typing import Any from typing import Any
import dash import dash
from dash import ALL, Input, Output, State, html, no_update from dash import ALL, Input, Output, State
from dash.exceptions import PreventUpdate
from impakt.plot.engine import DEFAULT_COLORS from impakt.plot.engine import DEFAULT_COLORS
from impakt.web.components.channel_grid import ( from impakt.web.components.channel_grid import (
@@ -52,7 +51,6 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
if visible_data is None: if visible_data is None:
return prev_selected or [] return prev_selected or []
prev = set(prev_selected or [])
visible_keys = {row["key"] for row in visible_data} visible_keys = {row["key"] for row in visible_data}
# Keys currently checked in the visible table # Keys currently checked in the visible table
@@ -77,7 +75,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
Output("selected-channels-badges", "children"), Output("selected-channels-badges", "children"),
[Input("selected-channels-store", "data")], [Input("selected-channels-store", "data")],
) )
def update_badges(selected_keys: list[str] | None) -> list: def update_badges(selected_keys: list[str] | None) -> list[Any]:
return build_selected_channels_badges(selected_keys or [], app_state) return build_selected_channels_badges(selected_keys or [], app_state)
@app.callback( @app.callback(
@@ -104,7 +102,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
direction: str | None, direction: str | None,
selected_keys: list[str] | None, selected_keys: list[str] | None,
all_rows: list[dict[str, Any]] | None, all_rows: list[dict[str, Any]] | None,
) -> tuple[list[dict[str, Any]], list[int], list[dict]]: ) -> tuple[list[dict[str, Any]], list[int], list[dict[str, Any]]]:
"""Filter rows AND recompute selected_rows + color styling.""" """Filter rows AND recompute selected_rows + color styling."""
if not all_rows: if not all_rows:
return [], [], [] return [], [], []
@@ -135,7 +133,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
selected_indices.append(idx) selected_indices.append(idx)
# Build style_data_conditional for coloring selected rows # Build style_data_conditional for coloring selected rows
style_cond: list[dict] = [] style_cond: list[dict[str, Any]] = []
for idx, row in enumerate(filtered): for idx, row in enumerate(filtered):
if row["key"] in color_map: if row["key"] in color_map:
ci = color_map[row["key"]] ci = color_map[row["key"]]
@@ -168,7 +166,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
Output("per-channel-overrides", "children"), Output("per-channel-overrides", "children"),
[Input("selected-channels-store", "data")], [Input("selected-channels-store", "data")],
) )
def update_per_channel_overrides(selected_keys: list[str] | None) -> list: def update_per_channel_overrides(selected_keys: list[str] | None) -> list[Any]:
"""Show per-channel CFC override controls for selected channels.""" """Show per-channel CFC override controls for selected channels."""
return build_per_channel_override_rows( return build_per_channel_override_rows(
selected_keys or [], selected_keys or [],

View File

@@ -9,11 +9,9 @@ Handles:
from __future__ import annotations from __future__ import annotations
import base64 import base64
import io
from typing import Any from typing import Any
import dash import dash
import numpy as np
from dash import Input, Output, State, html from dash import Input, Output, State, html
from dash.exceptions import PreventUpdate from dash.exceptions import PreventUpdate
@@ -42,7 +40,7 @@ def register_corridor_callbacks(app: dash.Dash, app_state: AppState) -> None:
filename: str | None, filename: str | None,
corridor_name: str | None, corridor_name: str | None,
current_corridors: list[dict[str, Any]] | None, current_corridors: list[dict[str, Any]] | None,
) -> tuple[list[dict[str, Any]], Any, list]: ) -> tuple[list[dict[str, Any]], Any, list[Any]]:
if contents is None: if contents is None:
raise PreventUpdate raise PreventUpdate
@@ -123,7 +121,7 @@ def register_corridor_callbacks(app: dash.Dash, app_state: AppState) -> None:
) )
def _build_corridor_list(corridors: list[dict[str, Any]]) -> list: def _build_corridor_list(corridors: list[dict[str, Any]]) -> list[Any]:
"""Build the active corridors display list.""" """Build the active corridors display list."""
if not corridors: if not corridors:
return [html.Div("No corridors loaded", className="text-muted", style={"fontSize": "10px"})] return [html.Div("No corridors loaded", className="text-muted", style={"fontSize": "10px"})]

View File

@@ -10,7 +10,6 @@ from typing import Any
import dash import dash
from dash import Input, Output, State, html from dash import Input, Output, State, html
from dash.exceptions import PreventUpdate
from impakt.web.components.criteria import build_criteria_results_display from impakt.web.components.criteria import build_criteria_results_display
from impakt.web.state import AppState from impakt.web.state import AppState

View File

@@ -10,8 +10,7 @@ from __future__ import annotations
from typing import Any from typing import Any
import dash import dash
from dash import Input, Output, State, html from dash import Input, Output, State
from dash.exceptions import PreventUpdate
from impakt.plot.engine import DEFAULT_COLORS from impakt.plot.engine import DEFAULT_COLORS
from impakt.web.callbacks.plot_callbacks import _resolve_channels from impakt.web.callbacks.plot_callbacks import _resolve_channels
@@ -61,7 +60,7 @@ def register_cursor_callbacks(app: dash.Dash, app_state: AppState) -> None:
selected_keys: list[str] | None, selected_keys: list[str] | None,
cfc_value: str, cfc_value: str,
y_align: bool, y_align: bool,
channel_overrides: dict | None, channel_overrides: dict[str, Any] | None,
x_align_method: str, x_align_method: str,
x_align_value: float | None, x_align_value: float | None,
show_resultant: bool, show_resultant: bool,
@@ -105,7 +104,7 @@ def register_cursor_callbacks(app: dash.Dash, app_state: AppState) -> None:
x_align_method: str, x_align_method: str,
x_align_value: float | None, x_align_value: float | None,
show_resultant: bool, show_resultant: bool,
) -> list[dict]: ) -> list[dict[str, Any]]:
if not selected_keys: if not selected_keys:
return [] return []
@@ -119,7 +118,7 @@ def register_cursor_callbacks(app: dash.Dash, app_state: AppState) -> None:
show_resultant, show_resultant,
) )
style_cond: list[dict] = [] style_cond: list[dict[str, Any]] = []
for i in range(len(channels)): for i in range(len(channels)):
color = DEFAULT_COLORS[i % len(DEFAULT_COLORS)] color = DEFAULT_COLORS[i % len(DEFAULT_COLORS)]
style_cond.append( style_cond.append(

View File

@@ -8,14 +8,12 @@ Handles:
from __future__ import annotations from __future__ import annotations
import io
import tempfile
from typing import Any from typing import Any
import dash import dash
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from dash import Input, Output, State, dcc, html, no_update from dash import Input, Output, State, dcc, html
from dash.exceptions import PreventUpdate from dash.exceptions import PreventUpdate
from impakt.web.callbacks.plot_callbacks import _resolve_channels from impakt.web.callbacks.plot_callbacks import _resolve_channels
@@ -116,7 +114,6 @@ def register_export_callbacks(app: dash.Dash, app_state: AppState) -> None:
# Generate report # Generate report
try: try:
from impakt.report.engine import generate_protocol_report
import tempfile import tempfile
with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w") as f: with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w") as f:

View File

@@ -13,7 +13,7 @@ from pathlib import Path
from typing import Any from typing import Any
import dash import dash
from dash import Input, Output, State, html, no_update from dash import Input, Output, State, no_update
from dash.exceptions import PreventUpdate from dash.exceptions import PreventUpdate
from impakt.web.state import AppState from impakt.web.state import AppState

View File

@@ -17,7 +17,6 @@ import dash
import numpy as np import numpy as np
import plotly.graph_objects as go import plotly.graph_objects as go
from dash import Input, Output, State from dash import Input, Output, State
from dash.exceptions import PreventUpdate
from impakt.channel.model import Channel from impakt.channel.model import Channel
from impakt.plot.engine import DEFAULT_COLORS, PlotEngine from impakt.plot.engine import DEFAULT_COLORS, PlotEngine
@@ -148,7 +147,7 @@ def _build_plot_spec(
channels: list[tuple[str, Channel]], channels: list[tuple[str, Channel]],
cursor_x1: float | None, cursor_x1: float | None,
cursor_x2: float | None, cursor_x2: float | None,
corridors: list[dict] | None = None, corridors: list[dict[str, Any]] | None = None,
) -> PlotSpec: ) -> PlotSpec:
"""Build a PlotSpec from resolved channels and UI state. """Build a PlotSpec from resolved channels and UI state.
@@ -228,8 +227,8 @@ def register_plot_callbacks(app: dash.Dash, app_state: AppState) -> None:
x_align_method: str, x_align_method: str,
cursor_x1: float | None, cursor_x1: float | None,
cursor_x2: float | None, cursor_x2: float | None,
channel_overrides: dict | None, channel_overrides: dict[str, Any] | None,
corridors_data: list[dict] | None, corridors_data: list[dict[str, Any]] | None,
x_align_value: float | None, x_align_value: float | None,
) -> go.Figure: ) -> go.Figure:
if not selected_keys: if not selected_keys:

View File

@@ -14,13 +14,12 @@ without reordering.
from __future__ import annotations from __future__ import annotations
import fnmatch import fnmatch
import re
from typing import Any from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dash_table, dcc, html from dash import dash_table, dcc, html
from impakt.channel.lookup import DIRECTIONS, MAIN_LOCATIONS, MEASUREMENTS from impakt.channel.lookup import MAIN_LOCATIONS, MEASUREMENTS
from impakt.plot.engine import DEFAULT_COLORS from impakt.plot.engine import DEFAULT_COLORS
from impakt.web.state import AppState from impakt.web.state import AppState
@@ -277,7 +276,7 @@ def build_channel_grid(app_state: AppState) -> dbc.Card:
def build_selected_channels_badges( def build_selected_channels_badges(
selected_keys: list[str], selected_keys: list[str],
app_state: AppState, app_state: AppState,
) -> list: ) -> list[Any]:
"""Build badge pills showing currently selected channels.""" """Build badge pills showing currently selected channels."""
if not selected_keys: if not selected_keys:
return [ return [

View File

@@ -14,9 +14,8 @@ from __future__ import annotations
from typing import Any from typing import Any
import numpy as np
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
import numpy as np
from dash import dash_table, dcc, html from dash import dash_table, dcc, html
from impakt.channel.model import Channel from impakt.channel.model import Channel
@@ -152,7 +151,7 @@ def build_channel_values_panel() -> dbc.Card:
"whiteSpace": "nowrap", "whiteSpace": "nowrap",
}, },
style_cell_conditional=[ style_cell_conditional=[
# Fixed-width columns (percentages sum to ~74%, leaving ~26% for Description) # Fixed-width columns (~74%, leaving ~26% for Description)
{ {
"if": {"column_id": "ch_num"}, "if": {"column_id": "ch_num"},
"width": "3%", "width": "3%",

View File

@@ -8,8 +8,6 @@ Provides:
from __future__ import annotations from __future__ import annotations
from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dcc, html from dash import dcc, html

View File

@@ -12,7 +12,7 @@ from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import html from dash import html
from impakt.channel.model import Channel, TestData from impakt.channel.model import TestData
from impakt.criteria import ( from impakt.criteria import (
CriterionResult, CriterionResult,
chest_deflection, chest_deflection,
@@ -21,7 +21,6 @@ from impakt.criteria import (
hic15, hic15,
nij, nij,
tibia_index, tibia_index,
viscous_criterion,
) )
from impakt.protocol.base import Color, ProtocolResult, Rating from impakt.protocol.base import Color, ProtocolResult, Rating
@@ -229,7 +228,7 @@ def build_criteria_results_display(
protocol_result: ProtocolResult | None = None, protocol_result: ProtocolResult | None = None,
) -> html.Div: ) -> html.Div:
"""Build the criteria results display.""" """Build the criteria results display."""
elements: list = [] elements: list[Any] = []
# Protocol summary # Protocol summary
if protocol_result: if protocol_result:

View File

@@ -7,7 +7,7 @@ and template selector.
from __future__ import annotations from __future__ import annotations
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dcc, html from dash import html
from impakt.web.state import AppState from impakt.web.state import AppState

View File

@@ -8,10 +8,8 @@ Example: sqrt(ax**2 + az**2) with ax=Head Accel X, az=Head Accel Z
from __future__ import annotations from __future__ import annotations
from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dcc, html from dash import html
from impakt.web.state import AppState from impakt.web.state import AppState

View File

@@ -6,10 +6,11 @@ plot panes. Each pane has its own channel selection and axis labels.
from __future__ import annotations from __future__ import annotations
from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dcc, html from dash import dcc, html
# Layout presets: (rows, cols) # Layout presets: (rows, cols)
LAYOUT_PRESETS: dict[str, tuple[int, int]] = { LAYOUT_PRESETS: dict[str, tuple[int, int]] = {
"1x1": (1, 1), "1x1": (1, 1),
@@ -57,7 +58,7 @@ def build_plot_grid(layout: str = "1x1") -> html.Div:
) )
def _build_panes(layout: str) -> list: def _build_panes(layout: str) -> list[Any]:
"""Build plot pane elements for a given layout.""" """Build plot pane elements for a given layout."""
rows, cols = LAYOUT_PRESETS.get(layout, (1, 1)) rows, cols = LAYOUT_PRESETS.get(layout, (1, 1))
pane_count = rows * cols pane_count = rows * cols
@@ -110,7 +111,7 @@ def _build_single_pane(pane_idx: int, total_panes: int) -> dbc.Card:
) )
def build_empty_plot_figure() -> dict: def build_empty_plot_figure() -> dict[str, Any]:
"""Build an empty plot figure with instruction text.""" """Build an empty plot figure with instruction text."""
return { return {
"data": [], "data": [],

View File

@@ -10,10 +10,8 @@ Provides:
from __future__ import annotations from __future__ import annotations
from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dcc, html from dash import html
from impakt.web.state import AppState from impakt.web.state import AppState

View File

@@ -5,6 +5,8 @@ Provides global transform defaults and per-channel override capability.
from __future__ import annotations from __future__ import annotations
from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dcc, html from dash import dcc, html
@@ -92,7 +94,7 @@ def build_transform_panel() -> dbc.Card:
def build_per_channel_override_rows( def build_per_channel_override_rows(
selected_keys: list[str], selected_keys: list[str],
overrides: dict[str, dict[str, str]], overrides: dict[str, dict[str, str]],
) -> list: ) -> list[Any]:
"""Build per-channel override controls for selected channels. """Build per-channel override controls for selected channels.
Shows a compact row per selected channel with a CFC dropdown override. Shows a compact row per selected channel with a CFC dropdown override.

View File

@@ -8,13 +8,12 @@ Two tabs:
from __future__ import annotations from __future__ import annotations
from typing import Any
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import dcc, html from dash import dcc, html
from impakt.web.components.channel_grid import build_channel_grid from impakt.web.components.channel_grid import build_channel_grid
from impakt.web.components.channel_values import build_channel_values_panel from impakt.web.components.channel_values import build_channel_values_panel
from impakt.web.components.corridors import build_corridor_panel
from impakt.web.components.criteria import build_criteria_panel from impakt.web.components.criteria import build_criteria_panel
from impakt.web.components.header import ( from impakt.web.components.header import (
build_header, build_header,
@@ -22,9 +21,8 @@ from impakt.web.components.header import (
build_overlay_modal, build_overlay_modal,
build_test_info_panel, build_test_info_panel,
) )
from impakt.web.components.plot_grid import build_plot_grid
from impakt.web.components.corridors import build_corridor_panel
from impakt.web.components.math_builder import build_math_panel from impakt.web.components.math_builder import build_math_panel
from impakt.web.components.plot_grid import build_plot_grid
from impakt.web.components.report import build_report_panel from impakt.web.components.report import build_report_panel
from impakt.web.components.templates import build_template_panel from impakt.web.components.templates import build_template_panel
from impakt.web.components.transforms import build_transform_panel from impakt.web.components.transforms import build_transform_panel

View File

@@ -15,12 +15,10 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from impakt.channel.model import Channel, TestData from impakt.channel.model import Channel
from impakt.script.api import Session from impakt.script.api import Session
from impakt.template.library import TemplateLibrary from impakt.template.library import TemplateLibrary
from impakt.template.model import PlotDefinition, TemplateSpec from impakt.template.model import PlotDefinition, TemplateSpec
from impakt.transform.align import YAlign
from impakt.transform.cfc import CFCFilter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)