42. Limitations
What isn't done yet, or is deliberately out of scope:
- A type with several names keeps the name a signature was saved with. Signatures name the codebase's types as they're called now (finding code), but a type named twice (an alias, or the same type saved under two names) is shown as it was printed, as is one too deep in a type for its shape that its code doesn't mention.
- A history entry's message changed after it was sent stays changed where it was changed. A message is in no
hash, so pushing and pulling send an entry only to a codebase that doesn't have it: one that does keeps the
message it has, and
tcm describeafter a push changes only this codebase's (messages). - The VS Code extension is a thin client for
tcm lsp. The server is tested end to end; the extension's sidebar and commands are tested by hand. There's no Zed extension yet. - Definitions open read-only (
tcm:documents); editing goes through a scratch file. - Choosing a package's version asks the registry (
npm view): a codebase's first install of one (React, as an app first uses it, say), in an end-to-end test's own codebase too. tcm's own tools (oxlint, oxfmt, the React Compiler) and its test runner (Vitest, Playwright) come with it. A registry that doesn't answer is asked twice more, a moment apart; offline, nothing new can be installed. - Cached test results can go stale when they depend on the outside world (the clock, the network, files a
test didn't make);
--freshre-runs them. A test reaching files, the environment or other processes is cached per platform, and watch expressions that reach outside aren't cached, but environment variables and what's on disk aren't part of any key. Only passes are reused: a failure, a test definition's or a suite's, runs again next time, so one the environment or the test runner caused goes once that's fixed. - A save has once left a dependent on a version nothing names: saving several definitions together pointed
one that used them at an in-between version of one (re-derived, then replaced). The causes found since are fixed:
a merge left code taken from one side using that side's versions of what the other changed (a conflicted name's
other side's definition, re-derived before what it used was, too), and a definition
that couldn't follow a change (one to do) didn't catch up with later changes to what replaced it; and a save
re-deriving a definition that already used such a version reported it as new; and a version that came back (a
change undone hashes as the code before it did) went on to what replaced it before, which nothing names, so
tcm editandtcm patchof a todo item using it imported that by its hash (from a path a session's scratch file couldn't reach, so the cause was lost among the errors following from it); and a merge where both sides had made the same change (a squashed landing, and the branch it came from) left what used a definition each side only re-hashed on the branch's version of it, as merging re-derived that to the other side's. If it happens still, a save says so at once (! … now uses a version of … nothing names);tcm editandtcm updateof that dependent point it at the current one. tcm patch --patcheswith two functions declared alike (findfrom two namespaces: a function's name is part of its hash, and one file can't hold both) saves them one file after the other, in one history entry. The later one is checked once the earlier is saved, so if it doesn't typecheck, the earlier stays saved.tcm editwon't write two such into one scratch file at all: the second is edited in a file of its own, and saved apart.- A change that would put two functions declared alike in one cycle isn't saved (or classes): what a scratch
file brings along of a cycle it changes is written with it, as stored, and a function's name is part of its hash,
so one that would be written with another name (
findfrom two namespaces, or afindbeside one the file declares) refuses the save, saying which. - A scratch file saved with what it brings along (the rest of its cycles, what it imports that uses what it changes) isn't checked for "used before its declaration": those are written after its code, and the module that runs orders definitions by what they use. A constant whose value uses another whose value uses it, across the two, isn't caught there.
- What code reaches outside is worked out from the code, not from runs: what it could reach (a function that
reads the clock somewhere counts, called or not), through Node's modules and globals, and npm packages' code
(packages). A package export that can't be followed (a computed
import(),eval) is taken as unseen: it isn't pure, and a test that uses one is cached as if it reached nothing more than its own code shows. Which tests reach a package's effects isn't worked out yet: only pure constants, kept server components and placement use what's read. - Code written out as source is recognised by its form: a function used only as
${f},f.toString()orString(f)doesn't place the code writing it (placement). Its source reached another way (f + "",${(f)}, through a variable or a function of one's own) counts as running it. A function in the same cycle as the code writing it out counts too. - Unused imports are found by name, not by what the name means. Saving takes out an import whose name the file doesn't mention outside its imports; one it mentions anywhere (a comment, a string, a property named alike) stays, though the code doesn't use it. What's stored isn't affected: a definition's imports are what its code uses.
- A kept constant's value is a copy. Code that compares it by identity with an object another definition's function returns (one it doesn't export) sees a different object.
import.metais location-dependent: it's evaluated inside.tcm/cache, so tcm warns about it.tcm importskips test files, except test definitions with--tests(importing), and refusesimport x = require(), decorators and top-level statements other than those setting up a constant.- Augmentations aren't dependencies (hashing): code relying on what a
declare globalordeclare moduleadds doesn't name it, so changing the augmentation doesn't put that code intcm todo; the next check of it (an edit,tcm verify) finds it. A scratch file writing a new version of one leaves the stored one out of its check (found by its// tcm: namespacelines), so adeclare globalwritten into a scratch file under another namespace is checked beside the stored one. In a scratch file,declare module "#codebase/lib/schema"adds to the shim's re-export, which TypeScript doesn't merge: code in that same file doesn't see what it adds until it's saved. - A function named
test…is a test, whatever it takes. An imported library's own helper (Effect'sSemVerRange.testBound(self, version)) runs, and fails, undertcm testof its namespace; name the tests to run (tcm test demo). - Narrowing another definition's constant doesn't reach closures.
buffer !== undefined ? (s) => buffer.from(s) : …narrowsbufferin the arrow function only wherebufferis declared in the same module; a definition of its own is imported, and re-checking the code using it (tcm verify, an edit) saysbufferis possibly undefined. Importing the file is checked as written, so it imports. - A statement setting up a constant runs with its declaration (hashing):
a constant declared between the two whose value reads the first one (
const first = codes[0]) sees it set up, where the module it came from ran in order. - Mirrors don't check the files near them. A rename reaches files outside the mirror that use the name (scripts,
an editor extension), but only the mirror's files are checked against the codebase afterwards. A definition
declared alongside others in one statement (
const a = 1, b = 2) is edited by hand. Files changed outside tcm are imported before each command, but not bytcm watchor the language server while they run. - A suite's cached result only knows the code it ran from the codebase. Files it reads (fixtures, a project
it builds), code it imports from source rather than through the command, and the outside world can change
without it running again:
tcm test --freshruns it. A suite that runs nothing of the codebase always runs. A constant that loads quietly and that nothing the suite ran refers to doesn't count, even when a change makes computing it throw, which would stop the command loading:--freshfinds that, as does any test definition using it. One that changes a built-in by calling something (Object.assign(Math, …)) rather than assigning to it isn't seen doing so. One whose computation calls a function written in it (xs.map((x) => …)) counts whenever it loads, as coverage can't tell loading it from a later call. A suite run that executed code no name points at, which no named code it ran refers to (with--bin, a build packed before a change), isn't kept: against that build, it runs every time. - Mirrored code that imports a file works only through tcm. It's written importing the page or image from
#codebase/…, which the project'spackage.jsonmaps to the codebase's shim, so the codebase is in the project (mirrors). - Links between pages are kept as written, not by hash (pages): pages link to each other both
ways, and by hash that would make cycles. They're checked when a page is saved, and
tcm mvandtcm rmfollow them. A directory's name in a mirror reads as a namespace the way code's do (my-docsismy_docs), so a new page goes back to a directory spelled that way. - Adding the same code at two paths creates two definitions (lineage salts, see
hashing). To share one definition under two names, use
tcm alias. - TypeScript 7.1 is a beta. tcm checks code with
typescript@7.1.0-beta(the compiler). - Re-exporting a package is followed one module deep. Through a module with
export * from "pkg",export { x } from "pkg"orexport * as ns from "pkg", a use ofxhashes as if it importedxfrompkg(importing). A barrel re-exporting that module in turn (export * from "./reexports") isn't followed: import from the module that re-exports the package. - Transitive npm dependencies aren't part of a hash; only the direct package's compatible range is. Within a range, tcm trusts semver: moving the lock to a newer version re-checks types, not behaviour.
- A package's browser build is taken at its word about Node's globals: an export of it that imports Node's
modules needs the server, but one that uses
Bufferorglobalis taken to have guarded or filled them in. - Packed packages run outside tcm (publishing): what needs
tcm devortcm start(server functions called from a browser, tables, jobs) has no server there, and packages code finds at runtime by name rather than importing them go in with--with. - The react library works with React 19 (React), the React tcm provides; an app on another React can't use it.
- A stylesheet the browser's code uses other than through
<Stylesheet css={…}>is in its code too (React): given through a name of its own, say, or read as text. It's still linked to. One only server components render is written into the page instead. - Server code runs in React's server components environment (React):
with React's server build, so server code (a server function too) can't use what only the browser's React has
(
react-dom/serverto render an email, say). Code both sides use is loaded once by each: module state in it isn't shared between a page's server rendering and server code. A package's browser component is found from its code ("use client", or a hook it imports by name): one reaching React throughimport * as Reactand nothing else isn't, and renders on the server. A server function given to a browser component must be one the server component names (save,save.bind(null, id)), not one made as it renders (() => save(id)). - Only
tcm startserves under a path of a site (--base), andtcm build --staticbuilds for one;tcm devserves at the root. - A static site has the pages its app links to (static sites): they're found by following
links from
/, each rendered for its path, not its query, and its server components only for the props its pages give them. A page only another site links to, or one whose content is in its query (?list=today), renders in the browser from404.html, or doesn't match the page it hydrates. And it can't call server functions. - React Compiler is oxc's port, which its makers call a work in progress; a component it gets wrong renders as it
computes, so
"use no memo"in a component leaves it as written. Compiled modules lose their source maps: the browser's debugger shows them as compiled. - What's found to render the same is found from the code (React): a server component using an npm package other than React (a Markdown library, say) is fetched each time, as is a page's shell when the app's browser code reads the clock or randomness anywhere, even in an event handler. Kept trees and shells are each server's, in memory.
- The web UI's history is made ahead only for its first ten pages (the web UI): an older page is worked out when it's first asked for (about half a second for one of tcm's own, whose entries re-hash hundreds of definitions each), and what the UI keeps is in memory, so it starts over when the UI does.
- A page read before it's gone to doesn't set cookies: a server component that sets one as it renders sets it when the page is followed and rendered again, not when it was read ahead.
- A PWA's service worker can't tell an app's page from its plain HTTP routes (PWAs):
an app that calls no server opens every navigation under it from the device, so a route served with
tcm start --httpunder the app's path isn't reached by a browser that has the app installed. - A branch on a server can hold code that doesn't typecheck (sync):
receiving checks code is what it claims, not its types. tcm saves only code that typechecks, but a client can
post code by hand, and the server keeps it on the branch it was pushed to; merging a contribution typechecks what
it brings, so it doesn't reach the target that way, but pulling that branch takes it as it is (
tcm verifyon it reports it). - History syncs whole: pushing or pulling a branch sends every definition its history names, not just its
current ones, and each is re-verified on arrival, so a first sync of a long history takes a while. Sessions land
squashed, so their steps don't add to it; a branch merged without
--squashbrings in every step it took. - How code reads is merged a definition at a time, not as text (history): where both sides changed one definition's comments or doc, this branch's is kept; where one side changed a definition's comments and the other its code, the comment change is merged into the new code as text, and dropped only where that overlaps (or would change the code). The merge names each, to write again by hand. (Code both sides edited is merged as text.)
- A conflict's sides are known only for conflicts a merge (or revert) made since tcm kept them: they're kept
per branch in the codebase's settings, beside its history (so no hash changes), and travel with a push or pull
only between tcms that carry them (a server from before doesn't keep them). Without them
tcm todolists a conflict's definitions unlabelled,tcm resolve --oursasks for a hash, andtcm editmerges the two whole, one conflict from nothing. A conflict's three-way view merges its two sides; a name holding more definitions (a side that was itself conflicted) shows the first two, and saving replaces them all. - Conflicts edited together share one namespace line: the file is in the first name's namespace, and the others
say where they go (
// tcm: path). A page (a file kept as written) is edited on its own. - A contribution merged as text is tried first: its preview counts edits that are apart as merged, before typechecking them; merging it then tries the merge on a branch of its own, and refuses it if what was merged doesn't typecheck.
- The web UI shows the default branch's code. Other branches show in its history, and a contribution's changes are its branch's; browsing a branch's code isn't there yet. A definition's history is found among the latest 300 entries.
- The web UI's first start builds it, installing what it uses (React) from npm, as any app's first build does: a server with no network can't start it.
- A server has one maintainer. Contributors each have a token, which signs
what they write; the server's own token can do anything, and what it signs is what the client says
(
$TCM_AUTHORor the OS user), not checked. There are no other roles (no second maintainer, no reviewer who can merge), and a contributor's tokens are managed only by hand (tcm user add,rm): no sign-in, no expiry. Merging one on the server briefly makes its target the server's current branch. - Semver checks see types, not behaviour. A definition whose code changed with the same API is a patch; release at a higher version when a behaviour change deserves one. Generic types and generic functions whose type parameters changed are compared as written, so some compatible changes to them count as breaking.
- Packages that use each other depend on each other's releases; one that uses another's internals, or changes to it that aren't released yet, can't be packed until they are.
- Transactions lock per process (data).
- A library upgrade can't follow a definition the new release removed or renamed (libraries):
definitions are matched by name, so one renamed and changed counts as removed (renamed alone, it's the same code,
still there), and code using what the new release doesn't have keeps the library from moving until it's changed.
A release is found by name only where releases are (the author's codebase, or one that pulled it; a codebase that
only uses a library serves it by hash). Releases can be announced and added by their author's handle
(announced releases); a hub searches what's been
announced, and
tcm lib searchasks one, the hub it's given or tcm's own (https://tcm.codes/hub), and what it answers is that hub's word (each library it names is still fetched by hash).tcm lib addchecks the announcement it uses, andtcm libthe reviews, advisories and yanks it shows (what people say), against their writers' signed repositories; which versions there are is still as the author's PDS lists them, so a PDS can leave a version out (not change what one is). An advisory doesn't stop an add or an upgrade: it's said, as a yank is. Only 50 pages of a listing are read. - Signing in to a hub only stars (hub): reviews and advisories are written with
tcm lib attest. The DID a visitor's browser tells the hub is taken on its word, for what to show them (their follows, the libraries they use), which anyone could see anyway. "Libraries I use" reads a codebase only from a public http(s) address readable without a token, and not the libraries of a codebase's branches other than main. Its code pages are the web UI's, whose links to a codebase's history and releases go nowhere on a hub. - A hub checks an address before it asks it, not as it connects (what a hub asks): a name is resolved and refused if any of its addresses is private, then resolved again to connect, so a name whose DNS answers change between the two (DNS rebinding) could still reach a local address. Run a hub where its machine's private network holds nothing it shouldn't reach, or behind an egress proxy.
- Announcing signs in only in a browser on the same machine (releases):
OAuth comes back to 127.0.0.1, so a machine without a browser (a CI runner, a server over ssh) can't sign in, and
there's no app-password sign-in. A sign-in kept elsewhere can be copied (
~/.config/tcm/atproto.json) while its tokens last. - A merge conflict inside a library can't be settled name by name:
tcm resolverefuses its names, as it does any change to them. - A function keeps the name it was written with at runtime. Its
.nameis part of its hash, sotcm mvchanges the name code is called by, not.name(hashing). - Skew protection is one server's. Earlier builds are kept, and served, by the server that built them; several servers behind a load balancer each keep their own.
- Leases on builds are by process id (deploying): pruning keeps what a running build or
tcm startholds, so processes sharing a codebase must share a machine; a lease left by a process that died, whose pid a new process got, holds until that process ends. - Only test results and lint findings are shared. Production builds, watch values and kept constants are computed on each machine; a kept constant's value is code, which sync doesn't take on trust.
- An app can't mix ranges of a package others are built against (packages): React 18 and 19 components in one app, say.
- Jobs and schedules (jobs): a job's function must be a definition, and its arguments plain data; a new schedule doesn't fire on its first deploy.
- Browsers without compression dictionaries get a changed chunk whole, not as the difference (deploying).
- Sessions (agents):
tcm_mergetests the session's branch withmainmerged in, then merges it intomainsquashed; if another session landed in between, what lands is that merge (made only if it's clean), untested together. - A merge waiting apart is stepped into by the commands that read or change code (
edit,update,resolve,todo,view,test, …) (history): what runs on (tcm watch,tcm dev, the language server) sees the branch as it was until the merge is made. The editor (the VS Code extension's language server) works on the codebase's own branch: a session's views are its own, but no editor follows a session by itself. A generated module's source map is the last branch's to render it, so branches running the same code in turn render it again each time. A session's branch holding work that never landed stays until it's landed or removed (tcm branch <it> --delete); one stopped by force (its server killed) stays too, for a server started again from the same directory to go on with. A session is the agent's by the directory its client starts the server from: two agents in one directory are told apart only while both run (the second gets a branch of its own), and a client that starts its new server more than a few seconds before stopping the old one gets a new branch (tcm_cli ["switch", "<old>"]goes back, once the old one's stopped). Servers running a tcm from before branches were claimed in a transaction can still clear away a branch a server is starting on: that server makes it again. Only the user should land work:tcm_mergelands nothing withoutapproved, but that's the agent's word that the user said so, and an agent running the CLI itself (TCM_BRANCH=main tcm merge …) isn't stopped. - The machine's budget (testing) counts work, not what each costs: a
suite that starts a browser and a build takes one slot, as a quick one or a checker process does, so the default
is set for a typical mix (measured, each keeps about a core busy). What a tcm does for one command (
tcm check,tcm add, an agent's edits) takes none: it's quick, and someone is waiting on it. Only tcms that have it take slots: a tcm from before it (an older build,--build) runs as many suites as it likes beside them, and one from before builds, checks and verifies took them runs those outside it. A slot is held by a pid: one left by a process that died, whose pid a new process got, stays taken until that process ends. A run given a smaller budget than the rest (TCM_SLOTS) keeps only the slots it shares with them to it. Inside a suite, what it starts runs in its slot, a suite at a time, however much it starts. - Verifies side by side (developing): a branch's verify whose
package is packed the same as the known-good one tests the known-good install where it is, so main's verify
making another tcm the known-good one meanwhile changes what the rest of its tests run. Verifies from before
each branch had its own place hold
dist/verify.lockfor their whole run, and main's waits for them to make its tcm the known-good one.pnpm examples:checkkeeps one codebase for each example (dist/examples/<name>-check): two at once check into the same one. - Background jobs (agents) are kept in the directory of the branch the session was
on when it started them: after
tcm_cli ["switch", …],tcm_joblists the new branch's, and an earlier one's are in its directory (.tcm/sessions/<branch>/jobs/). A job runs the tcm that started it, from where it's installed: one replaced under a running job (a new known-good build) is the job's to cope with. Stopping a job stops its process group; something it started in a group of its own runs on. - Reading another branch (
--branch, a tool'sbranch) reads its names, code, text versions, history and todo, but a page's examples (tcm docs) and a type query (tcm find ':…') are checked against the#codebasemodules already written (they follow the branches tcm works on, and reading one writes nothing), so one using what only the branch read has may not check.tcm statusnames that branch's session's unsaved scratch files without checking them.