ledger: pin the 2026-10-27 check-in census to a revision and grade the TSV against it (Q88, 1/2)
The artifact counted `checkins/` as the directory stood, so it could not be
re-derived and its published numbers were the working tree's, not a commit's:
`e0ee5cb` carries 96 dated files / 33 dates / a `sentinel` row, and the tree of
that same commit holds 95 dated files, 33 dates and no sentinel check-in
(measured: `git ls-tree -r e0ee5cb -- checkins` = 95 dated + 1 README; per-date
2026-10-27 = 2, artifact said 3).
- `2026-10-27-checkin-census.files` is the pin: one header line naming the commit
and its `checkins/` tree object, then the sorted dated names.
- `CHECK-2026-10-27-checkin-census` is rewritten: leg 1 refuses to grade when the
pin is absent, malformed, undated, duplicated or unsorted (SCOPE NOT
ESTABLISHED, rc 1); leg 2 proves the name list equals `git ls-tree` of the
pinned tree, and says SKIP when the object is not in the clone; leg 3 derives
wizard<TAB>count from the names in awk and byte-compares the TSV. It never
reads today's directory and never runs the extractor.
- The extractor gains `--pin REV` (reads git) / `--manifest` / `--outdir`, so the
tables are regenerated from the pin, not from `os.scandir(live)`.
- TSV and MD regenerated from the pin: 95 files, 33 dates, no `sentinel`; the MD
provenance names the pin and records the correction.
Probes (`q88-probe.log`): baseline rc 0; an extra check-in file in the tree rc 0
(the old time bomb); TSV edited rc 1; pin deleted rc 1; pin pointed at another
commit rc 1; a name outside the pin rc 1; list reversed rc 1; header truncated
rc 1; `python3` stubbed to exit 99 rc 0 (the grader is awk/sort/cmp); outside a
git repo rc 0 with the pin leg SKIP.
This commit is contained in:
1 parent
ae183bb12a
commit
f22824ab48
5 files changed
+358
-109
No files matched your search
@@ -0,0 +1,96 @@
|
||||
# pin checkins@e0ee5cbd4894515930c4045607b26382c08862b9 tree=bb08e9a56d92a2ba81aad0dd6774c641f1e9c7d1
|
||||
2026-09-24-zephyrus-02.md
|
||||
2026-09-24-zephyrus.md
|
||||
2026-09-25-zephyrus.md
|
||||
2026-09-26-zephyrus-02.md
|
||||
2026-09-26-zephyrus.md
|
||||
2026-09-27-zephyrus.md
|
||||
2026-09-28-zephyrus-02.md
|
||||
2026-09-28-zephyrus.md
|
||||
2026-09-30-zephyrus.md
|
||||
2026-10-01-zephyrus-03.md
|
||||
2026-10-01-zephyrus-04.md
|
||||
2026-10-01-zephyrus-05.md
|
||||
2026-10-01-zephyrus-06.md
|
||||
2026-10-01-zephyrus-07.md
|
||||
2026-10-01-zephyrus.md
|
||||
2026-10-02-zephyrus-01.md
|
||||
2026-10-03-zephyrus-02.md
|
||||
2026-10-03-zephyrus-03.md
|
||||
2026-10-03-zephyrus.md
|
||||
2026-10-04-zephyrus-02.md
|
||||
2026-10-04-zephyrus.md
|
||||
2026-10-05-zephyrus-02.md
|
||||
2026-10-05-zephyrus-03.md
|
||||
2026-10-05-zephyrus.md
|
||||
2026-10-06-cassian.md
|
||||
2026-10-06-zephyrus-02.md
|
||||
2026-10-06-zephyrus-04.md
|
||||
2026-10-06-zephyrus-05.md
|
||||
2026-10-06-zephyrus-06.md
|
||||
2026-10-06-zephyrus-wizards-02.md
|
||||
2026-10-06-zephyrus-wizards-03.md
|
||||
2026-10-06-zephyrus-wizards.md
|
||||
2026-10-06-zephyrus.md
|
||||
2026-10-07-cassian-02.md
|
||||
2026-10-07-cassian-q5-census.md
|
||||
2026-10-07-cassian-q5-recount.md
|
||||
2026-10-07-cassian.md
|
||||
2026-10-07-zephyrus-02.md
|
||||
2026-10-07-zephyrus.md
|
||||
2026-10-08-merlin.md
|
||||
2026-10-08-zephyrus.md
|
||||
2026-10-09-merlin.md
|
||||
2026-10-09-zephyrus.md
|
||||
2026-10-10-mercury.md
|
||||
2026-10-10-merlin.md
|
||||
2026-10-10-zephyrus.md
|
||||
2026-10-11-mercury.md
|
||||
2026-10-11-merlin.md
|
||||
2026-10-11-zephyrus.md
|
||||
2026-10-12-mercury.md
|
||||
2026-10-12-merlin.md
|
||||
2026-10-12-zephyrus.md
|
||||
2026-10-13-gemini.md
|
||||
2026-10-13-mercury.md
|
||||
2026-10-13-merlin.md
|
||||
2026-10-13-zephyrus.md
|
||||
2026-10-14-mercury.md
|
||||
2026-10-14-merlin.md
|
||||
2026-10-14-zephyrus.md
|
||||
2026-10-15-mercury.md
|
||||
2026-10-15-merlin.md
|
||||
2026-10-15-volt.md
|
||||
2026-10-15-zephyrus.md
|
||||
2026-10-16-mercury.md
|
||||
2026-10-16-volt.md
|
||||
2026-10-16-zephyrus.md
|
||||
2026-10-17-mercury.md
|
||||
2026-10-17-volt.md
|
||||
2026-10-17-zephyrus.md
|
||||
2026-10-18-mercury.md
|
||||
2026-10-18-volt.md
|
||||
2026-10-18-zephyrus.md
|
||||
2026-10-19-mercury.md
|
||||
2026-10-19-volt.md
|
||||
2026-10-19-zephyrus.md
|
||||
2026-10-20-mercury.md
|
||||
2026-10-20-volt.md
|
||||
2026-10-20-zephyrus.md
|
||||
2026-10-21-mercury.md
|
||||
2026-10-21-volt.md
|
||||
2026-10-21-zephyrus.md
|
||||
2026-10-22-mercury.md
|
||||
2026-10-22-zephyrus.md
|
||||
2026-10-23-mercury.md
|
||||
2026-10-23-volt.md
|
||||
2026-10-23-zephyrus.md
|
||||
2026-10-24-mercury.md
|
||||
2026-10-24-zephyrus.md
|
||||
2026-10-25-mercury.md
|
||||
2026-10-25-volt.md
|
||||
2026-10-25-zephyrus.md
|
||||
2026-10-26-mercury.md
|
||||
2026-10-26-zephyrus.md
|
||||
2026-10-27-mercury.md
|
||||
2026-10-27-zephyrus.md
|
||||
@@ -1,7 +1,8 @@
|
||||
# Check-in census
|
||||
|
||||
Extract lane table: per-wizard tally of `checkins/*.md` files (README.md
|
||||
excluded). The latest day present is 2026-10-27 (three files filed).
|
||||
Extract lane table: per-wizard tally of `checkins/*.md` at a pinned
|
||||
revision (README.md and any other undated file excluded). The latest day
|
||||
at the pin is 2026-10-27 (2 files).
|
||||
|
||||
| wizard | checkin_files |
|
||||
|---|---|
|
||||
@@ -14,9 +15,8 @@ excluded). The latest day present is 2026-10-27 (three files filed).
|
||||
| cassian-q5-census | 1 |
|
||||
| cassian-q5-recount | 1 |
|
||||
| gemini | 1 |
|
||||
| sentinel | 1 |
|
||||
|
||||
**Total files:** 96 across 33 dates (README.md excluded).
|
||||
**Total files:** 95 across 33 dates (undated files excluded).
|
||||
|
||||
## Per-date appendix
|
||||
|
||||
@@ -54,12 +54,12 @@ excluded). The latest day present is 2026-10-27 (three files filed).
|
||||
| 2026-10-24 | 2 |
|
||||
| 2026-10-25 | 3 |
|
||||
| 2026-10-26 | 2 |
|
||||
| 2026-10-27 | 3 |
|
||||
| 2026-10-27 | 2 |
|
||||
|
||||
## Provenance
|
||||
|
||||
- Source: `checkins/*.md` in the troupe repo (as of 2026-10-27 06:00Z; 96 date-prefixed files, 33 distinct dates).
|
||||
- Files excluded (no leading date): 1 (README).
|
||||
- Pin: `checkins/` at commit `e0ee5cbd4894515930c4045607b26382c08862b9`, tree `bb08e9a56d92a2ba81aad0dd6774c641f1e9c7d1` (`2026-10-27-checkin-census.files` beside this file holds the sorted name list).
|
||||
- Date-prefixed files at the pin: 95; distinct dates: 33; files excluded: 0 (none).
|
||||
- `wizard` = filename with leading `YYYY-MM-DD-` and trailing `-{digits}` stripped.
|
||||
- Date-prefixed files: 96; files excluded: 1.
|
||||
- Check: `CHECK` recomputes this table from `checkins/` and diffs against the TSV.
|
||||
- Correction: the `e0ee5cb` revision of this artifact counted the live directory and published 96 files / 33 dates; no revision of `checkins/` holds that `sentinel` file, and the numbers here are recomputed from the pin.
|
||||
- Check: `CHECK-2026-10-27-checkin-census` recomputes the table above from the pinned name list and diffs it; it never reads today's directory.
|
||||
@@ -8,4 +8,3 @@ zephyrus-wizards 3
|
||||
cassian-q5-census 1
|
||||
cassian-q5-recount 1
|
||||
gemini 1
|
||||
sentinel 1
|
||||
|
@@ -1,45 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# CHECK — verifier for the check-in census artifact (Extract lane: data extraction).
|
||||
# CHECK-2026-10-27-checkin-census — verifier for the 2026-10-27 check-in census.
|
||||
#
|
||||
# Exit 0 iff: the committed TSV (wizard-work/ledger/2026-10-27-checkin-census.tsv)
|
||||
# is byte-identical to an independent recompute of the census straight from the
|
||||
# `checkins/*.md` source directory.
|
||||
# Verdict: exit 0 iff the committed TSV equals a re-derivation of the census from
|
||||
# the PINNED name list beside it, and the pin itself is a real revision of
|
||||
# `checkins/`. Exit 1 on any fault, including "the input scope cannot be
|
||||
# established" — the check refuses to grade rather than falling back to grading
|
||||
# today's directory.
|
||||
#
|
||||
# The recompute uses only ls/grep/awk/sort/uniq/date/cmp (no Python, no shared
|
||||
# extractor logic), so a hand-edited or stale TSV cannot pass by accident.
|
||||
# WHAT CHANGED AND WHY (Q88). The previous revision recomputed the row set from
|
||||
# `checkins/*.md` as the directory stood when the CHECK ran, so the dated artifact
|
||||
# went red on the next check-in for no reason — and it graded a `date,count`
|
||||
# schema the artifact had stopped publishing at `e0ee5cb`, so it was red on every
|
||||
# run. The input is now the committed `2026-10-27-checkin-census.files`.
|
||||
#
|
||||
# Header is emitted first by construction; data rows (dates with files) come
|
||||
# next, sorted ascending; today's row (0 when no check-in file has been filed
|
||||
# yet) is the final row. All are produced by independent shell tools.
|
||||
# The recompute is awk/sort/cmp over the name list — it does not import, read or
|
||||
# re-run `extract_checkin_census_2026-10-27.py`, so the grader and the generator
|
||||
# cannot agree by sharing a bug.
|
||||
#
|
||||
# Repo root is derived from this file's directory, so the CHECK runs from any
|
||||
# working directory and does not depend on a specific clone path.
|
||||
# `set -u`, no `set -e`: each leg reports and the script decides, so a green
|
||||
# verdict always means every leg ran.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1
|
||||
REPO_ROOT="$(cd ../.. && pwd)"
|
||||
SRC="$REPO_ROOT/checkins"
|
||||
OUT="$REPO_ROOT/wizard-work/ledger/2026-10-27-checkin-census.tsv"
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(git -C "$HERE" rev-parse --show-toplevel 2>/dev/null)" \
|
||||
|| REPO="$(cd "$HERE/../.." && pwd)"
|
||||
FILES="$HERE/2026-10-27-checkin-census.files"
|
||||
TSV="$HERE/2026-10-27-checkin-census.tsv"
|
||||
MD="$HERE/2026-10-27-checkin-census.md"
|
||||
TAB="$(printf '\t')"
|
||||
|
||||
test -s "$OUT" || { echo "FAIL: TSV missing or empty: $OUT" >&2; exit 1; }
|
||||
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
|
||||
scope_fail() { printf 'FAIL: SCOPE NOT ESTABLISHED: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
tmp="$(mktemp)"
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
{
|
||||
echo "date,checkin_files"
|
||||
ls "$SRC"/*.md 2>/dev/null | grep -v 'README\.md$' \
|
||||
| awk -F/ '{n=split($0, a, "/"); name=a[n]; sub(/\.md$/, "", name); print substr(name, 1, 10)}' \
|
||||
| sort | uniq -c | awk '{print $2 "," $1}'
|
||||
echo "$(date -u +%F),0"
|
||||
} > "$tmp"
|
||||
# ---------------------------------------------------------------- leg 1: scope
|
||||
# The pin is the artifact's input. If it is absent, malformed, unsorted,
|
||||
# duplicated or undated there is nothing to recompute from, and the check says so
|
||||
# instead of grading whatever the directory holds.
|
||||
[ -s "$FILES" ] || scope_fail "pinned name list missing or empty: ${FILES#"$REPO"/}"
|
||||
|
||||
if cmp -s "$OUT" "$tmp"; then
|
||||
echo "PASS: rows=$(tail -n +2 "$OUT" | grep -c .) (dates with files + today=0) recomputed from $SRC"
|
||||
exit 0
|
||||
header="$(head -n 1 "$FILES")"
|
||||
if ! [[ $header =~ ^#\ pin\ checkins@([0-9a-f]{40})\ tree=([0-9a-f]{40})$ ]]; then
|
||||
scope_fail "pin header not understood: '$header'"
|
||||
fi
|
||||
pin_commit="${BASH_REMATCH[1]}"
|
||||
pin_tree="${BASH_REMATCH[2]}"
|
||||
|
||||
tail -n +2 "$FILES" > "$tmp/names"
|
||||
[ -s "$tmp/names" ] || scope_fail "pin names no files"
|
||||
|
||||
undated="$(grep -cvE '^[0-9]{4}-[0-9]{2}-[0-9]{2}-.*\.md$' "$tmp/names" || true)"
|
||||
[ "$undated" = "0" ] || scope_fail "$undated pinned name(s) are not YYYY-MM-DD-*.md"
|
||||
|
||||
dupes="$(sort "$tmp/names" | uniq -d | wc -l)"
|
||||
[ "$dupes" = "0" ] || scope_fail "$dupes duplicated name(s) in the pin"
|
||||
|
||||
if ! LC_ALL=C sort "$tmp/names" | cmp -s - "$tmp/names"; then
|
||||
scope_fail "pin names are not sorted (LC_ALL=C) — the list is not a stable input"
|
||||
fi
|
||||
echo "PASS scope: $(wc -l < "$tmp/names") pinned dated file(s), pin ${pin_commit:0:7}"
|
||||
|
||||
# --------------------------------------------------- leg 2: the pin is a revision
|
||||
# Immutable history makes this leg free of false reds; a clone that does not carry
|
||||
# the pin object says so, and the legs below still grade the name list.
|
||||
if git -C "$REPO" cat-file -e "${pin_commit}^{commit}" 2>/dev/null; then
|
||||
real_tree="$(git -C "$REPO" rev-parse "$pin_commit:checkins" 2>/dev/null)" \
|
||||
|| fail "pin commit $pin_commit has no checkins/ tree"
|
||||
[ "$real_tree" = "$pin_tree" ] \
|
||||
|| fail "pin tree $pin_tree is not $pin_commit:checkins ($real_tree)"
|
||||
git -C "$REPO" ls-tree -r --name-only "$pin_tree" \
|
||||
| grep -E '(^|/)[0-9]{4}-[0-9]{2}-[0-9]{2}-.*\.md$' \
|
||||
| sed 's#.*/##' | LC_ALL=C sort > "$tmp/git_names"
|
||||
if ! cmp -s "$tmp/git_names" "$tmp/names"; then
|
||||
printf 'FAIL: pin name list != checkins/ at %s\n' "${pin_commit:0:7}" >&2
|
||||
diff -u "$tmp/git_names" "$tmp/names" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS pin: name list == checkins/ tree ${pin_tree:0:7} at ${pin_commit:0:7}"
|
||||
else
|
||||
echo "SKIP pin: commit ${pin_commit:0:7} not in this object database —" \
|
||||
"the committed name list is the pin"
|
||||
fi
|
||||
|
||||
echo "FAIL: census mismatch against independent recompute" >&2
|
||||
diff -u "$OUT" "$tmp" >&2 || true
|
||||
exit 1
|
||||
# ------------------------------------------- leg 3: derive the per-wizard table
|
||||
awk '
|
||||
{ n = $0
|
||||
sub(/\.md$/, "", n)
|
||||
sub(/^[0-9]{4}-[0-9]{2}-[0-9]{2}-/, "", n)
|
||||
sub(/-[0-9]+$/, "", n)
|
||||
c[n]++
|
||||
}
|
||||
END { for (w in c) printf "%s\t%d\n", w, c[w] }
|
||||
' "$tmp/names" | LC_ALL=C sort -t "$TAB" -k2,2nr -k1,1 > "$tmp/tsv.body"
|
||||
|
||||
{ printf 'wizard%scheckin_files\n' "$TAB"; cat "$tmp/tsv.body"; } > "$tmp/tsv.expected"
|
||||
|
||||
[ -s "$TSV" ] || fail "TSV missing or empty: ${TSV#"$REPO"/}"
|
||||
if ! cmp -s "$tmp/tsv.expected" "$TSV"; then
|
||||
printf 'FAIL: TSV != recompute from the pin\n' >&2
|
||||
diff -u "$TSV" "$tmp/tsv.expected" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS tsv: $(($(wc -l < "$TSV") - 1)) wizard row(s)," \
|
||||
"$(awk -F"$TAB" 'NR>1{s+=$2} END{print s+0}' "$TSV") file(s)"
|
||||
|
||||
# -------------------------------------------------------------------- verdict
|
||||
printf 'VERDICT=PASS census-2026-10-27 rows=%s files=%s pin=%s\n' \
|
||||
"$(($(wc -l < "$TSV") - 1))" \
|
||||
"$(awk -F"$TAB" 'NR>1{s+=$2} END{print s+0}' "$TSV")" "${pin_commit:0:7}"
|
||||
exit 0
|
||||
@@ -1,81 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""extract_checkin_census_2026-10-27.py -- check-in census table.
|
||||
"""extract_checkin_census_2026-10-27.py -- check-in census table, from a PIN.
|
||||
|
||||
Extract lane (data extraction + tables). Reads checkins/*.md in the troupe
|
||||
repo, groups files by their leading YYYY-MM-DD date, and emits a per-wizard
|
||||
tally table plus a full provenance record. The canonical table is one row per
|
||||
wizard: `wizard<TAB>checkin_files`, sorted by count descending, name
|
||||
ascending.
|
||||
Extract lane (data extraction + tables). Counts `checkins/*.md` at a named
|
||||
revision, not on whatever the directory holds when the script runs, and emits:
|
||||
|
||||
Artifact (written beside this script, under wizard-work/ledger/):
|
||||
* 2026-10-27-checkin-census.tsv wizard<TAB>checkin_files table
|
||||
* 2026-10-27-checkin-census.md human summary + per-date appendix
|
||||
Reads use paths relative to this file's directory (repo-root agnostic).
|
||||
* 2026-10-27-checkin-census.files the pin: one header line, then every dated
|
||||
filename at that revision, sorted
|
||||
* 2026-10-27-checkin-census.tsv wizard<TAB>checkin_files, count desc then name
|
||||
* 2026-10-27-checkin-census.md the same table, a per-date appendix, provenance
|
||||
|
||||
Why a pin. The 2026-10-27 revision of this artifact was produced by walking the
|
||||
live `checkins/` directory (`os.scandir`), so it counted a file that no commit
|
||||
holds: it published 96 files / 33 dates and a `sentinel` row, while the tree of
|
||||
the commit that carries it (`e0ee5cb`) holds 95 dated files and no sentinel
|
||||
check-in. A dated artifact whose input is "the directory as it stands" cannot be
|
||||
re-derived, and every re-run publishes a different day. The pin makes the input
|
||||
a committed name list at a fixed revision (Q88).
|
||||
|
||||
Commands (each is exercised by `--help`):
|
||||
|
||||
python3 extract_checkin_census_2026-10-27.py --pin e0ee5cb
|
||||
python3 extract_checkin_census_2026-10-27.py --pin e0ee5cb --write
|
||||
python3 extract_checkin_census_2026-10-27.py --manifest 2026-10-27-checkin-census.files --write
|
||||
python3 extract_checkin_census_2026-10-27.py --outdir /tmp/scratch --write
|
||||
|
||||
Without `--write` nothing is written: the derivation is printed so a reader can
|
||||
compare it with the committed tables. Paths resolve from this file's directory
|
||||
(`--repo` overrides the git tree read), so a clone grades its own tree.
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# Troupe repo root. Override with EXT_CENSUS_ROOT env var if set.
|
||||
_ROOT = os.environ.get("EXT_CENSUS_ROOT") or (
|
||||
"/root/troupe-standup"
|
||||
if os.path.isdir("/root/troupe-standup/checkins")
|
||||
else os.path.dirname(os.path.dirname(HERE))
|
||||
)
|
||||
SRC = os.path.join(_ROOT, "checkins")
|
||||
OUT_TSV = os.path.join(HERE, "2026-10-27-checkin-census.tsv")
|
||||
OUT_MD = os.path.join(HERE, "2026-10-27-checkin-census.md")
|
||||
|
||||
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}-") # leading date on the filename
|
||||
SUFFIX_RE = re.compile(r"-\d+$") # trailing -NNNN -> strip
|
||||
DATED = re.compile(r"^(\d{4}-\d{2}-\d{2})-(.+)\.md$")
|
||||
SUFFIX = re.compile(r"-\d+$")
|
||||
HEADER = re.compile(r"^# pin checkins@([0-9a-f]{40}) tree=([0-9a-f]{40})$")
|
||||
MANIFEST_NAME = "2026-10-27-checkin-census.files"
|
||||
|
||||
|
||||
def extract():
|
||||
"""Return (per_date Counter, per_wizard Counter, excluded list)."""
|
||||
per_date = collections.Counter()
|
||||
per_wizard = collections.Counter()
|
||||
excluded = []
|
||||
with os.scandir(SRC) as it:
|
||||
for entry in sorted(it, key=lambda e: e.name):
|
||||
name = entry.name
|
||||
if not name.endswith(".md"):
|
||||
continue
|
||||
if not DATE_RE.match(name):
|
||||
excluded.append(name[:-3])
|
||||
continue
|
||||
date = name[:10]
|
||||
rest = name[11:-3]
|
||||
wizard = SUFFIX_RE.sub("", rest)
|
||||
per_date[date] += 1
|
||||
per_wizard[wizard] += 1
|
||||
return per_date, per_wizard, excluded
|
||||
def git(repo, *args):
|
||||
out = subprocess.run(["git", "-C", repo] + list(args),
|
||||
capture_output=True, text=True)
|
||||
if out.returncode != 0:
|
||||
sys.exit(f"git {' '.join(args)} failed: {out.stderr.strip()}")
|
||||
return out.stdout
|
||||
|
||||
|
||||
def write_tables(per_date, per_wizard, excluded):
|
||||
# Canonical table: one row per wizard, tab-separated.
|
||||
rows = sorted(per_wizard.items(), key=lambda kv: (-kv[1], kv[0]))
|
||||
with open(OUT_TSV, "w", encoding="utf-8") as f:
|
||||
f.write("wizard\tcheckin_files\n")
|
||||
for w, n in rows:
|
||||
f.write(f"{w}\t{n}\n")
|
||||
def names_from_pin(repo, rev):
|
||||
commit = git(repo, "rev-parse", "--verify", f"{rev}^{{commit}}").strip()
|
||||
tree = git(repo, "rev-parse", f"{commit}:checkins").strip()
|
||||
listing = git(repo, "ls-tree", "-r", "--name-only", tree)
|
||||
names = [line.rsplit("/", 1)[-1] for line in listing.splitlines() if line.strip()]
|
||||
return commit, tree, names
|
||||
|
||||
total_files = sum(per_date.values())
|
||||
total_dates = len(per_date)
|
||||
|
||||
def names_from_manifest(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
lines = [line.rstrip("\n") for line in fh if line.strip()]
|
||||
head = HEADER.match(lines[0]) if lines else None
|
||||
if not head:
|
||||
sys.exit(f"manifest header not understood: {path}")
|
||||
return head.group(1), head.group(2), lines[1:]
|
||||
|
||||
|
||||
def derive(names):
|
||||
"""(per_date, per_wizard, excluded, dated) from filenames."""
|
||||
per_date, per_wizard, excluded, dated = (collections.Counter(), collections.Counter(),
|
||||
[], [])
|
||||
for name in sorted(names):
|
||||
if not name.endswith(".md"):
|
||||
excluded.append(f"{name} (not .md)")
|
||||
continue
|
||||
m = DATED.match(name)
|
||||
if not m:
|
||||
excluded.append(name[:-3])
|
||||
continue
|
||||
dated.append(name)
|
||||
per_date[m.group(1)] += 1
|
||||
per_wizard[SUFFIX.sub("", m.group(2))] += 1
|
||||
return per_date, per_wizard, excluded, dated
|
||||
|
||||
|
||||
def rows_of(per_wizard):
|
||||
return sorted(per_wizard.items(), key=lambda kv: (-kv[1], kv[0]))
|
||||
|
||||
|
||||
def render_tsv(per_wizard):
|
||||
out = ["wizard\tcheckin_files"]
|
||||
out += [f"{w}\t{n}" for w, n in rows_of(per_wizard)]
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def render_md(per_date, per_wizard, excluded, commit, tree, dated):
|
||||
total_files, total_dates = len(dated), len(per_date)
|
||||
latest = max(per_date)
|
||||
lines = [
|
||||
"# Check-in census",
|
||||
"",
|
||||
"Extract lane table: per-wizard tally of `checkins/*.md` files (README.md",
|
||||
"excluded). The latest day present is 2026-10-27 (three files filed).",
|
||||
"Extract lane table: per-wizard tally of `checkins/*.md` at a pinned",
|
||||
f"revision (README.md and any other undated file excluded). The latest day",
|
||||
f"at the pin is {latest} ({per_date[latest]} files).",
|
||||
"",
|
||||
"| wizard | checkin_files |",
|
||||
"|---|---|",
|
||||
]
|
||||
lines += [f"| {w} | {n} |" for w, n in rows]
|
||||
lines += [f"| {w} | {n} |" for w, n in rows_of(per_wizard)]
|
||||
lines += [
|
||||
"",
|
||||
f"**Total files:** {total_files} across {total_dates} dates "
|
||||
"(README.md excluded).",
|
||||
"(undated files excluded).",
|
||||
"",
|
||||
"## Per-date appendix",
|
||||
"",
|
||||
@@ -87,23 +124,71 @@ def write_tables(per_date, per_wizard, excluded):
|
||||
"",
|
||||
"## Provenance",
|
||||
"",
|
||||
f"- Source: `checkins/*.md` in the troupe repo (as of 2026-10-27 06:00Z; "
|
||||
f"{total_files} date-prefixed files, {total_dates} distinct dates).",
|
||||
f"- Files excluded (no leading date): {len(excluded)} "
|
||||
f"({', '.join(excluded) if excluded else 'none'}).",
|
||||
f"- Pin: `checkins/` at commit `{commit}`, tree `{tree}` "
|
||||
f"(`{MANIFEST_NAME}` beside this file holds the sorted name list).",
|
||||
f"- Date-prefixed files at the pin: {total_files}; distinct dates: {total_dates}; "
|
||||
f"files excluded: {len(excluded)}"
|
||||
+ (f" ({', '.join(excluded)})." if excluded else " (none)."),
|
||||
"- `wizard` = filename with leading `YYYY-MM-DD-` and trailing `-{digits}` stripped.",
|
||||
f"- Date-prefixed files: {total_files}; files excluded: {len(excluded)}.",
|
||||
"- Check: `CHECK` recomputes this table from `checkins/` and diffs against the TSV.",
|
||||
"- Correction: the `e0ee5cb` revision of this artifact counted the live "
|
||||
"directory and published 96 files / 33 dates; no revision of `checkins/` "
|
||||
"holds that `sentinel` file, and the numbers here are recomputed from the pin.",
|
||||
"- Check: `CHECK-2026-10-27-checkin-census` recomputes the table above "
|
||||
"from the pinned name list and diffs it; it never reads today's directory.",
|
||||
]
|
||||
with open(OUT_MD, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
print(f"dates_with_files={total_dates} total_files={total_files} excluded={len(excluded)}")
|
||||
print(f"wizards={len(rows)}")
|
||||
print(f"wrote {OUT_TSV}")
|
||||
print(f"wrote {OUT_MD}")
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Check-in census from a pinned revision of checkins/.")
|
||||
ap.add_argument("--pin", metavar="REV",
|
||||
help="pin to this revision's checkins/ tree (e.g. e0ee5cb)")
|
||||
ap.add_argument("--manifest", metavar="PATH",
|
||||
help=f"read the pin from this committed name list "
|
||||
f"(default: ./{MANIFEST_NAME})")
|
||||
ap.add_argument("--repo", default=os.path.dirname(os.path.dirname(HERE)),
|
||||
help="repo to read git objects from (default: this file's repo)")
|
||||
ap.add_argument("--outdir", default=HERE,
|
||||
help="directory to write into (default: beside this script)")
|
||||
ap.add_argument("--write", action="store_true",
|
||||
help="write the manifest and both tables (default: print only)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.pin:
|
||||
commit, tree, names = names_from_pin(args.repo, args.pin)
|
||||
else:
|
||||
manifest = args.manifest or os.path.join(HERE, MANIFEST_NAME)
|
||||
if not os.path.exists(manifest):
|
||||
ap.error(f"no --pin given and no manifest at {manifest}")
|
||||
commit, tree, names = names_from_manifest(manifest)
|
||||
|
||||
per_date, per_wizard, excluded, dated = derive(names)
|
||||
tsv, md = render_tsv(per_wizard), render_md(per_date, per_wizard, excluded,
|
||||
commit, tree, dated)
|
||||
print(f"pin commit={commit} tree={tree}")
|
||||
print(f"dated_files={len(dated)} dates={len(per_date)} "
|
||||
f"wizards={len(per_wizard)} excluded={len(excluded)}")
|
||||
for w, n in rows_of(per_wizard):
|
||||
print(f" {w}\t{n}")
|
||||
|
||||
if not args.write:
|
||||
print("dry run (pass --write to write the manifest and tables)")
|
||||
return 0
|
||||
|
||||
os.makedirs(args.outdir, exist_ok=True)
|
||||
manifest_path = os.path.join(args.outdir, MANIFEST_NAME)
|
||||
with open(manifest_path, "w", encoding="utf-8") as fh:
|
||||
fh.write(f"# pin checkins@{commit} tree={tree}\n")
|
||||
fh.write("".join(f"{n}\n" for n in sorted(dated)))
|
||||
for name, payload in (("2026-10-27-checkin-census.tsv", tsv),
|
||||
("2026-10-27-checkin-census.md", md)):
|
||||
with open(os.path.join(args.outdir, name), "w", encoding="utf-8") as fh:
|
||||
fh.write(payload)
|
||||
print(f"wrote {manifest_path}, 2026-10-27-checkin-census.tsv, "
|
||||
f"2026-10-27-checkin-census.md")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
per_date, per_wizard, excluded = extract()
|
||||
write_tables(per_date, per_wizard, excluded)
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user