Skip to content

Commit f688f80

Browse files
committed
docs: expand architecture and persistence guides
1 parent 689669e commit f688f80

17 files changed

Lines changed: 717 additions & 2 deletions

docs/en/architecture.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# System Architecture
2+
3+
NBER-CLI is organized as a small layered application. The command line interface and the MCP server are entry points; the shared core performs network retrieval, parsing, formatting, downloads, feed processing, and local persistence.
4+
5+
## Component Map
6+
7+
```mermaid
8+
flowchart TD
9+
user[CLI user] --> cli[cli.py]
10+
agent[AI agent] --> mcp[mcp.py / FastMCP]
11+
cli --> fetcher[fetcher.py]
12+
cli --> download[download.py]
13+
cli --> feed[feed.py]
14+
cli --> cache[info_cache.py]
15+
mcp --> fetcher
16+
mcp --> download
17+
mcp --> cache
18+
fetcher --> nber[NBER web pages and search API]
19+
download --> pdf[NBER PDF endpoint]
20+
feed --> rss[NBER RSS feed]
21+
cache --> db[db.py / SQLite]
22+
feed --> db
23+
cli --> db
24+
db --> config[config_store.py / config.json]
25+
```
26+
27+
## Entry Points
28+
29+
| Surface | File | Main role |
30+
| --- | --- | --- |
31+
| Console script | `src/nber_cli/cli.py` | Parses arguments, prints text or JSON, records CLI logs, and maps user commands to shared functions. |
32+
| Python module | `src/nber_cli/__main__.py` | Lets users run `python -m nber_cli` with the same behavior as `nber-cli`. |
33+
| MCP server | `src/nber_cli/mcp.py` | Exposes `get_paper_info`, `search_papers`, and `download_paper` for agent clients. |
34+
| Public package API | `src/nber_cli/__init__.py` | Defines the top-level stable imports through `__all__`. |
35+
36+
## Core Workflows
37+
38+
| Workflow | Path through the system | Persistence behavior |
39+
| --- | --- | --- |
40+
| Search | `cli.py` or `mcp.py` -> `fetcher.search_nber` -> NBER search API -> `formatters.search_results` | CLI search records `query_log`; MCP search does not. |
41+
| Paper info | `cli.py` or `mcp.py` -> `info_cache.py` -> `db.py` cache lookup -> `fetcher.get_nber` on miss | Reads and writes `info_cache` when enabled; both surfaces record `info_log`. |
42+
| Download | `cli.py` or `mcp.py` -> `download.py` -> NBER PDF endpoint | CLI download records `download_log`; MCP download does not. |
43+
| Feed | `cli.py` -> `feed.py` -> NBER RSS -> `db.py` | Stores feed items in `feed_items` and fetch summaries in `feed_fetches`. |
44+
| Config | `cli.py` -> `config_store.py` | Reads and writes `~/.nber-cli/config.json`. |
45+
46+
## Network Layer
47+
48+
`fetcher.py` retrieves paper pages and search results. It sends browser-like headers for every NBER request, enforces TLS 1.2 for synchronous page loads, retries transient failures, and validates that a fetched paper page matches the requested paper ID.
49+
50+
`download.py` uses `aiohttp` for PDF downloads. Single downloads buffer the full PDF in memory before writing to disk. Batch downloads share a client session and use an `asyncio.Semaphore` to cap concurrency.
51+
52+
`feed.py` fetches the public RSS feed, parses it with `defusedxml`, repairs a narrow class of malformed text containing unescaped `<` characters, and ignores malformed items that cannot produce a paper ID.
53+
54+
## Output and Formatting
55+
56+
The CLI keeps the human-readable output separate from the structured payloads:
57+
58+
- `formatters.info` and `formatters.info_text` format paper metadata.
59+
- `formatters.search_results` and `formatters.search_results_text` format search results.
60+
- `formatters.feed_results` and `formatters.feed_results_text` format RSS feed results.
61+
62+
The MCP server returns dictionaries rather than CLI text. This lets agents consume the same underlying data without scraping terminal output.
63+
64+
## Trust Boundaries
65+
66+
NBER-CLI does not require credentials and does not send the local database to project infrastructure. The risky boundary is filesystem writes:
67+
68+
- CLI downloads are restricted to the current directory by default, but users can pass `--restrict false`.
69+
- MCP downloads always normalize the paper ID, restrict writes to the server process working directory, and return an error for paths outside that directory.
70+
- HTTP MCP transport has no built-in authentication. Treat it as local-only unless it is placed behind an authenticating proxy or tunnel.
71+
72+
## Source-to-Concept Reference
73+
74+
| Concept | Primary files |
75+
| --- | --- |
76+
| CLI command model | `src/nber_cli/cli.py` |
77+
| Agent tool model | `src/nber_cli/mcp.py` |
78+
| Public Python API | `src/nber_cli/__init__.py`, `docs/en/python-api.md` |
79+
| Search and metadata parsing | `src/nber_cli/fetcher.py` |
80+
| PDF download engine | `src/nber_cli/download.py` |
81+
| RSS feed system | `src/nber_cli/feed.py` |
82+
| Info cache | `src/nber_cli/info_cache.py`, `src/nber_cli/db.py` |
83+
| Local config | `src/nber_cli/config_store.py`, `src/nber_cli/config.schema.json` |
84+
| Local database | `src/nber_cli/db.py` |

