@techierathore/techieflow 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +1 -1
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.opencode/plugin/techieflow.js +179 -7
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +1 -1
- package/.tfcore/tasks/day1-greenfield.md +1 -1
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/telemetry/SCHEMA.md +4 -0
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -2
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +80 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +136 -6
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +92 -35
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +79 -29
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +15 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +60 -2
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +72 -32
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +171 -32
- package/.tfcore/utils/tf-verify-screens.sh +8 -3
- package/.tfcore/utils/tf-verify-tests.sh +215 -14
- package/.tfcore/utils/tf-verify-verdict.py +48 -9
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +4 -0
- package/docs/TechieFlow-Installation.md +5 -3
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!-- tf-schema
|
|
2
|
+
doc: brd
|
|
3
|
+
file: docs/{App}-P{n}-BRD.md
|
|
4
|
+
header: App, Kind, Size, Status, Date
|
|
5
|
+
section: Summary | required | max 200
|
|
6
|
+
section: Screens and flow | required
|
|
7
|
+
section: Requirements | required
|
|
8
|
+
section: Non-functional requirements | optional
|
|
9
|
+
section: Development status | required
|
|
10
|
+
section: Where the rest lives | required
|
|
11
|
+
budget: S 6000 8000 | M 10000 15000 | L 10000 15000
|
|
12
|
+
rule: brd-ledger
|
|
13
|
+
rule: screens-table
|
|
14
|
+
rule: mockup-links
|
|
15
|
+
rule: phase-pointer
|
|
16
|
+
-->
|
|
17
|
+
<!-- Authoring notes (agent only; never visible text).
|
|
18
|
+
A phase BRD is phase 2 onward of a Large project. Phase 1 is docs/{App}-BRD.md, written from
|
|
19
|
+
app-brd-tmpl.md, and it holds everything that belongs to the whole application: Scope, Users and
|
|
20
|
+
roles, the Non-functional requirements that apply everywhere, the Context diagram, Constraints and
|
|
21
|
+
assumptions, and Risks. A phase BRD never repeats them: two copies of the same fact become two
|
|
22
|
+
answers the day one of them is amended (TfLens TF-024, 2026-09-11). It holds this phase's screens,
|
|
23
|
+
this phase's requirements, and a "Where the rest lives" table that links back to phase 1.
|
|
24
|
+
"Non-functional requirements" is here only when this phase adds its own, for example a speed
|
|
25
|
+
budget on one of its screens; its ids run on like every other item.
|
|
26
|
+
The header carries "| Phase | n of m |" (the checker refuses the file without it). Size is the
|
|
27
|
+
phase's own size, Small or Medium. BRD-N runs on from the previous phase's last id and is never
|
|
28
|
+
renumbered. Sections and order are fixed by the schema above; the order is the whole-project
|
|
29
|
+
BRD's, with the sections a phase does not carry left out. -->
|
|
30
|
+
|
|
31
|
+
# {App} — Business Requirements — Phase {n}: {Phase name}
|
|
32
|
+
|
|
33
|
+
| | |
|
|
34
|
+
|---|---|
|
|
35
|
+
| App | {App} |
|
|
36
|
+
| Kind | app or library |
|
|
37
|
+
| Size | Small or Medium (this phase on its own) |
|
|
38
|
+
| Phase | {n} of {m} |
|
|
39
|
+
| Status | Draft, Approved |
|
|
40
|
+
| Date | {YYYY-MM-DD} |
|
|
41
|
+
|
|
42
|
+
## 1. Summary
|
|
43
|
+
|
|
44
|
+
{What this phase adds, for whom, and why it comes at this point. At most 200 words.}
|
|
45
|
+
|
|
46
|
+
## 2. Screens and flow
|
|
47
|
+
|
|
48
|
+
One row per routed page this phase adds; dialogs are written exactly as in the phase-1 BRD.
|
|
49
|
+
|
|
50
|
+
| Screen | Route | Role | Mockup | Fields |
|
|
51
|
+
|---|---|---|---|---|
|
|
52
|
+
| {Screen name} | `/route` | {Role} | [mockup](mockups/{screen-slug}.html) | {field, field, field} |
|
|
53
|
+
|
|
54
|
+
**Primary journey:**
|
|
55
|
+
1. {The user opens … and …}
|
|
56
|
+
|
|
57
|
+
## 3. Requirements
|
|
58
|
+
|
|
59
|
+
One item per thing the verifier will test, grouped under a `###` heading per screen. Same rules as the phase-1 BRD: an acceptance line is at most 30 words, target 20, and holds one behaviour.
|
|
60
|
+
|
|
61
|
+
### {Screen name}
|
|
62
|
+
|
|
63
|
+
{One plain sentence: what this screen does, for whom.}
|
|
64
|
+
|
|
65
|
+
- **BRD-{N}** — {Title}. *Screen:* {Screen name} · *Mockup:* [mockup](mockups/{screen-slug}.html)
|
|
66
|
+
- *Acceptance:* When {actor} {does what} on {screen}, then {a result a browser robot can observe}.
|
|
67
|
+
|
|
68
|
+
## 4. Non-functional requirements
|
|
69
|
+
|
|
70
|
+
Only what this phase adds. The ones that apply to the whole application are in the phase-1 BRD.
|
|
71
|
+
|
|
72
|
+
| Id | Area | Requirement | Measure |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| BRD-{N} | Performance | {…} | perf-budget: p95 load <= 2000ms @ concurrency 1 |
|
|
75
|
+
|
|
76
|
+
## 5. Development status
|
|
77
|
+
|
|
78
|
+
Written by the status gate after every build, verify and handoff; not by hand.
|
|
79
|
+
|
|
80
|
+
**Snapshot as of {YYYY-MM-DD}.** Live per-requirement status: `PROJECT-STATUS.md` and the Requirements Status table in `docs/{App}-P{n}-Checklist.md`.
|
|
81
|
+
|
|
82
|
+
| Screen | Requirements | Verified | Open | Status |
|
|
83
|
+
|---|---|---|---|---|
|
|
84
|
+
| {Screen} | {n} | {n} | {n} | Planned, In progress, Partial, Done |
|
|
85
|
+
|
|
86
|
+
## 6. Where the rest lives
|
|
87
|
+
|
|
88
|
+
| What | Where |
|
|
89
|
+
|---|---|
|
|
90
|
+
| Scope, users and roles, the context diagram | [phase 1 BRD]({App}-BRD.md) |
|
|
91
|
+
| Non-functional requirements for the whole application | [phase 1 BRD]({App}-BRD.md) |
|
|
92
|
+
| Constraints, assumptions and risks | [phase 1 BRD]({App}-BRD.md) |
|
|
93
|
+
| Every phase, its screens and its BRD range | [{App}-Phases.md]({App}-Phases.md) |
|
|
94
|
+
| This phase's work list | [{App}-P{n}-Checklist.md]({App}-P{n}-Checklist.md) |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!-- tf-schema
|
|
2
|
+
doc: uidesign
|
|
3
|
+
file: docs/{App}-P{n}-UIDesign.md
|
|
4
|
+
header: App, Kind, Size
|
|
5
|
+
section: Screens | required
|
|
6
|
+
section: Where the rest lives | required
|
|
7
|
+
entries: Screens | Screen:
|
|
8
|
+
per-entry: 250 400
|
|
9
|
+
rule: entry-mockup
|
|
10
|
+
rule: entry-regions-table
|
|
11
|
+
rule: entry-fields-table
|
|
12
|
+
rule: entry-states
|
|
13
|
+
rule: mockup-links
|
|
14
|
+
rule: phase-pointer
|
|
15
|
+
-->
|
|
16
|
+
<!-- Authoring notes (agent only; never visible text).
|
|
17
|
+
A phase UIDesign is phase 2 onward of a Large project. Phase 1 is docs/{App}-UIDesign.md, written
|
|
18
|
+
from app-uidesign-tmpl.md, and it holds everything the whole application shares: the UI library
|
|
19
|
+
and theme in its header, the Design system, the one Click-through flow across every phase (the
|
|
20
|
+
mockup folder is one set), and the Branding guide. A phase UIDesign never repeats them — two
|
|
21
|
+
copies become two answers the day one changes (owner, 2026-09-11, after TfLens TF-024). It holds
|
|
22
|
+
one `### Screen: Name (/route)` per screen in this phase's row of docs/{App}-Phases.md, written
|
|
23
|
+
exactly as in phase 1, and a "Where the rest lives" table linking back to the phase-1 UIDesign.
|
|
24
|
+
A new screen that changes the flow is added to phase 1's Click-through flow, not drawn here.
|
|
25
|
+
The header carries "| Phase | n of m |" (the checker refuses the file without it). -->
|
|
26
|
+
|
|
27
|
+
# {App} — UI Design — Phase {n}: {Phase name}
|
|
28
|
+
|
|
29
|
+
| | |
|
|
30
|
+
|---|---|
|
|
31
|
+
| App | {App} |
|
|
32
|
+
| Kind | app or library |
|
|
33
|
+
| Size | Small or Medium (this phase on its own) |
|
|
34
|
+
| Phase | {n} of {m} |
|
|
35
|
+
|
|
36
|
+
## Screens
|
|
37
|
+
|
|
38
|
+
### Screen: {Name} (`/route`)
|
|
39
|
+
|
|
40
|
+
**Mockup:** [mockups/{screen-slug}.html](mockups/{screen-slug}.html) · **Roles:** {who reaches it} · **BRD:** BRD-{N}
|
|
41
|
+
|
|
42
|
+
| Region | Control | Shows or binds |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| {Main list} | {control} | {…} |
|
|
45
|
+
|
|
46
|
+
| Field | Type | Required | Validation |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| {Title} | text | yes | {1 to 120 characters} |
|
|
49
|
+
|
|
50
|
+
**Dialogs opened here:** {Dialog name: fields …; or "none"}
|
|
51
|
+
|
|
52
|
+
**States:** empty: {…} · loading: {…} · error: {…}
|
|
53
|
+
|
|
54
|
+
## Where the rest lives
|
|
55
|
+
|
|
56
|
+
| What | Where |
|
|
57
|
+
|---|---|
|
|
58
|
+
| UI library, theme and the design system | [phase 1 UI design]({App}-UIDesign.md) |
|
|
59
|
+
| The click-through flow across every phase | [phase 1 UI design]({App}-UIDesign.md) |
|
|
60
|
+
| This phase's requirements | [{App}-P{n}-BRD.md]({App}-P{n}-BRD.md) |
|
|
@@ -21,8 +21,10 @@ rule: mockup-links
|
|
|
21
21
|
already shows. Use only controls that exist in the UI library's catalogue; a missing control goes
|
|
22
22
|
to the library feedback file, not into a workaround. The set of screens must equal the BRD's
|
|
23
23
|
Screens and flow table. For a library, this document is optional.
|
|
24
|
-
Large project: one UIDesign per phase
|
|
25
|
-
|
|
24
|
+
Large project: one UIDesign per phase with a header row "| Phase | n of m |"; its screens are exactly
|
|
25
|
+
that phase's row in docs/{App}-Phases.md. Phase 1 is this template and keeps what every phase shares
|
|
26
|
+
(library, theme, design system, the one click-through flow, branding); phase n is
|
|
27
|
+
docs/{App}-Pn-UIDesign.md from app-phase-uidesign-tmpl.md, screens only, linking back here.
|
|
26
28
|
The mockup folder stays one click-through set across every phase. -->
|
|
27
29
|
|
|
28
30
|
# {App} — UI Design
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- build-subagent-prompt.md — the prompt build-phase gives every cluster sub-agent.
|
|
2
|
-
Filled by tf-build-list.sh --prompts: {App} {Cluster} {Builder} {Section} {Rows} {Mockups} {Checklist}.
|
|
2
|
+
Filled by tf-build-list.sh --prompts: {App} {Cluster} {Builder} {Section} {Rows} {Mockups} {Checklist} {UsageGuide}.
|
|
3
3
|
Library agents (trblazeui, techierag) never read .tfcore/ tasks, so the two standing rules
|
|
4
4
|
reach them only through this text. Keep it short; the rows carry the detail. -->
|
|
5
5
|
You are building cluster {Cluster} of {App} ({Section}) as the {Builder}. Implement exactly these rows, nothing else:
|
|
@@ -10,7 +10,7 @@ Read first: `docs/{App}-Coding-Standards.md` (and the standard files it names un
|
|
|
10
10
|
|
|
11
11
|
Two standing rules:
|
|
12
12
|
1. Git is manual. Never run `git` or `gh` for any purpose. Record your row ids in the checklist Remarks (`[REQ-…]`), not in commits.
|
|
13
|
-
2. Smoke it yourself before you return: `bash .tfcore/utils/tf-build.sh` must PASS, then boot the app and open every screen you touched with headless Playwright (or the native driver the ladder names); the data must show and nothing may overlap; use a test user from `
|
|
13
|
+
2. Smoke it yourself before you return: `bash .tfcore/utils/tf-build.sh` must PASS, then boot the app and open every screen you touched with headless Playwright (or the native driver the ladder names); the data must show and nothing may overlap; use a test user from `{UsageGuide}`, never an invented one. A green compiler is not a smoke.
|
|
14
14
|
|
|
15
15
|
Unit tests for every FN, RAG and NFR row go under `tests/unit/`. Run-generated files go under `tests/.artifacts/`, never at the repository root.
|
|
16
16
|
|
|
@@ -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, ProductGuide, PROJECT-STATUS, a Decision Request
|
|
26
|
+
- **Only human-readable docs are ever rendered to HTML** (BRD, Architecture, UsageGuide, DevGuide, ProductGuide, PROJECT-STATUS, a Decision Request). **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. Three are banned by name, and `tf-render-html.py` refuses all three with exit 2: the **requirements checklist** `{App}-Checklist.md`, the **miss log** `{App}-Misses.md`, and an **upstream feedback file** `{App}-{Upstream}-Feedback.md`. If you are about to emit a `*-Checklist.html`, a `*-Misses.html` or a `*-Feedback.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.
|
|
@@ -216,6 +216,8 @@ The trailing `<a class="anchor-link">` lets users grab a deep link by hovering.
|
|
|
216
216
|
|
|
217
217
|
**Preserve hand-written anchors.** Some source docs place explicit non-heading anchors like `<a id="d-req-ui-001"></a>` directly above a list item, and a table cell links to them with `[view](#d-req-ui-001)`. This is how the Requirements Status table's **Details** column jumps to each REQ's detail entry. Emit every such `<a id="...">` verbatim in the HTML and keep the matching `href="#..."` link intact — do NOT drop them, renumber them, or run them through the heading-slug function. Dropping them silently breaks the Details links (the exact class of bug the user keeps reporting).
|
|
218
218
|
|
|
219
|
+
**Links to sibling documents open their HTML (2026-09-29).** A relative link to a `.md` file is written with `.html` when that page's HTML already sits beside the rendered page, or is written by the same render run; its `#anchor` is kept. Otherwise it stays `.md`: a markdown file with no rendered twin (an agent document, the checklist, the miss log, a feedback file) is never pointed at an HTML page that does not exist. Web links, absolute paths and `#anchor`-only links are never changed. So render a split guide's index together with its pages, or after them, and its links open the rendered pages.
|
|
220
|
+
|
|
219
221
|
---
|
|
220
222
|
|
|
221
223
|
## 5. Mermaid block conversion
|
|
@@ -489,7 +491,7 @@ The inline `<div class="toc-inline">` mirrors the MD Table of Contents. When the
|
|
|
489
491
|
|
|
490
492
|
- [ ] CSS palette matches §2 verbatim (themed `:root` light + `html[data-theme="dark"]` overrides; no hardcoded hex colors reintroduced in the body rules)
|
|
491
493
|
- [ ] Page skeleton matches §3 — flash-free theme-init `<script>` in `<head>` + the `#themeToggle` button present; §7 JS wires the toggle and Mermaid's theme follows `data-theme`
|
|
492
|
-
- [ ] This is a human-readable doc (BRD / Architecture / UsageGuide / DevGuide / PROJECT-STATUS) — NOT a checklist (
|
|
494
|
+
- [ ] This is a human-readable doc (BRD / Architecture / UsageGuide / DevGuide / PROJECT-STATUS) — NOT a checklist, miss log or feedback file (those are never rendered to HTML)
|
|
493
495
|
- [ ] Every H2/H3/H4 has `id="{slug}"` matching §1 algorithm
|
|
494
496
|
- [ ] Every TOC entry (MD inline AND HTML inline/sidebar) uses the same slug
|
|
495
497
|
- [ ] Every mermaid fence is wrapped in `<div class="diagram">` with the §5 toolbar
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// TechieFlow — the documents tf-assets.sh grades, fetched through a signed-in browser
|
|
2
|
+
// (AppManager TF-011, 2026-09-14). Driven by tf-assets.sh when --login-path is given; run that.
|
|
3
|
+
//
|
|
4
|
+
// tf-assets reads what a page DECLARES and fetches each asset. It fetched the page with a bare HTTP
|
|
5
|
+
// request, and an app whose sign-in lives in the page's own connection answers 401 to every bare
|
|
6
|
+
// request: "declared 0, graded 0" on every path. Here each path is opened in a browser tab that
|
|
7
|
+
// signed in through the page's form (tf-login.mjs), judged by what it draws, and its document — the
|
|
8
|
+
// head with every stylesheet and script it declares — handed back for grading. The assets themselves
|
|
9
|
+
// are still fetched one by one by tf-assets.sh, so a 404 stays observable.
|
|
10
|
+
//
|
|
11
|
+
// node tf-assets-browser.mjs --base URL --paths "/a,/b" --login-path /login --user U --password P
|
|
12
|
+
// [--storage-state file] [--cookie 'k=v'] --json-out <file>
|
|
13
|
+
// Writes {"login": {...}, "pages": {"/a": {"status": 401, "reached": "…", "signed_out": false,
|
|
14
|
+
// "url": "...", "html": "<!doctype…"}}}. Exit 0 always except a usage error (3).
|
|
15
|
+
|
|
16
|
+
import { chromium } from 'playwright';
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { dirname, resolve } from 'node:path';
|
|
19
|
+
import { signIn, reach } from './tf-login.mjs';
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
const arg = (name, dflt = null) => { const i = argv.indexOf(name); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : dflt; };
|
|
23
|
+
const BASE = (arg('--base') || '').replace(/\/$/, '');
|
|
24
|
+
const PATHS = (arg('--paths', '/') || '').split(',').map((p) => p.trim()).filter(Boolean).map((p) => (p.startsWith('/') ? p : '/' + p));
|
|
25
|
+
const OUT = arg('--json-out');
|
|
26
|
+
const COOKIE = arg('--cookie'); const STORAGE = arg('--storage-state');
|
|
27
|
+
const LOGIN_OPTS = { base: BASE, loginPath: arg('--login-path'), user: arg('--user'), password: arg('--password'), settle: 1500, renderWait: 5000 };
|
|
28
|
+
// Lekhak TF-006: a desktop head, attached over its DevTools port. Its pages and assets are served by
|
|
29
|
+
// the embedded browser itself, so no request from outside can fetch them: each path is opened by
|
|
30
|
+
// pushState, and every asset the document declares is fetched from inside the page, its status and
|
|
31
|
+
// size handed back beside the document ("fetched").
|
|
32
|
+
const CDP = arg('--cdp') || '';
|
|
33
|
+
if ((!BASE && !CDP) || !OUT) { console.error('usage: tf-assets-browser.mjs --base URL|--cdp URL --paths /a,/b [--login-path /login --user U --password P] --json-out file'); process.exit(3); }
|
|
34
|
+
|
|
35
|
+
if (CDP) {
|
|
36
|
+
let b, page;
|
|
37
|
+
try {
|
|
38
|
+
b = await chromium.connectOverCDP(CDP, { timeout: 15000 });
|
|
39
|
+
const c = b.contexts()[0]; page = c && c.pages()[0];
|
|
40
|
+
} catch (e) { page = null; }
|
|
41
|
+
mkdirSync(dirname(resolve(OUT)), { recursive: true });
|
|
42
|
+
if (!page) { writeFileSync(resolve(OUT), JSON.stringify({ unreachable: true, cdp: CDP })); process.exit(0); }
|
|
43
|
+
const pages = {};
|
|
44
|
+
let origin = '';
|
|
45
|
+
for (const p of PATHS) {
|
|
46
|
+
let status = 200, error = '', html = '', url = '', fetched = {};
|
|
47
|
+
try {
|
|
48
|
+
await page.evaluate((r) => { history.pushState({}, '', r); window.dispatchEvent(new PopStateEvent('popstate', { state: {} })); }, p);
|
|
49
|
+
await page.waitForTimeout(LOGIN_OPTS.settle);
|
|
50
|
+
url = page.url(); origin = origin || new URL(url).origin;
|
|
51
|
+
html = await page.content();
|
|
52
|
+
fetched = await page.evaluate(async () => {
|
|
53
|
+
const out = {};
|
|
54
|
+
const urls = [...document.querySelectorAll('link[href]')].filter((l) => /stylesheet|preload|modulepreload|icon/i.test(l.rel)).map((l) => l.href)
|
|
55
|
+
.concat([...document.querySelectorAll('script[src]')].map((s) => s.src));
|
|
56
|
+
for (const u of [...new Set(urls)]) {
|
|
57
|
+
if (!/^https?:/i.test(u)) continue;
|
|
58
|
+
try { const r = await fetch(u, { cache: 'no-store' }); const buf = await r.arrayBuffer(); out[u] = { status: r.status, bytes: buf.byteLength }; }
|
|
59
|
+
catch (e) { out[u] = { status: null, bytes: 0, error: String(e.message || e).slice(0, 120) }; }
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
});
|
|
63
|
+
} catch (e) { status = 0; error = e.message.split('\n')[0]; }
|
|
64
|
+
pages[p] = { status, document_status: status, url, reached: 'opened inside the attached app (pushState)', signed_out: false, error, html, fetched };
|
|
65
|
+
}
|
|
66
|
+
await b.close().catch(() => {}); // disconnects; the app keeps running
|
|
67
|
+
writeFileSync(resolve(OUT), JSON.stringify({ login: null, cdp: CDP, origin, pages }));
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const browser = await chromium.launch();
|
|
72
|
+
const ctxOpts = { ignoreHTTPSErrors: true };
|
|
73
|
+
if (STORAGE && existsSync(STORAGE)) ctxOpts.storageState = STORAGE;
|
|
74
|
+
const ctx = await browser.newContext(ctxOpts);
|
|
75
|
+
if (COOKIE) {
|
|
76
|
+
const u = new URL(BASE);
|
|
77
|
+
for (const pair of COOKIE.split(';')) {
|
|
78
|
+
const [name, ...v] = pair.trim().split('=');
|
|
79
|
+
if (name && v.length) await ctx.addCookies([{ name: name.trim(), value: v.join('='), domain: u.hostname, path: '/' }]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const page = await ctx.newPage();
|
|
83
|
+
const login = await signIn(page, LOGIN_OPTS);
|
|
84
|
+
const pages = {};
|
|
85
|
+
for (const p of PATHS) {
|
|
86
|
+
const nav = await reach(page, LOGIN_OPTS, p);
|
|
87
|
+
let html = '';
|
|
88
|
+
if (nav.status && nav.status < 400 && !nav.signedOut) {
|
|
89
|
+
try { html = await page.content(); } catch (e) { html = ''; }
|
|
90
|
+
}
|
|
91
|
+
pages[p] = { status: nav.status, document_status: nav.document_status ?? nav.status, url: nav.url || '',
|
|
92
|
+
reached: nav.reached || '', signed_out: !!nav.signedOut, error: nav.error || '', html };
|
|
93
|
+
}
|
|
94
|
+
await browser.close();
|
|
95
|
+
mkdirSync(dirname(resolve(OUT)), { recursive: true });
|
|
96
|
+
writeFileSync(resolve(OUT), JSON.stringify({ login, pages }));
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
#
|
|
4
4
|
# bash .tfcore/utils/tf-assets.sh --base http://localhost:5099 \
|
|
5
5
|
# --paths "/,/login,/export" --cookie 'AuthCookie=<value>'
|
|
6
|
+
# An app whose sign-in lives in the page and sets no cookie (AppManager TF-011):
|
|
7
|
+
# --login-path /login --user <u> --password <p> (or --storage-state <file>)
|
|
8
|
+
# opens each path in a browser that signed in through the form and grades the
|
|
9
|
+
# document it drew; `pages[].reached` says how a 401 page was reached.
|
|
10
|
+
# A desktop head (Lekhak TF-006): --cdp http://<host>:9223 in place of --base attaches to the app,
|
|
11
|
+
# opens each path by pushState and fetches every declared asset from inside the page, because
|
|
12
|
+
# nothing outside the embedded browser can reach the files it serves.
|
|
6
13
|
#
|
|
7
14
|
# WHAT IT ASKS, and it is the only question no other gate asks:
|
|
8
15
|
#
|
|
@@ -50,8 +57,10 @@
|
|
|
50
57
|
|
|
51
58
|
set -uo pipefail
|
|
52
59
|
|
|
60
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
53
61
|
BASE=""; PATHS="/"; TIMEOUT=20; LABEL=""; OUT=""; EXTERNAL=0; MINBYTES=1
|
|
54
62
|
HDRS="" # newline-separated "K: V" pairs (--header, repeatable; --cookie is sugar)
|
|
63
|
+
COOKIE=""; LOGIN_PATH=""; USER=""; PASS=""; STORAGE=""; CDP=""
|
|
55
64
|
while [[ $# -gt 0 ]]; do
|
|
56
65
|
case "$1" in
|
|
57
66
|
--base) BASE="${2:-}"; shift 2 ;;
|
|
@@ -62,18 +71,44 @@ while [[ $# -gt 0 ]]; do
|
|
|
62
71
|
--min-bytes) MINBYTES="${2:-}"; shift 2 ;;
|
|
63
72
|
--include-external) EXTERNAL=1; shift ;;
|
|
64
73
|
--header) HDRS="${HDRS}${2:-}"$'\n'; shift 2 ;;
|
|
65
|
-
--cookie) HDRS="${HDRS}Cookie: ${2:-}"$'\n'; shift 2 ;;
|
|
66
|
-
-
|
|
74
|
+
--cookie) HDRS="${HDRS}Cookie: ${2:-}"$'\n'; COOKIE="${2:-}"; shift 2 ;;
|
|
75
|
+
# a sign-in the page keeps for itself sets no cookie (AppManager TF-011): the pages are then
|
|
76
|
+
# opened in a browser that signed in through the form, the same options tf-verify-screens takes
|
|
77
|
+
--login-path) LOGIN_PATH="${2:-}"; shift 2 ;;
|
|
78
|
+
--user) USER="${2:-}"; shift 2 ;;
|
|
79
|
+
--password) PASS="${2:-}"; shift 2 ;;
|
|
80
|
+
--storage-state) STORAGE="${2:-}"; shift 2 ;;
|
|
81
|
+
--cdp) CDP="${2:-}"; shift 2 ;; # a desktop head over its DevTools port (Lekhak TF-006)
|
|
82
|
+
-h|--help) sed -n '2,56p' "$0"; exit 0 ;;
|
|
67
83
|
*) echo "unknown argument: $1" >&2; exit 3 ;;
|
|
68
84
|
esac
|
|
69
85
|
done
|
|
70
86
|
|
|
71
|
-
[[ -n "$BASE" ]] || { printf '%s\n' \
|
|
72
|
-
"usage: tf-assets.sh --base URL [--paths /,/login] [--cookie 'k=v'] [--include-external]" \
|
|
87
|
+
[[ -n "$BASE" || -n "$CDP" ]] || { printf '%s\n' \
|
|
88
|
+
"usage: tf-assets.sh --base URL|--cdp URL [--paths /,/login] [--cookie 'k=v'] [--include-external]" \
|
|
89
|
+
" [--login-path /login --user U --password P] [--storage-state file]" \
|
|
73
90
|
" [--json-out tests/.artifacts/assets/REQ-NFR-015.json]" >&2; exit 3; }
|
|
74
91
|
command -v python3 >/dev/null 2>&1 || { echo '{"status":"no-python3"}'; exit 3; }
|
|
75
92
|
|
|
76
|
-
|
|
93
|
+
# ---- the documents, through a signed-in browser when a sign-in is given (TF-011) --------------
|
|
94
|
+
DOCS=""
|
|
95
|
+
if [[ -n "$CDP" ]]; then
|
|
96
|
+
if ! command -v node >/dev/null 2>&1 || ! node -e "import('playwright').then(()=>process.exit(0)).catch(()=>process.exit(1))" 2>/dev/null; then
|
|
97
|
+
echo '{"status":"no-playwright","note":"--cdp needs node and playwright (verify-phase §1 installs the verify environment)"}'; exit 3
|
|
98
|
+
fi
|
|
99
|
+
DOCS="$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX")" && mv "$DOCS" "$DOCS.json" && DOCS="$DOCS.json"
|
|
100
|
+
node "$HERE/tf-assets-browser.mjs" --cdp "$CDP" --paths "$PATHS" --json-out "$DOCS" >&2 || { echo '{"status":"browser-failed"}'; rm -f "$DOCS"; exit 3; }
|
|
101
|
+
elif [[ -n "$LOGIN_PATH" || -n "$STORAGE" ]]; then
|
|
102
|
+
if ! command -v node >/dev/null 2>&1 || ! node -e "import('playwright').then(()=>process.exit(0)).catch(()=>process.exit(1))" 2>/dev/null; then
|
|
103
|
+
echo '{"status":"no-playwright","note":"--login-path needs node and playwright (verify-phase §1 installs the verify environment)"}'; exit 3
|
|
104
|
+
fi
|
|
105
|
+
DOCS="$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX")" && mv "$DOCS" "$DOCS.json" && DOCS="$DOCS.json" # only GNU mktemp takes a suffix option
|
|
106
|
+
node "$HERE/tf-assets-browser.mjs" --base "$BASE" --paths "$PATHS" --json-out "$DOCS" \
|
|
107
|
+
${LOGIN_PATH:+--login-path "$LOGIN_PATH"} ${USER:+--user "$USER"} ${PASS:+--password "$PASS"} \
|
|
108
|
+
${STORAGE:+--storage-state "$STORAGE"} ${COOKIE:+--cookie "$COOKIE"} >&2 || { echo '{"status":"browser-failed"}'; rm -f "$DOCS"; exit 3; }
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
TF_CDP="$CDP" TF_BASE="$BASE" TF_PATHS="$PATHS" TF_TIMEOUT="$TIMEOUT" TF_LABEL="$LABEL" TF_OUT="$OUT" TF_DOCS="$DOCS" \
|
|
77
112
|
TF_HEADERS="$HDRS" TF_EXTERNAL="$EXTERNAL" TF_MINBYTES="$MINBYTES" python3 - <<'PY'
|
|
78
113
|
import html.parser, json, os, ssl, sys, urllib.error, urllib.request
|
|
79
114
|
from urllib.parse import urljoin, urlsplit
|
|
@@ -85,6 +120,34 @@ LABEL = os.environ.get("TF_LABEL") or None
|
|
|
85
120
|
OUT = os.environ.get("TF_OUT") or None
|
|
86
121
|
EXTERNAL = os.environ.get("TF_EXTERNAL") == "1"
|
|
87
122
|
MINBYTES = int(os.environ.get("TF_MINBYTES") or 1)
|
|
123
|
+
# the documents a signed-in browser fetched, by path (TF-011); empty when none was asked for
|
|
124
|
+
DOCS = {}
|
|
125
|
+
LOGIN = None
|
|
126
|
+
if os.environ.get("TF_DOCS"):
|
|
127
|
+
try:
|
|
128
|
+
_d = json.load(open(os.environ["TF_DOCS"], encoding="utf-8"))
|
|
129
|
+
DOCS, LOGIN = _d.get("pages", {}), _d.get("login")
|
|
130
|
+
except Exception:
|
|
131
|
+
DOCS = {}
|
|
132
|
+
try:
|
|
133
|
+
os.remove(os.environ["TF_DOCS"])
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
# an attached desktop head (TF-006): the assets were fetched inside the page, by URL
|
|
137
|
+
CDP = os.environ.get("TF_CDP") or ""
|
|
138
|
+
FETCHED = {}
|
|
139
|
+
if CDP:
|
|
140
|
+
try:
|
|
141
|
+
_d
|
|
142
|
+
except NameError:
|
|
143
|
+
_d = {}
|
|
144
|
+
if _d.get("unreachable") or not DOCS:
|
|
145
|
+
print(json.dumps({"status": "unreachable", "base": CDP, "label": LABEL,
|
|
146
|
+
"note": "the desktop head's DevTools port did not answer — a `build` gate problem, not an assets result"}, indent=2))
|
|
147
|
+
raise SystemExit(2)
|
|
148
|
+
BASE = (_d.get("origin") or "").rstrip("/")
|
|
149
|
+
for _pg in DOCS.values():
|
|
150
|
+
FETCHED.update(_pg.get("fetched") or {})
|
|
88
151
|
|
|
89
152
|
HEADERS = []
|
|
90
153
|
for line in (os.environ.get("TF_HEADERS") or "").splitlines():
|
|
@@ -145,6 +208,11 @@ class AssetParser(html.parser.HTMLParser):
|
|
|
145
208
|
def fetch(url, want_body=True):
|
|
146
209
|
"""Return (status, nbytes, error). Never raises — a transport failure is a
|
|
147
210
|
finding, not a crash, and the caller needs every path's result."""
|
|
211
|
+
if CDP:
|
|
212
|
+
f = FETCHED.get(url)
|
|
213
|
+
if f is None:
|
|
214
|
+
return None, 0, "not fetched inside the attached app", b""
|
|
215
|
+
return f.get("status"), f.get("bytes", 0), f.get("error"), b""
|
|
148
216
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
|
149
217
|
for k, v in HEADERS:
|
|
150
218
|
req.add_header(k, v)
|
|
@@ -193,7 +261,7 @@ def fetch_page(url):
|
|
|
193
261
|
|
|
194
262
|
|
|
195
263
|
# --- reachability first: an unreachable base is a `build` problem, not an assets one
|
|
196
|
-
probe_status, _, probe_err = fetch_page(BASE + "/")
|
|
264
|
+
probe_status, _, probe_err = (200, b"", None) if CDP else fetch_page(BASE + "/")
|
|
197
265
|
if probe_status is None:
|
|
198
266
|
print(json.dumps({"status": "unreachable", "base": BASE, "error": probe_err,
|
|
199
267
|
"label": LABEL, "note": "app not reachable — this is a `build` "
|
|
@@ -207,7 +275,23 @@ graded_assets = 0
|
|
|
207
275
|
|
|
208
276
|
for p in PATHS:
|
|
209
277
|
page_url = BASE + (p if p.startswith("/") else "/" + p)
|
|
210
|
-
|
|
278
|
+
extra = {}
|
|
279
|
+
doc = DOCS.get(p if p.startswith("/") else "/" + p)
|
|
280
|
+
if doc is not None:
|
|
281
|
+
# the document as the signed-in browser drew it (TF-011). A page that stayed signed out is
|
|
282
|
+
# an auth wall: nothing of it is graded, like a redirect to the sign-in page.
|
|
283
|
+
if doc.get("signed_out"):
|
|
284
|
+
redirected += 1
|
|
285
|
+
pages.append({"path": p, "status": doc.get("document_status"), "assets": [], "declared": 0,
|
|
286
|
+
"graded": 0, "failed": 0, "redirected": True, "auth_wall": True, "url": doc.get("url")})
|
|
287
|
+
continue
|
|
288
|
+
status, body, err = (doc.get("status") or None), (doc.get("html") or "").encode("utf-8"), (doc.get("error") or None)
|
|
289
|
+
if status and status < 400 and body:
|
|
290
|
+
status = 200
|
|
291
|
+
if doc.get("reached"):
|
|
292
|
+
extra = {"document_status": doc.get("document_status"), "reached": doc["reached"]}
|
|
293
|
+
else:
|
|
294
|
+
status, body, err = fetch_page(page_url)
|
|
211
295
|
if status is None:
|
|
212
296
|
pages.append({"path": p, "status": None, "error": err, "assets": [],
|
|
213
297
|
"declared": 0, "graded": 0, "failed": 0})
|
|
@@ -269,16 +353,16 @@ for p in PATHS:
|
|
|
269
353
|
|
|
270
354
|
pages.append({"path": p, "status": status, "declared": len(rows),
|
|
271
355
|
"graded": sum(1 for r in rows if r.get("graded")),
|
|
272
|
-
"failed": failed, "assets": rows})
|
|
356
|
+
"failed": failed, "assets": rows, **extra})
|
|
273
357
|
|
|
274
358
|
# Every path turned away = nothing was graded. Reporting "0 findings" there would be
|
|
275
359
|
# a PASS on a screen the harness never saw — the same false green tf-perf.sh's exit 4
|
|
276
360
|
# exists to prevent, and the same shape as TF-011's ungradeable-looks-clean failure.
|
|
277
361
|
if PATHS and redirected == len(PATHS):
|
|
278
|
-
out = {"status": "redirected", "base": BASE, "label": LABEL, "pages": pages,
|
|
279
|
-
"findings": [], "note": "every path answered 3xx — nothing was graded. "
|
|
280
|
-
"Present a session with --cookie / --header and re-run.
|
|
281
|
-
"ASSETS-UNMEASURED (auth wall); never record this as a pass."}
|
|
362
|
+
out = {"status": "redirected", "base": BASE, "label": LABEL, "pages": pages, "login": LOGIN,
|
|
363
|
+
"findings": [], "note": "every path answered 3xx or stayed signed out — nothing was graded. "
|
|
364
|
+
"Present a session with --cookie / --header, or --login-path/--user/--password, and re-run. "
|
|
365
|
+
"Grade the REQ ASSETS-UNMEASURED (auth wall); never record this as a pass."}
|
|
282
366
|
print(json.dumps(out, indent=2))
|
|
283
367
|
if OUT:
|
|
284
368
|
os.makedirs(os.path.dirname(os.path.abspath(OUT)), exist_ok=True)
|
|
@@ -294,6 +378,7 @@ out = {
|
|
|
294
378
|
"findings_n": len(findings),
|
|
295
379
|
"findings": findings,
|
|
296
380
|
"pages": pages,
|
|
381
|
+
"login": LOGIN,
|
|
297
382
|
"include_external": EXTERNAL,
|
|
298
383
|
# An assets run that graded nothing is UNGRADEABLE, not clean. Said out loud in
|
|
299
384
|
# the payload so a consumer cannot read `findings_n: 0` as coverage.
|
|
@@ -16,8 +16,9 @@ docs/<App>-BRD.md (Screen | Requirements | Verified | Open | Status) with a
|
|
|
16
16
|
re-renders docs/<App>-BRD.html through tf-render-html.sh unless --no-render.
|
|
17
17
|
|
|
18
18
|
Status per screen: Done (every row terminal), Planned (no row started),
|
|
19
|
-
Partial (some terminal, some open), In progress (started, none terminal)
|
|
20
|
-
Terminal = Verified, Done (pre-existing)
|
|
19
|
+
Partial (some terminal, some open), In progress (started, none terminal), Not applicable
|
|
20
|
+
(every row N/A). Terminal = Verified, Done (pre-existing). An N/A row is left out of the counts,
|
|
21
|
+
never counted as verified (AppManager TF-018). Blocked is counted as open.
|
|
21
22
|
|
|
22
23
|
Python 3 standard library only. Exit 0 written or nothing to do, 2 could not run.
|
|
23
24
|
"""
|
|
@@ -27,7 +28,9 @@ import re
|
|
|
27
28
|
import subprocess
|
|
28
29
|
import sys
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
# norm() drops a bracketed tail, so "Done (pre-existing)" arrives as "done" and was counted open (AppManager TF-003)
|
|
32
|
+
TERMINAL = {"verified", "done", "done (pre-existing)"}
|
|
33
|
+
NA = {"n/a"}
|
|
31
34
|
NOT_STARTED = {"not started"}
|
|
32
35
|
|
|
33
36
|
|
|
@@ -80,6 +83,8 @@ def table(rows, screen_of):
|
|
|
80
83
|
if s not in per:
|
|
81
84
|
per[s] = {"n": 0, "v": 0, "ns": 0}
|
|
82
85
|
order.append(s)
|
|
86
|
+
if status in NA:
|
|
87
|
+
continue
|
|
83
88
|
per[s]["n"] += 1
|
|
84
89
|
per[s]["v"] += 1 if status in TERMINAL else 0
|
|
85
90
|
per[s]["ns"] += 1 if status in NOT_STARTED else 0
|
|
@@ -87,7 +92,9 @@ def table(rows, screen_of):
|
|
|
87
92
|
for s in order:
|
|
88
93
|
d = per[s]
|
|
89
94
|
open_n = d["n"] - d["v"]
|
|
90
|
-
if
|
|
95
|
+
if d["n"] == 0:
|
|
96
|
+
st = "Not applicable"
|
|
97
|
+
elif open_n == 0:
|
|
91
98
|
st = "Done"
|
|
92
99
|
elif d["ns"] == d["n"]:
|
|
93
100
|
st = "Planned"
|
|
@@ -153,7 +160,9 @@ def main(argv):
|
|
|
153
160
|
with open(brd, "w", encoding="utf-8") as f:
|
|
154
161
|
f.write(new_text)
|
|
155
162
|
verified = sum(1 for _, s in rows if s in TERMINAL)
|
|
156
|
-
|
|
163
|
+
na = sum(1 for _, s in rows if s in NA)
|
|
164
|
+
print(f"tf-brd-status: {brd_rel} Development status {how}: {screens} screens, {verified} of {len(rows) - na} "
|
|
165
|
+
f"requirements verified" + (f", {na} not applicable" if na else ""))
|
|
157
166
|
if render:
|
|
158
167
|
script = os.path.join(root, ".tfcore", "utils", "tf-render-html.sh")
|
|
159
168
|
if os.path.isfile(script):
|