@techierathore/techieflow 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/analyst.md +0 -5
- package/.claude/commands/TechieFlow/agents/architect.md +0 -6
- package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +39 -12
- package/.tfcore/agents/analyst.md +0 -5
- package/.tfcore/agents/architect.md +0 -6
- package/.tfcore/agents/flow-master.md +41 -215
- package/.tfcore/agents/verifier.md +23 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +56 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +25 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +20 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +1 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +43 -7
- package/.tfcore/telemetry/tf-metrics.sh +152 -17
- package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +191 -0
- package/.tfcore/utils/tf-build-list.sh +20 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1267 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +203 -13
- package/.tfcore/utils/tf-fix-close.sh +83 -0
- package/.tfcore/utils/tf-goal.sh +193 -62
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +211 -0
- package/.tfcore/utils/tf-misses-md.sh +13 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +74 -0
- package/.tfcore/utils/tf-routing-bind.sh +10 -5
- package/.tfcore/utils/tf-routing.sh +6 -7
- package/.tfcore/utils/tf-split-brd.py +304 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +309 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +283 -0
- package/.tfcore/utils/tf-verify-screens.sh +22 -0
- package/.tfcore/utils/tf-verify-tests.sh +123 -0
- package/.tfcore/utils/tf-verify-verdict.py +283 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +114 -1149
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +13 -33
- package/package.json +1 -5
- package/scripts/install.mjs +52 -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/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-build-list.py — the mechanical start of a build pass (see tf-build-list.sh).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-build-list.sh <App> [--phase N] [--prompts]
|
|
5
|
+
|
|
6
|
+
Reads the phase's checklist (appPhase in core-config.yaml, or --phase) and prints:
|
|
7
|
+
- the mode: FIX (rows FAIL / PARTIAL / In Progress / Needs re-verify exist), FRESH
|
|
8
|
+
(every open row), or NOTHING (every row terminal or Blocked)
|
|
9
|
+
- the working list, grouped into clusters by the checklist's page section, each
|
|
10
|
+
naming its builder from the row prefix: REQ-UI -> trblazeui, REQ-RAG -> techierag,
|
|
11
|
+
REQ-FN / REQ-NFR -> builder
|
|
12
|
+
- with --prompts, one ready sub-agent prompt per cluster, filled from
|
|
13
|
+
.tfcore/templates/v4custom/build-subagent-prompt.md with the rows, their acceptance
|
|
14
|
+
lines, their mockups and the two standing rules.
|
|
15
|
+
Writes nothing. Python 3 standard library only. Exit 0 printed, 2 could not run.
|
|
16
|
+
"""
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
TERMINAL = {"verified", "done (pre-existing)", "n/a"}
|
|
22
|
+
FIX = {"fail", "partial", "in progress", "needs re-verify"}
|
|
23
|
+
BLOCKED = {"blocked"}
|
|
24
|
+
BUILDER = {"UI": "trblazeui", "RAG": "techierag", "FN": "builder", "NFR": "builder"}
|
|
25
|
+
TPL = os.path.join(".tfcore", "templates", "v4custom", "build-subagent-prompt.md")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def die(msg):
|
|
29
|
+
print(f"tf-build-list: {msg}", file=sys.stderr)
|
|
30
|
+
sys.exit(2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def read(p):
|
|
34
|
+
with open(p, encoding="utf-8", errors="replace") as f:
|
|
35
|
+
return f.read()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def config_phase():
|
|
39
|
+
p = os.path.join(".tfcore", "core-config.yaml")
|
|
40
|
+
if os.path.isfile(p):
|
|
41
|
+
m = re.search(r"(?m)^appPhase:\s*(\d+)", read(p))
|
|
42
|
+
if m:
|
|
43
|
+
return int(m.group(1))
|
|
44
|
+
return 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def norm(s):
|
|
48
|
+
s = s.strip().strip("`* ").lower()
|
|
49
|
+
return re.sub(r"\s*\(.*$", "", s).strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rows_of(text):
|
|
53
|
+
rows = []
|
|
54
|
+
for line in text.splitlines():
|
|
55
|
+
if re.match(r"^\s*\|\s*`?REQ-", line):
|
|
56
|
+
c = [x.strip() for x in line.strip().strip("|").split("|")]
|
|
57
|
+
if len(c) >= 3:
|
|
58
|
+
rows.append({"id": c[0].strip("`* "), "title": c[1].strip("`* "), "status": norm(c[2]),
|
|
59
|
+
"remarks": c[4] if len(c) > 4 else ""})
|
|
60
|
+
return rows
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def entries_of(text):
|
|
64
|
+
"""id -> (section heading, acceptance line, mockup, brd)"""
|
|
65
|
+
out, current = {}, "Other"
|
|
66
|
+
lines = text.splitlines()
|
|
67
|
+
for i, line in enumerate(lines):
|
|
68
|
+
m = re.match(r"^##\s+(.+)$", line)
|
|
69
|
+
if m:
|
|
70
|
+
current = re.sub(r"^(?:page|screen)\s*:\s*", "", m.group(1).strip(), flags=re.I)
|
|
71
|
+
continue
|
|
72
|
+
a = re.search(r"<a id=['\"]d-(req-[a-z]+-\d+)['\"]", line, re.I)
|
|
73
|
+
if a:
|
|
74
|
+
rid = a.group(1).upper()
|
|
75
|
+
acc = mock = brd = ""
|
|
76
|
+
for j in range(i, min(i + 12, len(lines))):
|
|
77
|
+
l = lines[j]
|
|
78
|
+
if re.search(r"\*Acceptance:?\*|^\s*[-*]\s*Acceptance:", l, re.I) and not acc:
|
|
79
|
+
acc = re.sub(r"^\s*[-*]\s*\*?Acceptance:?\*?:?\s*", "", l).strip()
|
|
80
|
+
mm = re.search(r"((?:docs/)?mockups/[\w./-]+\.html)", l)
|
|
81
|
+
if mm and not mock:
|
|
82
|
+
mock = mm.group(1)
|
|
83
|
+
bm = re.search(r"BRD-\d+", l)
|
|
84
|
+
if bm and not brd:
|
|
85
|
+
brd = bm.group(0)
|
|
86
|
+
if j > i and re.match(r"^\s*(?:[-*]\s*)?<a id=|^## ", l):
|
|
87
|
+
break # the next entry starts (it begins with "- <a id="): nothing below is this row's
|
|
88
|
+
out[rid] = (current, acc, mock, brd)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main(argv):
|
|
93
|
+
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
|
94
|
+
print(__doc__)
|
|
95
|
+
return 0
|
|
96
|
+
app = argv[1]
|
|
97
|
+
prompts = "--prompts" in argv
|
|
98
|
+
phase = config_phase()
|
|
99
|
+
if "--phase" in argv:
|
|
100
|
+
i = argv.index("--phase")
|
|
101
|
+
if i + 1 >= len(argv) or not argv[i + 1].isdigit():
|
|
102
|
+
die("--phase needs a number")
|
|
103
|
+
phase = int(argv[i + 1])
|
|
104
|
+
pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
105
|
+
cl = os.path.join("docs", f"{pfx}Checklist.md")
|
|
106
|
+
if not os.path.isfile(cl):
|
|
107
|
+
die(f"{cl} does not exist; day-1 stage 2 writes it")
|
|
108
|
+
text = read(cl)
|
|
109
|
+
rows = rows_of(text)
|
|
110
|
+
if not rows:
|
|
111
|
+
die(f"{cl} has no REQ rows")
|
|
112
|
+
ent = entries_of(text)
|
|
113
|
+
|
|
114
|
+
fix_rows = [r for r in rows if r["status"] in FIX]
|
|
115
|
+
open_rows = [r for r in rows if r["status"] not in TERMINAL | BLOCKED]
|
|
116
|
+
if fix_rows:
|
|
117
|
+
mode, work = "FIX", fix_rows
|
|
118
|
+
elif open_rows:
|
|
119
|
+
mode, work = "FRESH", open_rows
|
|
120
|
+
else:
|
|
121
|
+
mode, work = "NOTHING", []
|
|
122
|
+
terminal = sum(1 for r in rows if r["status"] in TERMINAL)
|
|
123
|
+
blocked = [r for r in rows if r["status"] in BLOCKED]
|
|
124
|
+
print(f"# tf-build-list — {app} — {cl} — phase {phase}")
|
|
125
|
+
print(f"Mode: {mode} — {len(work)} row(s) to build; {terminal} terminal, {len(blocked)} Blocked, {len(rows)} total"
|
|
126
|
+
+ (f" — {', '.join(r['id'] for r in work[:8])}{' …' if len(work) > 8 else ''}" if work else ""))
|
|
127
|
+
if blocked:
|
|
128
|
+
print("Blocked (library gaps, pass through unless the feedback entry is resolved): " + ", ".join(r["id"] for r in blocked))
|
|
129
|
+
if not work:
|
|
130
|
+
print("Nothing to build. Run the status gate; the next command is the verifier's or the owner's.")
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
clusters = {}
|
|
134
|
+
for r in work:
|
|
135
|
+
sec, acc, mock, brd = ent.get(r["id"].upper(), ("Other", "", "", ""))
|
|
136
|
+
cls = re.match(r"REQ-(UI|FN|RAG|NFR)-", r["id"]).group(1)
|
|
137
|
+
key = ("Non-functional" if cls == "NFR" else "RAG" if cls == "RAG" else sec, BUILDER[cls])
|
|
138
|
+
clusters.setdefault(key, []).append((r, acc, mock, brd))
|
|
139
|
+
print()
|
|
140
|
+
print("## Clusters (one sub-agent each; spawn them all in one turn)")
|
|
141
|
+
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
142
|
+
named = []
|
|
143
|
+
for i, ((sec, builder), items) in enumerate(clusters.items()):
|
|
144
|
+
label = letters[i % 26] + ("" if i < 26 else str(i // 26))
|
|
145
|
+
named.append((label, sec, builder, items))
|
|
146
|
+
mocks = sorted({m for _r, _a, m, _b in items if m})
|
|
147
|
+
print(f"Cluster {label} [{builder}]: " + ", ".join(r["id"] for r, *_ in items)
|
|
148
|
+
+ f" ({sec}" + (f" — mockup: {', '.join(mocks)}" if mocks else "") + ")")
|
|
149
|
+
print()
|
|
150
|
+
print("## Working list")
|
|
151
|
+
for label, sec, builder, items in named:
|
|
152
|
+
for r, acc, mock, brd in items:
|
|
153
|
+
todo = "" if acc else " ⚠ no acceptance line: fix the checklist first"
|
|
154
|
+
print(f"- {r['id']} [{label}/{builder}] {r['title']} — {r['status']}{todo}")
|
|
155
|
+
if acc:
|
|
156
|
+
print(f" Acceptance: {acc}")
|
|
157
|
+
if mock:
|
|
158
|
+
print(f" Mockup: {mock}")
|
|
159
|
+
if not prompts:
|
|
160
|
+
print()
|
|
161
|
+
print("Add --prompts for one ready sub-agent prompt per cluster.")
|
|
162
|
+
return 0
|
|
163
|
+
if not os.path.isfile(TPL):
|
|
164
|
+
die(f"template {TPL} does not exist at its literal path")
|
|
165
|
+
tpl = read(TPL)
|
|
166
|
+
tpl = re.sub(r"<!--.*?-->\s*", "", tpl, flags=re.S)
|
|
167
|
+
for label, sec, builder, items in named:
|
|
168
|
+
lines = []
|
|
169
|
+
for r, acc, mock, brd in items:
|
|
170
|
+
lines.append(f"- {r['id']} — {r['title']}" + (f" ({brd})" if brd else ""))
|
|
171
|
+
lines.append(f" Acceptance: {acc or 'MISSING — refuse the row and report it'}")
|
|
172
|
+
if mock:
|
|
173
|
+
lines.append(f" Mockup: {mock}")
|
|
174
|
+
mocks = sorted({m for _r, _a, m, _b in items if m})
|
|
175
|
+
body = (tpl.replace("{App}", app).replace("{Cluster}", label).replace("{Builder}", builder)
|
|
176
|
+
.replace("{Section}", sec).replace("{Rows}", "\n".join(lines))
|
|
177
|
+
.replace("{Mockups}", ", ".join(mocks) if mocks else "none (not a UI cluster)")
|
|
178
|
+
.replace("{Checklist}", cl))
|
|
179
|
+
print()
|
|
180
|
+
print(f"## Prompt for cluster {label} [{builder}]")
|
|
181
|
+
print(body.strip())
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
try:
|
|
187
|
+
sys.exit(main(sys.argv))
|
|
188
|
+
except SystemExit:
|
|
189
|
+
raise
|
|
190
|
+
except Exception as e:
|
|
191
|
+
die(str(e))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-build-list.sh — the mechanical start of a build pass.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-build-list.sh MyApp # mode (FIX / FRESH / NOTHING), clusters, working list
|
|
5
|
+
# bash .tfcore/utils/tf-build-list.sh MyApp --prompts # plus one ready sub-agent prompt per cluster
|
|
6
|
+
# bash .tfcore/utils/tf-build-list.sh MyApp --phase 2 # a later phase of a Large project
|
|
7
|
+
#
|
|
8
|
+
# Reads the phase's checklist (appPhase in core-config.yaml unless --phase). Rows FAIL /
|
|
9
|
+
# PARTIAL / In Progress / Needs re-verify make it FIX mode over exactly those rows; else every
|
|
10
|
+
# open row is FRESH; Blocked rows pass through. Clusters follow the checklist's page sections,
|
|
11
|
+
# each with its builder from the prefix (UI -> trblazeui, RAG -> techierag, FN/NFR -> builder).
|
|
12
|
+
# Prompts are filled from .tfcore/templates/v4custom/build-subagent-prompt.md.
|
|
13
|
+
# Writes nothing. Thin wrapper over tf-build-list.py; Python 3 stdlib only.
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
17
|
+
echo "tf-build-list: python3 is required (standard library only)." >&2
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
exec python3 "$SELF_DIR/tf-build-list.py" "$@"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-build.sh — build, test or run a .NET target on whatever host this is, trying the
|
|
3
|
+
# invocation rungs in order and stopping at the first that works (Sitting 4b, 2026-09-05;
|
|
4
|
+
# replaces the prose ladder build-invocation-ladder.md §0 to §C).
|
|
5
|
+
#
|
|
6
|
+
# bash .tfcore/utils/tf-build.sh [build] [<target>] [-- <extra dotnet args>]
|
|
7
|
+
# bash .tfcore/utils/tf-build.sh test [<target>] [-- <extra dotnet args>]
|
|
8
|
+
# bash .tfcore/utils/tf-build.sh run <project> [-- <extra dotnet args>]
|
|
9
|
+
# bash .tfcore/utils/tf-build.sh probe # print the platform and the rungs, run nothing
|
|
10
|
+
#
|
|
11
|
+
# <target> is a .sln, .slnx or .csproj relative to the current folder (default: the one
|
|
12
|
+
# solution or project found here). Output goes to tests/.artifacts/build/<UTC time>.log.
|
|
13
|
+
#
|
|
14
|
+
# Prints ONE verdict line the agent copies into the status log row:
|
|
15
|
+
# PASS built on wsl via cmd.exe /c dotnet (rung 4) — 0 warnings
|
|
16
|
+
# FAIL real compile errors on wsl via ~/.dotnet/dotnet (rung 2): 3 CS errors, first: …
|
|
17
|
+
# NOT-RUN no rung could run dotnet on wsl; tried: dotnet, ~/.dotnet/dotnet, cmd.exe, winrun, powershell.exe
|
|
18
|
+
# Exit 0 PASS · 1 FAIL (the code is wrong: fix it) · 2 NOT-RUN (the host is wrong: never a
|
|
19
|
+
# project blocker; say which rungs were tried).
|
|
20
|
+
#
|
|
21
|
+
# Rungs by host (docs/TechieFlow-How-It-Works.md; build-invocation-ladder.md §D keeps the
|
|
22
|
+
# runtime-driver table, which this script does not replace):
|
|
23
|
+
# macOS / native Linux / native Windows: dotnet
|
|
24
|
+
# WSL, no MAUI head in the target: dotnet · ~/.dotnet/dotnet · winrun · cmd.exe /c dotnet · powershell.exe dotnet
|
|
25
|
+
# WSL, a MAUI head in the target: cmd.exe /c dotnet · winrun · powershell.exe dotnet (workloads live Windows-side)
|
|
26
|
+
# OpenCode Docker (TF_OPENCODE_DOCKER=1 or winrun + TF_WINDOWS_APP_PATH): MAUI → winrun; else dotnet
|
|
27
|
+
# A workload / SDK-missing error (NETSDK1178, Microsoft.iOS/Android/MacCatalyst.Sdk, Workload ID,
|
|
28
|
+
# WindowsAppSDK, command not found) means the wrong rung: the next one is tried, nothing is logged
|
|
29
|
+
# as a blocker. A CS#### error means the code is wrong: the rung stays and the script exits 1.
|
|
30
|
+
set -u
|
|
31
|
+
|
|
32
|
+
MODE="build"; TARGET=""; EXTRA=()
|
|
33
|
+
case "${1:-}" in
|
|
34
|
+
build|test|run|probe) MODE="$1"; shift ;;
|
|
35
|
+
esac
|
|
36
|
+
while [[ $# -gt 0 ]]; do
|
|
37
|
+
if [[ "$1" == "--" ]]; then shift; EXTRA=("$@"); break; fi
|
|
38
|
+
TARGET="$1"; shift
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
# ---- platform -------------------------------------------------------------------------
|
|
42
|
+
PLATFORM="linux"
|
|
43
|
+
U="$(uname -s 2>/dev/null || echo windows)"
|
|
44
|
+
if [[ "$U" == Darwin* ]]; then PLATFORM="macos"
|
|
45
|
+
elif [[ "$U" == Linux* ]]; then
|
|
46
|
+
if grep -qiE "microsoft|wsl" /proc/version 2>/dev/null; then PLATFORM="wsl"
|
|
47
|
+
elif [[ "${TF_OPENCODE_DOCKER:-}" == "1" ]] || { [[ -x /usr/local/bin/winrun ]] && [[ -n "${TF_WINDOWS_APP_PATH:-}" ]]; }; then PLATFORM="docker"
|
|
48
|
+
fi
|
|
49
|
+
elif [[ "$U" == windows || "$U" == MINGW* || "$U" == MSYS* || "$U" == CYGWIN* ]]; then PLATFORM="windows"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ---- target ---------------------------------------------------------------------------
|
|
53
|
+
if [[ -z "$TARGET" && "$MODE" != "probe" ]]; then
|
|
54
|
+
mapfile -t CANDS < <(ls -1 *.slnx *.sln 2>/dev/null; ls -1 *.csproj 2>/dev/null)
|
|
55
|
+
if [[ ${#CANDS[@]} -eq 1 ]]; then TARGET="${CANDS[0]}"
|
|
56
|
+
elif [[ ${#CANDS[@]} -gt 1 ]]; then echo "NOT-RUN more than one solution or project here; name one: ${CANDS[*]}"; exit 2
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# ---- does the target include a MAUI head? ----------------------------------------------
|
|
61
|
+
maui=0
|
|
62
|
+
projects=()
|
|
63
|
+
if [[ -n "$TARGET" ]]; then
|
|
64
|
+
case "$TARGET" in
|
|
65
|
+
*.csproj) projects=("$TARGET") ;;
|
|
66
|
+
*.sln) mapfile -t projects < <(grep -oE '"[^"]+\.csproj"' "$TARGET" 2>/dev/null | tr -d '"' | tr '\\' '/') ;;
|
|
67
|
+
*.slnx) mapfile -t projects < <(grep -oE 'Path="[^"]+\.csproj"' "$TARGET" 2>/dev/null | sed 's/Path="//; s/"$//' | tr '\\' '/') ;;
|
|
68
|
+
esac
|
|
69
|
+
base="$(dirname "$TARGET")"
|
|
70
|
+
for p in "${projects[@]}"; do
|
|
71
|
+
f="$p"; [[ -f "$f" ]] || f="$base/$p"
|
|
72
|
+
[[ -f "$f" ]] || continue
|
|
73
|
+
if grep -qiE '<UseMaui>\s*true|Microsoft\.NET\.Sdk\.Maui|net[0-9.]+-(android|ios|maccatalyst|windows10)' "$f"; then maui=1; fi
|
|
74
|
+
done
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# ---- rungs -----------------------------------------------------------------------------
|
|
78
|
+
rungs=()
|
|
79
|
+
case "$PLATFORM" in
|
|
80
|
+
macos|linux|windows) rungs=("dotnet") ;;
|
|
81
|
+
docker) if [[ $maui -eq 1 ]]; then rungs=("winrun"); else rungs=("dotnet" "winrun"); fi ;;
|
|
82
|
+
wsl)
|
|
83
|
+
if [[ $maui -eq 1 ]]; then rungs=("cmd.exe" "winrun" "powershell.exe")
|
|
84
|
+
else rungs=("dotnet" "$HOME/.dotnet/dotnet" "winrun" "cmd.exe" "powershell.exe"); fi ;;
|
|
85
|
+
esac
|
|
86
|
+
|
|
87
|
+
if [[ "$MODE" == "probe" ]]; then
|
|
88
|
+
echo "platform: $PLATFORM · target: ${TARGET:-none} · maui head: $([[ $maui -eq 1 ]] && echo yes || echo no)"
|
|
89
|
+
echo "rungs, in order: ${rungs[*]}"
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
[[ -z "$TARGET" && "$MODE" != "run" ]] && { echo "NOT-RUN no .sln, .slnx or .csproj here; name the target"; exit 2; }
|
|
93
|
+
|
|
94
|
+
# ---- the dotnet verb -------------------------------------------------------------------
|
|
95
|
+
verb="$MODE"
|
|
96
|
+
args=("$verb")
|
|
97
|
+
[[ -n "$TARGET" ]] && args+=("$TARGET")
|
|
98
|
+
[[ "$MODE" == "run" ]] && args=("run" "--project" "$TARGET")
|
|
99
|
+
args+=("${EXTRA[@]}")
|
|
100
|
+
|
|
101
|
+
mkdir -p tests/.artifacts/build
|
|
102
|
+
LOG="tests/.artifacts/build/$(date -u +%Y%m%dT%H%M%SZ)-$MODE.log"
|
|
103
|
+
WRONG_RUNG='NETSDK1178|Microsoft\.(iOS|Android|MacCatalyst|tvOS)\.Sdk|Workload ID|not recognized|WindowsAppSDK|command not found|No such file or directory|is not recognized as an internal or external command|The term .* is not recognized|workload.*not installed|Inadequate permissions'
|
|
104
|
+
|
|
105
|
+
tried=()
|
|
106
|
+
n=0
|
|
107
|
+
for r in "${rungs[@]}"; do
|
|
108
|
+
n=$((n+1))
|
|
109
|
+
case "$r" in
|
|
110
|
+
dotnet) cmd=(dotnet "${args[@]}"); label="dotnet" ;;
|
|
111
|
+
*/.dotnet/dotnet) cmd=("$r" "${args[@]}"); label="~/.dotnet/dotnet" ;;
|
|
112
|
+
winrun) cmd=(winrun "dotnet ${args[*]}"); label="winrun dotnet" ;;
|
|
113
|
+
cmd.exe) cmd=(cmd.exe /c "dotnet ${args[*]}"); label="cmd.exe /c dotnet" ;;
|
|
114
|
+
powershell.exe) cmd=(powershell.exe -Command "dotnet ${args[*]}"); label="powershell.exe dotnet" ;;
|
|
115
|
+
esac
|
|
116
|
+
tried+=("$label")
|
|
117
|
+
{ echo "### rung $n: ${cmd[*]}"; "${cmd[@]}" 2>&1; echo "### exit $?"; } >> "$LOG" 2>&1
|
|
118
|
+
rc="$(tail -1 "$LOG" | sed 's/### exit //')"
|
|
119
|
+
seg="$(awk -v n="### rung $n:" 'index($0,n)==1{p=1} p' "$LOG")"
|
|
120
|
+
if [[ "$rc" == "0" ]]; then
|
|
121
|
+
warns="$(grep -cE 'warning [A-Z]+[0-9]+' <<<"$seg" || true)"
|
|
122
|
+
echo "PASS $MODE on $PLATFORM via $label (rung $n) — $warns warning line(s); log $LOG"
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
# Code errors: C# (CS), Razor (RZ), Blazor (BL), XAML (XC, XLS), missing reference (MSB/NU).
|
|
126
|
+
# Until 2026-09-06 only CS counted, so a build failing on sixteen Razor RZ9991 errors was
|
|
127
|
+
# taken for a wrong rung and reported NOT-RUN "host issue" (MISS-TechieFlow-20260906-03).
|
|
128
|
+
if grep -qE 'error (CS|RZ|BL|XC|XLS)[0-9]{3,5}|error MSB[0-9]{4}: .*(does not exist|could not be found)|error NU1' <<<"$seg" && ! grep -qE "$WRONG_RUNG" <<<"$seg"; then
|
|
129
|
+
errs="$(grep -cE 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" || true)"
|
|
130
|
+
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^\s*//' | cut -c1-160)"
|
|
131
|
+
echo "FAIL real errors on $PLATFORM via $label (rung $n): $errs error line(s), first: $first; log $LOG"
|
|
132
|
+
grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | sed 's/^\s*//' | sort -u | head -10
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
if [[ "$MODE" == "test" ]] && grep -qE 'Failed!|Failed:\s+[1-9]|Tests? failed' <<<"$seg"; then
|
|
136
|
+
echo "FAIL tests failed on $PLATFORM via $label (rung $n); log $LOG"
|
|
137
|
+
grep -E 'Failed |Failed!|Error Message|Total tests|Passed!' <<<"$seg" | head -12
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
# anything else (workload, wrong rung, tool absent): try the next rung
|
|
141
|
+
done
|
|
142
|
+
echo "NOT-RUN no rung could $MODE on $PLATFORM; tried: ${tried[*]}. This is a host issue, never a project blocker; log $LOG"
|
|
143
|
+
exit 2
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# tf-cdp-relay.ps1 — relay a Windows loopback port to every interface (Sitting 4c, 2026-09-06).
|
|
2
|
+
#
|
|
3
|
+
# powershell.exe -File tf-cdp-relay.ps1 -ListenPort 9223 -TargetPort 9222
|
|
4
|
+
#
|
|
5
|
+
# A MAUI Blazor Hybrid app's WebView2 opens its DevTools port on 127.0.0.1 only, so WSL cannot
|
|
6
|
+
# reach it. This relay listens on every interface and forwards to the loopback port, so Playwright
|
|
7
|
+
# in WSL can attach with connectOverCDP('http://<windows host ip>:9223'). Started and stopped by
|
|
8
|
+
# tf-verify-boot.sh; proven on MyDiary's Windows head, 2026-09-06 (the DevGuide run).
|
|
9
|
+
param(
|
|
10
|
+
[int]$ListenPort = 9223,
|
|
11
|
+
[string]$TargetHost = '127.0.0.1',
|
|
12
|
+
[int]$TargetPort = 9222
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Add-Type -TypeDefinition @"
|
|
16
|
+
using System;
|
|
17
|
+
using System.Net;
|
|
18
|
+
using System.Net.Sockets;
|
|
19
|
+
using System.Threading;
|
|
20
|
+
|
|
21
|
+
public static class TcpRelay {
|
|
22
|
+
public static void Start(int listenPort, string targetHost, int targetPort) {
|
|
23
|
+
var listener = new TcpListener(IPAddress.Any, listenPort);
|
|
24
|
+
listener.Start();
|
|
25
|
+
while (true) {
|
|
26
|
+
var client = listener.AcceptTcpClient();
|
|
27
|
+
var t = new Thread(() => Handle(client, targetHost, targetPort));
|
|
28
|
+
t.IsBackground = true;
|
|
29
|
+
t.Start();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private static void Handle(TcpClient client, string targetHost, int targetPort) {
|
|
34
|
+
try {
|
|
35
|
+
using (client)
|
|
36
|
+
using (var target = new TcpClient(targetHost, targetPort)) {
|
|
37
|
+
using (var cs = client.GetStream())
|
|
38
|
+
using (var ts = target.GetStream()) {
|
|
39
|
+
var t1 = new Thread(() => Pump(cs, ts));
|
|
40
|
+
t1.IsBackground = true;
|
|
41
|
+
t1.Start();
|
|
42
|
+
Pump(ts, cs);
|
|
43
|
+
t1.Join();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static void Pump(System.IO.Stream src, System.IO.Stream dst) {
|
|
50
|
+
var buf = new byte[65536];
|
|
51
|
+
try {
|
|
52
|
+
int n;
|
|
53
|
+
while ((n = src.Read(buf, 0, buf.Length)) > 0) {
|
|
54
|
+
dst.Write(buf, 0, n);
|
|
55
|
+
dst.Flush();
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
try { dst.Close(); } catch {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
"@
|
|
62
|
+
|
|
63
|
+
Write-Host "relay listening on 0.0.0.0:$ListenPort -> $TargetHost`:$TargetPort"
|
|
64
|
+
[TcpRelay]::Start($ListenPort, $TargetHost, $TargetPort)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-checklist-edit.py — the checklist edits the bug scripts share (Sitting 4c, 2026-09-06).
|
|
3
|
+
|
|
4
|
+
Imported by tf-triage.py and tf-log-miss.py; not run on its own. Every function takes the
|
|
5
|
+
checklist path, edits the file in place and returns what it did. The shapes follow
|
|
6
|
+
docs/TechieFlow-Document-Schemas.md §3.4: the six-cell row, the `<a id="d-req-…">` entry, one
|
|
7
|
+
acceptance line, a Remarks cell of at most 60 words.
|
|
8
|
+
"""
|
|
9
|
+
import datetime
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
ROW = re.compile(r"^(\s*\|\s*`?)(REQ-[A-Z]+-\d+)(`?\s*\|)")
|
|
14
|
+
TODAY = datetime.date.today().isoformat()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read(p):
|
|
18
|
+
with open(p, encoding="utf-8") as f:
|
|
19
|
+
return f.read()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def write(p, s):
|
|
23
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
24
|
+
f.write(s)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def words(s, n=60):
|
|
28
|
+
w = s.split()
|
|
29
|
+
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)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rows(text):
|
|
33
|
+
"""id -> {status, pct, remarks, title, line_no}"""
|
|
34
|
+
out = {}
|
|
35
|
+
for i, line in enumerate(text.splitlines()):
|
|
36
|
+
m = ROW.match(line)
|
|
37
|
+
if m:
|
|
38
|
+
c = [x.strip() for x in line.strip().strip("|").split("|")]
|
|
39
|
+
if len(c) >= 6:
|
|
40
|
+
pct = re.sub(r"[^0-9]", "", c[3])
|
|
41
|
+
out[m.group(2).upper()] = {"status": c[2].strip("`* "), "pct": int(pct) if pct.isdigit() else None,
|
|
42
|
+
"remarks": c[4], "title": c[1].strip("`* "), "line_no": i}
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def set_cells(path, rid, status=None, pct=None, remark=None, append_remark=None):
|
|
47
|
+
"""rewrite the Status, % and Remarks cells of one row; returns the prior cells or None"""
|
|
48
|
+
text = read(path)
|
|
49
|
+
lines = text.splitlines(keepends=True)
|
|
50
|
+
prior = None
|
|
51
|
+
for i, line in enumerate(lines):
|
|
52
|
+
m = ROW.match(line)
|
|
53
|
+
if not m or m.group(2).upper() != rid.upper():
|
|
54
|
+
continue
|
|
55
|
+
nl = "\r\n" if line.endswith("\r\n") else "\n"
|
|
56
|
+
c = line.rstrip("\r\n").strip().strip("|").split("|")
|
|
57
|
+
if len(c) < 6:
|
|
58
|
+
break
|
|
59
|
+
prior = {"status": c[2].strip().strip("`* "), "pct": c[3].strip(), "remarks": c[4].strip()}
|
|
60
|
+
if status is not None:
|
|
61
|
+
c[2] = f" {status} "
|
|
62
|
+
if pct is not None:
|
|
63
|
+
c[3] = f" {pct}% " if "%" in c[3] or c[3].strip() == "" else f" {pct} "
|
|
64
|
+
if remark is not None:
|
|
65
|
+
c[4] = f" {words(remark.replace('|', '/'))} "
|
|
66
|
+
elif append_remark:
|
|
67
|
+
c[4] = f" {words((prior['remarks'] + ' ' + append_remark.replace('|', '/')).strip())} "
|
|
68
|
+
lines[i] = "|" + "|".join(c) + "|" + nl
|
|
69
|
+
break
|
|
70
|
+
if prior is not None:
|
|
71
|
+
write(path, "".join(lines))
|
|
72
|
+
return prior
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def next_id(text, prefix):
|
|
76
|
+
ids = [int(m.group(1)) for m in re.finditer(rf"REQ-{prefix}-(\d{{3}})", text)]
|
|
77
|
+
return f"REQ-{prefix}-{(max(ids) + 1 if ids else 1):03d}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def add_row(path, prefix, title, acceptance, section=None, remark=None, status="Not Started", mockup=None, brd="BRD-pending"):
|
|
81
|
+
"""append a row to the Requirements Status table and a detail entry under `section`
|
|
82
|
+
(a new section at the end when it does not exist); returns the new id"""
|
|
83
|
+
text = read(path)
|
|
84
|
+
rid = next_id(text, prefix)
|
|
85
|
+
nl = "\r\n" if "\r\n" in text else "\n"
|
|
86
|
+
lines = text.split(nl)
|
|
87
|
+
# the table: after the last REQ row
|
|
88
|
+
last = max((i for i, l in enumerate(lines) if ROW.match(l)), default=None)
|
|
89
|
+
if last is None:
|
|
90
|
+
raise SystemExit("no Requirements Status table with REQ rows in " + path)
|
|
91
|
+
remark = words((remark or f"logged from UAT {TODAY}").replace("|", "/"))
|
|
92
|
+
anchor = f"d-{rid.lower()}"
|
|
93
|
+
lines.insert(last + 1, f"| {rid} | {title.replace('|', '/')} | {status} | 0% | {remark} | [d](#{anchor}) |")
|
|
94
|
+
# the entry
|
|
95
|
+
entry = [f"- <a id=\"{anchor}\"></a> **{rid}** ({brd}) {title}" + (f" Mockup: {mockup}" if mockup else ""),
|
|
96
|
+
f" - *Acceptance:* {acceptance}", ""]
|
|
97
|
+
sec_idx = None
|
|
98
|
+
if section:
|
|
99
|
+
for i, l in enumerate(lines):
|
|
100
|
+
if re.match(rf"^##\s+(?:page|screen)?\s*:?\s*{re.escape(section)}\s*$", l, re.I):
|
|
101
|
+
sec_idx = i
|
|
102
|
+
break
|
|
103
|
+
if sec_idx is not None:
|
|
104
|
+
# after the last entry of that section: the line before the next "## " heading
|
|
105
|
+
j = sec_idx + 1
|
|
106
|
+
while j < len(lines) and not lines[j].startswith("## "):
|
|
107
|
+
j += 1
|
|
108
|
+
while j > sec_idx + 1 and lines[j - 1].strip() == "":
|
|
109
|
+
j -= 1
|
|
110
|
+
lines[j:j] = [""] + entry[:-1]
|
|
111
|
+
else:
|
|
112
|
+
heading = f"## {section}" if section else "## Logged from UAT"
|
|
113
|
+
if not any(l.strip() == heading for l in lines):
|
|
114
|
+
lines += ["", heading, ""]
|
|
115
|
+
lines += entry[:-1]
|
|
116
|
+
write(path, nl.join(lines).rstrip(nl) + nl)
|
|
117
|
+
return rid
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def demote(path, rid, symptom, kind=None, evidence=None, source="owner", prefix="⚠ UAT bug"):
|
|
121
|
+
"""Needs re-verify, % capped at 75, a dated Remark; returns the prior cells"""
|
|
122
|
+
r = rows(read(path)).get(rid.upper())
|
|
123
|
+
if not r:
|
|
124
|
+
return None
|
|
125
|
+
pct = min(r["pct"] if r["pct"] is not None else 75, 75)
|
|
126
|
+
ev = f"evidence: {evidence}" if evidence else f"evidence: {source} report"
|
|
127
|
+
remark = f"{prefix} {TODAY}: {symptom} ({ev}" + (f"; kind: {kind}" if kind else "") + ")"
|
|
128
|
+
return set_cells(path, rid, status="Needs re-verify", pct=pct, remark=remark)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def note(path, rid, text):
|
|
132
|
+
"""a dated remark, no status change"""
|
|
133
|
+
return set_cells(path, rid, append_remark=f"{TODAY} triage: {text}")
|