@techierathore/techieflow 1.0.1 → 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.
- package/.claude/commands/TechieFlow/agents/analyst.md +0 -5
- package/.claude/commands/TechieFlow/agents/architect.md +0 -6
- package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +39 -12
- package/.tfcore/agents/analyst.md +0 -5
- package/.tfcore/agents/architect.md +0 -6
- package/.tfcore/agents/flow-master.md +41 -215
- package/.tfcore/agents/verifier.md +23 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +56 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +25 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +20 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +1 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +43 -7
- package/.tfcore/telemetry/tf-metrics.sh +152 -17
- package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.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-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.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-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.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-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +191 -0
- package/.tfcore/utils/tf-build-list.sh +20 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1267 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +203 -13
- package/.tfcore/utils/tf-fix-close.sh +83 -0
- package/.tfcore/utils/tf-goal.sh +193 -62
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +211 -0
- package/.tfcore/utils/tf-misses-md.sh +13 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +74 -0
- package/.tfcore/utils/tf-routing-bind.sh +10 -5
- package/.tfcore/utils/tf-routing.sh +6 -7
- package/.tfcore/utils/tf-split-brd.py +304 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +309 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +283 -0
- package/.tfcore/utils/tf-verify-screens.sh +22 -0
- package/.tfcore/utils/tf-verify-tests.sh +123 -0
- package/.tfcore/utils/tf-verify-verdict.py +283 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +114 -1149
- package/docs/TechieFlow-Installation.md +27 -7
- package/opencode.jsonc +13 -33
- package/package.json +4 -7
- package/scripts/install.mjs +73 -46
- package/scripts/npm-cleanup.mjs +137 -0
- package/scripts/npm-postinstall.mjs +49 -0
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
- package/scripts/test-install.mjs +0 -505
- package/scripts/validate.mjs +0 -163
|
@@ -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" "$@"
|
package/.tfcore/utils/tf-yolo.sh
CHANGED
|
@@ -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,
|