@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.
- package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
- package/.claude/commands/TechieFlow/agents/architect.md +84 -0
- package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
- package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
- package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
- package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
- package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
- package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
- package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
- package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
- package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
- package/.claude/commands/generate-html.md +124 -0
- package/.codex/config.toml +46 -0
- package/.codex/hooks.json +63 -0
- package/.codex/rules/techieflow.rules +8 -0
- package/.opencode/command/generate-html.md +129 -0
- package/.opencode/plugin/techieflow.js +350 -0
- package/.tfcore/TOKEN-GUIDE.md +59 -0
- package/.tfcore/agents/analyst.md +97 -0
- package/.tfcore/agents/architect.md +84 -0
- package/.tfcore/agents/flow-master.md +217 -0
- package/.tfcore/agents/verifier.md +72 -0
- package/.tfcore/checklists/architect-checklist.md +438 -0
- package/.tfcore/core-config.yaml +47 -0
- package/.tfcore/data/brainstorming-techniques.md +36 -0
- package/.tfcore/data/elicitation-methods.md +154 -0
- package/.tfcore/data/techieflow-kb.md +807 -0
- package/.tfcore/data/technical-preferences.md +3 -0
- package/.tfcore/enhanced-ide-development-workflow.md +248 -0
- package/.tfcore/hooks/block-git.sh +301 -0
- package/.tfcore/hooks/codex-adapter.py +248 -0
- package/.tfcore/hooks/guard-artifacts.sh +101 -0
- package/.tfcore/hooks/guard-status-html.sh +102 -0
- package/.tfcore/hooks/guard-status.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +108 -0
- package/.tfcore/hooks/metrics-session.sh +129 -0
- package/.tfcore/hooks/session-pointer.sh +52 -0
- package/.tfcore/hooks/sweep-artifacts.sh +198 -0
- package/.tfcore/install-manifest.yaml +231 -0
- package/.tfcore/routing.yaml +105 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
- package/.tfcore/tasks/_smoke-test-policy.md +98 -0
- package/.tfcore/tasks/_status-update-gate.md +78 -0
- package/.tfcore/tasks/_yolo-mode.md +77 -0
- package/.tfcore/tasks/advanced-elicitation.md +117 -0
- package/.tfcore/tasks/amend-docs.md +149 -0
- package/.tfcore/tasks/author-brd.md +79 -0
- package/.tfcore/tasks/build-phase.md +231 -0
- package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
- package/.tfcore/tasks/create-doc.md +101 -0
- package/.tfcore/tasks/day1-brownfield.md +507 -0
- package/.tfcore/tasks/day1-greenfield.md +224 -0
- package/.tfcore/tasks/devguide.md +224 -0
- package/.tfcore/tasks/document-project.md +343 -0
- package/.tfcore/tasks/execute-checklist.md +86 -0
- package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
- package/.tfcore/tasks/fix-issues.md +134 -0
- package/.tfcore/tasks/generate-html.md +115 -0
- package/.tfcore/tasks/handoff-phase.md +126 -0
- package/.tfcore/tasks/index-docs.md +173 -0
- package/.tfcore/tasks/kb-mode-interaction.md +75 -0
- package/.tfcore/tasks/log-miss.md +160 -0
- package/.tfcore/tasks/metrics-report.md +143 -0
- package/.tfcore/tasks/mockups.md +122 -0
- package/.tfcore/tasks/productguide.md +105 -0
- package/.tfcore/tasks/refresh-status.md +135 -0
- package/.tfcore/tasks/render-workflow-docs.md +117 -0
- package/.tfcore/tasks/shard-doc.md +185 -0
- package/.tfcore/tasks/split-brd.md +131 -0
- package/.tfcore/tasks/triage-issues.md +157 -0
- package/.tfcore/tasks/verify-phase.md +579 -0
- package/.tfcore/telemetry/SCHEMA.md +678 -0
- package/.tfcore/telemetry/install-metrics.sh +390 -0
- package/.tfcore/telemetry/pre-commit +94 -0
- package/.tfcore/telemetry/tf-metrics.sh +1415 -0
- package/.tfcore/templates/architecture-tmpl.yaml +650 -0
- package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
- package/.tfcore/templates/market-research-tmpl.yaml +252 -0
- package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
- package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
- package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
- package/.tfcore/user-guide.md +633 -0
- package/.tfcore/utils/techieflow-doc-template.md +325 -0
- package/.tfcore/utils/tf-assets.sh +310 -0
- package/.tfcore/utils/tf-codex-bind.py +196 -0
- package/.tfcore/utils/tf-codex-telemetry.py +60 -0
- package/.tfcore/utils/tf-emit.sh +1075 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
- package/.tfcore/utils/tf-goal.sh +421 -0
- package/.tfcore/utils/tf-harness.sh +160 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
- package/.tfcore/utils/tf-mockup-parity.sh +65 -0
- package/.tfcore/utils/tf-opencode-audit.py +126 -0
- package/.tfcore/utils/tf-perf.sh +241 -0
- package/.tfcore/utils/tf-render-html.py +715 -0
- package/.tfcore/utils/tf-render-html.sh +28 -0
- package/.tfcore/utils/tf-routing-bind.sh +216 -0
- package/.tfcore/utils/tf-routing.sh +247 -0
- package/.tfcore/utils/tf-yolo.sh +119 -0
- package/.tfcore/working-in-the-brownfield.md +606 -0
- package/LICENSE +201 -0
- package/README.md +1212 -0
- package/WORKFLOW.html +1986 -0
- package/docs/TechieFlow-Installation.md +299 -0
- package/opencode.jsonc +929 -0
- package/package.json +60 -0
- package/scripts/install.mjs +992 -0
- package/scripts/test-install.mjs +505 -0
- 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
|