agent-project-sdlc 0.1.24 → 0.1.25
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 +14 -7
- package/assets/agents/AGENTS_CORE.md +14 -9
- package/assets/docs/README.md +21 -7
- package/assets/make/sdlc-harness.mk +5 -1
- package/assets/policies/allowed_paths.yaml +9 -0
- package/assets/policies/gates.yaml +6 -0
- package/assets/policies/phase_contracts.yaml +49 -0
- package/assets/skills/pjsdlc_architect_design/SKILL.md +14 -8
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +8 -3
- package/assets/skills/pjsdlc_implementation_doc/SKILL.md +9 -4
- package/assets/skills/pjsdlc_manager/SKILL.md +17 -16
- package/assets/skills/pjsdlc_reviewer/SKILL.md +6 -1
- package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +8 -5
- package/assets/skills/pjsdlc_tester/SKILL.md +12 -4
- package/assets/skills/pjsdlc_uiux_design/SKILL.md +76 -0
- package/assets/templates/PLAN_TEMPLATE.yaml +4 -0
- package/assets/templates/REVIEW_TEMPLATE.md +14 -4
- package/assets/templates/RFC_TEMPLATE.md +13 -5
- package/assets/templates/TECH_DESIGN_TEMPLATE.md +18 -10
- package/assets/templates/TEST_CASES_TEMPLATE.md +5 -3
- package/assets/templates/TEST_STRATEGY_TEMPLATE.md +4 -0
- package/assets/templates/UI_UX_DESIGN_TEMPLATE.md +67 -0
- package/assets/tools/harness_utils.py +4 -2
- package/assets/tools/validate_design.py +55 -2
- package/assets/tools/validate_harness.py +1 -0
- package/assets/tools/validate_rfc.py +31 -0
- package/assets/tools/validate_test_plan.py +118 -1
- package/assets/tools/validate_uiux_design.py +101 -0
- package/dist/commands/index.js +2 -1
- package/dist/lib/init.js +1 -0
- package/dist/lib/validators.js +319 -6
- package/package.json +2 -1
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
- 相关 APIs(Related APIs):
|
|
12
12
|
- 相关数据(Related data):
|
|
13
13
|
|
|
14
|
-
## 3.
|
|
14
|
+
## 3. Experience Input Review(体验输入审查)
|
|
15
|
+
|
|
16
|
+
- UI/UX slices:
|
|
17
|
+
- DESIGN.md:
|
|
18
|
+
- Screen contracts consumed:
|
|
19
|
+
- Handoff matrix consumed:
|
|
20
|
+
- Not applicable reason:
|
|
21
|
+
|
|
22
|
+
## 4. 方案架构
|
|
15
23
|
|
|
16
24
|
- 领域边界(Domain boundary):
|
|
17
25
|
- 主要组件(Main components):
|
|
@@ -25,27 +33,27 @@ Input
|
|
|
25
33
|
-> Output
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
##
|
|
36
|
+
## 5. 接口契约(Interface Contract)
|
|
29
37
|
|
|
30
38
|
| 接口(Interface) | 方法/事件(Method/Event) | 请求(Request) | 响应(Response) | 错误(Errors) |
|
|
31
39
|
|---|---|---|---|---|
|
|
32
40
|
| | | | | |
|
|
33
41
|
|
|
34
|
-
##
|
|
42
|
+
## 6. 数据模型(Data Model)
|
|
35
43
|
|
|
36
44
|
| 实体(Entity) | 字段(Fields) | 负责人(Owner) | 备注(Notes) |
|
|
37
45
|
|---|---|---|---|
|
|
38
46
|
| | | | |
|
|
39
47
|
|
|
40
|
-
##
|
|
48
|
+
## 7. 任务拆分(Task Breakdown)
|
|
41
49
|
|
|
42
|
-
| Task ID | 标题(Title) | Allowed Paths | Required Gates | Implementation Doc |
|
|
43
|
-
|
|
44
|
-
| TASK-001 | | `src/**`, `tests/**` | `make lint`, `make test-current-domain` | `.docs/04_implementation/...` |
|
|
50
|
+
| Task ID | 标题(Title) | UI/UX refs | Design system | Allowed Paths | Required Gates | Implementation Doc |
|
|
51
|
+
|---|---|---|---|---|---|---|
|
|
52
|
+
| TASK-001 | | `.docs/02_experience/...` | `DESIGN.md` | `src/**`, `tests/**` | `make lint`, `make test-current-domain` | `.docs/04_implementation/...` |
|
|
45
53
|
|
|
46
54
|
`Implementation Doc` 应指向模块、子系统或核心数据流级文档;多个 task 可以更新同一份文档,task id 和 commit 记录在该文档的 provenance / Change Log 中。
|
|
47
55
|
|
|
48
|
-
##
|
|
56
|
+
## 8. Development Self-Test Contract(开发自测合同,待执行)
|
|
49
57
|
|
|
50
58
|
> service / agent / runtime / worker / frontend app / provider-live / API / CLI 等可运行边界的开发 task 必填,并同步写入 `plan.draft.yaml.tasks[].self_test_contract`。
|
|
51
59
|
|
|
@@ -62,12 +70,12 @@ Input
|
|
|
62
70
|
|
|
63
71
|
- Not applicable reason:
|
|
64
72
|
|
|
65
|
-
##
|
|
73
|
+
## 9. 风险与缓解
|
|
66
74
|
|
|
67
75
|
| 风险(Risk) | 等级(Level) | 缓解措施(Mitigation) |
|
|
68
76
|
|---|---|---|
|
|
69
77
|
| | P1 | |
|
|
70
78
|
|
|
71
|
-
##
|
|
79
|
+
## 10. 需要关注的方案偏移
|
|
72
80
|
|
|
73
81
|
-
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
## 2. Cases(用例)
|
|
11
11
|
|
|
12
|
-
| ID | Requirement | Preconditions | Steps | Expected
|
|
13
|
-
|
|
14
|
-
| TC-001 | | | | |
|
|
12
|
+
| Case ID | Requirement / Risk Ref | Type | Priority | Runnable Entry | Preconditions | Steps | Expected Exit | Evidence Pointer |
|
|
13
|
+
|---|---|---|---|---|---|---|---|---|
|
|
14
|
+
| TC-001 | | unit/integration/e2e/regression/smoke/manual | P0/P1/P2 | | | | | |
|
|
15
15
|
|
|
16
16
|
## 3. Traceability(追溯)
|
|
17
17
|
|
|
18
18
|
- PRD acceptance criteria:
|
|
19
19
|
- Review findings:
|
|
20
20
|
- Risk paths:
|
|
21
|
+
- TEST_REPORT references:
|
|
21
22
|
|
|
22
23
|
## 4. Notes(备注)
|
|
23
24
|
|
|
24
25
|
- Fixture/live boundary:
|
|
25
26
|
- Data setup:
|
|
27
|
+
- Case body only records test design. Execution result, regression evidence, bugfix route and final decision belong in `TEST_REPORT.md`.
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
- In scope:
|
|
6
6
|
- Out of scope:
|
|
7
7
|
- Target release/module:
|
|
8
|
+
- UI/UX handoff refs:
|
|
9
|
+
- DESIGN.md:
|
|
8
10
|
|
|
9
11
|
## 2. Environment(环境)
|
|
10
12
|
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
- Expected exits / side effects:
|
|
13
15
|
- Config contract:
|
|
14
16
|
- Fixture/live boundary:
|
|
17
|
+
- Screen states / responsive / a11y environment:
|
|
15
18
|
|
|
16
19
|
## 3. Priority(优先级)
|
|
17
20
|
|
|
@@ -25,3 +28,4 @@
|
|
|
25
28
|
- Manual checks:
|
|
26
29
|
- Regression gates:
|
|
27
30
|
- Blocker handling:
|
|
31
|
+
- UI/UX contract coverage:
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# [Feature/Capability] UI/UX Design
|
|
2
|
+
|
|
3
|
+
## 1. PRD refs and Requirement IDs
|
|
4
|
+
|
|
5
|
+
- PRD:
|
|
6
|
+
- Requirement IDs:
|
|
7
|
+
|
|
8
|
+
## 2. Applicability
|
|
9
|
+
|
|
10
|
+
- Applicability: `visual_ui | cli_or_api_experience | not_applicable`
|
|
11
|
+
- N/A reason:
|
|
12
|
+
|
|
13
|
+
## 3. User journeys
|
|
14
|
+
|
|
15
|
+
| Journey ID | User / Role | Goal | Entry | Success exit | Failure / recovery |
|
|
16
|
+
|---|---|---|---|---|---|
|
|
17
|
+
| UXJ-001 | | | | | |
|
|
18
|
+
|
|
19
|
+
## 4. Information architecture / routes / screens
|
|
20
|
+
|
|
21
|
+
| Surface | Route / Entry | Screen ID | Purpose | Primary user action |
|
|
22
|
+
|---|---|---|---|---|
|
|
23
|
+
| web / mobile / CLI / API / operator | | SCR-001 | | |
|
|
24
|
+
|
|
25
|
+
## 5. Screen contracts
|
|
26
|
+
|
|
27
|
+
| Screen ID | Requirement refs | Entry / route | Loading | Empty | Error | Success | Permission | Primary actions | Validation rules |
|
|
28
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
29
|
+
| SCR-001 | | | | | | | | | |
|
|
30
|
+
|
|
31
|
+
### Responsive and accessibility acceptance
|
|
32
|
+
|
|
33
|
+
- Breakpoints / form factors:
|
|
34
|
+
- Keyboard / focus behavior:
|
|
35
|
+
- Touch target behavior:
|
|
36
|
+
- Contrast / readable states:
|
|
37
|
+
- Assistive text / labels:
|
|
38
|
+
|
|
39
|
+
## 6. Component and interaction contracts
|
|
40
|
+
|
|
41
|
+
| Component / Pattern | States | Interaction behavior | Feedback | Notes |
|
|
42
|
+
|---|---|---|---|---|
|
|
43
|
+
| navigation / form / table / list / modal / toast | default / hover / focus / disabled / error / success | | | |
|
|
44
|
+
|
|
45
|
+
## 7. Design system reference
|
|
46
|
+
|
|
47
|
+
- DESIGN.md: `DESIGN.md | not_applicable`
|
|
48
|
+
- Token / component notes:
|
|
49
|
+
- Known deviations:
|
|
50
|
+
|
|
51
|
+
## 8. Handoff matrix
|
|
52
|
+
|
|
53
|
+
| Requirement | Screen / state | Component / interaction | Acceptance / test seed |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| | | | |
|
|
56
|
+
|
|
57
|
+
## 9. Open Questions / Out of Scope
|
|
58
|
+
|
|
59
|
+
### Open Questions
|
|
60
|
+
|
|
61
|
+
| Question | Owner | Status |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| | | open |
|
|
64
|
+
|
|
65
|
+
### Out of Scope
|
|
66
|
+
|
|
67
|
+
-
|
|
@@ -28,6 +28,7 @@ PARALLEL_TRIGGERS = {"user_requested", "workflow_default"}
|
|
|
28
28
|
PARALLEL_RUNTIME_PROVIDERS = {"codex_native_subagents", "user_orchestrated", "codex_exec_worktree"}
|
|
29
29
|
PARALLEL_ALLOWED_PHASES = {
|
|
30
30
|
"REQUIREMENT_GATHERING",
|
|
31
|
+
"UI_UX_DESIGNING",
|
|
31
32
|
"ARCHITECTING",
|
|
32
33
|
"SPRINTING",
|
|
33
34
|
"REVIEWING",
|
|
@@ -35,7 +36,7 @@ PARALLEL_ALLOWED_PHASES = {
|
|
|
35
36
|
"RELEASING",
|
|
36
37
|
"RFC_RECALIBRATION",
|
|
37
38
|
}
|
|
38
|
-
PARALLEL_READ_ONLY_PHASES = {"REQUIREMENT_GATHERING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"}
|
|
39
|
+
PARALLEL_READ_ONLY_PHASES = {"REQUIREMENT_GATHERING", "UI_UX_DESIGNING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"}
|
|
39
40
|
PARALLEL_PROTECTED_WRITE_PATTERNS = {
|
|
40
41
|
".codex/state/**",
|
|
41
42
|
"<harnessRoot>/state/**",
|
|
@@ -48,6 +49,7 @@ PARALLEL_PROTECTED_WRITE_PATTERNS = {
|
|
|
48
49
|
TASK_ID_PATTERN = re.compile(r"^[A-Z]+-(\d+)$")
|
|
49
50
|
TASK_PHASES = {
|
|
50
51
|
"REQUIREMENT_GATHERING",
|
|
52
|
+
"UI_UX_DESIGNING",
|
|
51
53
|
"ARCHITECTING",
|
|
52
54
|
"SPRINTING",
|
|
53
55
|
"REVIEWING",
|
|
@@ -1172,7 +1174,7 @@ def validate_parallel_execution_contract(data: dict[str, Any], current_phase: st
|
|
|
1172
1174
|
require("linked_task_id" not in contract, "parallel_execution must not define linked_task_id; use plan.yaml current_task_id")
|
|
1173
1175
|
require(
|
|
1174
1176
|
current_phase in PARALLEL_ALLOWED_PHASES,
|
|
1175
|
-
"parallel_execution is only supported during REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION",
|
|
1177
|
+
"parallel_execution is only supported during REQUIREMENT_GATHERING, UI_UX_DESIGNING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION",
|
|
1176
1178
|
)
|
|
1177
1179
|
require(contract.get("coordinator") == "main_agent", 'parallel_execution.coordinator must be "main_agent"')
|
|
1178
1180
|
|
|
@@ -33,6 +33,24 @@ CROSS_CUTTING_CATEGORIES = [
|
|
|
33
33
|
"architecture_terms": ["compliance", "permission", "authorization", "audit", "合规", "权限", "审计", "授权", "客户确认", "回执归档"],
|
|
34
34
|
},
|
|
35
35
|
]
|
|
36
|
+
UI_DRAFT_TASK_TERMS = [
|
|
37
|
+
"frontend",
|
|
38
|
+
"front-end",
|
|
39
|
+
"browser",
|
|
40
|
+
"page",
|
|
41
|
+
"screen",
|
|
42
|
+
"route",
|
|
43
|
+
"component",
|
|
44
|
+
"visual_ui",
|
|
45
|
+
"design.md",
|
|
46
|
+
".docs/02_experience/",
|
|
47
|
+
"页面",
|
|
48
|
+
"前端",
|
|
49
|
+
"屏幕",
|
|
50
|
+
"交互",
|
|
51
|
+
"组件",
|
|
52
|
+
"视觉",
|
|
53
|
+
]
|
|
36
54
|
|
|
37
55
|
|
|
38
56
|
def main() -> None:
|
|
@@ -43,6 +61,7 @@ def main() -> None:
|
|
|
43
61
|
architecture_docs = markdown_deliverables(".docs/02_architecture")
|
|
44
62
|
tech_plan_docs = markdown_deliverables(".docs/03_tech_plan")
|
|
45
63
|
product_docs = markdown_deliverables(".docs/01_product")
|
|
64
|
+
experience_docs = markdown_deliverables(".docs/02_experience")
|
|
46
65
|
require(architecture_docs, "No architecture deliverables found in .docs/02_architecture/")
|
|
47
66
|
require(tech_plan_docs, "No technical plan deliverables found in .docs/03_tech_plan/")
|
|
48
67
|
|
|
@@ -50,12 +69,12 @@ def main() -> None:
|
|
|
50
69
|
require(contains_any(text, ["prd", "requirement", "需求"]), "Design must cite product requirements")
|
|
51
70
|
require(contains_any(text, ["api", "interface", "接口", "contract", "契约"]), "Design must describe interfaces or contracts")
|
|
52
71
|
require(contains_any(text, ["task", "任务", "breakdown"]), "Design must include task breakdown")
|
|
53
|
-
draft_tasks = validate_draft_task_tech_plan_refs(tech_plan_docs)
|
|
72
|
+
draft_tasks = validate_draft_task_tech_plan_refs(tech_plan_docs, experience_docs)
|
|
54
73
|
validate_cross_cutting_architecture(product_docs, tech_plan_docs, architecture_docs, draft_tasks)
|
|
55
74
|
print(f"Design artifacts OK: {len(architecture_docs)} architecture, {len(tech_plan_docs)} tech plan")
|
|
56
75
|
|
|
57
76
|
|
|
58
|
-
def validate_draft_task_tech_plan_refs(tech_plan_docs: list) -> list[dict]:
|
|
77
|
+
def validate_draft_task_tech_plan_refs(tech_plan_docs: list, experience_docs: list) -> list[dict]:
|
|
59
78
|
draft = load_plan(".codex/state/plan.draft.yaml")
|
|
60
79
|
require("current_phase" not in draft, "plan.draft.yaml must not define current_phase; lifecycle.yaml is the single source for current_phase")
|
|
61
80
|
require("current_task_id" not in draft, "plan.draft.yaml must not define current_task_id because drafts are not active task state")
|
|
@@ -63,6 +82,7 @@ def validate_draft_task_tech_plan_refs(tech_plan_docs: list) -> list[dict]:
|
|
|
63
82
|
require(tasks, "plan.draft.yaml must contain at least one task before leaving ARCHITECTING")
|
|
64
83
|
|
|
65
84
|
available_tech_plans = {repo_relative(path) for path in tech_plan_docs}
|
|
85
|
+
available_experience_docs = {repo_relative(path) for path in experience_docs}
|
|
66
86
|
development_tasks: list[dict] = []
|
|
67
87
|
primary_refs: list[str] = []
|
|
68
88
|
for index, task in enumerate(tasks):
|
|
@@ -80,6 +100,7 @@ def validate_draft_task_tech_plan_refs(tech_plan_docs: list) -> list[dict]:
|
|
|
80
100
|
for ref in normalized_refs:
|
|
81
101
|
require(ref.startswith(".docs/03_tech_plan/"), f"Draft task {task.get('id')} docs.tech_plan must point into .docs/03_tech_plan/: {ref}")
|
|
82
102
|
require(ref in available_tech_plans, f"Draft task {task.get('id')} references missing or generated tech plan slice: {ref}")
|
|
103
|
+
validate_uiux_design_refs_for_draft_task(task, available_experience_docs)
|
|
83
104
|
validate_self_test_contract_tech_plan_binding(task, normalized_refs)
|
|
84
105
|
primary_refs.append(normalized_refs[0])
|
|
85
106
|
|
|
@@ -92,6 +113,38 @@ def validate_draft_task_tech_plan_refs(tech_plan_docs: list) -> list[dict]:
|
|
|
92
113
|
return tasks
|
|
93
114
|
|
|
94
115
|
|
|
116
|
+
def validate_uiux_design_refs_for_draft_task(task: dict, available_experience_docs: set[str]) -> None:
|
|
117
|
+
docs = task.get("docs")
|
|
118
|
+
if not isinstance(docs, dict):
|
|
119
|
+
return
|
|
120
|
+
task_id = task.get("id")
|
|
121
|
+
uiux_refs = [normalize_doc_ref(ref) for ref in as_list(docs.get("uiux"))]
|
|
122
|
+
design_refs = [normalize_doc_ref(ref) for ref in as_list(docs.get("design_system"))]
|
|
123
|
+
ui_task = is_ui_draft_task(task)
|
|
124
|
+
|
|
125
|
+
if ui_task:
|
|
126
|
+
require(uiux_refs, f"UI/frontend draft task {task_id} must reference a UI/UX slice in docs.uiux")
|
|
127
|
+
require(design_refs, f"UI/frontend draft task {task_id} must reference DESIGN.md in docs.design_system")
|
|
128
|
+
|
|
129
|
+
for ref in uiux_refs:
|
|
130
|
+
require(ref.startswith(".docs/02_experience/"), f"Draft task {task_id} docs.uiux must point into .docs/02_experience/: {ref}")
|
|
131
|
+
require(ref in available_experience_docs, f"Draft task {task_id} references missing or generated UI/UX slice: {ref}")
|
|
132
|
+
|
|
133
|
+
for ref in design_refs:
|
|
134
|
+
require(ref == "DESIGN.md", f"Draft task {task_id} docs.design_system must point to DESIGN.md: {ref}")
|
|
135
|
+
require(repo_path("DESIGN.md").exists(), f"Draft task {task_id} references missing design system: DESIGN.md")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def is_ui_draft_task(task: dict) -> bool:
|
|
139
|
+
docs = task.get("docs")
|
|
140
|
+
docs_text = ""
|
|
141
|
+
if isinstance(docs, dict):
|
|
142
|
+
docs_text = "\n".join(ref for value in docs.values() for ref in as_list(value))
|
|
143
|
+
runtime_text = "\n".join(str(task.get(key) or "") for key in ["target_runtime_environment", "self_test_contract"])
|
|
144
|
+
text = "\n".join(str(task.get(key) or "") for key in ["id", "title", "summary", "phase"]) + "\n" + docs_text + "\n" + runtime_text
|
|
145
|
+
return contains_any(text, UI_DRAFT_TASK_TERMS)
|
|
146
|
+
|
|
147
|
+
|
|
95
148
|
def validate_cross_cutting_architecture(product_docs: list, tech_plan_docs: list, architecture_docs: list, draft_tasks: list[dict]) -> None:
|
|
96
149
|
source_text = "\n".join(
|
|
97
150
|
[
|
|
@@ -48,6 +48,23 @@ SELF_TEST_TRIGGER_TERMS = [
|
|
|
48
48
|
"阻塞",
|
|
49
49
|
]
|
|
50
50
|
SELF_TEST_IMPACT_TERMS = ["development self-test impact", "开发自测影响"]
|
|
51
|
+
UI_UX_TRIGGER_TERMS = [
|
|
52
|
+
"ui/ux",
|
|
53
|
+
"ux",
|
|
54
|
+
"screen",
|
|
55
|
+
"interaction",
|
|
56
|
+
"design.md",
|
|
57
|
+
"frontend",
|
|
58
|
+
"browser",
|
|
59
|
+
"page",
|
|
60
|
+
"visual",
|
|
61
|
+
"体验",
|
|
62
|
+
"屏幕",
|
|
63
|
+
"交互",
|
|
64
|
+
"视觉",
|
|
65
|
+
"前端",
|
|
66
|
+
]
|
|
67
|
+
UI_UX_IMPACT_TERMS = ["ui/ux impact", "体验影响"]
|
|
51
68
|
|
|
52
69
|
|
|
53
70
|
def superseded_test_docs(docs) -> list[str]:
|
|
@@ -85,6 +102,7 @@ def main() -> None:
|
|
|
85
102
|
invalid = [status for status in statuses if status not in allowed]
|
|
86
103
|
require(not invalid, "Invalid RFC status: " + ", ".join(invalid))
|
|
87
104
|
validate_development_self_test_impact(docs)
|
|
105
|
+
validate_uiux_impact(docs)
|
|
88
106
|
print(f"RFC artifacts OK: {len(docs)} file(s)")
|
|
89
107
|
|
|
90
108
|
|
|
@@ -101,6 +119,19 @@ def validate_development_self_test_impact(docs) -> None:
|
|
|
101
119
|
)
|
|
102
120
|
|
|
103
121
|
|
|
122
|
+
def validate_uiux_impact(docs) -> None:
|
|
123
|
+
for doc in docs:
|
|
124
|
+
number = rfc_number(doc.name)
|
|
125
|
+
if number is not None and number < 27:
|
|
126
|
+
continue
|
|
127
|
+
text = doc.read_text(encoding="utf-8")
|
|
128
|
+
if contains_any(text, UI_UX_TRIGGER_TERMS):
|
|
129
|
+
require(
|
|
130
|
+
contains_any(text, UI_UX_IMPACT_TERMS),
|
|
131
|
+
f"{doc.relative_to(repo_path('.')).as_posix()} must include UI/UX Impact when RFC changes experience docs, screen contracts, DESIGN.md, frontend, or browser behavior",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
104
135
|
def rfc_number(file_name: str) -> int | None:
|
|
105
136
|
match = re.match(r"^RFC[_-](\d+)", file_name, re.IGNORECASE)
|
|
106
137
|
return int(match.group(1)) if match else None
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
2
5
|
from harness_utils import (
|
|
3
6
|
changed_files,
|
|
4
7
|
contains_any,
|
|
@@ -13,7 +16,9 @@ from harness_utils import (
|
|
|
13
16
|
)
|
|
14
17
|
|
|
15
18
|
TEST_REPORT_PATH = ".docs/07_test/TEST_REPORT.md"
|
|
19
|
+
TEST_CASES_PATH = ".docs/07_test/TEST_CASES.md"
|
|
16
20
|
PLACEHOLDER_TERMS = ["pending", "tbd", "todo", "待填", "待补", "placeholder"]
|
|
21
|
+
CASE_ID_RE = re.compile(r"\bTC-\d{3,}\b")
|
|
17
22
|
MISSING_READINESS_TERMS = [
|
|
18
23
|
"missing entry",
|
|
19
24
|
"missing exit",
|
|
@@ -44,6 +49,10 @@ MISSING_READINESS_TERMS = [
|
|
|
44
49
|
]
|
|
45
50
|
|
|
46
51
|
|
|
52
|
+
def as_list(value) -> list:
|
|
53
|
+
return value if isinstance(value, list) else []
|
|
54
|
+
|
|
55
|
+
|
|
47
56
|
def read_test_report() -> tuple[str, str]:
|
|
48
57
|
require(
|
|
49
58
|
repo_path(TEST_REPORT_PATH).exists(),
|
|
@@ -52,8 +61,115 @@ def read_test_report() -> tuple[str, str]:
|
|
|
52
61
|
return read_text(TEST_REPORT_PATH), TEST_REPORT_PATH
|
|
53
62
|
|
|
54
63
|
|
|
64
|
+
def test_case_refs(text: str) -> list[str]:
|
|
65
|
+
return sorted(set(CASE_ID_RE.findall(text)))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def plan_references_test_cases(plan: dict) -> bool:
|
|
69
|
+
for task in as_list(plan.get("tasks")):
|
|
70
|
+
if not isinstance(task, dict):
|
|
71
|
+
continue
|
|
72
|
+
if str(task.get("phase") or "") != "TESTING":
|
|
73
|
+
continue
|
|
74
|
+
refs = [str(item) for item in as_list(task.get("result_docs"))]
|
|
75
|
+
if TEST_CASES_PATH in refs:
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def split_markdown_row(line: str) -> list[str]:
|
|
81
|
+
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_separator_row(line: str) -> bool:
|
|
85
|
+
stripped = line.strip()
|
|
86
|
+
return stripped.startswith("|") and set(stripped.replace("|", "").strip()) <= {"-", ":", " "}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def find_table_header(lines: list[str], row_index: int) -> Optional[list[str]]:
|
|
90
|
+
for index in range(row_index - 1, 0, -1):
|
|
91
|
+
if not is_separator_row(lines[index]):
|
|
92
|
+
continue
|
|
93
|
+
header = lines[index - 1]
|
|
94
|
+
if header.strip().startswith("|"):
|
|
95
|
+
return split_markdown_row(header)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def header_index(headers: list[str], *terms: str) -> Optional[int]:
|
|
100
|
+
lowered = [header.lower() for header in headers]
|
|
101
|
+
for index, header in enumerate(lowered):
|
|
102
|
+
if any(term in header for term in terms):
|
|
103
|
+
return index
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def required_cell(cells: list[str], index: Optional[int]) -> str:
|
|
108
|
+
if index is None or index >= len(cells):
|
|
109
|
+
return ""
|
|
110
|
+
return cells[index].strip()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_test_cases(text: str, report_text: str) -> list[str]:
|
|
114
|
+
errors: list[str] = []
|
|
115
|
+
if contains_any(text, PLACEHOLDER_TERMS):
|
|
116
|
+
errors.append("TEST_CASES.md must not contain pending/TBD/TODO/placeholder content")
|
|
117
|
+
|
|
118
|
+
lines = text.splitlines()
|
|
119
|
+
rows: list[tuple[int, list[str], list[str]]] = []
|
|
120
|
+
for index, line in enumerate(lines):
|
|
121
|
+
if not line.strip().startswith("|") or not CASE_ID_RE.search(line):
|
|
122
|
+
continue
|
|
123
|
+
header = find_table_header(lines, index)
|
|
124
|
+
if header is None:
|
|
125
|
+
errors.append("TEST_CASES.md cases must be listed in a Markdown table with headers")
|
|
126
|
+
continue
|
|
127
|
+
rows.append((index + 1, header, split_markdown_row(line)))
|
|
128
|
+
|
|
129
|
+
ids = [CASE_ID_RE.search("|".join(cells)).group(0) for _, _, cells in rows if CASE_ID_RE.search("|".join(cells))]
|
|
130
|
+
if not ids:
|
|
131
|
+
errors.append("TEST_CASES.md must include at least one TC-* case")
|
|
132
|
+
duplicates = sorted({case_id for case_id in ids if ids.count(case_id) > 1})
|
|
133
|
+
if duplicates:
|
|
134
|
+
errors.append(f"TEST_CASES.md Case ID must be unique: {', '.join(duplicates)}")
|
|
135
|
+
|
|
136
|
+
for line_number, headers, cells in rows:
|
|
137
|
+
requirement = header_index(headers, "requirement", "risk", "需求", "风险")
|
|
138
|
+
runnable_entry = header_index(headers, "runnable entry", "runnable", "entry", "入口")
|
|
139
|
+
steps = header_index(headers, "steps", "步骤")
|
|
140
|
+
expected_exit = header_index(headers, "expected exit", "expected result", "expected", "预期", "出口")
|
|
141
|
+
missing = []
|
|
142
|
+
if not required_cell(cells, requirement):
|
|
143
|
+
missing.append("Requirement / Risk Ref")
|
|
144
|
+
if not required_cell(cells, runnable_entry):
|
|
145
|
+
missing.append("Runnable Entry")
|
|
146
|
+
if not required_cell(cells, steps):
|
|
147
|
+
missing.append("Steps")
|
|
148
|
+
if not required_cell(cells, expected_exit):
|
|
149
|
+
missing.append("Expected Exit")
|
|
150
|
+
if missing:
|
|
151
|
+
errors.append(f"TEST_CASES.md row {line_number} missing required case fields: {', '.join(missing)}")
|
|
152
|
+
|
|
153
|
+
refs = test_case_refs(report_text)
|
|
154
|
+
missing_refs = sorted(set(refs) - set(ids))
|
|
155
|
+
if missing_refs:
|
|
156
|
+
errors.append(f"TEST_REPORT.md references case IDs not found in TEST_CASES.md: {', '.join(missing_refs)}")
|
|
157
|
+
return errors
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_test_cases_if_needed(plan: dict, report_text: str) -> None:
|
|
161
|
+
cases_path = repo_path(TEST_CASES_PATH)
|
|
162
|
+
should_validate = bool(test_case_refs(report_text)) or plan_references_test_cases(plan) or cases_path.exists()
|
|
163
|
+
if not should_validate:
|
|
164
|
+
return
|
|
165
|
+
require(cases_path.exists(), f"Missing test cases: expected {TEST_CASES_PATH} because TEST_REPORT.md or current TESTING task references test cases")
|
|
166
|
+
for error in validate_test_cases(read_text(TEST_CASES_PATH), report_text):
|
|
167
|
+
require(False, error)
|
|
168
|
+
|
|
169
|
+
|
|
55
170
|
def main() -> None:
|
|
56
|
-
|
|
171
|
+
plan = load_plan()
|
|
172
|
+
validate_plan_contract(plan, allow_open=False)
|
|
57
173
|
text, source = read_test_report()
|
|
58
174
|
require(
|
|
59
175
|
not contains_any(text, PLACEHOLDER_TERMS),
|
|
@@ -72,6 +188,7 @@ def main() -> None:
|
|
|
72
188
|
not contains_any(text, MISSING_READINESS_TERMS),
|
|
73
189
|
"Test report cannot PASS while runnable entry/exit or Development Evidence is missing; use BLOCKED with recovery conditions",
|
|
74
190
|
)
|
|
191
|
+
validate_test_cases_if_needed(plan, text)
|
|
75
192
|
if load_lifecycle().get("current_phase") == "TESTING":
|
|
76
193
|
for error in testing_boundary_errors_for_changed_files(changed_files()):
|
|
77
194
|
require(False, error)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from harness_utils import (
|
|
6
|
+
contains_any,
|
|
7
|
+
load_lifecycle,
|
|
8
|
+
load_plan,
|
|
9
|
+
markdown_deliverables,
|
|
10
|
+
repo_path,
|
|
11
|
+
run_main,
|
|
12
|
+
require,
|
|
13
|
+
validate_plan_contract,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESIGN_MD = "DESIGN.md"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
lifecycle = load_lifecycle()
|
|
22
|
+
plan = load_plan()
|
|
23
|
+
validate_plan_contract(plan, allow_open=lifecycle.get("current_phase") != "UI_UX_DESIGNING")
|
|
24
|
+
|
|
25
|
+
docs = markdown_deliverables(".docs/02_experience")
|
|
26
|
+
require(docs, "No UI/UX deliverables found in .docs/02_experience/")
|
|
27
|
+
visual_ui = False
|
|
28
|
+
for doc in docs:
|
|
29
|
+
text = doc.read_text(encoding="utf-8")
|
|
30
|
+
relative = doc.relative_to(repo_path(".")).as_posix()
|
|
31
|
+
not_applicable = contains_any(text, ["applicability: not_applicable", "applicability: `not_applicable`"])
|
|
32
|
+
visual_ui = visual_ui or contains_any(text, ["applicability: visual_ui", "applicability: `visual_ui`"])
|
|
33
|
+
if not_applicable:
|
|
34
|
+
continue
|
|
35
|
+
require(contains_any(text, ["prd", "requirement", "需求"]), f"{relative} must cite PRD and requirement IDs")
|
|
36
|
+
require(contains_any(text, ["user journey", "user journeys", "用户旅程"]), f"{relative} must include user journeys")
|
|
37
|
+
require(contains_any(text, ["handoff matrix", "交接矩阵"]), f"{relative} must include a handoff matrix")
|
|
38
|
+
require(
|
|
39
|
+
contains_any(text, ["loading", "empty", "error", "success", "permission", "加载", "空状态", "错误", "成功", "权限"]),
|
|
40
|
+
f"{relative} screen contracts must cover applicable loading/empty/error/success/permission states",
|
|
41
|
+
)
|
|
42
|
+
require(contains_any(text, ["responsive", "breakpoint", "响应式", "断点"]), f"{relative} must include responsive acceptance")
|
|
43
|
+
require(
|
|
44
|
+
contains_any(text, ["accessibility", "a11y", "focus", "keyboard", "touch", "无障碍", "焦点", "键盘", "触控"]),
|
|
45
|
+
f"{relative} must include accessibility/focus/keyboard/touch expectations",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if visual_ui:
|
|
49
|
+
design_path = repo_path(DESIGN_MD)
|
|
50
|
+
require(design_path.exists(), "visual UI experience requires root DESIGN.md")
|
|
51
|
+
validate_design_md()
|
|
52
|
+
|
|
53
|
+
print(f"UI/UX artifacts OK: {len(docs)} experience deliverable(s)")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_design_md() -> None:
|
|
57
|
+
command = designmd_command()
|
|
58
|
+
try:
|
|
59
|
+
proc = subprocess.run(
|
|
60
|
+
command + ["lint", DESIGN_MD],
|
|
61
|
+
cwd=repo_path("."),
|
|
62
|
+
text=True,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
check=False,
|
|
65
|
+
)
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
require(False, "DESIGN.md linter not found; install @google/design.md or run npm install")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
output = (proc.stdout or "").strip()
|
|
71
|
+
diagnostics = None
|
|
72
|
+
if output:
|
|
73
|
+
try:
|
|
74
|
+
diagnostics = json.loads(output)
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
diagnostics = None
|
|
77
|
+
|
|
78
|
+
errors = []
|
|
79
|
+
if isinstance(diagnostics, dict):
|
|
80
|
+
summary = diagnostics.get("summary") if isinstance(diagnostics.get("summary"), dict) else {}
|
|
81
|
+
if int(summary.get("errors") or 0) > 0:
|
|
82
|
+
errors.append("DESIGN.md linter reported errors")
|
|
83
|
+
for finding in diagnostics.get("findings") or []:
|
|
84
|
+
if isinstance(finding, dict) and str(finding.get("severity") or "").lower() == "error":
|
|
85
|
+
errors.append(str(finding.get("message") or "DESIGN.md linter error"))
|
|
86
|
+
elif proc.returncode != 0:
|
|
87
|
+
errors.append((proc.stderr or proc.stdout or "DESIGN.md linter failed").strip())
|
|
88
|
+
|
|
89
|
+
if errors:
|
|
90
|
+
require(False, "; ".join(errors))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def designmd_command() -> list[str]:
|
|
94
|
+
local_bin = repo_path("node_modules/.bin/designmd")
|
|
95
|
+
if local_bin.exists():
|
|
96
|
+
return [str(local_bin)]
|
|
97
|
+
return ["npx", "--no-install", "designmd"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
run_main(main)
|
package/dist/commands/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export const commands = {
|
|
|
15
15
|
"validate-current": (args) => validate(["validate-current", ...args]),
|
|
16
16
|
"validate-plan": (args) => validate(["validate-plan", ...args]),
|
|
17
17
|
"validate-pm": (args) => validate(["validate-pm", ...args]),
|
|
18
|
+
"validate-uiux": (args) => validate(["validate-uiux", ...args]),
|
|
18
19
|
"validate-design": (args) => validate(["validate-design", ...args]),
|
|
19
20
|
"validate-dev": (args) => validate(["validate-dev", ...args]),
|
|
20
21
|
"validate-review": (args) => validate(["validate-review", ...args]),
|
|
@@ -31,6 +32,6 @@ export function help() {
|
|
|
31
32
|
upgrade Run migrations and then sync
|
|
32
33
|
doctor Diagnose project configuration and drift
|
|
33
34
|
validate <gate> Run a Harness validation gate
|
|
34
|
-
validate-* Run a named gate directly, including validate-plan/review/test/release/rfc
|
|
35
|
+
validate-* Run a named gate directly, including validate-plan/uiux/design/dev/review/test/release/rfc
|
|
35
36
|
package <subcommand> Maintain package canonical source`);
|
|
36
37
|
}
|
package/dist/lib/init.js
CHANGED