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 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:53:00 -07:00
parent eb1497d574
commit c2a1cc7c26
3 changed files with 37 additions and 1 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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]))