From 70d826cc6a86145c326ea90962aafcf5b1bce4e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 15:34:39 -0700 Subject: [PATCH 1/5] ci: stage Global Ask semantic retrieval v2.18.1 --- ...pply_global_ask_semantic_retrieval_2181.py | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 .github/scripts/apply_global_ask_semantic_retrieval_2181.py diff --git a/.github/scripts/apply_global_ask_semantic_retrieval_2181.py b/.github/scripts/apply_global_ask_semantic_retrieval_2181.py new file mode 100644 index 00000000..13f2ec89 --- /dev/null +++ b/.github/scripts/apply_global_ask_semantic_retrieval_2181.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +"""Apply the one-shot Global Ask semantic candidate retrieval patch release.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from textwrap import dedent + +TEST_NAME = "test_global_sources_discover_posts_from_persisted_semantic_evidence" + +TEST_BLOCK = dedent( + ''' + + +def test_global_sources_discover_posts_from_persisted_semantic_evidence() -> None: + """A semantic fact can nominate a post when source text cannot.""" + source_row = { + "post_id": "semantic-only-post", + "post_title": "Operational note", + "post_body": "The visible source text deliberately omits the buyer term.", + "visibility_code": "public", + "corporate_entity_id": None, + } + calls: list[tuple[str, tuple[object, ...]]] = [] + + class FakeConnection: + async def fetch(self, query: str, *args): + calls.append((query, args)) + if "semantic_candidate_matches" in query: + return [{"post_id": "semantic-only-post"}] + if "post_id = any($2::uuid[])" in query: + return [source_row] if args[1] == ["semantic-only-post"] else [] + if "from post_project_mention" in query: + return [ + { + "post_id": "semantic-only-post", + "fact": ( + "project: Phoenix transformation | evidence: approved " + "[provenance=post_project_mention]" + ), + } + ] + return [] + + sources = asyncio.run( + gather_global_chat_sources( + FakeConnection(), + lambda row: True, + question="Who owns the Phoenix transformation?", + limit=4, + ) + ) + + assert [source.post_id for source in sources] == ["semantic-only-post"] + assert any( + fact.startswith("project: Phoenix transformation") + for fact in sources[0].evidence_facts + ) + semantic_query = next( + query for query, _ in calls if "semantic_candidate_matches" in query + ) + assert "post_project_mention" in semantic_query + assert "post_summary_role" in semantic_query + assert "post_person_mention" in semantic_query + assert "cataloged_person" in semantic_query +''' +) + +OLD_CANDIDATE_TAIL = ''' candidate_ids.extend(str(row["post_id"]) for row in candidate_rows) + candidate_ids = list(dict.fromkeys(candidate_ids)) +''' + +NEW_CANDIDATE_TAIL = ''' candidate_ids.extend(str(row["post_id"]) for row in candidate_rows) + semantic_candidate_rows = await conn.fetch( + """ + select post_id + from ( + (select mention.post_id, source.created_at + from post_project_mention mention + join source_post source on source.post_id = mention.post_id + where concat_ws(' ', mention.project_name, + mention.evidence_text, + mention.ontology_iri) + ilike '%' || $1 || '%' + limit 32) + union all + (select role.post_id, source.created_at + from post_summary_role role + join source_post source on source.post_id = role.post_id + where concat_ws(' ', role.actor_name, + role.responsibility, + role.affiliated_organization_name) + ilike '%' || $1 || '%' + limit 32) + union all + (select mention.post_id, source.created_at + from post_person_mention mention + join cataloged_person person on person.person_id = mention.person_id + join source_post source on source.post_id = mention.post_id + where concat_ws(' ', person.person_name, + person.last_known_job_title, + mention.mention_context) + ilike '%' || $1 || '%' + limit 32) + ) semantic_candidate_matches + order by created_at desc, post_id desc + limit 32 + """, + term, + ) + candidate_ids.extend( + str(row["post_id"]) for row in semantic_candidate_rows + ) + candidate_ids = list(dict.fromkeys(candidate_ids)) +''' + +CHANGELOG_ENTRY = dedent( + ''' + ## [2.18.1] - 2026-08-20 + + ### Fixed + + - Global Ask now nominates candidates from persisted project, + role/responsibility/affiliation, and cataloged Keyman evidence in addition + to lexical source text and raw source hints. A semantic-only buyer term can + reach the authorized cited post without bypassing visibility or ABAC + (ADR 0091 / ADR 0047 / ADR 0039). + + ''' +) + +ADR_TEXT = dedent( + ''' + # ADR 0091: Global Ask candidates include persisted semantic evidence + + - Status: Accepted + - Date: 2026-08-20 + - Owners: LineageWeave + - Extends: ADR 0090 authenticated Global Ask MCP + - Closes: the lexical-only implementation gap under ADR 0047 + + ## Context + + ADR 0047 requires Global Ask to retrieve the same authorized buyer evidence + exposed by Board: source identity, project mentions, stored roles, + affiliations, cataloged Keymen, title, and normalized body. The shared + assembler loaded persisted semantic facts only after a post had already + been selected from lexical text or raw source hints. + + A buyer or authenticated MCP client could therefore see a project or owner + in an evidence drawer, ask with that exact semantic fact, and receive no + cited post when the words were absent from raw source text. This was a + source-contract mismatch, not a model-quality problem. + + ## Decision + + Each bounded question term nominates candidates from both the existing + lexical/source-hint channels and these persisted channels: + + - `post_project_mention`: project name, evidence, ontology IRI; + - `post_summary_role`: actor, responsibility, affiliation; + - `post_person_mention` plus `cataloged_person`: Keyman name, job title, and + mention context. + + Candidate IDs are not evidence and grant no access. The existing authorized + `source_post` query and application ABAC predicate still run before body, + graph, or semantic facts enter contextual-orchestrator. Question terms, + per-channel candidates, final sources, body length, and graph facts remain + bounded. ADR 0090's MCP question bound and distributed rate limit remain in + force. + + ## Consequences + + - Browser and MCP Global Ask share semantic-only candidate retrieval. + - Hidden semantic matches remain hidden behind visibility and ABAC. + - This does not claim embedding similarity, exhaustive recall, causal + inference, or an answer unsupported by selected sources. + + ## Verification + + `tests/test_global_ask_sources.py` contains a regression whose title, body, + and raw hints omit the buyer term. Persisted semantic evidence alone + nominates the authorized source and accompanies the citation. + ''' +) + +RELEASE_TEXT = dedent( + ''' + # 2.18.1 Global Ask finds semantic-only evidence + + Browser and authenticated MCP Global Ask now find an authorized post when a + buyer term exists only in persisted project, responsibility, affiliation, + or Keyman evidence. The cited post still crosses the same visibility and + ABAC boundary before any content reaches contextual-orchestrator. + + This release preserves the MCP question bound and distributed rate limit. + It does not claim exhaustive semantic search or invent a source, + relationship, customer, cutoff body, or TEPP result. + ''' +) + +FRAGMENT_TEXT = dedent( + ''' + # 2.18.1 — Global Ask semantic candidate retrieval + + ## Fixed + + - Browser and MCP Global Ask now nominate authorized source posts from + persisted project, role/responsibility/affiliation, and Keyman evidence, + not only lexical source text and raw source hints (ADR 0091). + - Candidate nomination does not bypass the existing visibility and ABAC + gate, MCP question bound, or distributed invocation rate limit. + ''' +) + + +def append_regression() -> None: + """Append the semantic-only regression exactly once.""" + path = Path("tests/test_global_ask_sources.py") + text = path.read_text() + if TEST_NAME not in text: + path.write_text(text.rstrip() + TEST_BLOCK + "\n") + + +def replace_once(path_name: str, old: str, new: str) -> None: + """Replace one exact source contract, failing closed on drift.""" + path = Path(path_name) + text = path.read_text() + count = text.count(old) + if count != 1: + raise SystemExit(f"{path_name}: expected one match, found {count}") + path.write_text(text.replace(old, new, 1)) + + +def apply_implementation() -> None: + """Apply production, version, ADR, and release changes.""" + backend = Path("backend/app/post_chat_ingestion.py") + backend_text = backend.read_text() + if "semantic_candidate_matches" not in backend_text: + if backend_text.count(OLD_CANDIDATE_TAIL) != 1: + raise SystemExit("Global Ask candidate insertion point drifted") + backend.write_text( + backend_text.replace(OLD_CANDIDATE_TAIL, NEW_CANDIDATE_TAIL, 1) + ) + + for path_name, old, new in ( + ("pyproject.toml", 'version = "2.18.0"', 'version = "2.18.1"'), + ("frontend/package.json", '"version": "2.18.0"', '"version": "2.18.1"'), + ("backend/app/mcp_server.py", '_SERVER_VERSION = "2.18.0"', '_SERVER_VERSION = "2.18.1"'), + ): + text = Path(path_name).read_text() + if new not in text: + replace_once(path_name, old, new) + + changelog = Path("CHANGELOG.md") + changelog_text = changelog.read_text() + if "## [2.18.1] - 2026-08-20" not in changelog_text: + marker = "## [2.17.0] - 2026-08-19\n" + if changelog_text.count(marker) != 1: + raise SystemExit("CHANGELOG 2.17.0 marker drifted") + changelog.write_text(changelog_text.replace(marker, CHANGELOG_ENTRY + marker, 1)) + + Path("docs/adr/0091-global-ask-semantic-candidate-retrieval.md").write_text( + ADR_TEXT + ) + Path("docs/releases").mkdir(parents=True, exist_ok=True) + Path("docs/releases/2.18.1.md").write_text(RELEASE_TEXT) + Path("CHANGELOG.d").mkdir(parents=True, exist_ok=True) + Path("CHANGELOG.d/2.18.1-global-ask-semantic-candidate-retrieval.md").write_text( + FRAGMENT_TEXT + ) + + +def main() -> None: + """Run the requested one-shot phase.""" + parser = argparse.ArgumentParser() + parser.add_argument("phase", choices=("test-only", "apply")) + args = parser.parse_args() + append_regression() + if args.phase == "apply": + apply_implementation() + + +if __name__ == "__main__": + main() From e870f6e8e8deae8658f3ab50a610551ad68c2c11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 15:35:01 -0700 Subject: [PATCH 2/5] ci: apply Global Ask semantic retrieval v2.18.1 on PR --- ...ly-global-ask-semantic-retrieval-v2181.yml | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/apply-global-ask-semantic-retrieval-v2181.yml diff --git a/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml b/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml new file mode 100644 index 00000000..d9e5d96e --- /dev/null +++ b/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml @@ -0,0 +1,92 @@ +name: Apply Global Ask semantic retrieval v2.18.1 + +on: + pull_request: + branches: + - "agent/authenticated-global-ask-mcp-v2180" + types: [opened, reopened, synchronize] + +permissions: + contents: write + +concurrency: + group: apply-global-ask-semantic-retrieval-v2181-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + apply: + name: Apply and verify semantic-only retrieval + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Checkout pull request branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + persist-credentials: true + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up locked dependency manager + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Select pinned Rust toolchain + run: | + rustup toolchain install 1.97.1 --profile minimal + rustup default 1.97.1 + + - name: Install committed dependencies + run: uv sync --frozen --extra dev --extra backend + + - name: Add the semantic-only regression + run: python .github/scripts/apply_global_ask_semantic_retrieval_2181.py test-only + + - name: Prove the regression is red + run: | + set +e + uv run --frozen python -m pytest -q \ + tests/test_global_ask_sources.py::test_global_sources_discover_posts_from_persisted_semantic_evidence \ + > /tmp/global-ask-red.log 2>&1 + status=$? + set -e + cat /tmp/global-ask-red.log + if [ "$status" -eq 0 ]; then + echo "Expected semantic-only retrieval to fail before implementation." >&2 + exit 1 + fi + grep -F "test_global_sources_discover_posts_from_persisted_semantic_evidence" \ + /tmp/global-ask-red.log + + - name: Apply the bounded production implementation + run: python .github/scripts/apply_global_ask_semantic_retrieval_2181.py apply + + - name: Verify browser, MCP, and adjacent evidence contracts + run: | + uv run --frozen python -m pytest -q \ + tests/test_global_ask_sources.py \ + tests/test_mcp_server.py \ + tests/test_post_chat.py \ + tests/test_post_chat_ingestion.py \ + tests/test_semantic_hints.py \ + tests/test_semantic_keyman_context.py \ + tests/test_semantic_project_evidence.py + uv run --frozen python -m compileall -q backend lineageweave tests + git diff --check + + - name: Remove one-shot implementation files and publish + run: | + rm .github/workflows/apply-global-ask-semantic-retrieval-v2181.yml + rm .github/scripts/apply_global_ask_semantic_retrieval_2181.py + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --check + git commit -m "fix: retrieve Global Ask semantic evidence (v2.18.1)" + git push origin HEAD:${{ github.event.pull_request.head.ref }} From b395559fc44df8b9bc10d874f5cb3df592f6b956 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 15:55:05 -0700 Subject: [PATCH 3/5] ci: keep Global Ask semantic lookup index-backed --- ...pply_global_ask_semantic_retrieval_2181.py | 154 +++++++++++++++--- 1 file changed, 134 insertions(+), 20 deletions(-) diff --git a/.github/scripts/apply_global_ask_semantic_retrieval_2181.py b/.github/scripts/apply_global_ask_semantic_retrieval_2181.py index 13f2ec89..f7dc6eec 100644 --- a/.github/scripts/apply_global_ask_semantic_retrieval_2181.py +++ b/.github/scripts/apply_global_ask_semantic_retrieval_2181.py @@ -64,6 +64,19 @@ async def fetch(self, query: str, *args): assert "post_summary_role" in semantic_query assert "post_person_mention" in semantic_query assert "cataloged_person" in semantic_query + assert "concat_ws" not in semantic_query + for indexed_predicate in ( + "mention.project_name ilike", + "mention.evidence_text ilike", + "mention.ontology_iri ilike", + "role.actor_name ilike", + "role.responsibility ilike", + "role.affiliated_organization_name ilike", + "person.person_name ilike", + "person.last_known_job_title ilike", + "mention.mention_context ilike", + ): + assert indexed_predicate in semantic_query.lower() ''' ) @@ -79,29 +92,26 @@ async def fetch(self, query: str, *args): (select mention.post_id, source.created_at from post_project_mention mention join source_post source on source.post_id = mention.post_id - where concat_ws(' ', mention.project_name, - mention.evidence_text, - mention.ontology_iri) - ilike '%' || $1 || '%' + where mention.project_name ilike '%' || $1 || '%' + or mention.evidence_text ilike '%' || $1 || '%' + or mention.ontology_iri ilike '%' || $1 || '%' limit 32) union all (select role.post_id, source.created_at from post_summary_role role join source_post source on source.post_id = role.post_id - where concat_ws(' ', role.actor_name, - role.responsibility, - role.affiliated_organization_name) - ilike '%' || $1 || '%' + where role.actor_name ilike '%' || $1 || '%' + or role.responsibility ilike '%' || $1 || '%' + or role.affiliated_organization_name ilike '%' || $1 || '%' limit 32) union all (select mention.post_id, source.created_at from post_person_mention mention join cataloged_person person on person.person_id = mention.person_id join source_post source on source.post_id = mention.post_id - where concat_ws(' ', person.person_name, - person.last_known_job_title, - mention.mention_context) - ilike '%' || $1 || '%' + where person.person_name ilike '%' || $1 || '%' + or person.last_known_job_title ilike '%' || $1 || '%' + or mention.mention_context ilike '%' || $1 || '%' limit 32) ) semantic_candidate_matches order by created_at desc, post_id desc @@ -115,6 +125,83 @@ async def fetch(self, query: str, *args): candidate_ids = list(dict.fromkeys(candidate_ids)) ''' +MIGRATION_TEXT = dedent( + ''' + -- Keep every Global Ask semantic ILIKE predicate backed by pg_trgm. + -- Candidate IDs still cross the authoritative source_post visibility/ABAC gate. + begin; + + create extension if not exists pg_trgm; + + create index if not exists post_project_mention_evidence_trgm_idx + on post_project_mention using gin (evidence_text gin_trgm_ops); + create index if not exists post_project_mention_ontology_trgm_idx + on post_project_mention using gin (ontology_iri gin_trgm_ops); + create index if not exists post_summary_role_responsibility_trgm_idx + on post_summary_role using gin (responsibility gin_trgm_ops); + create index if not exists post_summary_role_affiliation_trgm_idx + on post_summary_role using gin (affiliated_organization_name gin_trgm_ops); + create index if not exists cataloged_person_job_title_trgm_idx + on cataloged_person using gin (last_known_job_title gin_trgm_ops); + create index if not exists post_person_mention_context_trgm_idx + on post_person_mention using gin (mention_context gin_trgm_ops); + + commit; + ''' +) + +ROLLBACK_TEXT = dedent( + ''' + begin; + + drop index if exists post_person_mention_context_trgm_idx; + drop index if exists cataloged_person_job_title_trgm_idx; + drop index if exists post_summary_role_affiliation_trgm_idx; + drop index if exists post_summary_role_responsibility_trgm_idx; + drop index if exists post_project_mention_ontology_trgm_idx; + drop index if exists post_project_mention_evidence_trgm_idx; + + commit; + ''' +) + +INDEX_TEST_TEXT = dedent( + ''' + """Contract tests for index-backed Global Ask semantic predicates.""" + + from pathlib import Path + + + INDEX_NAMES = ( + "post_project_mention_evidence_trgm_idx", + "post_project_mention_ontology_trgm_idx", + "post_summary_role_responsibility_trgm_idx", + "post_summary_role_affiliation_trgm_idx", + "cataloged_person_job_title_trgm_idx", + "post_person_mention_context_trgm_idx", + ) + + + def test_global_ask_semantic_lookup_migration_creates_each_index() -> None: + """Every newly queried semantic field has a trigram lookup index.""" + migration = Path( + "migrations/0046_global_ask_semantic_lookup_indexes.sql" + ).read_text() + for index_name in INDEX_NAMES: + assert f"create index if not exists {index_name}" in migration.lower() + assert "gin_trgm_ops" in migration + + + def test_global_ask_semantic_lookup_rollback_drops_each_index() -> None: + """Rollback removes only the indexes introduced by the migration.""" + rollback = Path( + "migrations/rollback/0046_global_ask_semantic_lookup_indexes.sql" + ).read_text() + for index_name in INDEX_NAMES: + assert f"drop index if exists {index_name}" in rollback.lower() + ''' +) + CHANGELOG_ENTRY = dedent( ''' ## [2.18.1] - 2026-08-20 @@ -123,9 +210,9 @@ async def fetch(self, query: str, *args): - Global Ask now nominates candidates from persisted project, role/responsibility/affiliation, and cataloged Keyman evidence in addition - to lexical source text and raw source hints. A semantic-only buyer term can - reach the authorized cited post without bypassing visibility or ABAC - (ADR 0091 / ADR 0047 / ADR 0039). + to lexical source text and raw source hints. Every new substring predicate + is backed by a dedicated trigram index, and candidate IDs still cross the + authoritative visibility and ABAC gate (ADR 0091 / ADR 0047 / ADR 0039). ''' ) @@ -163,6 +250,12 @@ async def fetch(self, query: str, *args): - `post_person_mention` plus `cataloged_person`: Keyman name, job title, and mention context. + Each semantic field is tested with its own `ILIKE` predicate so PostgreSQL + can use a matching `pg_trgm` index. Migration 0046 adds indexes for fields + not already covered by migration 0032. A concatenated cross-field expression + is intentionally avoided because it would bypass the existing per-column + indexes and permit table-wide scans despite a small result limit. + Candidate IDs are not evidence and grant no access. The existing authorized `source_post` query and application ABAC predicate still run before body, graph, or semantic facts enter contextual-orchestrator. Question terms, @@ -174,6 +267,7 @@ async def fetch(self, query: str, *args): - Browser and MCP Global Ask share semantic-only candidate retrieval. - Hidden semantic matches remain hidden behind visibility and ABAC. + - Buyer queries do not turn the semantic tables into unindexed scan paths. - This does not claim embedding similarity, exhaustive recall, causal inference, or an answer unsupported by selected sources. @@ -182,6 +276,8 @@ async def fetch(self, query: str, *args): `tests/test_global_ask_sources.py` contains a regression whose title, body, and raw hints omit the buyer term. Persisted semantic evidence alone nominates the authorized source and accompanies the citation. + `tests/test_global_ask_semantic_indexes.py` locks the forward and rollback + index contract. ''' ) @@ -191,8 +287,9 @@ async def fetch(self, query: str, *args): Browser and authenticated MCP Global Ask now find an authorized post when a buyer term exists only in persisted project, responsibility, affiliation, - or Keyman evidence. The cited post still crosses the same visibility and - ABAC boundary before any content reaches contextual-orchestrator. + or Keyman evidence. Each new semantic substring predicate is backed by a + trigram index, and the cited post still crosses the same visibility and ABAC + boundary before any content reaches contextual-orchestrator. This release preserves the MCP question bound and distributed rate limit. It does not claim exhaustive semantic search or invent a source, @@ -209,6 +306,8 @@ async def fetch(self, query: str, *args): - Browser and MCP Global Ask now nominate authorized source posts from persisted project, role/responsibility/affiliation, and Keyman evidence, not only lexical source text and raw source hints (ADR 0091). + - Dedicated `pg_trgm` indexes keep every new substring predicate from + becoming an unindexed semantic-table scan. - Candidate nomination does not bypass the existing visibility and ABAC gate, MCP question bound, or distributed invocation rate limit. ''' @@ -234,7 +333,7 @@ def replace_once(path_name: str, old: str, new: str) -> None: def apply_implementation() -> None: - """Apply production, version, ADR, and release changes.""" + """Apply production, database, version, ADR, and release changes.""" backend = Path("backend/app/post_chat_ingestion.py") backend_text = backend.read_text() if "semantic_candidate_matches" not in backend_text: @@ -244,10 +343,23 @@ def apply_implementation() -> None: backend_text.replace(OLD_CANDIDATE_TAIL, NEW_CANDIDATE_TAIL, 1) ) + Path("migrations/0046_global_ask_semantic_lookup_indexes.sql").write_text( + MIGRATION_TEXT + ) + Path("migrations/rollback").mkdir(parents=True, exist_ok=True) + Path( + "migrations/rollback/0046_global_ask_semantic_lookup_indexes.sql" + ).write_text(ROLLBACK_TEXT) + Path("tests/test_global_ask_semantic_indexes.py").write_text(INDEX_TEST_TEXT) + for path_name, old, new in ( ("pyproject.toml", 'version = "2.18.0"', 'version = "2.18.1"'), ("frontend/package.json", '"version": "2.18.0"', '"version": "2.18.1"'), - ("backend/app/mcp_server.py", '_SERVER_VERSION = "2.18.0"', '_SERVER_VERSION = "2.18.1"'), + ( + "backend/app/mcp_server.py", + '_SERVER_VERSION = "2.18.0"', + '_SERVER_VERSION = "2.18.1"', + ), ): text = Path(path_name).read_text() if new not in text: @@ -259,7 +371,9 @@ def apply_implementation() -> None: marker = "## [2.17.0] - 2026-08-19\n" if changelog_text.count(marker) != 1: raise SystemExit("CHANGELOG 2.17.0 marker drifted") - changelog.write_text(changelog_text.replace(marker, CHANGELOG_ENTRY + marker, 1)) + changelog.write_text( + changelog_text.replace(marker, CHANGELOG_ENTRY + marker, 1) + ) Path("docs/adr/0091-global-ask-semantic-candidate-retrieval.md").write_text( ADR_TEXT From 37ecc6eeffc4611aa8ace2de78564e184172f887 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 15:55:38 -0700 Subject: [PATCH 4/5] ci: verify Global Ask semantic lookup indexes --- .../workflows/apply-global-ask-semantic-retrieval-v2181.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml b/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml index d9e5d96e..90a2c905 100644 --- a/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml +++ b/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml @@ -64,13 +64,14 @@ jobs: grep -F "test_global_sources_discover_posts_from_persisted_semantic_evidence" \ /tmp/global-ask-red.log - - name: Apply the bounded production implementation + - name: Apply the bounded and index-backed implementation run: python .github/scripts/apply_global_ask_semantic_retrieval_2181.py apply - - name: Verify browser, MCP, and adjacent evidence contracts + - name: Verify browser, MCP, database, and adjacent evidence contracts run: | uv run --frozen python -m pytest -q \ tests/test_global_ask_sources.py \ + tests/test_global_ask_semantic_indexes.py \ tests/test_mcp_server.py \ tests/test_post_chat.py \ tests/test_post_chat_ingestion.py \ From 86e0a814e0527e672e0e4297bfc00135af7cbe87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 15:56:14 -0700 Subject: [PATCH 5/5] ci: cancel superseded Global Ask repair runs --- .github/workflows/apply-global-ask-semantic-retrieval-v2181.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml b/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml index 90a2c905..8e7f017a 100644 --- a/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml +++ b/.github/workflows/apply-global-ask-semantic-retrieval-v2181.yml @@ -11,7 +11,7 @@ permissions: concurrency: group: apply-global-ask-semantic-retrieval-v2181-${{ github.event.pull_request.number }} - cancel-in-progress: false + cancel-in-progress: true jobs: apply: