@techierathore/techieflow 1.0.2 → 1.0.3

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 (174) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +0 -5
  2. package/.claude/commands/TechieFlow/agents/architect.md +0 -6
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  9. package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
  10. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  11. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  12. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  13. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  14. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  15. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  16. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  17. package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
  18. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  19. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  20. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  21. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  22. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  23. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  24. package/.opencode/plugin/techieflow.js +39 -12
  25. package/.tfcore/agents/analyst.md +0 -5
  26. package/.tfcore/agents/architect.md +0 -6
  27. package/.tfcore/agents/flow-master.md +41 -215
  28. package/.tfcore/agents/verifier.md +23 -64
  29. package/.tfcore/core-config.yaml +10 -0
  30. package/.tfcore/hooks/block-git.sh +4 -4
  31. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  32. package/.tfcore/hooks/guard-build.sh +76 -0
  33. package/.tfcore/hooks/guard-db.sh +90 -0
  34. package/.tfcore/hooks/guard-metrics.sh +56 -0
  35. package/.tfcore/hooks/guard-status-html.sh +116 -71
  36. package/.tfcore/hooks/guard-status.sh +43 -16
  37. package/.tfcore/hooks/guard-verify.sh +70 -48
  38. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  39. package/.tfcore/install-manifest.yaml +0 -18
  40. package/.tfcore/routing.yaml +25 -14
  41. package/.tfcore/standards/coding-standards-core.md +61 -0
  42. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  43. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  44. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  45. package/.tfcore/tasks/_status-update-gate.md +10 -74
  46. package/.tfcore/tasks/_yolo-mode.md +20 -58
  47. package/.tfcore/tasks/amend-docs.md +20 -143
  48. package/.tfcore/tasks/build-phase.md +14 -224
  49. package/.tfcore/tasks/day1-brownfield.md +31 -505
  50. package/.tfcore/tasks/day1-greenfield.md +28 -213
  51. package/.tfcore/tasks/deploy-checklist.md +19 -0
  52. package/.tfcore/tasks/devguide.md +10 -219
  53. package/.tfcore/tasks/fix-issues.md +10 -129
  54. package/.tfcore/tasks/log-miss.md +19 -158
  55. package/.tfcore/tasks/metrics-report.md +1 -1
  56. package/.tfcore/tasks/mockups.md +12 -117
  57. package/.tfcore/tasks/refresh-status.md +9 -130
  58. package/.tfcore/tasks/split-brd.md +10 -126
  59. package/.tfcore/tasks/triage-and-fix.md +14 -0
  60. package/.tfcore/tasks/triage-issues.md +11 -152
  61. package/.tfcore/tasks/verify-phase.md +36 -572
  62. package/.tfcore/telemetry/SCHEMA.md +43 -7
  63. package/.tfcore/telemetry/tf-metrics.sh +152 -17
  64. package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
  65. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  66. package/.tfcore/templates/stack-questions.md +77 -0
  67. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  68. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  69. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  70. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  71. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  72. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  73. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  74. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  75. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  76. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  77. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  78. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  79. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  80. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  81. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  82. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  83. package/.tfcore/user-guide.md +0 -23
  84. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  90. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  91. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  92. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  93. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  94. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  95. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  96. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  97. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  98. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  99. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  100. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  101. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  102. package/.tfcore/utils/tf-brd-status.py +176 -0
  103. package/.tfcore/utils/tf-brd-status.sh +19 -0
  104. package/.tfcore/utils/tf-build-list.py +191 -0
  105. package/.tfcore/utils/tf-build-list.sh +20 -0
  106. package/.tfcore/utils/tf-build.sh +143 -0
  107. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  108. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  109. package/.tfcore/utils/tf-day1-files.py +208 -0
  110. package/.tfcore/utils/tf-day1-files.sh +20 -0
  111. package/.tfcore/utils/tf-devguide-list.py +249 -0
  112. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  113. package/.tfcore/utils/tf-doc-check.py +1267 -0
  114. package/.tfcore/utils/tf-doc-check.sh +29 -0
  115. package/.tfcore/utils/tf-emit.sh +203 -13
  116. package/.tfcore/utils/tf-fix-close.sh +83 -0
  117. package/.tfcore/utils/tf-goal.sh +193 -62
  118. package/.tfcore/utils/tf-harness.sh +4 -9
  119. package/.tfcore/utils/tf-log-miss.py +197 -0
  120. package/.tfcore/utils/tf-log-miss.sh +14 -0
  121. package/.tfcore/utils/tf-misses-md.py +211 -0
  122. package/.tfcore/utils/tf-misses-md.sh +13 -0
  123. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  124. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  125. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  126. package/.tfcore/utils/tf-perf-grade.py +97 -0
  127. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  128. package/.tfcore/utils/tf-phase.sh +74 -0
  129. package/.tfcore/utils/tf-routing-bind.sh +10 -5
  130. package/.tfcore/utils/tf-routing.sh +6 -7
  131. package/.tfcore/utils/tf-split-brd.py +304 -0
  132. package/.tfcore/utils/tf-split-brd.sh +22 -0
  133. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  134. package/.tfcore/utils/tf-status-facts.py +309 -0
  135. package/.tfcore/utils/tf-status-facts.sh +20 -0
  136. package/.tfcore/utils/tf-triage.py +243 -0
  137. package/.tfcore/utils/tf-triage.sh +16 -0
  138. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  139. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  140. package/.tfcore/utils/tf-verify-env.sh +88 -0
  141. package/.tfcore/utils/tf-verify-list.py +270 -0
  142. package/.tfcore/utils/tf-verify-list.sh +13 -0
  143. package/.tfcore/utils/tf-verify-screens.mjs +283 -0
  144. package/.tfcore/utils/tf-verify-screens.sh +22 -0
  145. package/.tfcore/utils/tf-verify-tests.sh +123 -0
  146. package/.tfcore/utils/tf-verify-verdict.py +283 -0
  147. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  148. package/.tfcore/utils/tf-yolo.sh +59 -0
  149. package/README.md +114 -1149
  150. package/docs/TechieFlow-Installation.md +1 -5
  151. package/opencode.jsonc +13 -33
  152. package/package.json +1 -5
  153. package/scripts/install.mjs +52 -46
  154. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  155. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  156. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  157. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  158. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  159. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  160. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  161. package/.codex/config.toml +0 -46
  162. package/.codex/hooks.json +0 -63
  163. package/.codex/rules/techieflow.rules +0 -8
  164. package/.tfcore/hooks/codex-adapter.py +0 -248
  165. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  166. package/.tfcore/tasks/author-brd.md +0 -79
  167. package/.tfcore/tasks/document-project.md +0 -343
  168. package/.tfcore/tasks/execute-checklist.md +0 -86
  169. package/.tfcore/tasks/index-docs.md +0 -173
  170. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  171. package/.tfcore/tasks/shard-doc.md +0 -185
  172. package/.tfcore/utils/tf-codex-bind.py +0 -196
  173. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  174. package/WORKFLOW.html +0 -1986
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # tf-verify-tests.sh — run the acceptance tests and map them to rows (Sitting 4c, 2026-09-06).
3
+ #
4
+ # bash .tfcore/utils/tf-verify-tests.sh [--base URL] [--target <sln|csproj>] [--no-browser] [--no-unit]
5
+ # [--json-out tests/.artifacts/verify/tests.json]
6
+ #
7
+ # Browser tests: `npx playwright test` over tests/verify/ (the JSON reporter). Unit tests:
8
+ # `tf-build.sh test` with normal console verbosity. A test belongs to a row when its name contains
9
+ # the row's id (REQ-UI-004 …). A row is PASS when every test carrying its id passed, FAIL when one
10
+ # failed (the reason and the screenshot, when the reporter kept one, are recorded), and absent
11
+ # when no test carries its id. Writes tests/.artifacts/verify/tests.json for the verdict script.
12
+ # Exit 0 ran (whatever the results) · 2 nothing could run.
13
+ set -uo pipefail
14
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ BASE=""; TARGET=""; BROWSER=1; UNIT=1; OUT="tests/.artifacts/verify/tests.json"
16
+ while [[ $# -gt 0 ]]; do
17
+ case "$1" in
18
+ --base) BASE="${2:-}"; shift 2 ;;
19
+ --target) TARGET="${2:-}"; shift 2 ;;
20
+ --no-browser) BROWSER=0; shift ;;
21
+ --no-unit) UNIT=0; shift ;;
22
+ --json-out) OUT="${2:-}"; shift 2 ;;
23
+ -h|--help) sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
24
+ *) echo "tf-verify-tests: unknown argument $1" >&2; exit 2 ;;
25
+ esac
26
+ done
27
+ mkdir -p "$(dirname "$OUT")" tests/.artifacts/verify
28
+ PWJSON="tests/.artifacts/verify/playwright.json"; PWLOG="tests/.artifacts/verify/playwright.log"; UNITLOG=""; UNITLINE=""
29
+ ran_any=0
30
+
31
+ if [[ $BROWSER -eq 1 ]]; then
32
+ if ls tests/verify/*.spec.* >/dev/null 2>&1 || ls tests/verify/**/*.spec.* >/dev/null 2>&1; then
33
+ if node -e "require.resolve('@playwright/test')" >/dev/null 2>&1; then
34
+ BASE_URL="$BASE" PLAYWRIGHT_JSON_OUTPUT_NAME="$PWJSON" npx playwright test --reporter=json > "$PWLOG" 2>&1
35
+ echo "browser tests: ran (log $PWLOG)"; ran_any=1
36
+ else
37
+ echo "browser tests: @playwright/test is not installed here (bash .tfcore/utils/tf-verify-env.sh); skipped"; rm -f "$PWJSON"
38
+ fi
39
+ else
40
+ echo "browser tests: no spec under tests/verify/; skipped"; rm -f "$PWJSON"
41
+ fi
42
+ else rm -f "$PWJSON"; fi
43
+
44
+ if [[ $UNIT -eq 1 ]]; then
45
+ if [[ -n "$TARGET" ]] || ls *.sln *.slnx >/dev/null 2>&1 || ls tests/*/*.csproj >/dev/null 2>&1; then
46
+ UNITLINE="$(bash "$HERE/tf-build.sh" test ${TARGET:+"$TARGET"} -- --logger "console;verbosity=normal" 2>&1 | head -1)"
47
+ UNITLOG="$(sed -n 's/.*log \(tests\/\.artifacts\/build\/[^ ;]*\).*/\1/p' <<<"$UNITLINE" | head -1)"
48
+ echo "unit tests: $UNITLINE"; [[ "$UNITLINE" != NOT-RUN* ]] && ran_any=1
49
+ else
50
+ echo "unit tests: no solution or test project found; skipped"
51
+ fi
52
+ fi
53
+
54
+ TF_PWJSON="$PWJSON" TF_UNITLOG="$UNITLOG" TF_UNITLINE="$UNITLINE" TF_OUT="$OUT" python3 - <<'PY'
55
+ import json, os, re
56
+ ID = re.compile(r"(REQ-(?:UI|FN|NFR|RAG)-\d{3})", re.I)
57
+ reqs, browser, unit = {}, {"ran": False, "passed": 0, "failed": 0, "tests": 0}, {"ran": False, "line": os.environ.get("TF_UNITLINE", "")}
58
+
59
+ def add(rid, ok, name, source, reason="", shot=""):
60
+ r = reqs.setdefault(rid.upper(), {"result": "PASS", "source": source, "tests": [], "reason": "", "screenshot": ""})
61
+ r["tests"].append(name)
62
+ if not ok:
63
+ r["result"] = "FAIL"
64
+ if not r["reason"]:
65
+ r["reason"] = reason[:200]
66
+ if shot and not r["screenshot"]:
67
+ r["screenshot"] = shot
68
+
69
+ pw = os.environ.get("TF_PWJSON", "")
70
+ if pw and os.path.isfile(pw):
71
+ try:
72
+ data = json.load(open(pw, encoding="utf-8"))
73
+ browser["ran"] = True
74
+ def walk(suite, path):
75
+ for sp in suite.get("specs", []):
76
+ title = " ".join(path + [sp.get("title", "")])
77
+ for t in sp.get("tests", []):
78
+ res = t.get("results", [])
79
+ last = res[-1] if res else {}
80
+ status = last.get("status", t.get("status", "unexpected"))
81
+ ok = status in ("passed", "expected") or t.get("status") == "expected"
82
+ browser["tests"] += 1
83
+ browser["passed" if ok else "failed"] += 1
84
+ err = ""
85
+ if last.get("error"):
86
+ err = re.sub(r"\x1b\[[0-9;]*m", "", str(last["error"].get("message", "")).split("\n")[0])
87
+ shot = ""
88
+ for a in last.get("attachments", []):
89
+ if a.get("name") == "screenshot" and a.get("path"):
90
+ shot = a["path"].replace("\\", "/")
91
+ shot = shot[shot.find("tests/"):] if "tests/" in shot else shot
92
+ break
93
+ for rid in set(ID.findall(title)):
94
+ add(rid, ok, title.strip(), "browser", err, shot)
95
+ for s in suite.get("suites", []):
96
+ walk(s, path + [s.get("title", "")] if s.get("title") and not s["title"].endswith(".ts") and not s["title"].endswith(".js") and not s["title"].endswith(".mjs") else path)
97
+ for s in data.get("suites", []):
98
+ walk(s, [])
99
+ except Exception as e:
100
+ browser["error"] = str(e)
101
+
102
+ ul = os.environ.get("TF_UNITLOG", "")
103
+ if ul and os.path.isfile(ul):
104
+ unit["ran"] = True
105
+ for line in open(ul, encoding="utf-8", errors="replace"):
106
+ m = re.match(r"\s*(Passed|Failed|Skipped)\s+(\S.*?)\s*(\[[^\]]*\])?\s*$", line)
107
+ if not m or m.group(1) == "Skipped":
108
+ continue
109
+ name = m.group(2).strip()
110
+ for rid in set(ID.findall(name)):
111
+ add(rid, m.group(1) == "Passed", name, "unit", "unit test failed: " + name[:120])
112
+ unit["passed"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "PASS")
113
+ unit["failed"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "FAIL")
114
+
115
+ json.dump({"reqs": reqs, "browser": browser, "unit": unit}, open(os.environ["TF_OUT"], "w"), indent=1)
116
+ p = sum(1 for r in reqs.values() if r["result"] == "PASS"); f = len(reqs) - p
117
+ print(f"rows with a test: {len(reqs)} — {p} PASS, {f} FAIL" + (f" (browser {browser['passed']}/{browser['tests']} tests passed)" if browser["ran"] else ""))
118
+ for rid, r in sorted(reqs.items()):
119
+ if r["result"] == "FAIL":
120
+ print(f" FAIL {rid} — {r['reason']}" + (f" — {r['screenshot']}" if r["screenshot"] else ""))
121
+ print(f"JSON: {os.environ['TF_OUT']}")
122
+ PY
123
+ [[ $ran_any -eq 1 ]] && exit 0 || exit 2
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python3
2
+ """tf-verify-verdict.py — turn a verify run's evidence into one verdict per row (see tf-verify-verdict.sh).
3
+
4
+ bash .tfcore/utils/tf-verify-verdict.sh <App> [--apply] [--dir tests/.artifacts/verify] [--started <ISO>]
5
+
6
+ Reads, from the evidence folder: list.json (the rows), boot.json (what was booted), tests.json (the
7
+ acceptance tests), screens.json (render and visual), assets.json (tf-assets.sh --json-out),
8
+ parity.json (tf-mockup-parity.sh --json-out) and perf/<REQ>.json (tf-perf.sh --json-out per budget
9
+ row). A file that is absent means that check did not run.
10
+
11
+ For each row, the seven checks in order, and the first that fails is the verdict:
12
+ build the app booted (or was built) — boot.json
13
+ acceptance a test carrying the row's id passed — tests.json
14
+ render every anchored control on its screen shows something — screens.json
15
+ assets every stylesheet and script its screen declares arrived — assets.json
16
+ visual nothing overlaps, nothing clipped or off-screen — screens.json
17
+ mockup the screen carries the structure its mockup draws — parity.json
18
+ speed within the row's perf-budget, only when it declares one — perf/<REQ>.json
19
+ (standards has no script yet and is never listed as run)
20
+
21
+ Verdicts: PASS · FAIL (acceptance) · BUILD-FAIL · RENDER-FAIL · ASSET-FAIL · VISUAL-FAIL · MOCKUP-FAIL ·
22
+ PERF-FAIL · NOT-TESTED (no test carried the id; the screen checks passed or did not run) ·
23
+ NOT-OBSERVABLE (an NFR row with no budget and no unit test) · NOT-DRIVEN (its screen was never driven:
24
+ the head could not be booted, or has no driver). A row is Verified only on PASS. NOT-* rows keep their
25
+ status and get a Remark saying so; nothing is ever written as a static-only pass.
26
+
27
+ Writes docs/.last-verify.json (the ledger the verify hook reads: date, app, scope, booted, the checks
28
+ that ran, and every row's verdict) and <dir>/verdicts.json (for tf-verify-emit.sh). With --apply it
29
+ rewrites the Status, % and Remarks cells of every graded row in the checklist. Prints the table.
30
+ Exit 0 · 2 could not run.
31
+ """
32
+ import datetime
33
+ import importlib.util
34
+ import json
35
+ import os
36
+ import re
37
+ import sys
38
+
39
+ HERE = os.path.dirname(os.path.abspath(__file__))
40
+ FAILING = {"BUILD-FAIL": "build", "FAIL": "acceptance", "RENDER-FAIL": "render", "ASSET-FAIL": "assets",
41
+ "VISUAL-FAIL": "visual", "MOCKUP-FAIL": "mockup-parity", "PERF-FAIL": "perf"}
42
+ STATUS = {"PASS": "Verified", "FAIL": "FAIL", "BUILD-FAIL": "FAIL", "RENDER-FAIL": "Needs re-verify", "ASSET-FAIL": "Needs re-verify",
43
+ "VISUAL-FAIL": "Needs re-verify", "MOCKUP-FAIL": "Needs re-verify", "PERF-FAIL": "Needs re-verify"}
44
+
45
+
46
+ def die(msg):
47
+ print(f"tf-verify-verdict: {msg}", file=sys.stderr)
48
+ sys.exit(2)
49
+
50
+
51
+ def load(path):
52
+ try:
53
+ with open(path, encoding="utf-8") as f:
54
+ return json.load(f)
55
+ except FileNotFoundError:
56
+ return None
57
+ except Exception as e:
58
+ die(f"{path} is not valid JSON: {e}")
59
+
60
+
61
+ def perf_grader():
62
+ spec = importlib.util.spec_from_file_location("tf_perf_grade", os.path.join(HERE, "tf-perf-grade.py"))
63
+ mod = importlib.util.module_from_spec(spec)
64
+ spec.loader.exec_module(mod)
65
+ return mod.grade
66
+
67
+
68
+ def norm_route(r):
69
+ r = (r or "").strip().lower().rstrip("/")
70
+ return r or "/"
71
+
72
+
73
+ def words(s, n=60):
74
+ w = s.split()
75
+ return s if len(w) <= n else " ".join(w[:n]) + "…" # the ellipsis rides on the last word: the checker counts words, and a bare "…" was a 61st (miss 29)
76
+
77
+
78
+ def main(argv):
79
+ if len(argv) < 2 or argv[1] in ("-h", "--help"):
80
+ print(__doc__)
81
+ return 0
82
+ app = argv[1]
83
+ apply = "--apply" in argv
84
+ d = argv[argv.index("--dir") + 1] if "--dir" in argv else os.path.join("tests", ".artifacts", "verify")
85
+ started = argv[argv.index("--started") + 1] if "--started" in argv else ""
86
+ today = datetime.date.today().isoformat()
87
+
88
+ lst = load(os.path.join(d, "list.json")) or die("list.json missing: run tf-verify-list.sh first")
89
+ boot = load(os.path.join(d, "boot.json")) or {"mode": "none", "reason": "tf-verify-boot.sh was not run", "reason_kind": "host", "head": "none"}
90
+ tests = (load(os.path.join(d, "tests.json")) or {}).get("reqs", {})
91
+ tests_ran = os.path.isfile(os.path.join(d, "tests.json"))
92
+ screens = load(os.path.join(d, "screens.json")) or {}
93
+ scr_by_name = {s["name"]: s for s in screens.get("screens", [])}
94
+ assets = load(os.path.join(d, "assets.json")) or {}
95
+ pages = {norm_route(p.get("path")): p for p in assets.get("pages", [])}
96
+ parity = load(os.path.join(d, "parity.json")) or {}
97
+ par_by_name = {s.get("screen"): s for s in parity.get("screens", [])}
98
+ par_by_route = {norm_route(s.get("route")): s for s in parity.get("screens", [])}
99
+ grade = perf_grader()
100
+
101
+ booted = boot.get("mode") not in (None, "none")
102
+ build_failed = boot.get("reason_kind") == "build-error"
103
+ boot_text = (f"{boot.get('head')} via {boot.get('rung')} at {boot.get('url')}" if booted
104
+ else f"not booted: {boot.get('reason')}")
105
+ checks_ran = set()
106
+ if booted or build_failed:
107
+ checks_ran.add("build")
108
+
109
+ rows_out, ledger_rows, table = [], {}, []
110
+ for r in lst["rows"]:
111
+ rid = r["id"]
112
+ prior = r.get("status_raw") or ""
113
+ notes = []
114
+ scr = scr_by_name.get(r.get("screen") or "")
115
+ driven = bool(scr) and scr.get("render") != "UNREACHABLE"
116
+ t = tests.get(rid)
117
+ # every check on its own: (name, ran, failed, verdict, class, detail, evidence); the first failure wins
118
+ checks = []
119
+
120
+ # 1 build
121
+ checks.append(("build", booted or build_failed, build_failed, "BUILD-FAIL", "build-error", boot.get("reason", "build failed"), ""))
122
+ # 2 acceptance
123
+ if t:
124
+ checks.append(("acceptance", True, t["result"] != "PASS", "FAIL", "assert-fail", t.get("reason") or "test failed", t.get("screenshot") or ""))
125
+ # 3 render
126
+ if scr:
127
+ if scr.get("render") == "UNREACHABLE":
128
+ f = next((x for w in scr["widths"] for x in w.get("findings", [])), {})
129
+ checks.append(("render", True, True, "RENDER-FAIL", "other", f.get("detail", "screen unreachable"), ""))
130
+ else:
131
+ bad = [(w, f) for w in scr["widths"] for f in w.get("findings", []) if f["check"] == "render"]
132
+ w, f = bad[0] if bad else (None, None)
133
+ checks.append(("render", True, bool(bad), "RENDER-FAIL", f["class"] if f else None,
134
+ f"{f['detail']} on {r['screen']} @{w['width']}" if f else "", w.get("screenshot", "") if w else ""))
135
+ # 4 assets
136
+ pg = pages.get(norm_route(r.get("route"))) if r.get("route") else None
137
+ if pg and pg.get("graded", 0) > 0:
138
+ bad = next((a for a in pg.get("assets", []) if a.get("graded") and (a.get("status") in (None, 0) or a.get("status") >= 400 or a.get("bytes", 1) == 0)), None)
139
+ checks.append(("assets", True, pg.get("failed", 0) > 0, "ASSET-FAIL", "missing-asset",
140
+ f"{pg.get('failed', 0)} declared asset(s) did not arrive on {r['route']}" + (f": {bad.get('url')} → {bad.get('status')}" if bad else ""), ""))
141
+ elif pg and pg.get("redirected"):
142
+ notes.append("assets not graded (auth wall)")
143
+ # 5 visual
144
+ if scr and driven:
145
+ bad = [(w, f) for w in scr["widths"] for f in w.get("findings", []) if f["check"] == "visual"]
146
+ w, f = bad[0] if bad else (None, None)
147
+ checks.append(("visual", True, bool(bad), "VISUAL-FAIL", f["class"] if f else None,
148
+ f"{f['detail']} on {r['screen']}" if f else "", w.get("screenshot", "") if w else ""))
149
+ # 6 mockup parity
150
+ ps = (par_by_name.get(r.get("screen")) or par_by_route.get(norm_route(r.get("route")))) if r.get("screen") else None
151
+ if ps:
152
+ v = ps.get("verdict")
153
+ if v in ("PASS", "FAIL"):
154
+ f = (ps.get("findings") or [{}])[0]
155
+ checks.append(("mockup-parity", True, v == "FAIL", "MOCKUP-FAIL", "mockup-drift",
156
+ f"{f.get('class', 'drift')} on {f.get('key', ps.get('screen'))} @{f.get('width', '')}: {str(f.get('detail', ''))[:80]}",
157
+ os.path.join(d, "parity.json")))
158
+ elif v == "UNGRADEABLE":
159
+ notes.append("mockup-parity UNGRADEABLE (add data-testid anchors to the mockup)")
160
+ elif v == "NO-MOCKUP":
161
+ notes.append("no mockup to compare")
162
+ # 7 speed
163
+ if r.get("perf_budget"):
164
+ pf = load(os.path.join(d, "perf", f"{rid}.json"))
165
+ if pf is None:
166
+ notes.append("perf not measured (no tf-perf run for this row)")
167
+ else:
168
+ g = grade(r["perf_budget"], pf)
169
+ if g["verdict"] in ("PERF-OK", "PERF-MARGINAL", "PERF-FAIL"):
170
+ checks.append(("perf", True, g["verdict"] == "PERF-FAIL", "PERF-FAIL", g.get("failure_class") or "slow-ttfb", g["reason"], ""))
171
+ if g["verdict"] == "PERF-MARGINAL":
172
+ notes.append(f"⚠ perf: {g['reason']}")
173
+ elif g["verdict"] == "PERF-UNMEASURED":
174
+ notes.append(f"perf not measured: {g['reason']}")
175
+
176
+ ran = [c[0] for c in checks if c[1]]
177
+ first = next((c for c in checks if c[1] and c[2]), None)
178
+ verdict, cls, detail, evidence = (first[3], first[4], first[5], first[6]) if first else (None, None, "", "")
179
+
180
+ # the honest non-verdicts
181
+ if verdict is None:
182
+ if r["class"] == "NFR" and not r.get("perf_budget") and not t:
183
+ verdict = "NOT-OBSERVABLE"
184
+ detail = "NFR row with no perf-budget and no unit test carrying its id"
185
+ elif not t and r.get("screen") and not driven:
186
+ verdict = "NOT-DRIVEN"
187
+ detail = (f"{boot.get('head', 'the app')} not driven: {boot.get('reason')}" if not booted
188
+ else f"screen {r['screen']} was not driven in this run")
189
+ elif not t:
190
+ verdict = "NOT-TESTED"
191
+ detail = f"no test named {rid} ran" + (f"; screen {r['screen']} renders and looks right" if driven else "")
192
+ elif r.get("screen") and not driven and not booted:
193
+ verdict = "NOT-DRIVEN"
194
+ detail = f"test passed but {boot.get('head', 'the app')} was not driven: {boot.get('reason')}"
195
+ else:
196
+ verdict = "PASS"
197
+ parts = [f"test {t['tests'][0][:60]}" if t and t.get("tests") else "test passed"]
198
+ if driven:
199
+ parts.append(f"{r['screen']} renders and looks right @{'/'.join(str(w['width']) for w in scr['widths'])}")
200
+ if "mockup-parity" in ran:
201
+ parts.append("matches its mockup")
202
+ if "perf" in ran:
203
+ parts.append("within its speed budget")
204
+ detail = "; ".join(parts)
205
+
206
+ status = STATUS.get(verdict, prior)
207
+ pct = r.get("pct")
208
+ if verdict == "PASS":
209
+ pct = 100
210
+ elif verdict in ("RENDER-FAIL", "ASSET-FAIL", "VISUAL-FAIL", "MOCKUP-FAIL", "PERF-FAIL"):
211
+ pct = min(pct if pct is not None else 75, 75)
212
+ gate = FAILING.get(verdict)
213
+ marks = {"FAIL": "⚠ acceptance", "BUILD-FAIL": "⚠ build", "RENDER-FAIL": "⚠ render", "ASSET-FAIL": "⚠ assets",
214
+ "VISUAL-FAIL": "⚠ visual", "MOCKUP-FAIL": "⚠ mockup-parity", "PERF-FAIL": "⚠ perf",
215
+ "NOT-TESTED": "not verified", "NOT-OBSERVABLE": "not observable", "NOT-DRIVEN": "not verified", "PASS": "PASS"}
216
+ remark = f"{today} verify: {marks[verdict]} — {detail}"
217
+ if evidence:
218
+ remark += f" ({evidence})"
219
+ if notes:
220
+ remark += "; " + "; ".join(notes[:2])
221
+ remark = words(remark.replace("|", "/"), 60)
222
+ emit = verdict == "PASS" or gate is not None
223
+ row = {"id": rid, "class": r["class"], "verdict": verdict, "gate": gate, "gates_run": ran, "failure_class": cls if gate else None,
224
+ "prior_verdict": prior or None, "status": status, "pct": pct, "remark": remark, "screen": r.get("screen"),
225
+ "emit_gate": emit, "title": r.get("title", "")}
226
+ rows_out.append(row)
227
+ ledger_rows[rid] = verdict
228
+ table.append((rid, verdict, gate or "-", status, detail[:70]))
229
+
230
+ checks = []
231
+ for c in ("build", "acceptance", "render", "assets", "visual", "mockup-parity", "perf"):
232
+ if any(c in x["gates_run"] for x in rows_out):
233
+ checks.append(c)
234
+ ledger = {"date": today, "app": app, "scope": lst.get("scope", "all"), "booted": boot_text, "gates": checks,
235
+ "evidence": d, "run_id": started, "rows": ledger_rows}
236
+ os.makedirs("docs", exist_ok=True)
237
+ with open(os.path.join("docs", ".last-verify.json"), "w", encoding="utf-8") as f:
238
+ json.dump(ledger, f, indent=1)
239
+ with open(os.path.join(d, "verdicts.json"), "w", encoding="utf-8") as f:
240
+ json.dump({"app": app, "scope": lst.get("scope", "all"), "date": today, "started": started, "booted": boot_text,
241
+ "build_result": "fail" if build_failed else ("pass" if booted else "not-run"), "checks": checks,
242
+ "checklist": lst.get("checklist"), "rows": rows_out}, f, indent=1)
243
+
244
+ applied = 0
245
+ if apply:
246
+ cl = lst.get("checklist")
247
+ text = open(cl, encoding="utf-8").read()
248
+ by_id = {x["id"]: x for x in rows_out}
249
+ out = []
250
+ for line in text.splitlines(keepends=True):
251
+ m = re.match(r"^(\s*\|\s*`?)(REQ-[A-Z]+-\d+)(`?\s*\|)", line)
252
+ if m and m.group(2) in by_id:
253
+ c = line.rstrip("\r\n").strip().strip("|").split("|")
254
+ if len(c) >= 6:
255
+ x = by_id[m.group(2)]
256
+ c[2] = f" {x['status']} "
257
+ c[3] = f" {x['pct'] if x['pct'] is not None else ''}{'%' if x['pct'] is not None and '%' in c[3] else ''} "
258
+ c[4] = f" {x['remark']} "
259
+ nl = "\r\n" if line.endswith("\r\n") else "\n"
260
+ line = "|" + "|".join(c) + "|" + nl
261
+ applied += 1
262
+ out.append(line)
263
+ with open(cl, "w", encoding="utf-8") as f:
264
+ f.write("".join(out))
265
+
266
+ counts = {}
267
+ for x in rows_out:
268
+ counts[x["verdict"]] = counts.get(x["verdict"], 0) + 1
269
+ print(f"# tf-verify-verdict — {app} — scope {lst.get('scope')} — booted: {boot_text}")
270
+ print(f"Checks that ran: {', '.join(checks) or 'none'}. Rows: " + ", ".join(f"{v} {k}" for k, v in sorted(counts.items())))
271
+ print()
272
+ print("| ID | Verdict | First failing check | Status | Detail |")
273
+ print("|---|---|---|---|---|")
274
+ for t in table:
275
+ print("| " + " | ".join(str(x).replace("|", "/") for x in t) + " |")
276
+ print()
277
+ print(f"Ledger: docs/.last-verify.json ({len(ledger_rows)} rows). Verdicts: {os.path.join(d, 'verdicts.json')}."
278
+ + (f" Checklist: {applied} row(s) rewritten in {lst.get('checklist')}." if apply else " Checklist untouched (no --apply)."))
279
+ return 0
280
+
281
+
282
+ if __name__ == "__main__":
283
+ sys.exit(main(sys.argv))
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # tf-verify-verdict.sh — one verdict per row from a verify run's evidence (Sitting 4c, 2026-09-06).
3
+ #
4
+ # bash .tfcore/utils/tf-verify-verdict.sh <App> [--apply] [--dir tests/.artifacts/verify] [--started <ISO>]
5
+ #
6
+ # Reads list.json, boot.json, tests.json, screens.json, assets.json, parity.json and perf/<REQ>.json
7
+ # from the evidence folder, applies the seven checks in order, records the first that fails, writes
8
+ # docs/.last-verify.json (the ledger the verify hook reads, with every row's verdict) and verdicts.json,
9
+ # and with --apply rewrites the Status, % and Remarks cells of the graded rows in the checklist.
10
+ # A row is Verified only when everything that ran passed; a row whose screen was never driven keeps
11
+ # its status and says so. Details: tf-verify-verdict.py.
12
+ set -u
13
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ command -v python3 >/dev/null 2>&1 || { echo "tf-verify-verdict: python3 is required" >&2; exit 2; }
15
+ exec python3 "$HERE/tf-verify-verdict.py" "$@"
@@ -99,6 +99,65 @@ case "$cmd" in
99
99
  OUTCOME="${1:-complete}"; SUMMARY="${2:-}"
