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,1995 @@
|
|
|
1
|
+
import { createStableId } from "../../core/stable-id.js";
|
|
2
|
+
import { computePaths } from "../../graph/paths.js";
|
|
3
|
+
import { validateAreaGraph } from "../../validation/index.js";
|
|
4
|
+
import {
|
|
5
|
+
codeNameCandidate,
|
|
6
|
+
componentIdentifierName,
|
|
7
|
+
controlNameCandidates,
|
|
8
|
+
literalNameCandidate,
|
|
9
|
+
resolveNameCandidates,
|
|
10
|
+
} from "../names/policy.js";
|
|
11
|
+
import { CallbackFlowTraversal } from "./callback-flow.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveComposeRouteArea,
|
|
14
|
+
resolveAreaOwnership,
|
|
15
|
+
} from "./area-ownership.js";
|
|
16
|
+
import { applyComponentVariants } from "./component-variants.js";
|
|
17
|
+
function factsOfKind(artifact, kind) {
|
|
18
|
+
return artifact.facts.filter((fact) => fact.kind === kind);
|
|
19
|
+
}
|
|
20
|
+
function resolved(value, supportRefs, confidence = "high") {
|
|
21
|
+
return {
|
|
22
|
+
status: "resolved",
|
|
23
|
+
value,
|
|
24
|
+
confidence,
|
|
25
|
+
supportRefs: [...new Set(supportRefs)].sort(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function graphId(namespace, caseId, entityKind, identity) {
|
|
29
|
+
return createStableId({
|
|
30
|
+
namespace,
|
|
31
|
+
caseId,
|
|
32
|
+
entityKind,
|
|
33
|
+
identity: [{ kind: "semantic-key", value: identity }],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function inferredTrigger(data) {
|
|
37
|
+
if (
|
|
38
|
+
data.elementType === "toggle" ||
|
|
39
|
+
data.elementType === "switch" ||
|
|
40
|
+
data.elementType === "checkbox" ||
|
|
41
|
+
data.elementType === "radio-button" ||
|
|
42
|
+
data.elementType === "toggle-button"
|
|
43
|
+
)
|
|
44
|
+
return "toggle";
|
|
45
|
+
if (data.elementType === "text-input" || data.elementType === "text-field")
|
|
46
|
+
return "text-input";
|
|
47
|
+
return "click";
|
|
48
|
+
}
|
|
49
|
+
function repeatedControlName(areaName) {
|
|
50
|
+
if (!areaName) return "List item";
|
|
51
|
+
return /\p{Script=Han}/u.test(areaName)
|
|
52
|
+
? `${areaName}列表项`
|
|
53
|
+
: `${areaName} item`;
|
|
54
|
+
}
|
|
55
|
+
function genericMenuOpenerName(value) {
|
|
56
|
+
const compact = value
|
|
57
|
+
.replace(/[^\p{Letter}\p{Number}]/gu, "")
|
|
58
|
+
.toLocaleLowerCase("en-US");
|
|
59
|
+
return new Set([
|
|
60
|
+
"menu",
|
|
61
|
+
"more",
|
|
62
|
+
"morehorizontal",
|
|
63
|
+
"morehoriz",
|
|
64
|
+
"morevertical",
|
|
65
|
+
"morevert",
|
|
66
|
+
"overflow",
|
|
67
|
+
"更多",
|
|
68
|
+
"菜单",
|
|
69
|
+
]).has(compact);
|
|
70
|
+
}
|
|
71
|
+
function contextualMenuName(value) {
|
|
72
|
+
const normalized = value.trim();
|
|
73
|
+
if (/菜单$/u.test(normalized) || /(?:^|\s)menu$/iu.test(normalized)) {
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
return /\p{Script=Han}/u.test(normalized)
|
|
77
|
+
? `${normalized}菜单`
|
|
78
|
+
: `${normalized} menu`;
|
|
79
|
+
}
|
|
80
|
+
function androidExternalTargetName(intentAction) {
|
|
81
|
+
switch (intentAction) {
|
|
82
|
+
case "android.intent.action.OPEN_DOCUMENT":
|
|
83
|
+
case "android.intent.action.GET_CONTENT":
|
|
84
|
+
return "Android 系统文件选择器";
|
|
85
|
+
case "android.intent.action.OPEN_DOCUMENT_TREE":
|
|
86
|
+
return "Android 系统文件夹选择器";
|
|
87
|
+
case "android.intent.action.CREATE_DOCUMENT":
|
|
88
|
+
return "Android 系统文件保存界面";
|
|
89
|
+
default:
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function effectText(data, targetName) {
|
|
94
|
+
const scalarValue =
|
|
95
|
+
data.value === null ||
|
|
96
|
+
typeof data.value === "string" ||
|
|
97
|
+
typeof data.value === "number" ||
|
|
98
|
+
typeof data.value === "boolean"
|
|
99
|
+
? String(data.value)
|
|
100
|
+
: undefined;
|
|
101
|
+
switch (data.effectKind) {
|
|
102
|
+
case "navigate-area":
|
|
103
|
+
return targetName ? `进入 ${targetName}` : "进入目标区域";
|
|
104
|
+
case "open-external":
|
|
105
|
+
return targetName ? `打开 ${targetName}` : "打开 App 外目标";
|
|
106
|
+
case "update-state":
|
|
107
|
+
if (data.stateKey?.split("\u0000").includes("preference-key")) {
|
|
108
|
+
return "更新该设置的选中值";
|
|
109
|
+
}
|
|
110
|
+
return scalarValue === undefined
|
|
111
|
+
? "更新当前状态"
|
|
112
|
+
: `将当前状态更新为“${scalarValue}”`;
|
|
113
|
+
case "show-feedback":
|
|
114
|
+
return scalarValue === undefined
|
|
115
|
+
? "显示短暂提示"
|
|
116
|
+
: `显示短暂提示“${scalarValue}”`;
|
|
117
|
+
case "business-action":
|
|
118
|
+
return "执行对应业务操作";
|
|
119
|
+
case "show-surface":
|
|
120
|
+
return targetName ? `显示 ${targetName}` : "显示交互区域";
|
|
121
|
+
case "close-surface":
|
|
122
|
+
return "关闭当前区域";
|
|
123
|
+
case "input":
|
|
124
|
+
return "接收并处理用户输入";
|
|
125
|
+
case "unknown":
|
|
126
|
+
return "操作结果待确认";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function sorted(values) {
|
|
130
|
+
return values.sort((left, right) => left.id.localeCompare(right.id, "en"));
|
|
131
|
+
}
|
|
132
|
+
function uniqueSorted(values) {
|
|
133
|
+
return [...new Set(values)].sort((left, right) =>
|
|
134
|
+
left.localeCompare(right, "en"),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
function canonicalSemanticValue(value) {
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
return `[${value.map(canonicalSemanticValue).join(",")}]`;
|
|
140
|
+
}
|
|
141
|
+
if (value !== null && typeof value === "object") {
|
|
142
|
+
return `{${Object.entries(value)
|
|
143
|
+
.sort(([left], [right]) => left.localeCompare(right, "en"))
|
|
144
|
+
.map(
|
|
145
|
+
([key, child]) =>
|
|
146
|
+
`${JSON.stringify(key)}:${canonicalSemanticValue(child)}`,
|
|
147
|
+
)
|
|
148
|
+
.join(",")}}`;
|
|
149
|
+
}
|
|
150
|
+
return JSON.stringify(value);
|
|
151
|
+
}
|
|
152
|
+
function stateKeyDisplayName(value) {
|
|
153
|
+
const parts = value.split("\u0000");
|
|
154
|
+
const preferenceKeyIndex = parts.indexOf("preference-key");
|
|
155
|
+
if (preferenceKeyIndex >= 0 && parts[preferenceKeyIndex + 1]?.trim()) {
|
|
156
|
+
return parts[preferenceKeyIndex + 1].trim();
|
|
157
|
+
}
|
|
158
|
+
if (parts[0] === "receiver" && parts[3]?.trim()) {
|
|
159
|
+
return parts[3].trim();
|
|
160
|
+
}
|
|
161
|
+
if (parts[2]?.trim()) return parts[2].trim();
|
|
162
|
+
if (parts[1]?.trim()) return parts[1].trim();
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
function normalizedUiPropertyName(property) {
|
|
166
|
+
const leaf = property.trim().split(/[.:/]/u).at(-1) ?? property;
|
|
167
|
+
return leaf.replace(/[^a-z0-9]/giu, "").toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
function staticEnabledFor(uiFactRef, propertyFacts) {
|
|
170
|
+
const enabledFacts = propertyFacts.filter(
|
|
171
|
+
(property) =>
|
|
172
|
+
property.data.ownerKey === uiFactRef &&
|
|
173
|
+
normalizedUiPropertyName(property.data.property) === "enabled" &&
|
|
174
|
+
typeof property.data.value === "boolean",
|
|
175
|
+
);
|
|
176
|
+
if (enabledFacts.length === 0) return undefined;
|
|
177
|
+
const values = new Set(enabledFacts.map((property) => property.data.value));
|
|
178
|
+
if (values.size !== 1) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Area graph property invariant for UI Element ${uiFactRef}: ` +
|
|
181
|
+
"static enabled properties disagree.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const confidenceRank = { high: 2, medium: 1, low: 0 };
|
|
185
|
+
const confidence = enabledFacts.reduce(
|
|
186
|
+
(weakest, property) =>
|
|
187
|
+
confidenceRank[property.confidence] < confidenceRank[weakest]
|
|
188
|
+
? property.confidence
|
|
189
|
+
: weakest,
|
|
190
|
+
"high",
|
|
191
|
+
);
|
|
192
|
+
return resolved(
|
|
193
|
+
enabledFacts[0].data.value,
|
|
194
|
+
enabledFacts.map((property) => property.id),
|
|
195
|
+
confidence,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
function descriptionText(value) {
|
|
199
|
+
if (typeof value !== "string") return undefined;
|
|
200
|
+
const normalized = value.trim().replace(/\s+/gu, " ");
|
|
201
|
+
return normalized.length > 0 && /[\p{Letter}\p{Number}]/u.test(normalized)
|
|
202
|
+
? normalized
|
|
203
|
+
: undefined;
|
|
204
|
+
}
|
|
205
|
+
function sameDescriptionText(left, right) {
|
|
206
|
+
return left.toLocaleLowerCase("en-US") === right.toLocaleLowerCase("en-US");
|
|
207
|
+
}
|
|
208
|
+
function weakestDescriptionConfidence(candidates) {
|
|
209
|
+
const rank = { high: 2, medium: 1, low: 0 };
|
|
210
|
+
return candidates.reduce(
|
|
211
|
+
(weakest, candidate) =>
|
|
212
|
+
rank[candidate.confidence] < rank[weakest]
|
|
213
|
+
? candidate.confidence
|
|
214
|
+
: weakest,
|
|
215
|
+
"high",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function stableDescriptionFacts(
|
|
219
|
+
caseId,
|
|
220
|
+
areaRef,
|
|
221
|
+
componentRefs,
|
|
222
|
+
areaName,
|
|
223
|
+
uiFacts,
|
|
224
|
+
propertyFacts,
|
|
225
|
+
boundUiFactRefs,
|
|
226
|
+
) {
|
|
227
|
+
const uiById = new Map(uiFacts.map((fact) => [fact.id, fact]));
|
|
228
|
+
const runtimeContentOwners = new Set(
|
|
229
|
+
propertyFacts.flatMap((property) =>
|
|
230
|
+
normalizedUiPropertyName(property.data.property) === "runtimecontent" &&
|
|
231
|
+
property.data.value === true
|
|
232
|
+
? [property.data.ownerKey]
|
|
233
|
+
: [],
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
const candidates = [];
|
|
237
|
+
for (const fact of uiFacts) {
|
|
238
|
+
const value = descriptionText(fact.data.visibleText);
|
|
239
|
+
if (
|
|
240
|
+
!value ||
|
|
241
|
+
fact.data.actionable ||
|
|
242
|
+
boundUiFactRefs.has(fact.id) ||
|
|
243
|
+
!componentRefs.has(fact.data.ownerKey) ||
|
|
244
|
+
runtimeContentOwners.has(fact.id)
|
|
245
|
+
)
|
|
246
|
+
continue;
|
|
247
|
+
candidates.push({
|
|
248
|
+
value,
|
|
249
|
+
sourceFactRefs: [fact.id],
|
|
250
|
+
confidence: fact.confidence,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
for (const property of propertyFacts) {
|
|
254
|
+
const value = descriptionText(property.data.value);
|
|
255
|
+
if (!value) continue;
|
|
256
|
+
const normalizedProperty = normalizedUiPropertyName(property.data.property);
|
|
257
|
+
const stableProperty =
|
|
258
|
+
property.data.property.startsWith("stable-content:") ||
|
|
259
|
+
normalizedProperty === "stablecontent";
|
|
260
|
+
if (!stableProperty && normalizedProperty !== "message") continue;
|
|
261
|
+
const ownedUi = uiById.get(property.data.ownerKey);
|
|
262
|
+
const ownedByArea =
|
|
263
|
+
componentRefs.has(property.data.ownerKey) ||
|
|
264
|
+
(ownedUi !== undefined &&
|
|
265
|
+
!ownedUi.data.actionable &&
|
|
266
|
+
!boundUiFactRefs.has(ownedUi.id) &&
|
|
267
|
+
componentRefs.has(ownedUi.data.ownerKey));
|
|
268
|
+
if (!ownedByArea) continue;
|
|
269
|
+
candidates.push({
|
|
270
|
+
value,
|
|
271
|
+
sourceFactRefs: [property.id],
|
|
272
|
+
confidence: property.confidence,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const areaNameValue =
|
|
276
|
+
areaName.status === "resolved"
|
|
277
|
+
? descriptionText(areaName.value)
|
|
278
|
+
: undefined;
|
|
279
|
+
const grouped = new Map();
|
|
280
|
+
for (const candidate of candidates) {
|
|
281
|
+
if (areaNameValue && sameDescriptionText(candidate.value, areaNameValue))
|
|
282
|
+
continue;
|
|
283
|
+
const key = candidate.value.toLocaleLowerCase("en-US");
|
|
284
|
+
const matches = grouped.get(key) ?? [];
|
|
285
|
+
matches.push(candidate);
|
|
286
|
+
grouped.set(key, matches);
|
|
287
|
+
}
|
|
288
|
+
return [...grouped.values()].map((matching) => {
|
|
289
|
+
const value = matching[0].value;
|
|
290
|
+
const sourceFactRefs = uniqueSorted(
|
|
291
|
+
matching.flatMap((candidate) => candidate.sourceFactRefs),
|
|
292
|
+
);
|
|
293
|
+
return {
|
|
294
|
+
id: graphId(
|
|
295
|
+
"description",
|
|
296
|
+
caseId,
|
|
297
|
+
"stable-content",
|
|
298
|
+
`${areaRef}\u0000${value.toLocaleLowerCase("en-US")}`,
|
|
299
|
+
),
|
|
300
|
+
kind: "stable-content",
|
|
301
|
+
value: resolved(
|
|
302
|
+
value,
|
|
303
|
+
sourceFactRefs,
|
|
304
|
+
weakestDescriptionConfidence(matching),
|
|
305
|
+
),
|
|
306
|
+
sourceFactRefs,
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const controlNameUnknownReasonCodes = new Set([
|
|
311
|
+
"INTRINSIC_CONTROL_NAME_UNKNOWN",
|
|
312
|
+
"INTRINSIC_COMPOSE_CONTROL_NAME_UNKNOWN",
|
|
313
|
+
]);
|
|
314
|
+
function sourceControlNameUnresolvedItems(fact, unresolvedById) {
|
|
315
|
+
return uniqueSorted(fact.unresolvedRefs ?? [])
|
|
316
|
+
.map((ref) => unresolvedById.get(ref))
|
|
317
|
+
.filter(
|
|
318
|
+
(item) =>
|
|
319
|
+
item !== undefined &&
|
|
320
|
+
item.ownerRef === fact.id &&
|
|
321
|
+
/^(?:data\.)?(?:controlName|name)$/u.test(item.fieldPath) &&
|
|
322
|
+
controlNameUnknownReasonCodes.has(item.reasonCode),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
function sourceControlNameUnresolvedRef(fact, unresolvedById) {
|
|
326
|
+
const matches = sourceControlNameUnresolvedItems(fact, unresolvedById);
|
|
327
|
+
return matches.length === 1 ? matches[0].id : undefined;
|
|
328
|
+
}
|
|
329
|
+
export function buildAreaGraph(source, options = {}) {
|
|
330
|
+
const componentFacts = factsOfKind(source, "component");
|
|
331
|
+
const componentVariantFacts = factsOfKind(source, "component-variant");
|
|
332
|
+
const uiFacts = factsOfKind(source, "ui-element").filter(
|
|
333
|
+
(fact) => fact.data.analysisRole !== "declaration-template",
|
|
334
|
+
);
|
|
335
|
+
const bindingFacts = factsOfKind(source, "interaction-binding");
|
|
336
|
+
const boundUiFactRefs = new Set(
|
|
337
|
+
bindingFacts.map((binding) => binding.data.elementKey),
|
|
338
|
+
);
|
|
339
|
+
const callbackFacts = factsOfKind(source, "callback");
|
|
340
|
+
const callbackFlowFacts = factsOfKind(source, "callback-flow");
|
|
341
|
+
const effectFacts = factsOfKind(source, "behavior-effect");
|
|
342
|
+
const navigationFacts = factsOfKind(source, "navigation-target");
|
|
343
|
+
const conditionFacts = factsOfKind(source, "condition");
|
|
344
|
+
const conditionById = new Map(
|
|
345
|
+
conditionFacts.map((condition) => [condition.id, condition]),
|
|
346
|
+
);
|
|
347
|
+
const uniqueConditionRefs = (refs) => {
|
|
348
|
+
const selectedByEvidence = new Map();
|
|
349
|
+
for (const ref of refs) {
|
|
350
|
+
const condition = conditionById.get(ref);
|
|
351
|
+
const key = condition
|
|
352
|
+
? JSON.stringify({
|
|
353
|
+
expression: condition.data.expressionAnchor,
|
|
354
|
+
evidence: condition.support
|
|
355
|
+
.map((support) => support.evidenceRef)
|
|
356
|
+
.sort(),
|
|
357
|
+
})
|
|
358
|
+
: ref;
|
|
359
|
+
if (!selectedByEvidence.has(key)) {
|
|
360
|
+
selectedByEvidence.set(key, ref);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return [...selectedByEvidence.values()].sort((left, right) =>
|
|
364
|
+
left.localeCompare(right, "en"),
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
const normalizeConditionAlternatives = (alternatives) => {
|
|
368
|
+
const byConditions = new Map();
|
|
369
|
+
for (const alternative of alternatives) {
|
|
370
|
+
const conditionRefs = uniqueConditionRefs(alternative.conditionRefs);
|
|
371
|
+
const key = conditionRefs.join("\u0000");
|
|
372
|
+
const existing = byConditions.get(key);
|
|
373
|
+
byConditions.set(key, {
|
|
374
|
+
conditionRefs,
|
|
375
|
+
sourceFactRefs: uniqueSorted([
|
|
376
|
+
...(existing?.sourceFactRefs ?? []),
|
|
377
|
+
...alternative.sourceFactRefs,
|
|
378
|
+
]),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return [...byConditions.values()].sort((left, right) =>
|
|
382
|
+
left.conditionRefs
|
|
383
|
+
.join("\u0000")
|
|
384
|
+
.localeCompare(right.conditionRefs.join("\u0000"), "en"),
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
const entryFacts = factsOfKind(source, "entry-point");
|
|
388
|
+
const propertyFacts = factsOfKind(source, "ui-property");
|
|
389
|
+
const containerFacts = factsOfKind(source, "container-binding");
|
|
390
|
+
const unresolved = [...source.unresolved];
|
|
391
|
+
const sourceUnresolvedById = new Map(
|
|
392
|
+
source.unresolved.map((item) => [item.id, item]),
|
|
393
|
+
);
|
|
394
|
+
const callbackTraversal = new CallbackFlowTraversal(callbackFlowFacts, {
|
|
395
|
+
maxCallDepth: options.maxCallDepth,
|
|
396
|
+
maxCallbackIterations: options.maxCallbackIterations,
|
|
397
|
+
conditionFacts,
|
|
398
|
+
});
|
|
399
|
+
const areaByComponent = new Map();
|
|
400
|
+
const areaById = new Map();
|
|
401
|
+
const componentById = new Map(
|
|
402
|
+
componentFacts.map((component) => [component.id, component]),
|
|
403
|
+
);
|
|
404
|
+
const sourceFactById = new Map(source.facts.map((fact) => [fact.id, fact]));
|
|
405
|
+
const sourceEvidenceById = new Map(
|
|
406
|
+
source.evidence.map((item) => [item.id, item]),
|
|
407
|
+
);
|
|
408
|
+
const evidenceRefsForNameSupport = (supportRefs) =>
|
|
409
|
+
uniqueSorted(
|
|
410
|
+
supportRefs.flatMap((supportRef) => {
|
|
411
|
+
const fact = sourceFactById.get(supportRef);
|
|
412
|
+
if (fact) return fact.support.map((support) => support.evidenceRef);
|
|
413
|
+
return sourceEvidenceById.has(supportRef) ? [supportRef] : [];
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
const explicitAppEntryComponentRefs = new Set([
|
|
417
|
+
...componentFacts.flatMap((component) =>
|
|
418
|
+
component.support.some(
|
|
419
|
+
(support) =>
|
|
420
|
+
sourceEvidenceById.get(support.evidenceRef)?.sourceKind === "java" ||
|
|
421
|
+
sourceEvidenceById.get(support.evidenceRef)?.sourceKind === "kotlin",
|
|
422
|
+
)
|
|
423
|
+
? [component.id]
|
|
424
|
+
: [],
|
|
425
|
+
),
|
|
426
|
+
...navigationFacts.flatMap((navigation) =>
|
|
427
|
+
navigation.data.targetKind === "component" && navigation.data.targetKey
|
|
428
|
+
? [navigation.data.targetKey]
|
|
429
|
+
: [],
|
|
430
|
+
),
|
|
431
|
+
...effectFacts.flatMap((effect) =>
|
|
432
|
+
effect.data.effectKind === "navigate-area" && effect.data.targetKey
|
|
433
|
+
? [effect.data.targetKey]
|
|
434
|
+
: [],
|
|
435
|
+
),
|
|
436
|
+
]);
|
|
437
|
+
const ownership = resolveAreaOwnership(componentFacts, containerFacts, {
|
|
438
|
+
explicitAppEntryComponentRefs,
|
|
439
|
+
});
|
|
440
|
+
for (const group of ownership.groups) {
|
|
441
|
+
const fact = group.anchor;
|
|
442
|
+
const id = graphId("area", source.caseId, group.kind, fact.id);
|
|
443
|
+
group.componentRefs.forEach((componentRef) =>
|
|
444
|
+
areaByComponent.set(componentRef, id),
|
|
445
|
+
);
|
|
446
|
+
const componentRefs = new Set(group.componentRefs);
|
|
447
|
+
const titleFacts = propertyFacts
|
|
448
|
+
.filter(
|
|
449
|
+
(property) =>
|
|
450
|
+
componentRefs.has(property.data.ownerKey) &&
|
|
451
|
+
/^(title|label|name)$/iu.test(property.data.property) &&
|
|
452
|
+
typeof property.data.value === "string",
|
|
453
|
+
)
|
|
454
|
+
.sort(
|
|
455
|
+
(left, right) =>
|
|
456
|
+
Number(right.data.ownerKey === fact.id) -
|
|
457
|
+
Number(left.data.ownerKey === fact.id) ||
|
|
458
|
+
left.id.localeCompare(right.id, "en"),
|
|
459
|
+
);
|
|
460
|
+
const nameDecision = resolveNameCandidates([
|
|
461
|
+
...titleFacts.map((title) =>
|
|
462
|
+
literalNameCandidate(
|
|
463
|
+
"visible-title",
|
|
464
|
+
typeof title.data.value === "string" ? title.data.value : undefined,
|
|
465
|
+
[title.id],
|
|
466
|
+
title.confidence,
|
|
467
|
+
{
|
|
468
|
+
frameworkGeneric: title.support.some(
|
|
469
|
+
(support) => support.role === "resolved Compose surface API name",
|
|
470
|
+
),
|
|
471
|
+
},
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
codeNameCandidate(
|
|
475
|
+
"component-identifier",
|
|
476
|
+
fact.data.qualifiedName,
|
|
477
|
+
[fact.id],
|
|
478
|
+
componentIdentifierName,
|
|
479
|
+
),
|
|
480
|
+
]);
|
|
481
|
+
let areaName;
|
|
482
|
+
const areaUnresolvedRefs = uniqueSorted(
|
|
483
|
+
group.componentRefs.flatMap(
|
|
484
|
+
(componentRef) => componentById.get(componentRef)?.unresolvedRefs ?? [],
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
if (
|
|
488
|
+
nameDecision.value !== undefined &&
|
|
489
|
+
nameDecision.confidence !== undefined
|
|
490
|
+
) {
|
|
491
|
+
areaName = resolved(
|
|
492
|
+
nameDecision.value,
|
|
493
|
+
nameDecision.supportRefs,
|
|
494
|
+
nameDecision.confidence,
|
|
495
|
+
);
|
|
496
|
+
} else {
|
|
497
|
+
const unresolvedId = graphId(
|
|
498
|
+
"unresolved",
|
|
499
|
+
source.caseId,
|
|
500
|
+
"area-name",
|
|
501
|
+
fact.id,
|
|
502
|
+
);
|
|
503
|
+
unresolved.push({
|
|
504
|
+
id: unresolvedId,
|
|
505
|
+
ownerRef: id,
|
|
506
|
+
fieldPath: "name",
|
|
507
|
+
reasonCode: "INTRINSIC_AREA_NAME_UNKNOWN",
|
|
508
|
+
message:
|
|
509
|
+
"区域没有可用标题;类名、路由和 Surface 身份候选也均为 raw/hash、抽象 Base 或框架泛称。",
|
|
510
|
+
evidenceRefs: uniqueSorted([
|
|
511
|
+
...fact.support.map((support) => support.evidenceRef),
|
|
512
|
+
...titleFacts.flatMap((title) =>
|
|
513
|
+
title.support.map((support) => support.evidenceRef),
|
|
514
|
+
),
|
|
515
|
+
]),
|
|
516
|
+
candidates: nameDecision.attempts.flatMap((attempt) =>
|
|
517
|
+
attempt.outcome === "rejected" && attempt.rawValue !== undefined
|
|
518
|
+
? [
|
|
519
|
+
{
|
|
520
|
+
value: attempt.rawValue,
|
|
521
|
+
supportRefs: evidenceRefsForNameSupport(attempt.supportRefs),
|
|
522
|
+
...(attempt.reason ? { note: attempt.reason } : {}),
|
|
523
|
+
},
|
|
524
|
+
]
|
|
525
|
+
: [],
|
|
526
|
+
),
|
|
527
|
+
impact: ["req-area-name"],
|
|
528
|
+
originRef: fact.id,
|
|
529
|
+
});
|
|
530
|
+
areaUnresolvedRefs.push(unresolvedId);
|
|
531
|
+
areaName = { status: "unresolved", unresolvedRef: unresolvedId };
|
|
532
|
+
}
|
|
533
|
+
areaById.set(id, {
|
|
534
|
+
id,
|
|
535
|
+
identityAnchors: uniqueSorted(
|
|
536
|
+
group.componentRefs.flatMap((componentRef) => {
|
|
537
|
+
const component = componentById.get(componentRef);
|
|
538
|
+
return component ? [component.id, component.data.qualifiedName] : [];
|
|
539
|
+
}),
|
|
540
|
+
),
|
|
541
|
+
kind: group.kind,
|
|
542
|
+
name: areaName,
|
|
543
|
+
descriptionFacts: stableDescriptionFacts(
|
|
544
|
+
source.caseId,
|
|
545
|
+
id,
|
|
546
|
+
componentRefs,
|
|
547
|
+
areaName,
|
|
548
|
+
uiFacts,
|
|
549
|
+
propertyFacts,
|
|
550
|
+
boundUiFactRefs,
|
|
551
|
+
),
|
|
552
|
+
controlRefs: [],
|
|
553
|
+
conditions: conditionFacts
|
|
554
|
+
.filter((condition) => componentRefs.has(condition.data.ownerKey))
|
|
555
|
+
.map((condition) => condition.id)
|
|
556
|
+
.sort(),
|
|
557
|
+
sourceFactRefs: uniqueSorted([
|
|
558
|
+
...group.componentRefs,
|
|
559
|
+
...group.containerBindingRefs,
|
|
560
|
+
]),
|
|
561
|
+
unresolvedRefs: uniqueSorted(areaUnresolvedRefs),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
const controls = [];
|
|
565
|
+
const controlByUi = new Map();
|
|
566
|
+
for (const fact of uiFacts) {
|
|
567
|
+
if (!fact.data.actionable && !boundUiFactRefs.has(fact.id)) continue;
|
|
568
|
+
const areaRef = areaByComponent.get(fact.data.ownerKey);
|
|
569
|
+
if (!areaRef) continue;
|
|
570
|
+
const id = graphId(
|
|
571
|
+
"control",
|
|
572
|
+
source.caseId,
|
|
573
|
+
fact.data.elementType,
|
|
574
|
+
fact.id,
|
|
575
|
+
);
|
|
576
|
+
controlByUi.set(fact.id, id);
|
|
577
|
+
const ownerArea = areaById.get(areaRef);
|
|
578
|
+
const areaName =
|
|
579
|
+
ownerArea?.name.status === "resolved" ? ownerArea.name.value : undefined;
|
|
580
|
+
const areaNameSupport =
|
|
581
|
+
ownerArea?.name.status === "resolved" ? ownerArea.name.supportRefs : [];
|
|
582
|
+
const repeatedItemName =
|
|
583
|
+
(fact.data.elementType === "list-item" ||
|
|
584
|
+
fact.data.elementType === "repeated-row") &&
|
|
585
|
+
fact.data.dynamicIdentity
|
|
586
|
+
? repeatedControlName(areaName)
|
|
587
|
+
: undefined;
|
|
588
|
+
const nameDecision = resolveNameCandidates(
|
|
589
|
+
controlNameCandidates(
|
|
590
|
+
fact.data,
|
|
591
|
+
fact.id,
|
|
592
|
+
fact.confidence,
|
|
593
|
+
repeatedItemName,
|
|
594
|
+
areaNameSupport,
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
const sourceNameUnresolvedItems = sourceControlNameUnresolvedItems(
|
|
598
|
+
fact,
|
|
599
|
+
sourceUnresolvedById,
|
|
600
|
+
);
|
|
601
|
+
const resolvedSourceNameUnresolvedRefs = new Set(
|
|
602
|
+
nameDecision.value !== undefined
|
|
603
|
+
? sourceNameUnresolvedItems.map((item) => item.id)
|
|
604
|
+
: [],
|
|
605
|
+
);
|
|
606
|
+
const unresolvedRefs = (fact.unresolvedRefs ?? []).filter(
|
|
607
|
+
(ref) => !resolvedSourceNameUnresolvedRefs.has(ref),
|
|
608
|
+
);
|
|
609
|
+
let supportedName;
|
|
610
|
+
if (
|
|
611
|
+
nameDecision.value !== undefined &&
|
|
612
|
+
nameDecision.confidence !== undefined
|
|
613
|
+
) {
|
|
614
|
+
for (let index = unresolved.length - 1; index >= 0; index -= 1) {
|
|
615
|
+
if (resolvedSourceNameUnresolvedRefs.has(unresolved[index].id)) {
|
|
616
|
+
unresolved.splice(index, 1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
supportedName = resolved(
|
|
620
|
+
nameDecision.value,
|
|
621
|
+
nameDecision.supportRefs,
|
|
622
|
+
nameDecision.confidence,
|
|
623
|
+
);
|
|
624
|
+
} else {
|
|
625
|
+
const sourceUnresolvedRef = sourceControlNameUnresolvedRef(
|
|
626
|
+
fact,
|
|
627
|
+
sourceUnresolvedById,
|
|
628
|
+
);
|
|
629
|
+
const unresolvedId =
|
|
630
|
+
sourceUnresolvedRef ??
|
|
631
|
+
graphId("unresolved", source.caseId, "control-name", fact.id);
|
|
632
|
+
if (!sourceUnresolvedRef) {
|
|
633
|
+
unresolved.push({
|
|
634
|
+
id: unresolvedId,
|
|
635
|
+
ownerRef: id,
|
|
636
|
+
fieldPath: "name",
|
|
637
|
+
reasonCode: "INTRINSIC_CONTROL_NAME_UNKNOWN",
|
|
638
|
+
message: "控件没有可见文字、无障碍标签或可证明的图标语义。",
|
|
639
|
+
evidenceRefs: fact.support.map((item) => item.evidenceRef),
|
|
640
|
+
candidates: nameDecision.attempts.flatMap((attempt) =>
|
|
641
|
+
attempt.outcome === "rejected" && attempt.rawValue !== undefined
|
|
642
|
+
? [
|
|
643
|
+
{
|
|
644
|
+
value: attempt.rawValue,
|
|
645
|
+
supportRefs: evidenceRefsForNameSupport(
|
|
646
|
+
attempt.supportRefs,
|
|
647
|
+
),
|
|
648
|
+
...(attempt.reason ? { note: attempt.reason } : {}),
|
|
649
|
+
},
|
|
650
|
+
]
|
|
651
|
+
: [],
|
|
652
|
+
),
|
|
653
|
+
impact: ["req-feature-name"],
|
|
654
|
+
originRef: fact.id,
|
|
655
|
+
});
|
|
656
|
+
unresolvedRefs.push(unresolvedId);
|
|
657
|
+
}
|
|
658
|
+
supportedName = { status: "unresolved", unresolvedRef: unresolvedId };
|
|
659
|
+
}
|
|
660
|
+
const ownedControlConditions = conditionFacts.filter(
|
|
661
|
+
(condition) => condition.data.ownerKey === fact.id,
|
|
662
|
+
);
|
|
663
|
+
const visibilityConditions = ownedControlConditions
|
|
664
|
+
.filter((condition) => condition.data.conditionRole === "visibility")
|
|
665
|
+
.map((condition) => condition.id)
|
|
666
|
+
.sort((left, right) => left.localeCompare(right, "en"));
|
|
667
|
+
const enabledConditions = ownedControlConditions
|
|
668
|
+
.filter((condition) => condition.data.conditionRole !== "visibility")
|
|
669
|
+
.map((condition) => condition.id)
|
|
670
|
+
.sort((left, right) => left.localeCompare(right, "en"));
|
|
671
|
+
const enabled = staticEnabledFor(fact.id, propertyFacts);
|
|
672
|
+
const control = {
|
|
673
|
+
id,
|
|
674
|
+
areaRef,
|
|
675
|
+
kind: fact.data.elementType,
|
|
676
|
+
name: supportedName,
|
|
677
|
+
...(enabled ? { enabled } : {}),
|
|
678
|
+
actionRefs: [],
|
|
679
|
+
visibilityConditions,
|
|
680
|
+
enabledConditions,
|
|
681
|
+
sourceFactRefs: uniqueSorted([
|
|
682
|
+
fact.id,
|
|
683
|
+
...visibilityConditions,
|
|
684
|
+
...enabledConditions,
|
|
685
|
+
...(enabled?.status === "resolved" ? enabled.supportRefs : []),
|
|
686
|
+
]),
|
|
687
|
+
unresolvedRefs: [...new Set(unresolvedRefs)].sort(),
|
|
688
|
+
};
|
|
689
|
+
controls.push(control);
|
|
690
|
+
const area = areaById.get(areaRef);
|
|
691
|
+
if (area) {
|
|
692
|
+
areaById.set(areaRef, {
|
|
693
|
+
...area,
|
|
694
|
+
controlRefs: [...area.controlRefs, id].sort(),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const actions = [];
|
|
699
|
+
const effects = [];
|
|
700
|
+
const edges = [];
|
|
701
|
+
const externalEndpoints = [];
|
|
702
|
+
const actionByBinding = new Map();
|
|
703
|
+
const effectGraphByFact = new Map();
|
|
704
|
+
const navigationByEffect = new Map(
|
|
705
|
+
navigationFacts.map((fact) => [fact.data.effectKey, fact]),
|
|
706
|
+
);
|
|
707
|
+
const exactTargetEffectSemanticKey = (effect) => {
|
|
708
|
+
if (!effect.data.targetKey) return undefined;
|
|
709
|
+
const effectData = Object.fromEntries(
|
|
710
|
+
Object.entries(effect.data).filter(([key]) => key !== "conditionKeys"),
|
|
711
|
+
);
|
|
712
|
+
const navigation = navigationByEffect.get(effect.id);
|
|
713
|
+
const navigationData = navigation
|
|
714
|
+
? Object.fromEntries(
|
|
715
|
+
Object.entries(navigation.data).filter(
|
|
716
|
+
([key]) => key !== "effectKey",
|
|
717
|
+
),
|
|
718
|
+
)
|
|
719
|
+
: undefined;
|
|
720
|
+
return canonicalSemanticValue({
|
|
721
|
+
effectData,
|
|
722
|
+
confidence: effect.confidence,
|
|
723
|
+
unresolvedRefs: uniqueSorted(effect.unresolvedRefs ?? []),
|
|
724
|
+
...(navigation
|
|
725
|
+
? {
|
|
726
|
+
navigation: {
|
|
727
|
+
data: navigationData,
|
|
728
|
+
confidence: navigation.confidence,
|
|
729
|
+
unresolvedRefs: uniqueSorted(navigation.unresolvedRefs ?? []),
|
|
730
|
+
},
|
|
731
|
+
}
|
|
732
|
+
: {}),
|
|
733
|
+
});
|
|
734
|
+
};
|
|
735
|
+
const hasResolvedAreaTarget = (effect) => {
|
|
736
|
+
if (effect.data.targetKey && areaByComponent.has(effect.data.targetKey))
|
|
737
|
+
return true;
|
|
738
|
+
const navigation = navigationByEffect.get(effect.id);
|
|
739
|
+
if (
|
|
740
|
+
navigation?.data.targetKind === "component" &&
|
|
741
|
+
navigation.data.targetKey &&
|
|
742
|
+
areaByComponent.has(navigation.data.targetKey)
|
|
743
|
+
)
|
|
744
|
+
return true;
|
|
745
|
+
return Boolean(
|
|
746
|
+
navigation?.data.targetKind === "route" &&
|
|
747
|
+
navigation.data.uri &&
|
|
748
|
+
resolveComposeRouteArea(
|
|
749
|
+
navigation.data.uri,
|
|
750
|
+
componentFacts,
|
|
751
|
+
areaByComponent,
|
|
752
|
+
).areaRef,
|
|
753
|
+
);
|
|
754
|
+
};
|
|
755
|
+
const addUnknownEffectForAction = (
|
|
756
|
+
action,
|
|
757
|
+
originRef,
|
|
758
|
+
sourceFactRefs,
|
|
759
|
+
evidenceRefs,
|
|
760
|
+
message,
|
|
761
|
+
) => {
|
|
762
|
+
const effectId = graphId("effect", source.caseId, "unknown", action.id);
|
|
763
|
+
const unresolvedId = graphId(
|
|
764
|
+
"unresolved",
|
|
765
|
+
source.caseId,
|
|
766
|
+
"control-action",
|
|
767
|
+
originRef,
|
|
768
|
+
);
|
|
769
|
+
unresolved.push({
|
|
770
|
+
id: unresolvedId,
|
|
771
|
+
ownerRef: action.id,
|
|
772
|
+
fieldPath: "effectRefs",
|
|
773
|
+
reasonCode: "INTRINSIC_CONTROL_ACTION_UNKNOWN",
|
|
774
|
+
message,
|
|
775
|
+
evidenceRefs: uniqueSorted(evidenceRefs),
|
|
776
|
+
candidates: [],
|
|
777
|
+
impact: ["req-feature-effect"],
|
|
778
|
+
originRef,
|
|
779
|
+
});
|
|
780
|
+
effects.push({
|
|
781
|
+
id: effectId,
|
|
782
|
+
actionRef: action.id,
|
|
783
|
+
kind: "unknown",
|
|
784
|
+
description: { status: "unresolved", unresolvedRef: unresolvedId },
|
|
785
|
+
sourceFactRefs: uniqueSorted(sourceFactRefs),
|
|
786
|
+
unresolvedRefs: [unresolvedId],
|
|
787
|
+
});
|
|
788
|
+
return { effectId, unresolvedId };
|
|
789
|
+
};
|
|
790
|
+
const addEffectForAction = (
|
|
791
|
+
action,
|
|
792
|
+
sourceEffect,
|
|
793
|
+
alternatives,
|
|
794
|
+
equivalentSourceEffects = [sourceEffect],
|
|
795
|
+
) => {
|
|
796
|
+
const equivalentEffectRefs = uniqueSorted(
|
|
797
|
+
equivalentSourceEffects.map((effect) => effect.id),
|
|
798
|
+
);
|
|
799
|
+
const equivalentNavigations = equivalentSourceEffects.flatMap((effect) => {
|
|
800
|
+
const candidate = navigationByEffect.get(effect.id);
|
|
801
|
+
return candidate ? [candidate] : [];
|
|
802
|
+
});
|
|
803
|
+
const equivalentNavigationRefs = uniqueSorted(
|
|
804
|
+
equivalentNavigations.map((candidate) => candidate.id),
|
|
805
|
+
);
|
|
806
|
+
const normalizedAlternatives = normalizeConditionAlternatives(alternatives);
|
|
807
|
+
const flowFactRefs = uniqueSorted(
|
|
808
|
+
normalizedAlternatives.flatMap(
|
|
809
|
+
(alternative) => alternative.sourceFactRefs,
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
const conditionRefs = uniqueConditionRefs(
|
|
813
|
+
normalizedAlternatives.flatMap(
|
|
814
|
+
(alternative) => alternative.conditionRefs,
|
|
815
|
+
),
|
|
816
|
+
);
|
|
817
|
+
const id = graphId(
|
|
818
|
+
"effect",
|
|
819
|
+
source.caseId,
|
|
820
|
+
sourceEffect.data.effectKind,
|
|
821
|
+
`${action.id}:${sourceEffect.id}`,
|
|
822
|
+
);
|
|
823
|
+
for (const equivalent of equivalentSourceEffects) {
|
|
824
|
+
effectGraphByFact.set(equivalent.id, id);
|
|
825
|
+
}
|
|
826
|
+
const navigation = navigationByEffect.get(sourceEffect.id);
|
|
827
|
+
const routeResolution =
|
|
828
|
+
!sourceEffect.data.targetKey &&
|
|
829
|
+
navigation?.data.targetKind === "route" &&
|
|
830
|
+
navigation.data.uri
|
|
831
|
+
? resolveComposeRouteArea(
|
|
832
|
+
navigation.data.uri,
|
|
833
|
+
componentFacts,
|
|
834
|
+
areaByComponent,
|
|
835
|
+
)
|
|
836
|
+
: undefined;
|
|
837
|
+
const targetArea = sourceEffect.data.targetKey
|
|
838
|
+
? areaByComponent.get(sourceEffect.data.targetKey)
|
|
839
|
+
: routeResolution?.areaRef;
|
|
840
|
+
const targetComponent = sourceEffect.data.targetKey
|
|
841
|
+
? componentFacts.find((fact) => fact.id === sourceEffect.data.targetKey)
|
|
842
|
+
: undefined;
|
|
843
|
+
const targetComponentName = targetComponent
|
|
844
|
+
? resolveNameCandidates([
|
|
845
|
+
codeNameCandidate(
|
|
846
|
+
"component-identifier",
|
|
847
|
+
targetComponent.data.qualifiedName,
|
|
848
|
+
[targetComponent.id],
|
|
849
|
+
componentIdentifierName,
|
|
850
|
+
),
|
|
851
|
+
])
|
|
852
|
+
: undefined;
|
|
853
|
+
const externalTargetName = androidExternalTargetName(
|
|
854
|
+
navigation?.data.intentAction,
|
|
855
|
+
);
|
|
856
|
+
const targetName = targetArea
|
|
857
|
+
? areaById.get(targetArea)?.name
|
|
858
|
+
: targetComponentName?.value !== undefined &&
|
|
859
|
+
targetComponentName.confidence !== undefined
|
|
860
|
+
? resolved(
|
|
861
|
+
targetComponentName.value,
|
|
862
|
+
targetComponentName.supportRefs,
|
|
863
|
+
targetComponentName.confidence,
|
|
864
|
+
)
|
|
865
|
+
: navigation?.data.presentation === "chooser"
|
|
866
|
+
? resolved(
|
|
867
|
+
"Android 系统选择器",
|
|
868
|
+
[navigation.id],
|
|
869
|
+
navigation.confidence,
|
|
870
|
+
)
|
|
871
|
+
: externalTargetName && navigation
|
|
872
|
+
? resolved(
|
|
873
|
+
externalTargetName,
|
|
874
|
+
[navigation.id],
|
|
875
|
+
navigation.confidence,
|
|
876
|
+
)
|
|
877
|
+
: undefined;
|
|
878
|
+
const inheritedNavigationUnresolvedRefs = uniqueSorted([
|
|
879
|
+
...equivalentSourceEffects.flatMap(
|
|
880
|
+
(effect) => effect.unresolvedRefs ?? [],
|
|
881
|
+
),
|
|
882
|
+
...equivalentNavigations.flatMap(
|
|
883
|
+
(candidate) => candidate.unresolvedRefs ?? [],
|
|
884
|
+
),
|
|
885
|
+
]);
|
|
886
|
+
const effectUnresolvedRefs = [...inheritedNavigationUnresolvedRefs];
|
|
887
|
+
const hasResolvedValue = Object.prototype.hasOwnProperty.call(
|
|
888
|
+
sourceEffect.data,
|
|
889
|
+
"value",
|
|
890
|
+
);
|
|
891
|
+
const hasInheritedFeedbackValueUnknown =
|
|
892
|
+
inheritedNavigationUnresolvedRefs.some((unresolvedRef) => {
|
|
893
|
+
const item = sourceUnresolvedById.get(unresolvedRef);
|
|
894
|
+
return (
|
|
895
|
+
item !== undefined &&
|
|
896
|
+
(/feedback|snackbar|toast/iu.test(item.reasonCode) ||
|
|
897
|
+
/(?:value|text|message)$/iu.test(item.fieldPath))
|
|
898
|
+
);
|
|
899
|
+
});
|
|
900
|
+
if (
|
|
901
|
+
sourceEffect.data.effectKind === "show-feedback" &&
|
|
902
|
+
!hasResolvedValue &&
|
|
903
|
+
!hasInheritedFeedbackValueUnknown
|
|
904
|
+
) {
|
|
905
|
+
const unresolvedId = graphId(
|
|
906
|
+
"unresolved",
|
|
907
|
+
source.caseId,
|
|
908
|
+
"feedback-value",
|
|
909
|
+
sourceEffect.id,
|
|
910
|
+
);
|
|
911
|
+
if (!unresolved.some((item) => item.id === unresolvedId)) {
|
|
912
|
+
unresolved.push({
|
|
913
|
+
id: unresolvedId,
|
|
914
|
+
ownerRef: id,
|
|
915
|
+
fieldPath: "value",
|
|
916
|
+
reasonCode: "INTRINSIC_FEEDBACK_VALUE_UNKNOWN",
|
|
917
|
+
message: "已确认会显示短暂提示,但静态源码不能确定提示内容。",
|
|
918
|
+
evidenceRefs: sourceEffect.support.map((item) => item.evidenceRef),
|
|
919
|
+
candidates: [],
|
|
920
|
+
impact: ["req-feature-effect"],
|
|
921
|
+
originRef: sourceEffect.id,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
effectUnresolvedRefs.push(unresolvedId);
|
|
925
|
+
}
|
|
926
|
+
const supportRefs = uniqueSorted([
|
|
927
|
+
...equivalentEffectRefs,
|
|
928
|
+
...flowFactRefs,
|
|
929
|
+
...equivalentNavigationRefs,
|
|
930
|
+
]);
|
|
931
|
+
effects.push({
|
|
932
|
+
id,
|
|
933
|
+
actionRef: action.id,
|
|
934
|
+
kind: sourceEffect.data.effectKind,
|
|
935
|
+
description: resolved(
|
|
936
|
+
effectText(
|
|
937
|
+
sourceEffect.data,
|
|
938
|
+
targetName?.status === "resolved" ? targetName.value : undefined,
|
|
939
|
+
),
|
|
940
|
+
uniqueSorted([...equivalentEffectRefs, ...equivalentNavigationRefs]),
|
|
941
|
+
sourceEffect.confidence,
|
|
942
|
+
),
|
|
943
|
+
...(normalizedAlternatives.some(
|
|
944
|
+
(alternative) => alternative.conditionRefs.length > 0,
|
|
945
|
+
)
|
|
946
|
+
? { conditionAlternatives: normalizedAlternatives }
|
|
947
|
+
: {}),
|
|
948
|
+
...(sourceEffect.data.closePolicy
|
|
949
|
+
? { closePolicy: sourceEffect.data.closePolicy }
|
|
950
|
+
: {}),
|
|
951
|
+
...(sourceEffect.data.transitionMode
|
|
952
|
+
? { transitionMode: sourceEffect.data.transitionMode }
|
|
953
|
+
: {}),
|
|
954
|
+
...(sourceEffect.data.stateKey
|
|
955
|
+
? {
|
|
956
|
+
stateKey: resolved(
|
|
957
|
+
stateKeyDisplayName(sourceEffect.data.stateKey),
|
|
958
|
+
equivalentEffectRefs,
|
|
959
|
+
sourceEffect.confidence,
|
|
960
|
+
),
|
|
961
|
+
}
|
|
962
|
+
: {}),
|
|
963
|
+
...(hasResolvedValue
|
|
964
|
+
? {
|
|
965
|
+
value: resolved(
|
|
966
|
+
sourceEffect.data.value ?? null,
|
|
967
|
+
equivalentEffectRefs,
|
|
968
|
+
sourceEffect.confidence,
|
|
969
|
+
),
|
|
970
|
+
}
|
|
971
|
+
: {}),
|
|
972
|
+
sourceFactRefs: supportRefs,
|
|
973
|
+
unresolvedRefs: uniqueSorted(effectUnresolvedRefs),
|
|
974
|
+
});
|
|
975
|
+
if (
|
|
976
|
+
sourceEffect.data.effectKind !== "navigate-area" &&
|
|
977
|
+
sourceEffect.data.effectKind !== "show-surface" &&
|
|
978
|
+
sourceEffect.data.effectKind !== "open-external" &&
|
|
979
|
+
navigation?.data.targetKind !== "external"
|
|
980
|
+
) {
|
|
981
|
+
return id;
|
|
982
|
+
}
|
|
983
|
+
let target;
|
|
984
|
+
if (targetArea) {
|
|
985
|
+
target = { kind: "area", ref: targetArea };
|
|
986
|
+
} else if (
|
|
987
|
+
sourceEffect.data.effectKind === "open-external" ||
|
|
988
|
+
navigation?.data.targetKind === "external"
|
|
989
|
+
) {
|
|
990
|
+
const endpointSemanticKey = [
|
|
991
|
+
navigation?.data.presentation ?? "direct",
|
|
992
|
+
navigation?.data.intentAction ?? "external",
|
|
993
|
+
navigation?.data.mimeType ?? "",
|
|
994
|
+
navigation?.data.uri ?? targetComponent?.data.qualifiedName ?? "",
|
|
995
|
+
].join("\u0000");
|
|
996
|
+
const endpointId = graphId(
|
|
997
|
+
"external",
|
|
998
|
+
source.caseId,
|
|
999
|
+
"endpoint",
|
|
1000
|
+
endpointSemanticKey,
|
|
1001
|
+
);
|
|
1002
|
+
const existingEndpoint = externalEndpoints.findIndex(
|
|
1003
|
+
(item) => item.id === endpointId,
|
|
1004
|
+
);
|
|
1005
|
+
if (existingEndpoint < 0) {
|
|
1006
|
+
externalEndpoints.push({
|
|
1007
|
+
id: endpointId,
|
|
1008
|
+
kind:
|
|
1009
|
+
navigation?.data.presentation === "chooser"
|
|
1010
|
+
? "android-intent-chooser"
|
|
1011
|
+
: (navigation?.data.intentAction ?? "external"),
|
|
1012
|
+
name:
|
|
1013
|
+
targetName ??
|
|
1014
|
+
resolved(
|
|
1015
|
+
navigation?.data.uri ?? "App 外目标",
|
|
1016
|
+
[navigation?.id ?? sourceEffect.id],
|
|
1017
|
+
"medium",
|
|
1018
|
+
),
|
|
1019
|
+
...(navigation?.data.presentation
|
|
1020
|
+
? { presentation: navigation.data.presentation }
|
|
1021
|
+
: {}),
|
|
1022
|
+
...(navigation?.data.intentAction
|
|
1023
|
+
? {
|
|
1024
|
+
intentAction: resolved(
|
|
1025
|
+
navigation.data.intentAction,
|
|
1026
|
+
[navigation.id],
|
|
1027
|
+
navigation.confidence,
|
|
1028
|
+
),
|
|
1029
|
+
}
|
|
1030
|
+
: {}),
|
|
1031
|
+
...(navigation?.data.mimeType
|
|
1032
|
+
? {
|
|
1033
|
+
mimeType: resolved(
|
|
1034
|
+
navigation.data.mimeType,
|
|
1035
|
+
[navigation.id],
|
|
1036
|
+
navigation.confidence,
|
|
1037
|
+
),
|
|
1038
|
+
}
|
|
1039
|
+
: {}),
|
|
1040
|
+
...(navigation?.data.uri
|
|
1041
|
+
? {
|
|
1042
|
+
uri: resolved(
|
|
1043
|
+
navigation.data.uri,
|
|
1044
|
+
[navigation.id],
|
|
1045
|
+
navigation.confidence,
|
|
1046
|
+
),
|
|
1047
|
+
}
|
|
1048
|
+
: {}),
|
|
1049
|
+
sourceFactRefs: [
|
|
1050
|
+
...equivalentEffectRefs,
|
|
1051
|
+
...equivalentNavigationRefs,
|
|
1052
|
+
].sort(),
|
|
1053
|
+
unresolvedRefs: [],
|
|
1054
|
+
});
|
|
1055
|
+
} else {
|
|
1056
|
+
externalEndpoints[existingEndpoint] = {
|
|
1057
|
+
...externalEndpoints[existingEndpoint],
|
|
1058
|
+
sourceFactRefs: uniqueSorted([
|
|
1059
|
+
...externalEndpoints[existingEndpoint].sourceFactRefs,
|
|
1060
|
+
...equivalentEffectRefs,
|
|
1061
|
+
...equivalentNavigationRefs,
|
|
1062
|
+
]),
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
target = { kind: "external", ref: endpointId };
|
|
1066
|
+
} else {
|
|
1067
|
+
const inheritedUnresolvedId =
|
|
1068
|
+
inheritedNavigationUnresolvedRefs.length === 1
|
|
1069
|
+
? inheritedNavigationUnresolvedRefs[0]
|
|
1070
|
+
: undefined;
|
|
1071
|
+
const unresolvedId =
|
|
1072
|
+
inheritedUnresolvedId ??
|
|
1073
|
+
graphId("unresolved", source.caseId, "edge-target", sourceEffect.id);
|
|
1074
|
+
if (!inheritedUnresolvedId) {
|
|
1075
|
+
unresolved.push({
|
|
1076
|
+
id: unresolvedId,
|
|
1077
|
+
ownerRef: id,
|
|
1078
|
+
fieldPath: "target",
|
|
1079
|
+
reasonCode: "INTRINSIC_NAVIGATION_TARGET_UNKNOWN",
|
|
1080
|
+
message: "静态源码不能确定该导航效果的目标。",
|
|
1081
|
+
evidenceRefs: sourceEffect.support.map((item) => item.evidenceRef),
|
|
1082
|
+
candidates: (routeResolution?.candidateAreaRefs ?? []).map(
|
|
1083
|
+
(value) => ({
|
|
1084
|
+
value,
|
|
1085
|
+
supportRefs: navigation
|
|
1086
|
+
? navigation.support.map((item) => item.evidenceRef)
|
|
1087
|
+
: [],
|
|
1088
|
+
}),
|
|
1089
|
+
),
|
|
1090
|
+
impact: ["area-path"],
|
|
1091
|
+
originRef: sourceEffect.id,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
target = { status: "unresolved", unresolvedRef: unresolvedId };
|
|
1095
|
+
}
|
|
1096
|
+
const control = controls.find((item) => item.id === action.controlRef);
|
|
1097
|
+
if (control) {
|
|
1098
|
+
edges.push({
|
|
1099
|
+
id: graphId(
|
|
1100
|
+
"edge",
|
|
1101
|
+
source.caseId,
|
|
1102
|
+
sourceEffect.data.effectKind,
|
|
1103
|
+
`${control.areaRef}:${id}`,
|
|
1104
|
+
),
|
|
1105
|
+
sourceAreaRef: control.areaRef,
|
|
1106
|
+
controlRef: control.id,
|
|
1107
|
+
actionRef: action.id,
|
|
1108
|
+
target,
|
|
1109
|
+
conditions: uniqueConditionRefs([
|
|
1110
|
+
...conditionRefs,
|
|
1111
|
+
...(areaById.get(control.areaRef)?.conditions ?? []),
|
|
1112
|
+
]),
|
|
1113
|
+
...(normalizedAlternatives.some(
|
|
1114
|
+
(alternative) => alternative.conditionRefs.length > 0,
|
|
1115
|
+
)
|
|
1116
|
+
? { conditionAlternatives: normalizedAlternatives }
|
|
1117
|
+
: {}),
|
|
1118
|
+
preconditions: uniqueConditionRefs([
|
|
1119
|
+
...control.visibilityConditions.filter(
|
|
1120
|
+
(conditionRef) =>
|
|
1121
|
+
conditionById.get(conditionRef)?.data.pathRequirement ===
|
|
1122
|
+
"runtime-availability",
|
|
1123
|
+
),
|
|
1124
|
+
...control.enabledConditions,
|
|
1125
|
+
]),
|
|
1126
|
+
sourceFactRefs: uniqueSorted([
|
|
1127
|
+
...equivalentEffectRefs,
|
|
1128
|
+
...flowFactRefs,
|
|
1129
|
+
...equivalentNavigationRefs,
|
|
1130
|
+
]),
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
return id;
|
|
1134
|
+
};
|
|
1135
|
+
for (const binding of [...bindingFacts].sort((left, right) =>
|
|
1136
|
+
left.id.localeCompare(right.id, "en"),
|
|
1137
|
+
)) {
|
|
1138
|
+
const controlRef = controlByUi.get(binding.data.elementKey);
|
|
1139
|
+
if (!controlRef) continue;
|
|
1140
|
+
const reachableCallbacks = callbackTraversal.resolve(
|
|
1141
|
+
binding.id,
|
|
1142
|
+
binding.data.callbackKey,
|
|
1143
|
+
);
|
|
1144
|
+
const reachableByCallback = new Map();
|
|
1145
|
+
for (const callback of reachableCallbacks) {
|
|
1146
|
+
const alternatives = reachableByCallback.get(callback.callbackKey) ?? [];
|
|
1147
|
+
alternatives.push(callback);
|
|
1148
|
+
reachableByCallback.set(callback.callbackKey, alternatives);
|
|
1149
|
+
}
|
|
1150
|
+
const callbackKeys = new Set(reachableByCallback.keys());
|
|
1151
|
+
const flowFactRefs = uniqueSorted(
|
|
1152
|
+
reachableCallbacks.flatMap((callback) => callback.flowFactRefs),
|
|
1153
|
+
);
|
|
1154
|
+
const actionId = graphId(
|
|
1155
|
+
"action",
|
|
1156
|
+
source.caseId,
|
|
1157
|
+
binding.data.trigger,
|
|
1158
|
+
binding.id,
|
|
1159
|
+
);
|
|
1160
|
+
actionByBinding.set(binding.id, actionId);
|
|
1161
|
+
const owningControl = controls.find((control) => control.id === controlRef);
|
|
1162
|
+
const controlEnabledConditions = owningControl?.enabledConditions ?? [];
|
|
1163
|
+
const ownerArea = owningControl
|
|
1164
|
+
? areaById.get(owningControl.areaRef)
|
|
1165
|
+
: undefined;
|
|
1166
|
+
const activationScopedAreaKinds = new Set([
|
|
1167
|
+
"bottom-sheet",
|
|
1168
|
+
"dialog",
|
|
1169
|
+
"drawer",
|
|
1170
|
+
"overlay",
|
|
1171
|
+
"pager-page",
|
|
1172
|
+
"popup",
|
|
1173
|
+
]);
|
|
1174
|
+
const areaActivationExpressions =
|
|
1175
|
+
ownerArea && activationScopedAreaKinds.has(ownerArea.kind)
|
|
1176
|
+
? new Set(
|
|
1177
|
+
ownerArea.conditions.flatMap((conditionRef) => {
|
|
1178
|
+
const expression = conditionById
|
|
1179
|
+
.get(conditionRef)
|
|
1180
|
+
?.data.expressionAnchor.trim();
|
|
1181
|
+
return expression ? [expression] : [];
|
|
1182
|
+
}),
|
|
1183
|
+
)
|
|
1184
|
+
: new Set();
|
|
1185
|
+
const compatibleReachabilitiesByEffect = new Map();
|
|
1186
|
+
const compatibleActionEffects = effectFacts.filter((effect) => {
|
|
1187
|
+
if (!callbackKeys.has(effect.data.callbackKey)) return false;
|
|
1188
|
+
const compatible = (
|
|
1189
|
+
reachableByCallback.get(effect.data.callbackKey) ?? []
|
|
1190
|
+
).filter((reachable) =>
|
|
1191
|
+
callbackTraversal.isCompatibleWithConditions(
|
|
1192
|
+
reachable,
|
|
1193
|
+
effect.data.conditionKeys ?? [],
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1196
|
+
if (compatible.length === 0) return false;
|
|
1197
|
+
compatibleReachabilitiesByEffect.set(effect.id, compatible);
|
|
1198
|
+
return true;
|
|
1199
|
+
});
|
|
1200
|
+
const compatibleEffectById = new Map(
|
|
1201
|
+
compatibleActionEffects.map((effect) => [effect.id, effect]),
|
|
1202
|
+
);
|
|
1203
|
+
const supersededImplementationEffects = new Set();
|
|
1204
|
+
for (const summary of compatibleActionEffects) {
|
|
1205
|
+
if (
|
|
1206
|
+
summary.data.effectKind !== "navigate-area" ||
|
|
1207
|
+
!hasResolvedAreaTarget(summary)
|
|
1208
|
+
)
|
|
1209
|
+
continue;
|
|
1210
|
+
for (const relatedRef of summary.relatedFactRefs ?? []) {
|
|
1211
|
+
const implementation = compatibleEffectById.get(relatedRef);
|
|
1212
|
+
if (
|
|
1213
|
+
implementation?.data.effectKind === "navigate-area" &&
|
|
1214
|
+
!hasResolvedAreaTarget(implementation)
|
|
1215
|
+
) {
|
|
1216
|
+
supersededImplementationEffects.add(implementation.id);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const actionEffects = compatibleActionEffects
|
|
1221
|
+
.filter((effect) => !supersededImplementationEffects.has(effect.id))
|
|
1222
|
+
.sort((left, right) => left.id.localeCompare(right.id, "en"));
|
|
1223
|
+
const alternativesByEffect = new Map();
|
|
1224
|
+
for (const effect of actionEffects) {
|
|
1225
|
+
const reachabilities =
|
|
1226
|
+
compatibleReachabilitiesByEffect.get(effect.id) ?? [];
|
|
1227
|
+
const rawAlternatives = reachabilities.map((reachability) => {
|
|
1228
|
+
const callbackPath = new Set(reachability.callbackPath);
|
|
1229
|
+
const conditionRefs =
|
|
1230
|
+
effect.data.conditionKeys !== undefined
|
|
1231
|
+
? uniqueConditionRefs([
|
|
1232
|
+
...reachability.conditionRefs,
|
|
1233
|
+
...effect.data.conditionKeys,
|
|
1234
|
+
]).filter((conditionRef) => {
|
|
1235
|
+
const expression = conditionById
|
|
1236
|
+
.get(conditionRef)
|
|
1237
|
+
?.data.expressionAnchor.trim();
|
|
1238
|
+
return (
|
|
1239
|
+
!expression || !areaActivationExpressions.has(expression)
|
|
1240
|
+
);
|
|
1241
|
+
})
|
|
1242
|
+
: uniqueConditionRefs([
|
|
1243
|
+
...conditionFacts
|
|
1244
|
+
.filter((condition) =>
|
|
1245
|
+
callbackPath.has(condition.data.ownerKey),
|
|
1246
|
+
)
|
|
1247
|
+
.map((condition) => condition.id),
|
|
1248
|
+
...reachability.conditionRefs,
|
|
1249
|
+
]).filter((conditionRef) => {
|
|
1250
|
+
const expression = conditionById
|
|
1251
|
+
.get(conditionRef)
|
|
1252
|
+
?.data.expressionAnchor.trim();
|
|
1253
|
+
return (
|
|
1254
|
+
!expression || !areaActivationExpressions.has(expression)
|
|
1255
|
+
);
|
|
1256
|
+
});
|
|
1257
|
+
return {
|
|
1258
|
+
conditionRefs,
|
|
1259
|
+
sourceFactRefs: reachability.flowFactRefs,
|
|
1260
|
+
};
|
|
1261
|
+
});
|
|
1262
|
+
alternativesByEffect.set(
|
|
1263
|
+
effect.id,
|
|
1264
|
+
normalizeConditionAlternatives(rawAlternatives),
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
const action = {
|
|
1268
|
+
id: actionId,
|
|
1269
|
+
controlRef,
|
|
1270
|
+
trigger: binding.data.trigger,
|
|
1271
|
+
effectRefs: [],
|
|
1272
|
+
/*
|
|
1273
|
+
* Callback-path guards describe whether an individual downstream effect
|
|
1274
|
+
* executes after the interaction. They are not prerequisites for the
|
|
1275
|
+
* user being able to trigger the control. Keep those guards on effects
|
|
1276
|
+
* (and navigation edges); Action preconditions are UI availability only.
|
|
1277
|
+
*/
|
|
1278
|
+
preconditionRefs: uniqueConditionRefs(controlEnabledConditions),
|
|
1279
|
+
sourceFactRefs: uniqueSorted([binding.id, ...flowFactRefs]),
|
|
1280
|
+
};
|
|
1281
|
+
const equivalentEffectGroups = new Map();
|
|
1282
|
+
for (const effect of actionEffects) {
|
|
1283
|
+
const semanticKey = exactTargetEffectSemanticKey(effect);
|
|
1284
|
+
const groupKey =
|
|
1285
|
+
semanticKey === undefined
|
|
1286
|
+
? `fact:${effect.id}`
|
|
1287
|
+
: `exact-target:${semanticKey}`;
|
|
1288
|
+
const group = equivalentEffectGroups.get(groupKey) ?? [];
|
|
1289
|
+
group.push(effect);
|
|
1290
|
+
equivalentEffectGroups.set(groupKey, group);
|
|
1291
|
+
}
|
|
1292
|
+
let effectRefs = [...equivalentEffectGroups.values()].map((group) => {
|
|
1293
|
+
const representative = group[0];
|
|
1294
|
+
return addEffectForAction(
|
|
1295
|
+
action,
|
|
1296
|
+
representative,
|
|
1297
|
+
normalizeConditionAlternatives(
|
|
1298
|
+
group.flatMap((effect) => alternativesByEffect.get(effect.id) ?? []),
|
|
1299
|
+
),
|
|
1300
|
+
group,
|
|
1301
|
+
);
|
|
1302
|
+
});
|
|
1303
|
+
let actionUnresolvedRef;
|
|
1304
|
+
if (effectRefs.length === 0) {
|
|
1305
|
+
const unknown = addUnknownEffectForAction(
|
|
1306
|
+
action,
|
|
1307
|
+
binding.id,
|
|
1308
|
+
action.sourceFactRefs,
|
|
1309
|
+
binding.support.map((item) => item.evidenceRef),
|
|
1310
|
+
"控件已绑定回调,但沿回调调用链没有解析到对应效果。",
|
|
1311
|
+
);
|
|
1312
|
+
effectRefs = [unknown.effectId];
|
|
1313
|
+
actionUnresolvedRef = unknown.unresolvedId;
|
|
1314
|
+
}
|
|
1315
|
+
actions.push({ ...action, effectRefs });
|
|
1316
|
+
const controlIndex = controls.findIndex((item) => item.id === controlRef);
|
|
1317
|
+
if (controlIndex >= 0) {
|
|
1318
|
+
controls[controlIndex] = {
|
|
1319
|
+
...controls[controlIndex],
|
|
1320
|
+
actionRefs: [...controls[controlIndex].actionRefs, actionId].sort(),
|
|
1321
|
+
...(actionUnresolvedRef
|
|
1322
|
+
? {
|
|
1323
|
+
unresolvedRefs: uniqueSorted([
|
|
1324
|
+
...controls[controlIndex].unresolvedRefs,
|
|
1325
|
+
actionUnresolvedRef,
|
|
1326
|
+
]),
|
|
1327
|
+
}
|
|
1328
|
+
: {}),
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
/*
|
|
1333
|
+
* Editable text has one intrinsic user interaction even when source code
|
|
1334
|
+
* only binds a separate submit/editor callback. This is an Android UI
|
|
1335
|
+
* contract, not a guessed business effect: typing changes the field value.
|
|
1336
|
+
*/
|
|
1337
|
+
for (const control of [...controls]) {
|
|
1338
|
+
if (control.kind !== "text-input") continue;
|
|
1339
|
+
const alreadyHasInput = control.actionRefs.some((actionRef) =>
|
|
1340
|
+
actions.some(
|
|
1341
|
+
(action) => action.id === actionRef && action.trigger === "text-input",
|
|
1342
|
+
),
|
|
1343
|
+
);
|
|
1344
|
+
if (alreadyHasInput) continue;
|
|
1345
|
+
const uiFact = uiFacts.find(
|
|
1346
|
+
(fact) => controlByUi.get(fact.id) === control.id,
|
|
1347
|
+
);
|
|
1348
|
+
if (!uiFact) continue;
|
|
1349
|
+
const actionId = graphId(
|
|
1350
|
+
"action",
|
|
1351
|
+
source.caseId,
|
|
1352
|
+
"text-input",
|
|
1353
|
+
`${uiFact.id}:intrinsic-input`,
|
|
1354
|
+
);
|
|
1355
|
+
const effectId = graphId("effect", source.caseId, "input", actionId);
|
|
1356
|
+
actions.push({
|
|
1357
|
+
id: actionId,
|
|
1358
|
+
controlRef: control.id,
|
|
1359
|
+
trigger: "text-input",
|
|
1360
|
+
effectRefs: [effectId],
|
|
1361
|
+
preconditionRefs: [...control.enabledConditions],
|
|
1362
|
+
sourceFactRefs: [uiFact.id],
|
|
1363
|
+
});
|
|
1364
|
+
effects.push({
|
|
1365
|
+
id: effectId,
|
|
1366
|
+
actionRef: actionId,
|
|
1367
|
+
kind: "input",
|
|
1368
|
+
description: resolved(
|
|
1369
|
+
"接收并处理用户输入",
|
|
1370
|
+
[uiFact.id],
|
|
1371
|
+
uiFact.confidence,
|
|
1372
|
+
),
|
|
1373
|
+
sourceFactRefs: [uiFact.id],
|
|
1374
|
+
unresolvedRefs: [],
|
|
1375
|
+
});
|
|
1376
|
+
const index = controls.findIndex((item) => item.id === control.id);
|
|
1377
|
+
controls[index] = {
|
|
1378
|
+
...controls[index],
|
|
1379
|
+
actionRefs: uniqueSorted([...controls[index].actionRefs, actionId]),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
for (const control of [...controls]) {
|
|
1383
|
+
if (control.actionRefs.length > 0) continue;
|
|
1384
|
+
const uiFact = uiFacts.find(
|
|
1385
|
+
(fact) => controlByUi.get(fact.id) === control.id,
|
|
1386
|
+
);
|
|
1387
|
+
if (!uiFact) continue;
|
|
1388
|
+
const actionId = graphId(
|
|
1389
|
+
"action",
|
|
1390
|
+
source.caseId,
|
|
1391
|
+
inferredTrigger(uiFact.data),
|
|
1392
|
+
`${uiFact.id}:unbound`,
|
|
1393
|
+
);
|
|
1394
|
+
const action = {
|
|
1395
|
+
id: actionId,
|
|
1396
|
+
controlRef: control.id,
|
|
1397
|
+
trigger: inferredTrigger(uiFact.data),
|
|
1398
|
+
effectRefs: [],
|
|
1399
|
+
preconditionRefs: [...control.enabledConditions],
|
|
1400
|
+
sourceFactRefs: [uiFact.id],
|
|
1401
|
+
};
|
|
1402
|
+
const unknown = addUnknownEffectForAction(
|
|
1403
|
+
action,
|
|
1404
|
+
uiFact.id,
|
|
1405
|
+
action.sourceFactRefs,
|
|
1406
|
+
uiFact.support.map((item) => item.evidenceRef),
|
|
1407
|
+
"控件可操作,但在所选源码中没有解析到对应回调效果。",
|
|
1408
|
+
);
|
|
1409
|
+
actions.push({ ...action, effectRefs: [unknown.effectId] });
|
|
1410
|
+
const index = controls.findIndex((item) => item.id === control.id);
|
|
1411
|
+
controls[index] = {
|
|
1412
|
+
...controls[index],
|
|
1413
|
+
actionRefs: [actionId],
|
|
1414
|
+
unresolvedRefs: uniqueSorted([
|
|
1415
|
+
...controls[index].unresolvedRefs,
|
|
1416
|
+
unknown.unresolvedId,
|
|
1417
|
+
]),
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
applyComponentVariants({
|
|
1421
|
+
caseId: source.caseId,
|
|
1422
|
+
variants: componentVariantFacts,
|
|
1423
|
+
areaByComponent,
|
|
1424
|
+
areasById: areaById,
|
|
1425
|
+
controls,
|
|
1426
|
+
actions,
|
|
1427
|
+
effects,
|
|
1428
|
+
edges,
|
|
1429
|
+
unresolved,
|
|
1430
|
+
bindings: bindingFacts,
|
|
1431
|
+
conditions: conditionFacts,
|
|
1432
|
+
id: graphId,
|
|
1433
|
+
});
|
|
1434
|
+
/*
|
|
1435
|
+
* A user action that resolves back to its own Area is still a real control
|
|
1436
|
+
* with a real effect, but it is not a transition between graph nodes. This
|
|
1437
|
+
* commonly occurs when the selected Pager tab is clicked again. Remove the
|
|
1438
|
+
* self-loop only after component variants have been expanded so the check
|
|
1439
|
+
* uses final Area identities. Startup settling is derived independently
|
|
1440
|
+
* from source effects below and therefore remains unaffected.
|
|
1441
|
+
*/
|
|
1442
|
+
for (let index = edges.length - 1; index >= 0; index -= 1) {
|
|
1443
|
+
const edge = edges[index];
|
|
1444
|
+
if (
|
|
1445
|
+
!("status" in edge.target) &&
|
|
1446
|
+
edge.target.kind === "area" &&
|
|
1447
|
+
edge.target.ref === edge.sourceAreaRef
|
|
1448
|
+
) {
|
|
1449
|
+
edges.splice(index, 1);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
/*
|
|
1453
|
+
* A close/back API carries no literal destination. When the static graph
|
|
1454
|
+
* proves one or more in-app predecessors, materialize the possible history
|
|
1455
|
+
* transitions from that evidence. Use a snapshot of forward edges so a
|
|
1456
|
+
* generated back edge can never become proof for another generated edge.
|
|
1457
|
+
*
|
|
1458
|
+
* An action can atomically close the current surface and show another one.
|
|
1459
|
+
* That is a replacement transition: it must not also emit a return edge,
|
|
1460
|
+
* and the new surface inherits the replaced surface's predecessors rather
|
|
1461
|
+
* than treating the replaced surface as its back destination.
|
|
1462
|
+
*/
|
|
1463
|
+
const forwardEdges = [...edges];
|
|
1464
|
+
const actionById = new Map(actions.map((action) => [action.id, action]));
|
|
1465
|
+
const effectById = new Map(effects.map((effect) => [effect.id, effect]));
|
|
1466
|
+
const actionHasEffect = (action, kinds) =>
|
|
1467
|
+
action.effectRefs.some((effectRef) => {
|
|
1468
|
+
const effect = effectById.get(effectRef);
|
|
1469
|
+
return effect !== undefined && kinds.has(effect.kind);
|
|
1470
|
+
});
|
|
1471
|
+
const closeKinds = new Set(["close-surface"]);
|
|
1472
|
+
const transitionKinds = new Set([
|
|
1473
|
+
"navigate-area",
|
|
1474
|
+
"show-surface",
|
|
1475
|
+
"open-external",
|
|
1476
|
+
]);
|
|
1477
|
+
const historyProofsFor = (targetAreaRef, visiting = new Set()) => {
|
|
1478
|
+
if (visiting.has(targetAreaRef)) return [];
|
|
1479
|
+
const nextVisiting = new Set([...visiting, targetAreaRef]);
|
|
1480
|
+
const proofs = [];
|
|
1481
|
+
for (const edge of forwardEdges) {
|
|
1482
|
+
if (
|
|
1483
|
+
"status" in edge.target ||
|
|
1484
|
+
edge.target.kind !== "area" ||
|
|
1485
|
+
edge.target.ref !== targetAreaRef ||
|
|
1486
|
+
edge.sourceAreaRef === targetAreaRef
|
|
1487
|
+
)
|
|
1488
|
+
continue;
|
|
1489
|
+
const incomingAction = actionById.get(edge.actionRef);
|
|
1490
|
+
if (incomingAction && actionHasEffect(incomingAction, closeKinds)) {
|
|
1491
|
+
for (const inherited of historyProofsFor(
|
|
1492
|
+
edge.sourceAreaRef,
|
|
1493
|
+
nextVisiting,
|
|
1494
|
+
)) {
|
|
1495
|
+
proofs.push({
|
|
1496
|
+
predecessorAreaRef: inherited.predecessorAreaRef,
|
|
1497
|
+
edges: [...inherited.edges, edge],
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
} else {
|
|
1501
|
+
proofs.push({
|
|
1502
|
+
predecessorAreaRef: edge.sourceAreaRef,
|
|
1503
|
+
edges: [edge],
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const unique = new Map();
|
|
1508
|
+
for (const proof of proofs) {
|
|
1509
|
+
const key = [
|
|
1510
|
+
proof.predecessorAreaRef,
|
|
1511
|
+
...proof.edges.map((edge) => edge.id),
|
|
1512
|
+
].join("\u0000");
|
|
1513
|
+
unique.set(key, proof);
|
|
1514
|
+
}
|
|
1515
|
+
return [...unique.values()];
|
|
1516
|
+
};
|
|
1517
|
+
const closeEffectsByAction = new Map();
|
|
1518
|
+
for (const closeEffect of effects.filter(
|
|
1519
|
+
(effect) =>
|
|
1520
|
+
effect.kind === "close-surface" && effect.closePolicy !== "host-exit",
|
|
1521
|
+
)) {
|
|
1522
|
+
const grouped = closeEffectsByAction.get(closeEffect.actionRef) ?? [];
|
|
1523
|
+
grouped.push(closeEffect);
|
|
1524
|
+
closeEffectsByAction.set(closeEffect.actionRef, grouped);
|
|
1525
|
+
}
|
|
1526
|
+
// One user event may reach several implementation callbacks that all close
|
|
1527
|
+
// the same surface. They remain separate effect evidence, but history has
|
|
1528
|
+
// only one transition for that Action and predecessor Area.
|
|
1529
|
+
for (const [closeActionRef, closeEffects] of closeEffectsByAction) {
|
|
1530
|
+
const action = actions.find((candidate) => candidate.id === closeActionRef);
|
|
1531
|
+
const control = action
|
|
1532
|
+
? controls.find((candidate) => candidate.id === action.controlRef)
|
|
1533
|
+
: undefined;
|
|
1534
|
+
if (!action || !control) continue;
|
|
1535
|
+
if (actionHasEffect(action, transitionKinds)) {
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const proofByPredecessor = new Map();
|
|
1539
|
+
for (const proof of historyProofsFor(control.areaRef)) {
|
|
1540
|
+
const edgesForArea =
|
|
1541
|
+
proofByPredecessor.get(proof.predecessorAreaRef) ?? [];
|
|
1542
|
+
edgesForArea.push(...proof.edges);
|
|
1543
|
+
proofByPredecessor.set(proof.predecessorAreaRef, edgesForArea);
|
|
1544
|
+
}
|
|
1545
|
+
for (const [predecessorAreaRef, proofs] of proofByPredecessor) {
|
|
1546
|
+
edges.push({
|
|
1547
|
+
id: graphId(
|
|
1548
|
+
"edge",
|
|
1549
|
+
source.caseId,
|
|
1550
|
+
"history-back",
|
|
1551
|
+
`${control.areaRef}:${action.id}:${predecessorAreaRef}`,
|
|
1552
|
+
),
|
|
1553
|
+
sourceAreaRef: control.areaRef,
|
|
1554
|
+
controlRef: control.id,
|
|
1555
|
+
actionRef: action.id,
|
|
1556
|
+
target: { kind: "area", ref: predecessorAreaRef },
|
|
1557
|
+
conditions: uniqueConditionRefs(
|
|
1558
|
+
proofs.flatMap((proof) => proof.conditions),
|
|
1559
|
+
),
|
|
1560
|
+
preconditions: uniqueSorted(
|
|
1561
|
+
proofs.flatMap((proof) => proof.preconditions),
|
|
1562
|
+
),
|
|
1563
|
+
sourceFactRefs: uniqueSorted([
|
|
1564
|
+
...closeEffects.flatMap((effect) => effect.sourceFactRefs),
|
|
1565
|
+
...proofs.flatMap((proof) => proof.sourceFactRefs),
|
|
1566
|
+
]),
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const entryPoints = [];
|
|
1571
|
+
const startupPromotedAreaRefs = new Set();
|
|
1572
|
+
const entryFactById = new Map(entryFacts.map((fact) => [fact.id, fact]));
|
|
1573
|
+
const launcherFactsWithDerivedDefault = new Set(
|
|
1574
|
+
entryFacts.flatMap((fact) =>
|
|
1575
|
+
fact.data.startupRole === "default" && fact.data.parentEntryKey
|
|
1576
|
+
? [fact.data.parentEntryKey]
|
|
1577
|
+
: [],
|
|
1578
|
+
),
|
|
1579
|
+
);
|
|
1580
|
+
const callbackById = new Map(
|
|
1581
|
+
callbackFacts.map((callback) => [callback.id, callback]),
|
|
1582
|
+
);
|
|
1583
|
+
for (const fact of entryFacts) {
|
|
1584
|
+
/*
|
|
1585
|
+
* A finite startup resolver replaces its technical Activity/shell root
|
|
1586
|
+
* with the proven visible default Area. Keeping both roots would make the
|
|
1587
|
+
* shell compete with its own content and reintroduce arbitrary paths.
|
|
1588
|
+
*/
|
|
1589
|
+
if (launcherFactsWithDerivedDefault.has(fact.id)) continue;
|
|
1590
|
+
const root = areaByComponent.get(fact.data.componentKey);
|
|
1591
|
+
if (!root || !areaById.has(root)) {
|
|
1592
|
+
unresolved.push({
|
|
1593
|
+
id: graphId("unresolved", source.caseId, "entry-root", fact.id),
|
|
1594
|
+
ownerRef: fact.id,
|
|
1595
|
+
fieldPath: "rootAreaRefs",
|
|
1596
|
+
reasonCode: "INTRINSIC_ENTRY_ROOT_UNKNOWN",
|
|
1597
|
+
message: "入口组件无法映射到可遍历 Area;该入口未参与路径计算。",
|
|
1598
|
+
evidenceRefs: fact.support.map((item) => item.evidenceRef),
|
|
1599
|
+
candidates: [],
|
|
1600
|
+
impact: ["area-path"],
|
|
1601
|
+
originRef: fact.id,
|
|
1602
|
+
});
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const rootArea = areaById.get(root);
|
|
1606
|
+
const rootConditions = rootArea?.conditions ?? [];
|
|
1607
|
+
if (fact.data.entryKind === "launcher" && fact.data.category === "normal") {
|
|
1608
|
+
const startupCallbacks = callbackFacts.filter(
|
|
1609
|
+
(callback) =>
|
|
1610
|
+
callback.data.ownerKey === fact.data.componentKey &&
|
|
1611
|
+
callback.data.callbackKind === "function" &&
|
|
1612
|
+
/#onCreate\(/u.test(callback.data.callableSymbol),
|
|
1613
|
+
);
|
|
1614
|
+
const lifecyclePagerTransitions = effectFacts.flatMap((effect) => {
|
|
1615
|
+
if (
|
|
1616
|
+
effect.data.effectKind !== "navigate-area" ||
|
|
1617
|
+
effect.data.transitionMode !== "startup-auto" ||
|
|
1618
|
+
!effect.data.targetKey
|
|
1619
|
+
)
|
|
1620
|
+
return [];
|
|
1621
|
+
const callback = callbackById.get(effect.data.callbackKey);
|
|
1622
|
+
const callbackAreaRef = callback
|
|
1623
|
+
? areaByComponent.get(callback.data.ownerKey)
|
|
1624
|
+
: undefined;
|
|
1625
|
+
const targetAreaRef = areaByComponent.get(effect.data.targetKey);
|
|
1626
|
+
if (
|
|
1627
|
+
callbackAreaRef !== root ||
|
|
1628
|
+
!targetAreaRef ||
|
|
1629
|
+
targetAreaRef === root
|
|
1630
|
+
)
|
|
1631
|
+
return [];
|
|
1632
|
+
const transitionConditions = uniqueConditionRefs(
|
|
1633
|
+
effect.data.conditionKeys ?? [],
|
|
1634
|
+
);
|
|
1635
|
+
return [
|
|
1636
|
+
{
|
|
1637
|
+
effect,
|
|
1638
|
+
targetAreaRef,
|
|
1639
|
+
transitionConditions,
|
|
1640
|
+
conditions: uniqueConditionRefs([
|
|
1641
|
+
...fact.data.conditionKeys,
|
|
1642
|
+
...transitionConditions,
|
|
1643
|
+
]),
|
|
1644
|
+
flowFactRefs: [],
|
|
1645
|
+
},
|
|
1646
|
+
];
|
|
1647
|
+
});
|
|
1648
|
+
const callbackStartupTransitions = startupCallbacks.flatMap(
|
|
1649
|
+
(startupCallback) =>
|
|
1650
|
+
callbackTraversal
|
|
1651
|
+
.resolve(
|
|
1652
|
+
`${fact.id}:startup:${startupCallback.id}`,
|
|
1653
|
+
startupCallback.id,
|
|
1654
|
+
)
|
|
1655
|
+
.flatMap((reachability) =>
|
|
1656
|
+
effectFacts
|
|
1657
|
+
.filter(
|
|
1658
|
+
(effect) =>
|
|
1659
|
+
effect.data.effectKind === "navigate-area" &&
|
|
1660
|
+
effect.data.targetKey !== undefined &&
|
|
1661
|
+
effect.data.callbackKey === reachability.callbackKey,
|
|
1662
|
+
)
|
|
1663
|
+
.flatMap((effect) => {
|
|
1664
|
+
const targetAreaRef = effect.data.targetKey
|
|
1665
|
+
? areaByComponent.get(effect.data.targetKey)
|
|
1666
|
+
: undefined;
|
|
1667
|
+
const transitionConditions = uniqueConditionRefs([
|
|
1668
|
+
...reachability.conditionRefs,
|
|
1669
|
+
...(effect.data.conditionKeys ?? []),
|
|
1670
|
+
]);
|
|
1671
|
+
return targetAreaRef &&
|
|
1672
|
+
targetAreaRef !== root &&
|
|
1673
|
+
transitionConditions.length > 0
|
|
1674
|
+
? [
|
|
1675
|
+
{
|
|
1676
|
+
effect,
|
|
1677
|
+
targetAreaRef,
|
|
1678
|
+
transitionConditions,
|
|
1679
|
+
conditions: uniqueConditionRefs([
|
|
1680
|
+
...fact.data.conditionKeys,
|
|
1681
|
+
...transitionConditions,
|
|
1682
|
+
]),
|
|
1683
|
+
flowFactRefs: reachability.flowFactRefs,
|
|
1684
|
+
},
|
|
1685
|
+
]
|
|
1686
|
+
: [];
|
|
1687
|
+
}),
|
|
1688
|
+
),
|
|
1689
|
+
);
|
|
1690
|
+
const startupTransitions = [
|
|
1691
|
+
...lifecyclePagerTransitions,
|
|
1692
|
+
...callbackStartupTransitions,
|
|
1693
|
+
];
|
|
1694
|
+
const seenTransitions = new Set();
|
|
1695
|
+
for (const transition of startupTransitions) {
|
|
1696
|
+
const conditions = transition.conditions;
|
|
1697
|
+
const transitionKey = `${transition.targetAreaRef}\u0000${conditions.join("|")}`;
|
|
1698
|
+
if (seenTransitions.has(transitionKey)) continue;
|
|
1699
|
+
seenTransitions.add(transitionKey);
|
|
1700
|
+
const targetArea = areaById.get(transition.targetAreaRef);
|
|
1701
|
+
if (targetArea) {
|
|
1702
|
+
startupPromotedAreaRefs.add(transition.targetAreaRef);
|
|
1703
|
+
areaById.set(transition.targetAreaRef, {
|
|
1704
|
+
...targetArea,
|
|
1705
|
+
conditions: uniqueConditionRefs([
|
|
1706
|
+
...targetArea.conditions,
|
|
1707
|
+
...conditions,
|
|
1708
|
+
]),
|
|
1709
|
+
sourceFactRefs: uniqueSorted([
|
|
1710
|
+
...targetArea.sourceFactRefs,
|
|
1711
|
+
transition.effect.id,
|
|
1712
|
+
...transition.flowFactRefs,
|
|
1713
|
+
...conditions,
|
|
1714
|
+
]),
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
entryPoints.push({
|
|
1718
|
+
id: graphId(
|
|
1719
|
+
"entry",
|
|
1720
|
+
source.caseId,
|
|
1721
|
+
fact.data.entryKind,
|
|
1722
|
+
`${fact.id}:${transitionKey}`,
|
|
1723
|
+
),
|
|
1724
|
+
kind: fact.data.entryKind,
|
|
1725
|
+
category: fact.data.category,
|
|
1726
|
+
rootAreaRefs: [transition.targetAreaRef],
|
|
1727
|
+
conditions,
|
|
1728
|
+
preconditions: conditions,
|
|
1729
|
+
startupRole: "conditional",
|
|
1730
|
+
launcherAreaRef: root,
|
|
1731
|
+
sourceFactRefs: uniqueSorted([
|
|
1732
|
+
fact.id,
|
|
1733
|
+
transition.effect.id,
|
|
1734
|
+
...transition.flowFactRefs,
|
|
1735
|
+
...conditions,
|
|
1736
|
+
]),
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
const deterministicStartupTarget = (() => {
|
|
1740
|
+
const unique = [
|
|
1741
|
+
...new Map(
|
|
1742
|
+
startupTransitions.map((transition) => [
|
|
1743
|
+
[
|
|
1744
|
+
transition.targetAreaRef,
|
|
1745
|
+
...transition.transitionConditions,
|
|
1746
|
+
].join("\u0000"),
|
|
1747
|
+
transition,
|
|
1748
|
+
]),
|
|
1749
|
+
).values(),
|
|
1750
|
+
];
|
|
1751
|
+
return unique.length === 1 &&
|
|
1752
|
+
unique[0].transitionConditions.length === 0
|
|
1753
|
+
? unique[0].targetAreaRef
|
|
1754
|
+
: undefined;
|
|
1755
|
+
})();
|
|
1756
|
+
if (deterministicStartupTarget) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const parentEntry = fact.data.parentEntryKey
|
|
1761
|
+
? entryFactById.get(fact.data.parentEntryKey)
|
|
1762
|
+
: undefined;
|
|
1763
|
+
const parentRoot = parentEntry
|
|
1764
|
+
? areaByComponent.get(parentEntry.data.componentKey)
|
|
1765
|
+
: undefined;
|
|
1766
|
+
const launcherAreaRef = parentRoot ?? root;
|
|
1767
|
+
entryPoints.push({
|
|
1768
|
+
id: graphId("entry", source.caseId, fact.data.entryKind, fact.id),
|
|
1769
|
+
kind: fact.data.entryKind,
|
|
1770
|
+
category: fact.data.category,
|
|
1771
|
+
rootAreaRefs: uniqueSorted([
|
|
1772
|
+
root,
|
|
1773
|
+
...(fact.data.parentEntryKey && launcherAreaRef !== root
|
|
1774
|
+
? [launcherAreaRef]
|
|
1775
|
+
: []),
|
|
1776
|
+
]),
|
|
1777
|
+
conditions: uniqueConditionRefs([
|
|
1778
|
+
...fact.data.conditionKeys,
|
|
1779
|
+
...rootConditions.filter(
|
|
1780
|
+
(conditionRef) =>
|
|
1781
|
+
conditionById.get(conditionRef)?.data.ownerKey ===
|
|
1782
|
+
fact.data.componentKey,
|
|
1783
|
+
),
|
|
1784
|
+
]),
|
|
1785
|
+
preconditions: uniqueConditionRefs(
|
|
1786
|
+
fact.data.preconditionKeys ?? fact.data.conditionKeys,
|
|
1787
|
+
),
|
|
1788
|
+
startupRole:
|
|
1789
|
+
fact.data.startupRole ??
|
|
1790
|
+
(fact.data.entryKind === "launcher" &&
|
|
1791
|
+
fact.data.category === "normal" &&
|
|
1792
|
+
fact.data.conditionKeys.length === 0
|
|
1793
|
+
? "default"
|
|
1794
|
+
: "conditional"),
|
|
1795
|
+
launcherAreaRef,
|
|
1796
|
+
sourceFactRefs: uniqueSorted([
|
|
1797
|
+
fact.id,
|
|
1798
|
+
...fact.data.conditionKeys,
|
|
1799
|
+
...(fact.data.preconditionKeys ?? []),
|
|
1800
|
+
...(fact.data.parentEntryKey ? [fact.data.parentEntryKey] : []),
|
|
1801
|
+
...rootConditions.filter(
|
|
1802
|
+
(conditionRef) =>
|
|
1803
|
+
conditionById.get(conditionRef)?.data.ownerKey ===
|
|
1804
|
+
fact.data.componentKey,
|
|
1805
|
+
),
|
|
1806
|
+
]),
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
/*
|
|
1810
|
+
* Startup branch analysis can promote a redirect target to a conditioned
|
|
1811
|
+
* launcher root after controls and forward edges have already been built.
|
|
1812
|
+
* Re-apply the final source-Area conditions to outgoing edges so paths and
|
|
1813
|
+
* REQ descriptions do not lose that proven entry predicate.
|
|
1814
|
+
*/
|
|
1815
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
1816
|
+
const edge = edges[index];
|
|
1817
|
+
if (!startupPromotedAreaRefs.has(edge.sourceAreaRef)) continue;
|
|
1818
|
+
const updated = {
|
|
1819
|
+
...edge,
|
|
1820
|
+
conditions: uniqueConditionRefs([
|
|
1821
|
+
...(areaById.get(edge.sourceAreaRef)?.conditions ?? []),
|
|
1822
|
+
...edge.conditions,
|
|
1823
|
+
]),
|
|
1824
|
+
};
|
|
1825
|
+
edges[index] = updated;
|
|
1826
|
+
const forwardIndex = forwardEdges.findIndex(
|
|
1827
|
+
(candidate) => candidate.id === edge.id,
|
|
1828
|
+
);
|
|
1829
|
+
if (forwardIndex >= 0) forwardEdges[forwardIndex] = updated;
|
|
1830
|
+
}
|
|
1831
|
+
/*
|
|
1832
|
+
* A title-less popup is perceived in the context of the control that opens
|
|
1833
|
+
* it. Derive that contextual product label only after the show edge is
|
|
1834
|
+
* proven. Generic overflow/menu openers use the host Area name (for example
|
|
1835
|
+
* "Playlist menu"); a semantic opener such as "Choose language" names its
|
|
1836
|
+
* own menu. No framework class name or application-specific vocabulary is
|
|
1837
|
+
* involved.
|
|
1838
|
+
*/
|
|
1839
|
+
for (const area of areaById.values()) {
|
|
1840
|
+
if (area.kind !== "popup" || area.name.status !== "unresolved") continue;
|
|
1841
|
+
const incoming = edges.filter((edge) => {
|
|
1842
|
+
if (
|
|
1843
|
+
"status" in edge.target ||
|
|
1844
|
+
edge.target.kind !== "area" ||
|
|
1845
|
+
edge.target.ref !== area.id
|
|
1846
|
+
)
|
|
1847
|
+
return false;
|
|
1848
|
+
const action = actionById.get(edge.actionRef);
|
|
1849
|
+
return (
|
|
1850
|
+
action?.effectRefs.some(
|
|
1851
|
+
(effectRef) => effectById.get(effectRef)?.kind === "show-surface",
|
|
1852
|
+
) === true
|
|
1853
|
+
);
|
|
1854
|
+
});
|
|
1855
|
+
const sourceAreaRefs = [
|
|
1856
|
+
...new Set(incoming.map((edge) => edge.sourceAreaRef)),
|
|
1857
|
+
];
|
|
1858
|
+
if (incoming.length === 0 || sourceAreaRefs.length !== 1) continue;
|
|
1859
|
+
const sourceArea = areaById.get(sourceAreaRefs[0]);
|
|
1860
|
+
const openerNames = [
|
|
1861
|
+
...new Set(
|
|
1862
|
+
incoming.flatMap((edge) => {
|
|
1863
|
+
const control = controls.find(
|
|
1864
|
+
(candidate) => candidate.id === edge.controlRef,
|
|
1865
|
+
);
|
|
1866
|
+
return control?.name.status === "resolved"
|
|
1867
|
+
? [control.name.value]
|
|
1868
|
+
: [];
|
|
1869
|
+
}),
|
|
1870
|
+
),
|
|
1871
|
+
];
|
|
1872
|
+
const openerName = openerNames.length === 1 ? openerNames[0] : undefined;
|
|
1873
|
+
const hostName =
|
|
1874
|
+
sourceArea?.name.status === "resolved"
|
|
1875
|
+
? sourceArea.name.value
|
|
1876
|
+
: undefined;
|
|
1877
|
+
const baseName =
|
|
1878
|
+
openerName && !genericMenuOpenerName(openerName)
|
|
1879
|
+
? openerName
|
|
1880
|
+
: (hostName ?? openerName);
|
|
1881
|
+
if (!baseName) continue;
|
|
1882
|
+
const supportRefs = uniqueSorted([
|
|
1883
|
+
...incoming.flatMap((edge) => edge.sourceFactRefs),
|
|
1884
|
+
...incoming.flatMap((edge) => {
|
|
1885
|
+
const control = controls.find(
|
|
1886
|
+
(candidate) => candidate.id === edge.controlRef,
|
|
1887
|
+
);
|
|
1888
|
+
return control?.name.status === "resolved"
|
|
1889
|
+
? control.name.supportRefs
|
|
1890
|
+
: [];
|
|
1891
|
+
}),
|
|
1892
|
+
...(sourceArea?.name.status === "resolved"
|
|
1893
|
+
? sourceArea.name.supportRefs
|
|
1894
|
+
: []),
|
|
1895
|
+
]);
|
|
1896
|
+
const oldUnresolvedRef = area.name.unresolvedRef;
|
|
1897
|
+
const unresolvedIndex = unresolved.findIndex(
|
|
1898
|
+
(candidate) => candidate.id === oldUnresolvedRef,
|
|
1899
|
+
);
|
|
1900
|
+
if (unresolvedIndex >= 0) unresolved.splice(unresolvedIndex, 1);
|
|
1901
|
+
areaById.set(area.id, {
|
|
1902
|
+
...area,
|
|
1903
|
+
name: resolved(contextualMenuName(baseName), supportRefs, "medium"),
|
|
1904
|
+
unresolvedRefs: area.unresolvedRefs.filter(
|
|
1905
|
+
(ref) => ref !== oldUnresolvedRef,
|
|
1906
|
+
),
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
const controlById = new Map(controls.map((control) => [control.id, control]));
|
|
1910
|
+
const pathEdges = forwardEdges.filter((edge) => {
|
|
1911
|
+
const enabled = controlById.get(edge.controlRef)?.enabled;
|
|
1912
|
+
return enabled?.status !== "resolved" || enabled.value !== false;
|
|
1913
|
+
});
|
|
1914
|
+
const pathDrafts = computePaths(
|
|
1915
|
+
{
|
|
1916
|
+
areas: [...areaById.values()],
|
|
1917
|
+
edges: pathEdges,
|
|
1918
|
+
entryPoints,
|
|
1919
|
+
},
|
|
1920
|
+
{
|
|
1921
|
+
maxGraphStates: options.maxGraphStates,
|
|
1922
|
+
conditionExpressions: new Map(
|
|
1923
|
+
conditionFacts.map((condition) => [
|
|
1924
|
+
condition.id,
|
|
1925
|
+
condition.data.expressionAnchor,
|
|
1926
|
+
]),
|
|
1927
|
+
),
|
|
1928
|
+
},
|
|
1929
|
+
);
|
|
1930
|
+
const paths = pathDrafts.map((path) => ({
|
|
1931
|
+
...path,
|
|
1932
|
+
id: graphId(
|
|
1933
|
+
"path",
|
|
1934
|
+
source.caseId,
|
|
1935
|
+
path.kind,
|
|
1936
|
+
[
|
|
1937
|
+
path.targetAreaRef,
|
|
1938
|
+
path.rootEntryRef ?? "none",
|
|
1939
|
+
...path.steps.map((step) => step.edgeRef),
|
|
1940
|
+
].join("|"),
|
|
1941
|
+
),
|
|
1942
|
+
selectionReasons:
|
|
1943
|
+
path.kind === "canonical"
|
|
1944
|
+
? ["shortest normal-launcher path"]
|
|
1945
|
+
: path.kind === "equivalent"
|
|
1946
|
+
? ["equal-cost normal-launcher path"]
|
|
1947
|
+
: path.kind === "alternate"
|
|
1948
|
+
? ["alternate entry path"]
|
|
1949
|
+
: ["not reachable from a normal launcher"],
|
|
1950
|
+
unresolvedRefs: [],
|
|
1951
|
+
}));
|
|
1952
|
+
const canonicalByArea = new Map(
|
|
1953
|
+
paths
|
|
1954
|
+
.filter(
|
|
1955
|
+
(path) => path.kind === "canonical" || path.kind === "unreachable",
|
|
1956
|
+
)
|
|
1957
|
+
.map((path) => [path.targetAreaRef, path.id]),
|
|
1958
|
+
);
|
|
1959
|
+
const areas = sorted(
|
|
1960
|
+
[...areaById.values()].map((area) => ({
|
|
1961
|
+
...area,
|
|
1962
|
+
...(canonicalByArea.has(area.id)
|
|
1963
|
+
? { canonicalPathRef: canonicalByArea.get(area.id) }
|
|
1964
|
+
: {}),
|
|
1965
|
+
})),
|
|
1966
|
+
);
|
|
1967
|
+
const graph = {
|
|
1968
|
+
schemaVersion: "1.0.0",
|
|
1969
|
+
artifactKind: "area-graph",
|
|
1970
|
+
caseId: source.caseId,
|
|
1971
|
+
analysisContext: source.analysisContext,
|
|
1972
|
+
areas,
|
|
1973
|
+
controls: sorted(controls),
|
|
1974
|
+
actions: sorted(actions),
|
|
1975
|
+
effects: sorted(effects),
|
|
1976
|
+
edges: sorted(edges),
|
|
1977
|
+
externalEndpoints: sorted(externalEndpoints),
|
|
1978
|
+
entryPoints: sorted(entryPoints),
|
|
1979
|
+
paths: sorted(paths),
|
|
1980
|
+
excludedAreaCandidates: ownership.exclusions.map((exclusion) => ({
|
|
1981
|
+
...exclusion,
|
|
1982
|
+
sourceFactRefs: [exclusion.componentRef],
|
|
1983
|
+
})),
|
|
1984
|
+
unresolved: sorted([
|
|
1985
|
+
...new Map(unresolved.map((item) => [item.id, item])).values(),
|
|
1986
|
+
]),
|
|
1987
|
+
};
|
|
1988
|
+
const validation = validateAreaGraph(graph);
|
|
1989
|
+
if (!validation.ok) {
|
|
1990
|
+
throw new Error(
|
|
1991
|
+
`Area graph invariants failed: ${validation.issues.map((item) => `${item.code}@${item.path}: ${item.message}`).join(", ")}`,
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
return graph;
|
|
1995
|
+
}
|