@harmonyos-arkts/d2h 0.1.27 → 0.1.29

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 (43) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/AGENTS.md +2 -0
  3. package/README.md +43 -3
  4. package/agents/d2h.md +16 -14
  5. package/dist/bin/install-opencode.js +28 -23
  6. package/dist/src/install-codex.js +5 -126
  7. package/dist/src/install-skills.js +147 -0
  8. package/dist/src/install-vscode.js +2 -2
  9. package/dist/src/select-agent.js +207 -0
  10. package/docs/environment-check.md +3 -3
  11. package/package.json +6 -6
  12. package/skills/arkts-mvvm-architecture/SKILL.md +2 -0
  13. package/skills/d2h-architecture-plan/SKILL.md +19 -26
  14. package/skills/d2h-architecture-plan/references/architecture-plan.example.json +80 -315
  15. package/skills/d2h-architecture-plan/references/directory-creation.md +27 -34
  16. package/skills/d2h-architecture-plan/references/hybrid-design.md +13 -15
  17. package/skills/d2h-architecture-plan/references/json-contract.md +29 -91
  18. package/skills/d2h-architecture-plan/references/layout-reference.md +30 -27
  19. package/skills/d2h-architecture-plan/references/plan-contract.md +27 -34
  20. package/skills/d2h-architecture-plan/references/source-review.md +1 -1
  21. package/skills/d2h-architecture-plan/scripts/architecture_plan.py +29 -163
  22. package/skills/d2h-architecture-plan/scripts/test_architecture_plan.py +205 -342
  23. package/skills/d2h-spec-develop/SKILL.md +5 -4
  24. package/{install-opencode.sh → tooling/install-opencode.sh} +3 -3
  25. package/{opencode → tooling/opencode}/interactive-tools/advance-progress-demo.mjs +1 -1
  26. package/{opencode → tooling/opencode}/interactive-tools/demo.mjs +3 -3
  27. package/{opencode → tooling/opencode}/interactive-tools/prepare-demo.mjs +1 -1
  28. package/{scripts → tooling/scripts}/check-environment.mjs +3 -3
  29. package/skills/d2h-architecture-plan/references/selection-flow.md +0 -71
  30. /package/{opencode → tooling/opencode}/agents.json +0 -0
  31. /package/{opencode → tooling/opencode}/interactive-tools/browser-host.ts +0 -0
  32. /package/{opencode → tooling/opencode}/interactive-tools/core.ts +0 -0
  33. /package/{opencode → tooling/opencode}/interactive-tools/demo-workflow.ts +0 -0
  34. /package/{opencode → tooling/opencode}/interactive-tools/package-lock.json +0 -0
  35. /package/{opencode → tooling/opencode}/interactive-tools/package.json +0 -0
  36. /package/{opencode → tooling/opencode}/interactive-tools/panel.ts +0 -0
  37. /package/{opencode → tooling/opencode}/interactive-tools/prepare-progress-demo.mjs +0 -0
  38. /package/{opencode → tooling/opencode}/interactive-tools/progress.ts +0 -0
  39. /package/{opencode → tooling/opencode}/interactive-tools/server-legacy.mjs +0 -0
  40. /package/{opencode → tooling/opencode}/interactive-tools/server.mjs +0 -0
  41. /package/{opencode → tooling/opencode}/interactive-tools/tui.tsx +0 -0
  42. /package/{opencode → tooling/opencode}/vscode-d2h-package.json +0 -0
  43. /package/{opencode → tooling/opencode}/vscode-d2h-plugin.mjs +0 -0
@@ -1,31 +1,20 @@
1
1
  #!/usr/bin/env python3
2
- """Validate dashboard architecture plans and record a user-selected directory scheme.
2
+ """Read-only validation of the confirmed architecture shown on the dashboard.
3
3
 
4
- Standard library only. This helper never creates a HarmonyOS project or directory shell.
5
- The caller must serialize writes and establish that a request came from the user.
4
+ Standard library only. User answers and authorization stay in architecture-plan.md;
5
+ valid JSON is neither proof of confirmation nor permission to create a project.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
10
  import argparse
11
- import copy
12
11
  import json
13
- import os
14
12
  from pathlib import Path, PurePosixPath
15
13
  import re
16
14
  import sys
17
- import tempfile
18
15
 
19
16
 
