mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 16:48:54 +01:00
Remove aur-publish and dopamine-ux modules
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Audit found no external consumers. Drop their refs in md_naming migrate tooling and the pre-commit comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYFhDShgeHByh6yT9L
This commit is contained in:
parent
fe4e34287a
commit
5a87868763
@ -13,7 +13,7 @@
|
||||
# linters, and carry this same hook so committing with one of those configs is
|
||||
# gated too. This root config is what `pre-commit install` wires up, and it is
|
||||
# the only one that covers the subprojects without a config of their own
|
||||
# (aur-publish/, guard-lib/, crdt_sync_dart/, file_length/, ...).
|
||||
# (guard-lib/, crdt_sync_dart/, file_length/, ...).
|
||||
#
|
||||
# The path is repo-relative on purpose. pre-commit always runs hooks from the
|
||||
# git root, so this resolves the same locally and on a CI runner -- an absolute
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
# aur-publish
|
||||
|
||||
Hands-off publishing of kuhy's Flutter apps to the AUR.
|
||||
|
||||
```bash
|
||||
~/src/utils/aur-publish/publish.sh # wait for registration, then publish
|
||||
~/src/utils/aur-publish/publish.sh --dry-run # everything except the push
|
||||
~/src/utils/aur-publish/publish.sh --skip-wait # account already works
|
||||
~/src/utils/aur-publish/publish.sh --only habit-stack
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
1. Polls `/register` until AUR signup reopens, opens it in a browser and
|
||||
prints the SSH key to paste. **Signup is the only manual step** — it
|
||||
requires email confirmation.
|
||||
2. Waits for `ssh aur@aur.archlinux.org` to authenticate.
|
||||
3. Then, unattended, per package: resolve the newest `vX.Y.Z` tag → set
|
||||
`pkgver` → `updpkgsums` → `makepkg -C` → `namcap` → AUR rule check →
|
||||
`pacman -U` → launch check → `.SRCINFO` → commit → push.
|
||||
|
||||
Every package is built, installed and **launched** before it is pushed, so a
|
||||
broken package cannot reach the AUR. A package failing any gate is skipped and
|
||||
reported; the others still publish. Safe to re-run.
|
||||
|
||||
## anubis_gate.py
|
||||
|
||||
The AUR sits behind [Anubis](https://anubis.techaro.lol/), which answers HTML
|
||||
requests with a proof-of-work challenge page carrying **HTTP 200**. A plain
|
||||
`curl` therefore reports 200 for a page that is actually unreachable — which is
|
||||
exactly how "is registration back?" gets answered wrong. This module solves the
|
||||
challenge and reports the status of the *real* page behind it.
|
||||
|
||||
```bash
|
||||
python3 anubis_gate.py /register # prints the true status code
|
||||
```
|
||||
|
||||
## AUR rules enforced before pushing
|
||||
|
||||
The AUR does not review uploads, but per the submission guidelines "packages
|
||||
that violate the rules may be deleted without warning". `check_aur_rules()`
|
||||
blocks a push on the mechanically checkable ones: `# Maintainer:` line,
|
||||
`x86_64` in `arch`, no `replaces=` (reserved for renames — use `conflicts`),
|
||||
not already in an official repo, `pkgdesc`/`url`/`license` present, and
|
||||
`.SRCINFO` matching the PKGBUILD.
|
||||
@ -1,138 +0,0 @@
|
||||
"""Fetch an AUR URL from behind the Anubis anti-bot gate.
|
||||
|
||||
The AUR sits behind Anubis (https://anubis.techaro.lol/), which answers every
|
||||
HTML request with a proof-of-work challenge page carrying HTTP **200**. A bare
|
||||
`curl` therefore reports 200 for a page that is actually unreachable, which is
|
||||
exactly how "is /register back up?" gets answered wrong.
|
||||
|
||||
This module solves the challenge and reports the status of the *real* page
|
||||
behind it, so the caller can distinguish:
|
||||
|
||||
* 200 -> genuinely available
|
||||
* 503 -> reached the site; that endpoint is disabled server-side
|
||||
* other-> whatever the app actually said
|
||||
|
||||
Exit codes mirror the HTTP status so shell callers can branch on them:
|
||||
0 for 200, 1 for anything else, 2 for a local/transport failure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import http.cookiejar
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
BASE = "https://aur.archlinux.org"
|
||||
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
|
||||
CHALLENGE_RE = re.compile(
|
||||
r'<script id="anubis_challenge" type="application/json">(.*?)</script>',
|
||||
re.S,
|
||||
)
|
||||
PASS_PATH = "/.within.website/x/cmd/anubis/api/pass-challenge"
|
||||
# A solve at difficulty 4 takes ~0.01s; this ceiling only guards a server-side
|
||||
# difficulty spike, so the poller degrades instead of spinning forever.
|
||||
MAX_NONCE = 50_000_000
|
||||
|
||||
|
||||
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
"""Keep redirects visible instead of silently following them."""
|
||||
|
||||
def redirect_request(self, *_args, **_kwargs): # noqa: D102 - see class doc
|
||||
return None
|
||||
|
||||
|
||||
def _opener() -> urllib.request.OpenerDirector:
|
||||
jar = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(jar), _NoRedirect
|
||||
)
|
||||
opener.addheaders = [
|
||||
("User-Agent", UA),
|
||||
("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
||||
]
|
||||
return opener
|
||||
|
||||
|
||||
def _solve(random_data: str, difficulty: int) -> tuple[int, str, float]:
|
||||
"""Find a nonce whose sha256 digest starts with `difficulty` zero hex chars."""
|
||||
prefix = "0" * difficulty
|
||||
started = time.time()
|
||||
for nonce in range(MAX_NONCE):
|
||||
digest = hashlib.sha256(f"{random_data}{nonce}".encode()).hexdigest()
|
||||
if digest.startswith(prefix):
|
||||
return nonce, digest, time.time() - started
|
||||
raise RuntimeError(f"no nonce found below {MAX_NONCE} at difficulty {difficulty}")
|
||||
|
||||
|
||||
def fetch(path: str, timeout: int = 30) -> tuple[int, str]:
|
||||
"""Return (status, body) for `path`, clearing Anubis first if challenged."""
|
||||
opener = _opener()
|
||||
url = f"{BASE}{path}"
|
||||
|
||||
try:
|
||||
with opener.open(url, timeout=timeout) as resp:
|
||||
body = resp.read().decode("utf-8", "replace")
|
||||
status = resp.status
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, exc.read().decode("utf-8", "replace")
|
||||
|
||||
match = CHALLENGE_RE.search(body)
|
||||
if not match:
|
||||
return status, body
|
||||
|
||||
challenge = json.loads(match.group(1))["challenge"]
|
||||
nonce, digest, elapsed = _solve(
|
||||
challenge["randomData"], int(challenge["difficulty"])
|
||||
)
|
||||
query = urllib.parse.urlencode(
|
||||
{
|
||||
"id": challenge["id"],
|
||||
"response": digest,
|
||||
"nonce": str(nonce),
|
||||
"redir": path,
|
||||
"elapsedTime": str(int(elapsed * 1000)),
|
||||
}
|
||||
)
|
||||
try:
|
||||
opener.open(f"{BASE}{PASS_PATH}?{query}", timeout=timeout)
|
||||
except urllib.error.HTTPError:
|
||||
# A redirect or non-2xx here is fine; what matters is the cookie the
|
||||
# opener's jar just captured.
|
||||
pass
|
||||
|
||||
try:
|
||||
with opener.open(url, timeout=timeout) as resp:
|
||||
return resp.status, resp.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, exc.read().decode("utf-8", "replace")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("path", help="AUR path, e.g. /register")
|
||||
parser.add_argument(
|
||||
"--print-body", action="store_true", help="write the page body to stdout"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
status, body = fetch(args.path)
|
||||
except (urllib.error.URLError, RuntimeError, TimeoutError) as exc:
|
||||
print(f"transport error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(status)
|
||||
if args.print_body:
|
||||
sys.stdout.write(body)
|
||||
return 0 if status == 200 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,107 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Building, verifying and pushing one package to the AUR.
|
||||
#
|
||||
# Sourced by publish.sh, never executed: these functions read the readonly
|
||||
# config (AUR_HOST, AUR_ROOT, DRY_RUN, PUBLISHED, FAILED) that the entry script declares, and
|
||||
# a `readonly` re-run inside a twice-sourced lib would abort under `set -e`.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
publish_one() {
|
||||
local pkg="$1" repo="$2" wrapper="$3" binary="$4" expected="$5"
|
||||
local dir="$AUR_ROOT/$pkg"
|
||||
local version
|
||||
|
||||
log "$pkg"
|
||||
|
||||
[[ -d "$repo" ]] || { warn "$pkg: no repo at $repo"; return 1; }
|
||||
[[ -f "$dir/PKGBUILD" ]] || { warn "$pkg: no PKGBUILD at $dir"; return 1; }
|
||||
|
||||
version="$(newest_tag "$repo")"
|
||||
[[ -n "$version" ]] || { warn "$pkg: no vX.Y.Z tag on the remote"; return 1; }
|
||||
info "newest release tag: v$version"
|
||||
|
||||
cd "$dir"
|
||||
sed -i -E "s/^pkgver=.*/pkgver=$version/" PKGBUILD
|
||||
# The tarball for a tag contains the PKGBUILD, so the checksum can only be
|
||||
# computed after the tag exists — never committed ahead of time.
|
||||
updpkgsums >/dev/null 2>&1 || { warn "$pkg: updpkgsums failed (tag missing?)"; return 1; }
|
||||
|
||||
info "building"
|
||||
if ! env PATH=/usr/bin:/bin:/usr/local/bin makepkg -Cf --noconfirm >/dev/null 2>&1; then
|
||||
warn "$pkg: makepkg failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local built
|
||||
built="$(find . -maxdepth 1 -name "$pkg-$version-*.pkg.tar.zst" | head -1)"
|
||||
[[ -n "$built" ]] || { warn "$pkg: no package produced"; return 1; }
|
||||
|
||||
# namcap's "ELF files outside of a valid path ('opt/')" is a false positive
|
||||
# for /opt-installed apps, so only genuinely new errors are worth failing on.
|
||||
local findings
|
||||
findings="$(namcap "$built" 2>&1 | grep -E ' E: ' | grep -v "outside of a valid path" || true)"
|
||||
if [[ -n "$findings" ]]; then
|
||||
warn "$pkg: namcap errors:"
|
||||
printf ' %s\n' "$findings"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "installing and launching"
|
||||
sudo pacman -U --noconfirm "$built" >/dev/null 2>&1 \
|
||||
|| { warn "$pkg: pacman -U failed"; return 1; }
|
||||
verify_installed "$binary" "$expected" "$wrapper" || { warn "$pkg: launch check failed"; return 1; }
|
||||
|
||||
# Generated before the rule check, which verifies it matches the PKGBUILD.
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
check_aur_rules "$pkg" || return 1
|
||||
|
||||
# The AUR repo is a separate remote living alongside the build dir; adding
|
||||
# it here (rather than cloning into a fresh path) keeps the verified
|
||||
# PKGBUILD and the thing being pushed byte-identical.
|
||||
if [[ ! -d .git ]]; then
|
||||
git init -q -b master .
|
||||
git remote add origin "ssh://aur@$AUR_HOST/$pkg.git"
|
||||
fi
|
||||
git remote get-url origin >/dev/null 2>&1 \
|
||||
|| git remote add origin "ssh://aur@$AUR_HOST/$pkg.git"
|
||||
|
||||
# Fetch first: an existing AUR package must be built on, not overwritten.
|
||||
if git fetch -q origin master 2>/dev/null; then
|
||||
git reset -q --soft FETCH_HEAD 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat > .gitignore <<'EOF'
|
||||
# Build products; the AUR tracks only the recipe.
|
||||
*.pkg.tar.zst
|
||||
*.tar.gz
|
||||
src/
|
||||
pkg/
|
||||
EOF
|
||||
|
||||
git add PKGBUILD .SRCINFO .gitignore
|
||||
if git diff --cached --quiet; then
|
||||
info "already up to date on the AUR; nothing to push"
|
||||
PUBLISHED+=("$pkg v$version (unchanged)")
|
||||
return 0
|
||||
fi
|
||||
|
||||
git -c user.name='Krzysztof Rudnicki' \
|
||||
-c user.email='krzysztofrudnicki0@gmail.com' \
|
||||
commit -q -m "$pkg $version-1"
|
||||
|
||||
if ((DRY_RUN)); then
|
||||
info "DRY RUN — not pushing"
|
||||
PUBLISHED+=("$pkg v$version (dry run)")
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "pushing to the AUR"
|
||||
git push -q origin master 2>&1 | tail -3 || { warn "$pkg: push failed"; return 1; }
|
||||
PUBLISHED+=("$pkg v$version")
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Waiting for AUR registration and SSH access to come up.
|
||||
#
|
||||
# Sourced by publish.sh, never executed: these functions read the readonly
|
||||
# config (AUR_HOST, SSH_KEY, GATE, POLL_SECONDS) that the entry script declares, and
|
||||
# a `readonly` re-run inside a twice-sourced lib would abort under `set -e`.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
register_status() {
|
||||
python3 "$GATE" /register 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
open_browser() {
|
||||
local url="$1"
|
||||
local browser
|
||||
for browser in xdg-open thorium-browser chromium google-chrome-stable firefox librewolf; do
|
||||
if command -v "$browser" >/dev/null 2>&1; then
|
||||
"$browser" "$url" >/dev/null 2>&1 &
|
||||
info "Opened $url in $browser"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
warn "No browser found; open this yourself: $url"
|
||||
}
|
||||
|
||||
ensure_key() {
|
||||
if [[ ! -f "$SSH_KEY" ]]; then
|
||||
log "Generating an AUR SSH key (none at $SSH_KEY)"
|
||||
ssh-keygen -t ed25519 -f "$SSH_KEY" -N '' -C 'kuhy@aur'
|
||||
fi
|
||||
if ! grep -q "Host $AUR_HOST" "$HOME/.ssh/config" 2>/dev/null; then
|
||||
log "Adding $AUR_HOST to ~/.ssh/config"
|
||||
mkdir -p "$HOME/.ssh"
|
||||
cat >> "$HOME/.ssh/config" <<EOF
|
||||
|
||||
# AUR uses a dedicated key so it stays separate from the GitHub identity.
|
||||
Host $AUR_HOST
|
||||
User aur
|
||||
IdentityFile $SSH_KEY
|
||||
IdentitiesOnly yes
|
||||
EOF
|
||||
chmod 600 "$HOME/.ssh/config"
|
||||
fi
|
||||
}
|
||||
|
||||
ssh_works() {
|
||||
# The AUR refuses interactive shells; "Interactive shell is disabled" on a
|
||||
# successful key auth is the success signal, not an error.
|
||||
ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=15 "aur@$AUR_HOST" help 2>&1 |
|
||||
grep -qiE 'interactive shell is disabled|Welcome to AUR'
|
||||
}
|
||||
|
||||
wait_for_registration() {
|
||||
local status
|
||||
status="$(register_status)"
|
||||
if [[ "$status" == "200" ]]; then
|
||||
log "AUR registration is OPEN"
|
||||
else
|
||||
log "AUR registration is closed (HTTP $status). Polling every ${POLL_SECONDS}s."
|
||||
info "Ctrl-C to stop; re-run any time, nothing is lost."
|
||||
while :; do
|
||||
sleep "$POLL_SECONDS"
|
||||
status="$(register_status)"
|
||||
if [[ "$status" == "200" ]]; then
|
||||
log "Registration is BACK (HTTP 200)"
|
||||
break
|
||||
fi
|
||||
printf ' %s still HTTP %s\n' "$(date +%H:%M)" "$status"
|
||||
done
|
||||
fi
|
||||
|
||||
open_browser "https://$AUR_HOST/register"
|
||||
cat <<EOF
|
||||
|
||||
------------------------------------------------------------------
|
||||
Sign up, and paste this into the "SSH Public Key" field:
|
||||
|
||||
$(sed 's/^/ /' "$SSH_KEY.pub")
|
||||
|
||||
You will need to confirm the address from your email before the
|
||||
account works. This script waits for that automatically.
|
||||
------------------------------------------------------------------
|
||||
EOF
|
||||
}
|
||||
|
||||
wait_for_ssh() {
|
||||
log "Waiting for the SSH key to authenticate against the AUR"
|
||||
local waited=0
|
||||
until ssh_works; do
|
||||
sleep 30
|
||||
waited=$((waited + 30))
|
||||
if ((waited % 300 == 0)); then
|
||||
info "still waiting ($((waited / 60)) min) — key not accepted yet"
|
||||
fi
|
||||
done
|
||||
log "SSH authentication OK"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: build, verify and publish one package
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Gates a package must pass before it is allowed near the AUR.
|
||||
#
|
||||
# Sourced by publish.sh, never executed: these functions read the readonly
|
||||
# config (AUR_ROOT, DRY_RUN) that the entry script declares, and
|
||||
# a `readonly` re-run inside a twice-sourced lib would abort under `set -e`.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
newest_tag() {
|
||||
# Newest clean vX.Y.Z on the remote. Build tags (vX.Y.Z-build.N) and the
|
||||
# grafted upstream Flutter SDK tags in diet-guard are excluded on purpose.
|
||||
git -C "$1" ls-remote --tags origin 2>/dev/null |
|
||||
awk '{print $2}' |
|
||||
sed 's|refs/tags/||' |
|
||||
grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' |
|
||||
sed 's/^v//' |
|
||||
sort -V |
|
||||
tail -1
|
||||
}
|
||||
|
||||
verify_installed() {
|
||||
local binary="$1" expected="$2" wrapper="$3" output
|
||||
|
||||
# An instance left over from a previous run would make the wrapper print
|
||||
# "already running" and hand off, which passes trivially and turns the
|
||||
# strongest gate in this script into the weakest. Clear it first so the
|
||||
# check always exercises a real cold start.
|
||||
#
|
||||
# Resolved through /proc/<pid>/exe rather than any form of name or
|
||||
# command-line matching. Every argv-based approach selects the wrong
|
||||
# processes here:
|
||||
# * `pkill -x` compares against the kernel's 15-character comm field, and
|
||||
# two of these wrappers are longer (habit_stack_desktop is 19), so a
|
||||
# name match would silently never fire.
|
||||
# * `pkill -f`, and equally `ps -eo args` piped through a pattern, match
|
||||
# any process whose command line merely *mentions* the path — this
|
||||
# script, the shell running it, and the matching tool itself all
|
||||
# qualify, so the run kills its own shell instead of the app. A
|
||||
# $$/$PPID guard does not cover it, because the offending shell is
|
||||
# often a more distant ancestor.
|
||||
# /proc/<pid>/exe is a kernel-maintained symlink to the binary actually
|
||||
# being executed, so it cannot collide with an incidental mention.
|
||||
# The wrapper name is unique per app, so the /opt/<pkg>/ segment is left as
|
||||
# a glob instead of threading the package name through another parameter.
|
||||
#
|
||||
# The link is read without `-f`: it is already fully resolved, and by the
|
||||
# time this runs `pacman -U` has replaced the binary, so a leftover process
|
||||
# points at an unlinked inode that the kernel reports with a " (deleted)"
|
||||
# suffix. That suffix has to be stripped or the match silently misses the
|
||||
# one process this exists to kill.
|
||||
local pid exe killed=()
|
||||
for pid in /proc/[0-9]*; do
|
||||
pid="${pid#/proc/}"
|
||||
exe="$(readlink "/proc/$pid/exe" 2>/dev/null)" || continue
|
||||
exe="${exe% (deleted)}"
|
||||
[[ "$exe" == /opt/*/bin/"$wrapper" ]] || continue
|
||||
kill -TERM "$pid" 2>/dev/null && killed+=("$pid")
|
||||
done
|
||||
if ((${#killed[@]})); then
|
||||
info "stopped a running instance so the launch check is meaningful"
|
||||
# `kill` only reports that the signal was sent. Waiting for the process
|
||||
# to actually go keeps a slow exit from failing the cold start below on
|
||||
# a port that is still bound.
|
||||
local waited=0
|
||||
while ((waited < 100)) && kill -0 "${killed[@]}" 2>/dev/null; do
|
||||
sleep 0.1
|
||||
((waited++))
|
||||
done
|
||||
fi
|
||||
|
||||
output="$(timeout 15 "/usr/bin/$binary" 2>&1 | head -3 || true)"
|
||||
if grep -qF "$expected" <<<"$output"; then
|
||||
return 0
|
||||
fi
|
||||
warn "unexpected startup output: $output"
|
||||
return 1
|
||||
}
|
||||
|
||||
# The AUR does not review uploads, but "packages that violate the rules may be
|
||||
# deleted without warning" (AUR submission guidelines). These are the rules that
|
||||
# are mechanically checkable, enforced here so a violation blocks the push
|
||||
# rather than surfacing later as a deletion request.
|
||||
check_aur_rules() {
|
||||
local pkg="$1" problems=()
|
||||
|
||||
grep -q '^# Maintainer:' PKGBUILD \
|
||||
|| problems+=("missing '# Maintainer:' comment on line 1")
|
||||
|
||||
grep -qE "^arch=\(.*x86_64" PKGBUILD \
|
||||
|| problems+=("x86_64 not in arch=() — packages without it are not allowed")
|
||||
|
||||
# `replaces` makes pacman rip out the named package on the next -Sy, which
|
||||
# is only correct for a genuine rename. `conflicts` is the right tool.
|
||||
grep -q '^replaces=' PKGBUILD \
|
||||
&& problems+=("uses replaces=; AUR rules reserve it for renames — use conflicts=")
|
||||
|
||||
# A package already in an official repo must never be duplicated.
|
||||
if pacman -Si "$pkg" >/dev/null 2>&1; then
|
||||
problems+=("$pkg exists in an official repo — must not be submitted")
|
||||
fi
|
||||
|
||||
grep -qE '^(pkgdesc|url|license)=' PKGBUILD \
|
||||
|| problems+=("missing pkgdesc/url/license")
|
||||
|
||||
# A stale .SRCINFO is the single most common upload rejection.
|
||||
if ! diff -q <(makepkg --printsrcinfo) .SRCINFO >/dev/null 2>&1; then
|
||||
problems+=(".SRCINFO does not match PKGBUILD")
|
||||
fi
|
||||
|
||||
if ((${#problems[@]})); then
|
||||
warn "$pkg: AUR rule violations:"
|
||||
printf ' - %s\n' "${problems[@]}"
|
||||
return 1
|
||||
fi
|
||||
info "AUR rule checks passed"
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -1,148 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Publish kuhy's three Flutter apps to the AUR, hands-off.
|
||||
#
|
||||
# Run this once AUR registration is back. It:
|
||||
# 1. Waits for https://aur.archlinux.org/register to stop returning 503,
|
||||
# then opens it in a browser and prints the SSH key to paste.
|
||||
# (Signup is the ONLY manual step: it needs an email confirmation, and
|
||||
# creating accounts is not something this script will do for you.)
|
||||
# 2. Waits for `ssh aur@aur.archlinux.org` to authenticate.
|
||||
# 3. From there, fully unattended, for each package:
|
||||
# newest release tag -> pkgver -> updpkgsums -> makepkg -C -> namcap
|
||||
# -> pacman -U -> launch check -> .SRCINFO -> clone AUR repo
|
||||
# -> commit -> push.
|
||||
#
|
||||
# Every package is BUILT, INSTALLED and LAUNCHED before it is pushed, so a
|
||||
# broken package cannot reach the AUR. A package that fails any gate is
|
||||
# skipped and reported; the others still publish.
|
||||
#
|
||||
# Idempotent and resumable: re-running skips work that is already done, and
|
||||
# an already-published package is updated rather than duplicated.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SCRIPT_DIR
|
||||
readonly GATE="$SCRIPT_DIR/anubis_gate.py"
|
||||
readonly AUR_HOST="aur.archlinux.org"
|
||||
readonly SSH_KEY="$HOME/.ssh/aur"
|
||||
readonly AUR_ROOT="$HOME/sdk/aur"
|
||||
|
||||
# package:repo:wrapper-process:cli-name:expected-startup-string
|
||||
# The wrapper process name is NOT derivable from the package name
|
||||
# (diet-guard-app runs diet_guard_desktop), so it is listed explicitly.
|
||||
readonly PACKAGES=(
|
||||
"todo-flutter:$HOME/src/todo:todo_desktop:todo:serving on http://localhost:8730"
|
||||
"habit-stack:$HOME/src/habit_stack:habit_stack_desktop:habit-stack:serving on http://localhost:8731"
|
||||
"diet-guard-app:$HOME/src/diet-guard:diet_guard_desktop:diet-guard-app:serving on http://localhost:8732"
|
||||
)
|
||||
|
||||
POLL_SECONDS=300
|
||||
SKIP_WAIT=0
|
||||
DRY_RUN=0
|
||||
ONLY=""
|
||||
FAILED=()
|
||||
PUBLISHED=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Options:
|
||||
--skip-wait Assume the account + SSH key already work; go straight
|
||||
to building and publishing.
|
||||
--dry-run Do everything except the final 'git push' to the AUR.
|
||||
--only NAME Publish just one package (todo-flutter, habit-stack,
|
||||
diet-guard-app).
|
||||
--poll SECONDS How often to re-check /register (default: $POLL_SECONDS).
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m ! %s\033[0m\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31m==> ERROR: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
require_tools() {
|
||||
local missing=()
|
||||
local tool
|
||||
for tool in git makepkg updpkgsums namcap ssh python3 curl; do
|
||||
command -v "$tool" >/dev/null 2>&1 || missing+=("$tool")
|
||||
done
|
||||
if ((${#missing[@]})); then
|
||||
info "Installing missing tools: ${missing[*]}"
|
||||
# namcap/updpkgsums come from namcap + pacman-contrib; the rest are base.
|
||||
sudo pacman -S --needed --noconfirm namcap pacman-contrib git openssh python \
|
||||
|| die "could not install: ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1: wait for registration, then for the key to work
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The rest lives in lib/, sourced (not executed) so every function sees the
|
||||
# readonly config above and the mutable FAILED/PUBLISHED arrays main() reads.
|
||||
# shellcheck source=lib/aur_registration.sh
|
||||
source "$SCRIPT_DIR/lib/aur_registration.sh"
|
||||
# shellcheck source=lib/aur_verify.sh
|
||||
source "$SCRIPT_DIR/lib/aur_verify.sh"
|
||||
# shellcheck source=lib/aur_publish_one.sh
|
||||
source "$SCRIPT_DIR/lib/aur_publish_one.sh"
|
||||
|
||||
main() {
|
||||
require_tools
|
||||
[[ -f "$GATE" ]] || die "missing $GATE"
|
||||
mkdir -p "$AUR_ROOT"
|
||||
|
||||
ensure_key
|
||||
if ((SKIP_WAIT)); then
|
||||
ssh_works || die "SSH key is not accepted yet; drop --skip-wait to wait for it"
|
||||
else
|
||||
wait_for_registration
|
||||
wait_for_ssh
|
||||
fi
|
||||
|
||||
local entry pkg repo wrapper binary expected
|
||||
for entry in "${PACKAGES[@]}"; do
|
||||
IFS=: read -r pkg repo wrapper binary expected <<<"$entry"
|
||||
[[ -z "$ONLY" || "$ONLY" == "$pkg" ]] || continue
|
||||
if ! publish_one "$pkg" "$repo" "$wrapper" "$binary" "$expected"; then
|
||||
FAILED+=("$pkg")
|
||||
fi
|
||||
done
|
||||
|
||||
log "Summary"
|
||||
if ((${#PUBLISHED[@]})); then
|
||||
printf ' published: %s\n' "${PUBLISHED[@]}"
|
||||
fi
|
||||
if ((${#FAILED[@]})); then
|
||||
printf '\033[1;31m FAILED: %s\033[0m\n' "${FAILED[@]}"
|
||||
info "Nothing broken was pushed; fix and re-run (safe to repeat)."
|
||||
return 1
|
||||
fi
|
||||
if ((${#PUBLISHED[@]} == 0)); then
|
||||
warn "nothing to do"
|
||||
return 1
|
||||
fi
|
||||
printf '\n Check: https://aur.archlinux.org/packages/todo-flutter\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--skip-wait) SKIP_WAIT=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--only) ONLY="$2"; shift 2 ;;
|
||||
--poll) POLL_SECONDS="$2"; shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
main "$@"
|
||||
@ -1,165 +0,0 @@
|
||||
# Dopamine UX — session prompts
|
||||
|
||||
Ten pasteable prompts derived from *"The Dopamine Architecture: How UX Designers
|
||||
Hold User Attention in 2026"* (london-post.co.uk), applied to kuhy's repos.
|
||||
|
||||
Paste **one file's contents** into a fresh Claude session. Each is
|
||||
self-contained: it restates the findings it depends on, so no session needs the
|
||||
context of another.
|
||||
|
||||
## The two tests — every change must pass both
|
||||
|
||||
The article's four mechanics (variable rewards, micro-animations, sound design,
|
||||
near-miss) are in scope, aimed at **kuhy's own stated goals**. They are gated by
|
||||
two questions, not by an allow/deny list:
|
||||
|
||||
1. **Is the displayed state true?** A progress ring at 93% is legitimate *iff*
|
||||
you are actually at 93%. A streak is legitimate iff it counts real days.
|
||||
Dopamine may come from framing, timing and salience — **never from a
|
||||
fabricated number**.
|
||||
2. **Is the variance real?** A random pick from a real pool (next workout, next
|
||||
task) is legitimate. Injected random bonus points to manufacture uncertainty
|
||||
are not.
|
||||
|
||||
### The load-bearing corollary
|
||||
|
||||
**Reward the recording, never the number recorded.** In diet-guard the
|
||||
satisfying moment fires on *logging a meal*, not on hitting a calorie target —
|
||||
otherwise the app rewards eating less rather than recording honestly, and
|
||||
corrupts its own data. Same shape in workout_app (reward logging a workout, not
|
||||
beating a PB) and todo (reward capture, not completion count).
|
||||
|
||||
## Running order
|
||||
|
||||
`01` gates everything. `07` gates `08`. Everything else is independent.
|
||||
|
||||
| # | File | Repo | Findings |
|
||||
|---|---|---|---|
|
||||
| 01 | `01-motion-tokens.md` | `~/src/utils` | 3 |
|
||||
| 02 | `02-structural-check.md` | `~/src/utils` | 2 |
|
||||
| 03 | todo note `85ca329b-9809-412c-a47f-eddf055322cf` | `~/src/diet-guard` | 4 |
|
||||
| 04 | todo note `73a20837-6480-4b31-b966-1898a4e7272e` | `~/src/screen-locker` | 3 |
|
||||
| 05 | todo note `e50d6a03-ae7d-4da5-bbd3-c33b17f6f074` | `~/src/screen-locker/stronglift_replacement/workout_app` | 4 |
|
||||
| 06 | `06-todo.md` | `~/src/todo` | 3 |
|
||||
| 07 | `07-dufs-theme.md` | `~/src/dufs-cloud` | 2 |
|
||||
| 08 | todo note `e301e090-b2ee-4e26-8387-00f356b63521` | `~/src/dufs-cloud` | 4 |
|
||||
| 09 | todo note `9e1e4922-9f29-4bad-bcf4-96826aa8ffa4` | `~/src/wake-alarm` | 2 |
|
||||
|
||||
**27 ranked findings total.**
|
||||
|
||||
### Dependency graph
|
||||
|
||||
```
|
||||
01-motion-tokens ──┬─→ 03, 04, 05, 09 (transcribe tokens locally)
|
||||
├─→ 06 (bump design_system-v0.2.0)
|
||||
└─→ 07 ─→ 08 (bump web_ui-v0.3.2)
|
||||
02-structural-check (independent; run after 01 so it has motion tokens to check)
|
||||
```
|
||||
|
||||
## The top three findings fleet-wide
|
||||
|
||||
Ranked by value-over-effort from the survey data:
|
||||
|
||||
1. **screen-locker streak render** (prompt 04) — `snapshot.streak` is already
|
||||
populated and no renderer reads it. Zero new state, zero conftest change,
|
||||
zero new dependency.
|
||||
2. **diet-guard `_onLogMeal` feedback** (prompt 03) — the flagship case.
|
||||
Dependency-free via `HapticFeedback` from `flutter/services.dart`.
|
||||
3. **workout_app finish-workout cue** (prompt 05) — `audioplayers`, `vibration`,
|
||||
the `assets:` block and the VIBRATE permission are **already present**. Only
|
||||
the call is missing.
|
||||
|
||||
## Distributed copies, and the completion protocol
|
||||
|
||||
Each prompt is **also copied into the repo it acts on**, at
|
||||
`<repo>/prompts/dopamine-ux-*.md`, so you can open a repo and say
|
||||
"do dopamine-ux-<name>" without going hunting. **This directory stays the source
|
||||
of truth** — if a prompt needs correcting, fix it here and re-distribute.
|
||||
|
||||
| Prompt | Distributed to |
|
||||
|---|---|
|
||||
| 01 | `~/src/utils/prompts/dopamine-ux-01-motion-tokens.md` — deleted (done) |
|
||||
| 02 | `~/src/utils/prompts/dopamine-ux-02-structural-check.md` |
|
||||
| 03 | `~/src/diet-guard/prompts/dopamine-ux-diet-guard.md` |
|
||||
| 04 | `~/src/screen-locker/prompts/dopamine-ux-04-screen-locker.md` — deleted (done) |
|
||||
| 05 | `~/src/screen-locker/prompts/dopamine-ux-05-workout-app.md` — deleted (done) |
|
||||
| 06 | `~/src/todo/prompts/dopamine-ux-todo.md` |
|
||||
| 07 | `~/src/dufs-cloud/prompts/dopamine-ux-07-theme.md` |
|
||||
| 08 | `~/src/dufs-cloud/prompts/dopamine-ux-08-motion.md` |
|
||||
| 09 | `~/src/wake-alarm/prompts/dopamine-ux-wake-alarm.md` |
|
||||
|
||||
When a session finishes a prompt it **deletes its repo copy** (in the same
|
||||
commit as the implementation) and **appends a row below**. A finished prompt left
|
||||
in a repo is indistinguishable from a pending one, and the next session re-runs
|
||||
it. Partial completion is never recorded as DONE — the file is edited to state
|
||||
what remains instead.
|
||||
|
||||
### Completion log
|
||||
|
||||
| Prompt | Status | Impl commit | Note |
|
||||
|---|---|---|---|
|
||||
| 01-motion-tokens.md | DONE 2026-08-22 | `f741896` | `motion.md` frozen; `AppDuration`/`AppCurve` + `--duration-*`/`--ease-*` shipped. Tags: `design_system-v0.2.0`, `web_ui-v0.3.2`. |
|
||||
| 02-structural-check.md | not started | — | — |
|
||||
| todo note `85ca329b-9809-412c-a47f-eddf055322cf` | not started | — | — |
|
||||
| todo note `73a20837-6480-4b31-b966-1898a4e7272e` | DONE 2026-11-01 | `89c65bd` | Status window `_section_streak` + week-transition reward text (kept in `extra_benefits_state.json`); the dead 09:00 early-bird claim is not shown. Per kuhy: no daily-streak state, no shutdown time, no freezes. Commit is on a worktree branch, unpushed. |
|
||||
| todo note `e50d6a03-ae7d-4da5-bbd3-c33b17f6f074` | DONE 2026-11-01 | `e938201` | Set/warmup `selectionClick`, finish `mediumImpact` + generated chime (music_theory, pinned tag) + dialog arrival, `AppDuration`/`AppCurve` in theme.dart, streak counts up, reduced motion honoured, one Settings "Sound" switch for chime and break-end (foreground service too). Worktree commits, unpushed; `62c1c2c` covers old export/import gaps. |
|
||||
| 06-todo.md | PARTIAL 2026-11-02 (step 1 DONE; step 2 sound REJECTED by kuhy) | `kuhyx/todo-app 2ffae26` | haptic + checkmark on capture, appRoute motion tokens, animated wizard bar; no sound by decision |
|
||||
| 07-dufs-theme.md | not started | — | — |
|
||||
| todo note `e301e090-b2ee-4e26-8387-00f356b63521` | not started | — | — |
|
||||
| todo note `9e1e4922-9f29-4bad-bcf4-96826aa8ffa4` | not started | — | — |
|
||||
|
||||
## Keeping these prompts honest
|
||||
|
||||
Every path and symbol cited across these files is machine-checked:
|
||||
|
||||
```
|
||||
python3 ~/src/utils/dopamine-ux/verify_prompts.py
|
||||
```
|
||||
|
||||
Exit 0 means every cited path resolves and every cited symbol is still greppable
|
||||
in the file it is attributed to. **Re-run it after any refactor of the surveyed
|
||||
repos** — a prompt pointing at a renamed function is worse than no prompt, and a
|
||||
path check alone cannot catch that (which is why it greps symbols too).
|
||||
|
||||
Verified 2026-08-16: 146 paths, 30 symbols, all passing.
|
||||
|
||||
## Rules that apply to every prompt
|
||||
|
||||
1. **Tag cuts and version bumps are explicit steps.** `todo` consumes
|
||||
`design_system` at git ref `design_system-v0.1.0`; `dufs-cloud/web` consumes
|
||||
`@kuhyx/web-ui` at `web_ui-v0.3.1`. Adding tokens to those packages does
|
||||
nothing for consumers until a new tag is cut. Prompt 01 ends by cutting
|
||||
`design_system-v0.2.0` and `web_ui-v0.3.2`; prompts 06 and 08 begin by
|
||||
bumping to those literal refs.
|
||||
2. **Anchor on symbols, not line numbers.** Line numbers in these prompts are
|
||||
"as of 2026-08-16" and will drift. The symbol name is the real anchor —
|
||||
grep for it rather than trusting the line.
|
||||
3. **Reduce-motion is mandatory and is NOT the sound toggle.** Web:
|
||||
`@media (prefers-reduced-motion: reduce)`. Flutter:
|
||||
`MediaQuery.of(context).disableAnimations`. The OS motion preference is
|
||||
honoured automatically; the sound opt-out is a separate user-facing toggle.
|
||||
4. **Motion and haptics first, sound second.** Haptics need no dependency;
|
||||
sound needs a pubspec dep, an `assets:` block, and a settings toggle. Each
|
||||
prompt stages them separately so a short session still lands the valuable
|
||||
half, and sound stays independently revertable.
|
||||
5. **Sound ships on (opt-out), never in screen-locker enforcement paths.**
|
||||
6. **250-line cap** applies to code and prose alike (`~/src/utils/file_length`).
|
||||
|
||||
## Source
|
||||
|
||||
Article findings, verbatim from the piece:
|
||||
|
||||
- **Variable reward schedules** — unpredictable outcomes produce stronger
|
||||
dopaminergic activity than guaranteed ones. Duolingo streaks, feed ordering,
|
||||
pull-to-refresh.
|
||||
- **Micro-animations** — "a button that bounces on press, a like counter that
|
||||
ticks upward with a slight delay". Replaces latency with feedback, so waiting
|
||||
reads as responsiveness rather than frustration.
|
||||
- **Sound design** — "operates below the threshold of conscious attention";
|
||||
the article's judgement is that it is more powerful than visual design and
|
||||
gets less critical attention.
|
||||
- **Near-miss effects** — a fitness ring at ninety-three per cent creates
|
||||
discomfort that drives re-engagement, and works *even when the user is
|
||||
explicitly told the mechanism*.
|
||||
- Core claim: **dopamine is released in anticipation of a reward, not on
|
||||
receipt. The uncertainty is the trigger.**
|
||||
@ -1,209 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify every path and symbol cited in the dopamine-ux prompt files.
|
||||
|
||||
A copy-pasteable prompt that points at a file which does not exist is worse than
|
||||
no prompt, so this gate adjudicates with an exit code rather than by eye.
|
||||
|
||||
Two checks:
|
||||
1. Every cited on-disk path resolves.
|
||||
2. Every cited symbol is greppable in the file it is attributed to. A path
|
||||
check alone cannot catch a stale line number; a symbol grep can.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROMPT_DIR = Path.home() / "src/utils" / "dopamine-ux"
|
||||
HOME = Path.home()
|
||||
|
||||
PATH_RE = re.compile(r"`(~?/[^`\s]+|[\w.\-]+/[\w./\-]+\.\w+)`")
|
||||
|
||||
# Not on-disk paths despite matching the shape above.
|
||||
SKIP_SUBSTRINGS = (
|
||||
"://",
|
||||
"$",
|
||||
"*",
|
||||
"<",
|
||||
">",
|
||||
"…",
|
||||
"devices/", # Firebase/crdt key path
|
||||
"settings/advanced", # Firebase key path
|
||||
"android.permission",
|
||||
"android.intent",
|
||||
"/sdcard/",
|
||||
"/etc/",
|
||||
"/dev/input/",
|
||||
"/opt/",
|
||||
"com.kuhy",
|
||||
"app-release.apk",
|
||||
"build/app/outputs", # build outputs
|
||||
"flutter/services.dart",
|
||||
"flutter/material.dart", # package imports
|
||||
)
|
||||
|
||||
# Files a prompt CREATES. Citing them forward is correct, so absence is expected
|
||||
# -- but pairing each with its creating prompt still catches a typo.
|
||||
CREATED_BY_PROMPT = {
|
||||
"~/src/utils/unified-design-system/motion.md": "01",
|
||||
"~/src/utils/unified-design-system/scripts/structural_check.py": "02",
|
||||
}
|
||||
|
||||
# Cited precisely to state that they do NOT exist.
|
||||
ASSERTED_ABSENT = {
|
||||
"~/gatelock",
|
||||
# Distributed prompt copies are `git rm`d on completion (see the protocol in
|
||||
# 00-INDEX.md), but 00-INDEX.md keeps citing them in its "Distributed copies"
|
||||
# table. Asserting ABSENT rather than dropping the row keeps the check
|
||||
# two-directional: a finished prompt reappearing in a repo is itself a
|
||||
# failure, because the next session would re-run it. Move a row here as each
|
||||
# prompt completes.
|
||||
"~/src/utils/prompts/dopamine-ux-01-motion-tokens.md",
|
||||
}
|
||||
|
||||
# Generic prose fragments rather than citations of one specific file.
|
||||
PROSE_FRAGMENTS = {"lib/ui/theme.dart", "lib/main.dart", "pubspec.yaml"}
|
||||
|
||||
# Symbols each prompt claims exist, as (repo-relative file, symbol).
|
||||
SYMBOL_CLAIMS = [
|
||||
("~/src/diet-guard/app/lib/screens/log_meal_screen.dart", "_onLogMeal"),
|
||||
("~/src/diet-guard/app/lib/widgets/today_progress_card.dart", "TodayProgressCard"),
|
||||
("~/src/diet-guard/app/lib/widgets/streak_summary_row.dart", "StreakSummaryRow"),
|
||||
("~/src/diet-guard/app/lib/screens/log_meal_progress.dart", "buildTodayProgress"),
|
||||
("~/src/diet-guard/app/lib/services/app_settings_service.dart", "_writeToDisk"),
|
||||
("~/src/screen-locker/screen_locker/_status_types.py", "StatusSnapshot"),
|
||||
("~/src/screen-locker/screen_locker/_extra_benefits.py", "process_week_transition"),
|
||||
("~/src/screen-locker/screen_locker/_extra_benefits.py", "current_streak"),
|
||||
("~/src/screen-locker/screen_locker/_unlock_view.py", "unlock_screen"),
|
||||
("~/src/screen-locker/screen_locker/_log_mixin.py", "write_signed_entry"),
|
||||
("~/src/screen-locker/screen_locker/tests/conftest.py", "_ISOLATED_STATE"),
|
||||
("~/src/screen-locker/screen_locker/_weekly_check.py", "WEEKLY_WORKOUT_MINIMUM"),
|
||||
(
|
||||
"~/src/screen-locker/stronglift_replacement/workout_app/lib/screens/"
|
||||
"workout_screen_session.dart",
|
||||
"_playBreakEndCue",
|
||||
),
|
||||
(
|
||||
"~/src/screen-locker/stronglift_replacement/workout_app/lib/screens/"
|
||||
"workout_screen_finish.dart",
|
||||
"_persistFinishedWorkout",
|
||||
),
|
||||
(
|
||||
"~/src/screen-locker/stronglift_replacement/workout_app/lib/widgets/"
|
||||
"workout_summary_dialog.dart",
|
||||
"WorkoutSummaryDialog",
|
||||
),
|
||||
(
|
||||
"~/src/screen-locker/stronglift_replacement/workout_app/lib/services/"
|
||||
"storage_service.dart",
|
||||
"_getSetting",
|
||||
),
|
||||
(
|
||||
"~/src/screen-locker/stronglift_replacement/workout_app/lib/widgets/"
|
||||
"exercise_tile_rows.dart",
|
||||
"AnimatedContainer",
|
||||
),
|
||||
("~/src/todo/lib/data/app_settings.dart", "withAdvancedMode"),
|
||||
("~/src/todo/lib/ui/settings_screen.dart", "SwitchListTile"),
|
||||
("~/src/dufs-cloud/web/src/hooks/use-cloud-index.ts", "walkInto"),
|
||||
("~/src/dufs-cloud/app/lib/services/cloud_index.dart", "buildCloudIndex"),
|
||||
("~/src/dufs-cloud/web/src/lib/download.ts", "buildSelectionZip"),
|
||||
("~/src/dufs-cloud/app/lib/services/download_zip.dart", "buildSelectionZip"),
|
||||
("~/src/wake-alarm/phone_app/lib/screens/home_screen.dart", "_setPhoneAlarm"),
|
||||
("~/src/wake-alarm/wake_alarm/_audio.py", "_play_on_all_sinks"),
|
||||
("~/src/utils/unified-design-system/scripts/palette_check.py", "NON_COLOUR_CSS"),
|
||||
("~/src/utils/unified-design-system/scripts/palette_map.py", "PALETTE"),
|
||||
("~/src/utils/design_system/lib/src/feedback.dart", "showToast"),
|
||||
("~/src/utils/design_system/lib/src/tokens.dart", "AppSpacing"),
|
||||
("~/src/utils/gatelock/gatelock/_window.py", "LockConfig"),
|
||||
]
|
||||
|
||||
|
||||
def expand(raw: str) -> Path:
|
||||
return HOME / raw[2:] if raw.startswith("~/") else Path(raw)
|
||||
|
||||
|
||||
def candidate_paths(text: str) -> set[str]:
|
||||
return {
|
||||
raw
|
||||
for raw in PATH_RE.findall(text)
|
||||
if not any(s in raw for s in SKIP_SUBSTRINGS)
|
||||
and raw not in PROSE_FRAGMENTS
|
||||
and not re.search(r"\.\w+:\d+$", raw) # trailing :line suffix
|
||||
}
|
||||
|
||||
|
||||
def prompt_roots(text: str) -> list[Path]:
|
||||
roots = []
|
||||
for label in ("Repo", "App", "Flutter app"):
|
||||
for m in re.finditer(rf"{label}: `(~/[^`]+)`", text):
|
||||
roots.append(expand(m.group(1)))
|
||||
return roots
|
||||
|
||||
|
||||
def check_paths() -> list[str]:
|
||||
failures = []
|
||||
checked = 0
|
||||
for prompt in sorted(PROMPT_DIR.glob("*.md")):
|
||||
text = prompt.read_text()
|
||||
roots = prompt_roots(text)
|
||||
for raw in sorted(candidate_paths(text)):
|
||||
if raw in ASSERTED_ABSENT:
|
||||
if expand(raw).exists():
|
||||
failures.append(
|
||||
f"{prompt.name}: `{raw}` is asserted ABSENT but EXISTS"
|
||||
)
|
||||
continue
|
||||
if raw in CREATED_BY_PROMPT:
|
||||
owner = CREATED_BY_PROMPT[raw]
|
||||
cites_owner = re.search(rf"[Pp]rompt {owner}", text) is not None
|
||||
if not prompt.name.startswith(owner) and not cites_owner:
|
||||
failures.append(
|
||||
f"{prompt.name}: cites `{raw}` without noting prompt "
|
||||
f"{owner} creates it"
|
||||
)
|
||||
continue
|
||||
|
||||
checked += 1
|
||||
if raw.startswith(("~/", "/")):
|
||||
if not expand(raw).exists():
|
||||
failures.append(f"{prompt.name}: `{raw}` MISSING")
|
||||
elif not any((r / raw).exists() for r in roots):
|
||||
failures.append(
|
||||
f"{prompt.name}: `{raw}` not found under any of "
|
||||
f"{[str(r) for r in roots]}"
|
||||
)
|
||||
print(f"paths: checked {checked}")
|
||||
return failures
|
||||
|
||||
|
||||
def check_symbols() -> list[str]:
|
||||
failures = []
|
||||
for raw_file, symbol in SYMBOL_CLAIMS:
|
||||
path = expand(raw_file)
|
||||
if not path.exists():
|
||||
failures.append(f"symbol source MISSING: {raw_file}")
|
||||
continue
|
||||
if symbol not in path.read_text(errors="replace"):
|
||||
failures.append(f"symbol `{symbol}` NOT FOUND in {raw_file}")
|
||||
print(f"symbols: checked {len(SYMBOL_CLAIMS)}")
|
||||
return failures
|
||||
|
||||
|
||||
def main() -> int:
|
||||
n_prompts = len(list(PROMPT_DIR.glob("*.md")))
|
||||
print(f"verifying {n_prompts} prompt files in {PROMPT_DIR}")
|
||||
failures = check_paths() + check_symbols()
|
||||
if failures:
|
||||
print(f"\nFAIL -- {len(failures)} problem(s):")
|
||||
for f in failures:
|
||||
print(f" {f}")
|
||||
return 1
|
||||
print("\nPASS -- every cited path exists and every cited symbol is present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -132,10 +132,7 @@ def rewrite(text: str, old: Path, new: Path) -> str:
|
||||
|
||||
|
||||
#: The remaining files are plain reference docs, renamed mechanically to
|
||||
#: DOCS-<existing-slug>.md. The one exception is the dopamine-ux programme:
|
||||
#: 00-INDEX records 8 of its 9 parts as "not started", so those parts are
|
||||
#: outstanding work and become TODO, not DOCS. The index itself is a record.
|
||||
AUTO_TASK_PREFIXES = ("utils/dopamine-ux/0",)
|
||||
#: DOCS-<existing-slug>.md.
|
||||
|
||||
|
||||
def slugify(stem: str) -> str:
|
||||
@ -150,8 +147,6 @@ def auto_name(rel: str) -> tuple[str, str]:
|
||||
# A misspelled README is a README, not a doc needing a new namespace.
|
||||
if stem.lower() == "readme":
|
||||
return "README.md", "record"
|
||||
if rel.startswith(AUTO_TASK_PREFIXES) and "INDEX" not in stem:
|
||||
return f"TODO-{slugify(stem)}.md", "task"
|
||||
return f"DOCS-{slugify(stem)}.md", "record"
|
||||
|
||||
|
||||
|
||||
@ -42,7 +42,6 @@
|
||||
"testsAndMisc/docs/shell-split-verification.md": "DOCS-shell-split-verification.md",
|
||||
"testsAndMisc/linux_configuration/docs/SUMMARY.md": "DOCS-pacman-security-layers.md",
|
||||
"utils/gatelock/docs/scrollable-viewport.md": "DOCS-scrollable-viewport.md",
|
||||
"utils/dopamine-ux/00-INDEX.md": "DOCS-dopamine-ux-index.md",
|
||||
"digital-wellbeing/llm-notes/units-and-tasks.md": "DOCS-shutdown-units-reference.md",
|
||||
"digital-wellbeing/README_COMPULSIVE_BLOCK_LLM.md": "DOCS-compulsive-block-reference.md",
|
||||
"digital-wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md": "DOCS-midnight-shutdown-reference.md",
|
||||
|
||||
@ -90,12 +90,6 @@ def test_auto_name_fixes_readme_casing() -> None:
|
||||
assert auto_name("x/Readme.md") == ("README.md", "record")
|
||||
|
||||
|
||||
def test_dopamine_ux_parts_are_tasks_but_index_is_not() -> None:
|
||||
"""00-INDEX records the programme; 01-09 are its unstarted parts."""
|
||||
assert auto_name("utils/dopamine-ux/03-diet-guard.md")[1] == "task"
|
||||
assert auto_name("utils/dopamine-ux/00-INDEX.md")[1] == "record"
|
||||
|
||||
|
||||
def test_classified_paths_do_not_overlap() -> None:
|
||||
"""A file cannot be both a task and a record."""
|
||||
assert not set(classify.TASKS) & set(classify.RECORDS)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user