From 4299095db8342343f1f1b390313126e4b00fe3a5 Mon Sep 17 00:00:00 2001 From: MendixMau Date: Sat, 17 Oct 2026 04:39:45 +0800 Subject: [PATCH 1/2] fix(progress-report): polish the fresh and updated demo reports - "done " in a build-plan.md State cell counts as built in the plan parser, so the Stage 5 card, the plan header and the phase badge agree ("not built" is still checked first). - A placeholder gate-check failure on a stage after the current one reads PENDING instead of FAIL. - The no-session-log notice is one calm line; the regenerate hint moves into the collapsed "How this was measured" block, and --no-log never claims it looked in folders. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + project-bin/build-plan-status.sh | 4 +- project-bin/progress-report.py | 16 ++- .../wave2/test-progress-report-field-fixes.sh | 14 +-- tests/wave2/test-progress-report-polish.sh | 106 ++++++++++++++++++ tests/wave2/test-progress-report.sh | 2 +- 6 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 tests/wave2/test-progress-report-polish.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b72c2b4..bab4ee0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(progress-report): **polish on a fresh and an updated demo project: a "done " plan row counts as built in the card, the plan header and the phase badge alike; a placeholder gate failure on a stage after the current one reads PENDING, not FAIL; the no-session-log notice is one calm line with the regenerate hint inside "How this was measured".** — field review of the #434 report - fix(progress-report): **round 6 of the review: the session cache is machine-local and tied to its project, the first cached run is not slower than a cold one, and the build ledger decides the Stage 4 to 5 switch.** (1) `.claude/progress-report-sessions.txt` holds absolute session-log paths, so `init-project.sh`, `doctor.sh` and `sync-project.sh` now put `/.claude/progress-report-sessions.txt` in the project's `.gitignore`, the way they do for the doctor receipt and `toolkit.env`. (2) The cache header records the project's real path and the attribution rules (25% of tool calls, 20 calls); a copied or moved project, or a changed threshold, reads it as cold, so a copy never lists the original's sessions. (3) Files skipped by the 300-file cap are recorded as `no`, so a cached run never reads more files than a cold one. (4) With rows in `metrics-ledger.tsv` (else BUILD-LOG) the switch comes from the ledger; an earlier exec.sh call or build skill in a session log is ignored, the timed sign-off rule stays, and the footer says how the switch is chosen. Tests: F15 to F17 in `test-progress-report-field-fixes.sh`, new `test-progress-report-gitignore.sh` — review of PR #434 - fix(progress-report): **round 5 of the field test: "building now" marks the phase of the last successful build step (its mdlsource folder, else the script name matched to the plan, else the highest active phase; blocked steps and earlier amber rows do not move it); discovery of sessions from other folders counts only tool calls that operate on the project (at least 25% of a session's calls and at least 20, gaps over 30 minutes cut, sessions that run progress-report.py excluded, text mentions never count) and lists the sessions used in a collapsed element; a Stage 4 sign-off row with a date but no time puts the Stage 5 switch at the first build activity on or after that date (marked inferred) instead of midnight; the exec log is now the primary source for build activity: `.claude/loop/metrics-ledger.tsv` (written by exec.sh, UTC shown as local time) gives the Stage 5 build facts with or without a session log (runs, applied, failed, blocked, restored, exec time, wall span, runs per phase, latest model score, share of exec time lost to failed or restored runs), the last build step, the building-now phase and the Stage 4 to 5 switch, with BUILD-LOG and then session activity as fallbacks; docs/progress/TIME-LEDGER.md is an optional hand-written extra and its reader maps columns by each table's header.** Tests F10 (existing-project fixture) and F11 (older toolkit, then sync-project.sh) added. — MendixMau - fix(progress-report): **field-test round on two real projects: one source of truth for stage state, a plan read from build-plan.md when the JSON is stale, wider log discovery with a ledger fallback, and a Build progress section.** (1) PROJECT.md (the stage line and the Decisions sign-off rows) beats a gate-check page that is older than it: the stage line's `· manual: 5` tail no longer swallows the last passed stage, the counter always equals the list it sits beside ("6 of 8" for P,0,1,2,3,4, plus "N waived"), and a passed stage the page does not list shows an amber "Gate-check page is N days old, re-run gate-check". (2) The report never lists itself as a stage artifact, nor any file outside the project folder. (3) `build-plan.json` older than `build-plan.md` or than the newest `docs/BUILD-LOG.md` row is no longer trusted: row state is read from the md State column by prefix (`not built` first; `built (...)` and `done ...` are built; `partial` and `in progress` are partial; `not needed` rows are not counted) and the page says "plan JSON was stale, counted from build-plan.md". (4) With no log in the project's own folder, discovery also scans other `~/.claude/projects/*` folders (30 days, newest 300 files): a log counts when its cwd is the project or when the project path appears in it at least 5 times, in which case only the stretch from the first to the last mention (padded 30 min) is counted, and the page says so; with no log at all, `docs/progress/TIME-LEDGER.md`, `docs/BUILD-LOG.md` and `.claude/loop/metrics-ledger.tsv` feed a labelled ledger card, and the banner says how to pass `--session-log`. (5) The Stage 4 to 5 switch for time and tokens is the earliest evidenced of the Stage 4 sign-off row in PROJECT.md, the first `exec.sh` call and the build skill, so a stale gate page no longer puts all build work in Stage 4. (6) The build detail section is now "Build progress", linked from the Stage 5 card: stacked built / partial / not built bars per phase, the headline "N of M rows built, K partial", the phase being built now marked, and the last build step with how long ago it ran. Test: `tests/wave2/test-progress-report-field-fixes.sh` (39 checks). — MendixMau diff --git a/project-bin/build-plan-status.sh b/project-bin/build-plan-status.sh index 75876f9d..2846d628 100755 --- a/project-bin/build-plan-status.sh +++ b/project-bin/build-plan-status.sh @@ -303,7 +303,7 @@ def column_map(headers): def classify(state): s = state.lower() if s.startswith("not built"): return "not built" - if s.startswith("built"): return "built" + if s.startswith(("built", "done")): return "built" if s.startswith("pending a person"): return "pending a person" return None @@ -559,7 +559,7 @@ def on_disk(pat): def classify(state): # the parser's rollup reading, per row s = (state or "").lower() if s.startswith("not built"): return "not built" - if s.startswith("built"): return "built" + if s.startswith(("built", "done")): return "built" if s.startswith("pending a person"): return "pending a person" return None diff --git a/project-bin/progress-report.py b/project-bin/progress-report.py index d063cca2..9cb4b203 100755 --- a/project-bin/progress-report.py +++ b/project-bin/progress-report.py @@ -1294,7 +1294,7 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0, .advi{border-left:4px solid var(--warn)}.advi.hi{border-left-color:var(--bad)}.advi .rk{font-size:12px;color:var(--mut)}.advi h3{margin:2px 0 6px}.advi p{margin:4px 0;font-size:13px}.advi .why{color:var(--mut);font-size:12px} details.ph>summary{list-style:none;cursor:pointer;color:var(--ink)}details.ph>summary::-webkit-details-marker{display:none} details.ph[open]{grid-column:1/-1}.steps td{font-size:12px;padding:3px 6px} -.note-card{margin-top:14px;border-left:4px solid var(--warn)}.live{font-size:12px;color:var(--muted)}.live a{color:inherit}details.how{margin:10px 0;font-size:13px;color:var(--muted)}details.how summary{cursor:pointer} +.nolog{margin:10px 0;font-size:13px}.note-card{margin-top:14px;border-left:4px solid var(--warn)}.live{font-size:12px;color:var(--muted)}.live a{color:inherit}details.how{margin:10px 0;font-size:13px;color:var(--muted)}details.how summary{cursor:pointer} .frag section.phase{margin-top:14px}.frag h2{font-size:16px;margin:24px 0 4px}.frag h3{margin:14px 0 2px}.frag .scroll{overflow-x:auto} .frag .lede{font-size:14px}.frag .empty{color:var(--mut)}.frag code{font-size:12px} .frag .st-built,.frag .done,.frag .CLEAN{color:var(--ok)}.frag .st-progress,.frag .in-progress,.frag .INCOMPLETE{color:var(--warn)} @@ -1380,13 +1380,15 @@ def money(c): return "n/a" if unp else "$%.0f" % c if SWITCH.get("inferred"): how_parts.append("

