cwip
v4.13.1
Published
Framework-free utility layer for the fleet — the bottom of cwip -> cursedbelt -> apps
Readme
cwip
A layered TypeScript utility toolbox: a zero-dependency, browser-safe core, with
opt-in subpaths for Node, databases, spreadsheets, media models, and a Bun test
toolkit. ESM-only and tree-shakeable ("sideEffects": false), so importing one function
ships only that function's code.
Working on (or with an agent in) this package? AGENTS.md is the dense, LLM-oriented map of every export — contributor/dev guidance that lives in the repo but does not ship in the npm tarball (only
dist/is published).
Installation
npm i cwip # or: bun add cwipThe core (cwip, cwip/node) has no required runtime dependencies. A few subpaths
declare an optional peer dependency that you install only if you use that subpath
(see the table). Peers are loaded with a dynamic import when the function runs and throw
a clear "install X" error if missing — importing the subpath never pulls the peer in.
Entry points
The package is split into subpaths by runtime capability and dependency surface, so a
browser consumer of cwip never resolves Node-, Bun-, or peer-only code. Import the
narrowest subpath — never a deep dist/... path.
| Import | Runtime | Peer dep | Contains |
| --- | --- | --- | --- |
| cwip | browser · Node · Bun | none | Deliberately empty since 4.0.0 — CWIP_PACKAGE_NAME and nothing else. It used to be the pre-2.0 library: one barrel re-exporting 23 modules and 616 symbols, of which the whole machine imported two (clamp, formatDuration — both available below). There is nothing to import here; pick the subpath. |
| cwip/audit | browser · Node · Bun | none | Framework-free append-only audit-log primitives shared by cursedbelt (SQLite-backed sink) and other hosts: AuditEntryInput/AuditRecord/AuditSink types (per-namespace + targetId version history, insert/update/delete) and createMemoryAuditSink — a zero-config in-process default; swap in a persistent sink (e.g. cursedbelt's createSqliteAuditSink) for real history across restarts. |
| cwip/calendar | browser · Node · Bun | none | Pure calendar engine (epoch-ms + LOCAL day keys, DST-safe wall-clock math): rule-based recurrence expansion (expandEvent, expandEvents, groupByDay, recurrenceSummary), local-time date helpers (dayKeyOf, dayStartMs/dayEndMs, monthMatrix, toLocalInput/fromLocalInput, fmt*), and a computed US-holiday overlay (holidaysOfYear, holidaysInRange). Zero UI/schema deps — the ONE model + expansion algorithm the calendar plugin, host jobs, and apps share. |
| cwip/file-kind | browser · Node · Bun | none | "What kind of file is this?", one answer for the server (classify at write time), the ingest pipeline (pick a text extractor) and the viewer UI (pick a renderer + icon): FileCategory (a deliberately FINER set than a media library's — a spreadsheet, a PDF and a code file are all "doc" to a coarser scheme), extensionOf, mimeForFilename, fileCategory, isTextCategory. Pure ext/mime knowledge, no I/O. Import the subpath, not cwip/string, from UI code — the barrel pulls in html-to-text, diffing, glob matching and token estimation, which measurably pushed a consuming app's eager bundle past its byte budget. cursedbelt/server/storage's mimeByExt/classifyFileType delegate here. |
| cwip/clipboard | browser · Node · Bun | none | Paste hygiene for combining sources into one note: sanitizeClipboardText strips the invisible cargo (BOM, zero-width space/joiners, C0 controls, PDF form feeds, CR halves) and NFC-normalizes so the same letter from two sources matches in search; tsvToMarkdownTable turns a spreadsheet selection into a table. 🔴 It never touches the AUTHOR'S characters — smart quotes, em dashes, accents, emoji, CJK and RTL (including the load-bearing bidi controls) all survive; a sanitizer that straightens quotes is corrupting the source. Own subpath for the same bundle reason as cwip/file-kind. |
| cwip/error | browser · Node · Bun | none | AppError + the error-hook registry. |
| cwip/timing | browser · Node · Bun | none | createThrottle/createDebounce (leading+trailing, cancel/flush) over an injectable TimerScheduler — fake clock in tests, rAF-able in prod, timers unref'd. The timing seam event/plugin brokers build on. |
| cwip/events | browser · Node · Bun | none | createEventBus<T> — minimal typed pub/sub (snapshot-safe emit, subscribe/clear) — createLifecycle (typed pre/post hook registry for before-commit + deferred after-commit side-effects) — and createSequenceMatcher (KMP-correct incremental token-sequence matcher with injectable timestamps and optional maxDelayMs/ignoreCase; drives Konami-style useKeySequence in cursedbelt). |
| cwip/object | browser · Node · Bun | none | The object toolkit (pick/omit/assocPath/mergeObjectsDeep/jsonClone/safeStringify, …) + produce — an immer-lite immutable recipe update over jsonClone, the one tested home for store patchData-style updates. |
| cwip/node | Node · Bun | none¹ | Filesystem/path/dir helpers, env + JSON config loaders, a porcelain-free git toolkit, node:crypto secret box (encryptSecret/deriveKey), obfuscate, worker pool, graceful-shutdown manager, process crash handlers, runWithTimeout (spawn-with-timeout, SIGTERM→SIGKILL), shell/dns helpers, PDF text extraction. |
| cwip/guard | Node · Bun | none | GuardEngine's storage-side security primitives. Key custody (the zeroization law): SecureKey holds key material ONLY in a native byte allocation — never a string, which cannot be zeroized and lingers in RAM until GC overwrites it — refuses use after destroy() (.fill(0)), and redacts itself from toString/JSON.stringify/util.inspect; createSecureKeyRing zeroizes every key in reverse-registration order BEFORE any teardown hook runs; readSecretBytes pulls a passphrase out of a request stream into a buffer (never await request.text()). The single encryption primitive: SecretEnvelope (v1, aes-256-gcm, argon2id params pinned per envelope, AAD = ${appId}:${key} so a relocated envelope fails authentication) with createSecretEnvelopeManager (unlock/lock/TTL auto-lock, seal/withOpened, openWithPassphrase/reseal for rotation) — byte-oriented throughout, with no openText() by design. The argon2id KDF is an injected seam (setArgon2idDeriver) since cwip ships no dependencies. Row integrity: createRowSigner HMACs a canonical, type-tagged, length-prefixed serialization of secrets/ports/meter_events rows and refuses any read whose signature does not verify. |
| cwip/signed-tables | browser · Node · Bun | none | The declaration of WHICH tables carry a row signature, with none of the crypto. CC_SIGNED_TABLES (secrets, ports, meter_events, users, memberships), the SignedTable type, and the SECURITY_INTEGRITY_VIOLATION event name. It is split out of cwip/guard because it is a declaration four tiers read, one of which is now a browser: since the studio canvas runs the verify pipeline client-side, V6's import of cwip/guard dragged timingSafeEqual — absent from Bun's node:crypto browser polyfill — into a bundled client graph and broke the build. A consumer that needs to KNOW which tables are signed no longer has to be able to sign. cwip/guard re-exports it, so its public API is unchanged. |
| cwip/query | browser · Node · Bun | none | SQL/Mongo query construction: buildSelect/toInlineSql, buildMongoFind/toMongoShell, isReadOnlySql/assertReadOnlySql. |
| cwip/dbquery | Node · Bun | pg · mysql2 · mssql · mongodb (lazy)² | DB query execution + env-keyed credential resolution (no secrets in your app DB), row-cap/timeout, lazy drivers. |
| cwip/sqlite | Node · Bun | none | Framework-free SQLite schema helpers over a bun:sqlite-shaped handle (structural — cwip never imports a driver). The one pragma controller: CC_PRAGMAS + assertCcPragmas apply the platform profile (WAL/NORMAL/busy_timeout 5000/foreign_keys ON/wal_autocheckpoint 1000), RE-READ every pragma and throw PragmaDriftError when the driver silently refused one, then install the Maximum Transaction Lifetime Guard (CC_MAX_TXN_LIFETIME_MS, withHeldLock) that rolls back any lock held long enough to starve WAL checkpoints. Plus the namespaced migration runner (runNamespacedMigrations, <domain>/<nnnn>_<name> enforced — un-namespaced chains from two packages silently collide), idempotent additive migrations (addColumnIfMissing, columnExists/tableExists/getColumnNames, identifier-validated), and the deprecated pre-CC applyRecommendedPragmas. Also the backup/lifecycle law: createSqliteBackupService snapshots a LIVE database only through VACUUM INTO (a byte copy of a WAL database restores torn) with assertSafeArchivePaths failing any archive that would tar a live *.db/-wal/-shm or a heavy AI cache, plus portable per-table JSON export; the Offline Migration Routine (runOfflineMigration, preflightLiveApps) pre-flights apps.boot_pid so DDL never runs against a database an app holds open, migrates and verifies an isolated VACUUM INTO copy, and swaps it in by atomic rename — a SQLITE_BUSY anywhere leaves the live file untouched instead of half-migrated; and splitSqlStatements splits a script into statements through strings, comments and BEGIN … END bodies. |
| cwip/domains | Node · Bun | none | The per-app domain tier (<machine-state-home>/instances/<appId>/<domain>.db): DOMAIN_NAMES (knowledge/media/search/system/staging), the knowledge.db Spine + Satellite DDL and migration chain (k_items spine + k_todo/k_bookmark/k_worklog/k_article/k_habit satellites, Kanban b_*, Airtable-style Lists l_* with TYPED value columns that fix SQLite's mixed-storage-class sort corruption, Contacts c_*, Oral History oh_*, Family f_*) with typed row interfaces, plus the tenancy law: every root table carries tenant_id/user_id NOT NULL (LOCAL_TENANT_ID/LOCAL_USER_ID sentinels) and assertTenancyColumns verifies it against the LIVE schema — including that every child table reaches a tenant through a NOT NULL foreign key. mintId/ID_PREFIXES namespace ids at mint (note_…, card_…). Also media.db (file catalogue + content-addressed mv_* vault, sources as ROWS rather than a path-array config, soft-delete rather than destroy), search.db (the only legitimately polymorphic tier — derived search_docs + an FTS5 external-content shadow kept in step by TRIGGERS, not application code, plus search_chunks embedding BLOBs), system.db (calendar as recurrence RULE + exception marks, notifications whose one-open-per-group coalescing is a partial unique INDEX rather than a race, time-off as an append-only ledger, budgets/subscriptions in integer cents), and staging.db containment: assertStagingSchemaContained inspects every statement of a plugin's schema BEFORE any of it runs (ATTACH DATABASE '../knowledge.db' is otherwise a one-line sandbox escape), requires each object to carry the plugin's own x_<slug>_ prefix, and applyStagingSchema/dropStagingPluginObjects keep an ownership ledger so promotion drops exactly that plugin's tables. |
| cwip/edge-warm | Node · Bun | none | Pre-fill Cloudflare's edge with a tenant's binary objects, and say how much of the fleet it holds. warmOne GETs an object and cancels the body — the edge Worker's cache.put works off an independent clone, so the entry lands and the warmer downloads 0 bytes (measured: 11,026 KB -> 0 KB over six segments, same 6/6 residency). An object the edge never looked up is counted bypassed, not filled (R2-backed tenants are never cached — warmedNothing is the question to ask before scheduling a sweep). warmLadder walks an HLS master -> rungs -> segments (playlists are no-store and counted as probes, never as warms); warmObjects takes a flat list; warmTarget is either or both against ONE request budget. startEdgeWarmer is the daily sweep plus a deduplicated warm-on-open, bounded by a request budget AND a wall clock because a cold pass moves ~6.7 MB/s through a household uplink. runWarmCli is bun run warm / --report / --json, identical in every app. fleetResidency + formatFleetResidency join per-app numerators against an object census into one fleet figure, and a tenant nothing warms lands in the denominator with a zero numerator rather than vanishing from the table. edgeWarmReportDir(stateRoot) is the one name for where the record goes. Holds no key, names no tenant, spells no path. |
| cwip/storage | Node · Bun | none | The StorageProvider seam every engine store is built ON: StorageProfile ('local' \| 'saas'), DomainHandle (exec(RepoOp), transaction, onMutation), and createLocalStorageProvider({ handles }) over INJECTED bun:sqlite handles — engines never open a DB file. Transactions nest through SAVEPOINTs, every op runs under the lifetime guard, and one connection is serialized so an await inside a transaction can't let another write leak into it. Includes the transaction-safe CDC stream (createMutationStream, MutationRecord): writes emit independently of UI events, records are buffered until the OUTERMOST transaction commits (a rollback discards them), and delivery is scheduled off the writer's stack so a slow consumer can't extend a write. Plus ObjectStore (all binary IO, local vault ↔ S3/R2) and registerSaasPool enforcing exactly ONE connection pool per process. createSaasStorageProvider is the pooled half: registration IS the enforcement (a second pool throws unpooled-saas-connection), every op sets the TRANSACTION-local tenant setting before its first statement (a pooled connection handed back still carrying another tenant's setting is the leak RLS was meant to prevent), a transaction pins ONE connection and nests through SAVEPOINTs, and a per-op wall-clock ceiling abandons a query that would otherwise hold a socket the whole pool is sized against. |
| cwip/repositories | Node · Bun | none | The repository tier every engine service imports INSTEAD of SQL: named RepoOps over a DomainHandle for knowledge (listRecords/getRecord/createRecord/updateRecord/deleteRecord/searchRecords), media (listMediaFiles/readPublicMediaFile/readPrivateMediaFile/readHlsRendition/catalogueMediaFile) and system (listCalendarEvents/listNotifications/markNotificationDone/createNotification). Two laws live HERE rather than at every call site: every statement carries tenant_id = ? from a required RepoScope (a read that "forgot" the predicate is a cross-tenant disclosure, not a slow query), and every write emits a CDC MutationRecord. The private-media law is structural: readPublicMediaFile filters encryption IS NULL, so the public bytes route CANNOT return a private row even for a caller holding private scope — there is no branch to forget. Pagination is keyset ((updated_at, id)), never OFFSET, so a row inserted mid-scroll cannot shift a page boundary, and searchRecords escapes LIKE wildcards so a bare % reads as a literal rather than matching everything.
| cwip/search-provider | browser · Node · Bun | none | The Vector & Search Provider seam: SearchProvider (index/remove/query, optional embed), SearchHit/SearchDoc, corpusFor, and indexOnMutation — wires a provider to a domain's CDC stream so the derived index follows row changes without any component remembering to call index(). Profiles: local → SQLite FTS5 + local embeddings; saas → pgvector/Qdrant, tenant-namespaced. Engines import the interface only. |
| cwip/telemetry | Node · Bun | none | The telemetry tier (<machine-state-home>/telemetry/api-telemetry.db) and its split-priority writer — two tables with OPPOSITE loss disciplines. openTelemetryDatabase({ open }) creates the directory/file, asserts CC_PRAGMAS at open, runs the telemetry/* migration chain and enforces the two-table allowlist. createTelemetryWriter({ db, signer, spillPath }) then gives recordApiCall — queued, flushed every ≤250ms or at 64 rows, never awaited on a request path, and DROPPED with a counter under sustained backpressure (bounded queue, oldest-first) — and recordMeterEvent, which is never dropped: on ANY write failure (lock, full disk, read-only mount) it fsyncs the HMAC-signed event to the append-only meter-spill.jsonl journal and replays it into meter_events when the lock clears, deduplicating on event_uuid; an event that reaches neither raises the fatal meter-event-loss. The journal drains by ATOMIC ROTATION, so a concurrent appender or a crash mid-drain loses nothing, and a tampered record is quarantined instead of billed. Flushing is synchronous underneath, so the process.on('exit') hook actually completes. createCCApiClient is the one downstream HTTP client: it injects an un-spoofable X-CC-Source-App header and is the SOLE telemetry emitter — every call records api_calls, and a metered client commits a meter_events row including a PARTIAL one on failure (the billing catcher). |
| cwip/registry-store | Node · Bun | none | The machine-tier registry store (<machine-state-home>/cc-registry.db). Station-owned DDL for exactly four tables (apps/ports/secrets/aliases), with assertRegistryTableAllowlist failing any table smuggled in (telemetry never shares the file every boot reads). claimPort is the only legal allocation pattern: an OS socket probe skips ports bound outside the registry, then the UNIQUE INSERT itself is the claim — read-then-write is a TOCTOU race — with idempotent re-claim for a restarting app; withClaimedPort implements the boot contract of releasing the row and retrying at p+1 on EADDRINUSE. createRegistrySecretsStore writes only SecretEnvelope blobs and verifies each row's HMAC signature on read. |
| cwip/constant-time | browser · Node · Bun | none | The one constant-time compare a library uses. constantTimeEqual(a, b) (strings, as UTF-8) and constantTimeEqualBytes(a, b): a pure-JS fold with the length difference IN the accumulator, so nothing exits early and nothing throws — timingSafeEqual throws on unequal lengths, and every hand-padded caller in the fleet had eventually turned that into an early return. Apps take cursedauth/constant-time (the same body); libraries cannot depend on an auth package, so theirs is here. safeEqual and secureEquals delegate to it. |
| cwip/registry-store/schema | browser · Node · Bun | none | The registry store's schema half without its node runtime — REGISTRY_FILENAME, REGISTRY_NAMESPACE, REGISTRY_TABLES and the migrations, with no bun:sqlite reach. Same browser-safety split as cwip/signed-tables: cursedbelt's V6 needs the table allowlist to check DDL, and importing the cwip/registry-store barrel to get it pulled the whole machine-tier runtime into the studio's client bundle. Import this when you need to know the registry's SHAPE; import cwip/registry-store when you need to talk to the database. |
| cwip/cc-registry, cwip/cc-registry-schema | — | — | ⚠️ Deprecated aliases of the two rows above, along with the CC_REGISTRY_* / CcRegistryTable symbol names. cc was the initials of cursed-core, a repo that no longer exists. They ship only so cursedbelt can move on a published version instead of a flag day, and are removed in the next major. Nothing new may import them. |
| cwip/notifications | browser · Node · Bun | none | The one persistent-notification engine (bell/inbox kind — transient toasts stay a UI concern): createNotificationCenter — storage/transport-agnostic dispatch with groupKey coalescing (repeat events bump count + resurface unread), explicit-user or host-resolved { role: 'owner' } audiences, a live-delivery NotificationSink (WS push + unread badge), producer paths that swallow their own errors by contract, read/done/prune lifecycle. Stores: createSqliteNotificationStore (structural bun:sqlite-shaped handle, race-safe open-group unique index, additive migration over a pre-existing table) + createMemoryNotificationStore. |
| cwip/env | browser · Node · Bun | none | .env parse/serialize/compare behind the apps' ".env editor": parseEnvFile/serializeEnv round-trip a file (comments, blanks, export, quoting preserved), parseEnvText → key→value map, upsertEnvVar/sortEnvEntries edit, diffEnvSets builds a key×source matrix. Powers cursedbelt's EnvEditor/EnvCompare; cwip/node's loadEnvFile reuses its parseEnvText. |
| cwip/csv | browser · Node · Bun | papaparse (lazy)² | The one robust CSV codec (typed PapaParse wrapper): parseCsv (string→rows sync, File→rows async; quoted commas/newlines/BOM handled), streamCsv (chunked File parse, Web Worker when available), toCsv (serialize records, RFC-4180 quoting). cursedbelt re-exports it — the codec is pure logic, so it lives here (STANDARDS §1/§3). Distinct from cwip/tabular's positional TabularTable parser, data/json's Format-Lab CSV⇆JSON converter, and core/format's record toCsv renderer. |
| cwip/yaml | browser · Node · Bun | none | The third configuration format the fleet reads, beside cwip/csv and cwip/env: parseYaml, stringifyYaml, formatYaml, yamlToJson/jsonToYaml, YamlError. Covers what a hand-written config file actually contains — block mappings and sequences, flow collections, all three scalar quotings, literal (|) and folded (>) blocks with every chomping mode, comments, and the YAML 1.2 core scalar types. 🔴 What it does NOT cover it REFUSES rather than guesses: an anchor, alias, tag or second document THROWS YamlError naming the line, because a formatter that quietly dropped an &anchor would hand back a file that parses, looks right and means something else. formatYaml normalizes; it does not round-trip (comments are not part of the value) — reach for parseYaml when the comments matter. |
| cwip/scheduler | browser · Node · Bun | none | The framework-free job scheduler: pure state transitions over JobRecords (Transition, cron and interval schedules, retry policy with backoff, timeout policy, and an AuditEntry trail) behind a JobStore seam a host implements. 🔴 The engine never touches a clock, a timer or a database — the caller supplies nowMs and persists what comes back, which is what makes ALL scheduling logic unit-testable and identical across hosts. |
| cwip/tabular | browser · Node · Bun | none | The positional TabularTable (header + string cells) model and its transforms (applyTabularOps, askAi) plus two CSV codecs over it: the lossy parseCsv/serializeCsv (normalizes CRLF, pads ragged rows — for pipelines), and the round-trip-fidelity parseCsvDocument/serializeCsvDocument (CsvDocument preserves EOL style, per-cell "was quoted" state, trailing-newline presence, ragged rows) with optional csvDoc* grid-mutation helpers (csvDocSetCell/csvDocAppendRow/csvDocDeleteRow/csvDocAppendColumn/csvDocRenameColumn/csvDocCellAt/csvDocColumnCount). The fidelity codec backs a spreadsheet editor over git-tracked .csv sheets, where a save must produce a minimal diff. |
| cwip/app-manifest | browser · Node · Bun | none | Standardized "cursed" package.json app manifest (ports, health route, standard-command→script map): validateAppManifest (structural + referential — every command must name an existing script), assertAppManifestConformance (one-line per-repo drift gate), scaffoldAppManifest; CLI cwip app-manifest init|check. Consumed by the app-manager supervisor. |
| cwip/app-supervisor | Node · Bun | none | The app-manager supervisor cwip/app-manifest was designed for. discoverManagedApps(roots) walks INTO repos collecting every "cursed"-manifest dir — nested manifests form the App → SubApp tree via parentId, broken manifests surface with manifestErrors, and shallow manifest-less repos come back as unregistered ("register me" feed). createAppSupervisor then supervises lifecycle per manifest: start (runs the declared start script with PORT injected), stop (SIGTERM→SIGKILL), restart, runCommand (one-shot build/test/update with timeout + captured output), 200-line log tails, health probes on the declared route, and honest pid-reconciled status (a stored "running" over a dead pid self-heals to stopped, on boot and on read). Store/spawn/clock/fetch all injectable; in-memory + JSON-file stores included. Engine behind the dev-portal plugin. |
| cwip/instance-lock | Node · Bun | none | Single-instance pidfile lock for long-lived hosts: acquireInstanceLock({ pidFile }) refuses to boot a second process against the same runtime dir (stale pids from dead processes self-heal), releaseInstanceLock on shutdown. Extracted from a daemon host so every daemon shares one implementation. |
| cwip/asset-budget | Node · Bun | none | The per-chunk gzip ratchet an app's scripts/check-assets.ts runs from verify right after build, because build fails on a BROKEN build and never on a fat one: measureAssets (gzip each emitted .js/.mjs/.css with node:zlib at its default level — byte for byte the figure Vite prints), chunkKey (content hash stripped, so a ceiling outlives a rebuild), parseBaseline/formatBaseline, judgeAssets (ok/over/under/new against a ±ALLOWANCE window, churn — passing — for gzip over with no raw growth, plus stale entries and two files colliding on one key), chunkName (the chunkFileNames helper that keeps shared chunks off the entry's index key), runAssetBudget and the assetBudgetCli front door (--dist/--baseline/--prune). A --prune may only LOWER a ceiling and copies the baseline's # header, and every kept entry's trailing note, through byte-for-byte; going more than ALLOWANCE UNDER is red too, so the tolerance cannot compound upwards. Each app keeps its own header, baseline and regression replay — this was the identical 753-line half of four copies. |
| cwip/editor-open | Node · Bun | none | Open a path in the machine's editor or file manager, cross-platform. detectEditor resolves a CLI on PATH (cursor/windsurf/code/zed/…) → macOS .app bundle → Finder/start/xdg-open; openInEditor (detached, honors an injected editorArgv), revealInFolder, whichSync. Detection is probe-injectable (unit-testable). |
| cwip/grid | browser · Node · Bun | none | The free-placement pixel/cell grid engine behind cursedbelt/react/dashboard-grid — absolute {col,row,colSpan,rowSpan} GridBox math: clampGridBox, boxesOverlap, resolveCollisions (push-down collision resolution), compactGrid, firstFitSlot (drop-in slot search), normalizeGridBoxes, pxToCell/pxToSpan. A different coordinate system and consumer set than cwip/layout's container-query span model — split out of cwip/layout (builder-11) once both grids co-existed there under one name. |
| cwip/json | browser · Node · Bun | none | Tolerant JSON / JS-object + CSV conversions behind the apps' JSON tools and cursedbelt's JsonEditor: parseLoose/formatJson accept JS-isms (single quotes, unquoted keys, trailing commas, comments) and report line/col; csvToJson/jsonToCsv/parseCsv. |
| cwip/layout | browser · Node · Bun | none | The framework-agnostic layout/widget engine core behind designed list cards/detail/dashboards + custom pages — the SINGLE source of truth (cursedbelt's ./layout re-exports it): the v2 LayoutNode tree types + idempotent migrateLayoutView/migrateLayoutConfig, pure treeOps (add/insert/remove/update/reorder/move/moveToIndex) for an editor, a generic bounded undo/redo history with tag coalescing (createUndoHistory/historyPush/…), LAYOUT_ENGINE_VERSION, computeAggregate/computeDistribution, the container-query 12-col grid + theme-token style class maps (nodeGridClass/nodeBoxClass/nodeTextClass — reflow to the node's own container via @lg: variants; neutrals ride semantic tokens), and the generic LayoutField/LayoutRow + resolveBinding contract. The React renderer/editor build on this in cursedbelt's layoutEngine; each app supplies its own widget registry + binding resolver. |
| cwip/timeline | browser · Node · Bun | none | The pure non-destructive multi-track timeline (NLE) engine behind a non-linear video editor — canonical clip/track model (TimelineClip source-window vs timeline coords, speed/volume, SourceAssetRef — ids only, binaries stay app-side), pure ops (splitClip/splitAllTracks, trimClipEdge with ripple + sync-lock, deleteClip ripple, setClipSpeed with continuity ripple, appendClip/insertClipAt push-to-fit, moveClip, closeGaps, rippleShift), px↔seconds geometry + zoom-anchored scroll + smart ruler ticks, magnetic snapStart (clip edges > playhead > grid), waveform downsamplePeaks/slicePeaks, and re-exported createUndoHistory (the one generic undo/redo, from cwip/layout). React timeline UI + ffmpeg planning live in cursedbelt. |
| cwip/search | browser · Node · Bun | none | Pure helpers for a "universal content search" over an app's own data: valueToText flattens a stored value to searchable/display text, buildSnippet/snippetForLabel excerpt around a match, jsonValuesMatch/firstMatchSnippet/firstNonEmptyValue search a JSON column's values while EXCLUDING secret keys, escapeLike/likePattern build safe SQL LIKE patterns. The app owns its data sources + SQL; powers the apps' universal-search UIs. |
| cwip/format | browser · Node · Bun | none | Presentation formatters: formatBytes, formatClock, formatTimeAgo, the record toCsv renderer, toTable/toTableColumns. Added 2026-08-04 as a client-safe subpath so a React component can format a byte count without importing the root barrel (which resolves to dist and drags the Node-only server pipeline into the browser bundle). |
| cwip/date | browser · Node · Bun | none | Date/time helpers: formatDuration, dateTime, getDateDaysAgo, getTimeFromISO. Client-safe sibling of cwip/format — same 2026-08-04 reason. |
| cwip/math | browser · Node · Bun | none | Numeric helpers: clamp, sumBy, the bytes conversions, and the math primitives. Client-safe sibling of cwip/format — same 2026-08-04 reason. |
| cwip/utils | browser · Node · Bun | none | Pure array/object/functional utilities: orderBy, mapA/filterA/ifItA/eitherA, identity, sleep, redactText, sanitizeString, storageJson, minMaxValue, trim, split, hasLength, findKeyMatch, callOrReturnIt/callWithKeys, getFunctionName/setFunctionName, getHtmlBody. |
| cwip/http | browser · Node · Bun | none | HTTP client-side surface: createApiClient, createSessionFetch, tokenProvider, buildUrl/joinUrl, decodeJwt, parseSSE/parseSSEStream, allowedOrigins, callbackLogin, HttpRequestError. Fetch-based, so it runs anywhere fetch does. |
| cwip/api-cache | browser · Node · Bun · edge | none | createApiCache — a versioned, per-viewer cache for expensive API JSON: wrap(req, ctx, compute) answers from the Workers Cache API (caches.default, synthetic key) or a bounded memory LRU elsewhere, stores only GET 200s without Set-Cookie, tells the client private, and stamps x-api-cache: HIT/MISS/BYPASS/REFRESH/OFF plus server-timing. version (the invalidation handle) and scope (the viewer, or null for a shared body) are REQUIRED. Owner/stage-only bypass/refresh controls and an enabled kill switch (apiCacheEnabled(env.API_CACHE)). AGENTS.md has the what-to-cache guide. |
| cwip/string | browser · Node · Bun | none | Pure string utilities: slugify (NFKD + diacritic-strip → URL-safe slug), interpolate, matchesGlob/globToRegExp, containsString/stringIncludesAny, estimateTokens, removeExtraWhitespace, removeSimpleHtmlByTag, convertEncoding. The ONE home for slug/glob/text helpers — consumers (cursedbelt, plugin-builder) import slugify here rather than re-hand-rolling one. |
| cwip/usage | browser · Node · Bun | none | Pure parsers for how much Claude capacity has been spent, from the two subscription-API-free sources: parseCcusageJson/costByModel/recentDaily over ccusage daily --json, and parseClaudeUsageText/parseUsageLimitLines/parseUsageResetAt over claude -p "/usage", plus the bucket vocabulary (USAGE_BUCKETS, MODEL_UNIT_WEIGHT, unitWeight, modelAliasFromId). Nothing here spawns a process or opens a database — the caller runs the CLI and feeds the captured stdout in. |
| cwip/seo | browser · Node · Bun · edge | none | Framework- and delivery-agnostic SEO core. A HeadModel (title/description/canonical/robots/OpenGraph/Twitter/JSON-LD/passthrough meta+link) + renderHead serializer; injectHead/removeManagedTags/stripBaseTitleAndDescription for splicing tags into an HTML shell idempotently; buildSitemap/buildRobots over an SeoPolicy. The SeoVisibility model (public/unlisted/private, private-by-default) with resolveEntry enforcing a no-leak rule (private routes emit only noindex); SeoDeliveryTarget/SeoContentSource/buildPolicy keep the delivery mechanism (prerender/edge/server) and content origin pluggable. |
| cwip/styles.css | CSS (Tailwind v4) | — | One-line Tailwind source registration: @import "cwip/styles.css"; (after @import "tailwindcss";) @sources the whole cwip/dist so the layout engine's literal utility classes (col-span-*, @lg:* container variants, tone/border classes) generate. Without it every layout node collapses to a 1/12-width track. Sourcing only a sub-tree of dist has the same failure — use this file. |
| cwip/test-report | Node · Bun | none | The structured test-run report model + renderers (createRunReport, renderReportText/renderReportHtml, summarizeReport), JUnit parser (parseJUnitXml), fs writer with debug-artifact materialization (writeReportFiles), and a Node-safe report-dir reader (readReportSummaries/readReport/resolveArtifactPath). Importable by production servers (unlike Bun-only cwip/testing, which re-exports it). |
| cwip/test-report/types | browser · Node · Bun | none | The browser-safe report types (TestRunReport/TestCaseResult/TestArtifact/TestRunSummary/TestStatus) for a UI report viewer — no node:fs. |
| cwip/capacity/types | browser · Node · Bun | none | The browser-safe capacity contract (SizeCategory/AssetRule/AssetMatch/SizeSample/SeriesPoint/GrowthStat/ProjectionPoint/AnomalyVerdict/SqliteSizeReport) for capacity dashboards — no node:fs. |
| cwip/capacity | Node · Bun | none | Whose bytes are these, and which way are they heading — the attribution + trend companion to cwip/disk-usage. createAssetClassifier maps a path/tenant/bucket to (app, component, category, regenerable) against a declared AssetRule[], longest-match-wins and scope-narrowed, filing anything unmatched under a VISIBLE unattributed rather than dropping it; aggregateByComponent folds many measurements into one row per component. growthOf/deltaOver/daysToFull/anomalyOf/projectSeries/rankMovers — least-squares rate in units per DAY (re-based to days-since-start, so ms-epoch x-values don't lose the slope to float error), windowed deltas, headroom projection with a residual band, and a per-series z-score that still fires on a metronomic series. probeSqliteSize — page_size/page_count/freelist_count plus the file, -wal and -shm on disk, because stat().size is a high-water mark that ignores the WAL; tableRowCounts for builds without dbstat. |
| cwip/disk-usage/types | browser · Node · Bun | none | The browser-safe disk-usage wire contract (DiskUsageNode/TreeSliceNode/ScanRules/VolumeStats/ScanSummary/DiskUsageStatus/RootInfo/… ) for UIs over the manager — no node:fs. |
| cwip/disk-usage | Node · Bun | none | Disk-space accounting behind the disk-usage plugin/UIs + the cwip du <path> CLI (the ssh fallback when an app is down): scanDiskUsage — du-style lstat tree walk (never follows symlinks, hard-links counted once, bytes-on-disk) with opaque rules (size node_modules in full WITHOUT itemizing its interior), skip rules (don't measure at all), minRecordSize aggregation and a timeBudgetMs/AbortSignal cutoff (truncated lower-bound nodes); getVolumeStats/listVolumes (df -kP, statfs fallback); deletePaths (containment-guarded bulk delete that reports bytes freed); createDiskUsageManager — the stateful server face: allowed roots, one-scan-at-a-time jobs with progress, per-root cached+persisted results served as depth/limit-bounded getTree slices for lazy UIs, UI-editable persisted rules, guarded deleteWithin that patches cached trees, and a low-space volume watch with renotify cooldown (onLowSpace). |
| cwip/testing | Bun test only | none (uses bun:test) | Test toolkit: startTestServer, makeHttpTestClient, isolateEnvDir/makeTempDir, fixtures, createPendingFileOperations, fs/console mocks. Re-exports cwip/test-report. |
| cwip/audio | browser | none | Shared deterministic generative-audio core for code-synthesized music: seeded RNG (hash32/splitmix32/mulberry32/hashString/rng) and music-theory pitch math (midiToFreq/freqToMidi/midiToNote/noteToMidi, scale/chord helpers). The DSP engines (voices/reverb/scheduler) stay per-game by design. |
| cwip/audio/testing | test only | none | Recording Web Audio test doubles for specs that must touch browser APIs without happy-dom or a real AudioContext: FakeParam (records every AudioParam op), FakeNode (captures connections, starts/stops, curve/modulation), FakeAudioContext (full createXxx superset + AudioWorklet simulation + byKind/reaches/activeOscillators helpers), makeFakeContext. Game-specific helpers (transport stubs, storage stubs) stay in their game. |
| cwip/noise | browser | none | Seeded deterministic spatial noise for procedural terrain, material, and particle-flow fields. Integer-hash spine (MurmurHash3, same as cwip/audio) + value2D, value3D, fbm2D (parameterised octaves), ridge2D, ridged, worley2D, curl2D. No Math.random, no clock — byte-identical on every machine. |
¹ cwip/node's extractPdfText lazy-loads the optional unpdf peer only when called.
² Lazy peers are dynamically imported the first time you call the function that needs them — so you install only the driver(s) you actually use.
import { createApiClient } from 'cwip/http'; // anywhere `fetch` runs
import { loadEnvFile, git, runWithTimeout } from 'cwip/node'; // Node / Bun
import { buildSelect } from 'cwip/query'; // query construction
import { runQuery } from 'cwip/dbquery'; // needs a driver installed
import { startTestServer } from 'cwip/testing'; // Bun tests onlyThe zero-dependency guarantee
cwip and cwip/node pull in nothing at install or runtime. Heavier capabilities
live behind their own subpath and declare their external as an optional peer
dependency — you install it; cwip never bundles it. Two guard tests keep the
guarantee from silently regressing: coreIsolation asserts no source file statically
imports a peer, and browserSafeRoot walks the transitive import graph of every
browser-reachable entry point — cwip/string, cwip/object, cwip/math, cwip/utils,
cwip/format, cwip/date, cwip/http, cwip/csv, … — and fails if any of them reaches
a node:*/bun:* builtin (Node-only code lives behind cwip/node). That list used to be
one entry, the root barrel, because the barrel re-exported everything; emptying the barrel
in 4.0.0 moved the promise onto the subpaths that actually make it, so the guard moved too.
Placement decisions
Architecture rulings that are decided once and then settled — cwip is the bottom
layer (zero required runtime deps, browser-safe root, never imports upward), and these
record the calls the cross-layer cursedbelt/docs/STANDARDS.md
§1 delegates here, so they aren't re-litigated per task:
- Framework adapters stay in cwip only while something imports them — as lazy peers.
cwip may host a thin adapter for an external framework as long as the framework is an
optional peer loaded lazily via
requirePeerat call time and no framework type leaks into a browser-safe subpath — the shapecwip/dbqueryandcwip/csvstill have. The Express toolkit (cwip/server) was the standing example and was removed on 2026-09-13 when the machine-wide scan found no importer: the ruling was always "if Express is ever fully retired,cwip/serveris removed, not moved up", and that is what happened. The same scan, the same day, removed the two spreadsheet subpathscwip/excel(peerxlsx) andcwip/excel-engine(peersexceljs,hyperformula) — 2,017 lines with zero importers anywhere on the machine, and the only holders of three of the thirteen peers. 🔴 A never-imported adapter is not an asset held "in case the app that needs it arrives." The spreadsheet app does exist — it carries its own dependency-free formula engine (lexer/parser/evaluator plus date/financial/lookup/stats/text function libraries) and its own CSV codec, has never imported cwip, and would not have adopted an exceljs binding if it had.xlsx's peer entry was a rawhttps://cdn.sheetjs.com/…tarball URL in a package published to npm — unresolvable by any--frozen-lockfileinstall without network to that host — so deleting its last importer deleted the worst line in thispackage.jsonalong with thepostinstallstep that existed to paper over it. Git keeps the code; if a step-automation engine is ever wanted, it gets built on the formula engine that ships, not resurrected here. node:cryptoid utils live undercwip/node, not core.randomAlphaNumeric/getUniqueId/makeIdFromData/makeCorrelationIdmoved fromsrc/core/utilstosrc/web/node/id— they importnode:crypto, so the browser-safe core dir was the wrong home (public surface unchanged; still exported bycwip/node).src/web/is not renamed: it holds capability-tiered leaf modules keyed by their export subpath (browseraudio/noise, Nodenode/server), not by the folder name — the runtime is decided by the README table and enforced bybrowserSafeRoot/coreIsolation, so a rename would be pure cross-repo churn for no consumer benefit.- CSV: cwip's zero-dep
parseCsv/serializeCsv(data/tabular) is canonical and stays. A zero-dep bottom layer can't adopt PapaParse and can't import upward, so convergence runs the other way — cursedbelt's PapaParse wrapper defers to cwip (or the two coexist by layer: browser-safe zero-dep here, heavy/robust variant in cursedbelt). Follow-up (own task): converge cwip's three internal CSV paths —data/tabular,data/json,format/toCsv— onto the singledata/tabularengine.
Usage
Each utility has a co-located *.test.ts next to it (e.g. src/array/chunk.test.ts) that
doubles as a usage example. AGENTS.md lists every export by subpath.
Testing utilities (Bun)
cwip/testing provides reusable mocks and a real-server test harness so consumers don't
re-implement fs/console mocking or server spin-up in every project. It uses
bun:test and is therefore only available under Bun.
// some.test.ts — run with `bun test`
import { beforeEach, expect, it } from 'bun:test';
import { initializeGlobalMocks, fake, fakeReject, resetAllMocks } from 'cwip/testing';
const { registry } = initializeGlobalMocks(); // replaces console/fs.* with mocks for the suite
beforeEach(() => resetAllMocks());
it('mocks an external async call by dotted path', async () => {
fake('fs.promises.readFile', 'pretend file contents'); // override resolved value
fakeReject('fs.promises.writeFile', new Error('disk full')); // force a rejection
// ...exercise the system under test that calls fs.promises.*
});Also exported: startTestServer, makeHttpTestClient, isolateEnvDir/makeTempDir,
defineFixture/seqId, run-report builders, parseJUnitXml, and the
makeMockApp/makeMockLogger/makeMockReq/makeMockRes/mockMongoDB factories.
Why a separate entry point?
bun:test only exists inside the Bun test runtime. Keeping these helpers behind
cwip/testing means browser and plain-Node consumers of cwip / cwip/node never
resolve bun:test, so they can't be broken by it. Under the hood the package's "bun"
export condition routes Bun to the real module and every other runtime to a stub that
throws a clear "requires the Bun runtime" error at import time.
License
ISC
