@harmonyos-arkts/d2h 0.1.19 → 0.1.21
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/.claude-plugin/plugin.json +1 -1
- package/README.md +40 -0
- package/agents/android-to-hmos-00-orchestrator.md +3 -315
- package/agents/d2h.md +53 -38
- package/contracts/workflow/artifacts.md +22 -0
- package/contracts/workflow/delivery.md +54 -0
- package/contracts/workflow/dispatch.md +26 -0
- package/contracts/workflow/execution.md +125 -0
- package/contracts/workflow/generation.md +40 -0
- package/contracts/workflow/orchestration.md +49 -0
- package/contracts/workflow/overview.md +58 -0
- package/contracts/workflow/plan.md +119 -0
- package/contracts/workflow/scope.md +151 -0
- package/dist/bin/install-opencode.js +34 -15
- package/dist/src/install-codex.js +131 -0
- package/dist/src/install-opencode.js +5 -5
- package/dist/src/workflow.js +54 -0
- package/docs/architecture-plan-guide.zh-CN.md +70 -0
- package/docs/incremental-spec-evaluation-design.md +670 -0
- package/docs/spec-plan-guide.md +226 -0
- package/docs/workflow-decoupling.md +59 -0
- package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
- package/opencode/interactive-tools/browser-host.ts +110 -0
- package/opencode/interactive-tools/core.ts +242 -0
- package/opencode/interactive-tools/demo-workflow.ts +55 -0
- package/opencode/interactive-tools/demo.mjs +37 -0
- package/opencode/interactive-tools/package-lock.json +1876 -0
- package/opencode/interactive-tools/package.json +23 -0
- package/opencode/interactive-tools/panel.ts +40 -0
- package/opencode/interactive-tools/prepare-demo.mjs +17 -0
- package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
- package/opencode/interactive-tools/progress.ts +118 -0
- package/opencode/interactive-tools/server-legacy.mjs +2 -0
- package/opencode/interactive-tools/server.mjs +40 -0
- package/opencode/interactive-tools/tui.tsx +230 -0
- package/package.json +9 -1
- package/schemas/native-libs-analysis-report.schema.json +695 -0
- package/schemas/native-libs-verify-report.schema.json +566 -0
- package/schemas/spec-native-libs-shard.schema.json +110 -0
- package/skills/arkts-icon-sizing/SKILL.md +0 -10
- package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
- package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
- package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
- package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
- package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
- package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
- package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
- package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
- package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
- package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
- package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
- package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
- package/skills/d2h-app-solution/SKILL.md +198 -0
- package/skills/d2h-app-solution/agents/openai.yaml +4 -0
- package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
- package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
- package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
- package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
- package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
- package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
- package/skills/d2h-architecture-plan/SKILL.md +35 -0
- package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
- package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
- package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
- package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
- package/skills/d2h-architecture-plan/references/source-review.md +50 -0
- package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
- package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
- package/skills/d2h-dashboard/SKILL.md +56 -0
- package/skills/d2h-product-family-migration/SKILL.md +103 -0
- package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
- package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
- package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
- package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
- package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
- package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
- package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
- package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
- package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
- package/skills/{d2h-page-develop → d2h-spec-develop}/SKILL.md +143 -132
- package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
- package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
- package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
- package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
- package/skills/d2h-spec-generate/references/from-plan.md +19 -0
- package/skills/d2h-spec-generate/references/spec-content.md +34 -0
- package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
- package/skills/d2h-spec-plan/SKILL.md +38 -0
- package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
- package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
- package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
- package/skills/d2h-spec-plan/references/planning-method.md +46 -0
- package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
- package/skills/d2h-spec-plan/references/work-packets.md +61 -0
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
- package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
- package/skills/d2h-utg-static-generate/SKILL.md +2 -2
- package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
- package/skills/managing-d2h-toolchain/toolchain.json +1 -1
- package/skills/preparing-migration-workspace/SKILL.md +32 -2
- package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
- package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
- package/workflow/artifact_bindings.py +224 -0
- package/workflow/assets/dashboard.html +88 -0
- package/workflow/build_batch_report.py +8 -0
- package/workflow/collect_code_metrics.py +75 -0
- package/workflow/dashboard_overview.py +109 -0
- package/workflow/delivery_view.py +82 -0
- package/workflow/feature_plan.py +234 -0
- package/workflow/page_batches.py +223 -0
- package/workflow/spec_generation.py +120 -0
- package/workflow/spec_runtime.py +890 -0
- package/workflow/work_packets.py +85 -0
- package/skills/d2h-page-develop/agents/openai.yaml +0 -7
- package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
- package/skills/native-library-substitution/SKILL.md +0 -385
- package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
- package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
- package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
- package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
- package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>d2h · Spec 交付看板</title><style>
|
|
2
|
+
:root{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#233144;background:#f5f7fa;--muted:#738093;--line:#e5eaf0;--blue:#2463eb;--green:#159474}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button,select{cursor:pointer}button,a,select,input{outline-offset:3px}button{border:1px solid #dce3ec;background:#fff;color:#34445c;border-radius:7px;padding:7px 12px}button:hover{border-color:#8aaaf2;background:#f0f5ff}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--blue);color:#fff;border-color:var(--blue)}aside{position:fixed;width:214px;inset:0 auto 0 0;background:#fff;border-right:1px solid var(--line);padding:25px 16px;display:flex;flex-direction:column}.brand{font-size:22px;font-weight:750;letter-spacing:-1px;padding:0 12px}.brand b{color:var(--blue)}.brand small{display:block;font-size:11px;color:var(--muted);font-weight:500;letter-spacing:1.6px;margin:0 0 32px}nav{display:grid;gap:6px}nav button{text-align:left;border:0;background:none;padding:11px 14px}nav button.active{background:#edf3ff;color:var(--blue);font-weight:600}nav span{display:inline-block;width:28px;color:#8191a8}.side-note{margin-top:auto;font-size:12px;color:var(--muted);padding:12px}.side-note strong{color:#41536e}.shell{margin-left:214px}header{height:73px;background:#fff;border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;justify-content:space-between;gap:12px}header strong{font-size:15px}small,.muted{color:var(--muted)}small{font-size:12px}.tag{font-size:11px;padding:3px 8px;border-radius:5px;background:#eef2f6;color:#5d6c80;white-space:nowrap}.tag.green{background:#e6f6ef;color:#15825e}.tag.blue{background:#eaf1ff;color:#245cc6}.tag.orange{background:#fff2df;color:#a15c14}.tag.red{background:#fff0f0;color:#bc4a47}main{max-width:1550px;margin:auto;padding:26px 32px 48px}.page-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:22px}h1{font-size:25px;letter-spacing:-.7px;margin:0 0 3px}h2{font-size:15px;margin:0 0 15px}h3{font-size:14px;margin:0 0 7px}p{margin:8px 0}.flow{display:grid;grid-template-columns:repeat(5,1fr);border:1px solid var(--line);border-radius:10px;background:#fff;margin-bottom:24px;overflow:hidden}.flow div{padding:13px 16px;border-right:1px solid var(--line);color:var(--muted);font-size:12px}.flow div:last-child{border:0}.flow div.current{background:#eef4ff;color:#245cc6}.flow b{display:block;font-size:13px;margin-bottom:2px}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}.card,.panel{border:1px solid var(--line);border-radius:10px;background:#fff}.card{padding:19px}.card strong{display:block;font-size:29px;font-weight:650;letter-spacing:-1px;margin:6px 0}.card strong em{font-size:15px;font-style:normal;color:var(--muted);font-weight:400;letter-spacing:0}.card .muted{font-size:12px}.bar{height:6px;background:#edf1f7;border-radius:6px;overflow:hidden;margin:10px 0}.bar i{display:block;height:100%;background:var(--blue);border-radius:6px}.bar i.green{background:var(--green)}.panel{padding:22px;margin-bottom:20px}.columns{display:grid;grid-template-columns:1.8fr 1fr;gap:20px}.row{display:flex;align-items:center;justify-content:space-between;gap:12px}.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0 0 18px}.toolbar .push{margin-left:auto}input[type=search]{padding:8px 11px;min-width:220px;border:1px solid #dce3ec;border-radius:7px}select{padding:5px 8px;border:1px solid #dce3ec;border-radius:6px;background:#fff;color:#34445c}.preset.active{background:#edf3ff;color:#2463eb;border-color:#90b2f9}table{width:100%;border-collapse:collapse;text-align:left;white-space:nowrap;font-size:13px}th{font-size:12px;color:var(--muted);font-weight:500;background:#f9fafc}td,th{padding:12px;border-bottom:1px solid #edf0f4}td small{display:block}td.title{white-space:normal;min-width:180px}td select{font-size:12px}.table-wrap{overflow:auto}input[type=checkbox]{accent-color:var(--blue);width:15px;height:15px;cursor:pointer}.scope{padding-left:36px!important;color:#61728a;font-size:12px}.scope:before{content:'↳';margin-right:9px;color:#acb6c5}.notice{background:#fff8e9;color:#976520;border:1px solid #fae8bf;border-radius:7px;padding:11px 14px;font-size:12px;margin-bottom:20px}.notice.blue{background:#f0f5ff;border-color:#dce7fc;color:#526b98}.task{padding:16px 0;border-top:1px solid #edf0f4}.task:first-of-type{border-top:0}.task p{font-size:12px;color:var(--muted);margin-bottom:0}.task strong{font-size:13px}.link{border:0;background:none;color:var(--blue);padding:0;font-size:12px}.layer{padding:13px 0;border-top:1px solid #edf0f4}.layer:first-of-type{border:0}.layer.indent{padding-left:20px}.layer small{font-size:11px}.layer .bar{margin:7px 0}.timeline{border-left:2px solid #e6ecf6;padding-left:16px;margin:12px 0 0 5px}.timeline p{font-size:12px}.timeline strong{color:#41536e}.empty{text-align:center;padding:35px;color:var(--muted)}dialog{border:1px solid var(--line);border-radius:12px;max-width:660px;width:90%;padding:26px;color:#233144;box-shadow:0 18px 80px #182a4930}dialog::backdrop{background:#14243c55}dialog p{white-space:pre-line}.status-line{font-size:12px;color:var(--muted);margin-top:20px}.toast{position:fixed;bottom:24px;right:24px;background:#223247;color:#fff;padding:12px 20px;border-radius:8px;box-shadow:0 5px 20px #0002;z-index:3}.review-select{max-width:140px}.analysis-step{padding:18px 0;border-top:1px solid #edf0f4}.analysis-step:first-of-type{border:0}.kicker{font-size:11px;text-transform:uppercase;letter-spacing:1.5px;color:#8a99ae;margin-bottom:5px}a{color:var(--blue)}@media(min-width:1500px){main{padding-top:32px}}@media(max-width:1050px){aside{width:180px}.shell{margin-left:180px}main{padding:24px}.cards{grid-template-columns:repeat(2,1fr)}.columns{grid-template-columns:1fr}}@media(max-width:700px){aside{position:static;width:auto;padding:14px}.brand small,.side-note{display:none}nav{display:flex;overflow:auto;margin-top:12px}nav button{white-space:nowrap;padding:8px}nav span{display:none}.shell{margin:0}header{padding:12px 18px;height:auto;flex-wrap:wrap}main{padding:18px}.flow{overflow:auto;display:flex}.flow div{min-width:140px}.cards{gap:10px}.card{padding:14px}.page-heading{align-items:flex-start;flex-direction:column}.panel{padding:16px}.toolbar .push{margin-left:0}.card strong{font-size:24px}}
|
|
3
|
+
|
|
4
|
+
.overview-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px}.overview-grid .panel{margin:0;min-width:0}.span-3{grid-column:span 3}.span-4{grid-column:span 4}.span-6{grid-column:span 6}.span-8{grid-column:span 8}.board-value{font-size:30px;font-weight:650;margin:6px 0 12px}.board-value small{font-weight:400;font-size:12px}.code-numbers{display:flex;gap:32px;margin:16px 0}.code-numbers strong{font-size:28px;font-weight:650;display:block}.code-numbers small{display:block}.compact-item{display:flex;gap:10px;justify-content:space-between;align-items:center;padding:9px 0;border-bottom:1px solid #eef1f5;font-size:12px}.compact-item .link{text-align:left}.board-entry{width:100%;margin-top:16px;font-size:12px}.language-list{display:grid;gap:7px;margin:14px 0}.next-up,.analysis-inline{padding-top:16px;margin-top:16px;border-top:1px solid #edf0f4}.current-title{font-size:14px;font-weight:600;text-align:left}.activity-rail{height:7px;background:#edf1f7;border-radius:8px;overflow:hidden;margin:12px 0}.activity-rail i{display:block;height:100%;background:#2463eb;border-radius:8px}.activity-rail.is-indeterminate i{width:35%;opacity:0}.activity-rail.is-active.is-indeterminate i{opacity:1;animation:d2h-indeterminate 1.5s ease-in-out infinite}.activity-rail.is-active:not(.is-indeterminate) i{background-image:linear-gradient(45deg,#ffffff30 25%,transparent 25%,transparent 50%,#ffffff30 50%,#ffffff30 75%,transparent 75%);background-size:24px 24px;animation:d2h-stripes 1s linear infinite}.activity-rail.is-stale i{animation-play-state:paused!important;opacity:.35}.overview-stage h2{margin-bottom:8px}.analysis-inline small{overflow-wrap:anywhere}@keyframes d2h-indeterminate{0%{transform:translateX(-110%)}100%{transform:translateX(390%)}}@keyframes d2h-stripes{to{background-position:24px 0}}@media(prefers-reduced-motion:reduce){.activity-rail i{animation:none!important;transform:none!important}}@media(max-width:1100px){.span-3{grid-column:span 6}.span-8{grid-column:span 12}.span-4{grid-column:span 6}}@media(max-width:700px){.span-4,.span-6,.span-8{grid-column:span 12}.code-numbers{gap:24px}}@media(max-width:430px){.span-3{grid-column:span 12}}
|
|
5
|
+
|
|
6
|
+
.section-index{font-size:11px;color:#8b9ab2;letter-spacing:1px;margin-right:10px}.status-overview{padding-bottom:0}.status-overview>.row h2{margin:0}.total-time{display:flex;align-items:center;gap:10px}.total-time strong{font-size:20px}.phase-track{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:20px 0}.phase-cell{text-align:left;padding:13px 14px;background:#f8fafc;border:1px solid #e9edf3;min-width:0}.phase-cell strong,.phase-cell small{display:block}.phase-cell strong{font-size:12px;margin:6px 0;font-weight:500}.phase-cell small{font-size:11px}.phase-label{font-weight:650;font-size:13px}.phase-active{background:#edf4ff;border-color:#b6ccfa}.live-dot{display:inline-block;width:7px;height:7px;background:#2463eb;border-radius:50%;margin-right:6px}.overview-sentence{background:#f8fafc;margin:0 -22px;padding:14px 22px;border-top:1px solid #edf0f4;display:flex;align-items:center;justify-content:space-between;gap:14px;border-radius:0 0 10px 10px}.overview-sentence strong{font-size:13px;font-weight:500}.overview-sentence small{display:block;margin-top:3px;font-size:11px}.task-overview,.project-overview{margin-bottom:18px}.task-overview .panel,.project-overview .panel{padding:20px}.summary-counts{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin:20px 0 14px}.summary-counts strong{display:block;font-size:26px;font-weight:650}.summary-counts small{font-size:11px}.blue-number{color:#2463eb}.current-brief{padding-top:5px}.idle-task{padding-top:10px;min-height:92px;display:flex;flex-direction:column;justify-content:center;gap:7px;color:#65758d}.current-title{font-size:14px}.project-metrics,.environment-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:15px 0}.environment-summary{grid-template-columns:repeat(4,1fr)}.project-metrics strong,.environment-summary strong{display:block;font-size:24px;font-weight:600}.project-metrics small,.environment-summary small{font-size:11px}.stack-pills{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 8px}.blocker-item{display:flex;gap:12px;align-items:flex-start;padding:14px 0;border-top:1px solid #edf0f4}.blocker-item>div{flex:1;min-width:0}.blocker-item strong{font-size:12px}.blocker-item p{font-size:12px;color:#6e7e92;margin:4px 0;overflow-wrap:anywhere}.blocker-item>.link{flex-shrink:0;margin-top:3px}.blocker-dot{width:6px;height:6px;background:#d9993a;border-radius:50%;margin-top:9px}.clear-blockers{font-size:13px;color:#547064}.clear-blockers small{display:block;margin-top:5px}@media(min-width:1101px){.task-overview .span-3 h2{font-size:14px}.task-overview .span-3 .row{gap:5px}}@media(max-width:1100px){.task-overview .span-6{grid-column:span 12}}@media(max-width:760px){.phase-track{grid-template-columns:repeat(3,minmax(0,1fr))}.overview-sentence{display:block}.overview-sentence>.tag{display:inline-block;margin-top:10px}}@media(max-width:700px){.overview-sentence{margin:0 -16px;padding:14px 16px}.project-overview .span-6{grid-column:span 12}}@media(max-width:430px){.phase-track{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
7
|
+
|
|
8
|
+
/* Restrained color: section accents identify areas; status colors keep their meaning. */
|
|
9
|
+
:root{--blue:#3766dc;--green:#168168;--line:#e2e8f1;background:#f3f6fb;color:#26364d}body{background:#f3f6fb}.panel{box-shadow:0 2px 8px #29456a03}header{background:#ffffffeb}nav button.active{background:#eaf0ff;color:#2d59c2;box-shadow:inset 3px 0 #3766dc}nav button.active span{color:#3766dc}.kicker{color:#6d83a9}.section-index{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:22px;border-radius:6px;color:#4566a4;background:#eaf0fc;font-weight:650;letter-spacing:0}.status-overview{border-top:3px solid #6285df;background:linear-gradient(110deg,#fbfcff,#fff 65%)}.total-time{padding:5px 11px;border-radius:7px;background:#f0f4fc}.total-time strong{color:#4d638b}.phase-cell{background:#f7f9fc;border-color:#e5eaf3}.phase-cell:hover{background:#f0f4fd;border-color:#adc1ea}.phase-active{background:#eaf1ff;border-color:#b5c9f7;box-shadow:inset 0 3px #4c77dc}.phase-active .phase-label,.phase-active strong{color:#2858c4}.phase-done{background:#edf8f3;border-color:#c6e8d9;box-shadow:inset 0 3px #55a68a}.phase-done .phase-label,.phase-done strong{color:#177657}.phase-error{background:#fff0f0;border-color:#f0c8c8;box-shadow:inset 0 3px #d26969}.phase-error strong{color:#b54545}.overview-sentence{background:#f0f4fc;border-top-color:#e0e8f7}.overview-sentence strong{color:#3b5279}.task-overview>.panel:first-child{border-top:3px solid #5e85dc;background:linear-gradient(125deg,#f8fbff,#fff 70%)}.task-overview>.panel:nth-child(2){border-top:3px solid #8c85c2}.task-overview>.panel:nth-child(3){border-top:3px solid #61a6a1}.summary-counts>div:first-child strong{color:#168168}.summary-counts>div:last-child strong{color:#66768b}.summary-counts .blue-number{color:#3766dc}.task-overview>.panel:nth-child(3) .bar i{background:#3e9d94}.project-overview>.panel:first-child{border-top:3px solid #65a7a3;background:linear-gradient(125deg,#f6fbfa,#fff 70%)}.project-overview>.panel:first-child .section-index{color:#39867f;background:#e7f5f1}.project-metrics strong{color:#356b69}.project-overview>.panel:last-child{border-top:3px solid #95a4b9}.stack-pills .tag{color:#48736e;background:#edf5f2}.environment-summary>div{padding:7px 9px;border-radius:7px;background:#f4f6fa}.environment-summary>div:nth-child(1){background:#edf8f2}.environment-summary>div:nth-child(1) strong{color:#168168}.environment-summary>div:nth-child(2) strong{color:#bc4a47}.environment-summary>div:nth-child(3) strong{color:#a96825}.environment-summary>div:nth-child(4) strong{color:#7c8798}.environment-summary.is-report-stale>div{background:#f5f3ef}.environment-summary.is-report-stale strong{color:#8c785c}.blockers-overview.has-blockers{border-top:3px solid #d6a25a;background:linear-gradient(110deg,#fffcf6,#fff 75%)}.blockers-overview .section-index{color:#9d7239;background:#fff0d9}.blockers-overview .blocker-item{border-top-color:#f0e6d7}.blockers-overview .blocker-item strong{color:#6c5435}.blockers-overview .blocker-dot{background:#cc8e39}.blockers-overview.all-clear{border-top:3px solid #78ad97}.blockers-overview.all-clear .section-index{background:#eaf6ef;color:#417e62}.tag.green{background:#e8f6ef;color:#177657}.tag.blue{background:#eaf0ff;color:#305fc1}.tag.orange{background:#fff0da;color:#986221}.tag.red{background:#fdecec;color:#b14646}@media(max-width:430px){.environment-summary>div{padding:7px 4px}}
|
|
10
|
+
|
|
11
|
+
/* Blockers and preparation gaps use a consistent red treatment. */
|
|
12
|
+
.blockers-overview.has-blockers{border-top-color:#cf6262;border-color:#edc7c7;border-top-width:3px;background:linear-gradient(110deg,#fff4f3,#fffafa 75%)}.blockers-overview.has-blockers .section-index{color:#aa4141;background:#fbe3e3}.blockers-overview.has-blockers h2{color:#9f3f3f}.blockers-overview.has-blockers .tag{background:#fbe4e4;color:#ad4242}.blockers-overview.has-blockers .blocker-item{border-top-color:#f0d7d7}.blockers-overview.has-blockers .blocker-item strong{color:#994343}.blockers-overview.has-blockers .blocker-dot{background:#cc5555}.blockers-overview.has-blockers .link{color:#b14848}
|
|
13
|
+
|
|
14
|
+
/* Stronger emphasis for recorded blockers, without flashing or motion. */
|
|
15
|
+
.blockers-overview.has-blockers{background:#fff0f0;border:1px solid #e9a2a2;border-top:4px solid #cc3434;border-left:6px solid #cc3434;box-shadow:0 3px 12px #b4231810}.blockers-overview.has-blockers h2{color:#aa2020;font-size:17px;font-weight:750}.blockers-overview.has-blockers h2:before{content:'!';display:inline-flex;align-items:center;justify-content:center;width:23px;height:23px;margin-right:9px;border-radius:50%;background:#c52e2e;color:#fff;font-size:16px;font-weight:800;vertical-align:middle}.blockers-overview.has-blockers .section-index{background:#f9cece;color:#9e2020}.blockers-overview.has-blockers h2>.tag{background:#c52e2e;color:#fff;font-weight:750;min-width:24px;text-align:center}.blockers-overview.has-blockers .blocker-item{border-top-color:#eababa;padding-top:17px;padding-bottom:17px}.blockers-overview.has-blockers .blocker-item strong{color:#a82020;font-size:14px;font-weight:700}.blockers-overview.has-blockers .blocker-item p{color:#874444}.blockers-overview.has-blockers .blocker-dot{width:8px;height:8px;background:#c52e2e;flex-shrink:0;box-shadow:0 0 0 3px #f8d1d1}.blockers-overview.has-blockers .link{color:#ac2424;font-weight:600}.blockers-overview.has-blockers>.row>.link{border:1px solid #e1a4a4;background:#fff7f7;border-radius:6px;padding:5px 9px}
|
|
16
|
+
</style></head><body>
|
|
17
|
+
<aside><div class="brand"><b>d2h</b> workbench<small>SPEC PLAN / DELIVERY</small></div><nav id="nav" aria-label="主导航"></nav><div class="side-note"><strong>统一派生视图</strong><br>执行真源:execution.json<br>范围真源:features.json<br>计划真源:pages.json</div></aside>
|
|
18
|
+
<div class="shell"><header><strong>迁移工作台</strong><div><span id="connection" class="tag orange">离线快照</span> <button id="open-json">导入运行快照</button><input type="file" id="file" accept=".json,application/json" hidden></div></header><main id="main"></main></div>
|
|
19
|
+
<dialog id="detail"><div class="row"><h2 id="detail-title"></h2><button id="close-detail" aria-label="关闭详情">关闭</button></div><pre id="detail-body"></pre></dialog><div class="toast" id="toast" role="status" hidden></div>
|
|
20
|
+
<script>
|
|
21
|
+
let data=/*__D2H_SNAPSHOT__*/null;
|
|
22
|
+
const $=id=>document.getElementById(id),esc=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
23
|
+
const pages=[['overview','◫','迁移总览'],['analysis','⌕','分析与报告'],['features','☷','功能清单'],['specs','▤','规格清单'],['tasks','☑','Spec 执行'],['project','▧','工程与环境'],['review','◎','交付 Review']],layerNames={mvp:'MVP 基础',core:'核心新增',full:'全量补齐'},targetNames={mvp:'最小化实现',core:'核心实现',full:'全量实现'};
|
|
24
|
+
let page='overview',draft=null,priorityDraft=null,draftHash=null,draftIdentity=null,draftTarget=null,query='',filter='',stale=false,pollUrl=null;
|
|
25
|
+
const tag=(v,c='')=>`<span class="tag ${c}">${esc(v)}</span>`,ratio=r=>r?`${esc(r.accepted)} / ${esc(r.total)}`:'未启用逐项追踪';
|
|
26
|
+
const bar=r=>r?`<div class="bar"><i style="width:${Number.isFinite(r.percent)?Math.max(0,Math.min(100,r.percent)):0}%"></i></div>`:'';
|
|
27
|
+
const card=(title,value,note,r)=>`<div class="card"><small>${title}</small><strong>${value}</strong>${bar(r)}<small>${note}</small></div>`;
|
|
28
|
+
function notify(text){$('toast').textContent=text;$('toast').hidden=false;clearTimeout(window.toastTimer);window.toastTimer=setTimeout(()=>$('toast').hidden=true,4000)}
|
|
29
|
+
function show(title,value){$('detail-title').textContent=title;$('detail-body').textContent=typeof value==='string'?value:JSON.stringify(value,null,2);$('detail').showModal()}
|
|
30
|
+
function valid(s){if(!s||s.schema!=='d2h-dashboard/v1'||!s.derived?.delivery_progress||!s.feature_plan||!s.plan||!s.execution||!Array.isArray(s.plan.specs)||!Array.isArray(s.feature_plan.features)||!Array.isArray(s.feature_plan.sources)||!Array.isArray(s.derived.running)||!Array.isArray(s.review?.items)||!Array.isArray(s.derived.validation_debt?.specs)||!s.execution.tasks||!s.derived.tasks||!Number.isInteger(s.feature_plan.revision)||!Number.isInteger(s.plan.revision)||!Number.isInteger(s.event_sequence))throw Error('需要当前 runtime 导出的 d2h-dashboard/v1 快照');return s}
|
|
31
|
+
function selected(s=data){return new Set(s.feature_plan.features.flatMap(f=>(f.scope_items||[]).filter(i=>f.scope_status==='included'&&!f.needs_clarification&&i.status==='included'&&i.milestone!=='unassigned').map(i=>JSON.stringify([f.feature_id,i.scope_id]))))}
|
|
32
|
+
function identity(){return JSON.stringify([data.execution.feature_hash,data.execution.plan_hash,data.event_sequence,data.execution.candidate])}
|
|
33
|
+
function deliveryPresetKeys(featurePlan,target){const tiers=['mvp','core','full'],cap=tiers.indexOf(target);if(cap<0||!featurePlan.delivery)return[];const eligible=new Set(featurePlan.features.filter(f=>f.scope_status!=='excluded').flatMap(f=>(f.scope_items||[]).filter(i=>i.status!=='excluded').map(i=>JSON.stringify([f.feature_id,i.scope_id]))));return [...new Set((Array.isArray(featurePlan.delivery.scope_layers)?featurePlan.delivery.scope_layers:[]).filter(r=>tiers.indexOf(r.layer)>=0&&tiers.indexOf(r.layer)<=cap).map(r=>JSON.stringify([r.feature_id,r.scope_id])).filter(k=>eligible.has(k)))];}
|
|
34
|
+
function presetSummary(){const fp=data.feature_plan;if(!fp.delivery)return '<div class="notice blue">尚未配置交付分层,三个预设暂不可用。请由功能清单技能补齐最低交付层并修订版本;仍可手动调整范围,不从 P0/P1/P2 猜分层。</div>';const counts=['mvp','core','full'].map(k=>deliveryPresetKeys(fp,k).length);return `<div class="notice ${counts.every(n=>n>0)?'blue':''}">累计可选范围:最小化 ${counts[0]} 项 · 核心 ${counts[1]} 项 · 全量 ${counts[2]} 项。${counts.some(n=>n===0)?'存在空预设,分层尚未可用;点击空预设将保留当前选择,请修订 scope_layers。':'预设只修改草稿,仍需主 Agent 核验依赖并更新规划。'}</div>`}
|
|
35
|
+
function resetDraft(){draft=selected();priorityDraft=Object.fromEntries(data.feature_plan.features.map(f=>[f.feature_id,f.user_priority]));draftHash=data.execution.feature_hash;draftIdentity=identity();draftTarget=data.feature_plan.delivery?.target??null}
|
|
36
|
+
function scopeRows(){return data.feature_plan.features.flatMap(f=>(f.scope_items||[]).map(i=>({...i,feature_id:f.feature_id,feature_title:f.title,feature_status:f.scope_status,key:JSON.stringify([f.feature_id,i.scope_id]),progress:data.derived.feature_progress[f.feature_id]?.items.find(x=>x.scope_id===i.scope_id)})))}
|
|
37
|
+
function stats(){const d=data.derived,rows=scopeRows().filter(i=>i.feature_status!=='excluded'&&i.status!=='excluded'),refs=new Set(data.plan.specs.filter(s=>d.relevant_specs.includes(s.spec_id)).flatMap(s=>(s.scope_refs||[]).map(i=>JSON.stringify([i.feature_id,i.scope_id]))));const xs=rows.filter(i=>refs.has(i.key));return {total:xs.length,accepted:xs.filter(i=>i.progress?.accepted).length}}
|
|
38
|
+
function heading(title,note){return `<div class="page-heading"><div><div class="kicker">D2H / ${page}</div><h1>${title}</h1><small>${note}</small></div></div>`}
|
|
39
|
+
function gate(v){return tag(v?'当前通过':'尚未通过',v?'green':'orange')}
|
|
40
|
+
function layers(){const g=data.derived.delivery_progress;if(!g.configured)return '<p class="muted">未配置 MVP / 核心 / 全量映射。保留原始里程碑,不按轮次名称猜测分层。</p>';return Object.entries(g.layers).map(([key,l])=>`<div class="layer"><div class="row"><strong>${layerNames[key]}</strong><span>新增 ${ratio(l.increment)}</span></div>${bar(l.cumulative)}<small>累计 ${ratio(l.cumulative)} · ${esc(l.cumulative.percent??'—')}% · 未规划 ${esc(l.cumulative.unplanned)}</small></div>`).join('')}
|
|
41
|
+
function overviewData(){return data.overview||{available:false,code:null,dependencies:[],tech_stack:[],tools:[]}}
|
|
42
|
+
function provisionLabel(value){return {'self-build':'计划自编译','provide-binary':'需要提供二进制','platform-sdk':'平台 SDK','replace':'计划替换',unknown:'处置待确认'}[value]||'处置待确认'}
|
|
43
|
+
function reportBadge(){const m=overviewData();return tag(m.stale?'来源已变化 · 需复核':m.available?'已采集':'未采集',m.stale?'orange':'')}
|
|
44
|
+
function activityRail(percent,active,at){const width=Number.isFinite(percent)?Math.max(0,Math.min(100,percent)):null;return `<div class="activity-rail ${active?'is-active':''} ${width===null?'is-indeterminate':''}" ${active?`data-live-at="${esc(at)}"`:''} role="progressbar" aria-label="${width===null?'活动状态,完成比例未知':'已处理进度'}" ${width===null?'':`aria-valuenow="${width}" aria-valuemin="0" aria-valuemax="100"`}><i style="${width===null?'':'width:'+width+'%'}"></i></div>`}
|
|
45
|
+
function analysisBlock(){const a=data.analysis,e=a?.latest;if(!e)return `<div class="muted">${esc(a?.message||'尚未接入分析事件。')}可读取 AppGraph 的独立进度文件;不从规格数量推算分析进度。</div>`;
|
|
46
|
+
const names={'index.scanning':'扫描源码','index.parsing':'解析代码','index.storing':'存储索引','index.resolving':'解析引用','graph.modules':'模块关系','graph.communities':'功能聚类','graph.capabilities-and-manifest':'能力与清单','graph.semantics':'语义分析','graph.rule-extensions':'规则分析','graph.ui-components':'界面结构','graph.entry-points':'入口分析','graph.behavior-slices':'行为切片','graph.interactions':'交互关系','facts.generate':'生成 Facts','facts.write':'输出 Facts','facts.validate':'校验 Facts',analysis:'分析运行'};
|
|
47
|
+
const running=a.state==='running'&&!a.stale&&['started','progress'].includes(e.status),percent=Number.isFinite(e.total)&&e.total>0&&Number.isFinite(e.current)?Math.min(100,e.current/e.total*100):null;
|
|
48
|
+
return `<div class="row"><strong>${esc(names[e.phase]||e.phase)}</strong>${tag(a.stale?'暂无新事件':a.state==='completed'?'分析已结束':a.state==='failed'?'分析失败':'记录进行中',a.state==='failed'?'red':running?'blue':'')}</div>${activityRail(percent,running,e.timestamp)}<small>${e.current===null||e.current===undefined?'处理数量未提供':esc(e.current)+' / '+(e.total===null?'总量未知':esc(e.total))} · ${esc(e.unit||'阶段通知')}<br>最后通知 ${esc(new Date(e.timestamp).toLocaleString())}${e.item?'<br>'+esc(e.item):''}${e.message?'<br>'+esc(e.message):''}${e.project_root?'<br>分析工程:'+esc(e.project_root):''}</small>${(a.warnings||[]).length?'<p>'+tag('分析警告 '+a.warnings.length,'orange')+'<small>'+esc(a.warnings.slice(-2).map(w=>w.message||w.phase).join(';'))+'</small></p>':''}`}
|
|
49
|
+
function duration(ms){if(!Number.isFinite(ms)||ms<0)return '未记录';if(ms<1000)return Math.round(ms)+' 毫秒';const sec=Math.floor(ms/1000),h=Math.floor(sec/3600),m=Math.floor(sec%3600/60);return h?h+' 小时 '+m+' 分':m?m+' 分 '+sec%60+' 秒':sec+' 秒'}
|
|
50
|
+
function taskElapsed(id){const at=data.timing?.task_started_at?.[id];return at?duration(Date.parse(data.captured_at)-Date.parse(at)):'未记录'}
|
|
51
|
+
function blockers(){const m=overviewData(),rows=[];for(const r of m.blockers||[])if(r.status!=='resolved')rows.push({title:r.name,reason:r.reason||'处置待确认',kind:r.category||'外部准备',action:r.action||'待明确解决方式',go:'project'});for(const r of m.dependencies||[])if(r.blocking===true)rows.push({title:r.name,reason:r.reason,kind:'SDK / SO',action:r.action||'补齐目标平台产物或确认替代方案',go:'project'});for(const r of m.tools||[])if(['failed','missing'].includes(r.status))rows.push({title:r.name,reason:r.message||'检查未通过',kind:m.stale?'历史环境异常':'环境异常',action:'复查环境报告',go:'project'});for(const [id,t] of Object.entries(data.derived.tasks)){for(const reason of t.external_blockers||[])rows.push({title:id,reason,kind:'外部阻塞',spec:id});for(const reason of t.readiness_gaps||[])rows.push({title:id,reason,kind:'工作包缺口',spec:id})}return rows.filter((r,i,a)=>a.findIndex(x=>x.title===r.title&&x.reason===r.reason)===i)}
|
|
52
|
+
function blockerRows(rows){return rows.map(r=>`<div class="blocker-item"><span class="blocker-dot"></span><div><strong>${esc(r.title)}</strong> ${tag(r.kind,'orange')}<p>${esc(r.reason)}</p>${r.action?'<small>下一步:'+esc(r.action)+'</small>':''}</div><button class="link" ${r.spec?'data-spec="'+esc(r.spec)+'"':'data-blocker="'+blockers().findIndex(x=>x.title===r.title&&x.reason===r.reason)+'"'}>查看详情 →</button></div>`).join('')}
|
|
53
|
+
function overview(){
|
|
54
|
+
const d=data.derived,m=overviewData(),fs=data.feature_plan.features,bs=blockers(),ids=d.running.length?d.running:(d.active_batch?.spec_ids||[]).filter(id=>!d.tasks[id].accepted),accepted=Object.values(d.tasks).filter(t=>t.accepted).length;
|
|
55
|
+
const featureDone=fs.filter(f=>f.scope_status!=='excluded'&&d.feature_progress[f.feature_id]?.complete).length,featureRunning=fs.filter(f=>!d.feature_progress[f.feature_id]?.complete&&data.plan.specs.some(s=>d.running.includes(s.spec_id)&&s.feature_refs.includes(f.feature_id))).length;
|
|
56
|
+
const a=data.analysis,anActive=a?.state==='running',anDone=a?.state==='completed',devActive=d.running.length>0,devDone=accepted===data.plan.specs.length&&accepted>0;
|
|
57
|
+
const stages=[['分析',anDone?'最近运行已结束':a?.state==='failed'?'最近运行失败':anActive?'记录进行中':data.feature_analysis?'已有分析报告':'未接入记录',a?.latest?duration(a.latest.elapsed_ms):'未记录','analysis',anActive&&!a.stale],['功能范围',fs.length+' 项 · 已有清单','未记录','features',false],['Spec 规划',data.plan.specs.length+' 项 · 已有规划','未记录','specs',false],['开发与验证',devActive?'正在实施':devDone?'Spec 已验收':d.validation_debt.specs.length?'待验证 / 修复':'待执行','未记录','tasks',devActive],['交付 Review','待人工复核','未记录','review',false]];
|
|
58
|
+
const sentence=anActive?'最近一次分析尚在处理,阶段状态以通知为准。':devActive?'正在实施 '+d.running.length+' 个 Spec,已验收 '+accepted+' / '+data.plan.specs.length+' 个。':d.validation_debt.specs.length?'有 '+d.validation_debt.specs.length+' 个 Spec 等待验证或修复。':'已有 '+fs.length+' 个功能、'+data.plan.specs.length+' 个 Spec,'+(data.execution.candidate?'当前等待下一步调度。':'尚未开始实现。');
|
|
59
|
+
return heading('迁移总览','阶段 → 任务 → 工程与环境 → 阻塞')+`
|
|
60
|
+
<section class="panel status-overview" aria-label="第一层:状态总览"><div class="row"><h2><span class="section-index">01</span>状态总览</h2><div class="total-time"><small>总用时</small><strong>未记录</strong></div></div><div class="phase-track">${stages.map(([name,status,time,go,active])=>`<button class="phase-cell ${active?'phase-active':go==='analysis'&&a?.state==='failed'?'phase-error':go==='analysis'&&anDone||go==='tasks'&&devDone?'phase-done':''}" data-go="${go}"><span class="phase-label">${active?'<i class="live-dot"></i>':''}${name}</span><strong>${esc(status)}</strong><small>${go==='analysis'?'最近运行用时':'用时'} ${esc(time)}</small></button>`).join('')}</div><div class="overview-sentence"><div><strong>${esc(sentence)}</strong><small>${bs.length?'另有 '+bs.length+' 项已记录阻塞或准备缺口。':'尚无已记录阻塞;未检查项见环境。'} 阶段未有完成凭据时,仅显示已有产物。</small></div>${ids.length?`<span class="tag blue">当前任务用时 ${esc(taskElapsed(ids[0]))}</span>`:''}</div></section>
|
|
61
|
+
<div class="overview-grid task-overview" aria-label="第二层:任务总览">
|
|
62
|
+
<section class="panel span-6"><div class="row"><h2><span class="section-index">02</span>当前任务</h2><button class="link" data-go="tasks">执行详情 →</button></div>${ids.length?ids.slice(0,2).map(id=>{const sp=data.plan.specs.find(s=>s.spec_id===id);return `<div class="current-brief"><div class="row"><button class="link current-title" data-spec="${esc(id)}">${esc(sp?.scope?.join(';')||id)}</button>${tag(d.running.includes(id)?'执行中':'已领取','blue')}</div><small>${esc(id)} · 本次派发已历时 ${esc(taskElapsed(id))}</small>${activityRail(null,d.running.includes(id),data.captured_at)}</div>`}).join(''):anActive?`<strong>AppGraph 分析</strong>${analysisBlock()}`:`<div class="idle-task"><span>当前无在途任务</span><small>${d.ready_batches?.length?'建议批次:'+esc(d.ready_batches[0]):d.ready.length?'建议下一项:'+esc(d.ready[0]):'等待调度或补齐前置条件'}</small></div>`}</section>
|
|
63
|
+
<section class="panel span-3"><div class="row"><h2>Spec 任务</h2><button class="link" data-go="tasks">全部 →</button></div><div class="summary-counts"><div><strong>${accepted}</strong><small>完成</small></div><div><strong class="blue-number">${d.running.length}</strong><small>进行中</small></div><div><strong>${data.plan.specs.length}</strong><small>总数</small></div></div>${bar({percent:data.plan.specs.length?100*accepted/data.plan.specs.length:0})}<small>完成按当前有效验收计</small></section>
|
|
64
|
+
<section class="panel span-3"><div class="row"><h2>功能清单</h2><button class="link" data-go="features">全部 →</button></div><div class="summary-counts"><div><strong>${featureDone}</strong><small>完成</small></div><div><strong class="blue-number">${featureRunning}</strong><small>进行中</small></div><div><strong>${fs.length}</strong><small>总数</small></div></div>${bar({percent:fs.length?100*featureDone/fs.length:0})}<small>${fs.filter(f=>f.scope_status==='excluded').length?'总数含排除项;':''}完成要求所有未排除范围通过</small></section></div>
|
|
65
|
+
<div class="overview-grid project-overview" aria-label="第三层:工程与环境">
|
|
66
|
+
<section class="panel span-6"><div class="row"><h2><span class="section-index">03</span>安卓工程</h2><button class="link" data-go="project">工程详情 →</button></div><div class="project-metrics"><div><strong>${esc(m.code?.files??'—')}</strong><small>源文件</small></div><div><strong>${esc(m.code?.lines?.toLocaleString()??'—')}</strong><small>物理行数</small></div><div><strong>${(m.dependencies||[]).length}</strong><small>已记录依赖</small></div></div><div class="stack-pills">${(m.tech_stack||[]).slice(0,3).map(r=>tag(r.name)).join('')||'<small>技术栈未采集</small>'}</div><small>${m.stale?'来源已变化,需重新采集':m.code?.complete===false?'代码统计不完整':'依赖数为已记录条目,完整扫描状态见详情'}</small></section>
|
|
67
|
+
<section class="panel span-6"><div class="row"><h2>环境检查</h2><button class="link" data-go="project">检查详情 →</button></div><div class="environment-summary ${m.stale?'is-report-stale':''}">${['passed','failed','missing','unknown'].map(k=>`<div><strong>${(m.tools||[]).filter(r=>r.status===k).length}</strong><small>${{passed:'通过',failed:'失败',missing:'缺失',unknown:'未检查'}[k]}</small></div>`).join('')}</div><div class="row"><small>${m.tools?.length?'检查范围:'+esc(m.tools.slice(0,3).map(r=>r.name).join(' / ')):'尚未载入环境检查报告'}</small>${m.stale?tag('历史结果','orange'):tag((m.tools||[]).some(r=>r.status==='unknown')?'有未检查项':m.tools?.length?'按报告展示':'未载入报告','orange')}</div></section></div>
|
|
68
|
+
<section class="panel blockers-overview ${bs.length?'has-blockers':'all-clear'}" aria-label="第四层:阻塞情况"><div class="row"><h2><span class="section-index">04</span>阻塞与准备缺口 ${tag(bs.length,bs.length?'orange':'green')}</h2><button class="link" data-go="project">全部详情 →</button></div>${bs.length?blockerRows(bs.slice(0,3)):'<div class="clear-blockers">尚无已记录阻塞 <small>闭源 SDK/SO、平台配置与环境尚未完成全面核验,不能据此判断已全部就绪。</small></div>'}${bs.length>3?'<small>另有 '+(bs.length-3)+' 项,进入详情查看。</small>':''}</section>`;
|
|
69
|
+
}
|
|
70
|
+
function project(){const m=overviewData();return heading('工程与环境','规模、技术依赖、环境检查及外部准备的完整记录')+`<section class="panel"><div class="row"><h2>代码规模与技术栈</h2>${reportBadge()}</div><p>${esc(m.code?.files??'—')} 个源码文件 · ${esc(m.code?.lines??'—')} 物理行</p><p>${esc(m.code?.method||'统计口径未采集')}</p><button class="link" data-overview="code">统计来源 →</button><div class="language-list">${(m.tech_stack||[]).map((r,i)=>`<div class="row"><button class="link" data-stack="${i}">${esc(r.name)}</button><small>${esc(r.version||r.scope||'版本未记录')}</small></div>`).join('')}</div></section><section class="panel"><h2>SDK / SO 依赖</h2><div class="table-wrap"><table><thead><tr><th>依赖 / 平台</th><th>类型</th><th>迁移处置</th><th>准备情况</th></tr></thead><tbody>${(m.dependencies||[]).map((r,i)=>`<tr><td><button class="link" data-dependency="${i}">${esc(r.name)}</button><small>${esc(r.platform)}</small></td><td>${esc(r.kind.toUpperCase())}</td><td>${tag(provisionLabel(r.provision))}</td><td class="title">${esc(r.status||'未验证')}<small>${esc(r.reason)}</small></td></tr>`).join('')}</tbody></table></div><small>清单为空不代表完成扫描;计划自编译不代表编译成功,Android 产物不等于 HarmonyOS 产物。</small></section><section class="panel"><h2>环境检查</h2>${(m.tools||[]).map((r,i)=>`<div class="compact-item"><div><button class="link" data-tool="${i}">${esc(r.name)}</button><small> ${esc(r.version||r.message||'')}</small></div>${tag((m.stale?'上次:':'')+({passed:'通过',failed:'失败',missing:'缺失',unknown:'未检查'}[r.status]),r.status==='passed'&&!m.stale?'green':'orange')}</div>`).join('')}<small>工具可执行、版本合规、目标 SDK 可用及设备构建是不同检查。</small></section><section class="panel"><h2>阻塞与准备缺口</h2>${blockerRows(blockers())||'<small>尚无已记录阻塞,完整核验仍需执行。</small>'}</section>`}
|
|
71
|
+
function analysisReport(){const report=data.feature_analysis;if(!report)return '<div class="notice blue">尚未接入功能分析产物;当前显示已有规划来源。新项目可通过 --feature-analysis 导出。</div>';const a=report.data,sourceIds=new Set(data.feature_plan.sources.filter(s=>s.locator===report.path&&s.revision==='sha256:'+report.sha256).map(s=>s.source_id));return `<section class="panel"><h2>功能清单分析 · ${esc(a.project)}</h2><p><small>分析行为行 ${a.feature_list.length} · 规划功能 ${data.feature_plan.features.length} · 未覆盖候选 ${(a.uncovered||[]).length} · 用户待办 ${(a.user_inputs||[]).length}</small></p><div class="notice blue">清单中的阶段/分析建议不代表用户授权或验收;范围与当前状态以版本化规划和执行证据为准,源码新鲜度在恢复时核验。</div><div class="table-wrap"><table><thead><tr><th>行为与条件</th><th>阶段或分析建议 / 可行性</th><th>来源证据</th><th>关联规划功能 / Spec</th></tr></thead><tbody>${a.feature_list.map((row,i)=>{const fs=data.feature_plan.features.filter(f=>f.source_refs.some(r=>sourceIds.has(r.source_id)&&r.ref===(report.pointer_prefix||'/feature_list/')+i)),ids=new Set(fs.map(f=>f.feature_id)),specs=data.plan.specs.filter(s=>s.feature_refs.some(id=>ids.has(id)));return `<tr><td class="title">${esc(row.trigger)} → ${esc(row.result)}<small>${esc(row.condition||'条件未限定')} · ${esc(row.carrier||'承载未注明')}</small></td><td>${tag(row.priority)}<small>${esc(row.feasibility||'未标注限制')} ${esc(row.note||'')}</small></td><td class="title">${esc(row.evidence)}</td><td class="title">${fs.map(f=>`<button class="link" data-go="features">${esc(f.title)}</button>`).join(' / ')}<small>关联功能的 Spec:</small>${specs.map(s=>`<button class="link" data-spec="${esc(s.spec_id)}">${esc(s.spec_id)}</button>`).join(' / ')||'<small>尚未规划或未分配范围</small>'}</td></tr>`}).join('')}</tbody></table></div><p><small>报告:${esc(report.path)} · SHA256 ${esc(report.sha256)}</small></p></section><section class="panel"><h2>未覆盖与待确认</h2>${(a.uncovered||[]).map(r=>`<p>${esc(r.component)} · ${esc(r.reason)}<br><small>${esc(r.evidence)}</small></p>`).join('')||'<small>报告未记录未覆盖候选;这不证明业务已全部验收。</small>'}</section>`}
|
|
72
|
+
function analysis(){return heading('分析与报告','结构化分析通知与带来源的项目摘要')+analysisReport()+`<section class="panel"><h2>分析进展</h2>${analysisBlock()}<p><small>阶段通知不会改变 Facts。事件总量未知时只显示处理数量;分析结束也可能存在覆盖警告。</small></p></section><section class="panel"><h2>功能规划输入来源</h2><div class="table-wrap"><table><thead><tr><th>来源</th><th>类型</th><th>定位 / 版本</th></tr></thead><tbody>${data.feature_plan.sources.map(s=>`<tr><td>${esc(s.source_id)}</td><td>${esc(s.kind)}</td><td class="title">${esc(s.locator)}<small>${esc(s.revision)}</small></td></tr>`).join('')}</tbody></table></div></section>`}
|
|
73
|
+
function updateActivity(){const now=Date.now();document.querySelectorAll('[data-live-at]').forEach(e=>{const old=now-Date.parse(e.dataset.liveAt)>30000;e.classList.toggle('is-stale',stale||(!data?.demo&&old))})}
|
|
74
|
+
function features(){const config=data.feature_plan.delivery,expired=draftIdentity!==identity();return heading('功能清单','勾选与优先级修改形成版本绑定草稿,由主 Agent 核对后修订')+presetSummary()+`<div class="notice ${expired?'':'blue'}">${expired?'运行基线已变化,草稿已过期;请撤回重载后合并。':'这里不直接保存工程。取消项形成延期建议;依赖、Spec 和在途窗口需由主 Agent 核验。'}</div><section class="panel"><div class="toolbar">${['mvp','core','full'].map(k=>`<button data-preset="${k}" ${config?'':'disabled'}>${targetNames[k]}${config?' · '+deliveryPresetKeys(data.feature_plan,k).length+' 项':''}</button>`).join('')}<button id="reset-draft">撤回并重载</button><button class="primary" id="export-draft" ${expired||!data.derived.scope_tracking?'disabled':''}>预览并导出修改建议</button><small id="draft-count">已选 ${draft.size} 范围项</small></div><div class="table-wrap"><table><thead><tr><th>本次选用</th><th>功能 / 范围</th><th>最低层 / 原处置</th><th>用户优先级</th><th>当前有效验收</th></tr></thead><tbody>${data.feature_plan.features.map(f=>`<tr><td><input type="checkbox" data-feature="${esc(f.feature_id)}" aria-label="选择功能${esc(f.title)}" ${!f.scope_items||f.scope_status==='excluded'?'disabled':''}></td><td><strong>${esc(f.title)}</strong><small>${esc(f.feature_id)}</small></td><td>${esc(f.scope_status)}</td><td><select data-priority="${esc(f.feature_id)}" aria-label="${esc(f.title)}优先级">${['must','normal','later'].map(k=>`<option ${priorityDraft[f.feature_id]===k?'selected':''}>${k}</option>`).join('')}</select></td><td>${tag(data.derived.feature_progress[f.feature_id]?.complete?'全功能已完成':'有未完成范围')}</td></tr>${(f.scope_items||[]).map(i=>{const key=JSON.stringify([f.feature_id,i.scope_id]),layer=config?.scope_layers.find(x=>x.feature_id===f.feature_id&&x.scope_id===i.scope_id)?.layer;return `<tr><td><input type="checkbox" data-scope="${esc(key)}" aria-label="选择${esc(i.description)}" ${draft.has(key)?'checked':''} ${f.scope_status==='excluded'||i.status==='excluded'?'disabled':''}></td><td class="scope title">${esc(i.description)}<small>${esc(i.scope_id)}</small></td><td>${esc(layerNames[layer]??'未配置')}<small>${esc(i.status)} / ${esc(i.milestone)}</small></td><td>沿用功能约束</td><td>${gate(data.derived.feature_progress[f.feature_id]?.items.find(x=>x.scope_id===i.scope_id)?.accepted)}</td></tr>`}).join('')}`).join('')}</tbody></table></div><small>预设保留手动优先级;包含未决/未授权项时只是修改建议,不能自动开工。</small></section>`}
|
|
75
|
+
function specs(){const d=data.derived;return heading('规格清单','里程碑 → 执行轮次 → 页面计划批次 → Spec;实际窗口单独记录')+`<section class="panel"><h2>分层交付与门禁</h2>${layers()}<p>阶段回归 ${gate(d.milestone_regression_passed)} 阶段验收 ${gate(d.milestone_accepted)}</p></section><section class="panel"><h2>全部规格</h2><div class="table-wrap"><table><thead><tr><th>Spec / 页面</th><th>范围映射</th><th>能力依赖</th><th>当前验收</th></tr></thead><tbody>${data.plan.specs.map(s=>`<tr><td><button class="link" data-spec="${esc(s.spec_id)}">${esc(s.spec_id)}</button><small>${esc(s.page_id)} / ${esc(s.milestone)}</small></td><td class="title">${esc((s.scope_refs||[]).map(r=>r.feature_id+'/'+r.scope_id).join(', ')||s.scope.join(';'))}</td><td class="title">${esc(s.requires.join(', ')||'无前置能力')}</td><td>${gate(d.tasks[s.spec_id].accepted)}</td></tr>`).join('')}</tbody></table></div></section><section class="panel"><h2>轮次与历史交付</h2><div class="table-wrap"><table><thead><tr><th>轮次 / 类型</th><th>目标</th><th>历史交付</th><th>当前验收</th></tr></thead><tbody>${Object.entries(d.rounds||{}).map(([id,r])=>`<tr><td>${esc(id)}<small>${esc(r.kind)}</small></td><td class="title">${esc(r.goal)}</td><td>${tag(r.delivered?'曾交付':'未交付')}</td><td>${gate(r.current_accepted)}</td></tr>`).join('')}</tbody></table></div></section><section class="panel"><h2>计划批次与当前建议</h2>${Object.entries(d.page_batches||{}).map(([id,b])=>`<details><summary>${esc(id)} · ${esc(b.round_id)} / ${esc(b.page_id)}</summary><p>计划成员:${esc(b.spec_ids.join(', '))}</p><p>当前建议:${esc(b.dispatchable_spec_ids.join(', '))}</p><p>实际窗口:${esc(d.active_batch?.batch_id===id?d.active_batch.spec_ids.join(', '):'未领取')}</p>${b.spec_ids.map(id=>`<p><button class="link" data-spec="${esc(id)}">${esc(id)} → 查看契约</button></p>`).join('')}</details>`).join('')||'<p class="muted">当前不是页面批次模式。</p>'}</section>`}
|
|
76
|
+
function tasks(){return heading('Spec 执行','实现、记录验证、记录验收与当前有效验收并列,保留所有阻塞原因')+`<section class="panel"><div class="toolbar"><input id="query" type="search" aria-label="搜索 Spec" placeholder="搜索 Spec / 范围"><select id="filter" aria-label="筛选 Spec"><option value="">全部 Spec</option><option value="running">正在实施</option><option value="gaps">工作包有缺口</option><option value="relevant">当前目标及必要依赖</option></select></div><div id="task-table" class="table-wrap"></div></section>`}
|
|
77
|
+
function taskTable(){const d=data.derived,xs=data.plan.specs.filter(s=>{const t=d.tasks[s.spec_id];return(s.spec_id+' '+s.scope.join(' ')).toLowerCase().includes(query.toLowerCase())&&(!filter||(filter==='gaps'&&t.readiness_gaps.length)||(filter==='running'&&d.running.includes(s.spec_id))||(filter==='relevant'&&d.relevant_specs.includes(s.spec_id)))});$('task-table').innerHTML=xs.length?`<table><thead><tr><th>Spec</th><th>实现 / 验证 / 验收记录</th><th>当前有效验收</th><th>工作包 / 接口</th><th>原因</th></tr></thead><tbody>${xs.map(s=>{const t=d.tasks[s.spec_id],raw=data.execution.tasks[s.spec_id];return `<tr><td><button class="link" data-spec="${esc(s.spec_id)}">${esc(s.spec_id)}</button><small>${esc(s.milestone)} / ${esc(s.page_id)}</small></td><td>${esc(raw.implementation)}<small>${esc(raw.validation)} / ${esc(raw.acceptance)}</small></td><td>${gate(t.accepted)}<small>缺 ${t.missing_checks.length} checks</small></td><td>${tag(!data.plan.work_packet_policy?'未启用门禁':t.work_packet_ready?'工作包齐备':'工作包待补')}<small>提供接口待交接 ${t.missing_interfaces.length}</small></td><td class="title">${esc([...t.blocked_by,...t.external_blockers,...t.readiness_gaps].join(';')||'无已知阻塞')}</td></tr>`}).join('')}</tbody></table>`:'<div class="empty">没有符合条件的 Spec</div>'}
|
|
78
|
+
function review(){return heading('交付 Review','原始基线 → 最新约定 → 实现与证据;自动对照尚未完成人工复核')+`<div class="notice">${data.review.baseline_available?'已提供原始范围基线。':'原始范围基线缺失,不能判断新增/修改;重新导出时提供 --baseline-features。'} 人工结论记录到 delivery-review.md;本表不补造测试 PASS。</div><section class="panel"><div class="table-wrap"><table><thead><tr><th>验收范围</th><th>基线差异</th><th>最新约定</th><th>Spec / 当前验收</th><th>人工 Review</th></tr></thead><tbody>${data.review.items.map((i,n)=>`<tr><td class="title"><button class="link" data-review="${n}">${esc(i.description)}</button><small>${esc(i.feature_id)} / ${esc(i.scope_id)}</small></td><td>${esc({'baseline-unavailable':'基线缺失',added:'新增',changed:'变更',unchanged:'无需求变化','missing-from-current':'当前清单缺失'}[i.difference])}</td><td>${esc(i.status)}</td><td>${esc(i.spec_id??'未规划')}<small>${i.accepted?'当前证据通过':'尚无有效通过'}</small></td><td>${tag('待复核','orange')}</td></tr>`).join('')}</tbody></table></div><small>同一 ID 的文本/验收差异只用于定位。拆分、合并、实际新增行为和代码语义需主 Agent 核验;不能靠名称自动判定等价。</small></section>`}
|
|
79
|
+
function syncDraft(){if(page!=='features')return;$('draft-count').textContent='已选 '+draft.size+' 范围项';document.querySelectorAll('[data-scope]').forEach(e=>e.checked=draft.has(e.dataset.scope));document.querySelectorAll('[data-feature]').forEach(e=>{const ids=scopeRows().filter(i=>i.feature_id===e.dataset.feature&&i.status!=='excluded'&&i.feature_status!=='excluded').map(i=>i.key),n=ids.filter(k=>draft.has(k)).length;e.checked=ids.length>0&&n===ids.length;e.indeterminate=n>0&&n<ids.length})}
|
|
80
|
+
function render(){if(!data){$('main').innerHTML=heading('尚未载入运行快照','使用 spec_runtime.py dashboard 导出 HTML,或导入同次运行 JSON。');return}if(!draft)resetDraft();$('nav').innerHTML=pages.map(([id,icon,label])=>`<button data-go="${id}" class="${page===id?'active':''}"><span>${icon}</span>${label}</button>`).join('');$('main').innerHTML=(data.demo?'<div class="notice">演示数据 · 使用回归 fixture 生成,非真实应用实现或测试结果。</div>':'')+({overview,analysis,features,specs,tasks,project,review})[page]()+`<div class="status-line">快照时间:${esc(new Date(data.captured_at).toLocaleString())} · ${stale?'连接失败,保留旧数据':pollUrl?'从同源端点刷新':'离线快照,需重新导出或导入更新'} · 功能 ${data.feature_plan.features.length} / Spec ${data.plan.specs.length}</div>`;if(page==='tasks'){query='';filter='';taskTable()}syncDraft();updateActivity()}
|
|
81
|
+
function download(value){const url=URL.createObjectURL(new Blob([JSON.stringify(value,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=url;a.download='scope-change-request.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}
|
|
82
|
+
$('close-detail').onclick=()=>$('detail').close();$('open-json').onclick=()=>$('file').click();$('file').onchange=async event=>{try{const next=valid(JSON.parse(await event.target.files[0].text()));if(pollUrl)throw Error('在线模式请通过端点更新,或移除 snapshot 参数后导入');data=next;draft=null;render();notify('已导入快照,未写入工程')}catch(error){notify(error.message)}};
|
|
83
|
+
document.addEventListener('click',event=>{const b=event.target.closest('button');if(!b)return;if(b.dataset.go){page=b.dataset.go;render();window.scrollTo(0,0)}if(b.dataset.blocker!==undefined)show('阻塞与处理建议',blockers()[Number(b.dataset.blocker)]);if(b.dataset.overview)show('代码规模与来源',{report:overviewData()[b.dataset.overview],sources:overviewData().sources,source_checks:overviewData().source_checks});if(b.dataset.dependency!==undefined)show('依赖依据与迁移处置',{item:overviewData().dependencies[Number(b.dataset.dependency)],sources:overviewData().sources});if(b.dataset.tool!==undefined)show('工具检查原始摘要',{item:overviewData().tools[Number(b.dataset.tool)],sources:overviewData().sources});if(b.dataset.stack!==undefined)show('技术栈来源',{item:overviewData().tech_stack[Number(b.dataset.stack)],sources:overviewData().sources});if(b.dataset.spec){const id=b.dataset.spec;show(id,{plan:data.plan.specs.find(s=>s.spec_id===id),recorded_execution:data.execution.tasks[id],current_derived:data.derived.tasks[id]})}if(b.dataset.review!==undefined)show('逐项 Review 输入',data.review.items[Number(b.dataset.review)]);if(b.id==='reset-draft'){resetDraft();render()}if(b.dataset.preset){if(draftIdentity!==identity()){notify('运行基线已变化,请撤回并重载后选择预设。');return}const keys=deliveryPresetKeys(data.feature_plan,b.dataset.preset);if(!keys.length){notify('该预设没有可选范围,交付分层尚未配置完整;已保留当前选择,请修订 scope_layers。');return}draft=new Set(keys);draftTarget=b.dataset.preset;render()}if(b.id==='export-draft'){const old=selected(),added=[...draft].filter(k=>!old.has(k)),removed=[...old].filter(k=>!draft.has(k));show('范围修改建议 · 依赖影响尚待主 Agent 核验',`新增 ${added.length} 项,移出本次 ${removed.length} 项;优先级修改保留在草稿。\n不会直接应用计划或改动执行窗口。`);const reason=document.createElement('input');reason.placeholder='修改原因(必填)';reason.setAttribute('aria-label','修改原因');const save=document.createElement('button');save.textContent='导出给主 Agent';save.onclick=()=>{if(!reason.value.trim()){notify('请填写修改原因');return}if(draftIdentity!==identity()){notify('草稿已过期,请重载后合并');return}download({schema:'d2h-scope-change-request/v1',feature_hash:draftHash,plan_hash:data.execution.plan_hash,event_sequence:data.event_sequence,candidate:data.execution.candidate,target:draftTarget,scope_refs:[...draft].map(k=>{const [feature_id,scope_id]=JSON.parse(k);return{feature_id,scope_id}}),user_priorities:priorityDraft,reason:reason.value.trim()});$('detail').close()};$('detail').append(reason,save);$('detail').addEventListener('close',()=>{reason.remove();save.remove()},{once:true})}});
|
|
84
|
+
document.addEventListener('change',event=>{const e=event.target;if(e.dataset.scope){e.checked?draft.add(e.dataset.scope):draft.delete(e.dataset.scope);syncDraft()}if(e.dataset.feature){scopeRows().filter(i=>i.feature_id===e.dataset.feature&&i.status!=='excluded'&&i.feature_status!=='excluded').forEach(i=>e.checked?draft.add(i.key):draft.delete(i.key));syncDraft()}if(e.dataset.priority)priorityDraft[e.dataset.priority]=e.value;if(e.id==='filter'){filter=e.value;taskTable()}});document.addEventListener('input',event=>{if(event.target.id==='query'){query=event.target.value;taskTable()}});
|
|
85
|
+
try{if(data)valid(data);const requested=new URL(location.href).searchParams.get('snapshot');if(requested){const url=new URL(requested,location.href);if(!['http:','https:'].includes(url.protocol)||url.origin!==location.origin)throw Error('快照端点必须为同源 HTTP(S)');pollUrl=url.href}render()}catch(error){notify(error.message);pollUrl=null;render()}
|
|
86
|
+
async function poll(){if(!pollUrl)return;try{const response=await fetch(pollUrl,{cache:'no-store'});if(!response.ok)throw Error('HTTP '+response.status);data=valid(await response.json());stale=false;$('connection').textContent='已连接 · 约 3 秒刷新';render()}catch(error){stale=true;$('connection').textContent='连接失败 · 保留最后快照';render()}finally{setTimeout(poll,3000)}}poll();
|
|
87
|
+
setInterval(updateActivity,3000);
|
|
88
|
+
</script></body></html>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only physical source-line counts; no feature or implementation inference."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
LANGUAGES = {'.kt': 'Kotlin', '.kts': 'Kotlin', '.java': 'Java', '.ets': 'ArkTS', '.ts': 'TypeScript',
|
|
10
|
+
'.tsx': 'TypeScript', '.js': 'JavaScript', '.jsx': 'JavaScript', '.c': 'C', '.h': 'C/C++ headers',
|
|
11
|
+
'.cpp': 'C++', '.cc': 'C++', '.hpp': 'C++', '.py': 'Python', '.swift': 'Swift', '.m': 'Objective-C',
|
|
12
|
+
'.mm': 'Objective-C++', '.rs': 'Rust', '.vue': 'Vue', '.html': 'HTML', '.css': 'CSS', '.gradle': 'Groovy'}
|
|
13
|
+
EXCLUDED = {'.git', '.gradle', '.codegraph', '.codegraph-appgraph', '.migration', '.hvigor', 'node_modules', 'oh_modules', 'build', 'dist'}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def collect(project, excluded=None):
|
|
17
|
+
root = Path(project).resolve()
|
|
18
|
+
if not root.is_dir():
|
|
19
|
+
raise ValueError('project must be a directory')
|
|
20
|
+
excluded = EXCLUDED if excluded is None else set(excluded)
|
|
21
|
+
# Walk the selected tree only. Do not follow links to external code or guess generated source.
|
|
22
|
+
rows, unreadable = {}, []
|
|
23
|
+
for directory, dirs, files in os.walk(root, followlinks=False):
|
|
24
|
+
dirs[:] = sorted(d for d in dirs if d not in excluded and not Path(directory, d).is_symlink())
|
|
25
|
+
for name in sorted(files):
|
|
26
|
+
file = Path(directory, name)
|
|
27
|
+
language = LANGUAGES.get(file.suffix.lower())
|
|
28
|
+
if language is None or file.is_symlink():
|
|
29
|
+
continue
|
|
30
|
+
try:
|
|
31
|
+
lines, size, last = 0, 0, b''
|
|
32
|
+
with file.open('rb') as source:
|
|
33
|
+
while True:
|
|
34
|
+
chunk = source.read(65536)
|
|
35
|
+
if not chunk:
|
|
36
|
+
break
|
|
37
|
+
lines += chunk.count(b'\n')
|
|
38
|
+
size += len(chunk)
|
|
39
|
+
last = chunk[-1:]
|
|
40
|
+
lines += int(size > 0 and last != b'\n')
|
|
41
|
+
except OSError:
|
|
42
|
+
unreadable.append(str(file.relative_to(root)))
|
|
43
|
+
continue
|
|
44
|
+
row = rows.setdefault(language, {'name': language, 'files': 0, 'lines': 0})
|
|
45
|
+
row['files'] += 1
|
|
46
|
+
row['lines'] += lines
|
|
47
|
+
try:
|
|
48
|
+
revision = subprocess.run(['git', '-C', str(root), 'rev-parse', 'HEAD'], capture_output=True, text=True, timeout=10)
|
|
49
|
+
source_revision = revision.stdout.strip() if revision.returncode == 0 else None
|
|
50
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
51
|
+
source_revision = None
|
|
52
|
+
return {'schema': 'd2h-code-metrics/v1', 'project_root': str(root),
|
|
53
|
+
'source_revision': source_revision,
|
|
54
|
+
'code': {'files': sum(r['files'] for r in rows.values()), 'lines': sum(r['lines'] for r in rows.values()),
|
|
55
|
+
'languages': [rows[name] for name in sorted(rows)],
|
|
56
|
+
'method': 'Physical LF-delimited lines including blank lines/comments; known source extensions only; symlinks skipped; directory exclusions listed in report.',
|
|
57
|
+
'complete': not unreadable, 'unreadable_files': unreadable},
|
|
58
|
+
'excluded_directories': sorted(excluded), 'unreadable_files': unreadable}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
63
|
+
parser.add_argument('--project', type=Path, required=True)
|
|
64
|
+
parser.add_argument('--exclude-dir', action='append', help='Replace default excluded directory names; repeat for each name')
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
try:
|
|
67
|
+
print(json.dumps(collect(args.project, args.exclude_dir), ensure_ascii=False, indent=2))
|
|
68
|
+
return 0
|
|
69
|
+
except (OSError, ValueError, subprocess.TimeoutExpired) as error:
|
|
70
|
+
print(json.dumps({'error': str(error)}, ensure_ascii=False))
|
|
71
|
+
return 2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == '__main__':
|
|
75
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Validate optional report inputs and project AppGraph notifications without changing execution."""
|
|
2
|
+
import copy
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def require(value, message):
|
|
11
|
+
if not value:
|
|
12
|
+
raise ValueError(message)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def prepare(data, candidate):
|
|
16
|
+
if data is None:
|
|
17
|
+
return {'available': False, 'stale': False, 'code': None, 'tech_stack': [], 'dependencies': [], 'tools': []}
|
|
18
|
+
require(isinstance(data, dict) and data.get('schema') == 'd2h-overview/v1', 'd2h-overview/v1 required')
|
|
19
|
+
timestamp = datetime.fromisoformat(data['generated_at'].replace('Z', '+00:00'))
|
|
20
|
+
require(timestamp.tzinfo is not None, 'overview timestamp needs timezone')
|
|
21
|
+
sources = data.get('sources')
|
|
22
|
+
require(isinstance(sources, list), 'overview sources required')
|
|
23
|
+
checks, identities = [], set()
|
|
24
|
+
for source in sources:
|
|
25
|
+
require(isinstance(source, dict) and isinstance(source.get('source_id'), str) and bool(source['source_id']) and source['source_id'] not in identities, 'unique overview source_id required')
|
|
26
|
+
identities.add(source['source_id'])
|
|
27
|
+
path = Path(source['path'])
|
|
28
|
+
require(path.is_absolute() and isinstance(source.get('sha256'), str) and len(source['sha256']) == 64 and all(c in '0123456789abcdef' for c in source['sha256']), 'overview absolute source path and sha256 required')
|
|
29
|
+
try:
|
|
30
|
+
status = 'current' if hashlib.sha256(path.read_bytes()).hexdigest() == source['sha256'] else 'changed'
|
|
31
|
+
except OSError:
|
|
32
|
+
status = 'missing'
|
|
33
|
+
checks.append({'source_id': source['source_id'], 'status': status})
|
|
34
|
+
|
|
35
|
+
def refs(row):
|
|
36
|
+
values = row.get('source_refs')
|
|
37
|
+
require(isinstance(values, list) and bool(values) and all(isinstance(v, str) and v in identities for v in values), 'overview item needs known source_refs')
|
|
38
|
+
|
|
39
|
+
code = data.get('code')
|
|
40
|
+
if code is not None:
|
|
41
|
+
require(isinstance(code, dict), 'overview code must be an object')
|
|
42
|
+
refs(code)
|
|
43
|
+
for key in ('files', 'lines'):
|
|
44
|
+
require(code.get(key) is None or type(code[key]) is int and code[key] >= 0, 'nonnegative code counts or null required')
|
|
45
|
+
require(isinstance(code.get('method'), str) and bool(code['method'].strip()), 'code counting method required')
|
|
46
|
+
require(isinstance(code.get('languages', []), list), 'code languages must be an array')
|
|
47
|
+
for row in code.get('languages', []):
|
|
48
|
+
require(isinstance(row, dict) and isinstance(row.get('name'), str), 'language name required')
|
|
49
|
+
require(all(type(row.get(k)) is int and row[k] >= 0 for k in ('files', 'lines')), 'language file/line counts required')
|
|
50
|
+
for group in ('tech_stack', 'dependencies', 'tools', 'blockers'):
|
|
51
|
+
rows = data.get(group, [])
|
|
52
|
+
require(isinstance(rows, list), f'overview {group} must be an array')
|
|
53
|
+
for row in rows:
|
|
54
|
+
require(isinstance(row, dict) and isinstance(row.get('name'), str) and bool(row['name'].strip()), f'{group} item name required')
|
|
55
|
+
refs(row)
|
|
56
|
+
if group == 'blockers':
|
|
57
|
+
require(row.get('status') in ('open', 'resolved'), 'blocker status must be open/resolved')
|
|
58
|
+
require(isinstance(row.get('reason'), str) and bool(row['reason'].strip()), 'blocker reason required')
|
|
59
|
+
if group == 'dependencies':
|
|
60
|
+
require('blocking' not in row or type(row['blocking']) is bool, 'dependency blocking must be explicit boolean')
|
|
61
|
+
require(row.get('kind') in ('sdk', 'so', 'library'), 'dependency kind must be sdk/so/library')
|
|
62
|
+
require(row.get('provision') in ('self-build', 'provide-binary', 'platform-sdk', 'replace', 'unknown'), 'dependency provision decision required')
|
|
63
|
+
require(isinstance(row.get('reason'), str) and bool(row['reason'].strip()), 'dependency assessment reason required')
|
|
64
|
+
if group == 'tools':
|
|
65
|
+
require(row.get('status') in ('passed', 'failed', 'missing', 'unknown'), 'tool status must reflect actual check results')
|
|
66
|
+
result = copy.deepcopy(data)
|
|
67
|
+
result.update(available=True, source_checks=checks,
|
|
68
|
+
stale=any(c['status'] != 'current' for c in checks) or 'candidate' in data and data['candidate'] != candidate)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read_analysis(file, now=None):
|
|
73
|
+
if file is None:
|
|
74
|
+
return None
|
|
75
|
+
content = Path(file).read_text(encoding='utf-8')
|
|
76
|
+
# A concurrently appended last line is not an event until its newline is present.
|
|
77
|
+
lines = content.split('\n')[:-1]
|
|
78
|
+
run_id, sequence, latest, terminal = None, 0, None, None
|
|
79
|
+
warnings, summary_counts = [], None
|
|
80
|
+
for line in lines:
|
|
81
|
+
if not line.strip():
|
|
82
|
+
continue
|
|
83
|
+
event = json.loads(line)
|
|
84
|
+
require(event.get('schema') == 'appgraph-progress/v1', 'unsupported analysis progress schema')
|
|
85
|
+
require(isinstance(event.get('run_id'), str) and bool(event['run_id']), 'analysis run_id required')
|
|
86
|
+
if run_id is None:
|
|
87
|
+
run_id = event['run_id']
|
|
88
|
+
require(event['run_id'] == run_id, 'one AppGraph run per progress file required')
|
|
89
|
+
require(type(event.get('sequence')) is int and event['sequence'] == sequence + 1, 'analysis progress sequence gap')
|
|
90
|
+
require(terminal is None, 'analysis event after terminal event')
|
|
91
|
+
require(event.get('status') in ('started', 'progress', 'completed', 'warning', 'failed'), 'unknown analysis event status')
|
|
92
|
+
require(isinstance(event.get('phase'), str), 'analysis phase required')
|
|
93
|
+
for key in ('current', 'total'):
|
|
94
|
+
require(event.get(key) is None or type(event[key]) in (int, float) and math.isfinite(event[key]) and event[key] >= 0, 'invalid analysis count')
|
|
95
|
+
sequence, latest = event['sequence'], event
|
|
96
|
+
if event['status'] == 'warning':
|
|
97
|
+
warnings.append({k: event[k] for k in ('phase', 'message', 'counts') if k in event})
|
|
98
|
+
if event['status'] == 'completed' and isinstance(event.get('counts'), dict):
|
|
99
|
+
summary_counts = event['counts']
|
|
100
|
+
if event['phase'] == 'analysis' and event['status'] in ('completed', 'failed'):
|
|
101
|
+
terminal = event['status']
|
|
102
|
+
if latest is None:
|
|
103
|
+
return {'state': 'unknown', 'message': '暂无完整进度事件', 'latest': None, 'stale': True}
|
|
104
|
+
timestamp = datetime.fromisoformat(latest['timestamp'].replace('Z', '+00:00'))
|
|
105
|
+
require(timestamp.tzinfo is not None, 'analysis timestamp needs timezone')
|
|
106
|
+
age = max(0, ((now or datetime.now(timezone.utc)) - timestamp).total_seconds())
|
|
107
|
+
return {'state': terminal or ('failed' if latest['status'] == 'failed' else 'running'), 'latest': latest, 'run_id': run_id, 'sequence': sequence,
|
|
108
|
+
'warnings': warnings, 'summary_counts': summary_counts,
|
|
109
|
+
'age_seconds': round(age, 1), 'stale': terminal is None and age > 30}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Read-only delivery projection. Acceptance always comes from spec_runtime.derive."""
|
|
2
|
+
import json
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
LAYERS = ('mvp', 'core', 'full')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ratio(items):
|
|
9
|
+
total = len(items)
|
|
10
|
+
accepted = sum(bool(i['accepted']) for i in items)
|
|
11
|
+
percent = (100.0 if accepted == total else min(99.9, round(100 * accepted / total, 1))) if total else None
|
|
12
|
+
return {'total': total, 'accepted': accepted, 'percent': percent,
|
|
13
|
+
'unplanned': sum(i.get('spec_id') is None for i in items)}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def progress(features, view):
|
|
17
|
+
if not view['scope_tracking']:
|
|
18
|
+
return {'configured': False, 'scope_tracking': False, 'selected': None, 'full': None, 'layers': {}}
|
|
19
|
+
rows = []
|
|
20
|
+
for feature in features['features']:
|
|
21
|
+
for item in view['feature_progress'][feature['feature_id']]['items']:
|
|
22
|
+
if feature['scope_status'] == 'excluded' or item['status'] == 'excluded':
|
|
23
|
+
continue
|
|
24
|
+
selected = feature['scope_status'] == 'included' and not feature['needs_clarification'] and item['status'] == 'included' and item['milestone'] != 'unassigned'
|
|
25
|
+
rows.append(dict(item, feature_id=feature['feature_id'], selected=selected))
|
|
26
|
+
config = features.get('delivery')
|
|
27
|
+
result = {'configured': config is not None, 'scope_tracking': True, 'target': config['target'] if config else None,
|
|
28
|
+
'customized': config['customized'] if config else None, 'selected': ratio([i for i in rows if i['selected']]),
|
|
29
|
+
'full': ratio(rows), 'layers': {}}
|
|
30
|
+
if config:
|
|
31
|
+
mapping = {(r['feature_id'], r['scope_id']): r['layer'] for r in config['scope_layers']}
|
|
32
|
+
for index, layer in enumerate(LAYERS):
|
|
33
|
+
result['layers'][layer] = {
|
|
34
|
+
'increment': ratio([i for i in rows if mapping[(i['feature_id'], i['scope_id'])] == layer]),
|
|
35
|
+
'cumulative': ratio([i for i in rows if mapping[(i['feature_id'], i['scope_id'])] in LAYERS[:index + 1]])}
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def snapshot(plan, features, state, view, captured_at, baseline=None):
|
|
40
|
+
"""Capture raw status and current proof together; baseline never substitutes for evidence."""
|
|
41
|
+
if view['running']:
|
|
42
|
+
activity = {'phase': 'implementation', 'label': '正在实施'}
|
|
43
|
+
elif view['validation_debt']['specs']:
|
|
44
|
+
activity = {'phase': 'verification', 'label': '有待验证或修复范围'}
|
|
45
|
+
elif state['candidate'] is None:
|
|
46
|
+
activity = {'phase': 'planning', 'label': '已有规划,尚未建立实现候选'}
|
|
47
|
+
else:
|
|
48
|
+
activity = {'phase': 'waiting', 'label': '当前无在途实现,等待下一步调度或 Review'}
|
|
49
|
+
before = {(f['feature_id'], i['scope_id']): i for f in (baseline or {}).get('features', []) for i in f.get('scope_items', [])}
|
|
50
|
+
current = {(f['feature_id'], i['scope_id']): (f, i) for f in features['features'] for i in f.get('scope_items', [])}
|
|
51
|
+
review = []
|
|
52
|
+
for key, (feature, item) in current.items():
|
|
53
|
+
derived = next((i for i in view['feature_progress'].get(key[0], {}).get('items', []) if i['scope_id'] == key[1]), {})
|
|
54
|
+
old = before.get(key)
|
|
55
|
+
difference = 'baseline-unavailable' if baseline is None else 'added' if old is None else 'changed' if any(old.get(k) != item.get(k) for k in ('description', 'acceptance')) else 'unchanged'
|
|
56
|
+
review.append({'feature_id': key[0], 'scope_id': key[1], 'description': item['description'], 'difference': difference,
|
|
57
|
+
'status': item['status'], 'spec_id': derived.get('spec_id'), 'accepted': derived.get('accepted', False),
|
|
58
|
+
'baseline': old, 'current': item, 'manual_review': 'pending'})
|
|
59
|
+
for key, item in before.items():
|
|
60
|
+
if key not in current:
|
|
61
|
+
review.append({'feature_id': key[0], 'scope_id': key[1], 'description': item['description'], 'difference': 'missing-from-current',
|
|
62
|
+
'status': 'unknown', 'spec_id': None, 'accepted': False, 'baseline': item, 'current': None, 'manual_review': 'pending'})
|
|
63
|
+
starts = {}
|
|
64
|
+
for entry in state['events']:
|
|
65
|
+
event = entry['event']
|
|
66
|
+
if event['type'] == 'start':
|
|
67
|
+
starts[event['spec_id']] = entry['timestamp']
|
|
68
|
+
# A task clock is the elapsed dispatch interval, not accumulated worker effort.
|
|
69
|
+
timing = {'task_started_at': {sid: starts[sid] for sid in view['running'] if sid in starts}}
|
|
70
|
+
return {'schema': 'd2h-dashboard/v1', 'captured_at': captured_at, 'activity': activity, 'timing': timing, 'feature_plan': features, 'plan': plan,
|
|
71
|
+
'execution': {k: state[k] for k in ('schema', 'candidate', 'plan_hash', 'feature_hash', 'tasks')},
|
|
72
|
+
'event_sequence': len(state['events']), 'derived': view, 'review': {
|
|
73
|
+
'baseline_available': baseline is not None,
|
|
74
|
+
'baseline_revision': baseline['revision'] if baseline else None,
|
|
75
|
+
'baseline_hash': hashlib.sha256(json.dumps(baseline, sort_keys=True, ensure_ascii=False).encode()).hexdigest() if baseline else None,
|
|
76
|
+
'items': review}}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def html(data, template):
|
|
80
|
+
# Payload may contain source/user text, including closing script tags.
|
|
81
|
+
payload = json.dumps(data, ensure_ascii=False).replace('<', '\\u003c').replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
|
|
82
|
+
return template.replace('/*__D2H_SNAPSHOT__*/null', payload)
|