@harmonyos-arkts/d2h 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -0
  3. package/agents/android-to-hmos-00-orchestrator.md +3 -315
  4. package/agents/d2h.md +53 -38
  5. package/contracts/workflow/artifacts.md +22 -0
  6. package/contracts/workflow/delivery.md +54 -0
  7. package/contracts/workflow/dispatch.md +26 -0
  8. package/contracts/workflow/execution.md +125 -0
  9. package/contracts/workflow/generation.md +40 -0
  10. package/contracts/workflow/orchestration.md +49 -0
  11. package/contracts/workflow/overview.md +58 -0
  12. package/contracts/workflow/plan.md +119 -0
  13. package/contracts/workflow/scope.md +151 -0
  14. package/dist/bin/install-opencode.js +34 -15
  15. package/dist/src/install-codex.js +131 -0
  16. package/dist/src/install-opencode.js +5 -5
  17. package/dist/src/workflow.js +54 -0
  18. package/docs/architecture-plan-guide.zh-CN.md +70 -0
  19. package/docs/incremental-spec-evaluation-design.md +670 -0
  20. package/docs/spec-plan-guide.md +226 -0
  21. package/docs/workflow-decoupling.md +59 -0
  22. package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
  23. package/opencode/interactive-tools/browser-host.ts +110 -0
  24. package/opencode/interactive-tools/core.ts +242 -0
  25. package/opencode/interactive-tools/demo-workflow.ts +55 -0
  26. package/opencode/interactive-tools/demo.mjs +37 -0
  27. package/opencode/interactive-tools/package-lock.json +1876 -0
  28. package/opencode/interactive-tools/package.json +23 -0
  29. package/opencode/interactive-tools/panel.ts +40 -0
  30. package/opencode/interactive-tools/prepare-demo.mjs +17 -0
  31. package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
  32. package/opencode/interactive-tools/progress.ts +118 -0
  33. package/opencode/interactive-tools/server-legacy.mjs +2 -0
  34. package/opencode/interactive-tools/server.mjs +40 -0
  35. package/opencode/interactive-tools/tui.tsx +230 -0
  36. package/package.json +9 -1
  37. package/schemas/native-libs-analysis-report.schema.json +695 -0
  38. package/schemas/native-libs-verify-report.schema.json +566 -0
  39. package/schemas/spec-native-libs-shard.schema.json +110 -0
  40. package/skills/arkts-icon-sizing/SKILL.md +0 -10
  41. package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
  42. package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
  43. package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
  44. package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
  45. package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
  46. package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
  47. package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
  48. package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
  49. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
  50. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
  51. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
  52. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
  53. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
  54. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
  55. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
  56. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
  57. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
  58. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
  59. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
  60. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
  61. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
  62. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
  63. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
  64. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
  65. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
  66. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
  67. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
  68. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
  69. package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
  70. package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
  71. package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
  72. package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
  73. package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
  74. package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
  75. package/skills/d2h-app-solution/SKILL.md +198 -0
  76. package/skills/d2h-app-solution/agents/openai.yaml +4 -0
  77. package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
  78. package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
  79. package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
  80. package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
  81. package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
  82. package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
  83. package/skills/d2h-architecture-plan/SKILL.md +35 -0
  84. package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
  85. package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
  86. package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
  87. package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
  88. package/skills/d2h-architecture-plan/references/source-review.md +50 -0
  89. package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
  90. package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
  91. package/skills/d2h-dashboard/SKILL.md +56 -0
  92. package/skills/d2h-product-family-migration/SKILL.md +103 -0
  93. package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
  94. package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
  95. package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
  96. package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
  97. package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
  98. package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
  99. package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
  100. package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
  101. package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
  102. package/skills/d2h-spec-develop/SKILL.md +41 -0
  103. package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
  104. package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
  105. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
  106. package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
  107. package/skills/d2h-spec-generate/references/from-plan.md +19 -0
  108. package/skills/d2h-spec-generate/references/spec-content.md +34 -0
  109. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
  110. package/skills/d2h-spec-plan/SKILL.md +38 -0
  111. package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
  112. package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
  113. package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
  114. package/skills/d2h-spec-plan/references/planning-method.md +46 -0
  115. package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
  116. package/skills/d2h-spec-plan/references/work-packets.md +61 -0
  117. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
  118. package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
  119. package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
  120. package/skills/d2h-utg-static-generate/SKILL.md +2 -2
  121. package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
  122. package/skills/managing-d2h-toolchain/toolchain.json +1 -1
  123. package/skills/preparing-migration-workspace/SKILL.md +32 -2
  124. package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
  125. package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
  126. package/workflow/artifact_bindings.py +224 -0
  127. package/workflow/assets/dashboard.html +88 -0
  128. package/workflow/build_batch_report.py +8 -0
  129. package/workflow/collect_code_metrics.py +75 -0
  130. package/workflow/dashboard_overview.py +109 -0
  131. package/workflow/delivery_view.py +82 -0
  132. package/workflow/feature_plan.py +234 -0
  133. package/workflow/page_batches.py +223 -0
  134. package/workflow/spec_generation.py +120 -0
  135. package/workflow/spec_runtime.py +890 -0
  136. package/workflow/work_packets.py +85 -0
  137. package/skills/d2h-page-develop/SKILL.md +0 -126
  138. package/skills/d2h-page-develop/agents/openai.yaml +0 -7
  139. package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
  140. package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
  141. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
  142. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
  143. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
  144. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
  145. package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
  146. package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
  147. package/skills/native-library-substitution/SKILL.md +0 -385
  148. package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
  149. package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
  150. package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
  151. package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
  152. 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())