agent-project-sdlc 0.1.18 → 0.1.20
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 +11 -9
- package/assets/agents/AGENTS_CORE.md +2 -2
- package/assets/docs/README.md +12 -10
- package/assets/skills/pjsdlc_architect_design/SKILL.md +4 -2
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +11 -8
- package/assets/skills/pjsdlc_implementation_doc/SKILL.md +7 -3
- package/assets/skills/pjsdlc_manager/SKILL.md +4 -4
- package/assets/skills/pjsdlc_pm_prd/SKILL.md +3 -3
- package/assets/skills/pjsdlc_release_manager/SKILL.md +2 -0
- package/assets/skills/pjsdlc_reviewer/SKILL.md +5 -2
- package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +5 -2
- package/assets/skills/pjsdlc_tester/SKILL.md +5 -4
- package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +21 -7
- package/assets/templates/PLAN_TEMPLATE.yaml +27 -6
- package/assets/templates/RFC_TEMPLATE.md +12 -1
- package/assets/templates/TECH_DESIGN_TEMPLATE.md +19 -2
- package/assets/tools/build_doc_overviews.py +152 -0
- package/assets/tools/harness_utils.py +858 -0
- package/assets/tools/impact_analyzer.py +51 -0
- package/assets/tools/run_current_gate.py +29 -0
- package/assets/tools/status.py +29 -0
- package/assets/tools/transition.py +68 -0
- package/assets/tools/validate_allowed_paths.py +44 -0
- package/assets/tools/validate_design.py +199 -0
- package/assets/tools/validate_dev_state.py +20 -0
- package/assets/tools/validate_harness.py +60 -0
- package/assets/tools/validate_plan.py +24 -0
- package/assets/tools/validate_plan_draft.py +19 -0
- package/assets/tools/validate_prd.py +27 -0
- package/assets/tools/validate_prompt_language.py +138 -0
- package/assets/tools/validate_release_plan.py +37 -0
- package/assets/tools/validate_review.py +59 -0
- package/assets/tools/validate_rfc.py +105 -0
- package/assets/tools/validate_task_docs.py +40 -0
- package/assets/tools/validate_test_plan.py +82 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/migrations.js +3 -0
- package/dist/lib/sync-engine.js +4 -0
- package/dist/lib/validators.js +351 -17
- package/package.json +1 -1
- package/source-mappings.yaml +6 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from harness_utils import ROOT, HarnessError, load_yaml, require, run_main
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SKILL_REQUIRED_SECTIONS = ["## 目的", "## 角色提示词", "## 输入", "## 规则", "## 完成检查"]
|
|
10
|
+
ARTIFACT_SKILLS_REQUIRE_SEMANTIC_SLICING = {
|
|
11
|
+
"pjsdlc_pm_prd",
|
|
12
|
+
"pjsdlc_architect_design",
|
|
13
|
+
"pjsdlc_dev_sprint",
|
|
14
|
+
"pjsdlc_implementation_doc",
|
|
15
|
+
"pjsdlc_reviewer",
|
|
16
|
+
"pjsdlc_tester",
|
|
17
|
+
"pjsdlc_release_manager",
|
|
18
|
+
"pjsdlc_rfc_recalibrate",
|
|
19
|
+
}
|
|
20
|
+
SKILL_FORBIDDEN_HEADINGS = [
|
|
21
|
+
"## Purpose",
|
|
22
|
+
"## Required Inputs",
|
|
23
|
+
"## Outputs",
|
|
24
|
+
"## Rules",
|
|
25
|
+
"## Completion Checklist",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
MACHINE_IDENTIFIERS = [
|
|
29
|
+
"current_phase",
|
|
30
|
+
"active_skill",
|
|
31
|
+
"allowed_paths",
|
|
32
|
+
"required_gates",
|
|
33
|
+
"implementation_doc",
|
|
34
|
+
"REQUIREMENT_GATHERING",
|
|
35
|
+
"ARCHITECTING",
|
|
36
|
+
"SPRINTING",
|
|
37
|
+
"REVIEWING",
|
|
38
|
+
"TESTING",
|
|
39
|
+
"RELEASING",
|
|
40
|
+
"RFC_RECALIBRATION",
|
|
41
|
+
"BLOCKED",
|
|
42
|
+
"pending",
|
|
43
|
+
"in_progress",
|
|
44
|
+
"done",
|
|
45
|
+
"pending_revision",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
REQUIRED_AGENTS_TERMS = [
|
|
49
|
+
"Skill Language Contract",
|
|
50
|
+
"中文解释 + 英文精确标识符",
|
|
51
|
+
".codex/state/lifecycle.yaml",
|
|
52
|
+
".codex/state/plan.yaml",
|
|
53
|
+
"make validate-current",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
YAML_KEYWORDS = {
|
|
57
|
+
"lifecycle": [
|
|
58
|
+
"project_name",
|
|
59
|
+
"version",
|
|
60
|
+
"current_phase",
|
|
61
|
+
"active_role",
|
|
62
|
+
"active_skill",
|
|
63
|
+
"current_milestone",
|
|
64
|
+
"allowed_next_phases",
|
|
65
|
+
],
|
|
66
|
+
"plan": [
|
|
67
|
+
"current_task_id",
|
|
68
|
+
"next_task_sequence",
|
|
69
|
+
"tasks",
|
|
70
|
+
],
|
|
71
|
+
"phase_contracts": ["phases"],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def text(path: Path) -> str:
|
|
76
|
+
return path.read_text(encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def validate_agents() -> None:
|
|
80
|
+
content = text(ROOT / "AGENTS.md")
|
|
81
|
+
for term in REQUIRED_AGENTS_TERMS:
|
|
82
|
+
require(term in content, f"AGENTS.md missing skill language contract term: {term}")
|
|
83
|
+
for identifier in MACHINE_IDENTIFIERS:
|
|
84
|
+
require(identifier in content, f"AGENTS.md should preserve machine identifier: {identifier}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_skills() -> None:
|
|
88
|
+
skill_files = sorted((ROOT / ".codex/skills").glob("*/SKILL.md"))
|
|
89
|
+
require(skill_files, "No workflow skill files found under .codex/skills/")
|
|
90
|
+
|
|
91
|
+
for path in skill_files:
|
|
92
|
+
content = text(path)
|
|
93
|
+
for section in SKILL_REQUIRED_SECTIONS:
|
|
94
|
+
require(section in content, f"{path.relative_to(ROOT)} missing Chinese section: {section}")
|
|
95
|
+
for heading in SKILL_FORBIDDEN_HEADINGS:
|
|
96
|
+
require(heading not in content, f"{path.relative_to(ROOT)} still uses English skill heading: {heading}")
|
|
97
|
+
require("name:" in content and "description:" in content, f"{path.relative_to(ROOT)} must keep frontmatter name/description")
|
|
98
|
+
skill_name = path.parent.name
|
|
99
|
+
if skill_name in ARTIFACT_SKILLS_REQUIRE_SEMANTIC_SLICING:
|
|
100
|
+
require("## 语义切片" in content, f"{path.relative_to(ROOT)} missing semantic slicing section: ## 语义切片")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def validate_skill_template() -> None:
|
|
104
|
+
path = ROOT / ".codex/pjsdlc_managed/templates/SKILL_TEMPLATE.md"
|
|
105
|
+
require(path.exists(), "Missing .codex/pjsdlc_managed/templates/SKILL_TEMPLATE.md")
|
|
106
|
+
content = text(path)
|
|
107
|
+
for section in SKILL_REQUIRED_SECTIONS:
|
|
108
|
+
require(section in content, f"SKILL_TEMPLATE.md missing Chinese section: {section}")
|
|
109
|
+
require("## 语义切片" in content, "SKILL_TEMPLATE.md missing semantic slicing section: ## 语义切片")
|
|
110
|
+
require("current_phase" in content and "make validate-current" in content, "SKILL_TEMPLATE.md must demonstrate English machine identifiers")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_yaml_keys() -> None:
|
|
114
|
+
lifecycle = load_yaml(".codex/state/lifecycle.yaml")
|
|
115
|
+
tasks = load_yaml(".codex/state/plan.yaml")
|
|
116
|
+
phase_contracts = load_yaml(".codex/pjsdlc_managed/policies/phase_contracts.yaml")
|
|
117
|
+
|
|
118
|
+
for key in YAML_KEYWORDS["lifecycle"]:
|
|
119
|
+
require(key in lifecycle, f"lifecycle.yaml key was removed or translated: {key}")
|
|
120
|
+
for key in YAML_KEYWORDS["plan"]:
|
|
121
|
+
require(key in tasks, f"plan.yaml key was removed or translated: {key}")
|
|
122
|
+
for key in YAML_KEYWORDS["phase_contracts"]:
|
|
123
|
+
require(key in phase_contracts, f"phase_contracts.yaml key was removed or translated: {key}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> None:
|
|
127
|
+
try:
|
|
128
|
+
validate_agents()
|
|
129
|
+
validate_skills()
|
|
130
|
+
validate_skill_template()
|
|
131
|
+
validate_yaml_keys()
|
|
132
|
+
except HarnessError:
|
|
133
|
+
raise
|
|
134
|
+
print("Skill language contract OK")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
run_main(main)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from harness_utils import (
|
|
3
|
+
combined_text,
|
|
4
|
+
contains_any,
|
|
5
|
+
load_plan,
|
|
6
|
+
markdown_deliverables,
|
|
7
|
+
repo_path,
|
|
8
|
+
require,
|
|
9
|
+
run_main,
|
|
10
|
+
validate_plan_contract,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CURRENT_RELEASE_REPORT = ".docs/08_release/CURRENT_RELEASE.md"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def release_deliverables():
|
|
18
|
+
current = repo_path(CURRENT_RELEASE_REPORT)
|
|
19
|
+
if current.exists():
|
|
20
|
+
return [current], CURRENT_RELEASE_REPORT
|
|
21
|
+
docs = markdown_deliverables(".docs/08_release")
|
|
22
|
+
return docs, "legacy .docs/08_release/*.md"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
validate_plan_contract(load_plan(), allow_open=False)
|
|
27
|
+
docs, source = release_deliverables()
|
|
28
|
+
require(docs, f"Missing current release report: expected {CURRENT_RELEASE_REPORT} or legacy .docs/08_release/*.md")
|
|
29
|
+
text = combined_text(docs)
|
|
30
|
+
require(contains_any(text, ["release", "发布"]), "Current release report must include release notes")
|
|
31
|
+
require(contains_any(text, ["smoke", "冒烟"]), "Current release report must include smoke test evidence")
|
|
32
|
+
require(contains_any(text, ["rollback", "回滚"]), "Current release report must include rollback plan")
|
|
33
|
+
print(f"Current release status OK: {source}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
run_main(main)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from harness_utils import contains_any, load_plan, read_text, require, run_main, validate_plan_contract
|
|
3
|
+
|
|
4
|
+
READINESS_FIELDS = [
|
|
5
|
+
"Runnable Entry",
|
|
6
|
+
"Observable Exit",
|
|
7
|
+
"Initialization",
|
|
8
|
+
"Config Contract",
|
|
9
|
+
"Testing Handoff Readiness",
|
|
10
|
+
]
|
|
11
|
+
RUNTIME_MISMATCH_TERMS = [
|
|
12
|
+
"not deployed",
|
|
13
|
+
"not initialized",
|
|
14
|
+
"local only",
|
|
15
|
+
"localhost only",
|
|
16
|
+
"fake adapter",
|
|
17
|
+
"fake send",
|
|
18
|
+
"未部署",
|
|
19
|
+
"未初始化",
|
|
20
|
+
"只在本地",
|
|
21
|
+
"仅本地",
|
|
22
|
+
"本地跑通",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
validate_plan_contract(load_plan(), allow_open=False)
|
|
28
|
+
text = read_text(".docs/06_review/REVIEW_REPORT.md")
|
|
29
|
+
require(contains_any(text, ["finding", "发现", "风险"]), "Review report must include findings or risks")
|
|
30
|
+
require(contains_any(text, ["test gap", "测试缺口", "coverage"]), "Review report must include test gaps or coverage notes")
|
|
31
|
+
require(
|
|
32
|
+
contains_any(text, ["entry/exit", "entrypoint", "入口", "出口", "runnable", "可运行"]),
|
|
33
|
+
"Review report must assess runnable entry/exit readiness before TESTING",
|
|
34
|
+
)
|
|
35
|
+
lowered = text.lower()
|
|
36
|
+
for field in READINESS_FIELDS:
|
|
37
|
+
field_lower = field.lower()
|
|
38
|
+
has_status = (
|
|
39
|
+
f"{field_lower}: pass" in lowered
|
|
40
|
+
or f"{field_lower}: `pass`" in lowered
|
|
41
|
+
or f"{field_lower}: blocked" in lowered
|
|
42
|
+
or f"{field_lower}: `blocked`" in lowered
|
|
43
|
+
)
|
|
44
|
+
require(has_status, f"Review report must include {field}: PASS/BLOCKED")
|
|
45
|
+
require(
|
|
46
|
+
f"{field_lower}: blocked" not in lowered and f"{field_lower}: `blocked`" not in lowered,
|
|
47
|
+
f"Review readiness is BLOCKED: {field}",
|
|
48
|
+
)
|
|
49
|
+
require(contains_any(text, ["pass", "blocked", "通过", "阻塞"]), "Review report must include PASS/BLOCKED decision")
|
|
50
|
+
if contains_any(lowered, ["decision: pass", "decision: `pass`", "final decision: pass"]):
|
|
51
|
+
require(
|
|
52
|
+
not contains_any(lowered, RUNTIME_MISMATCH_TERMS),
|
|
53
|
+
"Review report cannot PASS while target runtime or handoff evidence is missing or lower-level only",
|
|
54
|
+
)
|
|
55
|
+
print("Review report OK")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
run_main(main)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from harness_utils import (
|
|
6
|
+
combined_text,
|
|
7
|
+
contains_any,
|
|
8
|
+
load_plan,
|
|
9
|
+
markdown_deliverables,
|
|
10
|
+
read_text,
|
|
11
|
+
repo_path,
|
|
12
|
+
require,
|
|
13
|
+
run_main,
|
|
14
|
+
validate_plan_contract,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
TEST_FACT_SOURCE_REF = re.compile(r"\.docs/07_test/[^\s`,)]+")
|
|
19
|
+
SELF_TEST_TRIGGER_TERMS = [
|
|
20
|
+
"entry/exit",
|
|
21
|
+
"runnable entry",
|
|
22
|
+
"runnable exit",
|
|
23
|
+
"runnable entry/exit",
|
|
24
|
+
"runtime",
|
|
25
|
+
"environment",
|
|
26
|
+
"target_runtime_environment",
|
|
27
|
+
"target runtime",
|
|
28
|
+
"required_gates",
|
|
29
|
+
"gate",
|
|
30
|
+
"handoff",
|
|
31
|
+
"blocker",
|
|
32
|
+
"module key test path",
|
|
33
|
+
"test route",
|
|
34
|
+
"test path",
|
|
35
|
+
"debug path",
|
|
36
|
+
"测试路径",
|
|
37
|
+
"测试链路",
|
|
38
|
+
"自测链路",
|
|
39
|
+
"模块关键测试路径",
|
|
40
|
+
"入口",
|
|
41
|
+
"出口",
|
|
42
|
+
"运行环境",
|
|
43
|
+
"阻塞",
|
|
44
|
+
]
|
|
45
|
+
SELF_TEST_IMPACT_TERMS = ["development self-test impact", "开发自测影响"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def superseded_test_docs(docs) -> list[str]:
|
|
49
|
+
paths: set[str] = set()
|
|
50
|
+
for doc in docs:
|
|
51
|
+
for line in doc.read_text(encoding="utf-8").splitlines():
|
|
52
|
+
lowered = line.lower()
|
|
53
|
+
if "superseded" not in lowered and "被替代" not in lowered and "失效" not in lowered:
|
|
54
|
+
continue
|
|
55
|
+
for match in TEST_FACT_SOURCE_REF.findall(line):
|
|
56
|
+
paths.add(match.rstrip(".,;:"))
|
|
57
|
+
return sorted(paths)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
validate_plan_contract(load_plan(), allow_open=False)
|
|
62
|
+
docs = markdown_deliverables(".docs/rfc")
|
|
63
|
+
require(docs, "No RFC documents found in .docs/rfc/")
|
|
64
|
+
text = combined_text(docs)
|
|
65
|
+
require(contains_any(text, ["background", "背景"]), "RFC must include background")
|
|
66
|
+
require(contains_any(text, ["product impact", "产品影响"]), "RFC must include product impact")
|
|
67
|
+
require(contains_any(text, ["technical impact", "技术影响"]), "RFC must include technical impact candidates")
|
|
68
|
+
require(contains_any(text, ["regression", "回归"]), "RFC must include regression requirements")
|
|
69
|
+
require(
|
|
70
|
+
contains_any(text, ["test fact source impact", "测试事实源影响"]),
|
|
71
|
+
"RFC must include Test Fact Source Impact",
|
|
72
|
+
)
|
|
73
|
+
index_text = read_text(".docs/INDEX.md")
|
|
74
|
+
for path in superseded_test_docs(docs):
|
|
75
|
+
require(not repo_path(path).exists(), f"Superseded test doc still exists in current facts: {path}")
|
|
76
|
+
require(path not in index_text, f"Superseded test doc still linked from .docs/INDEX.md: {path}")
|
|
77
|
+
statuses = re.findall(r"Status:\s*([A-Z_]+)", text)
|
|
78
|
+
require(statuses, "RFC must include a Status line")
|
|
79
|
+
allowed = {"DRAFT", "APPLIED", "VERIFIED", "ARCHIVED"}
|
|
80
|
+
invalid = [status for status in statuses if status not in allowed]
|
|
81
|
+
require(not invalid, "Invalid RFC status: " + ", ".join(invalid))
|
|
82
|
+
validate_development_self_test_impact(docs)
|
|
83
|
+
print(f"RFC artifacts OK: {len(docs)} file(s)")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def validate_development_self_test_impact(docs) -> None:
|
|
87
|
+
for doc in docs:
|
|
88
|
+
number = rfc_number(doc.name)
|
|
89
|
+
if number is not None and number < 23:
|
|
90
|
+
continue
|
|
91
|
+
text = doc.read_text(encoding="utf-8")
|
|
92
|
+
if contains_any(text, SELF_TEST_TRIGGER_TERMS):
|
|
93
|
+
require(
|
|
94
|
+
contains_any(text, SELF_TEST_IMPACT_TERMS),
|
|
95
|
+
f"{doc.relative_to(repo_path('.')).as_posix()} must include Development Self-Test Impact when RFC changes entry/exit, runtime, gates, handoff, or blockers",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def rfc_number(file_name: str) -> int | None:
|
|
100
|
+
match = re.match(r"^RFC[_-](\d+)", file_name, re.IGNORECASE)
|
|
101
|
+
return int(match.group(1)) if match else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
run_main(main)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from harness_utils import contains_any, read_text, repo_path, require, run_main
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
RUNNABLE_ENTRY_EXIT_TERMS = [
|
|
6
|
+
"runnable entry/exit",
|
|
7
|
+
"entry/exit",
|
|
8
|
+
"entry points",
|
|
9
|
+
"entry point",
|
|
10
|
+
"可运行入口/出口",
|
|
11
|
+
"入口/出口",
|
|
12
|
+
"not applicable",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
index = read_text(".docs/INDEX.md")
|
|
18
|
+
docs_root = repo_path(".docs/04_implementation")
|
|
19
|
+
docs = sorted(
|
|
20
|
+
path for path in docs_root.rglob("*.md")
|
|
21
|
+
if path.name != "overview.md"
|
|
22
|
+
)
|
|
23
|
+
if not docs:
|
|
24
|
+
print("Implementation docs OK: no implementation docs yet")
|
|
25
|
+
return
|
|
26
|
+
for path in docs:
|
|
27
|
+
relative = path.relative_to(repo_path(".")).as_posix()
|
|
28
|
+
doc = relative if relative.startswith(".") else f".{relative}"
|
|
29
|
+
index_path = doc.removeprefix(".docs/")
|
|
30
|
+
require(doc in index or index_path in index, f".docs/INDEX.md does not link implementation doc: {doc}")
|
|
31
|
+
text = read_text(doc)
|
|
32
|
+
require(
|
|
33
|
+
contains_any(text, RUNNABLE_ENTRY_EXIT_TERMS),
|
|
34
|
+
f"Implementation doc must include Runnable Entry/Exit facts or explicit Not applicable: {doc}",
|
|
35
|
+
)
|
|
36
|
+
print(f"Implementation docs OK: {len(docs)} implementation doc(s)")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
run_main(main)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from harness_utils import (
|
|
3
|
+
changed_files,
|
|
4
|
+
contains_any,
|
|
5
|
+
load_lifecycle,
|
|
6
|
+
load_plan,
|
|
7
|
+
repo_path,
|
|
8
|
+
read_text,
|
|
9
|
+
require,
|
|
10
|
+
run_main,
|
|
11
|
+
testing_boundary_errors_for_changed_files,
|
|
12
|
+
validate_plan_contract,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
TEST_REPORT_PATH = ".docs/07_test/TEST_REPORT.md"
|
|
16
|
+
PLACEHOLDER_TERMS = ["pending", "tbd", "todo", "待填", "待补", "placeholder"]
|
|
17
|
+
MISSING_READINESS_TERMS = [
|
|
18
|
+
"missing entry",
|
|
19
|
+
"missing exit",
|
|
20
|
+
"missing runnable",
|
|
21
|
+
"missing development evidence",
|
|
22
|
+
"no runnable",
|
|
23
|
+
"no entry",
|
|
24
|
+
"no exit",
|
|
25
|
+
"入口缺失",
|
|
26
|
+
"出口缺失",
|
|
27
|
+
"缺少入口",
|
|
28
|
+
"缺少出口",
|
|
29
|
+
"缺少 development evidence",
|
|
30
|
+
"尚未交付",
|
|
31
|
+
"未交付",
|
|
32
|
+
"不存在",
|
|
33
|
+
"not deployed",
|
|
34
|
+
"not initialized",
|
|
35
|
+
"local only",
|
|
36
|
+
"localhost only",
|
|
37
|
+
"fake adapter",
|
|
38
|
+
"fake send",
|
|
39
|
+
"未部署",
|
|
40
|
+
"未初始化",
|
|
41
|
+
"只在本地",
|
|
42
|
+
"仅本地",
|
|
43
|
+
"本地跑通",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_test_report() -> tuple[str, str]:
|
|
48
|
+
require(
|
|
49
|
+
repo_path(TEST_REPORT_PATH).exists(),
|
|
50
|
+
f"Missing test report: expected executed evidence at {TEST_REPORT_PATH}",
|
|
51
|
+
)
|
|
52
|
+
return read_text(TEST_REPORT_PATH), TEST_REPORT_PATH
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main() -> None:
|
|
56
|
+
validate_plan_contract(load_plan(), allow_open=False)
|
|
57
|
+
text, source = read_test_report()
|
|
58
|
+
require(
|
|
59
|
+
not contains_any(text, PLACEHOLDER_TERMS),
|
|
60
|
+
"Test report must contain executed evidence, not pending/TBD/TODO/placeholder content",
|
|
61
|
+
)
|
|
62
|
+
require(contains_any(text, ["matrix", "矩阵"]), "Test report must include a test matrix")
|
|
63
|
+
require(contains_any(text, ["regression", "回归"]), "Test report must include regression evidence")
|
|
64
|
+
require(contains_any(text, ["coverage gap", "覆盖缺口", "gap"]), "Test report must include coverage gaps")
|
|
65
|
+
require(
|
|
66
|
+
contains_any(text, ["entry/exit", "entrypoint", "入口", "出口", "runnable", "可运行"]),
|
|
67
|
+
"Test report must state existing runnable entry/exit coverage or blocker status",
|
|
68
|
+
)
|
|
69
|
+
require(contains_any(text, ["pass", "blocked", "通过", "阻塞"]), "Test report must include PASS/BLOCKED decision")
|
|
70
|
+
if contains_any(text, ["decision\n\npass", "decision: pass", "decision: `pass`", "final decision: pass"]):
|
|
71
|
+
require(
|
|
72
|
+
not contains_any(text, MISSING_READINESS_TERMS),
|
|
73
|
+
"Test report cannot PASS while runnable entry/exit or Development Evidence is missing; use BLOCKED with recovery conditions",
|
|
74
|
+
)
|
|
75
|
+
if load_lifecycle().get("current_phase") == "TESTING":
|
|
76
|
+
for error in testing_boundary_errors_for_changed_files(changed_files()):
|
|
77
|
+
require(False, error)
|
|
78
|
+
print(f"Test report OK: {source}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
run_main(main)
|
package/dist/lib/config.js
CHANGED
|
@@ -15,6 +15,7 @@ export function defaultConfig(root) {
|
|
|
15
15
|
{ path: harnessPath(root, "pjsdlc_managed", "templates"), strategy: "managed" },
|
|
16
16
|
{ path: harnessPath(root, "pjsdlc_managed", "policies"), strategy: "merge-with-local" },
|
|
17
17
|
{ path: harnessPath(root, "pjsdlc_managed", "make", "sdlc-harness.mk"), strategy: "managed" },
|
|
18
|
+
{ path: "tools", strategy: "managed" },
|
|
18
19
|
{ path: ".github/workflows/harness.yml", strategy: "create-if-missing" }
|
|
19
20
|
],
|
|
20
21
|
local_overrides: [
|
package/dist/lib/migrations.js
CHANGED
|
@@ -162,6 +162,9 @@ function migrateManagedFiles(managedFiles, root) {
|
|
|
162
162
|
migrated.unshift(makefileEntry);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
if (!seen.has("tools")) {
|
|
166
|
+
push({ path: "tools", strategy: "managed" });
|
|
167
|
+
}
|
|
165
168
|
if (!seen.has(".github/workflows/harness.yml")) {
|
|
166
169
|
push({ path: ".github/workflows/harness.yml", strategy: "create-if-missing" });
|
|
167
170
|
}
|
package/dist/lib/sync-engine.js
CHANGED
|
@@ -49,6 +49,10 @@ async function syncManagedFile(projectRoot, root, managedFile, report) {
|
|
|
49
49
|
await syncFile(packageAssetPath("make", "sdlc-harness.mk"), destination, report, "skip-if-missing");
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
+
if (managedFile.path === "tools") {
|
|
53
|
+
await syncTree(packageAssetPath("tools"), destination, report);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
52
56
|
if (managedFile.path === ".github/workflows/harness.yml") {
|
|
53
57
|
await syncGithubWorkflow(packageAssetPath("github", "harness.yml"), destination, managedFile.path, report);
|
|
54
58
|
return;
|