@techierathore/techieflow 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +194 -14
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +112 -13
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +2 -2
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +5 -5
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +3 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +2 -2
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +10 -2
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +38 -2
- package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +95 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +163 -15
- package/.tfcore/utils/tf-checklist-edit.py +21 -3
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +53 -24
- package/.tfcore/utils/tf-doc-check.py +218 -26
- package/.tfcore/utils/tf-doc-tests.sh +116 -0
- package/.tfcore/utils/tf-emit.sh +132 -45
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +87 -31
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
- package/.tfcore/utils/tf-mockup-parity.sh +25 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +38 -4
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +79 -5
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +160 -45
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +74 -17
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +529 -54
- package/.tfcore/utils/tf-verify-emit.sh +57 -17
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +290 -24
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +341 -44
- package/.tfcore/utils/tf-verify-screens.sh +23 -5
- package/.tfcore/utils/tf-verify-tests.sh +310 -19
- package/.tfcore/utils/tf-verify-verdict.py +163 -21
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +272 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +6 -2
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
# the row, looked up in runs.jsonl, or left out when there is none
|
|
12
12
|
# runs.jsonl one `run` record for the verify pass: cmd verify-phase, the rows touched, the build
|
|
13
13
|
# result, whether YOLO was on
|
|
14
|
+
# Run and gate records carry app_phase, the phase whose checklist was graded (list.json). A second pass
|
|
15
|
+
# of one *verify --phase all starts where the first one's record ended (Sevak TF-005).
|
|
14
16
|
# Rows the run could not grade (not tested, not observable, not driven) get no record. Telemetry
|
|
15
17
|
# never blocks: a refused record is printed and the script still exits 0.
|
|
16
18
|
set -u
|
|
@@ -39,32 +41,66 @@ v = json.load(open(os.path.join(d, "verdicts.json"), encoding="utf-8"))
|
|
|
39
41
|
run_id = started or v.get("started") or ""
|
|
40
42
|
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
# A verify chained inside another command is handed that command's start as "the step-0 time", and
|
|
45
|
+
# its record then covered the caller's whole window, so the caller's own record was refused (TF-052).
|
|
46
|
+
# This run's start is its own marker's; with no verify-phase marker (step 0 skipped), when the list
|
|
47
|
+
# was written.
|
|
48
|
+
mk = {}
|
|
49
|
+
try:
|
|
50
|
+
pj = os.path.join(".tfcore", ".session", "phase.json")
|
|
51
|
+
if datetime.datetime.now().timestamp() - os.path.getmtime(pj) < 24 * 3600:
|
|
52
|
+
mk = json.load(open(pj, encoding="utf-8")) or {}
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
own, caller = "", (mk.get("outer") or {}).get("cmd") or "an earlier command"
|
|
56
|
+
if mk.get("cmd") == "verify-phase":
|
|
57
|
+
own = mk.get("started") or ""
|
|
58
|
+
elif mk.get("started") and mk.get("started") == run_id and os.path.isfile(os.path.join(d, "list.json")):
|
|
59
|
+
own, caller = datetime.datetime.fromtimestamp(os.path.getmtime(os.path.join(d, "list.json")), datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), mk.get("cmd")
|
|
60
|
+
if own and run_id and own > run_id:
|
|
61
|
+
print(f"tf-verify-emit: {run_id} is when {caller} started; this verify started {own} and is recorded from then")
|
|
62
|
+
run_id = own
|
|
49
63
|
|
|
50
64
|
def q(*args):
|
|
51
65
|
p = subprocess.run(["bash", EMIT, *args], capture_output=True, text=True)
|
|
52
66
|
return p.stdout.strip()
|
|
53
67
|
|
|
54
|
-
# origin lookup: the last build-phase or fix-issues run that touched the row
|
|
55
68
|
runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
|
|
56
|
-
|
|
69
|
+
runs = []
|
|
57
70
|
if os.path.isfile(runs_path):
|
|
58
71
|
for line in open(runs_path, encoding="utf-8", errors="replace"):
|
|
59
72
|
try:
|
|
60
|
-
|
|
73
|
+
runs.append(json.loads(line))
|
|
61
74
|
except Exception:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# Sevak TF-005: *verify --phase all is one pass per phase inside one command, all handed the same
|
|
78
|
+
# step-0 time. A pass that finds a run of this app already recorded since that time starts where it
|
|
79
|
+
# ended, so the second pass is not refused as an overlap (SCHEMA §2.7b) and no window is counted twice.
|
|
80
|
+
if run_id:
|
|
81
|
+
after = [r.get("ended", "") for r in runs if r.get("kind") == "run" and not r.get("backfilled")
|
|
82
|
+
and r.get("app") in (None, app) and r.get("started", "") >= run_id and r.get("ended", "") > run_id]
|
|
83
|
+
if after:
|
|
84
|
+
print(f"tf-verify-emit: a run of {app} was recorded since {run_id}; this pass is recorded from its end, {max(after)}")
|
|
85
|
+
run_id = max(after)
|
|
86
|
+
phase = v.get("phase")
|
|
87
|
+
|
|
88
|
+
def emit(stream, rec):
|
|
89
|
+
p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
|
|
90
|
+
msg = (p.stdout + p.stderr).strip()
|
|
91
|
+
if p.returncode != 0 or "refus" in msg.lower() or "error" in msg.lower():
|
|
92
|
+
print(f" {stream}: not written — {msg.splitlines()[0][:160] if msg else 'no reason printed'}")
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
# origin lookup: the last build-phase or fix-issues run that touched the row
|
|
97
|
+
origin = {}
|
|
98
|
+
for r in runs:
|
|
99
|
+
if r.get("kind") != "run" or r.get("backfilled") or r.get("cmd") not in ("build-phase", "fix-issues"):
|
|
100
|
+
continue
|
|
101
|
+
for rid in r.get("reqs_touched") or []:
|
|
102
|
+
if r.get("started", "") >= origin.get(rid, ("", ""))[0]:
|
|
103
|
+
origin[rid] = (r.get("started", ""), r.get("cmd"), (r.get("subagents") or ["flow-master"])[0] if r.get("subagents") else "flow-master")
|
|
68
104
|
|
|
69
105
|
gates = misses = 0
|
|
70
106
|
for row in v["rows"]:
|
|
@@ -75,6 +111,8 @@ for row in v["rows"]:
|
|
|
75
111
|
rec = {"kind": "gate", "app": app, "run_id": run_id, "req_id": rid, "req_class": row["class"],
|
|
76
112
|
"attempt": int(att) if att.isdigit() else 1, "verdict": row["status"], "gate": row["gate"],
|
|
77
113
|
"gates_run": row["gates_run"], "failure_class": row["failure_class"], "prior_verdict": row["prior_verdict"]}
|
|
114
|
+
if phase:
|
|
115
|
+
rec["app_phase"] = phase
|
|
78
116
|
if emit("gates", rec):
|
|
79
117
|
gates += 1
|
|
80
118
|
if not row["gate"]:
|
|
@@ -104,7 +142,9 @@ rec = {"kind": "run", "app": app, "cmd": "verify-phase", "mode": mode, "yolo": y
|
|
|
104
142
|
"build_result": v.get("build_result", "not-run")}
|
|
105
143
|
if run_id:
|
|
106
144
|
rec["started"] = run_id
|
|
145
|
+
if phase:
|
|
146
|
+
rec["app_phase"] = phase
|
|
107
147
|
ok = emit("runs", rec)
|
|
108
|
-
print(f"telemetry: {gates} gate record(s), {misses} new miss(es), run record {'written' if ok else 'not written'} (cmd verify-phase, yolo {str(yolo).lower()})")
|
|
148
|
+
print(f"telemetry: {gates} gate record(s), {misses} new miss(es), run record {'written' if ok else 'not written'} (cmd verify-phase, phase {phase or '?'}, yolo {str(yolo).lower()})")
|
|
109
149
|
PY
|
|
110
150
|
exit 0
|
|
@@ -4,17 +4,30 @@
|
|
|
4
4
|
# bash .tfcore/utils/tf-verify-env.sh [--check]
|
|
5
5
|
#
|
|
6
6
|
# Ensures, in the current project: package.json; the playwright and @playwright/test packages;
|
|
7
|
-
# the Chromium browser; playwright.config.ts with its output pinned under tests/.artifacts
|
|
7
|
+
# the Chromium browser; playwright.config.ts with its output pinned under tests/.artifacts/ and its
|
|
8
|
+
# address read from BASE_URL, which tf-verify-tests.sh --base sets (TF-031);
|
|
8
9
|
# the .gitignore lines for everything this tooling generates. --check reports and changes nothing.
|
|
9
10
|
# Prints one line per item and READY at the end, or NOT-READY with the one command the owner
|
|
10
11
|
# must run once (only when a browser install needs sudo and sudo is unavailable).
|
|
11
12
|
# Exit 0 ready · 1 not ready · 2 node or npm missing.
|
|
12
13
|
set -u
|
|
14
|
+
source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh" # tf_sed_inplace: GNU and BSD sed
|
|
13
15
|
CHECK=0; [[ "${1:-}" == "--check" ]] && CHECK=1
|
|
14
16
|
command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 || { echo "NOT-READY node and npm are required"; exit 2; }
|
|
15
17
|
ok=1
|
|
16
18
|
|
|
17
19
|
say() { echo " $*"; }
|
|
20
|
+
# cfg_sub FILE ALL ERE TEXT: replace the first match of ERE on each line (ALL=1), or only on the
|
|
21
|
+
# first line that matches (ALL=0, what GNU sed's 0,/re/ address did), with TEXT; \n in TEXT is a
|
|
22
|
+
# newline. awk, so it reads the same on GNU and BSD systems.
|
|
23
|
+
cfg_sub() {
|
|
24
|
+
local tmp
|
|
25
|
+
tmp="$(mktemp "$1.XXXXXX")" || return 1
|
|
26
|
+
awk -v all="$2" -v re="$3" -v rep="$4" '
|
|
27
|
+
(all == 1 || !done) && match($0, re) { $0 = substr($0, 1, RSTART - 1) rep substr($0, RSTART + RLENGTH); done = 1 }
|
|
28
|
+
{ print }' "$1" > "$tmp" && cat "$tmp" > "$1"
|
|
29
|
+
rm -f "$tmp"
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
# package.json
|
|
20
33
|
if [[ ! -f package.json ]]; then
|
|
@@ -55,7 +68,7 @@ export default defineConfig({
|
|
|
55
68
|
testDir: '"'"'./tests/verify'"'"',
|
|
56
69
|
outputDir: '"'"'./tests/.artifacts/test-results'"'"',
|
|
57
70
|
reporter: '"'"'line'"'"',
|
|
58
|
-
use: { headless: true, screenshot: '"'"'only-on-failure'"'"', trace: '"'"'retain-on-failure'"'"' },
|
|
71
|
+
use: { baseURL: process.env.BASE_URL, headless: true, screenshot: '"'"'only-on-failure'"'"', trace: '"'"'retain-on-failure'"'"' },
|
|
59
72
|
});'
|
|
60
73
|
if [[ ! -f playwright.config.ts ]]; then
|
|
61
74
|
if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: missing"; ok=0; else printf '%s\n' "$CFG" > playwright.config.ts; say "playwright.config.ts: written"; fi
|
|
@@ -63,13 +76,29 @@ elif ! grep -q "tests/.artifacts" playwright.config.ts; then
|
|
|
63
76
|
if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: outputDir not under tests/.artifacts"; ok=0
|
|
64
77
|
else
|
|
65
78
|
if grep -q "outputDir" playwright.config.ts; then
|
|
66
|
-
|
|
79
|
+
tf_sed_inplace "s#outputDir:[^,]*,#outputDir: './tests/.artifacts/test-results',#" playwright.config.ts
|
|
67
80
|
else
|
|
68
|
-
|
|
81
|
+
cfg_sub playwright.config.ts 1 "defineConfig[(][{]" "defineConfig({\n outputDir: './tests/.artifacts/test-results',"
|
|
69
82
|
fi
|
|
70
83
|
say "playwright.config.ts: outputDir pinned under tests/.artifacts"
|
|
71
84
|
fi
|
|
72
85
|
else say "playwright.config.ts: present, output pinned"; fi
|
|
86
|
+
# the address: tf-verify-tests.sh --base passes it as BASE_URL, which Playwright never reads by
|
|
87
|
+
# itself; a config that ignores it tests whatever is on its default port, an older build included
|
|
88
|
+
# (TfLens TF-031, 2026-09-11). A baseURL of the project's own is kept, as the fallback.
|
|
89
|
+
if [[ -f playwright.config.ts ]] && ! grep -q "BASE_URL" playwright.config.ts; then
|
|
90
|
+
if [[ $CHECK -eq 1 ]]; then say "playwright.config.ts: does not read BASE_URL, so --base is ignored"; ok=0
|
|
91
|
+
elif grep -qE "baseURL[\"']?[[:space:]]*:" playwright.config.ts; then
|
|
92
|
+
tf_sed_inplace -E "s#(baseURL[\"']?[[:space:]]*:[[:space:]]*)#\1process.env.BASE_URL || #" playwright.config.ts
|
|
93
|
+
say "playwright.config.ts: baseURL now reads BASE_URL first, its own address after"
|
|
94
|
+
elif grep -qE "use[[:space:]]*:[[:space:]]*\{" playwright.config.ts; then
|
|
95
|
+
cfg_sub playwright.config.ts 0 "use[[:space:]]*:[[:space:]]*[{]" "use: { baseURL: process.env.BASE_URL,"
|
|
96
|
+
say "playwright.config.ts: baseURL added, read from BASE_URL"
|
|
97
|
+
else
|
|
98
|
+
cfg_sub playwright.config.ts 0 "defineConfig[(][{]" "defineConfig({\n use: { baseURL: process.env.BASE_URL },"
|
|
99
|
+
say "playwright.config.ts: baseURL added, read from BASE_URL"
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
73
102
|
|
|
74
103
|
# .gitignore
|
|
75
104
|
missing=()
|
|
@@ -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
|
|
@@ -71,14 +76,22 @@ def entries_of(text):
|
|
|
71
76
|
out, current = {}, ""
|
|
72
77
|
lines = text.splitlines()
|
|
73
78
|
for i, line in enumerate(lines):
|
|
74
|
-
|
|
79
|
+
# a heading one level down (`### Page: …` under `## UI / Pages`) names the section too, and any
|
|
80
|
+
# other one (`### Cross-page: …`) ends the page above it
|
|
81
|
+
m = re.match(r"^#{2,3}\s+(.+)$", line)
|
|
75
82
|
if m:
|
|
76
83
|
current = re.sub(r"^(?:page|screen)\s*:\s*", "", m.group(1).strip(), flags=re.I)
|
|
77
84
|
continue
|
|
78
85
|
a = re.search(r"<a id=['\"]d-(req-[a-z]+-\d+)['\"]", line, re.I)
|
|
79
86
|
if a:
|
|
80
87
|
rid = a.group(1).upper()
|
|
81
|
-
|
|
88
|
+
# an anchor written above its own page heading belongs to that page, not the one before
|
|
89
|
+
# (AppManager's older entries: `<a id>` then `### Page:` then the row)
|
|
90
|
+
nxt = next((l for l in lines[i + 1:i + 3] if l.strip()), "")
|
|
91
|
+
h = re.match(r"^#{2,3}\s+(?:page|screen)\s*:\s*(.+)$", nxt, re.I)
|
|
92
|
+
if h:
|
|
93
|
+
current = h.group(1).strip()
|
|
94
|
+
e = {"section": current, "acceptance": "", "mockup": "", "brd": "", "perf_budget": "", "zoom_check": ""}
|
|
82
95
|
for j in range(i, min(i + 14, len(lines))):
|
|
83
96
|
l = lines[j]
|
|
84
97
|
if re.search(r"\*Acceptance:?\*|^\s*[-*]\s*Acceptance:", l, re.I) and not e["acceptance"]:
|
|
@@ -92,6 +105,9 @@ def entries_of(text):
|
|
|
92
105
|
pb = re.search(r"perf-budget:\s*([^|`]+)", l)
|
|
93
106
|
if pb and not e["perf_budget"]:
|
|
94
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)
|
|
95
111
|
if j > i and re.match(r"^\s*(?:[-*]\s*)?<a id=|^## ", l):
|
|
96
112
|
break # the next entry starts: nothing below belongs to this row
|
|
97
113
|
out[rid] = e
|
|
@@ -109,11 +125,62 @@ def screens_of_uidesign(path):
|
|
|
109
125
|
m = re.match(r"###\s+Screen:\s*(.+?)\s*\(`?([^)`]+)`?\)\s*$", p.splitlines()[0] if p.strip() else "")
|
|
110
126
|
if not m:
|
|
111
127
|
continue
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 ""}])
|
|
136
|
+
return out
|
|
137
|
+
|
|
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
|
+
|
|
165
|
+
def screens_of_checklist(text, ent):
|
|
166
|
+
"""[{name, route, mockup}] from the checklist's own '## Page: Name (`/route`, …)' headings, at ## or
|
|
167
|
+
###, for a project with no UIDesign: the first route that can be opened as typed (no `{id}`),
|
|
168
|
+
the mockup from the first entry under the heading. A heading with no such route names no screen.
|
|
169
|
+
Without this every UI row of such a project was graded by its test only (AppManager TF-016)."""
|
|
170
|
+
out, seen = [], set()
|
|
171
|
+
for line in text.splitlines():
|
|
172
|
+
m = re.match(r"^#{2,3}\s+(?:page|screen)\s*:\s*(.+?)\s*\((.*)\)\s*$", line, re.I)
|
|
173
|
+
if not m or norm_name(m.group(1)) in seen:
|
|
174
|
+
continue
|
|
175
|
+
inner = m.group(2)
|
|
176
|
+
cands = re.findall(r"`([^`]+)`", inner) or re.split(r"\s*,\s*", inner)
|
|
177
|
+
route = next((c.strip() for c in cands if c.strip().startswith("/") and "{" not in c and " " not in c.strip()), "")
|
|
178
|
+
if not route:
|
|
179
|
+
continue
|
|
180
|
+
section = re.sub(r"^#+\s+(?:page|screen)\s*:\s*", "", line.strip(), flags=re.I)
|
|
181
|
+
mock = next((e["mockup"] for e in ent.values() if e["section"] == section and e["mockup"]), "")
|
|
182
|
+
seen.add(norm_name(m.group(1)))
|
|
183
|
+
out.append({"name": m.group(1).strip(), "route": route, "mockup": mock, "section": section})
|
|
117
184
|
return out
|
|
118
185
|
|
|
119
186
|
|
|
@@ -131,17 +198,158 @@ def dialogs_of_brd(path):
|
|
|
131
198
|
return out
|
|
132
199
|
|
|
133
200
|
|
|
201
|
+
def usage_guide(app):
|
|
202
|
+
"""docs/{App}-UsageGuide.md, or docs/{App}-Usage-Guide.md: the two spellings tf-doc-check.py
|
|
203
|
+
accepts. Only the first was looked for, so AppManager's guide read as having no test users
|
|
204
|
+
(AppManager TF-009)."""
|
|
205
|
+
for name in (f"{app}-UsageGuide.md", f"{app}-Usage-Guide.md"):
|
|
206
|
+
p = os.path.join("docs", name)
|
|
207
|
+
if os.path.isfile(p):
|
|
208
|
+
return p
|
|
209
|
+
return os.path.join("docs", f"{app}-UsageGuide.md")
|
|
210
|
+
|
|
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
|
+
|
|
134
320
|
def test_users(path):
|
|
321
|
+
"""The rows of the guide's Test users table. The heading may sit at ## or ### (a guide that
|
|
322
|
+
numbers its chapters nests it one level down), and the table may be the template's
|
|
323
|
+
`| # | User | Password source | Role |` or any table whose header names a user or email column
|
|
324
|
+
and a role column, read by those names (AppManager TF-009)."""
|
|
135
325
|
users = []
|
|
136
326
|
if not os.path.isfile(path):
|
|
137
327
|
return users
|
|
138
|
-
m = re.search(r"(?ms)
|
|
328
|
+
m = re.search(r"(?ms)^#{2,3}\s+(?:\d+[.)]\s*)?Test users[^\n]*\n(.*?)(?=^#{1,3} |\Z)", read(path))
|
|
139
329
|
if not m:
|
|
140
330
|
return users
|
|
331
|
+
cols = None
|
|
141
332
|
for line in m.group(1).splitlines():
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
333
|
+
if not line.strip().startswith("|"):
|
|
334
|
+
continue
|
|
335
|
+
c = [x.strip() for x in line.strip().strip("|").split("|")]
|
|
336
|
+
if cols is None:
|
|
337
|
+
low = [x.lower() for x in c]
|
|
338
|
+
def col(*names):
|
|
339
|
+
return next((i for i, h in enumerate(low) if any(n in h for n in names)), None)
|
|
340
|
+
cols = {"user": col("user", "email", "login", "account"), "pw": col("password", "secret"), "role": col("role")}
|
|
341
|
+
if cols["user"] is None or cols["role"] is None:
|
|
342
|
+
cols = {"user": 1, "pw": 2, "role": 3} # the template's numbered shape
|
|
343
|
+
continue
|
|
344
|
+
if set("".join(c)) <= set("-: "):
|
|
345
|
+
continue # the header rule
|
|
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"]])})
|
|
145
353
|
return users
|
|
146
354
|
|
|
147
355
|
|
|
@@ -166,32 +374,67 @@ def screen_from_acceptance(acc):
|
|
|
166
374
|
return hits[-1].strip() if hits else ""
|
|
167
375
|
|
|
168
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
|
+
|
|
169
391
|
def main(argv):
|
|
170
392
|
if len(argv) < 3 or argv[1] in ("-h", "--help"):
|
|
171
393
|
print(__doc__)
|
|
172
394
|
return 0
|
|
173
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)
|
|
174
400
|
phase = int(cfg("appPhase", "1"))
|
|
175
401
|
if "--phase" in argv:
|
|
176
|
-
|
|
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)
|
|
177
414
|
out_path = os.path.join("tests", ".artifacts", "verify", "list.json")
|
|
178
415
|
if "--json-out" in argv:
|
|
179
416
|
out_path = argv[argv.index("--json-out") + 1]
|
|
180
417
|
pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
181
|
-
cl =
|
|
418
|
+
cl = checklist_of(app, phase)
|
|
182
419
|
if not os.path.isfile(cl):
|
|
183
|
-
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 ""))
|
|
184
421
|
text = read(cl)
|
|
185
422
|
rows = rows_of(text)
|
|
186
423
|
if not rows:
|
|
187
424
|
die(f"{cl} has no REQ rows")
|
|
188
425
|
ent = entries_of(text)
|
|
189
|
-
screens = screens_of_uidesign(os.path.join("docs", f"{pfx}UIDesign.md"))
|
|
426
|
+
screens, source = screens_of_uidesign(os.path.join("docs", f"{pfx}UIDesign.md")), "UIDesign"
|
|
427
|
+
if not screens:
|
|
428
|
+
screens, source = screens_of_checklist(text, ent), "checklist's Page headings"
|
|
190
429
|
dialogs = dialogs_of_brd(os.path.join("docs", f"{pfx}BRD.md"))
|
|
191
|
-
users = test_users(
|
|
430
|
+
users = test_users(usage_guide(app))
|
|
192
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)
|
|
193
435
|
by_mock = {s["mockup"]: s for s in screens if s["mockup"]}
|
|
194
436
|
by_route = {s["route"].lower(): s for s in screens}
|
|
437
|
+
by_section = {s["section"]: s for s in screens if s.get("section")}
|
|
195
438
|
|
|
196
439
|
work, skipped, unresolved = [], 0, []
|
|
197
440
|
for r in rows:
|
|
@@ -200,10 +443,15 @@ def main(argv):
|
|
|
200
443
|
if r["status"] in NA:
|
|
201
444
|
skipped += 1
|
|
202
445
|
continue
|
|
203
|
-
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": ""})
|
|
204
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", ""),
|
|
205
449
|
section=e["section"], screen="", route="", dialog="")
|
|
206
450
|
cand = [screen_from_acceptance(e["acceptance"]), e["section"], r["title"]]
|
|
451
|
+
if e["section"] in by_section: # a screen read from the checklist: the heading the row sits under
|
|
452
|
+
s = by_section[e["section"]]
|
|
453
|
+
row["screen"], row["route"] = s["name"], s["route"]
|
|
454
|
+
cand = []
|
|
207
455
|
for c in cand:
|
|
208
456
|
n = norm_name(c)
|
|
209
457
|
if not n:
|
|
@@ -218,6 +466,10 @@ def main(argv):
|
|
|
218
466
|
if not row["screen"] and e["mockup"] in by_mock:
|
|
219
467
|
s = by_mock[e["mockup"]]
|
|
220
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"]
|
|
221
473
|
if not row["screen"] and row["class"] != "NFR":
|
|
222
474
|
unresolved.append(row["id"])
|
|
223
475
|
work.append(row)
|
|
@@ -226,7 +478,8 @@ def main(argv):
|
|
|
226
478
|
for s in screens:
|
|
227
479
|
owned = [r["id"] for r in work if r["screen"] == s["name"]]
|
|
228
480
|
if owned:
|
|
229
|
-
screen_list.append(dict(s, rows=owned))
|
|
481
|
+
screen_list.append(dict(s, rows=owned, seed=seed_of(s)))
|
|
482
|
+
navigation(screen_list)
|
|
230
483
|
|
|
231
484
|
print(f"# tf-verify-list — {app} — {cl} — phase {phase} — scope {scope}")
|
|
232
485
|
counts = {}
|
|
@@ -236,14 +489,27 @@ def main(argv):
|
|
|
236
489
|
if not work:
|
|
237
490
|
print("NOTHING: no row in scope. Run the status gate and stop.")
|
|
238
491
|
print()
|
|
239
|
-
print(f"## Screens to drive ({len(screen_list)} of {len(screens)} in the
|
|
492
|
+
print(f"## Screens to drive ({len(screen_list)} of {len(screens)} in the {source})")
|
|
240
493
|
for s in screen_list:
|
|
241
|
-
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}")
|
|
242
505
|
print()
|
|
243
506
|
print("## Rows")
|
|
244
507
|
for r in work:
|
|
245
508
|
where = f"{r['screen']} {r['route']}" + (f" — dialog {r['dialog']}" if r["dialog"] else "") if r["screen"] else "no screen resolved"
|
|
246
|
-
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 "")
|
|
247
513
|
print(f"- {r['id']} [{r['status_raw']}] {r['title']} — {where}{extra}")
|
|
248
514
|
print(f" acceptance: {r['acceptance'] or 'MISSING — the row cannot be graded; fix the checklist'}")
|
|
249
515
|
if unresolved:
|
|
@@ -259,7 +525,7 @@ def main(argv):
|
|
|
259
525
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
260
526
|
with open(out_path, "w", encoding="utf-8") as f:
|
|
261
527
|
json.dump({"app": app, "scope": scope, "phase": phase, "kind": cfg("appKind", "app"), "checklist": cl,
|
|
262
|
-
"rows": work, "screens": screen_list, "unresolved": unresolved, "test_users": users,
|
|
528
|
+
"rows": work, "screens": screen_list, "screens_source": source, "unresolved": unresolved, "test_users": users,
|
|
263
529
|
"skipped_na": skipped, "total": len(rows)}, f, indent=1)
|
|
264
530
|
print()
|
|
265
531
|
print(f"JSON: {out_path}")
|
|
@@ -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
|