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
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.
-
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).
-
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.
-
Implement. Implementation happens in the owning crate or module only (MECE). The locked invariants hold, and
unsafestays inside FFI boundary modules. -
Unit + property tests. Unit and property tests live in-crate. The proptest discipline is that parsers never panic and round-trips are identity.
-
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.
-
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.
-
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. -
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 verifyon the published assets. -
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:
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 ( |
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 |
include/tebako/fs/c_api.h (tamatebako/tebako) |
|
C4 |
The handoff contract version — currently 2 |
crates/tebako-driver (tamatebako/tebako) |
The factory shim exports |
C2/C5 |
Store layout + canonical cache roots — |
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 · |
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 |
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 + |
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 |
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:
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.
See also: Who owns what · The artifact chain · A package, end to end