Limitations — tcm-ts

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 describe after 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); --fresh re-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 edit and tcm patch of 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 edit and tcm update of that dependent point it at the current one.
  • tcm patch --patches with two functions declared alike (find from 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 edit won'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 (find from two namespaces, or a find beside 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() or String(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.meta is location-dependent: it's evaluated inside .tcm/cache, so tcm warns about it.
  • tcm import skips test files, except test definitions with --tests (importing), and refuses import 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 global or declare module adds doesn't name it, so changing the augmentation doesn't put that code in tcm 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: namespace lines), so a declare global written 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's SemVerRange.testBound(self, version)) runs, and fails, under tcm test of 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) : … narrows buffer in the arrow function only where buffer is declared in the same module; a definition of its own is imported, and re-checking the code using it (tcm verify, an edit) says buffer is 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 by tcm watch or 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 --fresh runs 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: --fresh finds 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's package.json maps 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 mv and tcm rm follow them. A directory's name in a mirror reads as a namespace the way code's do (my-docs is my_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" or export * as ns from "pkg", a use of x hashes as if it imported x from pkg (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 Buffer or global is taken to have guarded or filled them in.
  • Packed packages run outside tcm (publishing): what needs tcm dev or tcm 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/server to 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 through import * as React and 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 start serves under a path of a site (--base), and tcm build --static builds for one; tcm dev serves 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 from 404.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 --http under 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 verify on 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 --squash brings 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 todo lists a conflict's definitions unlabelled, tcm resolve --ours asks for a hash, and tcm edit merges 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_AUTHOR or 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 search asks 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 add checks the announcement it uses, and tcm lib the 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 resolve refuses its names, as it does any change to them.
  • A function keeps the name it was written with at runtime. Its .name is part of its hash, so tcm mv changes 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 start holds, 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_merge tests the session's branch with main merged in, then merges it into main squashed; 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_merge lands nothing without approved, 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.lock for their whole run, and main's waits for them to make its tcm the known-good one. pnpm examples:check keeps 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_job lists 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's branch) 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 #codebase modules 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 status names that branch's session's unsaved scratch files without checking them.

built from c482a14e