feat: add trix use and dependency-aware commands#109
Merged
Conversation
Make consuming an existing published protocol a first-class trix workflow. `trix use <scope>/<name>:<version>` pulls the OCI artifact from the configured registry, caches it under `.tx3/protocols/...`, and records a pinned entry in `trix.toml`. Subsequent `check`/`build`/`codegen`/`inspect`/`invoke` invocations treat the dependency as a sibling protocol — each compiled in isolation, never stitched into the project's own source. New canonical reference grammar in `src/refs.rs` (`ProtocolRef`, `TxRef`) drives every place a protocol or tx is named: CLI flags, `trix.toml` `[dependencies.<alias>].ref`, error messages. Shared OCI helpers move out of `publish.rs` into `src/oci.rs` and are reused by the new pull path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A cloned/freshly-initialized project with [dependencies] but no [registry] previously failed on the first scoped command. Introduce DEFAULT_REGISTRY_URL + RootConfig::registry_url() in convention.rs as the single source of truth, and route use/restore/publish call-sites through it. The registry is still only contacted on a cache miss, so consuming an already-cached dependency now works with zero config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cache layout is `.tx3/protocols/<scope>/<name>/<version>/`. The `pin_version` degraded path used to invent a `sha256-<short>` pseudo-version, leaking an opaque string into both trix.toml and the cache layout and creating an inconsistent dual naming scheme. The cache is project-local with one entry per (scope,name), and content identity is already guarded by the digest check in verify_cached, so a readable concrete `<version>` is the right key. Make a missing concrete version a hard error pointing the user at the publisher instead of papering over it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`invoke --tx widget::transfer` only routed the TII; the tx portion was discarded because the wallet's interactive picker always chooses the transaction. Replace `--tx <TxRef>` with `--from <ProtocolRef>` so the flag does exactly what it can: select the protocol (project default, dependency alias, or full registry ref). The tx is chosen interactively, as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- use_cmd: fix stale pin_version comment (described deleted fallback) - use_cmd: drop dead scope_name/scope_and_name plumbing - dependencies: remove unused pub load_tii_json - oci::pull: move layer bytes via mem::take instead of cloning Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- unify identifier validation: convention.rs reuses refs::validate_ident (now pub), drop the duplicate is_valid_alias - verify_cached returns a typed CacheStatus; restore_all no longer double-parses metadata.json or re-stats files on the cache-bad path - drop builder::validate_dependencies_tii — restore_all→verify_cached already parses & validates every dep TII (was a 2nd read+parse per dep per build) - centralize cache filenames as CACHE_*_FILE consts - flatten pin_version and Resolver::resolve_protocol nesting Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #109 made the default/legacy codegen dependency-aware and left the unstable one untouched — backwards. Correct it: - Revert src/commands/codegen_legacy.rs to main verbatim (backward compatibility; byte-identical, `git diff main` empty). - Make the unstable src/commands/codegen.rs dependency-aware: it delegates to `tx3c codegen --tii`, so it points at each dependency's cached pre-built published TII (no recompilation), only building the project's own TII via builder::build_tii. - Unified output layout: always <output_dir>/<name>/ per protocol, with or without deps — removes the "layout shifts when you add your first dependency" cliff. Deliberate one-time break confined to the unstable path; default/legacy layout unchanged. - codegen_targets pure helper (unit-tested) is the single source of ordering; collect_codegen_targets resolves TII paths from it. - New #[cfg(feature = "unstable")] e2e tests/e2e/codegen_deps.rs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Explains the consume workflow: reference grammar, `trix use`, the trix.toml schema, the project-local cache + restore/digest semantics, per-command behavior, and the unstable-only dependency-aware codegen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `use` command layer should map CLI → business logic → view, nothing more. Extract the whole operation into `dependencies::add(config, AddRequest) -> AddOutcome`: registry resolution, default-to-latest, OCI pull, version pinning, alias defaulting, conflict/validation, cache write, and trix.toml persistence now live in `dependencies`. `use_cmd::run` collapses to arg-mapping + render (~20 lines). Also from the /simplify review: - share OCI pull via a private `pull_ref` (was duplicated in `fetch`) - `write_cache` is now private (only add/fetch use it, both in-module) - `pin_version`/`discover_transactions` moved out of the command layer - `pin_reference` replaces the command-layer `unreachable!()` with a defensive error (add is now a library entry point) - construct the entry once instead of insert-then-get-unwrap-clone - collapse the nested if in pin_version (let-chain) Skipped (noted): whole-config clone for trial validation reuses the single source-of-truth `validate_dependencies` and is dominated by the network pull on this one-shot path; per-fetch client and dirs.rs exists-check match existing module style and are cold paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`refs` was both the pure protocol/tx grammar AND `Resolver`, which
needed `crate::config`. That created a refs <-> config cycle and pulled
config knowledge into the otherwise-pure vocabulary module.
Resolution (parsed ref -> project | declared dependency) is
dependency-domain logic — it queries `config.dependencies` — so move
`Resolver`/`ResolvedProtocol`/`ResolveError` into a new
`dependencies::resolve` (re-exported from `dependencies`).
Result: `refs` is now a pure leaf module with zero `crate::` deps;
the only remaining edge is the clean one-directional `config -> refs`
(DependencyEntry.reference: ProtocolRef). Strict layering:
refs -> {config, oci} -> dependencies. `oci` stays an independent
shared push/pull transport (publish + dependencies as peers).
Consumers updated: inspect/tir.rs, invoke.rs import Resolver/
ResolvedProtocol from `dependencies` now. Behavior unchanged; also
collapsed the carried-over nested if (let-chain).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'dependency' implied a build-graph edge, but the project's protocol compiles in complete isolation and never ingests an external one. These are orthogonal interaction links — protocols you invoke/codegen/inspect against, consumed via their published TII. Rename throughout and align behaviour with the corrected abstraction. - dependencies module/config/types/strings -> interfaces - build is now strictly project-only (symmetric with check); interface validate+restore live only in consuming commands (invoke, codegen, inspect tir) - unified cache layout .tx3/tii/<scope>/<name>/<version>/ for both the project's own built TII and fetched interfaces (local scope fallback) - inspect tir decodes TIR from the normative cached .tii; .tx3 is now informative-only everywhere (like README), never compiled - regenerate e2e TII fixture with tx3c; design doc rewritten + renamed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scarmuega
commented
May 18, 2026
Interface-domain policy (alias rules, registry-ref pinning, no duplicate (scope,name)) belonged in interfaces, not the config schema layer — it even reached into crate::refs from config. RootConfig::validate_interfaces becomes a free interfaces::validate(&RootConfig), matching restore_all/ fetch/add. Addresses PR review comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes consuming an existing published protocol a first-class trix workflow.
trix use <scope>/<name>[:<version>]pulls the OCI artifact from the registry, caches it under.tx3/tii/<scope>/<name>/<version>/, and records a pinned[interfaces.<alias>]entry intrix.toml.invoke/codegen/inspectagainst, consumed via its published TII. No source stitching, no tx3 language changes.src/refs.rs) drives every protocol/tx string: CLI flags,trix.tomlrefvalues, error messages.Why "interface" (not "dependency")
A dependency is a build-graph edge — remove it and the build breaks. These protocols are not inputs to the project's build at all; removing every declared one changes zero bytes of output. The relationship lives at the interaction layer, mirroring Solidity (you don't compile a deployed contract, you hold its interface and call it). The artifact trix actually consumes is the TII — the Transaction Invoke Interface.
Normative vs informative: the published
main.tiiis the only artifact any command consumes. The cachedmain.tx3/README.mdare kept purely as human references — never compiled or treated as authoritative.Layering
The whole consume operation lives in
interfaces::add(config, AddRequest) -> AddOutcome. The command layer (use_cmd::run) only mapsArgs → AddRequestandAddOutcome → view.interfacesis the single home for this business logic; commands stay thin.Reference grammar
widget·acme/widget·acme/widget:0.1.0·transfer·widget::transfer·acme/widget:0.1.0::transfer. A token with/is a registry ref, else an alias;::separates protocol from tx; a bare tx targets the project.ProtocolRef/TxRefare typed (FromStr/Display/serde); a separateResolvermaps a parsed ref to the project or a declared interface.trix.tomlinterfacesis#[serde(default, skip_serializing_if = "is_empty")]— existing projects are unaffected and round-trip unchanged.How commands account for interfaces
The toolchain splits into project-only and consuming commands; interface machinery (
validate_interfaces+restore_all) lives only in the consuming set.main.tx3. An interface's source is the publisher's responsibility; re-analyzing it would only surface diagnostics the consumer cannot act on.--from <PROTOCOL_REF>— selects which protocol's TII to invoke against (project's freshly built TII, or the interface's cachedmain.tii); tx chosen interactively.--tx <TX_REF>— bare tx → project (lowered from the author's source);alias::tx/scope/name:ver::tx→ interface, with IR decoded straight out of the normative cached.tii(it carries the encoded TIR per tx). The informative.tx3is never read.src/oci.rs.Principle: the published TII is the contract. Interface source is never recompiled, anywhere.
Cache & restore
Unified layout
.tx3/tii/<scope>/<name>/<version>/{main.tx3,main.tii,README.md,metadata.json}— the project's own built TII and every fetched interface share the same tree (project uses[protocol] scope, orlocalwhen absent). Version-keyed (project-local + one entry per(scope,name)⇒ no collisions; content identity guarded by digest).restore_allis a no-op with no interfaces; per entry →Valid(use cache) /Missing(fetch) /Invalid(hard error,trix use --force). Thetrix.tomldigest is the lockfile, verified every restore.Codegen (interface-aware on the unstable path only)
src/commands/codegen_legacy.rs(default build) is reverted tomainverbatim — backward compatibility, single-protocol, unchanged layout. The unstablesrc/commands/codegen.rsdelegates totx3c codegen --tii, builds the project TII and uses each interface's cached published TII (no recompile). Output layout is unified — always<output_dir>/<name>/per protocol regardless of interface count (a deliberate one-time break confined to the unstable path).Key behavior decisions
trix userequires a registry ref (alias-only rejected at parse);trix.tomlrejects alias-only /latestrefs (it is a pinned lockfile).[registry]falls back toDEFAULT_REGISTRY_URL(oci.tx3.land) so cloned projects with a cached interface work zero-config; registry only contacted on a cache miss.sha256-…pseudo-version leaking intotrix.toml/cache).Commits
trix useand dependency-aware commands--from, drop no-op tx flagCacheStatus)Resolverout ofrefsintointerfaces::resolvebuild/checkstrictly project-only; unified.tx3/tii/layout;inspect tirdecodes TIR from the normative.tii; design doc rewrittenTest plan
src/refs.rsunit tests (grammar round-trip + rejections),conventionregistry_urlunit tests,codegen_targetsunit tests (gatedunstable)tests/e2e/use_command.rs— 11 tests, incl.inspect tiragainst an interface decoding TIR from atx3c-generated fixture TII (alias + full-ref forms), digest tamper, alias-only/latesttoml rejection,checkproject-only with a declared interface, no-interfaces-unchangedtests/e2e/codegen_deps.rs—#[cfg(feature = "unstable")], unified per-protocol subdir layout with and without interfacesgit diff main -- src/commands/{check,codegen_legacy}.rsempty (backward-compat guard)cargo build --features unstable+ clippy clean; unstable unit +codegen_depse2e passhappy_pathfailures —codegen_generates_bindings_from_fixture,devnet_starts_and_cshell_connects— fail identically on cleanmain)oci.tx3.landFollow-ups (out of scope)
trix publishrecording its own interfaces; cache GC for--forcerepins; discovery commands.🤖 Generated with Claude Code