T8.1 - CR-009: kitting statuses are a set, and Micron EUV is not kitting

The statuses: the proposed five, adopted as proposed - Not Started, Picking,
Staged, In Transit, Delivered - as one named constant (KIT_STATUSES) building
the select. They describe fulfillment; the old four ('Open', 'In Progress',
'Kitted', 'Delivered') mixed fulfillment with workflow. A value stored before
the set existed is kept, selected, and shown as "(legacy)" - CR-016's rule
that renamed vocabularies must not orphan recorded data - and round-trips
through collect unchanged.

Micron EUV: the sample SOP is the Micron configuration on record (CR-016 /
T5.7), and it now names kitting:false beside assets:false - off by CR-006
TOGGLE, in both the creator sample and the wizard sample. The section leaves
the form, the rail and the export; its data and model stay exactly where they
are (the probe loads the example package under the Micron sample and finds
its kitting values intact through collect). Any other SOP turns the section
on and it works fully - driven against sopA with everything enabled.

sections_check re-pointed, not relaxed: its sample-map pin said "naming only
assets"; it now says "naming exactly assets (CR-016) and kitting (CR-009)" -
still refusing any section that goes off without a recorded item behind it.

Verification (each probe run alone): NEW tests/kitting_check.py 14/14.
Regression: sections_check 95/95.

Items: CR-009

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 11:55:39 -07:00
parent b1a7fe04bc
commit 79c4a36c70
6 changed files with 195 additions and 8 deletions

View File