Stage switch inferred. The Stage 4 sign-off row gives a date (%s) without a time, so the switch to Stage 5 is %s, the earliest build activity on or after it (source: %s). Stage 4 time includes that day's planning.

" % (datetime.datetime.fromtimestamp(SWITCH["day"]).strftime("%Y-%m-%d"), local_hm(SWITCH["t"]), esc(SWITCH.get("src", "the build activity")))) if facts: how_parts.append("

Build runs come from %s (UTC in the file, shown in local time); a run counts as passed when the script was applied.

" % esc(facts["src"])) - if how_parts: w("
How this was measured%s
" % "".join(how_parts)) if not L: dsc = DISCOVERY - looked = (" Looked in this project's own log folder and %d other project folders (%d logs from the last %d days)." % (dsc.get("other_dirs", 0), dsc.get("other_files", 0), SCAN_DAYS)) if dsc.get("searched") else "" + looked = (" Looked in this project's own log folder and %d other project folders (%d logs from the last %d days)." % (dsc.get("other_dirs", 0), dsc.get("other_files", 0), SCAN_DAYS)) if dsc.get("searched") and not args.no_log else "" howto = (" To use a log from another place pass it with --session-log <path> (a .jsonl file, or a folder of them, for example ~/.claude/projects/<slug>/), then regenerate: project-bin/build-plan-status.sh <project> --html --session-log <path>.") if not args.no_log else "" ledgers = (" Build runs below come from the exec log (%s); tokens and cost need a session log." % ", ".join("%s" % esc(x) for x in args.ledgers["sources"])) if args.ledgers else "" - w("
Time, token and cost data from a session log are unavailable. %s%s%s%s Everything else (stages, gates, artifacts, decisions, the plan) is shown as usual.
" % (esc(args.log_note), looked, howto, ledgers)) + how_parts.append("

