@techierathore/techieflow 1.0.4 → 1.1.0
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/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.opencode/plugin/techieflow.js +179 -7
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +1 -1
- package/.tfcore/tasks/day1-greenfield.md +1 -1
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/telemetry/SCHEMA.md +4 -0
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -2
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.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-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +80 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +136 -6
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +92 -35
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +79 -29
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +15 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +60 -2
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +72 -32
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +171 -32
- package/.tfcore/utils/tf-verify-screens.sh +8 -3
- package/.tfcore/utils/tf-verify-tests.sh +215 -14
- package/.tfcore/utils/tf-verify-verdict.py +48 -9
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +4 -0
- package/docs/TechieFlow-Installation.md +5 -3
- package/package.json +1 -1
|
@@ -117,7 +117,7 @@ else:
|
|
|
117
117
|
# TF-019: a row can sit at `Needs re-verify` forever, which pinned the mode and dropped
|
|
118
118
|
# every not-started row out of the list AND out of the counts, so a build pass could report
|
|
119
119
|
# a phase finished with rows at 0%. The arithmetic is the check: every row lands in exactly
|
|
120
|
-
# one of open / terminal / Blocked.
|
|
120
|
+
# one of open / terminal / Blocked / roadmap.
|
|
121
121
|
apps = app_names()
|
|
122
122
|
if not apps:
|
|
123
123
|
skip("tf-build-list", "no checklist in docs/ yet")
|
|
@@ -128,7 +128,7 @@ for app, phase, path in apps:
|
|
|
128
128
|
if rc not in (0, 2):
|
|
129
129
|
broken("tf-build-list", "%s: exited %d" % (label, rc), out.strip().splitlines()[0][:120] if out.strip() else "")
|
|
130
130
|
continue
|
|
131
|
-
m = re.search(r"Mode: (\w+) — (\d+) row\(s\) to build; (\d+) terminal, (\d+) Blocked, (\d+) total", out)
|
|
131
|
+
m = re.search(r"Mode: (\w+) — (\d+) row\(s\) to build; (\d+) terminal, (\d+) Blocked, (?:(\d+) roadmap, )?(\d+) total", out)
|
|
132
132
|
if not m:
|
|
133
133
|
if "has no REQ rows" in out:
|
|
134
134
|
skip("tf-build-list", "%s: checklist has no requirement rows yet" % label)
|
|
@@ -136,11 +136,11 @@ for app, phase, path in apps:
|
|
|
136
136
|
broken("tf-build-list", "%s: no counts line to check" % label,
|
|
137
137
|
"the output shape changed; the arithmetic can no longer be verified")
|
|
138
138
|
continue
|
|
139
|
-
mode, work, term, blocked, total = m.group(1), *(int(x) for x in m.groups()[1:])
|
|
140
|
-
if work + term + blocked != total:
|
|
139
|
+
mode, work, term, blocked, roadmap, total = m.group(1), *(int(x or 0) for x in m.groups()[1:])
|
|
140
|
+
if work + term + blocked + roadmap != total:
|
|
141
141
|
broken("tf-build-list",
|
|
142
|
-
"%s: %d rows are in no category (%d open + %d terminal + %d blocked ≠ %d total)"
|
|
143
|
-
% (label, total - work - term - blocked, work, term, blocked, total),
|
|
142
|
+
"%s: %d rows are in no category (%d open + %d terminal + %d blocked + %d roadmap ≠ %d total)"
|
|
143
|
+
% (label, total - work - term - blocked - roadmap, work, term, blocked, roadmap, total),
|
|
144
144
|
"a build pass trusting this list would leave those rows at 0%")
|
|
145
145
|
else:
|
|
146
146
|
ok("tf-build-list", "%s: %d rows, all accounted for (%s)" % (label, total, mode))
|
|
@@ -288,12 +288,45 @@ for name, cmd in (("tf-doc-check", ["bash", os.path.join(UTILS, "tf-doc-check.sh
|
|
|
288
288
|
first = next((l for l in out.splitlines() if l.strip()), "")
|
|
289
289
|
ok(name, "runs (exit %d)" % rc)
|
|
290
290
|
|
|
291
|
+
# ---- 8. the upstream feedback files read as they are ------------------------------------
|
|
292
|
+
# 2026-09-11: PROJECT-STATUS counted every ### heading as an entry and nothing as fixed, so
|
|
293
|
+
# TfLens read "62 open of 62" and its agent told the owner two problems fixed two days earlier
|
|
294
|
+
# were open (MISS-TechieFlow-20260911-04). The one reader is run here, on the real files, and
|
|
295
|
+
# whatever an upstream team has fixed and this project has not re-checked is named below.
|
|
296
|
+
waiting = []
|
|
297
|
+
sys.path.insert(0, UTILS)
|
|
298
|
+
try:
|
|
299
|
+
import tf_feedback
|
|
300
|
+
fb = tf_feedback.files(REPO)
|
|
301
|
+
if not fb:
|
|
302
|
+
skip("tf_feedback", "no upstream feedback files in this project")
|
|
303
|
+
else:
|
|
304
|
+
n = 0
|
|
305
|
+
for f in fb:
|
|
306
|
+
for e in tf_feedback.entries(f):
|
|
307
|
+
n += 1
|
|
308
|
+
if e["state"] == "fixed":
|
|
309
|
+
waiting.append((e["id"], e["since"], os.path.basename(f)))
|
|
310
|
+
ok("tf_feedback", "%d feedback file(s), %d entries read; %d fixed upstream, not yet re-checked"
|
|
311
|
+
% (len(fb), n, len(waiting)))
|
|
312
|
+
except Exception as e:
|
|
313
|
+
broken("tf_feedback", "crashed on this project's feedback files", str(e)[:120])
|
|
314
|
+
|
|
291
315
|
# ---- output -----------------------------------------------------------------------------
|
|
292
316
|
proj = os.path.basename(REPO.rstrip(os.sep))
|
|
293
317
|
print("# tf-selfcheck — %s — the framework's own scripts against this project's real files" % proj)
|
|
294
318
|
for l in lines:
|
|
295
319
|
print(l)
|
|
296
320
|
print()
|
|
321
|
+
if waiting:
|
|
322
|
+
# Not a defect and not a failure: work this project owes. An upstream fix counts only once it
|
|
323
|
+
# has been re-checked here; until then it must never be reported to the owner as open.
|
|
324
|
+
print("Fixed upstream, not yet re-checked here — never report these as open problems:")
|
|
325
|
+
for rid, since, f in waiting:
|
|
326
|
+
print(" %-12s fixed %-10s %s" % (rid, since or "", f))
|
|
327
|
+
print("Re-check each with its entry's \"Verify from here\" step, then close it:")
|
|
328
|
+
print(' bash .tfcore/utils/tf-feedback.sh <App> --close <ID> "<what you ran and what it showed>"')
|
|
329
|
+
print()
|
|
297
330
|
if not findings:
|
|
298
331
|
print("Every framework script works on this project. Nothing to file.")
|
|
299
332
|
raise SystemExit(0)
|
|
@@ -223,7 +223,17 @@ def run(app, phase, force, add_missing, in_order=False):
|
|
|
223
223
|
existing = read(cl_p) if os.path.isfile(cl_p) else ""
|
|
224
224
|
if existing and not force and not add_missing:
|
|
225
225
|
die(f"{cl_p} already exists; use --add-missing to append rows for new BRD items, or --force to rewrite", 1)
|
|
226
|
-
|
|
226
|
+
# A BRD item already has a row when any row names it: on its `*BRD:*` detail line, or anywhere on
|
|
227
|
+
# its status-table line. The table form used to be read only as a bracket holding exactly one id,
|
|
228
|
+
# so "(BRD-76, Phase 3)" and "(BRD-118, BRD-120, Phase 3)" were invisible and --add-missing
|
|
229
|
+
# appended 44 rows to TfLens phase 3 for 13 new items, 31 of them copies of Verified rows (TF-025).
|
|
230
|
+
mapped = {b for line in re.findall(r"(?m)^.*\*BRD:\*.*$", existing) for b in re.findall(r"BRD-\d+", line)}
|
|
231
|
+
mapped |= {b for row in re.findall(r"(?m)^\|\s*REQ-.*$", existing) for b in re.findall(r"BRD-\d+", row)}
|
|
232
|
+
# ...and on the requirement's own line in its detail entry, with the lines indented under it. A
|
|
233
|
+
# checklist migrated from an older plan names its items there, as "(BRD-1, BRD-2)" or
|
|
234
|
+
# "*(BRD-84, BRD-85)*", with no *BRD:* line: AppManager's run appended a row for all 88 items (TF-001).
|
|
235
|
+
for block in re.findall(r"(?m)^\s*[-*]\s*(?:<a id=[^>]*>\s*</a>\s*)?\*\*REQ-[A-Z]+-\d{3}\*\*.*(?:\n[ \t]+\S.*)*", existing):
|
|
236
|
+
mapped |= set(re.findall(r"BRD-\d+", block))
|
|
227
237
|
counters = other_phase_counters(app, cl_p, below=phase if in_order else None)
|
|
228
238
|
if add_missing:
|
|
229
239
|
for m in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", existing):
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# 4. the build verdict from tf-build.sh (skipped with --no-build)
|
|
13
13
|
# Then the agent decides the interruption signature and runs tf-status-facts.sh for the gate.
|
|
14
14
|
set -uo pipefail
|
|
15
|
+
source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh" # GNU-only commands, for a stock Mac
|
|
15
16
|
APP="${1:-}"; [[ -z "$APP" ]] && { sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 2; }
|
|
16
17
|
shift
|
|
17
18
|
BUILD=1; PHASE=""
|
|
@@ -22,7 +23,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
22
23
|
*) shift ;;
|
|
23
24
|
esac
|
|
24
25
|
done
|
|
25
|
-
[[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase
|
|
26
|
+
[[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase:[[:space:]]*[0-9]+' .tfcore/core-config.yaml 2>/dev/null | grep -oE '[0-9]+' || echo 1)"
|
|
26
27
|
[[ "$PHASE" -le 1 ]] && CL="docs/${APP}-Checklist.md" || CL="docs/${APP}-P${PHASE}-Checklist.md"
|
|
27
28
|
ST="PROJECT-STATUS.md"
|
|
28
29
|
|
|
@@ -31,19 +32,20 @@ echo
|
|
|
31
32
|
echo "## 1. What the stale status file claims (a hypothesis, not a fact)"
|
|
32
33
|
if [[ -f "$ST" ]]; then
|
|
33
34
|
grep -E '^(last_updated|current_phase|last_verified_build|last_verified_date):' "$ST" | sed 's/^/- /'
|
|
34
|
-
awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '
|
|
35
|
-
echo "- written: $(
|
|
35
|
+
awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '^[[:space:]]*/' | sed 's/^[[:space:]]*/- next: /' | head -2
|
|
36
|
+
echo "- written: $(tf_date_from -u "@$(tf_stat_mtime "$ST")" +%Y-%m-%dT%H:%M:%SZ)"
|
|
36
37
|
else
|
|
37
38
|
echo "- no PROJECT-STATUS.md; treat as never written"
|
|
38
39
|
fi
|
|
39
40
|
echo
|
|
40
41
|
echo "## 2. Files changed after the status file was written (src/ and tests/, newest first)"
|
|
41
42
|
if [[ -f "$ST" ]]; then
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# xargs runs ls only when find listed something (GNU xargs -r; BSD xargs has no -r)
|
|
44
|
+
NEWER=(); _found="$(find src tests -type f -newer "$ST" \
|
|
45
|
+
-not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' -not -path '*/.artifacts/*' 2>/dev/null)"
|
|
46
|
+
[[ -n "$_found" ]] && tf_read_lines NEWER < <(printf '%s\n' "$_found" | xargs ls -t 2>/dev/null)
|
|
45
47
|
echo "- ${#NEWER[@]} file(s)"
|
|
46
|
-
printf -- '- %s\n' "${NEWER[@]:0:30}"
|
|
48
|
+
printf -- '- %s\n' ${NEWER[@]+"${NEWER[@]:0:30}"}
|
|
47
49
|
[[ ${#NEWER[@]} -gt 30 ]] && echo "- (${#NEWER[@]} in total; the rest with: find src tests -type f -newer PROJECT-STATUS.md)"
|
|
48
50
|
else
|
|
49
51
|
echo "- no status file to compare against"
|
|
@@ -51,9 +53,9 @@ fi
|
|
|
51
53
|
echo
|
|
52
54
|
echo "## 3. Checklist rows that look interrupted ($CL)"
|
|
53
55
|
if [[ -f "$CL" ]]; then
|
|
54
|
-
grep -E '
|
|
56
|
+
grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
|
|
55
57
|
| grep -iE ' — (Implemented|In Progress|Needs re-verify|PARTIAL|FAIL)' | sed 's/^/- /' | head -40
|
|
56
|
-
n=$(grep -E '
|
|
58
|
+
n=$(grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{print $4}' | grep -ciE 'Implemented|In Progress|Needs re-verify|PARTIAL|FAIL' || true)
|
|
57
59
|
[[ "$n" == "0" ]] && echo "- none: every row is terminal, Blocked or not started"
|
|
58
60
|
else
|
|
59
61
|
echo "- $CL does not exist"
|
|
@@ -27,6 +27,7 @@ import re
|
|
|
27
27
|
import sys
|
|
28
28
|
|
|
29
29
|
TERMINAL = {"verified", "done (pre-existing)", "n/a"}
|
|
30
|
+
NA = {"n/a"} # terminal, but never counted as verified (TF-018)
|
|
30
31
|
PASS_THROUGH = {"blocked"} # a library gap; counts as closed for the ladder
|
|
31
32
|
BUILT = {"implemented", "needs re-verify"}
|
|
32
33
|
OWNER = {"owner-uat"} # only the owner can close it, from the UsageGuide test plan
|
|
@@ -61,6 +62,9 @@ def read(path):
|
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
def checklist_rows(text):
|
|
65
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
66
|
+
import tf_roadmap
|
|
67
|
+
marked = tf_roadmap.ids(text)
|
|
64
68
|
rows = []
|
|
65
69
|
for line in text.splitlines():
|
|
66
70
|
if not re.match(r"^\s*\|\s*`?REQ-", line):
|
|
@@ -69,10 +73,34 @@ def checklist_rows(text):
|
|
|
69
73
|
if len(cells) < 3:
|
|
70
74
|
continue
|
|
71
75
|
rid = cells[0].strip("`* ")
|
|
72
|
-
|
|
76
|
+
status = norm_status(cells[2])
|
|
77
|
+
rows.append({"id": rid, "name": cells[1].strip("`* "), "status": status,
|
|
78
|
+
"remarks": cells[4] if len(cells) > 4 else "",
|
|
79
|
+
"roadmap": rid.upper() in marked and status.lower() not in TERMINAL})
|
|
73
80
|
return rows
|
|
74
81
|
|
|
75
82
|
|
|
83
|
+
def figures(rows):
|
|
84
|
+
"""(verified, counted, not applicable). An N/A row is terminal but was never verified, so it is
|
|
85
|
+
left out of both numbers and named: AppManager read "89 of 89 verified" with REQ-NFR-008 at N/A
|
|
86
|
+
(TF-018). tests/regression/run.sh am_018."""
|
|
87
|
+
na = sum(1 for r in rows if r["status"].lower() in NA)
|
|
88
|
+
return sum(1 for r in rows if r["status"].lower() in TERMINAL - NA), len(rows) - na, na
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# The defect marks live in tf_defect.py. A bare ⚠ is not enough: sending "not verifiable here" to a
|
|
92
|
+
# build is the TfLens loop again.
|
|
93
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
94
|
+
from tf_defect import DEFECT # noqa: E402
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def has_defect(r):
|
|
98
|
+
"""A Needs re-verify row whose Remarks carry a framework defect mark holds a defect no fix has
|
|
99
|
+
cleared, and a verify of the acceptance line cannot see it. So it is fix work, as build-phase step 7
|
|
100
|
+
and tf-build-list say, never a verify (AppManager TF-019). tests/regression/run.sh am_019."""
|
|
101
|
+
return r["status"].lower() == "needs re-verify" and bool(DEFECT.search(r.get("remarks", "")))
|
|
102
|
+
|
|
103
|
+
|
|
76
104
|
def ladder_rank(status):
|
|
77
105
|
return LADDER.index(status) if status in LADDER else 0
|
|
78
106
|
|
|
@@ -147,19 +175,13 @@ def handoff_ran(log_rows):
|
|
|
147
175
|
|
|
148
176
|
|
|
149
177
|
def feedback_lines(root, app):
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
closed = re.search(r"(?im)^\s*[-*]\s*\*\*(status|resolved|fixed)[^:]*:\*\*\s*(fixed|resolved|closed|done)", e)
|
|
158
|
-
if not closed:
|
|
159
|
-
open_n += 1
|
|
160
|
-
rel = os.path.relpath(path, root).replace(os.sep, "/")
|
|
161
|
-
out.append(f"- {lib}: {open_n} open of {len(entries)} — {rel}")
|
|
162
|
-
return out or ["- None"]
|
|
178
|
+
# One reader for every script (tf_feedback.py). This function used to count every `###`
|
|
179
|
+
# heading as an entry and accept only a closing line no file used, so TfLens's status read
|
|
180
|
+
# "TechieFlow: 62 open of 62" for 27 entries, 19 of them fixed or closed, and the agent told
|
|
181
|
+
# the owner two fixed problems were open (MISS-TechieFlow-20260911-04).
|
|
182
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
183
|
+
import tf_feedback
|
|
184
|
+
return [tf_feedback.summary_line(p, app) for p in tf_feedback.files(root, app)] or ["- None"]
|
|
163
185
|
|
|
164
186
|
|
|
165
187
|
def config_phase(root):
|
|
@@ -191,6 +213,15 @@ def phase_row(root, app, phase):
|
|
|
191
213
|
return name, total
|
|
192
214
|
|
|
193
215
|
|
|
216
|
+
def usage_guide(app):
|
|
217
|
+
"""UsageGuide or Usage-Guide, whichever exists: tf-verify-list.py's reader (AppManager TF-022)."""
|
|
218
|
+
import importlib.util
|
|
219
|
+
spec = importlib.util.spec_from_file_location("tf_verify_list", os.path.join(os.path.dirname(os.path.abspath(__file__)), "tf-verify-list.py"))
|
|
220
|
+
vl = importlib.util.module_from_spec(spec)
|
|
221
|
+
spec.loader.exec_module(vl)
|
|
222
|
+
return vl.usage_guide(app)
|
|
223
|
+
|
|
224
|
+
|
|
194
225
|
def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
195
226
|
"""Returns (phase, qualifier, cc_line, oc_line, reason)."""
|
|
196
227
|
verdicts = verdicts or {}
|
|
@@ -207,17 +238,23 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
|
207
238
|
f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
|
|
208
239
|
"no BRD and no checklist exist; day-1 stage 1 has not run")
|
|
209
240
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
241
|
+
# A roadmap row is not this phase's work, so it never decides the next command (TF-017).
|
|
242
|
+
now = [r for r in rows if not r.get("roadmap")]
|
|
243
|
+
unbuilt = [r for r in now if r["status"].lower() not in TERMINAL | PASS_THROUGH | BUILT | OWNER]
|
|
244
|
+
defects = [r for r in now if has_defect(r)]
|
|
245
|
+
built = [r for r in now if r["status"].lower() in BUILT and r not in defects]
|
|
246
|
+
owner = [r for r in now if r["status"].lower() in OWNER]
|
|
247
|
+
verified, counted, na = figures(rows)
|
|
248
|
+
q = f"{verified} of {counted} verified" + (f", {na} not applicable" if na else "")
|
|
249
|
+
|
|
250
|
+
if unbuilt or defects:
|
|
251
|
+
qual = ", ".join(x for x in (f"{len(unbuilt)} not built" if unbuilt else "",
|
|
252
|
+
f"{len(defects)} to fix" if defects else "") if x)
|
|
253
|
+
why = "; ".join(x for x in (f"{len(unbuilt)} rows are not built yet: {ids(unbuilt)}" if unbuilt else "",
|
|
254
|
+
f"{len(defects)} rows carry a defect (⚠ in Remarks) that a fix must clear before "
|
|
255
|
+
f"a verify: {ids(defects)}" if defects else "") if x)
|
|
256
|
+
return ("Build", f"{qual}, {q}",
|
|
257
|
+
f"{CC}flow-master *build-phase {app}", f"{OC}flow-master *build-phase {app}", why)
|
|
221
258
|
if built:
|
|
222
259
|
# A row the last verify could not MEASURE is not a row waiting for another verify.
|
|
223
260
|
# Its tests declined — the state they need does not exist in this environment — so
|
|
@@ -243,7 +280,7 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
|
243
280
|
reason)
|
|
244
281
|
if owner:
|
|
245
282
|
if handoff_ran(log_rows):
|
|
246
|
-
line = f"(owner-run)
|
|
283
|
+
line = f"(owner-run) {usage_guide(app)} — {ids(owner)}"
|
|
247
284
|
return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
|
|
248
285
|
f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
|
|
249
286
|
return ("Handoff", f"{len(owner)} owner-run, {q}",
|
|
@@ -252,10 +289,10 @@ def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
|
252
289
|
if handoff_ran(log_rows):
|
|
253
290
|
line = "(owner) set current_phase to Released after UAT — no agent command"
|
|
254
291
|
return ("UAT", f"handoff done, {q}", line, line,
|
|
255
|
-
"every row is terminal and handoff has run; waiting on the owner")
|
|
292
|
+
"every row in this phase's scope is terminal and handoff has run; waiting on the owner")
|
|
256
293
|
return ("Handoff", q,
|
|
257
294
|
f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
|
|
258
|
-
"every row is terminal and handoff has not run yet")
|
|
295
|
+
"every row in this phase's scope is terminal and handoff has not run yet")
|
|
259
296
|
|
|
260
297
|
|
|
261
298
|
def main(argv):
|
|
@@ -284,14 +321,17 @@ def main(argv):
|
|
|
284
321
|
log_rows = existing_log_rows(st_text)
|
|
285
322
|
vdate, vresult = last_verify(root, app)
|
|
286
323
|
phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n, ledger_verdicts(root))
|
|
324
|
+
roadmap = [r["id"] for r in rows if r["roadmap"]]
|
|
325
|
+
if roadmap:
|
|
326
|
+
reason += (f"; {len(roadmap)} roadmap row(s) are not in this phase's scope and are left for the owner "
|
|
327
|
+
f"(*amend-docs): {', '.join(roadmap)}")
|
|
287
328
|
pname, ptotal = phase_row(root, app, phase_n)
|
|
288
329
|
if ptotal or phase_n > 1:
|
|
289
330
|
tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
|
|
290
331
|
phase = f"{tag} · {phase}"
|
|
291
332
|
reason = f"{reason}; working {cl_rel}"
|
|
292
333
|
|
|
293
|
-
|
|
294
|
-
closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
|
|
334
|
+
verified, counted, na = figures(rows)
|
|
295
335
|
counts = {}
|
|
296
336
|
for r in rows:
|
|
297
337
|
counts[r["status"]] = counts.get(r["status"], 0) + 1
|
|
@@ -322,7 +362,7 @@ def main(argv):
|
|
|
322
362
|
print()
|
|
323
363
|
if open_rows:
|
|
324
364
|
for r in open_rows[:10]:
|
|
325
|
-
print(f"- [ ] {r['id']} — {r['name']} ({r['status']})")
|
|
365
|
+
print(f"- [ ] {r['id']} — {r['name']} ({r['status']}{', roadmap — not in this phase' if r['roadmap'] else ''})")
|
|
326
366
|
if len(open_rows) > 10:
|
|
327
367
|
print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
|
|
328
368
|
else:
|
|
@@ -332,7 +372,7 @@ def main(argv):
|
|
|
332
372
|
print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
|
|
333
373
|
print()
|
|
334
374
|
print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
|
|
335
|
-
new_row = f"| {today} | {label} | {
|
|
375
|
+
new_row = f"| {today} | {label} | {verified}/{counted} Verified{f', {na} N/A' if na else ''} | {cl_rel}#requirements-status |"
|
|
336
376
|
for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
|
|
337
377
|
print(r)
|
|
338
378
|
print(new_row)
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
|
|
9
9
|
bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--verify-ran] [--cmd triage-issues|fix-issues]
|
|
10
10
|
|
|
11
|
+
demote, new and note take --phase N for another phase's checklist; without it demote and note find the
|
|
12
|
+
row in whichever docs/<App>-Checklist.md or docs/<App>-P<N>-Checklist.md holds it.
|
|
13
|
+
|
|
11
14
|
demote the row goes to Needs re-verify (% capped at 75) with a dated Remark holding the symptom in the
|
|
12
15
|
reporter's words, the evidence path and the kind.
|
|
13
16
|
new a defect no row covers: the next free id in the prefix, a Not Started row, a detail entry with the
|
|
@@ -20,7 +23,8 @@ close what triage owes the telemetry, from the actions above (tests/.artifacts
|
|
|
20
23
|
spec for a new row, which the spec never had) (duplicates collapsed through
|
|
21
24
|
tf-emit.sh --open-miss; the origin run looked up with --origin-of), then the run record. It also
|
|
22
25
|
lists every file under src/, source/ or tests/ changed since the start: a triage that edited code
|
|
23
|
-
is reported and logged as an instruction-ignored miss (FR-28).
|
|
26
|
+
is reported and logged as an instruction-ignored miss (FR-28). Only actions logged since the start
|
|
27
|
+
count, and a close empties the list (TrBlazeUI TF-001).
|
|
24
28
|
Exit 0 · 2 could not run.
|
|
25
29
|
"""
|
|
26
30
|
import datetime
|
|
@@ -58,12 +62,36 @@ def cfg_phase():
|
|
|
58
62
|
return 1
|
|
59
63
|
|
|
60
64
|
|
|
61
|
-
def
|
|
62
|
-
ph
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
def phase_checklist(app, ph):
|
|
66
|
+
return os.path.join("docs", f"{app}-Checklist.md" if ph <= 1 else f"{app}-P{ph}-Checklist.md")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def checklist(app, argv, ce, rid=None):
|
|
70
|
+
"""--phase N names the checklist, as tf-verify-list.py takes it. Without it: the appPhase
|
|
71
|
+
checklist, or, for a row that is not there, the one earlier-phase checklist that holds it —
|
|
72
|
+
a Phase 3 verify finds Phase 1 rows failing, and demote could not reach them (TF-051)."""
|
|
73
|
+
if "--phase" in argv:
|
|
74
|
+
v = opt(argv, "--phase", "")
|
|
75
|
+
if not v.isdigit():
|
|
76
|
+
die("--phase needs a number")
|
|
77
|
+
p = phase_checklist(app, int(v))
|
|
78
|
+
if not os.path.isfile(p):
|
|
79
|
+
die(f"{p} does not exist")
|
|
80
|
+
return p
|
|
81
|
+
p = phase_checklist(app, cfg_phase())
|
|
82
|
+
if rid is None or (os.path.isfile(p) and rid in ce.rows(ce.read(p))):
|
|
83
|
+
if not os.path.isfile(p):
|
|
84
|
+
die(f"{p} does not exist")
|
|
85
|
+
return p
|
|
86
|
+
import glob
|
|
87
|
+
every = sorted(set(glob.glob(os.path.join("docs", f"{app}-Checklist.md")) + glob.glob(os.path.join("docs", f"{app}-P*-Checklist.md"))))
|
|
88
|
+
hits = [c for c in every if rid in ce.rows(ce.read(c))]
|
|
89
|
+
if len(hits) == 1:
|
|
90
|
+
print(f"note: {rid} is a row of {hits[0]}, not of {p}; written there")
|
|
91
|
+
return hits[0]
|
|
92
|
+
if len(hits) > 1:
|
|
93
|
+
die(f"{rid} is a row of {', '.join(hits)}; name one with --phase N")
|
|
94
|
+
die(f"{rid} is not a row of {', '.join(every) or p}")
|
|
67
95
|
|
|
68
96
|
|
|
69
97
|
def log_load():
|
|
@@ -112,7 +140,6 @@ def main(argv):
|
|
|
112
140
|
return 0 if len(argv) > 1 else 2
|
|
113
141
|
app, verb = argv[1], argv[2]
|
|
114
142
|
ce = mod()
|
|
115
|
-
cl = checklist(app)
|
|
116
143
|
log = log_load()
|
|
117
144
|
log.setdefault("app", app)
|
|
118
145
|
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -121,6 +148,7 @@ def main(argv):
|
|
|
121
148
|
if len(argv) < 5:
|
|
122
149
|
die("demote needs <REQ> \"<symptom>\"")
|
|
123
150
|
rid, symptom = argv[3].upper(), argv[4]
|
|
151
|
+
cl = checklist(app, argv, ce, rid)
|
|
124
152
|
kind = opt(argv, "--kind"); ev = opt(argv, "--evidence"); src = opt(argv, "--source", "owner")
|
|
125
153
|
prior = ce.demote(cl, rid, symptom, kind=kind, evidence=ev, source=src, prefix="⚠ prod bug" if src == "production" else "⚠ UAT bug")
|
|
126
154
|
if prior is None:
|
|
@@ -136,6 +164,7 @@ def main(argv):
|
|
|
136
164
|
if len(argv) < 5:
|
|
137
165
|
die("new needs \"<title>\" \"<acceptance>\"")
|
|
138
166
|
title, acc = argv[3], argv[4]
|
|
167
|
+
cl = checklist(app, argv, ce)
|
|
139
168
|
prefix = opt(argv, "--prefix", "FN").upper(); section = opt(argv, "--section"); ev = opt(argv, "--evidence")
|
|
140
169
|
src = opt(argv, "--source", "owner"); mockup = opt(argv, "--mockup")
|
|
141
170
|
if prefix == "UI" and not mockup:
|
|
@@ -152,6 +181,7 @@ def main(argv):
|
|
|
152
181
|
if len(argv) < 5:
|
|
153
182
|
die("note needs <REQ> \"<text>\"")
|
|
154
183
|
rid = argv[3].upper()
|
|
184
|
+
cl = checklist(app, argv, ce, rid)
|
|
155
185
|
if ce.note(cl, rid, argv[4]) is None:
|
|
156
186
|
die(f"{rid} is not a row of {cl}")
|
|
157
187
|
log["actions"].append({"verb": "note", "req_id": rid, "symptom": argv[4], "ts": now})
|
|
@@ -162,9 +192,11 @@ def main(argv):
|
|
|
162
192
|
if verb == "close":
|
|
163
193
|
started = opt(argv, "--started") or subprocess.run(["bash", os.path.join(HERE, "tf-phase.sh"), "show"], capture_output=True, text=True).stdout
|
|
164
194
|
if started.startswith("{"):
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
195
|
+
# the marker's own started, never its "outer" block's (TF-052 follow-up)
|
|
196
|
+
try:
|
|
197
|
+
started = (json.loads(started) or {}).get("started") or ""
|
|
198
|
+
except ValueError:
|
|
199
|
+
started = ""
|
|
168
200
|
started = started.strip() if started and not started.startswith("none") else ""
|
|
169
201
|
cmd = opt(argv, "--cmd", "triage-issues")
|
|
170
202
|
# code untouched?
|
|
@@ -173,16 +205,25 @@ def main(argv):
|
|
|
173
205
|
t0 = datetime.datetime.strptime(started, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc).timestamp()
|
|
174
206
|
for top in ("src", "source", "tests"):
|
|
175
207
|
for d, dirs, files in os.walk(top):
|
|
176
|
-
|
|
208
|
+
# a running app's own log files are not code (AppManager TF-026)
|
|
209
|
+
dirs[:] = [x for x in dirs if x not in ("bin", "obj", "node_modules", ".artifacts", "TestResults")
|
|
210
|
+
and x.lower() not in ("logs", "log")]
|
|
177
211
|
for f in files:
|
|
212
|
+
if f.lower().endswith(".log"):
|
|
213
|
+
continue
|
|
178
214
|
p = os.path.join(d, f)
|
|
179
215
|
try:
|
|
180
216
|
if os.path.getmtime(p) > t0:
|
|
181
217
|
changed.append(p.replace("\\", "/"))
|
|
182
218
|
except OSError:
|
|
183
219
|
pass
|
|
184
|
-
|
|
220
|
+
# only this triage's actions: an earlier run's leftovers were logged again under today's run (TrBlazeUI TF-001)
|
|
221
|
+
stale = [a for a in log.get("actions", []) if started and (a.get("ts") or "") < started]
|
|
222
|
+
if stale:
|
|
223
|
+
print(f" skipped {len(stale)} action(s) from before {started} — an earlier triage's, already closed or abandoned")
|
|
224
|
+
acts = [a for a in log.get("actions", []) if a["verb"] in ("demote", "new") and a not in stale]
|
|
185
225
|
gates = misses = 0
|
|
226
|
+
failed = []
|
|
186
227
|
for a in acts:
|
|
187
228
|
rid = a["req_id"]; cls_req = rid.split("-")[1]
|
|
188
229
|
att = q("--next-attempt", rid) or "1"
|
|
@@ -192,7 +233,9 @@ def main(argv):
|
|
|
192
233
|
"gate": "escaped", "gates_run": [], "failure_class": fc, "prior_verdict": a.get("prior")}
|
|
193
234
|
if emit("gates", rec):
|
|
194
235
|
gates += 1
|
|
195
|
-
|
|
236
|
+
else:
|
|
237
|
+
failed.append(a)
|
|
238
|
+
mc =("unspecified-gap" if a["verb"] == "new" else "regression" if (a.get("prior") or "").lower().startswith("verified") else "wrong-behaviour")
|
|
196
239
|
opened = q("--open-miss", rid)
|
|
197
240
|
if opened and opened.split()[-1] == mc:
|
|
198
241
|
print(f" {rid}: already open as {opened.split()[0]} ({mc}); no second miss")
|
|
@@ -210,9 +253,11 @@ def main(argv):
|
|
|
210
253
|
if emit("misses", m):
|
|
211
254
|
misses += 1
|
|
212
255
|
a["miss_id"] = mid
|
|
213
|
-
|
|
256
|
+
elif a not in failed:
|
|
257
|
+
failed.append(a)
|
|
258
|
+
if changed and cmd == "triage-issues" and log.get("code_miss_run") != started:
|
|
214
259
|
mid = q("--next-miss-id")
|
|
215
|
-
log["code_miss_id"] = mid
|
|
260
|
+
log["code_miss_id"] = mid; log["code_miss_run"] = started
|
|
216
261
|
emit("misses", {"kind": "miss", "miss_id": mid, "req_id": None, "req_class": None, "miss_class": "other", "artifact": "src",
|
|
217
262
|
"severity": "major", "why_missed": "instruction-ignored", "origin_phase": "triage-issues", "origin_agent": "flow-master",
|
|
218
263
|
"found_by": "gate", "found_phase": "triage-issues", "found_gate": None, "found_run_id": started, "failure_class": "other",
|
|
@@ -232,6 +277,7 @@ def main(argv):
|
|
|
232
277
|
else:
|
|
233
278
|
ok = None
|
|
234
279
|
log["closed"] = now
|
|
280
|
+
log["actions"] = failed # a closed action is never closed again; one whose record was refused waits for the next close
|
|
235
281
|
log_save(log)
|
|
236
282
|
print(f"triage: {len(acts)} row(s) logged — {gates} gate record(s) (escaped), {misses} miss(es)"
|
|
237
283
|
+ (f", run record {'written' if ok else 'not written'}" if ok is not None else "") + f"; code untouched: {'yes' if not changed else 'NO'}")
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
|
|
7
7
|
# bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--cmd triage-issues|fix-issues]
|
|
8
8
|
#
|
|
9
|
+
# demote, new and note take --phase N; without it a row of an earlier phase's checklist is found (TF-051).
|
|
10
|
+
#
|
|
9
11
|
# demote: Needs re-verify with a dated Remark in the reporter's words. new: a Not Started row with its
|
|
10
12
|
# acceptance line and BRD-pending. note: a remark, no status change. close: the escaped gate records,
|
|
11
13
|
# one miss per row (found by the owner, the symptom as `what`, duplicates collapsed), the run record,
|