@techierathore/techieflow 1.0.4 → 1.1.1

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 (148) hide show
  1. package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
  2. package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
  3. package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
  4. package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  6. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  9. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
  10. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  11. package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
  12. package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
  14. package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
  15. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  16. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  17. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  18. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  19. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  20. package/.opencode/plugin/techieflow.js +194 -14
  21. package/.tfcore/agents/flow-master.md +2 -0
  22. package/.tfcore/agents/verifier.md +2 -2
  23. package/.tfcore/core-config.yaml +2 -1
  24. package/.tfcore/hooks/block-git.sh +4 -2
  25. package/.tfcore/hooks/guard-build.sh +5 -1
  26. package/.tfcore/hooks/guard-db.sh +112 -13
  27. package/.tfcore/hooks/guard-status-html.sh +86 -2
  28. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  29. package/.tfcore/hooks/guard-verify.sh +8 -3
  30. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  31. package/.tfcore/hooks/yolo-allow.sh +27 -0
  32. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  33. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  34. package/.tfcore/standards/owner-words.txt +23 -0
  35. package/.tfcore/tasks/_develop-mode.md +34 -0
  36. package/.tfcore/tasks/_owner-language.md +1 -0
  37. package/.tfcore/tasks/_status-update-gate.md +4 -3
  38. package/.tfcore/tasks/_yolo-mode.md +2 -2
  39. package/.tfcore/tasks/amend-docs.md +2 -2
  40. package/.tfcore/tasks/day1-brownfield.md +3 -3
  41. package/.tfcore/tasks/day1-greenfield.md +5 -5
  42. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  43. package/.tfcore/tasks/fix-issues.md +3 -2
  44. package/.tfcore/tasks/generate-html.md +2 -2
  45. package/.tfcore/tasks/handoff-phase.md +2 -2
  46. package/.tfcore/tasks/mockups.md +2 -2
  47. package/.tfcore/tasks/productguide.md +2 -2
  48. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  49. package/.tfcore/tasks/triage-and-fix.md +2 -2
  50. package/.tfcore/tasks/triage-issues.md +2 -2
  51. package/.tfcore/tasks/verify-phase.md +12 -11
  52. package/.tfcore/telemetry/SCHEMA.md +10 -2
  53. package/.tfcore/telemetry/install-metrics.sh +3 -2
  54. package/.tfcore/telemetry/tf-metrics.sh +38 -2
  55. package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
  56. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  57. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  58. package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
  59. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  60. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  61. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  62. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  63. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  64. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  65. package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
  66. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  67. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  68. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  69. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  70. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  71. package/.tfcore/utils/tf-appium.mjs +179 -0
  72. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  73. package/.tfcore/utils/tf-assets.sh +97 -12
  74. package/.tfcore/utils/tf-brd-status.py +14 -5
  75. package/.tfcore/utils/tf-build-list.py +95 -12
  76. package/.tfcore/utils/tf-build-list.sh +2 -1
  77. package/.tfcore/utils/tf-build.sh +163 -15
  78. package/.tfcore/utils/tf-checklist-edit.py +21 -3
  79. package/.tfcore/utils/tf-ci-repro.py +685 -0
  80. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  81. package/.tfcore/utils/tf-day1-files.py +55 -28
  82. package/.tfcore/utils/tf-develop.sh +264 -0
  83. package/.tfcore/utils/tf-devguide-list.py +53 -24
  84. package/.tfcore/utils/tf-doc-check.py +218 -26
  85. package/.tfcore/utils/tf-doc-tests.sh +116 -0
  86. package/.tfcore/utils/tf-emit.sh +132 -45
  87. package/.tfcore/utils/tf-feedback.sh +13 -0
  88. package/.tfcore/utils/tf-fix-close.sh +87 -31
  89. package/.tfcore/utils/tf-goal.sh +12 -12
  90. package/.tfcore/utils/tf-harness.sh +9 -4
  91. package/.tfcore/utils/tf-lock.sh +29 -0
  92. package/.tfcore/utils/tf-log-miss.py +34 -8
  93. package/.tfcore/utils/tf-login.mjs +167 -0
  94. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  95. package/.tfcore/utils/tf-menus.mjs +401 -0
  96. package/.tfcore/utils/tf-misses-md.py +10 -4
  97. package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
  98. package/.tfcore/utils/tf-mockup-parity.sh +25 -2
  99. package/.tfcore/utils/tf-owner-text.py +233 -0
  100. package/.tfcore/utils/tf-owner-text.sh +14 -0
  101. package/.tfcore/utils/tf-phase.sh +38 -4
  102. package/.tfcore/utils/tf-portable.sh +230 -0
  103. package/.tfcore/utils/tf-render-html.py +79 -5
  104. package/.tfcore/utils/tf-req-ids.py +36 -0
  105. package/.tfcore/utils/tf-selfcheck.sh +39 -6
  106. package/.tfcore/utils/tf-split-brd.py +11 -1
  107. package/.tfcore/utils/tf-stack-check.sh +3 -0
  108. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  109. package/.tfcore/utils/tf-status-facts.py +160 -45
  110. package/.tfcore/utils/tf-theme.mjs +189 -0
  111. package/.tfcore/utils/tf-triage.py +74 -17
  112. package/.tfcore/utils/tf-triage.sh +2 -0
  113. package/.tfcore/utils/tf-verify-boot.sh +529 -54
  114. package/.tfcore/utils/tf-verify-emit.sh +57 -17
  115. package/.tfcore/utils/tf-verify-env.sh +33 -4
  116. package/.tfcore/utils/tf-verify-list.py +290 -24
  117. package/.tfcore/utils/tf-verify-list.sh +3 -1
  118. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  119. package/.tfcore/utils/tf-verify-screens.mjs +341 -44
  120. package/.tfcore/utils/tf-verify-screens.sh +23 -5
  121. package/.tfcore/utils/tf-verify-tests.sh +310 -19
  122. package/.tfcore/utils/tf-verify-verdict.py +163 -21
  123. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  124. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  125. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  126. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  127. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  128. package/.tfcore/utils/tf-yolo.sh +31 -3
  129. package/.tfcore/utils/tf_defect.py +27 -0
  130. package/.tfcore/utils/tf_feedback.py +272 -0
  131. package/.tfcore/utils/tf_roadmap.py +35 -0
  132. package/.tfcore/utils/tf_stack_check.py +93 -0
  133. package/README.md +6 -2
  134. package/docs/TechieFlow-Installation.md +5 -3
  135. package/opencode.jsonc +6 -2
  136. package/package.json +1 -1
  137. package/scripts/install.mjs +13 -2
  138. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  139. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  140. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  141. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  142. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  143. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  144. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  145. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  146. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  147. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  148. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
