From 05c5b86a74968217ce14d323fca7912145068144 Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 29 Sep 2026 10:55:55 +0200 Subject: [PATCH] fix(curation): record the real author of applied curation changes `_apply_patch` hard-coded `remote-curator` as the page-version author and link origin, so pages applied by the local curator were mislabelled. `apply_run` now takes `created_by` (default `remote-curator`, unchanged for remote runs, `sync apply` and conflict resolution); the local curator passes `local-curator`. Implemented by GLM 5.3 Flash; PostgreSQL tests rerun in review. Co-authored-by: Isaac --- CHANGELOG.md | 4 ++ src/wikibricks/curation/application.py | 24 ++++++-- src/wikibricks_curator/curator.py | 7 ++- tests/test_local_curator.py | 76 ++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03c66cb..b26f945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Label pages and links written by the local curator as `local-curator`. + ## 0.13.0 - 2026-09-29 - Added optional `wikibricks-curator propose` for local backlog curation. It diff --git a/src/wikibricks/curation/application.py b/src/wikibricks/curation/application.py index 0bb048d..e91a2ed 100644 --- a/src/wikibricks/curation/application.py +++ b/src/wikibricks/curation/application.py @@ -158,6 +158,7 @@ def _apply_patch( patch: dict[str, Any], *, force: bool, + created_by: str, ) -> UUID | None: operation = patch["operation"] proposal = patch["proposal"] @@ -172,7 +173,7 @@ def _apply_patch( proposal["title"], proposal["content"], page_type=proposal["page_type"], - created_by="remote-curator", + created_by=created_by, tags=proposal["tags"], source_ids=proposal["source_ids"], parent_id=proposal["parent_id"], @@ -187,7 +188,7 @@ def _apply_patch( proposal["title"], proposal["content"], page_type=proposal["page_type"], - created_by="remote-curator", + created_by=created_by, tags=proposal["tags"], source_ids=proposal["source_ids"], parent_id=proposal["parent_id"], @@ -210,7 +211,7 @@ def _apply_patch( proposal["title"], proposal["content"], page_type=proposal["page_type"], - created_by="remote-curator", + created_by=created_by, tags=proposal["tags"], source_ids=proposal["source_ids"], parent_id=proposal["parent_id"], @@ -234,13 +235,14 @@ def _apply_patch( conn.execute( "INSERT INTO links " "(link_id, source_page_id, target_page_id, link_type, origin, metadata, created_at) " - "VALUES (?, ?, ?, ?, 'remote-curator', ?, ?) " + "VALUES (?, ?, ?, ?, ?, ?, ?) " "ON CONFLICT (source_page_id, target_page_id, link_type) DO NOTHING", ( str(uuid4()), source["page_id"], target["page_id"], proposal["link_type"], + created_by, json.dumps(metadata, separators=(",", ":"), sort_keys=True), datetime.now(timezone.utc).isoformat(), ), @@ -251,13 +253,14 @@ def _apply_patch( conn.execute( "INSERT INTO links " "(link_id, source_page_id, target_page_id, link_type, origin, metadata) " - "VALUES (%s, %s, %s, %s, 'remote-curator', %s) " + "VALUES (%s, %s, %s, %s, %s, %s) " "ON CONFLICT (source_page_id, target_page_id, link_type) DO NOTHING", ( uuid4(), UUID(source["page_id"]), UUID(target["page_id"]), proposal["link_type"], + created_by, Jsonb(metadata), ), ) @@ -333,6 +336,7 @@ def _apply_group( *, force: bool, failpoint: Callable[[str], None], + created_by: str = "remote-curator", ) -> tuple[str, list[dict[str, Any]]]: processed = receipt_count(conn, patches) if processed == len(patches): @@ -386,7 +390,13 @@ def _apply_group( result_version_id=result_id, ) else: - result_id = _apply_patch(store, conn, patch, force=force) + result_id = _apply_patch( + store, + conn, + patch, + force=force, + created_by=created_by, + ) insert_receipt( conn, run_id, @@ -404,6 +414,7 @@ def apply_run( *, policy: str = "safe", failpoint: Callable[[str], None] | None = None, + created_by: str = "remote-curator", ) -> dict[str, Any]: if policy not in {"safe", "all"}: raise ValueError("curation policy must be 'safe' or 'all'") @@ -432,6 +443,7 @@ def apply_run( items, force=False, failpoint=trigger, + created_by=created_by, ) mark_run_applied(conn, run_id) results.append( diff --git a/src/wikibricks_curator/curator.py b/src/wikibricks_curator/curator.py index 2759177..558000f 100644 --- a/src/wikibricks_curator/curator.py +++ b/src/wikibricks_curator/curator.py @@ -222,7 +222,12 @@ def run_curator( store_manifest(store, manifest) result["run_id"] = str(run_id) if apply: - outcome = apply_run(store, run_id, policy="safe") + outcome = apply_run( + store, + run_id, + policy="safe", + created_by="local-curator", + ) result["counts"] = outcome["counts"] result["status"] = ( "applied" diff --git a/tests/test_local_curator.py b/tests/test_local_curator.py index f6d6aac..7c986c6 100644 --- a/tests/test_local_curator.py +++ b/tests/test_local_curator.py @@ -566,6 +566,82 @@ def chat(_prompt: str, request: dict, _schema: dict) -> dict: } +def test_run_curator_labels_created_page_and_applied_link(tmp_path: Path): + from wikibricks_curator.curator import run_curator + + store = _store(tmp_path) + store.write_page( + "topics/related-target", + "Related Target", + {"summary": "Label project target", "body": "The label project target already exists."}, + ) + with store.connection(write=True) as conn: + conn.execute( + "UPDATE pages SET updated_at = ? WHERE path = 'topics/related-target'", + (_recent_timestamp(4),), + ) + _backlog_project(store, project="label-project") + + calls = [] + + def chat(_prompt: str, request: dict, _schema: dict) -> dict: + calls.append(request) + if len(calls) == 1: + return {"proposals": [_proposal( + operation="create_page", + path=request["living_page"], + evidence_id=request["evidence"][0]["evidence_id"], + )]} + return { + "proposals": [{ + "group": "main", + "operation": "add_link", + "path": request["living_page"], + "target_path": "topics/related-target", + "link_type": "related", + "evidence_ids": [request["evidence"][0]["evidence_id"]], + "reason": "The pages cover related concepts.", + "risk_class": "low", + }] + } + + result = run_curator(store.database_path, chat=chat, projects=1) + assert result["projects"][0]["status"] == "applied" + + with store.connection(write=True) as conn: + conn.execute( + "UPDATE pages SET updated_at = ? WHERE path = ?", + (_recent_timestamp(3), "topics/label-project"), + ) + + _ingest( + store, + "label-project-second", + "/Users/u/work/label-project", + _recent_timestamp(1), + [SessionEvent("user-2", "user", "another durable fact")], + ) + + result = run_curator(store.database_path, chat=chat, projects=1) + assert result["projects"][0]["status"] == "applied" + assert result["projects"][0]["counts"] == {"applied": 1} + with store.connection() as conn: + version = conn.execute( + "SELECT created_by FROM page_versions " + "WHERE version_id = (SELECT current_version_id FROM pages WHERE path = ?)", + ("topics/label-project",), + ).fetchone() + link = conn.execute( + "SELECT origin FROM links WHERE source_page_id = " + "(SELECT page_id FROM pages WHERE path = ?) " + "AND target_page_id = (SELECT page_id FROM pages WHERE path = ?)", + ("topics/label-project", "topics/related-target"), + ).fetchone() + + assert tuple(version) == ("local-curator",) + assert tuple(link) == ("local-curator",) + + def test_shrink_guard_leaves_page_for_review( tmp_path: Path, monkeypatch: pytest.MonkeyPatch,