easy-coding-harness 0.10.0-beta.4 → 0.10.0-beta.5

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.
@@ -11,7 +11,9 @@ from typing import Any, Iterable
11
11
  from easy_dev_spec_protocol import SCHEMA, select_scope, validate_spec
12
12
 
13
13
 
14
- UPSTREAM_PROTOCOL_COMMIT = "7eb9b64cdb4c8c338c5871c3c759526f2c78fb8e"
14
+ UPSTREAM_PROTOCOL_COMMIT = "8239a5befae08b41da43b7cfbf41acf07e487d04"
15
+ UPSTREAM_PROTOCOL_SHA256 = "a6016f04b4ce18794038ebcdbcab6e400a8a08aa2929a3e777c2b35ee3f7e7a1"
16
+ UPSTREAM_EXECUTION_WRITER_SHA256 = "17f03314adce341269e2689aa41bb7bb29c236979be530a373fef58fe88a2524"
15
17
 
16
18
 
17
19
  class EasyDevSpecError(ValueError):
@@ -21,12 +23,17 @@ class EasyDevSpecError(ValueError):
21
23
  def _read_spec(
22
24
  path: Path,
23
25
  require_ready: bool = False,
24
- ) -> tuple[str, dict[str, Any], dict[str, str]]:
26
+ require_execution: bool = False,
27
+ ) -> tuple[str, dict[str, Any], dict[str, str], Any]:
25
28
  if not path.is_file():
26
29
  raise EasyDevSpecError(f"Spec file does not exist: {path}")
27
30
  try:
28
31
  text = path.read_text(encoding="utf-8")
29
- report = validate_spec(text, require_ready=require_ready)
32
+ report = validate_spec(
33
+ text,
34
+ require_ready=require_ready,
35
+ require_execution=require_execution,
36
+ )
30
37
  except (OSError, UnicodeError) as exc:
31
38
  raise EasyDevSpecError(f"Cannot read Spec as UTF-8: {path}: {exc}") from exc
32
39
  if report.protocol == "legacy":
@@ -40,7 +47,7 @@ def _read_spec(
40
47
  section_id: section.content
41
48
  for section_id, section in report.sections.items()
42
49
  }
43
- return text, report.manifest, sections
50
+ return text, report.manifest, sections, report
44
51
 
45
52
 
46
53
  def normalize_remote(remote: str) -> str:
@@ -153,7 +160,7 @@ def inspect_spec(
153
160
  selected_task_ids: Iterable[str] | None = None,
154
161
  ) -> dict[str, Any]:
155
162
  resolved_spec_path = spec_path.resolve()
156
- text, manifest, sections = _read_spec(resolved_spec_path)
163
+ text, manifest, sections, report = _read_spec(resolved_spec_path)
157
164
  repositories = manifest["repositories"]
158
165
  tasks = manifest["tasks"]
159
166
  changes = manifest["changes"]
