From c2a1cc7c26a560153122e8e3fc0a57a692509a4f Mon Sep 17 00:00:00 2001 From: "n.siegfried" Date: Wed, 19 Aug 2026 12:53:00 -0700 Subject: [PATCH] T9.2 - CR-017: Actual Hours is still there, still optional, still counted A guard, not a build. Removal was floated in the meeting and rejected - Marlena tracks actual hours so they can be measured - and CLAUDE.md carries that as a recorded decision. Verified after eight waves of change: - Actual Hours exists in Closeout (wp_actual_hrs), persists through collect, and prints on the export - it is OPTIONAL: a package closes with it empty (driven, not assumed) - it rolls up per T6.4: rollup_check has pinned actual-hours aggregation at every level since wave 6, and /api/wps/metrics carries actual_hours in its buckets The follow-up the done-when requires is logged as BL-023: a productivity factor (actual / estimated) - the rollup endpoints already carry both sums, so it is a presentation task awaiting its own item id and a placement call. Verification: export_check.py extended to 20/20 (the CR-017 section). Items: CR-017 Co-Authored-By: Claude Fable 5 --- docs/reference/file-map.md | 2 +- docs/waves/backlog.md | 14 ++++++++++++++ tests/export_check.py | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/reference/file-map.md b/docs/reference/file-map.md index 384a2f8..463c5ac 100644 --- a/docs/reference/file-map.md +++ b/docs/reference/file-map.md @@ -299,7 +299,7 @@ python tests/mreq_check.py # CR-013 - lightweight request, end to end Wave 9 adds these: ```bash -python tests/export_check.py # CR-008 - the export, walked field by field 17 checks +python tests/export_check.py # CR-008/CR-017 - export walk + hours guard 20 checks ``` **Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live diff --git a/docs/waves/backlog.md b/docs/waves/backlog.md index 89ddbc1..25fd0e0 100644 --- a/docs/waves/backlog.md +++ b/docs/waves/backlog.md @@ -493,3 +493,17 @@ deliberately deferred. change, criterion satisfied as written) or name the chrome to compress and it becomes a small T9 task. The strict check stays red so the question cannot be forgotten. + +### BL-023 — Productivity factor: actual against estimated hours + +- **Found during:** T9.2 (logged as that task's done-when requires) +- **Where:** future — dashboard / rollups +- **What:** Actual Hours is tracked (CR-017, deliberately kept) and estimated + hours exist on every package; nothing yet compares them. A productivity + factor (actual ÷ estimated, rolled up by discipline / building / type the way + CR-018 rolls cost) is the measurement Marlena's tracking exists to enable. + The rollup endpoints (`/api/wps/metrics`, `/api/projects/{id}/summary`) + already carry both sums, so this is a presentation task, not a data one. +- **Why not now:** new scope — needs its own item id per the working rules, and + a product conversation about where it displays and who reads it. +- **Suggested wave or follow-up:** next revision; needs Nick for placement. diff --git a/tests/export_check.py b/tests/export_check.py index 6d0f883..4e17f33 100644 --- a/tests/export_check.py +++ b/tests/export_check.py @@ -130,6 +130,28 @@ def main(): fits["boxFits"] and fits["poke"] == 0, ascii_(fits)) chk("390px: the base type is readable (>= 12px)", fits["font"] >= 12, ascii_(fits)) + # ── 5. CR-017: the Actual Hours guard (T9.2) ───────────────────────── + # A guard, not a build: removal was floated and REJECTED - Marlena + # tracks actual hours. Verify it survived eight waves of change. + print(chr(10) + "5. CR-017: Actual Hours survived") + page.viewport(1440, 900) + settle(0.5) + chk("Actual Hours still exists in Closeout and persists", + page.eval("!!document.getElementById('wp_actual_hrs')") + and page.eval("collectPackage().actualHrs") == "54") # the example's value + page.eval("document.getElementById('wp_actual_hrs').value=''") + page.eval("""(() => { pkgConstraints.forEach(c => c.status='na'); + buildConstraints(); updateReleaseBanner(); })()""") + page.eval("document.querySelector('#status-group .radio-pill[data-val=%s]').click()" + % json.dumps("Closed")) + settle(0.5) + chk("...it is OPTIONAL: a package closes without it", + page.eval("getRadio('status')") == "Closed", + page.eval("getRadio('status')")) + chk("...and the closeout prints it on the export", + "Actual" in page.eval("(() => { renderPackage(collectPackage()); " + "return document.getElementById('pkg-doc').textContent; })()")) + js_errors = [e for e in page.js_errors() if "beforeunload" not in e] chk("no JavaScript errors anywhere in this run", not js_errors, ascii_(js_errors[:2]))