diff --git a/docs/reference/file-map.md b/docs/reference/file-map.md
index 76d9445..6ce4bbb 100644
--- a/docs/reference/file-map.md
+++ b/docs/reference/file-map.md
@@ -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
diff --git a/html/work-package-suite-app.js b/html/work-package-suite-app.js
index cfc0e80..f9cdb2e 100644
--- a/html/work-package-suite-app.js
+++ b/html/work-package-suite-app.js
@@ -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
diff --git a/html/wp-creation-app.js b/html/wp-creation-app.js
index 1dfd561..7d2874b 100644
--- a/html/wp-creation-app.js
+++ b/html/wp-creation-app.js
@@ -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 — 3–5 days (≈40–80 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=''+KIT_STATUSES.map(k=>
+ ``).join('');
+ if(cur && KIT_STATUSES.indexOf(cur)<0){
+ html+=``;
+ }
+ 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='';});
diff --git a/html/wp-creation-index.html b/html/wp-creation-index.html
index 7802700..b2e04b3 100644
--- a/html/wp-creation-index.html
+++ b/html/wp-creation-index.html
@@ -387,7 +387,8 @@
Kitting & Material Movement (MIMO)
-
+
+
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.
scheduled material-move date & time
diff --git a/tests/kitting_check.py b/tests/kitting_check.py
new file mode 100644
index 0000000..37dc31c
--- /dev/null
+++ b/tests/kitting_check.py
@@ -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())
diff --git a/tests/sections_check.py b/tests/sections_check.py
index 6a11922..70fbc4f 100644
--- a/tests/sections_check.py
+++ b/tests/sections_check.py
@@ -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")