docs/en/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Most NBER-CLI runtime behavior uses built-in defaults. The local database also uses a small user config file to remember the database location selected by `nber-cli db init` or `nber-cli db migrate`.
44

5+
For the full SQLite schema, cache tables, behavior logs, and backup guidance, see [Persistence Layer](persistence.md).
6+
57
## Runtime Defaults
68

79
| Setting | Default | Description |

docs/en/development.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
This page covers local development, testing, documentation, and release preparation.
44

5+
For the detailed fixture and mocking layout, see [Test Infrastructure](testing.md). For runtime component relationships, see [System Architecture](architecture.md).
6+
57
## Repository Layout
68

79
```text

docs/en/glossary.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Glossary
2+
3+
## NBER Paper ID
4+
5+
A working paper identifier such as `w25000`. Most user-facing commands accept the ID with or without the `w` prefix, but URLs and default PDF filenames use the canonical `w` prefix.
6+
7+
## CLI
8+
9+
The `nber-cli` command implemented in `src/nber_cli/cli.py`. It is optimized for humans and scripts: text output by default, JSON output where supported, and stable non-zero exits for failures.
10+
11+
## MCP Server
12+
13+
The agent-facing server implemented in `src/nber_cli/mcp.py` with FastMCP. It exposes three tools: `get_paper_info`, `search_papers`, and `download_paper`.
14+
15+
## Info Cache
16+
17+
The `info_cache` table and `info_cache.py` helper flow. It stores paper metadata so repeated `info` and `get_paper_info` calls can avoid another NBER page fetch while the row is within the configured TTL.
18+
19+
## Sliding TTL
20+
21+
The info cache refresh model. On a cache hit, NBER-CLI updates `last_fetched_at` and increments `fetch_count`, so expiration is measured from the latest local hit rather than only from the original network fetch.
22+
23+
## Feed System
24+
25+
The RSS workflow in `feed.py`. It reads `https://www.nber.org/rss/new.xml`, parses items with `defusedxml`, stores them in `feed_items`, and returns new or all items depending on the command options.
26+
27+
## Local Database
28+
29+
The SQLite database resolved by `db.get_database_path()`. By default it lives at `~/.nber-cli/nber.db`. It stores feed cache rows, info cache rows, and behavior logs.
30+
31+
## Config File
32+
33+
The JSON file at `~/.nber-cli/config.json`. It stores the configured database path, database schema version, info cache settings, and download defaults.
34+
35+
## SQLModel
36+
37+
The typed ORM layer used by `db.py` on top of SQLAlchemy. Tables such as `FeedItem`, `InfoCache`, `QueryLog`, `DownloadLog`, and `InfoLog` are declared as SQLModel models.
38+
39+
## Behavior Logs
40+
41+
Local-only tables that record operational events:
42+
43+
- `query_log` records CLI search keywords and result counts.
44+
- `download_log` records CLI download outcomes.
45+
- `info_log` records CLI and MCP paper info lookups.
46+
47+
These logs are not sent to any project server.
48+
49+
## Soft Failure
50+
51+
A non-critical database write failure that should not break the main operation. For example, a failed `query_log` write should not prevent search results from being printed.
52+
53+
## Download Restriction
54+
55+
The filesystem safety rule that keeps download targets inside the current working directory by default. The CLI can override this with `--restrict false`; MCP downloads always enforce the restriction.
56+
57+
## `display_all`
58+
59+
A feed fetch option. When false, `feed fetch` displays only newly discovered RSS entries. When true, it displays all currently fetched RSS entries, including rows already present in the cache.
60+
61+
## `include_all`
62+
63+
An MCP `get_paper_info` option. When true, the returned dictionary includes related fields and `published_version` when NBER exposes them.
64+
65+
## `--refresh`
66+
67+
A CLI `info` flag that bypasses the local info cache for one call and fetches the paper page from NBER again. MCP does not currently expose an equivalent per-call flag.
68+
69+
## First-Week Restriction
70+
71+
NBER can return HTTP 403 for newly released papers that are not yet publicly downloadable. NBER-CLI reports this as a permission/access message rather than treating it as a missing paper.
72+
73+
## Future Schema
74+
75+
A database whose SQLite `PRAGMA user_version` is newer than the version supported by the installed NBER-CLI. The package rejects writes to this database to avoid downgrade corruption.

