@buaa_smat/hometrans 0.1.0
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/LICENSE +21 -0
- package/README.md +124 -0
- package/agents/build-fixer.md +380 -0
- package/agents/code-review-fix.md +355 -0
- package/agents/code-reviewer.md +237 -0
- package/agents/logic-coding/scripts/platform_context_query.py +568 -0
- package/agents/logic-coding.md +198 -0
- package/agents/logic-context-builder.md +193 -0
- package/agents/review-fixer.md +404 -0
- package/agents/self-test-fixer.md +295 -0
- package/agents/self-test-setup.md +165 -0
- package/agents/self-tester.md +354 -0
- package/agents/spec-generator.md +540 -0
- package/dist/cli/config-store.js +110 -0
- package/dist/cli/config.js +28 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init.js +224 -0
- package/dist/cli/mcp-setup.js +262 -0
- package/dist/cli/mcp.js +94 -0
- package/dist/cli/uninstall.js +310 -0
- package/dist/context/index.js +688 -0
- package/dist/context/resources/sdkConfig.json +24 -0
- package/package.json +60 -0
- package/skills/convert_pipeline/SKILL.md +439 -0
- package/src/context/resources/sdkConfig.json +24 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Minimal platform context query wrapper.
|
|
3
|
+
|
|
4
|
+
The public contract is provider-neutral:
|
|
5
|
+
PlatformContextRequest -> platform-context-result.json
|
|
6
|
+
|
|
7
|
+
Provider details are internal. The script currently supports a DeepWiki-backed
|
|
8
|
+
provider by compiling a small query pack and querying the provider internally.
|
|
9
|
+
It does not summarize evidence or make decisions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import datetime as dt
|
|
16
|
+
import ssl
|
|
17
|
+
import json
|
|
18
|
+
import random
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.request
|
|
25
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DEFAULT_API_LEVEL = 22
|
|
31
|
+
ALLOWED_STAGES = {"planner", "coder"}
|
|
32
|
+
DEFAULT_QUERY_TYPES = ("api", "pattern")
|
|
33
|
+
DEEPWIKI_URL = "https://mcp.deepwiki.com/mcp"
|
|
34
|
+
READ_TIMEOUT_SECONDS = 120
|
|
35
|
+
MAX_RESPONSE_CHARS = 1600
|
|
36
|
+
MAX_WORKERS = 2
|
|
37
|
+
MAX_RETRIES = 2
|
|
38
|
+
RETRY_BASE_SECONDS = 2.0
|
|
39
|
+
DEEPWIKI_REPO = "jjjzzz666/dev_kb_repo"
|
|
40
|
+
PROVIDER_FAILURE_PATTERNS = (
|
|
41
|
+
"Error processing question",
|
|
42
|
+
"Query failed",
|
|
43
|
+
"Error while executing query",
|
|
44
|
+
)
|
|
45
|
+
PLANNER_SLOTS = (
|
|
46
|
+
"feasibility",
|
|
47
|
+
"owner_boundary",
|
|
48
|
+
"forbidden_path",
|
|
49
|
+
"version_gate",
|
|
50
|
+
"protocol_variant",
|
|
51
|
+
"fallback_gap",
|
|
52
|
+
"blocking_unknown",
|
|
53
|
+
"completion_evidence",
|
|
54
|
+
"implementation_implication",
|
|
55
|
+
)
|
|
56
|
+
CODER_SLOTS = (
|
|
57
|
+
"exact_api",
|
|
58
|
+
"exact_import",
|
|
59
|
+
"return_error",
|
|
60
|
+
"context_permission",
|
|
61
|
+
"lifecycle_threading",
|
|
62
|
+
"local_forbidden",
|
|
63
|
+
"local_validation",
|
|
64
|
+
"planned_fallback",
|
|
65
|
+
)
|
|
66
|
+
ALLOWED_STRUCTURED_SLOTS = frozenset(PLANNER_SLOTS + CODER_SLOTS)
|
|
67
|
+
PREFACE_PATTERNS = (
|
|
68
|
+
"you are asking",
|
|
69
|
+
"you're asking",
|
|
70
|
+
"i understand",
|
|
71
|
+
"based on",
|
|
72
|
+
"summary:",
|
|
73
|
+
"here are",
|
|
74
|
+
)
|
|
75
|
+
STRUCTURED_LINE_RE = re.compile(
|
|
76
|
+
r"^[-*]\s*(?:\*\*)?(?P<slot>[a-z][a-z0-9_]*|extra:[a-z0-9_-]+)(?:\*\*)?\s*:\s*(?P<fact>.+)$",
|
|
77
|
+
re.I,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_json(path: Path) -> dict[str, Any]:
|
|
82
|
+
try:
|
|
83
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
84
|
+
except json.JSONDecodeError as exc:
|
|
85
|
+
raise SystemExit(f"{path}: invalid JSON: {exc}") from exc
|
|
86
|
+
if not isinstance(data, dict):
|
|
87
|
+
raise SystemExit("request must be a JSON object")
|
|
88
|
+
return data
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def write_json(path: Path, data: Any) -> None:
|
|
92
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def normalize(text: Any) -> str:
|
|
97
|
+
return re.sub(r"\s+", " ", str(text or "").strip())
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def normalize_list(value: Any, name: str) -> list[str]:
|
|
101
|
+
if value is None:
|
|
102
|
+
return []
|
|
103
|
+
if not isinstance(value, list):
|
|
104
|
+
raise SystemExit(f"{name} must be a list")
|
|
105
|
+
return [normalize(item) for item in value if normalize(item)]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def resolve_api_level(request: dict[str, Any], cli_api_level: int | None) -> tuple[int, str]:
|
|
109
|
+
if cli_api_level is not None:
|
|
110
|
+
return cli_api_level, "cli"
|
|
111
|
+
raw = request.get("api_level", request.get("api_version"))
|
|
112
|
+
if raw is not None:
|
|
113
|
+
try:
|
|
114
|
+
return int(raw), "request"
|
|
115
|
+
except (TypeError, ValueError) as exc:
|
|
116
|
+
raise SystemExit("api_level must be an integer") from exc
|
|
117
|
+
return DEFAULT_API_LEVEL, "default"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def validate_request(request: dict[str, Any], api_level: int) -> dict[str, Any]:
|
|
121
|
+
stage = normalize(request.get("stage"))
|
|
122
|
+
if stage not in ALLOWED_STAGES:
|
|
123
|
+
raise SystemExit("stage must be 'planner' or 'coder'")
|
|
124
|
+
|
|
125
|
+
focus_point = normalize(request.get("focus_point"))
|
|
126
|
+
task_excerpt = normalize(request.get("task_excerpt", request.get("spec_excerpt")))
|
|
127
|
+
if not focus_point:
|
|
128
|
+
raise SystemExit("focus_point is required")
|
|
129
|
+
if not task_excerpt:
|
|
130
|
+
raise SystemExit("task_excerpt is required")
|
|
131
|
+
|
|
132
|
+
project_evidence = normalize(request.get("project_evidence")) or "none"
|
|
133
|
+
platform_surfaces = normalize_list(request.get("platform_surfaces"), "platform_surfaces")
|
|
134
|
+
extra_constraints = normalize_list(request.get("extra_constraints"), "extra_constraints")
|
|
135
|
+
return {
|
|
136
|
+
"stage": stage,
|
|
137
|
+
"api_level": api_level,
|
|
138
|
+
"focus_point": focus_point,
|
|
139
|
+
"task_excerpt": task_excerpt,
|
|
140
|
+
"project_evidence": project_evidence,
|
|
141
|
+
"platform_surfaces": platform_surfaces,
|
|
142
|
+
"extra_constraints": extra_constraints,
|
|
143
|
+
"query_types": list(DEFAULT_QUERY_TYPES),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def short_module_name(stage: str, focus_point: str) -> str:
|
|
148
|
+
base = re.sub(r"[^a-zA-Z0-9]+", "_", focus_point).strip("_").lower()
|
|
149
|
+
if not base:
|
|
150
|
+
base = "platform_context"
|
|
151
|
+
return f"{stage}_{base[:72].rstrip('_')}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def join_items(items: list[str]) -> str:
|
|
155
|
+
return "; ".join(items) if items else "none"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def default_extra_constraints(stage: str) -> list[str]:
|
|
159
|
+
if stage == "planner":
|
|
160
|
+
return [
|
|
161
|
+
"Return only facts that affect feasibility, owner boundary, forbidden path, fallback/gap, edit boundary, blocking unknown, or completion evidence.",
|
|
162
|
+
"Check API-level gates, capability/permission/signing gates, owner/context/lifecycle constraints, sync/async protocol differences, state/persistence timing, return/error/null/default semantics, forbidden paths, false friends, fallback/gaps, completion evidence, and same-capability different-protocol traps.",
|
|
163
|
+
"Use the provided platform surfaces as natural signals. Do not require exact API names from the caller, and do not assume the obvious API family is the only valid protocol.",
|
|
164
|
+
]
|
|
165
|
+
return [
|
|
166
|
+
"Return only facts that affect exact API/import/type/signature/member availability, return/error/null/default/conflict behavior, context/permission/config, lifecycle/threading, local forbidden usage, planned fallback, or local validation.",
|
|
167
|
+
"Check exact module imports, types, members, signatures, API-level gates, capability/permission/signing gates, owner/context/lifecycle constraints, sync/async protocol differences, state/persistence timing, return/error/null/default/conflict semantics, forbidden paths, false friends, and compile/runtime traps.",
|
|
168
|
+
"Use the provided platform surfaces as natural signals. Keep the answer local to the approved implementation point; do not redesign the architecture.",
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_stage_checks(stage: str, query_type: str) -> str:
|
|
173
|
+
if stage == "planner":
|
|
174
|
+
if query_type == "api":
|
|
175
|
+
return (
|
|
176
|
+
"path feasibility at this API level; required owner/context/lifecycle/"
|
|
177
|
+
"capability gate; API family traps that can invalidate the plan; "
|
|
178
|
+
"fallback or blocking gap; code-observable completion evidence affected by this fact"
|
|
179
|
+
)
|
|
180
|
+
if query_type == "migration":
|
|
181
|
+
return (
|
|
182
|
+
"cross-platform assumptions that would make the plan invalid; "
|
|
183
|
+
"HarmonyOS primary path; forbidden migration paths; owner/lifecycle/"
|
|
184
|
+
"permission differences; fallback or blocking gap"
|
|
185
|
+
)
|
|
186
|
+
return (
|
|
187
|
+
"forbidden paths and false friends; negative space that looks valid but "
|
|
188
|
+
"should not be used; valid high-level pattern only if it changes the plan; "
|
|
189
|
+
"hidden owner/lifecycle/state pitfalls; fallback or blocking gap; "
|
|
190
|
+
"the smallest evidence a planner can require to know the path is complete"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if query_type == "api":
|
|
194
|
+
return (
|
|
195
|
+
"exact import/module/type/member/signature and minimal call shape; API-level "
|
|
196
|
+
"availability for this call; required permission/config/context; return/error/"
|
|
197
|
+
"null/default/conflict semantics; call-site lifecycle/threading constraints; "
|
|
198
|
+
"compile/runtime failure traps"
|
|
199
|
+
)
|
|
200
|
+
if query_type == "migration":
|
|
201
|
+
return (
|
|
202
|
+
"old-platform assumptions that would break this local edit; exact HarmonyOS "
|
|
203
|
+
"replacement API or local pattern; incompatible parameters/return semantics; "
|
|
204
|
+
"permission/lifecycle differences at this call site"
|
|
205
|
+
)
|
|
206
|
+
return (
|
|
207
|
+
"minimal valid local usage pattern; local forbidden usage that would fail "
|
|
208
|
+
"compile/runtime; lifecycle/permission/state pitfalls at this call site; "
|
|
209
|
+
"whether the planned fallback is valid; minimal validation checks for this edit; "
|
|
210
|
+
"do not redesign the architecture"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def stage_quality_bar(stage: str) -> str:
|
|
215
|
+
if stage == "planner":
|
|
216
|
+
return (
|
|
217
|
+
"Quality bar: a planner can write a short contract from the answer: "
|
|
218
|
+
"main path, forbidden path, fallback/gap, owner/lifecycle boundary, "
|
|
219
|
+
"blocking unknown if unresolved, and observable completion evidence. "
|
|
220
|
+
)
|
|
221
|
+
return (
|
|
222
|
+
"Quality bar: a coder can write or check the local code from the answer: "
|
|
223
|
+
"exact import/module/type/member/signature, minimal call shape, context/"
|
|
224
|
+
"permission/config, lifecycle/threading, return/error/null/default/conflict "
|
|
225
|
+
"semantics, forbidden local usage, and validation check. "
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def build_query(request: dict[str, Any], query_type: str) -> str:
|
|
230
|
+
stage_goal = (
|
|
231
|
+
"choose a valid implementation path before coding"
|
|
232
|
+
if request["stage"] == "planner"
|
|
233
|
+
else "verify exact local API usage while implementing"
|
|
234
|
+
)
|
|
235
|
+
surfaces = join_items(request["platform_surfaces"])
|
|
236
|
+
constraints = join_items(request["extra_constraints"] or default_extra_constraints(request["stage"]))
|
|
237
|
+
checks = build_stage_checks(request["stage"], query_type)
|
|
238
|
+
slots = ", ".join(PLANNER_SLOTS if request["stage"] == "planner" else CODER_SLOTS)
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
f"HarmonyOS ArkTS/ArkUI platform fact check. API level: {request['api_level']}. "
|
|
242
|
+
f"Stage goal: {stage_goal}. Focus: {request['focus_point']}. "
|
|
243
|
+
f"Task constraint: {request['task_excerpt']}. "
|
|
244
|
+
f"Project evidence: {request['project_evidence']}. "
|
|
245
|
+
f"Platform surfaces: {surfaces}. Extra constraints: {constraints}. "
|
|
246
|
+
f"Check only: {checks}. "
|
|
247
|
+
f"{stage_quality_bar(request['stage'])}"
|
|
248
|
+
"Return concise evidence bullets only. "
|
|
249
|
+
f"Use these slots when applicable: {slots}. "
|
|
250
|
+
"For critical slots, write `slot: none` when the fact is not applicable or not known. "
|
|
251
|
+
"If sources disagree or the fact is unavailable, state the conflict or unknown explicitly; do not smooth it over. "
|
|
252
|
+
"You may add extra:<name> if a critical fact does not fit. "
|
|
253
|
+
"Do not invent missing facts. "
|
|
254
|
+
"Max 8 bullets. Format each bullet as '- slot: fact'. "
|
|
255
|
+
"No preface, headings, examples, tutorials, or steps."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def clean_evidence_response(text: str) -> tuple[str, bool]:
|
|
260
|
+
lines = [line.strip() for line in text.splitlines()]
|
|
261
|
+
structured_lines: list[str] = []
|
|
262
|
+
fallback_lines: list[str] = []
|
|
263
|
+
for line in lines:
|
|
264
|
+
if not line:
|
|
265
|
+
continue
|
|
266
|
+
lowered = line.lower().lstrip("#*-0123456789. ")
|
|
267
|
+
if line.startswith("#") or any(lowered.startswith(pattern) for pattern in PREFACE_PATTERNS):
|
|
268
|
+
continue
|
|
269
|
+
match = STRUCTURED_LINE_RE.match(line)
|
|
270
|
+
if match:
|
|
271
|
+
slot = match.group("slot").lower()
|
|
272
|
+
fact = match.group("fact").strip()
|
|
273
|
+
if not fact:
|
|
274
|
+
continue
|
|
275
|
+
if slot not in ALLOWED_STRUCTURED_SLOTS and not slot.startswith("extra:"):
|
|
276
|
+
slot = f"extra:{slot}"
|
|
277
|
+
structured_lines.append(f"- {slot}: {fact}")
|
|
278
|
+
continue
|
|
279
|
+
fallback_lines.append(line)
|
|
280
|
+
if structured_lines:
|
|
281
|
+
return "\n".join(structured_lines), True
|
|
282
|
+
cleaned = "\n".join(fallback_lines).strip()
|
|
283
|
+
return cleaned or text, False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def build_queries(request: dict[str, Any]) -> list[dict[str, str]]:
|
|
287
|
+
module = short_module_name(request["stage"], request["focus_point"])
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
"module": module,
|
|
291
|
+
"type": query_type,
|
|
292
|
+
"query": build_query(request, query_type),
|
|
293
|
+
}
|
|
294
|
+
for query_type in request["query_types"]
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def read_cache_entries(path: Path) -> list[dict[str, Any]]:
|
|
299
|
+
if not path.exists():
|
|
300
|
+
return []
|
|
301
|
+
entries: list[dict[str, Any]] = []
|
|
302
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
303
|
+
if not line.strip():
|
|
304
|
+
continue
|
|
305
|
+
try:
|
|
306
|
+
entry = json.loads(line)
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
continue
|
|
309
|
+
if isinstance(entry, dict):
|
|
310
|
+
entries.append(entry)
|
|
311
|
+
return entries
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def parse_sse_response(content: str) -> str:
|
|
315
|
+
result = ""
|
|
316
|
+
for raw_line in content.splitlines():
|
|
317
|
+
line = raw_line.strip()
|
|
318
|
+
if not line.startswith("data: "):
|
|
319
|
+
continue
|
|
320
|
+
try:
|
|
321
|
+
data = json.loads(line[6:])
|
|
322
|
+
except json.JSONDecodeError:
|
|
323
|
+
continue
|
|
324
|
+
if "error" in data:
|
|
325
|
+
return f"[provider_error] {data['error']}"
|
|
326
|
+
for item in data.get("result", {}).get("content", []):
|
|
327
|
+
if item.get("type") == "text":
|
|
328
|
+
result += item.get("text", "")
|
|
329
|
+
return result.strip()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def truncate_response(text: str) -> str:
|
|
333
|
+
if len(text) <= MAX_RESPONSE_CHARS:
|
|
334
|
+
return text
|
|
335
|
+
cut = text.rfind("\n\n", 0, MAX_RESPONSE_CHARS)
|
|
336
|
+
if cut < MAX_RESPONSE_CHARS * 0.5:
|
|
337
|
+
cut = MAX_RESPONSE_CHARS
|
|
338
|
+
return text[:cut].rstrip() + "\n[truncated]"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def provider_text_failure(text: str) -> bool:
|
|
342
|
+
return any(pattern in text for pattern in PROVIDER_FAILURE_PATTERNS)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def retry_wait_seconds(attempt: int) -> float:
|
|
346
|
+
base = RETRY_BASE_SECONDS * (2 ** attempt)
|
|
347
|
+
return base * (0.8 + 0.4 * random.random())
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def ask_deepwiki_once(repo: str, question: str) -> tuple[bool, str, bool]:
|
|
351
|
+
payload = {
|
|
352
|
+
"jsonrpc": "2.0",
|
|
353
|
+
"method": "tools/call",
|
|
354
|
+
"id": 1,
|
|
355
|
+
"params": {
|
|
356
|
+
"name": "ask_question",
|
|
357
|
+
"arguments": {
|
|
358
|
+
"repoName": repo,
|
|
359
|
+
"question": question,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
request = urllib.request.Request(
|
|
364
|
+
DEEPWIKI_URL,
|
|
365
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
366
|
+
headers={
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
"Accept": "application/json, text/event-stream",
|
|
369
|
+
"User-Agent": "platform-context-query/0.1",
|
|
370
|
+
},
|
|
371
|
+
method="POST",
|
|
372
|
+
)
|
|
373
|
+
try:
|
|
374
|
+
context = ssl.create_default_context()
|
|
375
|
+
with urllib.request.urlopen(request, timeout=READ_TIMEOUT_SECONDS, context=context) as response:
|
|
376
|
+
text = response.read().decode("utf-8", errors="replace")
|
|
377
|
+
answer = parse_sse_response(text)
|
|
378
|
+
if not answer:
|
|
379
|
+
return False, "[provider_error] empty response", True
|
|
380
|
+
if answer.startswith("[provider_error]"):
|
|
381
|
+
return False, answer, True
|
|
382
|
+
if "429 Too Many Requests" in answer or "Client error '429" in answer:
|
|
383
|
+
return False, answer, True
|
|
384
|
+
if provider_text_failure(answer):
|
|
385
|
+
return False, f"[provider_error] {answer}", True
|
|
386
|
+
return True, truncate_response(answer), False
|
|
387
|
+
except urllib.error.HTTPError as exc:
|
|
388
|
+
return False, f"[http_error] {exc.code} {exc.reason}", exc.code in {429, 500, 502, 503, 504}
|
|
389
|
+
except urllib.error.URLError as exc:
|
|
390
|
+
return False, f"[network_error] {exc.reason}", True
|
|
391
|
+
except TimeoutError:
|
|
392
|
+
return False, "[timeout]", True
|
|
393
|
+
except Exception as exc:
|
|
394
|
+
return False, f"[provider_exception] {type(exc).__name__}: {exc}", False
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def ask_deepwiki(repo: str, question: str) -> tuple[bool, str]:
|
|
398
|
+
last_response = ""
|
|
399
|
+
for attempt in range(MAX_RETRIES + 1):
|
|
400
|
+
ok, response, retryable = ask_deepwiki_once(repo, question)
|
|
401
|
+
if ok:
|
|
402
|
+
return True, response
|
|
403
|
+
last_response = response
|
|
404
|
+
if not retryable or attempt >= MAX_RETRIES:
|
|
405
|
+
break
|
|
406
|
+
time.sleep(retry_wait_seconds(attempt))
|
|
407
|
+
return False, last_response
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def query_one(query: dict[str, str]) -> tuple[bool, dict[str, Any]]:
|
|
411
|
+
query_type = query.get("type", "pattern")
|
|
412
|
+
ok, response = ask_deepwiki(DEEPWIKI_REPO, query.get("query", ""))
|
|
413
|
+
entry = {
|
|
414
|
+
"ts": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"),
|
|
415
|
+
"provider": "deepwiki",
|
|
416
|
+
"repo": DEEPWIKI_REPO,
|
|
417
|
+
"module": query.get("module", ""),
|
|
418
|
+
"type": query_type,
|
|
419
|
+
"query": query.get("query", ""),
|
|
420
|
+
"response": response,
|
|
421
|
+
"quality": "verified" if ok else "query_failed",
|
|
422
|
+
}
|
|
423
|
+
return ok, entry
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def run_deepwiki_provider(queries: list[dict[str, str]], cache_path: Path) -> tuple[str, str]:
|
|
427
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
428
|
+
stderr_lines: list[str] = []
|
|
429
|
+
ok_count = 0
|
|
430
|
+
entries: list[dict[str, Any]] = []
|
|
431
|
+
with ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(queries) or 1)) as pool:
|
|
432
|
+
futures = {pool.submit(query_one, query): query for query in queries}
|
|
433
|
+
for future in as_completed(futures):
|
|
434
|
+
query = futures[future]
|
|
435
|
+
ok, entry = future.result()
|
|
436
|
+
entries.append(entry)
|
|
437
|
+
if ok:
|
|
438
|
+
ok_count += 1
|
|
439
|
+
else:
|
|
440
|
+
stderr_lines.append(f"{query.get('module', '')}/{query.get('type', '')}: {entry.get('response', '')}")
|
|
441
|
+
entries.sort(key=lambda item: str(item.get("type", "")))
|
|
442
|
+
with cache_path.open("a", encoding="utf-8") as handle:
|
|
443
|
+
for entry in entries:
|
|
444
|
+
handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
445
|
+
stdout = f"Queried: {ok_count}, Failed: {len(queries) - ok_count}, Total: {len(queries)}"
|
|
446
|
+
return stdout, "\n".join(stderr_lines)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def parse_args() -> argparse.Namespace:
|
|
450
|
+
parser = argparse.ArgumentParser(description="Query platform context evidence.")
|
|
451
|
+
parser.add_argument("--request", required=True, help="PlatformContextRequest JSON.")
|
|
452
|
+
parser.add_argument("--out-dir", required=True, help="Output directory.")
|
|
453
|
+
parser.add_argument("--api-level", type=int, help="Override API level. Defaults to request.api_level or 22.")
|
|
454
|
+
parser.add_argument("--dry-run", action="store_true", help="Build files without calling provider.")
|
|
455
|
+
return parser.parse_args()
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def main() -> int:
|
|
459
|
+
args = parse_args()
|
|
460
|
+
started = time.time()
|
|
461
|
+
|
|
462
|
+
request_path = Path(args.request)
|
|
463
|
+
out_dir = Path(args.out_dir)
|
|
464
|
+
provider_dir = out_dir / "provider"
|
|
465
|
+
result_path = out_dir / "platform-context-result.json"
|
|
466
|
+
trace_path = out_dir / "platform-context-trace.json"
|
|
467
|
+
queries_path = provider_dir / "provider-queries.json"
|
|
468
|
+
cache_path = provider_dir / "provider-cache.jsonl"
|
|
469
|
+
|
|
470
|
+
raw_request = load_json(request_path)
|
|
471
|
+
api_level, api_level_source = resolve_api_level(raw_request, args.api_level)
|
|
472
|
+
request = validate_request(raw_request, api_level)
|
|
473
|
+
queries = build_queries(request)
|
|
474
|
+
|
|
475
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
476
|
+
provider_dir.mkdir(parents=True, exist_ok=True)
|
|
477
|
+
cache_path.unlink(missing_ok=True)
|
|
478
|
+
shutil.copyfile(request_path, out_dir / "platform-context-request.json")
|
|
479
|
+
write_json(queries_path, queries)
|
|
480
|
+
|
|
481
|
+
status = "dry_run" if args.dry_run else "ok"
|
|
482
|
+
provider_stdout = ""
|
|
483
|
+
provider_stderr = ""
|
|
484
|
+
exit_code = 0
|
|
485
|
+
|
|
486
|
+
if not args.dry_run:
|
|
487
|
+
provider_stdout, provider_stderr = run_deepwiki_provider(queries, cache_path)
|
|
488
|
+
|
|
489
|
+
evidence = read_cache_entries(cache_path)
|
|
490
|
+
verified_count = sum(1 for item in evidence if item.get("quality") == "verified")
|
|
491
|
+
failed_count = len(evidence) - verified_count
|
|
492
|
+
structured_missing_count = 0
|
|
493
|
+
if not args.dry_run:
|
|
494
|
+
if verified_count == 0:
|
|
495
|
+
status = "provider_failed"
|
|
496
|
+
exit_code = 1
|
|
497
|
+
elif failed_count:
|
|
498
|
+
status = "partial_ok"
|
|
499
|
+
elapsed_ms = int((time.time() - started) * 1000)
|
|
500
|
+
|
|
501
|
+
evidence_list = []
|
|
502
|
+
structured_evidence_list = []
|
|
503
|
+
for item in evidence:
|
|
504
|
+
raw_response = item.get("response", "")
|
|
505
|
+
cleaned, structured = clean_evidence_response(str(raw_response))
|
|
506
|
+
if item.get("quality") == "verified" and not structured:
|
|
507
|
+
structured_missing_count += 1
|
|
508
|
+
evidence_list.append({
|
|
509
|
+
"type": item.get("type", ""),
|
|
510
|
+
"quality": item.get("quality", ""),
|
|
511
|
+
"query": item.get("query", ""),
|
|
512
|
+
"response": raw_response,
|
|
513
|
+
})
|
|
514
|
+
structured_evidence_list.append({
|
|
515
|
+
"type": item.get("type", ""),
|
|
516
|
+
"quality": item.get("quality", ""),
|
|
517
|
+
"structured": structured,
|
|
518
|
+
"response": cleaned,
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
result = {
|
|
522
|
+
"status": status,
|
|
523
|
+
"stage": request["stage"],
|
|
524
|
+
"api_level": api_level,
|
|
525
|
+
"api_level_source": api_level_source,
|
|
526
|
+
"focus_point": request["focus_point"],
|
|
527
|
+
"platform_surfaces": request["platform_surfaces"],
|
|
528
|
+
"evidence_count": len(evidence),
|
|
529
|
+
"verified_evidence_count": verified_count,
|
|
530
|
+
"failed_evidence_count": failed_count,
|
|
531
|
+
"evidence": evidence_list,
|
|
532
|
+
"structured_evidence": structured_evidence_list,
|
|
533
|
+
"structured_missing_count": structured_missing_count,
|
|
534
|
+
"llm_usage": [
|
|
535
|
+
"Use evidence only if it changes or confirms the platform contract, local API usage, forbidden path, fallback, blocking unknown, or validation condition.",
|
|
536
|
+
"If API facts and pattern facts disagree, do not merge them silently. Prefer the stricter lifecycle / permission / forbidden-path constraint; if the implementation path still depends on the disagreement, record blocking Unknown or platform_drift.",
|
|
537
|
+
"Do not paste raw evidence into plan.md or commit-info.md.",
|
|
538
|
+
"If status is not ok and the platform fact blocks the task, record blocking Unknown or platform_drift.",
|
|
539
|
+
],
|
|
540
|
+
}
|
|
541
|
+
trace = {
|
|
542
|
+
"status": status,
|
|
543
|
+
"provider": "deepwiki",
|
|
544
|
+
"api_level": api_level,
|
|
545
|
+
"api_level_source": api_level_source,
|
|
546
|
+
"query_count": len(queries),
|
|
547
|
+
"evidence_count": len(evidence),
|
|
548
|
+
"verified_evidence_count": verified_count,
|
|
549
|
+
"failed_evidence_count": failed_count,
|
|
550
|
+
"structured_missing_count": structured_missing_count,
|
|
551
|
+
"elapsed_ms": elapsed_ms,
|
|
552
|
+
"dry_run": bool(args.dry_run),
|
|
553
|
+
"provider_stdout": provider_stdout,
|
|
554
|
+
"provider_stderr": provider_stderr,
|
|
555
|
+
"internal_files": {
|
|
556
|
+
"queries": str(queries_path),
|
|
557
|
+
"cache": str(cache_path),
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
write_json(result_path, result)
|
|
562
|
+
write_json(trace_path, trace)
|
|
563
|
+
print(result_path)
|
|
564
|
+
return exit_code
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
if __name__ == "__main__":
|
|
568
|
+
raise SystemExit(main())
|