From 333d87fd6e4338e2cddddd9db1954eb63318e04b Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:53:27 -0400 Subject: [PATCH 1/3] test: barrel must export ScanFailure (RED) skip-changelog: test-only red commit; the behavior fragment lands with the implementation --- packages/python/dirsql/__init___test.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/python/dirsql/__init___test.py b/packages/python/dirsql/__init___test.py index a20b75825..8f8ff7d39 100644 --- a/packages/python/dirsql/__init___test.py +++ b/packages/python/dirsql/__init___test.py @@ -3,15 +3,22 @@ relative so the barrel under test is the unit, not an external collaborator. """ -from . import DirSQL, RowEvent, Table, __all__, __version__ +from . import DirSQL, RowEvent, ScanFailure, Table, __all__, __version__ def describe_public_barrel(): def it_exposes_exactly_the_declared_public_names(): - assert set(__all__) == {"DirSQL", "Table", "RowEvent", "__version__"} + assert set(__all__) == { + "DirSQL", + "Table", + "RowEvent", + "ScanFailure", + "__version__", + } def it_re_exports_the_runtime_values(): assert DirSQL is not None assert Table is not None assert RowEvent is not None + assert ScanFailure is not None assert isinstance(__version__, str) From 8af7dd2a43e42e712ef38b0bd6e43f445604ffe9 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:00:09 -0400 Subject: [PATCH 2/3] e2e attestation for claude/1140-scan-failure-export --- .../e2e-attestations/claude-1140-scan-failure-export.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/python/e2e-attestations/claude-1140-scan-failure-export.json diff --git a/packages/python/e2e-attestations/claude-1140-scan-failure-export.json b/packages/python/e2e-attestations/claude-1140-scan-failure-export.json new file mode 100644 index 000000000..109bad4d7 --- /dev/null +++ b/packages/python/e2e-attestations/claude-1140-scan-failure-export.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1789750809, + "exit_code": 0, + "commit": "333d87fd6e4338e2cddddd9db1954eb63318e04b", + "branch": "claude/1140-scan-failure-export" +} From 14a68b865954890be3b683bf53c03c68041a9e54 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:01:10 -0400 Subject: [PATCH 3/3] fix(python): export ScanFailure and sync the type stub (GREEN) The barrel now re-exports ScanFailure so callers can annotate what scan_failures() returns without reaching into dirsql._dirsql. The bundled stub gains ScanFailure, DirSQL.scan_failures, run_cli, and Table.on_file, all of which the bindings already define. PARITY.md's scan-failure claim now names where the Python type is importable from. --- PARITY.md | 4 ++-- .../changelog.d/2026-09-18-export-scan-failure.md | 7 +++++++ packages/python/dirsql/__init__.py | 4 ++-- packages/python/dirsql/_dirsql.pyi | 12 ++++++++++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 packages/python/changelog.d/2026-09-18-export-scan-failure.md diff --git a/PARITY.md b/PARITY.md index da83b0d85..830877611 100644 --- a/PARITY.md +++ b/PARITY.md @@ -419,7 +419,7 @@ public-API binding, so no drift. - `RowEvent` is a frozen class with attribute access (`event.action`, `event.row`). - `AsyncDirSQL` is a pure-Python wrapper using `asyncio.to_thread`. - Watch low-level methods are prefixed with `_` (private convention). -- Ships PEP 561 type information (`py.typed` + `dirsql/_dirsql.pyi`) so downstream consumers see types for `DirSQL`, `Table`, `RowEvent`. Parity-restoring: Rust types come from Rust source, TypeScript types from generated `.d.ts`, Python from the bundled stub. The stub MUST be updated in lockstep with `packages/python/src/lib.rs`. +- Ships PEP 561 type information (`py.typed` + `dirsql/_dirsql.pyi`) so downstream consumers see types for `DirSQL`, `Table`, `RowEvent`, `ScanFailure`. Parity-restoring: Rust types come from Rust source, TypeScript types from generated `.d.ts`, Python from the bundled stub. The stub MUST be updated in lockstep with `packages/python/src/lib.rs`. ### Rust - Uses `snake_case` for all identifiers. @@ -429,7 +429,7 @@ public-API binding, so no drift. - `AsyncDirSQL` uses tokio and `OnceCell` internally. - Watch returns `futures_channel::mpsc::UnboundedReceiver` implementing `Stream`. - All fallible operations return `Result`. Statements classified as writes by SQLite's `sqlite3_stmt_readonly` surface as the unit variant `DirSqlError::WriteForbidden`; in the Python/TS bindings the same condition is a `RuntimeError` / `Error` with a "read-only" message. -- **Parity restored (dirsql#715).** All three SDKs expose the files a scan could not index: Rust `DirSQL::scan_failures() -> &[OnFileFailure]`, Python `await db.scan_failures()` (a list of `ScanFailure` with `.path` / `.message`), TypeScript `await db.scanFailures()` (`ScanFailure[]` with `path` / `message`). A hook that fails, or a row the table rejects, skips that file rather than the scan; the list is how a caller learns the index is incomplete and which files are missing. Empty after a clean scan. The CLI additionally reports skips on stderr (capped at ten, then `... and N more`) and exits `23` — CLI-only by nature, since a library has no exit code. `--allow-skipped` remains unbuilt and is tracked in #715. +- **Parity restored (dirsql#715).** All three SDKs expose the files a scan could not index: Rust `DirSQL::scan_failures() -> &[OnFileFailure]`, Python `await db.scan_failures()` (a list of `ScanFailure` with `.path` / `.message`, importable as `from dirsql import ScanFailure`), TypeScript `await db.scanFailures()` (`ScanFailure[]` with `path` / `message`, exported from the package barrel). A hook that fails, or a row the table rejects, skips that file rather than the scan; the list is how a caller learns the index is incomplete and which files are missing. Empty after a clean scan. The CLI additionally reports skips on stderr (capped at ten, then `... and N more`) and exits `23` — CLI-only by nature, since a library has no exit code. `--allow-skipped` remains unbuilt and is tracked in #715. ### TypeScript - Uses `camelCase` for method names. diff --git a/packages/python/changelog.d/2026-09-18-export-scan-failure.md b/packages/python/changelog.d/2026-09-18-export-scan-failure.md new file mode 100644 index 000000000..c07b7e9a1 --- /dev/null +++ b/packages/python/changelog.d/2026-09-18-export-scan-failure.md @@ -0,0 +1,7 @@ +**Added** + +`ScanFailure` is now importable from the package root (`from dirsql import +ScanFailure`), so the objects `await db.scan_failures()` returns can be +annotated without reaching into `dirsql._dirsql`. The bundled type stub now +also declares `ScanFailure`, `DirSQL.scan_failures`, `run_cli`, and +`Table.on_file`, which it had been missing. No runtime behavior changes. diff --git a/packages/python/dirsql/__init__.py b/packages/python/dirsql/__init__.py index a63883bcb..52c63c348 100644 --- a/packages/python/dirsql/__init__.py +++ b/packages/python/dirsql/__init__.py @@ -3,7 +3,7 @@ Also available for Rust (crates.io: ``dirsql``) and TypeScript (npm: ``dirsql``). """ -from dirsql._dirsql import Table, RowEvent, __version__ +from dirsql._dirsql import Table, RowEvent, ScanFailure, __version__ from dirsql._async import DirSQL -__all__ = ["DirSQL", "Table", "RowEvent", "__version__"] +__all__ = ["DirSQL", "Table", "RowEvent", "ScanFailure", "__version__"] diff --git a/packages/python/dirsql/_dirsql.pyi b/packages/python/dirsql/_dirsql.pyi index 6504a4774..d6bb6aa0d 100644 --- a/packages/python/dirsql/_dirsql.pyi +++ b/packages/python/dirsql/_dirsql.pyi @@ -33,6 +33,7 @@ class Table: name: str ddl: str glob: str + on_file: Callable[[str], list[Row]] strict: bool def __init__( @@ -58,6 +59,15 @@ class RowEvent: @override def __repr__(self) -> str: ... +class ScanFailure: + """One file the initial scan could not index, with the hook's own error.""" + + path: str + message: str + + @override + def __repr__(self) -> str: ... + class DirSQL: """Synchronous binding class. ``dirsql._async.DirSQL`` wraps it.""" @@ -75,6 +85,7 @@ class DirSQL: suppress_config_extensions: bool = False, ) -> None: ... def query(self, sql: str) -> list[Row]: ... + def scan_failures(self) -> list[ScanFailure]: ... def _start_watcher(self) -> None: ... def _poll_events(self, timeout_ms: int) -> list[RowEvent]: ... @@ -92,6 +103,7 @@ class ExtensionPlanEntry: @override def __repr__(self) -> str: ... +def run_cli(argv: list[str]) -> int: ... def config_paths_from_argv(argv: list[str]) -> list[str]: ... def plan_config_extensions( configs: list[tuple[str, str | None]],