@alxmss/tracekit
v0.1.1
Published
Runtime instrumentation and source pruning for TypeScript modules
Readme
TraceKit
Runtime instrumentation and deterministic source pruning for TypeScript.
TraceKit wraps your modules in a transparent Proxy, records which functions are called during an execution, and then rewrites your source files to remove every function body that was never touched — replacing them with { /* pruned */ }. The result is a minimal, accurate picture of what your code actually does at runtime.
Status: Operational — 34/34 tests passing.
Node.js 22+, TypeScript 5.x, ESM.
How it works
your code → tk.slice() → Proxy → tk.track() → Registry
↓
tracekit distill ← trace.json ← registry.drain() ← traceId
↓
mathService.pruned.ts (complexMatrixMultiply body gone, add/subtract intact)- Slice — wrap a module with
tk.slice(name, module). The return type isT, so IntelliSense and JSDoc are fully preserved. - Track — run your code inside
tk.track(fn). Every function call on a wrapped module is tagged with a unique trace ID and stored in the in-process registry. - Drain — export the registry to a JSON file with
registry.drain(). - Distill — run the CLI against the JSON file. Functions that appear in the trace keep their bodies; everything else is pruned.
Is TraceKit right for your project?
TraceKit is a precision instrument. It works best when you know which execution you want to understand, not just where a function is defined.
The sweet spot
Language: TypeScript / JavaScript (Node.js & modern web)
TraceKit's engine is built on the ECMAScript Proxy API and Node's AsyncLocalStorage, making it native to the JS/TS ecosystem. It wraps any object or function export without an OS-level agent.
- Node.js backends — Express, Fastify, NestJS
- Modern frontend build pipelines — React/Vite, Next.js (server-side)
Architecture: modular codebases with clear service boundaries
TraceKit thrives when logic is separated into services, controllers, or slices. You tell it "watch only the auth and payment slices" and it ignores the other 200 files in your project — so the output is 100% focused on the transaction flow you care about.
Good fits: /services, /controllers, /slices, /repositories, or any Redux Toolkit-style layout.
Task type: interaction-heavy debugging with a clear start and end
TraceKit records a single execution boundary — everything between tk.track() start and finish.
- API request/response cycles — trace every function from route entry to response
- Failing tests — run a broken test inside
tk.track(); the distilled output is exactly the code that ran - State transitions — pinpoint why a Redux action or database mutation didn't behave as expected
Project scale: the "context wall"
| Project size | Recommendation | |---|---| | < 5k LOC | TraceKit is overkill — an LLM can read the whole project | | 20k–200k+ LOC | TraceKit becomes a game-changer — a search returns 50 matches, TraceKit shows the 2 that actually ran |
TraceKit vs. standard RAG (vector search)
| | Standard RAG | TraceKit |
|---|---|---|
| Relevance | Shows code that looks relevant | Shows code that actually ran |
| Async logic | Struggles to follow callbacks | Follows perfectly via AsyncLocalStorage |
| Token usage | Scalable but noisy | Ultra-minimal |
| Setup cost | Requires a vector DB | Zero-config via tracekit init |
| Ambiguity | High | Zero |
If you're working in a large TypeScript codebase where "finding where the logic actually lives" is the hardest part of the job, TraceKit turns a needle-in-a-haystack problem into a here is the needle solution.
Installation
In a project that already has TypeScript
npm install --save-dev @alxmss/tracekitThen initialise the config (this creates tracekit.config.ts and adds a helper script to package.json):
npx tracekit initFrom source (this repo)
git clone https://github.com/alxmss/TraceKit.git
cd TraceKit
npm install
npm run build # compiles src/ → dist/Quick start
1. Initialise
npx tracekit initThis creates tracekit.config.ts in your project root and adds trace:distill to your package.json scripts.
2. Edit the config
Open tracekit.config.ts and map your slice names to their source files:
// tracekit.config.ts
import { defineConfig } from '@alxmss/tracekit';
export default defineConfig({
slices: {
math: './src/mathService.ts',
auth: './src/auth.ts',
db: './src/db/index.ts',
},
outputDir: '.tracekit',
});3. Instrument your code
import { tk, registry, traceStorage } from '@alxmss/tracekit';
import { writeFile } from 'node:fs/promises';
import * as mathService from './src/mathService.js';
// Option A — explicit (no config required)
const math = tk.slice('math', mathService);
// Option B — config-aware (reads sliceMap from tracekit.config.ts automatically)
import config from './tracekit.config.js';
tk.configure(config);
const math = tk.autoSlice('math', mathService);4. Run a trace
let traceId!: string;
await tk.track(async () => {
traceId = traceStorage.getStore()!.traceId;
// Only call the functions you actually need right now
math.add(3, 7);
math.subtract(10, 4);
// math.complexMatrixMultiply is never called → will be pruned
});
// Export the trace
const records = registry.drain();
await writeFile('trace.json', JSON.stringify(records), 'utf8');
console.log('Trace ID:', traceId);5. Distill
# Auto-resolves slice→file from tracekit.config.ts
npx tracekit distill <traceId> --trace trace.json --root .
# Or print as markdown (useful for LLM context)
npx tracekit distill <traceId> --trace trace.json --root . --format md
# Or write to a directory
npx tracekit distill <traceId> --trace trace.json --root . --output .tracekitInput (src/mathService.ts):
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export function complexMatrixMultiply(a: number[][], b: number[][]): number[][] {
// ... 30 lines of O(n³) loops ...
}Output (distilled):
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export function complexMatrixMultiply(a: number[][], b: number[][]): number[][] { /* pruned */ }API reference
tk.slice<T>(name, module): T
Wraps a module in a transparent Proxy. The return type is exactly T — no type information is lost. Recording only happens inside a tk.track() context; calls made outside are passed through without being observed.
const auth = tk.slice('auth', authModule);tk.configure(config)
Sets the global config so that tk.autoSlice knows the slice→file mapping. Call once at startup before any autoSlice calls.
import config from './tracekit.config.js';
tk.configure(config);tk.autoSlice<T>(name, module): T
Like tk.slice, but validates that name is registered in the config. The CLI reads the same config file, so you never need to pass --map on the command line.
const db = tk.autoSlice('db', dbModule);tk.track<T>(fn: () => T | Promise<T>): Promise<T>
Runs fn inside a new trace context backed by AsyncLocalStorage. Every function call on a sliced module within fn — including across await boundaries — is tagged with the same traceId. Concurrent tk.track() calls never share a context.
const result = await tk.track(async () => {
const user = await userRepo.findById(42);
return user;
});tk.distill(traceId, options): Promise<Map<string, string>>
Programmatic distillation. Reads the registry for traceId, looks up source files via sliceMap, and returns a Map<filePath, prunedSource>. Does not drain the registry.
const pruned = await tk.distill(traceId, {
projectRoot: process.cwd(),
sliceMap: new Map([['auth', './src/auth.ts']]),
});registry
The global in-process ring buffer (10 000 records by default, configurable via TRACEKIT_MAX_RECORDS).
| Method | Description |
|--------|-------------|
| registry.drain() | Returns all records and clears the buffer |
| registry.snapshot() | Returns all records without clearing |
| registry.onFlush | Optional callback fired synchronously on every record() call |
Each record is a tuple [timestamp, traceId, sliceName, fnPath].
defineConfig(config): TraceKitConfig
Identity helper with TypeScript type inference — the same pattern as Vite's defineConfig. Use it in tracekit.config.ts for editor autocomplete.
CLI reference
tracekit init
Scaffolds tracekit.config.ts in the project root and patches package.json.
Options:
--root <path> project root (default: current directory)
--force overwrite an existing tracekit.config.tstracekit distill <traceId>
Prunes source files based on a recorded trace.
Options:
--trace <file> JSON file from registry.drain() (use "-" for stdin)
--root <path> project root (required)
--map <name=path> slice → file mapping, repeatable; overrides tracekit.config.ts
--output <dir> write pruned files here instead of printing to stdout
--format <fmt> text (default) or mdWhen --map is omitted, the CLI auto-resolves the mapping from tracekit.config.ts at --root.
When --format md and --output are both set, all files are combined into a single <traceId>.md file — useful for pasting into an LLM prompt.
Edge cases and known behaviour
| Scenario | Behaviour |
|----------|-----------|
| Calls made outside tk.track() | Executed normally, not recorded |
| Circular references | Safe — a WeakMap breaks cycles |
| new ClassName() | Recorded as new sliceName.ClassName via construct trap |
| Private class fields (#field) | Passed through without interception |
| Symbol-keyed properties | Passed through; wrapping Symbol.iterator would break iteration |
| Arrow functions with concise bodies | Bodies like () => expr are not pruned (no block to replace) |
| Concurrent tk.track() calls | Each gets its own traceId; AsyncLocalStorage keeps them isolated |
Development
npm run build # tsc → dist/
npm run dev # tsc --watch
npm test # vitest run (34 tests)
npm run test:watch # vitest interactive
npm run typecheck # tsc --noEmit (no emit, just type-check)Project structure
src/
registry.ts Ring-buffer record store
tracker.ts AsyncLocalStorage context (traceId per tk.track call)
slice.ts Proxy engine + tk namespace
distiller.ts ts-morph AST parser + magic-string body replacer
config.ts defineConfig helper and TraceKitConfig interface
config-loader.ts CLI-side static parser for tracekit.config.ts
cli.ts Commander CLI (init, distill)
index.ts Public API surface
__tests__/
slice.test.ts
tracker.test.ts
distiller.test.ts