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,1004 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdir,
|
|
3
|
+
readFile,
|
|
4
|
+
readdir,
|
|
5
|
+
rm,
|
|
6
|
+
stat,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { buildAreaContentTxt } from "./generation/index.js";
|
|
11
|
+
import {
|
|
12
|
+
filterRenderableAreas,
|
|
13
|
+
loadStage2AreaGraph,
|
|
14
|
+
loadStage2InputWithReport,
|
|
15
|
+
sortAreasReachableFirst,
|
|
16
|
+
} from "./stage2-input.js";
|
|
17
|
+
import { areaDirFor, renderDirFor } from "./agent-render/orchestrate.js";
|
|
18
|
+
import {
|
|
19
|
+
mergeAreaReqContents,
|
|
20
|
+
mergeReqContents,
|
|
21
|
+
} from "./agent-render/merge.js";
|
|
22
|
+
import { readAreaAttempt, readAttempt } from "./agent-render/snapshot.js";
|
|
23
|
+
/** Version of the local contract consumed by the a2h-spec-generate skill. */
|
|
24
|
+
export const SPEC_HANDOFF_CONTRACT_VERSION = "1.0.0";
|
|
25
|
+
export const SPEC_INPUT_FORMAT = "a2h-spec-generate-text-v1";
|
|
26
|
+
/** Validate the exact text boundary understood by
|
|
27
|
+
* `a2h-spec-generate/scripts/parse_requirements.mjs`. */
|
|
28
|
+
export function validateSpecInputText(text, expectedRequirementCount) {
|
|
29
|
+
const blocks = text
|
|
30
|
+
.replace(/^\uFEFF/u, "")
|
|
31
|
+
.replace(/\r\n?/gu, "\n")
|
|
32
|
+
.trim()
|
|
33
|
+
.split(/\n[ \t]*\n+/u)
|
|
34
|
+
.filter((block) => block.trim().length > 0);
|
|
35
|
+
if (
|
|
36
|
+
expectedRequirementCount !== undefined &&
|
|
37
|
+
blocks.length !== expectedRequirementCount
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Spec input contains ${blocks.length} parser blocks; expected ${expectedRequirementCount}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
for (const [index, block] of blocks.entries()) {
|
|
44
|
+
const firstLine = block.split("\n")[0]?.trim() ?? "";
|
|
45
|
+
if (firstLine.length === 0)
|
|
46
|
+
throw new Error(`Spec input block ${index + 1} has an empty title`);
|
|
47
|
+
if (/^(?:访问路径|区域说明|前置条件)\s*[::]/u.test(firstLine)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Spec input block ${index + 1} still exposes Stage 2 metadata`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const METADATA_PREFIXES = ["访问路径", "区域说明"];
|
|
55
|
+
function oneLine(value) {
|
|
56
|
+
return value
|
|
57
|
+
.replace(/\r\n?/gu, " ")
|
|
58
|
+
.replace(/[\t ]+/gu, " ")
|
|
59
|
+
.trim();
|
|
60
|
+
}
|
|
61
|
+
function uniqueNonEmpty(values) {
|
|
62
|
+
return [...new Set(values.map(oneLine).filter((value) => value.length > 0))];
|
|
63
|
+
}
|
|
64
|
+
function metadataKind(line) {
|
|
65
|
+
const match = /^(前置条件|访问路径|区域说明)\s*[::]/u.exec(line);
|
|
66
|
+
if (match?.[1] === "前置条件") return "precondition";
|
|
67
|
+
if (match?.[1] === "访问路径") return "path";
|
|
68
|
+
if (match?.[1] === "区域说明") return "description";
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function metadataValue(line) {
|
|
72
|
+
const separator = line.search(/[::]/u);
|
|
73
|
+
return separator < 0 ? "" : line.slice(separator + 1).trim();
|
|
74
|
+
}
|
|
75
|
+
function normalizeBody(value) {
|
|
76
|
+
// The downstream parser uses a blank line as the requirement separator.
|
|
77
|
+
// Keep ordinary line breaks (including list indentation), but remove blank
|
|
78
|
+
// lines so an Agent paragraph can never be mistaken for another REQ item.
|
|
79
|
+
return value
|
|
80
|
+
.replace(/^\uFEFF/u, "")
|
|
81
|
+
.replace(/\r\n?/gu, "\n")
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((line) => line.replace(/[ \t]+$/gu, ""))
|
|
84
|
+
.filter((line) => line.trim().length > 0)
|
|
85
|
+
.join("\n")
|
|
86
|
+
.trim();
|
|
87
|
+
}
|
|
88
|
+
function normalizeTitle(value) {
|
|
89
|
+
return oneLine(value);
|
|
90
|
+
}
|
|
91
|
+
/** Split the UI-path convention used by both the current renderer (` - `)
|
|
92
|
+
* and older requirement files (`-` without surrounding spaces). This helper
|
|
93
|
+
* is only used for ownership inference; the title itself is always preserved
|
|
94
|
+
* verbatim in the exported REQ text. */
|
|
95
|
+
function titleSegments(title) {
|
|
96
|
+
return normalizeTitle(title)
|
|
97
|
+
.split(/\s*-\s*/u)
|
|
98
|
+
.map((segment) => segment.trim())
|
|
99
|
+
.filter((segment) => segment.length > 0);
|
|
100
|
+
}
|
|
101
|
+
function comparableTitle(title) {
|
|
102
|
+
return titleSegments(title).join(" - ");
|
|
103
|
+
}
|
|
104
|
+
function looksLikeUiTitle(value, knownTitles) {
|
|
105
|
+
const title = normalizeTitle(value);
|
|
106
|
+
if (knownTitles.has(title)) return true;
|
|
107
|
+
// Stage 2's deterministic and Agent titles are navigation paths. The
|
|
108
|
+
// separator also keeps a continuation paragraph (usually a sentence or a
|
|
109
|
+
// list item) from being promoted to a new requirement in the legacy parser.
|
|
110
|
+
if (/^[-*]\s/u.test(title) || /^\d+[.)、]\s/u.test(title)) return false;
|
|
111
|
+
if (titleSegments(title).length >= 2) return true;
|
|
112
|
+
// A hand-written legacy title may contain only one page/control name. A
|
|
113
|
+
// sentence-like continuation normally ends in punctuation, so this keeps
|
|
114
|
+
// that useful compatibility case without reintroducing blank-line splits.
|
|
115
|
+
return title.length <= 160 && !/[。!?.!?;;::]$/u.test(title);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parse the old Stage 2 text formats without exposing their machine headers
|
|
119
|
+
* to the spec skill. This is deliberately a compatibility fallback; new
|
|
120
|
+
* Stage 2 runs are read from finalized output.json files instead.
|
|
121
|
+
*/
|
|
122
|
+
export function parseStage2ReqText(
|
|
123
|
+
text,
|
|
124
|
+
sourceFile = "REQ.txt",
|
|
125
|
+
knownTitles = new Set(),
|
|
126
|
+
) {
|
|
127
|
+
const normalized = text.replace(/^\uFEFF/u, "").replace(/\r\n?/gu, "\n");
|
|
128
|
+
const blocks = normalized.split(/\n[ \t]*\n+/u);
|
|
129
|
+
const preconditions = [];
|
|
130
|
+
const entries = [];
|
|
131
|
+
const warnings = [];
|
|
132
|
+
let activePreconditions = [];
|
|
133
|
+
for (const [blockIndex, rawBlock] of blocks.entries()) {
|
|
134
|
+
const lines = rawBlock
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((line) => line.trimEnd())
|
|
137
|
+
.filter((line) => !/^(```|~~~)\s*$/u.test(line));
|
|
138
|
+
if (lines.every((line) => line.trim().length === 0)) continue;
|
|
139
|
+
const contentLines = [];
|
|
140
|
+
const blockPreconditions = [];
|
|
141
|
+
let hasAreaMetadata = false;
|
|
142
|
+
for (const rawLine of lines) {
|
|
143
|
+
const line = rawLine.trim();
|
|
144
|
+
if (line.length === 0) continue;
|
|
145
|
+
const kind = metadataKind(line);
|
|
146
|
+
if (kind === "precondition") {
|
|
147
|
+
const value = metadataValue(line);
|
|
148
|
+
if (value.length > 0) {
|
|
149
|
+
preconditions.push(value);
|
|
150
|
+
blockPreconditions.push(value);
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (kind === "path" || kind === "description") {
|
|
155
|
+
// These labels describe an Area, not an individual requirement. They
|
|
156
|
+
// are intentionally not copied into the downstream input.
|
|
157
|
+
hasAreaMetadata = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
contentLines.push(rawLine);
|
|
161
|
+
}
|
|
162
|
+
if (hasAreaMetadata) {
|
|
163
|
+
// A path/description header marks a new Area in the old Markdown
|
|
164
|
+
// serializer. Do not leak the previous Area's preconditions into it.
|
|
165
|
+
activePreconditions = uniqueNonEmpty(blockPreconditions);
|
|
166
|
+
} else if (blockPreconditions.length > 0) {
|
|
167
|
+
// A plain req-N.txt commonly stores a standalone precondition block
|
|
168
|
+
// before its first title. Keep it active for all following entries in
|
|
169
|
+
// that file until another Area header resets it.
|
|
170
|
+
activePreconditions = uniqueNonEmpty([
|
|
171
|
+
...activePreconditions,
|
|
172
|
+
...blockPreconditions,
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
if (contentLines.length === 0) continue;
|
|
176
|
+
const title = normalizeTitle(contentLines[0] ?? "");
|
|
177
|
+
if (title.length === 0) continue;
|
|
178
|
+
if (!looksLikeUiTitle(title, knownTitles) && entries.length > 0) {
|
|
179
|
+
// Embedded blank lines are legal in Agent prose but have the same shape
|
|
180
|
+
// as the legacy separator. Treat a non-path block as continuation.
|
|
181
|
+
const previous = entries[entries.length - 1];
|
|
182
|
+
const continuation = normalizeBody(contentLines.join("\n"));
|
|
183
|
+
if (continuation.length > 0) {
|
|
184
|
+
entries[entries.length - 1] = {
|
|
185
|
+
...previous,
|
|
186
|
+
body: normalizeBody(`${previous.body}\n${continuation}`),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let body = normalizeBody(contentLines.slice(1).join("\n"));
|
|
192
|
+
if (body.startsWith(title)) {
|
|
193
|
+
body = normalizeBody(body.slice(title.length));
|
|
194
|
+
warnings.push(
|
|
195
|
+
`${sourceFile} block ${blockIndex + 1} repeated its title in the body; removed it`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
entries.push({
|
|
199
|
+
title,
|
|
200
|
+
body,
|
|
201
|
+
sourceIndex: blockIndex + 1,
|
|
202
|
+
preconditions: [...activePreconditions],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (entries.length === 0) {
|
|
206
|
+
warnings.push(
|
|
207
|
+
`${sourceFile} did not contain a recognizable UI-path requirement`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
preconditions: uniqueNonEmpty(preconditions),
|
|
212
|
+
entries,
|
|
213
|
+
warnings,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function inferAreaName(title) {
|
|
217
|
+
const segments = titleSegments(title);
|
|
218
|
+
if (segments.length >= 2) return segments[segments.length - 2];
|
|
219
|
+
return segments[0];
|
|
220
|
+
}
|
|
221
|
+
function inferPagePath(title, areaName) {
|
|
222
|
+
// A rendered title interleaves page names with actions (for example
|
|
223
|
+
// `点击"设置"`). Without the graph we cannot distinguish those boundaries
|
|
224
|
+
// reliably, so only return a path when the title contains no action-like
|
|
225
|
+
// segment. Otherwise the manifest uses the canonical empty path and emits
|
|
226
|
+
// a warning at the caller.
|
|
227
|
+
const segments = titleSegments(title);
|
|
228
|
+
if (segments.length < 2 || areaName === undefined) return [];
|
|
229
|
+
const areaIndex = segments.lastIndexOf(areaName);
|
|
230
|
+
if (areaIndex <= 0) return [];
|
|
231
|
+
const prefix = segments.slice(0, areaIndex);
|
|
232
|
+
return prefix.some((segment) =>
|
|
233
|
+
/^(?:点击|长按|选择|切换|输入|提交|返回|滑动|拖动|打开应用|满足启动条件)/u.test(
|
|
234
|
+
segment,
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
? []
|
|
238
|
+
: prefix;
|
|
239
|
+
}
|
|
240
|
+
function safeBatchStem(value, index, used) {
|
|
241
|
+
const base =
|
|
242
|
+
oneLine(value)
|
|
243
|
+
.normalize("NFKC")
|
|
244
|
+
.replace(/[<>:"\/\\|?*\u0000-\u001F]/gu, "-")
|
|
245
|
+
.replace(/\s+/gu, "_")
|
|
246
|
+
.replace(/-+/gu, "-")
|
|
247
|
+
.replace(/^[-_.]+|[-_.]+$/gu, "")
|
|
248
|
+
.slice(0, 72) || "area";
|
|
249
|
+
const prefix = `${String(index + 1).padStart(3, "0")}-${base}`;
|
|
250
|
+
let candidate = prefix;
|
|
251
|
+
let suffix = 2;
|
|
252
|
+
while (used.has(candidate) || candidate === "." || candidate === "..") {
|
|
253
|
+
candidate = `${prefix}-${suffix}`;
|
|
254
|
+
suffix += 1;
|
|
255
|
+
}
|
|
256
|
+
used.add(candidate);
|
|
257
|
+
return candidate;
|
|
258
|
+
}
|
|
259
|
+
function pagePathForArea(area) {
|
|
260
|
+
if (area.canonicalPath.status === "resolved") {
|
|
261
|
+
const values = area.canonicalPath.segments
|
|
262
|
+
.map((segment) => oneLine(segment.areaName))
|
|
263
|
+
.filter((value) => value.length > 0);
|
|
264
|
+
return [...new Set(values)];
|
|
265
|
+
}
|
|
266
|
+
// An unresolved or unreachable Area has no verified route. Returning its
|
|
267
|
+
// display name here would make the manifest look like a confirmed path;
|
|
268
|
+
// leave it empty and let the batch warning make the uncertainty visible.
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
function areaNameFor(area) {
|
|
272
|
+
if (area.displayName.status !== "resolved") return undefined;
|
|
273
|
+
const value = oneLine(area.displayName.value);
|
|
274
|
+
return value.length > 0 ? value : undefined;
|
|
275
|
+
}
|
|
276
|
+
function featureIdFor(source, index) {
|
|
277
|
+
const explicit = source.featureIds?.[index];
|
|
278
|
+
return explicit === undefined || explicit.length === 0 ? undefined : explicit;
|
|
279
|
+
}
|
|
280
|
+
function preconditionsFromHeaders(headers, warnings, sourceLabel) {
|
|
281
|
+
const values = [];
|
|
282
|
+
for (const header of headers) {
|
|
283
|
+
const line = oneLine(header);
|
|
284
|
+
const kind = metadataKind(line);
|
|
285
|
+
if (kind === "precondition") {
|
|
286
|
+
const value = metadataValue(line);
|
|
287
|
+
if (value.length > 0) values.push(value);
|
|
288
|
+
} else if (kind === "path" || kind === "description") {
|
|
289
|
+
// Explicitly discard the old Area metadata. It is not a requirement
|
|
290
|
+
// title and would become a phantom requirement in parse_requirements.mjs.
|
|
291
|
+
continue;
|
|
292
|
+
} else if (line.length > 0) {
|
|
293
|
+
warnings.push(`${sourceLabel}: discarded an unknown Area header`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return uniqueNonEmpty(values);
|
|
297
|
+
}
|
|
298
|
+
function normalizeFeatureEntry(
|
|
299
|
+
feature,
|
|
300
|
+
preconditions,
|
|
301
|
+
id,
|
|
302
|
+
warnings,
|
|
303
|
+
sourceLabel,
|
|
304
|
+
sourceIndex,
|
|
305
|
+
sourceFeatureId,
|
|
306
|
+
) {
|
|
307
|
+
let title = normalizeTitle(feature.title);
|
|
308
|
+
if (title.length === 0) {
|
|
309
|
+
warnings.push(
|
|
310
|
+
`${sourceLabel}: feature ${sourceIndex + 1} has no title and was skipped`,
|
|
311
|
+
);
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
let body = normalizeBody(feature.body);
|
|
315
|
+
if (body.length === 0) {
|
|
316
|
+
warnings.push(
|
|
317
|
+
`${sourceLabel}: ${title} has an empty body; it is kept for source-side completion`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const prefix =
|
|
321
|
+
preconditions.length === 0 ? "" : `前置条件:${preconditions.join(";")}`;
|
|
322
|
+
if (prefix.length > 0 && !body.startsWith(prefix)) {
|
|
323
|
+
body = body.length > 0 ? `${prefix}\n${body}` : prefix;
|
|
324
|
+
}
|
|
325
|
+
// A title is a single parser line. If a legacy producer put an Area or
|
|
326
|
+
// precondition label in that position, remove it rather than handing a
|
|
327
|
+
// machine header to the spec skill.
|
|
328
|
+
if (metadataKind(title) === "precondition") {
|
|
329
|
+
warnings.push(`${sourceLabel}: metadata-only feature was skipped`);
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
for (const prefixLabel of METADATA_PREFIXES) {
|
|
333
|
+
const metadataPrefix = new RegExp(`^${prefixLabel}\\s*[::]`, "u");
|
|
334
|
+
if (metadataPrefix.test(title))
|
|
335
|
+
title = title.replace(metadataPrefix, "").trim();
|
|
336
|
+
}
|
|
337
|
+
if (title.length === 0) {
|
|
338
|
+
warnings.push(`${sourceLabel}: metadata-only feature was skipped`);
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
id,
|
|
343
|
+
title,
|
|
344
|
+
body,
|
|
345
|
+
...(sourceFeatureId === undefined ? {} : { sourceFeatureId }),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Pure conversion from merged Area content to one spec-generator page batch
|
|
350
|
+
* per Area. Keeping one Area per input file is intentional: the downstream
|
|
351
|
+
* skill rejects a file whose requirements belong to different owning pages.
|
|
352
|
+
*/
|
|
353
|
+
export function buildSpecInputBatches(sources) {
|
|
354
|
+
const usedStems = new Set();
|
|
355
|
+
return sources.flatMap((source, sourceIndex) => {
|
|
356
|
+
const warnings = [];
|
|
357
|
+
const sourceLabel =
|
|
358
|
+
source.sourceFile ?? source.areaId ?? `Area ${sourceIndex + 1}`;
|
|
359
|
+
const headers = preconditionsFromHeaders(
|
|
360
|
+
source.content.headerLines,
|
|
361
|
+
warnings,
|
|
362
|
+
sourceLabel,
|
|
363
|
+
);
|
|
364
|
+
const requirements = source.content.features.flatMap(
|
|
365
|
+
(feature, featureIndex) => {
|
|
366
|
+
const explicitId = featureIdFor(source, featureIndex);
|
|
367
|
+
const id =
|
|
368
|
+
explicitId === undefined
|
|
369
|
+
? `${source.areaId ?? `area-${sourceIndex + 1}`}#${featureIndex + 1}`
|
|
370
|
+
: `${source.areaId ?? `area-${sourceIndex + 1}`}#${explicitId}`;
|
|
371
|
+
const requirement = normalizeFeatureEntry(
|
|
372
|
+
feature,
|
|
373
|
+
headers,
|
|
374
|
+
id,
|
|
375
|
+
warnings,
|
|
376
|
+
sourceLabel,
|
|
377
|
+
featureIndex,
|
|
378
|
+
explicitId,
|
|
379
|
+
);
|
|
380
|
+
return requirement === undefined ? [] : [requirement];
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
if (requirements.length === 0) {
|
|
384
|
+
warnings.push(`${sourceLabel}: no requirement was emitted for this Area`);
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
const firstTitle = requirements[0]?.title;
|
|
388
|
+
const owningPage =
|
|
389
|
+
oneLine(
|
|
390
|
+
source.areaName ??
|
|
391
|
+
(firstTitle === undefined ? "" : (inferAreaName(firstTitle) ?? "")),
|
|
392
|
+
) || "__shared_capability__";
|
|
393
|
+
const pagePath =
|
|
394
|
+
source.pagePath === undefined
|
|
395
|
+
? firstTitle === undefined
|
|
396
|
+
? []
|
|
397
|
+
: inferPagePath(firstTitle, owningPage)
|
|
398
|
+
: uniqueNonEmpty(source.pagePath);
|
|
399
|
+
if (pagePath.length === 0 && owningPage !== "__shared_capability__") {
|
|
400
|
+
warnings.push(
|
|
401
|
+
`${sourceLabel}: page-path was not available without a verified Stage 1 graph`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const stem = safeBatchStem(owningPage, sourceIndex, usedStems);
|
|
405
|
+
const text = `${requirements.map((item) => (item.body.length > 0 ? `${item.title}\n${item.body}` : item.title)).join("\n\n")}\n`;
|
|
406
|
+
return [
|
|
407
|
+
{
|
|
408
|
+
batchId: stem,
|
|
409
|
+
...(source.areaId === undefined ? {} : { areaId: source.areaId }),
|
|
410
|
+
owningPage,
|
|
411
|
+
pagePath,
|
|
412
|
+
requirements,
|
|
413
|
+
text,
|
|
414
|
+
...(source.sourceFile === undefined
|
|
415
|
+
? {}
|
|
416
|
+
: { sourceFile: source.sourceFile }),
|
|
417
|
+
warnings,
|
|
418
|
+
},
|
|
419
|
+
];
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
async function isFile(file) {
|
|
423
|
+
try {
|
|
424
|
+
return (await stat(file)).isFile();
|
|
425
|
+
} catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function isDirectory(directory) {
|
|
430
|
+
try {
|
|
431
|
+
return (await stat(directory)).isDirectory();
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function hasStage2Markers(directory) {
|
|
437
|
+
try {
|
|
438
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
439
|
+
return entries.some(
|
|
440
|
+
(entry) =>
|
|
441
|
+
(entry.isFile() &&
|
|
442
|
+
(/^(?:req|REQ)(?:-\d+)?\.txt$/u.test(entry.name) ||
|
|
443
|
+
entry.name === "req.md" ||
|
|
444
|
+
entry.name === "render-manifest.json")) ||
|
|
445
|
+
(entry.isDirectory() && entry.name === "rendering"),
|
|
446
|
+
);
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Resolve the conventional Stage 2 directory without making callers of the
|
|
453
|
+
* text-only compatibility path know whether they were handed a complete
|
|
454
|
+
* Stage 1 run or the `generation/` directory itself. An explicitly supplied
|
|
455
|
+
* `stage2Directory` always wins; otherwise a real `<input>/generation`
|
|
456
|
+
* directory is preferred, and a directory that already contains Stage 2
|
|
457
|
+
* files is used as-is.
|
|
458
|
+
*/
|
|
459
|
+
async function resolveStage2Directory(inputDirectory, explicitDirectory) {
|
|
460
|
+
if (explicitDirectory !== undefined) return path.resolve(explicitDirectory);
|
|
461
|
+
const conventional = path.join(inputDirectory, "generation");
|
|
462
|
+
if (await isDirectory(conventional)) return conventional;
|
|
463
|
+
if (await hasStage2Markers(inputDirectory)) return inputDirectory;
|
|
464
|
+
// Let readTextSources produce the more specific missing-directory error.
|
|
465
|
+
return conventional;
|
|
466
|
+
}
|
|
467
|
+
async function readTextSources(stage2Directory) {
|
|
468
|
+
let entries;
|
|
469
|
+
try {
|
|
470
|
+
entries = await readdir(stage2Directory, { withFileTypes: true });
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`Stage 2 output directory does not exist: ${stage2Directory}`,
|
|
474
|
+
{ cause: error },
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const numbered = entries
|
|
478
|
+
.filter((entry) => entry.isFile() && /^req-\d+\.txt$/iu.test(entry.name))
|
|
479
|
+
.sort(
|
|
480
|
+
(left, right) =>
|
|
481
|
+
Number(left.name.match(/\d+/u)?.[0] ?? 0) -
|
|
482
|
+
Number(right.name.match(/\d+/u)?.[0] ?? 0),
|
|
483
|
+
);
|
|
484
|
+
const legacy = entries
|
|
485
|
+
.filter(
|
|
486
|
+
(entry) =>
|
|
487
|
+
entry.isFile() && /^(?:REQ|req)(?:-\d+)?\.txt$/u.test(entry.name),
|
|
488
|
+
)
|
|
489
|
+
.filter((entry) => !numbered.some((item) => item.name === entry.name))
|
|
490
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
491
|
+
const selected = numbered.length > 0 ? numbered : legacy;
|
|
492
|
+
if (selected.length > 0) {
|
|
493
|
+
return Promise.all(
|
|
494
|
+
selected.map(async (entry) => ({
|
|
495
|
+
file: entry.name,
|
|
496
|
+
text: await readFile(path.join(stage2Directory, entry.name), "utf8"),
|
|
497
|
+
})),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const markdown = path.join(stage2Directory, "req.md");
|
|
501
|
+
if (await isFile(markdown)) {
|
|
502
|
+
return [{ file: "req.md", text: await readFile(markdown, "utf8") }];
|
|
503
|
+
}
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
function idsForModel(reqModel) {
|
|
507
|
+
return {
|
|
508
|
+
areaIds: new Set(reqModel.areas.map((area) => area.id)),
|
|
509
|
+
featureIds: new Set(
|
|
510
|
+
reqModel.areas.flatMap((area) =>
|
|
511
|
+
area.features.map((feature) => feature.id),
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
async function detectRenderMode(stage2Directory, reqModel, requested) {
|
|
517
|
+
if (requested !== "auto") return requested;
|
|
518
|
+
const ids = idsForModel(reqModel);
|
|
519
|
+
const manifestPath = path.join(stage2Directory, "render-manifest.json");
|
|
520
|
+
if (await isFile(manifestPath)) {
|
|
521
|
+
try {
|
|
522
|
+
const value = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
523
|
+
const entryIds = (value.entries ?? [])
|
|
524
|
+
.map((entry) => (typeof entry.id === "string" ? entry.id : undefined))
|
|
525
|
+
.filter((id) => id !== undefined);
|
|
526
|
+
const area = entryIds.some((id) => ids.areaIds.has(id));
|
|
527
|
+
const feature = entryIds.some((id) => ids.featureIds.has(id));
|
|
528
|
+
if (area && !feature) return "area";
|
|
529
|
+
if (feature && !area) return "feature";
|
|
530
|
+
} catch {
|
|
531
|
+
// A malformed optional manifest should not hide valid output.json files.
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const renderDir = path.join(stage2Directory, "rendering");
|
|
535
|
+
for (const area of reqModel.areas) {
|
|
536
|
+
if (await isFile(path.join(renderDir, areaDirFor(area.id), "output.json")))
|
|
537
|
+
return "area";
|
|
538
|
+
if (
|
|
539
|
+
await Promise.all(
|
|
540
|
+
area.features.map((feature) =>
|
|
541
|
+
isFile(path.join(renderDir, renderDirFor(feature.id), "output.json")),
|
|
542
|
+
),
|
|
543
|
+
).then((values) => values.some(Boolean))
|
|
544
|
+
)
|
|
545
|
+
return "feature";
|
|
546
|
+
}
|
|
547
|
+
return "text";
|
|
548
|
+
}
|
|
549
|
+
async function collectAreaResults(renderDirectory, reqModel) {
|
|
550
|
+
const pairs = await Promise.all(
|
|
551
|
+
reqModel.areas.map(async (area) => {
|
|
552
|
+
const result = await readAreaAttempt(
|
|
553
|
+
path.join(renderDirectory, areaDirFor(area.id), "output.json"),
|
|
554
|
+
);
|
|
555
|
+
// The directory name is the primary association, but finalized Area
|
|
556
|
+
// output must also carry the same non-empty embedded id. A copied or
|
|
557
|
+
// malformed output.json must not silently overwrite another Area's
|
|
558
|
+
// requirements.
|
|
559
|
+
if (
|
|
560
|
+
result === undefined ||
|
|
561
|
+
typeof result.areaId !== "string" ||
|
|
562
|
+
result.areaId.length === 0 ||
|
|
563
|
+
result.areaId !== area.id
|
|
564
|
+
) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
return [area.id, result];
|
|
568
|
+
}),
|
|
569
|
+
);
|
|
570
|
+
return new Map(pairs.filter((pair) => pair !== undefined));
|
|
571
|
+
}
|
|
572
|
+
async function collectFeatureResults(renderDirectory, reqModel) {
|
|
573
|
+
const pairs = await Promise.all(
|
|
574
|
+
reqModel.areas.flatMap((area) =>
|
|
575
|
+
area.features.map(async (feature) => {
|
|
576
|
+
const result = await readAttempt(
|
|
577
|
+
path.join(renderDirectory, renderDirFor(feature.id), "output.json"),
|
|
578
|
+
);
|
|
579
|
+
return result === undefined ? undefined : [feature.id, result];
|
|
580
|
+
}),
|
|
581
|
+
),
|
|
582
|
+
);
|
|
583
|
+
return new Map(pairs.filter((pair) => pair !== undefined));
|
|
584
|
+
}
|
|
585
|
+
function sourceFromMerged(merged, reqModel) {
|
|
586
|
+
const areas = new Map(reqModel.areas.map((area) => [area.id, area]));
|
|
587
|
+
return merged.flatMap((entry) => {
|
|
588
|
+
const area = areas.get(entry.areaId);
|
|
589
|
+
if (area === undefined) return [];
|
|
590
|
+
const remaining = [...area.features];
|
|
591
|
+
const featureIds = entry.content.features.map((feature) => {
|
|
592
|
+
const title = normalizeTitle(feature.title);
|
|
593
|
+
const matchIndex = remaining.findIndex((candidate) => {
|
|
594
|
+
const name =
|
|
595
|
+
candidate.displayName.status === "resolved"
|
|
596
|
+
? oneLine(candidate.displayName.value)
|
|
597
|
+
: "";
|
|
598
|
+
return (
|
|
599
|
+
name.length > 0 &&
|
|
600
|
+
(title.endsWith(` - ${name}`) || title.includes(` - ${name} - `))
|
|
601
|
+
);
|
|
602
|
+
});
|
|
603
|
+
if (matchIndex < 0) return undefined;
|
|
604
|
+
const [match] = remaining.splice(matchIndex, 1);
|
|
605
|
+
return match?.id;
|
|
606
|
+
});
|
|
607
|
+
return [
|
|
608
|
+
{
|
|
609
|
+
areaId: area.id,
|
|
610
|
+
areaName: areaNameFor(area),
|
|
611
|
+
pagePath: pagePathForArea(area),
|
|
612
|
+
content: entry.content,
|
|
613
|
+
featureIds,
|
|
614
|
+
},
|
|
615
|
+
];
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function knownTitlesFor(reqModel, graph) {
|
|
619
|
+
return new Set(
|
|
620
|
+
reqModel.areas.flatMap((area) =>
|
|
621
|
+
buildAreaContentTxt(area, graph).features.map((feature) =>
|
|
622
|
+
normalizeTitle(feature.title),
|
|
623
|
+
),
|
|
624
|
+
),
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
function matchAreaForTitle(title, reqModel, graph) {
|
|
628
|
+
const normalized = normalizeTitle(title);
|
|
629
|
+
const comparable = comparableTitle(normalized);
|
|
630
|
+
const exact = reqModel.areas.find((area) =>
|
|
631
|
+
buildAreaContentTxt(area, graph).features.some(
|
|
632
|
+
(feature) => comparableTitle(feature.title) === comparable,
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
if (exact !== undefined) return exact;
|
|
636
|
+
const candidates = reqModel.areas.filter((area) => {
|
|
637
|
+
const name = areaNameFor(area);
|
|
638
|
+
return name !== undefined && titleSegments(normalized).includes(name);
|
|
639
|
+
});
|
|
640
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
641
|
+
}
|
|
642
|
+
function sourcesFromText(
|
|
643
|
+
textSources,
|
|
644
|
+
reqModel,
|
|
645
|
+
graph,
|
|
646
|
+
includeUnreachableAreas,
|
|
647
|
+
) {
|
|
648
|
+
const warnings = [];
|
|
649
|
+
const known =
|
|
650
|
+
reqModel === undefined || graph === undefined
|
|
651
|
+
? new Set()
|
|
652
|
+
: knownTitlesFor(reqModel, graph);
|
|
653
|
+
const grouped = new Map();
|
|
654
|
+
for (const source of textSources) {
|
|
655
|
+
const parsed = parseStage2ReqText(source.text, source.file, known);
|
|
656
|
+
warnings.push(...parsed.warnings);
|
|
657
|
+
for (const entry of parsed.entries) {
|
|
658
|
+
const area =
|
|
659
|
+
reqModel === undefined || graph === undefined
|
|
660
|
+
? undefined
|
|
661
|
+
: matchAreaForTitle(entry.title, reqModel, graph);
|
|
662
|
+
if (reqModel !== undefined && area === undefined) {
|
|
663
|
+
warnings.push(
|
|
664
|
+
`${source.file} block ${entry.sourceIndex}: title could not be matched to a Stage 1 Area; page metadata is inferred`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const inferredName =
|
|
668
|
+
area === undefined
|
|
669
|
+
? (inferAreaName(entry.title) ?? source.file)
|
|
670
|
+
: (areaNameFor(area) ?? inferAreaName(entry.title) ?? source.file);
|
|
671
|
+
const key = area?.id ?? `${source.file}:${inferredName}`;
|
|
672
|
+
const current = grouped.get(key);
|
|
673
|
+
if (current === undefined) {
|
|
674
|
+
grouped.set(key, {
|
|
675
|
+
...(area === undefined ? {} : { area }),
|
|
676
|
+
file: source.file,
|
|
677
|
+
preconditions: [...entry.preconditions],
|
|
678
|
+
features: [{ title: entry.title, body: entry.body }],
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
current.features.push({ title: entry.title, body: entry.body });
|
|
682
|
+
current.preconditions.push(...entry.preconditions);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const groups = [...grouped.entries()];
|
|
687
|
+
const orderedGroups =
|
|
688
|
+
reqModel === undefined
|
|
689
|
+
? groups
|
|
690
|
+
: [
|
|
691
|
+
...groups.filter(
|
|
692
|
+
([, group]) => group.area?.canonicalPath.status !== "unreachable",
|
|
693
|
+
),
|
|
694
|
+
...groups.filter(
|
|
695
|
+
([, group]) => group.area?.canonicalPath.status === "unreachable",
|
|
696
|
+
),
|
|
697
|
+
];
|
|
698
|
+
const sources = [];
|
|
699
|
+
for (const [key, group] of orderedGroups) {
|
|
700
|
+
if (
|
|
701
|
+
!includeUnreachableAreas &&
|
|
702
|
+
group.area?.canonicalPath.status === "unreachable"
|
|
703
|
+
) {
|
|
704
|
+
warnings.push(
|
|
705
|
+
`${group.file}: dropped unreachable Area ${areaNameFor(group.area) ?? group.area.id}`,
|
|
706
|
+
);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const area = group.area;
|
|
710
|
+
const headers = uniqueNonEmpty(group.preconditions).map(
|
|
711
|
+
(value) => `前置条件:${value}`,
|
|
712
|
+
);
|
|
713
|
+
sources.push({
|
|
714
|
+
...(area === undefined
|
|
715
|
+
? { areaId: key }
|
|
716
|
+
: {
|
|
717
|
+
areaId: area.id,
|
|
718
|
+
areaName: areaNameFor(area),
|
|
719
|
+
pagePath: pagePathForArea(area),
|
|
720
|
+
}),
|
|
721
|
+
content: { headerLines: headers, features: group.features },
|
|
722
|
+
sourceFile: group.file,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
if (sources.length > 0)
|
|
726
|
+
warnings.push(
|
|
727
|
+
"使用了 req-N.txt/req.md 兼容解析;建议在下一次 Stage 2 运行中保留 rendering/output.json 以获得结构化合并",
|
|
728
|
+
);
|
|
729
|
+
return { sources, warnings };
|
|
730
|
+
}
|
|
731
|
+
async function loadStage1IfAvailable(inputDirectory) {
|
|
732
|
+
const required = ["run-report.json", "req-model.json", "area-graph.json"];
|
|
733
|
+
const available = await Promise.all(
|
|
734
|
+
required.map((name) => isFile(path.join(inputDirectory, name))),
|
|
735
|
+
);
|
|
736
|
+
if (!available.every(Boolean)) return undefined;
|
|
737
|
+
const loaded = await loadStage2InputWithReport(inputDirectory);
|
|
738
|
+
const graph = await loadStage2AreaGraph(
|
|
739
|
+
inputDirectory,
|
|
740
|
+
loaded.report,
|
|
741
|
+
loaded.reqModel,
|
|
742
|
+
);
|
|
743
|
+
const completeness =
|
|
744
|
+
loaded.report.semanticCompleteness ?? loaded.reqModel.semanticCompleteness;
|
|
745
|
+
return {
|
|
746
|
+
reqModel: sortAreasReachableFirst(loaded.reqModel),
|
|
747
|
+
graph,
|
|
748
|
+
...(completeness !== undefined && completeness !== "complete"
|
|
749
|
+
? {
|
|
750
|
+
semanticWarning: `Stage 1 标记为 ${completeness};本 handoff 保留该质量提示,未伪装为完整分析`,
|
|
751
|
+
}
|
|
752
|
+
: {}),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async function readReportMetadata(inputDirectory) {
|
|
756
|
+
try {
|
|
757
|
+
const value = JSON.parse(
|
|
758
|
+
await readFile(path.join(inputDirectory, "run-report.json"), "utf8"),
|
|
759
|
+
);
|
|
760
|
+
return {
|
|
761
|
+
...(typeof value.caseId === "string" ? { caseId: value.caseId } : {}),
|
|
762
|
+
...(typeof value.runId === "string" ? { runId: value.runId } : {}),
|
|
763
|
+
};
|
|
764
|
+
} catch {
|
|
765
|
+
return {};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async function replaceOutputDirectory(outputDirectory, replace) {
|
|
769
|
+
const absolute = path.resolve(outputDirectory);
|
|
770
|
+
if (absolute === path.parse(absolute).root || absolute === process.cwd()) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`Refusing to use a broad handoff output directory: ${absolute}`,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
let exists = false;
|
|
776
|
+
try {
|
|
777
|
+
exists = (await stat(absolute)).isDirectory();
|
|
778
|
+
} catch {
|
|
779
|
+
// The directory will be created below.
|
|
780
|
+
}
|
|
781
|
+
if (exists) {
|
|
782
|
+
const entries = await readdir(absolute);
|
|
783
|
+
if (entries.length > 0 && !replace) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Handoff output directory is not empty: ${absolute}; pass --replace to regenerate it`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
if (replace) await rm(absolute, { recursive: true, force: true });
|
|
789
|
+
}
|
|
790
|
+
await mkdir(absolute, { recursive: true });
|
|
791
|
+
}
|
|
792
|
+
function relativePath(from, to) {
|
|
793
|
+
const value = path.relative(from, to).replaceAll(path.sep, "/");
|
|
794
|
+
return value.length === 0 ? "." : value;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Export a completed Stage 2 run as page-local `REQ.txt` batches consumable by
|
|
798
|
+
* `$a2h-spec-generate`. The exporter never invokes a model or UTG/StageM;
|
|
799
|
+
* it only adapts the already finalized Lint + Stage 2 result.
|
|
800
|
+
*/
|
|
801
|
+
export async function exportSpecHandoff(options) {
|
|
802
|
+
const inputDirectory = path.resolve(options.inputDirectory);
|
|
803
|
+
const stage2Directory = await resolveStage2Directory(
|
|
804
|
+
inputDirectory,
|
|
805
|
+
options.stage2Directory,
|
|
806
|
+
);
|
|
807
|
+
const outputDirectory = path.resolve(
|
|
808
|
+
options.outputDirectory ?? path.join(stage2Directory, "spec-input"),
|
|
809
|
+
);
|
|
810
|
+
if (
|
|
811
|
+
outputDirectory === stage2Directory ||
|
|
812
|
+
outputDirectory === inputDirectory
|
|
813
|
+
) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
"Spec handoff output must be a separate directory from Stage 1/Stage 2 input",
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
const isAncestor = (parent, child) =>
|
|
819
|
+
child.startsWith(`${parent}${path.sep}`);
|
|
820
|
+
if (
|
|
821
|
+
isAncestor(outputDirectory, inputDirectory) ||
|
|
822
|
+
isAncestor(outputDirectory, stage2Directory)
|
|
823
|
+
) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
"Spec handoff output cannot contain the Stage 1 or Stage 2 input directory",
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const requestedMode = options.mode ?? "auto";
|
|
829
|
+
const loaded = await loadStage1IfAvailable(inputDirectory);
|
|
830
|
+
const warnings = [];
|
|
831
|
+
let textSources = [];
|
|
832
|
+
try {
|
|
833
|
+
textSources = await readTextSources(stage2Directory);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (loaded === undefined) throw error;
|
|
836
|
+
warnings.push("未找到 Stage 2 输出目录,将使用 Stage 1 确定性草稿");
|
|
837
|
+
}
|
|
838
|
+
let sources = [];
|
|
839
|
+
let selectedMode = "text";
|
|
840
|
+
let structuredModeAttempted = false;
|
|
841
|
+
if (loaded !== undefined && requestedMode !== "text") {
|
|
842
|
+
selectedMode = await detectRenderMode(
|
|
843
|
+
stage2Directory,
|
|
844
|
+
loaded.reqModel,
|
|
845
|
+
requestedMode,
|
|
846
|
+
);
|
|
847
|
+
if (selectedMode === "area" || selectedMode === "feature") {
|
|
848
|
+
const sorted = loaded.reqModel;
|
|
849
|
+
const renderable = filterRenderableAreas(sorted);
|
|
850
|
+
const renderDirectory = path.join(stage2Directory, "rendering");
|
|
851
|
+
if (selectedMode === "area") {
|
|
852
|
+
const areaResults = await collectAreaResults(
|
|
853
|
+
renderDirectory,
|
|
854
|
+
renderable,
|
|
855
|
+
);
|
|
856
|
+
if (areaResults.size > 0 || textSources.length === 0) {
|
|
857
|
+
structuredModeAttempted = true;
|
|
858
|
+
const merged = mergeAreaReqContents(
|
|
859
|
+
sorted,
|
|
860
|
+
renderable,
|
|
861
|
+
areaResults,
|
|
862
|
+
loaded.graph,
|
|
863
|
+
{
|
|
864
|
+
includeUnreachableAreas: options.includeUnreachableAreas ?? false,
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
sources = sourceFromMerged(merged, sorted);
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
870
|
+
const featureResults = await collectFeatureResults(
|
|
871
|
+
renderDirectory,
|
|
872
|
+
renderable,
|
|
873
|
+
);
|
|
874
|
+
if (featureResults.size > 0 || textSources.length === 0) {
|
|
875
|
+
structuredModeAttempted = true;
|
|
876
|
+
const merged = mergeReqContents(
|
|
877
|
+
sorted,
|
|
878
|
+
renderable,
|
|
879
|
+
featureResults,
|
|
880
|
+
loaded.graph,
|
|
881
|
+
{
|
|
882
|
+
includeUnreachableAreas: options.includeUnreachableAreas ?? false,
|
|
883
|
+
},
|
|
884
|
+
);
|
|
885
|
+
sources = sourceFromMerged(merged, sorted);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (sources.length === 0 && textSources.length > 0) {
|
|
889
|
+
warnings.push(
|
|
890
|
+
structuredModeAttempted
|
|
891
|
+
? "结构化 rendering 已被读取但没有可发布的 Area;不会用旧文本复活已过滤的内容"
|
|
892
|
+
: "结构化 rendering 输出为空,回退到已有 req 文本",
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
sources.length === 0 &&
|
|
899
|
+
textSources.length > 0 &&
|
|
900
|
+
!structuredModeAttempted
|
|
901
|
+
) {
|
|
902
|
+
const textResult = sourcesFromText(
|
|
903
|
+
textSources,
|
|
904
|
+
loaded?.reqModel,
|
|
905
|
+
loaded?.graph,
|
|
906
|
+
options.includeUnreachableAreas ?? false,
|
|
907
|
+
);
|
|
908
|
+
sources = textResult.sources;
|
|
909
|
+
warnings.push(...textResult.warnings);
|
|
910
|
+
selectedMode = "text";
|
|
911
|
+
}
|
|
912
|
+
if (sources.length === 0 && loaded !== undefined) {
|
|
913
|
+
// A Stage 1-only directory is still exportable, but make the quality
|
|
914
|
+
// degradation observable: this is a deterministic draft, not an Agent
|
|
915
|
+
// rendered Stage 2 result.
|
|
916
|
+
const sorted = loaded.reqModel;
|
|
917
|
+
const renderable = filterRenderableAreas(sorted);
|
|
918
|
+
const fallbackSources = sorted.areas.flatMap((area) => {
|
|
919
|
+
const content = buildAreaContentTxt(area, loaded.graph);
|
|
920
|
+
if (
|
|
921
|
+
content.features.length === 0 ||
|
|
922
|
+
!renderable.areas.some((candidate) => candidate.id === area.id)
|
|
923
|
+
)
|
|
924
|
+
return [];
|
|
925
|
+
return [
|
|
926
|
+
{
|
|
927
|
+
areaId: area.id,
|
|
928
|
+
areaName: areaNameFor(area),
|
|
929
|
+
pagePath: pagePathForArea(area),
|
|
930
|
+
content,
|
|
931
|
+
featureIds: area.features.map((feature) => feature.id),
|
|
932
|
+
},
|
|
933
|
+
];
|
|
934
|
+
});
|
|
935
|
+
sources = fallbackSources;
|
|
936
|
+
selectedMode = "text";
|
|
937
|
+
warnings.push(
|
|
938
|
+
"未找到 finalized Stage 2 输出;导出了 Stage 1 确定性草稿,请在交给 spec 前确认正文",
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (sources.length === 0) {
|
|
942
|
+
throw new Error(`No Stage 2 requirements found under ${stage2Directory}`);
|
|
943
|
+
}
|
|
944
|
+
if (loaded?.semanticWarning !== undefined)
|
|
945
|
+
warnings.push(loaded.semanticWarning);
|
|
946
|
+
const built = buildSpecInputBatches(sources);
|
|
947
|
+
warnings.push(...built.flatMap((batch) => batch.warnings));
|
|
948
|
+
if (built.length === 0)
|
|
949
|
+
throw new Error("Stage 2 output contains no non-empty requirement batch");
|
|
950
|
+
await replaceOutputDirectory(outputDirectory, options.replace ?? false);
|
|
951
|
+
const manifestBatches = [];
|
|
952
|
+
for (const batch of built) {
|
|
953
|
+
const batchDirectory = path.join(outputDirectory, batch.batchId);
|
|
954
|
+
await mkdir(batchDirectory, { recursive: true });
|
|
955
|
+
const requirementFile = `${batch.batchId}/REQ.txt`;
|
|
956
|
+
validateSpecInputText(batch.text, batch.requirements.length);
|
|
957
|
+
await writeFile(path.join(batchDirectory, "REQ.txt"), batch.text, "utf8");
|
|
958
|
+
if (/^(?:访问路径|区域说明)\s*[::]/mu.test(batch.text)) {
|
|
959
|
+
throw new Error(
|
|
960
|
+
`Generated spec input still contains Stage 2 Area metadata: ${requirementFile}`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
manifestBatches.push({
|
|
964
|
+
batchId: batch.batchId,
|
|
965
|
+
...(batch.areaId === undefined ? {} : { areaId: batch.areaId }),
|
|
966
|
+
owningPage: batch.owningPage,
|
|
967
|
+
pagePath: batch.pagePath,
|
|
968
|
+
requirementFile,
|
|
969
|
+
requirementCount: batch.requirements.length,
|
|
970
|
+
requirementIds: batch.requirements.map((requirement) => requirement.id),
|
|
971
|
+
...(batch.sourceFile === undefined
|
|
972
|
+
? {}
|
|
973
|
+
: { sourceFile: batch.sourceFile }),
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
const reportMetadata = await readReportMetadata(inputDirectory);
|
|
977
|
+
const manifest = {
|
|
978
|
+
contractVersion: SPEC_HANDOFF_CONTRACT_VERSION,
|
|
979
|
+
artifactKind: "spec-handoff-manifest",
|
|
980
|
+
inputFormat: SPEC_INPUT_FORMAT,
|
|
981
|
+
source: {
|
|
982
|
+
...(loaded === undefined
|
|
983
|
+
? {}
|
|
984
|
+
: { stage1Directory: relativePath(outputDirectory, inputDirectory) }),
|
|
985
|
+
stage2Directory: relativePath(outputDirectory, stage2Directory),
|
|
986
|
+
mode: selectedMode,
|
|
987
|
+
},
|
|
988
|
+
...(reportMetadata.caseId === undefined
|
|
989
|
+
? {}
|
|
990
|
+
: { caseId: reportMetadata.caseId }),
|
|
991
|
+
...(reportMetadata.runId === undefined
|
|
992
|
+
? {}
|
|
993
|
+
: { runId: reportMetadata.runId }),
|
|
994
|
+
batches: manifestBatches,
|
|
995
|
+
warnings: [...new Set(warnings)],
|
|
996
|
+
};
|
|
997
|
+
const manifestPath = path.join(outputDirectory, "manifest.json");
|
|
998
|
+
await writeFile(
|
|
999
|
+
manifestPath,
|
|
1000
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
1001
|
+
"utf8",
|
|
1002
|
+
);
|
|
1003
|
+
return { outputDirectory, manifestPath, manifest };
|
|
1004
|
+
}
|