@harmonyos-arkts/d2h 0.1.20 → 0.1.22
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-spec-develop/SKILL.md +41 -0
- 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/SKILL.md +0 -126
- 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,223 @@
|
|
|
1
|
+
"""Explicit page-round validation and bounded batch recommendations; no source inference."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def require(value, message):
|
|
7
|
+
if not value:
|
|
8
|
+
raise ValueError(message)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def text(value):
|
|
12
|
+
return isinstance(value, str) and bool(value.strip())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def records(items, key):
|
|
16
|
+
require(isinstance(items, list) and bool(items), f'{key}: nonempty records required')
|
|
17
|
+
result = {}
|
|
18
|
+
for item in items:
|
|
19
|
+
require(isinstance(item, dict) and text(item.get(key)) and item[key] not in result, f'{key}: missing or duplicate ID')
|
|
20
|
+
result[item[key]] = item
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def enabled(plan):
|
|
25
|
+
return plan.get('dispatch_mode', 'single-spec') == 'page-batches'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate(plan, features, specs, edges):
|
|
29
|
+
mode = plan.get('dispatch_mode', 'single-spec')
|
|
30
|
+
require(mode in {'single-spec', 'page-batches'}, 'unknown dispatch_mode')
|
|
31
|
+
if mode == 'single-spec':
|
|
32
|
+
require('rounds' not in plan and 'page_batches' not in plan, 'rounds require explicit page-batches dispatch_mode')
|
|
33
|
+
return
|
|
34
|
+
rounds = records(plan.get('rounds'), 'round_id')
|
|
35
|
+
batches = records(plan.get('page_batches'), 'batch_id')
|
|
36
|
+
milestones = [m['milestone_id'] for m in features['milestones']]
|
|
37
|
+
milestone_order = []
|
|
38
|
+
for index, item in enumerate(rounds.values(), 1):
|
|
39
|
+
require(type(item.get('order')) is int and item['order'] == index, 'round order must be consecutive and match array order')
|
|
40
|
+
require(item.get('kind') in {'baseline', 'incremental', 'completion'}, 'unknown round kind')
|
|
41
|
+
require(text(item.get('goal')) and text(item.get('rationale')), 'round goal and rationale required')
|
|
42
|
+
require(item.get('milestone') in milestones, 'round has unknown milestone')
|
|
43
|
+
milestone_order.append(milestones.index(item['milestone']))
|
|
44
|
+
checks = item.get('goal_checks')
|
|
45
|
+
require(isinstance(checks, list) and bool(checks) and all(text(c) for c in checks) and len(set(checks)) == len(checks), 'round goal_checks required and unique')
|
|
46
|
+
require(milestone_order == sorted(milestone_order), 'rounds cannot reverse milestone order')
|
|
47
|
+
for milestone in {r['milestone'] for r in rounds.values()}:
|
|
48
|
+
group = [r for r in rounds.values() if r['milestone'] == milestone]
|
|
49
|
+
require(group[0]['kind'] == 'baseline' and sum(r['kind'] == 'baseline' for r in group) == 1, 'each milestone starts with exactly one baseline round')
|
|
50
|
+
require(all(r['kind'] != 'completion' for r in group[:-1]), 'completion round must be last in its milestone')
|
|
51
|
+
pages = {p['page_id'] for p in plan['pages']}
|
|
52
|
+
owner = {}
|
|
53
|
+
for bid, batch in batches.items():
|
|
54
|
+
require(batch.get('round_id') in rounds and batch.get('page_id') in pages, f'{bid}: unknown round or page')
|
|
55
|
+
require(text(batch.get('goal')) and text(batch.get('rationale')), f'{bid}: batch goal and grouping rationale required')
|
|
56
|
+
members = batch.get('spec_ids')
|
|
57
|
+
require(isinstance(members, list) and bool(members) and all(text(s) for s in members) and len(set(members)) == len(members), f'{bid}: unique spec_ids required')
|
|
58
|
+
require(type(batch.get('max_specs')) is int and batch['max_specs'] > 0 and len(members) <= batch['max_specs'], f'{bid}: spec count exceeds batch budget')
|
|
59
|
+
for key in ('max_effort_hours', 'opportunistic_hours'):
|
|
60
|
+
value = batch.get(key)
|
|
61
|
+
require(type(value) in (int, float) and value >= 0 and value < float('inf'), f'{bid}: invalid {key}')
|
|
62
|
+
require(batch['max_effort_hours'] > 0 and batch['opportunistic_hours'] <= batch['max_effort_hours'], f'{bid}: invalid effort budget')
|
|
63
|
+
for sid in members:
|
|
64
|
+
require(sid in specs and sid not in owner, f'{bid}: unknown or multiply assigned Spec: {sid}')
|
|
65
|
+
require(specs[sid]['page_id'] == batch['page_id'], f'{bid}: cross-page grouping is not allowed')
|
|
66
|
+
require(specs[sid]['milestone'] == rounds[batch['round_id']]['milestone'], f'{bid}: round milestone mismatch')
|
|
67
|
+
owner[sid] = bid
|
|
68
|
+
require(sum(specs[s]['effort']['expected'] for s in members) <= batch['max_effort_hours'], f'{bid}: effort exceeds batch budget; split the page pass')
|
|
69
|
+
require(owner.keys() == specs.keys(), 'every Spec must belong to exactly one page batch')
|
|
70
|
+
for rid, item in rounds.items():
|
|
71
|
+
members = {s for s, bid in owner.items() if batches[bid]['round_id'] == rid}
|
|
72
|
+
require(bool(members), f'{rid}: empty round')
|
|
73
|
+
checks = {c for sid in members for c in specs[sid]['checks']}
|
|
74
|
+
require(set(item['goal_checks']) <= checks, f'{rid}: goal checks must map to member Specs')
|
|
75
|
+
batch_edges = {bid: set() for bid in batches}
|
|
76
|
+
for sid, parents in edges.items():
|
|
77
|
+
consumer = owner[sid]
|
|
78
|
+
consumer_round = rounds[batches[consumer]['round_id']]['order']
|
|
79
|
+
for parent in parents:
|
|
80
|
+
provider = owner[parent]
|
|
81
|
+
require(rounds[batches[provider]['round_id']]['order'] <= consumer_round, f'{sid}: dependency/order requires a later round')
|
|
82
|
+
if provider != consumer:
|
|
83
|
+
batch_edges[consumer].add(provider)
|
|
84
|
+
remaining = set(batches)
|
|
85
|
+
while remaining:
|
|
86
|
+
layer = {b for b in remaining if not batch_edges[b] & remaining}
|
|
87
|
+
require(layer, 'page batch contraction cycle; split interleaved page work')
|
|
88
|
+
remaining -= layer
|
|
89
|
+
for page in plan['pages']:
|
|
90
|
+
require(type(page.get('scope_complete')) is bool, 'page scope_complete coverage declaration required')
|
|
91
|
+
if page['scope_complete']:
|
|
92
|
+
refs = page.get('scope_evidence_refs')
|
|
93
|
+
require(isinstance(refs, list) and bool(refs) and all(text(ref) for ref in refs), 'complete page scope requires coverage evidence refs')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def round_signature(plan, features, round_id, edges):
|
|
97
|
+
specs = {s['spec_id']: s for s in plan['specs']}
|
|
98
|
+
item = next(r for r in plan['rounds'] if r['round_id'] == round_id)
|
|
99
|
+
members = {sid for b in plan['page_batches'] if b['round_id'] == round_id for sid in b['spec_ids']}
|
|
100
|
+
closure, pending = set(members), list(members)
|
|
101
|
+
while pending:
|
|
102
|
+
for parent in edges[pending.pop()]:
|
|
103
|
+
if parent not in closure:
|
|
104
|
+
closure.add(parent)
|
|
105
|
+
pending.append(parent)
|
|
106
|
+
semantic = ('page_id', 'feature_refs', 'scope', 'evidence_refs', 'requires', 'provides', 'checks')
|
|
107
|
+
needed_features = {f for sid in closure for f in specs[sid]['feature_refs']}
|
|
108
|
+
payload = {'round': {k: item[k] for k in ('round_id', 'kind', 'milestone', 'goal', 'goal_checks')}, 'members': sorted(members), 'specs': [{k: specs[sid][k] for k in ('spec_id', *semantic)} for sid in sorted(closure)], 'features': [{k: feature.get(k) for k in ('feature_id', 'required_scope', 'acceptance', 'source_refs', 'origin')} for feature in sorted(features['features'], key=lambda f: f['feature_id']) if feature['feature_id'] in needed_features]}
|
|
109
|
+
if features['schema'] == 'd2h-feature-plan/v2':
|
|
110
|
+
payload['scope_refs'] = {sid: specs[sid].get('scope_refs') for sid in sorted(closure)}
|
|
111
|
+
keys = {(ref['feature_id'], ref['scope_id']) for sid in closure for ref in specs[sid].get('scope_refs', [])}
|
|
112
|
+
payload['scope_items'] = [{'feature_id': f['feature_id'], **item} for f in features['features'] for item in f['scope_items'] if (f['feature_id'], item['scope_id']) in keys]
|
|
113
|
+
if plan.get('work_packet_policy') == 'required-v1':
|
|
114
|
+
payload['work_packets'] = {sid: {'brief': specs[sid].get('implementation_brief'), 'layers': specs[sid]['testing'].get('layers')} for sid in sorted(closure)}
|
|
115
|
+
payload['shared_contracts'] = [c for c in plan.get('shared_contracts', []) if set([c['producer_spec_id'], *c['consumer_spec_ids']]) & closure]
|
|
116
|
+
return hashlib.sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def relevant_specs(plan, initial, edges):
|
|
120
|
+
"""A required round goal cannot be delivered with only half its membership."""
|
|
121
|
+
if not enabled(plan):
|
|
122
|
+
return initial
|
|
123
|
+
rounds = {r['round_id']: r for r in plan['rounds']}
|
|
124
|
+
result = set(initial)
|
|
125
|
+
while True:
|
|
126
|
+
before = set(result)
|
|
127
|
+
touched = {b['round_id'] for b in plan['page_batches'] if set(b['spec_ids']) & result}
|
|
128
|
+
required_rounds = {rid for rid, item in rounds.items() if any(item['milestone'] == rounds[target]['milestone'] and item['order'] <= rounds[target]['order'] for target in touched)}
|
|
129
|
+
result.update(s for b in plan['page_batches'] if b['round_id'] in required_rounds for s in b['spec_ids'])
|
|
130
|
+
result.update(parent for sid in list(result) for parent in edges[sid])
|
|
131
|
+
if result == before:
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def batch_sequence(plan, edges):
|
|
136
|
+
batches = {b['batch_id']: b for b in plan['page_batches']}
|
|
137
|
+
owner = {sid: bid for bid, b in batches.items() for sid in b['spec_ids']}
|
|
138
|
+
result = []
|
|
139
|
+
for round_plan in plan['rounds']:
|
|
140
|
+
remaining = {bid for bid, b in batches.items() if b['round_id'] == round_plan['round_id']}
|
|
141
|
+
while remaining:
|
|
142
|
+
layer = sorted(bid for bid in remaining if not any(owner[parent] in remaining and owner[parent] != bid for sid in batches[bid]['spec_ids'] for parent in edges[sid]))
|
|
143
|
+
require(layer, 'invalid page batch order')
|
|
144
|
+
result.extend(layer)
|
|
145
|
+
remaining.difference_update(layer)
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def schedule(plan, features, state, view, edges, evidence_valid):
|
|
150
|
+
if not enabled(plan):
|
|
151
|
+
return view
|
|
152
|
+
specs = {s['spec_id']: s for s in plan['specs']}
|
|
153
|
+
rounds = {r['round_id']: r for r in plan['rounds']}
|
|
154
|
+
batches = {b['batch_id']: b for b in plan['page_batches']}
|
|
155
|
+
batch_order = {bid: index for index, bid in enumerate(batch_sequence(plan, edges))}
|
|
156
|
+
rows = view['tasks']
|
|
157
|
+
relevant = set(view['relevant_specs'])
|
|
158
|
+
round_rows = {}
|
|
159
|
+
for rid, item in rounds.items():
|
|
160
|
+
members = sorted(s for b in batches.values() if b['round_id'] == rid for s in b['spec_ids'])
|
|
161
|
+
signature = round_signature(plan, features, rid, edges)
|
|
162
|
+
proofs = [p for p in state.get('round_deliveries', []) if p['round_id'] == rid and p['signature'] == signature and evidence_valid(p['evidence'], p['candidate'])]
|
|
163
|
+
current = any(p['candidate'] == state['candidate'] for p in proofs) and all(rows[s]['accepted'] for s in members)
|
|
164
|
+
round_rows[rid] = {'order': item['order'], 'kind': item['kind'], 'goal': item['goal'], 'milestone': item['milestone'], 'spec_ids': members, 'delivered': bool(proofs), 'current_accepted': current, 'relevant': bool(set(members) & relevant)}
|
|
165
|
+
current_round = next((rid for rid, item in round_rows.items() if item['relevant'] and not item['delivered']), None)
|
|
166
|
+
active = state.get('active_batch')
|
|
167
|
+
if active:
|
|
168
|
+
require(active['batch_id'] in batches and set(active['spec_ids']) <= set(batches[active['batch_id']]['spec_ids']), 'active batch no longer matches plan; pause before revision')
|
|
169
|
+
old_ready = list(view['ready'])
|
|
170
|
+
stage_ready = [s for s in old_ready if any(b['round_id'] == current_round and s in b['spec_ids'] for b in batches.values())]
|
|
171
|
+
ranks = {'later': 0, 'normal': 1, 'must': 2}
|
|
172
|
+
last_page = state.get('batch_runs', [{}])[-1].get('page_id') if state.get('batch_runs') else None
|
|
173
|
+
batch_rows = {}
|
|
174
|
+
for bid, batch in batches.items():
|
|
175
|
+
members = batch['spec_ids']
|
|
176
|
+
potential = {s for s in members if state['tasks'][s]['implementation'] == 'planned' and not rows[s]['external_blockers'] and not rows[s].get('readiness_gaps') and s in relevant}
|
|
177
|
+
# Internal unverified prerequisites can be prepared in the same lease, but
|
|
178
|
+
# a blocked external dependency must never be hidden by grouping.
|
|
179
|
+
while True:
|
|
180
|
+
kept = {s for s in potential if set(rows[s]['blocked_by']) <= potential}
|
|
181
|
+
if kept == potential:
|
|
182
|
+
break
|
|
183
|
+
potential = kept
|
|
184
|
+
rank = max((ranks[rows[s]['effective_priority']] for s in potential), default=-1)
|
|
185
|
+
core = {s for s in potential if ranks[rows[s]['effective_priority']] == rank}
|
|
186
|
+
optional = potential - core
|
|
187
|
+
outside = [s for s in stage_ready if s not in members]
|
|
188
|
+
optional_hours = sum(specs[s]['effort']['expected'] for s in optional)
|
|
189
|
+
min_optional_rank = min((ranks[rows[s]['effective_priority']] for s in optional), default=rank)
|
|
190
|
+
higher_outside = any(ranks[rows[s]['effective_priority']] > min_optional_rank for s in outside)
|
|
191
|
+
selected = core | (optional if optional_hours <= batch['opportunistic_hours'] and not higher_outside else set())
|
|
192
|
+
# Keep deterministic dependency order within the fixed dispatch window.
|
|
193
|
+
selected_order, remaining = [], set(selected)
|
|
194
|
+
while remaining:
|
|
195
|
+
layer = sorted((s for s in remaining if not set(edges[s]) & remaining), key=lambda s: (-ranks[rows[s]['effective_priority']], s))
|
|
196
|
+
require(layer, 'invalid internal batch order')
|
|
197
|
+
selected_order.extend(layer)
|
|
198
|
+
remaining.difference_update(layer)
|
|
199
|
+
eligible = bool(set(selected_order) & set(stage_ready)) and batch['round_id'] == current_round and not active and not view['running'] and not view['validation_debt']['backpressure']
|
|
200
|
+
highest = [s for s in selected_order if ranks[rows[s]['effective_priority']] == rank]
|
|
201
|
+
critical = max((view['remaining_path_hours'][s] for s in highest), default=0)
|
|
202
|
+
shared = max((len(rows[s]['inherited_from']) for s in highest), default=0)
|
|
203
|
+
effort = sum(specs[s]['effort']['expected'] for s in selected_order)
|
|
204
|
+
score = sum(rows[s]['value_score'] for s in selected_order) / effort if effort else 0
|
|
205
|
+
batch_rows[bid] = {'page_id': batch['page_id'], 'round_id': batch['round_id'], 'goal': batch['goal'], 'spec_ids': list(members), 'dispatchable_spec_ids': selected_order, 'deferred_spec_ids': sorted(potential - set(selected_order)), 'eligible': eligible, 'accepted': all(rows[s]['accepted'] for s in members), 'implemented': all(state['tasks'][s]['implementation'] == 'implemented' for s in members), 'blocked_by': sorted({p for s in members for p in rows[s]['blocked_by'] if p not in members}), 'external_blockers': sorted({p for s in members for p in rows[s]['external_blockers']}), 'effective_priority_rank': rank, 'critical_hours': critical, 'shared_unlocks': shared, 'page_affinity': int(batch['page_id'] == last_page), 'priority_index': score, 'estimated_worker_hours': effort, 'planned_worker_hours': sum(specs[s]['effort']['expected'] for s in members)}
|
|
206
|
+
ordered_batches = [bid for bid, item in batch_rows.items() if item['eligible']]
|
|
207
|
+
ordered_batches.sort(key=lambda b: (-batch_rows[b]['effective_priority_rank'], -batch_rows[b]['critical_hours'], -batch_rows[b]['shared_unlocks'], -batch_rows[b]['page_affinity'], -batch_rows[b]['priority_index'], b))
|
|
208
|
+
page_rows = {}
|
|
209
|
+
for page in plan['pages']:
|
|
210
|
+
pid = page['page_id']
|
|
211
|
+
passes = [b for b in batches.values() if b['page_id'] == pid]
|
|
212
|
+
passes.sort(key=lambda b: batch_order[b['batch_id']])
|
|
213
|
+
first_order = rounds[passes[0]['round_id']]['order'] if passes else None
|
|
214
|
+
baseline_specs = {s for b in passes if rounds[b['round_id']]['order'] == first_order for s in b['spec_ids']}
|
|
215
|
+
all_specs = {s for b in passes for s in b['spec_ids']}
|
|
216
|
+
page_rows[pid] = {'scope_complete': page['scope_complete'], 'baseline_verified': bool(baseline_specs) and all(rows[s]['accepted'] for s in baseline_specs), 'complete': page['scope_complete'] and bool(all_specs) and all(rows[s]['accepted'] for s in all_specs), 'passes': [{'page_pass': i, 'round_id': b['round_id'], 'batch_id': b['batch_id'], 'spec_ids': list(b['spec_ids']), 'accepted': batch_rows[b['batch_id']]['accepted']} for i, b in enumerate(passes, 1)]}
|
|
217
|
+
view.update(rounds=round_rows, current_round=current_round, page_batches=batch_rows, ready_batches=ordered_batches, active_batch=active, pages=page_rows, ready_specs=stage_ready)
|
|
218
|
+
runs = state.get('batch_runs', [])
|
|
219
|
+
view['page_dispatch_metrics'] = {'finished_windows': sum(r['result'] == 'completed' for r in runs), 'paused_windows': sum(r['result'] == 'paused' for r in runs), 'page_switches': sum(a['page_id'] != b['page_id'] for a, b in zip(runs, runs[1:])), 'average_specs_per_window': sum(len(r['spec_ids']) for r in runs) / len(runs) if runs else None}
|
|
220
|
+
view['ready'] = [s for s in stage_ready if active and s in active['spec_ids']]
|
|
221
|
+
relevant_rounds = [r for r in round_rows.values() if r['relevant']]
|
|
222
|
+
view['milestone_accepted'] = view['milestone_accepted'] and all(r['current_accepted'] for r in relevant_rounds)
|
|
223
|
+
return view
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Bind selected, detailed Spec blocks to per-member planning inputs (not acceptance)."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
POLICY = 'required-v1'
|
|
8
|
+
SCHEMA = 'd2h-spec-generation/v1'
|
|
9
|
+
HEADINGS = ('页面范围', '开发顺序', '页面功能', 'UI 操作与验收', 'Android 证据', 'HarmonyOS 实现范围', '未决项与平台差异')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def digest(value):
|
|
13
|
+
return hashlib.sha256(value.encode('utf-8')).hexdigest()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def canonical(value):
|
|
17
|
+
return json.dumps(value, sort_keys=True, ensure_ascii=False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def signature(plan, features, spec):
|
|
21
|
+
page = next(p for p in plan['pages'] if p['page_id'] == spec['page_id'])
|
|
22
|
+
feature_map = {f['feature_id']: f for f in features['features']}
|
|
23
|
+
scopes = []
|
|
24
|
+
for ref in spec.get('scope_refs', []):
|
|
25
|
+
feature = feature_map[ref['feature_id']]
|
|
26
|
+
scopes.append({'feature_id': feature['feature_id'],
|
|
27
|
+
'scope': next(s for s in feature['scope_items'] if s['scope_id'] == ref['scope_id']),
|
|
28
|
+
'source': feature.get('source'), 'origin': feature.get('origin'), 'source_refs': feature.get('source_refs'),
|
|
29
|
+
'acceptance': feature.get('acceptance'), 'scope_status': feature['scope_status'],
|
|
30
|
+
'needs_clarification': feature['needs_clarification']})
|
|
31
|
+
if not scopes:
|
|
32
|
+
scopes = [feature_map[f] for f in spec['feature_refs']]
|
|
33
|
+
contracts = [c for c in plan.get('shared_contracts', []) if spec['spec_id'] in [c['producer_spec_id'], *c['consumer_spec_ids']]]
|
|
34
|
+
# Global revisions and unrelated scopes are deliberately excluded; actual member changes are not.
|
|
35
|
+
semantic_spec = {k: spec.get(k) for k in ('spec_id', 'page_id', 'feature_refs', 'scope_refs', 'scope', 'evidence_refs', 'requires', 'provides', 'checks', 'implementation_brief', 'allocation')}
|
|
36
|
+
semantic_spec['testing'] = {k: v for k, v in spec['testing'].items() if k != 'estimated_hours'}
|
|
37
|
+
return digest(canonical({'spec': semantic_spec, 'package': page['package'], 'scopes': scopes,
|
|
38
|
+
'shared_contracts': contracts, 'work_packet_policy': plan.get('work_packet_policy')}))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def document(plan, root, spec):
|
|
42
|
+
if root is None:
|
|
43
|
+
raise ValueError('generation root required')
|
|
44
|
+
root = Path(root).resolve()
|
|
45
|
+
page = next(p for p in plan['pages'] if p['page_id'] == spec['page_id'])
|
|
46
|
+
directory = root / page['package']
|
|
47
|
+
file = directory / 'spec.md'
|
|
48
|
+
if directory.is_symlink() or file.is_symlink() or not file.is_file() or file.resolve().parent != directory:
|
|
49
|
+
raise ValueError('local detailed spec.md required')
|
|
50
|
+
text = file.read_text(encoding='utf-8')
|
|
51
|
+
sid = spec['spec_id']
|
|
52
|
+
start, end = f'<!-- d2h-spec:start {sid} -->', f'<!-- d2h-spec:end {sid} -->'
|
|
53
|
+
if text.count(start) != 1 or text.count(end) != 1 or text.index(start) >= text.index(end):
|
|
54
|
+
raise ValueError('unique ordered Spec block markers required')
|
|
55
|
+
body = text.split(start, 1)[1].split(end, 1)[0]
|
|
56
|
+
if '<!-- d2h-spec:' in body:
|
|
57
|
+
raise ValueError('Spec blocks cannot be nested')
|
|
58
|
+
if not all(check in body for check in spec['checks']):
|
|
59
|
+
raise ValueError('all planned check IDs must occur in detailed block')
|
|
60
|
+
pattern = re.compile(r'<!-- d2h-spec:start ([^\r\n]+?) -->.*?<!-- d2h-spec:end \1 -->', re.S)
|
|
61
|
+
members = pattern.findall(text)
|
|
62
|
+
context = pattern.sub('', text)
|
|
63
|
+
if len(set(members)) != len(members) or '<!-- d2h-spec:' in context:
|
|
64
|
+
raise ValueError('all member blocks must have unique, ordered matching boundaries')
|
|
65
|
+
# All seven shared sections remain in the original layout, including sections after UI checks.
|
|
66
|
+
# Adding another member may add empty separator lines, not change shared behavior.
|
|
67
|
+
context = '\n'.join(line.rstrip() for line in context.splitlines() if line.strip())
|
|
68
|
+
if not all('## ' + heading in context for heading in HEADINGS):
|
|
69
|
+
raise ValueError('seven shared Spec sections required outside member blocks')
|
|
70
|
+
return {'context_sha256': digest(context), 'body_sha256': digest(body)}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def manifest(root):
|
|
74
|
+
path = Path(root) / 'generation.json'
|
|
75
|
+
if path.is_symlink():
|
|
76
|
+
raise ValueError('generation manifest cannot be linked')
|
|
77
|
+
if not path.exists():
|
|
78
|
+
return {'schema': SCHEMA, 'specs': {}}
|
|
79
|
+
data = json.loads(path.read_text(encoding='utf-8'))
|
|
80
|
+
if not isinstance(data, dict) or data.get('schema') != SCHEMA or not isinstance(data.get('specs'), dict):
|
|
81
|
+
raise ValueError('generation manifest schema mismatch')
|
|
82
|
+
return data
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def gaps(plan, features, spec, root):
|
|
86
|
+
if plan.get('spec_generation_policy') != POLICY:
|
|
87
|
+
return []
|
|
88
|
+
try:
|
|
89
|
+
if root is None:
|
|
90
|
+
raise ValueError('generation root required')
|
|
91
|
+
recorded = manifest(root)['specs'].get(spec['spec_id'], {})
|
|
92
|
+
if not isinstance(recorded, dict):
|
|
93
|
+
raise ValueError('generation entry must be an object')
|
|
94
|
+
current = {**document(plan, root, spec), 'input_sha256': signature(plan, features, spec)}
|
|
95
|
+
if any(recorded.get(key) != value for key, value in current.items()):
|
|
96
|
+
raise ValueError('detailed Spec absent, changed or bound to stale planning inputs')
|
|
97
|
+
return []
|
|
98
|
+
except (OSError, ValueError, KeyError, TypeError, StopIteration) as error:
|
|
99
|
+
return ['spec generation: ' + str(error)]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def record(plan, features, root, spec_ids, expected_hash):
|
|
103
|
+
if plan.get('spec_generation_policy') != POLICY:
|
|
104
|
+
raise ValueError('generation requires explicit spec_generation_policy=required-v1')
|
|
105
|
+
if expected_hash != digest(canonical(plan)):
|
|
106
|
+
raise ValueError('generation plan hash mismatch; re-read selected revision')
|
|
107
|
+
specs = {s['spec_id']: s for s in plan['specs']}
|
|
108
|
+
if not spec_ids or len(set(spec_ids)) != len(spec_ids) or not set(spec_ids) <= specs.keys():
|
|
109
|
+
raise ValueError('explicit unique known spec IDs required')
|
|
110
|
+
result = manifest(root)
|
|
111
|
+
for sid in spec_ids:
|
|
112
|
+
result['specs'][sid] = {**document(plan, root, specs[sid]),
|
|
113
|
+
'input_sha256': signature(plan, features, specs[sid]),
|
|
114
|
+
'plan_revision': plan['revision'], 'plan_sha256': expected_hash}
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def proof(root, sid):
|
|
119
|
+
entry = manifest(root)['specs'][sid]
|
|
120
|
+
return {key: entry[key] for key in ('input_sha256', 'context_sha256', 'body_sha256')}
|