@@ -223,8 +230,9 @@ def inspect_spec(
223
230
  try:
224
231
  source_path = portable_path(root, resolved_spec_path)
225
232
  except EasyDevSpecError:
226
- # inspect 是纯只读命令,允许展示项目外输入;创建任务时仍会拒绝项目外来源。
233
+ # Explicit project-external input remains readable and is stored as an absolute locator.
227
234
  source_path = str(resolved_spec_path)
235
+ execution = report.execution
228
236
  return {
229
237
  "protocol": "canonical-v1",
230
238
  "schema": SCHEMA,
@@ -234,6 +242,12 @@ def inspect_spec(
234
242
  "title": manifest["title"],
235
243
  "source_path": source_path,
236
244
  "source_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
245
+ "design_sha256": report.design_sha256,
246
+ "document_sha256": report.document_sha256,
247
+ "execution_revision": (
248
+ execution.get("execution_revision") if isinstance(execution, dict) else None
249
+ ),
250
+ "execution": execution,
237
251
  "repositories": repositories,
238
252
  "tasks": tasks,
239
253
  "changes": changes,
@@ -256,7 +270,11 @@ def select_consumption_scopes(
256
270
  """Return final-protocol consumption closures grouped by repository."""
257
271
 
258
272
  resolved_spec_path = spec_path.resolve()
259
- text, manifest, _ = _read_spec(resolved_spec_path, require_ready=True)
273
+ text, manifest, _, report = _read_spec(
274
+ resolved_spec_path,
275
+ require_ready=True,
276
+ require_execution=True,
277
+ )
260
278
  selected_ids = list(dict.fromkeys(selected_task_ids))
261
279
  if not selected_ids:
262
280
  raise EasyDevSpecError("At least one Canonical Spec task must be selected")
@@ -306,6 +324,9 @@ def select_consumption_scopes(
306
324
  "status": manifest["status"],
307
325
  "source_path": source_path,
308
326
  "source_sha256": source_sha256,
327
+ "design_sha256": report.design_sha256,
328
+ "document_sha256": report.document_sha256,
329
+ "execution_revision": report.execution.get("execution_revision"),
309
330
  "selected_task_ids": selected_ids,
310
331
  "scopes": scopes,
311
332
  }
@@ -331,6 +352,14 @@ def select_tasks(
331
352
 
332
353
  selected_set = set(selected_ids)
333
354
  evidence_by_dependency = dependency_evidence or {}
355
+ execution = inspection.get("execution")
356
+ execution_by_task = {
357
+ str(snapshot.get("task_id")): snapshot
358
+ for snapshot in execution.get("tasks", [])
359
+ if isinstance(execution, dict)
360
+ and isinstance(snapshot, dict)
361
+ and isinstance(snapshot.get("task_id"), str)
362
+ } if isinstance(execution, dict) else {}
334
363
  dependency_target_counts: dict[str, int] = {}
335
364
  for source_task_id in selected_ids:
336
365
  for dependency in task_by_id[source_task_id].get("depends_on", []):
@@ -346,15 +375,30 @@ def select_tasks(
346
375
  evidence = evidence_by_dependency.get(edge_key)
347
376
  if evidence is None and dependency_target_counts[dependency_id] == 1:
348
377
  evidence = evidence_by_dependency.get(dependency_id)
378
+ source_snapshot = execution_by_task.get(source_task_id, {})
379
+ shared_dependency = next(
380
+ (
381
+ item
382
+ for item in source_snapshot.get("dependencies", [])
383
+ if isinstance(item, dict) and item.get("task_id") == dependency_id
384
+ ),
385
+ None,
386
+ )
387
+ dependency_snapshot = execution_by_task.get(dependency_id, {})
388
+ shared_satisfied = bool(
389
+ isinstance(shared_dependency, dict)
390
+ and shared_dependency.get("status") == "satisfied"
391
+ )
392
+ dependency_completed = dependency_snapshot.get("status") == "completed"
349
393
  if dependency_type == "hard":
350
- satisfied = dependency_id in selected_set or bool(evidence)
351
- if not satisfied:
394
+ satisfied = shared_satisfied or dependency_completed or bool(evidence)
395
+ if dependency_id not in selected_set and not satisfied:
352
396
  missing_hard.append(f"{source_task_id}->{dependency_id}")
353
397
  elif dependency_type == "contract":
354
- satisfied = True
398
+ satisfied = shared_satisfied or inspection.get("status") == "READY"
355
399
  evidence = evidence or "canonical-spec-ready-contract"
356
400
  else:
357
- satisfied = bool(evidence)
401
+ satisfied = shared_satisfied or bool(evidence)
358
402
  dependency_records.append(
359
403
  {
360
404
  "source_task_id": source_task_id,
@@ -362,6 +406,11 @@ def select_tasks(
362
406
  "dependency_type": dependency_type,
363
407
  "required_evidence": dependency["required_evidence"],
364
408
  "status": "satisfied" if satisfied else "pending",
409
+ "shared_status": (
410
+ shared_dependency.get("status")
411
+ if isinstance(shared_dependency, dict)
412
+ else "pending"
413
+ ),
365
414
  **({"evidence": evidence} if evidence else {}),
366
415
  }
367
416
  )
@@ -391,6 +440,12 @@ def select_tasks(
391
440
  test for test in inspection["tests"] if test.get("task_id") in selected_set
392
441
  ],
393
442
  "dependency_records": dependency_records,
443
+ "execution_revision": inspection.get("execution_revision"),
444
+ "execution_tasks": [
445
+ execution_by_task[task_id]
446
+ for task_id in selected_ids
447
+ if task_id in execution_by_task
448
+ ],
394
449
  }
395
450
 
396
451
 
@@ -406,6 +461,9 @@ def inspection_summary(inspection: dict[str, Any]) -> dict[str, Any]:
406
461
  "title",
407
462
  "source_path",
408
463
  "source_sha256",
464
+ "design_sha256",
465
+ "document_sha256",
466
+ "execution_revision",
409
467
  "repositories",
410
468
  "tasks",
411
469
  "dependency_edges",
@@ -414,4 +472,21 @@ def inspection_summary(inspection: dict[str, Any]) -> dict[str, Any]:
414
472
  "baseline_status",
415
473
  "repository_bindings",
416
474
  )
417
- return {key: inspection[key] for key in keys}
475
+ summary = {key: inspection[key] for key in keys}
476
+ execution = inspection.get("execution")
477
+ if isinstance(execution, dict):
478
+ summary["execution"] = {
479
+ 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
+ )
489
+ }
490
+ else:
491
+ summary["execution"] = None
492
+ return summary