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
4 changes: 2 additions & 2 deletions .github/workflows/theme-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ jobs:
run: mypy src/

- name: Shell check
run: shellcheck --shell=bash --severity=warning scripts/*.sh
run: git ls-files 'scripts/*.sh' 'lib/*.sh' 'DreamcoderShell/*.sh' 'DreamcoderShell/**/*.sh' | xargs shellcheck --shell=bash --severity=warning

- name: Run tests
run: python -m pytest tests/ -v --tb=short

- name: Coverage
run: python -m pytest tests/ --cov=dreamcoder_theme --cov-fail-under=40
run: python -m pytest tests/ --cov=dreamcoder_theme --cov-fail-under=80

- name: Run theme health check
env:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ repos:
hooks:
- id: shellcheck
args: ["--shell=bash", "--severity=warning"]
files: ^(scripts|lib)/
files: ^(scripts|lib|DreamcoderShell)/.*\.sh$

- repo: local
hooks:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ extract() {
echo "Usage: extract <archive> [output_dir]"
return 1
fi
local file="$1" dir="${2:-${file%.*}}"
# Two statements: in bash a single `local` expands every value before any is
# assigned, so dir would not see file (ShellCheck SC2318).
local file="$1"
local dir="${2:-${file%.*}}"
case "$file" in
*.tar.gz | *.tgz) tar -xzf "$file" -C "$(dirname "$file")" 2>/dev/null || tar -xzf "$file" ;;
*.tar.bz2 | *.tbz2) tar -xjf "$file" ;;
Expand Down
6 changes: 3 additions & 3 deletions docs/adr/0003-python-quality-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ We adopt a three-layer quality strategy for all Python code:
### Layer 3 — Coverage (pytest-cov)

- **Metric**: Branch coverage via `pytest --cov=dreamcoder_theme`
- **Threshold**: `fail_under = 40` (baseline; expected to increase over time)
- **Threshold**: `fail_under = 80` (ratcheted from the original 40 baseline on 2026-09-29, when measured coverage was 83%; raise it again only when coverage rises — never lower it)
- **Reporting**: `--cov-report=term-missing` shows uncovered lines

All three layers run in CI on every push and pull request.
Expand All @@ -52,15 +52,15 @@ Positive:

Negative:
- Mypy strict mode requires explicit annotations throughout the codebase
- Coverage fail-under of 40 is low — new code should aim for 80%+
- The 80% floor sits just under the measured level (83%), so a large untested addition fails CI; raising it is a deliberate follow-up
- Initial migration required fixing pre-existing issues

## Compliance

- `ruff check src/ tests/` must pass
- `ruff format --check src/ tests/` must pass
- `mypy src/` must pass (strict mode)
- `pytest --cov=dreamcoder_theme --cov-fail-under=40` must pass
- `pytest --cov=dreamcoder_theme --cov-fail-under=80` must pass

## Alternatives Considered

Expand Down
5 changes: 3 additions & 2 deletions docs/configuration/ml4w.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ The ML4W 2.16 delta is ported as follows:
| Rewritten AZERTY detection (`fr`, `be`) | Ported; binds the AZERTY keysyms only on AZERTY layouts, since the profile owns the digit workspace binds |

`tests/ml4w/keybindings_variant.bats` fails on any new collision between the
variant and the profile. The `SUPER + SHIFT + arrows` overlap (variant resize,
profile move) predates 2.16 and is listed there as a known exception.
variant and the profile; there are no exceptions. The profile owns
`SUPER + SHIFT + arrows` (move window), so the variant's keyboard resize sits on
`SUPER + CTRL + arrows` instead of ML4W's upstream `SUPER + SHIFT + arrows`.

## hyprctl dispatch is broken on Hyprland 0.55+ — native dispatchers used

Expand Down
12 changes: 6 additions & 6 deletions ml4w_assets/hypr/conf/keybindings/dreamcoder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ local mainMod = "SUPER" -- Sets "Windows" key as main modifier
-- Only the emoji picker stays here.
hl.bind(mainMod .. " + CTRL + E", hl.dsp.exec_cmd("~/.config/ml4w/settings/emojipicker.sh"), { description = "Open the emoji picker" })