20
- PROPOSAL_IDS = {"android-mirror", "harmony-practice", "hybrid"}
21
- PLAN_FIELDS = {
22
- "planId", "project", "androidAnalysis", "proposals",
23
- "recommendedProposalId", "recommendationReason", "selectedProposalId",
24
- }
25
- DECISION_FIELDS = {
26
- "requestId", "planId", "selectedProposalId",
27
- "action", "source", "notes",
28
- }
17
+ PLAN_FIELDS = {"projectName", "directoryTree", "decisions"}
29
18
 
30
19
 
31
20
  class ContractError(ValueError):
@@ -46,9 +35,8 @@ def record(value: object, fields: set[str], label: str, *, optional: set[str] |
46
35
  return value
47
36
 
48
37
 
49
- def string(value: object, label: str, *, empty: bool = False) -> str:
50
- require(isinstance(value, str) and (empty or bool(value.strip())),
51
- f"{label} must be {'a' if empty else 'a non-empty'} string")
38
+ def string(value: object, label: str) -> str:
39
+ require(isinstance(value, str) and bool(value.strip()), f"{label} must be a non-empty string")
52
40
  return value
53
41
 
54
42
 
@@ -61,21 +49,21 @@ def array(value: object, label: str, *, nonempty: bool = False) -> list:
61
49
  def relative_path(value: object, label: str) -> str:
62
50
  value = string(value, label)
63
51
  require(value == "." or all(
64
- part not in ("", ".", "..") and
52
+ part not in ("", ".", "..", "…") and
65
53
  not re.search(r'[<>:"\\|?*{}\x00-\x1f]', part) and
66
54
  not part.endswith((".", " ")) and
67
- not re.fullmatch(r"(?i:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?", part)
55
+ not re.fullmatch(r"(?i:CON|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³])(?:\..*)?", part)
68
56
  for part in value.split("/")
69
57
  ), f"{label} must be a normalized, safe root-relative directory path")
70
58
  return value
71
59
 
72
60
 
73
- def directory_tree(value: object, label: str) -> set[str]:
61
+ def directory_tree(value: object, label: str = "directoryTree") -> set[str]:
74
62
  paths: set[str] = set()
75
63
  folded: set[str] = set()
76
64
  hap_paths: list[str] = []
77
65
 
78
- def visit(node: object, parent: str | None, module_parent: bool) -> None:
66
+ def visit(node: object, parent: str | None, module_roots: tuple[str, ...]) -> None:
79
67
  node = record(node, {"name", "path", "description", "children"}, label, optional={"kind"})
80
68
  name = string(node["name"], f"{label}.name")
81
69
  path = relative_path(node["path"], f"{label}.path")
@@ -89,120 +77,36 @@ def directory_tree(value: object, label: str) -> set[str]:
89
77
  str(PurePosixPath(path).parent) == parent,
90
78
  f"{label}[{path}] must be a direct child of {parent}, named by its last segment")
91
79
  require(path.casefold() not in folded, f"{label}: duplicate/case-conflicting path {path}")
92
- require(not module_parent or kind == "directory", f"{label}[{path}]: nested module")
80
+ require(kind == "directory" or all(
81
+ path != f"{root}/src" and not path.startswith(f"{root}/src/") for root in module_roots
82
+ ), f"{label}[{path}]: module inside another module's source tree")
93
83
  if kind == "HAP":
94
84
  hap_paths.append(path)
95
85
  paths.add(path)
96
86
  folded.add(path.casefold())
87
+ child_roots = module_roots + (path,) if kind != "directory" else module_roots
97
88
  for child in array(node["children"], f"{label}[{path}].children"):
98
- visit(child, path, module_parent or kind != "directory")
89
+ visit(child, path, child_roots)
99
90
 
100
- visit(value, None, False)
91
+ visit(value, None, ())
101
92
  require(bool(hap_paths), f"{label} must include an application HAP")
102
93
  for path in hap_paths:
103
- require(f"{path}/src/main/ets" in paths, f"{label}[{path}] must include its src/main/ets source root")
94
+ production_root = f"{path}/src/main"
95
+ require(production_root in paths and any(p.startswith(f"{production_root}/") for p in paths),
96
+ f"{label}[{path}] must include an expanded src/main production root with key directories")
104
97
  return paths
105
98
 
106
99
 
