personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

Python to Rust Porting Doctrine#

This document is for engineers and coding agents porting solstone behavior from Python into the Rust workspace under core/. It records the wave-0 rules before any behavior moves.

Workspace Scope#

The Rust workspace lives at core/. It contains a thin solstone-core bin, the solstone-core-cli adapter library, and subsystem crates such as solstone-core-journal as Python behavior is ported.

Rust crates use edition 2024, rust-version = "1.95", and license = "AGPL-3.0-only" inherited from core/Cargo.toml. Every .rs file starts with the two-line // SPDX header used by AGENTS.md.

Mobile Readiness#

Rust subsystem logic should stay eligible for the iOS canary unless a host-only adapter makes that impossible. The native markdown indexer keeps discovery, metadata, segment parsing, stream-marker reads, and markdown chunking in solstone-core-indexer, which remains covered by check-rust-ios. solstone-core-indexer-store is excluded because its bundled-C SQLite build cannot cross-compile from the Linux host. That exclusion is for the storage adapter, not for the indexer logic. The eventual iOS path is to link the system libsqlite3 that iOS ships instead of bundling SQLite, then return the store crate to the iOS gate.

Native Dependency Release Proof#

A Rust conversion that adds or bumps a dependency with C/C++ build steps or native linkage is not complete after source checks alone. Before the conversion wave closes, prove the supported release targets still build and pass artifact validation: Linux x86_64 musl, Linux aarch64 musl, and macOS arm64. Keep required toolchain, target, and linker behavior in checked-in repository release paths, not in a local shell profile. If a dependency cannot satisfy a supported target, document the blocker and stop the conversion before merging it.

Evidence Repository command Class Notes
Rust formatting make check-rust-fmt GNU-host check Host source-format evidence only.
Rust MSRV make check-rust-msrv GNU-host check Verifies the pinned MSRV rail without changing rust-version.
Rust lint make check-rust-clippy GNU-host check Runs the existing clippy -D warnings gate.
Rust tests make check-rust-test GNU-host check Runs workspace Rust tests on the GNU host.
Rust dependency policy make check-rust-deny GNU-host check Locked, offline bans/licenses/sources policy over the supported cargo-deny graph.
Rust advisories make audit GNU-host check Verifies a signed advisory mirror packet, materializes its bundle locally, then performs a locked offline advisory check without refreshing or mutating the operator inputs.
iOS canary make check-rust-ios iOS cross-target canary Cross-target drift evidence for eligible library crates; explicitly excludes solstone-core-indexer-store because the native SQLite store is not yet in the iOS gate.
Core sdist compile inputs make check-core-sdist-compile-inputs Packaging-source check Verifies shipping Rust compile-time inputs are discovered and covered by the normalized solstone-core sdist injection set.
Release candidate rail scripts/release.sh --candidate / scripts/release.sh --recover <version> <source-commit> Local readiness evidence DESTRUCTIVE: --candidate is fresh construction; before policy or build work it deletes prior raw build/dist outputs and that version's stale payload/evidence. It binds candidate payload, ledger, and per-target install/smoke proofs, then reports canonical local readiness JSON. --recover is retained-byte-only, read-only validation; it preserves retained payload, ledger, and proofs and never rebuilds or refreshes. Proofs cover local candidate bytes and native smoke only; publication is temporarily locked out of this rail.

Signed Advisory Mirror Audit#

make audit requires four operator-provided inputs: AUDIT_ADVISORY_BUNDLE for the local advisory bundle, AUDIT_ADVISORY_RECEIPT for the freshness receipt, AUDIT_ADVISORY_PUBKEY for the approved minisign public key, and AUDIT_ADVISORY_LOCATOR for the private mirror locator. The signature selector is derived from the receipt path as <receipt>.minisig; there is no separate signature option.

The audit is local-only. Git verifies and clones only the local bundle file, and the locator is used only as cargo-deny's advisory database identity in the offline check. It is never used as a clone source, fetched, pulled, or probed. Use a placeholder such as PRIVATE_MIRROR_LOCATOR in notes and logs; do not record a real private host, path, credential, or URL-derived token.

