android2harmony 0.1.6 → 0.1.8
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/agents/api-analyzer.md +113 -0
- package/agents/app-action.md +122 -0
- package/agents/code-reviewer.md +191 -295
- package/agents/coder.md +377 -0
- package/agents/self-tester.md +8 -8
- package/agents/spec-designer.md +56 -0
- package/package.json +1 -1
- package/skills/a2h-app-req-gen/SKILL.md +174 -0
- package/skills/a2h-app-req-gen/references/spec-handoff.md +99 -0
- package/skills/a2h-app-req-gen/references/subagent-render.md +115 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/build.gradle.kts +41 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle.properties +3 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew +251 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew.bat +94 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/settings.gradle.kts +17 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawAnalysisDetector.java +1911 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawIssueRegistry.java +37 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawJson.java +146 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawLocation.java +96 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawRecordWriter.java +227 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/dev/reqgen/analyzer/jvm/raw-record-v1.schema.json +93 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/assets/dashboard.html +151 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-lint.init.gradle +60 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-model.init.gradle +204 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/package.json +14 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/backend.js +323 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/cli.js +811 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/gap-audit-link.js +62 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/index.js +66 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/merge.js +605 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/name-trace.js +153 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/orchestrate.js +1324 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/output-parse.js +50 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/overview.js +10 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/pool.js +23 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/post-merge-fix.js +146 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt-loader.js +51 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt.js +967 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompts.md +301 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/serve.js +78 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/short-id.js +46 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/snapshot.js +267 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/validate.js +122 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/callback-call-sites.js +62 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/build.js +1291 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/area-ownership.js +392 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/build.js +1995 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/callback-flow.js +277 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/component-variants.js +725 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/index.js +15 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/index.js +12 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/policy.js +469 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/traceability.js +468 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/build.js +1066 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android-interaction-registry.js +174 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android.js +1062 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/area-titles.js +678 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/builder.js +350 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-api-registry.js +395 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-call-sites.js +2008 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose.js +9059 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/dynamic-repeated-call-sites.js +529 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/interaction-effects.js +1149 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/jvm.js +6272 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/normalize.js +33 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/pending-intents.js +565 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/resource-id.js +42 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/route-state-variants.js +781 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/state.js +234 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/extract.js +40 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/index.js +6 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/manifest.js +513 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/resources.js +398 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/views.js +447 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/xml-parser.js +175 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/cli.js +398 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/area-graph.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/artifact.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/common.js +16 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/diagnostic.js +10 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/gap-audit.js +14 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/index.js +11 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/name-traceability.js +26 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/raw-fact.js +40 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/req-model.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/run.js +19 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/semantic-completeness.js +11 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/source-facts.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/core/output-guard.js +41 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/core/stable-id.js +92 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dashboard-cli.js +169 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/diagnostics.js +104 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/dump.js +1028 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dump-cli.js +251 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/artifacts.js +49 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/index.js +18 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/quality-report.js +125 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/render.js +353 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/types.js +2 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/graph/paths.js +474 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/index.js +15 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/protocol.js +408 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/runner.js +452 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/index.js +8 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/output.js +165 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/run-stage1.js +891 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/discovery.js +299 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-model.js +358 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-runner.js +165 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/resource-profile.js +60 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff-cli.js +151 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff.js +1004 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-cli.js +109 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-input.js +419 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-name-trace-input.js +95 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/area-graph.js +444 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/index.js +7 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-model.js +580 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-text.js +195 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/semantic-area-graph.js +1431 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen-audit.mjs +298 -0
- package/skills/a2h-app-req-gen/scripts/reqgen-selfcheck.mjs +555 -0
- package/skills/a2h-code-review/SKILL.md +379 -0
- package/skills/{hmos-integration-test → a2h-integration-test}/README.md +6 -6
- package/skills/{hmos-integration-test → a2h-integration-test}/SKILL.md +6 -6
- package/skills/{hmos-integration-test → a2h-integration-test}/scripts/report-tool.mjs +1 -1
- package/skills/a2h-spec-design/SKILL.md +330 -0
- package/skills/a2h-spec-design/scripts/inspect_spec_design_inputs.mjs +207 -0
- package/skills/a2h-spec-design/scripts/move_spec_technical_reference.mjs +198 -0
- package/skills/a2h-spec-design/scripts/validate_spec_design_output.mjs +256 -0
- package/skills/a2h-spec-generate/SKILL.md +696 -0
- package/skills/a2h-spec-generate/references/trace-template.md +115 -0
- package/skills/a2h-spec-generate/scripts/parse_requirements.mjs +537 -0
- package/skills/a2h-spec-generate/template/REQ.txt +22 -0
- package/skills/a2h-spec-generate/template/REQ.xlsx +0 -0
- package/skills/a2h-spec-implement/SKILL.md +548 -0
- package/skills/a2h-spec-implement/references/harmony-pitfalls.md +186 -0
- package/skills/a2h-spec-implement/references/l1-unit-test.md +185 -0
- package/skills/a2h-spec-implement/references/l3-e2e-uitest.md +167 -0
- package/skills/a2h-spec-implement/references/test-core.md +132 -0
- package/skills/a2h-spec-implement/rules/arkts/arkts-standards.md +184 -0
- package/skills/a2h-spec-implement/rules/arkts/conventions/coding-style.md +96 -0
- package/skills/a2h-spec-implement/rules/arkts/conventions/security.md +126 -0
- package/skills/a2h-spec-implement/rules/arkts/language/arkts-rules.md +650 -0
- package/skills/a2h-spec-implement/rules/arkts/language/arkui-structure-rules.md +173 -0
- package/skills/a2h-spec-implement/rules/arkts/language/ts-to-arkts-rewrites.md +58 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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/a2h-spec-implement/rules/arkts/official/mvvm-v1/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/a2h-spec-implement/rules/arkts/official/mvvm-v1/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/a2h-spec-implement/rules/arkts/official/mvvm-v1/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/a2h-spec-implement/rules/arkts/official/mvvm-v1/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/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/a2h-spec-implement/rules/arkts/official/mvvm-v1//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/a2h-spec-implement/rules/arkts/official/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md +216 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md +442 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md +169 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md +763 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md +2088 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md +1258 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Once/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md +175 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Param/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md +850 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md +862 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md +110 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md +301 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md +1411 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/PersistenceV2/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 +1392 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/getTarget/346/216/245/345/217/243/357/274/232/350/216/267/345/217/226/347/212/266/346/200/201/347/256/241/347/220/206/346/241/206/346/236/266/344/273/243/347/220/206/345/211/215/347/232/204/345/216/237/345/247/213/345/257/271/350/261/241.md +288 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/makeObserved/346/216/245/345/217/243/357/274/232/345/260/206/351/235/236/350/247/202/345/257/237/346/225/260/346/215/256/345/217/230/344/270/272/345/217/257/350/247/202/345/257/237/346/225/260/346/215/256.md +768 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/222/214V2/346/267/267/347/224/250/346/214/207/345/257/274/357/274/210API version 19/345/217/212/344/271/213/345/220/216/357/274/211.md" +829 -0
- package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206/346/246/202/350/277/260.md +184 -0
- package/skills/a2h-spec-implement/rules/arkts/ui/component-cookbook.md +431 -0
- package/skills/a2h-spec-implement/rules/arkts/ui/state-management.md +152 -0
- package/skills/a2h-spec-implement/rules/arkts/ui/ui-quality.md +67 -0
- package/skills/a2h-spec-implement/rules/arkts/ui/ui-runtime-diagnosis.md +32 -0
- package/skills/a2h-spec-implement/rules/pipeline/build/package-set-collection.md +43 -0
- package/skills/a2h-spec-implement/scripts/_lib/common.mjs +97 -0
- package/skills/a2h-spec-implement/scripts/_lib/config.mjs +70 -0
- package/skills/a2h-spec-implement/scripts/_lib/lessons_cli.mjs +48 -0
- package/skills/a2h-spec-implement/scripts/build_lessons.mjs +24 -0
- package/skills/a2h-spec-implement/scripts/code_pattern_lessons.mjs +26 -0
- package/skills/a2h-spec-implement/scripts/device_ui.mjs +433 -0
- package/skills/a2h-spec-implement/scripts/device_ui_core.mjs +895 -0
- package/skills/a2h-spec-implement/scripts/ledger/aggregate_run.mjs +207 -0
- package/skills/a2h-spec-implement/scripts/manifest/emit_summary_line.mjs +37 -0
- package/skills/a2h-spec-implement/scripts/manifest/finalize_run.mjs +115 -0
- package/skills/a2h-spec-implement/scripts/manifest/render_final_summary.mjs +78 -0
- package/skills/a2h-spec-implement/scripts/manifest/root_mirror.mjs +137 -0
- package/skills/a2h-spec-implement/scripts/package.json +12 -0
- package/skills/a2h-spec-implement/scripts/state_propagation_check.mjs +676 -0
- package/skills/a2h-spec-implement/scripts/test/l2_run.mjs +244 -0
- package/skills/a2h-spec-implement/scripts/test/tp_coverage.mjs +231 -0
- package/skills/a2h-spec-implement/scripts/ui_memory.mjs +161 -0
- package/skills/a2h-spec-implement/scripts/utils/ensure_app.mjs +134 -0
- package/skills/a2h-ui-transfer/SKILL.md +20 -9
- package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +80 -0
- package/skills/hmos-convert-pipeline/SKILL.md +7 -5
- package/skills/hmos-incremental-ui-align/README.md +7 -7
- package/skills/hmos-incremental-ui-align/SKILL.md +1 -1
- package/skills/hmos-incremental-ui-align/page_align.md +1 -1
- package/skills/hmos-spec-generate/SKILL.md +2 -2
- package/skills/hmos-spec-generate/scripts/parse_requirements.mjs +537 -0
- package/skills/hmos-fix-build-errors/SKILL.md +0 -266
- package/skills/hmos-fix-build-errors/references/arkts-strict-patterns.md +0 -219
- package/skills/hmos-fix-build-errors/references/known-patterns.md +0 -157
- package/skills/hmos-fix-build-errors/references/rdb-entity-pattern.md +0 -131
- package/skills/hmos-spec-generate/scripts/parse_requirements.ts +0 -515
- /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/resolve-metadata-tool.mjs +0 -0
- /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/self-test-runner.mjs +0 -0
- /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/testcases-tool.mjs +0 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
// L0 state-propagation checker — the four ArkUI state pitfalls that compile clean, pass
|
|
2
|
+
// codelinter, and still fail on device as "value stored, screen never changes".
|
|
3
|
+
//
|
|
4
|
+
// Report shape is codelinter-compatible on purpose, so the L0 gate consumes it exactly like
|
|
5
|
+
// lint-report.json: [{ filePath, messages: [{ line, column, severity, message, rule }] }]
|
|
6
|
+
// severity=error → blocking (structural pattern, high precision)
|
|
7
|
+
// severity=warn → suspect (needs one human judgement call)
|
|
8
|
+
//
|
|
9
|
+
// P1 builder-value-param @Builder taking state by value never re-renders its body
|
|
10
|
+
// P2 controller-vs-callback on*Change overwrites what a controller setter just set
|
|
11
|
+
// P3 async-only-state a state member is written only in callback / post-await paths
|
|
12
|
+
// P5 foreach-key-omits-state keyGenerator misses the state that changes the item. Counts state
|
|
13
|
+
// reached through a helper the item body calls, and blocks (error)
|
|
14
|
+
// when the key is identity-only AND the omitted state is written.
|
|
15
|
+
//
|
|
16
|
+
// Usage: node state_propagation_check.mjs --dir <project-or-dir> [--out <report.json>]
|
|
17
|
+
// [--files a.ets,b.ets] [--all-error]
|
|
18
|
+
// Exit 0 = no blocking finding, 1 = blocking finding, 2 = bad invocation. Zero deps.
|
|
19
|
+
//
|
|
20
|
+
// Criteria and the fix for each pattern:
|
|
21
|
+
// ../rules/arkts/ui/state-management.md § State written but never rendered
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { emit } from "./_lib/common.mjs";
|
|
25
|
+
|
|
26
|
+
const STATE_DECORATORS = new Set([
|
|
27
|
+
"State", "StorageLink", "StorageProp", "Prop", "Link", "Local", "Param",
|
|
28
|
+
"Provide", "Consume", "ObjectLink", "Computed",
|
|
29
|
+
]);
|
|
30
|
+
const PRIMITIVE = /^(string|number|boolean|Resource)$/;
|
|
31
|
+
const KEYWORD = new Set([
|
|
32
|
+
"if", "for", "while", "switch", "catch", "return", "typeof", "new", "await", "function",
|
|
33
|
+
"constructor", "super", "else", "do", "try", "case", "delete", "void", "in", "of",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// --- blank comments and string bodies (length + newlines preserved) so scans never match
|
|
37
|
+
// inside them. Template `${...}` expressions stay live code — P5 must read them.
|
|
38
|
+
function blankNonCode(s) {
|
|
39
|
+
const out = s.split("");
|
|
40
|
+
const stack = [];
|
|
41
|
+
let i = 0, mode = "code", quote = "";
|
|
42
|
+
const blank = (k) => { if (out[k] !== "\n" && out[k] !== "\r") out[k] = " "; };
|
|
43
|
+
while (i < s.length) {
|
|
44
|
+
const c = s[i], d = s[i + 1];
|
|
45
|
+
if (mode === "code") {
|
|
46
|
+
if (c === "/" && d === "/") { blank(i); blank(i + 1); mode = "line"; i += 2; continue; }
|
|
47
|
+
if (c === "/" && d === "*") { blank(i); blank(i + 1); mode = "block"; i += 2; continue; }
|
|
48
|
+
if (c === "'" || c === '"') { quote = c; mode = "str"; i++; continue; }
|
|
49
|
+
if (c === "`") { stack.push("t"); mode = "tmpl"; i++; continue; }
|
|
50
|
+
if (c === "}" && stack.length && stack[stack.length - 1] === "e") { stack.pop(); mode = "tmpl"; i++; continue; }
|
|
51
|
+
i++; continue;
|
|
52
|
+
}
|
|
53
|
+
if (mode === "line") { if (c === "\n") { mode = "code"; i++; continue; } blank(i); i++; continue; }
|
|
54
|
+
if (mode === "block") {
|
|
55
|
+
if (c === "*" && d === "/") { blank(i); blank(i + 1); mode = "code"; i += 2; continue; }
|
|
56
|
+
blank(i); i++; continue;
|
|
57
|
+
}
|
|
58
|
+
if (mode === "str") {
|
|
59
|
+
if (c === "\\") { blank(i); blank(i + 1); i += 2; continue; }
|
|
60
|
+
if (c === quote) { mode = "code"; i++; continue; }
|
|
61
|
+
blank(i); i++; continue;
|
|
62
|
+
}
|
|
63
|
+
// tmpl
|
|
64
|
+
if (c === "\\") { blank(i); blank(i + 1); i += 2; continue; }
|
|
65
|
+
if (c === "$" && d === "{") { stack.push("e"); mode = "code"; i += 2; continue; }
|
|
66
|
+
if (c === "`") { stack.pop(); mode = "code"; i++; continue; }
|
|
67
|
+
blank(i); i++;
|
|
68
|
+
}
|
|
69
|
+
return out.join("");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function lineStarts(src) {
|
|
73
|
+
const a = [0];
|
|
74
|
+
for (let i = 0; i < src.length; i++) if (src[i] === "\n") a.push(i + 1);
|
|
75
|
+
return a;
|
|
76
|
+
}
|
|
77
|
+
function posOf(starts, idx) {
|
|
78
|
+
let lo = 0, hi = starts.length - 1;
|
|
79
|
+
while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid] <= idx) lo = mid; else hi = mid - 1; }
|
|
80
|
+
return { line: lo + 1, column: idx - starts[lo] + 1 };
|
|
81
|
+
}
|
|
82
|
+
function matchPair(code, i, open, close) {
|
|
83
|
+
let depth = 0;
|
|
84
|
+
for (let k = i; k < code.length; k++) {
|
|
85
|
+
if (code[k] === open) depth++;
|
|
86
|
+
else if (code[k] === close) { depth--; if (depth === 0) return k; }
|
|
87
|
+
}
|
|
88
|
+
return -1;
|
|
89
|
+
}
|
|
90
|
+
function splitArgs(text) {
|
|
91
|
+
const args = []; let depth = 0, cur = "";
|
|
92
|
+
for (const ch of text) {
|
|
93
|
+
if ("([{".includes(ch)) depth++;
|
|
94
|
+
else if (")]}".includes(ch)) depth--;
|
|
95
|
+
if (ch === "," && depth === 0) { args.push(cur.trim()); cur = ""; continue; }
|
|
96
|
+
cur += ch;
|
|
97
|
+
}
|
|
98
|
+
if (cur.trim()) args.push(cur.trim());
|
|
99
|
+
return args;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findStructs(code) {
|
|
103
|
+
const out = [];
|
|
104
|
+
const re = /\bstruct\s+(\w+)[^{]*\{/g;
|
|
105
|
+
let m;
|
|
106
|
+
while ((m = re.exec(code))) {
|
|
107
|
+
const open = code.indexOf("{", m.index);
|
|
108
|
+
const end = matchPair(code, open, "{", "}");
|
|
109
|
+
if (end < 0) continue;
|
|
110
|
+
out.push({ name: m[1], start: open + 1, end });
|
|
111
|
+
re.lastIndex = open + 1;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findMembers(code, start, end) {
|
|
117
|
+
const body = code.slice(start, end);
|
|
118
|
+
const members = new Map();
|
|
119
|
+
const re = /@(\w+)\s*(?:\([^)]*\))?\s*(?:(?:private|public|protected|readonly)\s+)*(\w+)\s*[?!]?\s*(?:=|:\s*([\w.<>\[\]|]+))/g;
|
|
120
|
+
let m;
|
|
121
|
+
while ((m = re.exec(body))) {
|
|
122
|
+
if (!STATE_DECORATORS.has(m[1])) continue;
|
|
123
|
+
if (!members.has(m[2])) members.set(m[2], { decorator: m[1], type: m[3] || "", idx: start + m.index });
|
|
124
|
+
}
|
|
125
|
+
return members;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Method bodies inside a struct, each tagged with whether @Builder precedes it.
|
|
129
|
+
function findMethods(code, start, end) {
|
|
130
|
+
const out = [];
|
|
131
|
+
const re = /(?:^|[\s;}])((?:private|public|protected|static|async)\s+)*(\w+)\s*\(/g;
|
|
132
|
+
re.lastIndex = start;
|
|
133
|
+
let m;
|
|
134
|
+
while ((m = re.exec(code)) && m.index < end) {
|
|
135
|
+
const name = m[2];
|
|
136
|
+
const paren = code.indexOf("(", m.index + (m[1] ? m[1].length : 0));
|
|
137
|
+
if (KEYWORD.has(name) || paren < 0 || paren > end) { continue; }
|
|
138
|
+
const pClose = matchPair(code, paren, "(", ")");
|
|
139
|
+
if (pClose < 0) continue;
|
|
140
|
+
let k = pClose + 1;
|
|
141
|
+
while (k < end && /\s/.test(code[k])) k++;
|
|
142
|
+
if (code[k] === ":") { // skip a return-type annotation
|
|
143
|
+
const brace = code.indexOf("{", k);
|
|
144
|
+
const semi = code.indexOf(";", k);
|
|
145
|
+
if (brace < 0 || (semi >= 0 && semi < brace)) continue;
|
|
146
|
+
k = brace;
|
|
147
|
+
}
|
|
148
|
+
if (code[k] !== "{") continue;
|
|
149
|
+
const bClose = matchPair(code, k, "{", "}");
|
|
150
|
+
if (bClose < 0) continue;
|
|
151
|
+
const before = code.slice(Math.max(start, m.index - 300), m.index);
|
|
152
|
+
out.push({
|
|
153
|
+
name,
|
|
154
|
+
isBuilder: /@Builder\s*$/.test(before),
|
|
155
|
+
params: splitArgs(code.slice(paren + 1, pClose)),
|
|
156
|
+
bodyStart: k + 1, bodyEnd: bClose, idx: m.index,
|
|
157
|
+
});
|
|
158
|
+
re.lastIndex = bClose;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Regions that are callback bodies. `kind` distinguishes the two families, which matter for
|
|
164
|
+
// different checks:
|
|
165
|
+
// ui — ArkUI attribute callbacks (.onClick/.onChange/.onTextSelectionChange). A user tap
|
|
166
|
+
// always arrives, so these are NOT a liveness risk (P3 ignores them), but they DO
|
|
167
|
+
// overwrite state on the next frame (P2 needs exactly these).
|
|
168
|
+
// engine — an engine/service subscription: `.on('stateChange', …)` with a string event name,
|
|
169
|
+
// or `.then/.catch`. These may never fire, which is the P3 defect.
|
|
170
|
+
function callbackRegions(code, start, end) {
|
|
171
|
+
const regions = [];
|
|
172
|
+
const re = /\.\s*(on\w*|then|catch|finally)\s*\(/g;
|
|
173
|
+
re.lastIndex = start;
|
|
174
|
+
let m;
|
|
175
|
+
while ((m = re.exec(code)) && m.index < end) {
|
|
176
|
+
const paren = code.indexOf("(", m.index);
|
|
177
|
+
const close = matchPair(code, paren, "(", ")");
|
|
178
|
+
if (close < 0) { re.lastIndex = paren + 1; continue; }
|
|
179
|
+
const name = m[1];
|
|
180
|
+
// `.on('event', cb)` — a string first argument means an engine/service subscription,
|
|
181
|
+
// not an ArkUI attribute callback.
|
|
182
|
+
const subscribes = name === "on" || /^(then|catch|finally)$/.test(name)
|
|
183
|
+
|| /^\s*['"]/.test(code.slice(paren + 1, paren + 3));
|
|
184
|
+
regions.push({ a: paren, b: close, kind: name, family: subscribes ? "engine" : "ui" });
|
|
185
|
+
re.lastIndex = paren + 1;
|
|
186
|
+
}
|
|
187
|
+
return regions;
|
|
188
|
+
}
|
|
189
|
+
const inRegions = (rs, i) => rs.some((r) => i > r.a && i < r.b);
|
|
190
|
+
|
|
191
|
+
// Callback bodies in `text` between `from` and `to`, covering BOTH spellings. `callbackRegions`
|
|
192
|
+
// only sees dot-calls (`.onClick(() => …)`); a handler handed to a child component is written as
|
|
193
|
+
// an object-literal property instead (`onTap: () => { … }`), which is not a dot-call at all. Both
|
|
194
|
+
// must be excluded wherever "is this value read during render, or only on interaction?" is the
|
|
195
|
+
// question — a field read inside a handler runs on tap, against the then-current value.
|
|
196
|
+
function allCallbackRegions(text, from, to) {
|
|
197
|
+
const cbs = callbackRegions(text, from, to);
|
|
198
|
+
const arrowRe = /(\w+)\s*:\s*(?:async\s+)?\(([^)]*)\)\s*=>\s*/g;
|
|
199
|
+
arrowRe.lastIndex = from;
|
|
200
|
+
let a;
|
|
201
|
+
while ((a = arrowRe.exec(text)) && a.index < to) {
|
|
202
|
+
const after = arrowRe.lastIndex;
|
|
203
|
+
const brace = text.indexOf("{", after);
|
|
204
|
+
let bodyEnd;
|
|
205
|
+
if (brace >= 0 && brace <= after + 1) {
|
|
206
|
+
bodyEnd = matchPair(text, brace, "{", "}");
|
|
207
|
+
} else {
|
|
208
|
+
// Expression-bodied arrow: runs to the next top-level comma or the enclosing close.
|
|
209
|
+
let d = 0;
|
|
210
|
+
let i = after;
|
|
211
|
+
for (; i < to; i++) {
|
|
212
|
+
const ch = text[i];
|
|
213
|
+
if ("([{".includes(ch)) d++;
|
|
214
|
+
else if (")]}".includes(ch)) d--;
|
|
215
|
+
else if (ch === "," && d === 0) break;
|
|
216
|
+
}
|
|
217
|
+
bodyEnd = i;
|
|
218
|
+
}
|
|
219
|
+
if (bodyEnd > after) cbs.push({ a: after, b: bodyEnd, kind: a[1], family: "ui" });
|
|
220
|
+
}
|
|
221
|
+
return cbs;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// A method named on<Something> that nothing inside the struct ever calls as `this.onX(` is an
|
|
225
|
+
// externally-invoked callback (registered with an engine / service). Writes inside it are as
|
|
226
|
+
// deferred as writes inside an inline `.on(...)` closure — P3 must count them the same way.
|
|
227
|
+
function externalCallbackMethods(code, start, end, methods) {
|
|
228
|
+
const out = [];
|
|
229
|
+
for (const m of methods) {
|
|
230
|
+
if (!/^on[A-Z]/.test(m.name)) continue;
|
|
231
|
+
const selfCall = new RegExp(`this\\.${m.name}\\s*\\(`, "g");
|
|
232
|
+
selfCall.lastIndex = start;
|
|
233
|
+
let called = false, c;
|
|
234
|
+
while ((c = selfCall.exec(code)) && c.index < end) {
|
|
235
|
+
if (c.index < m.bodyStart || c.index > m.bodyEnd) { called = true; break; } // ignore recursion
|
|
236
|
+
}
|
|
237
|
+
if (!called) out.push({ a: m.bodyStart - 1, b: m.bodyEnd, kind: m.name });
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// `TextArea({ text: this.body, controller: this.ctrl })` + `.onChange(v => this.body = v)` is the
|
|
243
|
+
// framework's own content-sync idiom, not the P2 defect. Detect the member sitting in an object
|
|
244
|
+
// literal that also carries `controller:`, and exempt it.
|
|
245
|
+
function controllerBoundMembers(code, start, end) {
|
|
246
|
+
const bound = new Set();
|
|
247
|
+
const re = /\{[^{}]*\bcontroller\s*:[^{}]*\}/g;
|
|
248
|
+
re.lastIndex = start;
|
|
249
|
+
let m;
|
|
250
|
+
while ((m = re.exec(code)) && m.index < end) {
|
|
251
|
+
const rr = /this\.(\w+)/g;
|
|
252
|
+
let r;
|
|
253
|
+
while ((r = rr.exec(m[0]))) bound.add(r[1]);
|
|
254
|
+
}
|
|
255
|
+
return bound;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function checkFile(raw) {
|
|
259
|
+
const code = blankNonCode(raw);
|
|
260
|
+
const starts = lineStarts(raw);
|
|
261
|
+
const msgs = [];
|
|
262
|
+
const push = (idx, severity, rule, message) => {
|
|
263
|
+
const p = posOf(starts, idx);
|
|
264
|
+
msgs.push({ line: p.line, column: p.column, severity, message, rule: `state-propagation/${rule}` });
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
for (const st of findStructs(code)) {
|
|
268
|
+
// ---- P7: a `get` accessor declared directly in an @Component / @Entry struct.
|
|
269
|
+
//
|
|
270
|
+
// The ArkUI struct transform does not carry get accessors through. The declaration compiles
|
|
271
|
+
// and lints clean, then `this.thing` reads `undefined` at runtime: a Text renders empty, an
|
|
272
|
+
// `if (this.flag)` never takes its branch. Verified on shipped HAPs — decompiling ets/modules.abc
|
|
273
|
+
// shows @State accessor pairs (#keepSorted / #keepSorted^1), plain methods (#tagNameById) and
|
|
274
|
+
// static class methods all present in the function table, while every `private get` is absent
|
|
275
|
+
// and its name survives only in the property-name pool. In one archived build the string
|
|
276
|
+
// 'No due date', reachable only through such a getter, appears zero times in the whole binary
|
|
277
|
+
// while its sibling literal rendered from build() appears once.
|
|
278
|
+
//
|
|
279
|
+
// This check must run before the `members.size` guard below: a struct can declare a getter and
|
|
280
|
+
// no state at all. Precision is high — across ten real ArkUI trees exactly one struct getter
|
|
281
|
+
// exists, and it is the known defect the harness itself patches out via scaffold-info-bar.patch.
|
|
282
|
+
{
|
|
283
|
+
const getRe = /(?:^|[\s;{}])(?:(?:private|protected|public|readonly)\s+)*get\s+(\w+)\s*\(\s*\)/g;
|
|
284
|
+
getRe.lastIndex = st.start;
|
|
285
|
+
let g;
|
|
286
|
+
while ((g = getRe.exec(code)) && g.index < st.end) {
|
|
287
|
+
const name = g[1];
|
|
288
|
+
if (KEYWORD.has(name)) continue;
|
|
289
|
+
// Only a direct member of the struct body — not a getter inside a nested class or object.
|
|
290
|
+
let depth = 0;
|
|
291
|
+
for (let i = st.start; i < g.index; i++) {
|
|
292
|
+
const ch = code[i];
|
|
293
|
+
if (ch === "{") depth++;
|
|
294
|
+
else if (ch === "}") depth--;
|
|
295
|
+
}
|
|
296
|
+
if (depth !== 0) continue;
|
|
297
|
+
// Is it actually read? An unread getter is dead code, not a rendering defect.
|
|
298
|
+
const readRe = new RegExp(`this\\.${name}\\b(?!\\s*\\()`, "g");
|
|
299
|
+
const body = code.slice(st.start, st.end);
|
|
300
|
+
const isRead = readRe.test(body);
|
|
301
|
+
push(g.index, isRead ? "error" : "warn", "P7-struct-getter-erased",
|
|
302
|
+
`'get ${name}()' is declared in struct '${st.name}'. The ArkUI struct transform drops get ` +
|
|
303
|
+
`accessors, so ` + (isRead
|
|
304
|
+
? `every \`this.${name}\` read resolves to undefined at runtime — text renders empty and ` +
|
|
305
|
+
`conditions never take their branch, with no compile or lint error. Convert it to a plain ` +
|
|
306
|
+
`method (\`private ${name}(): T { … }\` called as \`this.${name}()\`), or inline the ` +
|
|
307
|
+
`expression at the point of use.`
|
|
308
|
+
: `it would resolve to undefined if read. Convert it to a plain method or delete it.`));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---- P8: a CustomDialogController built as a FIELD INITIALISER, capturing state by value.
|
|
313
|
+
//
|
|
314
|
+
// A property initialiser runs once, when the struct is constructed. Any `this.<state>` in the
|
|
315
|
+
// builder's argument object is therefore evaluated at that instant and frozen — usually while
|
|
316
|
+
// the field still holds its declared default. Every later `open()` shows the dialog built from
|
|
317
|
+
// that stale snapshot, no matter how many times the state has changed since:
|
|
318
|
+
//
|
|
319
|
+
// private tagDialog: CustomDialogController = new CustomDialogController({
|
|
320
|
+
// builder: TagPicker({ initialSelectedIds: this.selectedTagIds }) // captured empty, forever
|
|
321
|
+
// });
|
|
322
|
+
//
|
|
323
|
+
// Assigning inside a method instead (`this.tagDialog = new CustomDialogController({…})` right
|
|
324
|
+
// before `open()`) re-reads the state each time. Writes are exempt: a callback that sets
|
|
325
|
+
// `this.x = …` runs on interaction, not at construction, so only reads are a defect.
|
|
326
|
+
//
|
|
327
|
+
// Severity is warn, not error: the archived trees carry no CustomDialogController at all, so
|
|
328
|
+
// there is no false-positive measurement behind this shape — unlike P7, where nine clean trees
|
|
329
|
+
// plus one known defect justified blocking. Revisit once a run exercises it.
|
|
330
|
+
{
|
|
331
|
+
const ctlRe = /(\w+)\s*:\s*CustomDialogController\s*=\s*new\s+CustomDialogController\s*\(/g;
|
|
332
|
+
ctlRe.lastIndex = st.start;
|
|
333
|
+
let c;
|
|
334
|
+
while ((c = ctlRe.exec(code)) && c.index < st.end) {
|
|
335
|
+
const paren = code.indexOf("(", c.index + c[1].length);
|
|
336
|
+
const close = matchPair(code, paren, "(", ")");
|
|
337
|
+
if (close < 0) { ctlRe.lastIndex = paren + 1; continue; }
|
|
338
|
+
ctlRe.lastIndex = paren + 1;
|
|
339
|
+
const cbs = allCallbackRegions(code, paren, close);
|
|
340
|
+
const captured = new Set();
|
|
341
|
+
const rr = /this\.(\w+)\b(?!\s*\()/g;
|
|
342
|
+
rr.lastIndex = paren;
|
|
343
|
+
let r;
|
|
344
|
+
while ((r = rr.exec(code)) && r.index < close) {
|
|
345
|
+
if (inRegions(cbs, r.index)) continue; // runs on interaction, not now
|
|
346
|
+
// A write (`this.x = …`) is not a capture; only reads freeze a value.
|
|
347
|
+
const tail = code.slice(r.index + r[0].length, r.index + r[0].length + 3);
|
|
348
|
+
if (/^\s*=[^=]/.test(tail)) continue;
|
|
349
|
+
captured.add(r[1]);
|
|
350
|
+
}
|
|
351
|
+
if (!captured.size) continue;
|
|
352
|
+
push(c.index, "warn", "P8-dialog-controller-captures-state",
|
|
353
|
+
`'${c[1]}' is a CustomDialogController built in a field initialiser, and its builder reads ` +
|
|
354
|
+
`[${[...captured].map((n) => `this.${n}`).join(", ")}] as construction argument(s). A property ` +
|
|
355
|
+
`initialiser runs once, at struct construction, so those values are frozen at their declared ` +
|
|
356
|
+
`defaults — every open() re-shows the dialog built from that first snapshot, however often the ` +
|
|
357
|
+
`state has changed since. Build the controller in a method immediately before open() ` +
|
|
358
|
+
`(\`this.${c[1]} = new CustomDialogController({…}); this.${c[1]}.open();\`) so the current ` +
|
|
359
|
+
`values are read each time.`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const members = findMembers(code, st.start, st.end);
|
|
364
|
+
if (!members.size) continue;
|
|
365
|
+
const methods = findMethods(code, st.start, st.end);
|
|
366
|
+
const cbRegions = callbackRegions(code, st.start, st.end);
|
|
367
|
+
const extCbRegions = externalCallbackMethods(code, st.start, st.end, methods);
|
|
368
|
+
// P3 liveness risk = engine subscriptions + engine-invoked named handlers. A UI attribute
|
|
369
|
+
// callback (.onClick) is excluded: a user tap always arrives, so state written there is not
|
|
370
|
+
// at risk of never being written.
|
|
371
|
+
const engineRegions = cbRegions.filter((r) => r.family === "engine").concat(extCbRegions);
|
|
372
|
+
const ctrlBound = controllerBoundMembers(code, st.start, st.end);
|
|
373
|
+
|
|
374
|
+
// State a method renders *transitively*. A ForEach item body is very often just
|
|
375
|
+
// `this.SomeBuilder(item)`, and the state that decides what the row looks like sits one or
|
|
376
|
+
// two calls deeper (`TagRow` → `isSelected()` → `this.selectedIds`). Scanning only the item
|
|
377
|
+
// body therefore misses the whole class where the key is an id and the changing value is
|
|
378
|
+
// reached through a helper — the defect P5 exists to catch.
|
|
379
|
+
const byName = new Map(methods.map((m) => [m.name, m]));
|
|
380
|
+
const transitiveState = (methodName, seen = new Set()) => {
|
|
381
|
+
const found = new Set();
|
|
382
|
+
if (seen.has(methodName)) return found; // recursion guard
|
|
383
|
+
seen.add(methodName);
|
|
384
|
+
const mth = byName.get(methodName);
|
|
385
|
+
if (!mth) return found;
|
|
386
|
+
const body = code.slice(mth.bodyStart, mth.bodyEnd);
|
|
387
|
+
// Reads inside this method's own event handlers are not rendered values (same rule the
|
|
388
|
+
// direct scan below applies), so skip them.
|
|
389
|
+
const hRegions = callbackRegions(code, mth.bodyStart, mth.bodyEnd);
|
|
390
|
+
const rr = /this\.(\w+)\b/g;
|
|
391
|
+
let r;
|
|
392
|
+
while ((r = rr.exec(body))) {
|
|
393
|
+
const name = r[1];
|
|
394
|
+
const abs = mth.bodyStart + r.index;
|
|
395
|
+
if (inRegions(hRegions, abs)) continue;
|
|
396
|
+
if (members.has(name)) { if (!/^on[A-Z]/.test(name)) found.add(name); continue; }
|
|
397
|
+
// not a state field — if it is a method of this struct, follow it
|
|
398
|
+
if (byName.has(name)) for (const n of transitiveState(name, seen)) found.add(n);
|
|
399
|
+
}
|
|
400
|
+
return found;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// a bare `this.x` read of a state member ($$ / $ prefixed forms are the correct binding)
|
|
404
|
+
const stateRead = (expr) => {
|
|
405
|
+
const t = expr.trim();
|
|
406
|
+
if (/^\$/.test(t)) return null;
|
|
407
|
+
const m = /^this\.(\w+)$/.exec(t);
|
|
408
|
+
return m && members.has(m[1]) ? m[1] : null;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// ---- P1: @Builder receiving state by value
|
|
412
|
+
for (const mth of methods) {
|
|
413
|
+
if (!mth.isBuilder) continue;
|
|
414
|
+
const primIdx = mth.params
|
|
415
|
+
.map((p, i) => { const t = /:\s*([\w.<>\[\]]+)/.exec(p); return t && PRIMITIVE.test(t[1]) ? i : -1; })
|
|
416
|
+
.filter((i) => i >= 0);
|
|
417
|
+
if (!primIdx.length) continue;
|
|
418
|
+
const callRe = new RegExp(`this\\.${mth.name}\\s*\\(`, "g");
|
|
419
|
+
callRe.lastIndex = st.start;
|
|
420
|
+
let c;
|
|
421
|
+
while ((c = callRe.exec(code)) && c.index < st.end) {
|
|
422
|
+
const paren = code.indexOf("(", c.index);
|
|
423
|
+
const close = matchPair(code, paren, "(", ")");
|
|
424
|
+
if (close < 0) break;
|
|
425
|
+
const args = splitArgs(code.slice(paren + 1, close));
|
|
426
|
+
for (const i of primIdx) {
|
|
427
|
+
const name = args[i] ? stateRead(args[i]) : null;
|
|
428
|
+
if (!name) continue;
|
|
429
|
+
push(c.index, "error", "P1-builder-value-param",
|
|
430
|
+
`@Builder '${mth.name}' receives state '${name}' (@${members.get(name).decorator}) by value as arg ${i + 1}. ` +
|
|
431
|
+
`A by-value @Builder parameter does not subscribe, so the builder body never re-renders when '${name}' changes ` +
|
|
432
|
+
`— the value is stored but the screen keeps the old text. Pass $$this.${name}, read this.${name} inside the ` +
|
|
433
|
+
`builder, or split the row into @Component + @Prop/@Link.`);
|
|
434
|
+
}
|
|
435
|
+
callRe.lastIndex = close;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---- P2: controller setter vs on*Change writing the same member
|
|
440
|
+
const cbWrites = new Map();
|
|
441
|
+
for (const r of cbRegions) {
|
|
442
|
+
if (!/^on/.test(r.kind)) continue;
|
|
443
|
+
const seg = code.slice(r.a, r.b);
|
|
444
|
+
const wr = /this\.(\w+)\s*=(?!=)/g;
|
|
445
|
+
let w;
|
|
446
|
+
while ((w = wr.exec(seg))) if (members.has(w[1]) && !cbWrites.has(w[1])) cbWrites.set(w[1], r.a + w.index);
|
|
447
|
+
}
|
|
448
|
+
const CTRL = /this\.\w*[Cc]ontroller\s*\.\s*\w+\s*\(/;
|
|
449
|
+
for (const mth of methods) {
|
|
450
|
+
const seg = code.slice(mth.bodyStart, mth.bodyEnd);
|
|
451
|
+
if (!CTRL.test(seg)) continue;
|
|
452
|
+
const wr = /this\.(\w+)\s*=(?!=)/g;
|
|
453
|
+
let w;
|
|
454
|
+
while ((w = wr.exec(seg))) {
|
|
455
|
+
const abs = mth.bodyStart + w.index;
|
|
456
|
+
if (!members.has(w[1]) || inRegions(cbRegions, abs) || !cbWrites.has(w[1])) continue;
|
|
457
|
+
// `Comp({ text: this.body, controller: this.ctrl })` + `.onChange(v => this.body = v)`
|
|
458
|
+
// is the framework's own content-sync idiom, not the overwrite defect.
|
|
459
|
+
if (ctrlBound.has(w[1])) continue;
|
|
460
|
+
push(abs, "error", "P2-controller-vs-callback",
|
|
461
|
+
`'${w[1]}' is assigned here alongside a controller setter in '${mth.name}', and is also written by a framework ` +
|
|
462
|
+
`on*Change callback (line ${posOf(starts, cbWrites.get(w[1])).line}). The callback fires afterwards and overwrites ` +
|
|
463
|
+
`this value, so the next action reads the framework's offset, not yours. Typical symptom: the forward action passes ` +
|
|
464
|
+
`and the inverse action fails. Guard with a suppression flag, or hold a semantic anchor (line index / item id) ` +
|
|
465
|
+
`instead of a raw offset.`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ---- P3: a BOOLEAN control state written ONLY from engine event callbacks.
|
|
470
|
+
// Two deliberate narrowings, both learned from false positives on real code:
|
|
471
|
+
// * post-await writes are NOT counted — "load then render" resolves on its own.
|
|
472
|
+
// * only boolean members are considered — a data field (title, elapsed, list) legitimately
|
|
473
|
+
// arrives from the engine; a control's on/off state must never depend on an event that
|
|
474
|
+
// may never fire, because every test that drives that control dies at step 1.
|
|
475
|
+
for (const [name, info] of members) {
|
|
476
|
+
if (info.type !== "boolean") continue;
|
|
477
|
+
const wr = new RegExp(`this\\.${name}\\s*=(?!=)`, "g");
|
|
478
|
+
wr.lastIndex = st.start;
|
|
479
|
+
let w, total = 0, viaCallback = 0;
|
|
480
|
+
while ((w = wr.exec(code)) && w.index < st.end) {
|
|
481
|
+
total++;
|
|
482
|
+
if (inRegions(engineRegions, w.index)) viaCallback++;
|
|
483
|
+
}
|
|
484
|
+
if (total > 0 && viaCallback === total) {
|
|
485
|
+
push(info.idx, "warn", "P3-async-only-state",
|
|
486
|
+
`Boolean control state '${name}' (@${info.decorator}) is written only from engine event callbacks ` +
|
|
487
|
+
`(${total} site(s), no synchronous write). If the event never arrives — a player that never reaches ` +
|
|
488
|
+
`'playing', a session that never calls back — the control renders its initial value forever, and every ` +
|
|
489
|
+
`test that must first drive this control fails at step 1. Add a synchronous write path (optimistic ` +
|
|
490
|
+
`update + rollback on failure) and a timeout fallback state.`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ---- P5: ForEach keyGenerator omits the state its item renders
|
|
495
|
+
const feRe = /\b(Lazy)?ForEach\s*\(/g;
|
|
496
|
+
feRe.lastIndex = st.start;
|
|
497
|
+
let f;
|
|
498
|
+
while ((f = feRe.exec(code)) && f.index < st.end) {
|
|
499
|
+
const paren = code.indexOf("(", f.index);
|
|
500
|
+
const close = matchPair(code, paren, "(", ")");
|
|
501
|
+
if (close < 0) break;
|
|
502
|
+
feRe.lastIndex = paren + 1;
|
|
503
|
+
const args = splitArgs(code.slice(paren + 1, close));
|
|
504
|
+
const kind = f[1] ? "LazyForEach" : "ForEach";
|
|
505
|
+
const item = args[1] || "";
|
|
506
|
+
// Only state that flows into RENDERED content counts. A `this.x` read inside an event
|
|
507
|
+
// handler (`.onClick(() => this.x = '')`) or a gesture closure is not a per-item rendered
|
|
508
|
+
// value, and the key is under no obligation to include it — counting those was the single
|
|
509
|
+
// largest source of false positives on real code.
|
|
510
|
+
const itemAbs = code.indexOf(item, paren);
|
|
511
|
+
const handlerRegions = itemAbs >= 0 ? callbackRegions(code, itemAbs, itemAbs + item.length) : [];
|
|
512
|
+
const used = new Set();
|
|
513
|
+
const viaHelper = new Set();
|
|
514
|
+
const rr = /this\.(\w+)\b/g;
|
|
515
|
+
let r;
|
|
516
|
+
while ((r = rr.exec(item))) {
|
|
517
|
+
const name = r[1];
|
|
518
|
+
if (itemAbs >= 0 && inRegions(handlerRegions, itemAbs + r.index)) continue;
|
|
519
|
+
if (members.has(name)) {
|
|
520
|
+
if (/^on[A-Z]/.test(name)) continue; // handlers are not rendered values
|
|
521
|
+
used.add(name);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
// `this.SomeBuilder(item)` — follow the call and collect the state it renders.
|
|
525
|
+
if (byName.has(name)) {
|
|
526
|
+
for (const n of transitiveState(name)) { used.add(n); viaHelper.add(n); }
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// ---- P6: a MUTABLE FIELD OF THE ITEM is handed to a child component as a construction
|
|
530
|
+
// argument while the key carries only the item's identity.
|
|
531
|
+
//
|
|
532
|
+
// This is P5's blind spot, and it is the more common shape. P5 follows `this.<state>`; here
|
|
533
|
+
// the changing value is `habit.todayValue` — a field of the loop variable, not struct state —
|
|
534
|
+
// so P5 never mentions it, and this check must run even when P5 found nothing:
|
|
535
|
+
//
|
|
536
|
+
// ForEach(this.habits, (habit: HabitData) => {
|
|
537
|
+
// NumberPanel({ todayValue: habit.todayValue }) // @Prop copy, taken once
|
|
538
|
+
// }, (habit: HabitData) => `${habit.id}`) // key cannot change when the field does
|
|
539
|
+
//
|
|
540
|
+
// Reassigning the array re-runs the loop, but a key that still matches lets ArkUI reuse the
|
|
541
|
+
// node, and a reused child does not re-evaluate its @Prop initialisers — the row keeps
|
|
542
|
+
// rendering the old value. Fixing only the *child's* inner key does not help: the parent row
|
|
543
|
+
// is reused first. Verified against an archived run where the candidate did exactly that
|
|
544
|
+
// (inner key rebuilt with the rendered value, P5 satisfied and silent) and the device test
|
|
545
|
+
// still read the stale seeded number.
|
|
546
|
+
if (args[2]) {
|
|
547
|
+
const itemParam = /^\s*\(?\s*([A-Za-z_$][\w$]*)/.exec(item);
|
|
548
|
+
const v = itemParam ? itemParam[1] : null;
|
|
549
|
+
const keyFields = v ? (args[2].match(new RegExp(`\\b${v}\\.(\\w+)`, "g")) || [])
|
|
550
|
+
.map((s) => s.split(".")[1]) : [];
|
|
551
|
+
// Identity-only key: it mentions the item, and every field it reads is an id.
|
|
552
|
+
const identityOnly = keyFields.length > 0 && keyFields.every((fd) => /^(id|key|uuid|guid)$/i.test(fd));
|
|
553
|
+
if (v && !KEYWORD.has(v) && identityOnly) {
|
|
554
|
+
const ctorRe = /\b([A-Z][\w$]*)\s*\(\s*\{/g;
|
|
555
|
+
const seen = new Set();
|
|
556
|
+
let c;
|
|
557
|
+
while ((c = ctorRe.exec(item))) {
|
|
558
|
+
const open = item.indexOf("{", c.index);
|
|
559
|
+
const shut = matchPair(item, open, "{", "}");
|
|
560
|
+
if (shut < 0) continue;
|
|
561
|
+
ctorRe.lastIndex = open + 1;
|
|
562
|
+
const argObj = item.slice(open + 1, shut);
|
|
563
|
+
// Fields read inside a callback are fine — a handler runs on tap, against the
|
|
564
|
+
// then-current item. Two spellings must both be excluded: an attribute callback
|
|
565
|
+
// (`.onClick(() => …)`, found by callbackRegions) and an object-literal property
|
|
566
|
+
// holding an arrow function (`onTap: () => { … }`), which is how handlers are passed
|
|
567
|
+
// into a child component and is NOT a dot-call, so callbackRegions cannot see it.
|
|
568
|
+
const cbs = allCallbackRegions(item, open, shut);
|
|
569
|
+
const passed = new Set();
|
|
570
|
+
const fr = new RegExp(`\\b${v}\\.(\\w+)`, "g");
|
|
571
|
+
let p;
|
|
572
|
+
while ((p = fr.exec(argObj))) {
|
|
573
|
+
if (inRegions(cbs, open + 1 + p.index)) continue;
|
|
574
|
+
if (/^(id|key|uuid|guid)$/i.test(p[1])) continue; // identity is already in the key
|
|
575
|
+
passed.add(p[1]);
|
|
576
|
+
}
|
|
577
|
+
if (!passed.size) continue;
|
|
578
|
+
const sig = `${c[1]}:${[...passed].join(",")}`;
|
|
579
|
+
if (seen.has(sig)) continue;
|
|
580
|
+
seen.add(sig);
|
|
581
|
+
push(f.index, "warn", "P6-foreach-item-field-to-child",
|
|
582
|
+
`${kind} passes mutable item field(s) [${[...passed].map((x) => `${v}.${x}`).join(", ")}] into ` +
|
|
583
|
+
`'${c[1]}' as construction arguments, but its keyGenerator ` +
|
|
584
|
+
`\`${args[2].replace(/\s+/g, " ").slice(0, 54)}\` carries only the item's identity. When those fields ` +
|
|
585
|
+
`change, the key still matches, ArkUI reuses the row, and '${c[1]}' keeps the @Prop copy it was built ` +
|
|
586
|
+
`with — the old value stays on screen. Add the changing field (or a render version) to the key; ` +
|
|
587
|
+
`rebuilding a key *inside* '${c[1]}' does not help, because the row above it is reused first.`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!used.size) continue;
|
|
593
|
+
// Severity stays `warn` deliberately. An identity-only key over state the struct writes
|
|
594
|
+
// looks like a certain defect, but measured against 20 archived runs it is not: the exact
|
|
595
|
+
// same shape ("selection indicator keyed by id") appears on specs that scored 100% —
|
|
596
|
+
// mihon-more-tab omits `currentTab` keyed by `tab.id`, readyou-accounts omits `accent`
|
|
597
|
+
// keyed by `s.id`, markor_002 omits `selectedNames` keyed by `entry.path` — and on specs
|
|
598
|
+
// that failed for exactly this reason (uhabits CheckmarkPanel omits `todayValue` keyed by
|
|
599
|
+
// `${dayIndex}`). Whether a reused node actually renders stale depends on how the value
|
|
600
|
+
// reaches the child (constructor argument vs read inline) and on whether anything drives
|
|
601
|
+
// the change, neither of which this scan can see. Promoting on the shape alone would have
|
|
602
|
+
// blocked 8 findings across 5 fully-passing specs. Use --all-error to force blocking.
|
|
603
|
+
if (!args[2]) {
|
|
604
|
+
push(f.index, "warn", "P5-foreach-key-omits-state",
|
|
605
|
+
`${kind} has no keyGenerator, but its item body renders state [${[...used].join(", ")}]. Without a key that ` +
|
|
606
|
+
`includes the changing value, items are not rebuilt when it changes.`);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const key = args[2];
|
|
610
|
+
const missing = [...used].filter((n) => !new RegExp(`\\b${n}\\b`).test(key));
|
|
611
|
+
if (missing.length === used.size) {
|
|
612
|
+
const indirect = missing.filter((n) => viaHelper.has(n));
|
|
613
|
+
const how = indirect.length
|
|
614
|
+
? ` The value is reached indirectly — the item body calls a helper of this struct that reads ` +
|
|
615
|
+
`[${indirect.join(", ")}] — so the key looks complete at the call site while the row still depends on ` +
|
|
616
|
+
`changing state.`
|
|
617
|
+
: "";
|
|
618
|
+
push(f.index, "warn", "P5-foreach-key-omits-state",
|
|
619
|
+
`${kind} keyGenerator \`${key.replace(/\s+/g, " ").slice(0, 70)}\` omits every state value its item renders ` +
|
|
620
|
+
`[${missing.join(", ")}], so changing them does not rebuild the item.${how} Include the rendered value in the ` +
|
|
621
|
+
`key, or hold it as @State in the child and update it locally on the interaction (an @Prop copy is not ` +
|
|
622
|
+
`re-evaluated for a reused node).`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return msgs;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ---- cli
|
|
630
|
+
function argOf(argv, flag) { const i = argv.indexOf(flag); return i >= 0 && argv[i + 1] ? argv[i + 1] : null; }
|
|
631
|
+
const SKIP_DIR = new Set(["node_modules", "oh_modules", "build", ".git", ".idea", "dist"]);
|
|
632
|
+
function walk(dir, acc = []) {
|
|
633
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
634
|
+
if (SKIP_DIR.has(e.name)) continue;
|
|
635
|
+
const p = path.join(dir, e.name);
|
|
636
|
+
if (e.isDirectory()) walk(p, acc);
|
|
637
|
+
else if (e.name.endsWith(".ets")) acc.push(p);
|
|
638
|
+
}
|
|
639
|
+
return acc;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const argv = process.argv.slice(2);
|
|
643
|
+
const dirArg = argOf(argv, "--dir");
|
|
644
|
+
const filesArg = argOf(argv, "--files");
|
|
645
|
+
const outArg = argOf(argv, "--out");
|
|
646
|
+
const allError = argv.includes("--all-error");
|
|
647
|
+
|
|
648
|
+
let files = [];
|
|
649
|
+
if (filesArg) files = filesArg.split(",").map((s) => s.trim()).filter(Boolean);
|
|
650
|
+
else if (dirArg && fs.existsSync(dirArg)) files = fs.statSync(dirArg).isDirectory() ? walk(dirArg) : [dirArg];
|
|
651
|
+
else if (!dirArg) { emit({ ok: false, error: "need --dir <project-or-dir> or --files a.ets,b.ets" }); process.exit(2); }
|
|
652
|
+
|
|
653
|
+
const report = [];
|
|
654
|
+
const byRule = {};
|
|
655
|
+
let nError = 0, nWarn = 0;
|
|
656
|
+
for (const f of files) {
|
|
657
|
+
let raw;
|
|
658
|
+
try { raw = fs.readFileSync(f, "utf8"); } catch { continue; }
|
|
659
|
+
const messages = checkFile(raw);
|
|
660
|
+
if (!messages.length) continue;
|
|
661
|
+
for (const m of messages) {
|
|
662
|
+
if (allError) m.severity = "error";
|
|
663
|
+
byRule[m.rule] = (byRule[m.rule] || 0) + 1;
|
|
664
|
+
if (m.severity === "error") nError++; else nWarn++;
|
|
665
|
+
}
|
|
666
|
+
report.push({ filePath: f, messages });
|
|
667
|
+
}
|
|
668
|
+
if (outArg) {
|
|
669
|
+
fs.mkdirSync(path.dirname(path.resolve(outArg)), { recursive: true });
|
|
670
|
+
fs.writeFileSync(outArg, JSON.stringify(report, null, 2), "utf8");
|
|
671
|
+
}
|
|
672
|
+
emit({
|
|
673
|
+
ok: nError === 0, files_scanned: files.length, files_with_findings: report.length,
|
|
674
|
+
error: nError, warn: nWarn, by_rule: byRule, report: outArg || null,
|
|
675
|
+
});
|
|
676
|
+
process.exit(nError > 0 ? 1 : 0);
|