@techierathore/techieflow 1.1.0 → 1.2.0
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/_smoke-test-policy.md +7 -5
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +15 -7
- 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 +25 -2
- package/.tfcore/hooks/guard-db.sh +106 -9
- package/.tfcore/hooks/guard-status-html.sh +5 -1
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- 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/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_smoke-test-policy.md +7 -5
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +4 -4
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +1 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +6 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -0
- package/.tfcore/templates/stack-defaults/dotnet.md +5 -1
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
- package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- 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-brd-status.py +10 -1
- package/.tfcore/utils/tf-build-list.py +16 -1
- package/.tfcore/utils/tf-build.sh +75 -0
- package/.tfcore/utils/tf-checklist-edit.py +28 -3
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +6 -2
- package/.tfcore/utils/tf-doc-check.py +124 -21
- package/.tfcore/utils/tf-doc-tests.sh +40 -3
- package/.tfcore/utils/tf-emit.sh +40 -10
- package/.tfcore/utils/tf-fix-close.sh +13 -7
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +361 -28
- package/.tfcore/utils/tf-mockup-parity.sh +14 -3
- package/.tfcore/utils/tf-owner-text.py +27 -5
- package/.tfcore/utils/tf-phase.sh +24 -4
- package/.tfcore/utils/tf-render-html.py +19 -3
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-facts.py +90 -15
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +30 -4
- package/.tfcore/utils/tf-verify-boot.sh +296 -21
- package/.tfcore/utils/tf-verify-emit.sh +37 -17
- package/.tfcore/utils/tf-verify-list.py +219 -20
- 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 +184 -20
- package/.tfcore/utils/tf-verify-screens.sh +17 -4
- package/.tfcore/utils/tf-verify-tests.sh +107 -17
- package/.tfcore/utils/tf-verify-verdict.py +118 -15
- 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_feedback.py +86 -18
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +2 -2
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +14 -3
- 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
|
@@ -59,13 +59,31 @@ def load_words(root):
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def upstream_state(root):
|
|
62
|
-
"""Every entry id in the project's feedback files -> (state, where),
|
|
63
|
-
every script uses (tf_feedback.py), so this check and PROJECT-STATUS can never disagree."""
|
|
62
|
+
"""Every entry id in the project's feedback files -> [(state, where, names)], one per file, from
|
|
63
|
+
the one reader every script uses (tf_feedback.py), so this check and PROJECT-STATUS can never disagree."""
|
|
64
64
|
sys.path.insert(0, HERE)
|
|
65
65
|
import tf_feedback
|
|
66
66
|
return tf_feedback.state_map(root)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def resolve(rid, cands, unit, text):
|
|
70
|
+
"""The feedback file an id in this unit means (TrBlazeUI TF-006). One file holding it: that one.
|
|
71
|
+
Several: the files whose project the unit names ("Lekhak TR-002"), else the files the whole text
|
|
72
|
+
names. -> (state, where), or (None, the files) when it stays several that disagree."""
|
|
73
|
+
if len(cands) == 1:
|
|
74
|
+
return cands[0][:2]
|
|
75
|
+
for txt in (unit, text):
|
|
76
|
+
# a name every candidate file shares (the library's own) says nothing about which one
|
|
77
|
+
shared = set.intersection(*(set(c[2]) for c in cands))
|
|
78
|
+
hit = [c for c in cands if any(n not in shared and re.search(rf"(?<![\w-]){re.escape(n)}(?![\w-])", txt) for n in c[2])]
|
|
79
|
+
if hit:
|
|
80
|
+
cands = hit
|
|
81
|
+
break
|
|
82
|
+
if len({c[0] for c in cands}) == 1:
|
|
83
|
+
return cands[0][:2]
|
|
84
|
+
return None, ", ".join(c[1].split(",")[0] for c in cands)
|
|
85
|
+
|
|
86
|
+
|
|
69
87
|
def split_text(text):
|
|
70
88
|
"""-> (prose lines [(n, line)], the text inside code blocks)."""
|
|
71
89
|
prose, fenced, fence = [], [], None
|
|
@@ -142,9 +160,13 @@ def check(text, label, root, final=False, words=None, ups=None):
|
|
|
142
160
|
for n, txt, header in us:
|
|
143
161
|
for rid in dict.fromkeys(ANY_ID.findall(txt)):
|
|
144
162
|
if rid in ups:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
st, where = resolve(rid, ups[rid], txt, text)
|
|
164
|
+
if st is None:
|
|
165
|
+
fail(n, f'{rid} is an entry in several feedback files ({where}); name whose it is with the id, '
|
|
166
|
+
f'as in "{ups[rid][0][2][0]} {rid}"')
|
|
167
|
+
continue
|
|
168
|
+
mentions.setdefault((rid, st, where), []).append((n, txt, header))
|
|
169
|
+
for (rid, st, where), ms in sorted(mentions.items(), key=lambda kv: kv[1][0][0]):
|
|
148
170
|
if st in ("fixed", "closed"):
|
|
149
171
|
# read the sentence that names it, or its table row: "TF-901 is filed. X is fixed." is wrong
|
|
150
172
|
said = lambda txt, h: [txt] if h is not None else [s for s in re.split(r"(?<=[.!?])\s+", txt) if rid in s]
|
|
@@ -42,12 +42,25 @@ case "${1:-}" in
|
|
|
42
42
|
# the outer command as "outer", written FIRST so a greedy `.*"started":"…"` read still takes this
|
|
43
43
|
# command's own start. tf-verify-emit.sh and tf-fix-close.sh read it to tell the two starts
|
|
44
44
|
# apart (TF-052: the inline verify took the fix's start and its record swallowed the fix's).
|
|
45
|
-
OUTER=""
|
|
45
|
+
OUTER=""; CHAINED=""
|
|
46
46
|
if [[ -f "$FILE" && -z "$(find "$FILE" -mmin +1440 2>/dev/null)" ]]; then
|
|
47
47
|
OC="$(sed -n 's/.*"cmd":"\([^"]*\)".*/\1/p' "$FILE")"; OA="$(sed -n 's/.*"app":"\([^"]*\)".*/\1/p' "$FILE")"
|
|
48
48
|
OS="$(sed -n 's/.*"started":"\([^"]*\)".*/\1/p' "$FILE")"
|
|
49
49
|
[[ -n "$OC" && -n "$OS" && "$OC" != "$CMD" && "$OC" != "goal" && "$OS" != "$NOW" ]] \
|
|
50
50
|
&& OUTER="\"outer\":{\"cmd\":\"$OC\",\"app\":\"$OA\",\"started\":\"$OS\"},"
|
|
51
|
+
# A command the task files run inside another one keeps the outer command's document baseline
|
|
52
|
+
# (TrBlazeUI TF-002: *triage-and-fix started verify-phase and metrics-report, each wrote a new
|
|
53
|
+
# baseline, and the rows the triage had just added had their findings relabelled OLD). The pairs
|
|
54
|
+
# are the chains the task files name: build-phase, fix-issues and triage-and-fix run verify-phase;
|
|
55
|
+
# triage-and-fix runs metrics-report. The marker is never ended, so a standalone verify after a
|
|
56
|
+
# fix also keeps the fix's baseline: that can only make the gate stricter, never hide a finding.
|
|
57
|
+
OOC="$(sed -n 's/^{"outer":{"cmd":"\([^"]*\)".*/\1/p' "$FILE")"
|
|
58
|
+
case "$CMD" in
|
|
59
|
+
verify-phase|metrics-report)
|
|
60
|
+
for c in "$OC" "$OOC"; do
|
|
61
|
+
case "$c" in build-phase|fix-issues|triage-and-fix) CHAINED="$c"; break ;; esac
|
|
62
|
+
done ;;
|
|
63
|
+
esac
|
|
51
64
|
fi
|
|
52
65
|
printf '{%s"cmd":"%s","app":"%s","started":"%s"}\n' "$OUTER" "$CMD" "$APP" "$NOW" > "$FILE"
|
|
53
66
|
echo "$NOW"
|
|
@@ -56,10 +69,17 @@ case "${1:-}" in
|
|
|
56
69
|
# status file already carry when the command starts are recorded, so the gate and the Stop
|
|
57
70
|
# hook block only on findings this command introduces (Schemas §7.1 decision 8: old projects
|
|
58
71
|
# are repaired through *amend-docs, never by whichever command happens to run next).
|
|
59
|
-
if [[ -
|
|
72
|
+
if [[ -n "$CHAINED" ]]; then
|
|
73
|
+
echo "tf-phase: $CMD runs inside $CHAINED — keeping its document baseline (findings since $CHAINED started stay FAIL)" >&2
|
|
74
|
+
elif [[ -x "$ROOT/.tfcore/utils/tf-doc-check.sh" || -f "$ROOT/.tfcore/utils/tf-doc-check.sh" ]]; then
|
|
75
|
+
# Every file `tf-doc-check.sh --app` reads, not only the checklists and the status file: *amend-docs
|
|
76
|
+
# and day-1 close on `--app`, so the BRD's, the Architecture's and the mockups' old findings read
|
|
77
|
+
# as new and blocked the run (Lekhak TF-023, 131 of them). A baseline only relabels what was
|
|
78
|
+
# already there, so a finding the command introduces still FAILs.
|
|
60
79
|
docs=(); for f in "$ROOT"/docs/*-Checklist.md "$ROOT"/PROJECT-STATUS.md; do [[ -f "$f" ]] && docs+=("$f"); done
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
appargs=(); [[ -n "$APP" && -d "$ROOT/docs" ]] && appargs=(--app "$APP")
|
|
81
|
+
if [[ ${#docs[@]} -gt 0 || ${#appargs[@]} -gt 0 ]]; then
|
|
82
|
+
( cd "$ROOT" && bash .tfcore/utils/tf-doc-check.sh --root "$ROOT" --baseline-write ${appargs[@]+"${appargs[@]}"} ${docs[@]+"${docs[@]}"} 2>/dev/null | tail -1 >&2 ) || true
|
|
63
83
|
fi
|
|
64
84
|
fi
|
|
65
85
|
# The framework's own scripts, against THIS project's real files, before the command
|
|
@@ -101,6 +101,20 @@ class Slugger(object):
|
|
|
101
101
|
self.seen[s] = c
|
|
102
102
|
return s if c == 1 else "%s-%d" % (s, c)
|
|
103
103
|
|
|
104
|
+
def github(self, text):
|
|
105
|
+
"""The id GitHub gives the same heading, which a markdown table of contents links to: digits
|
|
106
|
+
kept, punctuation dropped, spaces to hyphens, a repeat suffixed -1, -2 (AppManager TF-029:
|
|
107
|
+
`### 3.7 Group Service (GroupSvc)` is #37-group-service-groupsvc there, #group-service-groupsvc
|
|
108
|
+
here, so every numbered TOC link in the rendered guide went nowhere)."""
|
|
109
|
+
if not hasattr(self, "gh_seen"):
|
|
110
|
+
self.gh_seen = {}
|
|
111
|
+
s = re.sub(r"<[^>]+>", "", text)
|
|
112
|
+
s = re.sub(r"[*`~]", "", s).strip().lower()
|
|
113
|
+
s = re.sub(r"[^\w\- ]", "", s, flags=re.UNICODE).replace(" ", "-")
|
|
114
|
+
c = self.gh_seen.get(s, 0)
|
|
115
|
+
self.gh_seen[s] = c + 1
|
|
116
|
+
return s if c == 0 else "%s-%d" % (s, c)
|
|
117
|
+
|
|
104
118
|
|
|
105
119
|
# ----------------------------------------------------------------------------
|
|
106
120
|
# Inline conversion
|
|
@@ -205,11 +219,13 @@ class Renderer(object):
|
|
|
205
219
|
|
|
206
220
|
def heading(self, level, text):
|
|
207
221
|
slug = self.sl.slug(text)
|
|
222
|
+
gh = self.sl.github(text) # every level, so its repeat count matches GitHub's
|
|
208
223
|
if level in (2, 3, 4):
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
alias = '<a id="%s"></a>' % gh if gh and gh != slug else ""
|
|
225
|
+
self.emit('<h%d id="%s">%s%s<a class="anchor-link" href="#%s">#</a></h%d>'
|
|
226
|
+
% (level, slug, alias, inline(text), slug, level))
|
|
211
227
|
else:
|
|
212
|
-
self.emit("<h%d>%s</h%d>" % (level, inline(text), level))
|
|
228
|
+
self.emit("<h%d%s>%s</h%d>" % (level, ' id="%s"' % gh if gh else "", inline(text), level))
|
|
213
229
|
if level == 2:
|
|
214
230
|
self.h2 += 1
|
|
215
231
|
if level in (2, 3):
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""tf-req-ids.py — which rows a test's name claims (Chatur TF-012). Imported by tf-verify-tests.sh.
|
|
2
|
+
|
|
3
|
+
A test proves the ids its name STARTS with, nothing else. An xUnit theory reported as
|
|
4
|
+
`REQ-UI-048 requirement ids are found in test names(aName: "REQ-FN-053 …")` passed REQ-FN-053 and
|
|
5
|
+
REQ-FN-055, which no test had ever touched. The ids are read from the leading run of REQ tokens
|
|
6
|
+
(separated by spaces, commas, slashes, "and" …, after at most one short capital tag such as FIX or
|
|
7
|
+
UAT), before any argument list. An id named later in the name is the test's subject matter, not its
|
|
8
|
+
claim. A theory whose argument is NAMED for the row it proves (`row: "REQ-RAG-125"`, also req, reqId,
|
|
9
|
+
requirement) claims that row: TechieRag's per-row cases.
|
|
10
|
+
"""
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
ID = re.compile(r"(REQ-(?:UI|FN|NFR|RAG)-\d{3})", re.I)
|
|
14
|
+
LEAD_SEP = re.compile(r"(?:[\s,;/&+|·:\[\]–—-]|\band\b)*", re.I)
|
|
15
|
+
TAG = re.compile(r"\s*\[?[A-Z][A-Z0-9]{1,5}\]?:?\s+(?=REQ-)")
|
|
16
|
+
ROW_ARG = re.compile(r"\b(?:row|req|reqid|req_id|requirement)\s*[:=]\s*[\"']?(REQ-(?:UI|FN|NFR|RAG)-\d{3})\b", re.I)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ids_of(name):
|
|
20
|
+
"""the ids one name claims, in order"""
|
|
21
|
+
name = str(name or "")
|
|
22
|
+
head, out = name.split("(", 1)[0], [x.upper() for x in ROW_ARG.findall(name)]
|
|
23
|
+
t = TAG.match(head)
|
|
24
|
+
p = t.end() if t else 0
|
|
25
|
+
while True:
|
|
26
|
+
p = LEAD_SEP.match(head, p).end()
|
|
27
|
+
m = ID.match(head, p)
|
|
28
|
+
if not m:
|
|
29
|
+
return out
|
|
30
|
+
out.append(m.group(1).upper())
|
|
31
|
+
p = m.end()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def mentioned(name):
|
|
35
|
+
"""every id the name carries anywhere"""
|
|
36
|
+
return {x.upper() for x in ID.findall(str(name or ""))}
|
|
@@ -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.
|
|
@@ -170,8 +173,19 @@ def existing_log_rows(status_text):
|
|
|
170
173
|
return rows
|
|
171
174
|
|
|
172
175
|
|
|
173
|
-
def handoff_ran(log_rows):
|
|
174
|
-
|
|
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
|
|
175
189
|
|
|
176
190
|
|
|
177
191
|
def feedback_lines(root, app):
|
|
@@ -193,6 +207,13 @@ def config_phase(root):
|
|
|
193
207
|
return 1
|
|
194
208
|
|
|
195
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
|
+
|
|
196
217
|
def phase_prefix(app, phase):
|
|
197
218
|
return f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
198
219
|
|
|
@@ -222,9 +243,39 @@ def usage_guide(app):
|
|
|
222
243
|
return vl.usage_guide(app)
|
|
223
244
|
|
|
224
245
|
|
|
225
|
-
def
|
|
226
|
-
"""
|
|
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."""
|
|
227
277
|
verdicts = verdicts or {}
|
|
278
|
+
pflag = f" --phase {phase}" if multi else ""
|
|
228
279
|
|
|
229
280
|
def ids(lst):
|
|
230
281
|
return ", ".join(r["id"] for r in lst[:6]) + (" …" if len(lst) > 6 else "")
|
|
@@ -242,7 +293,8 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
|
242
293
|
now = [r for r in rows if not r.get("roadmap")]
|
|
243
294
|
unbuilt = [r for r in now if r["status"].lower() not in TERMINAL | PASS_THROUGH | BUILT | OWNER]
|
|
244
295
|
defects = [r for r in now if has_defect(r)]
|
|
245
|
-
|
|
296
|
+
waiting, unmeasured = to_verify(now, verdicts)
|
|
297
|
+
built = waiting + unmeasured
|
|
246
298
|
owner = [r for r in now if r["status"].lower() in OWNER]
|
|
247
299
|
verified, counted, na = figures(rows)
|
|
248
300
|
q = f"{verified} of {counted} verified" + (f", {na} not applicable" if na else "")
|
|
@@ -262,31 +314,34 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
|
262
314
|
# loop that looks like progress. Say what it actually needs: data, a changed
|
|
263
315
|
# acceptance line, or the row marked N/A. Only the owner can choose. FR-69's sibling
|
|
264
316
|
# case; the verdicts come from the ledger the verifier already writes.
|
|
265
|
-
unmeasured = [r for r in built if verdicts.get(r["id"]) == "NOT-TESTED"]
|
|
266
|
-
waiting = [r for r in built if r not in unmeasured]
|
|
267
317
|
if unmeasured and not waiting:
|
|
268
318
|
line = (f"(owner) {len(unmeasured)} row(s) cannot be measured here — create the data, "
|
|
269
319
|
f"change the acceptance line, or mark the row N/A: {ids(unmeasured)}")
|
|
270
320
|
return ("Verify", f"{len(unmeasured)} not measurable, {q}", line, line,
|
|
271
321
|
f"every test carrying these ids was skipped, so another verify run changes nothing: {ids(unmeasured)}")
|
|
272
|
-
|
|
273
|
-
scope = "ui" if len(ui) == len(waiting) else ("functional" if not ui else "all")
|
|
322
|
+
scope = scope_of(waiting)
|
|
274
323
|
reason = f"{len(waiting)} rows are built and not verified: {ids(waiting)}"
|
|
275
324
|
if unmeasured:
|
|
276
325
|
reason += (f"; {len(unmeasured)} more could not be measured (every test skipped) "
|
|
277
326
|
f"and need the owner: {ids(unmeasured)}")
|
|
278
327
|
return ("Verify", f"{len(waiting)} to verify, {q}",
|
|
279
|
-
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}",
|
|
280
329
|
reason)
|
|
281
330
|
if owner:
|
|
282
|
-
if handoff_ran(log_rows):
|
|
331
|
+
if handoff_ran(log_rows, app, phase):
|
|
283
332
|
line = f"(owner-run) {usage_guide(app)} — {ids(owner)}"
|
|
284
333
|
return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
|
|
285
334
|
f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
|
|
286
335
|
return ("Handoff", f"{len(owner)} owner-run, {q}",
|
|
287
336
|
f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
|
|
288
337
|
f"{len(owner)} rows are Owner-UAT and handoff has not run yet")
|
|
289
|
-
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")
|
|
290
345
|
line = "(owner) set current_phase to Released after UAT — no agent command"
|
|
291
346
|
return ("UAT", f"handoff done, {q}", line, line,
|
|
292
347
|
"every row in this phase's scope is terminal and handoff has run; waiting on the owner")
|
|
@@ -320,12 +375,32 @@ def main(argv):
|
|
|
320
375
|
st_text = read(st_path) if os.path.isfile(st_path) else ""
|
|
321
376
|
log_rows = existing_log_rows(st_text)
|
|
322
377
|
vdate, vresult = last_verify(root, app)
|
|
323
|
-
|
|
378
|
+
verdicts = ledger_verdicts(root)
|
|
379
|
+
others = other_phases(root, app, phase_n, verdicts)
|
|
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}"
|
|
324
400
|
roadmap = [r["id"] for r in rows if r["roadmap"]]
|
|
325
401
|
if roadmap:
|
|
326
402
|
reason += (f"; {len(roadmap)} roadmap row(s) are not in this phase's scope and are left for the owner "
|
|
327
403
|
f"(*amend-docs): {', '.join(roadmap)}")
|
|
328
|
-
pname, ptotal = phase_row(root, app, phase_n)
|
|
329
404
|
if ptotal or phase_n > 1:
|
|
330
405
|
tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
|
|
331
406
|
phase = f"{tag} · {phase}"
|
|
@@ -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);
|
|
@@ -13,8 +13,10 @@ row in whichever docs/<App>-Checklist.md or docs/<App>-P<N>-Checklist.md holds i
|
|
|
13
13
|
|
|
14
14
|
demote the row goes to Needs re-verify (% capped at 75) with a dated Remark holding the symptom in the
|
|
15
15
|
reporter's words, the evidence path and the kind.
|
|
16
|
-
new a defect no row covers: the next free id in the prefix,
|
|
17
|
-
|
|
16
|
+
new a defect no row covers: the next free id in the prefix, across every checklist of the app and the
|
|
17
|
+
telemetry streams, a Not Started row, a detail entry with the acceptance line and the marker
|
|
18
|
+
BRD-pending (*amend-docs gives it its BRD item). Without --prefix: FN, or the one prefix a
|
|
19
|
+
checklist with no FN row uses; with several such prefixes it asks for --prefix.
|
|
18
20
|
note a dated remark on the nearest row, no status change (could not reproduce).
|
|
19
21
|
close what triage owes the telemetry, from the actions above (tests/.artifacts/verify/triage.json):
|
|
20
22
|
one gate record per demoted or new row with gate "escaped" (no check caught it), one miss per row
|
|
@@ -31,6 +33,7 @@ import datetime
|
|
|
31
33
|
import importlib.util
|
|
32
34
|
import json
|
|
33
35
|
import os
|
|
36
|
+
import re
|
|
34
37
|
import subprocess
|
|
35
38
|
import sys
|
|
36
39
|
|
|
@@ -62,6 +65,17 @@ def cfg_phase():
|
|
|
62
65
|
return 1
|
|
63
66
|
|
|
64
67
|
|
|
68
|
+
def cfg_project_type():
|
|
69
|
+
"""metrics.project_type from core-config.yaml: a library or docs project has no mockups."""
|
|
70
|
+
p = os.path.join(".tfcore", "core-config.yaml")
|
|
71
|
+
if os.path.isfile(p):
|
|
72
|
+
import re
|
|
73
|
+
m = re.search(r"(?m)^\s+project_type:\s*(\S+)", open(p, encoding="utf-8").read())
|
|
74
|
+
if m:
|
|
75
|
+
return m.group(1).strip("'\"").lower()
|
|
76
|
+
return "app"
|
|
77
|
+
|
|
78
|
+
|
|
65
79
|
def phase_checklist(app, ph):
|
|
66
80
|
return os.path.join("docs", f"{app}-Checklist.md" if ph <= 1 else f"{app}-P{ph}-Checklist.md")
|
|
67
81
|
|
|
@@ -165,9 +179,21 @@ def main(argv):
|
|
|
165
179
|
die("new needs \"<title>\" \"<acceptance>\"")
|
|
166
180
|
title, acc = argv[3], argv[4]
|
|
167
181
|
cl = checklist(app, argv, ce)
|
|
168
|
-
prefix = opt(argv, "--prefix"
|
|
182
|
+
prefix = opt(argv, "--prefix")
|
|
183
|
+
if prefix is None:
|
|
184
|
+
# FN unless this checklist has no FN row: then the one prefix it uses is the default, and
|
|
185
|
+
# several are asked for (TechieRag TF-005: phase 3 is all RAG, and FN was a slip)
|
|
186
|
+
used = sorted(set(re.findall(r"(?m)^\|\s*REQ-([A-Z]+)-\d{3,}", ce.read(cl))))
|
|
187
|
+
if not used or "FN" in used:
|
|
188
|
+
prefix = "FN"
|
|
189
|
+
elif len(used) == 1:
|
|
190
|
+
prefix = used[0]
|
|
191
|
+
print(f"note: every row of {cl} is REQ-{prefix}-…, so the new row is one too; pass --prefix to choose another")
|
|
192
|
+
else:
|
|
193
|
+
die(f"{cl} has no REQ-FN rows; name the prefix: --prefix {'|'.join(used)}")
|
|
194
|
+
prefix = prefix.upper(); section = opt(argv, "--section"); ev = opt(argv, "--evidence")
|
|
169
195
|
src = opt(argv, "--source", "owner"); mockup = opt(argv, "--mockup")
|
|
170
|
-
if prefix == "UI" and not mockup:
|
|
196
|
+
if prefix == "UI" and not mockup and cfg_project_type() not in ("library", "docs"): # TrBlazeUI TF-003
|
|
171
197
|
print("note: a UI row needs a mockup link; pass --mockup docs/mockups/<screen>.html or the checker will flag it")
|
|
172
198
|
rid = ce.add_row(cl, prefix, title, acc, section=section, mockup=mockup,
|
|
173
199
|
remark=f"logged from {'production' if src == 'production' else 'UAT'} {ce.TODAY}" + (f" (evidence: {ev})" if ev else ""))
|