# legible-py — Python reference library

This library implements text ingestion, draft preparation, human-operated signing
helpers and state summaries over the Rust engine (`../legible-core`). It can use
a deterministic stub or an explicitly selected model provider. The import package
is `legible`; unsigned drafts can remain the final selected outcome.

## Components and boundaries
- **inferred ≠ signed.** Model output is a **provisional candidate**. Formalization prepares a new declaration from the person's selected formulation for exact-content review and human-operated signing through OpenSSH, followed by sealing and verification through the engine. A Genealogy working draft remains a separate document. A Nod is never inferred — it is a deliberate act.
- **Engine interface.** Record build/seal/verify delegates to the `legible` CLI
  executable. DID helpers and the independent canonicalizer have their own
  implementations and tests. The application also calls `check` and `id`.
- **Python can invoke signing.** `engine.sign` runs `ssh-keygen -Y sign -n legible` with the private-key path. The model should not receive key material, but processes on the host may have key access; the helper supplies no proof of exact-content human approval. Use these commands as human-operated helpers after review, not agent authorization to sign. Tests use throwaway keys; plain Ed25519 is supported, not `ed25519-sk`.
- **Interchangeable LLM.** `StubProvider` (offline, deterministic — tests and key-free use) and `ClaudeProvider` (API). The provider interface is swappable; availability, privacy, costs and output quality must be evaluated for each actual provider.
- **Zero external dependencies** (stdlib only).

## Quick start
```bash
export LEGIBLE_BIN=../legible-core/target/debug/legible   # or rely on the default
python3 -m legible.cli --repo /path/to/node demo          # runs the core cycle with the StubProvider
```

## Tests
```bash
python3 tests/test_core_path.py
```
The tests run **offline** (StubProvider) and generate a throwaway key to exercise sign/seal/verify end-to-end.

## Scope
- **Slice 1:** text ingestion, LLM draft (provisional), promote+sign an Appoint, emit a deliberate Nod, synthesis.
- **Slice 2:** the socratic human/AI pairing (a private CH-class profile). The node's lens (CH) is natural-language prose, private and never signed (spec §5) — it lives with the agent, not in this package's API.
- **Slice 3:** the pre-Trace (private CH-class radar), deliberate Trace, forgetting (redaction + removing the legible content) and local publish; the local web UI lives in `../../adapters/legible-app/`.

CLI commands: `pair · ingest · draft · show · sign-appoint · nod · synth · discover · reputation · demo`. Run `python3 -m legible.cli demo` to see it all at once. (The web UI moved to `../../adapters/legible-app/app.py` — a downstream adapter, not the interface; the canonical interface is the `legible` CLI + the MCP server in `../legible-mcp`.)

## Discovery status

`discover` and `reputation` return explicit source states:

| State | Meaning |
|---|---|
| `none` | No external source is configured |
| `ok` | All configured sources succeeded, including replies with no matches |
| `partial` | At least one source succeeded and at least one failed |
| `unreachable` | All configured sources failed |

`reputation` returns counts and source signals, not a demonstrated model reading
through the participant's CH. Live interoperability with an external indexer
has not been verified. The scoped MCP profile disables external discovery calls.

### External indexer interface

The reference code calls its external search interface ARD. The configured URLs
are stored in `ard_indexers` in `.ch/config.json`. The current adapter sends
`POST /search` with `{"query":{"text":"…"}}` and reads the documented fields
in [its implementation](legible/discovery.py). This describes the implemented
request shape, not verified compatibility with an identified external standard
or deployed service. An actual indexer needs its own integration test.

**Full tutorial (all functions + how to run): `TUTORIAL.md`.**

Tests (offline): `tests/test_core_path.py`, `test_fatia2.py`, `test_fatia3.py` (+ the web UI test in `../../adapters/legible-app/`).

