easy-coding-harness 0.10.0-beta.5 → 0.10.0-beta.6
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/CHANGELOG.md +16 -0
- package/README.md +13 -9
- package/package.json +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +27 -8
- package/templates/common/skills/ec-workflow/SKILL.md +33 -17
- package/templates/main-constraint/AGENTS.md.tpl +5 -0
- package/templates/main-constraint/CLAUDE.md.tpl +5 -0
- package/templates/shared-hooks/easy_coding_state.py +27 -10
- package/templates/shared-hooks/easy_dev_spec.py +385 -46
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.10.0-beta.6
|
|
10
|
+
|
|
11
|
+
- Canonical Spec 首次路由新增 `inspect-dev-spec --manifest-only`:只通过 normalized remote
|
|
12
|
+
识别当前 worktree、展示任务目录和共享 execution 状态,不再为未选仓库解析本地路径或
|
|
13
|
+
为任何未选任务计算 baseline;`path_hint` 明确降级为提示信息。
|
|
14
|
+
- 用户选定 task 后,`inspect-dev-spec --spec-task ...` 只检查所选任务所属仓库及其
|
|
15
|
+
change/test 范围;当前 worktree remote 唯一匹配时无需手工传 `--repo-path`,即使旧
|
|
16
|
+
`path_hint` 指向仍存在的原 checkout,也优先绑定当前 worktree。
|
|
17
|
+
- `ec-workflow` 不再在路由阶段提前读取消费闭包;`ec-analysis` 负责唯一一次精确 selector
|
|
18
|
+
调用,并对 `exact` / `scope-unchanged` 使用快速投影,对 `scope-drifted` 只分析所选任务
|
|
19
|
+
的漂移文件和符号,派生 dev-spec 不再被解释为第二轮 Spec 创作。
|
|
20
|
+
- 共享 `EDS:EXECUTION` 固化为依赖事实来源:已完成 hard task 或已满足依赖边直接放行;
|
|
21
|
+
禁止通过另一个本地 Harness task、Git 历史或 Agent 推断重复考古完成状态。
|
|
22
|
+
- 保持项目外原始 Spec absolute locator、身份校验 rebind、共享 writer/CAS 及旧版全量
|
|
23
|
+
`inspect-dev-spec` 调用兼容;新增 worktree、未选仓库隔离和三平台安装产物回归测试。
|
|
24
|
+
|
|
9
25
|
## 0.10.0-beta.5
|
|
10
26
|
|
|
11
27
|
- 兼容新版 `easy-dev-spec/v1` 共享执行区:同步设计/整文双摘要与 execution 投影协议,新增
|
package/README.md
CHANGED
|
@@ -101,15 +101,19 @@ any stage --[user abort via ec-task-close]--> CLOSED
|
|
|
101
101
|
|
|
102
102
|
Harness 可选择性消费 easy-dev-spec 生成的单文件 `easy-dev-spec/v1` Canonical Spec:
|
|
103
103
|
|
|
104
|
-
1. `ec-workflow`
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
1. `ec-workflow` 先用 `inspect-dev-spec --manifest-only` 只读校验 manifest,通过 normalized
|
|
105
|
+
remote 识别当前 worktree,并展示任务 DAG 与共享 execution;不会解析未选仓库路径,也
|
|
106
|
+
不在选择前计算任何任务的 baseline。旧 Spec 仍可只读检查,但成为可执行任务前必须
|
|
107
|
+
初始化共享执行区。
|
|
108
|
+
2. 用户明确选择一个或多个 Spec task 后,Harness 用 `--spec-task` 只检查所选任务仓库和
|
|
109
|
+
change/test 范围。当前 worktree remote 唯一匹配时无需手工路径;`path_hint` 不一致只会
|
|
110
|
+
形成一次运行时映射提示,不会复制或修复原 Spec。
|
|
111
|
+
3. 一次选择创建一个 Harness task;ANALYSIS 唯一一次调用 `select-dev-spec-scope` 提取确定性
|
|
112
|
+
消费闭包。`exact` / `scope-unchanged` 直接快速投影为 Unit、测试策略和派生 dev-spec,
|
|
113
|
+
`scope-drifted` 才读取所选范围内的漂移文件与符号;未选任务正文始终不进入上下文。
|
|
114
|
+
4. `EDS:EXECUTION` 是依赖事实来源:`hard` 依赖决定执行顺序,冻结的 `contract` 依赖允许
|
|
115
|
+
并行编码,`integration` 依赖在证据闭合前阻止全链路完成;不会再从本地任务或 Git 历史
|
|
116
|
+
重复推断共享状态。
|
|
113
117
|
|
|
114
118
|
Canonical Spec 的静态设计由 design revision + `design_sha256` 冻结;共享
|
|
115
119
|
`EDS:EXECUTION` 则接收 Harness 的 Task/Step/dependency 投影。写回使用
|
package/package.json
CHANGED
|
@@ -21,15 +21,18 @@ Communicate with the user in the user's language.
|
|
|
21
21
|
4. Inspect concrete code paths and tests. Expand context only when evidence reveals another
|
|
22
22
|
dependency or risk.
|
|
23
23
|
|
|
24
|
-
For a task with `task.json.spec_source`, re-run `inspect-dev-spec` against the stored source
|
|
25
|
-
|
|
26
|
-
`design_sha256` must still match. A changed `document_sha256` with the same design is
|
|
27
|
-
shared progress; refresh `execution_revision` without invalidating plan/review/verify
|
|
28
|
-
An execution revision rollback is blocking. Then call the selector for the exact
|
|
24
|
+
For a task with `task.json.spec_source`, re-run `inspect-dev-spec` against the stored source, exact
|
|
25
|
+
`selected_spec_tasks`, and only their stored `task.repo_paths` bindings. Schema, Spec ID, design
|
|
26
|
+
revision, and `design_sha256` must still match. A changed `document_sha256` with the same design is
|
|
27
|
+
normal shared progress; refresh `execution_revision` without invalidating plan/review/verify
|
|
28
|
+
evidence. An execution revision rollback is blocking. Then call the selector once for the exact
|
|
29
|
+
selection:
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
32
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py inspect-dev-spec \
|
|
32
|
-
--spec <stored-source>
|
|
33
|
+
--spec <stored-source> \
|
|
34
|
+
--spec-task <selected-task-id> [--spec-task <selected-task-id>]... \
|
|
35
|
+
[--repo-path <repo-id>=<stored-path>]...
|
|
33
36
|
|
|
34
37
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py select-dev-spec-scope \
|
|
35
38
|
--spec <stored-source> --spec-task <selected-task-id> [--spec-task <selected-task-id>]...
|
|
@@ -38,8 +41,20 @@ An execution revision rollback is blocking. Then call the selector for the exact
|
|
|
38
41
|
Load only the returned per-repository consumption closures: manifest/global context, selected
|
|
39
42
|
task and repository sections, related contracts, direct dependency summaries, selected
|
|
40
43
|
changes/steps/tests, and relevant integration rows. Never replace the selector with a whole-file
|
|
41
|
-
read.
|
|
42
|
-
|
|
44
|
+
read. Treat the Canonical closure as frozen design, not as a prompt to design the task again:
|
|
45
|
+
|
|
46
|
+
- `exact` and `scope-unchanged` use the fast projection path. Confirm the selected paths, symbols,
|
|
47
|
+
and test entry points, then map the closure into Units, `test-strategy.md`, and the derived
|
|
48
|
+
`dev-spec.md` in one pass. Do not reselect interfaces, fields, task boundaries, Steps, or Tests,
|
|
49
|
+
and do not ask questions already answered by the source Spec.
|
|
50
|
+
- `scope-drifted` reads and analyzes only changed files and symbols in the selected task scope.
|
|
51
|
+
Escalate to a static design revision only when that evidence changes a frozen contract or task
|
|
52
|
+
boundary; unrelated repository or completed-task changes are background, not current drift.
|
|
53
|
+
- `baseline-unavailable` or unresolved selected-repository identity remains blocked in ANALYSIS.
|
|
54
|
+
|
|
55
|
+
Use shared execution dependency status directly. Do not inspect another local Harness task or Git
|
|
56
|
+
history to re-prove a completed hard dependency, and do not repeat dependency or baseline
|
|
57
|
+
explanations after the selected inspection has recorded them.
|
|
43
58
|
|
|
44
59
|
## Analysis artifacts
|
|
45
60
|
|
|
@@ -107,6 +122,10 @@ digest, current document digest/execution revision/writeback status, selected ta
|
|
|
107
122
|
and repositories, baseline/conflict result, Unit mapping, source test mapping, and pending
|
|
108
123
|
integration edges.
|
|
109
124
|
|
|
125
|
+
The required skeleton is a mechanical artifact schema. For a Canonical-backed task it is filled
|
|
126
|
+
from the selected closure and current-code delta; it must never become a second round of Spec
|
|
127
|
+
authoring.
|
|
128
|
+
|
|
110
129
|
Every source test command remains mandatory. Additional commands from the current repository are
|
|
111
130
|
allowed only when `test-strategy.md` records why the Canonical command alone is insufficient.
|
|
112
131
|
For every selected source test, `test-strategy.md` must spell out its Test ID, source task ID,
|
|
@@ -61,36 +61,52 @@ plan decision; Auto continues immediately. Both remove later waiting, not qualit
|
|
|
61
61
|
- Present with `status != "COMPLETE"`: invoke `{{skill_trigger}}ec-init`, then stop.
|
|
62
62
|
- `[easy-coding:upgrade-init-pending:X]`: recommend `{{skill_trigger}}ec-init` for vX
|
|
63
63
|
adaptation, but allow the user to continue; this reminder is not a workflow block.
|
|
64
|
-
3. When the user explicitly references a Dev-Spec, run
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
3. When the user explicitly references a Dev-Spec, first run read-only `inspect-dev-spec
|
|
65
|
+
--manifest-only`. This routing pass validates the document, identifies the current worktree by
|
|
66
|
+
normalized remote, and returns the task catalog without resolving unrelated repositories. For
|
|
67
|
+
`protocol=canonical-v1`, show every task ID, repository, title, static status, actual execution
|
|
68
|
+
status, and dependency; never select all tasks by default and never calculate baseline drift
|
|
69
|
+
before selection.
|
|
70
|
+
|
|
71
|
+
After the user chooses one or more tasks, run one selected inspection with repeated
|
|
72
|
+
`--spec-task`. Resolve paths only for repositories that own selected tasks. The current
|
|
73
|
+
worktree needs no explicit mapping when its normalized remote matches uniquely; pass
|
|
74
|
+
`--repo-path` only for an additional selected repository or to confirm an ambiguous current
|
|
75
|
+
match. A differing `path_hint` is a one-time runtime mapping notice, not a Spec portability
|
|
76
|
+
failure: never copy, mirror, or rewrite the source Spec because of it.
|
|
77
|
+
|
|
78
|
+
Then call `create-task-from-spec` once for the complete selection. Do not call
|
|
79
|
+
`select-dev-spec-scope` during routing; `ec-analysis` owns the single consumption-closure read
|
|
80
|
+
after task creation. A document without a Canonical manifest remains a legacy ANALYSIS input
|
|
81
|
+
for an ordinary task. A malformed, DRAFT, or otherwise non-READY Canonical Spec stays blocked
|
|
82
|
+
and must never be downgraded to the legacy route. A READY Canonical Spec without shared
|
|
83
|
+
execution remains readable, but run `initialize-spec-execution` before selection can become an
|
|
84
|
+
executable Harness task.
|
|
73
85
|
|
|
74
86
|
```bash
|
|
75
87
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py inspect-dev-spec \
|
|
76
|
-
--spec <path>
|
|
88
|
+
--spec <path> --manifest-only
|
|
89
|
+
|
|
90
|
+
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py inspect-dev-spec \
|
|
91
|
+
--spec <path> --spec-task <task-id> [--spec-task <task-id>]... \
|
|
92
|
+
[--repo-path <repo-id>=<path>]...
|
|
77
93
|
|
|
78
94
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py initialize-spec-execution \
|
|
79
95
|
--spec <path>
|
|
80
96
|
|
|
81
|
-
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py select-dev-spec-scope \
|
|
82
|
-
--spec <path> --spec-task <task-id> [--spec-task <task-id>]...
|
|
83
|
-
|
|
84
97
|
{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py create-task-from-spec \
|
|
85
98
|
--spec <path> --spec-task <task-id> [--spec-task <task-id>]... \
|
|
86
99
|
--task-id <harness-task-id> --type <type> --title <title> \
|
|
87
|
-
--repo-path <repo-id>=<path>
|
|
100
|
+
[--repo-path <repo-id>=<path>]... \
|
|
101
|
+
[--dependency-evidence <dependency-id>=<evidence>]... \
|
|
88
102
|
--agent <agent-id> --session-file <P>
|
|
89
103
|
```
|
|
90
104
|
|
|
91
|
-
`
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
Shared `EDS:EXECUTION` is the dependency fact source. Accept a completed hard dependency or a
|
|
106
|
+
satisfied edge directly from that snapshot. Only accept manual dependency evidence when the
|
|
107
|
+
shared edge is still pending and the user explicitly supplies independently verifiable
|
|
108
|
+
evidence; never reconstruct completion from another local Harness task, Git history, or an
|
|
109
|
+
agent's inference.
|
|
94
110
|
|
|
95
111
|
When multiple selected tasks depend on the same target, disambiguate creation evidence with
|
|
96
112
|
`<source-task-id>-><dependency-task-id>=<evidence>`.
|
|
@@ -92,6 +92,11 @@ First run `ec-init`; daily work goes through `ec-workflow`.
|
|
|
92
92
|
external explicit Spec paths are allowed and may be repaired only with identity-checked rebind.
|
|
93
93
|
Runtime progress must use the shared writer with CAS/idempotency and reconciliation; static
|
|
94
94
|
design changes require revision + READY + `sync-spec-design`. Never hand-edit `EDS:EXECUTION`.
|
|
95
|
+
- Canonical routing is two-pass: first use manifest-only discovery for the current worktree, then
|
|
96
|
+
inspect only the explicitly selected task IDs and repositories. A remote-confirmed worktree
|
|
97
|
+
overrides a stale `path_hint`; never mirror the source Spec or re-check unselected repositories.
|
|
98
|
+
ANALYSIS reads the selected consumption closure once and treats exact/scope-unchanged as a fast
|
|
99
|
+
projection, while shared execution is the dependency fact source.
|
|
95
100
|
- MEMORY combines short-memory creation and the conditional long-memory gate. Entry follows the
|
|
96
101
|
effective confirmation mode; once memory processing completes, COMPLETE is automatic.
|
|
97
102
|
- NO CODE-TASK COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE.
|
|
@@ -90,6 +90,11 @@ First run `/ec-init`; daily work goes through `/ec-workflow`.
|
|
|
90
90
|
external explicit Spec paths are allowed and may be repaired only with identity-checked rebind.
|
|
91
91
|
Runtime progress must use the shared writer with CAS/idempotency and reconciliation; static
|
|
92
92
|
design changes require revision + READY + `sync-spec-design`. Never hand-edit `EDS:EXECUTION`.
|
|
93
|
+
- Canonical routing is two-pass: first use manifest-only discovery for the current worktree, then
|
|
94
|
+
inspect only the explicitly selected task IDs and repositories. A remote-confirmed worktree
|
|
95
|
+
overrides a stale `path_hint`; never mirror the source Spec or re-check unselected repositories.
|
|
96
|
+
ANALYSIS reads the selected consumption closure once and treats exact/scope-unchanged as a fast
|
|
97
|
+
projection, while shared execution is the dependency fact source.
|
|
93
98
|
- MEMORY combines short-memory creation and the conditional long-memory gate. Entry follows the
|
|
94
99
|
effective confirmation mode; once memory processing completes, COMPLETE is automatic.
|
|
95
100
|
- NO CODE-TASK COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE.
|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
|
|
17
17
|
from easy_dev_spec import (
|
|
18
18
|
EasyDevSpecError,
|
|
19
|
+
inspect_manifest,
|
|
19
20
|
inspect_spec,
|
|
20
21
|
inspection_summary,
|
|
21
22
|
select_consumption_scopes,
|
|
@@ -1754,8 +1755,16 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
|
|
|
1754
1755
|
if stored.get(field) != expected.get(field):
|
|
1755
1756
|
raise StateError("Canonical Spec dependency metadata no longer matches source selection.")
|
|
1756
1757
|
refreshed = dict(stored)
|
|
1757
|
-
|
|
1758
|
-
|
|
1758
|
+
for field in (
|
|
1759
|
+
"status",
|
|
1760
|
+
"shared_status",
|
|
1761
|
+
"dependency_task_status",
|
|
1762
|
+
"basis",
|
|
1763
|
+
):
|
|
1764
|
+
if expected.get(field) is None:
|
|
1765
|
+
refreshed.pop(field, None)
|
|
1766
|
+
else:
|
|
1767
|
+
refreshed[field] = expected.get(field)
|
|
1759
1768
|
if expected.get("evidence"):
|
|
1760
1769
|
refreshed["evidence"] = expected.get("evidence")
|
|
1761
1770
|
refreshed_dependencies.append(refreshed)
|
|
@@ -6735,6 +6744,8 @@ def main() -> int:
|
|
|
6735
6744
|
inspect_spec_parser = subcommands.add_parser("inspect-dev-spec", parents=[common])
|
|
6736
6745
|
inspect_spec_parser.add_argument("--spec", required=True)
|
|
6737
6746
|
inspect_spec_parser.add_argument("--repo-path", action="append", default=[])
|
|
6747
|
+
inspect_spec_parser.add_argument("--spec-task", action="append", default=[])
|
|
6748
|
+
inspect_spec_parser.add_argument("--manifest-only", action="store_true")
|
|
6738
6749
|
|
|
6739
6750
|
initialize_spec = subcommands.add_parser("initialize-spec-execution", parents=[common])
|
|
6740
6751
|
initialize_spec.add_argument("--spec", required=True)
|
|
@@ -6756,7 +6767,7 @@ def main() -> int:
|
|
|
6756
6767
|
create_from_spec.add_argument("--task-id", required=True)
|
|
6757
6768
|
create_from_spec.add_argument("--type", required=True)
|
|
6758
6769
|
create_from_spec.add_argument("--title", required=True)
|
|
6759
|
-
create_from_spec.add_argument("--repo-path",
|
|
6770
|
+
create_from_spec.add_argument("--repo-path", action="append", default=[])
|
|
6760
6771
|
create_from_spec.add_argument("--dependency-evidence", action="append", default=[])
|
|
6761
6772
|
create_from_spec.add_argument("--agent", required=True)
|
|
6762
6773
|
create_from_spec.add_argument("--no-set-current", action="store_true")
|
|
@@ -7017,15 +7028,21 @@ def main() -> int:
|
|
|
7017
7028
|
emit(snapshot_state(root, session_file))
|
|
7018
7029
|
elif command == "inspect-dev-spec":
|
|
7019
7030
|
spec_path = Path(args.spec).expanduser()
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7031
|
+
if args.manifest_only and args.spec_task:
|
|
7032
|
+
raise StateError("--manifest-only cannot be combined with --spec-task")
|
|
7033
|
+
resolved_spec = spec_path if spec_path.is_absolute() else root / spec_path
|
|
7034
|
+
repo_paths = parse_mapping_args(args.repo_path, "--repo-path")
|
|
7035
|
+
inspection = (
|
|
7036
|
+
inspect_manifest(resolved_spec, root, repo_paths)
|
|
7037
|
+
if args.manifest_only
|
|
7038
|
+
else inspect_spec(
|
|
7039
|
+
resolved_spec,
|
|
7040
|
+
root,
|
|
7041
|
+
repo_paths,
|
|
7042
|
+
args.spec_task or None,
|
|
7027
7043
|
)
|
|
7028
7044
|
)
|
|
7045
|
+
emit(inspection_summary(inspection))
|
|
7029
7046
|
elif command == "initialize-spec-execution":
|
|
7030
7047
|
emit(initialize_spec_execution_state(root, args.spec))
|
|
7031
7048
|
elif command == "select-dev-spec-scope":
|
|
@@ -128,31 +128,300 @@ def portable_path(root: Path, path: Path) -> str:
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _path_hint_status(root: Path, repository: dict[str, Any], repository_path: Path) -> str:
|
|
132
|
+
hint = Path(str(repository.get("path_hint") or ""))
|
|
133
|
+
if not str(hint):
|
|
134
|
+
return "missing"
|
|
135
|
+
resolved_hint = (hint if hint.is_absolute() else root / hint).resolve()
|
|
136
|
+
return "matched" if resolved_hint == repository_path.resolve() else "different"
|
|
137
|
+
|
|
138
|
+
|
|
131
139
|
def _candidate_repository_paths(
|
|
132
140
|
root: Path,
|
|
133
141
|
repository: dict[str, Any],
|
|
134
142
|
explicit_paths: dict[str, str],
|
|
135
|
-
) -> list[Path]:
|
|
143
|
+
) -> list[tuple[str, Path]]:
|
|
136
144
|
repo_id = str(repository["repo_id"])
|
|
137
|
-
candidates: list[Path] = []
|
|
145
|
+
candidates: list[tuple[str, Path]] = []
|
|
138
146
|
explicit = explicit_paths.get(repo_id)
|
|
139
147
|
if explicit:
|
|
140
148
|
path = Path(explicit)
|
|
141
149
|
# An explicit binding is authoritative. Falling back to path_hint/root would make a
|
|
142
150
|
# mistyped --repo-path appear valid while silently binding a different checkout.
|
|
143
|
-
return [(path if path.is_absolute() else root / path).resolve()]
|
|
151
|
+
return [("explicit", (path if path.is_absolute() else root / path).resolve())]
|
|
152
|
+
candidates.append(("current-root", root))
|
|
144
153
|
hint = Path(str(repository.get("path_hint") or ""))
|
|
145
154
|
if str(hint):
|
|
146
|
-
candidates.append(hint if hint.is_absolute() else root / hint)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
for candidate in candidates:
|
|
155
|
+
candidates.append(("path-hint", hint if hint.is_absolute() else root / hint))
|
|
156
|
+
unique: list[tuple[str, Path]] = []
|
|
157
|
+
seen: set[Path] = set()
|
|
158
|
+
for binding_source, candidate in candidates:
|
|
150
159
|
resolved = candidate.resolve()
|
|
151
|
-
if resolved not in
|
|
152
|
-
unique.append(resolved)
|
|
160
|
+
if resolved not in seen:
|
|
161
|
+
unique.append((binding_source, resolved))
|
|
162
|
+
seen.add(resolved)
|
|
153
163
|
return unique
|
|
154
164
|
|
|
155
165
|
|
|
166
|
+
def _resolve_repository_binding(
|
|
167
|
+
root: Path,
|
|
168
|
+
repository: dict[str, Any],
|
|
169
|
+
explicit_paths: dict[str, str],
|
|
170
|
+
) -> dict[str, Any] | None:
|
|
171
|
+
expected_remotes = {
|
|
172
|
+
normalize_remote(remote) for remote in repository["remote_urls"]
|
|
173
|
+
}
|
|
174
|
+
# 显式路径保持权威;否则当前 worktree 一旦通过 remote 验证就立即返回,不再探测旧 path_hint。
|
|
175
|
+
for binding_source, repository_path in _candidate_repository_paths(
|
|
176
|
+
root, repository, explicit_paths
|
|
177
|
+
):
|
|
178
|
+
if not repository_path.is_dir():
|
|
179
|
+
continue
|
|
180
|
+
if not expected_remotes.intersection(repository_remotes(repository_path)):
|
|
181
|
+
continue
|
|
182
|
+
return {
|
|
183
|
+
"path": repository_path,
|
|
184
|
+
"binding_source": binding_source,
|
|
185
|
+
"path_hint_status": _path_hint_status(root, repository, repository_path),
|
|
186
|
+
}
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _current_repository_match(
|
|
191
|
+
root: Path,
|
|
192
|
+
repositories: list[dict[str, Any]],
|
|
193
|
+
explicit_paths: dict[str, str],
|
|
194
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
195
|
+
resolved_root = root.resolve()
|
|
196
|
+
repository_by_id = {
|
|
197
|
+
str(repository["repo_id"]): repository for repository in repositories
|
|
198
|
+
}
|
|
199
|
+
if len(explicit_paths) > 1:
|
|
200
|
+
raise EasyDevSpecError(
|
|
201
|
+
"Manifest routing accepts at most one --repo-path for the current repository"
|
|
202
|
+
)
|
|
203
|
+
if explicit_paths:
|
|
204
|
+
repo_id, raw_path = next(iter(explicit_paths.items()))
|
|
205
|
+
repository = repository_by_id.get(repo_id)
|
|
206
|
+
if repository is None:
|
|
207
|
+
raise EasyDevSpecError(f"Unknown Canonical repository: {repo_id}")
|
|
208
|
+
path = Path(raw_path)
|
|
209
|
+
resolved_path = (path if path.is_absolute() else root / path).resolve()
|
|
210
|
+
if resolved_path != resolved_root:
|
|
211
|
+
raise EasyDevSpecError(
|
|
212
|
+
"Manifest routing --repo-path must identify the current Git worktree"
|
|
213
|
+
)
|
|
214
|
+
binding = _resolve_repository_binding(root, repository, explicit_paths)
|
|
215
|
+
if binding is None:
|
|
216
|
+
raise EasyDevSpecError(
|
|
217
|
+
f"Current worktree does not match Canonical repository {repo_id}"
|
|
218
|
+
)
|
|
219
|
+
return repository, binding
|
|
220
|
+
|
|
221
|
+
local_remotes = set(repository_remotes(resolved_root))
|
|
222
|
+
if not local_remotes:
|
|
223
|
+
raise EasyDevSpecError(
|
|
224
|
+
"Current worktree has no Git remote; configure a matching remote before "
|
|
225
|
+
"Canonical manifest routing"
|
|
226
|
+
)
|
|
227
|
+
matches = [
|
|
228
|
+
repository
|
|
229
|
+
for repository in repositories
|
|
230
|
+
if local_remotes.intersection(
|
|
231
|
+
normalize_remote(remote) for remote in repository["remote_urls"]
|
|
232
|
+
)
|
|
233
|
+
]
|
|
234
|
+
if len(matches) != 1:
|
|
235
|
+
candidates = ", ".join(str(item["repo_id"]) for item in matches) or "none"
|
|
236
|
+
raise EasyDevSpecError(
|
|
237
|
+
"Current worktree must match exactly one Canonical repository; "
|
|
238
|
+
f"matched: {candidates}. Pass --repo-path <repo-id>=<current-worktree>."
|
|
239
|
+
)
|
|
240
|
+
repository = matches[0]
|
|
241
|
+
return repository, {
|
|
242
|
+
"path": resolved_root,
|
|
243
|
+
"binding_source": "current-root-remote",
|
|
244
|
+
"path_hint_status": _path_hint_status(root, repository, resolved_root),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _dependency_execution_summary(
|
|
249
|
+
dependency: dict[str, Any],
|
|
250
|
+
source_snapshot: dict[str, Any],
|
|
251
|
+
execution_by_task: dict[str, dict[str, Any]],
|
|
252
|
+
manifest_status: str,
|
|
253
|
+
execution_available: bool,
|
|
254
|
+
) -> dict[str, Any]:
|
|
255
|
+
dependency_id = str(dependency["task_id"])
|
|
256
|
+
shared_dependency = next(
|
|
257
|
+
(
|
|
258
|
+
item
|
|
259
|
+
for item in source_snapshot.get("dependencies", [])
|
|
260
|
+
if isinstance(item, dict) and item.get("task_id") == dependency_id
|
|
261
|
+
),
|
|
262
|
+
None,
|
|
263
|
+
)
|
|
264
|
+
shared_status = (
|
|
265
|
+
shared_dependency.get("status")
|
|
266
|
+
if execution_available and isinstance(shared_dependency, dict)
|
|
267
|
+
else "pending"
|
|
268
|
+
if execution_available
|
|
269
|
+
else None
|
|
270
|
+
)
|
|
271
|
+
dependency_task_status = (
|
|
272
|
+
execution_by_task.get(dependency_id, {}).get("status", "not_started")
|
|
273
|
+
if execution_available
|
|
274
|
+
else None
|
|
275
|
+
)
|
|
276
|
+
dependency_type = str(dependency.get("type"))
|
|
277
|
+
if dependency_type == "contract":
|
|
278
|
+
status = "satisfied" if manifest_status == "READY" else "pending"
|
|
279
|
+
basis = "design-ready" if status == "satisfied" else "design-not-ready"
|
|
280
|
+
elif shared_status == "satisfied":
|
|
281
|
+
status = "satisfied"
|
|
282
|
+
basis = "recorded-evidence"
|
|
283
|
+
elif dependency_type == "hard" and dependency_task_status == "completed":
|
|
284
|
+
status = "satisfied"
|
|
285
|
+
basis = "dependency-task-completed"
|
|
286
|
+
else:
|
|
287
|
+
status = "pending"
|
|
288
|
+
basis = "pending-integration" if dependency_type == "integration" else "pending"
|
|
289
|
+
return {
|
|
290
|
+
"shared_status": shared_status,
|
|
291
|
+
"dependency_task_status": dependency_task_status,
|
|
292
|
+
"status": status,
|
|
293
|
+
"basis": basis,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def inspect_manifest(
|
|
298
|
+
spec_path: Path,
|
|
299
|
+
root: Path,
|
|
300
|
+
repo_paths: dict[str, str] | None = None,
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
"""Return the current-repository task catalog without resolving unrelated checkouts."""
|
|
303
|
+
|
|
304
|
+
resolved_spec_path = spec_path.resolve()
|
|
305
|
+
if not resolved_spec_path.is_file():
|
|
306
|
+
raise EasyDevSpecError(f"Spec file does not exist: {resolved_spec_path}")
|
|
307
|
+
try:
|
|
308
|
+
text = resolved_spec_path.read_text(encoding="utf-8")
|
|
309
|
+
report = validate_spec(text)
|
|
310
|
+
except (OSError, UnicodeError) as exc:
|
|
311
|
+
raise EasyDevSpecError(
|
|
312
|
+
f"Cannot read Spec as UTF-8: {resolved_spec_path}: {exc}"
|
|
313
|
+
) from exc
|
|
314
|
+
if report.protocol == "legacy":
|
|
315
|
+
return {
|
|
316
|
+
"inspection_mode": "manifest-only",
|
|
317
|
+
"protocol": "legacy",
|
|
318
|
+
"source_path": str(resolved_spec_path),
|
|
319
|
+
"source_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
|
320
|
+
"design_sha256": report.design_sha256,
|
|
321
|
+
"document_sha256": report.document_sha256,
|
|
322
|
+
"execution_revision": None,
|
|
323
|
+
"execution": None,
|
|
324
|
+
"selection_required": False,
|
|
325
|
+
"selected_task_ids": [],
|
|
326
|
+
"task_catalog": [],
|
|
327
|
+
"matched_repo_paths": {},
|
|
328
|
+
"unresolved_repositories": [],
|
|
329
|
+
"baseline_status": {},
|
|
330
|
+
"repository_bindings": [],
|
|
331
|
+
}
|
|
332
|
+
if report.protocol != "canonical-v1" or report.manifest is None or not report.ok:
|
|
333
|
+
details = "; ".join(
|
|
334
|
+
f"{issue.code}: {issue.message}" for issue in report.issues
|
|
335
|
+
) or "unknown validation failure"
|
|
336
|
+
raise EasyDevSpecError(f"Canonical Spec validation failed: {details}")
|
|
337
|
+
manifest = report.manifest
|
|
338
|
+
repositories = manifest["repositories"]
|
|
339
|
+
tasks = manifest["tasks"]
|
|
340
|
+
repository, binding = _current_repository_match(
|
|
341
|
+
root, repositories, repo_paths or {}
|
|
342
|
+
)
|
|
343
|
+
current_repo_id = str(repository["repo_id"])
|
|
344
|
+
repository_names = {
|
|
345
|
+
str(item["repo_id"]): str(item["name"]) for item in repositories
|
|
346
|
+
}
|
|
347
|
+
execution = report.execution
|
|
348
|
+
execution_by_task = {
|
|
349
|
+
str(snapshot.get("task_id")): snapshot
|
|
350
|
+
for snapshot in execution.get("tasks", [])
|
|
351
|
+
if isinstance(execution, dict)
|
|
352
|
+
and isinstance(snapshot, dict)
|
|
353
|
+
and isinstance(snapshot.get("task_id"), str)
|
|
354
|
+
} if isinstance(execution, dict) else {}
|
|
355
|
+
task_catalog: list[dict[str, Any]] = []
|
|
356
|
+
for task in tasks:
|
|
357
|
+
task_id = str(task["task_id"])
|
|
358
|
+
source_snapshot = execution_by_task.get(task_id, {})
|
|
359
|
+
dependencies = [
|
|
360
|
+
{
|
|
361
|
+
**dependency,
|
|
362
|
+
**_dependency_execution_summary(
|
|
363
|
+
dependency,
|
|
364
|
+
source_snapshot,
|
|
365
|
+
execution_by_task,
|
|
366
|
+
str(manifest["status"]),
|
|
367
|
+
isinstance(execution, dict),
|
|
368
|
+
),
|
|
369
|
+
}
|
|
370
|
+
for dependency in task.get("depends_on", [])
|
|
371
|
+
]
|
|
372
|
+
task_catalog.append(
|
|
373
|
+
{
|
|
374
|
+
"task_id": task_id,
|
|
375
|
+
"repo_id": task["repo_id"],
|
|
376
|
+
"repository_name": repository_names[str(task["repo_id"])],
|
|
377
|
+
"title": task["title"],
|
|
378
|
+
"status": task["status"],
|
|
379
|
+
"execution_status": (
|
|
380
|
+
execution_by_task.get(task_id, {}).get("status", "not_started")
|
|
381
|
+
if isinstance(execution, dict)
|
|
382
|
+
else None
|
|
383
|
+
),
|
|
384
|
+
"depends_on": dependencies,
|
|
385
|
+
"baseline_status": "not-inspected",
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
try:
|
|
389
|
+
source_path = portable_path(root, resolved_spec_path)
|
|
390
|
+
except EasyDevSpecError:
|
|
391
|
+
source_path = str(resolved_spec_path)
|
|
392
|
+
return {
|
|
393
|
+
"inspection_mode": "manifest-only",
|
|
394
|
+
"protocol": "canonical-v1",
|
|
395
|
+
"schema": SCHEMA,
|
|
396
|
+
"spec_id": manifest["spec_id"],
|
|
397
|
+
"revision": manifest["revision"],
|
|
398
|
+
"status": manifest["status"],
|
|
399
|
+
"title": manifest["title"],
|
|
400
|
+
"source_path": source_path,
|
|
401
|
+
"source_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
|
402
|
+
"design_sha256": report.design_sha256,
|
|
403
|
+
"document_sha256": report.document_sha256,
|
|
404
|
+
"execution_revision": (
|
|
405
|
+
execution.get("execution_revision") if isinstance(execution, dict) else None
|
|
406
|
+
),
|
|
407
|
+
"execution": execution,
|
|
408
|
+
"repository_match": {
|
|
409
|
+
"repo_id": current_repo_id,
|
|
410
|
+
"name": repository["name"],
|
|
411
|
+
"path": portable_path(root, binding["path"]),
|
|
412
|
+
"binding_source": binding["binding_source"],
|
|
413
|
+
"path_hint_status": binding["path_hint_status"],
|
|
414
|
+
},
|
|
415
|
+
"task_catalog": task_catalog,
|
|
416
|
+
"selection_required": True,
|
|
417
|
+
"selected_task_ids": [],
|
|
418
|
+
"matched_repo_paths": {},
|
|
419
|
+
"unresolved_repositories": [],
|
|
420
|
+
"baseline_status": {},
|
|
421
|
+
"repository_bindings": [],
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
156
425
|
def inspect_spec(
|
|
157
426
|
spec_path: Path,
|
|
158
427
|
root: Path,
|
|
@@ -168,6 +437,14 @@ def inspect_spec(
|
|
|
168
437
|
task_by_id = {task["task_id"]: task for task in tasks}
|
|
169
438
|
explicit_paths = repo_paths or {}
|
|
170
439
|
selected_task_set = set(selected_task_ids or [])
|
|
440
|
+
unknown_tasks = selected_task_set - set(task_by_id)
|
|
441
|
+
if unknown_tasks:
|
|
442
|
+
raise EasyDevSpecError(
|
|
443
|
+
"Unknown Canonical Spec tasks: " + ", ".join(sorted(unknown_tasks))
|
|
444
|
+
)
|
|
445
|
+
selected_repo_ids = {
|
|
446
|
+
str(task_by_id[task_id]["repo_id"]) for task_id in selected_task_set
|
|
447
|
+
}
|
|
171
448
|
matched_repo_paths: dict[str, str] = {}
|
|
172
449
|
unresolved_repositories: list[str] = []
|
|
173
450
|
baseline_status: dict[str, str] = {}
|
|
@@ -175,18 +452,15 @@ def inspect_spec(
|
|
|
175
452
|
|
|
176
453
|
for repository in repositories:
|
|
177
454
|
repo_id = str(repository["repo_id"])
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
]
|
|
184
|
-
matches = list(dict.fromkeys(matches))
|
|
185
|
-
if len(matches) != 1:
|
|
455
|
+
# task 已选定后只解析其所属仓库,未选仓库不得制造路径或 baseline 阻塞。
|
|
456
|
+
if selected_repo_ids and repo_id not in selected_repo_ids:
|
|
457
|
+
continue
|
|
458
|
+
binding = _resolve_repository_binding(root, repository, explicit_paths)
|
|
459
|
+
if binding is None:
|
|
186
460
|
unresolved_repositories.append(repo_id)
|
|
187
461
|
baseline_status[repo_id] = "baseline-unavailable"
|
|
188
462
|
continue
|
|
189
|
-
repository_path =
|
|
463
|
+
repository_path = binding["path"]
|
|
190
464
|
selected_paths = [
|
|
191
465
|
str(change["path"])
|
|
192
466
|
for change in changes
|
|
@@ -214,6 +488,8 @@ def inspect_spec(
|
|
|
214
488
|
"path": stored_path,
|
|
215
489
|
"baseline_commit": repository["baseline"]["commit"],
|
|
216
490
|
"baseline_status": status,
|
|
491
|
+
"binding_source": binding["binding_source"],
|
|
492
|
+
"path_hint_status": binding["path_hint_status"],
|
|
217
493
|
}
|
|
218
494
|
)
|
|
219
495
|
|
|
@@ -234,6 +510,7 @@ def inspect_spec(
|
|
|
234
510
|
source_path = str(resolved_spec_path)
|
|
235
511
|
execution = report.execution
|
|
236
512
|
return {
|
|
513
|
+
"inspection_mode": "selected" if selected_task_set else "full",
|
|
237
514
|
"protocol": "canonical-v1",
|
|
238
515
|
"schema": SCHEMA,
|
|
239
516
|
"spec_id": manifest["spec_id"],
|
|
@@ -255,6 +532,7 @@ def inspect_spec(
|
|
|
255
532
|
"tests": manifest["tests"],
|
|
256
533
|
"contracts": manifest["contracts"],
|
|
257
534
|
"dependency_edges": dependency_edges,
|
|
535
|
+
"selected_task_ids": sorted(selected_task_set),
|
|
258
536
|
"matched_repo_paths": matched_repo_paths,
|
|
259
537
|
"unresolved_repositories": unresolved_repositories,
|
|
260
538
|
"baseline_status": baseline_status,
|
|
@@ -376,29 +654,31 @@ def select_tasks(
|
|
|
376
654
|
if evidence is None and dependency_target_counts[dependency_id] == 1:
|
|
377
655
|
evidence = evidence_by_dependency.get(dependency_id)
|
|
378
656
|
source_snapshot = execution_by_task.get(source_task_id, {})
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
),
|
|
385
|
-
None,
|
|
657
|
+
execution_fact = _dependency_execution_summary(
|
|
658
|
+
dependency,
|
|
659
|
+
source_snapshot,
|
|
660
|
+
execution_by_task,
|
|
661
|
+
str(inspection.get("status")),
|
|
662
|
+
isinstance(execution, dict),
|
|
386
663
|
)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
and shared_dependency.get("status") == "satisfied"
|
|
664
|
+
shared_satisfied = execution_fact["shared_status"] == "satisfied"
|
|
665
|
+
dependency_completed = (
|
|
666
|
+
execution_fact["dependency_task_status"] == "completed"
|
|
391
667
|
)
|
|
392
|
-
|
|
668
|
+
basis = execution_fact.get("basis")
|
|
393
669
|
if dependency_type == "hard":
|
|
394
670
|
satisfied = shared_satisfied or dependency_completed or bool(evidence)
|
|
671
|
+
if execution_fact["status"] != "satisfied" and evidence:
|
|
672
|
+
basis = "manual-evidence"
|
|
395
673
|
if dependency_id not in selected_set and not satisfied:
|
|
396
674
|
missing_hard.append(f"{source_task_id}->{dependency_id}")
|
|
397
675
|
elif dependency_type == "contract":
|
|
398
|
-
satisfied =
|
|
676
|
+
satisfied = execution_fact["status"] == "satisfied"
|
|
399
677
|
evidence = evidence or "canonical-spec-ready-contract"
|
|
400
678
|
else:
|
|
401
679
|
satisfied = shared_satisfied or bool(evidence)
|
|
680
|
+
if execution_fact["status"] != "satisfied" and evidence:
|
|
681
|
+
basis = "manual-evidence"
|
|
402
682
|
dependency_records.append(
|
|
403
683
|
{
|
|
404
684
|
"source_task_id": source_task_id,
|
|
@@ -406,11 +686,11 @@ def select_tasks(
|
|
|
406
686
|
"dependency_type": dependency_type,
|
|
407
687
|
"required_evidence": dependency["required_evidence"],
|
|
408
688
|
"status": "satisfied" if satisfied else "pending",
|
|
409
|
-
"shared_status":
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
689
|
+
"shared_status": execution_fact["shared_status"],
|
|
690
|
+
"dependency_task_status": execution_fact[
|
|
691
|
+
"dependency_task_status"
|
|
692
|
+
],
|
|
693
|
+
"basis": basis,
|
|
414
694
|
**({"evidence": evidence} if evidence else {}),
|
|
415
695
|
}
|
|
416
696
|
)
|
|
@@ -453,6 +733,7 @@ def inspection_summary(inspection: dict[str, Any]) -> dict[str, Any]:
|
|
|
453
733
|
"""Return the discovery surface without unrelated implementation routing objects."""
|
|
454
734
|
|
|
455
735
|
keys = (
|
|
736
|
+
"inspection_mode",
|
|
456
737
|
"protocol",
|
|
457
738
|
"schema",
|
|
458
739
|
"spec_id",
|
|
@@ -467,26 +748,84 @@ def inspection_summary(inspection: dict[str, Any]) -> dict[str, Any]:
|
|
|
467
748
|
"repositories",
|
|
468
749
|
"tasks",
|
|
469
750
|
"dependency_edges",
|
|
751
|
+
"selected_task_ids",
|
|
752
|
+
"repository_match",
|
|
753
|
+
"task_catalog",
|
|
754
|
+
"selection_required",
|
|
470
755
|
"matched_repo_paths",
|
|
471
756
|
"unresolved_repositories",
|
|
472
757
|
"baseline_status",
|
|
473
758
|
"repository_bindings",
|
|
474
759
|
)
|
|
475
|
-
summary = {key: inspection[key] for key in keys}
|
|
760
|
+
summary = {key: inspection[key] for key in keys if key in inspection}
|
|
476
761
|
execution = inspection.get("execution")
|
|
762
|
+
relevant_execution_task_ids: set[str] | None = None
|
|
763
|
+
if inspection.get("inspection_mode") == "selected":
|
|
764
|
+
selected_task_ids = set(inspection.get("selected_task_ids", []))
|
|
765
|
+
selected_tasks = [
|
|
766
|
+
task
|
|
767
|
+
for task in inspection.get("tasks", [])
|
|
768
|
+
if str(task.get("task_id")) in selected_task_ids
|
|
769
|
+
]
|
|
770
|
+
selected_repo_ids = {str(task.get("repo_id")) for task in selected_tasks}
|
|
771
|
+
execution_by_task = {
|
|
772
|
+
str(task.get("task_id")): task
|
|
773
|
+
for task in execution.get("tasks", [])
|
|
774
|
+
if isinstance(execution, dict)
|
|
775
|
+
and isinstance(task, dict)
|
|
776
|
+
and isinstance(task.get("task_id"), str)
|
|
777
|
+
} if isinstance(execution, dict) else {}
|
|
778
|
+
selected_edges: list[dict[str, Any]] = []
|
|
779
|
+
for edge in inspection.get("dependency_edges", []):
|
|
780
|
+
source_task_id = str(edge.get("source_task_id"))
|
|
781
|
+
if source_task_id not in selected_task_ids:
|
|
782
|
+
continue
|
|
783
|
+
selected_edges.append(
|
|
784
|
+
{
|
|
785
|
+
**edge,
|
|
786
|
+
**_dependency_execution_summary(
|
|
787
|
+
{
|
|
788
|
+
"task_id": edge.get("task_id"),
|
|
789
|
+
"type": edge.get("dependency_type"),
|
|
790
|
+
},
|
|
791
|
+
execution_by_task.get(source_task_id, {}),
|
|
792
|
+
execution_by_task,
|
|
793
|
+
str(inspection.get("status")),
|
|
794
|
+
isinstance(execution, dict),
|
|
795
|
+
),
|
|
796
|
+
}
|
|
797
|
+
)
|
|
798
|
+
summary["repositories"] = [
|
|
799
|
+
repository
|
|
800
|
+
for repository in inspection.get("repositories", [])
|
|
801
|
+
if str(repository.get("repo_id")) in selected_repo_ids
|
|
802
|
+
]
|
|
803
|
+
summary["tasks"] = selected_tasks
|
|
804
|
+
summary["dependency_edges"] = selected_edges
|
|
805
|
+
relevant_execution_task_ids = selected_task_ids | {
|
|
806
|
+
str(edge.get("task_id")) for edge in selected_edges
|
|
807
|
+
}
|
|
477
808
|
if isinstance(execution, dict):
|
|
809
|
+
execution_keys = [
|
|
810
|
+
"schema",
|
|
811
|
+
"spec_id",
|
|
812
|
+
"design_revision",
|
|
813
|
+
"design_sha256",
|
|
814
|
+
"execution_revision",
|
|
815
|
+
"updated_at",
|
|
816
|
+
]
|
|
817
|
+
if inspection.get("inspection_mode") != "manifest-only":
|
|
818
|
+
execution_keys.append("tasks")
|
|
478
819
|
summary["execution"] = {
|
|
479
820
|
key: execution.get(key)
|
|
480
|
-
for key in
|
|
481
|
-
"schema",
|
|
482
|
-
"spec_id",
|
|
483
|
-
"design_revision",
|
|
484
|
-
"design_sha256",
|
|
485
|
-
"execution_revision",
|
|
486
|
-
"updated_at",
|
|
487
|
-
"tasks",
|
|
488
|
-
)
|
|
821
|
+
for key in execution_keys
|
|
489
822
|
}
|
|
823
|
+
if relevant_execution_task_ids is not None:
|
|
824
|
+
summary["execution"]["tasks"] = [
|
|
825
|
+
task
|
|
826
|
+
for task in execution.get("tasks", [])
|
|
827
|
+
if str(task.get("task_id")) in relevant_execution_task_ids
|
|
828
|
+
]
|
|
490
829
|
else:
|
|
491
830
|
summary["execution"] = None
|
|
492
831
|
return summary
|