@techierathore/techieflow 1.0.3 → 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/agents/analyst.md +5 -8
- package/.claude/commands/TechieFlow/agents/architect.md +1 -10
- package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
- package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
- 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 +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- 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/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +181 -9
- package/.tfcore/agents/analyst.md +5 -8
- package/.tfcore/agents/architect.md +1 -10
- package/.tfcore/agents/flow-master.md +1 -0
- package/.tfcore/agents/verifier.md +1 -0
- 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-metrics.sh +13 -1
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +27 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +6 -5
- package/.tfcore/tasks/build-phase.md +1 -1
- 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 +3 -3
- package/.tfcore/tasks/day1-greenfield.md +3 -3
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +7 -3
- package/.tfcore/tasks/handoff-phase.md +20 -18
- package/.tfcore/tasks/metrics-report.md +2 -0
- 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/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +61 -1
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +435 -21
- 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-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- 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-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.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-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 +99 -11
- package/.tfcore/utils/tf-build-list.sh +5 -3
- 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 +242 -10
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +413 -19
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +113 -31
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +105 -18
- 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 +24 -21
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +33 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +80 -2
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +356 -0
- package/.tfcore/utils/tf-split-brd.py +17 -2
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +133 -44
- 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 +254 -43
- package/.tfcore/utils/tf-verify-screens.sh +18 -4
- package/.tfcore/utils/tf-verify-tests.sh +272 -28
- package/.tfcore/utils/tf-verify-verdict.py +59 -11
- 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 +19 -5
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +24 -0
- package/package.json +1 -1
- package/scripts/install.mjs +4 -0
- 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
|
@@ -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,22 +73,76 @@ 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
|
|
|
79
107
|
|
|
108
|
+
def ledger(root):
|
|
109
|
+
"""docs/.last-verify.json as a dict, or {} when there is none.
|
|
110
|
+
|
|
111
|
+
The verifier writes it with `json.dump(..., indent=1)`, so it is ONE pretty-printed
|
|
112
|
+
object spanning many lines. This reader used to take `splitlines()[-1]` — written for
|
|
113
|
+
an append-per-line ledger that no version of the verifier has ever produced — so it
|
|
114
|
+
parsed the closing brace, threw, and every project reported `last_verified_build:
|
|
115
|
+
not-run` however many verify runs it had behind it (TfLens: 69 of 73 rows Verified and
|
|
116
|
+
a full ledger, reported as never verified). Whole file first, last line second for a
|
|
117
|
+
legacy JSONL ledger. tests/regression/run.sh tf_ledger."""
|
|
118
|
+
path = os.path.join(root, "docs", ".last-verify.json")
|
|
119
|
+
if not os.path.isfile(path):
|
|
120
|
+
return {}
|
|
121
|
+
text = read(path).strip()
|
|
122
|
+
for candidate in (text, text.splitlines()[-1] if text else ""):
|
|
123
|
+
try:
|
|
124
|
+
data = json.loads(candidate)
|
|
125
|
+
if isinstance(data, dict):
|
|
126
|
+
return data
|
|
127
|
+
except Exception:
|
|
128
|
+
continue
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def ledger_verdicts(root):
|
|
133
|
+
"""{REQ id: verdict} from the last verify run, or {} when none has run.
|
|
134
|
+
|
|
135
|
+
The verifier already writes every row's verdict into the ledger. Reading it here is
|
|
136
|
+
what lets the status line tell "built, waiting for a verify" apart from "verified as
|
|
137
|
+
far as it can be, and the rest needs a decision only the owner can make"."""
|
|
138
|
+
rows = ledger(root).get("rows")
|
|
139
|
+
return rows if isinstance(rows, dict) else {}
|
|
140
|
+
|
|
141
|
+
|
|
80
142
|
def last_verify(root, app):
|
|
81
143
|
"""(date, build_result) from docs/.last-verify.json and gates.jsonl; ('', 'not-run') if none."""
|
|
82
|
-
|
|
83
|
-
if not
|
|
84
|
-
return "", "not-run"
|
|
85
|
-
try:
|
|
86
|
-
data = json.loads(read(ledger).strip().splitlines()[-1])
|
|
87
|
-
except Exception:
|
|
144
|
+
data = ledger(root)
|
|
145
|
+
if not data:
|
|
88
146
|
return "", "not-run"
|
|
89
147
|
date = str(data.get("date", ""))[:10]
|
|
90
148
|
result = "PASS"
|
|
@@ -117,19 +175,13 @@ def handoff_ran(log_rows):
|
|
|
117
175
|
|
|
118
176
|
|
|
119
177
|
def feedback_lines(root, app):
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
closed = re.search(r"(?im)^\s*[-*]\s*\*\*(status|resolved|fixed)[^:]*:\*\*\s*(fixed|resolved|closed|done)", e)
|
|
128
|
-
if not closed:
|
|
129
|
-
open_n += 1
|
|
130
|
-
rel = os.path.relpath(path, root).replace(os.sep, "/")
|
|
131
|
-
out.append(f"- {lib}: {open_n} open of {len(entries)} — {rel}")
|
|
132
|
-
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"]
|
|
133
185
|
|
|
134
186
|
|
|
135
187
|
def config_phase(root):
|
|
@@ -161,8 +213,19 @@ def phase_row(root, app, phase):
|
|
|
161
213
|
return name, total
|
|
162
214
|
|
|
163
215
|
|
|
164
|
-
def
|
|
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
|
+
|
|
225
|
+
def next_command(app, rows, log_rows, phase=1, verdicts=None):
|
|
165
226
|
"""Returns (phase, qualifier, cc_line, oc_line, reason)."""
|
|
227
|
+
verdicts = verdicts or {}
|
|
228
|
+
|
|
166
229
|
def ids(lst):
|
|
167
230
|
return ", ".join(r["id"] for r in lst[:6]) + (" …" if len(lst) > 6 else "")
|
|
168
231
|
|
|
@@ -175,26 +238,49 @@ def next_command(app, rows, log_rows, phase=1):
|
|
|
175
238
|
f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
|
|
176
239
|
"no BRD and no checklist exist; day-1 stage 1 has not run")
|
|
177
240
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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)
|
|
189
258
|
if built:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
259
|
+
# A row the last verify could not MEASURE is not a row waiting for another verify.
|
|
260
|
+
# Its tests declined — the state they need does not exist in this environment — so
|
|
261
|
+
# running the verifier again produces the same NOT-TESTED and the project sits in a
|
|
262
|
+
# loop that looks like progress. Say what it actually needs: data, a changed
|
|
263
|
+
# acceptance line, or the row marked N/A. Only the owner can choose. FR-69's sibling
|
|
264
|
+
# case; the verdicts come from the ledger the verifier already writes.
|
|
265
|
+
unmeasured = [r for r in built if verdicts.get(r["id"]) == "NOT-TESTED"]
|
|
266
|
+
waiting = [r for r in built if r not in unmeasured]
|
|
267
|
+
if unmeasured and not waiting:
|
|
268
|
+
line = (f"(owner) {len(unmeasured)} row(s) cannot be measured here — create the data, "
|
|
269
|
+
f"change the acceptance line, or mark the row N/A: {ids(unmeasured)}")
|
|
270
|
+
return ("Verify", f"{len(unmeasured)} not measurable, {q}", line, line,
|
|
271
|
+
f"every test carrying these ids was skipped, so another verify run changes nothing: {ids(unmeasured)}")
|
|
272
|
+
ui = [r for r in waiting if r["id"].startswith("REQ-UI")]
|
|
273
|
+
scope = "ui" if len(ui) == len(waiting) else ("functional" if not ui else "all")
|
|
274
|
+
reason = f"{len(waiting)} rows are built and not verified: {ids(waiting)}"
|
|
275
|
+
if unmeasured:
|
|
276
|
+
reason += (f"; {len(unmeasured)} more could not be measured (every test skipped) "
|
|
277
|
+
f"and need the owner: {ids(unmeasured)}")
|
|
278
|
+
return ("Verify", f"{len(waiting)} to verify, {q}",
|
|
193
279
|
f"{CC}verifier *verify {scope} {app}", f"{OC}flow-verifier *verify {scope} {app}",
|
|
194
|
-
|
|
280
|
+
reason)
|
|
195
281
|
if owner:
|
|
196
282
|
if handoff_ran(log_rows):
|
|
197
|
-
line = f"(owner-run)
|
|
283
|
+
line = f"(owner-run) {usage_guide(app)} — {ids(owner)}"
|
|
198
284
|
return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
|
|
199
285
|
f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
|
|
200
286
|
return ("Handoff", f"{len(owner)} owner-run, {q}",
|
|
@@ -203,10 +289,10 @@ def next_command(app, rows, log_rows, phase=1):
|
|
|
203
289
|
if handoff_ran(log_rows):
|
|
204
290
|
line = "(owner) set current_phase to Released after UAT — no agent command"
|
|
205
291
|
return ("UAT", f"handoff done, {q}", line, line,
|
|
206
|
-
"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")
|
|
207
293
|
return ("Handoff", q,
|
|
208
294
|
f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
|
|
209
|
-
"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")
|
|
210
296
|
|
|
211
297
|
|
|
212
298
|
def main(argv):
|
|
@@ -234,15 +320,18 @@ def main(argv):
|
|
|
234
320
|
st_text = read(st_path) if os.path.isfile(st_path) else ""
|
|
235
321
|
log_rows = existing_log_rows(st_text)
|
|
236
322
|
vdate, vresult = last_verify(root, app)
|
|
237
|
-
phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n)
|
|
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)}")
|
|
238
328
|
pname, ptotal = phase_row(root, app, phase_n)
|
|
239
329
|
if ptotal or phase_n > 1:
|
|
240
330
|
tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
|
|
241
331
|
phase = f"{tag} · {phase}"
|
|
242
332
|
reason = f"{reason}; working {cl_rel}"
|
|
243
333
|
|
|
244
|
-
|
|
245
|
-
closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
|
|
334
|
+
verified, counted, na = figures(rows)
|
|
246
335
|
counts = {}
|
|
247
336
|
for r in rows:
|
|
248
337
|
counts[r["status"]] = counts.get(r["status"], 0) + 1
|
|
@@ -273,7 +362,7 @@ def main(argv):
|
|
|
273
362
|
print()
|
|
274
363
|
if open_rows:
|
|
275
364
|
for r in open_rows[:10]:
|
|
276
|
-
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 ''})")
|
|
277
366
|
if len(open_rows) > 10:
|
|
278
367
|
print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
|
|
279
368
|
else:
|
|
@@ -283,7 +372,7 @@ def main(argv):
|
|
|
283
372
|
print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
|
|
284
373
|
print()
|
|
285
374
|
print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
|
|
286
|
-
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 |"
|
|
287
376
|
for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
|
|
288
377
|
print(r)
|
|
289
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,
|