easy-coding-harness 0.10.0-beta.5 → 0.10.0-beta.7
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 +33 -0
- package/README.md +17 -10
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +10 -0
- package/templates/claude/agents/ec-reviewer.md +6 -1
- package/templates/codex/agents/ec-implementer.toml +10 -0
- package/templates/codex/agents/ec-reviewer.toml +6 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +8 -1
- package/templates/common/skills/ec-analysis/SKILL.md +74 -21
- package/templates/common/skills/ec-implementing/SKILL.md +27 -2
- package/templates/common/skills/ec-reviewing/SKILL.md +11 -1
- 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/qoder/agents/ec-implementer.md +10 -0
- package/templates/qoder/agents/ec-reviewer.md +6 -1
- package/templates/shared-hooks/easy_coding_state.py +126 -48
- package/templates/shared-hooks/easy_dev_spec.py +385 -46
|
@@ -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
|