@techierathore/techieflow 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +194 -14
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +112 -13
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +2 -2
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +5 -5
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +3 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +2 -2
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +10 -2
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +38 -2
- package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +95 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +163 -15
- package/.tfcore/utils/tf-checklist-edit.py +21 -3
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +53 -24
- package/.tfcore/utils/tf-doc-check.py +218 -26
- package/.tfcore/utils/tf-doc-tests.sh +116 -0
- package/.tfcore/utils/tf-emit.sh +132 -45
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +87 -31
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
- package/.tfcore/utils/tf-mockup-parity.sh +25 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +38 -4
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +79 -5
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +160 -45
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +74 -17
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +529 -54
- package/.tfcore/utils/tf-verify-emit.sh +57 -17
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +290 -24
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +341 -44
- package/.tfcore/utils/tf-verify-screens.sh +23 -5
- package/.tfcore/utils/tf-verify-tests.sh +310 -19
- package/.tfcore/utils/tf-verify-verdict.py +163 -21
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +272 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +6 -2
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
|
@@ -14,7 +14,10 @@ into the template (.tfcore/templates/v4custom/app-project-status-tmpl.md):
|
|
|
14
14
|
- the Open requirements section (counts by status, at most ten named rows)
|
|
15
15
|
- the Verification log table with its new row (last five kept)
|
|
16
16
|
- the Library feedback summary lines
|
|
17
|
-
- the Next command to run, in both harness forms, and why
|
|
17
|
+
- the Next command to run, in both harness forms, and why. On a project with more than one
|
|
18
|
+
phase a verify names its phase (`*verify ui <App> --phase 3`), and built rows waiting in
|
|
19
|
+
another phase's checklist are found too: `--phase N` for one, `--phase all` for several
|
|
20
|
+
(Sevak TF-005). A build of this phase still comes first.
|
|
18
21
|
|
|
19
22
|
Writes nothing. The agent writes PROJECT-STATUS.md with the harness Write tool.
|
|
20
23
|
Python 3 standard library only. Exit 0 printed, 2 could not run.
|
|
@@ -27,6 +30,7 @@ import re
|
|
|
27
30
|
import sys
|
|
28
31
|
|
|
29
32
|
TERMINAL = {"verified", "done (pre-existing)", "n/a"}
|
|
33
|
+
NA = {"n/a"} # terminal, but never counted as verified (TF-018)
|
|
30
34
|
PASS_THROUGH = {"blocked"} # a library gap; counts as closed for the ladder
|
|
31
35
|
BUILT = {"implemented", "needs re-verify"}
|
|
32
36
|
OWNER = {"owner-uat"} # only the owner can close it, from the UsageGuide test plan
|
|
@@ -61,6 +65,9 @@ def read(path):
|
|
|
61
65
|
|
|
62
66
|
|
|
63
67
|
def checklist_rows(text):
|
|
68
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
69
|
+
import tf_roadmap
|
|
70
|
+
marked = tf_roadmap.ids(text)
|
|
64
71
|
rows = []
|
|
65
72
|
for line in text.splitlines():
|
|
66
73
|
if not re.match(r"^\s*\|\s*`?REQ-", line):
|
|
@@ -69,10 +76,34 @@ def checklist_rows(text):
|
|
|
69
76
|
if len(cells) < 3:
|
|
70
77
|
continue
|
|
71
78
|
rid = cells[0].strip("`* ")
|
|
72
|
-
|
|
79
|
+
status = norm_status(cells[2])
|
|
80
|
+
rows.append({"id": rid, "name": cells[1].strip("`* "), "status": status,
|
|
81
|
+
"remarks": cells[4] if len(cells) > 4 else "",
|
|
82
|
+
"roadmap": rid.upper() in marked and status.lower() not in TERMINAL})
|
|
73
83
|
return rows
|
|
74
84
|
|
|
75
85
|
|
|
86
|
+
def figures(rows):
|
|
87
|
+
"""(verified, counted, not applicable). An N/A row is terminal but was never verified, so it is
|
|
88
|
+
left out of both numbers and named: AppManager read "89 of 89 verified" with REQ-NFR-008 at N/A
|
|
89
|
+
(TF-018). tests/regression/run.sh am_018."""
|
|
90
|
+
na = sum(1 for r in rows if r["status"].lower() in NA)
|
|
91
|
+
return sum(1 for r in rows if r["status"].lower() in TERMINAL - NA), len(rows) - na, na
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# The defect marks live in tf_defect.py. A bare ⚠ is not enough: sending "not verifiable here" to a
|
|
95
|
+
# build is the TfLens loop again.
|
|
96
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
97
|
+
from tf_defect import DEFECT # noqa: E402
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def has_defect(r):
|
|
101
|
+
"""A Needs re-verify row whose Remarks carry a framework defect mark holds a defect no fix has
|
|
102
|
+
cleared, and a verify of the acceptance line cannot see it. So it is fix work, as build-phase step 7
|
|
103
|
+
and tf-build-list say, never a verify (AppManager TF-019). tests/regression/run.sh am_019."""
|
|
104
|
+
return r["status"].lower() == "needs re-verify" and bool(DEFECT.search(r.get("remarks", "")))
|
|
105
|
+
|
|
106
|
+
|
|
76
107
|
def ladder_rank(status):
|
|
77
108
|
return LADDER.index(status) if status in LADDER else 0
|
|
78
109
|
|
|
@@ -142,24 +173,29 @@ def existing_log_rows(status_text):
|
|
|
142
173
|
return rows
|
|
143
174
|
|
|
144
175
|
|
|
145
|
-
def handoff_ran(log_rows):
|
|
146
|
-
|
|
176
|
+
def handoff_ran(log_rows, app="", phase=1):
|
|
177
|
+
"""A handoff of THIS phase: a log row naming handoff whose status-table cell names this phase's
|
|
178
|
+
checklist. Phase 2's handoff counted for phase 3, so a newly built phase read "handoff done" and
|
|
179
|
+
sent the owner to UAT with guides that did not describe it (TechieRag TF-003). A row naming no
|
|
180
|
+
checklist (an older log) counts in phase 1 only, where nothing else could be meant."""
|
|
181
|
+
own = f"{phase_prefix(app, phase)}Checklist.md" if app else ""
|
|
182
|
+
for r in log_rows:
|
|
183
|
+
if "handoff" not in r.lower():
|
|
184
|
+
continue
|
|
185
|
+
named = re.findall(r"[\w.-]+-Checklist\.md", r)
|
|
186
|
+
if (own in named) if named and own else (phase <= 1):
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
147
189
|
|
|
148
190
|
|
|
149
191
|
def feedback_lines(root, app):
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
closed = re.search(r"(?im)^\s*[-*]\s*\*\*(status|resolved|fixed)[^:]*:\*\*\s*(fixed|resolved|closed|done)", e)
|
|
158
|
-
if not closed:
|
|
159
|
-
open_n += 1
|
|
160
|
-
rel = os.path.relpath(path, root).replace(os.sep, "/")
|
|
161
|
-
out.append(f"- {lib}: {open_n} open of {len(entries)} — {rel}")
|
|
162
|
-
return out or ["- None"]
|
|
192
|
+
# One reader for every script (tf_feedback.py). This function used to count every `###`
|
|
193
|
+
# heading as an entry and accept only a closing line no file used, so TfLens's status read
|
|
194
|
+
# "TechieFlow: 62 open of 62" for 27 entries, 19 of them fixed or closed, and the agent told
|
|
195
|
+
# the owner two fixed problems were open (MISS-TechieFlow-20260911-04).
|
|
196
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
197
|
+
import tf_feedback
|
|
198
|
+
return [tf_feedback.summary_line(p, app) for p in tf_feedback.files(root, app)] or ["- None"]
|
|
163
199
|
|
|
164
200
|
|
|
165
201
|
def config_phase(root):
|
|
@@ -171,6 +207,13 @@ def config_phase(root):
|
|
|
171
207
|
return 1
|
|
172
208
|
|
|
173
209
|
|
|
210
|
+
def config_library(root):
|
|
211
|
+
"""metrics.project_type: library (or appKind: library): the documents ship inside the package."""
|
|
212
|
+
p = os.path.join(root, ".tfcore", "core-config.yaml")
|
|
213
|
+
t = read(p) if os.path.isfile(p) else ""
|
|
214
|
+
return bool(re.search(r"(?m)^\s+project_type:\s*['\"]?lib", t) or re.search(r"(?m)^appKind:\s*['\"]?lib", t))
|
|
215
|
+
|
|
216
|
+
|
|
174
217
|
def phase_prefix(app, phase):
|
|
175
218
|
return f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
176
219
|
|
|
@@ -191,9 +234,48 @@ def phase_row(root, app, phase):
|
|
|
191
234
|
return name, total
|
|
192
235
|
|
|
193
236
|
|
|
194
|
-
def
|
|
195
|
-
"""
|
|
237
|
+
def usage_guide(app):
|
|
238
|
+
"""UsageGuide or Usage-Guide, whichever exists: tf-verify-list.py's reader (AppManager TF-022)."""
|
|
239
|
+
import importlib.util
|
|
240
|
+
spec = importlib.util.spec_from_file_location("tf_verify_list", os.path.join(os.path.dirname(os.path.abspath(__file__)), "tf-verify-list.py"))
|
|
241
|
+
vl = importlib.util.module_from_spec(spec)
|
|
242
|
+
spec.loader.exec_module(vl)
|
|
243
|
+
return vl.usage_guide(app)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def to_verify(rows, verdicts):
|
|
247
|
+
"""(waiting, unmeasured): built rows a verify should grade, and built rows the last verify could
|
|
248
|
+
not measure (every test skipped). Roadmap rows and rows carrying a defect are neither."""
|
|
249
|
+
built = [r for r in rows if not r.get("roadmap") and r["status"].lower() in BUILT and not has_defect(r)]
|
|
250
|
+
unmeasured = [r for r in built if verdicts.get(r["id"]) == "NOT-TESTED"]
|
|
251
|
+
return [r for r in built if r not in unmeasured], unmeasured
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def scope_of(rows):
|
|
255
|
+
ui = [r for r in rows if r["id"].startswith("REQ-UI")]
|
|
256
|
+
return "ui" if len(ui) == len(rows) else ("functional" if not ui else "all")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def other_phases(root, app, phase_n, verdicts):
|
|
260
|
+
"""{phase: rows waiting for a verify} for every other phase with a checklist (Sevak TF-005: phases 1
|
|
261
|
+
and 2 held 86 rows waiting while the status read only phase 3's checklist)."""
|
|
262
|
+
found = {}
|
|
263
|
+
for f in sorted(os.listdir(os.path.join(root, "docs"))) if os.path.isdir(os.path.join(root, "docs")) else []:
|
|
264
|
+
m = re.fullmatch(re.escape(app) + r"-(?:P(\d+)-)?Checklist\.md", f)
|
|
265
|
+
p = int(m.group(1) or 1) if m else 0
|
|
266
|
+
if not p or p == phase_n:
|
|
267
|
+
continue
|
|
268
|
+
waiting, _ = to_verify(checklist_rows(read(os.path.join(root, "docs", f))), verdicts)
|
|
269
|
+
if waiting:
|
|
270
|
+
found[p] = waiting
|
|
271
|
+
return dict(sorted(found.items()))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def next_command(app, rows, log_rows, phase=1, verdicts=None, library=False, multi=False):
|
|
275
|
+
"""Returns (phase, qualifier, cc_line, oc_line, reason). multi: the project has more than one
|
|
276
|
+
phase, so a verify names the phase it grades."""
|
|
196
277
|
verdicts = verdicts or {}
|
|
278
|
+
pflag = f" --phase {phase}" if multi else ""
|
|
197
279
|
|
|
198
280
|
def ids(lst):
|
|
199
281
|
return ", ".join(r["id"] for r in lst[:6]) + (" …" if len(lst) > 6 else "")
|
|
@@ -207,17 +289,24 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
|
207
289
|
f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
|
|
208
290
|
"no BRD and no checklist exist; day-1 stage 1 has not run")
|
|
209
291
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
292
|
+
# A roadmap row is not this phase's work, so it never decides the next command (TF-017).
|
|
293
|
+
now = [r for r in rows if not r.get("roadmap")]
|
|
294
|
+
unbuilt = [r for r in now if r["status"].lower() not in TERMINAL | PASS_THROUGH | BUILT | OWNER]
|
|
295
|
+
defects = [r for r in now if has_defect(r)]
|
|
296
|
+
waiting, unmeasured = to_verify(now, verdicts)
|
|
297
|
+
built = waiting + unmeasured
|
|
298
|
+
owner = [r for r in now if r["status"].lower() in OWNER]
|
|
299
|
+
verified, counted, na = figures(rows)
|
|
300
|
+
q = f"{verified} of {counted} verified" + (f", {na} not applicable" if na else "")
|
|
301
|
+
|
|
302
|
+
if unbuilt or defects:
|
|
303
|
+
qual = ", ".join(x for x in (f"{len(unbuilt)} not built" if unbuilt else "",
|
|
304
|
+
f"{len(defects)} to fix" if defects else "") if x)
|
|
305
|
+
why = "; ".join(x for x in (f"{len(unbuilt)} rows are not built yet: {ids(unbuilt)}" if unbuilt else "",
|
|
306
|
+
f"{len(defects)} rows carry a defect (⚠ in Remarks) that a fix must clear before "
|
|
307
|
+
f"a verify: {ids(defects)}" if defects else "") if x)
|
|
308
|
+
return ("Build", f"{qual}, {q}",
|
|
309
|
+
f"{CC}flow-master *build-phase {app}", f"{OC}flow-master *build-phase {app}", why)
|
|
221
310
|
if built:
|
|
222
311
|
# A row the last verify could not MEASURE is not a row waiting for another verify.
|
|
223
312
|
# Its tests declined — the state they need does not exist in this environment — so
|
|
@@ -225,37 +314,40 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
|
225
314
|
# loop that looks like progress. Say what it actually needs: data, a changed
|
|
226
315
|
# acceptance line, or the row marked N/A. Only the owner can choose. FR-69's sibling
|
|
227
316
|
# case; the verdicts come from the ledger the verifier already writes.
|
|
228
|
-
unmeasured = [r for r in built if verdicts.get(r["id"]) == "NOT-TESTED"]
|
|
229
|
-
waiting = [r for r in built if r not in unmeasured]
|
|
230
317
|
if unmeasured and not waiting:
|
|
231
318
|
line = (f"(owner) {len(unmeasured)} row(s) cannot be measured here — create the data, "
|
|
232
319
|
f"change the acceptance line, or mark the row N/A: {ids(unmeasured)}")
|
|
233
320
|
return ("Verify", f"{len(unmeasured)} not measurable, {q}", line, line,
|
|
234
321
|
f"every test carrying these ids was skipped, so another verify run changes nothing: {ids(unmeasured)}")
|
|
235
|
-
|
|
236
|
-
scope = "ui" if len(ui) == len(waiting) else ("functional" if not ui else "all")
|
|
322
|
+
scope = scope_of(waiting)
|
|
237
323
|
reason = f"{len(waiting)} rows are built and not verified: {ids(waiting)}"
|
|
238
324
|
if unmeasured:
|
|
239
325
|
reason += (f"; {len(unmeasured)} more could not be measured (every test skipped) "
|
|
240
326
|
f"and need the owner: {ids(unmeasured)}")
|
|
241
327
|
return ("Verify", f"{len(waiting)} to verify, {q}",
|
|
242
|
-
f"{CC}verifier *verify {scope} {app}", f"{OC}flow-verifier *verify {scope} {app}",
|
|
328
|
+
f"{CC}verifier *verify {scope} {app}{pflag}", f"{OC}flow-verifier *verify {scope} {app}{pflag}",
|
|
243
329
|
reason)
|
|
244
330
|
if owner:
|
|
245
|
-
if handoff_ran(log_rows):
|
|
246
|
-
line = f"(owner-run)
|
|
331
|
+
if handoff_ran(log_rows, app, phase):
|
|
332
|
+
line = f"(owner-run) {usage_guide(app)} — {ids(owner)}"
|
|
247
333
|
return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
|
|
248
334
|
f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
|
|
249
335
|
return ("Handoff", f"{len(owner)} owner-run, {q}",
|
|
250
336
|
f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
|
|
251
337
|
f"{len(owner)} rows are Owner-UAT and handoff has not run yet")
|
|
252
|
-
if handoff_ran(log_rows):
|
|
338
|
+
if handoff_ran(log_rows, app, phase):
|
|
339
|
+
if library:
|
|
340
|
+
# a library's fix runs the handoff steps itself, before the gate, so the package ships with
|
|
341
|
+
# its documents; *handoff-phase after the release was the wrong order (TrBlazeUI TF-005)
|
|
342
|
+
line = "(owner) commit, then build and publish the package — its shipped documents are current; no agent command"
|
|
343
|
+
return ("Release", f"handoff done, {q}", line, line,
|
|
344
|
+
"every row in this phase's scope is terminal and the shipped documents were brought up to date")
|
|
253
345
|
line = "(owner) set current_phase to Released after UAT — no agent command"
|
|
254
346
|
return ("UAT", f"handoff done, {q}", line, line,
|
|
255
|
-
"every row is terminal and handoff has run; waiting on the owner")
|
|
347
|
+
"every row in this phase's scope is terminal and handoff has run; waiting on the owner")
|
|
256
348
|
return ("Handoff", q,
|
|
257
349
|
f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
|
|
258
|
-
"every row is terminal and handoff has not run yet")
|
|
350
|
+
"every row in this phase's scope is terminal and handoff has not run yet")
|
|
259
351
|
|
|
260
352
|
|
|
261
353
|
def main(argv):
|
|
@@ -283,15 +375,38 @@ def main(argv):
|
|
|
283
375
|
st_text = read(st_path) if os.path.isfile(st_path) else ""
|
|
284
376
|
log_rows = existing_log_rows(st_text)
|
|
285
377
|
vdate, vresult = last_verify(root, app)
|
|
286
|
-
|
|
378
|
+
verdicts = ledger_verdicts(root)
|
|
379
|
+
others = other_phases(root, app, phase_n, verdicts)
|
|
287
380
|
pname, ptotal = phase_row(root, app, phase_n)
|
|
381
|
+
multi = phase_n > 1 or (ptotal or 0) > 1 or bool(others)
|
|
382
|
+
phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n, verdicts, config_library(root), multi)
|
|
383
|
+
if others:
|
|
384
|
+
# A verify of another phase names that phase, or --phase all when several phases wait; a build
|
|
385
|
+
# of this phase still goes first.
|
|
386
|
+
n = sum(len(w) for w in others.values())
|
|
387
|
+
where = "; ".join(f"phase {p}: {len(w)} ({', '.join(r['id'] for r in w[:4])}{' …' if len(w) > 4 else ''})"
|
|
388
|
+
for p, w in others.items())
|
|
389
|
+
mine, _ = to_verify(rows, verdicts)
|
|
390
|
+
if phase == "Build":
|
|
391
|
+
reason += f"; after this build, {n} built rows of other phases wait for a verify — {where}"
|
|
392
|
+
else:
|
|
393
|
+
pick = list(others) + ([phase_n] if mine else [])
|
|
394
|
+
flag = f"--phase {pick[0]}" if len(pick) == 1 else "--phase all"
|
|
395
|
+
scope = scope_of([r for w in others.values() for r in w] + mine)
|
|
396
|
+
cc, oc = f"{CC}verifier *verify {scope} {app} {flag}", f"{OC}flow-verifier *verify {scope} {app} {flag}"
|
|
397
|
+
reason = (f"{n} rows of other phases are built and not verified — {where}"
|
|
398
|
+
+ (f"; and in this phase: {reason}" if phase == "Verify" else f"; this phase alone would be: {qual} ({reason})"))
|
|
399
|
+
phase, qual = "Verify", f"{n + len(mine)} to verify across phases {', '.join(map(str, sorted(pick)))}, {qual}"
|
|
400
|
+
roadmap = [r["id"] for r in rows if r["roadmap"]]
|
|
401
|
+
if roadmap:
|
|
402
|
+
reason += (f"; {len(roadmap)} roadmap row(s) are not in this phase's scope and are left for the owner "
|
|
403
|
+
f"(*amend-docs): {', '.join(roadmap)}")
|
|
288
404
|
if ptotal or phase_n > 1:
|
|
289
405
|
tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
|
|
290
406
|
phase = f"{tag} · {phase}"
|
|
291
407
|
reason = f"{reason}; working {cl_rel}"
|
|
292
408
|
|
|
293
|
-
|
|
294
|
-
closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
|
|
409
|
+
verified, counted, na = figures(rows)
|
|
295
410
|
counts = {}
|
|
296
411
|
for r in rows:
|
|
297
412
|
counts[r["status"]] = counts.get(r["status"], 0) + 1
|
|
@@ -322,7 +437,7 @@ def main(argv):
|
|
|
322
437
|
print()
|
|
323
438
|
if open_rows:
|
|
324
439
|
for r in open_rows[:10]:
|
|
325
|
-
print(f"- [ ] {r['id']} — {r['name']} ({r['status']})")
|
|
440
|
+
print(f"- [ ] {r['id']} — {r['name']} ({r['status']}{', roadmap — not in this phase' if r['roadmap'] else ''})")
|
|
326
441
|
if len(open_rows) > 10:
|
|
327
442
|
print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
|
|
328
443
|
else:
|
|
@@ -332,7 +447,7 @@ def main(argv):
|
|
|
332
447
|
print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
|
|
333
448
|
print()
|
|
334
449
|
print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
|
|
335
|
-
new_row = f"| {today} | {label} | {
|
|
450
|
+
new_row = f"| {today} | {label} | {verified}/{counted} Verified{f', {na} N/A' if na else ''} | {cl_rel}#requirements-status |"
|
|
336
451
|
for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
|
|
337
452
|
print(r)
|
|
338
453
|
print(new_row)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// tf-theme.mjs — draw a page in its light or its dark theme, and measure what a reader sees in it.
|
|
2
|
+
// Shared by tf-verify-screens and tf-mockup-parity, as tf-login.mjs is (Lekhak TF-024).
|
|
3
|
+
//
|
|
4
|
+
// Why: every screen was graded in light mode only. Lekhak's owner then found 20 of 20 desktop admin
|
|
5
|
+
// screens unreadable in dark mode (text at 1.03 : 1, white panels inside a dark shell), all Verified.
|
|
6
|
+
//
|
|
7
|
+
// A theme is put on a page the two ways a stack listens for one, knowing nothing about the stack:
|
|
8
|
+
// - the browser's colour scheme (prefers-color-scheme), emulated;
|
|
9
|
+
// - the light/dark value of every data-*theme/mode/scheme attribute on <html> and <body>, and the
|
|
10
|
+
// light/dark mode classes there (dark, theme-dark …). A page that declares none is given
|
|
11
|
+
// data-theme and data-bs-theme; a page with no such class is given, for dark, the class `dark`
|
|
12
|
+
// (Tailwind's switch, and the one an app setting data-theme and the class together also reads).
|
|
13
|
+
// Each call returns a function that puts the page back the way it was: an attached desktop head is
|
|
14
|
+
// the viewer's app. A page whose painted surface does not change between the two has no dark theme;
|
|
15
|
+
// `surface` and `differs` tell. `audit` is the contrast rule the visual check applies.
|
|
16
|
+
|
|
17
|
+
export const THEMES = ['light', 'dark'];
|
|
18
|
+
|
|
19
|
+
// --themes light,dark | light | dark | auto (the default: light, and dark when the app has one)
|
|
20
|
+
export function parseThemes(v) {
|
|
21
|
+
const s = String(v || 'auto').toLowerCase().trim();
|
|
22
|
+
if (s === 'auto') return null;
|
|
23
|
+
const t = [...new Set(s.split(',').map((x) => x.trim()).filter((x) => THEMES.includes(x)))];
|
|
24
|
+
return t.length ? t : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FORCE = (want) => {
|
|
28
|
+
const MODE = /^(?:(?:theme|mode|scheme|color)[-_])?(?:dark|light)(?:[-_](?:theme|mode|scheme))?$/i;
|
|
29
|
+
const other = want === 'dark' ? /light/ig : /dark/ig;
|
|
30
|
+
const before = [];
|
|
31
|
+
let declared = false, classed = false;
|
|
32
|
+
for (const [where, el] of [['html', document.documentElement], ['body', document.body]]) {
|
|
33
|
+
if (!el) continue;
|
|
34
|
+
for (const a of [...el.attributes]) {
|
|
35
|
+
if (!/^data-.*(theme|mode|scheme)/i.test(a.name) || !/(dark|light)/i.test(a.value)) continue;
|
|
36
|
+
declared = true;
|
|
37
|
+
before.push({ where, attr: a.name, value: a.value });
|
|
38
|
+
el.setAttribute(a.name, a.value.replace(other, want));
|
|
39
|
+
}
|
|
40
|
+
const toks = [...el.classList].filter((c) => MODE.test(c));
|
|
41
|
+
if (toks.length) {
|
|
42
|
+
declared = classed = true;
|
|
43
|
+
before.push({ where, cls: toks });
|
|
44
|
+
for (const c of toks) { el.classList.remove(c); el.classList.add(c.replace(other, want)); }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!declared) {
|
|
48
|
+
const h = document.documentElement;
|
|
49
|
+
for (const name of ['data-theme', 'data-bs-theme']) {
|
|
50
|
+
if (h.hasAttribute(name)) continue; // a theme by name (data-theme="corporate") is left alone
|
|
51
|
+
before.push({ where: 'html', attr: name, value: null });
|
|
52
|
+
h.setAttribute(name, want);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// a page with no light/dark class gets `dark` for the dark pass even when it declared data-theme:
|
|
56
|
+
// an app whose switch sets both (Lekhak) was otherwise drawn half dark, a state it never shows (Lekhak TF-033)
|
|
57
|
+
const h = document.documentElement;
|
|
58
|
+
if (want === 'dark' && !classed && !h.classList.contains('dark')) { before.push({ where: 'html', added: 'dark' }); h.classList.add('dark'); }
|
|
59
|
+
return before;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const RESTORE = (before) => {
|
|
63
|
+
for (const b of [...before].reverse()) {
|
|
64
|
+
const el = b.where === 'body' ? document.body : document.documentElement;
|
|
65
|
+
if (!el) continue;
|
|
66
|
+
if (b.added) el.classList.remove(b.added);
|
|
67
|
+
else if (b.cls) {
|
|
68
|
+
for (const c of [...el.classList]) if (/^(?:(?:theme|mode|scheme|color)[-_])?(?:dark|light)(?:[-_](?:theme|mode|scheme))?$/i.test(c)) el.classList.remove(c);
|
|
69
|
+
for (const c of b.cls) el.classList.add(c);
|
|
70
|
+
} else if (b.value === null) el.removeAttribute(b.attr);
|
|
71
|
+
else el.setAttribute(b.attr, b.value);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Draws the page in `want` and returns the function that puts it back.
|
|
76
|
+
export async function useTheme(page, want, settle = 350) {
|
|
77
|
+
await page.emulateMedia({ colorScheme: want }).catch(() => {});
|
|
78
|
+
const before = await page.evaluate(FORCE, want).catch(() => null);
|
|
79
|
+
if (settle) await page.waitForTimeout(settle);
|
|
80
|
+
return async () => {
|
|
81
|
+
if (before) await page.evaluate(RESTORE, before).catch(() => {});
|
|
82
|
+
await page.emulateMedia({ colorScheme: null }).catch(() => {});
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The luminance painted at five points of the window (and behind <body>), 0 = black, 1 = white.
|
|
87
|
+
const SURFACE = () => {
|
|
88
|
+
// the browser converts the colour, never a regex: a page themed in oklch() reads back as "oklch(…)",
|
|
89
|
+
// which an rgb() pattern skipped, so every surface counted as white and no dark theme was seen (Sevak TF-005)
|
|
90
|
+
const cv = document.createElement('canvas'); cv.width = cv.height = 1;
|
|
91
|
+
const cx = cv.getContext('2d', { willReadFrequently: true });
|
|
92
|
+
const parse = (c) => { if (!c || c === 'transparent' || !cx) return null; cx.clearRect(0, 0, 1, 1); cx.fillStyle = 'rgba(0,0,0,0)'; cx.fillStyle = String(c); cx.fillRect(0, 0, 1, 1); const d = cx.getImageData(0, 0, 1, 1).data; return [d[0], d[1], d[2], d[3] / 255]; };
|
|
93
|
+
const lum = (c) => { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * f(c[0]) + 0.7152 * f(c[1]) + 0.0722 * f(c[2]); };
|
|
94
|
+
const behind = (el) => {
|
|
95
|
+
const layers = [];
|
|
96
|
+
for (let e = el; e; e = e.parentElement) { const c = parse(getComputedStyle(e).backgroundColor); if (c && c[3] > 0) { layers.push(c); if (c[3] >= 0.99) break; } }
|
|
97
|
+
let out = layers.length && layers[layers.length - 1][3] >= 0.99 ? layers.pop() : [255, 255, 255, 1];
|
|
98
|
+
while (layers.length) { const t = layers.pop(), a = t[3]; out = [t[0] * a + out[0] * (1 - a), t[1] * a + out[1] * (1 - a), t[2] * a + out[2] * (1 - a), 1]; }
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
const w = window.innerWidth, h = window.innerHeight;
|
|
102
|
+
const pts = [[0.5, 0.5], [0.2, 0.3], [0.8, 0.3], [0.2, 0.8], [0.8, 0.8]];
|
|
103
|
+
return [document.body, ...pts.map(([x, y]) => document.elementFromPoint(x * w, y * h))].map((el) => (el ? +lum(behind(el)).toFixed(3) : null));
|
|
104
|
+
};
|
|
105
|
+
export const surface = (page) => page.evaluate(SURFACE).catch(() => null);
|
|
106
|
+
// two surface readings differ when any point's luminance moved by more than 0.15
|
|
107
|
+
export const differs = (a, b) => !!a && !!b && a.some((x, i) => x !== null && b[i] !== null && Math.abs(x - b[i]) > 0.15);
|
|
108
|
+
// a reading that is mostly dark: the theme a mockup that declares none is drawn in
|
|
109
|
+
export const looksDark = (s) => !!s && s.filter((x) => x !== null && x < 0.2).length > s.filter((x) => x !== null).length / 2;
|
|
110
|
+
|
|
111
|
+
// The contrast rule (Lekhak TF-024), measured on what is painted:
|
|
112
|
+
// - every visible text run and form-field value reaches `min` : 1 (3.0, WCAG AA for large text)
|
|
113
|
+
// against the first opaque background behind it; disabled controls are exempt, as WCAG exempts
|
|
114
|
+
// them, and text over an image or a gradient is not measured (the colour behind it is not one);
|
|
115
|
+
// - in dark mode, no box larger than 2% of the window paints a background lighter than 0.8
|
|
116
|
+
// relative luminance (a white card or panel left in a dark shell).
|
|
117
|
+
// Returns { low: [...], light: [...], low_n, light_n }, each list cut to `cap`.
|
|
118
|
+
const AUDIT = ({ dark, min, cap }) => {
|
|
119
|
+
const cv = document.createElement('canvas'); cv.width = cv.height = 1; // oklch() and every other syntax, as SURFACE
|
|
120
|
+
const cx = cv.getContext('2d', { willReadFrequently: true });
|
|
121
|
+
const parse = (c) => { if (!c || c === 'transparent' || !cx) return null; cx.clearRect(0, 0, 1, 1); cx.fillStyle = 'rgba(0,0,0,0)'; cx.fillStyle = String(c); cx.fillRect(0, 0, 1, 1); const d = cx.getImageData(0, 0, 1, 1).data; return [d[0], d[1], d[2], d[3] / 255]; };
|
|
122
|
+
const lum = (c) => { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * f(c[0]) + 0.7152 * f(c[1]) + 0.0722 * f(c[2]); };
|
|
123
|
+
const ratio = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
|
|
124
|
+
const blend = (t, u) => { const a = t[3]; return [t[0] * a + u[0] * (1 - a), t[1] * a + u[1] * (1 - a), t[2] * a + u[2] * (1 - a), 1]; };
|
|
125
|
+
const behind = (el) => {
|
|
126
|
+
const layers = [];
|
|
127
|
+
for (let e = el; e; e = e.parentElement) {
|
|
128
|
+
const cs = getComputedStyle(e);
|
|
129
|
+
if (cs.backgroundImage && cs.backgroundImage !== 'none') return null; // an image or a gradient
|
|
130
|
+
const c = parse(cs.backgroundColor);
|
|
131
|
+
if (c && c[3] > 0) { layers.push(c); if (c[3] >= 0.99) break; }
|
|
132
|
+
}
|
|
133
|
+
let out = layers.length && layers[layers.length - 1][3] >= 0.99 ? layers.pop() : [255, 255, 255, 1];
|
|
134
|
+
while (layers.length) out = blend(layers.pop(), out);
|
|
135
|
+
return out;
|
|
136
|
+
};
|
|
137
|
+
const visible = (el) => {
|
|
138
|
+
const r = el.getBoundingClientRect();
|
|
139
|
+
if (r.width < 2 || r.height < 2) return false;
|
|
140
|
+
for (let e = el; e; e = e.parentElement) {
|
|
141
|
+
const s = getComputedStyle(e);
|
|
142
|
+
if (s.display === 'none' || s.visibility === 'hidden' || Number(s.opacity) < 0.1) return false;
|
|
143
|
+
}
|
|
144
|
+
return r.bottom > 0 && r.right > 0;
|
|
145
|
+
};
|
|
146
|
+
const where = (el) => {
|
|
147
|
+
const id = el.getAttribute('data-testid');
|
|
148
|
+
const cls = String(el.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 2).join('.');
|
|
149
|
+
return `${el.tagName.toLowerCase()}${id ? `[data-testid=${id}]` : ''}${cls ? '.' + cls : ''}`;
|
|
150
|
+
};
|
|
151
|
+
const rgb = (c) => `rgb(${c.slice(0, 3).map(Math.round).join(',')})`;
|
|
152
|
+
const EXEMPT = '[disabled],[aria-disabled="true"],svg,script,style,noscript,template';
|
|
153
|
+
const low = [], light = [], seen = new Set();
|
|
154
|
+
const measure = (el, text) => {
|
|
155
|
+
const fg = parse(getComputedStyle(el).color);
|
|
156
|
+
if (!fg || fg[3] < 0.05) return;
|
|
157
|
+
const bg = behind(el);
|
|
158
|
+
if (!bg) return;
|
|
159
|
+
const r = ratio(blend(fg, bg), bg);
|
|
160
|
+
const key = where(el) + '|' + text.slice(0, 30);
|
|
161
|
+
if (r < min && !seen.has(key)) { seen.add(key); low.push({ text: text.slice(0, 40), fg: rgb(fg), bg: rgb(bg), ratio: Math.round(r * 100) / 100, where: where(el) }); }
|
|
162
|
+
};
|
|
163
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
164
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
165
|
+
const text = (n.textContent || '').trim();
|
|
166
|
+
const el = n.parentElement;
|
|
167
|
+
if (!text || !el || !/[\p{L}\p{N}]/u.test(text) || el.closest(EXEMPT) || !visible(el)) continue; // icon glyphs are not text a reader reads
|
|
168
|
+
measure(el, text);
|
|
169
|
+
}
|
|
170
|
+
for (const el of document.querySelectorAll('input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=hidden]):not([type=color]),textarea,select')) {
|
|
171
|
+
if (el.disabled || el.closest(EXEMPT) || !visible(el)) continue;
|
|
172
|
+
const text = String(el.value || '').trim();
|
|
173
|
+
if (text) measure(el, text);
|
|
174
|
+
}
|
|
175
|
+
if (dark) {
|
|
176
|
+
const area = window.innerWidth * window.innerHeight, flagged = [];
|
|
177
|
+
for (const el of document.querySelectorAll('body *')) {
|
|
178
|
+
if (el.closest('img,svg,video,canvas,picture,iframe') || flagged.some((f) => f.contains(el)) || !visible(el)) continue;
|
|
179
|
+
const c = parse(getComputedStyle(el).backgroundColor);
|
|
180
|
+
if (!c || c[3] < 0.5) continue;
|
|
181
|
+
const r = el.getBoundingClientRect();
|
|
182
|
+
const shown = Math.max(0, Math.min(r.right, window.innerWidth) - Math.max(r.left, 0)) * Math.max(0, Math.min(r.bottom, window.innerHeight) - Math.max(r.top, 0));
|
|
183
|
+
if (shown / area > 0.02 && lum(c) > 0.8) { flagged.push(el); light.push({ bg: rgb(c), share: Math.round(shown / area * 100), where: where(el), text: (el.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 40) }); }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
low.sort((a, b) => a.ratio - b.ratio);
|
|
187
|
+
return { low: low.slice(0, cap), light: light.slice(0, cap), low_n: low.length, light_n: light.length };
|
|
188
|
+
};
|
|
189
|
+
export const audit = (page, dark, min = 3.0, cap = 5) => page.evaluate(AUDIT, { dark, min, cap }).catch(() => null);
|