T9.9 - C4 + the backlog sweep: nine entries closed, each re-measured first
The colour half (C4, approved Aug 18 - "change them"):
- BL-004: the help centre's own 52-colour palette collapsed onto theme tokens
- BL-005: the JS-built dialogs (auth-guard, wp-format) and project-data's
badges read tokens; the creator's categorical badge palette moved to
theme-light as --wp-chart-1..10, read by computed style at boot; the print
popup - a document with no stylesheet - inlines live token VALUES
- BL-008: the second brand blue (#2563d6) is deleted; .sop-inherited tints
with THE blue at the same 7% alpha
- BL-009: the ninth amber (--wp-status-warning-text-alt) is deleted
- theme-light gained the two missing feedback tokens the consoles carried as
literals (--wp-status-success-text / -error-text)
- NEW tests/color_check.py 4/4: zero hex literals outside theme-light.css,
comments stripped (the BL-017 lesson), with the exceptions named in full
(meta theme-color cannot resolve a var; rgba alphas are opacity recipes)
The correctness half, each re-measured before touching, as the task ordered:
- BL-011 STILL REPRODUCED: the sync badge mounted on the first async sync
event; its holder now mounts at DOMContentLoaded, so the three overlays land
in script order deterministically
- BL-012 fixed and MEASURED: baseline_shots freezes Date and Math.random per
document; two consecutive admin captures came back byte-identical
- BL-016 fixed: a step-less wizard URL is step 1; stepper_check's deliberately
wrong pin flipped with the fix, exactly as the entry planned
- BL-018 fixed both halves: the false-complete write now requires the
{sop,state} production shape, and browser_check.seed writes that shape -
which un-detoured four probes' creators from the SOP gate. stepper_check
re-pointed at projB (no SOP) because its premise is a wizard someone is
STARTING, and projA now legitimately restores a finished one.
- BL-019 fixed: a stored cost code that left COST_CODES is kept as an option
(the gov_wosize pattern), so opening a package no longer blanks its record
- hold_check's AST sweep refined in passing detection: it flagged T8.3's
notification-row .status as a release transition; it now reads wp.status only
Every wave-9-pointing backlog entry is closed with its measurement recorded.
Verification (each probe run alone): color_check 4/4, stepper_check 71/71,
validation_check 77/77, url_state_check 23/23, autosave_check 34/34,
a11y_check 22/22, launcher_check 58/58, aggregates_check 16/16,
kitting_check 26/26, hold_check 50/50, mobile_check 24/24, frame_check 38/38,
sections_check 95/95, form_structure_check 50/51 (BL-022's question).
Items: C4, BL-004, BL-005, BL-008, BL-009, BL-011, BL-012, BL-016, BL-018, BL-019
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -178,6 +178,24 @@ def main():
|
||||
print(f"\n {len(pages)} page(s) x {len(widths)} width(s) -> {args.out}\n")
|
||||
browser = cdp.Browser()
|
||||
page = browser.page()
|
||||
# BL-012 (fixed at T9.9): admin's captured height varied ~600px between
|
||||
# runs and the creator at 1440px shifted, because live timestamps and
|
||||
# relative times re-render per run. Freezing Date (and Math.random) in
|
||||
# every new document makes a capture comparable with the last one.
|
||||
page.ws.call("Page.addScriptToEvaluateOnNewDocument", {"source": (
|
||||
"(function(){"
|
||||
"var FIXED = 1755600000000;" # 2026-08-19T10:40Z
|
||||
"var RealDate = Date;"
|
||||
"function FrozenDate(){ return new RealDate(FIXED); }"
|
||||
"FrozenDate.now = function(){ return FIXED; };"
|
||||
"FrozenDate.parse = RealDate.parse; FrozenDate.UTC = RealDate.UTC;"
|
||||
"FrozenDate.prototype = RealDate.prototype;"
|
||||
"window.Date = FrozenDate;"
|
||||
"var seed = 42;"
|
||||
"Math.random = function(){ seed = (seed * 9301 + 49297) % 233280;"
|
||||
" return seed / 233280; };"
|
||||
"})();"
|
||||
)})
|
||||
for name, filename, user, wait_for in pages:
|
||||
capture(page, base, tok, name, filename, user, wait_for,
|
||||
widths, args.out, args.label)
|
||||
|
||||
@@ -109,9 +109,34 @@ def seed(db_path):
|
||||
# Job A gets a complete SOP and two packages. Without a SOP the field view's
|
||||
# GET /api/sops/latest correctly answers 404 ("No SOP found") and the browser
|
||||
# logs it as an error — a false alarm in a page-boot check.
|
||||
# BL-018 (fixed at T9.9): the production shape is {sop, state}, as
|
||||
# ProjectData.pushSOP writes it. The old {"governance": ...} blob was a
|
||||
# shape no code path produces, and it sent four probes' creators to the
|
||||
# SOP gate until each imported set_sop() to overwrite it.
|
||||
db.add(models.Sop(id="sopA", project_id="projA", name="Job A SOP", number="A-1",
|
||||
complete=True,
|
||||
data={"governance": {"disciplines": ["Mechanical", "Electrical"]}}))
|
||||
data={"sop": {"meta": {"tool": "Work Package Configuration", "sample": False},
|
||||
"project": {"name": "Job A", "number": "A-1", "client": "Internal QA"},
|
||||
"governance": {"disciplines": ["Mechanical", "Electrical"],
|
||||
"woFormat": "WP##-[TYPE]"},
|
||||
"woTypes": [{"name": "Conduit Install", "enabled": True}],
|
||||
"sections": {}},
|
||||
"state": {"project": {"name": "Job A", "number": "A-1", "client": "Internal QA",
|
||||
"division": "Internal", "site": "QA Lab"},
|
||||
"team": {"pm": "", "apm": "", "cm": "", "qm": ""},
|
||||
"teamIds": {"pm": "", "apm": "", "cm": "", "qm": ""},
|
||||
"teamMembers": [], "sections": {},
|
||||
"signoffRoles": [{"role": "Superintendent", "name": ""},
|
||||
{"role": "Foreman", "name": ""}],
|
||||
"wpTypes": [{"name": "Conduit Install", "enabled": True}],
|
||||
"governance": {"woformat": "WP##-[TYPE]", "wosize": "", "issuance": [],
|
||||
"disciplines": ["Mechanical", "Electrical"],
|
||||
"discMode": "choice", "instanceSuffix": "letter",
|
||||
"sizeHoursMax": ""},
|
||||
"quality": {"qcreq": "Yes", "photo": "", "hold": ""},
|
||||
"platforms": {"tracking": "CxAlloy", "commissioning": "CxAlloy",
|
||||
"trackingUrl": "", "commissioningUrl": ""},
|
||||
"constraints": [], "sequence": [], "sources": []}}))
|
||||
db.flush()
|
||||
for wid, num, subj, status in (("wpA1", "WP01-COND", "1P horn/strobe conduit", "Issued"),
|
||||
("wpA2", "WP02-WIRE", "1P wire pull", "In Progress")):
|
||||
|
||||
78
tests/color_check.py
Normal file
78
tests/color_check.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Is theme-light.css the only place a colour exists? — C4, T9.9.
|
||||
|
||||
The token rule, finally enforceable everywhere: after this sweep no colour
|
||||
literal survives outside theme-light.css - not in page stylesheets, not in the
|
||||
help centre's injected styles (BL-004), not in the JS-built dialogs (BL-005),
|
||||
not in the print popup. One accent blue (BL-008 - the second brand blue is
|
||||
gone, .sop-inherited tints with THE blue) and one warning amber (BL-009 - the
|
||||
alt token is deleted). Comments are stripped first: quoting a hex while
|
||||
explaining it is not declaring one (the BL-017 lesson).
|
||||
|
||||
The exceptions, in full: <meta name="theme-color"> (a meta attribute cannot
|
||||
resolve a CSS var), and rgba() shadow/overlay alphas, which are opacity
|
||||
recipes, not palette entries.
|
||||
|
||||
Static sweep - no browser needed. Exit 0 all passed, 1 a failure.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from browser_check import chk, _PASS, _FAIL # noqa: E402
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HTML = os.path.join(ROOT, "html")
|
||||
|
||||
|
||||
def strip_comments(src, is_css):
|
||||
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
|
||||
if not is_css:
|
||||
src = "\n".join(re.sub(r"(?<![:'\"])//.*$", "", ln) for ln in src.split("\n"))
|
||||
src = re.sub(r"<!--.*?-->", "", src, flags=re.S)
|
||||
return src
|
||||
|
||||
|
||||
def main():
|
||||
print("\n1. hex literals outside theme-light.css")
|
||||
offenders = []
|
||||
for name in sorted(os.listdir(HTML)):
|
||||
if not name.endswith((".js", ".html", ".css")) or name == "theme-light.css":
|
||||
continue
|
||||
src = strip_comments(io.open(os.path.join(HTML, name), encoding="utf-8").read(),
|
||||
name.endswith(".css"))
|
||||
# the one exception: the browser-chrome hint, which cannot use var()
|
||||
src = re.sub(r'<meta name="theme-color" content="#[0-9a-fA-F]{6}"\s*/?>', "", src)
|
||||
for m in re.finditer(r"#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b", src):
|
||||
offenders.append("%s: %s" % (name, m.group(0)))
|
||||
chk("no hex colour literal outside theme-light.css; grep confirms",
|
||||
not offenders, offenders[:8])
|
||||
|
||||
print("\n2. one blue, one amber")
|
||||
theme = io.open(os.path.join(HTML, "theme-light.css"), encoding="utf-8").read()
|
||||
code = strip_comments(theme, True)
|
||||
chk("the second brand blue (#2563d6) is gone from the theme itself",
|
||||
"2563d6" not in code and "37, 99, 214" not in code)
|
||||
chk("the ninth amber (--wp-status-warning-text-alt) is deleted",
|
||||
"--wp-status-warning-text-alt" not in code)
|
||||
others = []
|
||||
for name in sorted(os.listdir(HTML)):
|
||||
if name == "theme-light.css" or not name.endswith((".js", ".css", ".html")):
|
||||
continue
|
||||
src = strip_comments(io.open(os.path.join(HTML, name), encoding="utf-8").read(),
|
||||
name.endswith(".css"))
|
||||
if "warning-text-alt" in src or "2563d6" in src:
|
||||
others.append(name)
|
||||
chk("...and no consumer still references either", not others, others)
|
||||
|
||||
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())
|
||||
@@ -460,8 +460,12 @@ def main():
|
||||
tree = ast.parse(open(os.path.join(ROOT, "server", "app.py"), encoding="utf-8").read())
|
||||
bad = []
|
||||
for fn in [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]:
|
||||
# wp.status specifically - T8.3's coalescer sets a NOTIFICATION
|
||||
# row's .status (held.status = "pending"), which is outbox state,
|
||||
# not a release transition.
|
||||
assigns = [n for n in ast.walk(fn) if isinstance(n, ast.Assign)
|
||||
and any(isinstance(t, ast.Attribute) and t.attr == "status"
|
||||
and isinstance(t.value, ast.Name) and t.value.id == "wp"
|
||||
for t in n.targets)]
|
||||
if not assigns:
|
||||
continue
|
||||
|
||||
@@ -186,7 +186,11 @@ def source_counts():
|
||||
|
||||
|
||||
def run(page, base, tok):
|
||||
def open_wizard(query="?project=projA"):
|
||||
# BL-018's fixture fix (T9.9) gave projA a PRODUCTION-shape completed SOP,
|
||||
# so the wizard on projA now legitimately restores a finished configuration.
|
||||
# This file's premise is a wizard someone is STARTING - projB has no SOP,
|
||||
# which is that premise, honestly.
|
||||
def open_wizard(query="?project=projB"):
|
||||
# Leave the outgoing page clean first: the unsaved-work guard from T4.3 is
|
||||
# doing its job, and a "Leave site?" prompt would stall the navigation.
|
||||
try:
|
||||
@@ -252,7 +256,7 @@ def run(page, base, tok):
|
||||
chk("...and it is the step being shown", cur and cur[0]["step"] == 1, cur)
|
||||
chk("...which also says so in words", cur and cur[0]["state"] == "Current step", cur)
|
||||
|
||||
# projA's fixture project has no division or site, so step 1 is incomplete on
|
||||
# projB has no SOP at all, so step 1 is incomplete on
|
||||
# a fresh load and everything ahead of it is genuinely out of reach.
|
||||
locked = [r for r in rows if r["ariaDisabled"] == "true"]
|
||||
chk("with step 1 incomplete, steps 2-10 are unavailable",
|
||||
@@ -410,14 +414,14 @@ def run(page, base, tok):
|
||||
settle(page, 1.1)
|
||||
chk("...and Back returns to the previous step", page.eval("currentStep") == 2,
|
||||
page.eval("currentStep"))
|
||||
# Back to a URL with NO step at all does not return to step 1 — the popstate
|
||||
# handler parses `step` and ignores a NaN. That is T4.2's restore rather than
|
||||
# the rail's, it predates this task, and it is logged as BL-016.
|
||||
chk("...and the known step-1 gap is still exactly that, and no wider",
|
||||
# BL-016, FIXED at T9.9: a step-less wizard URL is step 1. This check pinned
|
||||
# the WRONG behaviour until the fix landed, and flipped with it - which was
|
||||
# the plan recorded on the entry.
|
||||
chk("...and Back continues to work",
|
||||
page.eval("(() => { history.back(); return true; })()") is True)
|
||||
settle(page, 1.1)
|
||||
chk("...(BL-016) Back to a step-less URL leaves the step where it was",
|
||||
page.eval("currentStep") == 2 and "step=" not in page.eval("location.search"),
|
||||
chk("...(BL-016) Back to a step-less URL returns to step 1",
|
||||
page.eval("currentStep") == 1 and "step=" not in page.eval("location.search"),
|
||||
[page.eval("currentStep"), page.eval("location.search")])
|
||||
|
||||
# ── narrow width: the gloved-hands surface ────────────────────────────────
|
||||
@@ -484,7 +488,10 @@ def run(page, base, tok):
|
||||
chk("the page does not scroll sideways at 1440px",
|
||||
page.eval("document.documentElement.scrollWidth <= window.innerWidth + 1"),
|
||||
page.eval("[document.documentElement.scrollWidth, window.innerWidth]"))
|
||||
chk("the wizard still boots without a JavaScript error", not page.js_errors(),
|
||||
# projB has no SOP, so /api/sops/latest answering 404 is the CORRECT answer
|
||||
# being logged by the browser, not an error in the page.
|
||||
chk("the wizard still boots without a JavaScript error",
|
||||
not [e for e in page.js_errors() if "sops/latest" not in e],
|
||||
page.js_errors())
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user