T9.1 - CR-008: the export, finally walked through

Every required field verified present under the Micron configuration: P6
Activity id and description, Priority, the Building/Floor/Sector location
row, Scope & Work, Material List, Constraints with their status carried by
WORDS (a black-and-white print keeps its meaning), Quality & Hold Points, and
the Drawings & Attachments index with per-file descriptions (T7.7's columns).

What must be absent, absent: the Micron samples now carry
fields:{costCode:false, acumaticaTask:false} - CR-002's two removals,
expressed as the toggles CLAUDE.md requires, in the creator sample AND the
wizard sample - so ACU Cost Code and Acumatica Task appear nowhere on the
Micron export while the columns, model and recorded values stay. CR-006-
suppressed sections (assets, kitting) are absent, as pinned since T5.7.

Tablet legibility, three real defects fixed:
- a bare `table { min-width:520px }` in the narrow-screen media block reached
  the EXPORT tables too, dragging the whole document to 520px on a 390px
  screen; scoped to .table-wrap (the form's scroll containers), because the
  export must FIT a tablet, not scroll
- export tables now table-layout:fixed with overflow-wrap:anywhere
- at <=768px the doc sheds its 52/56px desk padding and neutralises the
  inline column widths (the one legitimate !important: outranking an inline
  style is its job)

RAISED, NOT DECIDED (the task says propose, do not assume) - merge versus
list for attachments: RECOMMEND MERGING image attachments into the printed
document (already done - they print inline as the sheet itself) and LISTING
PDFs as named, described links rather than merging them. Merging PDFs
server-side needs a PDF library dependency and re-renders every export for a
need the meeting expressed as "hand someone exactly the sheet" - which the
5MB single-sheet uploads plus inline images already serve. If merged-PDF
output is wanted anyway, it is a bounded server task - needs Nick.

Verification (each probe run alone): NEW tests/export_check.py 17/17.
Regressions: form_structure_check 50/51 (the standing F6 height question,
BL-022), sections_check 95/95.

Items: CR-008 (CR-002 field toggles applied to the samples)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:51:44 -07:00
parent bf28489954
commit eb1497d574
5 changed files with 185 additions and 2 deletions

View File

@@ -296,6 +296,12 @@ python tests/materials_check.py # D6 - material list, the CR-005 pattern
python tests/mreq_check.py # CR-013 - lightweight request, end to end 19 checks python tests/mreq_check.py # CR-013 - lightweight request, end to end 19 checks
``` ```
Wave 9 adds these:
```bash
python tests/export_check.py # CR-008 - the export, walked field by field 17 checks
```
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live **Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live
cross-frame toggle hand-off, which no longer exists; it now pins the surviving path and cross-frame toggle hand-off, which no longer exists; it now pins the surviving path and
the fact that an UNSAVED toggle correctly does not travel - a behaviour change, recorded the fact that an UNSAVED toggle correctly does not travel - a behaviour change, recorded

View File

@@ -456,6 +456,7 @@ function loadSampleData(){
// section added after today is not silently off for this SOP. // section added after today is not silently off for this SOP.
if(typeof WPSections !== 'undefined'){ if(typeof WPSections !== 'undefined'){
state.sections = WPSections.normalize({assets: false, kitting: false}); // CR-009: Micron EUV is not kitting today state.sections = WPSections.normalize({assets: false, kitting: false}); // CR-009: Micron EUV is not kitting today
state.fields = WPSections.normalizeFields({costCode: false, acumaticaTask: false}); // CR-002, mirrored on the export (T9.1)
} }
// The Micron FMCS sample includes BIM/VDC — enable it so the sequence shows the // The Micron FMCS sample includes BIM/VDC — enable it so the sequence shows the

View File

@@ -16,6 +16,10 @@ const SAMPLE_SOP = {
// Written as a partial map, not a full one: WPSections.normalize fills the rest // Written as a partial map, not a full one: WPSections.normalize fills the rest
// in as ON, so a section added later is not silently off for this SOP. // in as ON, so a section added later is not silently off for this SOP.
sections:{assets:false, kitting:false}, // kitting off: Micron EUV is not kitting today (CR-009/T8.1) sections:{assets:false, kitting:false}, // kitting off: Micron EUV is not kitting today (CR-009/T8.1)
// CR-002 / T9.1: the two fields the review removed, expressed as the toggles
// CLAUDE.md requires - hidden on the form AND absent from the export, while
// the columns, the model and every recorded value stay exactly where they are.
fields:{costCode:false, acumaticaTask:false},
project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'}, project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'},
roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}], roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}],
governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'Standard — 35 days (≈4080 hrs)', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'80' }, governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'Standard — 35 days (≈4080 hrs)', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'80' },

