From 7b4b5046aa136943124df965ba0cb630f8238b9c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:17:52 +0900 Subject: [PATCH 01/44] test(naming): define post keymen backfill identifiers --- ...st_post_keymen_backfill_naming_contract.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_post_keymen_backfill_naming_contract.py diff --git a/tests/test_post_keymen_backfill_naming_contract.py b/tests/test_post_keymen_backfill_naming_contract.py new file mode 100644 index 000000000..856ecfd11 --- /dev/null +++ b/tests/test_post_keymen_backfill_naming_contract.py @@ -0,0 +1,34 @@ +"""Naming contract for the bounded post-Keyman operator backfill.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +BACKFILL_SCRIPT = REPOSITORY_ROOT / "scripts" / "backfill_post_keymen.py" + + +def test_post_keymen_backfill_uses_bounded_operation_name() -> None: + """Require the operator coroutine to name the operation it owns.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + async_functions = { + node.name: node + for node in ast.walk(syntax_tree) + if isinstance(node, ast.AsyncFunctionDef) + } + + assert "_run_post_keymen_backfill" in async_functions + assert "_run" not in async_functions + operation = async_functions["_run_post_keymen_backfill"] + assert operation.args.args[0].arg == "backfill_arguments" + + +def test_post_keymen_backfill_main_uses_semantic_parsed_arguments() -> None: + """Keep parsed command-line arguments explicit at the script boundary.""" + source_text = BACKFILL_SCRIPT.read_text(encoding="utf-8") + + assert "backfill_arguments = parser.parse_args()" in source_text + assert "asyncio.run(_run_post_keymen_backfill(backfill_arguments))" in source_text + assert "args = parser.parse_args()" not in source_text From 76335b36e68c5f42b46538c81d57e39b0b54599a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:18:48 +0900 Subject: [PATCH 02/44] refactor(naming): name post keymen backfill operation --- scripts/backfill_post_keymen.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 59e2efd6c..b7f6a0fad 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -159,8 +159,11 @@ async def _select_posts( ) -async def _run(args: argparse.Namespace) -> dict[str, object]: - if args.post_id and args.all: +async def _run_post_keymen_backfill( + backfill_arguments: argparse.Namespace, +) -> dict[str, object]: + """Execute one bounded post-Keyman backfill operation.""" + if backfill_arguments.post_id and backfill_arguments.all: raise ValueError("--post-id and --all cannot be combined") base_url, api_key = _orchestrator_config() settings = load_settings() @@ -171,19 +174,25 @@ async def _run(args: argparse.Namespace) -> dict[str, object]: resolution_client = _organization_name_resolution_client() verification_client = _relation_verification_client() hierarchy_client = _corporate_hierarchy_inference_client() - limit = 1 if args.post_id or not args.all else args.limit + limit = ( + 1 + if backfill_arguments.post_id or not backfill_arguments.all + else backfill_arguments.limit + ) pool = await asyncpg.create_pool(settings.database_url, min_size=1, max_size=1) try: async with pool.acquire() as conn: - rows = await _select_posts(conn, limit=limit, post_id=args.post_id) + rows = await _select_posts( + conn, limit=limit, post_id=backfill_arguments.post_id + ) failures: Counter[str] = Counter() processed = 0 mention_count = 0 for row in rows: post_id = str(row["post_id"]) try: - async with asyncio.timeout(args.post_timeout): + async with asyncio.timeout(backfill_arguments.post_timeout): with use_llm_metadata(build_post_llm_metadata(post_id, dict(row))): normalized = normalize_post_body(row["post_body"] or "", vision_client) context_hints = await _load_post_semantic_hints(conn, post_id) @@ -227,12 +236,18 @@ def main() -> None: default=240.0, help="Maximum seconds per post including provider calls (default: 240)", ) - args = parser.parse_args() - if args.limit < 1: + backfill_arguments = parser.parse_args() + if backfill_arguments.limit < 1: parser.error("--limit must be positive") - if args.post_timeout <= 0: + if backfill_arguments.post_timeout <= 0: parser.error("--post-timeout must be positive") - print(json.dumps(asyncio.run(_run(args)), ensure_ascii=False, sort_keys=True)) + print( + json.dumps( + asyncio.run(_run_post_keymen_backfill(backfill_arguments)), + ensure_ascii=False, + sort_keys=True, + ) + ) if __name__ == "__main__": From b8897a282e884b2e3420141db4c71c247153df8f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:20:26 +0900 Subject: [PATCH 03/44] test(naming): define channel estimation operation name --- ...eight_estimation_script_naming_contract.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_channel_weight_estimation_script_naming_contract.py diff --git a/tests/test_channel_weight_estimation_script_naming_contract.py b/tests/test_channel_weight_estimation_script_naming_contract.py new file mode 100644 index 000000000..09804cff0 --- /dev/null +++ b/tests/test_channel_weight_estimation_script_naming_contract.py @@ -0,0 +1,34 @@ +"""Naming contract for the channel-weight estimation operator script.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +ESTIMATION_SCRIPT = REPOSITORY_ROOT / "scripts" / "estimate_channel_weights.py" + + +def test_channel_weight_estimation_uses_bounded_operation_name() -> None: + """Require the async operator entry point to state the estimation it runs.""" + syntax_tree = ast.parse(ESTIMATION_SCRIPT.read_text(encoding="utf-8")) + async_functions = { + node.name: node + for node in ast.walk(syntax_tree) + if isinstance(node, ast.AsyncFunctionDef) + } + + assert "_run_channel_weight_estimation" in async_functions + assert "_run" not in async_functions + operation = async_functions["_run_channel_weight_estimation"] + assert operation.args.args[0].arg == "estimation_arguments" + + +def test_channel_weight_estimation_main_uses_semantic_parsed_arguments() -> None: + """Keep parsed command-line arguments explicit at the script boundary.""" + source_text = ESTIMATION_SCRIPT.read_text(encoding="utf-8") + + assert "estimation_arguments = parser.parse_args()" in source_text + assert "asyncio.run(_run_channel_weight_estimation(estimation_arguments))" in source_text + assert "args = parser.parse_args()" not in source_text From 6fa817f9253b5f23acf00b7967bfc8a19cb161d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:21:23 +0900 Subject: [PATCH 04/44] refactor(naming): name channel estimation operation --- scripts/estimate_channel_weights.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/estimate_channel_weights.py b/scripts/estimate_channel_weights.py index b8ab9534f..20997ea1f 100644 --- a/scripts/estimate_channel_weights.py +++ b/scripts/estimate_channel_weights.py @@ -173,7 +173,10 @@ async def persist_estimate( return estimation_run_id -async def _run(args: argparse.Namespace) -> dict[str, object]: +async def _run_channel_weight_estimation( + estimation_arguments: argparse.Namespace, +) -> dict[str, object]: + """Execute one bounded lineage channel-weight estimation run.""" settings = load_settings() # Short-lived fetch connection; nothing stays open while fitting. conn = await asyncpg.connect(settings.database_url) @@ -184,7 +187,7 @@ async def _run(args: argparse.Namespace) -> dict[str, object]: "secondary_grouping_key " f"from source_post where {SOURCE_POST_ELIGIBILITY_SQL.format(alias='source_post')} " "order by created_at, post_id limit $1::bigint", - args.post_limit, + estimation_arguments.post_limit, ) finally: await conn.close() @@ -206,7 +209,7 @@ async def _run(args: argparse.Namespace) -> dict[str, object]: "named condition" ) estimation_run_id = None - if not args.dry_run: + if not estimation_arguments.dry_run: conn = await asyncpg.connect(settings.database_url) try: estimation_run_id = await persist_estimate( @@ -227,7 +230,7 @@ async def _run(args: argparse.Namespace) -> dict[str, object]: "estimation_run_id": estimation_run_id, "source_snapshot_sha256": snapshot_sha256, "knowledge_cutoff": knowledge_cutoff.isoformat(), - "persisted": not args.dry_run, + "persisted": not estimation_arguments.dry_run, "activation": ( "blocked_until_anchor_authorized (ADR 0200 point 3): the " "product loader refuses every anchor method today, so these " @@ -250,10 +253,16 @@ def main() -> None: action="store_true", help="Estimate and report, but persist nothing", ) - args = parser.parse_args() - if args.post_limit < 1: + estimation_arguments = parser.parse_args() + if estimation_arguments.post_limit < 1: parser.error("--post-limit must be positive") - print(json.dumps(asyncio.run(_run(args)), ensure_ascii=False, sort_keys=True)) + print( + json.dumps( + asyncio.run(_run_channel_weight_estimation(estimation_arguments)), + ensure_ascii=False, + sort_keys=True, + ) + ) if __name__ == "__main__": From 0247bf38dcfc4ab7131694d864d4180d458c2916 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:21:44 +0900 Subject: [PATCH 05/44] test(naming): define thread group backfill identifiers --- ...t_thread_group_backfill_naming_contract.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_thread_group_backfill_naming_contract.py diff --git a/tests/test_thread_group_backfill_naming_contract.py b/tests/test_thread_group_backfill_naming_contract.py new file mode 100644 index 000000000..8b88fcf75 --- /dev/null +++ b/tests/test_thread_group_backfill_naming_contract.py @@ -0,0 +1,34 @@ +"""Naming contract for the thread-group-key backfill operator script.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +BACKFILL_SCRIPT = REPOSITORY_ROOT / "scripts" / "backfill_thread_group_keys.py" + + +def test_thread_group_backfill_uses_bounded_operation_name() -> None: + """Require the pooled operator coroutine to name the backfill it executes.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + async_functions = { + node.name: node + for node in ast.walk(syntax_tree) + if isinstance(node, ast.AsyncFunctionDef) + } + + assert "_run_thread_group_key_backfill" in async_functions + assert "_run" not in async_functions + operation = async_functions["_run_thread_group_key_backfill"] + assert operation.args.args[0].arg == "backfill_arguments" + + +def test_thread_group_backfill_main_uses_semantic_parsed_arguments() -> None: + """Keep parsed command-line arguments explicit at the script boundary.""" + source_text = BACKFILL_SCRIPT.read_text(encoding="utf-8") + + assert "backfill_arguments = parser.parse_args()" in source_text + assert "asyncio.run(_run_thread_group_key_backfill(backfill_arguments))" in source_text + assert "args = parser.parse_args()" not in source_text From b83abd7ad3fb12392e7782106a8fbeaaa68cb55d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:22:15 +0900 Subject: [PATCH 06/44] refactor(naming): name thread group backfill operation --- scripts/backfill_thread_group_keys.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/backfill_thread_group_keys.py b/scripts/backfill_thread_group_keys.py index 8352c1ada..d50f4853e 100644 --- a/scripts/backfill_thread_group_keys.py +++ b/scripts/backfill_thread_group_keys.py @@ -132,14 +132,18 @@ def __init__(self, project_evidence: int, cleared: int) -> None: self.cleared = cleared -async def _run(args: argparse.Namespace) -> dict[str, object]: - """Execute one pooled backfill and convert dry-run rollback into counts.""" +async def _run_thread_group_key_backfill( + backfill_arguments: argparse.Namespace, +) -> dict[str, object]: + """Execute one pooled thread-group-key backfill and report aggregate counts.""" settings = load_settings() pool = await asyncpg.create_pool(settings.database_url, min_size=1, max_size=1) try: async with pool.acquire() as conn: try: - counts = await backfill_thread_group_keys(conn, dry_run=args.dry_run) + counts = await backfill_thread_group_keys( + conn, dry_run=backfill_arguments.dry_run + ) return {**counts, "dry_run": False} except _RollbackDryRun as rolled_back: return { @@ -159,8 +163,14 @@ def main() -> None: action="store_true", help="Report counts without writing (rolls back the transaction)", ) - args = parser.parse_args() - print(json.dumps(asyncio.run(_run(args)), ensure_ascii=False, sort_keys=True)) + backfill_arguments = parser.parse_args() + print( + json.dumps( + asyncio.run(_run_thread_group_key_backfill(backfill_arguments)), + ensure_ascii=False, + sort_keys=True, + ) + ) if __name__ == "__main__": From e15c43f31ce21e42fb95757a773e577ef0336d34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:51:42 +0900 Subject: [PATCH 07/44] test(operator): reject non-finite Keyman backfill timeouts --- ...t_post_keymen_backfill_timeout_contract.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_post_keymen_backfill_timeout_contract.py diff --git a/tests/test_post_keymen_backfill_timeout_contract.py b/tests/test_post_keymen_backfill_timeout_contract.py new file mode 100644 index 000000000..f0951fd74 --- /dev/null +++ b/tests/test_post_keymen_backfill_timeout_contract.py @@ -0,0 +1,64 @@ +"""Safety contract for the bounded post-Keyman operator timeout.""" + +from __future__ import annotations + +import ast +import math +from pathlib import Path + +import pytest + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +BACKFILL_SCRIPT = REPOSITORY_ROOT / "scripts" / "backfill_post_keymen.py" + + +def _load_timeout_validator(): + """Load only the pure timeout validator without importing operator dependencies.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + validator = next( + ( + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) + and node.name == "_post_timeout_is_valid" + ), + None, + ) + assert validator is not None, "operator must expose a pure timeout admission check" + namespace = {"math": math} + module = ast.fix_missing_locations(ast.Module(body=[validator], type_ignores=[])) + exec(compile(module, str(BACKFILL_SCRIPT), "exec"), namespace) + return namespace["_post_timeout_is_valid"] + + +@pytest.mark.parametrize("invalid_timeout", [0.0, -1.0, math.nan, math.inf, -math.inf]) +def test_post_keymen_backfill_rejects_non_positive_or_non_finite_timeout( + invalid_timeout: float, +) -> None: + validator = _load_timeout_validator() + + assert validator(invalid_timeout) is False + + +def test_post_keymen_backfill_accepts_positive_finite_timeout() -> None: + validator = _load_timeout_validator() + + assert validator(0.001) is True + assert validator(240.0) is True + + +def test_main_routes_timeout_through_the_admission_check() -> None: + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + main_function = next( + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) and node.name == "main" + ) + + assert any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_post_timeout_is_valid" + for node in ast.walk(main_function) + ) From 36d514b59e8b45dc84bcae0e1b2bfdb9ee3bf08a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:52:17 +0900 Subject: [PATCH 08/44] fix(operator): reject non-finite Keyman backfill timeouts --- scripts/backfill_post_keymen.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index b7f6a0fad..47f34fb08 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -10,6 +10,7 @@ import argparse import asyncio import json +import math import os import sys from collections import Counter @@ -48,6 +49,11 @@ def _orchestrator_config() -> tuple[str, str]: return base_url, api_key +def _post_timeout_is_valid(post_timeout: float) -> bool: + """Return whether an operator timeout is finite and strictly positive.""" + return math.isfinite(post_timeout) and post_timeout > 0 + + async def _select_posts( conn: asyncpg.Connection, *, limit: int, post_id: str | None ) -> list[asyncpg.Record]: @@ -239,8 +245,8 @@ def main() -> None: backfill_arguments = parser.parse_args() if backfill_arguments.limit < 1: parser.error("--limit must be positive") - if backfill_arguments.post_timeout <= 0: - parser.error("--post-timeout must be positive") + if not _post_timeout_is_valid(backfill_arguments.post_timeout): + parser.error("--post-timeout must be finite and positive") print( json.dumps( asyncio.run(_run_post_keymen_backfill(backfill_arguments)), From c3bf8505a8de0a2dca80ecf7f510abe7035e2602 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 05:15:15 +0900 Subject: [PATCH 09/44] test(operator): reject blank post Keyman selector --- ...t_post_keymen_backfill_post_id_contract.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/test_post_keymen_backfill_post_id_contract.py diff --git a/tests/test_post_keymen_backfill_post_id_contract.py b/tests/test_post_keymen_backfill_post_id_contract.py new file mode 100644 index 000000000..979ee4270 --- /dev/null +++ b/tests/test_post_keymen_backfill_post_id_contract.py @@ -0,0 +1,66 @@ +"""Identity admission contract for the bounded post-Keyman operator selector.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +BACKFILL_SCRIPT = REPOSITORY_ROOT / "scripts" / "backfill_post_keymen.py" + + +def _load_post_id_validator(): + """Load only the pure post-id validator without importing operator dependencies.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + validator = next( + ( + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) + and node.name == "_post_id_is_valid" + ), + None, + ) + assert validator is not None, "operator must expose a pure post-id admission check" + namespace: dict[str, object] = {} + module = ast.fix_missing_locations(ast.Module(body=[validator], type_ignores=[])) + exec(compile(module, str(BACKFILL_SCRIPT), "exec"), namespace) + return namespace["_post_id_is_valid"] + + +@pytest.mark.parametrize( + "invalid_post_id", + ["", " ", "\t", "post-123 ", " post-123", "post-123\n"], +) +def test_post_keymen_backfill_rejects_blank_or_padded_explicit_post_id( + invalid_post_id: str, +) -> None: + validator = _load_post_id_validator() + + assert validator(invalid_post_id) is False + + +def test_post_keymen_backfill_accepts_absent_or_exact_post_id() -> None: + validator = _load_post_id_validator() + + assert validator(None) is True + assert validator("post-123") is True + + +def test_main_routes_post_id_through_the_admission_check() -> None: + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + main_function = next( + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) and node.name == "main" + ) + + assert any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_post_id_is_valid" + for node in ast.walk(main_function) + ) From ab7807b890c349cbe5550e3637a97bfc50751d0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 05:16:00 +0900 Subject: [PATCH 10/44] fix(operator): fail closed on ambiguous post Keyman selector --- scripts/backfill_post_keymen.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 47f34fb08..dd4cfcbd0 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -54,6 +54,11 @@ def _post_timeout_is_valid(post_timeout: float) -> bool: return math.isfinite(post_timeout) and post_timeout > 0 +def _post_id_is_valid(post_id: str | None) -> bool: + """Return whether an optional explicit post identity is nonblank and unpadded.""" + return post_id is None or bool(post_id) and post_id == post_id.strip() + + async def _select_posts( conn: asyncpg.Connection, *, limit: int, post_id: str | None ) -> list[asyncpg.Record]: @@ -169,6 +174,8 @@ async def _run_post_keymen_backfill( backfill_arguments: argparse.Namespace, ) -> dict[str, object]: """Execute one bounded post-Keyman backfill operation.""" + if not _post_id_is_valid(backfill_arguments.post_id): + raise ValueError("--post-id must be nonblank and unpadded") if backfill_arguments.post_id and backfill_arguments.all: raise ValueError("--post-id and --all cannot be combined") base_url, api_key = _orchestrator_config() @@ -247,6 +254,8 @@ def main() -> None: parser.error("--limit must be positive") if not _post_timeout_is_valid(backfill_arguments.post_timeout): parser.error("--post-timeout must be finite and positive") + if not _post_id_is_valid(backfill_arguments.post_id): + parser.error("--post-id must be nonblank and unpadded") print( json.dumps( asyncio.run(_run_post_keymen_backfill(backfill_arguments)), From f2ddd8372b4f0d6026cb6a19186a491ac87f86e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 05:45:35 +0900 Subject: [PATCH 11/44] test(operator): reject non-string post selectors --- tests/test_post_keymen_backfill_post_id_contract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_post_keymen_backfill_post_id_contract.py b/tests/test_post_keymen_backfill_post_id_contract.py index 979ee4270..f3e5cfa9d 100644 --- a/tests/test_post_keymen_backfill_post_id_contract.py +++ b/tests/test_post_keymen_backfill_post_id_contract.py @@ -43,6 +43,16 @@ def test_post_keymen_backfill_rejects_blank_or_padded_explicit_post_id( assert validator(invalid_post_id) is False +@pytest.mark.parametrize("invalid_post_id", [7, True, ["post-123"], {"id": "post-123"}]) +def test_post_keymen_backfill_rejects_non_string_explicit_post_id( + invalid_post_id: object, +) -> None: + """Do not let truthy transport values reach string operations or identity lookup.""" + validator = _load_post_id_validator() + + assert validator(invalid_post_id) is False + + def test_post_keymen_backfill_accepts_absent_or_exact_post_id() -> None: validator = _load_post_id_validator() From 748888d099db1d46939c252912f01ea95268a6c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 05:46:27 +0900 Subject: [PATCH 12/44] test(operator): close programmatic timeout admission gap --- ...t_post_keymen_backfill_timeout_contract.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_post_keymen_backfill_timeout_contract.py b/tests/test_post_keymen_backfill_timeout_contract.py index f0951fd74..6fe1caffa 100644 --- a/tests/test_post_keymen_backfill_timeout_contract.py +++ b/tests/test_post_keymen_backfill_timeout_contract.py @@ -41,6 +41,16 @@ def test_post_keymen_backfill_rejects_non_positive_or_non_finite_timeout( assert validator(invalid_timeout) is False +@pytest.mark.parametrize("invalid_timeout", [True, False, "240", None]) +def test_post_keymen_backfill_rejects_non_numeric_or_boolean_timeout( + invalid_timeout: object, +) -> None: + """Reject malformed programmatic values instead of relying on argparse coercion.""" + validator = _load_timeout_validator() + + assert validator(invalid_timeout) is False + + def test_post_keymen_backfill_accepts_positive_finite_timeout() -> None: validator = _load_timeout_validator() @@ -62,3 +72,23 @@ def test_main_routes_timeout_through_the_admission_check() -> None: and node.func.id == "_post_timeout_is_valid" for node in ast.walk(main_function) ) + + +def test_programmatic_runner_revalidates_timeout_before_external_work() -> None: + """Keep direct callers behind the same bounded timeout admission contract.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + runner = next( + node + for node in syntax_tree.body + if isinstance(node, ast.AsyncFunctionDef) + and node.name == "_run_post_keymen_backfill" + ) + + calls = [ + node + for node in ast.walk(runner) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_post_timeout_is_valid" + ] + assert calls, "programmatic runner must revalidate timeout before provider/database work" From fc757f1a37cf13a3b361b40b75ea16515fdbff30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 05:47:32 +0900 Subject: [PATCH 13/44] fix(operator): fail closed on malformed direct-call admissions --- scripts/backfill_post_keymen.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index dd4cfcbd0..2ef2db4ba 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -49,14 +49,23 @@ def _orchestrator_config() -> tuple[str, str]: return base_url, api_key -def _post_timeout_is_valid(post_timeout: float) -> bool: - """Return whether an operator timeout is finite and strictly positive.""" - return math.isfinite(post_timeout) and post_timeout > 0 +def _post_timeout_is_valid(post_timeout: object) -> bool: + """Return whether an operator timeout is a finite, strictly positive number.""" + return ( + type(post_timeout) in (int, float) + and math.isfinite(post_timeout) + and post_timeout > 0 + ) -def _post_id_is_valid(post_id: str | None) -> bool: - """Return whether an optional explicit post identity is nonblank and unpadded.""" - return post_id is None or bool(post_id) and post_id == post_id.strip() +def _post_id_is_valid(post_id: object) -> bool: + """Return whether an optional explicit post identity is exact canonical text.""" + return ( + post_id is None + or type(post_id) is str + and bool(post_id) + and post_id == post_id.strip() + ) async def _select_posts( @@ -174,6 +183,8 @@ async def _run_post_keymen_backfill( backfill_arguments: argparse.Namespace, ) -> dict[str, object]: """Execute one bounded post-Keyman backfill operation.""" + if not _post_timeout_is_valid(backfill_arguments.post_timeout): + raise ValueError("--post-timeout must be finite and positive") if not _post_id_is_valid(backfill_arguments.post_id): raise ValueError("--post-id must be nonblank and unpadded") if backfill_arguments.post_id and backfill_arguments.all: From 0173ad3e8f873fa4bbf7a842b2bb503ba1ccb5ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 05:58:11 +0900 Subject: [PATCH 14/44] fix(ddd): retain orchestrator owner boundary in operator repair --- scripts/backfill_post_keymen.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 2ef2db4ba..77bff5b19 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -37,15 +37,15 @@ from lineageweave.post_content_normalization import normalize_post_body -def _first_env(*names: str) -> str: - return next((os.environ.get(name, "").strip() for name in names if os.environ.get(name, "").strip()), "") - - def _orchestrator_config() -> tuple[str, str]: - base_url = _first_env("ORCHESTRATOR_BASE_URL", "LLM_GATEWAY_API_URL", "LLM_GATEWAY_URL") - api_key = _first_env("ORCHESTRATOR_API_KEY", "LLM_GATEWAY_API_KEY") + """Return the published contextual-orchestrator consumer endpoint and bearer.""" + base_url = os.environ.get("ORCHESTRATOR_BASE_URL", "").strip() + api_key = os.environ.get("ORCHESTRATOR_API_KEY", "").strip() if not base_url or not api_key: - raise RuntimeError("contextual-orchestrator gateway configuration is unavailable") + raise RuntimeError( + "set ORCHESTRATOR_BASE_URL and ORCHESTRATOR_API_KEY to reach " + "contextual-orchestrator" + ) return base_url, api_key From 25ca75981659d1a8ff8c3a462e45978646769c3a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:04:06 +0900 Subject: [PATCH 15/44] test(operator): fail closed on malformed backfill limit --- ...est_post_keymen_backfill_limit_contract.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/test_post_keymen_backfill_limit_contract.py diff --git a/tests/test_post_keymen_backfill_limit_contract.py b/tests/test_post_keymen_backfill_limit_contract.py new file mode 100644 index 000000000..9c96a69db --- /dev/null +++ b/tests/test_post_keymen_backfill_limit_contract.py @@ -0,0 +1,82 @@ +"""Safety contract for the bounded post-Keyman operator batch limit.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +BACKFILL_SCRIPT = REPOSITORY_ROOT / "scripts" / "backfill_post_keymen.py" + + +def _load_limit_validator(): + """Load only the pure limit validator without importing operator dependencies.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + validator = next( + ( + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) + and node.name == "_post_limit_is_valid" + ), + None, + ) + assert validator is not None, "operator must expose a pure batch-limit admission check" + namespace: dict[str, object] = {} + module = ast.fix_missing_locations(ast.Module(body=[validator], type_ignores=[])) + exec(compile(module, str(BACKFILL_SCRIPT), "exec"), namespace) + return namespace["_post_limit_is_valid"] + + +@pytest.mark.parametrize("invalid_limit", [0, -1, True, False, 1.0, "1", None]) +def test_post_keymen_backfill_rejects_malformed_batch_limit(invalid_limit: object) -> None: + """Reject malformed direct-call limits instead of relying on argparse coercion.""" + validator = _load_limit_validator() + + assert validator(invalid_limit) is False + + +def test_post_keymen_backfill_accepts_positive_integer_batch_limit() -> None: + validator = _load_limit_validator() + + assert validator(1) is True + assert validator(100) is True + + +def test_main_routes_limit_through_the_admission_check() -> None: + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + main_function = next( + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) and node.name == "main" + ) + + assert any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_post_limit_is_valid" + for node in ast.walk(main_function) + ) + + +def test_programmatic_runner_revalidates_limit_before_external_work() -> None: + """Keep direct callers behind the same bounded batch admission contract.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + runner = next( + node + for node in syntax_tree.body + if isinstance(node, ast.AsyncFunctionDef) + and node.name == "_run_post_keymen_backfill" + ) + + calls = [ + node + for node in ast.walk(runner) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_post_limit_is_valid" + ] + assert calls, "programmatic runner must revalidate limit before provider/database work" From 23387d7531de4d2cc5dcce46dd3d06b2a6f26e54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:04:43 +0900 Subject: [PATCH 16/44] fix(operator): validate programmatic backfill limit --- scripts/backfill_post_keymen.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 77bff5b19..6851bee97 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -58,6 +58,11 @@ def _post_timeout_is_valid(post_timeout: object) -> bool: ) +def _post_limit_is_valid(post_limit: object) -> bool: + """Return whether a batch limit is an exact, strictly positive integer.""" + return type(post_limit) is int and post_limit > 0 + + def _post_id_is_valid(post_id: object) -> bool: """Return whether an optional explicit post identity is exact canonical text.""" return ( @@ -185,6 +190,8 @@ async def _run_post_keymen_backfill( """Execute one bounded post-Keyman backfill operation.""" if not _post_timeout_is_valid(backfill_arguments.post_timeout): raise ValueError("--post-timeout must be finite and positive") + if not _post_limit_is_valid(backfill_arguments.limit): + raise ValueError("--limit must be a positive integer") if not _post_id_is_valid(backfill_arguments.post_id): raise ValueError("--post-id must be nonblank and unpadded") if backfill_arguments.post_id and backfill_arguments.all: @@ -261,8 +268,8 @@ def main() -> None: help="Maximum seconds per post including provider calls (default: 240)", ) backfill_arguments = parser.parse_args() - if backfill_arguments.limit < 1: - parser.error("--limit must be positive") + if not _post_limit_is_valid(backfill_arguments.limit): + parser.error("--limit must be a positive integer") if not _post_timeout_is_valid(backfill_arguments.post_timeout): parser.error("--post-timeout must be finite and positive") if not _post_id_is_valid(backfill_arguments.post_id): From bd42b8bdaaf58a4b75c1f3a7b8ac87395fd79ed1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:05:34 +0900 Subject: [PATCH 17/44] docs(adr): record Keyman batch-limit admission --- docs/adr/0082-bounded-keyman-backfill.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index 397f18df7..ea755544e 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -27,7 +27,11 @@ It will: reconciliation; - carry `build_post_llm_metadata` and `use_llm_metadata` across all LLM/VISION calls for one post, yielding the same deterministic post session id; -- default to one post and require explicit `--all --limit N` for a batch. +- default to one post and require explicit `--all --limit N` for a batch; +- admit a batch limit only as an exact, strictly positive integer and apply the + same check to direct programmatic runner calls before gateway or database + work, so booleans and other transport-shaped values cannot silently become + a batch size; - enforce a per-post timeout, returning a typed failure count instead of allowing a provider workflow to hold an operator process indefinitely. From aace26ff5d870e98b51defed678384d32f014006 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:07:28 +0900 Subject: [PATCH 18/44] test(operator): reject malformed batch selector --- ...keymen_backfill_batch_selector_contract.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_post_keymen_backfill_batch_selector_contract.py diff --git a/tests/test_post_keymen_backfill_batch_selector_contract.py b/tests/test_post_keymen_backfill_batch_selector_contract.py new file mode 100644 index 000000000..d98e45e9c --- /dev/null +++ b/tests/test_post_keymen_backfill_batch_selector_contract.py @@ -0,0 +1,68 @@ +"""Safety contract for the post-Keyman operator batch selector.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +BACKFILL_SCRIPT = REPOSITORY_ROOT / "scripts" / "backfill_post_keymen.py" + + +def _load_batch_selector_validator(): + """Load only the pure batch-selector validator without operator dependencies.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + validator = next( + ( + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) + and node.name == "_post_all_is_valid" + ), + None, + ) + assert validator is not None, "operator must expose a pure batch-selector admission check" + namespace: dict[str, object] = {} + module = ast.fix_missing_locations(ast.Module(body=[validator], type_ignores=[])) + exec(compile(module, str(BACKFILL_SCRIPT), "exec"), namespace) + return namespace["_post_all_is_valid"] + + +def test_post_keymen_backfill_accepts_boolean_batch_selector() -> None: + validator = _load_batch_selector_validator() + + assert validator(False) is True + assert validator(True) is True + + +@pytest.mark.parametrize("invalid_selector", [0, 1, "false", "true", None, [], {}]) +def test_post_keymen_backfill_rejects_non_boolean_batch_selector( + invalid_selector: object, +) -> None: + """Reject transport-shaped values whose truthiness could select batch mode.""" + validator = _load_batch_selector_validator() + + assert validator(invalid_selector) is False + + +def test_programmatic_runner_revalidates_batch_selector_before_external_work() -> None: + """Keep direct callers behind the same selector-mode admission boundary.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + runner = next( + node + for node in syntax_tree.body + if isinstance(node, ast.AsyncFunctionDef) + and node.name == "_run_post_keymen_backfill" + ) + + calls = [ + node + for node in ast.walk(runner) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_post_all_is_valid" + ] + assert calls, "programmatic runner must revalidate batch selector before external work" From e9934ba08398d222098dfc14ccd768343c0e6902 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:08:05 +0900 Subject: [PATCH 19/44] fix(operator): validate programmatic batch selector --- scripts/backfill_post_keymen.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 6851bee97..09986629d 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -63,6 +63,11 @@ def _post_limit_is_valid(post_limit: object) -> bool: return type(post_limit) is int and post_limit > 0 +def _post_all_is_valid(post_all: object) -> bool: + """Return whether the batch-mode selector is an exact boolean.""" + return type(post_all) is bool + + def _post_id_is_valid(post_id: object) -> bool: """Return whether an optional explicit post identity is exact canonical text.""" return ( @@ -192,6 +197,8 @@ async def _run_post_keymen_backfill( raise ValueError("--post-timeout must be finite and positive") if not _post_limit_is_valid(backfill_arguments.limit): raise ValueError("--limit must be a positive integer") + if not _post_all_is_valid(backfill_arguments.all): + raise ValueError("--all must be a boolean selector") if not _post_id_is_valid(backfill_arguments.post_id): raise ValueError("--post-id must be nonblank and unpadded") if backfill_arguments.post_id and backfill_arguments.all: @@ -270,6 +277,8 @@ def main() -> None: backfill_arguments = parser.parse_args() if not _post_limit_is_valid(backfill_arguments.limit): parser.error("--limit must be a positive integer") + if not _post_all_is_valid(backfill_arguments.all): + parser.error("--all must be a boolean selector") if not _post_timeout_is_valid(backfill_arguments.post_timeout): parser.error("--post-timeout must be finite and positive") if not _post_id_is_valid(backfill_arguments.post_id): From 8697d9379c06f2ae2918690f6b23c7d8b7c481ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:08:27 +0900 Subject: [PATCH 20/44] docs(adr): record batch selector admission --- docs/adr/0082-bounded-keyman-backfill.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index ea755544e..858944e05 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -32,6 +32,9 @@ It will: same check to direct programmatic runner calls before gateway or database work, so booleans and other transport-shaped values cannot silently become a batch size; +- require the programmatic batch-mode selector to be an exact boolean before + using its truth value, so strings or integer-like transport values cannot + silently switch a direct call into or out of batch mode; - enforce a per-post timeout, returning a typed failure count instead of allowing a provider workflow to hold an operator process indefinitely. From 4a22ed3bfa4fa5edefa8146a22f3f4f95b1714a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:52:21 +0900 Subject: [PATCH 21/44] test(operator): bind Keyman transport to operator timeout --- ...t_post_keymen_backfill_timeout_contract.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_post_keymen_backfill_timeout_contract.py b/tests/test_post_keymen_backfill_timeout_contract.py index 6fe1caffa..c3fd9e1ea 100644 --- a/tests/test_post_keymen_backfill_timeout_contract.py +++ b/tests/test_post_keymen_backfill_timeout_contract.py @@ -92,3 +92,29 @@ def test_programmatic_runner_revalidates_timeout_before_external_work() -> None: and node.func.id == "_post_timeout_is_valid" ] assert calls, "programmatic runner must revalidate timeout before provider/database work" + + +def test_keyman_transport_uses_the_admitted_operator_timeout() -> None: + """Do not impose an unrelated shorter model-transport timeout inside the batch budget.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + runner = next( + node + for node in syntax_tree.body + if isinstance(node, ast.AsyncFunctionDef) + and node.name == "_run_post_keymen_backfill" + ) + client_call = next( + node + for node in ast.walk(runner) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "ContextualOrchestratorKeymanExtractionClient" + ) + timeout_keyword = next( + keyword for keyword in client_call.keywords if keyword.arg == "timeout" + ) + + assert isinstance(timeout_keyword.value, ast.Attribute) + assert isinstance(timeout_keyword.value.value, ast.Name) + assert timeout_keyword.value.value.id == "backfill_arguments" + assert timeout_keyword.value.attr == "post_timeout" From 75aefd7b9f3f987d4d2dd75eaa1c142e65b80ecb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:53:16 +0900 Subject: [PATCH 22/44] fix(operator): honor admitted Keyman timeout budget --- scripts/backfill_post_keymen.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 09986629d..035b6b080 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -206,7 +206,9 @@ async def _run_post_keymen_backfill( base_url, api_key = _orchestrator_config() settings = load_settings() keyman_client = ContextualOrchestratorKeymanExtractionClient( - base_url=base_url, api_key=api_key, timeout=180.0 + base_url=base_url, + api_key=api_key, + timeout=backfill_arguments.post_timeout, ) vision_client = orchestrator_vision_client(base_url, api_key) resolution_client = _organization_name_resolution_client() From 99af729cd6b3c12032b085af4ce16a1eb1134396 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:53:34 +0900 Subject: [PATCH 23/44] docs(adr): align Keyman transport with operator timeout --- docs/adr/0082-bounded-keyman-backfill.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index 858944e05..8658bba7c 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -35,8 +35,12 @@ It will: - require the programmatic batch-mode selector to be an exact boolean before using its truth value, so strings or integer-like transport values cannot silently switch a direct call into or out of batch mode; -- enforce a per-post timeout, returning a typed failure count instead of - allowing a provider workflow to hold an operator process indefinitely. +- enforce one admitted per-post timeout across the operator and its Keyman + contextual-orchestrator transport. The transport must not impose an + unrelated shorter fixed timeout that can terminate a valid long-running + model workflow before the operator's explicit administrative budget; +- return a typed timeout failure count instead of allowing a provider workflow + to hold an operator process indefinitely. Gateway credentials are read from runtime-injected environment variables. The script never reads or copies `~/.env`, and it is not exposed as a buyer HTTP @@ -51,5 +55,7 @@ route. No analysis-run registry tables are modified. - Re-running a selected post is idempotent through `ingest_post_keymen`'s replacement semantics, while the default selector may revisit an empty extraction because no evidence row exists. -- A provider workflow that exceeds the timeout is recorded as unavailable for - that attempt; it is not converted into an empty Keyman result. +- A provider workflow that exceeds the operator-selected timeout is recorded as + unavailable for that attempt; it is not converted into an empty Keyman + result, and an unrelated client-local fixed timeout does not pre-empt that + budget. \ No newline at end of file From cf97ac5ac5e8b5b5ee1534e7d9c50e594dcf2364 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:00:21 +0900 Subject: [PATCH 24/44] test(operator): reject unbounded Keyman batch limits --- tests/test_post_keymen_backfill_limit_contract.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/test_post_keymen_backfill_limit_contract.py b/tests/test_post_keymen_backfill_limit_contract.py index 9c96a69db..40f45c489 100644 --- a/tests/test_post_keymen_backfill_limit_contract.py +++ b/tests/test_post_keymen_backfill_limit_contract.py @@ -31,9 +31,14 @@ def _load_limit_validator(): return namespace["_post_limit_is_valid"] -@pytest.mark.parametrize("invalid_limit", [0, -1, True, False, 1.0, "1", None]) -def test_post_keymen_backfill_rejects_malformed_batch_limit(invalid_limit: object) -> None: - """Reject malformed direct-call limits instead of relying on argparse coercion.""" +@pytest.mark.parametrize( + "invalid_limit", + [0, -1, True, False, 1.0, "1", None, 101, 1_000_000], +) +def test_post_keymen_backfill_rejects_malformed_or_unbounded_batch_limit( + invalid_limit: object, +) -> None: + """Reject malformed or operationally unbounded direct-call batch limits.""" validator = _load_limit_validator() assert validator(invalid_limit) is False @@ -67,7 +72,7 @@ def test_programmatic_runner_revalidates_limit_before_external_work() -> None: syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) runner = next( node - for node in syntax_tree.body + for node in ast.walk(syntax_tree) if isinstance(node, ast.AsyncFunctionDef) and node.name == "_run_post_keymen_backfill" ) From ab769da595b907aa61ba4b068fc35ca02e8b3097 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:00:49 +0900 Subject: [PATCH 25/44] fix(operator): cap Keyman backfill batches at 100 posts --- scripts/backfill_post_keymen.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 035b6b080..1b7e111e5 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -59,8 +59,8 @@ def _post_timeout_is_valid(post_timeout: object) -> bool: def _post_limit_is_valid(post_limit: object) -> bool: - """Return whether a batch limit is an exact, strictly positive integer.""" - return type(post_limit) is int and post_limit > 0 + """Return whether a batch limit is an exact integer in the bounded 1..100 range.""" + return type(post_limit) is int and 1 <= post_limit <= 100 def _post_all_is_valid(post_all: object) -> bool: @@ -196,7 +196,7 @@ async def _run_post_keymen_backfill( if not _post_timeout_is_valid(backfill_arguments.post_timeout): raise ValueError("--post-timeout must be finite and positive") if not _post_limit_is_valid(backfill_arguments.limit): - raise ValueError("--limit must be a positive integer") + raise ValueError("--limit must be an integer between 1 and 100") if not _post_all_is_valid(backfill_arguments.all): raise ValueError("--all must be a boolean selector") if not _post_id_is_valid(backfill_arguments.post_id): @@ -269,7 +269,12 @@ def main() -> None: selector = parser.add_mutually_exclusive_group() selector.add_argument("--post-id", help="Re-extract one eligible post") selector.add_argument("--all", action="store_true", help="Process the explicit --limit batch") - parser.add_argument("--limit", type=int, default=1, help="Maximum posts for --all (default: 1)") + parser.add_argument( + "--limit", + type=int, + default=1, + help="Maximum posts for --all, 1..100 (default: 1)", + ) parser.add_argument( "--post-timeout", type=float, @@ -278,7 +283,7 @@ def main() -> None: ) backfill_arguments = parser.parse_args() if not _post_limit_is_valid(backfill_arguments.limit): - parser.error("--limit must be a positive integer") + parser.error("--limit must be an integer between 1 and 100") if not _post_all_is_valid(backfill_arguments.all): parser.error("--all must be a boolean selector") if not _post_timeout_is_valid(backfill_arguments.post_timeout): From dc5904b8b1c30a240aedb6f99c6608bbef8ed718 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:01:25 +0900 Subject: [PATCH 26/44] docs(adr): define the Keyman batch upper bound --- docs/adr/0082-bounded-keyman-backfill.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index 8658bba7c..a8fc2fbac 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -28,10 +28,12 @@ It will: - carry `build_post_llm_metadata` and `use_llm_metadata` across all LLM/VISION calls for one post, yielding the same deterministic post session id; - default to one post and require explicit `--all --limit N` for a batch; -- admit a batch limit only as an exact, strictly positive integer and apply the - same check to direct programmatic runner calls before gateway or database - work, so booleans and other transport-shaped values cannot silently become - a batch size; +- admit a batch limit only as an exact integer in the inclusive `1..100` + range, applying the same check to direct programmatic runner calls before + gateway or database work. The upper bound keeps one invocation genuinely + bounded even when a caller bypasses the CLI; larger work is split into + repeated observable invocations instead of turning one process into an + effectively unbounded serial crawl; - require the programmatic batch-mode selector to be an exact boolean before using its truth value, so strings or integer-like transport values cannot silently switch a direct call into or out of batch mode; @@ -49,7 +51,9 @@ route. No analysis-run registry tables are modified. ## Consequences - Keyman coverage can be increased incrementally with a bounded cost and - auditable operator output. + auditable operator output. One invocation processes at most 100 posts; larger + backfills require repeated invocations whose result summaries remain + independently attributable. - Empty extraction remains a real empty result; the script does not create a placeholder person or retry indefinitely through an implicit attempt table. - Re-running a selected post is idempotent through `ingest_post_keymen`'s @@ -58,4 +62,4 @@ route. No analysis-run registry tables are modified. - A provider workflow that exceeds the operator-selected timeout is recorded as unavailable for that attempt; it is not converted into an empty Keyman result, and an unrelated client-local fixed timeout does not pre-empt that - budget. \ No newline at end of file + budget. From 78f1633e96dbafc6f60ded80a80951aac3f3391e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:03:24 +0900 Subject: [PATCH 27/44] test(operator): bind Vision transport to Keyman backfill budget --- ...t_post_keymen_backfill_timeout_contract.py | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/tests/test_post_keymen_backfill_timeout_contract.py b/tests/test_post_keymen_backfill_timeout_contract.py index c3fd9e1ea..c85af4bda 100644 --- a/tests/test_post_keymen_backfill_timeout_contract.py +++ b/tests/test_post_keymen_backfill_timeout_contract.py @@ -94,8 +94,21 @@ def test_programmatic_runner_revalidates_timeout_before_external_work() -> None: assert calls, "programmatic runner must revalidate timeout before provider/database work" +def _assert_timeout_keyword_uses_operator_budget(client_call: ast.Call) -> None: + """Require one transport call to consume the already-admitted operator budget.""" + timeout_keyword = next( + (keyword for keyword in client_call.keywords if keyword.arg == "timeout"), + None, + ) + assert timeout_keyword is not None, "transport must receive the admitted operator timeout" + assert isinstance(timeout_keyword.value, ast.Attribute) + assert isinstance(timeout_keyword.value.value, ast.Name) + assert timeout_keyword.value.value.id == "backfill_arguments" + assert timeout_keyword.value.attr == "post_timeout" + + def test_keyman_transport_uses_the_admitted_operator_timeout() -> None: - """Do not impose an unrelated shorter model-transport timeout inside the batch budget.""" + """Do not impose an unrelated shorter Keyman transport cap inside the batch budget.""" syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) runner = next( node @@ -110,11 +123,25 @@ def test_keyman_transport_uses_the_admitted_operator_timeout() -> None: and isinstance(node.func, ast.Name) and node.func.id == "ContextualOrchestratorKeymanExtractionClient" ) - timeout_keyword = next( - keyword for keyword in client_call.keywords if keyword.arg == "timeout" + + _assert_timeout_keyword_uses_operator_budget(client_call) + + +def test_vision_transport_uses_the_admitted_operator_timeout() -> None: + """Keep synchronous Vision work inside the same per-post administrative budget.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + runner = next( + node + for node in syntax_tree.body + if isinstance(node, ast.AsyncFunctionDef) + and node.name == "_run_post_keymen_backfill" + ) + client_call = next( + node + for node in ast.walk(runner) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "orchestrator_vision_client" ) - assert isinstance(timeout_keyword.value, ast.Attribute) - assert isinstance(timeout_keyword.value.value, ast.Name) - assert timeout_keyword.value.value.id == "backfill_arguments" - assert timeout_keyword.value.attr == "post_timeout" + _assert_timeout_keyword_uses_operator_budget(client_call) From 0f1f7f003700d43c8595117701e7b45b507b40e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:04:43 +0900 Subject: [PATCH 28/44] fix(vision): expose bounded orchestrator transport timeout --- lineageweave/image_content.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lineageweave/image_content.py b/lineageweave/image_content.py index 9d0282e23..c28fcb8a7 100644 --- a/lineageweave/image_content.py +++ b/lineageweave/image_content.py @@ -381,15 +381,23 @@ def locate_regions(self, image_bytes: bytes, mime_type: str) -> tuple[ImageRegio return tuple(accepted) -def orchestrator_vision_client(base_url: str, api_key: str, model: str | None = None) -> ImageContentClient: +def orchestrator_vision_client( + base_url: str, + api_key: str, + model: str | None = None, + *, + timeout: float = 180.0, +) -> ImageContentClient: """Build a vision client against the same orchestrator root other channels use. Other clients POST ``{base_url}/v1/chat/completions``; :class:`OpenAiCompatibleVisionClient` POSTs ``{base_url}/chat/completions``, so this appends ``/v1`` unless already present. An ``http://`` orchestrator (local docker) is allowed because the other channels already talk to the - same URL. A construct-time error degrades to the unavailable null rather - than crashing the request that asked for a description. + same URL. The optional timeout lets a bounded caller share its admitted + transport budget with synchronous Vision requests. A construct-time error + degrades to the unavailable null rather than crashing the request that + asked for a description. """ if not (base_url and api_key): return NullImageContentClient() @@ -402,6 +410,7 @@ def orchestrator_vision_client(base_url: str, api_key: str, model: str | None = base_url=vision_base, api_key=api_key, model=model, + timeout=timeout, allow_insecure_http=parsed.scheme == "http", ) except ValueError: From 756f321d3493179776bfaa3da022c05fcb7c14ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:05:21 +0900 Subject: [PATCH 29/44] fix(operator): share Keyman budget with Vision transport --- scripts/backfill_post_keymen.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 1b7e111e5..427bb2fcd 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -210,7 +210,11 @@ async def _run_post_keymen_backfill( api_key=api_key, timeout=backfill_arguments.post_timeout, ) - vision_client = orchestrator_vision_client(base_url, api_key) + vision_client = orchestrator_vision_client( + base_url, + api_key, + timeout=backfill_arguments.post_timeout, + ) resolution_client = _organization_name_resolution_client() verification_client = _relation_verification_client() hierarchy_client = _corporate_hierarchy_inference_client() From d78640ad7b3da429606bdeffe2c69f39454fdc47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:05:56 +0900 Subject: [PATCH 30/44] docs(adr): align Keyman and Vision operator timeout budgets --- docs/adr/0082-bounded-keyman-backfill.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index a8fc2fbac..8b11109e1 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -37,10 +37,11 @@ It will: - require the programmatic batch-mode selector to be an exact boolean before using its truth value, so strings or integer-like transport values cannot silently switch a direct call into or out of batch mode; -- enforce one admitted per-post timeout across the operator and its Keyman - contextual-orchestrator transport. The transport must not impose an - unrelated shorter fixed timeout that can terminate a valid long-running - model workflow before the operator's explicit administrative budget; +- enforce one admitted per-post timeout across the operator and its Keyman and + Vision contextual-orchestrator transports. Neither synchronous Vision work + nor Keyman extraction may impose an unrelated shorter fixed timeout that can + terminate a valid long-running workflow before the operator's explicit + administrative budget; - return a typed timeout failure count instead of allowing a provider workflow to hold an operator process indefinitely. @@ -61,5 +62,5 @@ route. No analysis-run registry tables are modified. extraction because no evidence row exists. - A provider workflow that exceeds the operator-selected timeout is recorded as unavailable for that attempt; it is not converted into an empty Keyman - result, and an unrelated client-local fixed timeout does not pre-empt that + result, and unrelated client-local fixed timeouts do not pre-empt that budget. From 63b292337f50c5b5127b7f778d4cb2404cae75ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:06:18 +0900 Subject: [PATCH 31/44] test(vision): verify bounded timeout reaches HTTP transport --- tests/test_image_content_gateway.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_image_content_gateway.py b/tests/test_image_content_gateway.py index 9a1728a17..e4177e444 100644 --- a/tests/test_image_content_gateway.py +++ b/tests/test_image_content_gateway.py @@ -52,6 +52,34 @@ def post_json(url, payload, *, headers, timeout): assert normalized.convert("RGB").getpixel((0, 0)) == (255, 255, 255) +def test_vision_factory_forwards_bounded_transport_timeout(monkeypatch) -> None: + """Carry a caller-selected budget through the factory to synchronous HTTP work.""" + captured: dict[str, object] = {} + + def post_json(url, payload, *, headers, timeout): + captured["timeout"] = timeout + return { + "choices": [ + { + "message": { + "content": "TEXT: NONE\nCAPTION: synthetic timeout probe\nTAGS: probe" + } + } + ] + } + + monkeypatch.setattr(image_content, "post_json", post_json) + client = image_content.orchestrator_vision_client( + "http://orchestrator", + "gateway-key", + timeout=420.0, + ) + + client.describe(_transparent_png(), "image/png") + + assert captured["timeout"] == 420.0 + + def test_vision_factory_fails_closed_for_unsupported_url_scheme() -> None: client = image_content.orchestrator_vision_client("ftp://orchestrator", "gateway-key", "vision-model") assert isinstance(client, image_content.NullImageContentClient) From 15894bec68e79a02cc08295235e02dbb0c2f3307 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:09:43 +0900 Subject: [PATCH 32/44] docs(changelog): record bounded Keyman operator policy --- CHANGELOG.d/post-keymen-operator-bounds.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CHANGELOG.d/post-keymen-operator-bounds.md diff --git a/CHANGELOG.d/post-keymen-operator-bounds.md b/CHANGELOG.d/post-keymen-operator-bounds.md new file mode 100644 index 000000000..a99ec3ea6 --- /dev/null +++ b/CHANGELOG.d/post-keymen-operator-bounds.md @@ -0,0 +1,5 @@ +# Post-Keyman operator bounds + +The bounded Post Keyman backfill now admits only exact batch limits from 1 through 100, so one `--all` invocation cannot become an effectively unbounded serial crawl. Direct programmatic calls receive the same admission checks as the CLI. + +The operator-selected per-post timeout is also forwarded to both Keyman extraction and synchronous Vision requests through the contextual-orchestrator client boundary. This removes hidden shorter 180-second transport caps without introducing provider/model configuration in LineageWeave. From 21223c10dfd39f3833c2ba7d736c246d8231c156 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:50:51 +0900 Subject: [PATCH 33/44] test(operator): reject noncanonical post UUID selectors --- ...t_post_keymen_backfill_post_id_contract.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_post_keymen_backfill_post_id_contract.py b/tests/test_post_keymen_backfill_post_id_contract.py index f3e5cfa9d..8bcf041bc 100644 --- a/tests/test_post_keymen_backfill_post_id_contract.py +++ b/tests/test_post_keymen_backfill_post_id_contract.py @@ -10,6 +10,7 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1] BACKFILL_SCRIPT = REPOSITORY_ROOT / "scripts" / "backfill_post_keymen.py" +VALID_POST_ID = "123e4567-e89b-42d3-a456-426614174000" def _load_post_id_validator(): @@ -25,7 +26,7 @@ def _load_post_id_validator(): None, ) assert validator is not None, "operator must expose a pure post-id admission check" - namespace: dict[str, object] = {} + namespace: dict[str, object] = {"__builtins__": __builtins__} module = ast.fix_missing_locations(ast.Module(body=[validator], type_ignores=[])) exec(compile(module, str(BACKFILL_SCRIPT), "exec"), namespace) return namespace["_post_id_is_valid"] @@ -33,7 +34,7 @@ def _load_post_id_validator(): @pytest.mark.parametrize( "invalid_post_id", - ["", " ", "\t", "post-123 ", " post-123", "post-123\n"], + ["", " ", "\t", f"{VALID_POST_ID} ", f" {VALID_POST_ID}", f"{VALID_POST_ID}\n"], ) def test_post_keymen_backfill_rejects_blank_or_padded_explicit_post_id( invalid_post_id: str, @@ -43,7 +44,27 @@ def test_post_keymen_backfill_rejects_blank_or_padded_explicit_post_id( assert validator(invalid_post_id) is False -@pytest.mark.parametrize("invalid_post_id", [7, True, ["post-123"], {"id": "post-123"}]) +@pytest.mark.parametrize( + "invalid_post_id", + [ + "post-123", + "123E4567-E89B-42D3-A456-426614174000", + "{123e4567-e89b-42d3-a456-426614174000}", + "123e4567e89b42d3a456426614174000", + ], +) +def test_post_keymen_backfill_rejects_noncanonical_uuid_aliases( + invalid_post_id: str, +) -> None: + """Keep the internal source-post UUID one exact identity across logs and metadata.""" + validator = _load_post_id_validator() + + assert validator(invalid_post_id) is False + + +@pytest.mark.parametrize( + "invalid_post_id", [7, True, [VALID_POST_ID], {"id": VALID_POST_ID}] +) def test_post_keymen_backfill_rejects_non_string_explicit_post_id( invalid_post_id: object, ) -> None: @@ -57,7 +78,7 @@ def test_post_keymen_backfill_accepts_absent_or_exact_post_id() -> None: validator = _load_post_id_validator() assert validator(None) is True - assert validator("post-123") is True + assert validator(VALID_POST_ID) is True def test_main_routes_post_id_through_the_admission_check() -> None: From d0c0a05beff0a89493ce9057b5b4c1d53012bab1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:51:29 +0900 Subject: [PATCH 34/44] fix(operator): require canonical post UUID selectors --- scripts/backfill_post_keymen.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 427bb2fcd..023bcbf52 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -69,13 +69,17 @@ def _post_all_is_valid(post_all: object) -> bool: def _post_id_is_valid(post_id: object) -> bool: - """Return whether an optional explicit post identity is exact canonical text.""" - return ( - post_id is None - or type(post_id) is str - and bool(post_id) - and post_id == post_id.strip() - ) + """Return whether an optional post identity is one canonical UUID string.""" + if post_id is None: + return True + if type(post_id) is not str or not post_id or post_id != post_id.strip(): + return False + from uuid import UUID + + try: + return str(UUID(post_id)) == post_id + except ValueError: + return False async def _select_posts( @@ -200,7 +204,7 @@ async def _run_post_keymen_backfill( if not _post_all_is_valid(backfill_arguments.all): raise ValueError("--all must be a boolean selector") if not _post_id_is_valid(backfill_arguments.post_id): - raise ValueError("--post-id must be nonblank and unpadded") + raise ValueError("--post-id must be a canonical UUID") if backfill_arguments.post_id and backfill_arguments.all: raise ValueError("--post-id and --all cannot be combined") base_url, api_key = _orchestrator_config() @@ -271,7 +275,7 @@ async def _run_post_keymen_backfill( def main() -> None: parser = argparse.ArgumentParser(description=__doc__) selector = parser.add_mutually_exclusive_group() - selector.add_argument("--post-id", help="Re-extract one eligible post") + selector.add_argument("--post-id", help="Re-extract one eligible post UUID") selector.add_argument("--all", action="store_true", help="Process the explicit --limit batch") parser.add_argument( "--limit", @@ -293,7 +297,7 @@ def main() -> None: if not _post_timeout_is_valid(backfill_arguments.post_timeout): parser.error("--post-timeout must be finite and positive") if not _post_id_is_valid(backfill_arguments.post_id): - parser.error("--post-id must be nonblank and unpadded") + parser.error("--post-id must be a canonical UUID") print( json.dumps( asyncio.run(_run_post_keymen_backfill(backfill_arguments)), From 2c5cb7a9783940585933eb636725f70ef015bc44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:51:53 +0900 Subject: [PATCH 35/44] docs(adr): bind Keyman selector to source UUID identity --- docs/adr/0082-bounded-keyman-backfill.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index 8b11109e1..74b75a60a 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -11,6 +11,11 @@ come from the existing Keyman extraction and persistence projection. Imported real data has many posts without a `post_person_mention` row, so relying only on the per-post operator button leaves the author-group view mostly empty. +ADR 0046 defines `source_post.post_id` as the internal UUID identity and keeps +that UUID distinct from an opaque source-system record key. An operator selector +therefore must not accept arbitrary opaque text or alternate UUID spellings as +though they were the internal post identity. + ## Decision Provide `scripts/backfill_post_keymen.py` as an operator-only, bounded runner. @@ -18,6 +23,11 @@ It will: - select eligible, non-deleted, non-draft posts that have no existing `post_person_mention`, or one explicit `--post-id`; +- admit an explicit `--post-id` only when it is the canonical lowercase, + hyphenated UUID text for the ADR 0046 internal post identity. Blank, padded, + malformed, uppercase, braced, and hyphenless aliases fail before gateway or + database work rather than being normalized into a different textual + identity; - normalize HTML, OOXML-derived text, embedded images, and image regions with the existing VISION normalization path before extraction; - pass source author, account, PU, sales-pool, customer, company, and project @@ -55,6 +65,9 @@ route. No analysis-run registry tables are modified. auditable operator output. One invocation processes at most 100 posts; larger backfills require repeated invocations whose result summaries remain independently attributable. +- Explicit reruns use one stable textual form for the internal UUID in operator + logs and LLM metadata; source-system record keys remain separate ADR 0046 + evidence and are never accepted as `--post-id`. - Empty extraction remains a real empty result; the script does not create a placeholder person or retry indefinitely through an implicit attempt table. - Re-running a selected post is idempotent through `ingest_post_keymen`'s From 08428fa769ad428335dbe9a291d46ee557179995 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 07:51:58 +0900 Subject: [PATCH 36/44] docs(changelog): record canonical post selector admission --- CHANGELOG.d/post-keymen-operator-bounds.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.d/post-keymen-operator-bounds.md b/CHANGELOG.d/post-keymen-operator-bounds.md index a99ec3ea6..64faa2c7f 100644 --- a/CHANGELOG.d/post-keymen-operator-bounds.md +++ b/CHANGELOG.d/post-keymen-operator-bounds.md @@ -2,4 +2,6 @@ The bounded Post Keyman backfill now admits only exact batch limits from 1 through 100, so one `--all` invocation cannot become an effectively unbounded serial crawl. Direct programmatic calls receive the same admission checks as the CLI. +An explicit `--post-id` must now be the canonical lowercase, hyphenated UUID for `source_post.post_id`. Malformed values and alternate UUID spellings fail before gateway or database work instead of creating multiple textual representations of the same internal post identity; opaque source-system record keys remain separate evidence under ADR 0046. + The operator-selected per-post timeout is also forwarded to both Keyman extraction and synchronous Vision requests through the contextual-orchestrator client boundary. This removes hidden shorter 180-second transport caps without introducing provider/model configuration in LineageWeave. From d047a869b471363b435410a8f8919bd51bebcb2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:50:12 +0900 Subject: [PATCH 37/44] test(operator): reject unrepresentable integer timeout --- tests/test_post_keymen_backfill_timeout_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_post_keymen_backfill_timeout_contract.py b/tests/test_post_keymen_backfill_timeout_contract.py index c85af4bda..f8e492627 100644 --- a/tests/test_post_keymen_backfill_timeout_contract.py +++ b/tests/test_post_keymen_backfill_timeout_contract.py @@ -51,6 +51,13 @@ def test_post_keymen_backfill_rejects_non_numeric_or_boolean_timeout( assert validator(invalid_timeout) is False +def test_post_keymen_backfill_rejects_unrepresentable_integer_timeout() -> None: + """Fail closed instead of raising while checking an enormous direct-call integer.""" + validator = _load_timeout_validator() + + assert validator(10**10000) is False + + def test_post_keymen_backfill_accepts_positive_finite_timeout() -> None: validator = _load_timeout_validator() From 5b49b923751cb1cae18a0e70ad94afbedff93e12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:50:44 +0900 Subject: [PATCH 38/44] fix(operator): fail closed on unrepresentable timeout --- scripts/backfill_post_keymen.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 023bcbf52..2bbac43a4 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -51,11 +51,12 @@ def _orchestrator_config() -> tuple[str, str]: def _post_timeout_is_valid(post_timeout: object) -> bool: """Return whether an operator timeout is a finite, strictly positive number.""" - return ( - type(post_timeout) in (int, float) - and math.isfinite(post_timeout) - and post_timeout > 0 - ) + if type(post_timeout) not in (int, float) or post_timeout <= 0: + return False + try: + return math.isfinite(post_timeout) + except OverflowError: + return False def _post_limit_is_valid(post_limit: object) -> bool: From c5dda93fea9a5be10f0103a8da0b416cf766722b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:51:16 +0900 Subject: [PATCH 39/44] docs(adr): record total timeout admission --- docs/adr/0082-bounded-keyman-backfill.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index 74b75a60a..c8d50471b 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -47,6 +47,11 @@ It will: - require the programmatic batch-mode selector to be an exact boolean before using its truth value, so strings or integer-like transport values cannot silently switch a direct call into or out of batch mode; +- admit the per-post administrative timeout only when validation itself is + total: malformed direct-call values, including an integer too large for the + runtime finite-number check, fail closed instead of escaping admission with + an arithmetic exception before the operator can return its normal validation + error; - enforce one admitted per-post timeout across the operator and its Keyman and Vision contextual-orchestrator transports. Neither synchronous Vision work nor Keyman extraction may impose an unrelated shorter fixed timeout that can @@ -77,3 +82,6 @@ route. No analysis-run registry tables are modified. unavailable for that attempt; it is not converted into an empty Keyman result, and unrelated client-local fixed timeouts do not pre-empt that budget. +- Programmatic timeout admission remains fail-closed even for numeric values + whose magnitude cannot be represented by the runtime finite-number helper; + validation does not leak an `OverflowError` as an alternate control path. From b4524527e13d2a38c3ad37e4987976bd62b95828 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:51:23 +0900 Subject: [PATCH 40/44] docs(changelog): note total timeout admission --- CHANGELOG.d/post-keymen-operator-bounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.d/post-keymen-operator-bounds.md b/CHANGELOG.d/post-keymen-operator-bounds.md index 64faa2c7f..2482aa010 100644 --- a/CHANGELOG.d/post-keymen-operator-bounds.md +++ b/CHANGELOG.d/post-keymen-operator-bounds.md @@ -4,4 +4,4 @@ The bounded Post Keyman backfill now admits only exact batch limits from 1 throu An explicit `--post-id` must now be the canonical lowercase, hyphenated UUID for `source_post.post_id`. Malformed values and alternate UUID spellings fail before gateway or database work instead of creating multiple textual representations of the same internal post identity; opaque source-system record keys remain separate evidence under ADR 0046. -The operator-selected per-post timeout is also forwarded to both Keyman extraction and synchronous Vision requests through the contextual-orchestrator client boundary. This removes hidden shorter 180-second transport caps without introducing provider/model configuration in LineageWeave. +Per-post timeout admission now also fails closed for malformed direct-call numeric values whose magnitude cannot be represented by the runtime finite-number check, instead of leaking an `OverflowError` from validation. The admitted operator timeout is forwarded to both Keyman extraction and synchronous Vision requests through the contextual-orchestrator client boundary. This removes hidden shorter 180-second transport caps without introducing provider/model configuration in LineageWeave. From 61d94c3afdeadc3634785181d6b40a1f55d50b23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 09:55:37 +0900 Subject: [PATCH 41/44] test(operator): reject ignored post backfill limits --- ...keymen_backfill_batch_selector_contract.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_post_keymen_backfill_batch_selector_contract.py b/tests/test_post_keymen_backfill_batch_selector_contract.py index d98e45e9c..5550eb574 100644 --- a/tests/test_post_keymen_backfill_batch_selector_contract.py +++ b/tests/test_post_keymen_backfill_batch_selector_contract.py @@ -31,6 +31,29 @@ def _load_batch_selector_validator(): return namespace["_post_all_is_valid"] +def _load_selection_validator(): + """Load the pure cross-field selector contract without importing operator dependencies.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + function_names = { + "_post_limit_is_valid", + "_post_all_is_valid", + "_post_id_is_valid", + "_post_selection_is_valid", + } + functions = [ + node + for node in syntax_tree.body + if isinstance(node, ast.FunctionDef) and node.name in function_names + ] + assert any( + node.name == "_post_selection_is_valid" for node in functions + ), "operator must validate selector/limit combinations as one admission contract" + namespace: dict[str, object] = {} + module = ast.fix_missing_locations(ast.Module(body=functions, type_ignores=[])) + exec(compile(module, str(BACKFILL_SCRIPT), "exec"), namespace) + return namespace["_post_selection_is_valid"] + + def test_post_keymen_backfill_accepts_boolean_batch_selector() -> None: validator = _load_batch_selector_validator() @@ -48,6 +71,21 @@ def test_post_keymen_backfill_rejects_non_boolean_batch_selector( assert validator(invalid_selector) is False +def test_post_keymen_backfill_rejects_ignored_non_default_limits() -> None: + """An explicit non-default limit must not be silently ignored outside batch mode.""" + validator = _load_selection_validator() + post_id = "00000000-0000-0000-0000-000000000001" + + assert validator(False, None, 1) is True + assert validator(False, post_id, 1) is True + assert validator(True, None, 1) is True + assert validator(True, None, 100) is True + + assert validator(False, None, 2) is False + assert validator(False, post_id, 2) is False + assert validator(True, post_id, 1) is False + + def test_programmatic_runner_revalidates_batch_selector_before_external_work() -> None: """Keep direct callers behind the same selector-mode admission boundary.""" syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) @@ -66,3 +104,21 @@ def test_programmatic_runner_revalidates_batch_selector_before_external_work() - and node.func.id == "_post_all_is_valid" ] assert calls, "programmatic runner must revalidate batch selector before external work" + + +def test_programmatic_runner_revalidates_selector_combination_before_external_work() -> None: + """Keep ignored-limit ambiguity out of the direct-call operator boundary.""" + syntax_tree = ast.parse(BACKFILL_SCRIPT.read_text(encoding="utf-8")) + runner = next( + node + for node in syntax_tree.body + if isinstance(node, ast.AsyncFunctionDef) + and node.name == "_run_post_keymen_backfill" + ) + + assert any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_post_selection_is_valid" + for node in ast.walk(runner) + ) From fa109e6fb117c985624310d2230dd779287d9b3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 09:57:22 +0900 Subject: [PATCH 42/44] fix(operator): reject ignored backfill limits --- scripts/backfill_post_keymen.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 2bbac43a4..ba4541f83 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -83,6 +83,21 @@ def _post_id_is_valid(post_id: object) -> bool: return False +def _post_selection_is_valid(post_all: object, post_id: object, post_limit: object) -> bool: + """Return whether selector and limit fields form one unambiguous bounded request.""" + if ( + not _post_all_is_valid(post_all) + or not _post_id_is_valid(post_id) + or not _post_limit_is_valid(post_limit) + ): + return False + if post_all and post_id is not None: + return False + if not post_all and post_limit != 1: + return False + return True + + async def _select_posts( conn: asyncpg.Connection, *, limit: int, post_id: str | None ) -> list[asyncpg.Record]: @@ -206,8 +221,12 @@ async def _run_post_keymen_backfill( raise ValueError("--all must be a boolean selector") if not _post_id_is_valid(backfill_arguments.post_id): raise ValueError("--post-id must be a canonical UUID") - if backfill_arguments.post_id and backfill_arguments.all: - raise ValueError("--post-id and --all cannot be combined") + if not _post_selection_is_valid( + backfill_arguments.all, + backfill_arguments.post_id, + backfill_arguments.limit, + ): + raise ValueError("non-default --limit requires --all; --post-id and --all cannot be combined") base_url, api_key = _orchestrator_config() settings = load_settings() keyman_client = ContextualOrchestratorKeymanExtractionClient( @@ -299,6 +318,12 @@ def main() -> None: parser.error("--post-timeout must be finite and positive") if not _post_id_is_valid(backfill_arguments.post_id): parser.error("--post-id must be a canonical UUID") + if not _post_selection_is_valid( + backfill_arguments.all, + backfill_arguments.post_id, + backfill_arguments.limit, + ): + parser.error("non-default --limit requires --all; --post-id and --all cannot be combined") print( json.dumps( asyncio.run(_run_post_keymen_backfill(backfill_arguments)), From 933d28b99122daf8297108be4db2b65c474f7f3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 09:57:54 +0900 Subject: [PATCH 43/44] docs(adr): make backfill selector combinations explicit --- docs/adr/0082-bounded-keyman-backfill.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/adr/0082-bounded-keyman-backfill.md b/docs/adr/0082-bounded-keyman-backfill.md index c8d50471b..b5f01ad58 100644 --- a/docs/adr/0082-bounded-keyman-backfill.md +++ b/docs/adr/0082-bounded-keyman-backfill.md @@ -44,6 +44,10 @@ It will: bounded even when a caller bypasses the CLI; larger work is split into repeated observable invocations instead of turning one process into an effectively unbounded serial crawl; +- reject a non-default `--limit` unless batch mode is explicitly selected with + `--all`. The same cross-field admission applies to direct programmatic calls, + so a requested limit cannot be silently ignored by falling back to the + default one-post mode. `--post-id` and `--all` remain mutually exclusive; - require the programmatic batch-mode selector to be an exact boolean before using its truth value, so strings or integer-like transport values cannot silently switch a direct call into or out of batch mode; @@ -70,6 +74,9 @@ route. No analysis-run registry tables are modified. auditable operator output. One invocation processes at most 100 posts; larger backfills require repeated invocations whose result summaries remain independently attributable. +- An explicit non-default batch size is either honored under `--all` or rejected + before external work; it is never accepted and then silently collapsed to a + one-post execution. - Explicit reruns use one stable textual form for the internal UUID in operator logs and LLM metadata; source-system record keys remain separate ADR 0046 evidence and are never accepted as `--post-id`. From 39a4803e2e282a34e5f284a4815933c749196bb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 09:58:03 +0900 Subject: [PATCH 44/44] docs(changelog): record selector-limit admission repair --- CHANGELOG.d/post-keymen-operator-bounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.d/post-keymen-operator-bounds.md b/CHANGELOG.d/post-keymen-operator-bounds.md index 2482aa010..c85bc7b1e 100644 --- a/CHANGELOG.d/post-keymen-operator-bounds.md +++ b/CHANGELOG.d/post-keymen-operator-bounds.md @@ -1,6 +1,6 @@ # Post-Keyman operator bounds -The bounded Post Keyman backfill now admits only exact batch limits from 1 through 100, so one `--all` invocation cannot become an effectively unbounded serial crawl. Direct programmatic calls receive the same admission checks as the CLI. +The bounded Post Keyman backfill now admits only exact batch limits from 1 through 100, so one `--all` invocation cannot become an effectively unbounded serial crawl. Direct programmatic calls receive the same admission checks as the CLI. A non-default `--limit` is rejected unless `--all` is selected, so a requested batch size cannot be silently accepted and then ignored by the default one-post path; `--post-id` and `--all` remain mutually exclusive. An explicit `--post-id` must now be the canonical lowercase, hyphenated UUID for `source_post.post_id`. Malformed values and alternate UUID spellings fail before gateway or database work instead of creating multiple textual representations of the same internal post identity; opaque source-system record keys remain separate evidence under ADR 0046.