@techierathore/techieflow 1.0.2 → 1.0.4
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 +5 -13
- package/.claude/commands/TechieFlow/agents/architect.md +1 -16
- package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- 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 +21 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- 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/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -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 +40 -13
- package/.tfcore/agents/analyst.md +5 -13
- package/.tfcore/agents/architect.md +1 -16
- package/.tfcore/agents/flow-master.md +42 -215
- package/.tfcore/agents/verifier.md +24 -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 +68 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- 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 +86 -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/_owner-language.md +26 -0
- 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 +21 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- 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/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +3 -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 +100 -8
- package/.tfcore/telemetry/tf-metrics.sh +564 -32
- 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-brief-tmpl.md +65 -0
- 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-decision-request-tmpl.md +85 -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-library-feedback-tmpl.md +86 -31
- 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/html-render-shell.md +1 -1
- 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 +211 -0
- package/.tfcore/utils/tf-build-list.sh +21 -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 +1369 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +545 -18
- package/.tfcore/utils/tf-fix-close.sh +115 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +284 -66
- 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 +208 -0
- package/.tfcore/utils/tf-misses-md.sh +14 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-model-pick.sh +475 -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 +92 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +97 -6
- package/.tfcore/utils/tf-routing.sh +13 -7
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +309 -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 +358 -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 +355 -0
- package/.tfcore/utils/tf-verify-screens.sh +31 -0
- package/.tfcore/utils/tf-verify-tests.sh +166 -0
- package/.tfcore/utils/tf-verify-verdict.py +292 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +120 -1145
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +37 -33
- package/package.json +1 -5
- package/scripts/install.mjs +56 -46
- 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/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-verify-screens.sh — drive every screen and check render and visual truth (Sitting 4c, 2026-09-06).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-verify-screens.sh --list tests/.artifacts/verify/list.json --base http://localhost:5099 \
|
|
5
|
+
# [--login-path /login --user ravi --password '…'] [--cookie 'k=v'] [--storage-state file]
|
|
6
|
+
# bash .tfcore/utils/tf-verify-screens.sh --screen home=/ --screen posts=/posts --base URL # a smoke: changed screens only
|
|
7
|
+
# bash .tfcore/utils/tf-verify-screens.sh --list … --cdp http://172.18.144.1:9223 # an embedded-browser desktop head
|
|
8
|
+
# … --error-selector '.my-crash-banner' # the banner THIS stack shows when its client side falls over
|
|
9
|
+
# # (repeatable; defaults cover one framework's id and [data-error-ui])
|
|
10
|
+
#
|
|
11
|
+
# For every screen, at 1280 and 390 px: every control the mockup anchors is present and shows
|
|
12
|
+
# something, no header-only table, no blank page, no Blazor or console error (render); nothing
|
|
13
|
+
# overlaps, nothing has zero size or sits off-screen, no sideways scroll, a stylesheet is loaded
|
|
14
|
+
# (visual). Overlap and off-screen are measured on the rectangle an element paints in, clipped by
|
|
15
|
+
# every scrolling ancestor, and the page is read only after its first render (--render-wait,
|
|
16
|
+
# default 5000 ms). Saves a screenshot per screen and width under tests/.artifacts/verify/screens/ and
|
|
17
|
+
# writes tests/.artifacts/verify/screens.json; that JSON is the evidence a smoke names before a
|
|
18
|
+
# row is written Implemented, and what the verdict script reads. It decides no row's status.
|
|
19
|
+
# Exit 0 every screen OK · 5 a screen failed · 2 the app was unreachable · 4 Playwright missing.
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
# `--help` is a request and exits 0; no argument at all is a mistake and exits 3. (The one-line
|
|
23
|
+
# form this replaces computed "0--help" as its exit code and printed a bash error under the help.)
|
|
24
|
+
case "${1:-}" in
|
|
25
|
+
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
26
|
+
"") sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 3 ;;
|
|
27
|
+
esac
|
|
28
|
+
command -v node >/dev/null 2>&1 || { echo "tf-verify-screens: node is required (bash .tfcore/utils/tf-verify-env.sh)" >&2; exit 4; }
|
|
29
|
+
node -e "import('playwright').then(()=>process.exit(0)).catch(()=>process.exit(1))" 2>/dev/null || {
|
|
30
|
+
echo "tf-verify-screens: the 'playwright' package is not resolvable from this folder; run bash .tfcore/utils/tf-verify-env.sh" >&2; exit 4; }
|
|
31
|
+
exec node "$HERE/tf-verify-screens.mjs" "$@"
|
|
@@ -0,0 +1,166 @@
|
|
|
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 a test carrying its id passed and none failed,
|
|
10
|
+
# FAIL when one failed (the reason and the screenshot, when the reporter kept one, are recorded),
|
|
11
|
+
# NOT-TESTED when every test carrying its id was SKIPPED, and absent when no test carries its id.
|
|
12
|
+
# A skipped clause is a third outcome, never a failure: `test.skip(!SEEDED, …)` says the state does
|
|
13
|
+
# not exist in the data, which is not a defect (TF-022). Writes tests/.artifacts/verify/tests.json.
|
|
14
|
+
# Exit 0 ran (whatever the results) · 2 nothing could run.
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
BASE=""; TARGET=""; BROWSER=1; UNIT=1; OUT="tests/.artifacts/verify/tests.json"
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--base) BASE="${2:-}"; shift 2 ;;
|
|
21
|
+
--target) TARGET="${2:-}"; shift 2 ;;
|
|
22
|
+
--no-browser) BROWSER=0; shift ;;
|
|
23
|
+
--no-unit) UNIT=0; shift ;;
|
|
24
|
+
--json-out) OUT="${2:-}"; shift 2 ;;
|
|
25
|
+
-h|--help) sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
26
|
+
*) echo "tf-verify-tests: unknown argument $1" >&2; exit 2 ;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
mkdir -p "$(dirname "$OUT")" tests/.artifacts/verify
|
|
30
|
+
PWJSON="tests/.artifacts/verify/playwright.json"; PWLOG="tests/.artifacts/verify/playwright.log"; UNITLOG=""; UNITLINE=""
|
|
31
|
+
ran_any=0
|
|
32
|
+
|
|
33
|
+
if [[ $BROWSER -eq 1 ]]; then
|
|
34
|
+
if ls tests/verify/*.spec.* >/dev/null 2>&1 || ls tests/verify/**/*.spec.* >/dev/null 2>&1; then
|
|
35
|
+
if node -e "require.resolve('@playwright/test')" >/dev/null 2>&1; then
|
|
36
|
+
BASE_URL="$BASE" PLAYWRIGHT_JSON_OUTPUT_NAME="$PWJSON" npx playwright test --reporter=json > "$PWLOG" 2>&1
|
|
37
|
+
echo "browser tests: ran (log $PWLOG)"; ran_any=1
|
|
38
|
+
else
|
|
39
|
+
echo "browser tests: @playwright/test is not installed here (bash .tfcore/utils/tf-verify-env.sh); skipped"; rm -f "$PWJSON"
|
|
40
|
+
fi
|
|
41
|
+
else
|
|
42
|
+
echo "browser tests: no spec under tests/verify/; skipped"; rm -f "$PWJSON"
|
|
43
|
+
fi
|
|
44
|
+
else rm -f "$PWJSON"; fi
|
|
45
|
+
|
|
46
|
+
if [[ $UNIT -eq 1 ]]; then
|
|
47
|
+
if [[ -n "$TARGET" ]] || ls *.sln *.slnx >/dev/null 2>&1 || ls tests/*/*.csproj >/dev/null 2>&1; then
|
|
48
|
+
UNITLINE="$(bash "$HERE/tf-build.sh" test ${TARGET:+"$TARGET"} -- --logger "console;verbosity=normal" 2>&1 | head -1)"
|
|
49
|
+
UNITLOG="$(sed -n 's/.*log \(tests\/\.artifacts\/build\/[^ ;]*\).*/\1/p' <<<"$UNITLINE" | head -1)"
|
|
50
|
+
echo "unit tests: $UNITLINE"; [[ "$UNITLINE" != NOT-RUN* ]] && ran_any=1
|
|
51
|
+
else
|
|
52
|
+
echo "unit tests: no solution or test project found; skipped"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
TF_PWJSON="$PWJSON" TF_UNITLOG="$UNITLOG" TF_UNITLINE="$UNITLINE" TF_OUT="$OUT" python3 - <<'PY'
|
|
57
|
+
import json, os, re
|
|
58
|
+
ID = re.compile(r"(REQ-(?:UI|FN|NFR|RAG)-\d{3})", re.I)
|
|
59
|
+
reqs = {}
|
|
60
|
+
browser = {"ran": False, "passed": 0, "failed": 0, "skipped": 0, "tests": 0}
|
|
61
|
+
unit = {"ran": False, "line": os.environ.get("TF_UNITLINE", "")}
|
|
62
|
+
|
|
63
|
+
def add(rid, outcome, name, source, reason="", shot=""):
|
|
64
|
+
"""Record one test against a row. `outcome` is "pass", "fail" or "skip".
|
|
65
|
+
|
|
66
|
+
A SKIPPED clause is the third outcome and neither of the other two. A conditional
|
|
67
|
+
`test.skip(!SEEDED, "needs the seeded dataset")` says the state does not exist in the
|
|
68
|
+
data yet: that is not a pass, and it is not a defect either. Counting it as a failure
|
|
69
|
+
put FAIL on rows no code could clear and sent build-phase back into FIX mode against
|
|
70
|
+
them (TF-022, TfLens 2026-09-09). A row whose clauses were ALL skipped ends
|
|
71
|
+
NOT-TESTED, which the verdict script reads as not measured — never Verified, never a
|
|
72
|
+
defect. tests/regression/run.sh tf_022."""
|
|
73
|
+
r = reqs.setdefault(rid.upper(), {"result": "NOT-TESTED", "source": source, "tests": [],
|
|
74
|
+
"skipped": [], "passed": 0, "failed": 0,
|
|
75
|
+
"reason": "", "screenshot": ""})
|
|
76
|
+
if outcome == "skip":
|
|
77
|
+
r["skipped"].append(name)
|
|
78
|
+
if not r["reason"]:
|
|
79
|
+
r["reason"] = (reason or "the test was skipped")[:200]
|
|
80
|
+
return
|
|
81
|
+
r["tests"].append(name)
|
|
82
|
+
if outcome == "pass":
|
|
83
|
+
r["passed"] += 1
|
|
84
|
+
else:
|
|
85
|
+
if r["result"] != "FAIL": # the first real failure owns the reason
|
|
86
|
+
r["reason"] = reason[:200]
|
|
87
|
+
r["failed"] += 1
|
|
88
|
+
if shot and not r["screenshot"]:
|
|
89
|
+
r["screenshot"] = shot
|
|
90
|
+
r["result"] = "FAIL" if r["failed"] else "PASS"
|
|
91
|
+
|
|
92
|
+
pw = os.environ.get("TF_PWJSON", "")
|
|
93
|
+
if pw and os.path.isfile(pw):
|
|
94
|
+
try:
|
|
95
|
+
data = json.load(open(pw, encoding="utf-8"))
|
|
96
|
+
browser["ran"] = True
|
|
97
|
+
def walk(suite, path):
|
|
98
|
+
for sp in suite.get("specs", []):
|
|
99
|
+
title = " ".join(path + [sp.get("title", "")])
|
|
100
|
+
for t in sp.get("tests", []):
|
|
101
|
+
res = t.get("results", [])
|
|
102
|
+
last = res[-1] if res else {}
|
|
103
|
+
status = last.get("status", t.get("status", "unexpected"))
|
|
104
|
+
# skipped is read FIRST: it is neither a pass nor a failure (TF-022)
|
|
105
|
+
if status == "skipped" or t.get("status") == "skipped":
|
|
106
|
+
outcome = "skip"
|
|
107
|
+
elif status in ("passed", "expected") or t.get("status") == "expected":
|
|
108
|
+
outcome = "pass"
|
|
109
|
+
else:
|
|
110
|
+
outcome = "fail"
|
|
111
|
+
browser["tests"] += 1
|
|
112
|
+
browser[{"pass": "passed", "fail": "failed", "skip": "skipped"}[outcome]] += 1
|
|
113
|
+
err = ""
|
|
114
|
+
if outcome == "skip":
|
|
115
|
+
why = [a.get("description") for a in (t.get("annotations") or [])
|
|
116
|
+
if a.get("type") == "skip" and a.get("description")]
|
|
117
|
+
err = "skipped" + (": " + str(why[0]) if why else "")
|
|
118
|
+
elif last.get("error"):
|
|
119
|
+
err = re.sub(r"\x1b\[[0-9;]*m", "", str(last["error"].get("message", "")).split("\n")[0])
|
|
120
|
+
shot = ""
|
|
121
|
+
for a in last.get("attachments", []):
|
|
122
|
+
if a.get("name") == "screenshot" and a.get("path"):
|
|
123
|
+
shot = a["path"].replace("\\", "/")
|
|
124
|
+
shot = shot[shot.find("tests/"):] if "tests/" in shot else shot
|
|
125
|
+
break
|
|
126
|
+
for rid in set(ID.findall(title)):
|
|
127
|
+
add(rid, outcome, title.strip(), "browser", err, shot)
|
|
128
|
+
for s in suite.get("suites", []):
|
|
129
|
+
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)
|
|
130
|
+
for s in data.get("suites", []):
|
|
131
|
+
walk(s, [])
|
|
132
|
+
except Exception as e:
|
|
133
|
+
browser["error"] = str(e)
|
|
134
|
+
|
|
135
|
+
ul = os.environ.get("TF_UNITLOG", "")
|
|
136
|
+
if ul and os.path.isfile(ul):
|
|
137
|
+
unit["ran"] = True
|
|
138
|
+
for line in open(ul, encoding="utf-8", errors="replace"):
|
|
139
|
+
m = re.match(r"\s*(Passed|Failed|Skipped)\s+(\S.*?)\s*(\[[^\]]*\])?\s*$", line)
|
|
140
|
+
if not m:
|
|
141
|
+
continue
|
|
142
|
+
# a skipped unit test is recorded as skipped, not dropped: the row then reads
|
|
143
|
+
# NOT-TESTED with the evidence, rather than looking as if no test exists (TF-022)
|
|
144
|
+
outcome = {"Passed": "pass", "Failed": "fail", "Skipped": "skip"}[m.group(1)]
|
|
145
|
+
name = m.group(2).strip()
|
|
146
|
+
for rid in set(ID.findall(name)):
|
|
147
|
+
add(rid, outcome, name, "unit",
|
|
148
|
+
("unit test failed: " if outcome == "fail" else "unit test skipped: ") + name[:120])
|
|
149
|
+
unit["passed"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "PASS")
|
|
150
|
+
unit["failed"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "FAIL")
|
|
151
|
+
unit["not_tested"] = sum(1 for r in reqs.values() if r["source"] == "unit" and r["result"] == "NOT-TESTED")
|
|
152
|
+
|
|
153
|
+
json.dump({"reqs": reqs, "browser": browser, "unit": unit}, open(os.environ["TF_OUT"], "w"), indent=1)
|
|
154
|
+
p = sum(1 for r in reqs.values() if r["result"] == "PASS")
|
|
155
|
+
f = sum(1 for r in reqs.values() if r["result"] == "FAIL")
|
|
156
|
+
ns = sum(1 for r in reqs.values() if r["result"] == "NOT-TESTED")
|
|
157
|
+
print(f"rows with a test: {len(reqs)} — {p} PASS, {f} FAIL, {ns} NOT-TESTED (every clause skipped)"
|
|
158
|
+
+ (f" (browser {browser['passed']}/{browser['tests']} tests passed, {browser['skipped']} skipped)" if browser["ran"] else ""))
|
|
159
|
+
for rid, r in sorted(reqs.items()):
|
|
160
|
+
if r["result"] == "FAIL":
|
|
161
|
+
print(f" FAIL {rid} — {r['reason']}" + (f" — {r['screenshot']}" if r["screenshot"] else ""))
|
|
162
|
+
elif r["result"] == "NOT-TESTED":
|
|
163
|
+
print(f" NOT-TESTED {rid} — {len(r['skipped'])} test(s) skipped, none ran — {r['reason']}")
|
|
164
|
+
print(f"JSON: {os.environ['TF_OUT']}")
|
|
165
|
+
PY
|
|
166
|
+
[[ $ran_any -eq 1 ]] && exit 0 || exit 2
|
|
@@ -0,0 +1,292 @@
|
|
|
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, or every test carrying it was skipped; the screen
|
|
23
|
+
checks passed or did not run) ·
|
|
24
|
+
NOT-OBSERVABLE (an NFR row with no budget and no unit test) · NOT-DRIVEN (its screen was never driven:
|
|
25
|
+
the head could not be booted, or has no driver). A row is Verified only on PASS. NOT-* rows keep their
|
|
26
|
+
status and get a Remark saying so; nothing is ever written as a static-only pass.
|
|
27
|
+
|
|
28
|
+
Writes docs/.last-verify.json (the ledger the verify hook reads: date, app, scope, booted, the checks
|
|
29
|
+
that ran, and every row's verdict) and <dir>/verdicts.json (for tf-verify-emit.sh). With --apply it
|
|
30
|
+
rewrites the Status, % and Remarks cells of every graded row in the checklist. Prints the table.
|
|
31
|
+
Exit 0 · 2 could not run.
|
|
32
|
+
"""
|
|
33
|
+
import datetime
|
|
34
|
+
import importlib.util
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
41
|
+
FAILING = {"BUILD-FAIL": "build", "FAIL": "acceptance", "RENDER-FAIL": "render", "ASSET-FAIL": "assets",
|
|
42
|
+
"VISUAL-FAIL": "visual", "MOCKUP-FAIL": "mockup-parity", "PERF-FAIL": "perf"}
|
|
43
|
+
STATUS = {"PASS": "Verified", "FAIL": "FAIL", "BUILD-FAIL": "FAIL", "RENDER-FAIL": "Needs re-verify", "ASSET-FAIL": "Needs re-verify",
|
|
44
|
+
"VISUAL-FAIL": "Needs re-verify", "MOCKUP-FAIL": "Needs re-verify", "PERF-FAIL": "Needs re-verify"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def die(msg):
|
|
48
|
+
print(f"tf-verify-verdict: {msg}", file=sys.stderr)
|
|
49
|
+
sys.exit(2)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load(path):
|
|
53
|
+
try:
|
|
54
|
+
with open(path, encoding="utf-8") as f:
|
|
55
|
+
return json.load(f)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
return None
|
|
58
|
+
except Exception as e:
|
|
59
|
+
die(f"{path} is not valid JSON: {e}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def perf_grader():
|
|
63
|
+
spec = importlib.util.spec_from_file_location("tf_perf_grade", os.path.join(HERE, "tf-perf-grade.py"))
|
|
64
|
+
mod = importlib.util.module_from_spec(spec)
|
|
65
|
+
spec.loader.exec_module(mod)
|
|
66
|
+
return mod.grade
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def norm_route(r):
|
|
70
|
+
r = (r or "").strip().lower().rstrip("/")
|
|
71
|
+
return r or "/"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def words(s, n=60):
|
|
75
|
+
w = s.split()
|
|
76
|
+
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)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main(argv):
|
|
80
|
+
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
|
81
|
+
print(__doc__)
|
|
82
|
+
return 0
|
|
83
|
+
app = argv[1]
|
|
84
|
+
apply = "--apply" in argv
|
|
85
|
+
d = argv[argv.index("--dir") + 1] if "--dir" in argv else os.path.join("tests", ".artifacts", "verify")
|
|
86
|
+
started = argv[argv.index("--started") + 1] if "--started" in argv else ""
|
|
87
|
+
today = datetime.date.today().isoformat()
|
|
88
|
+
|
|
89
|
+
lst = load(os.path.join(d, "list.json")) or die("list.json missing: run tf-verify-list.sh first")
|
|
90
|
+
boot = load(os.path.join(d, "boot.json")) or {"mode": "none", "reason": "tf-verify-boot.sh was not run", "reason_kind": "host", "head": "none"}
|
|
91
|
+
tests = (load(os.path.join(d, "tests.json")) or {}).get("reqs", {})
|
|
92
|
+
tests_ran = os.path.isfile(os.path.join(d, "tests.json"))
|
|
93
|
+
screens = load(os.path.join(d, "screens.json")) or {}
|
|
94
|
+
scr_by_name = {s["name"]: s for s in screens.get("screens", [])}
|
|
95
|
+
assets = load(os.path.join(d, "assets.json")) or {}
|
|
96
|
+
pages = {norm_route(p.get("path")): p for p in assets.get("pages", [])}
|
|
97
|
+
parity = load(os.path.join(d, "parity.json")) or {}
|
|
98
|
+
par_by_name = {s.get("screen"): s for s in parity.get("screens", [])}
|
|
99
|
+
par_by_route = {norm_route(s.get("route")): s for s in parity.get("screens", [])}
|
|
100
|
+
grade = perf_grader()
|
|
101
|
+
|
|
102
|
+
booted = boot.get("mode") not in (None, "none")
|
|
103
|
+
build_failed = boot.get("reason_kind") == "build-error"
|
|
104
|
+
boot_text = (f"{boot.get('head')} via {boot.get('rung')} at {boot.get('url')}" if booted
|
|
105
|
+
else f"not booted: {boot.get('reason')}")
|
|
106
|
+
checks_ran = set()
|
|
107
|
+
if booted or build_failed:
|
|
108
|
+
checks_ran.add("build")
|
|
109
|
+
|
|
110
|
+
rows_out, ledger_rows, table = [], {}, []
|
|
111
|
+
for r in lst["rows"]:
|
|
112
|
+
rid = r["id"]
|
|
113
|
+
prior = r.get("status_raw") or ""
|
|
114
|
+
notes = []
|
|
115
|
+
scr = scr_by_name.get(r.get("screen") or "")
|
|
116
|
+
driven = bool(scr) and scr.get("render") != "UNREACHABLE"
|
|
117
|
+
t = tests.get(rid)
|
|
118
|
+
# A row whose every clause was SKIPPED is NOT-TESTED: the acceptance gate did not run on
|
|
119
|
+
# it. It is neither a pass nor a defect, so it grades exactly like a row with no test at
|
|
120
|
+
# all — never Verified, no gate record (TF-022). `t` is kept for the note.
|
|
121
|
+
skipped_only = bool(t) and t.get("result") == "NOT-TESTED"
|
|
122
|
+
if skipped_only:
|
|
123
|
+
notes.append(f"acceptance not measured ({len(t.get('skipped') or [])} test(s) skipped, none ran)")
|
|
124
|
+
t = None
|
|
125
|
+
# every check on its own: (name, ran, failed, verdict, class, detail, evidence); the first failure wins
|
|
126
|
+
checks = []
|
|
127
|
+
|
|
128
|
+
# 1 build
|
|
129
|
+
checks.append(("build", booted or build_failed, build_failed, "BUILD-FAIL", "build-error", boot.get("reason", "build failed"), ""))
|
|
130
|
+
# 2 acceptance
|
|
131
|
+
if t:
|
|
132
|
+
checks.append(("acceptance", True, t["result"] != "PASS", "FAIL", "assert-fail", t.get("reason") or "test failed", t.get("screenshot") or ""))
|
|
133
|
+
# 3 render
|
|
134
|
+
if scr:
|
|
135
|
+
if scr.get("render") == "UNREACHABLE":
|
|
136
|
+
f = next((x for w in scr["widths"] for x in w.get("findings", [])), {})
|
|
137
|
+
checks.append(("render", True, True, "RENDER-FAIL", "other", f.get("detail", "screen unreachable"), ""))
|
|
138
|
+
else:
|
|
139
|
+
bad = [(w, f) for w in scr["widths"] for f in w.get("findings", []) if f["check"] == "render"]
|
|
140
|
+
w, f = bad[0] if bad else (None, None)
|
|
141
|
+
checks.append(("render", True, bool(bad), "RENDER-FAIL", f["class"] if f else None,
|
|
142
|
+
f"{f['detail']} on {r['screen']} @{w['width']}" if f else "", w.get("screenshot", "") if w else ""))
|
|
143
|
+
# 4 assets
|
|
144
|
+
pg = pages.get(norm_route(r.get("route"))) if r.get("route") else None
|
|
145
|
+
if pg and pg.get("graded", 0) > 0:
|
|
146
|
+
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)
|
|
147
|
+
checks.append(("assets", True, pg.get("failed", 0) > 0, "ASSET-FAIL", "missing-asset",
|
|
148
|
+
f"{pg.get('failed', 0)} declared asset(s) did not arrive on {r['route']}" + (f": {bad.get('url')} → {bad.get('status')}" if bad else ""), ""))
|
|
149
|
+
elif pg and pg.get("redirected"):
|
|
150
|
+
notes.append("assets not graded (auth wall)")
|
|
151
|
+
# 5 visual
|
|
152
|
+
if scr and driven:
|
|
153
|
+
bad = [(w, f) for w in scr["widths"] for f in w.get("findings", []) if f["check"] == "visual"]
|
|
154
|
+
w, f = bad[0] if bad else (None, None)
|
|
155
|
+
checks.append(("visual", True, bool(bad), "VISUAL-FAIL", f["class"] if f else None,
|
|
156
|
+
f"{f['detail']} on {r['screen']}" if f else "", w.get("screenshot", "") if w else ""))
|
|
157
|
+
# 6 mockup parity
|
|
158
|
+
ps = (par_by_name.get(r.get("screen")) or par_by_route.get(norm_route(r.get("route")))) if r.get("screen") else None
|
|
159
|
+
if ps:
|
|
160
|
+
v = ps.get("verdict")
|
|
161
|
+
if v in ("PASS", "FAIL"):
|
|
162
|
+
f = (ps.get("findings") or [{}])[0]
|
|
163
|
+
checks.append(("mockup-parity", True, v == "FAIL", "MOCKUP-FAIL", "mockup-drift",
|
|
164
|
+
f"{f.get('class', 'drift')} on {f.get('key', ps.get('screen'))} @{f.get('width', '')}: {str(f.get('detail', ''))[:80]}",
|
|
165
|
+
os.path.join(d, "parity.json")))
|
|
166
|
+
elif v == "UNGRADEABLE":
|
|
167
|
+
notes.append("mockup-parity UNGRADEABLE (add data-testid anchors to the mockup)")
|
|
168
|
+
elif v == "NO-MOCKUP":
|
|
169
|
+
notes.append("no mockup to compare")
|
|
170
|
+
# 7 speed
|
|
171
|
+
if r.get("perf_budget"):
|
|
172
|
+
pf = load(os.path.join(d, "perf", f"{rid}.json"))
|
|
173
|
+
if pf is None:
|
|
174
|
+
notes.append("perf not measured (no tf-perf run for this row)")
|
|
175
|
+
else:
|
|
176
|
+
g = grade(r["perf_budget"], pf)
|
|
177
|
+
if g["verdict"] in ("PERF-OK", "PERF-MARGINAL", "PERF-FAIL"):
|
|
178
|
+
checks.append(("perf", True, g["verdict"] == "PERF-FAIL", "PERF-FAIL", g.get("failure_class") or "slow-ttfb", g["reason"], ""))
|
|
179
|
+
if g["verdict"] == "PERF-MARGINAL":
|
|
180
|
+
notes.append(f"⚠ perf: {g['reason']}")
|
|
181
|
+
elif g["verdict"] == "PERF-UNMEASURED":
|
|
182
|
+
notes.append(f"perf not measured: {g['reason']}")
|
|
183
|
+
|
|
184
|
+
ran = [c[0] for c in checks if c[1]]
|
|
185
|
+
first = next((c for c in checks if c[1] and c[2]), None)
|
|
186
|
+
verdict, cls, detail, evidence = (first[3], first[4], first[5], first[6]) if first else (None, None, "", "")
|
|
187
|
+
|
|
188
|
+
# the honest non-verdicts
|
|
189
|
+
if verdict is None:
|
|
190
|
+
if r["class"] == "NFR" and not r.get("perf_budget") and not t:
|
|
191
|
+
verdict = "NOT-OBSERVABLE"
|
|
192
|
+
detail = "NFR row with no perf-budget and no unit test carrying its id"
|
|
193
|
+
elif not t and r.get("screen") and not driven:
|
|
194
|
+
verdict = "NOT-DRIVEN"
|
|
195
|
+
detail = (f"{boot.get('head', 'the app')} not driven: {boot.get('reason')}" if not booted
|
|
196
|
+
else f"screen {r['screen']} was not driven in this run")
|
|
197
|
+
elif not t:
|
|
198
|
+
verdict = "NOT-TESTED"
|
|
199
|
+
detail = (f"every test named {rid} was skipped" if skipped_only else f"no test named {rid} ran") \
|
|
200
|
+
+ (f"; screen {r['screen']} renders and looks right" if driven else "")
|
|
201
|
+
elif r.get("screen") and not driven and not booted:
|
|
202
|
+
verdict = "NOT-DRIVEN"
|
|
203
|
+
detail = f"test passed but {boot.get('head', 'the app')} was not driven: {boot.get('reason')}"
|
|
204
|
+
else:
|
|
205
|
+
verdict = "PASS"
|
|
206
|
+
parts = [f"test {t['tests'][0][:60]}" if t and t.get("tests") else "test passed"]
|
|
207
|
+
if driven:
|
|
208
|
+
parts.append(f"{r['screen']} renders and looks right @{'/'.join(str(w['width']) for w in scr['widths'])}")
|
|
209
|
+
if "mockup-parity" in ran:
|
|
210
|
+
parts.append("matches its mockup")
|
|
211
|
+
if "perf" in ran:
|
|
212
|
+
parts.append("within its speed budget")
|
|
213
|
+
detail = "; ".join(parts)
|
|
214
|
+
|
|
215
|
+
status = STATUS.get(verdict, prior)
|
|
216
|
+
pct = r.get("pct")
|
|
217
|
+
if verdict == "PASS":
|
|
218
|
+
pct = 100
|
|
219
|
+
elif verdict in ("RENDER-FAIL", "ASSET-FAIL", "VISUAL-FAIL", "MOCKUP-FAIL", "PERF-FAIL"):
|
|
220
|
+
pct = min(pct if pct is not None else 75, 75)
|
|
221
|
+
gate = FAILING.get(verdict)
|
|
222
|
+
marks = {"FAIL": "⚠ acceptance", "BUILD-FAIL": "⚠ build", "RENDER-FAIL": "⚠ render", "ASSET-FAIL": "⚠ assets",
|
|
223
|
+
"VISUAL-FAIL": "⚠ visual", "MOCKUP-FAIL": "⚠ mockup-parity", "PERF-FAIL": "⚠ perf",
|
|
224
|
+
"NOT-TESTED": "not verified", "NOT-OBSERVABLE": "not observable", "NOT-DRIVEN": "not verified", "PASS": "PASS"}
|
|
225
|
+
remark = f"{today} verify: {marks[verdict]} — {detail}"
|
|
226
|
+
if evidence:
|
|
227
|
+
remark += f" ({evidence})"
|
|
228
|
+
if notes:
|
|
229
|
+
remark += "; " + "; ".join(notes[:2])
|
|
230
|
+
remark = words(remark.replace("|", "/"), 60)
|
|
231
|
+
emit = verdict == "PASS" or gate is not None
|
|
232
|
+
row = {"id": rid, "class": r["class"], "verdict": verdict, "gate": gate, "gates_run": ran, "failure_class": cls if gate else None,
|
|
233
|
+
"prior_verdict": prior or None, "status": status, "pct": pct, "remark": remark, "screen": r.get("screen"),
|
|
234
|
+
"emit_gate": emit, "title": r.get("title", "")}
|
|
235
|
+
rows_out.append(row)
|
|
236
|
+
ledger_rows[rid] = verdict
|
|
237
|
+
table.append((rid, verdict, gate or "-", status, detail[:70]))
|
|
238
|
+
|
|
239
|
+
checks = []
|
|
240
|
+
for c in ("build", "acceptance", "render", "assets", "visual", "mockup-parity", "perf"):
|
|
241
|
+
if any(c in x["gates_run"] for x in rows_out):
|
|
242
|
+
checks.append(c)
|
|
243
|
+
ledger = {"date": today, "app": app, "scope": lst.get("scope", "all"), "booted": boot_text, "gates": checks,
|
|
244
|
+
"evidence": d, "run_id": started, "rows": ledger_rows}
|
|
245
|
+
os.makedirs("docs", exist_ok=True)
|
|
246
|
+
with open(os.path.join("docs", ".last-verify.json"), "w", encoding="utf-8") as f:
|
|
247
|
+
json.dump(ledger, f, indent=1)
|
|
248
|
+
with open(os.path.join(d, "verdicts.json"), "w", encoding="utf-8") as f:
|
|
249
|
+
json.dump({"app": app, "scope": lst.get("scope", "all"), "date": today, "started": started, "booted": boot_text,
|
|
250
|
+
"build_result": "fail" if build_failed else ("pass" if booted else "not-run"), "checks": checks,
|
|
251
|
+
"checklist": lst.get("checklist"), "rows": rows_out}, f, indent=1)
|
|
252
|
+
|
|
253
|
+
applied = 0
|
|
254
|
+
if apply:
|
|
255
|
+
cl = lst.get("checklist")
|
|
256
|
+
text = open(cl, encoding="utf-8").read()
|
|
257
|
+
by_id = {x["id"]: x for x in rows_out}
|
|
258
|
+
out = []
|
|
259
|
+
for line in text.splitlines(keepends=True):
|
|
260
|
+
m = re.match(r"^(\s*\|\s*`?)(REQ-[A-Z]+-\d+)(`?\s*\|)", line)
|
|
261
|
+
if m and m.group(2) in by_id:
|
|
262
|
+
c = line.rstrip("\r\n").strip().strip("|").split("|")
|
|
263
|
+
if len(c) >= 6:
|
|
264
|
+
x = by_id[m.group(2)]
|
|
265
|
+
c[2] = f" {x['status']} "
|
|
266
|
+
c[3] = f" {x['pct'] if x['pct'] is not None else ''}{'%' if x['pct'] is not None and '%' in c[3] else ''} "
|
|
267
|
+
c[4] = f" {x['remark']} "
|
|
268
|
+
nl = "\r\n" if line.endswith("\r\n") else "\n"
|
|
269
|
+
line = "|" + "|".join(c) + "|" + nl
|
|
270
|
+
applied += 1
|
|
271
|
+
out.append(line)
|
|
272
|
+
with open(cl, "w", encoding="utf-8") as f:
|
|
273
|
+
f.write("".join(out))
|
|
274
|
+
|
|
275
|
+
counts = {}
|
|
276
|
+
for x in rows_out:
|
|
277
|
+
counts[x["verdict"]] = counts.get(x["verdict"], 0) + 1
|
|
278
|
+
print(f"# tf-verify-verdict — {app} — scope {lst.get('scope')} — booted: {boot_text}")
|
|
279
|
+
print(f"Checks that ran: {', '.join(checks) or 'none'}. Rows: " + ", ".join(f"{v} {k}" for k, v in sorted(counts.items())))
|
|
280
|
+
print()
|
|
281
|
+
print("| ID | Verdict | First failing check | Status | Detail |")
|
|
282
|
+
print("|---|---|---|---|---|")
|
|
283
|
+
for t in table:
|
|
284
|
+
print("| " + " | ".join(str(x).replace("|", "/") for x in t) + " |")
|
|
285
|
+
print()
|
|
286
|
+
print(f"Ledger: docs/.last-verify.json ({len(ledger_rows)} rows). Verdicts: {os.path.join(d, 'verdicts.json')}."
|
|
287
|
+
+ (f" Checklist: {applied} row(s) rewritten in {lst.get('checklist')}." if apply else " Checklist untouched (no --apply)."))
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
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,
|