The public trust pins are key ID 5FCC81CD3DE12315 and public-key SHA-256 c9fb713fe57791afbdebddde7b334e950ce1efcc167d49daf4cc1cbd930bb122. The receipt must be canonical JSON, its adjacent minisign signature must carry the trusted comment for the same advisory commit and UTC time, and the receipt UTC is the only freshness authority.

On success, stdout is exactly one compact JSON object with these fields: product, advisory_cohort, synced_commit, receipt_utc, max_age, checked_at, cargo_lock_sha256, cargo_deny_version, and verdict. The witness contains no paths, locators, credentials, or child process output.

The audit is non-destructive: packet inputs, the source tree, ambient Cargo state, and release candidate/evidence directories are not modified. The bundle-cloned advisory database and cargo-deny config are owned temporary materialization and are removed before the success witness is emitted. If any gate fails, reacquire the signed packet from the controlled mirror process, place the adjacent signature next to the receipt, verify the public key pin, and rerun make audit.

Owner Timezone#

The Python owner-timezone resolution is effectively identity.timezone from config/journal.json, then UTC. The apparent host-local branches in get_owner_timezone() are dead because CPython astimezone() returns a fixed-offset datetime.timezone without a .key. Reproducing host-local time in Rust would diverge from Python behavior.

Layering#

solstone-core is a process shell only: it reads std::env::args(), writes stdout or stderr, and returns process exit codes.

solstone-core-cli is the CLI adapter. It takes an argv slice as input and returns a typed outcome. It never reads std::env, never prints, and never exits.

Subsystem libraries added in later waves take config and paths as parameters, own no process-global state, and do not parse argv. The "no argv parsing in core logic" rule binds these subsystem libraries.

Error And Type Mapping#

Python exceptions become Result errors at the Rust boundary. A port should name the error cases it can emit; it should not collapse expected failures into strings or panics.

Python None becomes Option. Truthiness becomes explicit predicates or comparisons. A port must not rely on implicit emptiness checks when the Python source distinguished empty, missing, and false values.

Python context managers and __del__ cleanup become RAII ownership and Drop where cleanup is unconditional. Fallible cleanup remains explicit because Drop cannot return an error.

Monkeypatching, dynamic dispatch, decorators, middleware, and import-time side effects become explicit seams. Before porting code with any of these concerns, inventory the concern and add a conformance test that fails when the concern is absent; absence is otherwise invisible in a diff.

Data Boundaries#

Python integers are arbitrary precision. Rust ports use i64 for JSON-facing integers unless a specific writer documents another width. Overflow is a Result error at the boundary, never a silent wrap or debug-only assertion. JSON integers outside i64 are rejected at parse.

Python str maps to UTF-8 String or &str. Python bytes maps to Vec<u8>. Filesystem paths map to PathBuf or OsStr; POSIX paths are not guaranteed to be UTF-8, so ports must not use .to_str().unwrap().

Porting Instruments#

scripts/build_core_fixtures.py generates Rust-facing fixtures under core/fixtures/.

core/fixtures/markdown_chunks.json pins Python markdown chunking/token output for the Rust markdown indexer port.

core/fixtures/speaker_filterbank.json pins the production speaker-filterbank stage: both Python call sites must be bit-identical, feature rows are compared with FILTERBANK_VALUE_ABS_TOLERANCE, and platform provenance is diagnostic only so cross-architecture checks pass iff the fbank values agree within tolerance.

core/fixtures/speaker_stage_boundaries.json pins speaker-pipeline branch boundaries for interval selection, speaker-evidence gating, sentence assignment, and k-selection. Silhouette scores are compared with CLUSTER_SCORE_ABS_TOLERANCE; selected k values and cluster labels remain exact.

tests/verify_indexer_differential.py runs the indexer differential harness and writes its report under the harness work directory unless --report is supplied. tests/verify_speaker_differential.py runs the local speaker-pipeline differential harness and writes/compares versioned .npz result bundles for Python-to-port parity checks. tests/verify_speaker_verdict.py consumes those recorded bundles without rerunning speaker models, adding decision-flip replay for clustering, owner-claim, and acoustic-tier outcomes plus DER scoring against caller-supplied reference turns.

JSON And Hashing#