107
- def reference(value: object, allowed: set[str], label: str) -> str:
108
- value = string(value, label)
109
- require(value in allowed, f"{label}: unknown reference {value}")
110
- return value
111
-
112
-
113
- def validate_decision(value: object) -> dict:
114
- decision = record(value, DECISION_FIELDS, "selection")
115
- for key in ("requestId", "planId"):
116
- string(decision[key], f"selection.{key}")
117
- reference(decision["selectedProposalId"], PROPOSAL_IDS, "selection.selectedProposalId")
118
- require(decision["action"] in ("select", "select-and-create"), "selection.action invalid")
119
- require(decision["source"] in ("dashboard", "question", "conversation"), "selection.source invalid")
120
- string(decision["notes"], "selection.notes", empty=True)
121
- return decision
122
-
123
-
124
100
  def validate_plan(value: object) -> dict:
125
101
  plan = record(value, PLAN_FIELDS, "plan")
126
- string(plan["planId"], "planId")
127
- project = record(plan["project"], {"name", "summary"}, "project")
128
- for key in ("name", "summary"):
129
- string(project[key], f"project.{key}")
130
-
131
- analysis = record(plan["androidAnalysis"], {"summary", "techStack", "risks"}, "androidAnalysis")
132
- string(analysis["summary"], "androidAnalysis.summary")
133
- stack = record(analysis["techStack"], {"language", "ui", "storage", "network"}, "androidAnalysis.techStack")
134
- for key in ("language", "ui", "storage", "network"):
135
- string(stack[key], f"androidAnalysis.techStack.{key}")
136
- risks = array(analysis["risks"], "androidAnalysis.risks")
137
- require(len(risks) <= 5, "androidAnalysis.risks must contain at most five key risks")
138
- for risk in risks:
139
- risk = record(risk, {"title", "impact"}, "androidAnalysis.risks[]")
140
- string(risk["title"], "androidAnalysis.risks[].title")
141
- string(risk["impact"], "androidAnalysis.risks[].impact")
142
-
143
- proposals = array(plan["proposals"], "proposals", nonempty=True)
144
- require(len(proposals) == 3, "proposals must contain exactly three schemes")
145
- proposal_ids: set[str] = set()
146
- for proposal in proposals:
147
- proposal = record(proposal, {"id", "name", "summary", "directoryTree"}, "proposal")
148
- proposal_id = reference(proposal["id"], PROPOSAL_IDS, "proposal.id")
149
- require(proposal_id not in proposal_ids, f"duplicate proposal ID: {proposal_id}")
150
- proposal_ids.add(proposal_id)
151
- for key in ("name", "summary"):
152
- string(proposal[key], f"{proposal_id}.{key}")
153
- directory_tree(proposal["directoryTree"], f"{proposal_id}.directoryTree")
154
-
155
- reference(plan["recommendedProposalId"], proposal_ids, "recommendedProposalId")
156
- string(plan["recommendationReason"], "recommendationReason")
157
- if plan["selectedProposalId"] is not None:
158
- reference(plan["selectedProposalId"], proposal_ids, "selectedProposalId")
159
- return plan
102
+ string(plan["projectName"], "projectName")
103
+ directory_tree(plan["directoryTree"])
104
+ for decision in array(plan["decisions"], "decisions", nonempty=True):
105
+ decision = record(decision, {"title", "description"}, "decisions[]")
106
+ for key in ("title", "description"):
107
+ string(decision[key], f"decisions[].{key}")
160
108
 
