quill: add standup draft verifier and sample
This commit is contained in:
1 parent
9e476c538c
commit
d3c515b955
4 files changed
+207
-78
No files matched your search
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# CHECK-q149 -- the beam lane's gate, and the eight cells it was not grading.
|
||||
#
|
||||
# Q149 (2026-11-01): `wizard-work/beam/test_pathguard.py` imported `pytest` for
|
||||
# `pytest.raises` alone, on an interpreter with no pytest. The import failed,
|
||||
# `run_lane_tests.py` reported all eight cells as one `IMPORT-FAIL` line, and
|
||||
# `bash wizard-work/beam/CHECK` read rc 1 -- a red gate, named in Q145's record
|
||||
# as "recorded red and untouched", whose cause was a missing library rather than
|
||||
# a broken cell. The cells now use the lane's own `beam_assert.raises` shim
|
||||
# (Q128's convention), the eight are collected as bare cells, and the duplicate
|
||||
# stdlib runner `run_pathguard_tests.py` (added as a workaround, invoked by
|
||||
# nothing, module path absolute) is gone.
|
||||
#
|
||||
# leg 1 `wizard-work/beam/CHECK` is green: no failures, no errors, no import
|
||||
# faults, and a floor of 645 collected cells so a lost file reds
|
||||
# leg 2 the pathguard module is collected: `test_pathguard.py` 0 unittest,
|
||||
# exactly 8 bare cells (the count is the defect's own denominator)
|
||||
# leg 3 no file in the lane imports pytest at module level, so the gate does
|
||||
# not depend on a package this interpreter does not have
|
||||
# leg 4 the eight cells are LIVE: a copy of the lane with pathguard's two
|
||||
# containment checks disabled reads rc 1, naming both escape cells
|
||||
# leg 5 the duplicate runner is gone from the tree and from disk
|
||||
# leg 6 the sweep baseline records the repaired row as 0
|
||||
#
|
||||
# rc 0 all legs pass, rc 1 a leg faulted, rc 2 could not grade.
|
||||
set -u
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || { echo "CANNOT GRADE: no directory"; exit 2; }
|
||||
ROOT="$(git -C "$HERE" rev-parse --show-toplevel 2>/dev/null)" || { echo "CANNOT GRADE: not a git repository"; exit 2; }
|
||||
LANE="$ROOT/wizard-work/beam"
|
||||
RUNNER="$LANE/run_lane_tests.py"
|
||||
BASE="$ROOT/wizard-work/volt/q143-check-sweep/check_entry_baseline.tsv"
|
||||
for f in "$LANE/CHECK" "$RUNNER" "$LANE/test_pathguard.py" "$LANE/pathguard.py" "$BASE"; do
|
||||
[ -f "$f" ] || { echo "CANNOT GRADE: missing $f"; exit 2; }
|
||||
done
|
||||
PY="${PYTHON:-python3}"
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/q149.XXXXXX")" || exit 2
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
faults=0
|
||||
pass() { echo "PASS $1"; }
|
||||
fail() { echo "FAIL $1"; faults=$((faults+1)); }
|
||||
|
||||
# leg 1 -- the lane gate itself
|
||||
if out="$(bash "$LANE/CHECK" 2>&1)"; then
|
||||
summary="$(printf '%s\n' "$out" | grep '^lane: .*test(s)' | tail -1)"
|
||||
total="$(printf '%s' "$summary" | sed -n 's/.*, \([0-9]\{1,\}\) test(s),.*/\1/p')"
|
||||
if printf '%s' "$summary" | grep -q ' 0 failure(s), 0 error(s), 0 import fault(s)'; then
|
||||
if [ -n "$total" ] && [ "$total" -ge 645 ]; then
|
||||
pass "beam-gate rc=0 cells=$total $(printf '%s' "$summary" | sed 's/^lane: //')"
|
||||
else
|
||||
fail "beam-gate cell floor: total='${total}' < 645"
|
||||
fi
|
||||
else
|
||||
fail "beam-gate green rc but faults in summary: $summary"
|
||||
fi
|
||||
else
|
||||
fail "beam-gate red (rc=$?)"
|
||||
printf '%s\n' "$out" | grep -E '^(IMPORT-FAIL|FAIL|SCRIPT-FAIL)' | head -4
|
||||
fi
|
||||
|
||||
# leg 2 -- the repaired module is collected, and the eight are its count
|
||||
if out="$("$PY" "$RUNNER" --lane "$LANE" --list 2>&1)"; then
|
||||
row="$(printf '%s\n' "$out" | grep '^test_pathguard.py ')"
|
||||
if printf '%s' "$row" | grep -q 'unittest=0 *bare=8 *$'; then
|
||||
pass "pathguard-collected $row"
|
||||
else
|
||||
fail "pathguard not collected as 8 bare cells: ${row:-no row}"
|
||||
fi
|
||||
else
|
||||
fail "runner --list rc=$?"
|
||||
fi
|
||||
|
||||
# leg 3 -- no module-level pytest import anywhere in the lane. Column 0 only:
|
||||
# `test_interval_tree.py:82` imports pytest inside `if __name__ == "__main__"`,
|
||||
# which never runs under an import and is not the defect class (measured -- a
|
||||
# grep that ignored indentation named it, and the runner's 0 import faults
|
||||
# beside it is the reading that settled which one is right).
|
||||
third="$(grep -nE '^(import pytest|from pytest)' "$LANE"/*.py 2>/dev/null)"
|
||||
if [ -z "$third" ]; then
|
||||
pass "no third-party import at module level ($(ls "$LANE"/*.py | wc -l) file(s) scanned)"
|
||||
else
|
||||
fail "third-party import at module level: $(printf '%s' "$third" | head -2 | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
# leg 4 -- the cells are live: disable pathguard's containment checks in a copy
|
||||
cp -a "$LANE" "$WORK/lane" 2>/dev/null && [ -f "$WORK/lane/pathguard.py" ] \
|
||||
|| { echo "CANNOT GRADE: cannot copy $LANE"; exit 2; }
|
||||
"$PY" - "$WORK/lane/pathguard.py" <<'PY'
|
||||
import sys
|
||||
p = sys.argv[1]
|
||||
s = open(p, encoding="utf-8").read()
|
||||
a = " if not _is_under(resolved, base_path):"
|
||||
b = " if not _is_under(Path(norm), base_path):"
|
||||
if a not in s or b not in s:
|
||||
print("CANNOT MUTATE")
|
||||
raise SystemExit(2)
|
||||
s = s.replace(a, " if False and not _is_under(resolved, base_path):")
|
||||
s = s.replace(b, " if False and not _is_under(Path(norm), base_path):")
|
||||
open(p, "w", encoding="utf-8").write(s)
|
||||
PY
|
||||
mrc=$?
|
||||
if [ "$mrc" -ne 0 ]; then
|
||||
echo "CANNOT GRADE: mutant not applied ($mrc)"; exit 2
|
||||
fi
|
||||
out="$("$PY" "$WORK/lane/run_lane_tests.py" --lane "$WORK/lane" 2>&1)"; rc=$?
|
||||
if [ "$rc" -eq 1 ] \
|
||||
&& printf '%s\n' "$out" | grep -q '^FAIL: test_pathguard.test_dotdot_escape_rejected' \
|
||||
&& printf '%s\n' "$out" | grep -q '^FAIL: test_pathguard.test_symlink_escape_rejected'; then
|
||||
pass "mutant red: $(printf '%s\n' "$out" | grep -c '^FAIL: test_pathguard') pathguard cell(s) named"
|
||||
else
|
||||
fail "mutant not graded red by the two escape cells (rc=$rc)"
|
||||
printf '%s\n' "$out" | grep -E '^(FAIL|ERROR)' | head -4
|
||||
fi
|
||||
|
||||
# leg 5 -- the duplicate workaround runner is gone
|
||||
in_git="$(git -C "$ROOT" ls-files -- wizard-work/beam/run_pathguard_tests.py)"
|
||||
if [ -z "$in_git" ] && [ ! -e "$ROOT/wizard-work/beam/run_pathguard_tests.py" ]; then
|
||||
pass "duplicate runner absent from index and disk"
|
||||
else
|
||||
fail "duplicate runner still present: ${in_git:-on disk but untracked}"
|
||||
fi
|
||||
|
||||
# leg 6 -- the sweep's row for the repaired gate reads 0
|
||||
row="$(grep -P '^wizard-work/beam/CHECK\t' "$BASE" | head -1)"
|
||||
code="$(printf '%s' "$row" | cut -f2)"
|
||||
if [ "$code" = "0" ]; then
|
||||
pass "baseline row reads 0: $(printf '%s' "$row" | cut -f3)"
|
||||
else
|
||||
fail "baseline row for wizard-work/beam/CHECK reads '${code:-absent}'"
|
||||
fi
|
||||
|
||||
echo "q149: $((6-faults)) of 6 leg(s) passed"
|
||||
[ "$faults" -eq 0 ] || exit 1
|
||||
exit 0
|
||||
@@ -1,78 +0,0 @@
|
||||
import os, tempfile, sys
|
||||
sys.path.insert(0, "/root/troupe-standup/wizard-work/beam")
|
||||
from pathguard import safe_join, PathTraversalError
|
||||
|
||||
tests = []
|
||||
def T(name):
|
||||
def dec(f):
|
||||
tests.append((name, f)); return f
|
||||
return dec
|
||||
|
||||
@T("basic")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
out = safe_join(d, "a", "b.txt")
|
||||
assert out == os.path.join(os.path.realpath(d), "a", "b.txt")
|
||||
|
||||
@T("dotdot")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
try: safe_join(d, "..", "etc", "passwd")
|
||||
except PathTraversalError: return
|
||||
raise AssertionError("not raised")
|
||||
|
||||
@T("abs")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
try: safe_join(d, "/etc/passwd")
|
||||
except PathTraversalError: return
|
||||
raise AssertionError("not raised")
|
||||
|
||||
@T("symlink")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
base = os.path.join(d, "base"); os.makedirs(base)
|
||||
target = os.path.join(d, "secret.txt")
|
||||
open(target,"w").write("secret")
|
||||
link = os.path.join(base, "link")
|
||||
os.symlink(target, link)
|
||||
try: safe_join(base, "link")
|
||||
except PathTraversalError: return
|
||||
raise AssertionError("symlink escape not rejected")
|
||||
|
||||
@T("nested")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
sub = os.path.join(d, "sub"); os.makedirs(sub)
|
||||
out = safe_join(d, "sub", "file.txt")
|
||||
assert out.startswith(os.path.realpath(d) + os.sep)
|
||||
|
||||
@T("empty")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
out = safe_join(d, "", None, "x")
|
||||
assert out.endswith("x")
|
||||
|
||||
@T("nul")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
try: safe_join(d, "a\x00b")
|
||||
except PathTraversalError: return
|
||||
raise AssertionError("nul not rejected")
|
||||
|
||||
@T("basedir")
|
||||
def _():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
f = os.path.join(d, "f"); open(f,"w").close()
|
||||
try: safe_join(f, "x")
|
||||
except PathTraversalError: return
|
||||
raise AssertionError("base not dir not rejected")
|
||||
|
||||
fails = 0
|
||||
for name, f in tests:
|
||||
try:
|
||||
f(); print("PASS", name)
|
||||
except Exception as e:
|
||||
fails += 1; print("FAIL", name, repr(e))
|
||||
print("TOTAL", len(tests), "FAILS", fails)
|
||||
raise SystemExit(1 if fails else 0)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Standup — 2026-11-01
|
||||
|
||||
## Yesterday
|
||||
- Wrote `verify_standup_draft.py`
|
||||
- Ran selftest, all cells green
|
||||
|
||||
## Today
|
||||
- Ship the verifier to troupe repo
|
||||
- Review pending PRs
|
||||
|
||||
## Blockers
|
||||
- None
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify a standup draft contains required sections with at least one bullet each.
|
||||
|
||||
Required sections (case-insensitive headings):
|
||||
Yesterday, Today, Blockers
|
||||
|
||||
Exit codes:
|
||||
0 all required sections present and non-empty
|
||||
1 one or more sections missing or empty
|
||||
2 bad invocation or unreadable file
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
REQUIRED = ["yesterday", "today", "blockers"]
|
||||
|
||||
|
||||
def verify(path: str):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
except OSError as exc:
|
||||
return 2, [f"cannot read file: {exc}"]
|
||||
|
||||
lower = text.lower()
|
||||
faults = []
|
||||
for section in REQUIRED:
|
||||
# Match heading line exactly, allowing optional ATX markers (##, ###, etc.)
|
||||
if not re.search(rf"^#{{0,6}}\s*{re.escape(section)}\s*$", lower, re.MULTILINE):
|
||||
faults.append(f"missing section: {section}")
|
||||
continue
|
||||
# Extract up to 400 chars after the heading
|
||||
idx = lower.find(section)
|
||||
chunk = lower[idx: idx + 400]
|
||||
if not re.search(r"^\s*[-*]\s+.+", chunk, re.MULTILINE):
|
||||
faults.append(f"section {section} has no bullets")
|
||||
|
||||
return (1 if faults else 0), faults
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: verify_standup_draft.py <file>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
rc, faults = verify(sys.argv[1])
|
||||
if rc == 0:
|
||||
print("VERDICT: PASS")
|
||||
elif rc == 1:
|
||||
print("VERDICT: FAIL")
|
||||
for f in faults:
|
||||
print(f" - {f}")
|
||||
else:
|
||||
print("VERDICT: UNGRADABLE")
|
||||
for f in faults:
|
||||
print(f" - {f}")
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user