-- Windows
-- Profile owns: Q (kill), F (fullscreen), T (float), H/L/K/J + arrows (focus),
-- SHIFT+H/L/K/J + arrows (move window), Y (toggle split).
-- SHIFT+H/L/K/J + arrows (move window), Y (toggle split). Keyboard resize sits on
-- CTRL + arrows here (upstream ML4W uses SHIFT + arrows, which the profile owns).
hl.bind(mainMod .. " + SHIFT + Q", hl.dsp.exec_cmd("hyprctl activewindow | grep pid | tr -d 'pid:' | xargs kill"), { description = "Quit active window and all open instances" })
hl.bind(mainMod .. " + M", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }), { description = "Toggle Maximize Window" })
hl.bind(mainMod .. " + SHIFT + T", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-toggle-allfloat"), { description = "Toggle floating for all windows of workspace" })
hl.bind(mainMod .. " + ALT + T", function() hl.dispatch(hl.dsp.window.float({ action = "toggle" })); hl.dispatch(hl.dsp.window.pin()) end, { description = "Toggle floating + pinned" })
hl.bind(mainMod .. " + SHIFT + right", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { repeating = true }, { description = "Increase window width with keyboard" })
hl.bind(mainMod .. " + SHIFT + left", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true }, { description = "Reduce window width with keyboard" })
hl.bind(mainMod .. " + SHIFT + down", hl.dsp.window.resize({ x = 0, y = 100, relative = true }), { repeating = true }, { description = "Increase window height with keyboard" })
hl.bind(mainMod .. " + SHIFT + up", hl.dsp.window.resize({ x = 0, y = -100, relative = true }), { repeating = true }, { description = "Reduce window height with keyboard" })
hl.bind(mainMod .. " + CTRL + right", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { repeating = true }, { description = "Increase window width with keyboard" })
hl.bind(mainMod .. " + CTRL + left", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true }, { description = "Reduce window width with keyboard" })
hl.bind(mainMod .. " + CTRL + down", hl.dsp.window.resize({ x = 0, y = 100, relative = true }), { repeating = true }, { description = "Increase window height with keyboard" })
hl.bind(mainMod .. " + CTRL + up", hl.dsp.window.resize({ x = 0, y = -100, relative = true }), { repeating = true }, { description = "Reduce window height with keyboard" })
hl.bind(mainMod .. " + G", hl.dsp.group.toggle(), { description = "Toggle window group" })
-- hl.bind(mainMod .. " + SHIFT + G", hl.dsp.group.active("f"), { description = "Switch to next group window" })
hl.bind(mainMod .. " + ALT + left", hl.dsp.window.swap({ direction = "l" }), { description = "Swap tiled window left" })
Expand Down
93 changes: 93 additions & 0 deletions odd/tasks/engineering-quality-pass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Engineering quality pass

## Objective

Raise maintainability of the theme engine and the repo gates using measured
evidence, without changing behaviour: reduce the worst cyclomatic-complexity
hotspots, ratchet the coverage gate to the real level, and document publication.

## Problem (measured 2026-09-29)

- `mypy --strict`: clean (56 files). Coverage: 83% (gate is 40%). ruff: clean.
- radon cc worst offenders: `palette.validate_palette` E(34),
`design_system.evaluate_contract` D(26), `design_system._parse_renderer_output`
D(21), then C(13–19) in doctor, targets, audit, herdr renderer, repair, backups.
- Largest modules: `sync.py` 1061 lines, `cli_handlers.py` 620, `herdr_switch.py` 552.
- Coverage gate at 40% would let a 40-point regression through.

## Why

Central gates (contrast validation, design-system contract) with cyclomatic
complexity above 20 are hard to review and easy to break; a coverage floor far below
reality is not a gate.

## Scope

- In: behaviour-preserving extraction refactors of the three D/E functions (small
pure helpers, no new abstractions beyond need), characterization tests written
first where existing coverage of a branch is missing, coverage ratchet in
`pyproject.toml` and CI, PR slicing plan for the stacked branches.
- Out: splitting `sync.py`/`cli_handlers.py` wholesale (separate design decision,
in-flight `hexagonal-architecture-v2` change exists); pushing or opening PRs
(user decision); changing any generated artifact byte.

## Constraints

