easy-coding-harness 0.9.1 → 0.10.0-beta.1
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 +31 -0
- package/README.md +24 -3
- package/dist/cli.js +102 -13
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +6 -0
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +28 -5
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +73 -1
- package/templates/common/skills/ec-config/SKILL.md +54 -0
- package/templates/common/skills/ec-git/SKILL.md +4 -0
- package/templates/common/skills/ec-implementing/SKILL.md +25 -3
- package/templates/common/skills/ec-memory/SKILL.md +4 -0
- package/templates/common/skills/ec-reviewing/SKILL.md +15 -0
- package/templates/common/skills/ec-task-management/SKILL.md +13 -31
- package/templates/common/skills/ec-verification/SKILL.md +69 -2
- package/templates/common/skills/ec-workflow/SKILL.md +39 -4
- package/templates/main-constraint/AGENTS.md.tpl +10 -3
- package/templates/main-constraint/CLAUDE.md.tpl +10 -3
- package/templates/runtime/templates/dev-spec-skeleton.md +12 -5
- package/templates/runtime/tools/easy_coding_java_coverage.py +317 -0
- package/templates/shared-hooks/easy_coding_state.py +1655 -60
- package/templates/shared-hooks/easy_dev_spec.py +417 -0
- package/templates/shared-hooks/easy_dev_spec_protocol.py +1971 -0
- package/templates/shared-hooks/inject-subagent-context.py +5 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Easy Dev Spec Canonical v1 parser and repository binding helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Iterable
|
|
10
|
+
|
|
11
|
+
from easy_dev_spec_protocol import SCHEMA, select_scope, validate_spec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
UPSTREAM_PROTOCOL_COMMIT = "7eb9b64cdb4c8c338c5871c3c759526f2c78fb8e"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EasyDevSpecError(ValueError):
|
|
18
|
+
"""Canonical Spec 无法安全消费时抛出。"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read_spec(
|
|
22
|
+
path: Path,
|
|
23
|
+
require_ready: bool = False,
|
|
24
|
+
) -> tuple[str, dict[str, Any], dict[str, str]]:
|
|
25
|
+
if not path.is_file():
|
|
26
|
+
raise EasyDevSpecError(f"Spec file does not exist: {path}")
|
|
27
|
+
try:
|
|
28
|
+
text = path.read_text(encoding="utf-8")
|
|
29
|
+
report = validate_spec(text, require_ready=require_ready)
|
|
30
|
+
except (OSError, UnicodeError) as exc:
|
|
31
|
+
raise EasyDevSpecError(f"Cannot read Spec as UTF-8: {path}: {exc}") from exc
|
|
32
|
+
if report.protocol == "legacy":
|
|
33
|
+
raise EasyDevSpecError("Dev Spec does not contain a Canonical v1 manifest")
|
|
34
|
+
if report.protocol != "canonical-v1" or report.manifest is None or not report.ok:
|
|
35
|
+
details = "; ".join(
|
|
36
|
+
f"{issue.code}: {issue.message}" for issue in report.issues
|
|
37
|
+
) or "unknown validation failure"
|
|
38
|
+
raise EasyDevSpecError(f"Canonical Spec validation failed: {details}")
|
|
39
|
+
sections = {
|
|
40
|
+
section_id: section.content
|
|
41
|
+
for section_id, section in report.sections.items()
|
|
42
|
+
}
|
|
43
|
+
return text, report.manifest, sections
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_remote(remote: str) -> str:
|
|
47
|
+
value = remote.strip().removesuffix("/").removesuffix(".git")
|
|
48
|
+
if value.startswith("git@") and ":" in value:
|
|
49
|
+
host, path = value[4:].split(":", 1)
|
|
50
|
+
return f"{host.lower()}/{path.removesuffix('.git').strip('/')}"
|
|
51
|
+
match = re.match(r"^(?:https?|ssh|git)://(?:[^@/]+@)?([^/]+)/(.+)$", value)
|
|
52
|
+
if match:
|
|
53
|
+
return f"{match.group(1).lower()}/{match.group(2).removesuffix('.git').strip('/')}"
|
|
54
|
+
if value.startswith("file://"):
|
|
55
|
+
return str(Path(value[7:]).resolve())
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _git(repository: Path, *args: str) -> subprocess.CompletedProcess[str] | None:
|
|
60
|
+
try:
|
|
61
|
+
return subprocess.run(
|
|
62
|
+
["git", "-C", str(repository), *args],
|
|
63
|
+
stdout=subprocess.PIPE,
|
|
64
|
+
stderr=subprocess.PIPE,
|
|
65
|
+
text=True,
|
|
66
|
+
check=False,
|
|
67
|
+
)
|
|
68
|
+
except OSError:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def repository_remotes(repository: Path) -> list[str]:
|
|
73
|
+
result = _git(repository, "remote", "-v")
|
|
74
|
+
if result is None or result.returncode != 0:
|
|
75
|
+
return []
|
|
76
|
+
remotes: list[str] = []
|
|
77
|
+
for line in result.stdout.splitlines():
|
|
78
|
+
parts = line.split()
|
|
79
|
+
if len(parts) >= 2:
|
|
80
|
+
normalized = normalize_remote(parts[1])
|
|
81
|
+
if normalized and normalized not in remotes:
|
|
82
|
+
remotes.append(normalized)
|
|
83
|
+
return remotes
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def classify_baseline(repository: Path, commit: str, paths: Iterable[str]) -> str:
|
|
87
|
+
unique_paths = sorted(set(paths))
|
|
88
|
+
if unique_paths:
|
|
89
|
+
worktree = _git(repository, "status", "--porcelain", "--", *unique_paths)
|
|
90
|
+
if worktree is None or worktree.returncode != 0:
|
|
91
|
+
return "baseline-unavailable"
|
|
92
|
+
if worktree.stdout.strip():
|
|
93
|
+
return "scope-drifted"
|
|
94
|
+
head = _git(repository, "rev-parse", "HEAD")
|
|
95
|
+
if head is None or head.returncode != 0:
|
|
96
|
+
return "baseline-unavailable"
|
|
97
|
+
if head.stdout.strip().lower() == commit.lower():
|
|
98
|
+
return "exact"
|
|
99
|
+
available = _git(repository, "cat-file", "-e", commit + "^{commit}")
|
|
100
|
+
if available is None or available.returncode != 0:
|
|
101
|
+
return "baseline-unavailable"
|
|
102
|
+
if not unique_paths:
|
|
103
|
+
return "scope-unchanged"
|
|
104
|
+
changed = _git(repository, "diff", "--quiet", commit, "HEAD", "--", *unique_paths)
|
|
105
|
+
if changed is None or changed.returncode not in {0, 1}:
|
|
106
|
+
return "baseline-unavailable"
|
|
107
|
+
return "scope-unchanged" if changed.returncode == 0 else "scope-drifted"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def portable_path(root: Path, path: Path) -> str:
|
|
111
|
+
resolved_root = root.resolve()
|
|
112
|
+
resolved_path = path.resolve()
|
|
113
|
+
try:
|
|
114
|
+
return resolved_path.relative_to(resolved_root).as_posix()
|
|
115
|
+
except ValueError:
|
|
116
|
+
try:
|
|
117
|
+
return Path("..", resolved_path.relative_to(resolved_root.parent)).as_posix()
|
|
118
|
+
except ValueError:
|
|
119
|
+
raise EasyDevSpecError(
|
|
120
|
+
f"Path cannot be stored portably relative to the project root: {resolved_path}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _candidate_repository_paths(
|
|
125
|
+
root: Path,
|
|
126
|
+
repository: dict[str, Any],
|
|
127
|
+
explicit_paths: dict[str, str],
|
|
128
|
+
) -> list[Path]:
|
|
129
|
+
repo_id = str(repository["repo_id"])
|
|
130
|
+
candidates: list[Path] = []
|
|
131
|
+
explicit = explicit_paths.get(repo_id)
|
|
132
|
+
if explicit:
|
|
133
|
+
path = Path(explicit)
|
|
134
|
+
# An explicit binding is authoritative. Falling back to path_hint/root would make a
|
|
135
|
+
# mistyped --repo-path appear valid while silently binding a different checkout.
|
|
136
|
+
return [(path if path.is_absolute() else root / path).resolve()]
|
|
137
|
+
hint = Path(str(repository.get("path_hint") or ""))
|
|
138
|
+
if str(hint):
|
|
139
|
+
candidates.append(hint if hint.is_absolute() else root / hint)
|
|
140
|
+
candidates.append(root)
|
|
141
|
+
unique: list[Path] = []
|
|
142
|
+
for candidate in candidates:
|
|
143
|
+
resolved = candidate.resolve()
|
|
144
|
+
if resolved not in unique:
|
|
145
|
+
unique.append(resolved)
|
|
146
|
+
return unique
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def inspect_spec(
|
|
150
|
+
spec_path: Path,
|
|
151
|
+
root: Path,
|
|
152
|
+
repo_paths: dict[str, str] | None = None,
|
|
153
|
+
selected_task_ids: Iterable[str] | None = None,
|
|
154
|
+
) -> dict[str, Any]:
|
|
155
|
+
resolved_spec_path = spec_path.resolve()
|
|
156
|
+
text, manifest, sections = _read_spec(resolved_spec_path)
|
|
157
|
+
repositories = manifest["repositories"]
|
|
158
|
+
tasks = manifest["tasks"]
|
|
159
|
+
changes = manifest["changes"]
|
|
160
|
+
tests = manifest["tests"]
|
|
161
|
+
task_by_id = {task["task_id"]: task for task in tasks}
|
|
162
|
+
explicit_paths = repo_paths or {}
|
|
163
|
+
selected_task_set = set(selected_task_ids or [])
|
|
164
|
+
matched_repo_paths: dict[str, str] = {}
|
|
165
|
+
unresolved_repositories: list[str] = []
|
|
166
|
+
baseline_status: dict[str, str] = {}
|
|
167
|
+
repository_bindings: list[dict[str, Any]] = []
|
|
168
|
+
|
|
169
|
+
for repository in repositories:
|
|
170
|
+
repo_id = str(repository["repo_id"])
|
|
171
|
+
expected_remotes = {normalize_remote(remote) for remote in repository["remote_urls"]}
|
|
172
|
+
matches = [
|
|
173
|
+
candidate
|
|
174
|
+
for candidate in _candidate_repository_paths(root, repository, explicit_paths)
|
|
175
|
+
if candidate.is_dir() and expected_remotes.intersection(repository_remotes(candidate))
|
|
176
|
+
]
|
|
177
|
+
matches = list(dict.fromkeys(matches))
|
|
178
|
+
if len(matches) != 1:
|
|
179
|
+
unresolved_repositories.append(repo_id)
|
|
180
|
+
baseline_status[repo_id] = "baseline-unavailable"
|
|
181
|
+
continue
|
|
182
|
+
repository_path = matches[0]
|
|
183
|
+
selected_paths = [
|
|
184
|
+
str(change["path"])
|
|
185
|
+
for change in changes
|
|
186
|
+
if change.get("repo_id") == repo_id
|
|
187
|
+
and (not selected_task_set or change.get("task_id") in selected_task_set)
|
|
188
|
+
]
|
|
189
|
+
selected_paths.extend(
|
|
190
|
+
str(test["file"])
|
|
191
|
+
for test in tests
|
|
192
|
+
if task_by_id.get(str(test.get("task_id")), {}).get("repo_id") == repo_id
|
|
193
|
+
and (not selected_task_set or test.get("task_id") in selected_task_set)
|
|
194
|
+
)
|
|
195
|
+
status = classify_baseline(
|
|
196
|
+
repository_path,
|
|
197
|
+
str(repository["baseline"]["commit"]),
|
|
198
|
+
selected_paths,
|
|
199
|
+
)
|
|
200
|
+
stored_path = portable_path(root, repository_path)
|
|
201
|
+
matched_repo_paths[repo_id] = stored_path
|
|
202
|
+
baseline_status[repo_id] = status
|
|
203
|
+
repository_bindings.append(
|
|
204
|
+
{
|
|
205
|
+
"repo_id": repo_id,
|
|
206
|
+
"name": repository["name"],
|
|
207
|
+
"path": stored_path,
|
|
208
|
+
"baseline_commit": repository["baseline"]["commit"],
|
|
209
|
+
"baseline_status": status,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
dependency_edges = [
|
|
214
|
+
{
|
|
215
|
+
"source_task_id": task["task_id"],
|
|
216
|
+
"task_id": dependency["task_id"],
|
|
217
|
+
"dependency_type": dependency["type"],
|
|
218
|
+
"required_evidence": dependency["required_evidence"],
|
|
219
|
+
}
|
|
220
|
+
for task in tasks
|
|
221
|
+
for dependency in task.get("depends_on", [])
|
|
222
|
+
]
|
|
223
|
+
try:
|
|
224
|
+
source_path = portable_path(root, resolved_spec_path)
|
|
225
|
+
except EasyDevSpecError:
|
|
226
|
+
# inspect 是纯只读命令,允许展示项目外输入;创建任务时仍会拒绝项目外来源。
|
|
227
|
+
source_path = str(resolved_spec_path)
|
|
228
|
+
return {
|
|
229
|
+
"protocol": "canonical-v1",
|
|
230
|
+
"schema": SCHEMA,
|
|
231
|
+
"spec_id": manifest["spec_id"],
|
|
232
|
+
"revision": manifest["revision"],
|
|
233
|
+
"status": manifest["status"],
|
|
234
|
+
"title": manifest["title"],
|
|
235
|
+
"source_path": source_path,
|
|
236
|
+
"source_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
|
237
|
+
"repositories": repositories,
|
|
238
|
+
"tasks": tasks,
|
|
239
|
+
"changes": changes,
|
|
240
|
+
"steps": manifest["steps"],
|
|
241
|
+
"tests": manifest["tests"],
|
|
242
|
+
"contracts": manifest["contracts"],
|
|
243
|
+
"dependency_edges": dependency_edges,
|
|
244
|
+
"matched_repo_paths": matched_repo_paths,
|
|
245
|
+
"unresolved_repositories": unresolved_repositories,
|
|
246
|
+
"baseline_status": baseline_status,
|
|
247
|
+
"repository_bindings": repository_bindings,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def select_consumption_scopes(
|
|
252
|
+
spec_path: Path,
|
|
253
|
+
root: Path,
|
|
254
|
+
selected_task_ids: Iterable[str],
|
|
255
|
+
) -> dict[str, Any]:
|
|
256
|
+
"""Return final-protocol consumption closures grouped by repository."""
|
|
257
|
+
|
|
258
|
+
resolved_spec_path = spec_path.resolve()
|
|
259
|
+
text, manifest, _ = _read_spec(resolved_spec_path, require_ready=True)
|
|
260
|
+
selected_ids = list(dict.fromkeys(selected_task_ids))
|
|
261
|
+
if not selected_ids:
|
|
262
|
+
raise EasyDevSpecError("At least one Canonical Spec task must be selected")
|
|
263
|
+
|
|
264
|
+
task_by_id = {task["task_id"]: task for task in manifest["tasks"]}
|
|
265
|
+
unknown = [task_id for task_id in selected_ids if task_id not in task_by_id]
|
|
266
|
+
if unknown:
|
|
267
|
+
raise EasyDevSpecError("Unknown Canonical Spec tasks: " + ", ".join(unknown))
|
|
268
|
+
|
|
269
|
+
selected_by_repo: dict[str, list[str]] = {}
|
|
270
|
+
for task_id in selected_ids:
|
|
271
|
+
repo_id = str(task_by_id[task_id]["repo_id"])
|
|
272
|
+
selected_by_repo.setdefault(repo_id, []).append(task_id)
|
|
273
|
+
|
|
274
|
+
scopes: list[dict[str, Any]] = []
|
|
275
|
+
for repository in manifest["repositories"]:
|
|
276
|
+
repo_id = str(repository["repo_id"])
|
|
277
|
+
repo_task_ids = selected_by_repo.get(repo_id)
|
|
278
|
+
if not repo_task_ids:
|
|
279
|
+
continue
|
|
280
|
+
try:
|
|
281
|
+
scope = select_scope(
|
|
282
|
+
text,
|
|
283
|
+
repo_id,
|
|
284
|
+
repo_task_ids,
|
|
285
|
+
output_format="json",
|
|
286
|
+
)
|
|
287
|
+
except ValueError as exc:
|
|
288
|
+
raise EasyDevSpecError(f"Cannot select consumption scope for {repo_id}: {exc}") from exc
|
|
289
|
+
if not isinstance(scope, dict):
|
|
290
|
+
raise EasyDevSpecError(f"Canonical selector returned an invalid scope for {repo_id}")
|
|
291
|
+
scopes.append(scope)
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
source_path = portable_path(root, resolved_spec_path)
|
|
295
|
+
except EasyDevSpecError:
|
|
296
|
+
source_path = str(resolved_spec_path)
|
|
297
|
+
source_sha256 = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
298
|
+
for scope in scopes:
|
|
299
|
+
scope["source_path"] = source_path
|
|
300
|
+
scope["source_sha256"] = source_sha256
|
|
301
|
+
return {
|
|
302
|
+
"protocol": "canonical-v1",
|
|
303
|
+
"schema": SCHEMA,
|
|
304
|
+
"spec_id": manifest["spec_id"],
|
|
305
|
+
"revision": manifest["revision"],
|
|
306
|
+
"status": manifest["status"],
|
|
307
|
+
"source_path": source_path,
|
|
308
|
+
"source_sha256": source_sha256,
|
|
309
|
+
"selected_task_ids": selected_ids,
|
|
310
|
+
"scopes": scopes,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def select_tasks(
|
|
315
|
+
inspection: dict[str, Any],
|
|
316
|
+
selected_task_ids: Iterable[str],
|
|
317
|
+
dependency_evidence: dict[str, str] | None = None,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
selected_ids = list(dict.fromkeys(selected_task_ids))
|
|
320
|
+
if not selected_ids:
|
|
321
|
+
raise EasyDevSpecError("At least one Canonical Spec task must be selected")
|
|
322
|
+
task_by_id = {task["task_id"]: task for task in inspection["tasks"]}
|
|
323
|
+
unknown = [task_id for task_id in selected_ids if task_id not in task_by_id]
|
|
324
|
+
if unknown:
|
|
325
|
+
raise EasyDevSpecError("Unknown Canonical Spec tasks: " + ", ".join(unknown))
|
|
326
|
+
not_ready = [task_id for task_id in selected_ids if task_by_id[task_id].get("status") != "READY"]
|
|
327
|
+
if inspection.get("status") != "READY" or not_ready:
|
|
328
|
+
raise EasyDevSpecError(
|
|
329
|
+
"Canonical Spec and all selected tasks must be READY: " + ", ".join(not_ready)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
selected_set = set(selected_ids)
|
|
333
|
+
evidence_by_dependency = dependency_evidence or {}
|
|
334
|
+
dependency_target_counts: dict[str, int] = {}
|
|
335
|
+
for source_task_id in selected_ids:
|
|
336
|
+
for dependency in task_by_id[source_task_id].get("depends_on", []):
|
|
337
|
+
dependency_id = str(dependency["task_id"])
|
|
338
|
+
dependency_target_counts[dependency_id] = dependency_target_counts.get(dependency_id, 0) + 1
|
|
339
|
+
dependency_records: list[dict[str, Any]] = []
|
|
340
|
+
missing_hard: list[str] = []
|
|
341
|
+
for source_task_id in selected_ids:
|
|
342
|
+
for dependency in task_by_id[source_task_id].get("depends_on", []):
|
|
343
|
+
dependency_id = str(dependency["task_id"])
|
|
344
|
+
dependency_type = str(dependency["type"])
|
|
345
|
+
edge_key = f"{source_task_id}->{dependency_id}"
|
|
346
|
+
evidence = evidence_by_dependency.get(edge_key)
|
|
347
|
+
if evidence is None and dependency_target_counts[dependency_id] == 1:
|
|
348
|
+
evidence = evidence_by_dependency.get(dependency_id)
|
|
349
|
+
if dependency_type == "hard":
|
|
350
|
+
satisfied = dependency_id in selected_set or bool(evidence)
|
|
351
|
+
if not satisfied:
|
|
352
|
+
missing_hard.append(f"{source_task_id}->{dependency_id}")
|
|
353
|
+
elif dependency_type == "contract":
|
|
354
|
+
satisfied = True
|
|
355
|
+
evidence = evidence or "canonical-spec-ready-contract"
|
|
356
|
+
else:
|
|
357
|
+
satisfied = bool(evidence)
|
|
358
|
+
dependency_records.append(
|
|
359
|
+
{
|
|
360
|
+
"source_task_id": source_task_id,
|
|
361
|
+
"task_id": dependency_id,
|
|
362
|
+
"dependency_type": dependency_type,
|
|
363
|
+
"required_evidence": dependency["required_evidence"],
|
|
364
|
+
"status": "satisfied" if satisfied else "pending",
|
|
365
|
+
**({"evidence": evidence} if evidence else {}),
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
if missing_hard:
|
|
369
|
+
raise EasyDevSpecError(
|
|
370
|
+
"Selected tasks omit hard dependencies without evidence: " + ", ".join(missing_hard)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
selected_tasks = [task_by_id[task_id] for task_id in selected_ids]
|
|
374
|
+
selected_repo_ids = list(dict.fromkeys(task["repo_id"] for task in selected_tasks))
|
|
375
|
+
unresolved = [
|
|
376
|
+
repo_id for repo_id in selected_repo_ids if repo_id not in inspection["matched_repo_paths"]
|
|
377
|
+
]
|
|
378
|
+
if unresolved:
|
|
379
|
+
raise EasyDevSpecError("Selected tasks have unresolved repository paths: " + ", ".join(unresolved))
|
|
380
|
+
return {
|
|
381
|
+
"selected_task_ids": selected_ids,
|
|
382
|
+
"selected_tasks": selected_tasks,
|
|
383
|
+
"selected_repo_ids": selected_repo_ids,
|
|
384
|
+
"selected_changes": [
|
|
385
|
+
change for change in inspection["changes"] if change.get("task_id") in selected_set
|
|
386
|
+
],
|
|
387
|
+
"selected_steps": [
|
|
388
|
+
step for step in inspection["steps"] if step.get("task_id") in selected_set
|
|
389
|
+
],
|
|
390
|
+
"selected_tests": [
|
|
391
|
+
test for test in inspection["tests"] if test.get("task_id") in selected_set
|
|
392
|
+
],
|
|
393
|
+
"dependency_records": dependency_records,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def inspection_summary(inspection: dict[str, Any]) -> dict[str, Any]:
|
|
398
|
+
"""Return the discovery surface without unrelated implementation routing objects."""
|
|
399
|
+
|
|
400
|
+
keys = (
|
|
401
|
+
"protocol",
|
|
402
|
+
"schema",
|
|
403
|
+
"spec_id",
|
|
404
|
+
"revision",
|
|
405
|
+
"status",
|
|
406
|
+
"title",
|
|
407
|
+
"source_path",
|
|
408
|
+
"source_sha256",
|
|
409
|
+
"repositories",
|
|
410
|
+
"tasks",
|
|
411
|
+
"dependency_edges",
|
|
412
|
+
"matched_repo_paths",
|
|
413
|
+
"unresolved_repositories",
|
|
414
|
+
"baseline_status",
|
|
415
|
+
"repository_bindings",
|
|
416
|
+
)
|
|
417
|
+
return {key: inspection[key] for key in keys}
|