8 Ecosystem Field Guide: Design Questions in Practice
This guide is organized by decisions, not package inventory. Each exhibit is public and reader-verifiable, and each is evidence for a bounded design claim rather than a recommendation to copy a package wholesale.
8.1 How can one declaration serve several consumers?
8.1.1 rtemis: property contracts as inspectable data
rtemis treats a property declaration as more than a validator closure. Its public PropertySpec implementation retains value type, container shape, bounds, choices, defaults, null behavior, tuning intent, conditional applicability, and data-dependent bounds as data. That choice is causal: once the declaration can be inspected, the package can use the same facts to build executable S7 properties, establish defaults, project schemas and interfaces, and prepare a configuration against training data. A closure alone could enforce a value locally, but it could not explain the rule to those other consumers.
The distinction between a value and a candidate set shows why the extra model matters. Shape is not enough: a length-two vector might be one value, a range, or a search space. By recording tuning intent independently, PropertySpec prevents downstream code from reverse-engineering semantics from shape. Similarly, a data-dependent bound remains declared with the property even though it cannot be resolved until the training data supplies the missing context. Declaration and enforcement can therefore happen at different times without becoming different rules.
This concentration of knowledge creates obligations. A default should satisfy its own declaration; incompatible metadata axes should fail when the property is defined; and generated validators, schemas, and interfaces need alignment tests. Contract-as-data removes independently maintained definitions, not the possibility of a faulty projection. Nor does it make every rule reversible: reconstruction is credible only for the vocabulary that the declaration actually records.
Decision test. Use contract-as-data when several real consumers need the same facts and their agreement can be tested. A handwritten S7::new_property() remains simpler when construction is the only consumer.
8.2 Which objects are values, snapshots, or runtime owners?
8.2.1 deputy: portable definitions at a permission boundary
deputy runs tasks with an ellmer chat and R tools. Its public documentation separates declarative configuration from a live run: tool definitions, permission policies, and usage limits describe requested behavior, while the Agent owns execution, streaming, counters, and host interaction. AgentResult records how a completed run ended, including stop reason and usage information.
The split follows ownership. Definitions and policies can travel as data because they do not themselves contain a live model connection or grant a capability. At execution time, the host binds symbolic tools and applies its permission policy; valid configuration is therefore not confused with authority. The running agent needs identity because it coordinates changing state and effects, while its completed result can become a value-like fact. Deputy also composes with ellmer’s public tool and structured-output meanings instead of inventing parallel ones, keeping package policy at the layer that owns it.
Read-only result fields, where used, express the lifecycle of completed facts. They are not evidence that every S7 value should be read-only. Editable configuration can retain value semantics and invariant-preserving replacement.
Decision test. Choose a reference owner for shared identity, live resources, or an observable lifecycle. Use values for portable definitions and completed facts, and keep requested capability distinct from host-granted authority.
8.3 When is a semantic middle representation worth its cost?
8.3.1 sqlr: one model for rendering, reflection, and comparison
sqlr supplies a representation of a database schema that is neither SQL text nor a live connection. Constructors create that model; dialect objects render it; sqlr_reflect() reconstructs it from a catalog; and sqlr_equal() and sqlr_diff() compare its meaning. These are independent consumers, so the schema model prevents each operation from re-parsing text or inventing a private account of tables and constraints.
The model also makes policy visible. Rendering can order tables by dependency and defer cycle-closing foreign keys to later statements. Dialects carry engine-specific behavior without requiring a live connection, and the package deliberately declines to offer an untested “ANSI SQL” fallback. Reflection is bounded by what a database exposes, so missing catalog facts remain documented limits instead of guessed values. Comparison can then ignore incidental constraint order or generated names and report differences in domain terms.
The resulting round-trip law is semantic, not textual: rendering into a particular engine and reflecting back should preserve the modeled meaning that engine can represent. Exact representation, canonical form, and domain equality remain separate claims. The public generic definitions at revision 8325581 make that operation vocabulary inspectable.
8.3.2 Additional exhibit: quickr’s typed compiler IR
quickr gives an IR a different reason to exist. Its R-to-Fortran compiler reads an in-language type declaration, lowers source through typed objects, and passes those objects between analysis and code-generation stages; the public quick.R source shows compiled artifacts such as FortranSubroutine being consumed as structured values. A typed IR is justified here because translation and analysis are the product. For a single textual rewrite, the same class graph would be ceremony.
Decision test. Introduce an intermediate representation after at least two consumers need the same stable meaning. Rendering alone may need an adapter; rendering plus reflection and comparison, or analysis plus code generation, can justify a shared typed model.
8.4 Who owns the contract: S7 properties or an external schema?
8.4.1 S7schema: an externally validated container
S7schema validates YAML configuration against JSON Schema using AJV through V8. S7 classes supply the surrounding configuration and extension machinery, while detailed domain entries remain list content whose contract belongs to the external schema. This avoids translating every foreign rule into a second property system and keeps conformance with the authority that other implementations can share.
The cost is a wider gap between manipulation and certification. Arbitrary list editing does not automatically preserve validity, so an API must say which boundaries revalidate and which operations merely rearrange content. External authority removes duplicate rules only if consumers continue to consult it.
8.4.2 stacbuildr: native boundary constructors plus conformance
stacbuildr makes the other trade-off. It gives STAC catalogs, collections, and items native classes, projects them into familiar tabular or spatial forms, and validates emitted documents against published STAC schemas. Its item_from_terra(), item_from_sf(), and item_from_lidr() constructors translate objects users already hold while extracting spatial metadata. The native layer is useful because R-side construction, navigation, and projection need nominal concepts; the external validator remains useful because STAC, not the R package, owns wire conformance.
The contrast is not “schema versus classes.” S7schema keeps detailed entries under external-schema authority because the container and extension machinery are the main R-side concern. stacbuildr adds a semantic layer because its major nodes have operations in R, but still refuses to make that layer the sole judge of an external specification. In both designs, authority follows the knowledge rather than the implementation language.
8.4.3 Additional exhibit: typedjson’s minimal annotations
typedjson addresses a narrower authority problem: JSON cannot express every distinction among R values. Its public design at revision 7160c06 keeps ordinary values as ordinary JSON and adds annotations only where the wire format would otherwise lose information. Ordinary numeric spellings can preserve integer versus double (1 versus 1.0); annotations carry distinctions such as typed missing or empty values, non-finite numbers, attributes, and object-system metadata. The round-trip goal determines the annotations; it does not justify turning the whole document into an opaque serialization envelope. This is a useful minimum-metadata rule whenever readability and fidelity compete.
Decision test. Ask whether downstream operations need nominal identity and invariants inside R. If not, a validated external-schema container may suffice. If they do, add a semantic layer while retaining the external conformance oracle. Annotate a wire form only for distinctions its native vocabulary cannot preserve.
8.5 Should failure be a condition or a value?
8.5.1 resultr: explicit outcome sequencing
resultr defines an abstract Result parent with Success and Failure variants. Its Result.R implementation provides inspection, unwrapping, defaults, and and_then(). A Success applies the next computation; a Failure returns itself, making short-circuiting part of the protocol rather than a convention repeated by each caller.
The same source makes the condition/value boundary explicit. wrap_in_result() maps ordinary values to Success, errors and warnings to Failure, and existing results to themselves; and_then() uses rlang::try_fetch() at that boundary. This adapter matters because R conditions and result values serve different traffic. Conditions support immediate non-local control flow. Outcome values can be stored, inspected, combined, or returned through an ordinary value channel.
The exhibit therefore supports a choice, not a universal replacement for conditions. Programmer errors and failures requiring immediate unwinding still belong in the condition channel. A result protocol must also say whether failures short-circuit or accumulate; resultr demonstrates the former, while validation systems often need the latter.
Decision test. Choose the channel per subsystem, then define one deliberate adapter. Do not alternate ad hoc between thrown conditions and returned failures.
8.6 How much behavior belongs in a reusable contract?
8.6.1 Additional exhibit: s7contract’s executable laws
s7contract separates several strengths of agreement. Its public README at revision 41dce50 describes structural interfaces as named sets of required generics, explicit traits with registration and defaults, runtime argument and return checks, and generative laws with generators and shrinking. Method availability can show that an object has the required vocabulary; executable laws probe whether implementations preserve behavioral promises that a dispatch table cannot express.
The causal lesson is that “implements” has layers. Structural conformance is cheap and queryable, runtime contracts can localize bad inputs or outputs, and generative laws can be reused by third-party implementations. None supplies a compile-time proof, and sampled laws do not establish a theorem. Their value is to turn otherwise prose-only obligations into public, repeatable tests.
Decision test. Add reusable conformance laws when independent implementations share behavior important enough to test. Do not mistake method presence or generated examples for proof of all valid inputs.
8.7 When should policy become a composable object?
8.7.1 Additional exhibit: optimizers7 and parameters7
optimizers7 and parameters7 extract mathematical policies that modeling packages often bury inside loops and parameterizations. The public optimizers7 design models an optimizer separately from its stopping rule. Because “finished” is an independent policy, users can compose or replace stopping criteria without forking the algorithm. The parameters7 design models constrained parameters as reusable maps with inverses and derivatives, so an optimizer or likelihood can depend on a stable mathematical interface instead of one package’s private encoding.
This decomposition pays when behavior varies independently and has operations of its own. A tolerance used by exactly one loop may remain an argument. A stopping rule that combines criteria, or a parameter map that must be evaluated, inverted, and differentiated, has earned nominal identity and dispatch. Both packages also keep their abstractions narrow: optimization need not know what an observation is, and a parameter map need not own a statistical model.
Decision test. Promote policy to an object when it varies independently, composes with peers, or supports a reusable operation vocabulary. Keep a scalar option as data when no consumer needs those capabilities.
8.8 Cross-case synthesis
Across these cases, S7 is valuable less as a syntax for records than as a way to put authority and variation in inspectable places. rtemis centralizes facts because several projections need them. sqlr and quickr introduce middle representations because several transformations share stable semantics. S7schema and stacbuildr keep external specifications authoritative even when R-side objects add ergonomics. resultr names a control protocol, s7contract names conformance obligations, and optimizers7 makes an independently varying policy composable.
The recurring limit is equally important. Central metadata needs projection tests; an IR needs multiple substantive consumers; a native document model does not supersede its wire specification; a result value does not replace all conditions; generated laws are evidence rather than proof; and a policy object must earn its indirection. These are not package-specific cautions. They are the costs created by the same design moves that provide the benefit.
8.9 Questions to carry into a design
- How many consumers need the same declared knowledge?
- Is this object a portable value, completed snapshot, or live owner?
- Does a middle representation have more than one substantive consumer?
- Is contract authority local, external, or shared through tested projections?
- What equality relation makes a round trip meaningful?
- Does failure control execution immediately, or travel as data?
- Is a behavior merely configurable, or independently composable?
- Which unknown content is rejected, preserved inertly, or interpreted?
S7 earns its place when nominal meaning, invariant placement, or generic operations simplify consumers—not merely because structured data exists.