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.
@@ -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,
@@ -86,10 +87,24 @@ WORKFLOW_MODES = {"fast", "standard", "strict"}
86
87
  WORKFLOW_MODE_RANK = {"fast": 0, "standard": 1, "strict": 2}
87
88
  STRICT_VERIFICATION_CHECK_TYPES = {"lint", "typecheck", "test", "build"}
88
89
  REVIEW_FINDING_SEVERITIES = {"error", "warning", "info"}
89
- STRICT_WORKFLOW_RISK_PATTERN = re.compile(
90
- r"(migration|migrate|schema|state[-_ ]?machine|security|payment|data[-_ ]?loss|"
91
- r"concurren|cross[-_ ]?repo|public[-_ ]?(api|contract)|迁移|状态机|安全|支付|"
92
- r"数据丢失|并发|跨仓|公共接口|公共契约)",
90
+ HIGH_WORKFLOW_RISK_PATTERN = re.compile(
91
+ r"(\bhigh[-_ ]?risk\b|\bcritical\b|\bsevere\b|\birreversible\b|"
92
+ r"\bdata[-_ ]?loss\b|\bfinancial[-_ ]?loss\b|"
93
+ r"\bsecurity[-_ ]?(boundary|breach)\b|\bprivilege[-_ ]?escalation\b|"
94
+ r"\bproduction[-_ ]?outage\b|"
95
+ r"高风险|严重|不可逆|数据丢失|资损|安全边界|安全事件|权限提升|生产故障)",
96
+ re.IGNORECASE,
97
+ )
98
+ NEGATED_HIGH_WORKFLOW_RISK_PATTERN = re.compile(
99
+ r"(\b(?:non[-_ ]?|not[-_ ]+|no[-_ ]+)(?:high[-_ ]?risk|critical|severe|irreversible)\b|"
100
+ r"\b(?:no|without)[-_ ]+(?:risk[-_ ]+of[-_ ]+)?(?:data[-_ ]?loss|"
101
+ r"financial[-_ ]?loss|security[-_ ]?breach|production[-_ ]?outage)\b|"
102
+ r"低风险|非高风险|不严重|(?<!不)可逆|无(?:数据丢失|资损|安全事件|生产故障)|"
103
+ r"不会导致(?:数据丢失|资损|安全事件|生产故障))",
104
+ re.IGNORECASE,
105
+ )
106
+ WIDE_WORKFLOW_CONTRACT_PATTERN = re.compile(
107
+ r"(cross[-_ ]?repo|public[-_ ]?(api|contract)|跨仓|公共接口|公共契约)",
93
108
  re.IGNORECASE,
94
109
  )
95
110
  DEFAULT_APPROVAL_MODE = "guard"
@@ -1588,7 +1603,7 @@ def is_valid_execution_plan(
1588
1603
  has_empty_file_scope = True
1589
1604
  if not is_string_list(unit.get("depends_on")):
1590
1605
  return False
1591
- for optional_list in ("rules_sections", "abstract_modules"):
1606
+ for optional_list in ("rules_sections", "abstract_modules", "local_baseline"):
1592
1607
  if optional_list in unit and not is_string_list(unit.get(optional_list)):
1593
1608
  return False
1594
1609
  if require_unit_contracts:
@@ -1754,8 +1769,16 @@ def inspect_task_spec(root: Path, task: dict) -> tuple[dict, dict]:
1754
1769
  if stored.get(field) != expected.get(field):
1755
1770
  raise StateError("Canonical Spec dependency metadata no longer matches source selection.")
1756
1771
  refreshed = dict(stored)
1757
- refreshed["status"] = expected.get("status")
1758
- refreshed["shared_status"] = expected.get("shared_status")
1772
+ for field in (
1773
+ "status",
1774
+ "shared_status",
1775
+ "dependency_task_status",
1776
+ "basis",
1777
+ ):
1778
+ if expected.get(field) is None:
1779
+ refreshed.pop(field, None)
1780
+ else:
1781
+ refreshed[field] = expected.get(field)
1759
1782
  if expected.get("evidence"):
1760
1783
  refreshed["evidence"] = expected.get("evidence")
1761
1784
  refreshed_dependencies.append(refreshed)
@@ -2270,6 +2293,42 @@ def task_repository_roots(root: Path, task: dict | None, plan: dict) -> list[Pat
2270
2293
  ]
2271
2294
 
2272
2295
 
2296
+ def workflow_plan_repository_roots(root: Path, task: dict, plan: dict) -> list[Path]:
2297
+ """Resolve only repositories that own files in the current execution plan."""
2298
+ repositories: set[Path] = set()
2299
+ repo_paths = task.get("repo_paths")
2300
+ canonical = isinstance(task.get("spec_source"), dict)
2301
+
2302
+ for unit in plan.get("units", []):
2303
+ if not isinstance(unit, dict):
2304
+ continue
2305
+ if canonical:
2306
+ repo_id = unit.get("repo_id")
2307
+ if not is_non_empty_string(repo_id) or not isinstance(repo_paths, dict):
2308
+ raise StateError("Canonical workflow Unit is missing its repository binding.")
2309
+ raw_repo_path = repo_paths.get(str(repo_id))
2310
+ if not is_non_empty_string(raw_repo_path):
2311
+ raise StateError(f"Canonical workflow repository path is missing: {repo_id}")
2312
+ candidate = Path(str(raw_repo_path))
2313
+ resolved = (candidate if candidate.is_absolute() else root / candidate).resolve()
2314
+ repository = git_repository_root(resolved)
2315
+ if repository is None or repository.resolve() != resolved:
2316
+ raise StateError(f"Canonical workflow repository binding is not a Git root: {repo_id}")
2317
+ repositories.add(repository.resolve())
2318
+ continue
2319
+
2320
+ for file_name in unit.get("files", []):
2321
+ if not is_non_empty_string(file_name):
2322
+ continue
2323
+ candidate = Path(str(file_name))
2324
+ resolved = (candidate if candidate.is_absolute() else root / candidate).resolve()
2325
+ repository = git_repository_root(resolved)
2326
+ if repository is not None:
2327
+ repositories.add(repository.resolve())
2328
+
2329
+ return sorted(repositories, key=lambda item: item.as_posix())
2330
+
2331
+
2273
2332
  def tdd_repositories(root: Path, task: dict, plan: dict) -> dict[str, Path]:
2274
2333
  if isinstance(task.get("spec_source"), dict):
2275
2334
  repo_paths = task.get("repo_paths")
@@ -6074,54 +6133,65 @@ def calculate_workflow_floor(root: Path, task_id: str) -> tuple[str, list[str]]:
6074
6133
  if not plan:
6075
6134
  raise StateError("Cannot calculate workflow floor without a valid execution plan.")
6076
6135
  units = [unit for unit in plan.get("units", []) if isinstance(unit, dict)]
6136
+ missing_local_baseline = [
6137
+ str(unit.get("id") or "<unknown>")
6138
+ for unit in units
6139
+ if not is_string_list(unit.get("local_baseline"), allow_empty=False)
6140
+ ]
6141
+ if missing_local_baseline:
6142
+ raise StateError(
6143
+ "Workflow plan Units must record a non-empty local_baseline: "
6144
+ + ", ".join(missing_local_baseline)
6145
+ )
6077
6146
  files = {
6078
6147
  str(file_name)
6079
6148
  for unit in units
6080
6149
  for file_name in unit.get("files", [])
6081
6150
  if is_non_empty_string(file_name)
6082
6151
  }
6083
- repositories = task_repository_roots(root, task, plan)
6084
- repos = task.get("repos")
6085
- repo_paths = task.get("repo_paths")
6086
- metadata_repo_count = max(
6087
- len(repos) if isinstance(repos, list) else 0,
6088
- len(repo_paths) if isinstance(repo_paths, dict) else 0,
6089
- )
6090
- repo_count = max(len(repositories), metadata_repo_count)
6091
- risk_text = " ".join(
6092
- [
6093
- str(task.get("title") or ""),
6094
- task_type,
6095
- *files,
6096
- *[
6097
- str(item)
6098
- for unit in units
6099
- for field in ("risks", "contracts")
6100
- for item in unit.get(field, [])
6101
- if is_non_empty_string(item)
6102
- and str(item).strip().lower() not in {"none", "no", "n/a", "无", "无风险"}
6103
- ],
6152
+ repositories = workflow_plan_repository_roots(root, task, plan)
6153
+ ignored_values = {"none", "no", "n/a", "无", "无风险"}
6154
+ risk_values = [
6155
+ str(item)
6156
+ for unit in units
6157
+ for item in unit.get("risks", [])
6158
+ if is_non_empty_string(item) and str(item).strip().lower() not in ignored_values
6159
+ ]
6160
+ contract_values = [
6161
+ str(item)
6162
+ for unit in units
6163
+ for item in unit.get("contracts", [])
6164
+ if is_non_empty_string(item) and str(item).strip().lower() not in ignored_values
6165
+ ]
6166
+ risk_text = NEGATED_HIGH_WORKFLOW_RISK_PATTERN.sub("", " ".join(risk_values))
6167
+ high_risk = bool(HIGH_WORKFLOW_RISK_PATTERN.search(risk_text))
6168
+
6169
+ complexity_reasons: list[str] = []
6170
+ if len(repositories) > 1:
6171
+ complexity_reasons.append("cross-repository-change")
6172
+ if len(units) >= 4 or len(files) >= 10:
6173
+ complexity_reasons.append("broad-change-scope")
6174
+ if WIDE_WORKFLOW_CONTRACT_PATTERN.search(" ".join(contract_values)):
6175
+ complexity_reasons.append("wide-contract-impact")
6176
+ if high_risk and complexity_reasons:
6177
+ return "strict", [
6178
+ "compound-high-risk-and-complexity",
6179
+ "explicit-high-risk-signal",
6180
+ *complexity_reasons,
6104
6181
  ]
6105
- )
6106
- strict_reasons: list[str] = []
6107
- if repo_count > 1:
6108
- strict_reasons.append("cross-repository-scope")
6109
- if len(units) >= 4 or len(files) >= 8:
6110
- strict_reasons.append("broad-change-scope")
6111
- if STRICT_WORKFLOW_RISK_PATTERN.search(risk_text):
6112
- strict_reasons.append("high-risk-contract-or-domain")
6113
- if strict_reasons:
6114
- return "strict", strict_reasons
6115
6182
 
6116
6183
  standard_reasons: list[str] = []
6184
+ if high_risk:
6185
+ standard_reasons.append("bounded-high-risk-change")
6186
+ standard_reasons.extend(complexity_reasons)
6117
6187
  if len(units) > 1:
6118
6188
  standard_reasons.append("multiple-units")
6119
- if len(files) >= 3:
6189
+ if len(files) > 5:
6120
6190
  standard_reasons.append("multi-file-impact")
6121
6191
  if plan.get("strategy") == "parallel":
6122
6192
  standard_reasons.append("parallel-execution")
6123
6193
  if standard_reasons:
6124
- return "standard", standard_reasons
6194
+ return "standard", list(dict.fromkeys(standard_reasons))
6125
6195
  return "fast", ["single-bounded-unit"]
6126
6196
 
6127
6197
 
@@ -6735,6 +6805,8 @@ def main() -> int:
6735
6805
  inspect_spec_parser = subcommands.add_parser("inspect-dev-spec", parents=[common])
6736
6806
  inspect_spec_parser.add_argument("--spec", required=True)
6737
6807
  inspect_spec_parser.add_argument("--repo-path", action="append", default=[])
6808
+ inspect_spec_parser.add_argument("--spec-task", action="append", default=[])
6809
+ inspect_spec_parser.add_argument("--manifest-only", action="store_true")
6738
6810
 
6739
6811
  initialize_spec = subcommands.add_parser("initialize-spec-execution", parents=[common])
6740
6812
  initialize_spec.add_argument("--spec", required=True)
@@ -6756,7 +6828,7 @@ def main() -> int:
6756
6828
  create_from_spec.add_argument("--task-id", required=True)
6757
6829
  create_from_spec.add_argument("--type", required=True)
6758
6830
  create_from_spec.add_argument("--title", required=True)
6759
- create_from_spec.add_argument("--repo-path", required=True, action="append")
6831
+ create_from_spec.add_argument("--repo-path", action="append", default=[])
6760
6832
  create_from_spec.add_argument("--dependency-evidence", action="append", default=[])
6761
6833
  create_from_spec.add_argument("--agent", required=True)
6762
6834
  create_from_spec.add_argument("--no-set-current", action="store_true")
@@ -7017,15 +7089,21 @@ def main() -> int:
7017
7089
  emit(snapshot_state(root, session_file))
7018
7090
  elif command == "inspect-dev-spec":
7019
7091
  spec_path = Path(args.spec).expanduser()
7020
- emit(
7021
- inspection_summary(
7022
- inspect_spec(
7023
- spec_path if spec_path.is_absolute() else root / spec_path,
7024
- root,
7025
- parse_mapping_args(args.repo_path, "--repo-path"),
7026
- )
7092
+ if args.manifest_only and args.spec_task:
7093
+ raise StateError("--manifest-only cannot be combined with --spec-task")
7094
+ resolved_spec = spec_path if spec_path.is_absolute() else root / spec_path
7095
+ repo_paths = parse_mapping_args(args.repo_path, "--repo-path")
7096
+ inspection = (
7097
+ inspect_manifest(resolved_spec, root, repo_paths)
7098
+ if args.manifest_only
7099
+ else inspect_spec(
7100
+ resolved_spec,
7101
+ root,
7102
+ repo_paths,
7103
+ args.spec_task or None,
7027
7104
  )
7028
7105
  )
7106
+ emit(inspection_summary(inspection))
7029
7107
  elif command == "initialize-spec-execution":
7030
7108
  emit(initialize_spec_execution_state(root, args.spec))
7031
7109
  elif command == "select-dev-spec-scope":