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,1028 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildNameTraceLookup,
|
|
3
|
+
LITERAL_NAME_STRATEGIES,
|
|
4
|
+
nameTraceKey,
|
|
5
|
+
} from "../contracts/name-traceability.js";
|
|
6
|
+
const INDENT = " ";
|
|
7
|
+
const HEX_HASH = /^[0-9a-f]{10,}$/i;
|
|
8
|
+
function pad(depth) {
|
|
9
|
+
return INDENT.repeat(depth);
|
|
10
|
+
}
|
|
11
|
+
/** `tag:caseId/subkind/hash` -> `tag:hash6` (subkind duplicates {kind}, the
|
|
12
|
+
* hash alone is the only part that is actually unique per entity); ids with
|
|
13
|
+
* no "/" or a non-hash local part pass through unchanged. */
|
|
14
|
+
function shortId(id) {
|
|
15
|
+
const colon = id.indexOf(":");
|
|
16
|
+
if (colon === -1) return id;
|
|
17
|
+
const tag = id.slice(0, colon);
|
|
18
|
+
const segments = id.slice(colon + 1).split("/");
|
|
19
|
+
const local = segments[segments.length - 1] ?? "";
|
|
20
|
+
return `${tag}:${HEX_HASH.test(local) ? local.slice(0, 6) : local}`;
|
|
21
|
+
}
|
|
22
|
+
function fmtSupported(value) {
|
|
23
|
+
if (value === undefined) return "?";
|
|
24
|
+
if (value.status === "unresolved")
|
|
25
|
+
return `?<${shortId(value.unresolvedRef)}>`;
|
|
26
|
+
return typeof value.value === "string"
|
|
27
|
+
? `"${value.value}"`
|
|
28
|
+
: JSON.stringify(value.value);
|
|
29
|
+
}
|
|
30
|
+
/** For lists of literal values (e.g. feature option labels) — no id shortening. */
|
|
31
|
+
function fmtList(label, items, depth) {
|
|
32
|
+
if (items.length === 0) return "";
|
|
33
|
+
return `\n${pad(depth)}${label}: [${items.join(", ")}]`;
|
|
34
|
+
}
|
|
35
|
+
/** For lists of cross-reference ids. */
|
|
36
|
+
function fmtRefList(label, refs, depth) {
|
|
37
|
+
if (refs.length === 0) return "";
|
|
38
|
+
return `\n${pad(depth)}${label}: [${refs.map(shortId).join(", ")}]`;
|
|
39
|
+
}
|
|
40
|
+
function fmtTextItems(label, items, depth) {
|
|
41
|
+
if (items.length === 0) return "";
|
|
42
|
+
const lines = items.map(
|
|
43
|
+
(item) => `\n${pad(depth + 1)}- ${item.text} <${shortId(item.id)}>`,
|
|
44
|
+
);
|
|
45
|
+
return `\n${pad(depth)}${label}:` + lines.join("");
|
|
46
|
+
}
|
|
47
|
+
function fmtAlternatives(alternatives, depth) {
|
|
48
|
+
if (!alternatives || alternatives.length === 0) return "";
|
|
49
|
+
const lines = alternatives.map(
|
|
50
|
+
(alt, index) =>
|
|
51
|
+
`\n${pad(depth + 1)}OR[${index}]: [${alt.conditionRefs.map(shortId).join(", ")}]`,
|
|
52
|
+
);
|
|
53
|
+
return `\n${pad(depth)}alternatives:` + lines.join("");
|
|
54
|
+
}
|
|
55
|
+
function dumpUnresolved(items) {
|
|
56
|
+
if (items.length === 0) return "Unresolved: none";
|
|
57
|
+
const lines = [`Unresolved (${items.length}):`];
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
lines.push(
|
|
60
|
+
`${pad(1)}<${shortId(item.id)}> <${shortId(item.ownerRef)}> ${item.fieldPath} ` +
|
|
61
|
+
`${item.reasonCode}: ${item.message}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
function indexBy(items) {
|
|
67
|
+
return new Map(items.map((item) => [item.id, item]));
|
|
68
|
+
}
|
|
69
|
+
function edgeKey(sourceAreaRef, controlRef, actionRef) {
|
|
70
|
+
return `${sourceAreaRef}::${controlRef}::${actionRef}`;
|
|
71
|
+
}
|
|
72
|
+
function buildSourceFactsIndex(sourceFacts) {
|
|
73
|
+
if (!sourceFacts) return undefined;
|
|
74
|
+
return {
|
|
75
|
+
factsById: indexBy(sourceFacts.facts),
|
|
76
|
+
evidenceById: indexBy(sourceFacts.evidence),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The first `component` SourceFact reachable from an Area/Control/Action's
|
|
80
|
+
* own `sourceFactRefs` — the JVM/Compose declaration the entity was lifted
|
|
81
|
+
* from, when the run's source-facts.json was supplied to the dump. */
|
|
82
|
+
function componentFact(sourceFactRefs, idx) {
|
|
83
|
+
for (const ref of sourceFactRefs) {
|
|
84
|
+
const fact = idx.factsById.get(ref);
|
|
85
|
+
if (fact && fact.kind === "component") return fact;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
function basename(path) {
|
|
90
|
+
const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
91
|
+
return index === -1 ? path : path.slice(index + 1);
|
|
92
|
+
}
|
|
93
|
+
/** Basenames shared by more than one distinct full path within a given
|
|
94
|
+
* `@Anchor` universe (one dump() call): those must keep their full path to
|
|
95
|
+
* stay unambiguous, every other anchor in that same universe can shorten to
|
|
96
|
+
* its basename alone. Deeply nested paths that only ever differ by directory
|
|
97
|
+
* (e.g. every merged-manifest candidate) collapse to one readable line. */
|
|
98
|
+
function collidingBasenames(paths) {
|
|
99
|
+
const byBasename = new Map();
|
|
100
|
+
for (const path of paths) {
|
|
101
|
+
const name = basename(path);
|
|
102
|
+
const fullPaths = byBasename.get(name) ?? new Set();
|
|
103
|
+
fullPaths.add(path);
|
|
104
|
+
byBasename.set(name, fullPaths);
|
|
105
|
+
}
|
|
106
|
+
const collisions = new Set();
|
|
107
|
+
for (const [name, fullPaths] of byBasename) {
|
|
108
|
+
if (fullPaths.size > 1) collisions.add(name);
|
|
109
|
+
}
|
|
110
|
+
return collisions;
|
|
111
|
+
}
|
|
112
|
+
/** `path:startLine-endLine`, degrading to `path:startLine` for a single-line
|
|
113
|
+
* range. `path` is the file's basename unless it collides with another
|
|
114
|
+
* distinct full path in `collisions` (see `collidingBasenames`), in which
|
|
115
|
+
* case the full path (relative to the project root, matching evidence.path/
|
|
116
|
+
* SourceLocation.path) is kept so the anchor stays unambiguous. Omitting
|
|
117
|
+
* `collisions` (no known universe to check against) keeps the full path.
|
|
118
|
+
* This is the `@Anchor` token's payload (DEVELOPER.md). */
|
|
119
|
+
function fmtAnchorLocation(location, collisions) {
|
|
120
|
+
const { path, range } = location;
|
|
121
|
+
const name = basename(path);
|
|
122
|
+
const displayPath =
|
|
123
|
+
collisions !== undefined && !collisions.has(name) ? name : path;
|
|
124
|
+
return range.startLine === range.endLine
|
|
125
|
+
? `${displayPath}:${range.startLine}`
|
|
126
|
+
: `${displayPath}:${range.startLine}-${range.endLine}`;
|
|
127
|
+
}
|
|
128
|
+
/** Inline `@Anchor` token, per DEVELOPER.md's common shape: same line as the
|
|
129
|
+
* entity header, right after its `"text"` (or `{kind}` when there is no
|
|
130
|
+
* "text"), before any trailing `->`. */
|
|
131
|
+
function fmtAnchor(location, collisions) {
|
|
132
|
+
if (!location) return "";
|
|
133
|
+
return ` @${fmtAnchorLocation(location, collisions)}`;
|
|
134
|
+
}
|
|
135
|
+
/** Prefers the evidence backing the class declaration itself over incidental
|
|
136
|
+
* support entries (manifest merger, layout binding, ...). */
|
|
137
|
+
function preferredEvidence(fact, idx) {
|
|
138
|
+
const preferred =
|
|
139
|
+
fact.support.find((link) => link.role.includes("class declaration")) ??
|
|
140
|
+
fact.support[0];
|
|
141
|
+
return preferred ? idx.evidenceById.get(preferred.evidenceRef) : undefined;
|
|
142
|
+
}
|
|
143
|
+
/** `Symbol` token (DEVELOPER.md): "prefer className = functionName > other
|
|
144
|
+
* source code symbols. Short Name, not Qualified Name" — strips package
|
|
145
|
+
* qualification and parameter lists down to the bare identifier:
|
|
146
|
+
* "com.foo.Bar#baz(int)" -> "baz" (function wins over its class), "com.foo.Bar"
|
|
147
|
+
* -> "Bar", "@id/ivClose" -> "ivClose". */
|
|
148
|
+
function shortSymbol(raw) {
|
|
149
|
+
const withoutIdPrefix = raw.replace(/^@id\//u, "");
|
|
150
|
+
const hashIndex = withoutIdPrefix.indexOf("#");
|
|
151
|
+
if (hashIndex === -1) {
|
|
152
|
+
const segments = withoutIdPrefix.split(".");
|
|
153
|
+
return segments[segments.length - 1] || withoutIdPrefix;
|
|
154
|
+
}
|
|
155
|
+
return withoutIdPrefix.slice(hashIndex + 1).replace(/\([^]*\)\s*$/u, "");
|
|
156
|
+
}
|
|
157
|
+
/** Area/Control identity from a matching name-traceability.json entry —
|
|
158
|
+
* the precomputed, ranked identity decision Stage 2 itself grounds on (see
|
|
159
|
+
* agent-render/name-trace.ts), preferred over the source-facts-only
|
|
160
|
+
* heuristic below because that heuristic has no visibility into competing
|
|
161
|
+
* candidates (an Area backed by both an Activity and a hosted composable can
|
|
162
|
+
* pick the composable's generic name over the Activity's real one — see
|
|
163
|
+
* DEVELOPER.md's WebActivity/Content case). Only a code-origin selected
|
|
164
|
+
* strategy counts: a literal UI-text strategy (LITERAL_NAME_STRATEGIES) means
|
|
165
|
+
* this entity's *name* came from real text, not a code symbol — nothing to
|
|
166
|
+
* borrow here, so the source-facts heuristic still owns Symbol/@Anchor. */
|
|
167
|
+
function identityFromNameTrace(entry) {
|
|
168
|
+
if (!entry) return undefined;
|
|
169
|
+
const selected = entry.attempts.find(
|
|
170
|
+
(attempt) => attempt.outcome === "selected",
|
|
171
|
+
);
|
|
172
|
+
if (!selected?.rawValue || LITERAL_NAME_STRATEGIES.has(selected.strategy))
|
|
173
|
+
return undefined;
|
|
174
|
+
const selectedLocations = entry.sourceLocations.filter(
|
|
175
|
+
(location) => location.usage === "selected",
|
|
176
|
+
);
|
|
177
|
+
const preferred =
|
|
178
|
+
selectedLocations.find((location) => location.symbol !== undefined) ??
|
|
179
|
+
selectedLocations[0];
|
|
180
|
+
return { symbol: selected.rawValue, evidence: preferred };
|
|
181
|
+
}
|
|
182
|
+
/** Tier 1 — className#functionName (or bare #functionName when the owning
|
|
183
|
+
* class can't be resolved): the `symbol` an analyzer attached to the
|
|
184
|
+
* evidence itself backing one of this entity's own SourceFacts. Covers
|
|
185
|
+
* ~100% of JVM/Compose-backed Controls/Actions/Effects (verified against
|
|
186
|
+
* suvinemusicx/latest); XML-declared entities have no evidence.symbol. */
|
|
187
|
+
function symbolFromOwnEvidence(sourceFactRefs, idx) {
|
|
188
|
+
for (const ref of sourceFactRefs) {
|
|
189
|
+
const fact = idx.factsById.get(ref);
|
|
190
|
+
if (!fact) continue;
|
|
191
|
+
for (const support of fact.support) {
|
|
192
|
+
const evidence = idx.evidenceById.get(support.evidenceRef);
|
|
193
|
+
if (evidence?.symbol) return { symbol: evidence.symbol, evidence };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
/** Tier 1 continued — Actions/Effects reference an `interaction-binding` or
|
|
199
|
+
* `behavior-effect` fact rather than the `callback` fact directly; follow
|
|
200
|
+
* `callbackKey` one hop to reach the owning method's symbol/evidence. */
|
|
201
|
+
function symbolViaCallback(sourceFactRefs, idx) {
|
|
202
|
+
for (const ref of sourceFactRefs) {
|
|
203
|
+
const callbackKey = idx.factsById.get(ref)?.data?.callbackKey;
|
|
204
|
+
if (!callbackKey) continue;
|
|
205
|
+
const callback = idx.factsById.get(callbackKey);
|
|
206
|
+
if (!callback) continue;
|
|
207
|
+
const direct = symbolFromOwnEvidence([callback.id], idx);
|
|
208
|
+
if (direct) return direct;
|
|
209
|
+
const callableSymbol = callback.data.callableSymbol;
|
|
210
|
+
if (callableSymbol)
|
|
211
|
+
return {
|
|
212
|
+
symbol: callableSymbol,
|
|
213
|
+
evidence: preferredEvidence(callback, idx),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
/** Tier 2 — other source code symbols: declaration-time identifiers that
|
|
219
|
+
* aren't a class/method symbol (XML view id, icon/content-description
|
|
220
|
+
* anchors) for entities an analyzer can't attach JVM symbol evidence to. */
|
|
221
|
+
function otherCodeSymbol(sourceFactRefs, idx) {
|
|
222
|
+
for (const ref of sourceFactRefs) {
|
|
223
|
+
const fact = idx.factsById.get(ref);
|
|
224
|
+
if (!fact) continue;
|
|
225
|
+
const evidence = preferredEvidence(fact, idx);
|
|
226
|
+
if (fact.kind === "ui-element") {
|
|
227
|
+
const data = fact.data;
|
|
228
|
+
const symbol = data.symbol ?? data.resourceId;
|
|
229
|
+
if (symbol) return { symbol, evidence };
|
|
230
|
+
}
|
|
231
|
+
if (fact.kind === "callback") {
|
|
232
|
+
const callableSymbol = fact.data.callableSymbol;
|
|
233
|
+
if (callableSymbol) return { symbol: callableSymbol, evidence };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
/** Tier 3 — source code anchor only: the first evidence location reachable
|
|
239
|
+
* from this entity's own sourceFactRefs, no symbol identity attached. */
|
|
240
|
+
function anchorOnly(sourceFactRefs, idx) {
|
|
241
|
+
for (const ref of sourceFactRefs) {
|
|
242
|
+
const fact = idx.factsById.get(ref);
|
|
243
|
+
if (!fact) continue;
|
|
244
|
+
const evidence = preferredEvidence(fact, idx);
|
|
245
|
+
if (evidence) return { evidence };
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Control/Action/Effect source identity, most to least preferred (per
|
|
251
|
+
* DEVELOPER.md): className#functionName evidence symbol > the same one hop
|
|
252
|
+
* through a callback reference > other declaration-time symbols (XML view
|
|
253
|
+
* id, ...) > a bare source anchor (path:line[-line]), no symbol at all.
|
|
254
|
+
*/
|
|
255
|
+
function resolveSourceIdentity(sourceFactRefs, idx) {
|
|
256
|
+
return (
|
|
257
|
+
symbolFromOwnEvidence(sourceFactRefs, idx) ??
|
|
258
|
+
symbolViaCallback(sourceFactRefs, idx) ??
|
|
259
|
+
otherCodeSymbol(sourceFactRefs, idx) ??
|
|
260
|
+
anchorOnly(sourceFactRefs, idx) ??
|
|
261
|
+
{}
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Area identity: name-traceability's ranked decision first (see
|
|
266
|
+
* `identityFromNameTrace`), falling back to its own `component` SourceFact's
|
|
267
|
+
* qualifiedName directly, falling back further to the generic tiered
|
|
268
|
+
* resolver used by Control/Action/Effect. Takes the raw ref/sourceFactRefs
|
|
269
|
+
* pair rather than an `Area` object so ReqModel's ReqArea — which carries
|
|
270
|
+
* the same `areaRef` and a superset `supportRefs` merging the Area's own
|
|
271
|
+
* sourceFactRefs with its Controls' (see analysis/req-model/build.ts) — can
|
|
272
|
+
* resolve the identical identity without needing the AreaGraph at all. */
|
|
273
|
+
function areaSourceIdentity(sourceFactRefs, areaRef, sourceFacts, nameTrace) {
|
|
274
|
+
const fromNameTrace = identityFromNameTrace(
|
|
275
|
+
nameTrace?.get(nameTraceKey("area", areaRef)),
|
|
276
|
+
);
|
|
277
|
+
if (fromNameTrace) return fromNameTrace;
|
|
278
|
+
if (!sourceFacts) return {};
|
|
279
|
+
const component = componentFact(sourceFactRefs, sourceFacts);
|
|
280
|
+
return component
|
|
281
|
+
? {
|
|
282
|
+
symbol: component.data.qualifiedName,
|
|
283
|
+
evidence: preferredEvidence(component, sourceFacts),
|
|
284
|
+
}
|
|
285
|
+
: resolveSourceIdentity(sourceFactRefs, sourceFacts);
|
|
286
|
+
}
|
|
287
|
+
/** Control identity: name-traceability's ranked decision first, falling back
|
|
288
|
+
* to the generic tiered resolver used by Action/Effect. Same ref/
|
|
289
|
+
* sourceFactRefs-pair shape as `areaSourceIdentity`, for the same reason:
|
|
290
|
+
* ReqModel's ReqFeature carries `controlRef` + a superset `supportRefs`. */
|
|
291
|
+
function controlSourceIdentity(
|
|
292
|
+
sourceFactRefs,
|
|
293
|
+
controlRef,
|
|
294
|
+
sourceFacts,
|
|
295
|
+
nameTrace,
|
|
296
|
+
) {
|
|
297
|
+
const fromNameTrace = identityFromNameTrace(
|
|
298
|
+
nameTrace?.get(nameTraceKey("control", controlRef)),
|
|
299
|
+
);
|
|
300
|
+
if (fromNameTrace) return fromNameTrace;
|
|
301
|
+
return sourceFacts ? resolveSourceIdentity(sourceFactRefs, sourceFacts) : {};
|
|
302
|
+
}
|
|
303
|
+
/** `Symbol ` prefix for an entity's header line (empty when unresolved). */
|
|
304
|
+
function fmtSymbolPrefix(identity) {
|
|
305
|
+
return identity.symbol ? `${shortSymbol(identity.symbol)} ` : "";
|
|
306
|
+
}
|
|
307
|
+
function buildAreaGraphIndex(graph, sourceFacts, nameTraceability) {
|
|
308
|
+
const edgesByKey = new Map();
|
|
309
|
+
for (const edge of graph.edges) {
|
|
310
|
+
const key = edgeKey(edge.sourceAreaRef, edge.controlRef, edge.actionRef);
|
|
311
|
+
const bucket = edgesByKey.get(key);
|
|
312
|
+
if (bucket) {
|
|
313
|
+
bucket.push(edge);
|
|
314
|
+
} else {
|
|
315
|
+
edgesByKey.set(key, [edge]);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
areasById: indexBy(graph.areas),
|
|
320
|
+
controlsById: indexBy(graph.controls),
|
|
321
|
+
actionsById: indexBy(graph.actions),
|
|
322
|
+
effectsById: indexBy(graph.effects),
|
|
323
|
+
externalsById: indexBy(graph.externalEndpoints),
|
|
324
|
+
edgesByKey,
|
|
325
|
+
sourceFacts: buildSourceFactsIndex(sourceFacts),
|
|
326
|
+
nameTrace: nameTraceability
|
|
327
|
+
? buildNameTraceLookup(nameTraceability.entries)
|
|
328
|
+
: undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** `-> <area:id> "name" {kind}`, kind omitted when the target isn't in this graph. */
|
|
332
|
+
function fmtAreaRef(ref, area) {
|
|
333
|
+
const kind = area ? ` {${area.kind}}` : "";
|
|
334
|
+
return `<${shortId(ref)}> ${area ? fmtSupported(area.name) : "?"}${kind}`;
|
|
335
|
+
}
|
|
336
|
+
function dumpEdgeTarget(edge, idx, depth) {
|
|
337
|
+
const target = edge.target;
|
|
338
|
+
let out;
|
|
339
|
+
if ("unresolvedRef" in target) {
|
|
340
|
+
out = `\n${pad(depth)}-> ?<${shortId(target.unresolvedRef)}>`;
|
|
341
|
+
} else if (target.kind === "area") {
|
|
342
|
+
out = `\n${pad(depth)}-> ${fmtAreaRef(target.ref, idx.areasById.get(target.ref))}`;
|
|
343
|
+
} else {
|
|
344
|
+
const external = idx.externalsById.get(target.ref);
|
|
345
|
+
const kind = external ? ` {${external.kind}}` : "";
|
|
346
|
+
out = `\n${pad(depth)}-> <${shortId(target.ref)}> ${external ? fmtSupported(external.name) : "?"}${kind}`;
|
|
347
|
+
}
|
|
348
|
+
out += fmtRefList("when", edge.conditions, depth + 1);
|
|
349
|
+
out += fmtAlternatives(edge.conditionAlternatives, depth + 1);
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
function dumpEffect(effect, idx, depth, collisions) {
|
|
353
|
+
const identity = idx.sourceFacts
|
|
354
|
+
? resolveSourceIdentity(effect.sourceFactRefs, idx.sourceFacts)
|
|
355
|
+
: {};
|
|
356
|
+
const tags = [
|
|
357
|
+
effect.kind,
|
|
358
|
+
effect.closePolicy ? `close=${effect.closePolicy}` : undefined,
|
|
359
|
+
effect.transitionMode ? `transition=${effect.transitionMode}` : undefined,
|
|
360
|
+
effect.stateKey ? `state=${fmtSupported(effect.stateKey)}` : undefined,
|
|
361
|
+
effect.value !== undefined
|
|
362
|
+
? `value=${fmtSupported(effect.value)}`
|
|
363
|
+
: undefined,
|
|
364
|
+
].filter((tag) => tag !== undefined);
|
|
365
|
+
let out =
|
|
366
|
+
`\n${pad(depth)}<${shortId(effect.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
367
|
+
`{${tags.join(", ")}} ${fmtSupported(effect.description)}${fmtAnchor(identity.evidence, collisions)}`;
|
|
368
|
+
out += fmtAlternatives(effect.conditionAlternatives, depth + 1);
|
|
369
|
+
out += fmtRefList("unresolved", effect.unresolvedRefs ?? [], depth + 1);
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
function dumpAction(action, sourceAreaRef, idx, depth, collisions) {
|
|
373
|
+
const identity = idx.sourceFacts
|
|
374
|
+
? resolveSourceIdentity(action.sourceFactRefs, idx.sourceFacts)
|
|
375
|
+
: {};
|
|
376
|
+
let out =
|
|
377
|
+
`\n${pad(depth)}<${shortId(action.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
378
|
+
`{${action.trigger}}${fmtAnchor(identity.evidence, collisions)}`;
|
|
379
|
+
out += fmtRefList("pre", action.preconditionRefs, depth + 1);
|
|
380
|
+
out += fmtAlternatives(action.preconditionAlternatives, depth + 1);
|
|
381
|
+
for (const ref of action.effectRefs) {
|
|
382
|
+
const effect = idx.effectsById.get(ref);
|
|
383
|
+
if (effect) out += dumpEffect(effect, idx, depth + 1, collisions);
|
|
384
|
+
}
|
|
385
|
+
const edges =
|
|
386
|
+
idx.edgesByKey.get(edgeKey(sourceAreaRef, action.controlRef, action.id)) ??
|
|
387
|
+
[];
|
|
388
|
+
for (const edge of edges) {
|
|
389
|
+
out += dumpEdgeTarget(edge, idx, depth + 1);
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
function dumpControl(control, idx, depth, collisions) {
|
|
394
|
+
const identity = controlSourceIdentity(
|
|
395
|
+
control.sourceFactRefs,
|
|
396
|
+
control.id,
|
|
397
|
+
idx.sourceFacts,
|
|
398
|
+
idx.nameTrace,
|
|
399
|
+
);
|
|
400
|
+
let out =
|
|
401
|
+
`\n${pad(depth)}<${shortId(control.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
402
|
+
`{${control.kind}} ${fmtSupported(control.name)}${fmtAnchor(identity.evidence, collisions)}`;
|
|
403
|
+
if (control.enabled) out += ` enabled=${fmtSupported(control.enabled)}`;
|
|
404
|
+
out += fmtRefList("visible-when", control.visibilityConditions, depth + 1);
|
|
405
|
+
out += fmtRefList("enabled-when", control.enabledConditions, depth + 1);
|
|
406
|
+
for (const ref of control.actionRefs) {
|
|
407
|
+
const action = idx.actionsById.get(ref);
|
|
408
|
+
if (action)
|
|
409
|
+
out += dumpAction(action, control.areaRef, idx, depth + 1, collisions);
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
function dumpArea(area, idx, depth, collisions) {
|
|
414
|
+
const identity = areaSourceIdentity(
|
|
415
|
+
area.sourceFactRefs,
|
|
416
|
+
area.id,
|
|
417
|
+
idx.sourceFacts,
|
|
418
|
+
idx.nameTrace,
|
|
419
|
+
);
|
|
420
|
+
let out =
|
|
421
|
+
`${pad(depth)}<${shortId(area.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
422
|
+
`{${area.kind}} ${fmtSupported(area.name)}${fmtAnchor(identity.evidence, collisions)}`;
|
|
423
|
+
out += fmtRefList("conditions", area.conditions, depth + 1);
|
|
424
|
+
for (const ref of area.controlRefs) {
|
|
425
|
+
const control = idx.controlsById.get(ref);
|
|
426
|
+
if (control) out += dumpControl(control, idx, depth + 1, collisions);
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
/** Every evidence path this dump will actually render as an `@Anchor` across
|
|
431
|
+
* all Areas/Controls/Actions/Effects — the same "one dump() call" collision
|
|
432
|
+
* universe `dumpGapAudit` computes for its own candidates, reused here so
|
|
433
|
+
* `collidingBasenames` decides full-path-vs-basename consistently. */
|
|
434
|
+
function allSourceEvidencePaths(graph, idx) {
|
|
435
|
+
const paths = [];
|
|
436
|
+
const collect = (identity) => {
|
|
437
|
+
if (identity.evidence) paths.push(identity.evidence.path);
|
|
438
|
+
};
|
|
439
|
+
for (const area of graph.areas) {
|
|
440
|
+
collect(
|
|
441
|
+
areaSourceIdentity(
|
|
442
|
+
area.sourceFactRefs,
|
|
443
|
+
area.id,
|
|
444
|
+
idx.sourceFacts,
|
|
445
|
+
idx.nameTrace,
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
for (const control of graph.controls) {
|
|
450
|
+
collect(
|
|
451
|
+
controlSourceIdentity(
|
|
452
|
+
control.sourceFactRefs,
|
|
453
|
+
control.id,
|
|
454
|
+
idx.sourceFacts,
|
|
455
|
+
idx.nameTrace,
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (idx.sourceFacts) {
|
|
460
|
+
for (const action of graph.actions)
|
|
461
|
+
collect(resolveSourceIdentity(action.sourceFactRefs, idx.sourceFacts));
|
|
462
|
+
for (const effect of graph.effects)
|
|
463
|
+
collect(resolveSourceIdentity(effect.sourceFactRefs, idx.sourceFacts));
|
|
464
|
+
}
|
|
465
|
+
return paths;
|
|
466
|
+
}
|
|
467
|
+
function dumpEntryPoint(entry, idx) {
|
|
468
|
+
const roots = entry.rootAreaRefs
|
|
469
|
+
.map((ref) => fmtAreaRef(ref, idx.areasById.get(ref)))
|
|
470
|
+
.join(", ");
|
|
471
|
+
return `${pad(1)}<${shortId(entry.id)}> {${entry.kind}, ${entry.category}} -> ${roots}`;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Dumps an AreaGraphArtifact as an indented tree instead of JSON. Passing the
|
|
475
|
+
* matching source-facts.json (same run) additionally resolves each
|
|
476
|
+
* Area/Control/Action/Effect's `Symbol` (short className/functionName) and
|
|
477
|
+
* `@Anchor` (declaration location) per DEVELOPER.md. Passing the matching
|
|
478
|
+
* name-traceability.json (same run) further overrides Area/Control's Symbol/
|
|
479
|
+
* @Anchor with its precomputed, ranked identity decision when one exists —
|
|
480
|
+
* see `identityFromNameTrace`; Action/Effect have no name-traceability
|
|
481
|
+
* coverage and always use the source-facts heuristic. Omitting both, dump
|
|
482
|
+
* renders exactly as before, with no Symbol/Anchor.
|
|
483
|
+
*/
|
|
484
|
+
export function dumpAreaGraph(graph, sourceFacts, nameTraceability) {
|
|
485
|
+
const idx = buildAreaGraphIndex(graph, sourceFacts, nameTraceability);
|
|
486
|
+
const collisions = collidingBasenames(allSourceEvidencePaths(graph, idx));
|
|
487
|
+
const lines = [];
|
|
488
|
+
lines.push(
|
|
489
|
+
`AreaGraph <${graph.caseId}> [${graph.analysisContext.applicationModule}:${graph.analysisContext.buildVariant}]`,
|
|
490
|
+
);
|
|
491
|
+
lines.push("Entry Points:");
|
|
492
|
+
for (const entry of graph.entryPoints) lines.push(dumpEntryPoint(entry, idx));
|
|
493
|
+
lines.push("Areas:");
|
|
494
|
+
for (const area of graph.areas)
|
|
495
|
+
lines.push(dumpArea(area, idx, 1, collisions));
|
|
496
|
+
if (graph.excludedAreaCandidates && graph.excludedAreaCandidates.length > 0) {
|
|
497
|
+
lines.push("Excluded Area Candidates:");
|
|
498
|
+
for (const candidate of graph.excludedAreaCandidates) {
|
|
499
|
+
lines.push(
|
|
500
|
+
`${pad(1)}<${shortId(candidate.componentRef)}> excluded: ${candidate.reasonCode} - ${candidate.reason}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (graph.externalEndpoints.length > 0) {
|
|
505
|
+
lines.push("External Endpoints:");
|
|
506
|
+
for (const external of graph.externalEndpoints) {
|
|
507
|
+
lines.push(
|
|
508
|
+
`${pad(1)}<${shortId(external.id)}> {${external.kind}} ${fmtSupported(external.name)}`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
lines.push(dumpUnresolved(graph.unresolved));
|
|
513
|
+
return lines.join("\n");
|
|
514
|
+
}
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// ReqModel
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
function dumpCanonicalPath(path, depth) {
|
|
519
|
+
if (path.status === "unresolved")
|
|
520
|
+
return `\n${pad(depth)}path: ?<${shortId(path.unresolvedRef)}>`;
|
|
521
|
+
if (path.status === "unreachable") {
|
|
522
|
+
const alternates =
|
|
523
|
+
path.alternatePathRefs.length > 0
|
|
524
|
+
? path.alternatePathRefs.map(shortId).join(", ")
|
|
525
|
+
: "none";
|
|
526
|
+
return `\n${pad(depth)}path: unreachable (alternates: ${alternates})`;
|
|
527
|
+
}
|
|
528
|
+
if (path.segments.length === 0) {
|
|
529
|
+
return `\n${pad(depth)}path (${path.reachability}): (self)`;
|
|
530
|
+
}
|
|
531
|
+
let chain = path.segments[0]?.areaName ?? "?";
|
|
532
|
+
for (const segment of path.segments) {
|
|
533
|
+
const label =
|
|
534
|
+
segment.controlName && segment.action
|
|
535
|
+
? `${segment.controlName}: ${segment.action}`
|
|
536
|
+
: (segment.action ?? "");
|
|
537
|
+
chain += ` -[${label}]-> ${segment.targetAreaName ?? "?"}`;
|
|
538
|
+
}
|
|
539
|
+
return `\n${pad(depth)}path (${path.reachability}): ${chain}`;
|
|
540
|
+
}
|
|
541
|
+
function dumpReqTarget(target, depth) {
|
|
542
|
+
if (!target) return "";
|
|
543
|
+
if ("unresolvedRef" in target)
|
|
544
|
+
return `\n${pad(depth)}-> ?<${shortId(target.unresolvedRef)}>`;
|
|
545
|
+
switch (target.kind) {
|
|
546
|
+
case "area":
|
|
547
|
+
return `\n${pad(depth)}-> <${shortId(target.ref)}> ${fmtSupported(target.displayName)}`;
|
|
548
|
+
case "external":
|
|
549
|
+
return `\n${pad(depth)}-> <${shortId(target.ref)}> ${fmtSupported(target.displayName)}`;
|
|
550
|
+
case "history-back":
|
|
551
|
+
return `\n${pad(depth)}-> history-back [${target.refs.map(shortId).join(", ")}] ${fmtSupported(target.displayName)}`;
|
|
552
|
+
case "conditional-area-set": {
|
|
553
|
+
let out = `\n${pad(depth)}-> one-of ${fmtSupported(target.displayName)}`;
|
|
554
|
+
if (target.branches) {
|
|
555
|
+
for (const branch of target.branches) {
|
|
556
|
+
out += `\n${pad(depth + 1)}<${shortId(branch.ref)}>`;
|
|
557
|
+
out += fmtTextItems("when", branch.conditions, depth + 2);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
out += fmtRefList("candidates", target.refs, depth + 1);
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
}
|
|
564
|
+
case "alternative-target-set": {
|
|
565
|
+
let out = `\n${pad(depth)}-> alternatives ${fmtSupported(target.displayName)}`;
|
|
566
|
+
for (const branch of target.branches) {
|
|
567
|
+
out += dumpReqTarget(branch.target, depth + 1);
|
|
568
|
+
out += fmtTextItems("when", branch.conditions, depth + 2);
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function buildReqModelIndex(graph, sourceFacts, nameTraceability) {
|
|
575
|
+
return {
|
|
576
|
+
sourceFacts: buildSourceFactsIndex(sourceFacts),
|
|
577
|
+
nameTrace: nameTraceability
|
|
578
|
+
? buildNameTraceLookup(nameTraceability.entries)
|
|
579
|
+
: undefined,
|
|
580
|
+
areasById: graph ? indexBy(graph.areas) : undefined,
|
|
581
|
+
controlsById: graph ? indexBy(graph.controls) : undefined,
|
|
582
|
+
actionsById: graph ? indexBy(graph.actions) : undefined,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function reqAreaIdentity(area, idx) {
|
|
586
|
+
const graphArea = idx.areasById?.get(area.areaRef);
|
|
587
|
+
return graphArea
|
|
588
|
+
? areaSourceIdentity(
|
|
589
|
+
graphArea.sourceFactRefs,
|
|
590
|
+
graphArea.id,
|
|
591
|
+
idx.sourceFacts,
|
|
592
|
+
idx.nameTrace,
|
|
593
|
+
)
|
|
594
|
+
: areaSourceIdentity(
|
|
595
|
+
area.supportRefs,
|
|
596
|
+
area.areaRef,
|
|
597
|
+
idx.sourceFacts,
|
|
598
|
+
idx.nameTrace,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
function reqFeatureIdentity(feature, idx) {
|
|
602
|
+
const graphControl = idx.controlsById?.get(feature.controlRef);
|
|
603
|
+
return graphControl
|
|
604
|
+
? controlSourceIdentity(
|
|
605
|
+
graphControl.sourceFactRefs,
|
|
606
|
+
graphControl.id,
|
|
607
|
+
idx.sourceFacts,
|
|
608
|
+
idx.nameTrace,
|
|
609
|
+
)
|
|
610
|
+
: controlSourceIdentity(
|
|
611
|
+
feature.supportRefs,
|
|
612
|
+
feature.controlRef,
|
|
613
|
+
idx.sourceFacts,
|
|
614
|
+
idx.nameTrace,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
/** ReqInteraction has no name-traceability coverage (only "area"/"control"
|
|
618
|
+
* entity kinds do — same as its AreaGraph Action counterpart). */
|
|
619
|
+
function reqInteractionIdentity(interaction, idx) {
|
|
620
|
+
if (!idx.sourceFacts) return {};
|
|
621
|
+
const graphAction = idx.actionsById?.get(interaction.actionRef);
|
|
622
|
+
return resolveSourceIdentity(
|
|
623
|
+
graphAction?.sourceFactRefs ?? interaction.supportRefs,
|
|
624
|
+
idx.sourceFacts,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
/** ReqEffectItem's own `supportRefs` is exactly the underlying Effect's
|
|
628
|
+
* `sourceFactRefs` (see analysis/req-model/build.ts's `textItem` call for
|
|
629
|
+
* "feature-effect") — already precise, no graph lookup needed. */
|
|
630
|
+
function reqEffectIdentity(effect, idx) {
|
|
631
|
+
return idx.sourceFacts
|
|
632
|
+
? resolveSourceIdentity(effect.supportRefs, idx.sourceFacts)
|
|
633
|
+
: {};
|
|
634
|
+
}
|
|
635
|
+
function dumpReqEffect(effect, idx, collisions, depth) {
|
|
636
|
+
const identity = reqEffectIdentity(effect, idx);
|
|
637
|
+
let out =
|
|
638
|
+
`\n${pad(depth)}<${shortId(effect.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
639
|
+
`${effect.kind ? `{${effect.kind}} ` : ""}${effect.text}${fmtAnchor(identity.evidence, collisions)}`;
|
|
640
|
+
if (effect.stateKey) out += ` state=${fmtSupported(effect.stateKey)}`;
|
|
641
|
+
if (effect.value !== undefined) out += ` value=${fmtSupported(effect.value)}`;
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
function dumpReqInteraction(interaction, idx, collisions, depth) {
|
|
645
|
+
const identity = reqInteractionIdentity(interaction, idx);
|
|
646
|
+
let out =
|
|
647
|
+
`\n${pad(depth)}<${shortId(interaction.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
648
|
+
`{${interaction.trigger}}${fmtAnchor(identity.evidence, collisions)}`;
|
|
649
|
+
out += fmtTextItems("conditions", interaction.conditions, depth + 1);
|
|
650
|
+
for (const effect of interaction.effects)
|
|
651
|
+
out += dumpReqEffect(effect, idx, collisions, depth + 1);
|
|
652
|
+
out += dumpReqTarget(interaction.target, depth + 1);
|
|
653
|
+
out += fmtRefList("unresolved", interaction.unresolvedRefs, depth + 1);
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* v1 artifacts populate only the deprecated top-level trigger/conditions/
|
|
658
|
+
* effects/target fields; synthesize one interaction so dump() still renders.
|
|
659
|
+
*/
|
|
660
|
+
function reqFeatureInteractions(feature) {
|
|
661
|
+
if (feature.interactions && feature.interactions.length > 0)
|
|
662
|
+
return feature.interactions;
|
|
663
|
+
return [
|
|
664
|
+
{
|
|
665
|
+
id: `${feature.id}#legacy`,
|
|
666
|
+
actionRef: feature.controlRef,
|
|
667
|
+
trigger: feature.trigger,
|
|
668
|
+
conditions: feature.conditions,
|
|
669
|
+
effects: feature.effects,
|
|
670
|
+
...(feature.target ? { target: feature.target } : {}),
|
|
671
|
+
unresolvedRefs: feature.unresolvedRefs,
|
|
672
|
+
supportRefs: feature.supportRefs,
|
|
673
|
+
},
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
function dumpReqFeature(feature, idx, collisions, depth) {
|
|
677
|
+
const identity = reqFeatureIdentity(feature, idx);
|
|
678
|
+
let out =
|
|
679
|
+
`\n${pad(depth)}<${shortId(feature.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
680
|
+
`${fmtSupported(feature.displayName)}${fmtAnchor(identity.evidence, collisions)} ` +
|
|
681
|
+
`(control=<${shortId(feature.controlRef)}>)`;
|
|
682
|
+
if (feature.enabled) out += ` enabled=${fmtSupported(feature.enabled)}`;
|
|
683
|
+
if (feature.defaultValue !== undefined)
|
|
684
|
+
out += ` default=${fmtSupported(feature.defaultValue)}`;
|
|
685
|
+
out += fmtList(
|
|
686
|
+
"options",
|
|
687
|
+
feature.options.map((option) => option.value),
|
|
688
|
+
depth + 1,
|
|
689
|
+
);
|
|
690
|
+
for (const interaction of reqFeatureInteractions(feature)) {
|
|
691
|
+
out += dumpReqInteraction(interaction, idx, collisions, depth + 1);
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
function dumpReqArea(area, idx, collisions, depth) {
|
|
696
|
+
const identity = reqAreaIdentity(area, idx);
|
|
697
|
+
let out =
|
|
698
|
+
`${pad(depth)}<${shortId(area.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
699
|
+
`${fmtSupported(area.displayName)}${fmtAnchor(identity.evidence, collisions)} ` +
|
|
700
|
+
`(area=<${shortId(area.areaRef)}>)`;
|
|
701
|
+
out += dumpCanonicalPath(area.canonicalPath, depth + 1);
|
|
702
|
+
out += fmtTextItems("preconditions", area.preconditions, depth + 1);
|
|
703
|
+
out += fmtTextItems("description", area.descriptionItems, depth + 1);
|
|
704
|
+
for (const feature of area.features)
|
|
705
|
+
out += dumpReqFeature(feature, idx, collisions, depth + 1);
|
|
706
|
+
out += fmtRefList("unresolved", area.unresolvedRefs, depth + 1);
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
709
|
+
/** Every evidence path this dump will actually render as an `@Anchor` across
|
|
710
|
+
* all ReqAreas/ReqFeatures/ReqInteractions/ReqEffectItems — same collision
|
|
711
|
+
* universe pattern as `allSourceEvidencePaths` (AreaGraph) / `dumpGapAudit`. */
|
|
712
|
+
function allReqModelEvidencePaths(model, idx) {
|
|
713
|
+
const paths = [];
|
|
714
|
+
const collect = (identity) => {
|
|
715
|
+
if (identity.evidence) paths.push(identity.evidence.path);
|
|
716
|
+
};
|
|
717
|
+
for (const area of model.areas) {
|
|
718
|
+
collect(reqAreaIdentity(area, idx));
|
|
719
|
+
for (const feature of area.features) {
|
|
720
|
+
collect(reqFeatureIdentity(feature, idx));
|
|
721
|
+
for (const interaction of reqFeatureInteractions(feature)) {
|
|
722
|
+
collect(reqInteractionIdentity(interaction, idx));
|
|
723
|
+
for (const effect of interaction.effects)
|
|
724
|
+
collect(reqEffectIdentity(effect, idx));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return paths;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Dumps a ReqModelArtifact as an indented tree instead of JSON. Same Symbol/
|
|
732
|
+
* @Anchor resolution as `dumpAreaGraph`. Passing the matching area-graph.json
|
|
733
|
+
* (same run) resolves ReqArea/ReqFeature/ReqInteraction identity from the
|
|
734
|
+
* exact underlying Area/Control/Action's own `sourceFactRefs` — otherwise
|
|
735
|
+
* (area-graph.json omitted) falls back to ReqArea/ReqFeature's own broader
|
|
736
|
+
* `supportRefs`, which can occasionally pick up a nested interaction's/
|
|
737
|
+
* effect's symbol instead of the Control's own (e.g. an XML-declared Control
|
|
738
|
+
* with no JVM symbol of its own). ReqEffectItem always resolves from its own
|
|
739
|
+
* `supportRefs` (exactly the underlying Effect's `sourceFactRefs`, no graph
|
|
740
|
+
* needed). Passing the matching name-traceability.json further overrides
|
|
741
|
+
* Area/Control's Symbol/@Anchor with its precomputed, ranked identity
|
|
742
|
+
* decision when one exists (see `identityFromNameTrace`). Omitting all three,
|
|
743
|
+
* dump renders exactly as before, with no Symbol/Anchor.
|
|
744
|
+
*/
|
|
745
|
+
export function dumpReqModel(
|
|
746
|
+
model,
|
|
747
|
+
graph,
|
|
748
|
+
sourceFactsArtifact,
|
|
749
|
+
nameTraceability,
|
|
750
|
+
) {
|
|
751
|
+
const idx = buildReqModelIndex(graph, sourceFactsArtifact, nameTraceability);
|
|
752
|
+
const collisions = collidingBasenames(allReqModelEvidencePaths(model, idx));
|
|
753
|
+
const lines = [];
|
|
754
|
+
lines.push(`ReqModel <${model.caseId}>`);
|
|
755
|
+
lines.push("Areas:");
|
|
756
|
+
for (const area of model.areas)
|
|
757
|
+
lines.push(dumpReqArea(area, idx, collisions, 1));
|
|
758
|
+
lines.push(dumpUnresolved(model.unresolved));
|
|
759
|
+
return lines.join("\n");
|
|
760
|
+
}
|
|
761
|
+
function fmtCounts(counts) {
|
|
762
|
+
return (
|
|
763
|
+
`observed=${counts.observed} normalized=${counts.normalized} ` +
|
|
764
|
+
`possibleGap=${counts.possibleGap} unassessed=${counts.unassessed} ` +
|
|
765
|
+
`recognized=${counts.recognized} unresolved=${counts.unresolved} ` +
|
|
766
|
+
`excludedWithReason=${counts.excludedWithReason} limited=${counts.limited}`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
/** `component` facts prefer their own qualifiedName as `Symbol` (mirrors
|
|
770
|
+
* `areaSourceIdentity`); every other kind reuses the generic Control/Action/
|
|
771
|
+
* Effect resolver tiers via the fact's own id as a one-element ref list. */
|
|
772
|
+
function normalizedFactIdentity(fact, idx) {
|
|
773
|
+
if (fact.kind === "component") {
|
|
774
|
+
const component = fact;
|
|
775
|
+
return {
|
|
776
|
+
symbol: component.data.qualifiedName,
|
|
777
|
+
evidence: preferredEvidence(component, idx),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return resolveSourceIdentity([fact.id], idx);
|
|
781
|
+
}
|
|
782
|
+
/** One resolved `normalizedFactRefs` entry: the real SourceFact this gap-audit
|
|
783
|
+
* candidate turned into, in the same `Symbol {kind} @Anchor` shape used
|
|
784
|
+
* everywhere else — this is what makes `normalizedFactRefs` informative
|
|
785
|
+
* instead of an opaque `fact:hash` id (its `kind` is always one of the
|
|
786
|
+
* candidate's own `expectedFactKinds` by construction, see buildCategory in
|
|
787
|
+
* analysis/gap-audit/build.ts, so `expectedFactKinds` would be redundant here).
|
|
788
|
+
* Nested directly under the candidate line with no header label — its own
|
|
789
|
+
* `<fact:id>` tag already says what it is, same convention as Area's Controls
|
|
790
|
+
* or Control's Actions. */
|
|
791
|
+
function dumpNormalizedFact(ref, idx, depth, collisions) {
|
|
792
|
+
const fact = idx.factsById.get(ref);
|
|
793
|
+
if (!fact) return `\n${pad(depth)}<${shortId(ref)}>`;
|
|
794
|
+
const identity = normalizedFactIdentity(fact, idx);
|
|
795
|
+
return (
|
|
796
|
+
`\n${pad(depth)}<${shortId(fact.id)}> ${fmtSymbolPrefix(identity)}` +
|
|
797
|
+
`{${fact.kind}}${fmtAnchor(identity.evidence, collisions)}`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
/** Free-text `reason`/`unassessedReason` are deliberately not rendered here:
|
|
801
|
+
* `status` (in the header tags) is the "kind of gap", and what each kind
|
|
802
|
+
* means/why is documented once in docs/dump-format.md §5.2 instead of being
|
|
803
|
+
* repeated as near-identical boilerplate prose across thousands of candidate
|
|
804
|
+
* lines (most `reason` strings are one of a handful of fixed templates from
|
|
805
|
+
* analysis/gap-audit/build.ts, not per-candidate information). The full text
|
|
806
|
+
* is still in gap-audit.json for anyone who needs the exact wording. */
|
|
807
|
+
function dumpGapAuditCandidate(candidate, depth, collisions, sourceFacts) {
|
|
808
|
+
const tags = [
|
|
809
|
+
candidate.source,
|
|
810
|
+
candidate.status,
|
|
811
|
+
candidate.critical ? "critical" : undefined,
|
|
812
|
+
].filter((tag) => tag !== undefined);
|
|
813
|
+
const symbol = candidate.evidence?.symbol;
|
|
814
|
+
const rawRecord = candidate.rawRecordRef
|
|
815
|
+
? ` (rawRecord=<${shortId(candidate.rawRecordRef)}>)`
|
|
816
|
+
: "";
|
|
817
|
+
let out =
|
|
818
|
+
`\n${pad(depth)}<${shortId(candidate.id)}>${symbol ? ` ${symbol}` : ""} ` +
|
|
819
|
+
`{${tags.join(", ")}} "${candidate.semanticKey}"${fmtAnchor(candidate.evidence, collisions)}${rawRecord}`;
|
|
820
|
+
const resolved =
|
|
821
|
+
sourceFacts !== undefined && candidate.normalizedFactRefs.length > 0;
|
|
822
|
+
if (resolved) {
|
|
823
|
+
for (const ref of candidate.normalizedFactRefs) {
|
|
824
|
+
out += dumpNormalizedFact(ref, sourceFacts, depth + 1, collisions);
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
// No resolved fact to show its own {kind} — expectedFactKinds is the
|
|
828
|
+
// only kind information available, gap or not.
|
|
829
|
+
out += fmtList("expectedFactKinds", candidate.expectedFactKinds, depth + 1);
|
|
830
|
+
out += fmtRefList(
|
|
831
|
+
"normalizedFactRefs",
|
|
832
|
+
candidate.normalizedFactRefs,
|
|
833
|
+
depth + 1,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return out;
|
|
837
|
+
}
|
|
838
|
+
/** `recognized` candidates aren't gaps — they already matched a real fact
|
|
839
|
+
* (see `normalizedFacts:`) — so they sink to the bottom of each category,
|
|
840
|
+
* keeping the statuses worth a reviewer's attention (unresolved, excluded-
|
|
841
|
+
* with-reason, limited) at the top. Stable within each group: ties keep
|
|
842
|
+
* `category.candidates`' own id order (see buildCategory's final `.sort`). */
|
|
843
|
+
function sortCandidatesForDump(candidates) {
|
|
844
|
+
return candidates
|
|
845
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
846
|
+
.sort((left, right) => {
|
|
847
|
+
const leftRecognized = left.candidate.status === "recognized" ? 1 : 0;
|
|
848
|
+
const rightRecognized = right.candidate.status === "recognized" ? 1 : 0;
|
|
849
|
+
return leftRecognized - rightRecognized || left.index - right.index;
|
|
850
|
+
})
|
|
851
|
+
.map((entry) => entry.candidate);
|
|
852
|
+
}
|
|
853
|
+
function dumpGapAuditCategory(category, depth, collisions, sourceFacts) {
|
|
854
|
+
let out =
|
|
855
|
+
`${pad(depth)}<category:${category.id}> {${category.assessment}} ` +
|
|
856
|
+
`"${category.label}" (${fmtCounts(category)})`;
|
|
857
|
+
out += fmtList("expectedFactKinds", category.expectedFactKinds, depth + 1);
|
|
858
|
+
if (category.unassessedReason) {
|
|
859
|
+
out += `\n${pad(depth + 1)}unassessedReason: ${category.unassessedReason}`;
|
|
860
|
+
}
|
|
861
|
+
for (const candidate of sortCandidatesForDump(category.candidates)) {
|
|
862
|
+
out += dumpGapAuditCandidate(candidate, depth + 1, collisions, sourceFacts);
|
|
863
|
+
}
|
|
864
|
+
return out;
|
|
865
|
+
}
|
|
866
|
+
function dumpSemanticLimitation(limitation, depth) {
|
|
867
|
+
const tags = [
|
|
868
|
+
limitation.code,
|
|
869
|
+
limitation.critical ? "critical" : undefined,
|
|
870
|
+
].filter((tag) => tag !== undefined);
|
|
871
|
+
let out = `${pad(depth)}<${shortId(limitation.id)}> {${tags.join(", ")}} ${limitation.message}`;
|
|
872
|
+
out += fmtRefList("candidates", limitation.candidateRefs, depth + 1);
|
|
873
|
+
out += fmtRefList("support", limitation.supportRefs, depth + 1);
|
|
874
|
+
return out;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Dumps a GapAuditArtifact as an indented tree instead of JSON. Passing the
|
|
878
|
+
* matching source-facts.json (same run) additionally resolves each
|
|
879
|
+
* candidate's `normalizedFactRefs` into their real Symbol/{kind}/@Anchor
|
|
880
|
+
* identity (dropping the now-redundant `expectedFactKinds` for those
|
|
881
|
+
* candidates — see `dumpNormalizedFact`); omitted, dump renders exactly as
|
|
882
|
+
* before, with bare `normalizedFactRefs: [<id>, ...]`.
|
|
883
|
+
*/
|
|
884
|
+
export function dumpGapAudit(audit, sourceFactsArtifact) {
|
|
885
|
+
const sourceFacts = buildSourceFactsIndex(sourceFactsArtifact);
|
|
886
|
+
const lines = [];
|
|
887
|
+
lines.push(
|
|
888
|
+
audit.target
|
|
889
|
+
? `GapAudit <${audit.caseId}> [${audit.target.applicationModule}:${audit.target.buildVariant}]`
|
|
890
|
+
: `GapAudit <${audit.caseId}>`,
|
|
891
|
+
);
|
|
892
|
+
lines.push(
|
|
893
|
+
`Availability: xml=${audit.sourceAvailability.androidXml} ` +
|
|
894
|
+
`jvm=${audit.sourceAvailability.rawJvm} source-facts=${audit.sourceAvailability.sourceFacts}`,
|
|
895
|
+
);
|
|
896
|
+
lines.push(`Completeness: {${audit.semanticCompleteness}}`);
|
|
897
|
+
lines.push(`Summary: ${fmtCounts(audit.summary)}`);
|
|
898
|
+
const allCandidates = audit.categories.flatMap(
|
|
899
|
+
(category) => category.candidates,
|
|
900
|
+
);
|
|
901
|
+
const candidateEvidencePaths = allCandidates.flatMap((candidate) =>
|
|
902
|
+
candidate.evidence ? [candidate.evidence.path] : [],
|
|
903
|
+
);
|
|
904
|
+
// Same collision universe also covers resolved normalizedFacts' own
|
|
905
|
+
// anchors, so a shortened candidate anchor and its resolved fact's anchor
|
|
906
|
+
// never disagree on whether a given basename needed the full path.
|
|
907
|
+
const normalizedFactPaths =
|
|
908
|
+
sourceFacts === undefined
|
|
909
|
+
? []
|
|
910
|
+
: allCandidates
|
|
911
|
+
.flatMap((candidate) => candidate.normalizedFactRefs)
|
|
912
|
+
.flatMap((ref) => {
|
|
913
|
+
const fact = sourceFacts.factsById.get(ref);
|
|
914
|
+
const evidence = fact
|
|
915
|
+
? normalizedFactIdentity(fact, sourceFacts).evidence
|
|
916
|
+
: undefined;
|
|
917
|
+
return evidence ? [evidence.path] : [];
|
|
918
|
+
});
|
|
919
|
+
const collisions = collidingBasenames([
|
|
920
|
+
...candidateEvidencePaths,
|
|
921
|
+
...normalizedFactPaths,
|
|
922
|
+
]);
|
|
923
|
+
lines.push("Categories:");
|
|
924
|
+
for (const category of audit.categories) {
|
|
925
|
+
lines.push(dumpGapAuditCategory(category, 1, collisions, sourceFacts));
|
|
926
|
+
}
|
|
927
|
+
if (audit.semanticLimitations.length === 0) {
|
|
928
|
+
lines.push("Semantic Limitations: none");
|
|
929
|
+
} else {
|
|
930
|
+
lines.push(`Semantic Limitations (${audit.semanticLimitations.length}):`);
|
|
931
|
+
for (const limitation of audit.semanticLimitations) {
|
|
932
|
+
lines.push(dumpSemanticLimitation(limitation, 1));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (audit.limitations.length === 0) {
|
|
936
|
+
lines.push("Limitations: none");
|
|
937
|
+
} else {
|
|
938
|
+
lines.push("Limitations:");
|
|
939
|
+
for (const text of audit.limitations) lines.push(`${pad(1)}- ${text}`);
|
|
940
|
+
}
|
|
941
|
+
return lines.join("\n");
|
|
942
|
+
}
|
|
943
|
+
// ---------------------------------------------------------------------------
|
|
944
|
+
// Diagnostics
|
|
945
|
+
// ---------------------------------------------------------------------------
|
|
946
|
+
/** `details` string values are near-always cross-reference ids (unresolvedRef,
|
|
947
|
+
* evidenceRefs, candidateRefs, ...) — run them through `shortId` like every
|
|
948
|
+
* other id in this dump instead of printing the full `tag:caseId/subkind/hash`
|
|
949
|
+
* (harmless no-op on the rare plain-text/non-id string, since `shortId`
|
|
950
|
+
* already passes those through unchanged). Arrays render `[a, b]` (matching
|
|
951
|
+
* `fmtRefList`'s bracket style) instead of a quoted JSON array. */
|
|
952
|
+
function fmtDetailValue(value) {
|
|
953
|
+
if (typeof value === "string") return shortId(value);
|
|
954
|
+
if (Array.isArray(value)) return `[${value.map(fmtDetailValue).join(", ")}]`;
|
|
955
|
+
return JSON.stringify(value);
|
|
956
|
+
}
|
|
957
|
+
/** `details.impact` (present only on diagnostics derived from an
|
|
958
|
+
* UnresolvedItem, see `diagnosticsForUnresolved` in pipeline/run-stage1.ts)
|
|
959
|
+
* promoted to its own line — it is the concrete "what breaks downstream"
|
|
960
|
+
* answer, the single most debugging-relevant field `details` ever carries,
|
|
961
|
+
* so it does not belong lumped into the generic key=value dump below. */
|
|
962
|
+
function detailImpact(details) {
|
|
963
|
+
const impact = details?.impact;
|
|
964
|
+
return Array.isArray(impact) &&
|
|
965
|
+
impact.every((item) => typeof item === "string")
|
|
966
|
+
? impact
|
|
967
|
+
: [];
|
|
968
|
+
}
|
|
969
|
+
/** Every remaining `details` key (impact excluded, already its own line) as
|
|
970
|
+
* one key=value line — shape varies by diagnostic origin (budget numbers,
|
|
971
|
+
* gap-audit candidateRefs, unresolved's evidenceRefs, ...), genuinely
|
|
972
|
+
* per-instance debugging payload, unlike `remediation`/`disposition` (see
|
|
973
|
+
* `dumpDiagnostics`'s own doc comment for why those are omitted instead). */
|
|
974
|
+
function fmtDetails(details, depth) {
|
|
975
|
+
if (!details) return "";
|
|
976
|
+
const entries = Object.entries(details).filter(([key]) => key !== "impact");
|
|
977
|
+
if (entries.length === 0) return "";
|
|
978
|
+
const line = entries
|
|
979
|
+
.map(([key, value]) => `${key}=${fmtDetailValue(value)}`)
|
|
980
|
+
.join(" ");
|
|
981
|
+
return `\n${pad(depth)}details: ${line}`;
|
|
982
|
+
}
|
|
983
|
+
function dumpDiagnostic(diagnostic, collisions) {
|
|
984
|
+
const tags = [
|
|
985
|
+
diagnostic.stage,
|
|
986
|
+
diagnostic.severity,
|
|
987
|
+
diagnostic.category,
|
|
988
|
+
].join(", ");
|
|
989
|
+
const entity = diagnostic.entityKey
|
|
990
|
+
? ` (entity=<${shortId(diagnostic.entityKey)}>)`
|
|
991
|
+
: "";
|
|
992
|
+
let out =
|
|
993
|
+
`${pad(1)}<${shortId(diagnostic.id)}> {${tags}} ${diagnostic.code}: ${diagnostic.message}` +
|
|
994
|
+
`${fmtAnchor(diagnostic.location, collisions)}${entity}`;
|
|
995
|
+
out += fmtList("impact", detailImpact(diagnostic.details), 2);
|
|
996
|
+
out += fmtDetails(diagnostic.details, 2);
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Dumps a flat `Diagnostic[]` (diagnostics.json) as one line per diagnostic
|
|
1001
|
+
* instead of a raw JSON array. Distinct from `dumpGapAudit`: gap-audit is a
|
|
1002
|
+
* full structural census — every source candidate the analyzer looked at,
|
|
1003
|
+
* recognized or not, organized by category (docs/dump-format.md §5) —
|
|
1004
|
+
* while diagnostics.json is the flat, error-oriented run log actually meant
|
|
1005
|
+
* to be read top to bottom. Some entries are *derived from* gap-audit's own
|
|
1006
|
+
* unresolved-critical candidates (`diagnosticsForGapAudit`) or from
|
|
1007
|
+
* `graph.unresolved` (`diagnosticsForUnresolved`, both in
|
|
1008
|
+
* pipeline/run-stage1.ts); the rest (budget/infrastructure failures) never
|
|
1009
|
+
* appear in gap-audit at all. Two JSON fields are deliberately not rendered
|
|
1010
|
+
* (see docs/dump-format.md §6): `remediation` (fixed boilerplate text per
|
|
1011
|
+
* `code`, not per-instance information — same reasoning as gap-audit's own
|
|
1012
|
+
* `reason`/`unassessedReason`) and `disposition` (100% derivable from
|
|
1013
|
+
* `severity`: "error" only ever occurs on a `fail-run` diagnostic,
|
|
1014
|
+
* "info"/"warning" only on "continue" — see FailingDiagnostic/
|
|
1015
|
+
* ContinuingDiagnostic in contracts/diagnostic.ts).
|
|
1016
|
+
*/
|
|
1017
|
+
export function dumpDiagnostics(diagnostics) {
|
|
1018
|
+
if (diagnostics.length === 0) return "Diagnostics: none";
|
|
1019
|
+
const collisions = collidingBasenames(
|
|
1020
|
+
diagnostics.flatMap((diagnostic) =>
|
|
1021
|
+
diagnostic.location ? [diagnostic.location.path] : [],
|
|
1022
|
+
),
|
|
1023
|
+
);
|
|
1024
|
+
const lines = [`Diagnostics (${diagnostics.length}):`];
|
|
1025
|
+
for (const diagnostic of diagnostics)
|
|
1026
|
+
lines.push(dumpDiagnostic(diagnostic, collisions));
|
|
1027
|
+
return lines.join("\n");
|
|
1028
|
+
}
|