Canonical JSON is a per-writer contract, not a repository default. A Rust port inherits the ordering and separators of the specific writer it replaces. Examples with explicit sorted output today include solstone/think/talent_provenance.py, solstone/think/data_state.py, solstone/think/readiness.py, and solstone/think/steward.py.

solstone/think/talent_provenance.py computes identity hashes from the exact string returned by _canonical_json. Byte drift changes the SHA-256 identity. Two traps matter:

  • Float exponent spelling: Python's repr-backed JSON formatting emits 1e+30. Rust's standard JSON float formatting emits 1e30. Same value, different bytes, different SHA-256.
  • Non-finite values: Python emits bare NaN and Infinity tokens, which are not valid JSON. Rust's standard JSON serializers refuse to emit them, so a payload Python hashes today cannot round-trip through a conforming Rust writer.

Hashed canonical payloads therefore carry no floats and no non-finite values. If a future port must hash a float, it owes a byte-exact Python repr emitter plus a conformance test.

There is a pre-existing Python hazard: _canonical_json does not reject non-finite values. A non-finite value can enter a hashed identity today. This lode documents that hazard but does not change Python behavior.

Unsupported Inputs#

Native ports reserve process exit code 69 for inputs the native command cannot process. Wrappers should surface that code unless a command-specific design says otherwise. It is distinct from success, usage errors (64), empty-input codes, and temporary failures (75). Signal death is normalized to temporary failure (75). The supervisor intentionally keeps mapping non-zero scheduled-task exits to error; command stderr carries the operator-facing detail.

Indexer Native Write Routing#

journal indexer routes command writes (--reset, --rebuild-edges, --rescan, --rescan-full, and --rescan-file) to the sibling solstone-core indexer binary. Query-only invocations remain in Python. Mixed write+query invocations run native writes first; on native success they enter the Python query path, and on native non-zero they return that code without querying. The command no longer reads journal config for write routing, so stale old routing keys in config/journal.json are inert.

Before launching the native helper, the wrapper checks that the current runtime has a compatible solstone-core wheel: the normalized host tuple must be in probe.SOLSTONE_CORE_COVERED_PLATFORMS, and the platform tags advertised by packaging.tags.sys_tags() must intersect the tag set recorded in probe.SOLSTONE_CORE_PLATFORM_TAGS. Linux x86_64 and Linux aarch64 require the manylinux 2.17 / manylinux2014 glibc floor. macOS requires macosx_14_0_arm64; older arm64 macOS hosts therefore report no compatible wheel. A covered source checkout without solstone-core distribution metadata also returns 78 for every write-bearing command until the developer runs make install.

Backup-restore full rescans, direct index_file() callers, chat stream appends, importers, day-accumulator writes, and index-mutating deletes bypass journal indexer and continue to use the named Python in-process writers.

The wrapper normalizes --rescan-file to an absolute path with the same Python journal-path resolver used by index_file() before passing it to native. This keeps chronicle/-prefixed relative paths from being interpreted differently by the Rust relative-path resolver.

Native indexer compound writes are atomic at the logical replacement-unit boundary. A content file replacement deletes old chunks, inserts new chunks, writes its files mtime, and co-commits its segment aggregate rebuild. An edge file replacement deletes old edge rows and edge_files state, extracts and inserts replacement rows, and writes the edge_files mtime as one unit. Entity search deletes stale entity-search chunks, inserts replacement chunks, and writes both watermarks as one unit. Reset is SQLite-native: it drops and recreates index objects transactionally and does not unlink the database, WAL, or SHM files.

Command writes now use the native path only. Journals containing edge source files whose extraction fails preserve prior native edge_files rows and mtime so the unchanged file retries on the next scan. The remaining Python in-process bypass consumers keep their existing Python semantics because they do not enter journal indexer.

The detailed native atomicity design is in docs/design/indexer-native-atomicity.md.

Native sol client design records: docs/design/native-sol-client/00-prep-findings.md, docs/design/native-sol-client/01-oracle-repro.md, docs/design/native-sol-client/02-design.md, docs/design/native-sol-client/03-batch-prep.md, docs/design/native-sol-client/04-batch-design.md, docs/design/native-sol-client/05-raw-body-parity.md, docs/design/native-sol-client/06-cutover-design.md, and docs/design/native-sol-client/07-notify-contract-design.md.