@@ -25,9 +25,11 @@ rule: mockup-links
25
25
  Size and requirement cap: Small up to 10 screens, one role, 50 requirements; Medium up to 20 screens,
26
26
  100 requirements; Large is split into phases, each phase its own BRD. Every routed page is a screen;
27
27
  dialogs, tabs and panels are regions of their page and are listed under it.
28
- Large project: one BRD per phase, phase 1 in docs/{App}-BRD.md, phase n in docs/{App}-Pn-BRD.md; the
29
- header gains a row "| Phase | n of m |", Size is the phase's own size (Small or Medium), and BRD-N
30
- runs on from the previous phase's last id. The phase plan is docs/{App}-Phases.md.
28
+ Large project: one BRD per phase, phase 1 in docs/{App}-BRD.md from this template, phase n in
29
+ docs/{App}-Pn-BRD.md from app-phase-brd-tmpl.md, which carries only that phase's screens and
30
+ requirements; this file keeps the whole application's scope, users, non-functionals, constraints
31
+ and risks. The header gains a row "| Phase | n of m |", Size is the phase's own size (Small or
32
+ Medium), and BRD-N runs on from the previous phase's last id. The phase plan is docs/{App}-Phases.md.
31
33
  Mermaid: quote every label (A["Order (v2)"]); never use `end` as a node id. -->
