The safety layer: two presses for anything you cannot take back
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The safety idioms are only worth having if a refactor cannot quietly drop
|
||||
# them. This pins the Tier-1 layer:
|
||||
#
|
||||
# 1. SettingsButton carries its own keyboard contract (Tab stop, Return /
|
||||
# Enter / Space, Accessible role, focus ring) -- the reason no consumer
|
||||
# hand-rolls those any more.
|
||||
# 2. ConfirmAction exists, is registered, and every instantiation names an
|
||||
# actionId; the one-armed-at-a-time token it arbitrates through lives on
|
||||
# ShellState. An anonymous ConfirmAction shares "" with every other
|
||||
# anonymous one, which arms them all at once.
|
||||
# 3. ErrorRow instantiations bind a message, and NotMeasuredRow
|
||||
# instantiations say `because:` -- a bare "Not measured" invites the
|
||||
# reader to assume zero, and zero is a measurement.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
qs="$repo_dir/config/dot/quickshell"
|
||||
|
||||
fail() { printf 'settings idiom contract: %s\n' "$1" >&2; exit 1; }
|
||||
[[ -r "$qs/modules/settings/SettingsButton.qml" ]] || fail "missing SettingsButton.qml"
|
||||
|
||||
python3 - "$qs" <<'PY'
|
||||
import re, sys, pathlib
|
||||
qs = pathlib.Path(sys.argv[1])
|
||||
settings = qs / 'modules' / 'settings'
|
||||
problems = []
|
||||
|
||||
# 1. The button's own keyboard contract.
|
||||
button = (settings / 'SettingsButton.qml').read_text()
|
||||
for needle, why in [
|
||||
('activeFocusOnTab', 'no Tab stop'),
|
||||
('Keys.onReturnPressed', 'Return does nothing'),
|
||||
('Keys.onSpacePressed', 'Space does nothing'),
|
||||
('Accessible.role', 'invisible to screen readers'),
|
||||
('Theme.accentSecondary', 'no focus ring'),
|
||||
]:
|
||||
if needle not in button:
|
||||
problems.append(f'SettingsButton.qml: {needle} gone -- {why}')
|
||||
|
||||
# 2. ConfirmAction registered, arbitrated through ShellState, always named.
|
||||
qmldir = (settings / 'qmldir').read_text()
|
||||
for component in ('ConfirmAction', 'ErrorRow', 'NotMeasuredRow'):
|
||||
if not re.search(rf'^{component} ', qmldir, re.M):
|
||||
problems.append(f'qmldir: {component} unregistered -- pages referencing it fail to load')
|
||||
|
||||
confirm = (settings / 'ConfirmAction.qml').read_text()
|
||||
if 'ShellState.armedConfirm' not in confirm:
|
||||
problems.append('ConfirmAction.qml: not arbitrated through ShellState.armedConfirm -- two rows can be armed at once')
|
||||
shellstate = (qs / 'services' / 'ShellState.qml').read_text()
|
||||
if 'armedConfirm' not in shellstate:
|
||||
problems.append('ShellState.qml: armedConfirm token gone -- ConfirmAction has nothing to arbitrate through')
|
||||
|
||||
# 3. Instantiation-shape checks. A component use spans the braces that follow
|
||||
# it; requiring the property inside that span is a cheap parse that has caught
|
||||
# every real miss so far.
|
||||
def block_after(text, start):
|
||||
depth, i = 0, text.index('{', start)
|
||||
for j in range(i, len(text)):
|
||||
if text[j] == '{': depth += 1
|
||||
elif text[j] == '}':
|
||||
depth -= 1
|
||||
if depth == 0: return text[i:j]
|
||||
return text[i:]
|
||||
|
||||
REQUIRED = {'ConfirmAction': 'actionId', 'ErrorRow': 'message', 'NotMeasuredRow': 'because'}
|
||||
for page in sorted(qs.rglob('*.qml')):
|
||||
if page.name in ('ConfirmAction.qml', 'ErrorRow.qml', 'NotMeasuredRow.qml'):
|
||||
continue
|
||||
text = page.read_text()
|
||||
for component, prop in REQUIRED.items():
|
||||
for m in re.finditer(rf'\b{component}\s*\{{', text):
|
||||
if not re.search(rf'\b{prop}\s*:', block_after(text, m.start())):
|
||||
problems.append(f'{page.name}: {component} without {prop}:')
|
||||
|
||||
if problems:
|
||||
print(f"settings idiom contract: {len(problems)} problem(s)")
|
||||
for p in problems: print(" -", p)
|
||||
sys.exit(1)
|
||||
|
||||
uses = {c: 0 for c in REQUIRED}
|
||||
for page in qs.rglob('*.qml'):
|
||||
if page.name in ('ConfirmAction.qml', 'ErrorRow.qml', 'NotMeasuredRow.qml'):
|
||||
continue
|
||||
text = page.read_text()
|
||||
for c in uses:
|
||||
uses[c] += len(re.findall(rf'\b{c}\s*\{{', text))
|
||||
print("settings idiom contract: ok ("
|
||||
+ ", ".join(f"{n} {c}" for c, n in uses.items())
|
||||
+ ", every one carries its required property)")
|
||||
PY
|
||||
Reference in New Issue
Block a user