fix(c5b): persist human-review object changes + read content_before from parent commit
- C5A objects table: object_id nullable + reason_code column so HUMAN_REVIEW/ SECRET_DETECTED object changes (object_id=None) are persisted, not silently dropped (was: object_id TEXT NOT NULL, no reason_code field) - C5B poll_once: content_before now read from parent_sha (state BEFORE the change) instead of sha, so MODIFIED changes classify as CONTENT_UPDATE instead of being misclassified (Checker-Befund) - test_c5b: add test_modified_content_reads_parent regression test C5B 40/40, C5A 25/25, real-repo dry run: 233 object changes (109 IN_SCOPE with valid id, 124 HUMAN_REVIEW), idempotent.
This commit is contained in:
parent
091c1828c7
commit
ed374ac4ce
3 changed files with 37 additions and 12 deletions
|
|
@ -280,7 +280,7 @@ class C5AStore:
|
|||
CREATE TABLE IF NOT EXISTS objects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
commit_sha TEXT NOT NULL,
|
||||
object_id TEXT NOT NULL,
|
||||
object_id TEXT,
|
||||
path_before TEXT,
|
||||
path_after TEXT,
|
||||
operation TEXT NOT NULL,
|
||||
|
|
@ -290,6 +290,7 @@ class C5AStore:
|
|||
metadata_hash_after TEXT,
|
||||
representation TEXT,
|
||||
state TEXT,
|
||||
reason_code TEXT,
|
||||
UNIQUE(commit_sha, object_id, operation)
|
||||
)
|
||||
""")
|
||||
|
|
@ -472,7 +473,7 @@ class C5AStore:
|
|||
|
||||
# -- Objects ------------------------------------------------------------
|
||||
|
||||
def add_object_change(self, obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def add_object_change(self, obj: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Fügt eine Object-Change hinzu (idempotent per commit_sha+object_id+operation)."""
|
||||
if obj.get("operation") not in OPERATIONS:
|
||||
raise InvalidOperationError(f"Unbekannte Operation: {obj.get('operation')}")
|
||||
|
|
@ -481,22 +482,22 @@ class C5AStore:
|
|||
"""
|
||||
INSERT OR IGNORE INTO objects (commit_sha, object_id, path_before, path_after,
|
||||
operation, content_hash_before, content_hash_after,
|
||||
metadata_hash_before, metadata_hash_after, representation, state)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
metadata_hash_before, metadata_hash_after, representation, state, reason_code)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
obj["commit_sha"], obj["object_id"], obj.get("path_before"),
|
||||
obj["commit_sha"], obj.get("object_id"), obj.get("path_before"),
|
||||
obj.get("path_after"), obj["operation"],
|
||||
obj.get("content_hash_before"), obj.get("content_hash_after"),
|
||||
obj.get("metadata_hash_before"), obj.get("metadata_hash_after"),
|
||||
obj.get("representation"), obj.get("state"),
|
||||
obj.get("representation"), obj.get("state"), obj.get("reason_code"),
|
||||
),
|
||||
)
|
||||
return self.get_object_change(obj["commit_sha"], obj["object_id"], obj["operation"])
|
||||
return self.get_object_change(obj["commit_sha"], obj.get("object_id"), obj["operation"])
|
||||
|
||||
def get_object_change(self, commit_sha: str, object_id: str, operation: str) -> Optional[Dict[str, Any]]:
|
||||
def get_object_change(self, commit_sha: str, object_id: Optional[str], operation: str) -> Optional[Dict[str, Any]]:
|
||||
row = self._conn.execute(
|
||||
"SELECT * FROM objects WHERE commit_sha = ? AND object_id = ? AND operation = ?",
|
||||
"SELECT * FROM objects WHERE commit_sha = ? AND object_id IS ? AND operation = ?",
|
||||
(commit_sha, object_id, operation),
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
|
|
|||
|
|
@ -663,15 +663,18 @@ class C5BPoller:
|
|||
|
||||
# Diff gegen Parent
|
||||
changes = self.reader.diff_name_status(sha)
|
||||
parent_sha = meta.get("parent_sha")
|
||||
object_changes = []
|
||||
for ch in changes:
|
||||
path_before: Optional[str] = ch.get("path_before")
|
||||
path_after: Optional[str] = ch.get("path_after")
|
||||
# Inhalte read-only lesen
|
||||
# Inhalte read-only lesen.
|
||||
# content_before stammt aus dem PARENT-Commit (der Zustand VOR der
|
||||
# Aenderung), content_after aus dem aktuellen Commit (sha).
|
||||
content_before: Optional[str] = None
|
||||
content_after: Optional[str] = None
|
||||
if path_before:
|
||||
content_before = self.reader.file_content(sha, path_before)
|
||||
if path_before and parent_sha:
|
||||
content_before = self.reader.file_content(parent_sha, path_before)
|
||||
if path_after:
|
||||
content_after = self.reader.file_content(sha, path_after)
|
||||
|
||||
|
|
|
|||
|
|
@ -374,6 +374,27 @@ class TestC5BPoller(unittest.TestCase):
|
|||
finally:
|
||||
fx.cleanup()
|
||||
|
||||
def test_modified_content_reads_parent(self):
|
||||
"""Regression: content_before muss aus dem PARENT-Commit gelesen werden,
|
||||
nicht aus dem aktuellen Commit. Sonst wird ein MODIFIED-Change faelschlich
|
||||
als METADATA_UPDATE/kein CONTENT_UPDATE klassifiziert (Checker-Befund)."""
|
||||
fx = FixtureRepo()
|
||||
try:
|
||||
fx.write("modul-09.md", _fm("object/77b02661-d67b-4bae-b612-01d1287cea6b") + "body v1")
|
||||
fx.commit("c1")
|
||||
fx.write("modul-09.md", _fm("object/77b02661-d67b-4bae-b612-01d1287cea6b") + "body v2")
|
||||
sha2 = fx.commit("c2")
|
||||
store = self._new_store()
|
||||
poller = C5BPoller(store, fx.dir)
|
||||
r = poller.poll_once()
|
||||
self.assertEqual(r["commits_discovered"], 2)
|
||||
objs = store.list_object_changes(sha2)
|
||||
ops = [o["operation"] for o in objs]
|
||||
self.assertIn(OP_CONTENT_UPDATE, ops, f"erwartet CONTENT_UPDATE, habe {ops}")
|
||||
store.close()
|
||||
finally:
|
||||
fx.cleanup()
|
||||
|
||||
def test_lost_poll_recovery(self):
|
||||
fx = FixtureRepo()
|
||||
try:
|
||||
|
|
|
|||
Loading…
Reference in a new issue