docs/en/index.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,14 @@ Add it to an MCP client:
6363
- [MCP Server](mcp.md): agent configuration, transports, and available tools.
6464
- [Python API](python-api.md): async functions and data models.
6565
- [Configuration](configuration.md): runtime defaults and operational behavior.
66+
- [Persistence Layer](persistence.md): local SQLite schema, cache behavior, logs, migration, and cleanup limits.
67+
- [System Architecture](architecture.md): how the CLI, MCP server, network layer, download engine, feed system, and persistence layer fit together.
68+
- [Glossary](glossary.md): project-specific terms, table names, error models, and paper ID conventions.
6669
- [Development](development.md): local setup, tests, docs, CI, and release workflow.
70+
- [Test Infrastructure](testing.md): fixtures, mocking strategy, async tests, and robustness coverage.
6771
- [Contributing](contributing.md): contribution standards and review expectations.
6872
- [Changelog](changelog.md): notable project changes.
6973

7074
## Project Status
7175

72-
The current public command model is `nber-cli` v0.3.0. The CLI is intentionally small and script-friendly: text output is optimized for human reading, and `--format json` is available where structured output matters.
76+
The current public command model is `nber-cli` v0.7.0. The CLI is intentionally small and script-friendly: text output is optimized for human reading, and `--format json` is available where structured output matters.

docs/en/mcp.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
NBER-CLI includes an MCP server so agents can search NBER, inspect paper metadata, and download PDFs without scraping command output.
44

5+
For how the MCP server shares core modules with the CLI, see [System Architecture](architecture.md).
6+
57
## Start the Server
68

79
The default transport is stdio:

