Skip to content

feat(documents): shared documents commands and a contract kit - #2966

Open
leemour wants to merge 3 commits into
mainfrom
feat/documents-commands
Open

leemour wants to merge 3 commits into
mainfrom
feat/documents-commands

Conversation

@leemour

@leemour leemour commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Task

Lane L2 of notion-cli (C4): one shared documents command group in cli-core, so every documents or wiki CLI (drive, notion, …) gets the same commands over the existing documents services.

Why

drive-cli built folders, sync, status, documents, search, catalog, tags, relationships, notes, memories, facts, identities and mirrors on its own. notion-cli needs the same commands. Copying them would make two CLIs that drift apart. A CLI now passes its ports, its provider name and its word for the remote side, and gets the whole tree.

What

  • @wirecat/cli-core/documents/cli:
    • addDocumentCommands(program, { words, output, folders, sync, documents, search, catalog, knowledge, mirrors? }) mounts the group (src/cli/documents/index.ts:69).
    • Each part can also be mounted alone: addDocumentFolderCommands, addDocumentSyncCommands, addLocalDocumentCommands, addDocumentSearchCommand, addDocumentCatalogCommands, addDocumentKnowledgeCommands, addDocumentMirrorCommands. A CLI can add its own subcommands to documents and its own groups.
    • DocumentWords (src/cli/documents/words.ts:8) holds the app, the provider, the place, the remote word for --keep local|<remote>|both and the <remote>-id of identities, plus optional text overrides.
  • Neutral entities and fields, the same in every CLI: document, selected_folder, mirror, document_tag, note, document_relationship, document_identity (remote_id, provider, role of owner or last-editor, person). The family audit's same-entity-schema sees one schema.
  • Every builder is honoursDryRun(sharedCommand(…)), as addCalendarCommands and addCrmCommands are.
  • catalog tree reads only what it shows (src/cli/documents/catalog.ts, catalogTree and catalogSource):
    • each shown entry's children come from the service's catalog.list({ parent }), 1 000 a page;
    • the branches cut at --depth are checked 200 parents at a time (one parent:a OR parent:b … query), not one by one;
    • any entry with children can be a root or a branch, so a page with sub-pages (feat(documents): a synced node can be a folder and a document at once #2941, kind file) works;
    • an entry in two folders is shown in full once and marked repeated at its other place;
    • over 10 000 entries it answers catalog.tree-limit: "name a smaller FOLDER or lower --depth".
    • Cost, measured on the kit's reference CLI: a 1 000-file folder at --depth 1 in a 12 000-entry account takes 6.6 s. Asking about each cut branch alone took 32 s. The store's parent filter scans the account's catalog (no index), so each read costs the whole account. See follow-ups.
  • Reasons the shared commands raise are cli-core's: catalog.unavailable|not-found|depth-invalid|cycle|tree-limit, mirror.none|not-found|not-folder, export.exists, listing.too-large. failureContext accepts an app's copy with the same code (drive@main still lists them), and refuses one with another code. No CLI in the family lists one under another code. sync.partial names no provider.
  • mirrors watch restarts the change feed on listing.too-large from the connection's changes. That is now documented on DocumentMirrorConnection.changes and in DOCUMENTS.md for notion. The mirrors sync description is words.text.mirrorsSync.
  • @wirecat/cli-core/documents/testing:
    • documentSourceContract: isDocument for pages that are folders and documents at once, change to put a real change in the feed, and bytes is called.
    • documentCommandsContract: parses answers and checks ids, checks each shared path's effects against a table, and checks that the CLI overrides no shared contract.
    • The kit's own test runs both over a reference CLI on a temporary store. It also runs a broken CLI (an override, a wrong effect) that the contract must refuse, and a page-tree source. mirrors sync --keep <remote> now reaches the engine with keep: "remote".

Placement. The module lives in src/cli/documents/, not src/documents/cli/. Biome forbids src/documents/** (the domain layer) from importing commands, the store or services, and these commands need all three.

Not spread into shared code (drive breaks the handbook here, see the drive PR): its own profile lock, its own policy and --read-only, and its own MCP server.

Testing

  • pnpm lint, typecheck, docs:check: ok. Tests of src/errors, src/cli/documents, src/documents, src/services/documents, the store catalog and the dry-run guard: 835 passed. The full suite runs in CI.
  • Each review fix has a test that fails on the code before it: the tree by parent over an account of 12 000 entries (reads 3 entries for a 3-file folder), a page as root, repeats, catalog.tree-limit through the CLI, a cut branch with an odd id, the page-tree source, the change feed, a broken CLI refused by the contract, and --keep box reaching the engine as remote.
  • drive-cli on a local pack of this branch (draft PR: refactor(documents): use cli-core's shared documents commands drive-cli#374): the commands --json diff against drive@main changes only the entity names and the identity fields. contracts:check (help.snap), test:matrix (0 missing), lint, lint:commands and docs:check pass. In the family cli-dev audit, drive's rows and the 24 blocking findings are identical to the baseline before this change.

Follow-ups (lane L1)

Rebased on #2938, #2941 and #2944.

  • An index on the catalog's parents. catalog.list({ parent }) and parent: queries scan every entry with json_each (src/store/sqlite/catalog.ts). With an index, a tree of 1 000 entries in a big account would take milliseconds, not seconds.
  • The documents service's catalog.get and catalog.list({ parent }) take a bare id and add .md, but the entries they return carry <id>.md. So every caller strips .md before asking again. The service could accept both forms.
  • identities needs a provider hook for people (people, linkPerson). drive passes its Google owners. The services could offer it.

🤖 Generated with Claude Code

leemour and others added 2 commits November 14, 2026 13:20
Every documents and wiki CLI gets the same commands, options and answers from
cli-core instead of copying drive-cli's. `@wirecat/cli-core/documents/cli` has
one builder per group, over ports the CLI supplies (its store, lock, principal,
provider source and item shapes); `addDocumentCommands` mounts them all:

- folders add|list|remove (the CLI adds its own `browse`, outside the contract)
- sync, status
- documents list|show|export, then the CLI's live subcommands on the group
- search, catalog import|show|list|search|tree|tags
- documents context, tags, relationships, notes, memories, facts, identities
- mirrors add|list|remove|status|sync|watch

`words` names the CLI and its provider: `--keep local|<remote>|both` maps the
CLI's word to the engine's `remote`, identities are `<remote>-id`/`<remote>_id`,
and a partial sync's issue names the CLI's store provider rather than a
hard-coded google-drive. Entity names are neutral and the same everywhere:
document (views imported, catalog), document_tag, document_relationship,
document_identity, selected_folder, mirror.

`catalog tree` now reads each folder's children by parent, page by page, with
no total cap; it used to read the first 10 000 entries of the account.

`@wirecat/cli-core/documents/testing`: documentSourceContract for an adapter,
documentCommandsContract for a CLI's mounted commands, fakeDocumentSource.
The kit's test runs both over a reference CLI on a temporary store.

The module lives in src/cli/documents/: Biome forbids src/documents/ (the
domain) from importing commands, the store or services.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
catalog tree reads only what it shows: each shown entry's children come
from catalog.list({ parent }), 1 000 a page, and the branches cut at
--depth are checked 200 parents at a time. Any entry with children can be
a root or a branch, so a page with sub-pages works. An entry in two
folders is shown once in full and marked repeated elsewhere. Over 10 000
entries it answers catalog.tree-limit, which says to narrow the folder or
the depth.

document_identity has one schema in every CLI: remote_id, provider, a
fixed role list and person. The mirrors sync description is a word of the
CLI. listing.too-large, which mirrors watch recovers from, and
catalog.tree-limit are core reasons; sync.partial names no provider. Every
builder honours a global --dry-run.

The contract kit checks more: the source contract takes isDocument and a
change in the feed and calls bytes; the commands contract parses answers,
checks ids, each path's effects and that no shared contract is overridden.
The kit's own test runs a broken CLI, a page-tree source and mirrors sync
through to the engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@leemour
leemour force-pushed the feat/documents-commands branch from 2d777bd to 99259fd Compare November 14, 2026 12:24
Since #2949 a command's own --yes takes the value the root took, and the
root gate guard refuses a read of --yes off the root by a command that
declares its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant