@techierathore/techieflow 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
- package/.claude/commands/TechieFlow/agents/architect.md +1 -16
- package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +40 -13
- package/.tfcore/agents/analyst.md +5 -13
- package/.tfcore/agents/architect.md +1 -16
- package/.tfcore/agents/flow-master.md +42 -215
- package/.tfcore/agents/verifier.md +24 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +68 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +86 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +21 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +3 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +100 -8
- package/.tfcore/telemetry/tf-metrics.sh +564 -32
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +211 -0
- package/.tfcore/utils/tf-build-list.sh +21 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1369 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +545 -18
- package/.tfcore/utils/tf-fix-close.sh +115 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +284 -66
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +208 -0
- package/.tfcore/utils/tf-misses-md.sh +14 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +92 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +97 -6
- package/.tfcore/utils/tf-routing.sh +13 -7
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +309 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +358 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +355 -0
- package/.tfcore/utils/tf-verify-screens.sh +31 -0
- package/.tfcore/utils/tf-verify-tests.sh +166 -0
- package/.tfcore/utils/tf-verify-verdict.py +292 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +120 -1145
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +37 -33
- package/package.json +1 -5
- package/scripts/install.mjs +56 -46
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-split-brd.py — draft docs/<App>-Checklist.md from the BRD ledger (see tf-split-brd.sh).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-split-brd.sh <App> # write the checklist (refuses if one exists)
|
|
5
|
+
bash .tfcore/utils/tf-split-brd.sh <App> --force # overwrite an existing checklist
|
|
6
|
+
bash .tfcore/utils/tf-split-brd.sh <App> --add-missing # append rows only for BRD items with no row yet
|
|
7
|
+
bash .tfcore/utils/tf-split-brd.sh <App> --phase 2 # a later phase of a Large project
|
|
8
|
+
bash .tfcore/utils/tf-split-brd.sh <App> --all-phases # every phase in docs/<App>-Phases.md
|
|
9
|
+
|
|
10
|
+
Large projects (docs/TechieFlow-Document-Schemas.md §2): phase 1 is docs/<App>-BRD.md and
|
|
11
|
+
docs/<App>-Checklist.md; phase n is docs/<App>-Pn-BRD.md and docs/<App>-Pn-Checklist.md. Without
|
|
12
|
+
--phase the phase comes from appPhase in .tfcore/core-config.yaml (1 when absent). REQ numbers run
|
|
13
|
+
on across phases: a new checklist starts each prefix after the highest number in every other
|
|
14
|
+
phase's checklist, so an id is never reused.
|
|
15
|
+
|
|
16
|
+
Reads docs/<App>-BRD.md: the header (App, Size), the Screens and flow table (screen, route),
|
|
17
|
+
every `**BRD-N**` ledger item with its title, screen, mockup link and acceptance line, and
|
|
18
|
+
the Non-functional table. Writes the checklist in the template shape: header, Goal, the one
|
|
19
|
+
Requirements Status table, then one section per screen holding the detail entries, plus
|
|
20
|
+
"RAG / AI requirements" and "Non-functional". Acceptance and perf-budget lines are copied
|
|
21
|
+
verbatim; an item without one gets a TODO line the checker will refuse, never an invented one.
|
|
22
|
+
|
|
23
|
+
Class is guessed, and the agent corrects it: Non-functional table -> REQ-NFR; text about
|
|
24
|
+
embeddings, vectors, prompts, chat or a model -> REQ-RAG; text about what a screen shows
|
|
25
|
+
(page, screen, list, form, dialog, button, menu, layout, theme, displays, shows) -> REQ-UI;
|
|
26
|
+
everything else -> REQ-FN. Exit 0 written, 1 refused, 2 could not run.
|
|
27
|
+
"""
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
RAG_WORDS = re.compile(r"\b(embedding|embeddings|vector|rag\b|prompt template|llm|language model|retriev|semantic search|chatbot|chat completion)", re.I)
|
|
33
|
+
UI_WORDS = re.compile(r"\b(page|screen|list|lists|form|dialog|button|menu|layout|theme|display|displays|shows|shown|grid|table|card|panel|tab|tabs|badge|icon|responsive|mobile)\b", re.I)
|
|
34
|
+
STATUS_LINE = ("**Status values:** `Not Started` · `In Progress` · `Implemented` · `Verified` · `Done (pre-existing)` · "
|
|
35
|
+
"`Needs re-verify` · `PARTIAL` · `FAIL` · `Blocked` · `Owner-UAT` · `N/A`.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def die(msg, code=2):
|
|
39
|
+
print(f"tf-split-brd: {msg}", file=sys.stderr)
|
|
40
|
+
sys.exit(code)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def read(p):
|
|
44
|
+
with open(p, encoding="utf-8", errors="replace") as f:
|
|
45
|
+
return f.read()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def header_field(text, name):
|
|
49
|
+
m = re.search(rf"(?im)^\|\s*{re.escape(name)}\s*\|\s*([^|]+?)\s*\|", text)
|
|
50
|
+
return m.group(1).strip() if m else ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def section(text, name):
|
|
54
|
+
m = re.search(rf"(?ms)^##\s+(?:\d+[.)]\s*)?{name}[^\n]*\n(.*?)(?=^## |\Z)", text)
|
|
55
|
+
return m.group(1) if m else ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def screens(text):
|
|
59
|
+
"""[(name, route)] from the Screens and flow table; dialog rows are skipped."""
|
|
60
|
+
out = []
|
|
61
|
+
for line in section(text, "Screens and flow").splitlines():
|
|
62
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
63
|
+
if len(cells) < 2 or cells[0].lower() in ("screen", "") or set(cells[0]) <= set("-: "):
|
|
64
|
+
continue
|
|
65
|
+
route = cells[1].strip("` ")
|
|
66
|
+
if route.lower().startswith("on "):
|
|
67
|
+
continue
|
|
68
|
+
out.append((cells[0].strip("`* "), route))
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ledger(text):
|
|
73
|
+
"""Every **BRD-N** item: id, title, screen, mockup, acceptance, perf, section name."""
|
|
74
|
+
items = []
|
|
75
|
+
nfr_ids = set()
|
|
76
|
+
for line in section(text, "Non-functional requirements").splitlines():
|
|
77
|
+
m = re.match(r"^\s*\|\s*`?\**(BRD-\d+)", line)
|
|
78
|
+
if m:
|
|
79
|
+
nfr_ids.add(m.group(1))
|
|
80
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
81
|
+
title = cells[2] if len(cells) > 2 else cells[-1]
|
|
82
|
+
measure = cells[3] if len(cells) > 3 else ""
|
|
83
|
+
items.append({"id": m.group(1), "title": title.strip("`* "), "screen": "", "mockup": "",
|
|
84
|
+
"acceptance": "", "perf": measure if "perf-budget" in measure.lower() else "", "nfr": True})
|
|
85
|
+
lines = text.splitlines()
|
|
86
|
+
i = 0
|
|
87
|
+
while i < len(lines):
|
|
88
|
+
# The anchor is not decoration: a BRD's §9 screen inventory links to each item as
|
|
89
|
+
# [BRD-21](#brd-21), markdown generates no id for bold text inside a list item, and
|
|
90
|
+
# tf-doc-check refuses a broken link -- so a real BRD carries `<a id="brd-N"></a>`
|
|
91
|
+
# here and this regex matched NONE of TfLens's 179 items. tests/regression tf_017.
|
|
92
|
+
m = re.match(r"^\s*[-*]\s*(?:<a id=[\"\']?[^\"\'>]*[\"\']?\s*>\s*</a>\s*)?"
|
|
93
|
+
r"\*\*(BRD-\d+)\*\*\s*[—:-]+\s*(.*)$", lines[i])
|
|
94
|
+
if m and m.group(1) not in nfr_ids:
|
|
95
|
+
rid, rest = m.group(1), m.group(2)
|
|
96
|
+
title = re.split(r"\s*(?:\.\s+\*|\s\*Screen|\s·\s\*|\s\*Mockup)", rest, 1)[0].strip().rstrip(".")
|
|
97
|
+
sm = re.search(r"\*Screen:\*\s*([^·*\n]+)", rest)
|
|
98
|
+
mm = re.search(r"\(((?:\./|docs/)?mockups/[^)\s]+)\)", rest) or re.search(r"((?:docs/)?mockups/[\w./-]+\.html)", rest)
|
|
99
|
+
acc = perf = ""
|
|
100
|
+
j = i + 1
|
|
101
|
+
while j < len(lines) and (lines[j].startswith(" ") or lines[j].strip() == ""):
|
|
102
|
+
if re.search(r"\*Acceptance:?\*|^\s*[-*]\s*Acceptance:", lines[j], re.I):
|
|
103
|
+
acc = re.sub(r"^\s*[-*]\s*\*?Acceptance:?\*?:?\s*", "", lines[j]).strip()
|
|
104
|
+
pm = re.search(r"perf-budget:[^\n|]+", lines[j], re.I)
|
|
105
|
+
if pm:
|
|
106
|
+
perf = pm.group(0).strip()
|
|
107
|
+
if lines[j].strip() == "" and j + 1 < len(lines) and not lines[j + 1].startswith(" "):
|
|
108
|
+
break
|
|
109
|
+
j += 1
|
|
110
|
+
items.append({"id": rid, "title": title, "screen": sm.group(1).strip() if sm else "",
|
|
111
|
+
"mockup": mm.group(1) if mm else "", "acceptance": acc, "perf": perf, "nfr": False})
|
|
112
|
+
i += 1
|
|
113
|
+
return [it for it in items if not it["nfr"]] + [it for it in items if it["nfr"]]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def classify(it):
|
|
117
|
+
if it["nfr"]:
|
|
118
|
+
return "NFR"
|
|
119
|
+
blob = it["title"] + " " + it["acceptance"]
|
|
120
|
+
if RAG_WORDS.search(blob):
|
|
121
|
+
return "RAG"
|
|
122
|
+
if UI_WORDS.search(blob):
|
|
123
|
+
return "UI"
|
|
124
|
+
return "FN"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def slug(s):
|
|
128
|
+
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def entry(req, it):
|
|
132
|
+
mock = f" · *Mockup:* {it['mockup'].replace('docs/', '')}" if it["mockup"] else ""
|
|
133
|
+
acc = it["acceptance"] or "TODO — write: When <actor> <does what> on <screen>, then <a result a browser robot can observe>"
|
|
134
|
+
if it["perf"] and it["perf"].lower() not in acc.lower():
|
|
135
|
+
acc = acc.rstrip(".") + "; " + it["perf"]
|
|
136
|
+
return (f'<a id="d-{req.lower()}"></a>\n- **{req}** — {it["title"]}. *BRD:* {it["id"]}{mock}\n'
|
|
137
|
+
f" - *Acceptance:* {acc}\n")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def config_phase():
|
|
141
|
+
"""appPhase from .tfcore/core-config.yaml; 1 when absent or null."""
|
|
142
|
+
p = os.path.join(".tfcore", "core-config.yaml")
|
|
143
|
+
if os.path.isfile(p):
|
|
144
|
+
m = re.search(r"(?m)^appPhase:\s*(\d+)", read(p))
|
|
145
|
+
if m:
|
|
146
|
+
return int(m.group(1))
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def phase_names(app, phase):
|
|
151
|
+
"""(BRD path, checklist path) for a phase: plain names for phase 1, App-Pn- otherwise."""
|
|
152
|
+
pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
153
|
+
return os.path.join("docs", f"{pfx}BRD.md"), os.path.join("docs", f"{pfx}Checklist.md")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def phases_listed(app):
|
|
157
|
+
"""Phase numbers from the Phases table of docs/<App>-Phases.md, in order."""
|
|
158
|
+
p = os.path.join("docs", f"{app}-Phases.md")
|
|
159
|
+
if not os.path.isfile(p):
|
|
160
|
+
die(f"{p} does not exist; --all-phases needs the Phases document")
|
|
161
|
+
nums = []
|
|
162
|
+
for line in section(read(p), "Phases").splitlines():
|
|
163
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
164
|
+
if cells and cells[0].strip("`* ").isdigit():
|
|
165
|
+
nums.append(int(cells[0].strip("`* ")))
|
|
166
|
+
if not nums:
|
|
167
|
+
die(f"{p} has no numbered rows in its Phases table")
|
|
168
|
+
return nums
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def other_phase_counters(app, own_cl, below=None):
|
|
172
|
+
"""Highest REQ number per prefix across the other phases' checklists, so ids run on.
|
|
173
|
+
below=n counts only phases under n (an --all-phases run, which rewrites them in order);
|
|
174
|
+
otherwise every other phase counts, so a single-phase run can never reuse an id."""
|
|
175
|
+
counters = {"UI": 0, "FN": 0, "RAG": 0, "NFR": 0}
|
|
176
|
+
if not os.path.isdir("docs"):
|
|
177
|
+
return counters
|
|
178
|
+
for f in os.listdir("docs"):
|
|
179
|
+
m = re.fullmatch(rf"{re.escape(app)}(?:-P(\d+))?-Checklist\.md", f)
|
|
180
|
+
if not m or os.path.join("docs", f) == own_cl:
|
|
181
|
+
continue
|
|
182
|
+
if below is not None and int(m.group(1) or 1) >= below:
|
|
183
|
+
continue
|
|
184
|
+
for r in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", read(os.path.join("docs", f))):
|
|
185
|
+
counters[r.group(1)] = max(counters[r.group(1)], int(r.group(2)))
|
|
186
|
+
return counters
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def main(argv):
|
|
190
|
+
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
|
191
|
+
print(__doc__)
|
|
192
|
+
return 0
|
|
193
|
+
app = argv[1]
|
|
194
|
+
force = "--force" in argv
|
|
195
|
+
add_missing = "--add-missing" in argv
|
|
196
|
+
phase = None
|
|
197
|
+
if "--phase" in argv:
|
|
198
|
+
i = argv.index("--phase")
|
|
199
|
+
if i + 1 >= len(argv) or not argv[i + 1].isdigit() or int(argv[i + 1]) < 1:
|
|
200
|
+
die("--phase needs a number from 1 upwards")
|
|
201
|
+
phase = int(argv[i + 1])
|
|
202
|
+
if "--all-phases" in argv:
|
|
203
|
+
rc = 0
|
|
204
|
+
for n in phases_listed(app):
|
|
205
|
+
rc = max(rc, run(app, n, force, add_missing, in_order=True))
|
|
206
|
+
return rc
|
|
207
|
+
return run(app, phase if phase else config_phase(), force, add_missing)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def run(app, phase, force, add_missing, in_order=False):
|
|
211
|
+
brd_p, cl_p = phase_names(app, phase)
|
|
212
|
+
if not os.path.isfile(brd_p):
|
|
213
|
+
die(f"{brd_p} does not exist" + (f" (phase {phase}; phase 1 is docs/{app}-BRD.md)" if phase > 1 else ""))
|
|
214
|
+
brd = read(brd_p)
|
|
215
|
+
items = ledger(brd)
|
|
216
|
+
if not items:
|
|
217
|
+
die(f"{brd_p} has no **BRD-N** items in its Requirements section")
|
|
218
|
+
scr = screens(brd)
|
|
219
|
+
routes = {s.lower(): r for s, r in scr}
|
|
220
|
+
size = header_field(brd, "Size") or "Small"
|
|
221
|
+
phase_label = header_field(brd, "Phase")
|
|
222
|
+
|
|
223
|
+
existing = read(cl_p) if os.path.isfile(cl_p) else ""
|
|
224
|
+
if existing and not force and not add_missing:
|
|
225
|
+
die(f"{cl_p} already exists; use --add-missing to append rows for new BRD items, or --force to rewrite", 1)
|
|
226
|
+
mapped = set(re.findall(r"\*BRD:\*\s*(BRD-\d+)", existing)) | set(re.findall(r"\((BRD-\d+)\)", existing))
|
|
227
|
+
counters = other_phase_counters(app, cl_p, below=phase if in_order else None)
|
|
228
|
+
if add_missing:
|
|
229
|
+
for m in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", existing):
|
|
230
|
+
counters[m.group(1)] = max(counters[m.group(1)], int(m.group(2)))
|
|
231
|
+
new_counts = {"UI": 0, "FN": 0, "RAG": 0, "NFR": 0}
|
|
232
|
+
|
|
233
|
+
rows, entries = [], {}
|
|
234
|
+
for it in items:
|
|
235
|
+
if add_missing and it["id"] in mapped:
|
|
236
|
+
continue
|
|
237
|
+
cls = classify(it)
|
|
238
|
+
counters[cls] += 1
|
|
239
|
+
new_counts[cls] += 1
|
|
240
|
+
req = f"REQ-{cls}-{counters[cls]:03d}"
|
|
241
|
+
rows.append(f"| {req} | {it['title']} | Not Started | 0% | — | [view](#d-{req.lower()}) |")
|
|
242
|
+
key = ("Non-functional" if cls == "NFR" else "RAG / AI requirements" if cls == "RAG"
|
|
243
|
+
else (it["screen"] or "Other"))
|
|
244
|
+
entries.setdefault(key, []).append(entry(req, it))
|
|
245
|
+
|
|
246
|
+
if add_missing and existing:
|
|
247
|
+
if not rows:
|
|
248
|
+
print(f"tf-split-brd: every BRD item already has a row in {cl_p}; nothing to add")
|
|
249
|
+
return 0
|
|
250
|
+
text = existing
|
|
251
|
+
m = re.search(r"(?ms)^## Requirements Status\s*\n.*?(?=^## |\Z)", text)
|
|
252
|
+
if not m:
|
|
253
|
+
die(f"{cl_p} has no Requirements Status section")
|
|
254
|
+
tbl_end = m.end()
|
|
255
|
+
# insert rows after the last table row of the status table
|
|
256
|
+
seg = text[m.start():tbl_end]
|
|
257
|
+
last_row = list(re.finditer(r"(?m)^\|\s*REQ-[^\n]*$", seg))
|
|
258
|
+
if last_row:
|
|
259
|
+
pos = m.start() + last_row[-1].end()
|
|
260
|
+
text = text[:pos] + "\n" + "\n".join(rows) + text[pos:]
|
|
261
|
+
for key, ents in entries.items():
|
|
262
|
+
hm = re.search(rf"(?ms)^##\s+(?:Page:\s*)?{re.escape(key)}\b[^\n]*\n.*?(?=^## |\Z)", text)
|
|
263
|
+
block = "\n".join(ents)
|
|
264
|
+
if hm:
|
|
265
|
+
text = text[:hm.end()].rstrip("\n") + "\n\n" + block + "\n" + text[hm.end():]
|
|
266
|
+
else:
|
|
267
|
+
heading = key if key in ("Non-functional", "RAG / AI requirements") else f"Page: {key}" + (f" (`{routes[key.lower()]}`)" if key.lower() in routes else "")
|
|
268
|
+
text = text.rstrip("\n") + f"\n\n## {heading}\n\n{block}\n"
|
|
269
|
+
with open(cl_p, "w", encoding="utf-8", newline="\n") as f:
|
|
270
|
+
f.write(text)
|
|
271
|
+
print(f"tf-split-brd: appended {len(rows)} row(s) to {cl_p}: " + ", ".join(r.split("|")[1].strip() for r in rows))
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
goal = re.sub(r"\s+", " ", section(brd, "Summary").strip().split("\n\n")[0]).strip() or f"Build {app} as the BRD describes."
|
|
275
|
+
title = f"# {app} — Checklist" + (f" (phase {phase})" if phase_label else "")
|
|
276
|
+
out = [title, "", "| | |", "|---|---|", f"| App | {app} |", f"| Size | {size} |"]
|
|
277
|
+
if phase_label:
|
|
278
|
+
out.append(f"| Phase | {phase_label} |")
|
|
279
|
+
out += ["", "## Goal", "", goal + (" This checklist is the whole work list of this phase." if phase_label else " This single checklist is the whole work list."), "",
|
|
280
|
+
"## Requirements Status", "",
|
|
281
|
+
"| ID | Requirement | Status | % | Remarks | Details |", "|----|-------------|--------|---|---------|---------|"]
|
|
282
|
+
out += rows
|
|
283
|
+
out += ["", STATUS_LINE, ""]
|
|
284
|
+
ordered = [s for s, _ in scr if s in entries] + [k for k in entries if k not in dict(scr) and k not in ("RAG / AI requirements", "Non-functional")]
|
|
285
|
+
for key in ordered:
|
|
286
|
+
route = routes.get(key.lower(), "")
|
|
287
|
+
out.append(f"## Page: {key}" + (f" (`{route}`)" if route else ""))
|
|
288
|
+
out.append("")
|
|
289
|
+
out.append("\n".join(entries[key]))
|
|
290
|
+
for key in ("RAG / AI requirements", "Non-functional"):
|
|
291
|
+
if key in entries:
|
|
292
|
+
out += [f"## {key}", "", "\n".join(entries[key])]
|
|
293
|
+
with open(cl_p, "w", encoding="utf-8", newline="\n") as f:
|
|
294
|
+
f.write("\n".join(out).rstrip("\n") + "\n")
|
|
295
|
+
n = {k: v for k, v in new_counts.items() if v}
|
|
296
|
+
todo = sum(1 for it in items if not it["acceptance"] and not it["nfr"])
|
|
297
|
+
print(f"tf-split-brd: wrote {cl_p}: {len(rows)} rows (" + ", ".join(f"{k} {v}" for k, v in n.items()) + f"), {len(ordered)} screen section(s)"
|
|
298
|
+
+ (f"; {todo} item(s) had no acceptance line and carry a TODO the checker will refuse" if todo else ""))
|
|
299
|
+
print("tf-split-brd: correct any class the guess got wrong, then run: bash .tfcore/utils/tf-doc-check.sh --app " + app)
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
try:
|
|
305
|
+
sys.exit(main(sys.argv))
|
|
306
|
+
except SystemExit:
|
|
307
|
+
raise
|
|
308
|
+
except Exception as e:
|
|
309
|
+
die(str(e))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-split-brd.sh — draft docs/<App>-Checklist.md from the BRD ledger.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-split-brd.sh MyApp # write it (refuses if one exists)
|
|
5
|
+
# bash .tfcore/utils/tf-split-brd.sh MyApp --force # rewrite
|
|
6
|
+
# bash .tfcore/utils/tf-split-brd.sh MyApp --add-missing # append rows for BRD items with no row (amend-docs)
|
|
7
|
+
# bash .tfcore/utils/tf-split-brd.sh MyApp --phase 2 # a later phase of a Large project (docs/MyApp-P2-BRD.md)
|
|
8
|
+
# bash .tfcore/utils/tf-split-brd.sh MyApp --all-phases # every phase listed in docs/MyApp-Phases.md
|
|
9
|
+
#
|
|
10
|
+
# Without --phase the phase is appPhase in core-config.yaml (1 when absent).
|
|
11
|
+
# REQ numbers run on across phases; an id is never reused.
|
|
12
|
+
#
|
|
13
|
+
# One row per BRD item in the template shape, acceptance lines copied verbatim,
|
|
14
|
+
# class guessed (UI / FN / RAG / NFR) for the agent to correct. Then run
|
|
15
|
+
# tf-doc-check.sh --app MyApp. Thin wrapper over tf-split-brd.py; Python 3 stdlib only.
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
19
|
+
echo "tf-split-brd: python3 is required (standard library only)." >&2
|
|
20
|
+
exit 2
|
|
21
|
+
fi
|
|
22
|
+
exec python3 "$SELF_DIR/tf-split-brd.py" "$@"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-status-evidence.sh — the evidence *refresh-status rebuilds PROJECT-STATUS from, after a
|
|
3
|
+
# session died before its status gate (Sitting 4b, 2026-09-06). Reads only; never git.
|
|
4
|
+
#
|
|
5
|
+
# bash .tfcore/utils/tf-status-evidence.sh <App> [--no-build] [--phase N]
|
|
6
|
+
#
|
|
7
|
+
# Prints, in this order:
|
|
8
|
+
# 1. what the stale PROJECT-STATUS.md claims (last_updated, current_phase, the next command)
|
|
9
|
+
# 2. files under src/ and tests/ changed after that file was last written (work the gate never saw)
|
|
10
|
+
# 3. checklist rows left at Implemented / In Progress / Needs re-verify / PARTIAL / FAIL
|
|
11
|
+
# (the fingerprint of a smoke that ran and a verify that did not)
|
|
12
|
+
# 4. the build verdict from tf-build.sh (skipped with --no-build)
|
|
13
|
+
# Then the agent decides the interruption signature and runs tf-status-facts.sh for the gate.
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
APP="${1:-}"; [[ -z "$APP" ]] && { sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 2; }
|
|
16
|
+
shift
|
|
17
|
+
BUILD=1; PHASE=""
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--no-build) BUILD=0; shift ;;
|
|
21
|
+
--phase) PHASE="$2"; shift 2 ;;
|
|
22
|
+
*) shift ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
[[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase:\s*[0-9]+' .tfcore/core-config.yaml 2>/dev/null | grep -oE '[0-9]+' || echo 1)"
|
|
26
|
+
[[ "$PHASE" -le 1 ]] && CL="docs/${APP}-Checklist.md" || CL="docs/${APP}-P${PHASE}-Checklist.md"
|
|
27
|
+
ST="PROJECT-STATUS.md"
|
|
28
|
+
|
|
29
|
+
echo "# tf-status-evidence — $APP — $(date -u +%Y-%m-%dT%H:%M:%SZ) — phase $PHASE"
|
|
30
|
+
echo
|
|
31
|
+
echo "## 1. What the stale status file claims (a hypothesis, not a fact)"
|
|
32
|
+
if [[ -f "$ST" ]]; then
|
|
33
|
+
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 '^\s*/' | sed 's/^\s*/- next: /' | head -2
|
|
35
|
+
echo "- written: $(date -u -r "$ST" +%Y-%m-%dT%H:%M:%SZ)"
|
|
36
|
+
else
|
|
37
|
+
echo "- no PROJECT-STATUS.md; treat as never written"
|
|
38
|
+
fi
|
|
39
|
+
echo
|
|
40
|
+
echo "## 2. Files changed after the status file was written (src/ and tests/, newest first)"
|
|
41
|
+
if [[ -f "$ST" ]]; then
|
|
42
|
+
mapfile -t NEWER < <(find src tests -type f -newer "$ST" \
|
|
43
|
+
-not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' -not -path '*/.artifacts/*' 2>/dev/null \
|
|
44
|
+
| xargs -r ls -t 2>/dev/null)
|
|
45
|
+
echo "- ${#NEWER[@]} file(s)"
|
|
46
|
+
printf -- '- %s\n' "${NEWER[@]:0:30}"
|
|
47
|
+
[[ ${#NEWER[@]} -gt 30 ]] && echo "- (${#NEWER[@]} in total; the rest with: find src tests -type f -newer PROJECT-STATUS.md)"
|
|
48
|
+
else
|
|
49
|
+
echo "- no status file to compare against"
|
|
50
|
+
fi
|
|
51
|
+
echo
|
|
52
|
+
echo "## 3. Checklist rows that look interrupted ($CL)"
|
|
53
|
+
if [[ -f "$CL" ]]; then
|
|
54
|
+
grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
|
|
55
|
+
| grep -iE ' — (Implemented|In Progress|Needs re-verify|PARTIAL|FAIL)' | sed 's/^/- /' | head -40
|
|
56
|
+
n=$(grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{print $4}' | grep -ciE 'Implemented|In Progress|Needs re-verify|PARTIAL|FAIL' || true)
|
|
57
|
+
[[ "$n" == "0" ]] && echo "- none: every row is terminal, Blocked or not started"
|
|
58
|
+
else
|
|
59
|
+
echo "- $CL does not exist"
|
|
60
|
+
fi
|
|
61
|
+
echo
|
|
62
|
+
echo "## 4. Build"
|
|
63
|
+
if [[ $BUILD -eq 1 ]]; then
|
|
64
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/tf-build.sh" build 2>&1 | tail -3 | sed 's/^/- /'
|
|
65
|
+
else
|
|
66
|
+
echo "- skipped (--no-build)"
|
|
67
|
+
fi
|
|
68
|
+
exit 0
|