android2harmony 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/README.md +407 -0
- package/agents/build-fixer.md +385 -0
- package/agents/code-reviewer.md +478 -0
- package/agents/logic-coder.md +212 -0
- package/agents/logic-context-builder.md +199 -0
- package/agents/scripts/hmos-precheck/arkts.mjs +534 -0
- package/agents/scripts/hmos-precheck/codelinter.mjs +362 -0
- package/agents/scripts/hmos-precheck/index.mjs +926 -0
- package/agents/scripts/platform_query.mjs +535 -0
- package/dist/index.js +179 -0
- package/dist/index.js.map +7 -0
- package/package.json +29 -0
- package/skills/hmos-batch-ui-align/SKILL.md +141 -0
- package/skills/hmos-batch-ui-align/references/conversion-procedure.md +217 -0
- package/skills/hmos-batch-ui-align/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2533 -0
- package/skills/hmos-batch-ui-align/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
- package/skills/hmos-batch-ui-align/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
- package/skills/hmos-batch-ui-align/references/mvvm/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md +648 -0
- package/skills/hmos-batch-ui-align/references/mvvm/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md +2089 -0
- package/skills/hmos-batch-ui-align/references/mvvm/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md +1033 -0
- package/skills/hmos-batch-ui-align/references/mvvm/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md +1183 -0
- package/skills/hmos-batch-ui-align/references/mvvm/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md +576 -0
- package/skills/hmos-batch-ui-align/references/mvvm/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md +297 -0
- package/skills/hmos-batch-ui-align/references/mvvm/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md +395 -0
- package/skills/hmos-batch-ui-align/references/mvvm/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md +903 -0
- package/skills/hmos-batch-ui-align/references/mvvm/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
- package/skills/hmos-batch-ui-align/references/mvvm/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md +1178 -0
- package/skills/hmos-batch-ui-align/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
- package/skills/hmos-batch-ui-align/references/mvvm/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +355 -0
- package/skills/hmos-batch-ui-align/references/mvvm//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md +11 -0
- package/skills/hmos-batch-ui-align/scripts/android_parse_fast.ts +1750 -0
- package/skills/hmos-convert-pipeline/SKILL.md +446 -0
- package/skills/hmos-fix-build-errors/SKILL.md +281 -0
- package/skills/hmos-fix-build-errors/references/arkts-strict-patterns.md +219 -0
- package/skills/hmos-fix-build-errors/references/known-patterns.md +157 -0
- package/skills/hmos-fix-build-errors/references/rdb-entity-pattern.md +131 -0
- package/skills/hmos-resources-convert/SKILL.md +654 -0
- package/skills/hmos-resources-convert/references/conversion-rules.md +663 -0
- package/skills/hmos-resources-convert/references/dependency-analysis-rules.md +388 -0
- package/skills/hmos-resources-convert/references/resource-mapping-rules.md +457 -0
- package/skills/hmos-resources-convert/references/xml-drawable-to-svg-rules.md +513 -0
- package/skills/hmos-resources-convert/template/AppScope/app.json5 +10 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +8 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +7 -0
- package/skills/hmos-resources-convert/template/build-profile.json5 +42 -0
- package/skills/hmos-resources-convert/template/code-linter.json5 +32 -0
- package/skills/hmos-resources-convert/template/entry/build-profile.json5 +33 -0
- package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +6 -0
- package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +23 -0
- package/skills/hmos-resources-convert/template/entry/oh-package.json5 +10 -0
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +48 -0
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +16 -0
- package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +23 -0
- package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +55 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +8 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +8 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +16 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +7 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +3 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +5 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +8 -0
- package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +2 -0
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +35 -0
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +5 -0
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +16 -0
- package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +5 -0
- package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +33 -0
- package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +23 -0
- package/skills/hmos-resources-convert/template/hvigorfile.ts +6 -0
- package/skills/hmos-resources-convert/template/oh-package-lock.json5 +28 -0
- package/skills/hmos-resources-convert/template/oh-package.json5 +10 -0
- package/skills/hmos-spec-generate/SKILL.md +413 -0
- package/skills/hmos-spec-generate/references/android-platform-tokens.md +105 -0
- package/skills/hmos-spec-generate/references/spec-sample-1.md +78 -0
- package/skills/hmos-spec-generate/references/spec-sample-2.md +58 -0
- package/skills/hmos-spec-generate/references/spec-sample-3.md +116 -0
- package/skills/hmos-spec-generate/references/step4-report-template.md +33 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_API_LEVEL = 22;
|
|
7
|
+
const ALLOWED_STAGES = new Set(['planner', 'coder']);
|
|
8
|
+
const DEFAULT_QUERY_TYPES_BY_STAGE = {
|
|
9
|
+
planner: ['api', 'pattern'],
|
|
10
|
+
coder: ['api', 'pattern'],
|
|
11
|
+
};
|
|
12
|
+
const DEEPWIKI_URL = 'https://mcp.deepwiki.com/mcp';
|
|
13
|
+
const READ_TIMEOUT_MS = 120000;
|
|
14
|
+
const MAX_RESPONSE_CHARS = 1600;
|
|
15
|
+
const MAX_RETRIES = 2;
|
|
16
|
+
const RETRY_BASE_MS = 2000;
|
|
17
|
+
const DEEPWIKI_REPO = 'jjjzzz666/dev_kb_repo';
|
|
18
|
+
const PROVIDER_FAILURE_PATTERNS = [
|
|
19
|
+
'Error processing question',
|
|
20
|
+
'Query failed',
|
|
21
|
+
'Error while executing query',
|
|
22
|
+
];
|
|
23
|
+
const PLANNER_SLOTS = [
|
|
24
|
+
'feasibility',
|
|
25
|
+
'owner_boundary',
|
|
26
|
+
'forbidden_path',
|
|
27
|
+
'version_gate',
|
|
28
|
+
'protocol_variant',
|
|
29
|
+
'fallback_gap',
|
|
30
|
+
'blocking_unknown',
|
|
31
|
+
'completion_evidence',
|
|
32
|
+
'implementation_implication',
|
|
33
|
+
];
|
|
34
|
+
const CODER_SLOTS = [
|
|
35
|
+
'exact_api',
|
|
36
|
+
'exact_import',
|
|
37
|
+
'return_error',
|
|
38
|
+
'context_permission',
|
|
39
|
+
'lifecycle_threading',
|
|
40
|
+
'local_forbidden',
|
|
41
|
+
'local_validation',
|
|
42
|
+
'planned_fallback',
|
|
43
|
+
];
|
|
44
|
+
const ALLOWED_STRUCTURED_SLOTS = new Set([...PLANNER_SLOTS, ...CODER_SLOTS]);
|
|
45
|
+
const PREFACE_PATTERNS = [
|
|
46
|
+
'you are asking',
|
|
47
|
+
"you're asking",
|
|
48
|
+
'i understand',
|
|
49
|
+
'based on',
|
|
50
|
+
'summary:',
|
|
51
|
+
'here are',
|
|
52
|
+
];
|
|
53
|
+
const STRUCTURED_LINE_RE = /^[-*]\s*(?:\*\*)?(?<slot>extra:[a-z0-9_-]+|[a-z][a-z0-9_]*)(?:\*\*)?\s*:\s*(?<fact>.+)$/i;
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const args = {
|
|
57
|
+
request: '',
|
|
58
|
+
outDir: '',
|
|
59
|
+
apiLevel: null,
|
|
60
|
+
dryRun: false,
|
|
61
|
+
};
|
|
62
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
63
|
+
const token = argv[index];
|
|
64
|
+
if (token === '--request' && argv[index + 1]) {
|
|
65
|
+
args.request = argv[++index];
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (token === '--out-dir' && argv[index + 1]) {
|
|
69
|
+
args.outDir = argv[++index];
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (token === '--api-level' && argv[index + 1]) {
|
|
73
|
+
args.apiLevel = Number.parseInt(argv[++index], 10);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (token === '--dry-run') {
|
|
77
|
+
args.dryRun = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`unknown argument: ${token}`);
|
|
81
|
+
}
|
|
82
|
+
if (!args.request || !args.outDir) {
|
|
83
|
+
throw new Error('--request and --out-dir are required');
|
|
84
|
+
}
|
|
85
|
+
if (args.apiLevel !== null && !Number.isInteger(args.apiLevel)) {
|
|
86
|
+
throw new Error('--api-level must be an integer');
|
|
87
|
+
}
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loadJson(filePath) {
|
|
92
|
+
const parsed = JSON.parse(await readFile(filePath, 'utf8'));
|
|
93
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
94
|
+
throw new Error('request must be a JSON object');
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function writeJson(filePath, data) {
|
|
100
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
101
|
+
await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalize(value) {
|
|
105
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeList(value, name) {
|
|
109
|
+
if (value == null) return [];
|
|
110
|
+
if (!Array.isArray(value)) throw new Error(`${name} must be a list`);
|
|
111
|
+
return value.map((item) => normalize(item)).filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveApiLevel(request, cliApiLevel) {
|
|
115
|
+
if (cliApiLevel != null) return { apiLevel: cliApiLevel, source: 'cli' };
|
|
116
|
+
const raw = request.api_level ?? request.api_version;
|
|
117
|
+
if (raw != null) {
|
|
118
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
119
|
+
if (!Number.isInteger(parsed)) throw new Error('api_level must be an integer');
|
|
120
|
+
return { apiLevel: parsed, source: 'request' };
|
|
121
|
+
}
|
|
122
|
+
return { apiLevel: DEFAULT_API_LEVEL, source: 'default' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateRequest(request, apiLevel) {
|
|
126
|
+
const stage = normalize(request.stage);
|
|
127
|
+
if (!ALLOWED_STAGES.has(stage)) throw new Error("stage must be 'planner' or 'coder'");
|
|
128
|
+
const focusPoint = normalize(request.focus_point);
|
|
129
|
+
const taskExcerpt = normalize(request.task_excerpt ?? request.spec_excerpt);
|
|
130
|
+
if (!focusPoint) throw new Error('focus_point is required');
|
|
131
|
+
if (!taskExcerpt) throw new Error('task_excerpt is required');
|
|
132
|
+
const queryTypes = Array.isArray(request.query_types)
|
|
133
|
+
? request.query_types.map((item) => normalize(item).toLowerCase()).filter(Boolean)
|
|
134
|
+
: [...DEFAULT_QUERY_TYPES_BY_STAGE[stage]];
|
|
135
|
+
for (const queryType of queryTypes) {
|
|
136
|
+
if (!['api', 'pattern', 'migration'].includes(queryType)) {
|
|
137
|
+
throw new Error(`unsupported query_type: ${queryType}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
stage,
|
|
142
|
+
api_level: apiLevel,
|
|
143
|
+
focus_point: focusPoint,
|
|
144
|
+
task_excerpt: taskExcerpt,
|
|
145
|
+
project_evidence: normalize(request.project_evidence) || 'none',
|
|
146
|
+
platform_surfaces: normalizeList(request.platform_surfaces, 'platform_surfaces'),
|
|
147
|
+
extra_constraints: normalizeList(request.extra_constraints, 'extra_constraints'),
|
|
148
|
+
query_types: [...new Set(queryTypes)],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function shortModuleName(stage, focusPoint) {
|
|
153
|
+
const base = focusPoint.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '').toLowerCase() || 'platform_context';
|
|
154
|
+
return `${stage}_${base.slice(0, 72).replace(/_+$/g, '')}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function joinItems(items) {
|
|
158
|
+
return items.length ? items.join('; ') : 'none';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function defaultExtraConstraints(stage) {
|
|
162
|
+
if (stage === 'planner') {
|
|
163
|
+
return [
|
|
164
|
+
'Return only facts that affect feasibility, owner boundary, forbidden path, fallback/gap, edit boundary, blocking unknown, or completion evidence.',
|
|
165
|
+
'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.',
|
|
166
|
+
'Use the provided platform surfaces as natural signals. Prefer one primary API-family answer; do not enumerate broad alternative protocol families unless local evidence already narrows to the same capability family.',
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
return [
|
|
170
|
+
'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.',
|
|
171
|
+
'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.',
|
|
172
|
+
'Use the provided platform surfaces as natural signals. Keep the answer local to the approved implementation point, prefer one primary API-family answer, and do not redesign the architecture.',
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildStageChecks(stage, queryType) {
|
|
177
|
+
if (stage === 'planner') {
|
|
178
|
+
if (queryType === 'api') {
|
|
179
|
+
return 'path feasibility at this API level; required owner/context/lifecycle/capability gate; API family traps that can invalidate the plan; fallback or blocking gap; code-observable completion evidence affected by this fact';
|
|
180
|
+
}
|
|
181
|
+
if (queryType === 'migration') {
|
|
182
|
+
return 'cross-platform assumptions that would make the plan invalid; HarmonyOS primary path; forbidden migration paths; owner/lifecycle/permission differences; fallback or blocking gap';
|
|
183
|
+
}
|
|
184
|
+
return 'forbidden paths and false friends; negative space that looks valid but should not be used; valid high-level pattern only if it changes the plan; hidden owner/lifecycle/state pitfalls; fallback or blocking gap; the smallest evidence a planner can require to know the path is complete';
|
|
185
|
+
}
|
|
186
|
+
if (queryType === 'api') {
|
|
187
|
+
return 'exact import/module/type/member/signature and minimal call shape; API-level availability for this call; required permission/config/context; return/error/null/default/conflict semantics; call-site lifecycle/threading constraints; compile/runtime failure traps';
|
|
188
|
+
}
|
|
189
|
+
if (queryType === 'migration') {
|
|
190
|
+
return 'old-platform assumptions that would break this local edit; exact HarmonyOS replacement API or local pattern; incompatible parameters/return semantics; permission/lifecycle differences at this call site';
|
|
191
|
+
}
|
|
192
|
+
return 'minimal valid local usage pattern; local forbidden usage that would fail compile/runtime; lifecycle/permission/state pitfalls at this call site; whether the planned fallback is valid; minimal validation checks for this edit; do not redesign the architecture';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function stageQualityBar(stage) {
|
|
196
|
+
if (stage === 'planner') {
|
|
197
|
+
return 'Quality bar: a planner can write a short contract from the answer: main path, forbidden path, fallback/gap, owner/lifecycle boundary, blocking unknown if unresolved, and observable completion evidence. ';
|
|
198
|
+
}
|
|
199
|
+
return 'Quality bar: a coder can write or check the local code from the answer: exact import/module/type/member/signature, minimal call shape, context/permission/config, lifecycle/threading, return/error/null/default/conflict semantics, forbidden local usage, and validation check. ';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildQuery(request, queryType) {
|
|
203
|
+
const stageGoal = request.stage === 'planner'
|
|
204
|
+
? 'choose a valid implementation path before coding'
|
|
205
|
+
: 'verify exact local API usage while implementing';
|
|
206
|
+
const slots = request.stage === 'planner' ? PLANNER_SLOTS.join(', ') : CODER_SLOTS.join(', ');
|
|
207
|
+
return [
|
|
208
|
+
`HarmonyOS ArkTS/ArkUI platform fact check. API level: ${request.api_level}.`,
|
|
209
|
+
`Stage goal: ${stageGoal}. Focus: ${request.focus_point}.`,
|
|
210
|
+
`Task constraint: ${request.task_excerpt}.`,
|
|
211
|
+
`Project evidence: ${request.project_evidence}.`,
|
|
212
|
+
`Platform surfaces: ${joinItems(request.platform_surfaces)}.`,
|
|
213
|
+
`Extra constraints: ${joinItems(request.extra_constraints.length ? request.extra_constraints : defaultExtraConstraints(request.stage))}.`,
|
|
214
|
+
`Check only: ${buildStageChecks(request.stage, queryType)}.`,
|
|
215
|
+
stageQualityBar(request.stage),
|
|
216
|
+
'Return concise evidence bullets only.',
|
|
217
|
+
`Use these slots when applicable: ${slots}.`,
|
|
218
|
+
'For critical slots, write `slot: none` when the fact is not applicable or not known.',
|
|
219
|
+
'If sources disagree or the fact is unavailable, state the conflict or unknown explicitly; do not smooth it over.',
|
|
220
|
+
'You may add extra:<name> if a critical fact does not fit.',
|
|
221
|
+
'Do not invent missing facts.',
|
|
222
|
+
'Max 8 bullets. Format each bullet as \'- slot: fact\'.',
|
|
223
|
+
'No preface, headings, examples, tutorials, or steps.',
|
|
224
|
+
].join(' ');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cleanEvidenceResponse(text) {
|
|
228
|
+
const structuredLines = [];
|
|
229
|
+
const fallbackLines = [];
|
|
230
|
+
for (const rawLine of String(text).split('\n')) {
|
|
231
|
+
const line = rawLine.trim();
|
|
232
|
+
if (!line) continue;
|
|
233
|
+
const lowered = line.toLowerCase().replace(/^[#*\-0-9.\s]+/, '');
|
|
234
|
+
if (line.startsWith('#') || PREFACE_PATTERNS.some((pattern) => lowered.startsWith(pattern))) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const match = STRUCTURED_LINE_RE.exec(line);
|
|
238
|
+
if (match?.groups) {
|
|
239
|
+
let { slot } = match.groups;
|
|
240
|
+
const fact = match.groups.fact.trim();
|
|
241
|
+
if (!fact) continue;
|
|
242
|
+
slot = slot.toLowerCase();
|
|
243
|
+
if (!ALLOWED_STRUCTURED_SLOTS.has(slot) && !slot.startsWith('extra:')) {
|
|
244
|
+
slot = `extra:${slot}`;
|
|
245
|
+
}
|
|
246
|
+
structuredLines.push(`- ${slot}: ${fact}`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
fallbackLines.push(line);
|
|
250
|
+
}
|
|
251
|
+
if (structuredLines.length) return { cleaned: structuredLines.join('\n'), structured: true };
|
|
252
|
+
const cleaned = fallbackLines.join('\n').trim();
|
|
253
|
+
return { cleaned: cleaned || String(text), structured: false };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildQueries(request) {
|
|
257
|
+
const moduleName = shortModuleName(request.stage, request.focus_point);
|
|
258
|
+
return request.query_types.map((queryType) => ({
|
|
259
|
+
module: moduleName,
|
|
260
|
+
type: queryType,
|
|
261
|
+
query: buildQuery(request, queryType),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseSseResponse(content) {
|
|
266
|
+
let result = '';
|
|
267
|
+
for (const rawLine of content.split('\n')) {
|
|
268
|
+
const line = rawLine.trim();
|
|
269
|
+
if (!line.startsWith('data: ')) continue;
|
|
270
|
+
try {
|
|
271
|
+
const data = JSON.parse(line.slice(6));
|
|
272
|
+
if (data.error) return `[provider_error] ${data.error}`;
|
|
273
|
+
for (const item of data.result?.content ?? []) {
|
|
274
|
+
if (item?.type === 'text') result += item.text ?? '';
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return result.trim();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function truncateResponse(text) {
|
|
284
|
+
if (text.length <= MAX_RESPONSE_CHARS) return text;
|
|
285
|
+
let cut = text.lastIndexOf('\n\n', MAX_RESPONSE_CHARS);
|
|
286
|
+
if (cut < MAX_RESPONSE_CHARS * 0.5) cut = MAX_RESPONSE_CHARS;
|
|
287
|
+
return `${text.slice(0, cut).trimEnd()}\n[truncated]`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function providerTextFailure(text) {
|
|
291
|
+
return PROVIDER_FAILURE_PATTERNS.some((pattern) => text.includes(pattern));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function retryWaitMillis(attempt) {
|
|
295
|
+
const base = RETRY_BASE_MS * (2 ** attempt);
|
|
296
|
+
return Math.round(base * (0.8 + (0.4 * Math.random())));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function askDeepwikiOnce(repo, question) {
|
|
300
|
+
const controller = new AbortController();
|
|
301
|
+
const timer = setTimeout(() => controller.abort(), READ_TIMEOUT_MS);
|
|
302
|
+
try {
|
|
303
|
+
const response = await fetch(DEEPWIKI_URL, {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: {
|
|
306
|
+
'Content-Type': 'application/json',
|
|
307
|
+
Accept: 'application/json, text/event-stream',
|
|
308
|
+
'User-Agent': 'platform-context-query/0.1',
|
|
309
|
+
},
|
|
310
|
+
body: JSON.stringify({
|
|
311
|
+
jsonrpc: '2.0',
|
|
312
|
+
method: 'tools/call',
|
|
313
|
+
id: 1,
|
|
314
|
+
params: {
|
|
315
|
+
name: 'ask_question',
|
|
316
|
+
arguments: {
|
|
317
|
+
repoName: repo,
|
|
318
|
+
question,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
signal: controller.signal,
|
|
323
|
+
});
|
|
324
|
+
const text = await response.text();
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
response: `[http_error] ${response.status} ${response.statusText}`,
|
|
329
|
+
retryable: [429, 500, 502, 503, 504].includes(response.status),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const answer = parseSseResponse(text);
|
|
333
|
+
if (!answer) return { ok: false, response: '[provider_error] empty response', retryable: true };
|
|
334
|
+
if (answer.startsWith('[provider_error]')) return { ok: false, response: answer, retryable: true };
|
|
335
|
+
if (answer.includes('429 Too Many Requests') || answer.includes("Client error '429")) {
|
|
336
|
+
return { ok: false, response: answer, retryable: true };
|
|
337
|
+
}
|
|
338
|
+
if (providerTextFailure(answer)) return { ok: false, response: `[provider_error] ${answer}`, retryable: true };
|
|
339
|
+
return { ok: true, response: truncateResponse(answer), retryable: false };
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error?.name === 'AbortError') return { ok: false, response: '[timeout]', retryable: true };
|
|
342
|
+
return { ok: false, response: `[provider_exception] ${error?.name ?? 'Error'}: ${error?.message ?? String(error)}`, retryable: false };
|
|
343
|
+
} finally {
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function askDeepwiki(repo, question) {
|
|
349
|
+
let lastResponse = '';
|
|
350
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
351
|
+
const result = await askDeepwikiOnce(repo, question);
|
|
352
|
+
if (result.ok) return { ok: true, response: result.response };
|
|
353
|
+
lastResponse = result.response;
|
|
354
|
+
if (!result.retryable || attempt >= MAX_RETRIES) break;
|
|
355
|
+
await new Promise((resolve) => setTimeout(resolve, retryWaitMillis(attempt)));
|
|
356
|
+
}
|
|
357
|
+
return { ok: false, response: lastResponse };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function queryOne(query) {
|
|
361
|
+
const { ok, response } = await askDeepwiki(DEEPWIKI_REPO, query.query);
|
|
362
|
+
return {
|
|
363
|
+
ok,
|
|
364
|
+
entry: {
|
|
365
|
+
ts: new Date().toISOString(),
|
|
366
|
+
provider: 'deepwiki',
|
|
367
|
+
repo: DEEPWIKI_REPO,
|
|
368
|
+
module: query.module ?? '',
|
|
369
|
+
type: query.type ?? 'pattern',
|
|
370
|
+
query: query.query ?? '',
|
|
371
|
+
response,
|
|
372
|
+
quality: ok ? 'verified' : 'query_failed',
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function runDeepwikiProvider(queries, cachePath) {
|
|
378
|
+
const results = await Promise.all(queries.map((query) => queryOne(query)));
|
|
379
|
+
const sortedEntries = results.map((result) => result.entry).sort((left, right) => String(left.type).localeCompare(String(right.type)));
|
|
380
|
+
const stderrLines = [];
|
|
381
|
+
let okCount = 0;
|
|
382
|
+
for (const { ok, entry } of results) {
|
|
383
|
+
if (ok) {
|
|
384
|
+
okCount += 1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
stderrLines.push(`${entry.module}/${entry.type}: ${entry.response}`);
|
|
388
|
+
}
|
|
389
|
+
await writeFile(cachePath, `${sortedEntries.map((entry) => JSON.stringify(entry)).join('\n')}${sortedEntries.length ? '\n' : ''}`, 'utf8');
|
|
390
|
+
return {
|
|
391
|
+
stdout: `Queried: ${okCount}, Failed: ${queries.length - okCount}, Total: ${queries.length}`,
|
|
392
|
+
stderr: stderrLines.join('\n'),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function readCacheEntries(cachePath) {
|
|
397
|
+
try {
|
|
398
|
+
const content = await readFile(cachePath, 'utf8');
|
|
399
|
+
return content
|
|
400
|
+
.split('\n')
|
|
401
|
+
.map((line) => line.trim())
|
|
402
|
+
.filter(Boolean)
|
|
403
|
+
.map((line) => {
|
|
404
|
+
try {
|
|
405
|
+
return JSON.parse(line);
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
.filter((entry) => entry && typeof entry === 'object');
|
|
411
|
+
} catch {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function main(argv = process.argv.slice(2)) {
|
|
417
|
+
const started = Date.now();
|
|
418
|
+
const args = parseArgs(argv);
|
|
419
|
+
const requestPath = path.resolve(args.request);
|
|
420
|
+
const outDir = path.resolve(args.outDir);
|
|
421
|
+
const providerDir = path.join(outDir, 'provider');
|
|
422
|
+
const resultPath = path.join(outDir, 'platform-context-result.json');
|
|
423
|
+
const tracePath = path.join(outDir, 'platform-context-trace.json');
|
|
424
|
+
const queriesPath = path.join(providerDir, 'provider-queries.json');
|
|
425
|
+
const cachePath = path.join(providerDir, 'provider-cache.jsonl');
|
|
426
|
+
|
|
427
|
+
const rawRequest = await loadJson(requestPath);
|
|
428
|
+
const { apiLevel, source: apiLevelSource } = resolveApiLevel(rawRequest, args.apiLevel);
|
|
429
|
+
const request = validateRequest(rawRequest, apiLevel);
|
|
430
|
+
const queries = buildQueries(request);
|
|
431
|
+
|
|
432
|
+
await mkdir(providerDir, { recursive: true });
|
|
433
|
+
await rm(cachePath, { force: true, recursive: true });
|
|
434
|
+
await copyFile(requestPath, path.join(outDir, 'platform-context-request.json'));
|
|
435
|
+
await writeJson(queriesPath, queries);
|
|
436
|
+
|
|
437
|
+
let status = args.dryRun ? 'dry_run' : 'ok';
|
|
438
|
+
let providerStdout = '';
|
|
439
|
+
let providerStderr = '';
|
|
440
|
+
let exitCode = 0;
|
|
441
|
+
|
|
442
|
+
if (!args.dryRun) {
|
|
443
|
+
const providerResult = await runDeepwikiProvider(queries, cachePath);
|
|
444
|
+
providerStdout = providerResult.stdout;
|
|
445
|
+
providerStderr = providerResult.stderr;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const evidence = await readCacheEntries(cachePath);
|
|
449
|
+
const verifiedCount = evidence.filter((entry) => entry.quality === 'verified').length;
|
|
450
|
+
const failedCount = evidence.length - verifiedCount;
|
|
451
|
+
let structuredMissingCount = 0;
|
|
452
|
+
if (!args.dryRun) {
|
|
453
|
+
if (verifiedCount === 0) {
|
|
454
|
+
status = 'provider_failed';
|
|
455
|
+
exitCode = 1;
|
|
456
|
+
} else if (failedCount > 0) {
|
|
457
|
+
status = 'partial_ok';
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const evidenceList = [];
|
|
462
|
+
const structuredEvidenceList = [];
|
|
463
|
+
for (const item of evidence) {
|
|
464
|
+
const rawResponse = String(item.response ?? '');
|
|
465
|
+
const { cleaned, structured } = cleanEvidenceResponse(rawResponse);
|
|
466
|
+
if (item.quality === 'verified' && !structured) structuredMissingCount += 1;
|
|
467
|
+
evidenceList.push({
|
|
468
|
+
type: item.type ?? '',
|
|
469
|
+
quality: item.quality ?? '',
|
|
470
|
+
query: item.query ?? '',
|
|
471
|
+
response: rawResponse,
|
|
472
|
+
});
|
|
473
|
+
structuredEvidenceList.push({
|
|
474
|
+
type: item.type ?? '',
|
|
475
|
+
quality: item.quality ?? '',
|
|
476
|
+
structured,
|
|
477
|
+
response: cleaned,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const result = {
|
|
482
|
+
status,
|
|
483
|
+
stage: request.stage,
|
|
484
|
+
api_level: apiLevel,
|
|
485
|
+
api_level_source: apiLevelSource,
|
|
486
|
+
focus_point: request.focus_point,
|
|
487
|
+
platform_surfaces: request.platform_surfaces,
|
|
488
|
+
evidence_count: evidence.length,
|
|
489
|
+
verified_evidence_count: verifiedCount,
|
|
490
|
+
failed_evidence_count: failedCount,
|
|
491
|
+
evidence: evidenceList,
|
|
492
|
+
structured_evidence: structuredEvidenceList,
|
|
493
|
+
structured_missing_count: structuredMissingCount,
|
|
494
|
+
llm_usage: [
|
|
495
|
+
'Use evidence only if it changes or confirms the platform contract, local API usage, forbidden path, fallback, blocking unknown, or validation condition.',
|
|
496
|
+
'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.',
|
|
497
|
+
'Do not paste raw evidence into plan.md or commit-info.md.',
|
|
498
|
+
'If status is not ok and the platform fact blocks the task, record blocking Unknown or platform_drift.',
|
|
499
|
+
],
|
|
500
|
+
};
|
|
501
|
+
const trace = {
|
|
502
|
+
status,
|
|
503
|
+
provider: 'deepwiki',
|
|
504
|
+
api_level: apiLevel,
|
|
505
|
+
api_level_source: apiLevelSource,
|
|
506
|
+
query_count: queries.length,
|
|
507
|
+
evidence_count: evidence.length,
|
|
508
|
+
verified_evidence_count: verifiedCount,
|
|
509
|
+
failed_evidence_count: failedCount,
|
|
510
|
+
structured_missing_count: structuredMissingCount,
|
|
511
|
+
elapsed_ms: Date.now() - started,
|
|
512
|
+
dry_run: args.dryRun,
|
|
513
|
+
provider_stdout: providerStdout,
|
|
514
|
+
provider_stderr: providerStderr,
|
|
515
|
+
internal_files: {
|
|
516
|
+
queries: queriesPath,
|
|
517
|
+
cache: cachePath,
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
await writeJson(resultPath, result);
|
|
522
|
+
await writeJson(tracePath, trace);
|
|
523
|
+
process.stdout.write(`${resultPath}\n`);
|
|
524
|
+
return exitCode;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
main().then(
|
|
528
|
+
(code) => {
|
|
529
|
+
process.exitCode = code;
|
|
530
|
+
},
|
|
531
|
+
(error) => {
|
|
532
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
533
|
+
process.exitCode = 1;
|
|
534
|
+
},
|
|
535
|
+
);
|