diff --git a/.github/workflows/md-naming.yml b/.github/workflows/md-naming.yml new file mode 100644 index 0000000..1bb197c --- /dev/null +++ b/.github/workflows/md-naming.yml @@ -0,0 +1,38 @@ +name: md-naming + +# The four markdown namespaces, enforced on push as well as at commit time. +# The local pre-commit hook can be skipped (a fresh clone that never ran +# `pre-commit install` has no hooks at all), so the convention needs a second +# gate that no one can forget to install. +# +# Deliberately a SEPARATE workflow from any release job: a badly named doc +# should fail lint, not block publishing an artifact. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + md-naming: + name: markdown naming convention + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The gate and its exemption list live in the shared utils repo so every + # repo enforces the same namespaces. Checking it out beats vendoring a + # copy here, which is exactly how a gate and a survey start disagreeing. + - name: Check out the shared gate + uses: actions/checkout@v4 + with: + repository: kuhyx/utils + path: .utils + + - name: Enforce the naming convention + run: bash .utils/scripts/check_md_naming.sh --all + + # Warns only: no machine can prove a task is finished, so an old TODO + # gets a nudge, never a failure. + - name: Warn about stale TODOs + run: PYTHONPATH=.utils python3 .utils/md_naming/stale.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f9158b..ad474f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -154,8 +154,20 @@ repos: files: '^wake_alarm/.*\.py$' pass_filenames: false require_serial: true - - id: max-file-length - name: Max file length (500 lines) - entry: python3 scripts/check_file_length.py + # The shared 250-line cap. This repo used to run a local script with a + # higher limit under this same hook id, so it looked gated while letting + # through files every other repo rejects. + - id: file-length + name: file length <= 250 lines + entry: scripts/check_file_length.sh language: system - types_or: [python, shell] + + # Four markdown namespaces (README / CLAUDE* / DOCS* / TODO*), plus the + # rule that a TODO carries "REMOVE ME AFTER FINISH" and nothing else may. + # Shared gate in ~/utils owns the namespaces and the exemption list, so + # one repo's idea of a valid name cannot drift from every other repo's. + - id: md-naming + name: markdown naming convention + entry: scripts/check_md_naming.sh + language: system + files: \.(md|markdown)$ diff --git a/DESIGN_AUDIT_TODO.md b/DESIGN_AUDIT_TODO.md deleted file mode 100644 index 55bea41..0000000 --- a/DESIGN_AUDIT_TODO.md +++ /dev/null @@ -1,251 +0,0 @@ -# Design audit — wake-alarm - -Generated against safe-design-rules (anthonyhobday.com/sideprojects/saferules). -Report only — nothing in this repo was changed by the audit itself. - -This repo has two distinct UI surfaces, audited separately below: -1. Flutter app — `phone_app/lib/` (theme in `main.dart`, screens in `screens/`) -2. Python/Tkinter alarm dismiss-challenge UI — `wake_alarm/_alarm.py` - -Passing/violation/N-A counts per surface are given at the top of each section -so all 28 rules are accounted for even though only violations get expanded. - ---- - -## Flutter app (`phone_app/`) - -Theme entry point: `phone_app/lib/main.dart:17-21` — -`ThemeData.dark(useMaterial3: true).copyWith(colorScheme: -ColorScheme.fromSeed(seedColor: Colors.orange, brightness: Brightness.dark))`. -Material 3's `fromSeed` auto-generates near-black surfaces, tinted neutrals, -and contrast-checked roles, so rules 1/2/3/9/10/18 pass "for free" via the -theme — the actual findings are places where code reaches for a literal -`Colors.*` constant instead of a role from that scheme, or a shadow/padding -value the theme doesn't control. - -Verdict roster: Pass — 1,2,3,8,10,17,18,23,26. Violation — 4,9(partial, -see notes),11,20,26(Card),28. N/A — 5,6,7,12,13,14,15,16,19,21,22,24,25,27. -(9 and 26 appear in both lists — see the per-rule notes.) - -### Violations - -- **Rule 4 (everything deliberate)** — `phone_app/lib/screens/home_screen.dart:198-199` and - `settings_screen.dart:126-128,161,245` use raw `Colors.redAccent` / - `Colors.greenAccent` / `Colors.red` for status/error text instead of - `Theme.of(context).colorScheme.error` (or a `tertiary`/success role); - `home_screen.dart:207` uses raw `Colors.orange` and `settings_screen.dart:138` - uses raw `Colors.grey` for the same reason. None of these are derived from - the seeded `ColorScheme`, so they won't stay in sync if the seed color ever - changes, and they bypass whatever contrast guarantee `fromSeed` gives the - rest of the app. → Replace with `colorScheme.error` / a defined success - color / `colorScheme.onSurfaceVariant`. -- **Rule 9 (distinct brightness / no competing colors)** — `home_screen.dart:198-199` - vs `settings_screen.dart:126-128,161`: `Colors.redAccent`/`Colors.greenAccent` - are reused ad hoc as the only "status" signal color pair across two separate - screens with no shared definition — not a brightness clash per se, but an - undeliberate ad-hoc pairing (same root cause as rule 4). -- **Rule 11 (mathematically related measurements)** — the app mostly uses an - 8px scale (`home_screen.dart:149,154,165,170,191,203` = 32/16/8/32/24/16; - `settings_screen.dart:93,98,121,131` = 16/16/16/32) but breaks it twice: - `settings_screen.dart:111` (`padding: EdgeInsets.symmetric(vertical: 14)`) - and `settings_screen.dart:163` (`SizedBox(width: 12)`) — 14 and 12 aren't on - the 8px grid the rest of the file uses. → change to 16/8 or otherwise round - to the established scale. -- **Rule 20 (body text ≥16px)** — `settings_screen.dart:138`: - `TextStyle(fontSize: 12, color: Colors.grey)` for the PC-config help text is - below the 16px minimum. `home_screen.dart:153` (`fontSize: 16`) and other - `fontSize: 18` usages are fine. -- **Rule 26 (no shadows in dark interfaces)** — `settings_screen.dart:154` - (`return Card(...)`) is an unmodified Material 3 `Card`, which renders with - default elevation (a soft shadow/tonal overlay) inside a `ThemeData.dark()` - app (`main.dart:17`). Per the rule, dark UIs should avoid shadows entirely. - → Either set `elevation: 0` and rely on a filled/tonal surface color for - the card, or accept the default and drop this from future review — flagging - now since it's the only place shadows appear at all (see rule 27 below). -- **Rule 28 (lower icon contrast next to text)** — `settings_screen.dart:159-162` - (`Icon(connected ? Icons.check_circle : Icons.cancel, color: connected ? - Colors.greenAccent : Colors.redAccent)`) sits directly next to `Text(...)` - at full theme-text contrast — the icon isn't dimmed relative to the - adjacent label. Same issue for the settings-gear `IconButton` at - `home_screen.dart:133-134` (default `Icons.settings` color, undimmed, next - to the "Wake Alarm Sync" AppBar title). - -### Not applicable - -- Rule 5 (optical alignment) — only stock Material icons/text used, no custom - shapes needing eyeballed alignment. -- Rule 6 (letter-spacing/line-height by size) — no explicit `letterSpacing`/ - `height` overrides anywhere in `phone_app/lib/` (grep confirmed); relies on - Material defaults, nothing to audit. -- Rule 7 (border contrast) — no `Border`/`BorderRadius`/`border:` usage found - anywhere under `phone_app/lib/` (grep confirmed); no borders exist. -- Rule 12 (order by visual weight) — single linear `Column` per screen, no - competing-weight layout to order. -- Rule 13 (12-column grid) — single-column mobile screens, no grid layout. -- Rule 14 (space between high-contrast points) — can't be assessed from - source; spacing is via `SizedBox`/`Padding` bounding boxes, not measured - edge-to-edge. -- Rule 15 (closer = lighter) — the one elevated element (`Card`, - `settings_screen.dart:154`) follows M3's automatic surface-tint elevation - overlay, which already lightens on elevation — no manual z-stack to check. -- Rule 16 (shadow blur = 2x distance) — shadow is Material's default (see - rule 26), not a custom `BoxShadow`, so there's no explicit offset/blur pair - to check a ratio on. -- Rule 19 (outer padding ≥ inner) — `settings_screen.dart:93` outer body - padding (16) vs `settings_screen.dart:156` `_StatusCard`'s own inner - padding (16): equal, satisfies "≥". `home_screen.dart:149` outer padding - (32) vs the button's inner vertical padding (16, `home_screen.dart:186`): - outer > inner. Both pass, no violation to report. -- Rule 21 (line length ~70 chars) — only short status strings/labels; the one - paragraph (`settings_screen.dart:132-137`) is already manually broken into - short lines, no long unbroken runs to measure. -- Rule 22 (button padding horizontal = 2x vertical) — `home_screen.dart:185-188` - and `settings_screen.dart:110-113` only override `vertical` padding - (16 and 14) and leave horizontal at the `ElevatedButton` theme default — - the actual rendered ratio can't be read from source alone. -- Rule 24 (nest corners) — no custom `BorderRadius` anywhere (grep - confirmed), only default Material shapes. -- Rule 25 (adjacent hard divides) — only one `Divider` in the whole app - (`settings_screen.dart:131`), not adjacent to another border/transition. -- Rule 27 (don't mix depth techniques) — only one depth technique appears at - all (the `Card`'s default elevation, rule 26) — nothing to mix. - -### Notes - -- No dedicated `theme.dart` — the `ColorScheme`/`ThemeData` is inline in - `main.dart:17-21`. That's fine at this size (2 screens), but every raw - `Colors.*` literal found above is a spot that would have been caught by a - lint/review pass if a shared palette module existed to import from instead. - ---- - -## Python/Tkinter alarm UI (`wake_alarm/_alarm.py`) - -No shared style/constants module exists for color or font — confirmed via -`find` (no `theme`/`style`/`colors` module under `wake_alarm/`) and via -`_constants.py`, which defines timing/day constants but nothing visual. -Every `tk.Label`/`tk.Frame`/`tk.Entry` call in `_alarm.py` repeats its own -hex-color and `font=(...)` tuple literal inline (confirmed: `_alarm.py:166, -172-174,181-183,190-192,200-206,209-211,219-224,228-232,326-331,409`). This -structural absence is itself the rule 4 finding — it also means the same -rule (e.g. rule 2, rule 10) is violated independently at every call site -rather than in one place, so a future fix should introduce a small -`_style.py`/constants block rather than patch each `tk.Label(...)` call. - -Verdict roster: Pass — 3,8,17,20,21,23,26,27. Violation — 1(partial), -2,4,9(mixed reuse—see below),10,11,12. N/A — 5,6,7,13,14,15,16,18,19,22,24,25,28. - -### Violations - -- **Rule 1 (near-black/near-white, not pure)** — `_alarm.py:166` (`bg="#1a1a1a"`) - correctly uses near-black, not pure black — that part passes. But - `_alarm.py:191-192` (`info_label`, `fg="white"`) uses **pure white** text, - which the rule explicitly calls out to avoid. → use an off-white like - `#e8e8e8`/`#f0f0ee` instead of the literal `"white"`. -- **Rule 2 (saturate your neutrals)** — the only "neutral" grey values used - are perfectly desaturated (zero-chroma) greys: `_alarm.py:232` - (`timer_label`, `fg="#aaaaaa"`) and `_alarm.py:305` (hidden-code flash - state, `fg="#555555"`). Neither has any color tint, so they read as - generic gray rather than part of the warm/cool palette the rest of the - screen uses (see rule 10). → tint both toward the same hue family as the - accent colors (e.g. a warm dark gray like `#4a3f3f` / `#8a7d7d`). -- **Rule 4 (everything deliberate / centralize styling)** — see the surface - intro above; every color/font is a repeated inline literal across - `_alarm.py:166-234` and `_alarm.py:326-331`, with no single source of - truth. → extract a small `_COLORS`/`_FONTS` mapping (mirroring the - existing `_constants.py` pattern for non-visual constants) and reference - it from every widget call. -- **Rule 9 (distinct brightness / deliberate reuse)** — `title_label` - (`_alarm.py:173`, `fg="#ff4444"`) and `status_label` - (`_alarm.py:223`, `fg="#ff4444"`) reuse the exact same color for two - different semantic roles ("WAKE UP!" heading vs. a transient error/status - message) with no evidence it was a deliberate choice rather than - copy-paste — flagging alongside rule 4 since fixing the missing style - module would force this decision to be made explicitly. -- **Rule 10 (warm OR cool, not both)** — the screen simultaneously uses warm - reds/ambers (`_alarm.py:173` `#ff4444`, `:182` `#ffaa00`, `:409` - `#ff0000` flash) and a fully saturated cool green (`_alarm.py:204,263,274, - 347` `#00ff00`) on the same screen at the same time. → pick one - temperature family for the "safe/positive" accent (e.g. a warm amber-green - or switch the alert red to a cooler red) so the palette reads as one - coherent set rather than a stoplight. -- **Rule 11 (mathematically related measurements)** — `pady` values across - the widget stack are `20, 5, 10, 30, 10, 10, 5` (`_alarm.py:176, 185, 194, - 207, 215, 226, 235`) — mostly multiples of 5/10, but `pady=5` at lines 185 - and 235 breaks the 10/20/30 pattern used by the surrounding widgets. - → round the two `pady=5` calls to `10` for a consistent scale. -- **Rule 12 (order by visual weight)** — the largest, boldest element on - screen is `code_label` at up to 72pt bold (`_alarm.py:198-206`, - `code_font_size = 48 if ... else 72`), sitting in the visual middle of the - stack, while the actual title (`_alarm.py:172`, 48pt bold) anchors the top - edge and the lightest elements (`timer_label`, 14pt, `_alarm.py:231`) sit - at the bottom edge. The rule wants the heaviest element toward an outer - edge, not buried in the middle. → this is likely intentional here (the - code you must type back is the whole point of the screen), so treat as a - judgment-call violation to confirm rather than an unambiguous bug. - -### Not applicable - -- Rule 5 (optical alignment) — only text `Label`s stacked via `.pack()`, no - asymmetric shapes/icons to align by eye. -- Rule 6 (letter-spacing/line-height by size) — Tkinter's `font=(family, - size, weight)` tuple has no letter-spacing/line-height control at all; - not expressible in this toolkit. -- Rule 7 (border contrast) — no `relief`/`highlightthickness`/`borderwidth` - set on any widget (grep confirmed); nothing has a visible border. -- Rule 13 (12-column grid) — single fullscreen widget centered via - `.place(relx=0.5, rely=0.5)` (`_alarm.py:167`), no grid layout in use. -- Rule 14 (space between high-contrast points) — `pack(pady=...)` controls - bounding-box spacing, not measured edge-to-edge visual contrast; not - determinable from source. -- Rule 15 (closer = lighter) — Tkinter has no z-stack/elevation concept; - this is a flat 2D layout on one background plane. -- Rule 16 (shadow blur = 2x distance) — no shadow effects implemented - anywhere in the file. -- Rule 18 (container brightness limits) — only one background plane exists - (`#1a1a1a`, toggled with a full-screen alert flash to `#ff0000` at - `_alarm.py:409` — not a stacked container), so there's no second surface - brightness to compare it against. -- Rule 19 (outer padding ≥ inner) — `container` (`_alarm.py:166`) has no - configured padding of its own; only individual children set `pady`. No - outer/inner pair exists to compare. -- Rule 22 (button padding) — no `tk.Button` widget exists anywhere in the - file (grep confirmed) — dismissal is via `tk.Entry` + `` bind - (`_alarm.py:209-217`), not a button. -- Rule 24 (nest corners) — Tkinter widgets are square by default with no - corner-radius concept; not implemented here. -- Rule 25 (adjacent hard divides) — no border/divider lines used anywhere. -- Rule 28 (icon contrast next to text) — no icons used anywhere in this UI; - text-only. - -### Passes (brief) - -- Rule 3 (high contrast for important elements) — `_alarm.py:172-174` - "WAKE UP!" title in `#ff4444` on `#1a1a1a` background is clearly the - highest-contrast element on screen. -- Rule 8 (align with something) — `container.place(relx=0.5, rely=0.5, - anchor="center")` (`_alarm.py:167`) plus uniform `.pack()` centering means - every widget shares the same center-alignment axis. -- Rule 17 (simple on complex) — flat solid `#1a1a1a` background behind flat - text (`_alarm.py:166`) is plain-on-plain throughout; no visual noise. -- Rule 20 (body text ≥16px) — smallest font used is 14pt - (`_alarm.py:231`, `timer_label`) ≈ 18-19px at typical DPI, above the 16px - floor; all other labels are 18pt+ (`_alarm.py:190,222`) or larger. -- Rule 21 (line length ~70 chars) — only short single-line status strings - are used (longest is ~55 characters, `_alarm.py:372`); no wrapped - paragraph text to overflow the readable range. -- Rule 23 (two typefaces max) — exactly two families used throughout, - `"Arial"` and `"Courier"` (`_alarm.py:172,181,190,203,211,222,231,329`) — - at the limit, not over it. -- Rule 26 (no shadows in dark interfaces) — trivially satisfied: no shadow - effects are implemented anywhere in this file. -- Rule 27 (don't mix depth techniques) — trivially satisfied: zero depth - techniques are used, so none can conflict. - -### Notes - -- The most impactful single fix for this surface is structural, not any one - color/font value: introduce one `_style.py` (or a `_COLORS`/`_FONTS` dict - in `_constants.py`) and reference it from every `tk.Label`/`tk.Frame` call - in `_alarm.py`. That alone would make rules 2, 9, 10, and 11 fixable in one - place instead of ~15 separate call sites. diff --git a/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index e987b86..0000000 --- a/MIGRATION.md +++ /dev/null @@ -1,89 +0,0 @@ -# Migration: GitHub Gist → shared `crdt_sync` - -`phone_app`'s alarm sync was moved off a bespoke GitHub Gist onto the shared -[`crdt_sync`](https://github.com/kuhyx/utils/tree/main/crdt_sync_dart) library -(pinned tag `crdt_sync_dart-v0.2.0` — see `phone_app/pubspec.yaml`, which is -the authority; `pubspec.lock` resolves it to `5be99ab`), matching how the other -companion apps sync. A `crdt_sync_dart-v0.3.0` tag also exists upstream and is -*not* in use here — bumping to it is an untaken decision, not a drift to fix. - -## What changed - -- **phone_app (Dart):** `gist_service.dart` is gone. `AlarmSyncService` stores - the alarm locally in a `crdt_sync` `LogStore` (one HLC-stamped `Record`) and - pushes it to `wake-alarm-sync/devices/phone/alarm.json` in `kuhyx/syncs` via - `crdt_sync`'s `GitHubClient`. The device-flow OAuth scope moved `gist` → - `repo` (the Contents API needs private-repo access). -- **shutdown-wrapper.sh (PC, Bash):** now reads that alarm from the Contents - API (`Accept: application/vnd.github.raw`, so no base64) and parses the - `crdt_sync` `Record` log with `jq` (`.alarm.fields.alarm_hour[0]`). Config - moved from `~/.config/wake_alarm/gist_sync.json` `{token, gist_id}` to - `~/.config/wake_alarm/sync.json` `{token}` (repo-scoped). - -## Setup after pulling this - -1. On the phone: Settings → **Connect GitHub** (now requests `repo` scope; the - old gist-scoped token won't reach the Contents API). -2. On the PC: `~/.config/wake_alarm/sync.json` → `{ "token": "" }`, - then re-run `install.sh` so `/usr/local/bin/shutdown` picks up the new - reader. - -## Surprises / non-obvious decisions - -- **The consumer is a Python-less Bash reader** (`shutdown-wrapper.sh` runs at - shutdown, when a Python runtime may not be available), so it can't use the - `crdt_sync` library — it parses the log with `jq`. What makes that tractable: - `Hlc.toStr()` is **lexicographically sortable == chronological**, so - "latest wins" across device dirs is a plain string max, no date math. -- **The phone is push-only** (sole writer); there is no pull/merge. Its local - `LogStore` is always the source of truth. -- Any parse miss still falls back to `WAKE_AFTER_HOURS`, so a bad read can - never brick the morning wake. - -# Migration: hibernate-scheduled → per-minute trigger timer - -The PC is no longer shut down at night, which broke the whole scheduling -model: the wake time used to be decided once, at hibernate, and baked into the -RTC. With no shutdown there is no resume, so nothing fired the alarm at all — -and `wake-alarm.service` was `WantedBy=graphical-session.target`, so on an -always-on machine it fired at *login* time instead (`_should_run_alarm()` has -no time gate). - -## What changed - -- **New `wake-alarm-trigger.timer`** ticks every minute and runs - `wake_alarm._trigger`, which fires `morning-routine.service` when the - current local time is inside - `[phone alarm, phone alarm + TRIGGER_CATCHUP_MINUTES)`. This is now the - primary path; the phone can move the alarm at any hour and the PC follows. -- **New `wake_alarm/_sync.py`** — a Python reader for the same alarm file - `shutdown-wrapper.sh` parses with `jq`, plus an on-disk cache - (`~/.config/wake_alarm/alarm_cache.json`). -- **`wake-alarm.service` lost its `[Install]` section** and is no longer - enabled; `install.sh` disables it to clear stale symlinks. -- **Token resolution gained a fallback** to the sibling apps' - `~/.config/{diet_guard,screen_locker}/sync_token`, in both readers. -- `shutdown-wrapper.sh` and `sleep-hook.sh` keep working unchanged, now as the - PC-is-powered-down fallback rather than the main mechanism. - -## Surprises / non-obvious decisions - -- **A timer, not a resident scheduler.** Each tick is a fresh process, so - there is no daemon to die silently and no wall-clock jump to reason about - across suspend/resume. `Persistent=true` plus the catch-up window is what - makes a suspended PC still fire on resume. -- **Per-minute ticks, but a 5-minute cache.** Ticking every 5 minutes would - also quantise *firing* to 5 minutes (07:03 → 07:05). Ticking every minute - and only hitting the network when the cache is stale gives exact-minute - firing at ~288 GitHub reads/day. -- **Degraded reads log at INFO.** At one tick a minute, a WARNING on a - persistent condition is 1440 journal lines a day. The single WARNING that - survives is `_trigger`'s "firing now on a degraded source". -- **The sync had been dead since the crdt_sync migration.** `wake-alarm-sync/` - did not exist in `kuhyx/syncs` at all — the phone had never successfully - pushed, and `~/.config/wake_alarm/sync.json` was never created, so the - hibernate path had been silently using the `+8h` fallback. The phone app - needs one successful "Set Alarm & Sync" to create that path. -- **Still open:** alarms edited directly in the Android Clock app do not reach - the sync. Closing that needs an `ACTION_NEXT_ALARM_CLOCK_CHANGED` receiver - in the phone app — planned, not yet built. diff --git a/refactor_claude_todo.md b/TODO-file-length-250.md similarity index 100% rename from refactor_claude_todo.md rename to TODO-file-length-250.md diff --git a/prompts/dopamine-ux-wake-alarm.md b/prompts/TODO-dopamine-ux-wake-alarm.md similarity index 100% rename from prompts/dopamine-ux-wake-alarm.md rename to prompts/TODO-dopamine-ux-wake-alarm.md diff --git a/scripts/check_file_length.py b/scripts/check_file_length.py deleted file mode 100755 index d30d747..0000000 --- a/scripts/check_file_length.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -"""Pre-commit hook: fail if any file exceeds MAX_LINES lines.""" - -from pathlib import Path -import sys - -MAX_LINES = 500 - - -def main() -> int: - """Return 1 if any file exceeds the line limit, else 0.""" - failed = False - for filepath in sys.argv[1:]: - try: - with Path(filepath).open(encoding="utf-8", errors="replace") as fh: - count = sum(1 for _ in fh) - except OSError: - failed = True - continue - if count > MAX_LINES: - failed = True - return 1 if failed else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/check_file_length.sh b/scripts/check_file_length.sh new file mode 100755 index 0000000..b58a32c --- /dev/null +++ b/scripts/check_file_length.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# ============================================================================ +# Fail if any file in the commit exceeds the shared 250-line cap. +# +# Thin delegate to the shared gate in ~/utils, which owns the cap and the +# exemption list (generated / vendored / data files). This repo previously ran +# a LOCAL check_file_length.py with its own higher limit, so it reported +# "gated" while permitting files every other repo rejects -- which is why its +# violations went untouched. Delegating is what stops that recurring. +# +# Usage: +# scripts/check_file_length.sh [ ...] # pre-commit passes these +# scripts/check_file_length.sh --all # whole tree, from cwd +# ============================================================================ + +set -euo pipefail + +readonly SHARED_GATE="${UTILS_ROOT:-$HOME/utils}/scripts/check_file_length.sh" + +main() { + if [[ ! -x "$SHARED_GATE" ]]; then + echo "Error: shared file-length gate not found at $SHARED_GATE" >&2 + echo " Clone github.com/kuhyx/utils to ~/utils, or set" >&2 + echo " UTILS_ROOT to where it lives." >&2 + exit 1 + fi + + exec bash "$SHARED_GATE" "$@" +} + +main "$@" diff --git a/scripts/check_md_naming.sh b/scripts/check_md_naming.sh new file mode 100755 index 0000000..50deaa0 --- /dev/null +++ b/scripts/check_md_naming.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# ============================================================================ +# Fail if any markdown file breaks the shared naming convention. +# +# Thin delegate to the shared gate in ~/utils, which owns the four namespaces +# (README / CLAUDE* / DOCS* / TODO*), the "REMOVE ME AFTER FINISH" marker rule +# and the exemption list. Copying that logic here is what lets one repo's idea +# of a valid name drift from every other repo's -- so this script only locates +# the shared checker and forwards its arguments. +# +# Usage: +# scripts/check_md_naming.sh [ ...] # pre-commit passes these +# scripts/check_md_naming.sh --all # whole tree, from cwd +# ============================================================================ + +set -euo pipefail + +readonly SHARED_GATE="${UTILS_ROOT:-$HOME/utils}/scripts/check_md_naming.sh" + +main() { + if [[ ! -x "$SHARED_GATE" ]]; then + echo "Error: shared markdown-naming gate not found at $SHARED_GATE" >&2 + echo " Clone github.com/kuhyx/utils to ~/utils, or set" >&2 + echo " UTILS_ROOT to where it lives." >&2 + exit 1 + fi + + exec bash "$SHARED_GATE" "$@" +} + +main "$@"