Skip to content

feat: add trix use and dependency-aware commands#109

Merged
scarmuega merged 12 commits into
mainfrom
feat/trix-use
May 18, 2026
Merged

feat: add trix use and dependency-aware commands#109
scarmuega merged 12 commits into
mainfrom
feat/trix-use

Conversation

@scarmuega
Copy link
Copy Markdown
Contributor

@scarmuega scarmuega commented May 17, 2026

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 in trix.toml.
  • An external protocol is modelled as an interface, not a dependency: the project's protocol compiles in complete isolation and never ingests one. It is an orthogonal interaction link — a protocol you invoke/codegen/inspect against, consumed via its published TII. No source stitching, no tx3 language changes.
  • A single canonical reference grammar (src/refs.rs) drives every protocol/tx string: CLI flags, trix.toml ref values, 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.tii is the only artifact any command consumes. The cached main.tx3/README.md are 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 maps Args → AddRequest and AddOutcome → view. interfaces is 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/TxRef are typed (FromStr/Display/serde); a separate Resolver maps a parsed ref to the project or a declared interface.

trix.toml

[registry]
url = "https://oci.tx3.land"   # optional; defaults to DEFAULT_REGISTRY_URL

[interfaces.widget]
ref    = "acme/widget:0.1.3"   # ProtocolRef::Registry, concrete pinned version
digest = "sha256:..."          # lockfile-style OCI manifest digest

interfaces is #[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.

  • buildstrictly project-only: produces the project's own TII and nothing else. Symmetric with check.
  • checkstrictly project-only: parses/analyzes only the project's main.tx3. An interface's source is the publisher's responsibility; re-analyzing it would only surface diagnostics the consumer cannot act on.
  • invoke --from <PROTOCOL_REF> — selects which protocol's TII to invoke against (project's freshly built TII, or the interface's cached main.tii); tx chosen interactively.
  • codegen (unstable path only) — one client per protocol from each one's TII.
  • inspect tir --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 .tx3 is never read.
  • publish — behavior unchanged; OCI helpers shared via 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, or local when absent). Version-keyed (project-local + one entry per (scope,name) ⇒ no collisions; content identity guarded by digest). restore_all is a no-op with no interfaces; per entry → Valid (use cache) / Missing (fetch) / Invalid (hard error, trix use --force). The trix.toml digest is the lockfile, verified every restore.

Codegen (interface-aware on the unstable path only)

src/commands/codegen_legacy.rs (default build) is reverted to main verbatim — backward compatibility, single-protocol, unchanged layout. The unstable src/commands/codegen.rs delegates to tx3c 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 use requires a registry ref (alias-only rejected at parse); trix.toml rejects alias-only / latest refs (it is a pinned lockfile).
  • Missing [registry] falls back to DEFAULT_REGISTRY_URL (oci.tx3.land) so cloned projects with a cached interface work zero-config; registry only contacted on a cache miss.
  • No concrete version resolvable → hard error directing at the publisher (no sha256-… pseudo-version leaking into trix.toml/cache).
  • Digest mismatch is surfaced directly (no silent refetch past a lockfile disagreement).

Commits

  1. feat: add trix use and dependency-aware commands
  2. fix: resolve registry URL through a default instead of erroring
  3. fix: keep cache dirs version-keyed; drop sha256 pseudo-version fallback
  4. refactor(invoke): protocol-only --from, drop no-op tx flag
  5. fix: address review correctness/hygiene items
  6. refactor: simplify per /simplify review (unify validators, typed CacheStatus)
  7. refactor(codegen): move dependency-awareness to the unstable path
  8. docs: add the protocol-interfaces design doc
  9. refactor(use): move business logic into the module (thin command layer)
  10. refactor: move Resolver out of refs into interfaces::resolve
  11. refactor: model external protocols as interfaces, not dependencies — rename throughout; build/check strictly project-only; unified .tx3/tii/ layout; inspect tir decodes TIR from the normative .tii; design doc rewritten

Test plan

  • src/refs.rs unit tests (grammar round-trip + rejections), convention registry_url unit tests, codegen_targets unit tests (gated unstable)
  • tests/e2e/use_command.rs — 11 tests, incl. inspect tir against an interface decoding TIR from a tx3c-generated fixture TII (alias + full-ref forms), digest tamper, alias-only/latest toml rejection, check project-only with a declared interface, no-interfaces-unchanged
  • tests/e2e/codegen_deps.rs#[cfg(feature = "unstable")], unified per-protocol subdir layout with and without interfaces
  • Default build/clippy clean; git diff main -- src/commands/{check,codegen_legacy}.rs empty (backward-compat guard)
  • cargo build --features unstable + clippy clean; unstable unit + codegen_deps e2e pass
  • Existing lib + e2e green (the two pre-existing env-dependent happy_path failures — codegen_generates_bindings_from_fixture, devnet_starts_and_cshell_connects — fail identically on clean main)
  • Manual smoke against real oci.tx3.land

Follow-ups (out of scope)

trix publish recording its own interfaces; cache GC for --force repins; discovery commands.

🤖 Generated with Claude Code

scarmuega and others added 11 commits May 17, 2026 16:21
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>
Comment thread src/config/convention.rs Outdated
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>
@scarmuega scarmuega merged commit aa99f6e into main May 18, 2026
5 of 6 checks passed
@scarmuega scarmuega deleted the feat/trix-use branch May 18, 2026 22:40
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