Tags
The tree rules decide visibility: which symbols the chains of exposures bring to each module. Test support must stay out of foreign production contracts, UI contracts must stay out of non-UI source, and browser code needs browser-safe runtime dependencies. These restrictions require the importing source area's classification and the exposed symbol's tags to be compatible, on top of everything the tree already decided.
Tags carry those facts, and they are purely restrictive: a tag can only
withhold a symbol that is visible, never expose one. Ramify fixes two rule
kinds. Its default tag registry defines testing, ui, dispatch, and
browser; projects may add tags of either kind. The examples on this page use
that default registry. The testing
and browser diagrams below each use a small example universe and keep the
tree trivial - two hops deep, everything exposed to the root and back to
every descendant, so visibility is identical in every box by construction.
The tag is the only variable in each picture. Their module boxes show
ordinary-source availability; the nested src/tests/ profile is explained below.
testing - exposing internals to tests only
In this small app, we keep order handling in an orders module. Its tests
need an internal that must never become a production dependency: the
resetOrderStore hook that clears the module's store between runs. Without
tags we would face a false choice - expose the hook to everyone or to no one.
testing resolves it: we expose exactly the
internals tests may touch, tagged, through the ordinary channels.
So orders exposes two symbols to its parent - OrderService, the real
contract, and resetOrderStore, tagged - and app exposes both to its
descendants, where integration-tests is a
testing module beside the production consumer
billing. Identical chains, identical arrivals - and one
symbol wears a tag.
(The diagrams are live, exactly as on the core model page: click a traced symbol to isolate its chain, drag to pan, scroll over a figure to zoom.)
Every symbol has a tag set, assigned once by
its owner and immutable from then on: no module along the way can add, remove
or change a tag, which is why resetOrderStore's chip rides on the owner's
row and on every arrival alike. A module can carry tags
too, declared in its own definition -
integration-tests is a tagged module, and
wears its tag as the dashed box filling the node.
A module's tags classify its ordinary source under src/, including
src/interfaces/. Its nested src/tests/ has a separate testing profile.
Ordinary subdirectories share their source area's profile;
separately declared submodules own their files and declare their own tags.
Tags are not inherited across ownership boundaries. A *.test.ts filename
outside src/tests/ does not confer testing classification.
And a tag is not just a name: each registry definition selects one of the
two rule kinds fixed by Ramify.
Project-defined tags follow those same rules. That is why the diagrams write a tag
behind the glyph of its rule - ⇥ testing - the name
and the rule are one thing. testing carries the required importer tag
rule, written ⇥, and that rule fires from the symbol: the
demand rides out with the symbol and is checked where it lands - the symbol
is available from exposure only in
source areas carrying the same tag.
Read billing's box: resetOrderStore is listed, provenance and all, with
its name struck - visible there, not available: you can see it, you
cannot have it. The same strike appears in app's own box: app carries no
tag either, and re-exposing a symbol earns no right to import it. In the
testing module the name is plain. Selecting a tagged symbol shows the same
verdicts in motion: exactly the arrivals where it is available blink, and
withheld rows stay dark.
On a source area, testing classifies its files as testing source, and
every new exported binding defined there must carry testing. An explicit tag set
that omits it is invalid. This applies to a testing module's src/ and to
every module's src/tests/. A production-defined reset hook can also carry
testing for its cross-module contract while remaining usable within its
owner's production source.
Received symbols keep their owner's tags. A testing module re-exposing a production symbol does not turn it into test support.
Two properties make this small mechanism enough. There is no global test
channel: resetOrderStore reaches the testing module because the chain of
exposures brought it there, not because the importer is a test. And exposing
to descendants preserves this testing restriction at every subtree width:
the availability rule travels with the symbol and withholds it from
non-testing source areas everywhere the exposure reaches.
Tests within their module's source
Each module owns src/, with optional test and interface directories inside it;
separately owned children live under subs/:
orders/
├── module.ramify
├── src/ # this module's source
│ ├── interfaces/ # ordinary contract vocabulary
│ └── tests/ # this module's tests and helpers
└── subs/ # separately declared child modules
Ordinary source and src/tests/ have one owner. The testing profile takes
precedence within src/tests/; the module header classifies the rest of src/,
including src/interfaces/. The interface directory adds no separate profile
or automatic exposure. Tests can import their own implementation's internal
exports without exposure declarations. Foreign symbols still need
an exposure path, and a parent's tests gain no automatic access to a child's
private internals.
The test profile is fixed: testing plus every required-importer tag in the
module header. Required-symbol tags are not inherited, and the profile cannot
be overridden or extended. For example:
ramify 1
module widgets tagged [ui, browser]
Here browser implementation and Node/jsdom tests share ownership. The
src/tests/ profile is [testing, ui]. Each source area has one profile;
individual filenames do not override it.
A module tagged [ui, dispatch, browser] similarly has
[testing, ui, dispatch] in src/tests/. The same derivation applies to
project-defined tags according to their rule kind.
Tests needing additional tags use a separate testing module under subs/.
For example, browser-classified tests can use:
ramify 1
module browser-tests tagged [testing, ui, browser]
Put these tests in that module's ordinary src/, where all three tags apply.
Its optional src/tests/ would still derive [testing, ui]. The separate
module needs explicit exposure and compatible tags for the implementation it
tests; it gains no automatic access to its parent's private exports. An owner
can expose a small testing-only helper or wrapper where needed. Forwarding
aliases retain the original tags.
Non-testing source cannot import, re-export, or load testing source, including within the same owner and through a testing barrel. This source restriction also covers type-only imports. Forwarding preserves the original symbol's identity and tags; it cannot hide a testing source origin.
ui - keeping UI contracts out of core source
ui carries the required importer tag rule, just like testing.
An exposed UI symbol requires ui on the importing source area for both
value and type-only imports. A shared component may reach every branch
through an ancestor's exposure while remaining unavailable to non-UI core.
Every exported binding owned by a UI module must carry ui, including its test
bindings. A test area tagged ui also requires it on its new exported bindings.
Received symbols keep their original tags.
ui classifies architectural coupling; browser classifies runtime safety.
For an exposed component tagged [ui, browser]:
| Importing area | Value import | Type-only import |
|---|---|---|
UI implementation [ui, browser] | Allowed | Allowed |
Node UI tests [testing, ui] | Allowed | Allowed |
Non-UI core [] | Denied | Denied |
Browser-only utility [browser] | Denied | Denied |
These examples assume the component is visible and its import path passes the testing-source restriction. A UI tag supplies no additional reach.
dispatch - keeping transport coupling explicit
dispatch also carries the required importer tag rule. A foreign transport
contract or typed client tagged dispatch requires a dispatch-classified
importer for both values and types. Its exposure path still decides where
that contract can reach.
A connected view can carry [ui, dispatch, browser], while a pure UI module
uses [ui, browser]. The connected view can import visible dispatch contracts;
the pure UI cannot. Every newly owned export of dispatch source must retain
dispatch, including its test bindings. Forwarded symbols keep their original
tags. The tag does not prove that a function only dispatches; implementation
responsibilities need separate review.
browser - keeping Node code out of the browser
Our second app has a ui module that runs in the browser, a server module
beside it, and a shared module both of them draw on. Browser code must not
pull in Node-only dependencies - and that line runs through shared, not
between modules: shared owns both formatMoney (browser-safe) and
queryDb (Node-only). Splitting every such module in two would multiply
modules to encode one fact; browser states the
fact per symbol instead.
The universe is the mirror image of the last one: here the module wears the working tag, and the rule asks the symbols for a match.
browser carries the required symbol tag rule, written
⇤ - the mirror of testing's. Where testing's rule fires
from the symbol, this one fires from the importing source area. On a source
area, browser classifies files that run in a browser, and the demand
is checked on everything arriving: that area may value-import from
exposure only symbols tagged browser. On a symbol, browser is the
other side of that demand - a falsifiable promise that the symbol's entire
transitive runtime closure is browser-safe, the owner's private files
included. The chip still wears the rule's glyph, because the name and the
rule are one thing, but nothing fires there. The promise is verified
separately, and a false claim is reported at the owner - never at the
importer, which did nothing wrong by believing it.
Read ui's box: it is a browser module - the
same dashed box as the testing module, labeled
⇤ browser - and queryDb's row in it is struck:
visible there, not available, like billing's struck row in the last
picture. Selecting queryDb shows the dynamic version - server lights up
and ui stays dark.
The general shape
Two availability rules exist, and
the diagrams showed one of each: the required importer tag
(⇥, used by testing, ui, and dispatch) rides out with the symbol;
the required symbol tag (⇤, browser's) is checked for
the importing source area.
When several rules apply to the same symbol and source area, all of them must be
satisfied. And tags never expose: no rule makes an import possible that
the exposure chain did not already allow. A tag can only withhold a symbol
that is visible.
All modules in a check share one tag registry. Project-defined tags choose one of the two kinds; they cannot introduce new matching algorithms, implications, or exceptions. New owned exported bindings must carry all required-importer tags of their defining source area, including unexposed exports, new wrappers, type aliases, and resource bindings. Required-symbol tags are assigned explicitly. Registering a tag alone does not classify any source or export.
Unknown tags, conflicting definitions, and per-module overrides are errors.
Only testing has a reserved structural role: it must retain its required-importer
kind, its connection to src/tests/, and production-to-testing isolation.
The other default tags use the same matching and propagation as project tags.
Type imports
The diagrams distinguish ordinary value imports from type-only imports.
A value import can put another module's code in your bundle. TypeScript's other form,
import type, is erased at compile time and carries no runtime dependency,
and it gets one refinement of its own: a symbol is
type-available in a source area when that
area's files may import it as a type. The diagrams show each module's ordinary
source profile.
The two verdicts can only differ one way - available always implies type-available, never the reverse (the specification decides "no rule is type-only"). So the full chain is:
available ⊆ type-available ⊆ visible
Whether a rule exempts type imports follows from what its claim is about:
browser's claim is about runtime - "this closure is safe to execute in a browser." An erased import executes nothing, so the rule covers value imports only.queryDbis therefore struck inui(not available) yet still type-available there - the unstruck∗after its struck name is this fact:uimay reference its signature -runQuery: typeof queryDb- without ever bundling the implementation.testing's claim is about coupling - "this is not part of the production contract." A production function typed in terms of a test fake violates that at the type level just as thoroughly as at runtime, so the rule covers both forms:resetOrderStoreinbillingis not even type-available.uialso restricts coupling: non-UI source cannot depend on a UI contract through either value or type-only imports.dispatchrestricts coupling too: transport-independent source cannot import dispatch-tagged contracts in either form.
An interface or type alias imported without a type modifier also receives
a type-only availability check when its resolved original has no runtime
binding. It must still pass exposure, required-importer tags, and testing-source
isolation. A runtime-bearing class or function needs an explicit type-only
import to be exempt from the browser rule; later type-only usage is insufficient.
These checks do not prohibit symbol-free initialization imports across owners. Known testing-source restrictions still apply. Source checking checks explicit namespace and lazy-import selections and reports unsupported portions as nonblocking coverage notes by default; definite violations still fail.
The diagrams give all of this exactly one character: the strike means "not
available", and an unstruck ∗ after a struck name marks the middle band of
the chain - type-available but not available. Everything else about type
imports lives here in prose, where it can be explained rather than drawn.
Where the precision lives
This page teaches the tag rules. The complete, authoritative rules, including
tag composition and type-only imports, are in Cross-Module Importability
Principles at docs/model/cross-module-importability.principles.md in the
repository. Its companion, docs/model/glossary.md, defines the vocabulary
reproduced in the glossary. The website must conform to these two
internal documents.