@@ -287,6 +287,12 @@ python tests/usage_check.py # D5 - one analytics core, admin report
python tests/creator_dialogs_check.py # S1 creator - 0 natives, errors at fields 20 checks
```
Wave 8 adds these:
```bash
python tests/kitting_check.py # CR-009 - status set + off for Micron EUV 14 checks
```
**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
the fact that an UNSAVED toggle correctly does not travel - a behaviour change, recorded

View File

@@ -455,7 +455,7 @@ function loadSampleData(){
// A partial map on purpose. WPSections.normalize fills the rest in as ON, so a
// section added after today is not silently off for this SOP.
if(typeof WPSections !== 'undefined'){
state.sections = WPSections.normalize({assets: false});
state.sections = WPSections.normalize({assets: false, kitting: false}); // CR-009: Micron EUV is not kitting today
}
// The Micron FMCS sample includes BIM/VDC — enable it so the sequence shows the

View File

@@ -15,7 +15,7 @@ const SAMPLE_SOP = {
//
// 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.
sections:{assets:false},
sections:{assets:false, kitting:false}, // kitting off: Micron EUV is not kitting today (CR-009/T8.1)
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'}],
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' },
@@ -52,6 +52,23 @@ const ISSUED_IDX = STATUS_ORDER.indexOf('Issued');
// the most urgent last). Do not add a fourth: the meeting settled on three, and a
// fourth level is how a priority scale stops meaning anything.
const WP_PRIORITIES = ['Normal', 'High', 'Urgent'];
// CR-009 / T8.1: kitting statuses are an explicit set, not free text. The
// proposed five, adopted as proposed: they describe FULFILLMENT (what the
// warehouse is doing), where the old four mixed fulfillment with workflow
// ('Open'). A value stored before this set existed is kept and shown as a
// legacy option - renamed sets must not orphan recorded data (CR-016's rule).
const KIT_STATUSES = ['Not Started', 'Picking', 'Staged', 'In Transit', 'Delivered'];
function buildKitStatusOptions(current){
const sel=document.getElementById('wp_kit_status'); if(!sel) return;
const cur=(current!=null?current:sel.value)||'';
let html='<option value="">—</option>'+KIT_STATUSES.map(k=>
`<option${k===cur?' selected':''}>${esc(k)}</option>`).join('');
if(cur && KIT_STATUSES.indexOf(cur)<0){
html+=`<option selected value="${cur.replace(/"/g,'&quot;')}">${esc(cur)} (legacy)</option>`;
}
sel.innerHTML=html;
}
const WP_PRIORITY_DEFAULT = 'Normal';
const COST_CODES = ['1000|Project Management','2000|Design and Development','2100|Design','2110|Control System Design','2120|Instrument Design','2130|Electrical Design','2140|Panel Design','2141|Panel Design Rework','2150|BIM','2151|BIM Rework','2160|Documentation','2200|Development','2210|PLC Programming','2220|OIT Programming','2230|SCADA Programming','2240|Simulation Development','2290|Programming Subcontract','2300|Customer Training','3000|Operational Technology','3100|OT Design','3200|Rack Assembly','3300|Network Configuration','3400|Computer Configuration','4000|Construction','4010|Instruments Install','4020|Network & Computers Install','4040|PLC Install','4050|Panel Install','4060|Electrical Install','4070|Mechanical Install','4080|Security Install','4090|Radio Install','4100|Commissioning','4940|Contract Labor','4960|Electrical Subcontract','4970|Mechanical Subcontract','4980|Security Subcontract','4990|Other/Radio Subcontract'];
@@ -2689,6 +2706,7 @@ function loadPackageIntoForm(p){
loadPeopleFromPkg(p);
set('wp_assignee',p.assigneeId);
set('wp_due',p.due); set('wp_spec',p.spec); set('wp_desc',p.desc); set('wp_hours',p.hours);
buildKitStatusOptions(p.kitStatus||'');
set('wp_kit_owner',p.kitOwner); set('wp_kit_date',p.kitDate); set('wp_mimo_time',p.mimoTime); set('wp_mimo_loc',p.mimoLoc);
set('wp_actual_hrs',p.actualHrs); set('wp_installed_qty',p.installedQty); set('wp_redlines',p.redlines); set('wp_lessons',p.lessons);
set('wp_bimlink',p.bimlink); set('wp_iff',p.iff); set('wp_model_area',p.modelArea);
@@ -2775,7 +2793,7 @@ function newPackage(){
if(typeof WPUrl !== 'undefined' && window.wpCreatorReady && WPUrl.get('wp')) urlSyncPackage('', {replace:true});
editingId=null;
['wp_subject','wp_system','wp_location','wp_p6_id','wp_p6_desc','wp_wbs','wp_assignee','wp_assignees','wp_distribution','wp_due','wp_spec','wp_desc','wp_hours','wp_kit_owner','wp_kit_date','wp_mimo_time','wp_mimo_loc','wp_actual_hrs','wp_installed_qty','wp_redlines','wp_lessons','wp_bimlink','wp_model_area','wp_scan_link'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';});
document.getElementById('wp_type').value=''; document.getElementById('wp_kit_status').value=''; document.getElementById('wp_cost').value='';
document.getElementById('wp_type').value=''; buildKitStatusOptions(''); document.getElementById('wp_cost').value='';
const prio=document.getElementById('wp_priority'); if(prio) prio.value=WP_PRIORITY_DEFAULT; // CR-003
buildLocationPickers({building:'', floor:'', sector:''}); // CR-004
['wp_iff','wp_clash'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';});

View File

@@ -387,7 +387,8 @@
<div class="sub-heading">Kitting & Material Movement (MIMO)</div>
<div class="field-grid">
<div class="field"><label>Kitting status</label>
<select id="wp_kit_status"><option value=""></option><option>Open</option><option>In Progress</option><option>Kitted</option><option>Delivered</option></select></div>
<select id="wp_kit_status"><option value=""></option></select>
<div class="field-hint">Options come from KIT_STATUSES (CR-009): an explicit set, never free text. A pre-CR-009 value on a saved package is kept as a legacy option.</div></div>
<div class="field"><label>Warehouse owner</label><input type="text" id="wp_kit_owner" placeholder="name"></div>
<div class="field"><label>Kitting need date</label><input type="date" id="wp_kit_date"></div>
<div class="field"><label>MIMO sch. time</label><input type="datetime-local" id="wp_mimo_time"><div class="field-hint">scheduled material-move date &amp; time</div></div>

158
tests/kitting_check.py Normal file
View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Are kitting statuses a set, and is kitting off for Micron EUV? — CR-009, T8.1.
The statuses become an explicit five (Not Started / Picking / Staged /
In Transit / Delivered) - fulfillment states, not free text. A value stored
before the set existed is kept and shown as a legacy option: renamed sets must
not orphan recorded data. The Micron EUV configuration (the sample SOP, per
CR-016/T5.7) turns the section OFF through the CR-006 toggle; any other SOP
can turn it on and everything works; nothing is deleted either way.
Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome.
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
KIT = ["Not Started", "Picking", "Staged", "In Transit", "Delivered"]
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 kit_options(page):
return json.loads(page.eval(
"JSON.stringify([...document.getElementById('wp_kit_status').options]"
".map(o=>o.textContent))"))
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-kit-")
db_path = os.path.join(tmpdir, "check.db")
server = None
browser = None
try:
tok = seed(db_path)
set_sop(db_path, {}) # sopA: every section ON, kitting included
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;")
# ── 1. the set ────────────────────────────────────────────────────────
print("\n1. an explicit set, not free text")
opts = kit_options(page)
chk("the kitting status control is a select offering exactly the five states",
opts == [""] + KIT, ascii_(opts))
chk("...and the set is one named constant",
json.loads(page.eval("JSON.stringify(KIT_STATUSES)")) == KIT)
# a pre-CR-009 value survives as a legacy option, selected
page.eval("""loadPackageIntoForm({subject:'legacy kit', type:'Conduit Install',
kitStatus:'Kitted', kitOwner:'Paul Coonrod', constraints:[], holds:[]})""")
settle(0.8)
chk("a stored pre-CR-009 value is kept, selected, and marked legacy",
page.eval("document.getElementById('wp_kit_status').value") == "Kitted"
and any("legacy" in o for o in kit_options(page)), ascii_(kit_options(page)))
chk("...and it round-trips through collect unchanged - nothing re-written",
page.eval("collectPackage().kitStatus") == "Kitted")
page.eval("document.getElementById('wp_kit_status').value='Staged'")
chk("...and picking a new value works from the same control",
page.eval("collectPackage().kitStatus") == "Staged")
# ── 2. this SOP (kitting ON) has the section, working ─────────────────
print("\n2. an SOP with kitting on")
vis = page.eval("""(() => { const c=document.getElementById('mimo-card');
return c && !c.hidden && getComputedStyle(c).display !== 'none'; })()""")
chk("the Kitting & MIMO section is present and rendered", bool(vis))
chk("...and appears in the section rail", page.eval(
"!!document.querySelector(%s)" % json.dumps('.sec-rail-item[data-sec="mimo-card"]')))
# ── 3. Micron EUV (the sample): kitting OFF ───────────────────────────
print("\n3. Micron EUV: off, hidden, not deleted")
page.eval("loadSampleSOP()")
settle(1.0)
chk("the sample IS the Micron configuration and turns kitting off",
page.eval("SOP.sections && SOP.sections.kitting === false"))
hidden = page.eval("""(() => { const c=document.getElementById('mimo-card');
return c && (c.hidden || getComputedStyle(c).display === 'none'); })()""")
chk("the section is gone from the form", bool(hidden))
chk("...and from the rail", page.eval(
"!document.querySelector(%s)" % json.dumps('.sec-rail-item[data-sec="mimo-card"]')))
page.eval("loadExample()")
settle(1.0)
doc = page.eval("(() => { renderPackage(collectPackage()); "
"return document.getElementById('pkg-doc').innerHTML; })()")
chk("...and from the export", "Kitting & MIMO" not in doc)
chk("the example package's kitting DATA is still on the record - hidden, "
"never deleted",
page.eval("collectPackage().kitStatus") != ""
and bool(page.eval("collectPackage().kitOwner")), ascii_(
(page.eval("collectPackage().kitStatus"),
page.eval("collectPackage().kitOwner"))))
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())

View File

@@ -600,8 +600,10 @@ def run(page, base, tok, db_path):
chk("...nor in the detail view", MARKERS["assets"] not in mdoc)
chk("...nor in the PDF export, which is that document",
page.eval("document.getElementById('pkg-doc').innerHTML") == mdoc)
# T8.1 added kitting to the sample's off-list (CR-009), so "every other"
# now means every section not deliberately off by a RECORDED item.
chk("every other section is untouched by CR-016",
all(v in mdoc for k, v in MARKERS.items() if k != "assets"),
all(v in mdoc for k, v in MARKERS.items() if k not in ("assets", "kitting")),
[k for k, v in MARKERS.items() if v not in mdoc])
chk("the section and its model are still in the codebase, not deleted",
page.eval("!!document.getElementById('asset-card')")
@@ -609,9 +611,11 @@ def run(page, base, tok, db_path):
chk("...and the sample switches it off by TOGGLE, not by removing the data",
page.eval("JSON.stringify((SOP||{}).sections||{})").find("assets") >= 0,
page.eval("JSON.stringify((SOP||{}).sections||{})"))
chk("...naming only assets, so a section added later is not silently off too",
json.loads(page.eval("JSON.stringify(Object.keys((SOP||{}).sections||{})"
".filter(k => (SOP.sections||{})[k] === false))")) == ["assets"],
chk("...naming exactly assets (CR-016) and kitting (CR-009), so a section "
"added later is not silently off too",
sorted(json.loads(page.eval("JSON.stringify(Object.keys((SOP||{}).sections||{})"
".filter(k => (SOP.sections||{})[k] === false))")))
== ["assets", "kitting"],
page.eval("JSON.stringify((SOP||{}).sections||{})"))
print(" another SOP enables Assets and it works")