The draft exchange commands are `python3 -m legible.drafts`; they preserve exact
Genealogy 0.1 files and origin metadata, without inference or a hosted service.
`legible.contacts` delegates optional contact verification and import to the
Rust CLI. Export requires `selected=True` and copies only the current active
signed profile. It does not sign, publish or verify a person's civil identity.
The shared Rust `parse-draft` reader tolerates missing quotes around descriptive
single-line text containing `: ` and accepts BOM/CRLF. `inspect_details` returns
the original bytes, checked metadata, exact-byte hash and versioned reading
diagnostics. Import/export keeps the original file; transport `reading` reports
recoveries, which the receiver reproduces. It does not supply missing identity,
dates or relationships, certify original YAML, or relax signed-record parsing.
Rebuild Rust with `cargo build --offline` and restart an existing MCP process
after updating its Python modules; a static guide does not execute this reader.
`python3 -m legible.doctor desktop-mcp` diagnoses local dependencies only.
`ClaudeProvider` requires an explicitly selected available model and caller
authorization for the actual inputs. The CLI requires `--allow-external-processing`;
visibility, an API key or environment configuration is not permission. The local
reference web UI remains offline-only until it has a provider/inputs consent flow.

## Draft retrieval and diagnostics

`drafts.search_report(repo, query)` returns `drafts`, `complete`, `issues`,
`checked_entries`, an attribution notice and a local-scope notice. Corrupt,
missing or unreadable entries are reported individually while healthy results
remain readable. The operation never deletes, repairs or silently discards files.
`drafts.search` preserves the strict list interface and raises
`DraftDiagnosticError` on incomplete retrieval; its `.report` contains the details.
`get` and selected `export` resolve one exact origin/handle/hash directly rather
than scanning unrelated entries. They still reject corruption in that target.

The configured Rust reader checks cached metadata and recovery notices against
the preserved Markdown. `metadata-mismatch` and `reading-mismatch` exclude the
affected entry from partial results and prevent its read/export; no cache file
is repaired automatically. `draft-reading-failed` makes unavailable interpretation
explicit. A bounded process-local cache reuses Rust readings for small, identical
inputs and is invalidated when the parser executable changes. It never trusts
metadata merely because the Markdown hash matches. Hashing still does not
authenticate the declared author.

CLI `python3 -m legible.drafts search --repo /PRIVATE/ROOT --report` returns
that JSON report (exit 0 when complete, 2 when partial). Without `--report`,
search retains its strict-list contract. Failed operations return exit 1 with
JSON diagnostics. A path/hash diagnostic contains no raw corrupt content.

`DraftDiagnosticError.diagnostic` distinguishes a stored integrity error,
packet mismatch and `draft-conflict`. With a known original, `change` can be
`line-endings-only`, `bytes-changed` or `identical-bytes`; without one, packet
comparison is `not-determined`. Classification does not normalize data, replace
the original or decide whether a reformulation is cognitively substantial.
Resend the exported original with its manifest. Review separate revisions;
do not update a hash merely to bypass a conflict.

Local draft ingestion installs complete private files in the local store without
overwriting an existing entry, including when independent processes share that
store. A staged file is flushed before an atomic, no-replace hard link makes it
available to local readers. For the same origin and handle, the first complete
entry wins: identical bytes are a duplicate; different bytes produce
`draft-conflict`. Shared content blobs use the same installation rule. Readers
never use temporary staging files.

This requires a filesystem supporting atomic hard-link creation in the store's
directory. Unsupported operations fail explicitly; there is no overwrite
fallback. Tests exercise the local development filesystem, not Windows or
network-share guarantees. This is not a defense against a process with write
access deliberately modifying files already installed in the store, nor a guarantee against storage
hardware failure or power loss. Integrity diagnostics remain necessary. A
conflicting or interrupted attempt may leave a complete unreferenced blob;
forcibly terminating a writer may leave its private `.draft-*.tmp` staging file.
Neither is a searchable contribution or a lock blocking retry. No files from
other operations are deleted automatically.

Transport fields and the draft convention are unchanged. Origin/author remain
unsigned declarations, not authenticated by hashing. Export is not automatic
redaction/DLP or proof of human selection. Restore a damaged file only from a
trusted exact copy after preserving the affected local files.

```sh
python3 tests/test_draft_diagnostics.py
python3 tests/test_draft_concurrency.py
```