Dual Paths And Shims#

The repository no-shims rule still stands. During an active port, a temporary old/new route is a deliberate, time-boxed, per-change exception. Each dual path needs a named deletion schedule. Do not add compatibility aliases, deprecated-parameter handling, or compatibility re-exports.

The native sol cutover has one sanctioned temporary delegation boundary: the finite private compatibility inventory in solstone/think/sol_compat_inventory.py, checked by scripts/check_native_sol_compat.py. The inventory is the only authority for that command set; do not copy the list into docs or gates. The removal criterion is zero Python delegation from supported-platform native sol: every remaining compatibility path has either a native authority with a production aggregate handler or an explicit direct native match-arm home for top-level local behavior, then the compatibility inventory and module exec bridge are deleted together.

Version Lockstep#

scripts/render_packaging.py keeps Python leaf packages and Cargo metadata in lockstep with the root pyproject.toml version. The current lockstep assumes X.Y.Z. A Python pre-release such as 0.9.0rc1 is not a valid Cargo version; before tagging one, add and test an explicit translation rule.

Journal Resolution Decisions#

The first behavior port is get_journal_info() / get_journal() from solstone/think/utils.py, backed by solstone/think/user_config.py.

  1. MSRV is 1.95 for the locked native dependency set. Rust 1.87 is enough for the safe home path, but the current bundled SQLite dependency line requires Rust 1.95. The journal resolver uses the hybrid shape: literal HOME when present, and std::env::home_dir() only when HOME is absent. This avoids a hand-rolled unsafe getpwuid_r implementation.
  2. No unsafe passwd FFI. Keeping the old 1.85 floor would require libc backup code with buffer sizing and retry behavior for a home-directory lookup. That defect surface is not justified for this port.
  3. Home normalization follows str(Path.home() / "journal"), not just os.path.expanduser("~"). The port reproduces the observed layers needed by user_config.default_journal(): present-but-empty HOME becomes /, trailing slashes are stripped with an or-root default, repeated separators and . components are collapsed lexically, exactly two leading slashes are preserved, .. is not collapsed, and . joined with journal renders as journal. If the expanded home still starts with ~, the port raises the same home-unavailable error as Python's pathlib guard. This is not a general pathlib port.
  4. Config stripping is Python stripping. Rust str::trim() is not equivalent to Python str.strip() because Python also strips U+001C..U+001F. Journal config values use a small Python-compatible strip helper. Environment values are never stripped.
  5. TOML parsing uses toml_edit 0.22 parse-only. The latest TOML crates track TOML 1.1 behavior such as accepting \e, which Python tomllib rejects. toml_edit = 0.22.27 with only the parse feature matches the tomllib cases this port needs and keeps the lock cost smaller than the toml facade.
  6. Unit vector tests do not mutate process env. The shared JSON vectors carry raw HOME / SOLSTONE_JOURNAL inputs, config bytes, checkout-root state, and observed Python outcomes. Rust unit tests replay those cases by passing values directly to library functions. Subprocess binary tests may use Command::env and env_remove.
  7. The binary wires no source-checkout root. A native binary in a venv has no meaningful Python checkout root, so solstone-core journal-path deliberately resolves only CLI override, env, config, and default. The library still keeps the four Python resolver sources: env, config, source, and default.
  8. The binary label vocabulary is a superset. journal-path --journal PATH is a binary-surface override with no Python equivalent in get_journal_info(). It short-circuits the library resolver and prints label cli; the library Source enum does not add a fifth variant.
  9. Non-UTF-8 env paths stay as paths. The Rust API accepts OsStr / PathBuf and has Rust-only Unix tests for non-UTF-8 env paths. The shared JSON vector file is UTF-8 and does not encode arbitrary env bytes.
  10. Create errors are structural and shape-equivalent. Directory creation errors carry source label, path, and io::Error fields. Their display shape mirrors Python's could not create journal directory ({source}): {path}: ..., but the OS-error text is not byte-equivalent to Python's OSError. Nothing consumes that message programmatically.
  11. No improvements to path meaning. The port does no tilde expansion, canonicalization, resolving, absolutization, caching, new env vars, or config-gated dual path. ~/journal from config remains a literal relative path.