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.
Files changed (32) hide show
  1. package/README.md +14 -7
  2. package/assets/agents/AGENTS_CORE.md +14 -9
  3. package/assets/docs/README.md +21 -7
  4. package/assets/make/sdlc-harness.mk +5 -1
  5. package/assets/policies/allowed_paths.yaml +9 -0
  6. package/assets/policies/gates.yaml +6 -0
  7. package/assets/policies/phase_contracts.yaml +49 -0
  8. package/assets/skills/pjsdlc_architect_design/SKILL.md +14 -8
  9. package/assets/skills/pjsdlc_dev_sprint/SKILL.md +8 -3
  10. package/assets/skills/pjsdlc_implementation_doc/SKILL.md +9 -4
  11. package/assets/skills/pjsdlc_manager/SKILL.md +17 -16
  12. package/assets/skills/pjsdlc_reviewer/SKILL.md +6 -1
  13. package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +8 -5
  14. package/assets/skills/pjsdlc_tester/SKILL.md +12 -4
  15. package/assets/skills/pjsdlc_uiux_design/SKILL.md +76 -0
  16. package/assets/templates/PLAN_TEMPLATE.yaml +4 -0
  17. package/assets/templates/REVIEW_TEMPLATE.md +14 -4
  18. package/assets/templates/RFC_TEMPLATE.md +13 -5
  19. package/assets/templates/TECH_DESIGN_TEMPLATE.md +18 -10
  20. package/assets/templates/TEST_CASES_TEMPLATE.md +5 -3
  21. package/assets/templates/TEST_STRATEGY_TEMPLATE.md +4 -0
  22. package/assets/templates/UI_UX_DESIGN_TEMPLATE.md +67 -0
  23. package/assets/tools/harness_utils.py +4 -2
  24. package/assets/tools/validate_design.py +55 -2
  25. package/assets/tools/validate_harness.py +1 -0
  26. package/assets/tools/validate_rfc.py +31 -0
  27. package/assets/tools/validate_test_plan.py +118 -1
  28. package/assets/tools/validate_uiux_design.py +101 -0
  29. package/dist/commands/index.js +2 -1
  30. package/dist/lib/init.js +1 -0
  31. package/dist/lib/validators.js +319 -6
  32. 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
- ## 4. 接口契约(Interface Contract)
36
+ ## 5. 接口契约(Interface Contract)
29
37
 
30
38
  | 接口(Interface) | 方法/事件(Method/Event) | 请求(Request) | 响应(Response) | 错误(Errors) |
31
39
  |---|---|---|---|---|
32
40
  | | | | | |
33
41
 
34
- ## 5. 数据模型(Data Model)
42
+ ## 6. 数据模型(Data Model)
35
43
 
36
44
  | 实体(Entity) | 字段(Fields) | 负责人(Owner) | 备注(Notes) |
37
45
  |---|---|---|---|
38
46
  | | | | |
39
47
 
40
- ## 6. 任务拆分(Task Breakdown)
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
- ## 7. Development Self-Test Contract(开发自测合同,待执行)
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
- ## 8. 风险与缓解
73
+ ## 9. 风险与缓解
66
74
 
67
75
  | 风险(Risk) | 等级(Level) | 缓解措施(Mitigation) |
68
76
  |---|---|---|
69
77
  | | P1 | |
70
78
 
71
- ## 9. 需要关注的方案偏移
79
+ ## 10. 需要关注的方案偏移
72
80
 
73
81
  -
@@ -9,17 +9,19 @@
9
9
 
10
10
  ## 2. Cases(用例)
11
11
 
12
- | ID | Requirement | Preconditions | Steps | Expected Result |
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
  [
@@ -32,6 +32,7 @@ def main() -> None:
32
32
  required_dirs = [
33
33
  ".docs/00_raw",
34
34
  ".docs/01_product",
35
+ ".docs/02_experience",
35
36
  ".docs/02_architecture",
36
37
  ".docs/03_tech_plan",
37
38
  ".docs/04_implementation",
@@ -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
- validate_plan_contract(load_plan(), allow_open=False)
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)
@@ -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
@@ -7,6 +7,7 @@ import { syncDocsIndexMaintenanceSection, syncMemoryGuidanceSection } from "./us
7
7
  const DOC_DIRS = [
8
8
  ".docs/00_raw",
9
9
  ".docs/01_product",
10
+ ".docs/02_experience",
10
11
  ".docs/02_architecture",
11
12
  ".docs/03_tech_plan",
12
13
  ".docs/04_implementation",