Limitations
What isn't done yet, or is out of scope.
Writing and saving code
- TypeScript 7.1 is a beta. tcm checks code with
typescript@7.1.0-beta(the compiler). tcm importskips test files (test definitions come in with--tests; importing), and refusesimport x = require(), decorators, and top-level statements other than ones setting up a constant.import.metais location-dependent: code runs from.tcm/cache, so tcm warns about it.- Two functions declared alike can't be saved in one file (
findfrom two namespaces: a function's name is part of its hash).tcm editputs the second in a file of its own.tcm patch --patchessaves them one after the other in one history entry; if the later one fails to typecheck, the earlier stays saved. - A change that would put two functions (or classes) declared alike in one cycle is refused, naming which.
- "Used before its declaration" isn't checked across what a save brings along (the rest of its cycles, code using what it changes). Two constants whose values use each other across the two aren't caught there.
- Unused imports are found by name. Saving removes an import the file doesn't mention; one mentioned anywhere (a comment, a string) stays. What's stored isn't affected.
- Narrowing another definition's constant doesn't reach closures.
buffer !== undefined ? (s) => buffer.from(s) : …narrows inside the arrow only ifbufferis in the same module; a re-check (tcm verify, an edit) says it's possibly undefined. Importing the file checks it as written. - A statement setting up a constant runs with its declaration (hashing): a
constant declared between the two that reads the first (
const first = codes[0]) sees it already set up. - Augmentations aren't dependencies (hashing): changing a
declare globalordeclare moduledoesn't put code relying on it intcm todo; the next check (an edit,tcm verify) finds it. Adeclare globalwritten into a scratch file under another namespace is checked beside the stored one. In a scratch file, code doesn't see what its owndeclare module "#codebase/…"adds until it's saved. - A dependent left on a version nothing names: if a save ever does this, it says so at once
(
! … now uses a version of … nothing names);tcm editandtcm updateof that dependent fix it. - Definitions open read-only in the editor (
tcm:documents); edit through a scratch file.
Names and identity
- A function keeps the name it was written with at runtime.
.nameis part of its hash, sotcm mvchanges the name code calls it by, not.name(hashing). - The same code added at two paths is two definitions (hashing). Use
tcm aliasfor one definition under two names. - A type with several names shows the name a signature was saved with (an alias, or one type saved under two names), as does a type nested too deep for its shape that the code doesn't mention. Other signatures use current names (finding code).
- A function named
test…is a test, whatever it takes. An imported library's helper (Effect'sSemVerRange.testBound) runs, and fails, undertcm testof its namespace; name the tests to run (tcm test demo). - A kept constant's value is a copy: comparing it by identity with an object another definition's function returns gives a different object.
Mirrors and pages
- Mirrors don't check files near them. A rename reaches files outside the mirror that use the name, but only the
mirror's files are checked afterwards.
const a = 1, b = 2is edited by hand. Files changed outside tcm are imported before each command, but not bytcm watchor the language server while they run. - Mirrored code that imports a file works only through tcm: it imports the page or image from
#codebase/…(mirrors). - Links between pages are kept as written, not by hash (pages). They're checked when a page is
saved, and
tcm mvandtcm rmfollow them. A mirror directory's name reads as a namespace (my-docsismy_docs), so a new page goes back to a directory spelled that way.
Packages
- Choosing a package's version asks the npm registry (
npm view) on a codebase's first install of it, a suite's own codebase included. A registry that doesn't answer is retried twice; offline, nothing new installs. tcm's own tools (oxlint, oxfmt, the React Compiler, Vitest, Playwright) come with it. - Transitive npm dependencies aren't part of a hash; only the direct package's range is. Within a range tcm trusts semver: moving the lock re-checks types, not behaviour.
- An app can't mix ranges of a package others are built against (packages): React 18 and 19 components in one app, say.
- Re-exports of a package are followed one module deep (importing):
export * from "pkg"is followed, a barrel re-exporting that module isn't. Import from the module that re-exports the package. - What code reaches outside is worked out from the code, not from runs (packages):
a function that could read the clock counts, called or not. A package export that can't be followed (a computed
import(),eval) is taken as impure, and a test using one is cached as if it reached only what its code shows. Which tests reach a package's effects isn't worked out yet. - A package's browser build is trusted about Node's globals: an export importing Node's modules needs the
server, but one using
Bufferorglobalis assumed to guard or polyfill them. - Packed packages run outside tcm (publishing): server functions called
from a browser, tables and jobs have no server there. Packages code finds at runtime by name go in with
--with. - Packages that use each other depend on releases: one using another's internals or unreleased changes can't be packed until they're released.
- Semver checks see types, not behaviour. Same API, changed code is a patch; release higher when behaviour deserves it. Generic types and functions whose type parameters changed are compared as written, so some compatible changes count as breaking.
Tests
- Cached results can go stale when they depend on the clock, the network or files a test didn't make;
--freshre-runs them. Tests reaching files, the environment or other processes are cached per platform; environment variables and disk contents aren't part of any key. Only passes are reused. - A suite's cached result knows only the codebase code it ran. Files it reads, code it imports from source and
the outside world can change without it re-running:
tcm test --freshruns it. A suite that runs none of the codebase always runs. Not seen: a constant that loads quietly and nothing the suite ran refers to (even if a change makes it throw); a built-in changed by a call (Object.assign(Math, …)). A constant whose computation calls a function written in it counts whenever it loads. Against a--binbuild packed before a change, a suite runs every time. - The machine's budget counts work, not cost (testing): a suite that
starts a browser takes one slot, as a quick one does. Single commands (
tcm check,tcm add) take none. An older tcm (--build) ignores the budget. A slot left by a dead process whose pid was reused stays taken until that process ends. A run with a smallerTCM_SLOTSthan the rest keeps only the slots they share.
Apps and React
- What code writes 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). Other ways (f + "", through a variable) count as running it. - The react library needs React 19 (React), the React tcm provides.
- Server code runs in React's server components environment (React): it
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, so module state isn't shared between server rendering and server code. A package component reaching React only throughimport * as Reactisn't found to be a browser component, 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)). - A stylesheet the browser's code uses other than through
<Stylesheet css={…}>is in its code too, and still linked (React). One only server components render is written into the page. - React Compiler is oxc's port, a work in progress. If it gets a component wrong,
"use no memo"leaves it as written. Compiled modules lose their source maps. - What renders the same is found from the code (React): a server component using an npm package other than React is fetched each time, as is a page's shell when the app's browser code reads the clock or randomness anywhere. Kept trees are in each server's memory.
- A page read ahead doesn't set cookies: a server component setting one sets it when the page is followed.
- A static site has only the pages its app links to (static sites), found from
/, each rendered for its path, not its query. A page only another site links to, or whose content is in its query (?list=today), renders in the browser from404.html, or doesn't match what it hydrates. It can't call server functions. - A PWA's service worker can't tell app pages from plain HTTP routes (PWAs):
in an app that calls no server, a
tcm start --httproute under the app's path isn't reached once it's installed. - 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.
- Transactions lock per process (data).
Deploying
- Only
tcm startserves under a path (--base), andtcm build --staticbuilds for one;tcm devserves at the root. - Skew protection is one server's. Earlier builds are kept and served by the server that built them; servers behind a load balancer each keep their own.
- Build leases are by process id (deploying): processes sharing a codebase must share a machine. A lease left by a dead process whose pid was reused 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.
- Browsers without compression dictionaries get a changed chunk whole (deploying).
- Traces cover what tcm answers and runs (traces): calls, renders, pages, navigations,
routes, jobs and statements, not every function (
tcm dev --trace-callsadds those on the server, except in a page's own server rendering). A page's call is under its view or navigation, not the click. Each server keeps only its own spans;tcm tracesreads this machine's. OTLP is JSON over HTTP, not gRPC. A process that ends abruptly loses its last second of spans.
History, branches and sync
- History syncs whole: a push or pull sends every definition the branch's history names, each re-verified on
arrival, so a first sync of a long history is slow. Squashed landings add nothing; a merge without
--squashbrings every step. - A branch on a server can hold code that doesn't typecheck (sync):
receiving checks hashes, not types. Merging a contribution typechecks it, but pulling that branch takes it as it
is (
tcm verifyreports it). - Changing a message after a push changes only this codebase's (messages): messages are in no hash, and sync sends an entry only to a codebase that lacks it.
- Comments and docs merge a definition at a time (history): where both sides changed one definition's comments, this branch's win; a comment change on one side and a code change on the other merges as text, dropped where it overlaps. The merge names each to redo by hand.
- A conflict's sides are known only for conflicts made since tcm kept them, and travel only between tcms that
carry them. Without them
tcm todolists a conflict unlabelled,tcm resolve --oursasks for a hash, andtcm editmerges the two whole. A name holding more than two definitions shows the first two; saving replaces all. - Conflicts edited together share one namespace line; the others say where they go (
// tcm: path). A page is edited on its own. - A contribution merged as text is previewed before typechecking; merging it tries the merge on a branch of its own and refuses it if it doesn't typecheck.
- A merge waiting apart is stepped into by commands that read or change code (
edit,update,todo,view, …; history).tcm watch,tcm devand the language server see the branch as it was until the merge is made. - Reading another branch (
--branch) reads its names, code, history and todo, but a page's examples (tcm docs) and type queries (tcm find ':…') are checked against the modules already written, so they may not check.tcm statusnames that branch's unsaved scratch files without checking them. - A server has one maintainer (contributors): no other roles. Contributors'
tokens are managed by hand (
tcm user add,rm), with no sign-in or expiry. What the server's own token signs is what the client says ($TCM_AUTHORor the OS user), unchecked. Merging on the server briefly makes the target its current branch. - An older tcm can't read a codebase a newer one moved on (hashing):
tcm --build <older>refuses one migrated to a newer layout or format, and one from before compressed texts (files) fails on one this tcm has written ortcm compacted. Sync is unaffected.
Libraries and registries
- A library upgrade can't follow a definition the new release removed or renamed (libraries): matched by name, one renamed and changed counts as removed, and code using it 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.
- A registry's search results are its word (registry):
tcm lib searchasks the one given, orhttps://tcm.codes/registry; each library is still fetched by hash.tcm lib addchecks the announcement, andtcm libthe reviews, advisories and yanks (what people say), against their writers' signed repositories. Which versions exist is as the author's PDS lists them, so a PDS can leave one out. An advisory or yank is reported, not enforced. Only 50 pages of a listing are read. - Signing in to a registry only stars (registry); reviews and advisories are
tcm lib attest. The DID a browser gives is taken on its word, only for what to show. "Libraries I use" reads only codebases at a public http(s) address with no token, and only their main branch. Links from its code pages to history and releases go nowhere. - A registry checks an address before connecting, not as it connects (what a registry asks): DNS rebinding could still reach a local address. Run it where the private network holds nothing it shouldn't reach, or behind an egress proxy.
- Signing in in a browser needs one on the same machine (releases): OAuth
returns to 127.0.0.1. On CI, over ssh or from a phone, use an app password (
TCM_ATPROTO_APP_PASSWORD). It can write any record in the account and is kept in~/.config/tcm/atproto.json; revoke it in the account's settings. - A merge conflict inside a library can't be settled name by name:
tcm resolverefuses its names.
The web UI and studio
- The web UI shows the default branch's code. Other branches appear in its history; browsing their code isn't there yet. A definition's history is found among the latest 300 entries.
- The web UI's history is made ahead only for its first ten pages (the web UI): older pages are worked out when first asked for (about half a second each on tcm's own), and kept in memory only.
- A web UI page has 30 seconds to begin (the web UI): past that it gets a 503 and reloads itself, quicker the second time. A UI silent for a minute is restarted, losing what it kept.
- The web UI's first start builds it, installing React from npm: a server with no network can't start it. Nor can the studio.
- The studio edits this machine's data (the studio): a saved row is checked as a table
write is, but no
migratestep runs on it. A notebook element that can't run in the browser shows its first server render. A deployed server's traces are the copy last fetched (a week at most), not live.
Editors and agents
- The VS Code extension is a thin client for
tcm lsp. There's no Zed extension yet. - Sessions land untested together if another landed first (agents):
tcm_mergetests the session's branch withmainmerged in, then lands it squashed; ifmainmoved meanwhile, what lands is that clean merge, untested. - Only the user should land work, but
tcm_merge'sapprovedis the agent's word, and an agent running the CLI itself (TCM_BRANCH=main tcm merge …) isn't stopped. - A session is told apart by the directory its client starts from: two agents in one directory get separate
branches only while both run. A client that starts a new server more than a few seconds before stopping the old
one gets a new branch (
tcm_cli ["switch", "<old>"]goes back). A session's unlanded branch (or one whose server was killed) stays until landed or deleted (tcm branch <it> --delete). - The editor works on the codebase's own branch, not a session's; a session's views are its own.
- Background jobs are kept per branch (agents): after
tcm_cli ["switch", …],tcm_joblists the new branch's; earlier ones are in.tcm/sessions/<branch>/jobs/. Stopping a job stops its process group; something it started in a group of its own keeps running.