Skip to content

ARCHITECTURE · 24

For contributors.

Every interface in the ecosystem has exactly one owner, one declarative contract, and one verification point. Contributing starts with knowing which repository owns the value that a change touches, and which suite will judge the change.

The engineering order

The order every change follows is fixed, in every repository. It is not a suggestion: a change that skips a step is sent back to that step.

  1. Design, spec-first. Contract changes land in the spec set (or a linked docs/*.md) and, where applicable, a versioned schema, before code. Wire formats get byte diagrams; behaviors get named errors and exit codes up front; every new concept is assigned to exactly one layer (L0–L3).

  2. Oracle pin. If a C++/gem predecessor exists, golden vectors and fixtures are generated from it first; without a golden oracle, there is no parity claim.

  3. Implement. Implementation happens in the owning crate or module only (MECE). The locked invariants hold, and unsafe stays inside FFI boundary modules.

  4. Unit + property tests. Unit and property tests live in-crate. The proptest discipline is that parsers never panic and round-trips are identity.

  5. Contract / parity suite. The ported C++ corpus runs against the Rust implementation (tests/contract), and the parity legs diff byte-for-byte against the oracles.

  6. E2E, tiered. E2E testing is tiered. The PR tier is one smoke per OS with a cached runtime, under ten minutes; the nightly tier is the full matrix; the weekly tier is adversarial, covering cold cache, network failure, corrupted downloads, and jail enforcement.

  7. Size + hygiene gates. The size and hygiene gates are the bootstrap size table per platform (hard fail at ≥ 3 MB), the exported-symbol audit (only the tebako_* surface leaks), clippy -D warnings, and fmt.

  8. Release + verify. Release and verify run as tag, then per-platform artifacts, then SHA256SUMS, then the completeness gate (a partial asset set fails the release), then tebako-pkg verify on the published assets.

  9. Doc sync. The spec set and README status sections update in the same PR. Unshipped behavior is marked PLANNED and is never described as done.

The per-language rules sit under the order. Ruby tooling is autoload-only, with no require_relative, no send, no instance_variable_get, and no respond_to?. Rust keeps unsafe inside FFI boundary modules, names its errors on every trailer/exec/network path, and holds the bootstrap to opt-level="z", fat LTO, one codegen unit, panic="abort", and stripped symbols. Tebako-owned C/C++ exists only in dwarfs-t and the ruby fork’s io-routing patches, nowhere else.

One owner per contract value

A contract value that crosses a repo boundary has exactly one authoritative owner. Every other consumer either flows it — a generated file, a manifest, an exported symbol — or, when flowing is genuinely impossible, asserts parity in CI. A second hand-written copy of any of these values, in any language, in any repo, in any workflow, is a bug on arrival. The mount root is the worked example:

OWNER tamatebako/ruby the patch literals /__tfs__ · A:/t FLOWS source tarball tebako-mount-root manifest + SHA256SUMS FLOWS runtime factory builder.rb → -DFS_MOUNT_POINT manifest absent → exit 132 FLOWS runtime exe compiled-in fs_mount_point the factory-owned shim READS — NO COPY tebako-driver mounts the env image at the exe’s root Everything downstream reads the value; nothing re-authors it. The 2026-08-01 msys incident — the driver mounted the POSIX root while ruby looked in the drive-letter root — is the drift class this shape makes impossible. TEBAKO_MOUNT_ROOT overrides the baked root at boot — validated (exit 65), gated on the image’s layout grant (exit 78)

Figure 1 — The mount root flows from the patch literals through the source tarball manifest and the runtime factory into the runtime exe; the driver reads it and carries no copy.

Contract value Single owner How everyone else gets it Edge

The tpkg wire format — 166 B header · 280 B slot records · ≤ 8 slots · flags + extension blocks

crates/tpkg (tamatebako/tebako)

Every Rust consumer links the crate (#![forbid(unsafe_code)]). The C mini-lib in libtfs is the golden oracle, and the format is asserted byte-exact against it. Extensions live in flags and extension blocks, never in a version bump, so v1 readers parse v2 packages untouched.

C6

The runtime mount root — /tfs on POSIX · A:/t on windows

tamatebako/ruby — the patch literals

The value flows: patch literals → the tarball’s tebako-mount-root manifest → the factory’s builder.rb (-DFS_MOUNT_POINT) → the exe’s compiled-in fs_mount_point → the driver reads it at boot. A tarball without the manifest is refused with exit 132. The driver crate carries no copy, so exe and driver cannot drift.

C1/C3

The tfs C ABI (stat layout) — the tebako_fs_* surface

include/tebako/fs/c_api.h (tamatebako/tebako)

_Static_assert`s pin the layout (`tebako_stat: 56 bytes, st_size at 24, st_mtime at 40), and both sides compiling is the check. The factory CI adds an nm provenance assert. The Rust tfs crate is the shipping implementation, and C++ libtfs stays the parity oracle.

C4

The handoff contract version — currently 2

crates/tebako-driver (tamatebako/tebako)

The factory shim exports TEBAKO_CONTRACT_VERSION from the driver’s getter, never a factory-side literal. A preflight gate fails the factory build when contract.yml disagrees with the driver; the release manifest declares the same value; the loader refuses a mismatch before any download, with exit 75.

C2/C5

Store layout + canonical cache roots — $TEBAKO_HOME · ~/.tebako · %LOCALAPPDATA%\\tebako

crates/tebako-resolve (tamatebako/tebako)

store.rs owns the layout-version stamp: a newer stamp is an upgrade refusal, and an older one gets a named migration, never a silent mixed layout. The size-capped bootstrap cannot link the crate, so it mirrors the semantics, and both sides' tests pin the constant identical. When a value cannot flow, parity is asserted, not assumed.

C13

Name + reference grammars — payload names · tfs:github: · tfs+git:// · ?sha256= pins

crates/tpkg (manifest model) · crates/tebako-resolve (reference syntax)

There is one parser per grammar, and every other consumer links it. Unparseable input is a named error, never a guess and never a default service.

—

In-image layout paths — /tpkg/manifest.yaml · /lib/tebako/layout.yaml

crates/tpkg (L1 model) · tebako-runtime-ruby (layout emission)

The factory emits the env image’s layout.yaml, and the driver verifies it post-mount, before the interpreter starts. A mismatch against the exe’s compiled expectation is exit 78, never a ruby LoadError.

C3

The dwarfs_c ABI — the only C++ surface in the ecosystem

dwarfs-t/include/dwarfs_c.h

The header owns DWARFS_C_ABI_VERSION. The FFI crate (dwarfs-t-sys in tamatebako/dwarfs-t-rs) pins the number and calls dwarfs_c_abi_version() at bind, and a mismatch is refused with both numbers printed.

C20

The schema registry itself — nine versioned schemas

docs/spec/schemas/ (tamatebako/tebako)

One registry holds every grammar in the ecosystem. Each schema carries schema_version + schema_minor and its evolution metadata; every producer validates its declarations in its own CI; every consumer validates before use and refuses invalid input with a named error.

—

The contract graph

The contract model is the complete map: every component, every interface between them, and the declarative contract on each interface, twenty edges, C1–C20. Its law is nothing is read until it breaks. Consumers verify fail-closed before execution, a mismatch is a named error naming both sides, and anything that predates declarations is era 1 and is refused by name rather than assumed or silently served.

Cluster Edges What it pins

The factory chain

C1 · C2 · C3 · C4 · C19 · C20

The source tarball’s layout (tebako-mount-root + SHA256SUMS); the runtime release card (era, contract_version, mount_root, abi, the exe’s sha pinned to the image’s sha); the exe ↔ env image layout.yaml; the c_api.h compile-time ABI; the vcpkg baseline tag; the dwarfs_c FFI version.

The loader family

C5 · C6 · C7 · C8 · C9 · C13

The loader-to-driver handoff (argv grammar + TEBAKO_CONTRACT_VERSION); the package trailer + its L2 manifest block; payload mounts (duplicate mount → EEXIST, any failure → unmount-all); the registry schema; the store layout-version.

Payload edges

C10 · C11 · C12

Per-entrypoint runtime_requirement (unsatisfiable → a named error, never a segfault); provides_abi for feature payloads (a consumer binds only on an ABI match); publish-time registry validation.

Trust

C14 · C15

The v2 signature block in the canonical signed region; the key ring with per-key validity eras — old artifacts verify against the key that signed them, a revoked key is a strict failure naming it.

Enforcement details

C16 · C17 · C18

The feedstock’s pin of the product release; the jail grammar (malformed → exit 73, unknown directive → a named error, no silent drops); the preload shim’s TEBAKO_TFS_MOUNTS grammar (unparseable → fail closed, never a half-mounted child).

The era model: how schemas evolve without silent breakage

Every schema in the ecosystem obeys one evolution law, with no exceptions and no local dialects:

  • schema_version is a single integer MAJOR, and schema_minor is an additive counter. MAJOR breaks, MINOR adds.

  • Readers ignore unknown fields within their MAJOR, without inventing semantics for them. A field the reader does not understand changes nothing the reader does.

  • An unknown MAJOR is a named refusal: the artifact, its schema and version, the consumer, its maximum spoken version, and the remedy.

  • A missing schema_version means era 1, which is a named refusal ("pre-era document; regenerate with a current tool"), never a silent default.

  • Type changes are MAJOR. There is no silent coercion, not string→list, not kebab→snake, nothing.

  • Renames cross a deprecation window: writers emit both old and new for two consecutive MINORs, readers prefer new and warn on old, and then the old field drops.

  • An entry marked critical: true that a reader does not understand is refused with a named error; skipping is for decoration, never for semantics.

Every rule answers to the scenario catalog, S1–S61, each number naming an expected behavior, for example package era > bootstrap era → refuse, naming both eras and layout.yaml’s mount root ≠ the exe’s expectation → exit 78, never a LoadError. The S-numbers are the e2e test ids to implement, and building that catalog out as the cross-repo e2e suite is tracked work. The refusal exits are allocated today:

Exit Refusal What it means

75

contract

the runtime speaks a handoff contract this loader does not — raised pre-download, naming both generations and the remedy

77

era mismatch

a package or payload’s contract era does not fit the reader, in either direction

78

image layout

the env image’s layout.yaml mismatches the runtime exe’s compiled expectation — raised by the driver, post-mount, before the interpreter starts

132

pre-era source

a source tarball without its tebako-mount-root manifest — the runtime factory refuses to build from it

The loader’s own codes (65–76) and the full table live on the anatomy page; the grammars themselves are the normative spec set.

The contract suites, per repo

Every cross-repo claim in this architecture is backed by an executable check. When a change touches a boundary, these are the suites that will judge it.

tamatebako/tebako — the product

Note

This suite is shipped.

CI runs the workspace on ubuntu-24.04 and macOS against a pinned vcpkg baseline. tests/contract drives the ported C-ABI corpus through the tebako_fs_* symbols — 164 tests at the milestone-3 audit, limnifs backend cases since — plus a plain-C harness that proves the ABI from a C consumer. Golden legs diff tebako-pkg against the C++ tebakofs, tfs-cli mkimage against mkdwarfs, and tebako-cli’s press against the reference gem, byte-for-byte or a failed build, with each leg auto-skipped when its oracle is absent. The bootstrap size gate fails at ≥ 3 MB, a --no-default-features leg keeps the no-dwarfs configuration honest, and clippy runs with -D warnings. Releases add the per-platform size table, SHA256SUMS + manifest.json, and a completeness gate that fails on a partial asset set.

tamatebako/ruby — the source factory

Note

This suite is shipped.

The lint-patches matrix re-applies every supported version’s full patch set to its official tarball on every change, and a patch that fails git apply --check aborts the build, loudly, never silently. Schema files enforce versions.yml’s shape, and the compile-smoke gate builds the patches before they may release (the v0.2.8 lesson). release-src publishes the per-line tfs-ruby-<ver>-src tarballs with SHA256SUMS.

tebako-runtime-ruby — the runtime factory

Note

This suite is shipped.

A preflight gate fails before the matrix builds anything when contract.yml and the driver’s TEBAKO_CONTRACT_VERSION disagree. The matrix builds per (version × triplet); every built runtime passes a boot smoke — boot, the stat family, IO, bundler, locks — and nm export inspection before publish. Releases carry the interpreter exe, the env image, manifest.json, and SHA256SUMS.

dwarfs-t + dwarfs-t-rs — the format library

Note

This suite is shipped.

The only tebako-owned C, behind the stable `dwarfs_c_*` ABI: no C headers leak to consumers, the library is read-only at runtime forever, and it has a creation-time writer. The Rust FFI crate pins DWARFS_C_ABI_VERSION and refuses a bind mismatch with both numbers printed.

libtfs (C++) — the parity oracle

Note

This suite is shipped.

The 493-test ctest suite is the executable definition of tebako_fs_* behavior: errno mapping, mount semantics, dir cookies, pread position-independence, multi-mount isolation. docs/parity.md audits every group, ported to the Rust contract suite or consciously not ported, with the reason recorded. The repository is superseded as the implementation and kept as the judge.

tebako-bootstrap (C99) — the v1 behavioral oracle

Note

This suite is shipped.

The repository is retired from production and kept as the behavioral reference: self-test.sh stitches a lean package from a freshly built launcher and a fake runtime, then exercises the download, cache-hit, offline, checksum-mismatch, and ABI-mismatch paths. The Rust bootstrap’s stderr bodies match the C++ reference 1:1, golden parity rather than recollection.

The drift monitor

Note

The drift monitor is shipped.

Ruby upstream moves constantly, and tebako’s patches are pinned to exact tarballs. The release monitor runs on a daily cron in tamatebako/ruby and closes most of that gap unattended:

daily cron · release-monitor.yml
  1. detect    diff official ruby-lang.org releases against versions.yml
  2. onboard   add to versions.yml (official URL + sha256) → select the patch set
               → git apply --check every patch in the resolved set
  3a. clean    open the "Onboard ruby X.Y.Z" PR → merge runs the lint matrix
               → tag → release-src publishes tfs-ruby-<v>-src-*.tar.gz + SHA256SUMS
  3b. drift    any patch failing → a named issue carrying the failing hunk output
               — never a silent bad release

The loop closes downstream: a successful source release can dispatch a pin-bump PR in the runtime factory (bot/source-pin-<tag>; there is no auto-merge, because the build matrix runs on the PR). The issue history in tamatebako/ruby is the record: each "Onboard ruby X.Y.Z: patches fail to apply" issue named a version whose upstream changes broke a patch, and each stayed open until the patch set caught up.

Where to start

Three on-ramps exist, in increasing order of commitment. All three begin the same way: read the spec section that owns the behavior, because contract changes land there before code, which is step 01 of the order.

Fix a drifted patch

The release monitor’s issues name the ruby version and the failing hunks. Forking the named patch for that version, re-verifying with git apply --check, and landing it via PR is the most mechanical and most welcome first contribution to the ecosystem.

Close a parity gap

docs/parity.md lists the C++ test groups consciously not ported, each with its reason. Porting one through the C ABI, or demonstrating that the reason no longer holds, strengthens the contract suite where it is thinnest.

Implement a scenario

The scenario catalog (S1–S61) names the expected behavior of every contract edge, and those S-numbers are the e2e test ids to implement. Pick one, write the failing case, and make it pass in the owning crate.