161
-
162
- def apply_selection(plan: dict, decision: dict, saved: dict | None = None) -> tuple[dict, dict]:
163
- """Return a validated new plan and continuation instruction; no filesystem effects."""
164
- validate_plan(plan)
165
- validate_decision(decision)
166
- require(decision["planId"] == plan["planId"], "planId mismatch; refresh the displayed plan")
167
- if saved is not None:
168
- validate_decision(saved)
169
- if saved is not None and decision["requestId"] == saved["requestId"]:
170
- require(decision == saved, "requestId already used with different content")
171
- if decision == saved and plan["selectedProposalId"] == decision["selectedProposalId"]:
172
- status = "already-applied"
173
- updated = plan
174
- else:
175
- updated = copy.deepcopy(plan)
176
- updated["selectedProposalId"] = decision["selectedProposalId"]
177
- validate_plan(updated)
178
- status = "applied"
179
- return updated, {
180
- "status": status,
181
- "planId": updated["planId"],
182
- "selectedProposalId": updated["selectedProposalId"],
183
- "nextAction": "create-directory-shell" if decision["action"] == "select-and-create" else "selection-recorded",
184
- }
185
-
186
-
187
- def read_selection(plan: dict, decision: dict) -> tuple[dict, dict]:
188
- """Resolve only a persisted confirmation matching the plan's selected ID."""
189
- validate_plan(plan)
190
- validate_decision(decision)
191
- require(plan["selectedProposalId"] is not None, "no confirmed proposal selected")
192
- require(decision["planId"] == plan["planId"] and
193
- decision["selectedProposalId"] == plan["selectedProposalId"],
194
- "confirmation does not match the plan/selected proposal")
195
- proposal = next(p for p in plan["proposals"] if p["id"] == plan["selectedProposalId"])
196
- return proposal, {
197
- "status": "confirmed",
198
- "planId": plan["planId"],
199
- "selectedProposalId": proposal["id"],
200
- "requestId": decision["requestId"],
201
- "source": decision["source"],
202
- "action": decision["action"],
203
- "notes": decision["notes"],
204
- "nextAction": "create-directory-shell" if decision["action"] == "select-and-create" else "selection-recorded",
205
- }
109
+ return plan
206
110
 
207
111
 
208
112
  def unique_keys(pairs: list[tuple[str, object]]) -> dict:
@@ -222,57 +126,19 @@ def load_json(path: Path) -> object:
222
126
  parse_constant=reject_constant)
223
127
 
224
128
 
225
- def save_json(path: Path, value: dict) -> None:
226
- temporary = None
227
- try:
228
- with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent,
229
- prefix=f".{path.name}.", suffix=".tmp", delete=False) as stream:
230
- temporary = Path(stream.name)
231
- json.dump(value, stream, ensure_ascii=False, indent=2)
232
- stream.write("\n")
233
- os.replace(temporary, path)
234
- finally:
235
- if temporary is not None and temporary.exists():
236
- temporary.unlink()
237
-
238
-
239
129
  def main() -> int:
240
130
  if hasattr(sys.stdout, "reconfigure"):
241
131
  sys.stdout.reconfigure(encoding="utf-8")
242
132
  parser = argparse.ArgumentParser(description=__doc__)
243
133
  subparsers = parser.add_subparsers(dest="command", required=True)
244
- check = subparsers.add_parser("validate", help="read-only plan validation")
134
+ check = subparsers.add_parser("validate", help="read-only final-plan validation")
245
135
  check.add_argument("plan", type=Path)
246
- apply = subparsers.add_parser("apply-selection", help="record a real user request; never creates a project")
247
- apply.add_argument("plan", type=Path)
248
- apply.add_argument("decision", type=Path)
249
- apply.add_argument("--recover", action="store_true", help="reuse the script-owned confirmation after an interrupted apply")
250
- read = subparsers.add_parser("read-selection", help="reload the saved confirmation before directory implementation")
251
- read.add_argument("plan", type=Path)
252
136
  args = parser.parse_args()
253
137
  try:
254
138
  plan = validate_plan(load_json(args.plan))
255
- if args.command == "validate":
256
- result = {"status": "valid", "planId": plan["planId"]}
257
- elif args.command == "apply-selection":
258
- confirmation_file = args.plan.parent / "architecture-selection.json"
259
- same_file = args.decision.resolve() == confirmation_file.resolve()
260
- require(same_file == args.recover,
261
- "new submissions need a separate input file; use --recover only with architecture-selection.json")
262
- decision = load_json(args.decision)
263
- saved = load_json(confirmation_file) if confirmation_file.exists() else None
264
- updated, result = apply_selection(plan, decision, saved)
265
- # Persist confirmation first. If the selected ID changes and the
266
- # second write fails, read-selection rejects the mismatch.
267
- if saved != decision:
268
- save_json(confirmation_file, decision)
269
- if result["status"] == "applied":
270
- save_json(args.plan, updated)
271
- else:
272
- _, result = read_selection(plan, load_json(args.plan.parent / "architecture-selection.json"))
273
- print(json.dumps(result, ensure_ascii=False))
139
+ print(json.dumps({"status": "valid", "projectName": plan["projectName"]}, ensure_ascii=False))
274
140
  return 0
275
- except (ContractError, OSError, ValueError, RecursionError) as error:
141
+ except (OSError, ValueError, RecursionError) as error:
276
142
  print(json.dumps({"status": "error", "error": str(error)}, ensure_ascii=False))
277
143
  return 1
278
144