docs/en/persistence.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Persistence Layer
2+
3+
NBER-CLI uses a local SQLite database plus a JSON config file. The database stores caches and operational logs; the config file stores user-selected runtime settings such as the database path, info cache behavior, and download defaults.
4+
5+
## Files
6+
7+
| File | Default path | Purpose |
8+
| --- | --- | --- |
9+
| Config | `~/.nber-cli/config.json` | Stores `schema_version`, `feed.db-path`, `info` cache settings, and `download` settings. |
10+
| Database | `~/.nber-cli/nber.db` | Stores feed cache, paper info cache, and behavior logs. |
11+
| Legacy database | `~/.nber-cli/feed.db` | Used only as a fallback when upgrading from older releases and no `nber.db` exists. |
12+
| Debug log | `~/.nber-cli/debug.log` | Rotating log file for warnings, errors, and debug output when enabled. |
13+
14+
## Database Tables
15+
16+
| Table | Primary use | Written by | Cleaned by |
17+
| --- | --- | --- | --- |
18+
| `feed_items` | Cached RSS paper entries keyed by paper ID. | `feed fetch` | `feed clean` |
19+
| `feed_fetches` | Audit trail for RSS fetch attempts and counts. | `feed fetch` | No CLI cleanup command. |
20+
| `info_cache` | Cached paper metadata used by `info` and `get_paper_info`. | `info`, MCP `get_paper_info` | `info cache clear` |
21+
| `query_log` | CLI search query history and result counts. | CLI `search` | No CLI cleanup command. |
22+
| `download_log` | CLI download successes and failures. | CLI `download` | No CLI cleanup command. |
23+
| `info_log` | Paper info lookup history. | CLI `info`, MCP `get_paper_info` | No CLI cleanup command. |
24+
25+
The schema version is stored in SQLite `PRAGMA user_version`. Version `2` is the current schema. NBER-CLI refuses to write to a database created by a newer schema version.
26+
27+
## Info Cache Behavior
28+
29+
The info cache is controlled by:
30+
31+
```json
32+
{
33+
"info": {
34+
"cache_enabled": true,
35+
"cache_ttl_days": 30
36+
}
37+
}
38+
```
39+
40+
When the cache is enabled, `info` and MCP `get_paper_info` first check `info_cache`. A fresh cache hit returns local data and then updates `last_fetched_at` plus `fetch_count`. This makes the TTL sliding: frequently used paper metadata remains fresh relative to the most recent local hit.
41+
42+
Use `--refresh` on the CLI to bypass the cache for one `info` call:
43+
44+
```bash
45+
nber-cli info w25000 --refresh
46+
```
47+
48+
The MCP tool does not expose a per-call refresh flag. To force a live MCP lookup, disable the cache temporarily or wait for the TTL to expire.
49+
50+
## Feed Cache Behavior
51+
52+
`feed fetch` stores every fetched RSS item, then returns either only newly discovered items or all fetched items:
53+
54+
```bash
55+
nber-cli feed fetch
56+
nber-cli feed fetch --display-all true
57+
nber-cli feed fetch --max-items 5
58+
```
59+
60+
`feed_items` is keyed by paper ID. Existing rows are updated with the latest title, abstract, URL, source URL, GUID, authors, and `last_seen_at`. New rows keep their original `first_seen_at`.
61+
62+
`feed_fetches` is an append-only audit table. `feed clean` does not remove it.
63+
64+
## Logs and Soft Failures
65+
66+
Search, download, and info operations try to append behavior logs. These writes are intentionally non-critical: a database error can emit a warning, but it should not prevent the main search, download, or metadata lookup from completing.
67+
68+
Cache reads also fail soft where possible. If a cache read cannot be completed safely, the command falls back to the network path or returns an empty cache count depending on the helper.
69+
70+
## Migration and Path Rules
71+
72+
Initialize or move the database:
73+
74+
```bash
75+
nber-cli db init
76+
nber-cli db init --db-path ~/data/nber.db
77+
nber-cli db init --db-path sqlite:////Users/name/data/nber.db
78+
nber-cli db migrate ~/data/nber.db
79+
```
80+
81+
On macOS and Linux, the database path must stay inside the user's home directory. This limit avoids accidental writes into system or shared locations. The destination for `db migrate` must not already exist, and sidecar files such as `-wal`, `-shm`, and `-journal` move with the database.
82+
83+
## Cleanup Coverage
84+
85+
```bash
86+
nber-cli feed clean --days 30
87+
nber-cli feed clean --all
88+
nber-cli info cache clear --days 30
89+
nber-cli info cache clear --all
90+
```
91+
92+
Both cleanup commands show a preview and require confirmation before deleting rows. `feed clean` deletes only `feed_items`. `info cache clear` deletes only `info_cache`. Logs and `feed_fetches` require manual SQLite maintenance or a fresh database.
93+
94+
## Backup
95+
96+
For a safe backup, stop any running CLI or MCP process and copy `nber.db` together with any `nber.db-wal` and `nber.db-shm` sidecar files. For live systems, use SQLite's backup command:
97+
98+
```bash
99+
sqlite3 ~/.nber-cli/nber.db ".backup '/path/to/nber-backup.db'"
100+
```

