@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.
- package/.claude/hooks/route-edit.ps1 +86 -0
- package/INSTALLATION.md +550 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/bin/cgc-build.js +4 -0
- package/bin/cgc-doctor.js +4 -0
- package/bin/cgc-entry.js +4 -0
- package/bin/cgc-external-audit.js +4 -0
- package/bin/cgc-fix.js +4 -0
- package/bin/cgc-history.js +4 -0
- package/bin/cgc-install.js +4 -0
- package/bin/cgc-lifecycle.js +4 -0
- package/bin/cgc-package-audit.js +4 -0
- package/bin/cgc-plan.js +4 -0
- package/bin/cgc-release-readiness.js +4 -0
- package/bin/cgc-review.js +4 -0
- package/bin/cgc-route.js +4 -0
- package/bin/cgc-status.js +4 -0
- package/bin/cgc-test.js +4 -0
- package/bin/cgc.js +4 -0
- package/bin/codecgc.js +1284 -0
- package/codecgc/cgc/SKILL.md +46 -0
- package/codecgc/cgc-arch/SKILL.md +61 -0
- package/codecgc/cgc-build/SKILL.md +53 -0
- package/codecgc/cgc-decide/SKILL.md +55 -0
- package/codecgc/cgc-fix/SKILL.md +47 -0
- package/codecgc/cgc-learn/SKILL.md +46 -0
- package/codecgc/cgc-onboard/SKILL.md +52 -0
- package/codecgc/cgc-plan/SKILL.md +48 -0
- package/codecgc/cgc-refactor/SKILL.md +46 -0
- package/codecgc/cgc-req/SKILL.md +61 -0
- package/codecgc/cgc-review/SKILL.md +57 -0
- package/codecgc/cgc-roadmap/SKILL.md +55 -0
- package/codecgc/cgc-test/SKILL.md +21 -0
- package/codecgc/reference/api-cgc-review-libdoc.md +13 -0
- package/codecgc/reference/artifact-class-policy.md +81 -0
- package/codecgc/reference/build-flow.md +95 -0
- package/codecgc/reference/checklist-contract.md +103 -0
- package/codecgc/reference/execution-audit.md +121 -0
- package/codecgc/reference/execution-model.md +118 -0
- package/codecgc/reference/execution-routing.md +130 -0
- package/codecgc/reference/executor-contract.md +87 -0
- package/codecgc/reference/external-capability-registry.json +104 -0
- package/codecgc/reference/fix-flow.md +94 -0
- package/codecgc/reference/fixture-governance.md +60 -0
- package/codecgc/reference/flow-execution.md +65 -0
- package/codecgc/reference/lifecycle-map.md +172 -0
- package/codecgc/reference/lifecycle-playbook.md +104 -0
- package/codecgc/reference/long-lived-artifacts.md +98 -0
- package/codecgc/reference/operation-guide.md +242 -0
- package/codecgc/reference/release-maintenance-playbook.md +150 -0
- package/codecgc/reference/review-writeback.md +141 -0
- package/codecgc/reference/role-model.md +128 -0
- package/codecgc/reference/runtime-boundary.md +72 -0
- package/codecgc/reference/shared-conventions.md +93 -0
- package/codecgc/reference/workflow-scaffold.md +57 -0
- package/codexmcp/LICENSE +21 -0
- package/codexmcp/README.md +294 -0
- package/codexmcp/pyproject.toml +37 -0
- package/codexmcp/src/codexmcp/__init__.py +4 -0
- package/codexmcp/src/codexmcp/cli.py +12 -0
- package/codexmcp/src/codexmcp/server.py +529 -0
- package/geminimcp/README.md +258 -0
- package/geminimcp/pyproject.toml +15 -0
- package/geminimcp/src/geminimcp/__init__.py +4 -0
- package/geminimcp/src/geminimcp/cli.py +12 -0
- package/geminimcp/src/geminimcp/server.py +465 -0
- package/model-routing.yaml +30 -0
- package/package.json +90 -0
- package/requirements.txt +1 -0
- package/scripts/README-codecgc-cli.md +89 -0
- package/scripts/audit_codecgc_external_capabilities.py +276 -0
- package/scripts/audit_codecgc_historical_audits.py +242 -0
- package/scripts/audit_codecgc_lifecycle.py +241 -0
- package/scripts/audit_codecgc_package_runtime.py +445 -0
- package/scripts/audit_codecgc_release_readiness.py +202 -0
- package/scripts/audit_codecgc_review_policy.py +82 -0
- package/scripts/audit_codecgc_workflow_history.py +317 -0
- package/scripts/build_codecgc_task.py +487 -0
- package/scripts/codecgc_artifact_roots.py +40 -0
- package/scripts/codecgc_cli.py +843 -0
- package/scripts/codecgc_command_surface.py +28 -0
- package/scripts/codecgc_console_io.py +45 -0
- package/scripts/codecgc_executor_registry.py +54 -0
- package/scripts/codecgc_file_evidence.py +349 -0
- package/scripts/codecgc_flow_control.py +233 -0
- package/scripts/codecgc_governance_dedupe.py +161 -0
- package/scripts/codecgc_plan_decision.py +103 -0
- package/scripts/codecgc_review_control.py +588 -0
- package/scripts/codecgc_roadmap_templates.py +149 -0
- package/scripts/codecgc_routing_paths.py +16 -0
- package/scripts/codecgc_routing_template.py +135 -0
- package/scripts/codecgc_runtime_paths.py +22 -0
- package/scripts/codecgc_session_recovery.py +44 -0
- package/scripts/codecgc_step_control.py +154 -0
- package/scripts/codecgc_workflow_runtime.py +63 -0
- package/scripts/codecgc_workflow_templates.py +437 -0
- package/scripts/entry_codecgc_workflow.py +3419 -0
- package/scripts/exercise_mcp_tools.py +109 -0
- package/scripts/expand_codecgc_roadmap.py +664 -0
- package/scripts/init_codecgc_roadmap.py +134 -0
- package/scripts/init_codecgc_workflow.py +207 -0
- package/scripts/install_codecgc.py +938 -0
- package/scripts/migrate_demo_workflows_to_fixtures.py +128 -0
- package/scripts/normalize_codecgc_audits.py +114 -0
- package/scripts/normalize_codecgc_governance_docs.py +79 -0
- package/scripts/normalize_codecgc_workflow_docs.py +269 -0
- package/scripts/plan_codecgc_workflow.py +970 -0
- package/scripts/refresh_codecgc_review_policy.py +223 -0
- package/scripts/review_codecgc_workflow.py +88 -0
- package/scripts/route_codecgc_workflow.py +671 -0
- package/scripts/run_codecgc_build.py +104 -0
- package/scripts/run_codecgc_fix.py +104 -0
- package/scripts/run_codecgc_flow_step.py +165 -0
- package/scripts/run_codecgc_task.py +410 -0
- package/scripts/run_codecgc_test.py +105 -0
- package/scripts/sync_codecgc_mcp_config.py +41 -0
- package/scripts/write_codecgc_architecture.py +78 -0
- package/scripts/write_codecgc_decision.py +83 -0
- package/scripts/write_codecgc_explore.py +118 -0
- package/scripts/write_codecgc_guide.py +141 -0
- package/scripts/write_codecgc_learning.py +87 -0
- package/scripts/write_codecgc_libdoc.py +140 -0
- package/scripts/write_codecgc_refactor.py +78 -0
- package/scripts/write_codecgc_requirement.py +78 -0
- package/scripts/write_codecgc_review.py +291 -0
- package/scripts/write_codecgc_roadmap.py +122 -0
- 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
|
+
}
|