@hunyed15/codecgc 0.1.0

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.
Files changed (128) hide show
  1. package/.claude/hooks/route-edit.ps1 +86 -0
  2. package/INSTALLATION.md +550 -0
  3. package/LICENSE +21 -0
  4. package/README.md +171 -0
  5. package/bin/cgc-build.js +4 -0
  6. package/bin/cgc-doctor.js +4 -0
  7. package/bin/cgc-entry.js +4 -0
  8. package/bin/cgc-external-audit.js +4 -0
  9. package/bin/cgc-fix.js +4 -0
  10. package/bin/cgc-history.js +4 -0
  11. package/bin/cgc-install.js +4 -0
  12. package/bin/cgc-lifecycle.js +4 -0
  13. package/bin/cgc-package-audit.js +4 -0
  14. package/bin/cgc-plan.js +4 -0
  15. package/bin/cgc-release-readiness.js +4 -0
  16. package/bin/cgc-review.js +4 -0
  17. package/bin/cgc-route.js +4 -0
  18. package/bin/cgc-status.js +4 -0
  19. package/bin/cgc-test.js +4 -0
  20. package/bin/cgc.js +4 -0
  21. package/bin/codecgc.js +1284 -0
  22. package/codecgc/cgc/SKILL.md +46 -0
  23. package/codecgc/cgc-arch/SKILL.md +61 -0
  24. package/codecgc/cgc-build/SKILL.md +53 -0
  25. package/codecgc/cgc-decide/SKILL.md +55 -0
  26. package/codecgc/cgc-fix/SKILL.md +47 -0
  27. package/codecgc/cgc-learn/SKILL.md +46 -0
  28. package/codecgc/cgc-onboard/SKILL.md +52 -0
  29. package/codecgc/cgc-plan/SKILL.md +48 -0
  30. package/codecgc/cgc-refactor/SKILL.md +46 -0
  31. package/codecgc/cgc-req/SKILL.md +61 -0
  32. package/codecgc/cgc-review/SKILL.md +57 -0
  33. package/codecgc/cgc-roadmap/SKILL.md +55 -0
  34. package/codecgc/cgc-test/SKILL.md +21 -0
  35. package/codecgc/reference/api-cgc-review-libdoc.md +13 -0
  36. package/codecgc/reference/artifact-class-policy.md +81 -0
  37. package/codecgc/reference/build-flow.md +95 -0
  38. package/codecgc/reference/checklist-contract.md +103 -0
  39. package/codecgc/reference/execution-audit.md +121 -0
  40. package/codecgc/reference/execution-model.md +118 -0
  41. package/codecgc/reference/execution-routing.md +130 -0
  42. package/codecgc/reference/executor-contract.md +87 -0
  43. package/codecgc/reference/external-capability-registry.json +104 -0
  44. package/codecgc/reference/fix-flow.md +94 -0
  45. package/codecgc/reference/fixture-governance.md +60 -0
  46. package/codecgc/reference/flow-execution.md +65 -0
  47. package/codecgc/reference/lifecycle-map.md +172 -0
  48. package/codecgc/reference/lifecycle-playbook.md +104 -0
  49. package/codecgc/reference/long-lived-artifacts.md +98 -0
  50. package/codecgc/reference/operation-guide.md +242 -0
  51. package/codecgc/reference/release-maintenance-playbook.md +150 -0
  52. package/codecgc/reference/review-writeback.md +141 -0
  53. package/codecgc/reference/role-model.md +128 -0
  54. package/codecgc/reference/runtime-boundary.md +72 -0
  55. package/codecgc/reference/shared-conventions.md +93 -0
  56. package/codecgc/reference/workflow-scaffold.md +57 -0
  57. package/codexmcp/LICENSE +21 -0
  58. package/codexmcp/README.md +294 -0
  59. package/codexmcp/pyproject.toml +37 -0
  60. package/codexmcp/src/codexmcp/__init__.py +4 -0
  61. package/codexmcp/src/codexmcp/cli.py +12 -0
  62. package/codexmcp/src/codexmcp/server.py +529 -0
  63. package/geminimcp/README.md +258 -0
  64. package/geminimcp/pyproject.toml +15 -0
  65. package/geminimcp/src/geminimcp/__init__.py +4 -0
  66. package/geminimcp/src/geminimcp/cli.py +12 -0
  67. package/geminimcp/src/geminimcp/server.py +465 -0
  68. package/model-routing.yaml +30 -0
  69. package/package.json +90 -0
  70. package/requirements.txt +1 -0
  71. package/scripts/README-codecgc-cli.md +89 -0
  72. package/scripts/audit_codecgc_external_capabilities.py +276 -0
  73. package/scripts/audit_codecgc_historical_audits.py +242 -0
  74. package/scripts/audit_codecgc_lifecycle.py +241 -0
  75. package/scripts/audit_codecgc_package_runtime.py +445 -0
  76. package/scripts/audit_codecgc_release_readiness.py +202 -0
  77. package/scripts/audit_codecgc_review_policy.py +82 -0
  78. package/scripts/audit_codecgc_workflow_history.py +317 -0
  79. package/scripts/build_codecgc_task.py +487 -0
  80. package/scripts/codecgc_artifact_roots.py +40 -0
  81. package/scripts/codecgc_cli.py +843 -0
  82. package/scripts/codecgc_command_surface.py +28 -0
  83. package/scripts/codecgc_console_io.py +45 -0
  84. package/scripts/codecgc_executor_registry.py +54 -0
  85. package/scripts/codecgc_file_evidence.py +349 -0
  86. package/scripts/codecgc_flow_control.py +233 -0
  87. package/scripts/codecgc_governance_dedupe.py +161 -0
  88. package/scripts/codecgc_plan_decision.py +103 -0
  89. package/scripts/codecgc_review_control.py +588 -0
  90. package/scripts/codecgc_roadmap_templates.py +149 -0
  91. package/scripts/codecgc_routing_paths.py +16 -0
  92. package/scripts/codecgc_routing_template.py +135 -0
  93. package/scripts/codecgc_runtime_paths.py +22 -0
  94. package/scripts/codecgc_session_recovery.py +44 -0
  95. package/scripts/codecgc_step_control.py +154 -0
  96. package/scripts/codecgc_workflow_runtime.py +63 -0
  97. package/scripts/codecgc_workflow_templates.py +437 -0
  98. package/scripts/entry_codecgc_workflow.py +3419 -0
  99. package/scripts/exercise_mcp_tools.py +109 -0
  100. package/scripts/expand_codecgc_roadmap.py +664 -0
  101. package/scripts/init_codecgc_roadmap.py +134 -0
  102. package/scripts/init_codecgc_workflow.py +207 -0
  103. package/scripts/install_codecgc.py +938 -0
  104. package/scripts/migrate_demo_workflows_to_fixtures.py +128 -0
  105. package/scripts/normalize_codecgc_audits.py +114 -0
  106. package/scripts/normalize_codecgc_governance_docs.py +79 -0
  107. package/scripts/normalize_codecgc_workflow_docs.py +269 -0
  108. package/scripts/plan_codecgc_workflow.py +970 -0
  109. package/scripts/refresh_codecgc_review_policy.py +223 -0
  110. package/scripts/review_codecgc_workflow.py +88 -0
  111. package/scripts/route_codecgc_workflow.py +671 -0
  112. package/scripts/run_codecgc_build.py +104 -0
  113. package/scripts/run_codecgc_fix.py +104 -0
  114. package/scripts/run_codecgc_flow_step.py +165 -0
  115. package/scripts/run_codecgc_task.py +410 -0
  116. package/scripts/run_codecgc_test.py +105 -0
  117. package/scripts/sync_codecgc_mcp_config.py +41 -0
  118. package/scripts/write_codecgc_architecture.py +78 -0
  119. package/scripts/write_codecgc_decision.py +83 -0
  120. package/scripts/write_codecgc_explore.py +118 -0
  121. package/scripts/write_codecgc_guide.py +141 -0
  122. package/scripts/write_codecgc_learning.py +87 -0
  123. package/scripts/write_codecgc_libdoc.py +140 -0
  124. package/scripts/write_codecgc_refactor.py +78 -0
  125. package/scripts/write_codecgc_requirement.py +78 -0
  126. package/scripts/write_codecgc_review.py +291 -0
  127. package/scripts/write_codecgc_roadmap.py +122 -0
  128. package/scripts/write_codecgc_trick.py +123 -0
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ INTERNAL_TO_PUBLIC_COMMAND = {
5
+ "cgc-plan": "cgc-plan",
6
+ "cgc-build": "cgc-build",
7
+ "cgc-fix": "cgc-fix",
8
+ "cgc-test": "cgc-test",
9
+ "cgc-review": "cgc-review",
10
+ "cgc-route": "cgc-route",
11
+ }
12
+
13
+ PUBLIC_TO_INTERNAL_COMMAND = {value: key for key, value in INTERNAL_TO_PUBLIC_COMMAND.items()}
14
+
15
+
16
+ def to_public_command(command: str) -> str:
17
+ normalized = str(command or "").strip()
18
+ return INTERNAL_TO_PUBLIC_COMMAND.get(normalized, normalized)
19
+
20
+
21
+ def to_internal_command(command: str) -> str:
22
+ normalized = str(command or "").strip()
23
+ return PUBLIC_TO_INTERNAL_COMMAND.get(normalized, normalized)
24
+
25
+
26
+ def matches_command(command: str, *aliases: str) -> bool:
27
+ normalized = to_internal_command(command)
28
+ return normalized in {to_internal_command(alias) for alias in aliases}
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from typing import Any
6
+
7
+
8
+ def configure_utf8_stdio() -> None:
9
+ for stream_name in ("stdout", "stderr"):
10
+ stream = getattr(sys, stream_name, None)
11
+ if stream is None:
12
+ continue
13
+ reconfigure = getattr(stream, "reconfigure", None)
14
+ if callable(reconfigure):
15
+ try:
16
+ reconfigure(encoding="utf-8", errors="replace")
17
+ except Exception:
18
+ pass
19
+
20
+
21
+ def print_json(payload: dict[str, Any], *, file: Any | None = None) -> None:
22
+ target = file or sys.stdout
23
+ text = json.dumps(payload, ensure_ascii=False, indent=2)
24
+ target.write(text)
25
+ target.write("\n")
26
+
27
+
28
+ def render_summary_block(title: str, base_lines: list[str], next_actions: list[str]) -> str:
29
+ lines = [title, *base_lines]
30
+ unique_actions: list[str] = []
31
+ seen: set[str] = set()
32
+ for item in next_actions:
33
+ normalized = str(item).strip()
34
+ if not normalized or normalized in seen:
35
+ continue
36
+ seen.add(normalized)
37
+ unique_actions.append(normalized)
38
+
39
+ if unique_actions:
40
+ lines.append(f"- 下一步: {unique_actions[0]}")
41
+ for item in unique_actions[1:]:
42
+ lines.append(f"- 备选动作: {item}")
43
+ else:
44
+ lines.append("- 下一步: 无")
45
+ return "\n".join(lines)
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ WORKSPACE = Path(__file__).resolve().parents[1]
10
+
11
+
12
+ def resolve_python_command() -> str:
13
+ override = os.environ.get("CODECGC_PYTHON_COMMAND", "").strip()
14
+ if override:
15
+ return override
16
+ return sys.executable
17
+
18
+
19
+ def build_executor_registry() -> dict[str, dict[str, Any]]:
20
+ python_command = resolve_python_command()
21
+ return {
22
+ "backend": {
23
+ "target": "backend",
24
+ "mcp_server_name": "codex",
25
+ "routing_executor": "codexmcp",
26
+ "tool_name": "implement_backend_task",
27
+ "python_module": "codexmcp.cli",
28
+ "pythonpath": str(WORKSPACE / "codexmcp" / "src"),
29
+ "python_command": python_command,
30
+ },
31
+ "frontend": {
32
+ "target": "frontend",
33
+ "mcp_server_name": "gemini",
34
+ "routing_executor": "geminimcp",
35
+ "tool_name": "implement_frontend_task",
36
+ "python_module": "geminimcp.cli",
37
+ "pythonpath": str(WORKSPACE / "geminimcp" / "src"),
38
+ "python_command": python_command,
39
+ },
40
+ }
41
+
42
+
43
+ def get_executor_config(target: str) -> dict[str, Any]:
44
+ registry = build_executor_registry()
45
+ if target not in registry:
46
+ raise ValueError(f"Unsupported target: {target}")
47
+ return registry[target]
48
+
49
+
50
+ def build_executor_env(target: str) -> dict[str, str]:
51
+ env = dict(os.environ)
52
+ config = get_executor_config(target)
53
+ env["PYTHONPATH"] = str(config["pythonpath"])
54
+ return env
@@ -0,0 +1,349 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import hashlib
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ WORKSPACE = Path(__file__).resolve().parents[1]
11
+
12
+ IGNORED_PREFIXES = (
13
+ ".git/",
14
+ ".ace-tool/",
15
+ "__pycache__/",
16
+ ".pytest_cache/",
17
+ ".mypy_cache/",
18
+ "node_modules/",
19
+ )
20
+
21
+
22
+ def normalize_path_text(path_text: str) -> str:
23
+ normalized = path_text.replace("\\", "/").strip()
24
+ while normalized.startswith("./"):
25
+ normalized = normalized[2:]
26
+ return normalized
27
+
28
+
29
+ def should_ignore_path(relative_path: str) -> bool:
30
+ normalized = normalize_path_text(relative_path)
31
+ return any(
32
+ normalized == prefix.rstrip("/") or normalized.startswith(prefix)
33
+ for prefix in IGNORED_PREFIXES
34
+ )
35
+
36
+
37
+ def iter_workspace_files(root: Path = WORKSPACE) -> list[Path]:
38
+ files: list[Path] = []
39
+ for path in root.rglob("*"):
40
+ if not path.is_file():
41
+ continue
42
+ relative = normalize_path_text(str(path.relative_to(root)))
43
+ if should_ignore_path(relative):
44
+ continue
45
+ files.append(path)
46
+ return files
47
+
48
+
49
+ def fingerprint_file(path: Path) -> str:
50
+ digest = hashlib.sha256()
51
+ with path.open("rb") as handle:
52
+ while True:
53
+ chunk = handle.read(1024 * 1024)
54
+ if not chunk:
55
+ break
56
+ digest.update(chunk)
57
+ return digest.hexdigest()
58
+
59
+
60
+ def read_text_preview(path: Path, max_chars: int = 240) -> str:
61
+ try:
62
+ text = path.read_text(encoding="utf-8")
63
+ except Exception:
64
+ return "[non-text-or-unreadable]"
65
+ compact = " ".join(text.split())
66
+ return compact[:max_chars]
67
+
68
+
69
+ def read_full_text(path: Path) -> str | None:
70
+ try:
71
+ return path.read_text(encoding="utf-8")
72
+ except Exception:
73
+ return None
74
+
75
+
76
+ def extract_text(snapshot_value: Any) -> str | None:
77
+ if isinstance(snapshot_value, dict):
78
+ text = snapshot_value.get("text")
79
+ return text if isinstance(text, str) else None
80
+ return None
81
+
82
+
83
+ def build_unified_diff(
84
+ relative_path: str,
85
+ before_text: str | None,
86
+ after_text: str | None,
87
+ *,
88
+ max_lines: int = 80,
89
+ ) -> str:
90
+ if before_text is None and after_text is None:
91
+ return ""
92
+ before_lines = [] if before_text is None else before_text.splitlines()
93
+ after_lines = [] if after_text is None else after_text.splitlines()
94
+ diff_lines = list(
95
+ difflib.unified_diff(
96
+ before_lines,
97
+ after_lines,
98
+ fromfile=f"a/{relative_path}",
99
+ tofile=f"b/{relative_path}",
100
+ lineterm="",
101
+ n=3,
102
+ )
103
+ )
104
+ if not diff_lines:
105
+ return ""
106
+ truncated = diff_lines[:max_lines]
107
+ if len(diff_lines) > max_lines:
108
+ truncated.append("... [diff truncated]")
109
+ return "\n".join(truncated)
110
+
111
+
112
+ def count_changed_diff_lines(diff_excerpt: str) -> int:
113
+ count = 0
114
+ for line in diff_excerpt.splitlines():
115
+ if line.startswith(("+++", "---", "@@")):
116
+ continue
117
+ if line.startswith("+") or line.startswith("-"):
118
+ count += 1
119
+ return count
120
+
121
+
122
+ def snapshot_workspace(root: Path = WORKSPACE) -> dict[str, dict[str, Any]]:
123
+ snapshot: dict[str, dict[str, Any]] = {}
124
+ for path in iter_workspace_files(root):
125
+ relative = normalize_path_text(str(path.relative_to(root)))
126
+ fingerprint = fingerprint_file(path)
127
+ full_text = read_full_text(path)
128
+ snapshot[relative] = {
129
+ "hash": fingerprint,
130
+ "size": path.stat().st_size,
131
+ "preview": read_text_preview(path),
132
+ "text": full_text,
133
+ }
134
+ return snapshot
135
+
136
+
137
+ def extract_hash(snapshot_value: Any) -> str:
138
+ if isinstance(snapshot_value, dict):
139
+ return str(snapshot_value.get("hash", ""))
140
+ return str(snapshot_value or "")
141
+
142
+
143
+ def extract_size(snapshot_value: Any) -> int:
144
+ if isinstance(snapshot_value, dict):
145
+ try:
146
+ return int(snapshot_value.get("size", 0) or 0)
147
+ except Exception:
148
+ return 0
149
+ return 0
150
+
151
+
152
+ def extract_preview(snapshot_value: Any) -> str:
153
+ if isinstance(snapshot_value, dict):
154
+ return str(snapshot_value.get("preview", ""))
155
+ return ""
156
+
157
+
158
+ def classify_scope_match(relative_path: str, target_paths: list[str]) -> tuple[bool, str, str]:
159
+ normalized_path = normalize_path_text(relative_path)
160
+ normalized_targets = [normalize_path_text(path) for path in target_paths if str(path).strip()]
161
+
162
+ for target in normalized_targets:
163
+ if any(char in target for char in "*?[]"):
164
+ if Path(normalized_path).match(target):
165
+ return True, "glob", target
166
+ continue
167
+ if normalized_path == target:
168
+ return True, "exact", target
169
+ if normalized_path.startswith(target.rstrip("/") + "/"):
170
+ return True, "child", target
171
+ return False, "out-of-scope", ""
172
+
173
+
174
+ def path_matches_scope(relative_path: str, target_paths: list[str]) -> bool:
175
+ matched, _, _ = classify_scope_match(relative_path, target_paths)
176
+ return matched
177
+
178
+
179
+ def detect_git_root(root: Path = WORKSPACE) -> Path | None:
180
+ try:
181
+ completed = subprocess.run(
182
+ ["git", "-C", str(root), "rev-parse", "--show-toplevel"],
183
+ capture_output=True,
184
+ text=True,
185
+ encoding="utf-8",
186
+ check=False,
187
+ )
188
+ except FileNotFoundError:
189
+ return None
190
+ if completed.returncode != 0:
191
+ return None
192
+ candidate = completed.stdout.strip()
193
+ return Path(candidate) if candidate else None
194
+
195
+
196
+ def run_git_text_command(git_root: Path, args: list[str]) -> tuple[bool, str]:
197
+ try:
198
+ completed = subprocess.run(
199
+ ["git", "-C", str(git_root), *args],
200
+ capture_output=True,
201
+ text=True,
202
+ encoding="utf-8",
203
+ check=False,
204
+ )
205
+ except FileNotFoundError:
206
+ return False, ""
207
+ if completed.returncode != 0:
208
+ return False, completed.stderr.strip() or completed.stdout.strip()
209
+ return True, completed.stdout.strip()
210
+
211
+
212
+ def collect_git_evidence(changed_paths: list[str], root: Path = WORKSPACE) -> dict[str, Any]:
213
+ git_root = detect_git_root(root)
214
+ if git_root is None:
215
+ return {
216
+ "git_repository_detected": False,
217
+ "git_root": "",
218
+ "history_available": False,
219
+ "status": "not-a-git-worktree",
220
+ "tracked_changed_files": [],
221
+ "untracked_changed_files": [],
222
+ "git_changed_files": [],
223
+ }
224
+
225
+ git_changed_files: list[dict[str, Any]] = []
226
+ tracked_changed_files: list[str] = []
227
+ untracked_changed_files: list[str] = []
228
+ history_available = False
229
+
230
+ for relative_path in changed_paths:
231
+ tracked_ok, _ = run_git_text_command(
232
+ git_root,
233
+ ["ls-files", "--error-unmatch", "--", relative_path],
234
+ )
235
+ last_commit_hash = ""
236
+ last_commit_subject = ""
237
+ if tracked_ok:
238
+ tracked_changed_files.append(relative_path)
239
+ log_ok, log_output = run_git_text_command(
240
+ git_root,
241
+ ["log", "-n", "1", "--format=%H%x1f%s", "--", relative_path],
242
+ )
243
+ if log_ok and log_output:
244
+ parts = log_output.split("\x1f", 1)
245
+ last_commit_hash = parts[0].strip()
246
+ last_commit_subject = parts[1].strip() if len(parts) > 1 else ""
247
+ history_available = history_available or bool(last_commit_hash)
248
+ else:
249
+ untracked_changed_files.append(relative_path)
250
+
251
+ git_changed_files.append(
252
+ {
253
+ "path": relative_path,
254
+ "tracked": tracked_ok,
255
+ "last_commit_hash": last_commit_hash,
256
+ "last_commit_subject": last_commit_subject,
257
+ }
258
+ )
259
+
260
+ status = "git-history-available" if history_available else "git-repository-without-history-proof"
261
+ return {
262
+ "git_repository_detected": True,
263
+ "git_root": str(git_root),
264
+ "history_available": history_available,
265
+ "status": status,
266
+ "tracked_changed_files": tracked_changed_files,
267
+ "untracked_changed_files": untracked_changed_files,
268
+ "git_changed_files": git_changed_files,
269
+ }
270
+
271
+
272
+ def verify_workspace_changes(
273
+ before: dict[str, dict[str, Any]],
274
+ after: dict[str, dict[str, Any]],
275
+ target_paths: list[str],
276
+ ) -> dict[str, Any]:
277
+ changed_paths: list[str] = []
278
+ all_paths = sorted(set(before) | set(after))
279
+ file_diffs: list[dict[str, Any]] = []
280
+
281
+ for relative_path in all_paths:
282
+ before_item = before.get(relative_path)
283
+ after_item = after.get(relative_path)
284
+ if extract_hash(before_item) != extract_hash(after_item):
285
+ in_scope, scope_match_kind, matched_target = classify_scope_match(relative_path, target_paths)
286
+ changed_paths.append(relative_path)
287
+ before_text = extract_text(before_item)
288
+ after_text = extract_text(after_item)
289
+ diff_excerpt = build_unified_diff(
290
+ relative_path,
291
+ before_text,
292
+ after_text,
293
+ )
294
+ diff_kind = (
295
+ "unified-text-diff"
296
+ if diff_excerpt
297
+ else "binary-or-unreadable"
298
+ if before_text is None and after_text is None
299
+ else "hash-only-diff"
300
+ )
301
+ file_diffs.append(
302
+ {
303
+ "path": relative_path,
304
+ "change_type": (
305
+ "added"
306
+ if before_item is None
307
+ else "deleted"
308
+ if after_item is None
309
+ else "modified"
310
+ ),
311
+ "before_hash": extract_hash(before_item),
312
+ "after_hash": extract_hash(after_item),
313
+ "before_size": extract_size(before_item),
314
+ "after_size": extract_size(after_item),
315
+ "before_preview": extract_preview(before_item),
316
+ "after_preview": extract_preview(after_item),
317
+ "diff_excerpt": diff_excerpt,
318
+ "diff_kind": diff_kind,
319
+ "changed_line_count": count_changed_diff_lines(diff_excerpt),
320
+ "in_scope": in_scope,
321
+ "scope_match_kind": scope_match_kind,
322
+ "matched_target": matched_target,
323
+ }
324
+ )
325
+
326
+ verified_changed_files = [
327
+ path for path in changed_paths if path_matches_scope(path, target_paths)
328
+ ]
329
+ out_of_scope_changed_files = [
330
+ path for path in changed_paths if not path_matches_scope(path, target_paths)
331
+ ]
332
+ git_evidence = collect_git_evidence(changed_paths)
333
+ history_available = bool(git_evidence.get("history_available"))
334
+
335
+ return {
336
+ "evidence_source": "workspace-unified-diff-snapshot",
337
+ "workspace_changed_files": changed_paths,
338
+ "verified_changed_files": verified_changed_files,
339
+ "out_of_scope_changed_files": out_of_scope_changed_files,
340
+ "file_diffs": file_diffs,
341
+ "evidence_confidence": (
342
+ "stronger-than-self-report-with-git-history"
343
+ if file_diffs and history_available
344
+ else "stronger-than-self-report"
345
+ if file_diffs
346
+ else "no-observed-diff"
347
+ ),
348
+ "git_evidence": git_evidence,
349
+ }