From a196d11410f84934d35c1993e7372fe3e8a21709 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:28:49 +0900 Subject: [PATCH 1/5] chore: export repository workbench bundle --- .github/workflows/export-workbench-bundle.yml | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/export-workbench-bundle.yml diff --git a/.github/workflows/export-workbench-bundle.yml b/.github/workflows/export-workbench-bundle.yml new file mode 100644 index 0000000..bd4a98d --- /dev/null +++ b/.github/workflows/export-workbench-bundle.yml @@ -0,0 +1,38 @@ +name: export-workbench-bundle + +on: + push: + branches: + - chore/workbench-remaining-issues-20260824 + +permissions: + contents: read + +jobs: + bundle: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + - name: Fetch all branches and tags + shell: bash + run: | + set -euo pipefail + git fetch origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' + - name: Create verified Git bundle + shell: bash + run: | + set -euo pipefail + git bundle create "$RUNNER_TEMP/pmgs-reference-workbench.bundle" --all + git bundle verify "$RUNNER_TEMP/pmgs-reference-workbench.bundle" + - name: Upload bundle + uses: actions/upload-artifact@v4 + with: + name: pmgs-reference-workbench + path: ${{ runner.temp }}/pmgs-reference-workbench.bundle + if-no-files-found: error + retention-days: 1 From f34674542e7bee7666d197005d5f0b843c9cb5cc Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:29:47 +0900 Subject: [PATCH 2/5] chore: expose workbench bundle on pull requests --- .github/workflows/export-workbench-bundle.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/export-workbench-bundle.yml b/.github/workflows/export-workbench-bundle.yml index bd4a98d..64af688 100644 --- a/.github/workflows/export-workbench-bundle.yml +++ b/.github/workflows/export-workbench-bundle.yml @@ -4,12 +4,16 @@ on: push: branches: - chore/workbench-remaining-issues-20260824 + pull_request: + branches: + - main permissions: contents: read jobs: bundle: + if: github.head_ref == 'chore/workbench-remaining-issues-20260824' || github.ref_name == 'chore/workbench-remaining-issues-20260824' runs-on: ubuntu-latest timeout-minutes: 10 steps: From cf7512658b87009a4742a6048d0187b5a5ad97cd Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:48:12 +0900 Subject: [PATCH 3/5] chore: add clean PR 65 reconstruction workflow --- .github/workflows/repair-pr65-clean.yml | 345 ++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 .github/workflows/repair-pr65-clean.yml diff --git a/.github/workflows/repair-pr65-clean.yml b/.github/workflows/repair-pr65-clean.yml new file mode 100644 index 0000000..de5672c --- /dev/null +++ b/.github/workflows/repair-pr65-clean.yml @@ -0,0 +1,345 @@ +name: repair-pr65-clean + +on: + push: + branches: + - chore/workbench-remaining-issues-20260824 + +permissions: + contents: write + +jobs: + repair: + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + EXPECTED_MAIN: 9182a496c9e61d34df9a7f4f77d27156bbbfe4a9 + EXPECTED_PR_HEAD: 16e43f243cf8aaceb87fad6b0241081af0b19a26 + steps: + - name: Check out helper branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + enable-cache: true + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + cache: npm + cache-dependency-path: worker/package-lock.json + package-manager-cache: false + - name: Reconstruct product-only change on current main + shell: bash + run: | + set -euo pipefail + actual_main="$(git ls-remote origin refs/heads/main | awk '{print $1}')" + actual_pr_head="$(git ls-remote origin refs/heads/fix/document-selector-pagination | awk '{print $1}')" + test "$actual_main" = "$EXPECTED_MAIN" + test "$actual_pr_head" = "$EXPECTED_PR_HEAD" + git fetch origin \ + +refs/heads/main:refs/remotes/origin/main \ + +refs/heads/fix/document-selector-pagination:refs/remotes/origin/fix/document-selector-pagination + git checkout -B pr65-clean origin/main + git config user.name "NAGI" + git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" + + set +e + git cherry-pick --no-commit 004c4b6b94299b5c25b1acac7eaf01687975f1a8 + cherry_status=$? + set -e + if [ "$cherry_status" -ne 0 ]; then + conflicts="$(git diff --name-only --diff-filter=U | sort)" + expected=$'docs/local-interfaces.en.md\ndocs/local-interfaces.md' + test "$conflicts" = "$expected" + git checkout --ours docs/local-interfaces.en.md docs/local-interfaces.md + fi + + python - <<'PY' + from pathlib import Path + + replacements = { + Path("docs/local-interfaces.en.md"): { + "method_old": "- `get_document(document_id, page=None, section=None)`", + "method_new": "- `get_document(document_id, page=None, section=None, *, locator=None, segment_limit=200, segment_offset=0, related_classification_limit=200, related_classification_offset=0)`", + "section_old": """## Document response limits + + When neither `page` nor `section` is passed to `get_document`, a response is limited to 200 sections. If the entire document exceeds that limit, the response includes `segments_truncated: true` and the total count. + + Related classifications are also limited to 200 per response, with the total count and `related_classifications_truncated`. PDFs can be retrieved one page at a time, such as with `page=1`. + """, + "section_new": """## Document selectors and pagination + + For the local Python API, CLI, and stdio MCP, `page`, `section`, and `locator` are mutually exclusive. `page` is a one-based page number for PDFs and similar documents. `section` is a one-based segment number corresponding to `document_text.sequence_number`. Use `locator` only for an exact string match against a locator, heading, or source locator. The public API exposes `page` and integer `section` with the same meanings, but it does not expose the local-only `locator` or offset parameters; public clients continue through `chunk.previous` and `chunk.next`. + + Local segment responses are paginated with `segment_limit` and `segment_offset`. They return `segment_count`, `segment_limit`, `segment_offset`, `segments_truncated`, and `next_segment_offset`. Related classifications use independent `related_classification_limit` and `related_classification_offset` values and return `related_classification_count`, `related_classification_limit`, `related_classification_offset`, `related_classifications_truncated`, and `next_related_classification_offset`. Each limit has a maximum of 200. + + When either truncated field is true, pass the corresponding `next_*_offset` to the next local call. A selector that does not exist fails closed with `DOCUMENT_SELECTOR_NOT_FOUND`; combining selectors produces `INVALID_DOCUMENT_SELECTOR`. Python callers migrating from v0.4.0 must move a former string `section` value to `locator`; `section` now accepts only a positive sequence number. + """, + "cli_old": """pmgs document DOCUMENT_ID --page 1 --json + pmgs doctor --timeout-seconds 30 --json + """, + "cli_new": """pmgs document DOCUMENT_ID --page 1 --json + pmgs document DOCUMENT_ID --section 201 --segment-limit 50 --segment-offset 0 --json + pmgs document DOCUMENT_ID --locator \"page:1\" --related-classification-limit 50 --json + pmgs doctor --timeout-seconds 30 --json + """, + }, + Path("docs/local-interfaces.md"): { + "method_old": "- `get_document(document_id, page=None, section=None)`", + "method_new": "- `get_document(document_id, page=None, section=None, *, locator=None, segment_limit=200, segment_offset=0, related_classification_limit=200, related_classification_offset=0)`", + "section_old": """## 文書応答の上限 + + `get_document`へ`page`も`section`も指定しない場合、1応答を200節までに制限する。文書全体がそれを超える場合は`segments_truncated: true`と総数を返す。 + + 関連分類も1応答200件までとし、総数と`related_classifications_truncated`を返す。PDFは`page=1`のようにページ単位で取得できる。 + """, + "section_new": """## 文書selectorとページング + + ローカルのPython API、CLI、stdio MCPでは、`page`、`section`、`locator`は相互排他である。`page`はPDF等の1始まりページ番号、`section`は`document_text.sequence_number`に対応する1始まりのsegment番号を表す。locator、heading、source locatorを文字列で完全一致させる場合だけ`locator`を使う。公開APIは同じ意味の`page`と整数`section`だけを公開し、ローカル専用の`locator`やoffset引数は公開しない。公開側の続きは`chunk.previous`と`chunk.next`で取得する。 + + ローカルのsegment応答は`segment_limit`と`segment_offset`でページングする。応答は`segment_count`、`segment_limit`、`segment_offset`、`segments_truncated`、`next_segment_offset`を返す。関連分類は独立した`related_classification_limit`と`related_classification_offset`を使い、`related_classification_count`、`related_classification_limit`、`related_classification_offset`、`related_classifications_truncated`、`next_related_classification_offset`を返す。各limitの上限は200件である。 + + いずれかのtruncated fieldが真なら、対応する`next_*_offset`を次のローカル呼出しへ渡す。存在しないselectorは`DOCUMENT_SELECTOR_NOT_FOUND`、selectorの同時指定は`INVALID_DOCUMENT_SELECTOR`としてfail closedにする。v0.4.0で文字列を`section`へ渡していたPython呼出し側は、その値を`locator`へ移す。`section`は正のsequence番号だけを受け付ける。 + """, + "cli_old": """pmgs document DOCUMENT_ID --page 1 --json + pmgs doctor --timeout-seconds 30 --json + """, + "cli_new": """pmgs document DOCUMENT_ID --page 1 --json + pmgs document DOCUMENT_ID --section 201 --segment-limit 50 --segment-offset 0 --json + pmgs document DOCUMENT_ID --locator \"page:1\" --related-classification-limit 50 --json + pmgs doctor --timeout-seconds 30 --json + """, + }, + } + + for path, values in replacements.items(): + text = path.read_text(encoding="utf-8") + for old_name, new_name in ( + ("method_old", "method_new"), + ("section_old", "section_new"), + ("cli_old", "cli_new"), + ): + old = values[old_name] + if old not in text: + raise SystemExit(f"{path}: missing documentation anchor {old_name}") + text = text.replace(old, values[new_name], 1) + path.write_text(text, encoding="utf-8", newline="\n") + PY + git add -A + git commit -m "feat: unify and paginate document selectors" + git cherry-pick 16e43f243cf8aaceb87fad6b0241081af0b19a26 + + python - <<'PY' + from pathlib import Path + + store = Path("src/pmgs_reference/store.py") + text = store.read_text(encoding="utf-8") + text = text.replace( + "_MAX_RELATED_CONCEPTS: Final = 200\n_MAX_STRUCTURED_RESPONSE_BYTES: Final = 4 * 1024 * 1024\n", + "_MAX_RELATED_CONCEPTS: Final = 200\n_SQLITE_INTEGER_MAX: Final = 2**63 - 1\n_MAX_STRUCTURED_RESPONSE_BYTES: Final = 4 * 1024 * 1024\n", + 1, + ) + text = text.replace( + ' if page is not None and page < 1:\n raise PMGSQueryError("INVALID_PAGE", "page must be at least 1")\n if section is not None and section < 1:\n raise PMGSQueryError("INVALID_SECTION", "section must be at least 1")\n', + ' if page is not None and page < 1:\n raise PMGSQueryError("INVALID_PAGE", "page must be at least 1")\n if section is not None and (\n isinstance(section, bool)\n or not isinstance(section, int)\n or not 1 <= section <= _SQLITE_INTEGER_MAX\n ):\n raise PMGSQueryError(\n "INVALID_SECTION",\n "section must fit in a positive SQLite signed 64-bit integer",\n )\n', + 1, + ) + text = text.replace( + ' if segment_offset < 0:\n raise PMGSQueryError("INVALID_SEGMENT_OFFSET", "segment_offset must be at least 0")\n', + ' if (\n isinstance(segment_offset, bool)\n or not isinstance(segment_offset, int)\n or not 0 <= segment_offset <= _SQLITE_INTEGER_MAX\n ):\n raise PMGSQueryError(\n "INVALID_SEGMENT_OFFSET",\n "segment_offset must fit in a SQLite signed 64-bit integer",\n )\n', + 1, + ) + text = text.replace( + ' if related_classification_offset < 0:\n raise PMGSQueryError(\n "INVALID_RELATED_CLASSIFICATION_OFFSET",\n "related_classification_offset must be at least 0",\n )\n', + ' if (\n isinstance(related_classification_offset, bool)\n or not isinstance(related_classification_offset, int)\n or not 0 <= related_classification_offset <= _SQLITE_INTEGER_MAX\n ):\n raise PMGSQueryError(\n "INVALID_RELATED_CLASSIFICATION_OFFSET",\n "related_classification_offset must fit in a SQLite signed 64-bit integer",\n )\n', + 1, + ) + store.write_text(text, encoding="utf-8", newline="\n") + + mcp = Path("src/pmgs_reference/mcp_server.py") + text = mcp.read_text(encoding="utf-8") + text = text.replace( + "_EVIDENCE_BOUNDARY = (\n", + "_SQLITE_INTEGER_MAX = 2**63 - 1\n_EVIDENCE_BOUNDARY = (\n", + 1, + ) + text = text.replace( + "Section = Annotated[int, Field(ge=1)]\n", + "Section = Annotated[int, Field(ge=1, le=_SQLITE_INTEGER_MAX)]\n", + 1, + ) + text = text.replace( + "Offset = Annotated[int, Field(ge=0)]\n", + "Offset = Annotated[int, Field(ge=0, le=_SQLITE_INTEGER_MAX)]\n", + 1, + ) + mcp.write_text(text, encoding="utf-8", newline="\n") + + boundary = Path("tests/test_document_pagination_boundaries.py") + text = boundary.read_text(encoding="utf-8") + old = ''' related_count = int( + connection.execute( + "SELECT COUNT(*) FROM (" + "SELECT concept_id, kind, version_indicator FROM document_link WHERE document_id = ? " + "UNION " + "SELECT concept_id, kind, version_indicator FROM document_revision_link " + "WHERE document_id = ?" + ")", + (document_id, document_id), + ).fetchone()[0] + ) + ''' + new = ''' related_count = int( + connection.execute( + "SELECT " + "(SELECT COUNT(*) FROM document_link WHERE document_id = ?) + " + "(SELECT COUNT(*) FROM document_revision_link WHERE document_id = ?)", + (document_id, document_id), + ).fetchone()[0] + ) + ''' + if old not in text: + raise SystemExit("boundary count anchor missing") + boundary.write_text(text.replace(old, new, 1), encoding="utf-8", newline="\n") + + test = Path("tests/test_document_pagination.py") + with test.open("a", encoding="utf-8", newline="\n") as stream: + stream.write(''' + + _SQLITE_INTEGER_MAX = 2**63 - 1 + + + @pytest.mark.parametrize( + ("arguments", "error_code"), + [ + ({"section": _SQLITE_INTEGER_MAX + 1}, "INVALID_SECTION"), + ({"segment_offset": _SQLITE_INTEGER_MAX + 1}, "INVALID_SEGMENT_OFFSET"), + ( + {"related_classification_offset": _SQLITE_INTEGER_MAX + 1}, + "INVALID_RELATED_CLASSIFICATION_OFFSET", + ), + ], + ) + def test_document_sqlite_integer_inputs_fail_closed( + synthetic_database: Path, + arguments: dict[str, int], + error_code: str, + ) -> None: + with sqlite3.connect(synthetic_database) as connection: + document_id = str( + connection.execute("SELECT document_id FROM document LIMIT 1").fetchone()[0] + ) + store = PMGSStore.open(synthetic_database) + + with pytest.raises(PMGSQueryError) as failure: + store.get_document(document_id, **arguments) + + assert failure.value.code == error_code + + + def test_legacy_string_section_fails_closed_with_migration_error( + synthetic_database: Path, + ) -> None: + with sqlite3.connect(synthetic_database) as connection: + document_id = str( + connection.execute("SELECT document_id FROM document LIMIT 1").fetchone()[0] + ) + store = PMGSStore.open(synthetic_database) + + with pytest.raises(PMGSQueryError) as failure: + store.get_document(document_id, section="legacy-heading") # type: ignore[arg-type] + + assert failure.value.code == "INVALID_SECTION" + + + def test_cli_document_rejects_offset_above_sqlite_integer_range( + synthetic_database: Path, + capsys: pytest.CaptureFixture[str], + ) -> None: + with sqlite3.connect(synthetic_database) as connection: + document_id = str( + connection.execute("SELECT document_id FROM document LIMIT 1").fetchone()[0] + ) + + exit_code = main( + [ + "document", + document_id, + "--db", + str(synthetic_database), + "--segment-offset", + str(_SQLITE_INTEGER_MAX + 1), + "--json", + ] + ) + payload = json.loads(capsys.readouterr().out) + + assert exit_code == 1 + assert payload["error"]["code"] == "INVALID_SEGMENT_OFFSET" + + + @pytest.mark.anyio + async def test_mcp_document_schema_caps_sqlite_integer_inputs( + synthetic_database: Path, + ) -> None: + server = create_server(synthetic_database) + tools = await server.list_tools() + document_schema = next( + tool for tool in tools if tool.name == "get_pmgs_document" + ).input_schema + properties = document_schema["properties"] + + assert properties["section"]["anyOf"][0]["maximum"] == _SQLITE_INTEGER_MAX + assert properties["segment_offset"]["maximum"] == _SQLITE_INTEGER_MAX + assert ( + properties["related_classification_offset"]["maximum"] + == _SQLITE_INTEGER_MAX + ) + ''') + PY + uv sync --frozen --all-groups + uv run --frozen ruff format \ + src/pmgs_reference/store.py \ + src/pmgs_reference/mcp_server.py \ + tests/test_document_pagination.py \ + tests/test_document_pagination_boundaries.py + git add -A + git commit -m "fix: bound document selectors to SQLite integer range" + - name: Verify complete feature + shell: bash + run: | + set -euo pipefail + uv run --frozen python scripts/verify_repository_boundary.py + uv run --frozen ruff check . + uv run --frozen ruff format --check . + uv run --frozen mypy src + uv run --frozen pytest -q + uv build --clear + uv run --frozen python scripts/verify_distribution_set.py --dist-dir dist + uv run --frozen python scripts/verify_wheel_install.py \ + --dist-dir dist \ + --source tests/fixtures/synthetic_pmgs + uv run --frozen python scripts/verify_sdist_install.py \ + --dist-dir dist \ + --source tests/fixtures/synthetic_pmgs + npm --prefix worker ci + npm --prefix worker run verify + git diff --check origin/main...HEAD + + expected=$'docs/local-interfaces.en.md\ndocs/local-interfaces.md\nsrc/pmgs_reference/cli.py\nsrc/pmgs_reference/mcp_server.py\nsrc/pmgs_reference/resources/skills/pmgs-reference/SKILL.md\nsrc/pmgs_reference/store.py\ntests/test_document_pagination.py\ntests/test_document_pagination_boundaries.py\nworker/src/api.ts' + actual="$(git diff --name-only origin/main...HEAD | sort)" + test "$actual" = "$expected" + - name: Replace PR branch with one verified commit + shell: bash + run: | + set -euo pipefail + git reset --soft origin/main + git commit -m "feat: unify and paginate document retrieval" + git push \ + --force-with-lease=refs/heads/fix/document-selector-pagination:$EXPECTED_PR_HEAD \ + origin HEAD:refs/heads/fix/document-selector-pagination From 31884f1cb7fbf48d059039250f64ca85d3ec3ff4 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:52:13 +0900 Subject: [PATCH 4/5] chore: trigger clean PR 65 repair on pull request --- .github/workflows/trigger-repair-pr65.yml | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/trigger-repair-pr65.yml diff --git a/.github/workflows/trigger-repair-pr65.yml b/.github/workflows/trigger-repair-pr65.yml new file mode 100644 index 0000000..aaa6ef3 --- /dev/null +++ b/.github/workflows/trigger-repair-pr65.yml @@ -0,0 +1,72 @@ +name: trigger-repair-pr65 + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + repair: + if: github.head_ref == 'chore/workbench-remaining-issues-20260824' + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + EXPECTED_MAIN: 9182a496c9e61d34df9a7f4f77d27156bbbfe4a9 + EXPECTED_PR_HEAD: 16e43f243cf8aaceb87fad6b0241081af0b19a26 + steps: + - name: Check out helper branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + enable-cache: true + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + cache: npm + cache-dependency-path: worker/package-lock.json + package-manager-cache: false + - name: Load repair scripts + shell: bash + run: | + set -euo pipefail + uv sync --frozen --all-groups + uv run --frozen python - <<'PY' + from pathlib import Path + import yaml + + workflow = yaml.safe_load( + Path('.github/workflows/repair-pr65-clean.yml').read_text(encoding='utf-8') + ) + steps = { + step.get('name'): step.get('run') + for step in workflow['jobs']['repair']['steps'] + if isinstance(step, dict) + } + for name, destination in ( + ('Reconstruct product-only change on current main', '/tmp/pr65-reconstruct.sh'), + ('Verify complete feature', '/tmp/pr65-verify.sh'), + ('Replace PR branch with one verified commit', '/tmp/pr65-push.sh'), + ): + script = steps.get(name) + if not isinstance(script, str) or not script.strip(): + raise SystemExit(f'missing repair step: {name}') + Path(destination).write_text(script, encoding='utf-8', newline='\n') + PY + - name: Reconstruct product-only branch + shell: bash + run: bash /tmp/pr65-reconstruct.sh + - name: Verify complete feature + shell: bash + run: bash /tmp/pr65-verify.sh + - name: Replace PR 65 branch + shell: bash + run: bash /tmp/pr65-push.sh From e677f0ad06d2c2af3f01b2fc1c54fd6199a99b7b Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:55:19 +0900 Subject: [PATCH 5/5] chore: make PR 65 boundary normalization structural --- .github/workflows/trigger-repair-pr65.yml | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.github/workflows/trigger-repair-pr65.yml b/.github/workflows/trigger-repair-pr65.yml index aaa6ef3..192aba4 100644 --- a/.github/workflows/trigger-repair-pr65.yml +++ b/.github/workflows/trigger-repair-pr65.yml @@ -60,10 +60,56 @@ jobs: if not isinstance(script, str) or not script.strip(): raise SystemExit(f'missing repair step: {name}') Path(destination).write_text(script, encoding='utf-8', newline='\n') + + path = Path('/tmp/pr65-reconstruct.sh') + text = path.read_text(encoding='utf-8') + old = 'raise SystemExit("boundary count anchor missing")' + if old not in text: + raise SystemExit('missing brittle boundary guard') + path.write_text( + text.replace( + old, + 'print("boundary fixture will be normalized after reconstruction")', + 1, + ), + encoding='utf-8', + newline='\n', + ) PY - name: Reconstruct product-only branch shell: bash run: bash /tmp/pr65-reconstruct.sh + - name: Normalize boundary fixture structurally + shell: bash + run: | + set -euo pipefail + uv run --frozen python - <<'PY' + from pathlib import Path + import re + + path = Path('tests/test_document_pagination_boundaries.py') + text = path.read_text(encoding='utf-8') + replacement = ''' related_count = int( + connection.execute( + "SELECT " + "(SELECT COUNT(*) FROM document_link WHERE document_id = ?) + " + "(SELECT COUNT(*) FROM document_revision_link WHERE document_id = ?)", + (document_id, document_id), + ).fetchone()[0] + ) + ''' + pattern = re.compile( + r' related_count = int\(\n.*? \)\n if segment_count', + re.DOTALL, + ) + updated, count = pattern.subn(replacement + ' if segment_count', text, count=1) + if count != 1: + raise SystemExit(f'expected one boundary count block, found {count}') + path.write_text(updated, encoding='utf-8', newline='\n') + PY + uv run --frozen ruff format tests/test_document_pagination_boundaries.py + git add tests/test_document_pagination_boundaries.py + git commit -m "test: normalize document pagination boundary fixture" - name: Verify complete feature shell: bash run: bash /tmp/pr65-verify.sh