@techierathore/techieflow 1.0.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 (151) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
  2. package/.claude/commands/TechieFlow/agents/architect.md +84 -0
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
  9. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
  11. package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
  12. package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
  13. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
  14. package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
  17. package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
  18. package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
  19. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
  20. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
  21. package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
  22. package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
  23. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
  24. package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
  25. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
  26. package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
  27. package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
  28. package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
  29. package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
  30. package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
  31. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
  32. package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
  33. package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
  34. package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
  35. package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
  36. package/.claude/commands/generate-html.md +124 -0
  37. package/.codex/config.toml +46 -0
  38. package/.codex/hooks.json +63 -0
  39. package/.codex/rules/techieflow.rules +8 -0
  40. package/.opencode/command/generate-html.md +129 -0
  41. package/.opencode/plugin/techieflow.js +350 -0
  42. package/.tfcore/TOKEN-GUIDE.md +59 -0
  43. package/.tfcore/agents/analyst.md +97 -0
  44. package/.tfcore/agents/architect.md +84 -0
  45. package/.tfcore/agents/flow-master.md +217 -0
  46. package/.tfcore/agents/verifier.md +72 -0
  47. package/.tfcore/checklists/architect-checklist.md +438 -0
  48. package/.tfcore/core-config.yaml +47 -0
  49. package/.tfcore/data/brainstorming-techniques.md +36 -0
  50. package/.tfcore/data/elicitation-methods.md +154 -0
  51. package/.tfcore/data/techieflow-kb.md +807 -0
  52. package/.tfcore/data/technical-preferences.md +3 -0
  53. package/.tfcore/enhanced-ide-development-workflow.md +248 -0
  54. package/.tfcore/hooks/block-git.sh +301 -0
  55. package/.tfcore/hooks/codex-adapter.py +248 -0
  56. package/.tfcore/hooks/guard-artifacts.sh +101 -0
  57. package/.tfcore/hooks/guard-status-html.sh +102 -0
  58. package/.tfcore/hooks/guard-status.sh +124 -0
  59. package/.tfcore/hooks/guard-verify.sh +108 -0
  60. package/.tfcore/hooks/metrics-session.sh +129 -0
  61. package/.tfcore/hooks/session-pointer.sh +52 -0
  62. package/.tfcore/hooks/sweep-artifacts.sh +198 -0
  63. package/.tfcore/install-manifest.yaml +231 -0
  64. package/.tfcore/routing.yaml +105 -0
  65. package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
  66. package/.tfcore/tasks/_smoke-test-policy.md +98 -0
  67. package/.tfcore/tasks/_status-update-gate.md +78 -0
  68. package/.tfcore/tasks/_yolo-mode.md +77 -0
  69. package/.tfcore/tasks/advanced-elicitation.md +117 -0
  70. package/.tfcore/tasks/amend-docs.md +149 -0
  71. package/.tfcore/tasks/author-brd.md +79 -0
  72. package/.tfcore/tasks/build-phase.md +231 -0
  73. package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
  74. package/.tfcore/tasks/create-doc.md +101 -0
  75. package/.tfcore/tasks/day1-brownfield.md +507 -0
  76. package/.tfcore/tasks/day1-greenfield.md +224 -0
  77. package/.tfcore/tasks/devguide.md +224 -0
  78. package/.tfcore/tasks/document-project.md +343 -0
  79. package/.tfcore/tasks/execute-checklist.md +86 -0
  80. package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
  81. package/.tfcore/tasks/fix-issues.md +134 -0
  82. package/.tfcore/tasks/generate-html.md +115 -0
  83. package/.tfcore/tasks/handoff-phase.md +126 -0
  84. package/.tfcore/tasks/index-docs.md +173 -0
  85. package/.tfcore/tasks/kb-mode-interaction.md +75 -0
  86. package/.tfcore/tasks/log-miss.md +160 -0
  87. package/.tfcore/tasks/metrics-report.md +143 -0
  88. package/.tfcore/tasks/mockups.md +122 -0
  89. package/.tfcore/tasks/productguide.md +105 -0
  90. package/.tfcore/tasks/refresh-status.md +135 -0
  91. package/.tfcore/tasks/render-workflow-docs.md +117 -0
  92. package/.tfcore/tasks/shard-doc.md +185 -0
  93. package/.tfcore/tasks/split-brd.md +131 -0
  94. package/.tfcore/tasks/triage-issues.md +157 -0
  95. package/.tfcore/tasks/verify-phase.md +579 -0
  96. package/.tfcore/telemetry/SCHEMA.md +678 -0
  97. package/.tfcore/telemetry/install-metrics.sh +390 -0
  98. package/.tfcore/telemetry/pre-commit +94 -0
  99. package/.tfcore/telemetry/tf-metrics.sh +1415 -0
  100. package/.tfcore/templates/architecture-tmpl.yaml +650 -0
  101. package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
  102. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
  103. package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
  104. package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
  105. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
  106. package/.tfcore/templates/market-research-tmpl.yaml +252 -0
  107. package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
  108. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
  109. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
  110. package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
  111. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
  112. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
  113. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
  114. package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
  115. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
  116. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
  117. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
  118. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
  119. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
  120. package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
  121. package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
  122. package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
  123. package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
  124. package/.tfcore/user-guide.md +633 -0
  125. package/.tfcore/utils/techieflow-doc-template.md +325 -0
  126. package/.tfcore/utils/tf-assets.sh +310 -0
  127. package/.tfcore/utils/tf-codex-bind.py +196 -0
  128. package/.tfcore/utils/tf-codex-telemetry.py +60 -0
  129. package/.tfcore/utils/tf-emit.sh +1075 -0
  130. package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
  131. package/.tfcore/utils/tf-goal.sh +421 -0
  132. package/.tfcore/utils/tf-harness.sh +160 -0
  133. package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
  134. package/.tfcore/utils/tf-mockup-parity.sh +65 -0
  135. package/.tfcore/utils/tf-opencode-audit.py +126 -0
  136. package/.tfcore/utils/tf-perf.sh +241 -0
  137. package/.tfcore/utils/tf-render-html.py +715 -0
  138. package/.tfcore/utils/tf-render-html.sh +28 -0
  139. package/.tfcore/utils/tf-routing-bind.sh +216 -0
  140. package/.tfcore/utils/tf-routing.sh +247 -0
  141. package/.tfcore/utils/tf-yolo.sh +119 -0
  142. package/.tfcore/working-in-the-brownfield.md +606 -0
  143. package/LICENSE +201 -0
  144. package/README.md +1212 -0
  145. package/WORKFLOW.html +1986 -0
  146. package/docs/TechieFlow-Installation.md +299 -0
  147. package/opencode.jsonc +929 -0
  148. package/package.json +60 -0
  149. package/scripts/install.mjs +992 -0
  150. package/scripts/test-install.mjs +505 -0
  151. package/scripts/validate.mjs +155 -0
