From 010cee2a27b16891630d51cc00c0183cb97a19cd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 05:03:07 +0000 Subject: [PATCH 1/3] fix(progress-report): show the build plan once, in the phase cards; open the now phase (#474) Phase cards now carry the full row detail (Produces/Proves, Depends on, Scripts, Claims) from build-plan-status.sh via --phase-detail; the duplicate 'Full plan tables' block is removed and views A and B stay as their own section. The now phase renders open + selected; phase cards behave as an accordion; #phase-N links and saved reload state still work. Field proof: no real project available in this session; ran against a fixture architecture/build-plan.md (2 phases, 4 rows, claims). grep -c '12' = 1, '3' = 1. Chromium at 375px: now phase open on load, click opens/closes previous, #phase-2 hash opens it, no horizontal scroll. Fixtures run: test-progress-report{,-polish,-pages,-field-fixes,-gitignore}, test-build-plan-json, test-build-plan-status-args, test-windows-msys-paths (all pass). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XruEA24qrAkK7Zqxc5Z59E --- CHANGELOG.md | 2 ++ project-bin/build-plan-status.sh | 42 ++++++++++++---------- project-bin/progress-report.py | 35 +++++++++--------- tests/wave2/test-progress-report-polish.sh | 19 ++++++---- 4 files changed, 56 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..64add8d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(progress-report): **the build plan is shown once: the phase cards are the plan.** Expanding a card now shows its full rows (#, Kind, Step, Produces/Proves, Depends on, State, Scripts) and Claims, which only the duplicate "Full plan tables" block carried; that block is gone and views A (script folders) and B (module proof) are their own section. The `now` phase renders open with a selected style, opening another phase closes the previous one (accordion; `#phase-N` links still open their card), and a viewer's saved open/closed state survives the 30 s reload and wins over the default (#474) — field feedback from a requirements-driven project at Stage 5 + ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-bin/build-plan-status.sh b/project-bin/build-plan-status.sh index b9530361..fd115d4f 100755 --- a/project-bin/build-plan-status.sh +++ b/project-bin/build-plan-status.sh @@ -532,10 +532,10 @@ render_plan_section() { # stdout: the plan section's HTML echo "

The plan

Nothing to show yet: ${PLAN_MSG:-build-plan.md was not read}.

" return 0 fi - "$PY" - "$PLAN_JSON" "$MDLSOURCE" <<'PYEOF' || echo "

The plan

Could not render the plan section from build-plan.md.

" + "$PY" - "$PLAN_JSON" "$MDLSOURCE" "${PHASE_DETAIL:-/dev/null}" <<'PYEOF' || echo "

The plan

Could not render the plan section from build-plan.md.

" import fnmatch, html, json, os, re, sys -src, mdl = sys.argv[1], sys.argv[2] +src, mdl, detail_out = sys.argv[1], sys.argv[2], sys.argv[3] d = json.load(open(src, encoding="utf-8")) e = lambda s: html.escape(s or "", quote=True) # `code` spans in plan cells render as , not literal backticks @@ -631,29 +631,32 @@ if named_total: print("

Scripts named in the plan: %d, of which %d are renamed done- under mdlsource/.%s

" % ( named_total, named_done, (" ⚠ %s where the plan and the disk disagree." % plural(drift_rows, "row")) if drift_rows else "")) +# One view of the plan: each phase's rows and claims go to the detail file, which progress-report.py +# splices into that phase's card. They are not printed here, or the rows would appear twice. +det = [] for p, out in zip(phases, rows_html): - print('

Phase %s — %s %s

' % ( - e(p["id"]), e(p["name"]), CLS.get(p["state"], "st-unknown"), e(p["state"]))) + h = ['' % e(p["id"]), + '

Phase state: %s

' % (CLS.get(p["state"], "st-unknown"), e(p["state"]))] if p.get("note"): - print("

%s

" % e(p["note"])) + h.append("

%s

" % e(p["note"])) if p["steps"]: - b = sum(1 for s in p["steps"] if classify(s["state"]) == "built") - print("

%d of %s built

" % (b, plural(len(p["steps"]), "row"))) - print("
" - "") - print("\n".join(out)) - print("
#KindStepProduces / ProvesDepends onStateScripts
") + h.append("
" + "") + h.append("\n".join(out)) + h.append("
#KindStepProduces / ProvesDepends onStateScripts
") else: - print("

No row table under this phase.

") + h.append("

No row table under this phase.

") if p.get("claims"): - print("
Claims (%d)
    " % len(p["claims"])) + h.append("
    Claims (%d)
      " % len(p["claims"])) for c in p["claims"]: extra = (" (%d)" % c["count"] if c.get("count") is not None else "") + (" [%s]" % c["brd"] if c.get("brd") else "") - print("
    • %s%s
    • " % (e(c["pointer"]), e(extra))) - print("
    ") + h.append("
  • %s%s
  • " % (e(c["pointer"]), e(extra))) + h.append("
") elif p.get("claimsNote"): - print("

Claims: %s

" % e(p["claimsNote"])) - print("
") + h.append("

Claims: %s

" % e(p["claimsNote"])) + h.append('') + det.append("\n".join(h)) +open(detail_out, "w", encoding="utf-8").write("\n".join(det)) w = d.get("warnings") or [] if w: print("
Parser notes (%d): lines in build-plan.md this page could not read as plan data
    " % len(w)) @@ -694,6 +697,7 @@ if [ "$WRITE_HTML" -eq 1 ]; then mkdir -p "$ROOT/architecture" OUT_TMP="$OUT.tmp.$$" FRAG_TMP="$OUT.frag.$$" + PHASE_DETAIL="$OUT.phases.$$" { render_plan_section @@ -725,7 +729,7 @@ if [ "$WRITE_HTML" -eq 1 ]; then fi echo "

    The plan: what build-plan.md says. A: built, from mdlsource/ done- prefixes. B: proven, from verify-module.sh + docs/improvement-register.md. A phase at 100% with no reviewed row in B is built, not proven.

    " } > "$FRAG_TMP" - GEN_ARGS=(--out "$OUT_TMP" --fragment "$FRAG_TMP" --plan-msg "${PLAN_MSG:-}") + GEN_ARGS=(--out "$OUT_TMP" --fragment "$FRAG_TMP" --phase-detail "$PHASE_DETAIL" --plan-msg "${PLAN_MSG:-}") [ -n "$PLAN_JSON" ] && GEN_ARGS+=(--plan-json "$PLAN_JSON") for _sl in ${SESSION_LOGS[@]+"${SESSION_LOGS[@]}"}; do GEN_ARGS+=(--session-log "$_sl"); done if "$PY" "$GEN" "$ROOT" "${GEN_ARGS[@]}" >/dev/null 2>"$OUT.err.$$"; then @@ -744,7 +748,7 @@ if [ "$WRITE_HTML" -eq 1 ]; then if [ "$QUIET" -eq 0 ]; then echo "" && echo " $REPORT_FILE not written: the generator failed: $(head -c 300 "$OUT.err.$$" 2>/dev/null)" else echo "WARN: $REPORT_FILE not updated, progress-report.py failed: ${_why:-no message}" >&2; fi # unattended callers stay quiet on success, never on a crash fi - rm -f "${FRAG_TMP:?}" "${OUT}.err.$$" + rm -f "${FRAG_TMP:?}" "${PHASE_DETAIL:?}" "${OUT}.err.$$" fi fi diff --git a/project-bin/progress-report.py b/project-bin/progress-report.py index 88b51bda..9e424353 100755 --- a/project-bin/progress-report.py +++ b/project-bin/progress-report.py @@ -1392,7 +1392,7 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0, health = run_health(cs) return fired[:ADV["max_items"]], quiet + fired[ADV["max_items"]:], health, cs, hidden[0] -JS = "" +JS = "" # ----------------------------------------------------------------- render CSS = """ :root{--bg:#f6f7f9;--card:#fff;--ink:#1c2430;--mut:#667085;--line:#e3e7ee;--accent:#2f6fed;--ok:#1f9d63;--warn:#d98a0b;--bad:#c93c3c;--idle:#d9dde5;--main:#2f6fed;--sub:#8b5cf6;--soft:#eef2fb;--shadow:0 1px 2px rgba(20,30,50,.06)} @@ -1441,14 +1441,14 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0, .layout{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:16px;align-items:start}.layout.solo{display:block}.content{min-width:0}.side{position:sticky;top:52px;max-height:calc(100vh - 64px);overflow:auto;padding-right:2px}.side h2{margin-top:0}.health{margin-top:8px}.health .hi{font-size:13px;margin-top:3px}.grp{margin-top:8px;border:1px solid var(--line);border-radius:10px;background:var(--card)}.grp>summary{cursor:pointer;padding:9px 12px;font-size:14px}.grp .speed,.grp>.split,.grp>div,.grp .tip{padding:0 12px 10px}.tip{border-top:1px solid var(--line);padding-top:10px}.tip h3{font-size:13px;margin:0 0 4px}.tip p{margin:0 0 4px;font-size:12.5px}.p-info{background:#8793a6;color:#fff}@media(max-width:1000px){.layout{grid-template-columns:minmax(0,1fr)}.side{position:static;max-height:none}}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:4px}.p-green{background:#2e9e5b;color:#fff}.p-amber{background:#e0a100;color:#fff}.p-red{background:#d1383d;color:#fff}.pill{padding:1px 9px;margin-left:6px;border-radius:99px;font-size:12px}.speed{margin-top:8px;font-size:13px}.split{display:flex;gap:2px;margin:6px 0}.seg{background:var(--accent);color:#fff;font-size:11px;padding:3px 6px;border-radius:4px;white-space:nowrap;overflow:hidden}.seg:nth-child(2){opacity:.75}.seg:nth-child(3){background:var(--idle);color:var(--ink)}.seg:nth-child(4){opacity:.55}.muted{color:var(--mut);font-size:12px}\n.advl{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:10px;margin-top:8px} .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} +details.ph[open]{grid-column:1/-1;border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);background:var(--soft)}.steps td{font-size:12px;padding:3px 6px} .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)} -.frag .st-todo,.frag .pending{color:var(--mut)}.frag .st-person{color:#8b5cf6}.frag .st-unknown{color:var(--mut);font-style:italic} -.frag .FINDINGS,.frag .drift{color:var(--bad)}.frag .drift{font-size:12px}.frag .claims{font-size:12px}.frag .warn summary{color:var(--warn)} -.frag .badge{margin-left:6px;border:1px solid currentColor;background:transparent} +.frag .st-built,.ph .st-built,.frag .done,.frag .CLEAN{color:var(--ok)}.frag .st-progress,.ph .st-progress,.frag .in-progress,.frag .INCOMPLETE{color:var(--warn)} +.frag .st-todo,.ph .st-todo,.frag .pending{color:var(--mut)}.frag .st-person,.ph .st-person{color:#8b5cf6}.frag .st-unknown,.ph .st-unknown{color:var(--mut);font-style:italic} +.frag .FINDINGS,.frag .drift,.ph .drift{color:var(--bad)}.frag .drift,.ph .drift{font-size:12px}.ph .scroll{overflow-x:auto}.ph .claims{font-size:12px}.frag .claims{font-size:12px}.frag .warn summary{color:var(--warn)} +.frag .badge,.ph .badge{margin-left:6px;border:1px solid currentColor;background:transparent} .pages .tree{margin-top:6px;font-size:13px}.pg{display:grid;grid-template-columns:minmax(120px,220px) minmax(0,1fr);gap:2px 12px;padding:5px 0;border-bottom:1px solid var(--line)}.pg:last-child{border-bottom:0} .pg .dir{font-weight:600;overflow-wrap:anywhere}.pg ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:3px 14px}.pg li{overflow-wrap:anywhere}.pg details{margin:0}.pg details ul{margin-top:4px} footer{margin-top:36px;font-size:13px;color:var(--mut)}footer li{margin:3px 0} @@ -1739,8 +1739,8 @@ def chip(f, cls=""): if bp: kinds = bp["kinds"]; tr = "" runs_by = {str(k_[0]): d["runs"] for k_, d in (facts["phases"] if facts else []) if k_[0] != ""} - cur = now_phase(bp, proj) - w("
    %d of %d rows built%sJump to the plan tables
    " % + cur = now_phase(bp, proj); phase_detail = args.phase_detail + w("
    " % (bp["built"], bp["total"], (", %d partial" % bp["partial"]) if bp["partial"] else "", pct(bp["built"], bp["total"]), pct(bp["partial"], bp["total"]))) w("
    built %dpartial %dnot built %d%s
    " % (bp["built"], bp["partial"], bp["todo"], ("not needed %d (not counted)" % bp["skip"]) if bp["skip"] else "")) w("
    " + "".join("" % (k, v, pct(v, bp["total"]), KC.get(k, "#888")) for k, v in kinds.items()) + "
    ") @@ -1753,16 +1753,18 @@ def chip(f, cls=""): w("
    ") for p in bp["phases"]: now = p is cur - w("
    Phase %s: %s%s
    %d of %d rows built%s%s
    %s
    module files: %s
    Click to show the rows
    " % - (" now" if now else "", esc(p["id"]), esc(p["id"]), esc(p["name"]), " building now" if now else "", pct(p["built"], p["n"]), pct(p["partial"], p["n"]), p["built"], p["n"], (", %d partial" % p["partial"]) if p["partial"] else "", (" · %d runs" % runs_by.get(str(p["id"]), 0)) if runs_by.get(str(p["id"])) else "", esc(" ".join("%s %d" % kv for kv in p["kinds"].items())), + w("
    Phase %s: %s%s
    %d of %d rows built%s%s
    %s
    module files: %s
    Click to show the rows
    " % + (" now" if now else "", esc(p["id"]), " open" if now else "", esc(p["id"]), esc(p["name"]), " building now" if now else "", pct(p["built"], p["n"]), pct(p["partial"], p["n"]), p["built"], p["n"], (", %d partial" % p["partial"]) if p["partial"] else "", (" · %d runs" % runs_by.get(str(p["id"]), 0)) if runs_by.get(str(p["id"])) else "", esc(" ".join("%s %d" % kv for kv in p["kinds"].items())), esc(" ".join(("%s %s" % (n, "yes" if v else "no")) for n, v in (("definition", p["def"]), ("brief", p["brief"]), ("ledger", p["ledger"])))))) - w("
    " + "".join("" % (x["cls"], esc(x["n"]), esc(x["kind"]), esc(x["step"]), esc(x["state"][:90])) for x in p["steps"]) + "
    #KindRowState
    %s%s%s%s
    ") + if str(p["id"]) in phase_detail: w(phase_detail[str(p["id"])]) + else: w("
    " + "".join("" % (x["cls"], esc(x["n"]), esc(x["kind"]), esc(x["step"]), esc(x["state"][:90])) for x in p["steps"]) + "
    #KindRowState
    %s%s%s%s
    ") + w("
    ") w("
    ") else: w("
    No plan to summarise yet: %s
    " % esc(args.plan_msg or "architecture/build-plan.md was not read.")) - if args.fragment: # the full plan tables repeat the phase cards above row by row, so they start folded - w("
    Full plan tables and views A and B
    Every row of build-plan.md with its claims and drift, the script folders (A) and module proof (B). The phase cards above already show each row.
    ") + if args.fragment: # plan tally, views A (script folders) and B (module proof); the per-phase rows live in the cards above + w("
    ") w(args.fragment) - w("
    ") + w("") w(render_pages(proj, args.outdir, [SELF_OUT])) # footer w("