View File

@@ -344,7 +344,19 @@
.output-doc p { margin-bottom: 10px; } .output-doc p { margin-bottom: 10px; }
.output-doc ul { padding-left: 20px; margin-bottom: 10px; } .output-doc ul { padding-left: 20px; margin-bottom: 10px; }
.output-doc li { margin-bottom: 3px; } .output-doc li { margin-bottom: 3px; }
.output-doc table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; border: 1px solid var(--border); } .output-doc table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; border: 1px solid var(--border);
/* CR-008 / T9.1: the export opens on tablets. Fixed layout + wrap-anywhere
keeps every table inside the screen instead of laying out at its content's
natural 520px and dragging the whole document sideways. */
table-layout: fixed; }
.output-doc td, .output-doc th { overflow-wrap: anywhere; }
/* On a phone/tablet the inline column widths the desktop layout carries
(width:200px on header cells) exceed the available line; !important is the
one CSS mechanism that outranks an inline style, which is exactly the job. */
@media (max-width: 768px) {
.output-doc { padding: 20px 14px; }
.output-doc th, .output-doc td { width: auto !important; }
}
.output-doc th { background: var(--surface2); border: 1px solid var(--border); padding: 6px 10px; font-family: var(--mono); font-size: 9px; letter-spacing: .08em; text-transform: uppercase; text-align: left; color: var(--text-muted); } .output-doc th { background: var(--surface2); border: 1px solid var(--border); padding: 6px 10px; font-family: var(--mono); font-size: 9px; letter-spacing: .08em; text-transform: uppercase; text-align: left; color: var(--text-muted); }
.output-doc td { border: 1px solid var(--border); padding: 7px 10px; vertical-align: top; } .output-doc td { border: 1px solid var(--border); padding: 7px 10px; vertical-align: top; }
.output-doc .badge { display: inline-block; padding: 1px 8px; border-radius: 3px; font-family: var(--mono); font-size: 10px; font-weight: 600; } .output-doc .badge { display: inline-block; padding: 1px 8px; border-radius: 3px; font-family: var(--mono); font-size: 10px; font-weight: 600; }
@@ -659,7 +671,10 @@
.modal { max-width:100% !important; } .modal { max-width:100% !important; }
.output-toolbar { flex-wrap:wrap; } .output-toolbar { flex-wrap:wrap; }
.material-actions .add-btn { flex:1 1 auto; } .material-actions .add-btn { flex:1 1 auto; }
table { min-width:520px; } /* keep columns legible; .table-wrap scrolls */ /* Scoped to the FORM's scroll containers only. This was a bare `table` and
it reached the export document too, forcing every printed table to 520px
inside a 390px screen - the export must FIT a tablet, not scroll (CR-008). */
.table-wrap table { min-width:520px; } /* keep columns legible; .table-wrap scrolls */
.cstatus button { padding:6px 8px; } .cstatus button { padding:6px 8px; }
} }
@media (max-width: 480px) { @media (max-width: 480px) {

157
tests/export_check.py Normal file
View File

@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Does the export print what the field needs, and nothing it does not? — CR-008, T9.1.
The export was never walked through in the meeting. This is the walk: every
required field present, CR-006-suppressed sections absent, the two CR-002
fields (ACU Cost Code, Acumatica Task) appearing NOWHERE under the Micron
configuration, status carried by words (a black-and-white print keeps its
meaning), and the whole document legible on a tablet.
Exit 0 all passed, 1 a failure, 2 could not run.
"""
import json
import os
import re
import sys
import tempfile
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cdp # noqa: E402
from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
from sections_check import set_sop # noqa: E402
from stepper_check import dismiss_dialogs # noqa: E402
def ascii_(v, n=280):
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
def settle(seconds=0.5):
time.sleep(seconds)
def wait_creator(page, tries=40):
for _ in range(tries):
if page.eval("!!window.wpCreatorReady"):
return True
time.sleep(0.3)
return False
def main():
exe = cdp.find_browser()
if not exe:
print("no headless-capable browser found; set WP_BROWSER.")
return 2
tmpdir = tempfile.mkdtemp(prefix="wpsuite-export-")
db_path = os.path.join(tmpdir, "check.db")
server = None
browser = None
try:
tok = seed(db_path)
set_sop(db_path, {})
port = cdp.free_port()
base = "http://127.0.0.1:%d" % port
server = start_server(port, db_path)
browser = cdp.Browser(exe)
page = browser.page()
page.clear_cookies()
page.set_cookie("wp_session", tok["root"])
page.viewport(1440, 900)
page.goto(base + "/wp-creation-index.html?project=projA")
dismiss_dialogs(page)
chk("the creator boots", wait_creator(page))
settle(1.6)
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
# The Micron configuration and its example package - the real shape.
page.eval("loadSampleSOP()")
settle(1.0)
page.eval("loadExample()")
settle(1.0)
doc = page.eval("(() => { renderPackage(collectPackage()); "
"return document.getElementById('pkg-doc').innerHTML; })()")
text = page.eval("(document.getElementById('pkg-doc')||{textContent:''}).textContent")
print("\n1. every listed field, present")
for label, needle in [
("P6 Activity id + description", "P6 Activity"),
("Priority", "Priority"),
("scope of work", "Scope & Work"),
("materials", "Material List"),
("constraints", "Constraints — Release Readiness"),
("QA/QC", "Quality & Hold Points"),
]:
chk("the export carries %s" % label, needle in text or needle in doc, needle)
chk("...and the location row (Building / Floor / Sector is a row of "
"General Information)", "Location" in text)
chk("constraint status prints as WORDS - a black-and-white print keeps "
"its meaning", "Cleared" in text and "Open" in text)
print("\n2. what must be absent, absent")
chk("ACU Cost Code appears nowhere", "Cost Code" not in text)
chk("Acumatica Task appears nowhere", "Acumatica Task" not in text)
chk("the toggled-off sections are absent (assets, kitting - the Micron "
"sample's CR-006 state)",
"Kitting & MIMO" not in text and "Asset" not in text.replace("Assets is off", ""))
print("\n3. the attachment index")
chk("every attachment is listed", "Drawings & Attachments" in text)
chk("...uploads would carry their descriptions (the columns exist)",
"Focus area" in text or "Link / Note" in text)
print("\n4. the tablet")
page.viewport(390, 844, mobile=True)
settle(0.8)
page.eval("renderPackage(collectPackage())")
settle(0.8)
# scrollWidth vs clientWidth is the wrong ruler for a padded overflow-y
# container: Chrome reports scrollWidth as content+padding even with
# nothing protruding, so the naive comparison is off by exactly the
# padding+border. What legibility actually needs: the BOX fits the
# screen, and no element inside pokes past the box.
fits = page.eval("""(() => {
const d = document.getElementById('pkg-doc');
const dr = d.getBoundingClientRect();
const cs = getComputedStyle(d);
let poke = 0;
d.querySelectorAll('*').forEach(el => {
const r = el.getBoundingClientRect();
if (r.right > dr.right + 2) poke++;
});
return {boxFits: dr.width <= 390, poke, font: parseFloat(cs.fontSize)};
})()""")
chk("390px: the document fits the screen and nothing inside pokes past it",
fits["boxFits"] and fits["poke"] == 0, ascii_(fits))
chk("390px: the base type is readable (>= 12px)", fits["font"] >= 12, ascii_(fits))
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]))
finally:
if browser is not None:
try:
browser.close()
except Exception:
pass
if server is not None:
try:
server.terminate()
except Exception:
pass
print("\n" + "-" * 54)
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
for f in _FAIL:
print(" - " + f)
return 1 if _FAIL else 0
if __name__ == "__main__":
sys.exit(main())