@@ -0,0 +1,325 @@
1
+ # TechieFlow Document Template Specification
2
+
3
+ ## Overview
4
+
5
+ TechieFlow document templates are defined in YAML format to drive interactive document generation and agent interaction. Templates separate structure definition from content generation, making them both human and LLM-agent-friendly.
6
+
7
+ ## Template Structure
8
+
9
+ ```yaml
10
+ template:
11
+ id: template-identifier
12
+ name: Human Readable Template Name
13
+ version: 1.0
14
+ output:
15
+ format: markdown
16
+ filename: default-path/to/{{filename}}.md
17
+ title: '{{variable}} Document Title'
18
+
19
+ workflow:
20
+ mode: interactive
21
+ elicitation: advanced-elicitation
22
+
23
+ sections:
24
+ - id: section-id
25
+ title: Section Title
26
+ instruction: |
27
+ Detailed instructions for the LLM on how to handle this section
28
+ # ... additional section properties
29
+ ```
30
+
31
+ ## Core Fields
32
+
33
+ ### Template Metadata
34
+
35
+ - **id**: Unique identifier for the template
36
+ - **name**: Human-readable name displayed in UI
37
+ - **version**: Template version for tracking changes
38
+ - **output.format**: Default "markdown" for document templates
39
+ - **output.filename**: Default output file path (can include variables)
40
+ - **output.title**: Document title (becomes H1 in markdown)
41
+
42
+ ### Workflow Configuration
43
+
44
+ - **workflow.mode**: Default interaction mode ("interactive" or "yolo")
45
+ - **workflow.elicitation**: Elicitation task to use ("advanced-elicitation")
46
+
47
+ ## Section Properties
48
+
49
+ ### Required Fields
50
+
51
+ - **id**: Unique section identifier
52
+ - **title**: Section heading text
53
+ - **instruction**: Detailed guidance for LLM on handling this section
54
+
55
+ ### Optional Fields
56
+
57
+ #### Content Control
58
+
59
+ - **type**: Content type hint for structured sections
60
+ - **template**: Fixed template text for section content
61
+ - **item_template**: Template for repeatable items within section
62
+ - **prefix**: Prefix for numbered items (e.g., "FR", "NFR")
63
+
64
+ #### Behavior Flags
65
+
66
+ - **elicit**: Boolean - Apply elicitation after section rendered
67
+ - **repeatable**: Boolean - Section can be repeated multiple times
68
+ - **condition**: String - Condition for including section (e.g., "has ui requirements")
69
+
70
+ #### Agent Permissions
71
+
72
+ - **owner**: String - Agent role that initially creates/populates this section
73
+ - **editors**: Array - List of agent roles allowed to modify this section
74
+ - **readonly**: Boolean - Section cannot be modified after initial creation
75
+
76
+ #### Content Guidance
77
+
78
+ - **examples**: Array of example content (not included in output)
79
+ - **choices**: Object with choice options for common decisions
80
+ - **placeholder**: Default placeholder text
81
+
82
+ #### Structure
83
+
84
+ - **sections**: Array of nested child sections
85
+
86
+ ## Supported Types
87
+
88
+ ### Content Types
89
+
90
+ - **bullet-list**: Unordered list items
91
+ - **numbered-list**: Ordered list with optional prefix
92
+ - **paragraphs**: Free-form paragraph text
93
+ - **table**: Structured table data
94
+ - **code-block**: Code or configuration blocks
95
+ - **template-text**: Fixed template with variable substitution
96
+ - **mermaid**: Mermaid diagram with specified type and details
97
+
98
+ ### Special Types
99
+
100
+ - **repeatable-container**: Container for multiple instances
101
+ - **conditional-block**: Content shown based on conditions
102
+ - **choice-selector**: Present choices to user
103
+
104
+ ## Advanced Features
105
+
106
+ ### Variable Substitution
107
+
108
+ Use `{{variable_name}}` in titles, templates, and content:
109
+
110
+ ```yaml
111
+ title: 'Epic {{epic_number}} {{epic_title}}'
112
+ template: 'As a {{user_type}}, I want {{action}}, so that {{benefit}}.'
113
+ ```
114
+
115
+ ### Conditional Sections
116
+
117
+ ```yaml
118
+ - id: ui-section
119
+ title: User Interface Design
120
+ condition: Project has UX/UI Requirements
121
+ instruction: Only include if project has UI components
122
+ ```
123
+
124
+ ### Choice Integration
125
+
126
+ ```yaml
127
+ choices:
128
+ architecture: [Monolith, Microservices, Serverless]
129
+ testing: [Unit Only, Unit + Integration, Full Pyramid]
130
+ ```
131
+
132
+ ### Mermaid Diagrams
133
+
134
+ ```yaml
135
+ - id: system-architecture
136
+ title: System Architecture Diagram
137
+ type: mermaid
138
+ instruction: Create a system architecture diagram showing key components and data flow
139
+ mermaid_type: flowchart
140
+ details: |
141
+ Show the following components:
142
+ - User interface layer
143
+ - API gateway
144
+ - Core services
145
+ - Database layer
146
+ - External integrations
147
+ ```
148
+
149
+ **Supported mermaid_type values:**
150
+
151
+ **Core Diagram Types:**
152
+
153
+ - `flowchart` - Flow charts and process diagrams
154
+ - `sequenceDiagram` - Sequence diagrams for interactions
155
+ - `classDiagram` - Class relationship diagrams (UML)
156
+ - `stateDiagram` - State transition diagrams
157
+ - `erDiagram` - Entity relationship diagrams
158
+ - `gantt` - Gantt charts for timelines
159
+ - `pie` - Pie charts for data visualization
160
+
161
+ **Advanced Diagram Types:**
162
+
163
+ - `journey` - User journey maps
164
+ - `mindmap` - Mindmaps for brainstorming
165
+ - `timeline` - Timeline diagrams for chronological events
166
+ - `quadrantChart` - Quadrant charts for data categorization
167
+ - `xyChart` - XY charts (bar charts, line charts)
168
+ - `sankey` - Sankey diagrams for flow visualization
169
+
170
+ **Specialized Types:**
171
+
172
+ - `c4Context` - C4 context diagrams (experimental)
173
+ - `requirement` - Requirement diagrams
174
+ - `packet` - Network packet diagrams
175
+ - `block` - Block diagrams
176
+ - `kanban` - Kanban boards
177
+
178
+ ### Agent Permissions Example
179
+
180
+ ```yaml
181
+ - id: story-details
182
+ title: Story
183
+ owner: scrum-master
184
+ editors: [scrum-master]
185
+ readonly: false
186
+ sections:
187
+ - id: dev-notes
188
+ title: Dev Notes
189
+ owner: dev-agent
190
+ editors: [dev-agent]
191
+ readonly: false
192
+ instruction: Implementation notes and technical details
193
+ - id: qa-results
194
+ title: QA Results
195
+ owner: qa-agent
196
+ editors: [qa-agent]
197
+ readonly: true
198
+ instruction: Quality assurance test results
199
+ ```
200
+
201
+ ### Repeatable Sections
202
+
203
+ ```yaml
204
+ - id: epic-details
205
+ title: Epic {{epic_number}} {{epic_title}}
206
+ repeatable: true
207
+ sections:
208
+ - id: story
209
+ title: Story {{epic_number}}.{{story_number}} {{story_title}}
210
+ repeatable: true
211
+ sections:
212
+ - id: criteria
213
+ title: Acceptance Criteria
214
+ type: numbered-list
215
+ item_template: '{{criterion_number}}: {{criteria}}'
216
+ repeatable: true
217
+ ```
218
+
219
+ ### Examples with Code Blocks
220
+
221
+ ````yaml
222
+ examples:
223
+ - 'FR6: The system must authenticate users within 2 seconds'
224
+ - |
225
+ ```mermaid
226
+ sequenceDiagram
227
+ participant User
228
+ participant API
229
+ participant DB
230
+ User->>API: POST /login
231
+ API->>DB: Validate credentials
232
+ DB-->>API: User data
233
+ API-->>User: JWT token
234
+ ```
235
+ - |
236
+ **Architecture Decision Record**
237
+
238
+ **Decision**: Use PostgreSQL for primary database
239
+ **Rationale**: ACID compliance and JSON support needed
240
+ **Consequences**: Requires database management expertise
241
+ ````
242
+
243
+ ## Section Hierarchy
244
+
245
+ Templates define the complete document structure starting with the first H2 - each level in is the next H#:
246
+
247
+ ```yaml
248
+ sections:
249
+ - id: overview
250
+ title: Project Overview
251
+ sections:
252
+ - id: goals
253
+ title: Goals
254
+ - id: scope
255
+ title: Scope
256
+ sections:
257
+ - id: in-scope
258
+ title: In Scope
259
+ - id: out-scope
260
+ title: Out of Scope
261
+ ```
262
+
263
+ ## Processing Flow
264
+
265
+ 1. **Parse Template**: Load and validate YAML structure
266
+ 2. **Initialize Workflow**: Set interaction mode and elicitation
267
+ 3. **Process Sections**: Handle each section in order:
268
+ - Check conditions
269
+ - Apply instructions
270
+ - Generate content
271
+ - Handle choices and variables
272
+ - Apply elicitation if specified
273
+ - Process nested sections
274
+ 4. **Generate Output**: Create clean markdown document
275
+
276
+ ## Best Practices
277
+
278
+ ### Template Design
279
+
280
+ - Keep instructions clear and specific
281
+ - Use examples for complex content
282
+ - Structure sections logically
283
+ - Include all necessary guidance for LLM
284
+
285
+ ### Content Instructions
286
+
287
+ - Be explicit about expected format
288
+ - Include reasoning for decisions
289
+ - Specify interaction patterns
290
+ - Reference other documents when needed
291
+
292
+ ### Variable Naming
293
+
294
+ - Use descriptive variable names
295
+ - Follow consistent naming conventions
296
+ - Document expected variable values
297
+
298
+ ### Examples Usage
299
+
300
+ - Provide concrete examples for complex sections
301
+ - Include both simple and complex cases
302
+ - Use realistic project scenarios
303
+ - Include code blocks and diagrams when helpful
304
+
305
+ ## Validation
306
+
307
+ Templates should be validated for:
308
+
309
+ - Valid YAML syntax
310
+ - Required fields present
311
+ - Consistent section IDs
312
+ - Proper nesting structure
313
+ - Valid variable references
314
+
315
+ ## Migration from Legacy
316
+
317
+ When converting from markdown+frontmatter templates:
318
+
319
+ 1. Extract embedded `[[LLM:]]` instructions to `instruction` fields
320
+ 2. Convert `<<REPEAT>>` blocks to `repeatable: true` sections
321
+ 3. Extract `^^CONDITIONS^^` to `condition` fields
322
+ 4. Move `@{examples}` to `examples` arrays
323
+ 5. Convert `{{placeholders}}` to proper variable syntax
324
+
325
+ This specification ensures templates are both human-readable and machine-processable while maintaining the flexibility needed for complex document generation.
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow — asset-integrity harness. Feeds verify-phase §4a2 (the assets gate).
3
+ #
4
+ # bash .tfcore/utils/tf-assets.sh --base http://localhost:5099 \
5
+ # --paths "/,/login,/export" --cookie 'AuthCookie=<value>'
6
+ #
7
+ # WHAT IT ASKS, and it is the only question no other gate asks:
8
+ #
9
+ # "Did the assets this page DECLARES actually arrive?"
10
+ #
11
+ # WHY IT EXISTS (TfLens TF-007, 2026-08-28). A build reporting 140 of 143 `Verified`
12
+ # — acceptance, data-render, visual-truth, standards, parity and perf all green —
13
+ # shipped a `/login` that rendered as an unstyled single column, because one
14
+ # stylesheet (a Blazor scoped-CSS bundle carrying 100% of that page's layout) 404ed.
15
+ # Every gate passed, and none of them COULD have caught it:
16
+ #
17
+ # acceptance every control was present and every behavioural assertion held.
18
+ # An unstyled page behaves correctly.
19
+ # render §4a "does this control carry non-placeholder text?" — yes. Text renders
20
+ # fine with no CSS at all.
21
+ # visual §4b "do these boxes overlap / clip / leave the viewport?" — no. A single
22
+ # stacked column overlaps nothing. It is the TIDIEST possible failure:
23
+ # partial breakage overlaps, complete breakage stacks neatly.
24
+ # standards file-level; never loads the app.
25
+ # perf an unstyled page is, if anything, faster.
26
+ #
27
+ # A 404 on a <link rel=stylesheet> or a <script src> produces no console error the
28
+ # gates read, no server log line, no Blazor error boundary and no failed assertion.
29
+ # The app renders something that looks intentional. The check is generic — it needs
30
+ # no knowledge of the app, only the rendered document — which is why it belongs in
31
+ # the framework rather than being re-written in every project.
32
+ #
33
+ # WHAT IT IS NOT: a verdict. Like tf-perf.sh it produces findings, never a status.
34
+ # verify-phase §4a2 decides what a finding means for a REQ. A measurement tool that
35
+ # also judges is a tool you cannot audit when a result looks wrong.
36
+ #
37
+ # SAME-ORIGIN BY DEFAULT, and this is deliberate. A CDN that is briefly slow or
38
+ # rate-limited would fail the gate on every screen for a reason that is not the
39
+ # app's defect — and a gate that cries wolf teaches the owner to ignore verdicts,
40
+ # which costs more than the defects it catches (verify-phase §4c states the same
41
+ # rule for perf). Cross-origin assets are still LISTED, marked `"external":true`,
42
+ # and only graded when you pass --include-external.
43
+ #
44
+ # Exit codes: 0 every declared asset arrived · 2 base unreachable (that is a `build`
45
+ # gate problem, not an assets result) · 3 bad arguments
46
+ # · 4 every path redirected (auth wall — nothing was graded; present a
47
+ # session with --cookie / --header and re-run)
48
+ # · 5 at least one declared asset did not arrive -> the gate FAILS.
49
+ # Requires python3 only.
50
+
51
+ set -uo pipefail
52
+
53
+ BASE=""; PATHS="/"; TIMEOUT=20; LABEL=""; OUT=""; EXTERNAL=0; MINBYTES=1
54
+ HDRS="" # newline-separated "K: V" pairs (--header, repeatable; --cookie is sugar)
55
+ while [[ $# -gt 0 ]]; do
56
+ case "$1" in
57
+ --base) BASE="${2:-}"; shift 2 ;;
58
+ --paths) PATHS="${2:-}"; shift 2 ;;
59
+ --timeout) TIMEOUT="${2:-}"; shift 2 ;;
60
+ --label) LABEL="${2:-}"; shift 2 ;;
61
+ --json-out) OUT="${2:-}"; shift 2 ;;
62
+ --min-bytes) MINBYTES="${2:-}"; shift 2 ;;
63
+ --include-external) EXTERNAL=1; shift ;;
64
+ --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 ;;
67
+ *) echo "unknown argument: $1" >&2; exit 3 ;;
68
+ esac
69
+ done
70
+
71
+ [[ -n "$BASE" ]] || { printf '%s\n' \
72
+ "usage: tf-assets.sh --base URL [--paths /,/login] [--cookie 'k=v'] [--include-external]" \
73
+ " [--json-out tests/.artifacts/assets/REQ-NFR-015.json]" >&2; exit 3; }
74
+ command -v python3 >/dev/null 2>&1 || { echo '{"status":"no-python3"}'; exit 3; }
75
+
76
+ TF_BASE="$BASE" TF_PATHS="$PATHS" TF_TIMEOUT="$TIMEOUT" TF_LABEL="$LABEL" TF_OUT="$OUT" \
77
+ TF_HEADERS="$HDRS" TF_EXTERNAL="$EXTERNAL" TF_MINBYTES="$MINBYTES" python3 - <<'PY'
78
+ import html.parser, json, os, ssl, sys, urllib.error, urllib.request
79
+ from urllib.parse import urljoin, urlsplit
80
+
81
+ BASE = os.environ["TF_BASE"].rstrip("/")
82
+ PATHS = [p.strip() for p in os.environ["TF_PATHS"].split(",") if p.strip()]
83
+ TIMEOUT = float(os.environ["TF_TIMEOUT"])
84
+ LABEL = os.environ.get("TF_LABEL") or None
85
+ OUT = os.environ.get("TF_OUT") or None
86
+ EXTERNAL = os.environ.get("TF_EXTERNAL") == "1"
87
+ MINBYTES = int(os.environ.get("TF_MINBYTES") or 1)
88
+
89
+ HEADERS = []
90
+ for line in (os.environ.get("TF_HEADERS") or "").splitlines():
91
+ if ":" in line:
92
+ k, _, v = line.partition(":")
93
+ HEADERS.append((k.strip(), v.strip()))
94
+
95
+ # A dev cert in WSL is routinely untrusted; refusing to look would make the gate
96
+ # unusable on exactly the local https head it is meant to check. This harness reads
97
+ # public assets off a machine-local dev server — it is not a security boundary.
98
+ CTX = ssl.create_default_context()
99
+ CTX.check_hostname = False
100
+ CTX.verify_mode = ssl.CERT_NONE
101
+
102
+ UA = "TechieFlow-tf-assets/1.0"
103
+
104
+
105
+ class AssetParser(html.parser.HTMLParser):
106
+ """Collect what the DOCUMENT DECLARES — not what a browser eventually loaded.
107
+
108
+ That distinction is the whole point. A headless browser check would report the
109
+ assets that *succeeded*; a 404ed stylesheet simply would not be in the list, so
110
+ the failure would be invisible in exactly the way TF-007 describes. Reading the
111
+ declaration and then fetching each one is what makes the absence observable.
112
+
113
+ Honours <base href>, because a page that sets one and an asset resolver that
114
+ ignores it disagree about every relative URL on the page."""
115
+
116
+ def __init__(self):
117
+ super().__init__(convert_charrefs=True)
118
+ self.base_href = None
119
+ self.assets = [] # (kind, raw_url)
120
+
121
+ def handle_starttag(self, tag, attrs):
122
+ a = {k.lower(): (v or "") for k, v in attrs}
123
+ if tag == "base" and a.get("href") and self.base_href is None:
124
+ self.base_href = a["href"]
125
+ elif tag == "link":
126
+ rel = " ".join(a.get("rel", "").lower().split())
127
+ href = a.get("href", "").strip()
128
+ if not href:
129
+ return
130
+ if "stylesheet" in rel:
131
+ self.assets.append(("stylesheet", href))
132
+ elif "preload" in rel and a.get("as", "").lower() in ("style", "script", "font"):
133
+ # A preload that 404s is a real miss on a page that then depends on it.
134
+ self.assets.append(("preload-" + a["as"].lower(), href))
135
+ elif "modulepreload" in rel:
136
+ self.assets.append(("modulepreload", href))
137
+ elif "icon" in rel:
138
+ self.assets.append(("icon", href))
139
+ elif tag == "script":
140
+ src = a.get("src", "").strip()
141
+ if src:
142
+ self.assets.append(("script", src))
143
+
144
+
145
+ def fetch(url, want_body=True):
146
+ """Return (status, nbytes, error). Never raises — a transport failure is a
147
+ finding, not a crash, and the caller needs every path's result."""
148
+ req = urllib.request.Request(url, headers={"User-Agent": UA})
149
+ for k, v in HEADERS:
150
+ req.add_header(k, v)
151
+ try:
152
+ with urllib.request.urlopen(req, timeout=TIMEOUT, context=CTX) as r:
153
+ body = r.read() if want_body else b""
154
+ return r.status, len(body), None, body
155
+ except urllib.error.HTTPError as e:
156
+ try:
157
+ body = e.read()
158
+ except Exception:
159
+ body = b""
160
+ return e.code, len(body), None, body
161
+ except Exception as e:
162
+ return None, 0, "%s: %s" % (type(e).__name__, e), b""
163
+
164
+
165
+ class NoRedirect(urllib.request.HTTPRedirectHandler):
166
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
167
+ return None # surface the 3xx instead of following it
168
+
169
+
170
+ REDIRECT_OPENER = urllib.request.build_opener(
171
+ NoRedirect, urllib.request.HTTPSHandler(context=CTX))
172
+
173
+
174
+ def fetch_page(url):
175
+ """Pages are fetched WITHOUT following redirects, so an auth wall is visible as
176
+ an auth wall rather than as a successfully-loaded /login (which has its own,
177
+ perfectly healthy assets — the exact way a gate reports green on a page nobody
178
+ asked about). Same lesson as tf-perf.sh's exit 4."""
179
+ req = urllib.request.Request(url, headers={"User-Agent": UA})
180
+ for k, v in HEADERS:
181
+ req.add_header(k, v)
182
+ try:
183
+ with REDIRECT_OPENER.open(req, timeout=TIMEOUT) as r:
184
+ return r.status, r.read(), None
185
+ except urllib.error.HTTPError as e:
186
+ try:
187
+ body = e.read()
188
+ except Exception:
189
+ body = b""
190
+ return e.code, body, None
191
+ except Exception as e:
192
+ return None, b"", "%s: %s" % (type(e).__name__, e)
193
+
194
+
195
+ # --- reachability first: an unreachable base is a `build` problem, not an assets one
196
+ probe_status, _, probe_err = fetch_page(BASE + "/")
197
+ if probe_status is None:
198
+ print(json.dumps({"status": "unreachable", "base": BASE, "error": probe_err,
199
+ "label": LABEL, "note": "app not reachable — this is a `build` "
200
+ "gate problem, not an assets result"}, indent=2))
201
+ raise SystemExit(2)
202
+
203
+ origin = urlsplit(BASE)
204
+ pages, findings = [], []
205
+ redirected = 0
206
+ graded_assets = 0
207
+
208
+ for p in PATHS:
209
+ page_url = BASE + (p if p.startswith("/") else "/" + p)
210
+ status, body, err = fetch_page(page_url)
211
+ if status is None:
212
+ pages.append({"path": p, "status": None, "error": err, "assets": [],
213
+ "declared": 0, "graded": 0, "failed": 0})
214
+ findings.append({"path": p, "kind": "page", "url": page_url,
215
+ "problem": "unreachable", "detail": err})
216
+ continue
217
+ if 300 <= status < 400:
218
+ redirected += 1
219
+ pages.append({"path": p, "status": status, "assets": [], "declared": 0,
220
+ "graded": 0, "failed": 0, "redirected": True})
221
+ continue
222
+ if status != 200:
223
+ pages.append({"path": p, "status": status, "assets": [], "declared": 0,
224
+ "graded": 0, "failed": 0})
225
+ findings.append({"path": p, "kind": "page", "url": page_url,
226
+ "problem": "non-200", "detail": status})
227
+ continue
228
+
229
+ parser = AssetParser()
230
+ try:
231
+ parser.feed(body.decode("utf-8", "replace"))
232
+ except Exception:
233
+ pass
234
+ doc_base = urljoin(page_url, parser.base_href) if parser.base_href else page_url
235
+
236
+ seen, rows, failed = set(), [], 0
237
+ for kind, raw in parser.assets:
238
+ if raw.startswith(("data:", "blob:", "javascript:", "#")):
239
+ continue
240
+ url = urljoin(doc_base, raw)
241
+ if url in seen:
242
+ continue
243
+ seen.add(url)
244
+ u = urlsplit(url)
245
+ is_external = (u.scheme, u.netloc) != (origin.scheme, origin.netloc)
246
+ row = {"kind": kind, "url": url, "external": is_external}
247
+ if is_external and not EXTERNAL:
248
+ row["graded"] = False
249
+ row["note"] = "cross-origin — not graded (pass --include-external to grade it)"
250
+ rows.append(row)
251
+ continue
252
+ st, n, ferr, _ = fetch(url)
253
+ row.update({"graded": True, "status": st, "bytes": n})
254
+ graded_assets += 1
255
+ problem = None
256
+ if ferr:
257
+ problem, row["error"] = "unreachable", ferr
258
+ elif st != 200:
259
+ problem = "status-%s" % st
260
+ elif n < MINBYTES:
261
+ problem = "empty-body"
262
+ if problem:
263
+ failed += 1
264
+ row["problem"] = problem
265
+ findings.append({"path": p, "kind": kind, "url": url,
266
+ "problem": problem, "status": st, "bytes": n,
267
+ "external": is_external})
268
+ rows.append(row)
269
+
270
+ pages.append({"path": p, "status": status, "declared": len(rows),
271
+ "graded": sum(1 for r in rows if r.get("graded")),
272
+ "failed": failed, "assets": rows})
273
+
274
+ # Every path turned away = nothing was graded. Reporting "0 findings" there would be
275
+ # a PASS on a screen the harness never saw — the same false green tf-perf.sh's exit 4
276
+ # exists to prevent, and the same shape as TF-011's ungradeable-looks-clean failure.
277
+ 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."}
282
+ print(json.dumps(out, indent=2))
283
+ if OUT:
284
+ os.makedirs(os.path.dirname(os.path.abspath(OUT)), exist_ok=True)
285
+ open(OUT, "w", encoding="utf-8").write(json.dumps(out, indent=2))
286
+ raise SystemExit(4)
287
+
288
+ out = {
289
+ "status": "measured" if not findings else "failed",
290
+ "base": BASE, "label": LABEL,
291
+ "paths_checked": len(PATHS),
292
+ "paths_redirected": redirected,
293
+ "assets_graded": graded_assets,
294
+ "findings_n": len(findings),
295
+ "findings": findings,
296
+ "pages": pages,
297
+ "include_external": EXTERNAL,
298
+ # An assets run that graded nothing is UNGRADEABLE, not clean. Said out loud in
299
+ # the payload so a consumer cannot read `findings_n: 0` as coverage.
300
+ "ungradeable": graded_assets == 0,
301
+ }
302
+ if graded_assets == 0:
303
+ out["note"] = ("no asset was graded — the pages declared none, or all of them "
304
+ "were cross-origin. This is ASSETS-UNGRADEABLE, never a pass.")
305
+ print(json.dumps(out, indent=2))
306
+ if OUT:
307
+ os.makedirs(os.path.dirname(os.path.abspath(OUT)), exist_ok=True)
308
+ open(OUT, "w", encoding="utf-8").write(json.dumps(out, indent=2))
309
+ raise SystemExit(5 if findings else 0)
310
+ PY