Skip to content

Delete the conversion layer now that both sides name one type (#18 §A1) - #30

Open
YellowSnnowmann wants to merge 11 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/18-a1-delete-the-conversion-layer
Open

Delete the conversion layer now that both sides name one type (#18 §A1)#30
YellowSnnowmann wants to merge 11 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/18-a1-delete-the-conversion-layer

Conversation

@YellowSnnowmann

Copy link
Copy Markdown
Contributor

Blocked on tinyhumansai/tinycortex#149 — this bumps the gitlink to the commit that carries the re-export, so that PR has to land first.

Stacked on #19 → … → #29. Once those merge this reduces to the single commit Delete the conversion layer now that both sides name one type.

Summary

Issue #18 §A1, the tinymemory half.

convert.rs existed because the engine's contract crate and this one described the same values under two names. Not similar values — the same code: api/ was extracted from tinycortex-api and held byte-identical, and types.rs still differs between them by 14 lines, all doc comments, doctest paths, and one extra parse arm.

Being nominally distinct meant every call across the seam translated, and a field added to either contract had to be added to the other and to the conversion. Three places, or the value was silently dropped.

tinycortex#149 makes tinycortex-api re-export this workspace's contract. With one type set there is nothing left to convert.

What goes

  • convert.rs and convert_test.rsdeleted
  • the adapter's 11 conversion call sites — now plain delegation
  • list and namespace_summaries lose an into_iter().collect() that had become an identity map
  • store / store_with_taint pass arguments straight through

The [patch] is load-bearing

The workspace root gains:

[patch."https://github.com/tinyhumansai/tinymemory"]
tinymemory-api = { path = "api" }

Without it, cargo resolves the git copy (pulled in by tinycortex-api) and the path copy as two distinct crates:

expected `tinymemory_core::MemoryCategory`, found `tinymemory::types::MemoryCategory`
  --> ~/.cargo/git/checkouts/tinymemory-.../api/src/recall.rs

That is the exact duplication §A1 deletes, reintroduced by the fix for it. I hit it at the seam rather than reasoning about it in advance. The patch table is the same mechanism this workspace already uses for tinycortex, and OpenCompany's manifest documents the same hazard from the other side.

Acceptance for §A1 — checked, not asserted

Criterion Result
adapters/tinycortex/src/convert.rs is deleted yes
one set of value types in the workspace grep for any conversion function: 0 hits
builds and tests green with no conversion at the seam yes

Validation

Command Result
cargo fmt --all -- --check pass
cargo clippy --all-targets --all-features -- -D warnings pass
cargo clippy -p tinymemory-tinycortex --all-targets --no-default-features -- -D warnings pass
cargo build --all-targets --all-features pass
cargo test --all-features 1101 passed, 0 failed
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features pass

On the test count: 1101 is down 9 from 1110. That difference is exactly the nine tests in convert_test.rs — I checked the deleted file's test count rather than waving at the delta. They tested the layer this change removes.

Public API changes

  • Removed: tinymemory_tinycortex::convert and its nine functions. They existed to bridge two type sets that are now one; there is nothing for a caller to do with them.
  • TinycortexMemory and provider() are unchanged.

Why the engine can absorb this

Verified on the tinycortex side before writing either half — see #149 for the detail:

  • capability vocabulary grows 13 → 18; the engine matches on Capability in zero places
  • CONTRACT_VERSION moves (1,0) → (2,2); the engine never reads it
  • MemoryCategory and MemoryTaint, which the engine does match on (16 and 9 sites), are variant-identical

Related

Part of #18 (§A1). Unblocks §A2 — tinymemory_core::traits::Memory becoming a re-export of the contract's rather than the engine's — which was previously impossible: repointing it without collapsing the types produced 10 errors, 9 of which wanted more conversions.

Issue tinyhumansai#18 §E3. `AGENTS.md` mandates a `tests/` directory exercising only the
public API; the repository had none at the root, and `core/tests/` holds
fixtures with no test files.

Four targets, matching the four §E3 names:

`driver_selection.rs` pins admission: reserved ids resolve to fixed classes, an
external driver with no entry is refused fail-closed, an untrusted external
driver is refused even with one, a reserved id's class cannot be overridden by
config, and a class typo is echoed back so the operator can find the line.

`capability_negotiation.rs` covers both directions of the bind-time
negotiation, including a deliberately lying provider that advertises a summary
tree it has no accessor for — the failure `audit_provider` exists to catch, and
which was previously asserted only in unit tests inside the contract crate.

`taint_end_to_end.rs` drives provenance through store, get, list, recall, and
the export/import round trip, at every driver this workspace ships. It also
pins the fail-closed reading of an unknown persisted value, which is the one
direction that cannot be undone.

`null_provider.rs` asserts the compiled-out configuration is genuinely usable:
every mandatory method answers rather than panicking, it reports Ready rather
than a fault, and no optional family is either advertised or reachable.

Two of the four are deliberately narrower than §E3 describes, and both say so
in their module docs. `driver_selection.rs` cannot yet assert that a bound
provider's `driver_id()` matches configuration, because nothing selects an
engine from config — that is §A5. `taint_end_to_end.rs` cannot drive the sync
path, because sync is welded to the engine until §B. Both are written against
current behaviour, per the sequencing note in the issue, and each names where
its missing leg joins.

Refs tinyhumansai#18 (§E3)
Issue tinyhumansai#18 §C3. The adapter advertised Core, Recall and Portability and nothing
else, which is the reason anything wanting a summary tree, entities, or a diff
ledger reached past the contract to the engine directly — the families existed,
but not through `MemoryProvider`. `crates/tinymemory-module` had grown all of
them, because it needed them and nowhere else had them.

They were never module-specific. Every one delegates to `tinymemory-core` on a
blocking thread, and the two types they hold — `MemoryClient` and the host
config — are core and contract types respectively. So the whole
`ModuleMemoryProvider` moves down to `tinymemory-tinycortex` as
`engine::TinycortexProvider`, and the module crate keeps only the thing that
genuinely is its own: turning a `ModuleConfig` into the engine's runtime
configuration. Its `provider.rs` goes from 2189 lines to 38.

Nineteen family implementations move: documents, ingest, graph, goals,
tool-memory, tree, entities, diff, sources, maintenance, people, chunks,
retrieval, profile, and episodic, alongside the mandatory three.

The diff family gets a `memory-git` feature rather than riding along
unconditionally. It is what drags `git2` / `libgit2-sys` / `libz-sys` — a native
build — into the graph, and this adapter had no such dependency before today;
making it unconditional would hand every consumer a libgit2 build they never
asked for. `cargo tree --no-default-features` confirms none is linked.

The gate reaches `capabilities()`, not just the accessor. A build without
`memory-git` neither advertises nor reaches `Diff`, so `audit_provider` still
passes — which is the whole reason that audit exists. That rule is extracted as
`engine::advertised_capabilities` so it can be tested directly: constructing a
provider needs a `MemoryClient`, which needs the host's process-global seams
installed, and a test that installs a process global is order-dependent.

The new module is `engine`, not `provider`: the crate already has a `provider`
function returning the mandatory-only driver, and both are worth keeping — a
host with no workspace, config, or client still has the lighter one.

Refs tinyhumansai#18 (§C3)
Issue tinyhumansai#18 §A5. The driver registry could answer "is this driver id real, and is
it allowed to answer for memory", and nothing asked it: `DriverRegistry::admit`
had no caller outside its own tests, `MemoryHostConfig::memory_provider()` had
no reader, and the memory client factory constructed TinyCortex unconditionally.
Configuration could not choose an engine.

`DriverRegistry::select` closes that: it reads the engine from the host's
configuration and puts it through `admit`, so both surfaces are now live.

Two corrections to the issue, both load-bearing.

§A5 names `memory_provider()` as the selector. That method is a `provider:model`
routing string for the memory *workload* — which language model does
summarisation and entity extraction — not the store the memory lives in.
Reading it would have let a model change repoint a company's storage. Selection
reads a new `memory_driver()` instead, defaulted to `None` so it breaks no
existing implementation, and a test pins that the two fields stay independent.

§A5 also asks that `create_memory_*` return a bound `Arc<dyn MemoryProvider>`.
It cannot, and the reason is structural rather than unfinished: since §C3
`adapters/tinycortex` depends on `tinymemory-core`, so a core factory returning
a constructed adapter provider is a dependency cycle. Selection therefore
resolves the decision and the host constructs — which is what `src/registry`'s
module docs have said all along: "It resolves the class, not the instance."

A configuration naming no engine gets the reserved embedded default, so adding
selection does not turn "I configured nothing" into a host that fails to start.
Going through `select` does not loosen admission either: an external engine
named in config is still refused without endpoint, credential and trust.

Refs tinyhumansai#18 (§A5)
The `memory-git` feature added alongside the lifted diff family gates that
family, and the test asserting it is *withheld* without the feature is
`#[cfg(not(feature = "memory-git"))]`. Both existing jobs — `--all-features`
and default — compile that test out, so it was checked by nothing: a
feature-gated test whose default fate is to be built by one job and executed
by none.

Adds the configuration as its own step, and asserts the property the feature
exists for: `cargo tree --no-default-features` must link no `git2` or
`libgit2-sys`. A guard rather than a comment, because "this feature keeps the
native build out of the graph" is a claim that silently stops being true the
first time a dependency picks it up transitively.

Verified: `diff_is_withheld_when_the_snapshot_store_is_compiled_out` appears in
`--no-default-features -- --list` and is absent from the `--all-features`
listing, which is what "running nowhere" looked like.

Refs tinyhumansai#18 (§C3, §E2)
Issue tinyhumansai#18 §D4, with §D3 alongside it.

`api/Cargo.toml` spells out a `cargo tree` command in a comment and asks that
the contract crate never link a storage engine, a native library, an HTTP
client, or an async runtime. It was left as a comment, so nothing ran it. A
forbidden dependency does not arrive by someone typing it into the manifest; it
arrives transitively, through a feature enabled two crates away, which is
exactly the way nobody notices.

The forward form is the one that works, and the manifest already explains why:
`cargo tree -i <crate> -p tinymemory-api` discards the `-p` scope, prints the
whole-workspace inverse tree, and exits 0 looking clean even when this crate is
the one at fault. This runs what the comment says to run.

Verified in both directions. The rule holds today — no match against
`rusqlite|libsqlite|git2|reqwest|regex|tokio`. And injecting `regex = "1"` into
the manifest makes the guard fire on `regex`, `regex-automata` and
`regex-syntax`, then reverting makes it pass again. A guard nobody has watched
fail is not yet a guard.

§D3 asks that `--no-default-features` still compile and bind
`NullMemoryProvider`. It does, so this pins it rather than changing anything:
the minimal configuration builds, and tinyhumansai#21's `null_provider` integration test
runs against it — so "usable" is asserted, not just "compiles". That test is
the one that would catch a null driver which panics instead of answering, or
reports a fault instead of `Ready`.

Refs tinyhumansai#18 (§D3, §D4)
Issue tinyhumansai#18 §E6: "backend error, timeout, and partial-page responses on every
remote adapter — currently zero coverage". The three existing per-adapter test
files all drive a backend that answers correctly, which is the half that was
never in doubt.

Six tests, each running against all three adapters over a real TCP socket, using
the axum-double harness the happy-path tests already use:

- a 500 on write is reported rather than swallowed;
- a 500 on read is not laundered into `Ok(None)`;
- a 401 is not presented as an empty store, on either `list` or `recall`;
- a `200 OK` carrying unparseable JSON is an error and not a panic;
- an unreachable backend is reported rather than hanging;
- a paginated export terminates instead of looping.

The read case is the one that matters. `Ok(None)` after a 500 says "this memory
does not exist" when the truth is "I could not ask", and a caller cannot tell
those apart: it writes the memory again, or tells a user their memory is gone,
or a sync job treats the empty read as authoritative and prunes. Nothing
surfaces until much later.

All three adapters already behave correctly — this pins behaviour rather than
fixing it. That is worth stating plainly, because six tests passing on the first
run is exactly what a test that never exercises its subject also looks like. So
the read assertion additionally requires the backend's status to survive into
the error message; without that it would pass just as happily if the adapter had
failed on URL construction and never reached the network. Confirmed against the
live errors: `memory API v3/container-tags/list returned HTTP 500`,
`memory API memories?top_k=1000 returned HTTP 500`, and
`memory API api/v1/datasets returned HTTP 500`.

The assertions are deliberately about whether a failure comes back at all, not
about which error it is. The adapters' HTTP layer `bail!`s into `anyhow`, so
every one of these arrives as `MemoryError::Other` and "unsupported" is not yet
distinguishable from "failed" — that is §A4, and it is not what this change is.

The unreachable-backend test binds a port, reads its number, and drops the
listener, so the address is reliably closed rather than merely unlikely to be in
use. The export test is bounded by a timeout so a non-terminating implementation
fails the test instead of hanging the suite.

Refs tinyhumansai#18 (§E6)
Issue tinyhumansai#18 §E7. `AGENTS.md` documents an `examples/` directory and tells the
reader to run `cargo run --example basic`. Neither existed: no `examples/`
directory at all, so the documented command has been failing for as long as it
has been documented. (The issue attributes the reference to `README.md`; it is
`AGENTS.md` lines 30 and 74.)

The example binds the null driver, which needs no engine, no workspace and no
network, so it runs anywhere. It walks the order a host actually follows —
admit an id, then construct, then check the negotiated capabilities, then use
the mandatory families — because that order is the part worth demonstrating.
Admission is engine-neutral and answers "is this id real and may it answer for
memory"; construction needs everything an engine needs. Swapping
`NullMemoryProvider` for an adapter's provider changes nothing else in the file.

Writing it surfaced an API gap, which is the argument for having a compiled
example at all: `FallbackReason` implemented `Display` but not
`std::error::Error`, so the obvious `registry.admit(..)?` in a function
returning `Box<dyn Error>` or `anyhow::Error` did not compile. Adding the impl
is purely additive and changes nothing about the type; it makes the message
usable where refusals actually travel.

CI runs the example rather than only building it. `cargo build --all-targets`
compiles examples, so a broken one still passes — and a compiled example can
panic on its first line. Running it is what makes the documented command a
promise rather than a comment.

Not attempted here: §E7's "one example per engine". The three hosted engines
need a live endpoint and a credential, which is why
`adapters/remote/examples/conformance.rs` is a manual CLI rather than a CI
target, and the embedded engine needs the host's process-global seams
installed. Neither belongs in a `cargo run --example` a contributor is told to
run.

Refs tinyhumansai#18 (§E7)
Issue tinyhumansai#18 §D5, §E8, and the second half of §E2. Three CI additions that all
answer the same kind of question — what is this build actually costing, and is
it still true — so they land together.

**Feature powerset (§E2).** `cargo hack --feature-powerset --depth 2`, check
only. Cargo features are additive: enabling one for a crate enables it for
every consumer in the graph, so a combination nobody builds deliberately can
still be built by somebody else's dependency. `--depth 2` covers every pair
without the blow-up of the full set.

§E2 also lists per-feature lanes — `--features tinycortex`, `mem0`,
`supermemory`, `cognee`, `sync-composio`. Those are not here because those
features do not exist yet: they are §D1, and §D1 needs OpenHuman to opt into
features it currently gets unconditionally. The powerset covers whatever
features exist, so it starts useful and stays useful as §D1 adds them.

Verified by hand across all eight combinations the workspace can express today
(core: none, contacts, memory-git, test-support and their pairs; the tinycortex
adapter with and without memory-git). Every one compiles, so this pins a
property that currently holds rather than papering over a break.

**Dependency budget (§D5).** `scripts/ci/dependency-budget.sh` prints the crate
count for every configuration and fails when the minimal one grows past a
ceiling. Today: minimal 40, api 39, the tinycortex adapter 168 — and 172 with
`memory-git`, which is the native git stack the feature exists to keep out, so
the +4 is the gate from §C3 working.

The ceiling is 50 against a current 40, deliberately. A limit set at today's
exact count fails on the first legitimate addition, gets raised without thought,
and teaches everyone to ignore it. Only the minimal configuration is gated; the
richer numbers are reported, because a number nobody chose is not a budget.

**Coverage (§E8).** `AGENTS.md` asks for 80% of meaningful library behaviour and
nothing measured it. The workspace is at **76.22% lines / 76.44% regions /
63.81% functions** — below the number it asks for, which is worth seeing rather
than assuming. Some of the gap is stark: `core/src/tree/score/store.rs` and
`core/src/tree/score/extract/mod.rs` are at 0.00%.

Reported, not enforced, to begin with. A threshold picked before anyone has seen
the number is a guess, and a gate that fails on day one gets disabled rather
than fixed.

The powerset and coverage run as their own job: both are slower than the main
lane and independent of it, so a failure in one should not mask the other or
delay the fast feedback the main job gives.

Refs tinyhumansai#18 (§D5, §E2, §E8)
Issue tinyhumansai#18 §E5. The loader E2E drove three of the eighteen families the module
advertises, and nothing checked that a write reached the workspace it was given.

**Every declared method is routed.** The module advertises `Capabilities::all()`
and declares 88 methods; the existing manifest test compares two lists, so it
passes for a method that is declared, listed, and answers "unknown member" —
the bus-level version of a capability set that overstates its accessors. This
calls each declared method and distinguishes the kind of refusal: a wired method
rejects the empty argument list with
`ai.tinyhumans.tinybus.Error.BadArguments`, while an unwired one is
`ai.tinyhumans.tinybus.Error.UnknownMethod`. The two names are what make the
test discriminate rather than pass vacuously, and the unrouted shape is taken
from the build under test rather than hard-coded.

Eighteen bespoke round trips would assert more, and would also need eighteen
sets of valid arguments and eighteen engine preconditions. This asserts the one
thing that is true of all of them and is cheap to keep true.

**A write lands in the host's workspace.** Every other test here stores and
reads back inside one admission, so a module that kept its store in a temporary
directory of its own, or in memory, passes all of them — and the difference
shows on the user's next launch.

§E5 asks for a shutdown/restart cycle, and it is not here because it cannot be:
TinyBus never unloads a library, so a second admission in the same process is
refused with `ModuleRefused { reason: "module initialization failed" }`. That is
the same constraint that already forces every test in this file to be the only
one in its process. A real restart needs a second process against a shared
workspace, which is a change to the CI loop rather than a test, so the
directory assertion covers what restart would have been checking and the
limitation is written down rather than left as a gap someone rediscovers.

Verified the way CI runs them — every ignored test in its own process, all ten
green.

Refs tinyhumansai#18 (§E5)
Issue tinyhumansai#18 §A1, the tinymemory half. Companion to tinyhumansai/tinycortex#149,
which makes `tinycortex-api` re-export this workspace's contract rather than
redefining it.

`convert.rs` existed because the engine's contract crate and this one described
the same values under two names — the same code, in fact, since `api/` was
extracted from `tinycortex-api` and held byte-identical. Being nominally
distinct meant every call across the seam translated, and a field added to
either contract had to be added to the other and to the conversion. Three
places, or the value was silently dropped.

With one type set there is nothing to convert: `convert.rs` and its tests are
deleted, and the adapter's eleven conversion call sites become plain
delegation. `list` and `namespace_summaries` lose an `into_iter().collect()`
that had become an identity map, and `store`/`store_with_taint` pass their
arguments straight through.

The gitlink moves to the commit carrying the re-export. That commit also stops
publishing tinycortex, which is what makes the git dependency legal — and
records the state that repository was already in, since `cargo package` there
has failed since `api/` was split out.

The workspace root gains a `[patch]` for the git contract dependency. Without
it cargo resolves the git copy *and* the path copy as two distinct crates, and
`MemoryCategory` from one is not the same type as the other — the exact
duplication this change deletes, reintroduced by the fix for it. Found by the
compiler at the seam rather than reasoned about, and the patch table is the
same mechanism this workspace already uses for tinycortex itself.

Acceptance for §A1, checked rather than asserted:

- `adapters/tinycortex/src/convert.rs` is deleted
- no conversion function remains anywhere in the workspace (grep: 0 hits)
- the workspace builds and tests green with no conversion at the seam

`cargo test --all-features` reports 1101 passing, down 9 from 1110. That
difference is exactly the nine tests in `convert_test.rs`, which tested the
layer this change removes.

Refs tinyhumansai#18 (§A1)
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review available on request

  • 🔍 Trigger review

Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment @coderabbitai review to review the latest changes. For a full review, comment @coderabbitai full review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9523f889-e7e0-43bc-8d27-3aa1697564ab


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out · 786 embedded · openrouter/openai/text-embedding-3-small

Comment thread .github/workflows/ci.yml
submodules: recursive
persist-credentials: false

- uses: dtolnay/rust-toolchain@stable

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security uncertain

dtolnay/rust-toolchain is pinned to stable, which is mutable

A tag or branch can be repointed by whoever owns dtolnay, and the new code runs with this workflow's secrets. Pin to a full commit SHA and let Dependabot bump it.

[RULE] unpinned-action ·

Comment thread .github/workflows/ci.yml
with:
components: llvm-tools-preview

- uses: Swatinem/rust-cache@v2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security uncertain

Swatinem/rust-cache is pinned to v2, which is mutable

A tag or branch can be repointed by whoever owns Swatinem, and the new code runs with this workflow's secrets. Pin to a full commit SHA and let Dependabot bump it.

[RULE] unpinned-action ·

Comment thread .github/workflows/ci.yml

- uses: Swatinem/rust-cache@v2

- uses: taiki-e/install-action@v2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security uncertain

taiki-e/install-action is pinned to v2, which is mutable

A tag or branch can be repointed by whoever owns taiki-e, and the new code runs with this workflow's secrets. Pin to a full commit SHA and let Dependabot bump it.

[RULE] unpinned-action ·

@tinysweeper

tinysweeper Bot commented Aug 17, 2026

Copy link
Copy Markdown

How this change flows

2 changed behaviours across 7 relationships. 6 surrounding behaviours are shown (60 graph nodes walked). 26 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["mem0_provider<br/>changed"]:::changed
  n1["TinycortexMemory<br/>changed"]:::changed
  n2["Memory"]:::impacted
  n3["main"]:::impacted
  n4["iter"]:::impacted
  n5["MemoryProvider"]:::impacted
  n6["main"]:::impacted
  n7["TinycortexProvider"]:::impacted
  n1 -->|uses| n2
  n1 -->|implements| n2
  n3 -->|calls| n4
  n6 -->|calls| n0
  n6 -->|calls| n4
  n6 -->|uses| n5
  n7 -->|implements| n5
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. label Aug 17, 2026
The module crate is its own workspace root, and a patch table applies only from
the root being built — so the parent workspace's entry does not reach it.
Without its own, cargo resolves the git copy of `tinymemory-api` alongside the
path copy and `MemoryTaint` from one is not the same type as from the other.

Belongs in this commit rather than a later one: this is where the git
dependency arrives, so this is where its consequence has to be handled. CI
caught it — the module lane failed with eight type mismatches while every other
lane was green, because it is the only job that builds from that root.

Refs tinyhumansai#18 (§A1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant