Agent docs

Toolkit

The framework-free utility package: what is in it, what each module belongs with, and who uses it.
@versable-git/toolkit

Framework-free utilities shared across every app and package here
Zero React, zero Next. Anything touching JSX or a hook lives in the kit instead.

TypeScript vitest React
modules exports publish



../ui · ../../docs/app-patterns · ../../docs/plan/archive/02-toolkit.md


What is this?#

Nineteen small TypeScript modules (dates, text, numbers, urls, results, sampling, crypto tokens) with no React and no view layer, published as @versable-git/toolkit 0.1.0 and consumed by nothing yet. They were ported from enhancement-product/frontend/src/utils/ with four bugs fixed on the way in. Use one when an app needs exactly that helper; import it by subpath; and if it is rough, say so on the board, because no consumer has worn these edges smooth.

The toolkit
StackTypeScript only. No React, no Next, no JSX, by design
Ships19 modules, 20 subpath exports plus a barrel
Testsvitest, one test file per module, 19 in total
PublishedTo GitHub Packages on a version bump pushed to main (publish-kit workflow); 0.1.0 is the first
Consumed byNothing yet. Zero imports outside this package
OriginA port, with four known bugs fixed as part of it

Adoption is zero. Nothing in walmart-mvp, speedway, apps/playground or packages/ui imports this package today. That is stated plainly rather than left for someone to discover, because a package existing is not evidence it is the proven path. If you are the first caller, expect to find rough edges no consumer has hit yet.


Architecture#

The layering rule is the whole point, and it runs one direction only.

┌──────────────────────────────────────────────────────────┐
│ apps walmart-mvp · speedway · apps/playground │
└───────────────────────┬──────────────────────────────────┘
│ may import both
┌───────────────┴───────────────┐
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ @versable-git/ui │ ───────▶ │ @versable-git/ │
│ React components │ may │ toolkit │
│ hooks, JSX │ import │ pure functions │
└────────────────────┘ └────────────────────┘
✗ never imports upward

A module here can be used by a server, a worker, an edge runtime or a browser. The moment something needs a React hook it stops being a toolkit concern and becomes a kit concern, which is why text.tsx became text.ts on the port and useEventListener stayed behind.


Use#

import { listToMap } from "@versable-git/toolkit";
import { generateCryptoToken } from "@versable-git/toolkit/random.node";
import { createUUID } from "@versable-git/toolkit/random.web";

Everything is available from the barrel and from its own subpath, with one deliberate exception below.

The runtime-suffix convention#

random.node.ts and random.web.ts are NOT re-exported from the barrel. Import them directly, as above. This keeps a browser bundle from ever pulling in node:crypto, and a server bundle from pulling in the web crypto path.


What is in it#

ModuleWhat it holdsBelongs withUsed by
iterlist and collection helpers, listToMap, uniqueKeyValsnot yet mappednone yet
objectobject shaping and parseSafenot yet mappednone yet
string / textstring predicates, RegexMatch, line splittingpackages/ui/docs/stat-tile.md, /patterns/dashboard-stats (count copy is where the pluralize sites concentrate)none yet
numbernumeric guards, clampNumbernot yet mappednone yet
datedate formatting and comparison, frontend-agnosticpackages/ui/docs/timestamp.md (the same relative-time ladder, solved twice: packages/ui/src/timestamp/timestamp.tsx:17 against src/date.ts:88)none yet
urlthe QueryParams key registry and the FilterFor factorynot yet mappednone yet
file / downloadsize thresholds, human-readable sizes, blob downloadpackages/ui/docs/dropzone.md (the blob-download half of the upload and export round trip)none yet
randomids and tokens, plus the two runtime-specific entry pointsnot yet mappednone yet
eventa framework-free event busnot yet mappednone yet
promiseasync helpersnot yet mappednone yet
resulterror shaping, getApiError, throwIfErrorAsyncpackages/ui/docs/alert.md, /patterns/confirm-and-report (error-to-display-string is what both surfaces render)none yet
validation / securityinput checks and sanitisationnot yet mappednone yet
samplingrow sampling for previewsnot yet mappednone yet
typesshared type aliases, none of them framework-couplednot yet mappednone yet

Belongs with names the contract doc or pattern route a module serves, and each named doc links back here; a pair is written only where a real consumer or a measured demand exists (docs/plan/62-toolkit-proposals-design.md §4), so "not yet mapped" is honest rather than missing. Used by is the call site outside this package, and stays "none yet" until a real call site imports the module. Helpers that might belong here are filed in PROPOSALS.md.


Known issues fixed on port#