- Public function signatures and outputs unchanged; every Light/Dark artifact
byte-identical (`./scripts/dreamcoder sync` repo-only must report 0 changes).
- ruff, ruff format, `mypy --strict`, shellcheck stay clean.
- Never stage the unrelated timer-rewritten / user working-tree files.
- TDD: off (no project configuration). Runners: `python -m pytest tests/`,
`bats tests/shell/ tests/ml4w/`. Complexity check: `uvx radon cc src -s -n C`.
- Delivery: work-unit commits on `refactor/remove-night-profile`.

## Tasks

- [x] Q0 — Resolve the SUPER+SHIFT+arrows overlap (variant resize vs profile move):
resize moved to SUPER+CTRL+arrows, collision test has no exceptions. Route: inline.
- [x] Q1 — Refactor `validate_palette` (E→ at most B) preserving every error message and order. Route: delegated writer.
`validate_palette` E(34) → B(6); helpers all ≤ B(8). Commits: 4c4214b (refactor),
c4db159 (fail-fast/skip tests); characterization tests landed in 684026f (see note).
- [x] Q2 — Refactor `evaluate_contract` and `_parse_renderer_output` (D→ at most B). Route: delegated writer.
`evaluate_contract` D(26) → A(1), `_parse_renderer_output` D(21) → A(2) via a per-target
parser table; helpers all ≤ B(7). Commit: d503d2e.
- [x] Q3 — Ratchet coverage `fail_under` (pyproject + CI + ADR-0003) from 40 to 80 (measured 83–84%). Commit: 089f513. Route: inline.
- [x] Q4 — PR slicing plan (nothing pushed; user decides): keep commit order, cut at
commit boundaries into chained PRs: (1) ML4W 2.16 compat f7c2b9b..339ab7e,
(2) stow layer + Herdr 0.9.1 3398856..a7c925a, (3) theme-mode correctness + listener
hook 702751f..8127674, (4) shell safety c9cf723..754d352, (5) Night removal
319ffdd..f7597b1, (6) quality pass 089f513 onwards. PRs 1, 2, 3 and 5 exceed the
~400-line heuristic (tests, fixtures and generated deletions dominate); 684026f mixes
characterization tests into the extract() fix, to be re-cut when slicing. Route: inline.
- [x] Q5 — Found while verifying Q1/Q2: `dreamcoder sync --help` ran a real sync.
`main(argv)` now parses arguments before any side effect (--help exits 0, unknown
options exit 2); tests inject argv. Route: inline.

## Acceptance criteria

- radon reports no function above C(15) in the three refactored areas; `validate_palette` at most B.
- Full pytest + bats green; repo-only sync reports 0 changes; mypy/ruff clean.

## Progress

- Baseline metrics above. Q0 done.
- Q1/Q2 evidence: characterization tests written first and green on the old code
(golden ordered error list over a frozen dark palette; exact findings for
MISSING_MODE, ROLE_RESOLUTION, RENDER_FAILURE, SEMANTIC_PROVENANCE_INVALID; parser
invalid-JSON/unknown-target). The three functions are now 100% line-covered.
Coverage total 83% → 84%. pytest exit 0; bats 147/147 ok; ruff, ruff format, mypy
(56 files) clean. Byte identity: sync run in an isolated worktree with a throwaway
HOME/XDG_* (`DREAMCODER_THEME_MODE=dark|light`, `DREAMCODER_WRITE_REPO=1`), sha256
of all 900 tracked non-src files plus 20 HOME outputs identical HEAD vs refactor in
both modes.
- Note: a concurrent commit (684026f `fix(shell): ...`) swept the staged
characterization tests in with it; left as-is (no history rewrite). Later commits
use `git commit --only <paths>`.
- Incident: `./scripts/dreamcoder sync --help` does not parse `--help` and ran a real
sync once (active light identity re-applied to live paths; it also regenerated the
timer-modified `DreamcoderPi/.../dreamcoder-dark.json` and
`DreamcoderWarp/.../Dreamcoder-Dark.yaml` back to their committed bytes).

## Next step

Feature complete; publication is the user's decision.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ relative_files = true

[tool.coverage.report]
show_missing = true
fail_under = 40
fail_under = 80

[dependency-groups]
dev = ["mypy>=2.1.0"]
Loading