32
34
 
33
35
  # {App} — Business Requirements
@@ -92,8 +94,9 @@ One item per thing the verifier will test. Each names its screen, links the mock
92
94
  | BRD-{N} | Performance | {…} | perf-budget: p95 load <= 2000ms @ concurrency 1 |
93
95
  | BRD-{N} | Security | {…} | {…} |
94
96
  | BRD-{N} | Logging | {from the Stack answer set} | {…} |
97
+ | BRD-{N} | Zoom (a desktop head drawn in a web view) | Cmd/Ctrl+=, - and 0 zoom 50% to 300% in 10% steps; the level is kept across restarts | zoom-check: keys · zoom-check: kept |
95
98
 
96
- The `perf-budget:` measure is machine-read by the verifier, in exactly this form: `perf-budget: <p50|p95|max> <ttfb|load> <= <N>ms [@ concurrency <N>]`. Write one only where the owner stated a number.
99
+ The `perf-budget:` measure is machine-read by the verifier, in exactly this form: `perf-budget: <p50|p95|max> <ttfb|load> <= <N>ms [@ concurrency <N>]`. Write one only where the owner stated a number. The zoom row is for an app whose desktop head draws its screens in a web view (the stack's coding standards say how), and is deleted otherwise; its two checklist rows end `zoom-check: keys` and `zoom-check: kept`, which the verifier grades with `tf-verify-zoom.sh`.
97
100
 
98
101
  ## 7. Development status
99
102
 
@@ -45,6 +45,8 @@ rule: checklist-rows
45
45
  | REQ-UI-001 | {short name} | Not Started | 0% | — | [view](#d-req-ui-001) |
46
46
  | REQ-FN-001 | {short name} | Not Started | 0% | — | [view](#d-req-fn-001) |
47
47
  | REQ-NFR-001 | {short name} | Not Started | 0% | — | [view](#d-req-nfr-001) |
48
+ | REQ-NFR-002 | Zoom with the OS keys (web-view desktop head only) | Not Started | 0% | — | [view](#d-req-nfr-002) |
49
+ | REQ-NFR-003 | Zoom level kept (web-view desktop head only) | Not Started | 0% | — | [view](#d-req-nfr-003) |
48
50
 
49
51
  **Status values:** `Not Started` · `In Progress` · `Implemented` · `Verified` · `Done (pre-existing)` · `Needs re-verify` · `PARTIAL` · `FAIL` · `Blocked` · `Owner-UAT` · `N/A`. `Owner-UAT` marks a row only the owner can close, by following the UsageGuide test plan; the verifier cannot reach it from this machine.
50
52
 
@@ -63,3 +65,12 @@ rule: checklist-rows
63
65
  <a id="d-req-nfr-001"></a>
64
66
  - **REQ-NFR-001** — {the requirement}. *BRD:* BRD-{N}
65
67
  - *Acceptance:* When {the measurement is taken}, then {the value within its limit}; perf-budget: p95 load <= 2000ms @ concurrency 1
68
+
69
+ <!-- A desktop head drawn in a web view carries these two rows (the stack's coding standards say how it zooms); delete them otherwise. -->
70
+ <a id="d-req-nfr-002"></a>
71
+ - **REQ-NFR-002** — Zoom with the OS keys. *BRD:* BRD-{N}
72
+ - *Acceptance:* When a user presses Cmd+= (Mac) or Ctrl+= (Windows) three times on {first screen}, then the page is drawn at 130%; zoom-check: keys
73
+
74
+ <a id="d-req-nfr-003"></a>
75
+ - **REQ-NFR-003** — The zoom level is kept. *BRD:* BRD-{N}
76
+ - *Acceptance:* When a user zooms to 130% on {first screen} and restarts the app, then it opens at 130%; zoom-check: kept
@@ -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) |
@@ -13,7 +13,8 @@ rule: phases-table
13
13
  phases and are never reused, so the ranges never overlap. An item added to an earlier phase after a
14
14
  later phase exists takes the next free id and the row gets a second range: "BRD-1 to BRD-73, BRD-111 to BRD-118". Every screen sits in exactly one phase;
15
15
  the screens of a phase are exactly the "### Screen:" entries of that phase's UIDesign.
16
- Status is planned, building or done. The phase being worked is appPhase in core-config.yaml
16
+ Status is planned, building, done or kept-whole. kept-whole is only for a phase finished before the
17
+ size cap that the owner chose to keep as one document: its cap and word maximum then WARN, not FAIL. The phase being worked is appPhase in core-config.yaml
17
18
  (bash .tfcore/utils/tf-day1-files.sh {App} --phase N); handoff moves it on.
18
19
  tf-doc-check.sh --app {App} refuses a screen in two phases, an id in two phases, a BRD id outside
19
20
  its phase's range, and a phase row without its BRD file. -->
@@ -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.
@@ -48,6 +48,8 @@ The same slug function MUST produce the `id="..."` on each heading AND the `href
48
48
 
49
49
  The MD inline TOC and the HTML sidebar TOC use the SAME slug. The heading `id` uses the SAME slug. No exceptions.
50
50
 
51
+ **A second id, GitHub's.** A markdown table of contents written by hand links to the id GitHub gives the heading: digits kept, punctuation dropped, spaces to hyphens, a repeat suffixed `-1`, `-2` (`### 3.7 Group Service (GroupSvc)` → `#37-group-service-groupsvc`). When that id differs from the slug above, the heading also carries `<a id="{github-id}"></a>`, so those links land too (AppManager TF-029). A heading with no slug (H1, H5, H6) carries the GitHub id as its own.
52
+
51
53
  ---
52
54
 
53
55
  ## 2. CSS (inline at top of `<head>`, verbatim)
@@ -216,6 +218,8 @@ The trailing `<a class="anchor-link">` lets users grab a deep link by hovering.
216
218
 
217
219
  **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
220
 
221
+ **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.
222
+
219
223
  ---
220
224
 
221
225
  ## 5. Mermaid block conversion
@@ -489,7 +493,7 @@ The inline `<div class="toc-inline">` mirrors the MD Table of Contents. When the
489
493
 
490
494
  - [ ] CSS palette matches §2 verbatim (themed `:root` light + `html[data-theme="dark"]` overrides; no hardcoded hex colors reintroduced in the body rules)
491
495
  - [ ] 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)
496
+ - [ ] 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
497
  - [ ] Every H2/H3/H4 has `id="{slug}"` matching §1 algorithm
494
498
  - [ ] Every TOC entry (MD inline AND HTML inline/sidebar) uses the same slug
495
499
  - [ ] Every mermaid fence is wrapped in `<div class="diagram">` with the §5 toolbar
@@ -0,0 +1,179 @@
1
+ // tf-appium.mjs — the few WebDriver calls a native head needs, over Node's own fetch (no package).
2
+ // Used by tf-verify-native.mjs; an acceptance test of a native head may import it too:
3
+ // import { openSession, parseSource } from '<repo>/.tfcore/utils/tf-appium.mjs';
4
+ // const s = await openSession(process.env.APPIUM_URL, { bundleId: process.env.TF_BUNDLE_ID, appPath: process.env.TF_APP_PATH });
5
+ // await s.click(await s.find('accessibility id', 'save')); … await s.close();
6
+ // Every control is found inside the app under test by its AutomationId (the accessibility
7
+ // identifier); nothing here types or clicks at a screen position. Type into a field with
8
+ // `await s.type(el, 'text')`, the one way a Blazor Hybrid form on a Mac took the text.
9
+
10
+ import { execFileSync } from 'node:child_process';
11
+ import { statSync } from 'node:fs';
12
+
13
+ const W3C_EL = 'element-6066-11e4-a52e-4f735466cecf';
14
+
15
+ async function call(base, method, path, body) {
16
+ const r = await fetch(base + path, {
17
+ method, headers: { 'Content-Type': 'application/json' },
18
+ body: body === undefined ? undefined : JSON.stringify(body),
19
+ signal: AbortSignal.timeout(300000),
20
+ });
21
+ const j = await r.json().catch(() => ({ value: { message: `HTTP ${r.status}, not JSON` } }));
22
+ if (!r.ok || (j.value && j.value.error)) throw new Error((j.value && j.value.message || `HTTP ${r.status}`).split('\n')[0]);
23
+ return j.value;
24
+ }
25
+
26
+ // A session on the Mac Catalyst app (mac2), by its .app path when known: by bundle id alone mac2
27
+ // answered "could not be found" for a build outside /Applications that macOS had not registered
28
+ // (Lekhak TF-003; seen again 2026-10-07). skipAppKill: the app is tf-verify-boot.sh's to stop, so
29
+ // ending the session leaves it running for the next check.
30
+ export async function openSession(url, { bundleId, appPath = '', automationName = 'Mac2', platformName = 'mac', extra = {} } = {}) {
31
+ const base = url.replace(/\/$/, '');
32
+ const which = appPath ? { 'appium:appPath': appPath } : { 'appium:bundleId': bundleId };
33
+ const v = await call(base, 'POST', '/session', { capabilities: { alwaysMatch: {
34
+ platformName, 'appium:automationName': automationName, ...which,
35
+ 'appium:skipAppKill': true, 'appium:newCommandTimeout': 300, ...extra } } });
36
+ const sp = `/session/${v.sessionId}`;
37
+ const s = {
38
+ id: v.sessionId,
39
+ call: (m, p, b) => call(base, m, sp + p, b),
40
+ source: () => s.call('GET', '/source'),
41
+ find: async (using, value, from = null) => Object.values(await s.call('POST', from ? `/element/${from}/element` : '/element', { using, value }))[0],
42
+ findAll: async (using, value, from = null) => (await s.call('POST', from ? `/element/${from}/elements` : '/elements', { using, value })).map((e) => e[W3C_EL] || Object.values(e)[0]),
43
+ click: (el) => s.call('POST', `/element/${el}/click`, {}),
44
+ // Typing into a field. A Blazor Hybrid form on a Mac kept its @bind empty or half filled when the
45
+ // value was set through Accessibility, through mac2's "set value", or by key events posted to the
46
+ // process ("The text must have valid value", Lekhak TF-026). What reached it: a click on the field,
47
+ // then W3C key actions, a key down and up per character.
48
+ type: async (el, text) => {
49
+ await s.click(el);
50
+ const actions = [...String(text)].flatMap((c) => [{ type: 'keyDown', value: c }, { type: 'keyUp', value: c }]);
51
+ await s.call('POST', '/actions', { actions: [{ type: 'key', id: 'keyboard', actions }] });
52
+ await s.call('DELETE', '/actions').catch(() => {});
53
+ },
54
+ shot: (el) => s.call('GET', el ? `/element/${el}/screenshot` : '/screenshot'),
55
+ close: () => call(base, 'DELETE', sp).catch(() => {}),
56
+ };
57
+ return s;
58
+ }
59
+
60
+ // What the app shows first, for tf-verify-boot.sh: BOOTED only when a mac2 session opens and the app
61
+ // has a window of its own that is not a system dialog (Lekhak TF-026: BOOTED was printed while no
62
+ // session could open, and once while the app's only window was macOS's "quit unexpectedly while
63
+ // reopening windows" dialog). That dialog is answered Don't Reopen, which only skips restoring old
64
+ // windows, and the answer is reported. The window alone is saved to `shot`. Never throws.
65
+ export async function firstScreen(url, { bundleId = '', appPath = '', shot = '', wait = 30000 } = {}) {
66
+ let s;
67
+ try { s = await openSession(url, { bundleId, appPath }); }
68
+ catch (e) { return { ok: false, stage: 'session', reason: e.message }; }
69
+ const sleep = (ms) => new Promise((ok) => setTimeout(ok, ms));
70
+ const DIALOG = new Set(['Dialog', 'Sheet', 'SystemDialog']);
71
+ const LEAF = new Set(['Button', 'TextField', 'SecureTextField', 'TextView', 'Image', 'CheckBox', 'Switch', 'Slider', 'PopUpButton',
72
+ 'ComboBox', 'Link', 'RadioButton', 'SegmentedControl', 'Stepper', 'DatePicker', 'ProgressIndicator', 'Cell']);
73
+ const out = { ok: false, stage: 'window', dismissed: '' };
74
+ try {
75
+ for (const t0 = Date.now(); Date.now() - t0 < wait; await sleep(500)) {
76
+ const nodes = parseSource(await s.source());
77
+ const label = (n) => n.title || n.label || '';
78
+ const reopen = nodes.find((n) => n.type === 'Button' && /^don.t reopen$/i.test(label(n)));
79
+ if (reopen && !out.dismissed) {
80
+ const el = await s.find('-ios predicate string', `elementType == 9 AND (title BEGINSWITH[c] 'Don' OR label BEGINSWITH[c] 'Don')`).catch(() => null);
81
+ if (el) { await s.click(el); out.dismissed = 'macOS asked to reopen the windows of a run that did not quit; answered Don\'t Reopen'; continue; }
82
+ }
83
+ const dialog = nodes.find((n) => DIALOG.has(n.type)) || reopen;
84
+ const win = nodes.find((n) => n.type === 'Window' && n.w >= 200 && n.h >= 150 && !descendants(nodes, n.i).some((d) => DIALOG.has(d.type)));
85
+ // drawn: text or a control in the window's content, as tf-verify-native counts it: what sits
86
+ // directly on the window (its title, its three buttons) and the bars are not content. A web view
87
+ // took about eight seconds to draw its first page; until then the window is blank.
88
+ const chrome = (n) => n.parent === win.i || ['TabBar', 'Toolbar', 'NavigationBar', 'MenuBar', 'Menu'].some((t) => {
89
+ for (let p = n.i; p >= 0; p = nodes[p].parent) if (nodes[p].type === t) return true;
90
+ return false; });
91
+ const drawn = win && descendants(nodes, win.i).some((n) => !chrome(n) && (n.type === 'StaticText' ? !!label(n).trim() || !!(n.value || '').trim() : LEAF.has(n.type)));
92
+ if (win && !dialog && drawn) {
93
+ out.ok = true; out.window = { w: win.w, h: win.h };
94
+ if (shot) {
95
+ await sleep(500); // the first frame of a web view fades in
96
+ // the window alone (TF-027: mac2's element shot is a crop of the display, banners included)
97
+ if (appPath && windowShot(appPid(appPath), shot)) out.shot = shot;
98
+ else {
99
+ const el = await s.find('-ios predicate string', 'elementType == 4').catch(() => null);
100
+ if (el) { const { writeFileSync } = await import('node:fs'); writeFileSync(shot, Buffer.from(await s.shot(el), 'base64')); out.shot = shot; }
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ out.reason = dialog ? `the app shows a dialog, not its first screen: "${(label(dialog) || dialog.type).slice(0, 80)}"`
106
+ : win ? 'the app\'s window is on view but blank (no text, no control)' : 'the app has no window of its own on view';
107
+ }
108
+ out.reason = `${out.reason || 'no window'} after ${Math.round(wait / 1000)} s`;
109
+ return out;
110
+ } catch (e) { return { ...out, reason: `Appium: ${e.message}` }; }
111
+ finally { await s.close(); }
112
+ }
113
+
114
+ // The page source (XCUIElementType… XML) as a flat list of nodes with their parent index, so a
115
+ // caller can walk a subtree without an XML package.
116
+ export function parseSource(xml) {
117
+ const un = (s) => s.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#10;/g, '\n').replace(/&amp;/g, '&');
118
+ const nodes = []; const stack = [];
119
+ const re = /<(\/?)(XCUIElementType\w+)([^>]*?)(\/?)>/g; let m;
120
+ while ((m = re.exec(xml))) {
121
+ if (m[1]) { stack.pop(); continue; }
122
+ const a = {}; let am; const are = /(\w+)="([^"]*)"/g;
123
+ while ((am = are.exec(m[3]))) a[am[1]] = un(am[2]);
124
+ const n = { i: nodes.length, type: m[2].replace('XCUIElementType', ''), id: a.identifier || '', label: a.label || '', value: a.value || '',
125
+ title: a.title || '', x: +a.x || 0, y: +a.y || 0, w: +a.width || 0, h: +a.height || 0, parent: stack.length ? stack[stack.length - 1] : -1, kids: [] };
126
+ if (n.parent >= 0) nodes[n.parent].kids.push(n.i);
127
+ nodes.push(n);
128
+ if (!m[4]) stack.push(n.i);
129
+ }
130
+ return nodes;
131
+ }
132
+
133
+ export function descendants(nodes, i) {
134
+ const out = []; const todo = [...nodes[i].kids];
135
+ while (todo.length) { const k = todo.shift(); out.push(nodes[k]); todo.push(...nodes[k].kids); }
136
+ return out;
137
+ }
138
+
139
+ // --- the app's own window, through macOS rather than mac2 (Lekhak TF-027) ---------------------------
140
+ // mac2 cannot resize a window, and its element screenshot is a crop of the display, so a notification
141
+ // banner over the window landed in every picture. macOS itself does both: System Events resizes the
142
+ // window (the app's minimum size still holds, and the size it really took is returned), and
143
+ // screencapture -l takes the window alone by its window number.
144
+ const run = (cmd, args) => { try { return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 20000 }).trim(); } catch { return ''; } };
145
+
146
+ // the pid of the app running from this .app (a mac2 session starts it again under a new pid)
147
+ export function appPid(appPath) {
148
+ const out = run('pgrep', ['-f', `${appPath.replace(/\/$/, '')}/Contents/MacOS/`]);
149
+ return parseInt(out.split('\n')[0], 10) || 0;
150
+ }
151
+
152
+ // { id, x, y, w, h } of the app's largest normal window on view, or null
153
+ export function macWindow(pid) {
154
+ if (!pid) return null;
155
+ const js = `ObjC.import('CoreGraphics');
156
+ const l = ObjC.deepUnwrap(ObjC.castRefToObject($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly | $.kCGWindowListExcludeDesktopElements, 0)));
157
+ JSON.stringify(l.filter((w) => w.kCGWindowOwnerPID == ${pid} && w.kCGWindowLayer == 0)
158
+ .map((w) => ({ id: w.kCGWindowNumber, x: w.kCGWindowBounds.X, y: w.kCGWindowBounds.Y, w: w.kCGWindowBounds.Width, h: w.kCGWindowBounds.Height })))`;
159
+ try { return JSON.parse(run('osascript', ['-l', 'JavaScript', '-e', js]) || '[]').sort((a, b) => b.w * b.h - a.w * a.h)[0] || null; }
160
+ catch { return null; }
161
+ }
162
+
163
+ // sets the front window's size; returns the size it took ({ w, h }), or null when macOS refused
164
+ // (the terminal needs Accessibility, as mac2 does)
165
+ export function resizeWindow(pid, w, h) {
166
+ if (!pid) return null;
167
+ const tell = `tell application "System Events" to tell (first process whose unix id is ${pid})`;
168
+ run('osascript', ['-e', `${tell} to set size of window 1 to {${w}, ${h}}`]);
169
+ const got = run('osascript', ['-e', `${tell} to get size of window 1`]).split(/,\s*/).map(Number);
170
+ return got.length === 2 && got.every((n) => n > 0) ? { w: got[0], h: got[1] } : null;
171
+ }
172
+
173
+ // the window alone, by its window number; true when the file was written
174
+ export function windowShot(pid, file) {
175
+ const win = macWindow(pid);
176
+ if (!win) return false;
177
+ run('screencapture', ['-x', '-o', '-l', String(win.id), file]);
178
+ try { return statSync(file).size > 0; } catch { return false; }
179
+ }
@@ -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 }));