agent-project-sdlc 0.1.24 → 0.1.26
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 +35 -10
- package/assets/agents/AGENTS_CORE.md +14 -9
- package/assets/docs/README.md +64 -11
- 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 +92 -18
- package/assets/tools/transition.py +2 -1
- package/assets/tools/validate_allowed_paths.py +2 -2
- package/assets/tools/validate_design.py +56 -3
- package/assets/tools/validate_dev_state.py +1 -1
- package/assets/tools/validate_harness.py +17 -14
- package/assets/tools/validate_plan_draft.py +1 -1
- package/assets/tools/validate_prompt_language.py +17 -17
- 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 +5 -1
- package/dist/commands/inspect-workflow.d.ts +1 -0
- package/dist/commands/inspect-workflow.js +71 -0
- package/dist/lib/harness-root.js +5 -5
- package/dist/lib/init.js +7 -3
- package/dist/lib/validators.js +341 -27
- package/dist/lib/workflow-inspector.d.ts +35 -0
- package/dist/lib/workflow-inspector.js +340 -0
- package/package.json +2 -1
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
## 1. Review 范围
|
|
4
4
|
|
|
5
5
|
- PRD:
|
|
6
|
+
- UI/UX:
|
|
7
|
+
- DESIGN.md:
|
|
6
8
|
- 技术方案(Technical design):
|
|
7
9
|
- 实现文档(Implementation docs):
|
|
8
10
|
- Diff/commit:
|
|
@@ -21,11 +23,19 @@
|
|
|
21
23
|
|
|
22
24
|
-
|
|
23
25
|
|
|
24
|
-
## 5.
|
|
26
|
+
## 5. UX / Design Conformance(体验与设计一致性)
|
|
27
|
+
|
|
28
|
+
- Screen contracts:
|
|
29
|
+
- Interaction states:
|
|
30
|
+
- Responsive / accessibility:
|
|
31
|
+
- DESIGN.md token usage:
|
|
32
|
+
- Blocking gaps before TESTING:
|
|
33
|
+
|
|
34
|
+
## 6. Test Gaps(测试缺口)
|
|
25
35
|
|
|
26
36
|
-
|
|
27
37
|
|
|
28
|
-
##
|
|
38
|
+
## 7. Runnable Entry/Exit Readiness(可运行入口/出口)
|
|
29
39
|
|
|
30
40
|
- Entry points:
|
|
31
41
|
- Exit / side effects:
|
|
@@ -37,7 +47,7 @@
|
|
|
37
47
|
- Testing Handoff Contract:
|
|
38
48
|
- Blocking gaps before TESTING:
|
|
39
49
|
|
|
40
|
-
##
|
|
50
|
+
## 8. Application Readiness Checklist(应用就绪检查)
|
|
41
51
|
|
|
42
52
|
- Runnable Entry: `PASS` / `BLOCKED`
|
|
43
53
|
- Observable Exit: `PASS` / `BLOCKED`
|
|
@@ -45,7 +55,7 @@
|
|
|
45
55
|
- Config Contract: `PASS` / `BLOCKED`
|
|
46
56
|
- Testing Handoff Readiness: `PASS` / `BLOCKED`
|
|
47
57
|
|
|
48
|
-
##
|
|
58
|
+
## 9. Gate Result(阶段结论)
|
|
49
59
|
|
|
50
60
|
- Decision: `PASS` / `BLOCKED`
|
|
51
61
|
- Required before testing:
|
|
@@ -23,22 +23,30 @@
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| | | low/medium/high |
|
|
25
25
|
|
|
26
|
-
## 5.
|
|
26
|
+
## 5. UI/UX Impact(体验影响)
|
|
27
|
+
|
|
28
|
+
- Reviewed experience docs:
|
|
29
|
+
- DESIGN.md impact:
|
|
30
|
+
- Superseded screen contracts: none
|
|
31
|
+
- Retained UX facts:
|
|
32
|
+
- Reason:
|
|
33
|
+
|
|
34
|
+
## 6. Acceptance Criteria
|
|
27
35
|
|
|
28
36
|
- [ ]
|
|
29
37
|
|
|
30
|
-
##
|
|
38
|
+
## 7. Regression Requirements(回归要求)
|
|
31
39
|
|
|
32
40
|
- [ ]
|
|
33
41
|
|
|
34
|
-
##
|
|
42
|
+
## 8. Test Fact Source Impact(测试事实源影响)
|
|
35
43
|
|
|
36
44
|
- Reviewed test docs:
|
|
37
45
|
- Superseded test docs: none
|
|
38
46
|
- Retained test docs:
|
|
39
47
|
- Reason:
|
|
40
48
|
|
|
41
|
-
##
|
|
49
|
+
## 9. Development Self-Test Impact(开发自测影响)
|
|
42
50
|
|
|
43
51
|
- Entry/exit impact:
|
|
44
52
|
- Runtime / target environment impact:
|
|
@@ -49,6 +57,6 @@
|
|
|
49
57
|
- Module key test path impact:
|
|
50
58
|
- Review / Testing handoff impact:
|
|
51
59
|
|
|
52
|
-
##
|
|
60
|
+
## 10. Status
|
|
53
61
|
|
|
54
62
|
- Status: DRAFT
|
|
@@ -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
|
+
- TBD
|
|
@@ -12,6 +12,8 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
DEFAULT_HARNESS_ROOT = ".agent"
|
|
16
|
+
HARNESS_JSON_CONFIG_PATH = "sdlc-harness.config.json"
|
|
15
17
|
|
|
16
18
|
TASK_STATUSES = {
|
|
17
19
|
"pending",
|
|
@@ -28,6 +30,7 @@ PARALLEL_TRIGGERS = {"user_requested", "workflow_default"}
|
|
|
28
30
|
PARALLEL_RUNTIME_PROVIDERS = {"codex_native_subagents", "user_orchestrated", "codex_exec_worktree"}
|
|
29
31
|
PARALLEL_ALLOWED_PHASES = {
|
|
30
32
|
"REQUIREMENT_GATHERING",
|
|
33
|
+
"UI_UX_DESIGNING",
|
|
31
34
|
"ARCHITECTING",
|
|
32
35
|
"SPRINTING",
|
|
33
36
|
"REVIEWING",
|
|
@@ -35,7 +38,7 @@ PARALLEL_ALLOWED_PHASES = {
|
|
|
35
38
|
"RELEASING",
|
|
36
39
|
"RFC_RECALIBRATION",
|
|
37
40
|
}
|
|
38
|
-
PARALLEL_READ_ONLY_PHASES = {"REQUIREMENT_GATHERING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"}
|
|
41
|
+
PARALLEL_READ_ONLY_PHASES = {"REQUIREMENT_GATHERING", "UI_UX_DESIGNING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"}
|
|
39
42
|
PARALLEL_PROTECTED_WRITE_PATTERNS = {
|
|
40
43
|
".codex/state/**",
|
|
41
44
|
"<harnessRoot>/state/**",
|
|
@@ -48,6 +51,7 @@ PARALLEL_PROTECTED_WRITE_PATTERNS = {
|
|
|
48
51
|
TASK_ID_PATTERN = re.compile(r"^[A-Z]+-(\d+)$")
|
|
49
52
|
TASK_PHASES = {
|
|
50
53
|
"REQUIREMENT_GATHERING",
|
|
54
|
+
"UI_UX_DESIGNING",
|
|
51
55
|
"ARCHITECTING",
|
|
52
56
|
"SPRINTING",
|
|
53
57
|
"REVIEWING",
|
|
@@ -68,7 +72,67 @@ def repo_path(relative: str | Path) -> Path:
|
|
|
68
72
|
return ROOT / relative
|
|
69
73
|
|
|
70
74
|
|
|
75
|
+
def normalize_harness_folder_name(value: str) -> str:
|
|
76
|
+
normalized = value.strip().replace("\\", "/").rstrip("/")
|
|
77
|
+
if not normalized or normalized in {".", ".."}:
|
|
78
|
+
raise HarnessError("harnessFolderName must be a non-empty relative directory")
|
|
79
|
+
if Path(normalized).is_absolute() or ".." in normalized.split("/"):
|
|
80
|
+
raise HarnessError("harnessFolderName must not be absolute or contain '..'")
|
|
81
|
+
return normalized
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def folder_name_from_object(value: Any) -> str | None:
|
|
85
|
+
if not isinstance(value, dict):
|
|
86
|
+
return None
|
|
87
|
+
folder_name = value.get("harnessFolderName") or value.get("harnessFloderName")
|
|
88
|
+
if isinstance(folder_name, str) and folder_name.strip():
|
|
89
|
+
return folder_name
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def read_json_config(relative: str) -> Any:
|
|
94
|
+
path = repo_path(relative)
|
|
95
|
+
if not path.exists():
|
|
96
|
+
return None
|
|
97
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def harness_root() -> str:
|
|
101
|
+
package_json = read_json_config("package.json")
|
|
102
|
+
package_config = package_json.get("sdlcHarness") if isinstance(package_json, dict) else None
|
|
103
|
+
package_value = folder_name_from_object(package_config)
|
|
104
|
+
if package_value:
|
|
105
|
+
return normalize_harness_folder_name(package_value)
|
|
106
|
+
|
|
107
|
+
explicit_config = read_json_config(HARNESS_JSON_CONFIG_PATH)
|
|
108
|
+
explicit_value = folder_name_from_object(explicit_config)
|
|
109
|
+
if explicit_value:
|
|
110
|
+
return normalize_harness_folder_name(explicit_value)
|
|
111
|
+
|
|
112
|
+
return DEFAULT_HARNESS_ROOT
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def harness_path(*segments: str) -> str:
|
|
116
|
+
return (Path(harness_root()).joinpath(*segments)).as_posix()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_harness_relative(relative: str | Path) -> str:
|
|
120
|
+
value = str(relative).replace("\\", "/")
|
|
121
|
+
root = harness_root()
|
|
122
|
+
if value == "<harnessRoot>":
|
|
123
|
+
return root
|
|
124
|
+
if value.startswith("<harnessRoot>/"):
|
|
125
|
+
return f"{root}/{value.removeprefix('<harnessRoot>/')}"
|
|
126
|
+
if root != ".codex":
|
|
127
|
+
if value == ".codex":
|
|
128
|
+
return root
|
|
129
|
+
if value.startswith(".codex/"):
|
|
130
|
+
return f"{root}/{value.removeprefix('.codex/')}"
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
71
134
|
def read_text(relative: str | Path) -> str:
|
|
135
|
+
relative = resolve_harness_relative(relative)
|
|
72
136
|
path = repo_path(relative)
|
|
73
137
|
if not path.exists():
|
|
74
138
|
raise HarnessError(f"Missing required file: {relative}")
|
|
@@ -76,6 +140,7 @@ def read_text(relative: str | Path) -> str:
|
|
|
76
140
|
|
|
77
141
|
|
|
78
142
|
def load_yaml(relative: str | Path) -> Any:
|
|
143
|
+
relative = resolve_harness_relative(relative)
|
|
79
144
|
path = repo_path(relative)
|
|
80
145
|
if not path.exists():
|
|
81
146
|
raise HarnessError(f"Missing required YAML file: {relative}")
|
|
@@ -112,6 +177,7 @@ def parse_yaml_text(text: str) -> Any:
|
|
|
112
177
|
|
|
113
178
|
|
|
114
179
|
def dump_yaml(data: Any, relative: str | Path) -> None:
|
|
180
|
+
relative = resolve_harness_relative(relative)
|
|
115
181
|
path = repo_path(relative)
|
|
116
182
|
path.write_text(to_simple_yaml(data).rstrip() + "\n", encoding="utf-8")
|
|
117
183
|
|
|
@@ -328,7 +394,8 @@ def require(condition: Any, message: str) -> None:
|
|
|
328
394
|
|
|
329
395
|
def require_paths(paths: list[str]) -> None:
|
|
330
396
|
for relative in paths:
|
|
331
|
-
|
|
397
|
+
resolved = resolve_harness_relative(relative)
|
|
398
|
+
require(repo_path(resolved).exists(), f"Missing required path: {resolved}")
|
|
332
399
|
|
|
333
400
|
|
|
334
401
|
def combined_text(paths: list[Path]) -> str:
|
|
@@ -842,13 +909,13 @@ def is_testing_runtime_boundary_change(path: str) -> bool:
|
|
|
842
909
|
|
|
843
910
|
|
|
844
911
|
def load_lifecycle() -> dict[str, Any]:
|
|
845
|
-
data = load_yaml("
|
|
912
|
+
data = load_yaml(harness_path("state", "lifecycle.yaml"))
|
|
846
913
|
require(isinstance(data, dict), "lifecycle.yaml must be a mapping")
|
|
847
914
|
return data
|
|
848
915
|
|
|
849
916
|
|
|
850
917
|
def load_phase_contract_data() -> dict[str, Any]:
|
|
851
|
-
data = load_yaml("
|
|
918
|
+
data = load_yaml(harness_path("pjsdlc_managed", "policies", "phase_contracts.yaml"))
|
|
852
919
|
require(isinstance(data, dict) and isinstance(data.get("phases"), dict), "phase_contracts.yaml must contain phases")
|
|
853
920
|
return data
|
|
854
921
|
|
|
@@ -1019,11 +1086,12 @@ def phase_transition_contract_errors(contract_data: dict[str, Any], require_tran
|
|
|
1019
1086
|
return errors
|
|
1020
1087
|
|
|
1021
1088
|
|
|
1022
|
-
def load_plan(path: str =
|
|
1023
|
-
|
|
1024
|
-
|
|
1089
|
+
def load_plan(path: str | None = None) -> dict[str, Any]:
|
|
1090
|
+
plan_path = path or harness_path("state", "plan.yaml")
|
|
1091
|
+
data = load_yaml(plan_path)
|
|
1092
|
+
require(isinstance(data, dict), f"{resolve_harness_relative(plan_path)} must be a mapping")
|
|
1025
1093
|
tasks = data.get("tasks", [])
|
|
1026
|
-
require(isinstance(tasks, list), f"{
|
|
1094
|
+
require(isinstance(tasks, list), f"{resolve_harness_relative(plan_path)} must contain a tasks list")
|
|
1027
1095
|
return data
|
|
1028
1096
|
|
|
1029
1097
|
|
|
@@ -1172,7 +1240,7 @@ def validate_parallel_execution_contract(data: dict[str, Any], current_phase: st
|
|
|
1172
1240
|
require("linked_task_id" not in contract, "parallel_execution must not define linked_task_id; use plan.yaml current_task_id")
|
|
1173
1241
|
require(
|
|
1174
1242
|
current_phase in PARALLEL_ALLOWED_PHASES,
|
|
1175
|
-
"parallel_execution is only supported during REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION",
|
|
1243
|
+
"parallel_execution is only supported during REQUIREMENT_GATHERING, UI_UX_DESIGNING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION",
|
|
1176
1244
|
)
|
|
1177
1245
|
require(contract.get("coordinator") == "main_agent", 'parallel_execution.coordinator must be "main_agent"')
|
|
1178
1246
|
|
|
@@ -1245,28 +1313,34 @@ def validate_parallel_worker_path_lock(data: dict[str, Any], worker: dict[str, A
|
|
|
1245
1313
|
require(not glob_patterns_overlap(owned, forbidden), f"{prefix}.owned_paths must not overlap forbidden paths: {owned} vs {forbidden}")
|
|
1246
1314
|
|
|
1247
1315
|
|
|
1248
|
-
def
|
|
1249
|
-
|
|
1316
|
+
def normalize_harness_pattern(pattern: str, root: str | None = None) -> str:
|
|
1317
|
+
actual_root = root or harness_root()
|
|
1318
|
+
return pattern.replace("\\", "/").replace("<harnessRoot>", actual_root)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def glob_prefix(pattern: str, root: str | None = None) -> str:
|
|
1322
|
+
normalized = normalize_harness_pattern(pattern, root)
|
|
1250
1323
|
wildcard_positions = [pos for pos in (normalized.find("*"), normalized.find("["), normalized.find("?")) if pos >= 0]
|
|
1251
1324
|
if wildcard_positions:
|
|
1252
1325
|
normalized = normalized[: min(wildcard_positions)]
|
|
1253
1326
|
return normalized.rstrip("/")
|
|
1254
1327
|
|
|
1255
1328
|
|
|
1256
|
-
def glob_patterns_overlap(left: str, right: str) -> bool:
|
|
1257
|
-
left_clean = left
|
|
1258
|
-
right_clean = right
|
|
1329
|
+
def glob_patterns_overlap(left: str, right: str, root: str | None = None) -> bool:
|
|
1330
|
+
left_clean = normalize_harness_pattern(left, root)
|
|
1331
|
+
right_clean = normalize_harness_pattern(right, root)
|
|
1259
1332
|
if fnmatch.fnmatch(left_clean, right_clean) or fnmatch.fnmatch(right_clean, left_clean):
|
|
1260
1333
|
return True
|
|
1261
|
-
left_prefix = glob_prefix(left_clean)
|
|
1262
|
-
right_prefix = glob_prefix(right_clean)
|
|
1334
|
+
left_prefix = glob_prefix(left_clean, root)
|
|
1335
|
+
right_prefix = glob_prefix(right_clean, root)
|
|
1263
1336
|
if not left_prefix or not right_prefix:
|
|
1264
1337
|
return left_prefix == right_prefix
|
|
1265
1338
|
return left_prefix.startswith(right_prefix + "/") or right_prefix.startswith(left_prefix + "/") or left_prefix == right_prefix
|
|
1266
1339
|
|
|
1267
1340
|
|
|
1268
|
-
def expand_harness_root(patterns: list[str], root: str =
|
|
1269
|
-
|
|
1341
|
+
def expand_harness_root(patterns: list[str], root: str | None = None) -> list[str]:
|
|
1342
|
+
actual_root = root or harness_root()
|
|
1343
|
+
return [str(pattern).replace("<harnessRoot>", actual_root) for pattern in patterns]
|
|
1270
1344
|
|
|
1271
1345
|
|
|
1272
1346
|
def task_by_id(plan_data: dict[str, Any], task_id: str) -> dict[str, Any] | None:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from harness_utils import (
|
|
3
3
|
dump_yaml,
|
|
4
4
|
find_phase_transition,
|
|
5
|
+
harness_path,
|
|
5
6
|
load_lifecycle,
|
|
6
7
|
load_phase_contract_data,
|
|
7
8
|
make_arg_parser,
|
|
@@ -51,7 +52,7 @@ def main() -> None:
|
|
|
51
52
|
str(lifecycle.get("suspended_phase") or ""),
|
|
52
53
|
)
|
|
53
54
|
|
|
54
|
-
dump_yaml(lifecycle, "
|
|
55
|
+
dump_yaml(lifecycle, harness_path("state", "lifecycle.yaml"))
|
|
55
56
|
print(f"Transitioned {current} -> {target}")
|
|
56
57
|
if args.reason:
|
|
57
58
|
print(f"Note: {args.reason}")
|
|
@@ -18,7 +18,7 @@ def main() -> None:
|
|
|
18
18
|
tasks = [task for task in data.get("tasks", []) if isinstance(task, dict)]
|
|
19
19
|
open_tasks = [task for task in tasks if task.get("status") in OPEN_TASK_STATUSES]
|
|
20
20
|
|
|
21
|
-
policies = load_yaml("
|
|
21
|
+
policies = load_yaml("<harnessRoot>/pjsdlc_managed/policies/allowed_paths.yaml")
|
|
22
22
|
lifecycle = load_lifecycle()
|
|
23
23
|
current_phase = lifecycle.get("current_phase") or "SPRINTING"
|
|
24
24
|
phase_policy = ((policies.get("phases") or {}).get(current_phase) or {})
|
|
@@ -29,7 +29,7 @@ def main() -> None:
|
|
|
29
29
|
task = task_by_id(data, current_task_id) if current_task_id else None
|
|
30
30
|
require(task, "current_task_id must point to the task being validated")
|
|
31
31
|
require(task.get("status") in OPEN_TASK_STATUSES, "current_task_id must point to an open task for path validation")
|
|
32
|
-
allowed = list(task.get("allowed_paths") or []) + list(always_allow)
|
|
32
|
+
allowed = expand_harness_root(list(task.get("allowed_paths") or [])) + list(always_allow)
|
|
33
33
|
else:
|
|
34
34
|
print("Allowed paths skipped: no open task")
|
|
35
35
|
return
|
|
@@ -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,19 +69,20 @@ 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]:
|
|
59
|
-
draft = load_plan("
|
|
77
|
+
def validate_draft_task_tech_plan_refs(tech_plan_docs: list, experience_docs: list) -> list[dict]:
|
|
78
|
+
draft = load_plan("<harnessRoot>/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")
|
|
62
81
|
tasks = draft.get("tasks", [])
|
|
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
|
[
|
|
@@ -3,7 +3,7 @@ from harness_utils import load_plan, require, run_main
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def main() -> None:
|
|
6
|
-
draft = load_plan("
|
|
6
|
+
draft = load_plan("<harnessRoot>/state/plan.draft.yaml")
|
|
7
7
|
require("current_phase" not in draft, "plan.draft.yaml must not define current_phase; lifecycle.yaml is the single source for current_phase")
|
|
8
8
|
require("current_task_id" not in draft, "plan.draft.yaml must not define current_task_id because drafts are not active task state")
|
|
9
9
|
tasks = [task for task in draft.get("tasks", []) if isinstance(task, dict)]
|