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 (
|
CREATE TABLE IF NOT EXISTS objects (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
commit_sha TEXT NOT NULL,
|
commit_sha TEXT NOT NULL,
|
||||||
object_id TEXT NOT NULL,
|
object_id TEXT,
|
||||||
path_before TEXT,
|
path_before TEXT,
|
||||||
path_after TEXT,
|
path_after TEXT,
|
||||||
operation TEXT NOT NULL,
|
operation TEXT NOT NULL,
|
||||||
|
|
@ -290,6 +290,7 @@ class C5AStore:
|
||||||
metadata_hash_after TEXT,
|
metadata_hash_after TEXT,
|
||||||
representation TEXT,
|
representation TEXT,
|
||||||
state TEXT,
|
state TEXT,
|
||||||
|
reason_code TEXT,
|
||||||
UNIQUE(commit_sha, object_id, operation)
|
UNIQUE(commit_sha, object_id, operation)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
@ -472,7 +473,7 @@ class C5AStore:
|
||||||
|
|
||||||
# -- Objects ------------------------------------------------------------
|
# -- 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)."""
|
"""Fügt eine Object-Change hinzu (idempotent per commit_sha+object_id+operation)."""
|
||||||
if obj.get("operation") not in OPERATIONS:
|
if obj.get("operation") not in OPERATIONS:
|
||||||
raise InvalidOperationError(f"Unbekannte Operation: {obj.get('operation')}")
|
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,
|
INSERT OR IGNORE INTO objects (commit_sha, object_id, path_before, path_after,
|
||||||
operation, content_hash_before, content_hash_after,
|
operation, content_hash_before, content_hash_after,
|
||||||
metadata_hash_before, metadata_hash_after, representation, state)
|
metadata_hash_before, metadata_hash_after, representation, state, reason_code)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
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("path_after"), obj["operation"],
|
||||||
obj.get("content_hash_before"), obj.get("content_hash_after"),
|
obj.get("content_hash_before"), obj.get("content_hash_after"),
|
||||||
obj.get("metadata_hash_before"), obj.get("metadata_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(
|
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),
|
(commit_sha, object_id, operation),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
|
||||||
|
|
@ -663,15 +663,18 @@ class C5BPoller:
|
||||||
|
|
||||||
# Diff gegen Parent
|
# Diff gegen Parent
|
||||||
changes = self.reader.diff_name_status(sha)
|
changes = self.reader.diff_name_status(sha)
|
||||||
|
parent_sha = meta.get("parent_sha")
|
||||||
object_changes = []
|
object_changes = []
|
||||||
for ch in changes:
|
for ch in changes:
|
||||||
path_before: Optional[str] = ch.get("path_before")
|
path_before: Optional[str] = ch.get("path_before")
|
||||||
path_after: Optional[str] = ch.get("path_after")
|
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_before: Optional[str] = None
|
||||||
content_after: Optional[str] = None
|
content_after: Optional[str] = None
|
||||||
if path_before:
|
if path_before and parent_sha:
|
||||||
content_before = self.reader.file_content(sha, path_before)
|
content_before = self.reader.file_content(parent_sha, path_before)
|
||||||
if path_after:
|
if path_after:
|
||||||
content_after = self.reader.file_content(sha, path_after)
|
content_after = self.reader.file_content(sha, path_after)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -374,6 +374,27 @@ class TestC5BPoller(unittest.TestCase):
|
||||||
finally:
|
finally:
|
||||||
fx.cleanup()
|
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):
|
def test_lost_poll_recovery(self):
|
||||||
fx = FixtureRepo()
|
fx = FixtureRepo()
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue