@harmonyos-arkts/d2h 0.1.20 → 0.1.21
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/plugin.json +1 -1
- package/README.md +40 -0
- package/agents/android-to-hmos-00-orchestrator.md +3 -315
- package/agents/d2h.md +53 -38
- package/contracts/workflow/artifacts.md +22 -0
- package/contracts/workflow/delivery.md +54 -0
- package/contracts/workflow/dispatch.md +26 -0
- package/contracts/workflow/execution.md +125 -0
- package/contracts/workflow/generation.md +40 -0
- package/contracts/workflow/orchestration.md +49 -0
- package/contracts/workflow/overview.md +58 -0
- package/contracts/workflow/plan.md +119 -0
- package/contracts/workflow/scope.md +151 -0
- package/dist/bin/install-opencode.js +34 -15
- package/dist/src/install-codex.js +131 -0
- package/dist/src/install-opencode.js +5 -5
- package/dist/src/workflow.js +54 -0
- package/docs/architecture-plan-guide.zh-CN.md +70 -0
- package/docs/incremental-spec-evaluation-design.md +670 -0
- package/docs/spec-plan-guide.md +226 -0
- package/docs/workflow-decoupling.md +59 -0
- package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
- package/opencode/interactive-tools/browser-host.ts +110 -0
- package/opencode/interactive-tools/core.ts +242 -0
- package/opencode/interactive-tools/demo-workflow.ts +55 -0
- package/opencode/interactive-tools/demo.mjs +37 -0
- package/opencode/interactive-tools/package-lock.json +1876 -0
- package/opencode/interactive-tools/package.json +23 -0
- package/opencode/interactive-tools/panel.ts +40 -0
- package/opencode/interactive-tools/prepare-demo.mjs +17 -0
- package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
- package/opencode/interactive-tools/progress.ts +118 -0
- package/opencode/interactive-tools/server-legacy.mjs +2 -0
- package/opencode/interactive-tools/server.mjs +40 -0
- package/opencode/interactive-tools/tui.tsx +230 -0
- package/package.json +9 -1
- package/schemas/native-libs-analysis-report.schema.json +695 -0
- package/schemas/native-libs-verify-report.schema.json +566 -0
- package/schemas/spec-native-libs-shard.schema.json +110 -0
- package/skills/arkts-icon-sizing/SKILL.md +0 -10
- package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
- package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
- package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
- package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
- package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
- package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
- package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
- package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
- package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
- package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
- package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
- package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
- package/skills/d2h-app-solution/SKILL.md +198 -0
- package/skills/d2h-app-solution/agents/openai.yaml +4 -0
- package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
- package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
- package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
- package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
- package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
- package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
- package/skills/d2h-architecture-plan/SKILL.md +35 -0
- package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
- package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
- package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
- package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
- package/skills/d2h-architecture-plan/references/source-review.md +50 -0
- package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
- package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
- package/skills/d2h-dashboard/SKILL.md +56 -0
- package/skills/d2h-product-family-migration/SKILL.md +103 -0
- package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
- package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
- package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
- package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
- package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
- package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
- package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
- package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
- package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
- package/skills/{d2h-page-develop → d2h-spec-develop}/SKILL.md +143 -126
- package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
- package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
- package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
- package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
- package/skills/d2h-spec-generate/references/from-plan.md +19 -0
- package/skills/d2h-spec-generate/references/spec-content.md +34 -0
- package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
- package/skills/d2h-spec-plan/SKILL.md +38 -0
- package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
- package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
- package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
- package/skills/d2h-spec-plan/references/planning-method.md +46 -0
- package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
- package/skills/d2h-spec-plan/references/work-packets.md +61 -0
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
- package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
- package/skills/d2h-utg-static-generate/SKILL.md +2 -2
- package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
- package/skills/managing-d2h-toolchain/toolchain.json +1 -1
- package/skills/preparing-migration-workspace/SKILL.md +32 -2
- package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
- package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
- package/workflow/artifact_bindings.py +224 -0
- package/workflow/assets/dashboard.html +88 -0
- package/workflow/build_batch_report.py +8 -0
- package/workflow/collect_code_metrics.py +75 -0
- package/workflow/dashboard_overview.py +109 -0
- package/workflow/delivery_view.py +82 -0
- package/workflow/feature_plan.py +234 -0
- package/workflow/page_batches.py +223 -0
- package/workflow/spec_generation.py +120 -0
- package/workflow/spec_runtime.py +890 -0
- package/workflow/work_packets.py +85 -0
- package/skills/d2h-page-develop/agents/openai.yaml +0 -7
- package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
- package/skills/native-library-substitution/SKILL.md +0 -385
- package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
- package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
- package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
- package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
- package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create a bounded, auditable evidence index for application repositories."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
from collections import Counter, defaultdict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SKIP_DIRS = {
|
|
17
|
+
".git", ".gradle", ".idea", ".harmonyos", ".dart_tool", ".swiftpm",
|
|
18
|
+
"node_modules", "build", "dist", "out", "target", "vendor", "Pods",
|
|
19
|
+
"DerivedData", "coverage", "generated", "tmp", "temp",
|
|
20
|
+
}
|
|
21
|
+
TEXT_SUFFIXES = {
|
|
22
|
+
".kt", ".kts", ".java", ".xml", ".gradle", ".ets", ".ts", ".tsx",
|
|
23
|
+
".js", ".jsx", ".swift", ".m", ".mm", ".dart", ".json", ".json5",
|
|
24
|
+
".yaml", ".yml", ".toml", ".plist", ".storyboard", ".xib", ".html",
|
|
25
|
+
".vue", ".svelte", ".md", ".txt", ".arb", ".properties", ".c", ".cc", ".cpp", ".cxx", ".h", ".hpp",
|
|
26
|
+
}
|
|
27
|
+
SPECIAL_TEXT_NAMES = {"AndroidManifest.xml", "Podfile", "Podfile.lock", "Package.swift", "Package.resolved", "CMakeLists.txt", "Makefile"}
|
|
28
|
+
TEST_PARTS = {"test", "tests", "androidtest", "ohostest", "uitest", "__tests__"}
|
|
29
|
+
DOC_PARTS = {"doc", "docs", "documentation", "user-manual", "manual"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
PATTERNS = [
|
|
33
|
+
("entry", "android", r"android\.intent\.action\.MAIN|android\.intent\.category\.LAUNCHER"),
|
|
34
|
+
("entry", "harmonyos", r"loadContent\s*\(|@Entry\b|srcEntry\b"),
|
|
35
|
+
("entry", "ios", r"@main\b|WindowGroup\s*\{|UIApplicationMain"),
|
|
36
|
+
("entry", "flutter", r"runApp\s*\(|MaterialApp\s*\(|CupertinoApp\s*\("),
|
|
37
|
+
("entry", "react-native", r"AppRegistry\.registerComponent|NavigationContainer"),
|
|
38
|
+
("route_declaration", "android", r"\bNavHost\s*\(|\bcomposable\s*\(|<fragment\b|<activity\b|<deepLink\b"),
|
|
39
|
+
("route_declaration", "harmonyos", r"NavDestination\s*\(|enum\s+Route|main_pages|\"pages\"\s*:"),
|
|
40
|
+
("route_declaration", "ios", r"NavigationStack\s*\{|NavigationLink\s*\(|navigationDestination\s*\("),
|
|
41
|
+
("route_declaration", "flutter", r"GoRoute\s*\(|routes\s*:|onGenerateRoute\s*:|AutoRoute\s*\("),
|
|
42
|
+
("route_declaration", "react-native", r"\.Screen\s+name=|create(?:Native)?StackNavigator|createBottomTabNavigator|createDrawerNavigator"),
|
|
43
|
+
("route_declaration", "web", r"<Route\b|createBrowserRouter\s*\(|defineNuxtRouteMiddleware|pages/"),
|
|
44
|
+
("navigation_call", "android", r"\.navigate\s*\(|startActivity\s*\(|findNavController\s*\(|FragmentTransaction|fragmentManager\b"),
|
|
45
|
+
("navigation_call", "harmonyos", r"pushPath(?:ByName)?\s*\(|router\.(?:pushUrl|replaceUrl|back)\s*\(|startAbility\s*\("),
|
|
46
|
+
("navigation_call", "ios", r"present\s*\(|pushViewController\s*\(|\.sheet\s*\(|fullScreenCover\s*\("),
|
|
47
|
+
("navigation_call", "flutter", r"Navigator\.(?:push|pushNamed|pop)|context\.(?:push|go)\s*\("),
|
|
48
|
+
("navigation_call", "react-native", r"navigation\.(?:navigate|push|replace|goBack)\s*\(|router\.(?:push|replace|back)\s*\("),
|
|
49
|
+
("navigation_call", "web", r"router\.(?:push|replace)\s*\(|navigate\s*\(|<Link\b"),
|
|
50
|
+
("ui_interaction", "generic", r"\bonClick\b|\.onClick\s*\(|setOnClickListener|\bclickable\s*\{|\bonTap\b|\bonPressed\b|\bonPress\b|addEventListener\s*\(\s*['\"]click"),
|
|
51
|
+
("ui_interaction", "generic", r"\bonSubmit\b|\.onSubmit\s*\(|\bonSelected\b|\.onChange\s*\(|onOptionsItemSelected"),
|
|
52
|
+
("tab", "generic", r"\bTabs?\s*\(|TabContent\s*\(|BottomNavigation|NavigationBar\s*\(|createBottomTabNavigator"),
|
|
53
|
+
("dialog", "generic", r"AlertDialog|DialogFragment|\.sheet\s*\(|showDialog\s*\(|showModal|Popover"),
|
|
54
|
+
("visibility_condition", "generic", r"\bvisible\s*=|android:visible=|\.visibility\b|\benabled\s*=|featureFlag|feature_flag"),
|
|
55
|
+
("effect", "generic", r"showToast|Snackbar|save\s*\(|delete\s*\(|remove\s*\(|send\s*\(|share\s*\(|write\s*\(|persist"),
|
|
56
|
+
("dependency_declaration", "generic", r"\b(?:implementation|api|compileOnly|runtimeOnly|ksp|classpath)\s*\(|\bpod\s+['\"]|\.package\s*\(|\bdependencies\s*:|\bdev_dependencies\s*:|\"dependencies\"\s*:|\"peerDependencies\"\s*:"),
|
|
57
|
+
("native_library", "android", r"System\.loadLibrary\s*\(|ReLinker\.loadLibrary\s*\(|external\s+fun\b|jniLibs|externalNativeBuild|cmake\s*\{|ndkBuild"),
|
|
58
|
+
("native_library", "generic", r"JNIEXPORT\b|RegisterNatives\s*\(|\bdlopen\s*\(|\.so\b|XCFramework|\.framework\b"),
|
|
59
|
+
("permission_declaration", "android", r"<uses-permission\b|<uses-feature\b|android\.permission\.[A-Z0-9_]+"),
|
|
60
|
+
("permission_request", "android", r"requestPermissions\s*\(|ActivityResultContracts\.RequestPermission|ActivityResultContracts\.RequestMultiplePermissions|checkSelfPermission\s*\(|shouldShowRequestPermissionRationale\s*\("),
|
|
61
|
+
("tech_stack", "android", r"AndroidView\s*\(|WebView\s*\(|loadUrl\s*\(|addJavascriptInterface\s*\("),
|
|
62
|
+
("tech_stack", "flutter", r"MethodChannel\s*\(|EventChannel\s*\(|PlatformViewLink\s*\("),
|
|
63
|
+
("tech_stack", "react-native", r"ReactContextBaseJavaModule|RCT_EXPORT_MODULE|NativeModules\.|requireNativeComponent\s*\("),
|
|
64
|
+
]
|
|
65
|
+
COMPILED_PATTERNS = [(kind, fw, re.compile(expr, re.IGNORECASE)) for kind, fw, expr in PATTERNS]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_args() -> argparse.Namespace:
|
|
69
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
70
|
+
parser.add_argument("--repo", action="append", required=True, metavar="ID=PATH")
|
|
71
|
+
parser.add_argument("--output", required=True, type=Path)
|
|
72
|
+
parser.add_argument("--max-file-bytes", type=int, default=1_500_000)
|
|
73
|
+
parser.add_argument("--max-records-per-file", type=int, default=500)
|
|
74
|
+
parser.add_argument("--exclude-tests", action="store_true")
|
|
75
|
+
parser.add_argument("--all-locales", action="store_true", help="index every locale instead of base/Chinese resources")
|
|
76
|
+
return parser.parse_args()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_repo(value: str) -> tuple[str, Path]:
|
|
80
|
+
if "=" not in value:
|
|
81
|
+
raise ValueError(f"--repo must use ID=PATH: {value}")
|
|
82
|
+
repo_id, raw_path = value.split("=", 1)
|
|
83
|
+
if not re.fullmatch(r"[a-z][a-z0-9_]*", repo_id):
|
|
84
|
+
raise ValueError(f"invalid repository ID: {repo_id}")
|
|
85
|
+
path = Path(raw_path).expanduser().resolve()
|
|
86
|
+
if not path.is_dir():
|
|
87
|
+
raise ValueError(f"repository path is not a directory: {path}")
|
|
88
|
+
return repo_id, path
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def detect_frameworks(repo: Path, files: list[Path]) -> list[str]:
|
|
92
|
+
rels = {p.relative_to(repo).as_posix() for p in files}
|
|
93
|
+
names = {p.name for p in files}
|
|
94
|
+
suffixes = Counter(p.suffix.lower() for p in files)
|
|
95
|
+
frameworks: list[str] = []
|
|
96
|
+
if "AndroidManifest.xml" in names or suffixes[".kt"] + suffixes[".java"] > 0 and any("gradle" in n for n in names):
|
|
97
|
+
frameworks.append("android")
|
|
98
|
+
if "module.json5" in names and suffixes[".ets"] > 0:
|
|
99
|
+
frameworks.append("harmonyos")
|
|
100
|
+
if "pubspec.yaml" in names and suffixes[".dart"] > 0:
|
|
101
|
+
frameworks.append("flutter")
|
|
102
|
+
if suffixes[".swift"] > 0 or any(r.endswith(".xcodeproj/project.pbxproj") for r in rels):
|
|
103
|
+
frameworks.append("ios")
|
|
104
|
+
package_files = [p for p in files if p.name == "package.json"]
|
|
105
|
+
package_text = ""
|
|
106
|
+
for package in package_files[:5]:
|
|
107
|
+
package_text += read_text(package, 300_000)
|
|
108
|
+
if "react-native" in package_text or suffixes[".tsx"] + suffixes[".jsx"] > 0:
|
|
109
|
+
frameworks.append("react-native")
|
|
110
|
+
if package_files and any(token in package_text for token in ("next", "nuxt", "vite", "react", "vue", "svelte")):
|
|
111
|
+
frameworks.append("web")
|
|
112
|
+
return frameworks or ["generic"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def iter_files(repo: Path, max_bytes: int) -> list[Path]:
|
|
116
|
+
result: list[Path] = []
|
|
117
|
+
for root, dirs, files in os.walk(repo):
|
|
118
|
+
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS and not d.startswith(".cache"))
|
|
119
|
+
for name in sorted(files):
|
|
120
|
+
path = Path(root) / name
|
|
121
|
+
if path.suffix.lower() not in TEXT_SUFFIXES and name not in SPECIAL_TEXT_NAMES:
|
|
122
|
+
continue
|
|
123
|
+
try:
|
|
124
|
+
if path.stat().st_size <= max_bytes:
|
|
125
|
+
result.append(path)
|
|
126
|
+
except OSError:
|
|
127
|
+
continue
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def is_secondary_locale(rel: str) -> bool:
|
|
132
|
+
"""Keep base and Chinese resources by default; suppress duplicate translations."""
|
|
133
|
+
normalized = "/" + rel.replace("\\", "/")
|
|
134
|
+
android_match = re.search(r"/res/(values-[^/]+)/", normalized)
|
|
135
|
+
if android_match and "zh" not in android_match.group(1).lower():
|
|
136
|
+
return True
|
|
137
|
+
harmony_match = re.search(r"/resources/([^/]+)/element/string\.json$", normalized)
|
|
138
|
+
if harmony_match:
|
|
139
|
+
locale = harmony_match.group(1).lower()
|
|
140
|
+
if locale != "base" and "zh" not in locale:
|
|
141
|
+
return True
|
|
142
|
+
name = Path(rel).name.lower()
|
|
143
|
+
if name.endswith(".arb") and not any(token in name for token in ("_en", "-en", "_zh", "-zh")):
|
|
144
|
+
return True
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def read_text(path: Path, max_bytes: int) -> str:
|
|
149
|
+
try:
|
|
150
|
+
return path.read_bytes()[:max_bytes].decode("utf-8", errors="replace")
|
|
151
|
+
except OSError:
|
|
152
|
+
return ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def source_role(rel: str) -> str:
|
|
156
|
+
parts = {part.lower() for part in Path(rel).parts}
|
|
157
|
+
if parts & TEST_PARTS or re.search(r"(?:Test|Spec)\.[^.]+$", rel):
|
|
158
|
+
return "test"
|
|
159
|
+
if parts & DOC_PARTS or Path(rel).suffix.lower() == ".md":
|
|
160
|
+
return "doc"
|
|
161
|
+
if any(part.lower() in {"sample", "samples", "demo", "preview", "catalog", "benchmark"} for part in Path(rel).parts):
|
|
162
|
+
return "non-production"
|
|
163
|
+
if "baseline" in Path(rel).name.lower() or "config/detekt/" in rel.lower() or "config/lint/" in rel.lower():
|
|
164
|
+
return "non-production"
|
|
165
|
+
return "production"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def line_number(text: str, offset: int) -> int:
|
|
169
|
+
return text.count("\n", 0, offset) + 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def clean_excerpt(value: str, limit: int = 360) -> str:
|
|
173
|
+
value = re.sub(r"\s+", " ", value).strip()
|
|
174
|
+
return value[:limit]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def record(repo_id: str, framework: str, kind: str, rel: str, line: int, role: str, excerpt: str, symbol: str = "") -> dict:
|
|
178
|
+
return {
|
|
179
|
+
"repo_id": repo_id,
|
|
180
|
+
"framework": framework,
|
|
181
|
+
"type": kind,
|
|
182
|
+
"path": rel,
|
|
183
|
+
"line": line,
|
|
184
|
+
"source_role": role,
|
|
185
|
+
"symbol": symbol,
|
|
186
|
+
"excerpt": clean_excerpt(excerpt),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def source_technology(path: Path, frameworks: list[str]) -> str | None:
|
|
191
|
+
suffix = path.suffix.lower()
|
|
192
|
+
mapping = {
|
|
193
|
+
".kt": "kotlin", ".kts": "kotlin", ".java": "java",
|
|
194
|
+
".c": "cpp", ".cc": "cpp", ".cpp": "cpp", ".cxx": "cpp", ".h": "cpp", ".hpp": "cpp",
|
|
195
|
+
".dart": "flutter", ".swift": "swift", ".m": "objective-c", ".mm": "objective-c",
|
|
196
|
+
".ets": "arkts", ".ts": "typescript", ".tsx": "typescript", ".js": "javascript", ".jsx": "javascript",
|
|
197
|
+
".html": "h5", ".vue": "h5", ".svelte": "h5",
|
|
198
|
+
}
|
|
199
|
+
technology = mapping.get(suffix)
|
|
200
|
+
if technology in {"typescript", "javascript"} and "react-native" in frameworks:
|
|
201
|
+
return "react-native"
|
|
202
|
+
return technology
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def extract_structured_labels(repo_id: str, framework: str, rel: str, role: str, text: str) -> list[dict]:
|
|
206
|
+
found: list[dict] = []
|
|
207
|
+
if rel.endswith(".xml"):
|
|
208
|
+
for match in re.finditer(r"<string\b[^>]*\bname=['\"]([^'\"]+)['\"][^>]*>(.*?)</string>", text, re.I | re.S):
|
|
209
|
+
value = re.sub(r"<[^>]+>", "", match.group(2))
|
|
210
|
+
found.append(record(repo_id, framework, "ui_label", rel, line_number(text, match.start()), role, value, match.group(1)))
|
|
211
|
+
for match in re.finditer(r"<(?:item|Preference\w*)\b.*?(?:android:)?title=['\"]([^'\"]+)['\"].*?>", text, re.I | re.S):
|
|
212
|
+
tag = match.group(0)
|
|
213
|
+
id_match = re.search(r"(?:android:)?id=['\"]([^'\"]+)", tag)
|
|
214
|
+
found.append(record(repo_id, framework, "menu_item", rel, line_number(text, match.start()), role, tag, id_match.group(1) if id_match else match.group(1)))
|
|
215
|
+
if rel.endswith("string.json"):
|
|
216
|
+
try:
|
|
217
|
+
parsed = json.loads(text)
|
|
218
|
+
for item in parsed.get("string", []):
|
|
219
|
+
if isinstance(item, dict) and isinstance(item.get("value"), str):
|
|
220
|
+
needle = json.dumps(item.get("name", ""), ensure_ascii=False)
|
|
221
|
+
offset = text.find(needle)
|
|
222
|
+
found.append(record(repo_id, framework, "ui_label", rel, line_number(text, max(offset, 0)), role, item["value"], str(item.get("name", ""))))
|
|
223
|
+
except json.JSONDecodeError:
|
|
224
|
+
pass
|
|
225
|
+
if rel.endswith((".strings", ".properties")) or "/locales/" in rel or "/i18n/" in rel:
|
|
226
|
+
for match in re.finditer(r"^\s*['\"]?([\w.:-]+)['\"]?\s*[=:]\s*['\"](.+?)['\"]\s*[,;]?\s*$", text, re.M):
|
|
227
|
+
found.append(record(repo_id, framework, "ui_label", rel, line_number(text, match.start()), role, match.group(2), match.group(1)))
|
|
228
|
+
return found
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def git_revision(repo: Path) -> tuple[str, bool | None]:
|
|
232
|
+
try:
|
|
233
|
+
revision = subprocess.run(["git", "-C", str(repo), "rev-parse", "HEAD"], check=True, capture_output=True, text=True, timeout=10).stdout.strip()
|
|
234
|
+
dirty = bool(subprocess.run(["git", "-C", str(repo), "status", "--porcelain"], check=True, capture_output=True, text=True, timeout=15).stdout.strip())
|
|
235
|
+
return revision, dirty
|
|
236
|
+
except (OSError, subprocess.SubprocessError):
|
|
237
|
+
return "working-tree", None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main() -> int:
|
|
241
|
+
args = parse_args()
|
|
242
|
+
repos = [parse_repo(value) for value in args.repo]
|
|
243
|
+
if len({repo_id for repo_id, _ in repos}) != len(repos):
|
|
244
|
+
raise ValueError("repository IDs must be unique")
|
|
245
|
+
args.output.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
|
|
247
|
+
all_records: list[dict] = []
|
|
248
|
+
summaries: list[dict] = []
|
|
249
|
+
candidate_scores: dict[tuple[str, str], Counter] = defaultdict(Counter)
|
|
250
|
+
|
|
251
|
+
for repo_id, repo in repos:
|
|
252
|
+
files = iter_files(repo, args.max_file_bytes)
|
|
253
|
+
frameworks = detect_frameworks(repo, files)
|
|
254
|
+
counts = Counter()
|
|
255
|
+
for path in files:
|
|
256
|
+
rel = path.relative_to(repo).as_posix()
|
|
257
|
+
if not args.all_locales and is_secondary_locale(rel):
|
|
258
|
+
continue
|
|
259
|
+
role = source_role(rel)
|
|
260
|
+
if args.exclude_tests and role == "test":
|
|
261
|
+
continue
|
|
262
|
+
text = read_text(path, args.max_file_bytes)
|
|
263
|
+
if not text:
|
|
264
|
+
continue
|
|
265
|
+
file_records = extract_structured_labels(repo_id, frameworks[0], rel, role, text)
|
|
266
|
+
for kind, framework, regex in COMPILED_PATTERNS:
|
|
267
|
+
if framework != "generic" and framework not in frameworks:
|
|
268
|
+
continue
|
|
269
|
+
for match in regex.finditer(text):
|
|
270
|
+
start = text.rfind("\n", 0, match.start()) + 1
|
|
271
|
+
end = text.find("\n", match.end())
|
|
272
|
+
if end == -1:
|
|
273
|
+
end = len(text)
|
|
274
|
+
excerpt = text[start:end].strip()
|
|
275
|
+
if excerpt.startswith(("//", "<!--", "*", "#")):
|
|
276
|
+
continue
|
|
277
|
+
file_records.append(record(repo_id, framework, kind, rel, line_number(text, match.start()), role, excerpt))
|
|
278
|
+
if len(file_records) >= args.max_records_per_file:
|
|
279
|
+
break
|
|
280
|
+
if len(file_records) >= args.max_records_per_file:
|
|
281
|
+
break
|
|
282
|
+
strong_types = {"entry", "route_declaration", "navigation_call", "ui_interaction", "menu_item", "tab", "dialog", "effect"}
|
|
283
|
+
technology = source_technology(path, frameworks)
|
|
284
|
+
if technology and any(item["type"] in strong_types for item in file_records):
|
|
285
|
+
file_records.append(record(repo_id, frameworks[0], "tech_stack", rel, 1, role, f"source language/framework: {technology}", technology))
|
|
286
|
+
unique = {}
|
|
287
|
+
for item in file_records[: args.max_records_per_file]:
|
|
288
|
+
key = (item["type"], item["line"], item["symbol"], item["excerpt"])
|
|
289
|
+
unique[key] = item
|
|
290
|
+
for item in unique.values():
|
|
291
|
+
all_records.append(item)
|
|
292
|
+
counts[item["type"]] += 1
|
|
293
|
+
weight = {
|
|
294
|
+
"entry": 8, "route_declaration": 7, "navigation_call": 7,
|
|
295
|
+
"menu_item": 6, "ui_interaction": 5, "tab": 5,
|
|
296
|
+
"dialog": 4, "effect": 1, "visibility_condition": 1,
|
|
297
|
+
"ui_label": 0, "tech_stack": 1, "dependency_declaration": 2,
|
|
298
|
+
"native_library": 3, "permission_declaration": 2, "permission_request": 3,
|
|
299
|
+
}.get(item["type"], 1)
|
|
300
|
+
if role == "production":
|
|
301
|
+
candidate_scores[(repo_id, rel)]["score"] += weight
|
|
302
|
+
candidate_scores[(repo_id, rel)][item["type"]] += 1
|
|
303
|
+
revision, dirty = git_revision(repo)
|
|
304
|
+
fingerprint = hashlib.sha256("\n".join(sorted(p.relative_to(repo).as_posix() for p in files)).encode()).hexdigest()[:16]
|
|
305
|
+
summaries.append({
|
|
306
|
+
"id": repo_id,
|
|
307
|
+
"path": str(repo),
|
|
308
|
+
"frameworks": frameworks,
|
|
309
|
+
"revision": revision,
|
|
310
|
+
"dirty": dirty,
|
|
311
|
+
"scanned_files": len(files),
|
|
312
|
+
"file_list_fingerprint": fingerprint,
|
|
313
|
+
"evidence_counts": dict(sorted(counts.items())),
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
all_records.sort(key=lambda item: (item["repo_id"], item["path"], item["line"], item["type"]))
|
|
317
|
+
with (args.output / "evidence.jsonl").open("w", encoding="utf-8") as handle:
|
|
318
|
+
for item in all_records:
|
|
319
|
+
handle.write(json.dumps(item, ensure_ascii=False) + "\n")
|
|
320
|
+
(args.output / "scan-summary.json").write_text(json.dumps({"schema_version": "1.1", "repositories": summaries, "evidence_records": len(all_records)}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
321
|
+
|
|
322
|
+
header = ["repo_id", "score", "path", "entry", "route", "navigation", "interaction", "menu", "label", "effect"]
|
|
323
|
+
rows = ["\t".join(header)]
|
|
324
|
+
for (repo_id, rel), counter in sorted(candidate_scores.items(), key=lambda item: (-item[1]["score"], item[0])):
|
|
325
|
+
strong = sum(counter[kind] for kind in ("entry", "route_declaration", "navigation_call", "ui_interaction", "menu_item", "tab", "dialog"))
|
|
326
|
+
if counter["score"] <= 0 or strong <= 0:
|
|
327
|
+
continue
|
|
328
|
+
rows.append("\t".join(map(str, [
|
|
329
|
+
repo_id, counter["score"], rel, counter["entry"], counter["route_declaration"],
|
|
330
|
+
counter["navigation_call"], counter["ui_interaction"], counter["menu_item"],
|
|
331
|
+
counter["ui_label"], counter["effect"],
|
|
332
|
+
])))
|
|
333
|
+
(args.output / "candidate-files.tsv").write_text("\n".join(rows) + "\n", encoding="utf-8")
|
|
334
|
+
technical_header = ["repo_id", "score", "path", "tech_stack", "dependency", "native_library", "permission_declaration", "permission_request"]
|
|
335
|
+
technical_rows = ["\t".join(technical_header)]
|
|
336
|
+
for (repo_id, rel), counter in sorted(candidate_scores.items(), key=lambda item: (-item[1]["score"], item[0])):
|
|
337
|
+
technical_count = sum(counter[kind] for kind in ("tech_stack", "dependency_declaration", "native_library", "permission_declaration", "permission_request"))
|
|
338
|
+
if technical_count <= 0:
|
|
339
|
+
continue
|
|
340
|
+
technical_rows.append("\t".join(map(str, [
|
|
341
|
+
repo_id, counter["score"], rel, counter["tech_stack"], counter["dependency_declaration"],
|
|
342
|
+
counter["native_library"], counter["permission_declaration"], counter["permission_request"],
|
|
343
|
+
])))
|
|
344
|
+
(args.output / "technical-files.tsv").write_text("\n".join(technical_rows) + "\n", encoding="utf-8")
|
|
345
|
+
print(json.dumps({"output": str(args.output.resolve()), "repositories": len(repos), "evidence_records": len(all_records)}, ensure_ascii=False))
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
raise SystemExit(main())
|