docs/en/testing.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Test Infrastructure
2+
3+
The test suite is built with Pytest and is designed around isolation. Tests avoid writing into the real home directory, avoid live NBER network calls, and cover both CLI and library behavior.
4+
5+
## Run Tests
6+
7+
```bash
8+
uv run pytest
9+
uv run pytest tests/test_cli.py
10+
uv run pytest -m "not slow"
11+
```
12+
13+
Run linting and documentation checks with:
14+
15+
```bash
16+
uv run ruff check .
17+
uv run --group docs mkdocs build --strict
18+
```
19+
20+
## Test Suite Map
21+
22+
| Area | Representative files | What it covers |
23+
| --- | --- | --- |
24+
| CLI | `tests/test_cli.py`, `tests/test_main.py` | Argument parsing, subcommand behavior, output formats, exit behavior. |
25+
| Network fetcher | `tests/test_fetcher.py` | Paper page parsing, search payload parsing, retry and request behavior. |
26+
| Downloads | `tests/test_downloader.py` | Single and batch download paths, validation, failures, concurrency behavior. |
27+
| Feed | `tests/test_feed.py` | RSS parsing, malformed XML handling, new-item detection, cleanup. |
28+
| Database | `tests/test_db.py`, `tests/test_config_store.py` | Schema creation, config persistence, migration, path normalization, cache tables. |
29+
| Info cache | `tests/test_info_cache.py`, `tests/test_info_cache_flow.py` | Cache hits, refresh behavior, TTL logic, integration with `info`. |
30+
| MCP | `tests/test_mcp.py` | Tool return shapes, error handling, paper ID normalization, download path restrictions. |
31+
| Logging | `tests/test_logging.py`, `tests/test_logs.py` | Log configuration, debug behavior, rotating file setup. |
32+
33+
## Isolation Model
34+
35+
The global fixture in `tests/conftest.py` redirects the NBER-CLI home directory behavior into a temporary path. This protects the user's real `~/.nber-cli/config.json`, database, and debug log during test runs.
36+
37+
Tests patch `Path.home()`, database paths, network functions, and HTTP sessions as needed. The goal is that a test can be run repeatedly without depending on the developer's machine state or network access.
38+
39+
```mermaid
40+
flowchart LR
41+
pytest[Pytest] --> fixture[isolated_nber_home]
42+
fixture --> temp[temporary home]
43+
temp --> config[config.json]
44+
temp --> database[nber.db]
45+
pytest --> mocks[network and session mocks]
46+
mocks --> fetcher[fetcher.py]
47+
mocks --> download[download.py]
48+
mocks --> feed[feed.py]
49+
```
50+
51+
## Mocking Strategy
52+
53+
Network-facing tests mock the lowest practical boundary:
54+
55+
- Synchronous page and feed retrieval patch `_load_text_sync` or `urllib.request.urlopen`.
56+
- Async search and download paths use fake `aiohttp` sessions or async mocks.
57+
- CLI tests patch high-level functions when the test is about argument dispatch rather than parsing NBER responses.
58+
59+
This split keeps parser tests focused on parsing, command tests focused on CLI behavior, and integration-style tests focused on component interaction.
60+
61+
## Async Patterns
62+
63+
Async functions are tested through `pytest-asyncio` or by invoking CLI paths that internally call `asyncio.run`. Download batch tests assert both successful paths and collected failures because `download_multiple_papers` returns a `DownloadBatchResult` instead of raising when individual downloads fail.
64+
65+
## Robustness Coverage
66+
67+
The tests intentionally cover edge cases that are easy to break:
68+
69+
- Paper IDs with and without the `w` prefix.
70+
- Invalid paper IDs and mismatched fetched paper IDs.
71+
- Search pagination limits and date defaults.
72+
- XML entities and malformed RSS text.
73+
- Database schema upgrades and future-schema rejection.
74+
- Download path restrictions for CLI and MCP surfaces.
75+
- Cache refresh, sliding TTL, and cleanup date ranges.
76+
77+
## Adding Tests
78+
79+
When changing user-visible behavior, add tests at the surface where the behavior is observed. For example, add CLI tests for command output and exit behavior, MCP tests for tool return shapes, and lower-level tests for parser or database helpers.
80+
81+
Keep fixtures local to a test file unless they are reused broadly. If a fixture touches home directories, database files, network calls, or environment variables, make sure it restores state automatically.

0 commit comments

Comments
 (0)