@techierathore/techieflow 1.0.3 → 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 -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 +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- 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 +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- 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/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +2 -2
- 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/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- 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 +2 -2
- package/.tfcore/tasks/day1-greenfield.md +2 -2
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +57 -1
- package/.tfcore/telemetry/tf-metrics.sh +416 -19
- 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/html-render-shell.md +1 -1
- 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-build-list.py +23 -3
- package/.tfcore/utils/tf-build-list.sh +3 -2
- package/.tfcore/utils/tf-doc-check.py +106 -4
- package/.tfcore/utils/tf-emit.sh +353 -16
- package/.tfcore/utils/tf-fix-close.sh +34 -2
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +93 -6
- package/.tfcore/utils/tf-misses-md.py +14 -17
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-phase.sh +18 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +6 -1
- package/.tfcore/utils/tf-status-facts.py +61 -12
- package/.tfcore/utils/tf-verify-screens.mjs +85 -13
- package/.tfcore/utils/tf-verify-screens.sh +12 -3
- package/.tfcore/utils/tf-verify-tests.sh +60 -17
- package/.tfcore/utils/tf-verify-verdict.py +11 -2
- package/README.md +15 -5
- 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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!-- tf-schema
|
|
2
|
+
doc: brief
|
|
3
|
+
file: docs/{App}-Brief.md
|
|
4
|
+
header: App, Date
|
|
5
|
+
section: What it is | required | max 150
|
|
6
|
+
section: Who it is for | required | max 150
|
|
7
|
+
section: Must do | required
|
|
8
|
+
section: Out of scope | required | max 150
|
|
9
|
+
section: Open questions | optional | max 150
|
|
10
|
+
rule: brief-must-do
|
|
11
|
+
rule: no-glossary
|
|
12
|
+
target-lines: 60
|
|
13
|
+
max-lines: 120
|
|
14
|
+
-->
|
|
15
|
+
<!-- Authoring notes (agent only; never visible text).
|
|
16
|
+
|
|
17
|
+
The brief is the input to day-1: `*day1-greenfield` and `*day1-brownfield` read it
|
|
18
|
+
and turn it into the Architecture, the BRD and the mockups. Its whole job is to be
|
|
19
|
+
ONE PAGE the owner can read and correct in two minutes, before anything expensive
|
|
20
|
+
is built on it.
|
|
21
|
+
|
|
22
|
+
It replaced a 221-line inherited template (2026-09-08) whose twelve sections —
|
|
23
|
+
executive summary, problem statement, proposed solution, goals and metrics, KPIs,
|
|
24
|
+
post-MVP vision, expansion opportunities — produced a document nobody read and
|
|
25
|
+
nothing checked. What the framework's own review says a brief is: "a one-page
|
|
26
|
+
brief: product, users, must-do, out of scope". That is what this template holds.
|
|
27
|
+
|
|
28
|
+
Each "Must do" line becomes one or more BRD items at day-1, so write them as
|
|
29
|
+
things the product does, one per line, numbered. Not features to build: outcomes
|
|
30
|
+
a user gets. If a line needs a paragraph to explain it, it is two lines.
|
|
31
|
+
|
|
32
|
+
"Open questions" is where a decision the owner has not taken goes, so day-1 asks
|
|
33
|
+
rather than assumes. An empty brief with three honest open questions is worth more
|
|
34
|
+
than a full one built on guesses. -->
|
|
35
|
+
|
|
36
|
+
# {App} — Brief
|
|
37
|
+
|
|
38
|
+
| | |
|
|
39
|
+
|---|---|
|
|
40
|
+
| App | {App} |
|
|
41
|
+
| Date | {YYYY-MM-DD} |
|
|
42
|
+
|
|
43
|
+
## What it is
|
|
44
|
+
|
|
45
|
+
{Two or three sentences, at most 150 words. What the product does, in the words you
|
|
46
|
+
would use to a person who has never heard of it. No technology.}
|
|
47
|
+
|
|
48
|
+
## Who it is for
|
|
49
|
+
|
|
50
|
+
{Who uses it and what they are trying to get done. Name each role. At most 150 words.}
|
|
51
|
+
|
|
52
|
+
## Must do
|
|
53
|
+
|
|
54
|
+
1. {One thing the product does, as an outcome a user gets.}
|
|
55
|
+
2. {Another.}
|
|
56
|
+
3. {Another.}
|
|
57
|
+
|
|
58
|
+
## Out of scope
|
|
59
|
+
|
|
60
|
+
{What this is deliberately not doing, so day-1 does not build it. At most 150 words.}
|
|
61
|
+
|
|
62
|
+
## Open questions
|
|
63
|
+
|
|
64
|
+
{A decision not yet taken, one per line, so day-1 asks instead of assuming. Delete the
|
|
65
|
+
section when there are none.}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<!-- tf-schema
|
|
2
|
+
doc: decision-request
|
|
3
|
+
file: docs/{App}-Decision-Request.md
|
|
4
|
+
header: App, Written, Waiting on
|
|
5
|
+
section: What happened | required | max 250
|
|
6
|
+
section: What I need you to decide | required
|
|
7
|
+
section: What I do when you answer | required | max 200
|
|
8
|
+
section: Copy this back to me | required | max 250
|
|
9
|
+
entries: What I need you to decide |
|
|
10
|
+
per-entry: 150 300
|
|
11
|
+
rule: entry-decision-options
|
|
12
|
+
rule: entry-recommendation
|
|
13
|
+
rule: paste-back-block
|
|
14
|
+
rule: no-glossary
|
|
15
|
+
target-lines: 120
|
|
16
|
+
max-lines: 260
|
|
17
|
+
-->
|
|
18
|
+
<!-- Authoring notes (agent only; never visible text).
|
|
19
|
+
|
|
20
|
+
WHEN TO WRITE ONE. A command has hit a choice only the owner can make and the
|
|
21
|
+
answer changes what gets built. Write this file, render it to HTML, and say one
|
|
22
|
+
line in the terminal: which file to open, and that it holds N decisions. Never
|
|
23
|
+
argue the decision out in the terminal — that fills the context and buries the
|
|
24
|
+
question. Everything the owner needs is in the file.
|
|
25
|
+
|
|
26
|
+
WHO READS IT. The owner, and nobody else. No task ever loads a Decision Request
|
|
27
|
+
as input, and it is never named in core-config.yaml. The way the answer comes
|
|
28
|
+
back is the owner pasting the block from §"Copy this back to me" — that pasted
|
|
29
|
+
prompt is the input, and the file's job is done the moment it is answered.
|
|
30
|
+
|
|
31
|
+
HOW IT IS WRITTEN. Plain, simple English, the way you would explain it to
|
|
32
|
+
someone who has not read a line of the framework. No coined terms. No framework
|
|
33
|
+
shorthand. No requirement ids in the question itself — name the thing, not its
|
|
34
|
+
number. If a word needs defining, it is the wrong word: change it. A glossary
|
|
35
|
+
section is refused by `rule: no-glossary`, because a document that has to teach
|
|
36
|
+
its own vocabulary before it can ask its question was written for the wrong
|
|
37
|
+
reader.
|
|
38
|
+
|
|
39
|
+
WHAT IT IS NOT. Not a defect report — that is the feedback file. Not an
|
|
40
|
+
architecture decision record — that is DECISIONS.md, written after the fact.
|
|
41
|
+
This file asks a question and then stops existing: when it is answered, it moves
|
|
42
|
+
unchanged to docs/OldDocs/ and the live name is free for the next one.
|
|
43
|
+
|
|
44
|
+
ONE LIVE FILE. All open decisions go in this one file as separate entries. Never
|
|
45
|
+
a dated copy, never a -v2. -->
|
|
46
|
+
|
|
47
|
+
# {App} — decisions I need from you
|
|
48
|
+
|
|
49
|
+
| | |
|
|
50
|
+
|---|---|
|
|
51
|
+
| App | {App} |
|
|
52
|
+
| Written | {YYYY-MM-DD} |
|
|
53
|
+
| Waiting on | {N} decisions. Nothing has been changed yet. |
|
|
54
|
+
|
|
55
|
+
## What happened
|
|
56
|
+
|
|
57
|
+
{Two short paragraphs, at most 250 words. What was being done, what stopped it, and
|
|
58
|
+
why the choice is yours rather than something to be worked out. Plain words.}
|
|
59
|
+
|
|
60
|
+
## What I need you to decide
|
|
61
|
+
|
|
62
|
+
### {1. The short name of the choice}
|
|
63
|
+
|
|
64
|
+
{One paragraph: what the choice actually is, in plain words.}
|
|
65
|
+
|
|
66
|
+
| Option | What happens | What it costs |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| **A — {name}** | {what the project looks like afterwards} | {work, and what is given up} |
|
|
69
|
+
| **B — {name}** | {what the project looks like afterwards} | {work, and what is given up} |
|
|
70
|
+
|
|
71
|
+
**My recommendation: {A}** — {one sentence saying why.}
|
|
72
|
+
|
|
73
|
+
## What I do when you answer
|
|
74
|
+
|
|
75
|
+
{The steps, in order, one line each. What gets edited, what gets re-checked, roughly
|
|
76
|
+
how long. So the answer is given with the consequences in view.}
|
|
77
|
+
|
|
78
|
+
## Copy this back to me
|
|
79
|
+
|
|
80
|
+
{One fenced block per decision. Each block is a complete instruction that stands on
|
|
81
|
+
its own — the owner pastes it and nothing else needs saying.}
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
{App}: decision 1 — go with option A. {The one thing that changes.}
|
|
85
|
+
```
|
|
@@ -1,31 +1,86 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
<!-- tf-schema
|
|
2
|
+
doc: feedback
|
|
3
|
+
file: docs/{App}-{Upstream}-Feedback.md
|
|
4
|
+
header: App, Upstream, Updated
|
|
5
|
+
section: Summary | required | max 200
|
|
6
|
+
section: Entries | required
|
|
7
|
+
section: Replies from* | optional
|
|
8
|
+
section: Resolution status* | optional
|
|
9
|
+
entries: Entries |
|
|
10
|
+
per-entry: 120 250
|
|
11
|
+
rule: entry-feedback-fields
|
|
12
|
+
rule: entry-blocks-line
|
|
13
|
+
rule: feedback-blocking-count
|
|
14
|
+
rule: no-glossary
|
|
15
|
+
-->
|
|
16
|
+
<!-- Authoring notes (agent only; never visible text).
|
|
17
|
+
|
|
18
|
+
ONE FEEDBACK FILE PER UPSTREAM — never a combined file. Each upstream owns a
|
|
19
|
+
separate codebase and receives only its own file:
|
|
20
|
+
|
|
21
|
+
docs/{App}-TrBlazeUI-Feedback.md ids TR-NNN (the UI library)
|
|
22
|
+
docs/{App}-TechieRag-Feedback.md ids TR-RAG-NNN (the RAG library)
|
|
23
|
+
docs/{App}-TechieFlow-Feedback.md ids TF-NNN (the framework itself)
|
|
24
|
+
|
|
25
|
+
A future library {LibName} gets docs/{App}-{LibName}-Feedback.md with its own
|
|
26
|
+
prefix. Create the file on the FIRST entry for that upstream.
|
|
27
|
+
|
|
28
|
+
REPORTING IS NEVER REFUSED. If something upstream is wrong, file it — whatever
|
|
29
|
+
the severity, whatever the day. What this schema constrains is the SHAPE, not
|
|
30
|
+
the right to report:
|
|
31
|
+
|
|
32
|
+
1. Every entry says whether it BLOCKS the work, in one word. That is the field
|
|
33
|
+
the owner reads first, and it decides whether the run stops.
|
|
34
|
+
2. A non-blocking entry is filed and the work CONTINUES. Never stop, never ask,
|
|
35
|
+
never write a page of analysis about it mid-run. One entry, then carry on.
|
|
36
|
+
3. 250 words for the REPORT, hard: the eight fields, and nothing else. An entry is
|
|
37
|
+
a bug report, not an essay. Evidence goes in a code block, not in prose.
|
|
38
|
+
Working-out that is genuinely worth keeping goes under a `#### Detail` heading
|
|
39
|
+
inside the entry, and is not counted — so a thorough analysis costs nothing and
|
|
40
|
+
an unreadable entry is still refused.
|
|
41
|
+
4. Plain English. No coined terms, no framework shorthand, no word the reader
|
|
42
|
+
would have to look up. If you find yourself writing a glossary, the words
|
|
43
|
+
are wrong — change the words. `rule: no-glossary` refuses the section.
|
|
44
|
+
5. A decision for the owner does NOT go in this file. It goes in a Decision
|
|
45
|
+
Request (`app-decision-request-tmpl.md`), which is written for the owner and
|
|
46
|
+
carries the prompt to paste back.
|
|
47
|
+
|
|
48
|
+
"Replies from {Upstream}" holds the upstream team's answers, newest block first.
|
|
49
|
+
Ids are append-only and never renumbered. A fixed entry keeps its body and gains
|
|
50
|
+
a resolution line; the record of what was wrong is the point. -->
|
|
51
|
+
|
|
52
|
+
# {Upstream} feedback — found while building {App}
|
|
53
|
+
|
|
54
|
+
| | |
|
|
55
|
+
|---|---|
|
|
56
|
+
| App | {App} |
|
|
57
|
+
| Upstream | {Upstream} |
|
|
58
|
+
| Updated | {YYYY-MM-DD} |
|
|
59
|
+
|
|
60
|
+
## Summary
|
|
61
|
+
|
|
62
|
+
{N} entries: {n} blocking now, {n} filed and not blocking, {n} fixed upstream.
|
|
63
|
+
|
|
64
|
+
{One sentence naming what is blocked right now, or the sentence "Nothing is blocked."}
|
|
65
|
+
|
|
66
|
+
## Entries
|
|
67
|
+
|
|
68
|
+
### {TF-001} — {one line saying what is wrong}
|
|
69
|
+
|
|
70
|
+
- **Severity:** blocker | major | minor
|
|
71
|
+
- **Blocks:** yes — {the row or command that cannot proceed} | no — {what was done instead, and the work carried on}
|
|
72
|
+
- **Repro:** {the shortest command or snippet that shows it}
|
|
73
|
+
- **Expected:** {what should have happened}
|
|
74
|
+
- **Actual:** {what happened}
|
|
75
|
+
- **Encountered in:** {REQ-UI-013, or the command that was running}
|
|
76
|
+
- **Workaround:** {what was done in this project, or "none"}
|
|
77
|
+
- **Suggested fix:** {one concrete change for the upstream team}
|
|
78
|
+
|
|
79
|
+
#### Detail
|
|
80
|
+
|
|
81
|
+
{Optional, and not counted against the 250 words: the root cause, the reasoning, the
|
|
82
|
+
longer version of the fix. Everything above must stand on its own without it.}
|
|
83
|
+
|
|
84
|
+
## Replies from {Upstream}
|
|
85
|
+
|
|
86
|
+
<!-- The upstream team's answers, newest block first. Left in full: this is the record. -->
|
|
@@ -23,7 +23,7 @@ Each rendered HTML file is:
|
|
|
23
23
|
- A **single self-contained `.html` file**, no external CSS, no external JS except the two `<script>` tags below (mermaid + svg-pan-zoom from jsdelivr CDN).
|
|
24
24
|
- Sibling to the source MD (same filename, `.html` extension).
|
|
25
25
|
- **Light + dark theme with a persisted toggle** (§2 + §3 + §7). Default is chosen by **time of day** on first open — light between 07:00 and 19:00 local, softened dark otherwise — then the user's explicit toggle choice (saved in `localStorage` as `tf-theme`) wins on every later visit. Light is a **warm off-white** (never harsh bright white); dark is a softened near-black. Monospace for code/pre, sans for body.
|
|
26
|
-
- **Only human-readable docs are ever rendered to HTML** (BRD, Architecture, UsageGuide, DevGuide, PROJECT-STATUS).
|
|
26
|
+
- **Only human-readable docs are ever rendered to HTML** (BRD, Architecture, UsageGuide, DevGuide, ProductGuide, PROJECT-STATUS, a Decision Request, a feedback file). **An agent working document is never rendered to HTML** — it is read in markdown, and an HTML mirror wastes tokens and drifts from the source. Two are banned by name, and `tf-render-html.py` refuses both with exit 2: the **requirements checklist** `{App}-Checklist.md`, and the **miss log** `{App}-Misses.md`. If you are about to emit a `*-Checklist.html` or a `*-Misses.html`, STOP — that is banned. The test for anything else: if the only reader is an agent, it stays markdown.
|
|
27
27
|
- Working anchor links from any auto-generated or hand-written TOC to its target heading.
|
|
28
28
|
- Copy buttons on every **code** `<pre>` — but NOT on `pre.mermaid` (a button injected there corrupts the diagram source mermaid reads → "Syntax error"; mermaid blocks get copy/export from their own diagram toolbar).
|
|
29
29
|
- Mermaid toolbar on every rendered diagram.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
bash .tfcore/utils/tf-build-list.sh <App> [--phase N] [--prompts]
|
|
5
5
|
|
|
6
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
|
|
8
|
-
|
|
7
|
+
- the mode: FIX (rows FAIL / PARTIAL / In Progress / Needs re-verify exist, and they
|
|
8
|
+
come FIRST in the list), FRESH (nothing failing), or NOTHING (every row terminal or
|
|
9
|
+
Blocked). The working list is every open row in both FIX and FRESH: the mode says
|
|
10
|
+
what to build first, never what to leave out.
|
|
9
11
|
- the working list, grouped into clusters by the checklist's page section, each
|
|
10
12
|
naming its builder from the row prefix: REQ-UI -> trblazeui, REQ-RAG -> techierag,
|
|
11
13
|
REQ-FN / REQ-NFR -> builder
|
|
@@ -113,8 +115,17 @@ def main(argv):
|
|
|
113
115
|
|
|
114
116
|
fix_rows = [r for r in rows if r["status"] in FIX]
|
|
115
117
|
open_rows = [r for r in rows if r["status"] not in TERMINAL | BLOCKED]
|
|
118
|
+
# Repair before new work, but never INSTEAD of it. A row can sit at `Needs re-verify`
|
|
119
|
+
# permanently -- TfLens REQ-FN-067 and -070 need a repository that emits events.ndjson
|
|
120
|
+
# and none exists, so every honest verify run re-confirms that gate rather than clearing
|
|
121
|
+
# it. FIX used to win outright, which pinned the mode forever: the twelve rows a later
|
|
122
|
+
# *amend-docs added were never once printed, and a pass that trusted the working list
|
|
123
|
+
# would have met build-phase's ending condition with twelve requirements at 0%.
|
|
124
|
+
# So the working list is every open row, and FIX only says which ones come first.
|
|
125
|
+
# tests/regression/run.sh tf_019.
|
|
116
126
|
if fix_rows:
|
|
117
|
-
|
|
127
|
+
rest = [r for r in open_rows if r not in fix_rows]
|
|
128
|
+
mode, work = "FIX", fix_rows + rest
|
|
118
129
|
elif open_rows:
|
|
119
130
|
mode, work = "FRESH", open_rows
|
|
120
131
|
else:
|
|
@@ -124,6 +135,15 @@ def main(argv):
|
|
|
124
135
|
print(f"# tf-build-list — {app} — {cl} — phase {phase}")
|
|
125
136
|
print(f"Mode: {mode} — {len(work)} row(s) to build; {terminal} terminal, {len(blocked)} Blocked, {len(rows)} total"
|
|
126
137
|
+ (f" — {', '.join(r['id'] for r in work[:8])}{' …' if len(work) > 8 else ''}" if work else ""))
|
|
138
|
+
# Every row is in exactly one of the four counts, and the arithmetic is printed so a
|
|
139
|
+
# reader can check it rather than trust it. A row missing from all of them is the
|
|
140
|
+
# failure this line exists to make impossible.
|
|
141
|
+
if len(work) + terminal + len(blocked) != len(rows):
|
|
142
|
+
print(f"⚠ {len(rows) - len(work) - terminal - len(blocked)} row(s) are in no category — "
|
|
143
|
+
"the checklist carries a status this script does not know; fix the row or the script")
|
|
144
|
+
if fix_rows and len(work) > len(fix_rows):
|
|
145
|
+
print(f"Order: {len(fix_rows)} failing row(s) first ({', '.join(r['id'] for r in fix_rows)}), "
|
|
146
|
+
f"then {len(work) - len(fix_rows)} not yet started")
|
|
127
147
|
if blocked:
|
|
128
148
|
print("Blocked (library gaps, pass through unless the feedback entry is resolved): " + ", ".join(r["id"] for r in blocked))
|
|
129
149
|
if not work:
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
# bash .tfcore/utils/tf-build-list.sh MyApp --phase 2 # a later phase of a Large project
|
|
7
7
|
#
|
|
8
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
|
|
10
|
-
# open row
|
|
9
|
+
# PARTIAL / In Progress / Needs re-verify make it FIX mode, which puts those rows FIRST and
|
|
10
|
+
# then lists every other open row; FRESH is the same list when nothing is failing; Blocked rows
|
|
11
|
+
# pass through. Clusters follow the checklist's page sections,
|
|
11
12
|
# each with its builder from the prefix (UI -> trblazeui, RAG -> techierag, FN/NFR -> builder).
|
|
12
13
|
# Prompts are filled from .tfcore/templates/v4custom/build-subagent-prompt.md.
|
|
13
14
|
# Writes nothing. Thin wrapper over tf-build-list.py; Python 3 stdlib only.
|
|
@@ -52,12 +52,20 @@ DOC_KINDS = [
|
|
|
52
52
|
("-devguide.md", "devguide", "app-devguide-tmpl.md"),
|
|
53
53
|
("-productguide.md", "productguide", "app-productguide-tmpl.md"),
|
|
54
54
|
("-phases.md", "phases", "app-phases-tmpl.md"),
|
|
55
|
+
("-brief.md", "brief", "app-brief-tmpl.md"),
|
|
56
|
+
("-decision-request.md", "decision-request", "app-decision-request-tmpl.md"),
|
|
57
|
+
("-feedback.md", "feedback", "app-library-feedback-tmpl.md"),
|
|
55
58
|
]
|
|
56
59
|
SIZED_DOCS = {"brd", "architecture", "uidesign", "checklist", "usageguide", "devguide", "productguide"}
|
|
57
60
|
# Large projects: these four split by phase. Phase 1 keeps the plain name (App-BRD.md);
|
|
58
61
|
# phase 2 onward are App-P2-BRD.md and so on. docs/TechieFlow-Document-Schemas.md §2.
|
|
59
62
|
PHASED_DOCS = {"brd", "checklist", "uidesign", "devguide"}
|
|
60
63
|
PHASED_SUFFIX = {"brd": "BRD", "checklist": "Checklist", "uidesign": "UIDesign", "devguide": "DevGuide"}
|
|
64
|
+
# A feedback entry's heading opens with its id: TF-016, TR-010, TR-RAG-002.
|
|
65
|
+
ENTRY_ID = re.compile(r"^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+\b")
|
|
66
|
+
# A closed entry carries its resolution banner near the top of its body. The banner is not
|
|
67
|
+
# always first: an entry may open with a renumbering or scope note, so the window is generous.
|
|
68
|
+
FEEDBACK_CLOSED = re.compile(r"(?i)(✅|\bfixed upstream\b|\bwill not fix\b|\bwont-fix\b|is \*\*closed\*\*)")
|
|
61
69
|
DOC_NAME = re.compile(
|
|
62
70
|
r"^(.+?)(?:-P(\d+))?-(BRD|Deployment-Checklist(?:-[\w]+)?|Checklist|UIDesign|DevGuide|Architecture|Coding-Standards|UsageGuide|Usage-Guide|ProductGuide|Phases)\.md$",
|
|
63
71
|
re.I)
|
|
@@ -429,6 +437,9 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
|
|
|
429
437
|
continue
|
|
430
438
|
if doc == "checklist" and re.search(r"<a id=['\"]d-req-", txt, re.I):
|
|
431
439
|
continue # a page group holding detail entries
|
|
440
|
+
if doc == "feedback" and ENTRY_ID.match(h.strip()):
|
|
441
|
+
continue # an entry written at ## level: the layout every feedback file shipped
|
|
442
|
+
# with before this schema existed. Read as an entry, not a stranger.
|
|
432
443
|
extra = "; bugs and feedback go to the misses stream, not the checklist" if doc == "checklist" else ""
|
|
433
444
|
rep.fail(rel, f'section "{h}" is not in the template{extra}')
|
|
434
445
|
|
|
@@ -438,6 +449,9 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
|
|
|
438
449
|
d = schema.declared(key)
|
|
439
450
|
if not d:
|
|
440
451
|
continue
|
|
452
|
+
if doc == "feedback" and d[1] != "required":
|
|
453
|
+
continue # correspondence blocks are appended over time and sit either side
|
|
454
|
+
# of the entries; where they land carries no meaning
|
|
441
455
|
idx = order_of[norm_heading(d[0])]
|
|
442
456
|
if idx < last_idx:
|
|
443
457
|
rep.fail(rel, f'section "{h}" comes after "{last_name}"; the template order is the other way round')
|
|
@@ -464,7 +478,9 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
|
|
|
464
478
|
for key, h, txt in present:
|
|
465
479
|
d = schema.declared(key)
|
|
466
480
|
if d and d[2]:
|
|
467
|
-
|
|
481
|
+
# A `####` block inside a section is detail the reader chooses to open, and is not
|
|
482
|
+
# counted — the same rule as a feedback entry's `#### Detail` (owner, 2026-09-08).
|
|
483
|
+
n = word_count(re.split(r"(?m)^####\s", txt)[0])
|
|
468
484
|
if n > d[2]:
|
|
469
485
|
rep.fail(rel, f'section "{h}" is {n} words; at most {d[2]}')
|
|
470
486
|
if schema.max_lines:
|
|
@@ -479,8 +495,14 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
|
|
|
479
495
|
if schema.entries:
|
|
480
496
|
sec_txt = section_text(present, schema.entries[0])
|
|
481
497
|
prefix = schema.entries[1]
|
|
498
|
+
pairs = list(split_sections(sec_txt, 3)) if sec_txt is not None else []
|
|
499
|
+
if doc == "feedback":
|
|
500
|
+
# Entries may sit under "Entries" as ###, or stand at ## level as every
|
|
501
|
+
# feedback file wrote them before this schema. Both are read the same way.
|
|
502
|
+
pairs += [(h, t) for _k, h, t in present if ENTRY_ID.match(h.strip())]
|
|
503
|
+
sec_txt = sec_txt if sec_txt is not None else ""
|
|
482
504
|
if sec_txt is not None:
|
|
483
|
-
for h3, etxt in
|
|
505
|
+
for h3, etxt in pairs:
|
|
484
506
|
if h3 is None:
|
|
485
507
|
continue
|
|
486
508
|
if prefix and not h3.lower().startswith(prefix.lower()):
|
|
@@ -489,8 +511,16 @@ def check_document(path: str, rep: Report, cli_size=None, root=None):
|
|
|
489
511
|
ename = h3[len(prefix):].strip() if prefix else h3
|
|
490
512
|
ename = re.sub(r"\s*\(.*$", "", ename).strip("` ").strip()
|
|
491
513
|
entry_names.append(ename)
|
|
514
|
+
if doc == "feedback" and FEEDBACK_CLOSED.search(etxt[:2500]):
|
|
515
|
+
continue # a closed entry is the record of what was wrong and why.
|
|
516
|
+
# History may be as long as it needs to be; the shape and the
|
|
517
|
+
# word cap bind the LIVE ones, which are what the owner reads.
|
|
492
518
|
if schema.per_entry:
|
|
493
|
-
|
|
519
|
+
# The cap binds the REPORT, not the working-out. Anything under a `####`
|
|
520
|
+
# heading inside the entry is detail a reader chooses to open, and is not
|
|
521
|
+
# counted: a thorough analysis is worth having, an entry the owner cannot
|
|
522
|
+
# read at a glance is not (owner, 2026-09-08).
|
|
523
|
+
n = word_count(re.split(r"(?m)^####\s", etxt)[0])
|
|
494
524
|
if n > schema.per_entry[1]:
|
|
495
525
|
rep.fail(rel, f'entry "{ename}" is {n} words; maximum {schema.per_entry[1]} (target {schema.per_entry[0]})')
|
|
496
526
|
elif n > schema.per_entry[0]:
|
|
@@ -549,6 +579,29 @@ def check_entry_rule(rule, ename, etxt, rel, root, path, rep):
|
|
|
549
579
|
elif rule == "entry-expected":
|
|
550
580
|
if not re.search(r"(?im)^\s*[-*]?\s*\**expected", etxt):
|
|
551
581
|
rep.fail(rel, f'entry "{ename}" has no "Expected:" line')
|
|
582
|
+
elif rule == "entry-feedback-fields":
|
|
583
|
+
# A feedback entry is a bug report, not an essay. Same eight fields every time,
|
|
584
|
+
# so the upstream team reads them in one shape and the owner reads "Blocks" first.
|
|
585
|
+
for field in ("Severity", "Blocks", "Repro", "Expected", "Actual",
|
|
586
|
+
"Encountered in", "Workaround", "Suggested fix"):
|
|
587
|
+
if not re.search(r"(?im)^\s*[-*]?\s*\**%s\**\s*:" % re.escape(field), etxt):
|
|
588
|
+
rep.fail(rel, f'entry "{ename}" has no "{field}:" line')
|
|
589
|
+
elif rule == "entry-blocks-line":
|
|
590
|
+
# The one word that decides whether the run stops. "no" also has to say what was
|
|
591
|
+
# done instead, because a non-blocking entry is filed and the work carries on.
|
|
592
|
+
m = re.search(r"(?im)^\s*[-*]?\s*\**Blocks\**\s*:\s*(.+)$", etxt)
|
|
593
|
+
if m:
|
|
594
|
+
val = m.group(1).strip().strip("*` ").lower()
|
|
595
|
+
if not re.match(r"^(yes|no)\b", val):
|
|
596
|
+
rep.fail(rel, f'entry "{ename}" says "Blocks: {m.group(1).strip()[:40]}"; it must start "yes" or "no"')
|
|
597
|
+
elif val.startswith("no") and len(val) < 8:
|
|
598
|
+
rep.fail(rel, f'entry "{ename}" says "Blocks: no" and stops; say what was done instead and that the work carried on')
|
|
599
|
+
elif rule == "entry-decision-options":
|
|
600
|
+
if not has_table_with(etxt, "option", "what happens"):
|
|
601
|
+
rep.fail(rel, f'decision "{ename}" has no options table (Option, What happens, What it costs)')
|
|
602
|
+
elif rule == "entry-recommendation":
|
|
603
|
+
if not re.search(r"(?im)^\s*\**My recommendation", etxt):
|
|
604
|
+
rep.fail(rel, f'decision "{ename}" has no "My recommendation:" line — the owner is owed one, with the reason in a sentence')
|
|
552
605
|
|
|
553
606
|
|
|
554
607
|
def check_doc_rule(rule, c, rep):
|
|
@@ -782,6 +835,50 @@ def check_doc_rule(rule, c, rep):
|
|
|
782
835
|
if re.search(r"\b(and|or)\b|,|/", tgt):
|
|
783
836
|
rep.fail(rel, f'Hosting target "{tgt}" names more than one target; one document per hosting target')
|
|
784
837
|
|
|
838
|
+
elif rule == "brief-must-do":
|
|
839
|
+
txt = section_text(present, "Must do")
|
|
840
|
+
if txt is None:
|
|
841
|
+
return
|
|
842
|
+
n = len(re.findall(r"(?m)^\s*\d+[.)]\s+\S", txt))
|
|
843
|
+
if n == 0:
|
|
844
|
+
rep.fail(rel, 'the "Must do" section is not a numbered list; day-1 turns each line into requirements, so one line is one thing the product does')
|
|
845
|
+
elif n > 25:
|
|
846
|
+
rep.fail(rel, f'the "Must do" section has {n} lines; a brief that long is a BRD. Keep the page, and let day-1 expand it')
|
|
847
|
+
|
|
848
|
+
elif rule == "no-glossary":
|
|
849
|
+
# A document written for the owner explains itself in plain words or it is
|
|
850
|
+
# written wrong. A section that defines the document's own vocabulary before it
|
|
851
|
+
# can ask its question is the tell: the fix is to change the words, never to
|
|
852
|
+
# teach them (owner, 2026-09-08).
|
|
853
|
+
for _key, h, _txt in present:
|
|
854
|
+
if re.search(r"(?i)\b(glossary|terminology|words this document uses|"
|
|
855
|
+
r"terms? (used|you)|what these words mean|definitions)\b", h):
|
|
856
|
+
rep.fail(rel, f'section "{h}" defines this document\'s own vocabulary; '
|
|
857
|
+
f'write it in plain English instead — if a word needs a glossary, use a different word')
|
|
858
|
+
|
|
859
|
+
elif rule == "feedback-blocking-count":
|
|
860
|
+
txt = section_text(present, "Summary")
|
|
861
|
+
if txt is None:
|
|
862
|
+
return
|
|
863
|
+
if not re.search(r"(?i)\bblocking\b|\bnothing is blocked\b", txt):
|
|
864
|
+
rep.fail(rel, 'the Summary must say how many entries are blocking right now, '
|
|
865
|
+
'or "Nothing is blocked" — it is the first thing the owner reads')
|
|
866
|
+
|
|
867
|
+
elif rule == "paste-back-block":
|
|
868
|
+
# `present` is built from the prose-only copy, which drops fenced blocks — and a
|
|
869
|
+
# fenced block is exactly what this rule looks for. Re-split the uncommented source.
|
|
870
|
+
raw_present = [(norm_heading(h), h, t) for h, t in split_sections(nocomment, 2) if h is not None]
|
|
871
|
+
txt = section_text(raw_present, "Copy this back to me")
|
|
872
|
+
if txt is None:
|
|
873
|
+
return
|
|
874
|
+
blocks = re.findall(r"^```", txt, re.M)
|
|
875
|
+
if len(blocks) < 2:
|
|
876
|
+
rep.fail(rel, 'the "Copy this back to me" section has no fenced block; '
|
|
877
|
+
'the owner answers by pasting it, so it must be there to paste')
|
|
878
|
+
n_dec = len(c.get("entry_names") or [])
|
|
879
|
+
if n_dec and len(blocks) // 2 < n_dec:
|
|
880
|
+
rep.fail(rel, f'{n_dec} decisions but {len(blocks) // 2} paste-back block(s); one per decision')
|
|
881
|
+
|
|
785
882
|
elif rule == "phases-table":
|
|
786
883
|
txt = section_text(present, "Phases")
|
|
787
884
|
if txt is None:
|
|
@@ -1148,7 +1245,7 @@ def check_mockups(root: str, rep: Report):
|
|
|
1148
1245
|
# ----------------------------------------------------------------------------
|
|
1149
1246
|
def app_files(root: str, app: str):
|
|
1150
1247
|
docs = os.path.join(root, "docs")
|
|
1151
|
-
names = [f"{app}-Phases.md", f"{app}-BRD.md", f"{app}-Architecture.md", f"{app}-UIDesign.md", f"{app}-Checklist.md",
|
|
1248
|
+
names = [f"{app}-Brief.md", f"{app}-Phases.md", f"{app}-BRD.md", f"{app}-Architecture.md", f"{app}-UIDesign.md", f"{app}-Checklist.md",
|
|
1152
1249
|
f"{app}-Coding-Standards.md", f"{app}-UsageGuide.md", f"{app}-DevGuide.md", f"{app}-ProductGuide.md"]
|
|
1153
1250
|
# later phases of a Large project: App-P2-BRD.md, App-P2-Checklist.md, … in phase order
|
|
1154
1251
|
extra = []
|
|
@@ -1160,6 +1257,11 @@ def app_files(root: str, app: str):
|
|
|
1160
1257
|
names += [f for _p, _o, f in sorted(extra)]
|
|
1161
1258
|
if os.path.isdir(docs):
|
|
1162
1259
|
names += sorted(f for f in os.listdir(docs) if re.match(rf"^{re.escape(app)}-Deployment-Checklist(-[\w]+)?\.md$", f))
|
|
1260
|
+
# the two documents written for a reader outside this project: the owner's open
|
|
1261
|
+
# decisions, and whatever this project has reported upstream (one file per upstream)
|
|
1262
|
+
names += sorted(f for f in os.listdir(docs)
|
|
1263
|
+
if re.match(rf"^{re.escape(app)}-Decision-Request\.md$", f)
|
|
1264
|
+
or re.match(rf"^{re.escape(app)}-[\w.]+-Feedback\.md$", f))
|
|
1163
1265
|
out = [os.path.join(docs, n) for n in names if os.path.exists(os.path.join(docs, n))]
|
|
1164
1266
|
ps = os.path.join(root, "PROJECT-STATUS.md")
|
|
1165
1267
|
if os.path.exists(ps):
|