No session log. %s%s%s%s

" % (esc(args.log_note), looked, howto, ledgers)) + if how_parts: w("
How this was measured%s
" % "".join(how_parts)) + if not L: + w("

%s Time, token and cost figures are not shown; everything else is (details in How this was measured).

" % esc(args.log_note)) if args.ledgers and args.ledgers["phases"]: render_ledgers(w, args.ledgers) w("

%s

%d of 8 gates passed%s
" % (info["current"] or "P", esc(info["current_line"] or "Current stage unknown"), npass, (", %d waived" % sum(1 for s_ in STAGES if states[s_] == "waived")) if "waived" in states.values() else "")) w("
" % overall) @@ -1470,7 +1472,11 @@ def money(c): return "n/a" if unp else "$%.0f" % c if states[s] == "passed" and info.get("gate_stale_s") and (not row or row["status"] != "PASS"): age = time.time() - gate["mtime"] w("
Gate-check page is %s old, re-run gate-check
" % (("%d days" % round(age / 86400)) if age >= 86400 else ("%d hours" % max(1, round(age / 3600))))) - elif row: w("
Gate-check page: %s %s
" % (esc(row["detail"]), esc(row["status"]), esc(row["detail"][:110] + ("..." if len(row["detail"]) > 110 else "")))) + elif row: + # a placeholder FAIL on a stage after the current one is just "not started yet": show it as pending, not as a failure + rs = row["status"] + if rs == "FAIL" and "placeholder" in row["detail"].lower() and info["current"] in STAGES and STAGES.index(s) > STAGES.index(info["current"]): rs = "PENDING" + w("
Gate-check page: %s %s
" % (esc(row["detail"]), esc(rs), esc(row["detail"][:110] + ("..." if len(row["detail"]) > 110 else "")))) files = list(arts[s]["files"].items()); prim = [(f, i) for f, i in files if link_kind(f) == "html"]; rest = [(f, i) for f, i in files if link_kind(f) != "html"] def chip(f, cls=""): return "%s" % (cls, href(f), esc(os.path.relpath(f, proj))) diff --git a/tests/wave2/test-progress-report-field-fixes.sh b/tests/wave2/test-progress-report-field-fixes.sh index 8923a645..bf387456 100644 --- a/tests/wave2/test-progress-report-field-fixes.sh +++ b/tests/wave2/test-progress-report-field-fixes.sh @@ -10,7 +10,7 @@ # ("not built" is not built, "built (...)" and "done ..." are built, "partial" is partial) and the page says so. # F4 no log in the project's own slug: found in another slug when 25% of its tool calls and at least 20 operate on the # project path, counted only between those calls; a session that merely mentions the project is ignored. With no log at -# all the time ledger and BUILD-LOG feed the page, labelled, and the banner says how to pass --session-log. +# all the time ledger and BUILD-LOG feed the page, labelled, and the details block says how to pass --session-log. # F5 stage boundaries use the same truth: log events after the Stage 4 sign-off count toward Stage 5 even when the # gate-check page is stale. # F6 Build progress: stacked phase bars, headline "N of M rows built, K partial", the phase being built now is marked, @@ -185,16 +185,16 @@ P1R="$(cd "$P1" && pwd -P)" CFG="$WORK/cfg"; mklog "$CFG" "$P1R" 25 CLAUDE_CONFIG_DIR="$CFG" "$PY" "$GEN" "$P1" --out "$WORK/out4/r.html" >/dev/null 2>&1 has "$WORK/out4/r.html" "Sessions from other folders (1)" "a log in another slug qualifies by tool calls on the project (28 of 28)" -hasnt "$WORK/out4/r.html" "Time, token and cost data from a session log are unavailable" "no unavailable banner when a log is found" +hasnt "$WORK/out4/r.html" "class=nolog" "no unavailable banner when a log is found" hasnt "$WORK/out4/r.html" "13 Oct" "events three days after the last mention are clipped out" CFG2="$WORK/cfg2"; mklog "$CFG2" "$P1R" 10 CLAUDE_CONFIG_DIR="$CFG2" "$PY" "$GEN" "$P1" --out "$WORK/out4/r2.html" >/dev/null 2>&1 -has "$WORK/out4/r2.html" "Time, token and cost data from a session log are unavailable" "13 tool calls on the project are not enough (20 needed): no log" +has "$WORK/out4/r2.html" "class=nolog" "13 tool calls on the project are not enough (20 needed): no log" has "$WORK/out4/r2.html" "docs/progress/TIME-LEDGER.md" "fallback names the time ledger as its source" has "$WORK/out4/r2.html" "docs/BUILD-LOG.md" "fallback names BUILD-LOG as its source" has "$WORK/out4/r2.html" "Time and effort from the project ledgers" "ledger card shown" has "$WORK/out4/r2.html" "136k" "ledger phase row shown (tokens)" -has "$WORK/out4/r2.html" "session-log <path>" "banner says how to pass --session-log" +has "$WORK/out4/r2.html" "session-log <path>" "details block says how to pass --session-log" CLAUDE_CONFIG_DIR="$CFG2" "$PY" "$GEN" "$P1" --out "$WORK/out4/r3.html" --session-log "$CFG2/projects/-tmp-elsewhere/s1.jsonl" >/dev/null 2>&1 hasnt "$WORK/out4/r3.html" "Sessions from other folders" "--session-log replaces discovery" @@ -255,13 +255,13 @@ has "$WORK/out8/a.html" "How this was measured" "the sessions has "$WORK/out8/a.html" "s12026-10-10100%" "list row: short id, date, share of tool calls on the project" CFG8B="$WORK/cfg8b"; FILL=150 mklog "$CFG8B" "$P1R" 25 CLAUDE_CONFIG_DIR="$CFG8B" "$PY" "$GEN" "$P1" --out "$WORK/out8/b.html" >/dev/null 2>&1 -has "$WORK/out8/b.html" "Time, token and cost data from a session log are unavailable" "28 of 178 calls (16%) is under the 25% share: not used" +has "$WORK/out8/b.html" "class=nolog" "28 of 178 calls (16%) is under the 25% share: not used" CFG8C="$WORK/cfg8c"; TEXT=60 mklog "$CFG8C" "$P1R" 10 CLAUDE_CONFIG_DIR="$CFG8C" "$PY" "$GEN" "$P1" --out "$WORK/out8/c.html" >/dev/null 2>&1 -has "$WORK/out8/c.html" "Time, token and cost data from a session log are unavailable" "60 text mentions do not attribute a session" +has "$WORK/out8/c.html" "class=nolog" "60 text mentions do not attribute a session" CFG8D="$WORK/cfg8d"; REPORTRUN=1 mklog "$CFG8D" "$P1R" 25 CLAUDE_CONFIG_DIR="$CFG8D" "$PY" "$GEN" "$P1" --out "$WORK/out8/d.html" >/dev/null 2>&1 -has "$WORK/out8/d.html" "Time, token and cost data from a session log are unavailable" "a session that runs progress-report.py is excluded" +has "$WORK/out8/d.html" "class=nolog" "a session that runs progress-report.py is excluded" CFG8E="$WORK/cfg8e"; mklog "$CFG8E" "$P1R" 25 s1; FILL=100 mklog "$CFG8E" "$P1R" 25 s2 CLAUDE_CONFIG_DIR="$CFG8E" "$PY" "$GEN" "$P1" --out "$WORK/out8/e.html" >/dev/null 2>&1 has "$WORK/out8/e.html" "s1" "two sessions: the qualifying one is listed" diff --git a/tests/wave2/test-progress-report-polish.sh b/tests/wave2/test-progress-report-polish.sh new file mode 100644 index 00000000..5f6ed0bc --- /dev/null +++ b/tests/wave2/test-progress-report-polish.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# test-progress-report-polish.sh: pin three polish fixes to the progress report, on invented demo projects. +# +# P1 a build-plan.md row whose State is "done " counts as built everywhere: the Stage 5 card +# ("N of M rows built"), the plan header ("N built"), the phase badge and the per-phase count agree, and +# nothing is reported as a State cell the page cannot read. "not built" is still checked first. +# P2 a gate-check placeholder failure on a stage AFTER the current one reads PENDING, not FAIL; the same failure +# on the current stage stays FAIL. +# P3 no session log: one calm line (not the orange box), the regenerate hint inside "How this was measured"; +# with --no-log the page never claims it looked in folders or logs. +# +# Usage: bash tests/wave2/test-progress-report-polish.sh + +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCRIPT="$ROOT/project-bin/build-plan-status.sh" +WORK="$(mktemp -d "${TMPDIR:-/tmp}/pr-polish.XXXXXX")" +trap '[ -n "${KEEP:-}" ] || rm -rf "$WORK"' EXIT +. "$ROOT/bin/lib/portable.sh" +PY="$(resolve_py 2>/dev/null || true)" +if [ -z "$PY" ]; then echo "SKIP: no Python 3 found"; echo "PASS=0 FAIL=0"; exit 0; fi +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " PASS: $1"; } +bad() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } +has() { grep -q -- "$2" "$1" && ok "$3" || bad "$3"; } +hasnt() { grep -q -- "$2" "$1" && bad "$3" || ok "$3"; } + +mkproj() { # dir stage-line + mkdir -p "$1/architecture" + printf '# PROJECT.md\n\n## Current stage\n\n%s\n\nEntry mode: requirements-driven\n\n## Decisions\n\n| Stage | Decision | Status | Notes |\n|---|---|---|---|\n' "$2" > "$1/PROJECT.md" +} + +echo "P1 'done ' counts as built everywhere" +P1="$WORK/p1"; mkproj "$P1" "**Stage 5 - Build**, in progress - gates passed: P, 0, 1, 2, 3, 4" +cat > "$P1/architecture/build-plan.md" <<'MD' +# Build plan + +### Phase 1 - Orders + +| # | Kind | Step | Produces | Depends on | State | +|---|---|---|---|---|---| +| 1 | BUILD | orders domain | 01-orders.mdl | - | built (2026-10-01) | + +### Phase 2 - Billing + +| # | Kind | Step | Produces | Depends on | State | +|---|---|---|---|---|---| +| 2 | BUILD | billing domain | 02-billing.mdl | 1 | done 2026-10-12 | +| 3 | BUILD | invoice page | 03-invoice.mdl | 2 | not built | +MD +EMPTY="$WORK/cfg"; mkdir -p "$EMPTY/projects" +CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P1" --html >/dev/null 2>&1 +R="$P1/architecture/progress-report.html" +[ -f "$R" ] && ok "report written" || bad "report written" +has "$R" "2 of 3 rows built" "Stage 5 card counts the done row as built (2 of 3)" +has "$R" "2 built, 1 not built" "plan header agrees: 2 built, 1 not built" +hasnt "$R" "cannot read" "no State cell is called unreadable" +has "$R" "1 of 2 rows built" "Phase 2 shows 1 of 2 built" +hasnt "$R" 'st-unknown">unknown' "no phase is labelled unknown" + +echo "P2 placeholder failure on a later stage is pending" +P2="$WORK/p2"; mkproj "$P2" "**Stage P - Kickoff**, in progress - gates passed: none" +cat > "$P2/index.html" <<'HTML' + + + +
PKickoffFAIL'Confirmed by:' still holds the shipped placeholder: "[user] on [date]"
0TriageFAIL'Confirmed by:' still holds the shipped placeholder: "[user] on [date]"
+HTML +CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P2" --html >/dev/null 2>&1 +R2="$P2/architecture/progress-report.html" +"$PY" - "$R2" <<'PY' && ok "stage 0 (later) reads PENDING, stage P (current) stays FAIL" || bad "stage 0 (later) reads PENDING, stage P (current) stays FAIL" +import re, sys +h = open(sys.argv[1], encoding="utf-8").read() +def line(s): + i = h.index("id='stage-%s'" % s); j = h.find("class='card stage", i + 10) + m = re.search(r"Gate-check page: (\w+)", h[i:j if j > 0 else len(h)]); return m.group(1) +assert line("0") == "PENDING", line("0") +assert line("P") == "FAIL", line("P") +PY + +echo "P3 no-log line is calm and honest" +has "$R" "class=nolog" "one calm line" +hasnt "$R" "class='card note-card'" "no big banner box" +hasnt "$R" "Time, token and cost data from a session log are unavailable" "old banner text is gone" +"$PY" - "$R" <<'PY' && ok "hint sits inside the collapsed How this was measured block, line is outside it" || bad "hint sits inside the collapsed How this was measured block, line is outside it" +import re, sys +h = open(sys.argv[1], encoding="utf-8").read() +m = re.search(r"
.*?
", h, re.S); assert m +assert "No session log" in m.group(0) +assert "class=nolog" not in m.group(0) +PY +P4="$WORK/p4"; mkproj "$P4" "**Stage P - Kickoff**, in progress - gates passed: none" +R4="$WORK/r4.html"; "$PY" "$ROOT/project-bin/progress-report.py" "$P4" --no-log --out "$R4" >/dev/null 2>&1 +has "$R4" "class=nolog" "--no-log: the calm line is shown" +has "$R4" "Session logs were not read" "--no-log says the logs were not read" +hasnt "$R4" "Looked in" "--no-log never claims it looked in folders" +hasnt "$R4" "then regenerate: project-bin" "--no-log gives no regenerate hint for a log it never read" +P3="$WORK/p3"; mkproj "$P3" "**Stage P - Kickoff**, in progress - gates passed: none" +CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P3" --html >/dev/null 2>&1 +R3="$P3/architecture/progress-report.html" +has "$R3" "class=nolog" "without --no-log the calm line is also shown" +has "$R3" "Looked in" "without --no-log the looked-in counts live inside the details" +has "$R3" "then regenerate: project-bin/build-plan-status.sh" "regenerate hint present inside the details" + +echo "PASS=$PASS FAIL=$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/tests/wave2/test-progress-report.sh b/tests/wave2/test-progress-report.sh index faf7ea2e..119fe4fc 100755 --- a/tests/wave2/test-progress-report.sh +++ b/tests/wave2/test-progress-report.sh @@ -115,7 +115,7 @@ CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P1" --html >/dev/null 2>&1 R1="$P1/architecture/progress-report.html" [ -f "$R1" ] && ok "progress-report.html written" || bad "progress-report.html written" has "$R1" "Orders" "plan content present" -has "$R1" "unavailable" "says time and token data are unavailable" +has "$R1" "Time, token and cost figures are not shown" "says time and token data are not shown" hasnt "$R1" 'id=tips' "no Tips section without a log" echo "T2-T6 with a session log" From 98649f93ff97317748fd0bf8204b4dfbd2864950 Mon Sep 17 00:00:00 2001 From: MendixMau Date: Sat, 17 Oct 2026 04:47:36 +0800 Subject: [PATCH 2/2] fix(progress-report): test-moments wording names the CAC-5 unit-test choice Module done runs the per-flow unit tests chosen at CAC-5 (listed in the module brief), not "unit tests when switched on". Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- project-bin/progress-report.py | 2 +- tests/wave2/test-progress-report-polish.sh | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bab4ee0b..1fcea574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- fix(progress-report): **polish on a fresh and an updated demo project: a "done " plan row counts as built in the card, the plan header and the phase badge alike; a placeholder gate failure on a stage after the current one reads PENDING, not FAIL; the no-session-log notice is one calm line with the regenerate hint inside "How this was measured".** — field review of the #434 report +- fix(progress-report): **polish on a fresh and an updated demo project: a "done " plan row counts as built in the card, the plan header and the phase badge alike; a placeholder gate failure on a stage after the current one reads PENDING, not FAIL; the no-session-log notice is one calm line with the regenerate hint inside "How this was measured"; the test-moments table says unit tests run for the flows chosen at CAC-5, not "when switched on".** — field review of the #434 report - fix(progress-report): **round 6 of the review: the session cache is machine-local and tied to its project, the first cached run is not slower than a cold one, and the build ledger decides the Stage 4 to 5 switch.** (1) `.claude/progress-report-sessions.txt` holds absolute session-log paths, so `init-project.sh`, `doctor.sh` and `sync-project.sh` now put `/.claude/progress-report-sessions.txt` in the project's `.gitignore`, the way they do for the doctor receipt and `toolkit.env`. (2) The cache header records the project's real path and the attribution rules (25% of tool calls, 20 calls); a copied or moved project, or a changed threshold, reads it as cold, so a copy never lists the original's sessions. (3) Files skipped by the 300-file cap are recorded as `no`, so a cached run never reads more files than a cold one. (4) With rows in `metrics-ledger.tsv` (else BUILD-LOG) the switch comes from the ledger; an earlier exec.sh call or build skill in a session log is ignored, the timed sign-off rule stays, and the footer says how the switch is chosen. Tests: F15 to F17 in `test-progress-report-field-fixes.sh`, new `test-progress-report-gitignore.sh` — review of PR #434 - fix(progress-report): **round 5 of the field test: "building now" marks the phase of the last successful build step (its mdlsource folder, else the script name matched to the plan, else the highest active phase; blocked steps and earlier amber rows do not move it); discovery of sessions from other folders counts only tool calls that operate on the project (at least 25% of a session's calls and at least 20, gaps over 30 minutes cut, sessions that run progress-report.py excluded, text mentions never count) and lists the sessions used in a collapsed element; a Stage 4 sign-off row with a date but no time puts the Stage 5 switch at the first build activity on or after that date (marked inferred) instead of midnight; the exec log is now the primary source for build activity: `.claude/loop/metrics-ledger.tsv` (written by exec.sh, UTC shown as local time) gives the Stage 5 build facts with or without a session log (runs, applied, failed, blocked, restored, exec time, wall span, runs per phase, latest model score, share of exec time lost to failed or restored runs), the last build step, the building-now phase and the Stage 4 to 5 switch, with BUILD-LOG and then session activity as fallbacks; docs/progress/TIME-LEDGER.md is an optional hand-written extra and its reader maps columns by each table's header.** Tests F10 (existing-project fixture) and F11 (older toolkit, then sync-project.sh) added. — MendixMau - fix(progress-report): **field-test round on two real projects: one source of truth for stage state, a plan read from build-plan.md when the JSON is stale, wider log discovery with a ledger fallback, and a Build progress section.** (1) PROJECT.md (the stage line and the Decisions sign-off rows) beats a gate-check page that is older than it: the stage line's `· manual: 5` tail no longer swallows the last passed stage, the counter always equals the list it sits beside ("6 of 8" for P,0,1,2,3,4, plus "N waived"), and a passed stage the page does not list shows an amber "Gate-check page is N days old, re-run gate-check". (2) The report never lists itself as a stage artifact, nor any file outside the project folder. (3) `build-plan.json` older than `build-plan.md` or than the newest `docs/BUILD-LOG.md` row is no longer trusted: row state is read from the md State column by prefix (`not built` first; `built (...)` and `done ...` are built; `partial` and `in progress` are partial; `not needed` rows are not counted) and the page says "plan JSON was stale, counted from build-plan.md". (4) With no log in the project's own folder, discovery also scans other `~/.claude/projects/*` folders (30 days, newest 300 files): a log counts when its cwd is the project or when the project path appears in it at least 5 times, in which case only the stretch from the first to the last mention (padded 30 min) is counted, and the page says so; with no log at all, `docs/progress/TIME-LEDGER.md`, `docs/BUILD-LOG.md` and `.claude/loop/metrics-ledger.tsv` feed a labelled ledger card, and the banner says how to pass `--session-log`. (5) The Stage 4 to 5 switch for time and tokens is the earliest evidenced of the Stage 4 sign-off row in PROJECT.md, the first `exec.sh` call and the build skill, so a stale gate page no longer puts all build work in Stage 4. (6) The build detail section is now "Build progress", linked from the Stage 5 card: stacked built / partial / not built bars per phase, the headline "N of M rows built, K partial", the phase being built now marked, and the last build step with how long ago it ran. Test: `tests/wave2/test-progress-report-field-fixes.sh` (39 checks). — MendixMau diff --git a/project-bin/progress-report.py b/project-bin/progress-report.py index 9cb4b203..3051b750 100755 --- a/project-bin/progress-report.py +++ b/project-bin/progress-report.py @@ -114,7 +114,7 @@ def context_window(msgs, compacts=(), flag_1m=False): if nxt is None: return None, "a context of %s exceeds every known window" % fmt_tok(obs) return nxt, "inferred: the log shows %s and no 1M marker or compaction" % fmt_tok(obs) TEST_MOMENTS = [("Every change", "Gate check plus only the tests that touch the change"), - ("Module done", "Module journeys, plus unit tests when the project has them switched on"), + ("Module done", "Module journeys, plus the per-flow unit tests chosen at CAC-5 (listed in the module brief)"), ("Build complete", "Everything, once"), ("Before every later deploy", "Everything"), ("User asks", "Everything")] diff --git a/tests/wave2/test-progress-report-polish.sh b/tests/wave2/test-progress-report-polish.sh index 5f6ed0bc..e33de53c 100644 --- a/tests/wave2/test-progress-report-polish.sh +++ b/tests/wave2/test-progress-report-polish.sh @@ -102,5 +102,10 @@ has "$R3" "class=nolog" "without --no-log the calm line is also shown" has "$R3" "Looked in" "without --no-log the looked-in counts live inside the details" has "$R3" "then regenerate: project-bin/build-plan-status.sh" "regenerate hint present inside the details" +echo "P4 test-moments wording (the table is defined in the generator)" +GENPY="$ROOT/project-bin/progress-report.py" +has "$GENPY" "per-flow unit tests chosen at CAC-5" "unit tests run for the flows chosen at CAC-5" +hasnt "$GENPY" "switched on" "the old 'switched on' wording is gone" + echo "PASS=$PASS FAIL=$FAIL" [ "$FAIL" -eq 0 ]