Each has a regression test named after the bug.

  • B1 object.ts's parseSafe had a comma-operator bug that always returned the literal 2 instead of the parsed JSON value. Fixed; see object.test.ts.
  • B8 number.ts's clampNumber defaulted max to 5000, silently capping any generic numeric value passed through it. Default max is now Infinity; callers pass an explicit ceiling when one applies.
  • B9 file.ts's FileSizeThresholds.medium was 100 * Mb (100MB) despite being documented and named as 100KB. Fixed to 100 * Kb.
  • B10 string.ts's RegexMatch.url/uuid/dateISO only matched JSON-stringified (double-quoted) input, so testing a raw string, the normal case, always failed silently. The patterns no longer require surrounding quotes.

What was deliberately not ported#

  • React/Next coupling dropped wholesale. text.tsx became text.ts: the file no longer contains JSX, so the extension changed too. splitLines no longer defaults to rendering a <div> per line; it defaults to returning trimmed string[], with an optional transform callback for a caller that wants to map lines to something else (including JSX, in @versable-git/ui). event.ts ports the event bus only; useEventListener (the React hook) belongs in ui's hooks. types.ts drops PageProps/PagePropsV2 (ReactNode/React.Attributes), SetState (Dispatch/SetStateAction), SetAtom (Jotai-specific), and the Next-App-Router IdUrlParam* family, all framework-coupled types with no place in a zero-React package.
  • result.ts's getApiError no longer imports axios. The old code checked error instanceof AxiosError; this package detects the same shape structurally (error.isAxiosError === true + error.response.data) instead, so the toolkit does not carry an axios dependency ahead of query-kit's fetch-vs-axios decision (02-toolkit.md §2.1).
  • result.ts drops the sync throwIfError. Its mixed sync/async callback handling let a thrown value race past its own .then(); the ledger calls this out as a footgun. Only the async form, throwIfErrorAsync, is ported.
  • url.ts's QueryParams registry is trimmed to generic keys (Tab, Search, Page, Action, SSR, CallbackUrl, Email) plus the FilterFor factory. Keys tied to the old app's domain (JobId, DemoJobType, TemplateId, NewTemplateType, ShowContextColDropdown, ShowPreview, Legacy, Allowed) and the AppActionKeys registry are dropped. The pattern, a single named registry of query-param keys, is what is worth keeping; each app grows its own keys the same way. QArgs (options for the old useQueryParams/useQParam hooks) is dropped too, since those hooks are not being ported: qsync is the one URL-state system going forward.
  • date.ts drops getPyDate and getCurrentDateForDb. Both emulate a Python/Mongo-side date format and belong to the backend boundary, not a frontend-agnostic date util.
  • file.ts and sampling.ts lost their project-coupled imports. file.ts no longer imports a human-readable-size helper from an app component; it has its own small getHumanReadableFileSize. sampling.ts no longer imports DataRow from the app's pipeline types (redefined locally as Record<string, string | number | boolean | null>) or the Next-specific faker.client.ts wrapper (replaced with a plain dynamic import("@faker-js/faker/locale/en")).
  • download.ts's downloadDataAsFile now builds a Blob plus object URL instead of a data: URI, removing the old approach's size ceiling and percent-encoding overhead.
  • iter.ts keeps the lodash-es/get dependency. If a consumer needs this module on an edge runtime that cannot bundle lodash, replace get() in listToMap/uniqueKeyVals with a hand-rolled dot-path resolver.

Pending decision, flagged rather than resolved#

Two modules (event.ts, promise.ts) read process.env.NODE_ENV for a tiny internal dev-mode flag. No project-wide env-access convention exists in this monorepo. Given the footprint, one private unexported one-liner in two files, this port kept it inline rather than introducing a central config module for a single boolean. Revisit once a real env-access convention is decided.


Developing#

pnpm typecheck # here, or from the root for every package
pnpm test # vitest, one file per module

The regression tests are named after the bugs above, so a failure tells you which ported defect came back rather than only which function broke.


Documentation#

WhereWhat it answers
docs/plan/archive/02-toolkit.mdthe original port spec, including the qsync and query-kit plans this package was carved from
packages/ui/README.mdthe layer above, where anything React-shaped belongs instead
docs/app-patterns/how an app is built and documented on top of both packages
PROPOSALS.mdthe backlog of helpers that might belong here: six lines to file, processed as a group
docs/plan/43-toolkit-rca.mdwhy nothing consumes this package yet, and the publish step that unblocks it

Repo Structure#

packages/toolkit/
├── src/
│ ├── index.ts · the barrel, minus the runtime-suffixed pair
│ ├── random.node.ts · node:crypto path, subpath import only
│ ├── random.web.ts · web crypto path, subpath import only
│ └── *.ts · one module per concern, each with a *.test.ts
├── package.json · 20 subpath exports, publishes on a version bump
└── vitest.config.ts · test runner config
@versable-git/ui · reference, canon, and method, read in place