@harmonyos-arkts/d2h 0.1.11 → 0.1.13
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/.claude-plugin/marketplace.json +15 -13
- package/.claude-plugin/plugin.json +13 -7
- package/AGENTS.md +5 -5
- package/README.md +212 -212
- package/agents/android-to-hmos-00-orchestrator.md +318 -318
- package/agents/d2h.md +157 -157
- package/dist/bin/install-opencode.js +30 -30
- package/docs/environment-check.md +86 -86
- package/install-opencode.sh +160 -160
- package/opencode/agents.json +16 -16
- package/opencode/vscode-d2h-package.json +7 -7
- package/opencode/vscode-d2h-plugin.mjs +29 -29
- package/package.json +54 -54
- package/schemas/android-source-manifest.schema.json +25 -25
- package/schemas/checkpoint-provenance.schema.json +67 -67
- package/schemas/final-acceptance.schema.json +29 -29
- package/schemas/managed-evidence-index.schema.json +15 -15
- package/schemas/managed-evidence.schema.json +114 -114
- package/schemas/migration-config.schema.json +51 -51
- package/schemas/migration-report-index.schema.json +39 -39
- package/schemas/migration-report-item.schema.json +51 -51
- package/schemas/migration-report-summary.schema.json +25 -25
- package/schemas/migration-status.schema.json +202 -202
- package/schemas/preflight.schema.json +60 -60
- package/schemas/source-order-audit.schema.json +16 -16
- package/schemas/source-provenance-event.schema.json +19 -19
- package/schemas/spec-app-shard.schema.json +45 -45
- package/schemas/spec-fact-corrections-shard.schema.json +13 -13
- package/schemas/spec-features-shard.schema.json +43 -43
- package/schemas/spec-index.schema.json +26 -26
- package/schemas/spec-interactions-shard.schema.json +40 -40
- package/schemas/spec-page.schema.json +98 -98
- package/schemas/spec-pages-index.schema.json +1 -1
- package/schemas/spec-unresolved-shard.schema.json +1 -1
- package/schemas/task-envelope.schema.json +109 -109
- package/schemas/task-plan.schema.json +341 -341
- package/scripts/check-environment.mjs +320 -320
- package/skills/android2hmos_resources_convert/SKILL.md +180 -180
- package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
- package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
- package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
- package/skills/arkts-app-identity/SKILL.md +238 -238
- package/skills/arkts-i18n/SKILL.md +496 -496
- package/skills/arkts-i18n/evals/evals.json +84 -84
- package/skills/arkts-i18n/references/code-examples.md +302 -302
- package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
- package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
- package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
- package/skills/arkts-i18n/references/language-codes.md +104 -104
- package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
- package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
- package/skills/arkts-i18n/references/v1-compat.md +244 -244
- package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
- package/skills/arkts-icon-sizing/SKILL.md +211 -211
- package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
- package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
- package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
- package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
- package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
- package/skills/d2h-hypium-forge/SKILL.md +309 -309
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
- package/skills/d2h-hypium-forge/references/official-api.md +135 -135
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
- package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
- package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
- package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
- package/skills/d2h-page-develop/SKILL.md +112 -112
- package/skills/d2h-page-develop/agents/openai.yaml +7 -7
- package/skills/d2h-page-spec-generate/SKILL.md +206 -206
- package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
- package/skills/d2h-utg-static-generate/SKILL.md +73 -73
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
- package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
- package/skills/maintaining-migration-report/SKILL.md +144 -144
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
- package/skills/managing-d2h-toolchain/SKILL.md +24 -24
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
- package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
- package/skills/managing-d2h-toolchain/toolchain.json +16 -16
- package/skills/native-library-substitution/SKILL.md +384 -384
- package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
- package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
- package/skills/preparing-migration-workspace/SKILL.md +131 -131
- package/skills/reviewing-migration-process/SKILL.md +62 -62
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Create the initial batch_report.md from a validated pages.json plan."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import hashlib
|
|
8
|
-
import json
|
|
9
|
-
import os
|
|
10
|
-
import sys
|
|
11
|
-
import tempfile
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def sha256(path: Path) -> str:
|
|
17
|
-
digest = hashlib.sha256()
|
|
18
|
-
with path.open("rb") as stream:
|
|
19
|
-
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
20
|
-
digest.update(chunk)
|
|
21
|
-
return digest.hexdigest()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def markdown_text(value: object) -> str:
|
|
25
|
-
return str(value).replace("|", "\\|").replace("\n", " ")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def require_int(value: object, where: str) -> int:
|
|
29
|
-
if not isinstance(value, int) or isinstance(value, bool):
|
|
30
|
-
raise ValueError(f"{where} must be an integer")
|
|
31
|
-
return value
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def main() -> int:
|
|
35
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
36
|
-
parser.add_argument("--pages-plan", required=True, type=Path)
|
|
37
|
-
parser.add_argument("--output-root", required=True, type=Path)
|
|
38
|
-
args = parser.parse_args()
|
|
39
|
-
|
|
40
|
-
pages_plan = args.pages_plan.resolve()
|
|
41
|
-
output_root = args.output_root.resolve()
|
|
42
|
-
report_path = output_root / "batch_report.md"
|
|
43
|
-
if not pages_plan.is_file():
|
|
44
|
-
raise FileNotFoundError(f"missing page plan: {pages_plan}")
|
|
45
|
-
if report_path.exists():
|
|
46
|
-
raise FileExistsError(
|
|
47
|
-
f"refusing to overwrite existing execution history: {report_path}; update it in place"
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
payload = json.loads(pages_plan.read_text(encoding="utf-8"))
|
|
51
|
-
if not isinstance(payload, dict) or payload.get("schema_version") != "d2h-page-plan/v1":
|
|
52
|
-
raise ValueError("pages plan must use schema d2h-page-plan/v1")
|
|
53
|
-
pages = payload.get("pages")
|
|
54
|
-
if not isinstance(pages, list) or not pages:
|
|
55
|
-
raise ValueError("pages plan must contain a non-empty pages array")
|
|
56
|
-
|
|
57
|
-
normalized: list[dict[str, object]] = []
|
|
58
|
-
seen_packages: set[str] = set()
|
|
59
|
-
seen_page_ids: set[str] = set()
|
|
60
|
-
for index, page in enumerate(pages):
|
|
61
|
-
if not isinstance(page, dict):
|
|
62
|
-
raise ValueError(f"pages[{index}] must be an object")
|
|
63
|
-
order = require_int(page.get("order"), f"pages[{index}].order")
|
|
64
|
-
if order != index + 1:
|
|
65
|
-
raise ValueError(f"pages[{index}].order must be {index + 1}")
|
|
66
|
-
|
|
67
|
-
package = page.get("package")
|
|
68
|
-
page_id = page.get("page_id")
|
|
69
|
-
if not isinstance(package, str) or not package:
|
|
70
|
-
raise ValueError(f"pages[{index}].package must be a non-empty string")
|
|
71
|
-
if Path(package).name != package or not package.startswith(f"page_{order:04d}_"):
|
|
72
|
-
raise ValueError(
|
|
73
|
-
f"pages[{index}].package must be a single directory named page_{order:04d}_<name>-<id>"
|
|
74
|
-
)
|
|
75
|
-
if package in seen_packages:
|
|
76
|
-
raise ValueError(f"duplicate package: {package}")
|
|
77
|
-
seen_packages.add(package)
|
|
78
|
-
if not isinstance(page_id, str) or not page_id:
|
|
79
|
-
raise ValueError(f"pages[{index}].page_id must be a non-empty string")
|
|
80
|
-
if page_id in seen_page_ids:
|
|
81
|
-
raise ValueError(f"duplicate page_id: {page_id}")
|
|
82
|
-
seen_page_ids.add(page_id)
|
|
83
|
-
|
|
84
|
-
spec_path = output_root / package / "spec.md"
|
|
85
|
-
if not spec_path.is_file():
|
|
86
|
-
raise FileNotFoundError(
|
|
87
|
-
f"all page specs must exist before batch report generation; missing {spec_path}"
|
|
88
|
-
)
|
|
89
|
-
package_files = sorted(path.name for path in spec_path.parent.iterdir() if path.is_file())
|
|
90
|
-
if package_files != ["spec.md"]:
|
|
91
|
-
raise ValueError(
|
|
92
|
-
f"page package must contain only spec.md: {spec_path.parent} contains {package_files}"
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
ledger = page.get("decision_ledger")
|
|
96
|
-
if not isinstance(ledger, dict):
|
|
97
|
-
raise ValueError(f"pages[{index}].decision_ledger must be an object")
|
|
98
|
-
dimensions: dict[str, int] = {}
|
|
99
|
-
for dimension in ("enablement", "ordinary_reachability", "shallowness"):
|
|
100
|
-
item = ledger.get(dimension)
|
|
101
|
-
if not isinstance(item, dict):
|
|
102
|
-
raise ValueError(f"pages[{index}].decision_ledger.{dimension} must be an object")
|
|
103
|
-
dimensions[dimension] = require_int(
|
|
104
|
-
item.get("rating"), f"pages[{index}].decision_ledger.{dimension}.rating"
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
normalized.append(
|
|
108
|
-
{
|
|
109
|
-
"order": order,
|
|
110
|
-
"task_id": f"PAGE-{order:04d}",
|
|
111
|
-
"page_id": page_id,
|
|
112
|
-
"kind": page.get("kind", "page"),
|
|
113
|
-
"page_score": require_int(page.get("page_score"), f"pages[{index}].page_score"),
|
|
114
|
-
"package": package,
|
|
115
|
-
"page_path": page.get("page_path", []),
|
|
116
|
-
**dimensions,
|
|
117
|
-
}
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
created_at = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
121
|
-
plan_link = Path(os.path.relpath(pages_plan, output_root)).as_posix()
|
|
122
|
-
lines = [
|
|
123
|
-
"# 页面开发批次执行记录",
|
|
124
|
-
"",
|
|
125
|
-
f"- 页面计划:[pages.json]({plan_link})",
|
|
126
|
-
f"- 页面计划 SHA-256:`{sha256(pages_plan)}`",
|
|
127
|
-
f"- 记录创建时间:`{created_at}`",
|
|
128
|
-
"- 更新规则:后续开发直接更新本文件;不要重新生成或覆盖已有执行历史。",
|
|
129
|
-
"",
|
|
130
|
-
"## 状态约定",
|
|
131
|
-
"",
|
|
132
|
-
"- 开发状态:`pending | in_progress | implemented | blocked`",
|
|
133
|
-
"- 构建状态:`not_run | passed | failed | blocked`",
|
|
134
|
-
"- 验证状态:`not_run | passed | failed | blocked`",
|
|
135
|
-
"- 完成条件:开发为 `implemented`,且构建、验证均为 `passed`。",
|
|
136
|
-
"",
|
|
137
|
-
"## 批次汇总",
|
|
138
|
-
"",
|
|
139
|
-
f"- 总任务:{len(normalized)}",
|
|
140
|
-
f"- 待开始:{len(normalized)}",
|
|
141
|
-
"- 进行中:0",
|
|
142
|
-
"- 已完成:0",
|
|
143
|
-
"- 阻塞:0",
|
|
144
|
-
"",
|
|
145
|
-
"## 执行队列",
|
|
146
|
-
"",
|
|
147
|
-
"| 任务 | 顺序 | 页面 | 类型 | 分数 | Spec | 开发 | 构建 | 验证 |",
|
|
148
|
-
"|---|---:|---|---|---:|---|---|---|---|",
|
|
149
|
-
]
|
|
150
|
-
for page in normalized:
|
|
151
|
-
spec_link = f"./{page['package']}/spec.md"
|
|
152
|
-
lines.append(
|
|
153
|
-
"| {task} | {order} | {page_id} | {kind} | {score} | "
|
|
154
|
-
"[spec.md]({spec}) | `pending` | `not_run` | `not_run` |".format(
|
|
155
|
-
task=page["task_id"],
|
|
156
|
-
order=page["order"],
|
|
157
|
-
page_id=markdown_text(page["page_id"]),
|
|
158
|
-
kind=markdown_text(page["kind"]),
|
|
159
|
-
score=page["page_score"],
|
|
160
|
-
spec=spec_link,
|
|
161
|
-
)
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
lines.extend(["", "## 任务记录"])
|
|
165
|
-
for page in normalized:
|
|
166
|
-
page_path = page["page_path"]
|
|
167
|
-
path_text = (
|
|
168
|
-
" → ".join(markdown_text(value) for value in page_path)
|
|
169
|
-
if isinstance(page_path, list)
|
|
170
|
-
else "—"
|
|
171
|
-
)
|
|
172
|
-
spec_link = f"./{page['package']}/spec.md"
|
|
173
|
-
lines.extend(
|
|
174
|
-
[
|
|
175
|
-
"",
|
|
176
|
-
f"### {page['task_id']} · {markdown_text(page['page_id'])}",
|
|
177
|
-
"",
|
|
178
|
-
f"- 执行顺序:{page['order']}",
|
|
179
|
-
f"- 页面路径:{path_text or '—'}",
|
|
180
|
-
f"- 页面分数:{page['page_score']}(enablement={page['enablement']},ordinary_reachability={page['ordinary_reachability']},shallowness={page['shallowness']})",
|
|
181
|
-
f"- 任务 Spec:[spec.md]({spec_link})",
|
|
182
|
-
"- 开发状态:`pending`",
|
|
183
|
-
"- 构建状态:`not_run`",
|
|
184
|
-
"- 验证状态:`not_run`",
|
|
185
|
-
"- 实现文件:—",
|
|
186
|
-
"- 测试文件:—",
|
|
187
|
-
"- 代码提交 / 源码快照:—",
|
|
188
|
-
"- 构建证据:—",
|
|
189
|
-
"- 测试证据:—",
|
|
190
|
-
"- 最后更新:—",
|
|
191
|
-
"- 阻塞原因:—",
|
|
192
|
-
"",
|
|
193
|
-
"#### 执行记录",
|
|
194
|
-
"",
|
|
195
|
-
"- 尚未开始。",
|
|
196
|
-
]
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
output_root.mkdir(parents=True, exist_ok=True)
|
|
200
|
-
report_text = "\n".join(lines) + "\n"
|
|
201
|
-
with tempfile.NamedTemporaryFile(
|
|
202
|
-
mode="w", encoding="utf-8", dir=output_root, prefix=".batch-report-", delete=False
|
|
203
|
-
) as stream:
|
|
204
|
-
stream.write(report_text)
|
|
205
|
-
temporary_path = Path(stream.name)
|
|
206
|
-
os.replace(temporary_path, report_path)
|
|
207
|
-
print(
|
|
208
|
-
json.dumps(
|
|
209
|
-
{
|
|
210
|
-
"ok": True,
|
|
211
|
-
"task_count": len(normalized),
|
|
212
|
-
"pages_plan": str(pages_plan),
|
|
213
|
-
"batch_report": str(report_path),
|
|
214
|
-
},
|
|
215
|
-
ensure_ascii=False,
|
|
216
|
-
indent=2,
|
|
217
|
-
)
|
|
218
|
-
)
|
|
219
|
-
return 0
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if __name__ == "__main__":
|
|
223
|
-
try:
|
|
224
|
-
raise SystemExit(main())
|
|
225
|
-
except (FileNotFoundError, FileExistsError, ValueError, KeyError, json.JSONDecodeError) as error:
|
|
226
|
-
print(f"error: {error}", file=sys.stderr)
|
|
227
|
-
raise SystemExit(2)
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create the initial batch_report.md from a validated pages.json plan."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sha256(path: Path) -> str:
|
|
17
|
+
digest = hashlib.sha256()
|
|
18
|
+
with path.open("rb") as stream:
|
|
19
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
20
|
+
digest.update(chunk)
|
|
21
|
+
return digest.hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def markdown_text(value: object) -> str:
|
|
25
|
+
return str(value).replace("|", "\\|").replace("\n", " ")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def require_int(value: object, where: str) -> int:
|
|
29
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
30
|
+
raise ValueError(f"{where} must be an integer")
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
36
|
+
parser.add_argument("--pages-plan", required=True, type=Path)
|
|
37
|
+
parser.add_argument("--output-root", required=True, type=Path)
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
pages_plan = args.pages_plan.resolve()
|
|
41
|
+
output_root = args.output_root.resolve()
|
|
42
|
+
report_path = output_root / "batch_report.md"
|
|
43
|
+
if not pages_plan.is_file():
|
|
44
|
+
raise FileNotFoundError(f"missing page plan: {pages_plan}")
|
|
45
|
+
if report_path.exists():
|
|
46
|
+
raise FileExistsError(
|
|
47
|
+
f"refusing to overwrite existing execution history: {report_path}; update it in place"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
payload = json.loads(pages_plan.read_text(encoding="utf-8"))
|
|
51
|
+
if not isinstance(payload, dict) or payload.get("schema_version") != "d2h-page-plan/v1":
|
|
52
|
+
raise ValueError("pages plan must use schema d2h-page-plan/v1")
|
|
53
|
+
pages = payload.get("pages")
|
|
54
|
+
if not isinstance(pages, list) or not pages:
|
|
55
|
+
raise ValueError("pages plan must contain a non-empty pages array")
|
|
56
|
+
|
|
57
|
+
normalized: list[dict[str, object]] = []
|
|
58
|
+
seen_packages: set[str] = set()
|
|
59
|
+
seen_page_ids: set[str] = set()
|
|
60
|
+
for index, page in enumerate(pages):
|
|
61
|
+
if not isinstance(page, dict):
|
|
62
|
+
raise ValueError(f"pages[{index}] must be an object")
|
|
63
|
+
order = require_int(page.get("order"), f"pages[{index}].order")
|
|
64
|
+
if order != index + 1:
|
|
65
|
+
raise ValueError(f"pages[{index}].order must be {index + 1}")
|
|
66
|
+
|
|
67
|
+
package = page.get("package")
|
|
68
|
+
page_id = page.get("page_id")
|
|
69
|
+
if not isinstance(package, str) or not package:
|
|
70
|
+
raise ValueError(f"pages[{index}].package must be a non-empty string")
|
|
71
|
+
if Path(package).name != package or not package.startswith(f"page_{order:04d}_"):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"pages[{index}].package must be a single directory named page_{order:04d}_<name>-<id>"
|
|
74
|
+
)
|
|
75
|
+
if package in seen_packages:
|
|
76
|
+
raise ValueError(f"duplicate package: {package}")
|
|
77
|
+
seen_packages.add(package)
|
|
78
|
+
if not isinstance(page_id, str) or not page_id:
|
|
79
|
+
raise ValueError(f"pages[{index}].page_id must be a non-empty string")
|
|
80
|
+
if page_id in seen_page_ids:
|
|
81
|
+
raise ValueError(f"duplicate page_id: {page_id}")
|
|
82
|
+
seen_page_ids.add(page_id)
|
|
83
|
+
|
|
84
|
+
spec_path = output_root / package / "spec.md"
|
|
85
|
+
if not spec_path.is_file():
|
|
86
|
+
raise FileNotFoundError(
|
|
87
|
+
f"all page specs must exist before batch report generation; missing {spec_path}"
|
|
88
|
+
)
|
|
89
|
+
package_files = sorted(path.name for path in spec_path.parent.iterdir() if path.is_file())
|
|
90
|
+
if package_files != ["spec.md"]:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"page package must contain only spec.md: {spec_path.parent} contains {package_files}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
ledger = page.get("decision_ledger")
|
|
96
|
+
if not isinstance(ledger, dict):
|
|
97
|
+
raise ValueError(f"pages[{index}].decision_ledger must be an object")
|
|
98
|
+
dimensions: dict[str, int] = {}
|
|
99
|
+
for dimension in ("enablement", "ordinary_reachability", "shallowness"):
|
|
100
|
+
item = ledger.get(dimension)
|
|
101
|
+
if not isinstance(item, dict):
|
|
102
|
+
raise ValueError(f"pages[{index}].decision_ledger.{dimension} must be an object")
|
|
103
|
+
dimensions[dimension] = require_int(
|
|
104
|
+
item.get("rating"), f"pages[{index}].decision_ledger.{dimension}.rating"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
normalized.append(
|
|
108
|
+
{
|
|
109
|
+
"order": order,
|
|
110
|
+
"task_id": f"PAGE-{order:04d}",
|
|
111
|
+
"page_id": page_id,
|
|
112
|
+
"kind": page.get("kind", "page"),
|
|
113
|
+
"page_score": require_int(page.get("page_score"), f"pages[{index}].page_score"),
|
|
114
|
+
"package": package,
|
|
115
|
+
"page_path": page.get("page_path", []),
|
|
116
|
+
**dimensions,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
created_at = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
121
|
+
plan_link = Path(os.path.relpath(pages_plan, output_root)).as_posix()
|
|
122
|
+
lines = [
|
|
123
|
+
"# 页面开发批次执行记录",
|
|
124
|
+
"",
|
|
125
|
+
f"- 页面计划:[pages.json]({plan_link})",
|
|
126
|
+
f"- 页面计划 SHA-256:`{sha256(pages_plan)}`",
|
|
127
|
+
f"- 记录创建时间:`{created_at}`",
|
|
128
|
+
"- 更新规则:后续开发直接更新本文件;不要重新生成或覆盖已有执行历史。",
|
|
129
|
+
"",
|
|
130
|
+
"## 状态约定",
|
|
131
|
+
"",
|
|
132
|
+
"- 开发状态:`pending | in_progress | implemented | blocked`",
|
|
133
|
+
"- 构建状态:`not_run | passed | failed | blocked`",
|
|
134
|
+
"- 验证状态:`not_run | passed | failed | blocked`",
|
|
135
|
+
"- 完成条件:开发为 `implemented`,且构建、验证均为 `passed`。",
|
|
136
|
+
"",
|
|
137
|
+
"## 批次汇总",
|
|
138
|
+
"",
|
|
139
|
+
f"- 总任务:{len(normalized)}",
|
|
140
|
+
f"- 待开始:{len(normalized)}",
|
|
141
|
+
"- 进行中:0",
|
|
142
|
+
"- 已完成:0",
|
|
143
|
+
"- 阻塞:0",
|
|
144
|
+
"",
|
|
145
|
+
"## 执行队列",
|
|
146
|
+
"",
|
|
147
|
+
"| 任务 | 顺序 | 页面 | 类型 | 分数 | Spec | 开发 | 构建 | 验证 |",
|
|
148
|
+
"|---|---:|---|---|---:|---|---|---|---|",
|
|
149
|
+
]
|
|
150
|
+
for page in normalized:
|
|
151
|
+
spec_link = f"./{page['package']}/spec.md"
|
|
152
|
+
lines.append(
|
|
153
|
+
"| {task} | {order} | {page_id} | {kind} | {score} | "
|
|
154
|
+
"[spec.md]({spec}) | `pending` | `not_run` | `not_run` |".format(
|
|
155
|
+
task=page["task_id"],
|
|
156
|
+
order=page["order"],
|
|
157
|
+
page_id=markdown_text(page["page_id"]),
|
|
158
|
+
kind=markdown_text(page["kind"]),
|
|
159
|
+
score=page["page_score"],
|
|
160
|
+
spec=spec_link,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
lines.extend(["", "## 任务记录"])
|
|
165
|
+
for page in normalized:
|
|
166
|
+
page_path = page["page_path"]
|
|
167
|
+
path_text = (
|
|
168
|
+
" → ".join(markdown_text(value) for value in page_path)
|
|
169
|
+
if isinstance(page_path, list)
|
|
170
|
+
else "—"
|
|
171
|
+
)
|
|
172
|
+
spec_link = f"./{page['package']}/spec.md"
|
|
173
|
+
lines.extend(
|
|
174
|
+
[
|
|
175
|
+
"",
|
|
176
|
+
f"### {page['task_id']} · {markdown_text(page['page_id'])}",
|
|
177
|
+
"",
|
|
178
|
+
f"- 执行顺序:{page['order']}",
|
|
179
|
+
f"- 页面路径:{path_text or '—'}",
|
|
180
|
+
f"- 页面分数:{page['page_score']}(enablement={page['enablement']},ordinary_reachability={page['ordinary_reachability']},shallowness={page['shallowness']})",
|
|
181
|
+
f"- 任务 Spec:[spec.md]({spec_link})",
|
|
182
|
+
"- 开发状态:`pending`",
|
|
183
|
+
"- 构建状态:`not_run`",
|
|
184
|
+
"- 验证状态:`not_run`",
|
|
185
|
+
"- 实现文件:—",
|
|
186
|
+
"- 测试文件:—",
|
|
187
|
+
"- 代码提交 / 源码快照:—",
|
|
188
|
+
"- 构建证据:—",
|
|
189
|
+
"- 测试证据:—",
|
|
190
|
+
"- 最后更新:—",
|
|
191
|
+
"- 阻塞原因:—",
|
|
192
|
+
"",
|
|
193
|
+
"#### 执行记录",
|
|
194
|
+
"",
|
|
195
|
+
"- 尚未开始。",
|
|
196
|
+
]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
report_text = "\n".join(lines) + "\n"
|
|
201
|
+
with tempfile.NamedTemporaryFile(
|
|
202
|
+
mode="w", encoding="utf-8", dir=output_root, prefix=".batch-report-", delete=False
|
|
203
|
+
) as stream:
|
|
204
|
+
stream.write(report_text)
|
|
205
|
+
temporary_path = Path(stream.name)
|
|
206
|
+
os.replace(temporary_path, report_path)
|
|
207
|
+
print(
|
|
208
|
+
json.dumps(
|
|
209
|
+
{
|
|
210
|
+
"ok": True,
|
|
211
|
+
"task_count": len(normalized),
|
|
212
|
+
"pages_plan": str(pages_plan),
|
|
213
|
+
"batch_report": str(report_path),
|
|
214
|
+
},
|
|
215
|
+
ensure_ascii=False,
|
|
216
|
+
indent=2,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
try:
|
|
224
|
+
raise SystemExit(main())
|
|
225
|
+
except (FileNotFoundError, FileExistsError, ValueError, KeyError, json.JSONDecodeError) as error:
|
|
226
|
+
print(f"error: {error}", file=sys.stderr)
|
|
227
|
+
raise SystemExit(2)
|