100
100
  case "$OUTCOME" in complete|blocked) ;; *) SUMMARY="$OUTCOME $SUMMARY"; OUTCOME="complete" ;; esac
101
101
  mkdir -p "$STATE_DIR" 2>/dev/null || true
102
+ # The sentinel comes AFTER the status gate: on 2026-09-06 an OpenCode build wrote
103
+ # `blocked` with the status file untouched since day-1, every row Not Started and no run
104
+ # record (MISS-TechieFlow-20260906-13). So `done` is refused, without writing the
105
+ # sentinel, while the status file is older than the goal run's start or no run record
106
+ # has been appended since the goal started. Exit stays 0 (no veto); the message says
107
+ # what to finish first. TF_YOLO_DONE_FORCE=1 skips the check (owner use).
108
+ if [[ "${TF_YOLO_DONE_FORCE:-0}" != "1" && -f "$STATE_DIR/goal.json" ]]; then
109
+ _why="$(python3 - "$STATE_DIR/goal.json" "$ROOT" <<'PY2' 2>/dev/null
110
+ import json, os, sys, datetime
111
+ gj, root = sys.argv[1], sys.argv[2]
112
+ try:
113
+ started = json.load(open(gj)).get("started") or ""
114
+ t0 = datetime.datetime.strptime(started[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=datetime.timezone.utc).timestamp()
115
+ except Exception:
116
+ sys.exit(0)
117
+ why = []
118
+ ps = os.path.join(root, "PROJECT-STATUS" + ".md")
119
+ if os.path.isfile(ps) and os.path.getmtime(ps) < t0 - 60:
120
+ why.append("the status file has not been written since this goal run started (%s)" % started)
121
+ # The run record must be for the command the phase marker names (tf-phase.sh start <cmd>):
122
+ # a verify-phase record alone let a build close without its build-phase record
123
+ # (MISS-TechieFlow-20260906-15).
124
+ want_cmd = None
125
+ try:
126
+ pj = os.path.join(root, ".tfcore", ".session", "phase.json")
127
+ if os.path.isfile(pj) and os.path.getmtime(pj) >= t0 - 60:
128
+ want_cmd = (json.load(open(pj)) or {}).get("cmd") or None
129
+ except Exception:
130
+ want_cmd = None
131
+ runs = os.path.join(root, "docs", "metrics", "runs.jsonl"); last = None; last_cmd = None
132
+ try:
133
+ for line in open(runs, encoding="utf-8", errors="replace"):
134
+ try: r = json.loads(line)
135
+ except Exception: continue
136
+ ts = r.get("ts")
137
+ if not ts: continue
138
+ try:
139
+ te = datetime.datetime.strptime(ts[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=datetime.timezone.utc).timestamp()
140
+ except Exception:
141
+ continue
142
+ if te >= t0 - 60:
143
+ last = ts
144
+ if want_cmd and r.get("kind") == "run" and r.get("cmd") == want_cmd:
145
+ last_cmd = ts
146
+ except Exception:
147
+ pass
148
+ if os.path.isfile(runs):
149
+ if last is None:
150
+ why.append("no run record has been appended to docs/metrics/runs.jsonl since the goal started")
151
+ elif want_cmd and last_cmd is None:
152
+ why.append("no run record for %s (the command the phase marker names) has been appended since the goal started" % want_cmd)
153
+ print("; ".join(why))
154
+ PY2
155
+ )"
156
+ if [[ -n "$_why" ]]; then
157
+ echo "GOAL-DONE refused: the status gate has not run — $_why. Finish the gate (_status-update-gate.md: the status file, its HTML, the checker, the run record), then run this command again. Nothing was written."
158
+ exit 0
159
+ fi
160
+ fi
102
161
  printf '{"outcome":%s,"summary":%s,"ts":"%s"}\n' \
103
162
  "$(json_escape "$OUTCOME")" "$(json_escape "$SUMMARY")" "$(now_utc)" > "$DONE" 2>/dev/null || true
104
163
  # The grant ENDS WITH THE GOAL. Until 2026-08-28 `done` left the flag in place,