@hustle-together/api-dev-tools 3.9.2 → 3.10.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/README.md +311 -19
- package/bin/cli.js +54 -7
- package/commands/hustle-api-create.md +13 -13
- package/commands/hustle-ui-create-page.md +933 -0
- package/hooks/api-workflow-check.py +160 -2
- package/hooks/check-api-routes.py +168 -0
- package/hooks/enforce-a11y-audit.py +202 -0
- package/hooks/enforce-brand-guide.py +115 -5
- package/hooks/enforce-page-components.py +186 -0
- package/hooks/enforce-page-data-schema.py +155 -0
- package/hooks/generate-manifest-entry.py +181 -1
- package/hooks/session-startup.py +95 -5
- package/hooks/update-ui-showcase.py +67 -3
- package/package.json +1 -2
- package/templates/api-dev-state.json +39 -1
- package/templates/settings.json +16 -0
- package/templates/shared/HeroHeader.tsx +1 -1
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
- package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
- package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
- package/demo/hustle-together/index.html +0 -1312
- package/demo/workflow-demo-v3.5-backup.html +0 -5008
- package/demo/workflow-demo.html +0 -6202
- /package/templates/api-showcase/{APICard.tsx → _components/APICard.tsx} +0 -0
- /package/templates/api-showcase/{APIModal.tsx → _components/APIModal.tsx} +0 -0
- /package/templates/api-showcase/{APIShowcase.tsx → _components/APIShowcase.tsx} +0 -0
- /package/templates/api-showcase/{APITester.tsx → _components/APITester.tsx} +0 -0
- /package/templates/ui-showcase/{PreviewCard.tsx → _components/PreviewCard.tsx} +0 -0
- /package/templates/ui-showcase/{PreviewModal.tsx → _components/PreviewModal.tsx} +0 -0
- /package/templates/ui-showcase/{UIShowcase.tsx → _components/UIShowcase.tsx} +0 -0
|
@@ -49,6 +49,127 @@ RECOMMENDED_PHASES = [
|
|
|
49
49
|
("documentation", "Documentation updates"),
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
+
# Combine workflow specific phases
|
|
53
|
+
COMBINE_REQUIRED_PHASES = [
|
|
54
|
+
("selection", "API selection (2+ APIs required)"),
|
|
55
|
+
("scope", "Scope confirmation"),
|
|
56
|
+
("research_initial", "Initial research"),
|
|
57
|
+
("interview", "User interview"),
|
|
58
|
+
("research_deep", "Deep research"),
|
|
59
|
+
("schema_creation", "Combined schema creation"),
|
|
60
|
+
("environment_check", "Environment check"),
|
|
61
|
+
("tdd_red", "TDD Red phase"),
|
|
62
|
+
("tdd_green", "TDD Green phase"),
|
|
63
|
+
("verify", "Verification phase"),
|
|
64
|
+
("documentation", "Documentation updates"),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# UI workflow specific phases
|
|
68
|
+
UI_REQUIRED_PHASES = [
|
|
69
|
+
("disambiguation", "Component/Page type disambiguation"),
|
|
70
|
+
("scope", "Scope confirmation"),
|
|
71
|
+
("design_research", "Design research"),
|
|
72
|
+
("interview", "User interview"),
|
|
73
|
+
("tdd_red", "TDD Red phase"),
|
|
74
|
+
("tdd_green", "TDD Green phase"),
|
|
75
|
+
("verify", "Verification phase (4-step)"),
|
|
76
|
+
("documentation", "Documentation updates"),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_workflow_type(state):
|
|
81
|
+
"""Detect the workflow type from state."""
|
|
82
|
+
workflow = state.get("workflow", "")
|
|
83
|
+
if workflow:
|
|
84
|
+
return workflow
|
|
85
|
+
|
|
86
|
+
# Infer from state structure
|
|
87
|
+
if state.get("combine_config"):
|
|
88
|
+
return "combine-api"
|
|
89
|
+
if state.get("ui_config"):
|
|
90
|
+
mode = state.get("ui_config", {}).get("mode", "")
|
|
91
|
+
return f"ui-create-{mode}" if mode else "ui-create-component"
|
|
92
|
+
|
|
93
|
+
return "api-create"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_required_phases_for_workflow(workflow_type):
|
|
97
|
+
"""Get the required phases list for a given workflow type."""
|
|
98
|
+
if workflow_type == "combine-api":
|
|
99
|
+
return COMBINE_REQUIRED_PHASES
|
|
100
|
+
elif workflow_type.startswith("ui-create"):
|
|
101
|
+
return UI_REQUIRED_PHASES
|
|
102
|
+
else:
|
|
103
|
+
return REQUIRED_PHASES
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_combine_workflow(state):
|
|
107
|
+
"""Validate combine-specific requirements.
|
|
108
|
+
|
|
109
|
+
Returns list of issues if validation fails, empty list if OK.
|
|
110
|
+
"""
|
|
111
|
+
issues = []
|
|
112
|
+
|
|
113
|
+
combine_config = state.get("combine_config", {})
|
|
114
|
+
if not combine_config:
|
|
115
|
+
issues.append("❌ Combine config not found in state")
|
|
116
|
+
return issues
|
|
117
|
+
|
|
118
|
+
# Check that at least 2 APIs are selected
|
|
119
|
+
source_elements = combine_config.get("source_elements", [])
|
|
120
|
+
if len(source_elements) < 2:
|
|
121
|
+
issues.append(f"❌ Combine requires 2+ APIs, found {len(source_elements)}")
|
|
122
|
+
issues.append(" Select more APIs in Phase 1 (SELECTION)")
|
|
123
|
+
|
|
124
|
+
# Verify all source APIs exist in registry
|
|
125
|
+
try:
|
|
126
|
+
registry_path = STATE_FILE.parent / "registry.json"
|
|
127
|
+
if registry_path.exists():
|
|
128
|
+
registry = json.loads(registry_path.read_text())
|
|
129
|
+
apis = registry.get("apis", {})
|
|
130
|
+
|
|
131
|
+
for elem in source_elements:
|
|
132
|
+
elem_name = elem.get("name", "") if isinstance(elem, dict) else str(elem)
|
|
133
|
+
if elem_name and elem_name not in apis:
|
|
134
|
+
issues.append(f"⚠️ Source API '{elem_name}' not found in registry")
|
|
135
|
+
issues.append(f" Run /api-create {elem_name} first")
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
# Check flow type is defined
|
|
140
|
+
flow_type = combine_config.get("flow_type", "")
|
|
141
|
+
if not flow_type:
|
|
142
|
+
issues.append("⚠️ Flow type not defined (sequential/parallel/conditional)")
|
|
143
|
+
|
|
144
|
+
return issues
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def validate_ui_workflow(state):
|
|
148
|
+
"""Validate UI-specific requirements.
|
|
149
|
+
|
|
150
|
+
Returns list of issues if validation fails, empty list if OK.
|
|
151
|
+
"""
|
|
152
|
+
issues = []
|
|
153
|
+
|
|
154
|
+
ui_config = state.get("ui_config", {})
|
|
155
|
+
if not ui_config:
|
|
156
|
+
# Try to get from active element
|
|
157
|
+
active = state.get("active_element", "")
|
|
158
|
+
if active:
|
|
159
|
+
elements = state.get("elements", {})
|
|
160
|
+
element = elements.get(active, {})
|
|
161
|
+
ui_config = element.get("ui_config", {})
|
|
162
|
+
|
|
163
|
+
if not ui_config:
|
|
164
|
+
issues.append("⚠️ UI config not found in state")
|
|
165
|
+
return issues
|
|
166
|
+
|
|
167
|
+
# Check brand guide was applied
|
|
168
|
+
if not ui_config.get("use_brand_guide"):
|
|
169
|
+
issues.append("⚠️ Brand guide not applied - design may not match project standards")
|
|
170
|
+
|
|
171
|
+
return issues
|
|
172
|
+
|
|
52
173
|
|
|
53
174
|
def get_active_endpoint(state):
|
|
54
175
|
"""Get active endpoint - supports both old and new state formats."""
|
|
@@ -58,11 +179,23 @@ def get_active_endpoint(state):
|
|
|
58
179
|
return active, state["endpoints"][active]
|
|
59
180
|
return None, None
|
|
60
181
|
|
|
182
|
+
# Support for elements (UI workflow)
|
|
183
|
+
if "elements" in state and "active_element" in state:
|
|
184
|
+
active = state.get("active_element")
|
|
185
|
+
if active and active in state["elements"]:
|
|
186
|
+
return active, state["elements"][active]
|
|
187
|
+
return None, None
|
|
188
|
+
|
|
61
189
|
# Old format: single endpoint
|
|
62
190
|
endpoint = state.get("endpoint")
|
|
63
191
|
if endpoint:
|
|
64
192
|
return endpoint, state
|
|
65
193
|
|
|
194
|
+
# Try active_element without elements dict
|
|
195
|
+
active = state.get("active_element")
|
|
196
|
+
if active:
|
|
197
|
+
return active, state
|
|
198
|
+
|
|
66
199
|
return None, None
|
|
67
200
|
|
|
68
201
|
|
|
@@ -465,6 +598,9 @@ def main():
|
|
|
465
598
|
print(json.dumps({"decision": "approve"}))
|
|
466
599
|
sys.exit(0)
|
|
467
600
|
|
|
601
|
+
# Detect workflow type
|
|
602
|
+
workflow_type = get_workflow_type(state)
|
|
603
|
+
|
|
468
604
|
# Get active endpoint (multi-API support)
|
|
469
605
|
endpoint, endpoint_data = get_active_endpoint(state)
|
|
470
606
|
|
|
@@ -476,7 +612,12 @@ def main():
|
|
|
476
612
|
|
|
477
613
|
# Check if workflow was even started
|
|
478
614
|
research = phases.get("research_initial", {})
|
|
479
|
-
|
|
615
|
+
design_research = phases.get("design_research", {}) # For UI workflows
|
|
616
|
+
selection = phases.get("selection", {}) # For combine workflows
|
|
617
|
+
|
|
618
|
+
if (research.get("status") == "not_started" and
|
|
619
|
+
design_research.get("status") == "not_started" and
|
|
620
|
+
selection.get("status") == "not_started"):
|
|
480
621
|
# Workflow not started, allow stop
|
|
481
622
|
print(json.dumps({"decision": "approve"}))
|
|
482
623
|
sys.exit(0)
|
|
@@ -484,9 +625,26 @@ def main():
|
|
|
484
625
|
# Collect all issues
|
|
485
626
|
all_issues = []
|
|
486
627
|
|
|
628
|
+
# Workflow-specific validation
|
|
629
|
+
if workflow_type == "combine-api":
|
|
630
|
+
combine_issues = validate_combine_workflow(state)
|
|
631
|
+
if combine_issues:
|
|
632
|
+
all_issues.append("❌ COMBINE WORKFLOW VALIDATION FAILED:")
|
|
633
|
+
all_issues.extend(combine_issues)
|
|
634
|
+
all_issues.append("")
|
|
635
|
+
|
|
636
|
+
elif workflow_type.startswith("ui-create"):
|
|
637
|
+
ui_issues = validate_ui_workflow(state)
|
|
638
|
+
if ui_issues:
|
|
639
|
+
all_issues.extend(ui_issues)
|
|
640
|
+
all_issues.append("")
|
|
641
|
+
|
|
642
|
+
# Get the correct required phases for this workflow
|
|
643
|
+
required_phases = get_required_phases_for_workflow(workflow_type)
|
|
644
|
+
|
|
487
645
|
# Check required phases
|
|
488
646
|
incomplete_required = []
|
|
489
|
-
for phase_key, phase_name in
|
|
647
|
+
for phase_key, phase_name in required_phases:
|
|
490
648
|
phase = phases.get(phase_key, {})
|
|
491
649
|
status = phase.get("status", "not_started")
|
|
492
650
|
if status != "complete":
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: check-api-routes.py
|
|
4
|
+
Trigger: PreToolUse (Write|Edit)
|
|
5
|
+
Purpose: Verify required API routes exist before page implementation
|
|
6
|
+
|
|
7
|
+
For ui-create-page workflow, ensures Phase 7 (ENVIRONMENT) has verified
|
|
8
|
+
that required API routes are available.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
import glob
|
|
15
|
+
|
|
16
|
+
def load_state():
|
|
17
|
+
"""Load the api-dev-state.json file"""
|
|
18
|
+
state_paths = [
|
|
19
|
+
".claude/api-dev-state.json",
|
|
20
|
+
os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/api-dev-state.json")
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
for path in state_paths:
|
|
24
|
+
if os.path.exists(path):
|
|
25
|
+
with open(path, 'r') as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
def is_page_workflow(state):
|
|
30
|
+
"""Check if current workflow is ui-create-page"""
|
|
31
|
+
workflow = state.get("workflow", "")
|
|
32
|
+
return workflow == "ui-create-page"
|
|
33
|
+
|
|
34
|
+
def get_active_element(state):
|
|
35
|
+
"""Get the active element being worked on"""
|
|
36
|
+
active = state.get("active_element", "")
|
|
37
|
+
if not active:
|
|
38
|
+
active = state.get("endpoint", "")
|
|
39
|
+
return active
|
|
40
|
+
|
|
41
|
+
def is_page_implementation(file_path, element_name):
|
|
42
|
+
"""Check if the file is a page implementation file"""
|
|
43
|
+
if not file_path or not element_name:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
patterns = [
|
|
47
|
+
f"src/app/{element_name}/page.tsx",
|
|
48
|
+
f"app/{element_name}/page.tsx",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
return any(pattern in file_path for pattern in patterns)
|
|
52
|
+
|
|
53
|
+
def check_environment_phase(state, element_name):
|
|
54
|
+
"""Check if environment phase is complete"""
|
|
55
|
+
elements = state.get("elements", {})
|
|
56
|
+
element = elements.get(element_name, {})
|
|
57
|
+
phases = element.get("phases", {})
|
|
58
|
+
|
|
59
|
+
environment = phases.get("environment_check", {})
|
|
60
|
+
return environment.get("status") == "complete"
|
|
61
|
+
|
|
62
|
+
def get_required_api_routes(state, element_name):
|
|
63
|
+
"""Get list of required API routes from interview decisions"""
|
|
64
|
+
elements = state.get("elements", {})
|
|
65
|
+
element = elements.get(element_name, {})
|
|
66
|
+
ui_config = element.get("ui_config", {})
|
|
67
|
+
|
|
68
|
+
# Check if data sources were defined
|
|
69
|
+
data_sources = ui_config.get("data_sources", [])
|
|
70
|
+
return data_sources
|
|
71
|
+
|
|
72
|
+
def find_existing_api_routes():
|
|
73
|
+
"""Find all existing API routes in the project"""
|
|
74
|
+
routes = []
|
|
75
|
+
|
|
76
|
+
# Check src/app/api paths
|
|
77
|
+
api_patterns = [
|
|
78
|
+
"src/app/api/**/*.ts",
|
|
79
|
+
"src/app/api/**/*.tsx",
|
|
80
|
+
"app/api/**/*.ts",
|
|
81
|
+
"app/api/**/*.tsx",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
for pattern in api_patterns:
|
|
85
|
+
for file_path in glob.glob(pattern, recursive=True):
|
|
86
|
+
if "route.ts" in file_path or "route.tsx" in file_path:
|
|
87
|
+
# Extract route name from path
|
|
88
|
+
route = file_path.replace("src/app/api/", "/api/")
|
|
89
|
+
route = route.replace("app/api/", "/api/")
|
|
90
|
+
route = route.replace("/route.ts", "")
|
|
91
|
+
route = route.replace("/route.tsx", "")
|
|
92
|
+
routes.append(route)
|
|
93
|
+
|
|
94
|
+
return routes
|
|
95
|
+
|
|
96
|
+
def main():
|
|
97
|
+
try:
|
|
98
|
+
# Read tool input from stdin
|
|
99
|
+
input_data = json.loads(sys.stdin.read())
|
|
100
|
+
tool_name = input_data.get("tool_name", "")
|
|
101
|
+
tool_input = input_data.get("tool_input", {})
|
|
102
|
+
|
|
103
|
+
# Only check Write tool
|
|
104
|
+
if tool_name != "Write":
|
|
105
|
+
print(json.dumps({"decision": "allow"}))
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
file_path = tool_input.get("file_path", "")
|
|
109
|
+
|
|
110
|
+
# Load state
|
|
111
|
+
state = load_state()
|
|
112
|
+
if not state:
|
|
113
|
+
print(json.dumps({"decision": "allow"}))
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Only apply to ui-create-page workflow
|
|
117
|
+
if not is_page_workflow(state):
|
|
118
|
+
print(json.dumps({"decision": "allow"}))
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
element_name = get_active_element(state)
|
|
122
|
+
if not element_name:
|
|
123
|
+
print(json.dumps({"decision": "allow"}))
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# Check if writing main page file
|
|
127
|
+
if is_page_implementation(file_path, element_name):
|
|
128
|
+
# Verify environment phase is complete
|
|
129
|
+
if not check_environment_phase(state, element_name):
|
|
130
|
+
# Find existing API routes for reference
|
|
131
|
+
existing_routes = find_existing_api_routes()
|
|
132
|
+
routes_list = "\n".join([f" - {r}" for r in existing_routes[:15]])
|
|
133
|
+
if len(existing_routes) > 15:
|
|
134
|
+
routes_list += f"\n ... and {len(existing_routes) - 15} more"
|
|
135
|
+
|
|
136
|
+
print(json.dumps({
|
|
137
|
+
"decision": "block",
|
|
138
|
+
"reason": f"""
|
|
139
|
+
ENVIRONMENT CHECK REQUIRED (Phase 7)
|
|
140
|
+
|
|
141
|
+
You are implementing the main page, but the Environment phase is not complete.
|
|
142
|
+
|
|
143
|
+
Before implementing page.tsx:
|
|
144
|
+
1. Verify required API routes exist
|
|
145
|
+
2. Check authentication configuration
|
|
146
|
+
3. Verify required packages are installed
|
|
147
|
+
4. Update state: phases.environment_check.status = "complete"
|
|
148
|
+
|
|
149
|
+
Existing API Routes Found:
|
|
150
|
+
{routes_list if existing_routes else " (No API routes found)"}
|
|
151
|
+
|
|
152
|
+
If you need new API routes, use /api-create to create them first.
|
|
153
|
+
"""
|
|
154
|
+
}))
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Allow everything else
|
|
158
|
+
print(json.dumps({"decision": "allow"}))
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
# On error, allow to avoid blocking workflow
|
|
162
|
+
print(json.dumps({
|
|
163
|
+
"decision": "allow",
|
|
164
|
+
"error": str(e)
|
|
165
|
+
}))
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
main()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PostToolUse for Write/Edit
|
|
4
|
+
Purpose: Trigger accessibility audit after UI component/page implementation
|
|
5
|
+
|
|
6
|
+
This hook runs after Phase 9 (TDD GREEN) for UI workflows. It notifies Claude
|
|
7
|
+
to run axe-core audit on Storybook stories or pages to verify WCAG compliance.
|
|
8
|
+
|
|
9
|
+
Version: 3.10.0
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
- {"continue": true} - Always continues
|
|
13
|
+
- May include "notify" with accessibility check reminder
|
|
14
|
+
- May include "additionalContext" with accessibility guidelines
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# State file is in .claude/ directory (sibling to hooks/)
|
|
21
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
22
|
+
|
|
23
|
+
# WCAG 2.1 Level AA Quick Reference
|
|
24
|
+
WCAG_AA_CHECKLIST = [
|
|
25
|
+
"Color contrast: 4.5:1 for normal text, 3:1 for large text",
|
|
26
|
+
"Focus visible: All interactive elements show focus state",
|
|
27
|
+
"Keyboard nav: All functionality accessible via keyboard",
|
|
28
|
+
"Labels: All form inputs have associated labels",
|
|
29
|
+
"Alt text: All images have meaningful alt text",
|
|
30
|
+
"Headings: Proper heading hierarchy (h1-h6)",
|
|
31
|
+
"Touch targets: Min 44x44px for touch targets",
|
|
32
|
+
"Error messages: Clear error identification and suggestions",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_workflow_type(state):
|
|
37
|
+
"""Detect the workflow type from state."""
|
|
38
|
+
workflow = state.get("workflow", "")
|
|
39
|
+
if workflow:
|
|
40
|
+
return workflow
|
|
41
|
+
|
|
42
|
+
if state.get("ui_config"):
|
|
43
|
+
mode = state.get("ui_config", {}).get("mode", "")
|
|
44
|
+
return f"ui-create-{mode}" if mode else "ui-create-component"
|
|
45
|
+
|
|
46
|
+
return "api-create"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_active_element(state):
|
|
50
|
+
"""Get active element name and data."""
|
|
51
|
+
if "elements" in state and "active_element" in state:
|
|
52
|
+
active = state.get("active_element")
|
|
53
|
+
if active and active in state["elements"]:
|
|
54
|
+
return active, state["elements"][active]
|
|
55
|
+
return None, None
|
|
56
|
+
|
|
57
|
+
active = state.get("active_element")
|
|
58
|
+
if active:
|
|
59
|
+
return active, state
|
|
60
|
+
|
|
61
|
+
return None, None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_verify_phase(phases):
|
|
65
|
+
"""Check if we're in or just completed the verify phase."""
|
|
66
|
+
verify = phases.get("verify", {})
|
|
67
|
+
tdd_green = phases.get("tdd_green", {})
|
|
68
|
+
|
|
69
|
+
# After green, before or during verify
|
|
70
|
+
return (
|
|
71
|
+
tdd_green.get("status") == "complete" and
|
|
72
|
+
verify.get("status") in ["not_started", "in_progress"]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_accessibility_level(state, element_data):
|
|
77
|
+
"""Get the accessibility level requirement."""
|
|
78
|
+
ui_config = state.get("ui_config", {})
|
|
79
|
+
if not ui_config and element_data:
|
|
80
|
+
ui_config = element_data.get("ui_config", {})
|
|
81
|
+
|
|
82
|
+
return ui_config.get("accessibility_level", "AA")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_a11y_commands(element_name, workflow_type):
|
|
86
|
+
"""Generate accessibility testing commands."""
|
|
87
|
+
commands = []
|
|
88
|
+
|
|
89
|
+
if "component" in workflow_type:
|
|
90
|
+
commands.extend([
|
|
91
|
+
f"# Storybook accessibility check",
|
|
92
|
+
f"pnpm storybook --ci",
|
|
93
|
+
f"# Then run axe in browser or:",
|
|
94
|
+
f"pnpm dlx @storybook/test-runner --url http://localhost:6006",
|
|
95
|
+
f"",
|
|
96
|
+
f"# Or manual axe-core check:",
|
|
97
|
+
f"pnpm dlx @axe-core/cli http://localhost:6006/?path=/story/{element_name.lower()}--default"
|
|
98
|
+
])
|
|
99
|
+
else:
|
|
100
|
+
commands.extend([
|
|
101
|
+
f"# Page accessibility check",
|
|
102
|
+
f"pnpm dev",
|
|
103
|
+
f"# Then in another terminal:",
|
|
104
|
+
f"pnpm dlx @axe-core/cli http://localhost:3000/{element_name}",
|
|
105
|
+
f"",
|
|
106
|
+
f"# Or use Playwright accessibility tests:",
|
|
107
|
+
f"pnpm test:e2e --grep 'accessibility'"
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
return "\n".join(commands)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main():
|
|
114
|
+
# Read hook input from stdin
|
|
115
|
+
try:
|
|
116
|
+
input_data = json.load(sys.stdin)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
print(json.dumps({"continue": True}))
|
|
119
|
+
sys.exit(0)
|
|
120
|
+
|
|
121
|
+
tool_name = input_data.get("tool_name", "")
|
|
122
|
+
|
|
123
|
+
# Only process Write/Edit operations
|
|
124
|
+
if tool_name not in ["Write", "Edit"]:
|
|
125
|
+
print(json.dumps({"continue": True}))
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
# Check if state file exists
|
|
129
|
+
if not STATE_FILE.exists():
|
|
130
|
+
print(json.dumps({"continue": True}))
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
# Load state
|
|
134
|
+
try:
|
|
135
|
+
state = json.loads(STATE_FILE.read_text())
|
|
136
|
+
except json.JSONDecodeError:
|
|
137
|
+
print(json.dumps({"continue": True}))
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
workflow_type = get_workflow_type(state)
|
|
141
|
+
|
|
142
|
+
# Only apply for UI workflows
|
|
143
|
+
if not workflow_type.startswith("ui-create"):
|
|
144
|
+
print(json.dumps({"continue": True}))
|
|
145
|
+
sys.exit(0)
|
|
146
|
+
|
|
147
|
+
# Get active element
|
|
148
|
+
element_name, element_data = get_active_element(state)
|
|
149
|
+
if not element_name or not element_data:
|
|
150
|
+
print(json.dumps({"continue": True}))
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
phases = element_data.get("phases", {}) if element_data else state.get("phases", {})
|
|
154
|
+
|
|
155
|
+
# Check if we should trigger a11y audit (after TDD Green)
|
|
156
|
+
if not is_verify_phase(phases):
|
|
157
|
+
print(json.dumps({"continue": True}))
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
# Get accessibility level
|
|
161
|
+
a11y_level = get_accessibility_level(state, element_data)
|
|
162
|
+
|
|
163
|
+
# Generate audit commands
|
|
164
|
+
commands = generate_a11y_commands(element_name, workflow_type)
|
|
165
|
+
|
|
166
|
+
# Build accessibility context
|
|
167
|
+
checklist = "\n".join([f" - {item}" for item in WCAG_AA_CHECKLIST])
|
|
168
|
+
|
|
169
|
+
context = f"""
|
|
170
|
+
## Accessibility Audit Required (WCAG 2.1 {a11y_level})
|
|
171
|
+
|
|
172
|
+
The TDD Green phase is complete. Before marking verify as complete, run an accessibility audit.
|
|
173
|
+
|
|
174
|
+
### Quick Commands
|
|
175
|
+
```bash
|
|
176
|
+
{commands}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### WCAG 2.1 {a11y_level} Checklist
|
|
180
|
+
{checklist}
|
|
181
|
+
|
|
182
|
+
### 4-Step Verification for UI
|
|
183
|
+
1. **Responsive**: Test at 320px, 768px, 1024px, 1440px
|
|
184
|
+
2. **Data Binding**: Verify all data sources load correctly
|
|
185
|
+
3. **Tests**: All unit/e2e tests pass
|
|
186
|
+
4. **Accessibility**: Run axe-core, fix any violations
|
|
187
|
+
|
|
188
|
+
If violations are found, fix them before completing the verify phase.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
output = {
|
|
192
|
+
"continue": True,
|
|
193
|
+
"notify": f"Accessibility audit required for {element_name} (WCAG 2.1 {a11y_level})",
|
|
194
|
+
"additionalContext": context
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
print(json.dumps(output))
|
|
198
|
+
sys.exit(0)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
main()
|