···8282- Jacquard deps are declared once in root `[workspace.dependencies]` and referenced via `workspace = true` in both the app and the generated crate, giving a single jacquard version across the workspace.
8383- ATProto has no float type: dimension fields (e.g. `bbox` x/y/z) are decimal strings parsed to floats in app code. Images use the `#image` (blobref) / `#imageView` (CDN URL) split in `space.polymodel.library.defs`. Polymodel `*View` lexicon types are appview/UI contracts: shape them around fields the UI needs, including required hydrated identity/author/count/viewer fields, instead of treating views as lossy transport objects and adding app-local adapter workarounds. Preserve typed ATProto values (`Handle`, `AtUri`, `Datetime`, URI types, etc.) once constructed; do not `.as_str()`/stringify and re-parse them through convenience constructors just to move them between fixtures, helpers, or components. Borrow typed values only at display/serialization boundaries. Keep record-authored fields on records unless a view/feed UI intentionally needs them directly; external or aggregated hydrated metadata such as display tags belongs on view types.
84848585+## Local projection: reads & writes
8686+8787+The server keeps the browser single-origin: `space.polymodel.*` reads are served from the local SQLite projection, and a fixed allowlist of other XRPC methods is proxied to the user's PDS / public AppView (PM-47; design history in [PM-9 §4.5, Confluence 131467](https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/131467)).
8888+8989+- **Writes to our own records are eagerly projected.** Every write the app performs to `space.polymodel.*` (PM-28 mediated writes *and* proxied `repo.createRecord`/`putRecord`/`deleteRecord`) is persisted to the local SQLite projection immediately after the user's PDS accepts it, for read-your-own-writes. Hydrant/firehose later re-delivers the same event; the shared projection rules keep that convergence idempotent.
9090+- **Reads hit the PDS/AppView but reflect pending local ops.** `repo.getRecord` of a `space.polymodel.*` record consults the `pending_writes`/`pending_deletes` tables while a local op awaits firehose confirmation (the request repo is canonicalized to a DID, so this holds for handle-authority reads): a `pending_writes` row not yet reflected by the PDS — a divergent cid, or the PDS reporting not-found — returns the local value/cid; a `pending_deletes` row returns `RecordNotFound` (HTTP 400) even if a lagging source still serves the record. The matching row is cleared once the Hydrant `#commit` re-delivers the event.
9191+- **Only `space.polymodel.*` is indexed.** Other records are proxied without local persistence.
9292+- **Upstream errors are forwarded faithfully.** The passthrough surface forwards the real upstream HTTP status + error body verbatim (`RecordNotFound` stays HTTP 400, `InvalidSwap` stays 409, 429/5xx unchanged); only genuine transport/internal failures become 500.
9393+- **The proxy allowlist is structural.** Non-`space.polymodel.*` methods are served only for the direct repo record operations (`createRecord`, `putRecord`, `deleteRecord`, `getRecord`, `listRecords`, `describeRepo`, `uploadBlob`) plus `identity.resolveHandle` and `actor.getProfile`, dispatched over their generated typed request structs through the OAuth session agent's signing send path. Batch/admin repo methods (`applyWrites`, `importRepo`, `listMissingBlobs`) and every other NSID have no route and 404.
9494+8595## Agent behavior
86968797- Investigate code and docs before asking factual questions.
···11+-- Read-your-writes pending-ops tracking (PM-47).
22+--
33+-- Eager local writes/deletes — both PM-28 mediated writes and the PM-47
44+-- passthrough proxy — mark a record "pending" until the Hydrant firehose
55+-- re-delivers the same event, at which point the PDS/AppView is authoritative
66+-- again. `repo.getRecord` of a `space.polymodel.*` record consults these tables
77+-- to reflect a not-yet-confirmed local op:
88+-- * a pending write whose cid differs from the PDS returns the local value;
99+-- * a pending delete returns RecordNotFound even if a lagging source still
1010+-- serves the record.
1111+--
1212+-- These tables are collection-agnostic (keyed by record URI); they do not touch
1313+-- the per-collection projection tables. The matching row is cleared by the
1414+-- firehose record-event projection path once the Hydrant `#commit` redelivers
1515+-- the event.
1616+1717+CREATE TABLE pending_writes (
1818+ -- Canonical record URI: at://{did}/{collection}/{rkey}.
1919+ uri TEXT PRIMARY KEY,
2020+ cid TEXT NOT NULL,
2121+ -- The raw record JSON, so repo.getRecord can rebuild `value` without a
2222+ -- collection-specific lookup.
2323+ value TEXT NOT NULL,
2424+ written_at INTEGER NOT NULL
2525+);
2626+2727+CREATE TABLE pending_deletes (
2828+ uri TEXT PRIMARY KEY,
2929+ deleted_at INTEGER NOT NULL
3030+);
···11+Local OpenCascade.js STEP conversion artifacts.
22+Generated by tools/renderer-step/build-and-stage-opencascade.mjs.
33+Single-threaded build: no pthread sidecar and no cross-origin isolation requirement.
44+Generated/ignored; do not commit.
···66//! [`IntoRouter`] and share [`AppState`] (SQLite pool + identity resolver + bsky
77//! client).
88//!
99-//! Not yet implemented: the SSR bridge (PM-46) and the PDS passthrough proxy
1010-//! (PM-47).
99+//! In addition to the `space.polymodel.*` endpoints below, this module mounts a
1010+//! fixed, structural allowlist of non-polymodel XRPC methods as a single-origin
1111+//! passthrough proxy (PM-47): the direct `com.atproto.repo.*` record operations
1212+//! plus `com.atproto.identity.resolveHandle` and `app.bsky.actor.getProfile`,
1313+//! dispatched over their generated typed structs to the user's PDS / public
1414+//! AppView. Every other NSID has no route and 404s, and upstream XRPC errors are
1515+//! forwarded with their real status + body.
11161217pub mod error;
1318pub mod state;
···1621mod actor;
1722mod graph;
1823mod library;
2424+mod proxy;
1925mod writes;
20262127#[cfg(test)]
···4248 },
4349};
44505151+use polymodel_api::app_bsky::actor::get_profile::GetProfileRequest as AppBskyGetProfileRequest;
5252+use polymodel_api::com_atproto::identity::resolve_handle::ResolveHandleRequest;
5353+use polymodel_api::com_atproto::repo::{
5454+ create_record::CreateRecordRequest, delete_record::DeleteRecordRequest,
5555+ describe_repo::DescribeRepoRequest, get_record::GetRecordRequest,
5656+ list_records::ListRecordsRequest, put_record::PutRecordRequest, upload_blob::UploadBlobRequest,
5757+};
5858+4559use self::state::AppState;
46604761/// Build the appview XRPC router with the given state applied.
···7488 .merge(DeleteSaveRequest::into_router(writes::delete_save))
7589 .merge(CreateTagRequest::into_router(writes::create_tag))
7690 .merge(DeleteTagRequest::into_router(writes::delete_tag))
9191+ // PM-47 passthrough proxy: a structural allowlist of non-polymodel XRPC
9292+ // methods forwarded to the user's PDS / public AppView over their
9393+ // generated typed request structs. Any other NSID has no route → 404.
9494+ .merge(GetRecordRequest::into_router(proxy::repo_get_record))
9595+ .merge(ListRecordsRequest::into_router(proxy::repo_list_records))
9696+ .merge(DescribeRepoRequest::into_router(proxy::repo_describe_repo))
9797+ .merge(CreateRecordRequest::into_router(proxy::repo_create_record))
9898+ .merge(PutRecordRequest::into_router(proxy::repo_put_record))
9999+ .merge(DeleteRecordRequest::into_router(proxy::repo_delete_record))
100100+ .merge(UploadBlobRequest::into_router(proxy::repo_upload_blob))
101101+ .merge(ResolveHandleRequest::into_router(
102102+ proxy::identity_resolve_handle,
103103+ ))
104104+ .merge(AppBskyGetProfileRequest::into_router(
105105+ proxy::actor_get_profile,
106106+ ))
77107 // getSession takes no parameters, so it bypasses ExtractXrpc (which
78108 // decodes the query string; a unit-struct request from an empty query is
79109 // rejected by serde_html_form). Identity comes from the session.