CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""trials — what the players said, and where (CB-WP-0027 T04).
|
|
|
|
|
|
|
|
|
|
**Surfacing is the deliverable, not storage.** ADR-0014 D6: a commentary
|
|
|
|
|
feature that stores comments and shows them nowhere would be this
|
|
|
|
|
project's signature failure in a new medium — after the message that sat
|
|
|
|
|
unread for four days and the ten rulings that arrived and were never
|
|
|
|
|
collected.
|
|
|
|
|
|
|
|
|
|
So this reports every note from every trial log, with its position and its
|
|
|
|
|
age, and flags the ones whose position can no longer be reached.
|
|
|
|
|
|
|
|
|
|
Reuses `design.py`'s parsing shape because the trial log deliberately
|
|
|
|
|
reuses `FindingRegister.md`'s idiom: a table between HTML-comment markers
|
|
|
|
|
inside a readable Markdown file.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os, re, sys, glob, datetime
|
|
|
|
|
|
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
TRIALS = os.path.join(ROOT, "trials")
|
|
|
|
|
|
|
|
|
|
BEGIN = "<!-- trial-log:begin -->"
|
|
|
|
|
END = "<!-- trial-log:end -->"
|
|
|
|
|
|
|
|
|
|
# GameDesign §3.1's figure, shared rather than reinvented: a note that
|
|
|
|
|
# cannot expire ages into an apparent finding.
|
|
|
|
|
NOTE_EXPIRY_DAYS = 30
|
|
|
|
|
|
2026-08-07 16:09:36 +02:00
|
|
|
# The trial-log table's columns, named once. `write_trial_log` in
|
|
|
|
|
# `hotseat.rs` writes them; this reads them; a row that is not this shape
|
|
|
|
|
# is an error rather than a row to skip.
|
|
|
|
|
COLUMNS = ("n", "round", "step", "state_hash", "comment")
|
|
|
|
|
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
|
|
|
|
def parse(text):
|
|
|
|
|
"""Rows between the markers. A log without the block is an error, not
|
|
|
|
|
an empty log — silently reporting zero notes for a file full of them
|
|
|
|
|
is precisely the failure this tool exists to prevent."""
|
|
|
|
|
if BEGIN not in text or END not in text:
|
|
|
|
|
raise ValueError("no trial-log block")
|
|
|
|
|
block = text.split(BEGIN)[1].split(END)[0]
|
|
|
|
|
rows = []
|
|
|
|
|
for line in block.splitlines():
|
|
|
|
|
line = line.strip()
|
|
|
|
|
if not line.startswith("|") or line.startswith("|---"):
|
|
|
|
|
continue
|
|
|
|
|
cells = [c.strip() for c in line.strip("|").split("|")]
|
2026-08-07 16:09:36 +02:00
|
|
|
if cells[0] == "n":
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
continue
|
2026-08-07 16:09:36 +02:00
|
|
|
# CB-WP-0032. A wrong column count RAISES; it used to `continue`.
|
|
|
|
|
#
|
|
|
|
|
# The docstring above already says silently reporting zero notes
|
|
|
|
|
# for a file full of them is the failure this tool exists to
|
|
|
|
|
# prevent -- and a `continue` here did exactly that for every row
|
|
|
|
|
# at once. Adding a sixth column to the trial log (which CB-WP-0032
|
|
|
|
|
# considered, and did not do) would have emptied every existing
|
|
|
|
|
# log without a word.
|
|
|
|
|
if len(cells) != len(COLUMNS):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"trial-log row has {len(cells)} columns, expected {len(COLUMNS)}: {line!r}"
|
|
|
|
|
)
|
|
|
|
|
rows.append(dict(zip(COLUMNS, cells)))
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
return rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def logs(root=TRIALS):
|
|
|
|
|
"""Every trial log, with its recording beside it if there is one."""
|
|
|
|
|
out = []
|
|
|
|
|
for path in sorted(glob.glob(os.path.join(root, "*.md"))):
|
|
|
|
|
try:
|
|
|
|
|
rows = parse(open(path).read())
|
2026-08-07 16:09:36 +02:00
|
|
|
except ValueError as e:
|
|
|
|
|
# The REASON, not a guess at it. This said "no trial-log block
|
|
|
|
|
# -- not a trial log?" for every failure, which is now wrong
|
|
|
|
|
# for two of them: a malformed row is not a missing block, and
|
|
|
|
|
# telling a maintainer to check for the wrong thing costs more
|
|
|
|
|
# than saying nothing.
|
|
|
|
|
print(f" WARN {os.path.basename(path)}: {e}")
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
continue
|
|
|
|
|
recording = os.path.splitext(path)[0] + ".yaml"
|
|
|
|
|
out.append((path, rows, recording if os.path.exists(recording) else None))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reachability(row, recording):
|
|
|
|
|
"""Can this note's position be reached? One of three answers.
|
|
|
|
|
|
|
|
|
|
**"No recording" and "the hash is not in the recording" are different
|
|
|
|
|
states and must not be one number.** A note written mid-game is
|
|
|
|
|
pending — the recording is only written when the game ends — while a
|
|
|
|
|
note whose hash is absent from a recording that *exists* means the
|
|
|
|
|
position moved. Collapsing them makes every live session report
|
|
|
|
|
orphans, and a metric that cries wolf is one nobody reads, which is
|
|
|
|
|
the failure this whole pass is about.
|
|
|
|
|
|
|
|
|
|
Orphans are reported, never deleted (ADR-0014 D3): a moved position is
|
|
|
|
|
worth knowing, by the same reasoning that rewrote `gr-e01` rather than
|
|
|
|
|
retiring it.
|
|
|
|
|
"""
|
|
|
|
|
if not recording:
|
|
|
|
|
return "pending"
|
|
|
|
|
return "ok" if row["state_hash"] in open(recording).read() else "orphan"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def age_days(path, today):
|
|
|
|
|
"""Dated from the log's filename (`YYYY-MM-DD-slug.md`), falling back
|
|
|
|
|
to mtime — the name is the intent, the mtime is what happened."""
|
|
|
|
|
m = re.match(r"(\d{4}-\d{2}-\d{2})", os.path.basename(path))
|
|
|
|
|
if m:
|
|
|
|
|
try:
|
|
|
|
|
return (today - datetime.date.fromisoformat(m.group(1))).days
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
return (today - datetime.date.fromtimestamp(os.path.getmtime(path))).days
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def report(root=TRIALS, today=None):
|
|
|
|
|
today = today or datetime.date.today()
|
|
|
|
|
if not os.path.isdir(root):
|
|
|
|
|
print("trials — no trials/ directory yet\n")
|
|
|
|
|
print(" Play one: cb-play --serve 0 --record trials/<date>-<slug>.yaml \\")
|
|
|
|
|
print(" --trial trials/<date>-<slug>.md")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
found = logs(root)
|
|
|
|
|
print("trials — what the players said, and where\n")
|
|
|
|
|
total, orphans, pending, expired = 0, 0, 0, 0
|
|
|
|
|
for path, rows, recording in found:
|
|
|
|
|
name = os.path.basename(path)
|
|
|
|
|
age = age_days(path, today)
|
|
|
|
|
print(f" {name} ({len(rows)} note(s), {age}d"
|
|
|
|
|
f"{', recording not written yet' if not recording else ''})")
|
|
|
|
|
for r in rows:
|
|
|
|
|
total += 1
|
|
|
|
|
state = reachability(r, recording)
|
|
|
|
|
orphans += state == "orphan"
|
|
|
|
|
pending += state == "pending"
|
|
|
|
|
expired += age > NOTE_EXPIRY_DAYS
|
|
|
|
|
mark = {"orphan": "orphan", "pending": " .. ", "ok": " "}[state]
|
|
|
|
|
print(f" {mark} r{r['round']:<2} {r['step']:<8} {r['state_hash']:<12} "
|
|
|
|
|
f"{r['comment'][:70]}")
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
print(f" trial logs {len(found)}")
|
|
|
|
|
print(f" notes {total}")
|
|
|
|
|
print(f" positions unreachable {orphans} target 0"
|
|
|
|
|
+ (" <-- the recording exists but the position moved" if orphans else ""))
|
|
|
|
|
if pending:
|
|
|
|
|
print(f" awaiting a recording {pending}"
|
|
|
|
|
" (normal during a live session; the recording lands at game end)")
|
|
|
|
|
print(f" notes past {NOTE_EXPIRY_DAYS} days {expired} target 0")
|
|
|
|
|
if total and not orphans:
|
|
|
|
|
print("\n Every note points at a position a reader can reach.")
|
|
|
|
|
print("\n Raw notes stay in this repo (ADR-0014 D4). A note reaches"
|
|
|
|
|
"\n ground-game only by being promoted to a register finding, by a"
|
|
|
|
|
"\n human, with the wording chosen then.")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def self_test():
|
|
|
|
|
"""Positive controls, including for the REPORTING path.
|
|
|
|
|
|
|
|
|
|
`design-baseline.py` had a green self-test and an unexercised
|
|
|
|
|
reporting path, and the reporting path is where it rotted (ADR-0012
|
|
|
|
|
D8). So this runs `report` against a fixture and checks what it says.
|
|
|
|
|
"""
|
|
|
|
|
import tempfile, io, contextlib
|
|
|
|
|
|
|
|
|
|
ok = True
|
|
|
|
|
|
|
|
|
|
def check(name, cond, detail=""):
|
|
|
|
|
nonlocal ok
|
|
|
|
|
ok = ok and bool(cond)
|
|
|
|
|
print(f" [{'ok ' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
|
|
|
|
|
|
|
|
|
|
good = (f"# Trial log\n\n{BEGIN}\n\n"
|
|
|
|
|
"| n | round | step | state_hash | comment |\n|---|---|---|---|---|\n"
|
|
|
|
|
"| 1 | 3 | Select | abc123def456 | why is SOLVE doing nothing |\n"
|
|
|
|
|
f"\n{END}\n")
|
|
|
|
|
check("a trial log parses", len(parse(good)) == 1)
|
|
|
|
|
check("the header row is not a note", all(r["n"] != "n" for r in parse(good)))
|
|
|
|
|
|
2026-08-07 16:09:36 +02:00
|
|
|
# CB-WP-0032. A row of the wrong shape must be LOUD. This used to
|
|
|
|
|
# `continue`, so a format change would have reported zero notes for
|
|
|
|
|
# every log at once -- the failure the parser's own docstring names.
|
|
|
|
|
six = good.replace("| 1 | 3 | Select | abc123def456 | why is SOLVE doing nothing |",
|
|
|
|
|
"| 1 | 3 | Select | abc123def456 | extra | why is SOLVE doing nothing |")
|
|
|
|
|
try:
|
|
|
|
|
parse(six)
|
|
|
|
|
check("a row with the wrong column count is an error", False)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
check("a row with the wrong column count is an error", "6 columns" in str(e),
|
|
|
|
|
"a silent skip would empty every log at once")
|
|
|
|
|
|
|
|
|
|
# And the surrounding walk must report the REASON it failed.
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
with open(os.path.join(d, "2026-08-07-bad.md"), "w") as fh:
|
|
|
|
|
fh.write(six)
|
|
|
|
|
buf = io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(buf):
|
|
|
|
|
logs(d)
|
|
|
|
|
check("the walk names the real reason", "6 columns" in buf.getvalue(),
|
|
|
|
|
"it used to blame a missing block for every failure")
|
|
|
|
|
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
try:
|
|
|
|
|
parse("# Trial log\n\nno block here\n")
|
|
|
|
|
check("a file with no block is an error, not an empty log", False)
|
|
|
|
|
except ValueError:
|
|
|
|
|
check("a file with no block is an error, not an empty log", True,
|
|
|
|
|
"silently reporting zero is the failure this tool prevents")
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
open(os.path.join(d, "2026-08-06-x.md"), "w").write(good)
|
|
|
|
|
# No .yaml beside it → the position cannot be reached.
|
|
|
|
|
rows = parse(good)
|
|
|
|
|
check("a note with no recording is PENDING, not orphaned",
|
|
|
|
|
reachability(rows[0], None) == "pending",
|
|
|
|
|
"a live session must not report orphans")
|
|
|
|
|
rec = os.path.join(d, "2026-08-06-x.yaml")
|
|
|
|
|
open(rec, "w").write("state_hash: abc123def456\n")
|
|
|
|
|
check("a note whose hash is in the recording is reachable",
|
|
|
|
|
reachability(rows[0], rec) == "ok",
|
|
|
|
|
"without this the check could always say orphan")
|
|
|
|
|
open(rec, "w").write("state_hash: something-else\n")
|
|
|
|
|
check("a note whose hash is absent from a REAL recording is an orphan",
|
|
|
|
|
reachability(rows[0], rec) == "orphan",
|
|
|
|
|
"the position moved \u2014 the case the flag exists for")
|
|
|
|
|
|
|
|
|
|
# THE control design-baseline.py lacked: exercise the reporting
|
|
|
|
|
# path and assert on what it printed.
|
|
|
|
|
buf = io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(buf):
|
|
|
|
|
report(d, datetime.date(2026, 8, 6))
|
|
|
|
|
out = buf.getvalue()
|
|
|
|
|
check("the reporting path runs and names the note",
|
|
|
|
|
"why is SOLVE doing nothing" in out, "not just the parser")
|
|
|
|
|
check("the reporting path counts", "notes 1" in out)
|
|
|
|
|
|
|
|
|
|
print("trials self-test (positive control)")
|
|
|
|
|
return 0 if ok else 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(self_test() if "--self-test" in sys.argv else report())
|