@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
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""tf-verify-list.py — the working list of a verify run (see tf-verify-list.sh).
|
|
3
3
|
|
|
4
|
-
bash .tfcore/utils/tf-verify-list.sh <App> <scope> [--phase N] [--json-out <file>]
|
|
4
|
+
bash .tfcore/utils/tf-verify-list.sh <App> <scope> [--phase N|all] [--json-out <file>]
|
|
5
5
|
|
|
6
6
|
scope: ui (REQ-UI rows) · functional (REQ-FN, REQ-NFR, REQ-RAG) · all · a comma list of ids.
|
|
7
|
+
--phase N grades phase N's checklist (default appPhase); --phase all only prints the phases that have
|
|
8
|
+
a checklist, phase 1 first, as "PHASES 1 2 3", and writes nothing: the task runs one pass per phase.
|
|
7
9
|
Reads the phase's checklist, the UIDesign, the BRD screens table and the UsageGuide, and prints:
|
|
8
10
|
- every row in scope with its status, acceptance line, screen, route, mockup and perf budget;
|
|
9
11
|
N/A rows are skipped, every other status is graded (a Verified row is re-confirmed)
|
|
10
12
|
- the screens to drive, each with its route, its mockup and the rows it owns; a dialog row
|
|
11
13
|
is listed under its parent screen (Schemas §2: a dialog is verified on its page)
|
|
12
14
|
- rows whose screen could not be resolved (graded by their test only)
|
|
13
|
-
- the test users from the UsageGuide
|
|
15
|
+
- the test users from the UsageGuide, as plain text (no Markdown)
|
|
16
|
+
- for each screen, from the code: the page file that serves its route, that file's project, and
|
|
17
|
+
the labels of the links or buttons to click from the app's menu to reach it (Lekhak TF-027)
|
|
14
18
|
Writes the same as JSON (default tests/.artifacts/verify/list.json) for the other verify scripts.
|
|
15
19
|
Python 3 standard library only. Exit 0 printed, 2 could not run.
|
|
16
20
|
"""
|
|
21
|
+
import html
|
|
17
22
|
import json
|
|
18
23
|
import os
|
|
19
24
|
import re
|
|
@@ -86,7 +91,7 @@ def entries_of(text):
|
|
|
86
91
|
h = re.match(r"^#{2,3}\s+(?:page|screen)\s*:\s*(.+)$", nxt, re.I)
|
|
87
92
|
if h:
|
|
88
93
|
current = h.group(1).strip()
|
|
89
|
-
e = {"section": current, "acceptance": "", "mockup": "", "brd": "", "perf_budget": ""}
|
|
94
|
+
e = {"section": current, "acceptance": "", "mockup": "", "brd": "", "perf_budget": "", "zoom_check": ""}
|
|
90
95
|
for j in range(i, min(i + 14, len(lines))):
|
|
91
96
|
l = lines[j]
|
|
92
97
|
if re.search(r"\*Acceptance:?\*|^\s*[-*]\s*Acceptance:", l, re.I) and not e["acceptance"]:
|
|
@@ -100,6 +105,9 @@ def entries_of(text):
|
|
|
100
105
|
pb = re.search(r"perf-budget:\s*([^|`]+)", l)
|
|
101
106
|
if pb and not e["perf_budget"]:
|
|
102
107
|
e["perf_budget"] = pb.group(1).strip()
|
|
108
|
+
zc = re.search(r"zoom-check:\s*(keys|kept)\b", l) # coding-standards-dotnet.md §9b
|
|
109
|
+
if zc and not e["zoom_check"]:
|
|
110
|
+
e["zoom_check"] = zc.group(1)
|
|
103
111
|
if j > i and re.match(r"^\s*(?:[-*]\s*)?<a id=|^## ", l):
|
|
104
112
|
break # the next entry starts: nothing below belongs to this row
|
|
105
113
|
out[rid] = e
|
|
@@ -117,14 +125,43 @@ def screens_of_uidesign(path):
|
|
|
117
125
|
m = re.match(r"###\s+Screen:\s*(.+?)\s*\(`?([^)`]+)`?\)\s*$", p.splitlines()[0] if p.strip() else "")
|
|
118
126
|
if not m:
|
|
119
127
|
continue
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
mocks = []
|
|
129
|
+
for mk in re.findall(r"((?:docs/)?mockups/[\w./-]+\.html)", p):
|
|
130
|
+
mk = mk if mk.startswith("docs/") else "docs/" + mk
|
|
131
|
+
if mk not in mocks:
|
|
132
|
+
mocks.append(mk)
|
|
133
|
+
name, route = m.group(1).strip(), m.group(2).strip()
|
|
134
|
+
tabs = split_by_mockup(name, route, mocks)
|
|
135
|
+
out.extend(tabs or [{"name": name, "route": route, "mockup": mocks[0] if mocks else ""}])
|
|
125
136
|
return out
|
|
126
137
|
|
|
127
138
|
|
|
139
|
+
def split_by_mockup(name, route, mocks):
|
|
140
|
+
"""One screen per mockup when the route takes one value and the entry links a mockup per value:
|
|
141
|
+
`/settings/{tab}` with settings-providers.html, settings-agents.html … is seven screens,
|
|
142
|
+
`/settings/providers` and so on. Kept as one screen it was never driven, and its rows were
|
|
143
|
+
Verified on their tests alone (Chatur TF-003). [] when the route or the mockups do not fit."""
|
|
144
|
+
params = re.findall(r"\{[^}]+\}", route)
|
|
145
|
+
if len(params) != 1 or len(mocks) < 2:
|
|
146
|
+
return []
|
|
147
|
+
stems = [os.path.basename(m)[:-5] for m in mocks]
|
|
148
|
+
prefix = os.path.commonprefix(stems)
|
|
149
|
+
prefix = prefix[:prefix.rfind("-") + 1] if "-" in prefix else ""
|
|
150
|
+
values = [s[len(prefix):] for s in stems]
|
|
151
|
+
if not prefix or not all(re.fullmatch(r"[a-z0-9][a-z0-9-]*", v) for v in values):
|
|
152
|
+
return []
|
|
153
|
+
return [{"name": f"{name} / {v}", "route": route.replace(params[0], v), "mockup": mk,
|
|
154
|
+
"route_pattern": route, "parent": name} for v, mk in zip(values, mocks)]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def seed_of(screen):
|
|
158
|
+
"""tests/verify/seed/<mockup name>.sh, when the project has one: it puts the running app in the
|
|
159
|
+
state the mockup draws before the screen is driven (Chatur TF-002). tf-verify-screens runs it."""
|
|
160
|
+
stem = os.path.basename(screen.get("mockup") or "")[:-5] or re.sub(r"[^a-z0-9]+", "-", screen["name"].lower()).strip("-")
|
|
161
|
+
p = os.path.join("tests", "verify", "seed", f"{stem}.sh")
|
|
162
|
+
return p if os.path.isfile(p) else ""
|
|
163
|
+
|
|
164
|
+
|
|
128
165
|
def screens_of_checklist(text, ent):
|
|
129
166
|
"""[{name, route, mockup}] from the checklist's own '## Page: Name (`/route`, …)' headings, at ## or
|
|
130
167
|
###, for a project with no UIDesign: the first route that can be opened as typed (no `{id}`),
|
|
@@ -172,6 +209,114 @@ def usage_guide(app):
|
|
|
172
209
|
return os.path.join("docs", f"{app}-UsageGuide.md")
|
|
173
210
|
|
|
174
211
|
|
|
212
|
+
def plain(cell):
|
|
213
|
+
"""A Markdown table cell as plain text: code spans, emphasis and links unwrapped."""
|
|
214
|
+
t = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", cell)
|
|
215
|
+
t = re.sub(r"`([^`]*)`", r"\1", t).replace("`", "")
|
|
216
|
+
return re.sub(r"(\*\*|__|\*)", "", t).strip()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --- how each screen is reached, read from the code (Lekhak TF-027) ---------------------------------
|
|
220
|
+
# A Blazor Hybrid head on a Mac shows mac2 no data-testid and no URL, so its screens are reached by
|
|
221
|
+
# clicking what a person clicks. The code says what that is: a page declares its route with @page, and
|
|
222
|
+
# a link or button anywhere names a route in href/Href with its visible text. A link in a file that is
|
|
223
|
+
# not a page (a layout, the menu) is on every screen; a link on a page is on that page only. Only the
|
|
224
|
+
# code of the screen's own project and the projects it references counts, so a website page is never
|
|
225
|
+
# looked for in the desktop app's menu.
|
|
226
|
+
CODE_DIRS = ("src", "source")
|
|
227
|
+
SKIP_DIRS = {"bin", "obj", "node_modules", ".git", ".tfbuild", "tests", "wwwroot"}
|
|
228
|
+
LINK = re.compile(r"<([A-Za-z][\w.]*)\b([^>]*?)\b[Hh]ref\s*=\s*\"([^\"]+)\"([^>]*)>(.*?)</\1\s*>", re.S)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def route_key(r):
|
|
232
|
+
"""'/admin/llm-signin/{ProviderId:long}' and 'admin/llm-signin/@p.Id' -> ('admin', 'llm-signin', '*')"""
|
|
233
|
+
r = re.split(r"[?#]", r.strip())[0].strip("/").lower()
|
|
234
|
+
return tuple("*" if ("{" in seg or "@" in seg) else seg for seg in r.split("/")) if r else ()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def label_of(inner):
|
|
238
|
+
t = re.sub(r"<[^>]+>", " ", inner)
|
|
239
|
+
if "@" in t:
|
|
240
|
+
return "" # a label the code computes is not known here
|
|
241
|
+
t = html.unescape(t)
|
|
242
|
+
t = "".join(ch for ch in t if ch.isalnum() or ch in " +-'&/.,()") # icon glyphs out
|
|
243
|
+
return re.sub(r"\s+", " ", t).strip()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def code_map():
|
|
247
|
+
"""-> projects {dir: [referenced dirs]}, pages {route_key: (file, project dir)}, links [(file, own keys, project, key, label)]"""
|
|
248
|
+
projects, pages, links, files = {}, {}, [], []
|
|
249
|
+
for top in CODE_DIRS:
|
|
250
|
+
for d, subs, names in os.walk(top):
|
|
251
|
+
subs[:] = [x for x in subs if x not in SKIP_DIRS and not x.startswith(".")]
|
|
252
|
+
for n in names:
|
|
253
|
+
p = os.path.join(d, n)
|
|
254
|
+
if n.endswith(".csproj"):
|
|
255
|
+
refs = re.findall(r'<ProjectReference\s+Include="([^"]+)"', read(p))
|
|
256
|
+
projects[d] = [os.path.normpath(os.path.join(d, os.path.dirname(x.replace("\\", "/")))) for x in refs]
|
|
257
|
+
elif n.endswith(".razor") or n.endswith(".cshtml"):
|
|
258
|
+
files.append(p)
|
|
259
|
+
def project_of(f):
|
|
260
|
+
d = os.path.dirname(f)
|
|
261
|
+
while d and d not in projects:
|
|
262
|
+
nd = os.path.dirname(d)
|
|
263
|
+
if nd == d:
|
|
264
|
+
return ""
|
|
265
|
+
d = nd
|
|
266
|
+
return d
|
|
267
|
+
for f in files:
|
|
268
|
+
t = read(f)
|
|
269
|
+
own = [route_key(r) for r in re.findall(r'(?m)^\s*@page\s+"([^"]+)"', t)]
|
|
270
|
+
for k in own:
|
|
271
|
+
pages.setdefault(k, (f, project_of(f)))
|
|
272
|
+
for m in LINK.finditer(t):
|
|
273
|
+
lab = label_of(m.group(5))
|
|
274
|
+
if lab and not m.group(3).startswith(("http:", "https:", "mailto:", "#")):
|
|
275
|
+
links.append((f, own, project_of(f), route_key(m.group(3)), lab))
|
|
276
|
+
return projects, pages, links
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def closure(projects, d):
|
|
280
|
+
seen, todo = [], [d]
|
|
281
|
+
while todo:
|
|
282
|
+
x = todo.pop()
|
|
283
|
+
if x in seen:
|
|
284
|
+
continue
|
|
285
|
+
seen.append(x)
|
|
286
|
+
todo.extend(projects.get(x, []))
|
|
287
|
+
return seen
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def matches(a, b):
|
|
291
|
+
return len(a) == len(b) and all(x == y or "*" in (x, y) for x, y in zip(a, b))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def navigation(screens):
|
|
295
|
+
"""Adds page, project and nav (labels to click, in order) to each screen the code serves."""
|
|
296
|
+
projects, pages, links = code_map()
|
|
297
|
+
for s in screens:
|
|
298
|
+
k = route_key(s["route"])
|
|
299
|
+
hit = next(((f, pr) for pk, (f, pr) in pages.items() if matches(pk, k)), None)
|
|
300
|
+
if not hit:
|
|
301
|
+
continue
|
|
302
|
+
s["page"], s["project"] = hit[0], hit[1]
|
|
303
|
+
scope = set(closure(projects, hit[1]))
|
|
304
|
+
mine = [l for l in links if l[2] in scope]
|
|
305
|
+
# breadth first, from the links on every screen (a layout, the menu: files that are not pages)
|
|
306
|
+
paths, todo = {}, []
|
|
307
|
+
for f, own, _, to, lab in mine:
|
|
308
|
+
if not own and to not in paths:
|
|
309
|
+
paths[to] = [lab]; todo.append(to)
|
|
310
|
+
while todo:
|
|
311
|
+
cur = todo.pop(0)
|
|
312
|
+
for f, own, _, to, lab in mine:
|
|
313
|
+
if any(matches(o, cur) for o in own) and to not in paths:
|
|
314
|
+
paths[to] = paths[cur] + [lab]; todo.append(to)
|
|
315
|
+
nav = next((p for to, p in paths.items() if matches(to, k)), None)
|
|
316
|
+
if nav is not None:
|
|
317
|
+
s["nav"] = nav
|
|
318
|
+
|
|
319
|
+
|
|
175
320
|
def test_users(path):
|
|
176
321
|
"""The rows of the guide's Test users table. The heading may sit at ## or ### (a guide that
|
|
177
322
|
numbers its chapters nests it one level down), and the table may be the template's
|
|
@@ -187,7 +332,7 @@ def test_users(path):
|
|
|
187
332
|
for line in m.group(1).splitlines():
|
|
188
333
|
if not line.strip().startswith("|"):
|
|
189
334
|
continue
|
|
190
|
-
c = [x.strip()
|
|
335
|
+
c = [x.strip() for x in line.strip().strip("|").split("|")]
|
|
191
336
|
if cols is None:
|
|
192
337
|
low = [x.lower() for x in c]
|
|
193
338
|
def col(*names):
|
|
@@ -198,10 +343,13 @@ def test_users(path):
|
|
|
198
343
|
continue
|
|
199
344
|
if set("".join(c)) <= set("-: "):
|
|
200
345
|
continue # the header rule
|
|
201
|
-
if len(c) > max(cols["user"], cols["role"]) and c[cols["user"]] and not c[cols["user"]].isdigit():
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
346
|
+
if len(c) > max(cols["user"], cols["role"]) and plain(c[cols["user"]]) and not plain(c[cols["user"]]).isdigit():
|
|
347
|
+
# The cell is Markdown. The user is its first code span when it has one ("`a@b.c` (username
|
|
348
|
+
# `a`)" is a@b.c); every field is plain text, never with a stray backtick (Lekhak TF-027).
|
|
349
|
+
u = re.match(r"\s*`([^`]+)`", c[cols["user"]])
|
|
350
|
+
users.append({"user": u.group(1).strip() if u else plain(c[cols["user"]]),
|
|
351
|
+
"password_source": plain(c[cols["pw"]]) if cols["pw"] is not None and len(c) > cols["pw"] else "",
|
|
352
|
+
"role": plain(c[cols["role"]])})
|
|
205
353
|
return users
|
|
206
354
|
|
|
207
355
|
|
|
@@ -226,21 +374,50 @@ def screen_from_acceptance(acc):
|
|
|
226
374
|
return hits[-1].strip() if hits else ""
|
|
227
375
|
|
|
228
376
|
|
|
377
|
+
def checklist_of(app, phase):
|
|
378
|
+
return os.path.join("docs", f"{app}-Checklist.md" if phase <= 1 else f"{app}-P{phase}-Checklist.md")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def phases_of(app):
|
|
382
|
+
"""The phase numbers that have a checklist, phase 1 first."""
|
|
383
|
+
found = [1] if os.path.isfile(checklist_of(app, 1)) else []
|
|
384
|
+
for f in os.listdir("docs") if os.path.isdir("docs") else []:
|
|
385
|
+
m = re.fullmatch(re.escape(app) + r"-P(\d+)-Checklist\.md", f)
|
|
386
|
+
if m and int(m.group(1)) > 1:
|
|
387
|
+
found.append(int(m.group(1)))
|
|
388
|
+
return sorted(set(found))
|
|
389
|
+
|
|
390
|
+
|
|
229
391
|
def main(argv):
|
|
230
392
|
if len(argv) < 3 or argv[1] in ("-h", "--help"):
|
|
231
393
|
print(__doc__)
|
|
232
394
|
return 0
|
|
233
395
|
app, scope = argv[1], argv[2]
|
|
396
|
+
# Sevak TF-005: *verify graded only appPhase's checklist, so phases 1 and 2 (86 rows waiting) were
|
|
397
|
+
# never graded. --phase N grades that phase's checklist; --phase all prints the phases to grade, in
|
|
398
|
+
# order, and the task runs one pass per phase. Every later script reads the checklist from list.json.
|
|
399
|
+
have = phases_of(app)
|
|
234
400
|
phase = int(cfg("appPhase", "1"))
|
|
235
401
|
if "--phase" in argv:
|
|
236
|
-
|
|
402
|
+
val = argv[argv.index("--phase") + 1] if argv.index("--phase") + 1 < len(argv) else ""
|
|
403
|
+
if val == "all":
|
|
404
|
+
if not have:
|
|
405
|
+
die(f"no checklist for {app} under docs/")
|
|
406
|
+
print(f"# tf-verify-list — {app} — phase all — scope {scope}")
|
|
407
|
+
print("PHASES " + " ".join(str(p) for p in have))
|
|
408
|
+
print("Grade them one at a time, in this order: steps 1 and 4 to 6 of verify-phase.md with "
|
|
409
|
+
+ ", then ".join(f"--phase {p} ({checklist_of(app, p)})" for p in have) + ". Nothing was written.")
|
|
410
|
+
return 0
|
|
411
|
+
if not val.isdigit() or int(val) < 1:
|
|
412
|
+
die(f"--phase takes a phase number or all, not {val or 'nothing'}")
|
|
413
|
+
phase = int(val)
|
|
237
414
|
out_path = os.path.join("tests", ".artifacts", "verify", "list.json")
|
|
238
415
|
if "--json-out" in argv:
|
|
239
416
|
out_path = argv[argv.index("--json-out") + 1]
|
|
240
417
|
pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
241
|
-
cl =
|
|
418
|
+
cl = checklist_of(app, phase)
|
|
242
419
|
if not os.path.isfile(cl):
|
|
243
|
-
die(f"{cl} does not exist")
|
|
420
|
+
die(f"{cl} does not exist" + (f"; the phases with a checklist are {', '.join(map(str, have))}" if have else ""))
|
|
244
421
|
text = read(cl)
|
|
245
422
|
rows = rows_of(text)
|
|
246
423
|
if not rows:
|
|
@@ -252,6 +429,9 @@ def main(argv):
|
|
|
252
429
|
dialogs = dialogs_of_brd(os.path.join("docs", f"{pfx}BRD.md"))
|
|
253
430
|
users = test_users(usage_guide(app))
|
|
254
431
|
by_name = {norm_name(s["name"]): s for s in screens}
|
|
432
|
+
for s in screens: # a split screen's own name reaches its first tab (Chatur TF-003)
|
|
433
|
+
if s.get("parent"):
|
|
434
|
+
by_name.setdefault(norm_name(s["parent"]), s)
|
|
255
435
|
by_mock = {s["mockup"]: s for s in screens if s["mockup"]}
|
|
256
436
|
by_route = {s["route"].lower(): s for s in screens}
|
|
257
437
|
by_section = {s["section"]: s for s in screens if s.get("section")}
|
|
@@ -263,8 +443,9 @@ def main(argv):
|
|
|
263
443
|
if r["status"] in NA:
|
|
264
444
|
skipped += 1
|
|
265
445
|
continue
|
|
266
|
-
e = ent.get(r["id"], {"section": "", "acceptance": "", "mockup": "", "brd": "", "perf_budget": ""})
|
|
446
|
+
e = ent.get(r["id"], {"section": "", "acceptance": "", "mockup": "", "brd": "", "perf_budget": "", "zoom_check": ""})
|
|
267
447
|
row = dict(r, acceptance=e["acceptance"], mockup=e["mockup"], brd=e["brd"], perf_budget=e["perf_budget"],
|
|
448
|
+
zoom_check=e.get("zoom_check", ""),
|
|
268
449
|
section=e["section"], screen="", route="", dialog="")
|
|
269
450
|
cand = [screen_from_acceptance(e["acceptance"]), e["section"], r["title"]]
|
|
270
451
|
if e["section"] in by_section: # a screen read from the checklist: the heading the row sits under
|
|
@@ -285,6 +466,10 @@ def main(argv):
|
|
|
285
466
|
if not row["screen"] and e["mockup"] in by_mock:
|
|
286
467
|
s = by_mock[e["mockup"]]
|
|
287
468
|
row["screen"], row["route"] = s["name"], s["route"]
|
|
469
|
+
# a row on a split screen belongs to the tab whose mockup it names
|
|
470
|
+
tab = by_mock.get(e["mockup"])
|
|
471
|
+
if row["screen"] and tab and tab.get("parent") and by_name.get(norm_name(row["screen"]), {}).get("parent") == tab["parent"]:
|
|
472
|
+
row["screen"], row["route"] = tab["name"], tab["route"]
|
|
288
473
|
if not row["screen"] and row["class"] != "NFR":
|
|
289
474
|
unresolved.append(row["id"])
|
|
290
475
|
work.append(row)
|
|
@@ -293,7 +478,8 @@ def main(argv):
|
|
|
293
478
|
for s in screens:
|
|
294
479
|
owned = [r["id"] for r in work if r["screen"] == s["name"]]
|
|
295
480
|
if owned:
|
|
296
|
-
screen_list.append(dict(s, rows=owned))
|
|
481
|
+
screen_list.append(dict(s, rows=owned, seed=seed_of(s)))
|
|
482
|
+
navigation(screen_list)
|
|
297
483
|
|
|
298
484
|
print(f"# tf-verify-list — {app} — {cl} — phase {phase} — scope {scope}")
|
|
299
485
|
counts = {}
|
|
@@ -305,12 +491,25 @@ def main(argv):
|
|
|
305
491
|
print()
|
|
306
492
|
print(f"## Screens to drive ({len(screen_list)} of {len(screens)} in the {source})")
|
|
307
493
|
for s in screen_list:
|
|
308
|
-
print(f"- {s['name']} ({s['route']}) — mockup {s['mockup'] or 'none'}
|
|
494
|
+
print(f"- {s['name']} ({s['route']}) — mockup {s['mockup'] or 'none'}"
|
|
495
|
+
+ (f" — seed {s['seed']}" if s["seed"] else "")
|
|
496
|
+
+ (f" — served by {s['project']}" if s.get("project") else "")
|
|
497
|
+
+ (f" — reached by clicking {' > '.join(repr(x) for x in s['nav'])}" if s.get("nav") else "")
|
|
498
|
+
+ f" — rows {', '.join(s['rows'])}")
|
|
499
|
+
par = [f"--screen {os.path.basename(s['mockup'])[:-5]}={s['route']}" for s in screen_list
|
|
500
|
+
if s["mockup"] and "{" not in s["route"]]
|
|
501
|
+
if par:
|
|
502
|
+
# --list carries each screen's mockup path: the --screen name is the file's stem only, so
|
|
503
|
+
# docs/mockups/admin/prompt-manager.html read as NO-MOCKUP (Lekhak TF-022)
|
|
504
|
+
print(f"Mockup parity: {' '.join(par)} --list {out_path}")
|
|
309
505
|
print()
|
|
310
506
|
print("## Rows")
|
|
311
507
|
for r in work:
|
|
312
508
|
where = f"{r['screen']} {r['route']}" + (f" — dialog {r['dialog']}" if r["dialog"] else "") if r["screen"] else "no screen resolved"
|
|
313
|
-
extra = f" — perf-budget: {r['perf_budget']}" if r["perf_budget"] else ""
|
|
509
|
+
extra = (f" — perf-budget: {r['perf_budget']}" if r["perf_budget"] else "") \
|
|
510
|
+
+ (f" — zoom-check: {r['zoom_check']} (bash .tfcore/utils/tf-verify-zoom.sh --appium|--cdp <url>"
|
|
511
|
+
+ (" --expect-restored 1.3, after a run with --leave 3 and a restart" if r.get("zoom_check") == "kept" else "")
|
|
512
|
+
+ f" --json-out tests/.artifacts/verify/zoom/{r['id']}.json)" if r.get("zoom_check") else "")
|
|
314
513
|
print(f"- {r['id']} [{r['status_raw']}] {r['title']} — {where}{extra}")
|
|
315
514
|
print(f" acceptance: {r['acceptance'] or 'MISSING — the row cannot be graded; fix the checklist'}")
|
|
316
515
|
if unresolved:
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# tf-verify-list.sh — the working list of a verify run (Sitting 4c, 2026-09-06).
|
|
3
3
|
#
|
|
4
|
-
# bash .tfcore/utils/tf-verify-list.sh <App> <scope> [--phase N] [--json-out <file>]
|
|
4
|
+
# bash .tfcore/utils/tf-verify-list.sh <App> <scope> [--phase N|all] [--json-out <file>]
|
|
5
5
|
#
|
|
6
|
+
# --phase N grades phase N's checklist (default appPhase); --phase all prints the phases to grade, in
|
|
7
|
+
# order, and writes nothing (Sevak TF-005).
|
|
6
8
|
# scope: ui · functional · all · a comma list of REQ ids. Prints every row to grade with its
|
|
7
9
|
# acceptance line, screen, route, mockup and perf budget, the screens to drive, and the test
|
|
8
10
|
# users; writes tests/.artifacts/verify/list.json for the other verify scripts. Skips N/A rows
|