@techierathore/techieflow 1.1.0 → 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.
Files changed (98) hide show
  1. package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
  2. package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
  3. package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
  4. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  5. package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
  6. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
  8. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  9. package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
  10. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  11. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  12. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  13. package/.opencode/plugin/techieflow.js +15 -7
  14. package/.tfcore/agents/flow-master.md +2 -0
  15. package/.tfcore/agents/verifier.md +2 -2
  16. package/.tfcore/core-config.yaml +2 -1
  17. package/.tfcore/hooks/guard-db.sh +106 -9
  18. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  19. package/.tfcore/hooks/yolo-allow.sh +27 -0
  20. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  21. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  22. package/.tfcore/tasks/_develop-mode.md +34 -0
  23. package/.tfcore/tasks/_yolo-mode.md +2 -2
  24. package/.tfcore/tasks/amend-docs.md +1 -1
  25. package/.tfcore/tasks/day1-brownfield.md +2 -2
  26. package/.tfcore/tasks/day1-greenfield.md +4 -4
  27. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  28. package/.tfcore/tasks/fix-issues.md +1 -0
  29. package/.tfcore/tasks/mockups.md +1 -1
  30. package/.tfcore/tasks/triage-and-fix.md +2 -2
  31. package/.tfcore/tasks/verify-phase.md +12 -11
  32. package/.tfcore/telemetry/SCHEMA.md +6 -2
  33. package/.tfcore/telemetry/tf-metrics.sh +19 -0
  34. package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
  35. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  36. package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
  37. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  38. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  39. package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
  40. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  41. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  42. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  43. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  44. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  45. package/.tfcore/utils/tf-appium.mjs +179 -0
  46. package/.tfcore/utils/tf-build-list.py +16 -1
  47. package/.tfcore/utils/tf-build.sh +75 -0
  48. package/.tfcore/utils/tf-checklist-edit.py +21 -3
  49. package/.tfcore/utils/tf-day1-files.py +55 -28
  50. package/.tfcore/utils/tf-develop.sh +264 -0
  51. package/.tfcore/utils/tf-devguide-list.py +6 -2
  52. package/.tfcore/utils/tf-doc-check.py +83 -21
  53. package/.tfcore/utils/tf-doc-tests.sh +40 -3
  54. package/.tfcore/utils/tf-emit.sh +40 -10
  55. package/.tfcore/utils/tf-fix-close.sh +13 -7
  56. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  57. package/.tfcore/utils/tf-menus.mjs +401 -0
  58. package/.tfcore/utils/tf-mockup-parity.mjs +355 -27
  59. package/.tfcore/utils/tf-mockup-parity.sh +14 -3
  60. package/.tfcore/utils/tf-phase.sh +24 -4
  61. package/.tfcore/utils/tf-render-html.py +19 -3
  62. package/.tfcore/utils/tf-req-ids.py +36 -0
  63. package/.tfcore/utils/tf-stack-check.sh +3 -0
  64. package/.tfcore/utils/tf-status-facts.py +90 -15
  65. package/.tfcore/utils/tf-theme.mjs +189 -0
  66. package/.tfcore/utils/tf-triage.py +12 -1
  67. package/.tfcore/utils/tf-verify-boot.sh +274 -19
  68. package/.tfcore/utils/tf-verify-emit.sh +37 -17
  69. package/.tfcore/utils/tf-verify-list.py +219 -20
  70. package/.tfcore/utils/tf-verify-list.sh +3 -1
  71. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  72. package/.tfcore/utils/tf-verify-screens.mjs +178 -20
  73. package/.tfcore/utils/tf-verify-screens.sh +17 -4
  74. package/.tfcore/utils/tf-verify-tests.sh +107 -17
  75. package/.tfcore/utils/tf-verify-verdict.py +118 -15
  76. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  77. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  78. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  79. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  80. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  81. package/.tfcore/utils/tf-yolo.sh +31 -3
  82. package/.tfcore/utils/tf_feedback.py +79 -16
  83. package/.tfcore/utils/tf_stack_check.py +93 -0
  84. package/README.md +2 -2
  85. package/opencode.jsonc +6 -2
  86. package/package.json +1 -1
  87. package/scripts/install.mjs +13 -2
  88. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  90. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  91. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  92. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  93. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  94. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  95. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  96. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  97. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  98. 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
- mk = re.search(r"((?:docs/)?mockups/[\w./-]+\.html)", p)
121
- mock = mk.group(1) if mk else ""
122
- if mock and not mock.startswith("docs/"):
123
- mock = "docs/" + mock
124
- out.append({"name": m.group(1).strip(), "route": m.group(2).strip(), "mockup": mock})
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().strip("`* ") for x in line.strip().strip("|").split("|")]
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
- users.append({"user": c[cols["user"]],
203
- "password_source": c[cols["pw"]] if cols["pw"] is not None and len(c) > cols["pw"] else "",
204
- "role": c[cols["role"]]})
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
- phase = int(argv[argv.index("--phase") + 1])
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 = os.path.join("docs", f"{pfx}Checklist.md")
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'} — rows {', '.join(s['rows'])}")
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