@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.
Files changed (98) hide show
  1. package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
  2. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  3. package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
  4. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +1 -1
  5. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +1 -1
  6. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
  9. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  11. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  12. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  13. package/.opencode/plugin/techieflow.js +179 -7
  14. package/.tfcore/hooks/block-git.sh +4 -2
  15. package/.tfcore/hooks/guard-build.sh +5 -1
  16. package/.tfcore/hooks/guard-db.sh +6 -4
  17. package/.tfcore/hooks/guard-status-html.sh +86 -2
  18. package/.tfcore/hooks/guard-verify.sh +8 -3
  19. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  20. package/.tfcore/standards/owner-words.txt +23 -0
  21. package/.tfcore/tasks/_owner-language.md +1 -0
  22. package/.tfcore/tasks/_status-update-gate.md +4 -3
  23. package/.tfcore/tasks/amend-docs.md +1 -1
  24. package/.tfcore/tasks/day1-brownfield.md +1 -1
  25. package/.tfcore/tasks/day1-greenfield.md +1 -1
  26. package/.tfcore/tasks/fix-issues.md +2 -2
  27. package/.tfcore/tasks/generate-html.md +2 -2
  28. package/.tfcore/tasks/handoff-phase.md +2 -2
  29. package/.tfcore/tasks/mockups.md +1 -1
  30. package/.tfcore/tasks/productguide.md +2 -2
  31. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  32. package/.tfcore/tasks/triage-issues.md +2 -2
  33. package/.tfcore/telemetry/SCHEMA.md +4 -0
  34. package/.tfcore/telemetry/install-metrics.sh +3 -2
  35. package/.tfcore/telemetry/tf-metrics.sh +19 -2
  36. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  37. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  38. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  39. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  40. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  41. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  42. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  43. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  44. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  45. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  46. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  47. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  48. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  49. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  50. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  51. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  52. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  53. package/.tfcore/utils/tf-assets.sh +97 -12
  54. package/.tfcore/utils/tf-brd-status.py +14 -5
  55. package/.tfcore/utils/tf-build-list.py +80 -12
  56. package/.tfcore/utils/tf-build-list.sh +2 -1
  57. package/.tfcore/utils/tf-build.sh +88 -15
  58. package/.tfcore/utils/tf-ci-repro.py +685 -0
  59. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  60. package/.tfcore/utils/tf-devguide-list.py +47 -22
  61. package/.tfcore/utils/tf-doc-check.py +136 -6
  62. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  63. package/.tfcore/utils/tf-emit.sh +92 -35
  64. package/.tfcore/utils/tf-feedback.sh +13 -0
  65. package/.tfcore/utils/tf-fix-close.sh +79 -29
  66. package/.tfcore/utils/tf-goal.sh +12 -12
  67. package/.tfcore/utils/tf-harness.sh +9 -4
  68. package/.tfcore/utils/tf-lock.sh +29 -0
  69. package/.tfcore/utils/tf-log-miss.py +34 -8
  70. package/.tfcore/utils/tf-login.mjs +167 -0
  71. package/.tfcore/utils/tf-misses-md.py +10 -4
  72. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  73. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  74. package/.tfcore/utils/tf-owner-text.py +233 -0
  75. package/.tfcore/utils/tf-owner-text.sh +14 -0
  76. package/.tfcore/utils/tf-phase.sh +15 -1
  77. package/.tfcore/utils/tf-portable.sh +230 -0
  78. package/.tfcore/utils/tf-render-html.py +60 -2
  79. package/.tfcore/utils/tf-selfcheck.sh +39 -6
  80. package/.tfcore/utils/tf-split-brd.py +11 -1
  81. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  82. package/.tfcore/utils/tf-status-facts.py +72 -32
  83. package/.tfcore/utils/tf-triage.py +62 -16
  84. package/.tfcore/utils/tf-triage.sh +2 -0
  85. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  86. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  87. package/.tfcore/utils/tf-verify-env.sh +33 -4
  88. package/.tfcore/utils/tf-verify-list.py +75 -8
  89. package/.tfcore/utils/tf-verify-screens.mjs +171 -32
  90. package/.tfcore/utils/tf-verify-screens.sh +8 -3
  91. package/.tfcore/utils/tf-verify-tests.sh +215 -14
  92. package/.tfcore/utils/tf-verify-verdict.py +48 -9
  93. package/.tfcore/utils/tf_defect.py +27 -0
  94. package/.tfcore/utils/tf_feedback.py +209 -0
  95. package/.tfcore/utils/tf_roadmap.py +35 -0
  96. package/README.md +4 -0
  97. package/docs/TechieFlow-Installation.md +5 -3
  98. 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 (docs/{App}-UIDesign.md, docs/{App}-P2-UIDesign.md, …) with a
25
- header row "| Phase | n of m |"; its screens are exactly that phase's row in docs/{App}-Phases.md.
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 `docs/{App}-UsageGuide.md`, never an invented one. A green compiler is not a smoke.
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, 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.
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 (checklists are never rendered to HTML)
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
@@ -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
- -h|--help) sed -n '2,48p' "$0"; exit 0 ;;
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
- TF_BASE="$BASE" TF_PATHS="$PATHS" TF_TIMEOUT="$TIMEOUT" TF_LABEL="$LABEL" TF_OUT="$OUT" \
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
- status, body, err = fetch_page(page_url)
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. Grade the REQ "
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), N/A. Blocked is counted as open.
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
- TERMINAL = {"verified", "done (pre-existing)", "n/a"}
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 open_n == 0:
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
- print(f"tf-brd-status: {brd_rel} Development status {how}: {screens} screens, {verified} of {len(rows)} requirements verified")
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):