Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/tooling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ on:
- 'tools/**'
- '.vscode/setup.sh'
- '.github/workflows/tooling.yml'
- 'client'
- 'reader'
- '.gitmodules'
pull_request:
paths:
- 'tools/**'
- '.vscode/setup.sh'
- '.github/workflows/tooling.yml'
- 'client'
- 'reader'
- '.gitmodules'
workflow_dispatch:

permissions:
Expand All @@ -21,6 +27,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: '3.12'
Expand All @@ -32,3 +40,5 @@ jobs:
run: tools/papyrus check tooling
- name: Check setup script
run: shellcheck .vscode/setup.sh
- name: Check committed reader references
run: tools/papyrus check references
51 changes: 51 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,57 @@
"panel": "dedicated",
"reveal": "always"
}
},
{
"label": "Check reader",
"type": "process",
"command": "${workspaceFolder}/tools/papyrus",
"args": [
"check",
"reader"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "always"
}
},
{
"label": "Test reader",
"type": "process",
"command": "${workspaceFolder}/tools/papyrus",
"args": [
"test",
"reader"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "always"
}
},
{
"label": "Check all",
"type": "process",
"command": "${workspaceFolder}/tools/papyrus",
"args": [
"check",
"all"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "always"
}
}
],
"inputs": [
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Flutter is pinned in `.fvmrc` to the client CI version; use `tools/flutter` and
`tools/dart` rather than the machine's potentially newer SDK.

- `tools/papyrus doctor`: tools, SDK, local configuration, submodules, GitHub auth.
- `tools/papyrus deps client|server`: locked dependency setup.
- `tools/papyrus check client|server|tooling`: non-mutating quality checks.
- `tools/papyrus deps client|reader|server|all`: locked dependency setup.
- `tools/papyrus check client|reader|server|references|tooling|all`: non-mutating quality checks.
- `tools/papyrus test client -- test/path_test.dart`: focused Flutter test.
- `tools/papyrus test server -- tests/services/test_sync.py`: focused pytest run.
- `tools/papyrus run client|server`: development processes.
Expand Down Expand Up @@ -60,4 +60,4 @@ contract decisions with both implementers and run database tests serially.
Follow the existing design tokens and e-ink motion preferences. Library operations
must work offline and remain isolated across guest, user, and server profiles.
Treat stored reading positions and user media as durable data. See
`DEVELOPMENT.md` for setup, measured baseline and known gaps.
`DEVELOPMENT.md` for setup, checks and integration workflow.
71 changes: 20 additions & 51 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
Papyrus is a workspace of independent Git submodules. The client is Flutter with
Provider, SQLite/PowerSync, platform adapters and a Git-pinned EPUB/PDF reader.
The server is FastAPI with async SQLAlchemy, Pydantic, Alembic, PostgreSQL and a
self-hosted PowerSync service. Inspect the source for implemented capabilities;
some README feature lists and setup links are ahead of the current checkout.
self-hosted PowerSync service. Product requirements describe planned capabilities; component READMEs and source
describe current behavior.

## Setup

Expand Down Expand Up @@ -49,11 +49,14 @@ directory. All subprocesses use the appropriate component directory.
| `tools/papyrus doctor` | Inspect tools, config, SDK, repos, Docker and GitHub authentication |
| `tools/papyrus sdk` | Install/link the FVM version from `.fvmrc` |
| `tools/papyrus deps client` | Install client dependencies with its committed lock |
| `tools/papyrus deps reader` | Install reader dependencies with its committed lock |
| `tools/papyrus deps server` | Install server/dev dependencies with the uv lock |
| `tools/papyrus check client` | Non-writing Dart format check, Flutter analysis, four web-bootstrap tests |
| `tools/papyrus check server` | Ruff lint, Ruff format check, strict Mypy |
| `tools/papyrus check tooling` | Eight CLI regression tests and ShellCheck |
| `tools/papyrus check all` | All three check groups above; reports independent failures |
| `tools/papyrus check reader` | Formatting, worker asset drift, library and example analysis |
| `tools/papyrus check references` | Match committed workspace reader and client dependency revisions |
| `tools/papyrus check tooling` | CLI regression tests and ShellCheck |
| `tools/papyrus check all` | Tooling, reference consistency, client, reader and server; reports independent failures |
| `tools/papyrus test client -- test/auth/token_store_test.dart` | Focused Flutter tests; pass options after `--` |
| `tools/papyrus test client -- --coverage` | Full client suite and coverage |
| `tools/papyrus test server -- tests/services/test_sync.py` | Focused backend tests |
Expand All @@ -63,9 +66,18 @@ directory. All subprocesses use the appropriate component directory.

Use `tools/flutter` and `tools/dart` when a command is not covered by the CLI. These
wrappers fail if the pinned SDK is missing. They do not silently use global Flutter.
Reader `deps`, `check`, and `test` commands are also supported; its library lockfile
is ignored, so reader dependency setup resolves its declared constraints normally.
Website and Sphinx checks remain in their own repos.
Reader `deps`, `check`, and `test` commands also use the pinned SDK and committed
lockfile. Run the reader browser suite after UI, layout, focus, or worker changes
as described in `reader/docs/validation.md`.

Website checks use `npm ci && npm run build` in `website/`. Documentation checks
use `uv sync --locked --extra dev && make build` in `docs/` (Graphviz required).

For a joint reader/client change, validate a local reader using an ignored
`client/app/pubspec_overrides.yaml`, then remove the override and pin the published
reader commit in `client/app/pubspec.yaml`. Regenerate the client lock and update
the workspace's reader and client gitlinks together. The reference check compares
committed gitlinks and committed dependency files, independently of local overrides.

Server pytest fixtures drop and recreate test tables. The CLI checks for a separate
local database named `*_test` or `test_*` and locks overlapping CLI server test runs.
Expand Down Expand Up @@ -104,8 +116,7 @@ in disjoint files, then use the contract reviewer. Run database tests serially.

`.codex/config.toml` starts the official Dart tooling MCP through `tools/dart-mcp`,
which uses the pinned SDK. The launcher finds the workspace from the root or a
nested component directory. Protocol initialization and enumeration of **25 tools**
were verified from both the root and `client/app`. These include analysis, tests,
nested component directory. It provides analysis, tests,
hot reload, widget inspection and runtime-error inspection.

Restart the Codex session to load new project MCP/agent configuration if needed.
Expand All @@ -119,45 +130,3 @@ The layout follows [Codex skills](https://learn.chatgpt.com/docs/build-skills),
and [Dart MCP setup](https://docs.flutter.dev/ai/get-started). FVM's
[project configuration](https://fvm.app/documentation/getting-started/configuration)
keeps SDK selection separate from the global toolchain.

## Verified baseline — 2026-10-03

| Scope | Result |
| --- | --- |
| Client lockfile | Five transitive versions normalized to the CI SDK; locked installation succeeds |
| Client formatting | 449 Dart files checked, no changes required |
| Client analysis | No issues |
| Client tests | 1,387 passed; 19 skipped |
| Web bootstrap | Four tests passed |
| Web build | Release compilation and Wasm dry run succeeded |
| Reader tests | 93 passed; 21 skipped in the sibling checkout |
| Server lint/types | Ruff lint and Mypy pass (138 source files) |
| Server tests | 340 passed; two provider smoke tests excluded |
| Server formatting | Five pre-existing files need formatting; check correctly fails |
| Tooling | Eight regression tests, ShellCheck and skill frontmatter validation pass |
| Dart MCP | Initialized successfully; 25 tools enumerated from root and nested cwd |
| Local platforms | Flutter doctor finds Android SDK, Xcode, Chrome and macOS target |
| GitHub CLI | Installed; account sign-in remains pending |

The five server formatting files are `papyrus/models/powersync_demo.py`,
`tests/api/routes/test_auth_sandbox.py`, `test_powersync_sandbox.py`,
`tests/integration/test_auth_smoke.py`, and `tests/services/test_auth.py`.
They were not reformatted as part of tooling setup. Local verification logs are
ignored under `.local/tooling/`.

Live cross-device PowerSync and external OAuth/SMTP smoke tests were not run.
Native release builds and Windows/Linux builds were not run. The global Flutter
SDK remains newer; Flutter doctor may report that PATH mismatch while project
commands and VS Code use FVM correctly.

The client CI now enforces its lockfile and checks formatting without writing.
The workspace tooling workflow runs the CLI regressions and ShellCheck on relevant
pushes and pull requests; it does not require the application submodules or services.

The server README links to auth, acquisition and PowerSync guides absent from
this checkout. Its existing `.env.example`, tests, services, and
`docs/opds-relay.md` are usable sources. The client reader dependency is pinned to
`08a5161b9d00eb73581f74ce087b9ad6c1568ca7`, while the sibling reader checkout is at a
different revision. Editing it alone does not change client behavior. For a joint
reader change, use an ignored `client/app/pubspec_overrides.yaml` with a local path
override during validation, then coordinate a deliberate revision update.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ Use the workspace CLI from the repository root:

```bash
tools/papyrus doctor
tools/papyrus check client
tools/papyrus check server
tools/papyrus deps all
tools/papyrus check all
tools/papyrus test client -- test/auth/token_store_test.dart
tools/papyrus test server -- tests/services/test_sync.py
```
Expand All @@ -90,4 +90,4 @@ It uses the Flutter version pinned in `.fvmrc` and the server's uv lockfile.
The same checks and test suites are available as VS Code tasks.

See [development tooling](DEVELOPMENT.md) for the project skills, agent roles,
Dart MCP integration, setup instructions, and verified baseline.
Dart MCP integration, and setup instructions.
2 changes: 1 addition & 1 deletion client
Submodule client updated 53 files
+0 −41 .github/workflows/server-ci.yml
+0 −40 .github/workflows/server-release.yml
+3 −1 .gitignore
+20 −15 README.md
+ − app/assets/images/auth-illustration-25.png
+ − app/assets/images/auth-illustration-3.png
+ − app/assets/images/book_placeholder_2.jpg
+ − app/assets/images/profile.png
+149 −0 app/lib/acquisition/acquisition_downloads_gateway.dart
+0 −142 app/lib/forms/login_form.dart
+0 −156 app/lib/forms/register_form.dart
+0 −46 app/lib/models/active_filter.dart
+0 −150 app/lib/models/search_filter.dart
+0 −74 app/lib/pages/books_page.dart
+16 −18 app/lib/pages/reader_page.dart
+3 −148 app/lib/providers/acquisition_downloads_provider.dart
+0 −182 app/lib/utils/search_query_parser.dart
+0 −544 app/lib/widgets/add_book/book_import_results_sheet.dart
+0 −189 app/lib/widgets/add_book/digital_book_import_sheet.dart
+0 −169 app/lib/widgets/book/book.dart
+0 −87 app/lib/widgets/book_details/eink_book_details_tab_bar.dart
+0 −108 app/lib/widgets/filter/active_filter_bar.dart
+0 −51 app/lib/widgets/goals/add_goal_card.dart
+0 −31 app/lib/widgets/heading.dart
+0 −19 app/lib/widgets/input/text_input.dart
+10 −413 app/lib/widgets/library/library_advanced_filter_sheet.dart
+411 −0 app/lib/widgets/library/library_filter_fields.dart
+0 −118 app/lib/widgets/profile/profile_stats_card.dart
+0 −36 app/lib/widgets/profile_button.dart
+0 −37 app/lib/widgets/search.dart
+0 −10 app/lib/widgets/search_settings.dart
+0 −38 app/lib/widgets/shared/eink_page_header.dart
+0 −65 app/lib/widgets/shared/quick_filter_chips.dart
+0 −26 app/lib/widgets/shared/view_mode_toggle.dart
+3 −11 app/pubspec.lock
+1 −5 app/pubspec.yaml
+2 −18 app/test/media/media_profile_switch_contract_test.dart
+0 −70 app/test/models/active_filter_test.dart
+0 −353 app/test/models/search_filter_test.dart
+70 −0 app/test/pages/reader_page_test.dart
+112 −0 app/test/pages/reader_theme_test.dart
+0 −210 app/test/utils/search_query_parser_test.dart
+133 −0 app/test/widgets/add_book/book_import_controller_test.dart
+0 −711 app/test/widgets/add_book/book_import_results_sheet_test.dart
+61 −0 app/test/widgets/add_book/book_import_sheet_test.dart
+0 −224 app/test/widgets/add_book/digital_book_import_sheet_test.dart
+3 −18 docs/catalogs-ui-redesign.md
+17 −85 docs/opds-browser-access-analysis.md
+0 −18 docs/opds-support.md
+0 −18 docs/superpowers/plans/2026-09-21-opds-relay.md
+0 −50 docs/superpowers/plans/2026-09-22-opds-library-cache.md
+0 −9 docs/superpowers/plans/2026-09-23-catalog-details-content.md
+0 −13 docs/superpowers/plans/2026-09-26-library-grid-controls.md
2 changes: 1 addition & 1 deletion reader
Submodule reader updated 75 files
+25 −1 .github/workflows/ci.yml
+3 −2 .gitignore
+33 −0 AGENTS.md
+17 −0 CHANGELOG.md
+36 −12 README.md
+536 −0 assets/epub_worker.NOTICES
+20,578 −0 assets/epub_worker.js
+112 −0 docs/architecture.md
+47 −0 docs/integration.md
+17 −0 docs/supported-formats.md
+67 −0 docs/validation.md
+ − example/assets/a_tiny_pdf.pdf
+ − example/assets/reading_lab.epub
+ − example/assets/the_garden_letter.epub
+1 −0 example/ios/Flutter/Debug.xcconfig
+1 −0 example/ios/Flutter/Release.xcconfig
+43 −0 example/ios/Podfile
+76 −2 example/lib/main.dart
+4 −0 example/linux/flutter/generated_plugin_registrant.cc
+1 −0 example/linux/flutter/generated_plugins.cmake
+1 −0 example/macos/Flutter/Flutter-Debug.xcconfig
+1 −0 example/macos/Flutter/Flutter-Release.xcconfig
+2 −0 example/macos/Flutter/GeneratedPluginRegistrant.swift
+48 −0 example/macos/Podfile
+35 −0 example/macos/Podfile.lock
+100 −4 example/macos/Runner.xcodeproj/project.pbxproj
+3 −0 example/macos/Runner.xcworkspace/contents.xcworkspacedata
+2 −0 example/macos/Runner/DebugProfile.entitlements
+2 −0 example/macos/Runner/Release.entitlements
+78 −6 example/pubspec.lock
+2 −0 example/pubspec.yaml
+26 −2 example/test/widget_test.dart
+3 −0 example/windows/flutter/generated_plugin_registrant.cc
+1 −0 example/windows/flutter/generated_plugins.cmake
+1 −0 lib/papyrus_reader.dart
+23 −12 lib/src/controller/reader_controller.dart
+39 −2 lib/src/domain/reader_locator.dart
+6 −0 lib/src/domain/reader_snapshot.dart
+18 −2 lib/src/engine/epub/epub_content_renderer.dart
+199 −594 lib/src/engine/epub/epub_reader_engine.dart
+217 −0 lib/src/engine/epub/epub_rich_layout.dart
+477 −0 lib/src/engine/epub/epub_viewport.dart
+179 −0 lib/src/engine/epub/worker/epub_content.dart
+324 −0 lib/src/engine/epub/worker/epub_processor.dart
+17 −0 lib/src/engine/epub/worker/epub_worker.dart
+118 −0 lib/src/engine/epub/worker/epub_worker_native.dart
+105 −0 lib/src/engine/epub/worker/epub_worker_web.dart
+404 −87 lib/src/engine/pdf/pdf_facade.dart
+113 −29 lib/src/engine/pdf/pdf_reader_engine.dart
+345 −840 lib/src/presentation/papyrus_reader.dart
+123 −0 lib/src/presentation/reader_contents_panel.dart
+278 −0 lib/src/presentation/reader_controls.dart
+112 −0 lib/src/presentation/reader_material_theme.dart
+56 −0 lib/src/presentation/reader_panel_header.dart
+393 −0 lib/src/presentation/reader_settings_panel.dart
+2 −2 lib/src/presentation/reader_theme_data.dart
+6 −0 lib/src/presentation/reader_ui_builders.dart
+626 −0 pubspec.lock
+8 −37 pubspec.yaml
+6 −4 test/controller/reader_controller_test.dart
+66 −50 test/engine/epub_reader_engine_test.dart
+164 −0 test/engine/epub_rich_layout_test.dart
+118 −0 test/engine/epub_worker_test.dart
+210 −29 test/engine/pdf_reader_engine_test.dart
+219 −0 test/presentation/epub_reading_flow_test.dart
+435 −3 test/presentation/papyrus_reader_test.dart
+1 −0 test/support/fake_reader_engine.dart
+42 −0 test/support/in_process_epub_worker.dart
+5 −3 test/support/synthetic_epub.dart
+42 −0 tool/browser/package-lock.json
+7 −0 tool/browser/package.json
+349 −0 tool/browser/smoke.mjs
+20 −0 tool/build_epub_worker.sh
+23 −0 tool/epub_worker.dart
+32 −12 tool/generate_example_assets.dart
35 changes: 35 additions & 0 deletions tools/check_references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Check the committed workspace reader pin against the committed client lock."""

import re
import subprocess
from pathlib import Path


def git(root: Path, *args: str) -> str:
return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip()


def check(root: Path) -> None:
client = git(root, "ls-tree", "HEAD", "client").split()[2]
reader = git(root, "ls-tree", "HEAD", "reader").split()[2]
manifest = git(root / "client", "show", f"{client}:app/pubspec.yaml")
lock = git(root / "client", "show", f"{client}:app/pubspec.lock")
for source, field in [(manifest, "ref"), (lock, "resolved-ref")]:
block = re.search(r"(?m)^ papyrus_reader:\n((?: .*\n?)+)", source)
pin = (
re.search(rf'{field}:\s*["\x27]?([a-f0-9]{{40}})\b', block[1])
if block
else None
)
if pin is None or pin[1] != reader:
raise ValueError(
f"Committed client {field} must match workspace reader {reader}"
)
print(f"Committed reader references agree: {reader}")


if __name__ == "__main__":
try:
check(Path(__file__).resolve().parent.parent)
except (IndexError, ValueError, subprocess.CalledProcessError) as error:
raise SystemExit(f"Reader reference check failed: {error}") from error
24 changes: 18 additions & 6 deletions tools/papyrus
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CLIENT = ROOT / "client/app"
SERVER = ROOT / "server"
READER = ROOT / "reader"


def run(args, cwd=ROOT, env=None):
Expand Down Expand Up @@ -77,11 +78,18 @@ def checks(component):
"--set-exit-if-changed",
"lib",
"test",
"tool",
"example/lib",
"example/test",
],
ROOT / "reader",
READER,
),
([sdk_command("flutter"), "analyze", "--no-pub"], ROOT / "reader"),
(["bash", "tool/build_epub_worker.sh", "--check"], READER),
([sdk_command("flutter"), "analyze", "--no-pub"], READER),
([sdk_command("flutter"), "analyze", "--no-pub"], READER / "example"),
]
if component == "references":
return [([sys.executable, "tools/check_references.py"], ROOT)]
return [
(
[sys.executable, "-m", "unittest", "discover", "-s", "tools/tests", "-v"],
Expand All @@ -92,7 +100,11 @@ def checks(component):


def check(component):
components = ["tooling", "client", "server"] if component == "all" else [component]
components = (
["tooling", "references", "client", "reader", "server"]
if component == "all"
else [component]
)
failed = []
for item in components:
for args, cwd in checks(item):
Expand Down Expand Up @@ -226,7 +238,7 @@ def main(argv=None):
)
quality.add_argument(
"component",
choices=["client", "server", "reader", "tooling", "all"],
choices=["client", "server", "reader", "tooling", "references", "all"],
default="all",
nargs="?",
)
Expand All @@ -252,7 +264,7 @@ def main(argv=None):
return test(options.component, args)
if options.command == "deps":
selected = (
["client", "server"]
["client", "reader", "server"]
if options.component == "all"
else [options.component]
)
Expand All @@ -270,7 +282,7 @@ def main(argv=None):
sdk_command("flutter"),
"pub",
"get",
*(["--enforce-lockfile"] if item == "client" else []),
"--enforce-lockfile",
],
directory,
)
Expand Down
21 changes: 21 additions & 0 deletions tools/tests/test_papyrus.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ def test_client_format_check_does_not_write_files(self):
self.assertIn("--set-exit-if-changed", args)
self.assertEqual(cwd, cli.CLIENT)

def test_all_checks_include_reader_and_reference_gate(self):
with patch.object(cli, "checks", return_value=[]) as checks:
self.assertEqual(cli.check("all"), 0)
self.assertEqual(
[call.args[0] for call in checks.call_args_list],
["tooling", "references", "client", "reader", "server"],
)

def test_all_flutter_dependencies_enforce_locks(self):
with (
patch.object(cli, "sdk_command", return_value="flutter"),
patch.object(cli, "run", return_value=0) as run,
):
self.assertEqual(cli.main(["deps", "all"]), 0)
self.assertEqual(
[call.args[1] for call in run.call_args_list],
[cli.CLIENT, cli.READER, cli.SERVER],
)
for call in run.call_args_list[:2]:
self.assertIn("--enforce-lockfile", call.args[0])

def test_sdk_absent_does_not_fall_back_to_global_flutter(self):
with (
tempfile.TemporaryDirectory() as directory,
Expand Down
Loading
Loading