Skip to content

Commit 5be7040

Browse files
committed
feat(config): validate user config with Zod; document in architecture
- Add zod dependency; codemapUserConfigSchema + parseCodemapUserConfig via strict object schema. - Export codemapUserConfigSchema from the package entry. - docs: User config section in architecture.md; index table + cross-cutting in docs/README.md; packaging.md notes zod; README links to User config anchor. - Tests: adjust assertions for Zod error messages.
1 parent fd46af1 commit 5be7040

9 files changed

Lines changed: 140 additions & 34 deletions

File tree

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ bun add @stainless-code/codemap
1616
# or: npm install @stainless-code/codemap
1717
```
1818

19-
The package exposes a **`codemap`** binary, a **library** entry (`import` / `exports`), compiled **`dist/`**, and **`templates/agents`** for **`codemap agents init`** — see [docs/packaging.md](docs/packaging.md).
20-
2119
**Engines:** Node **`^20.19.0 || >=22.12.0`** and/or Bun **`>=1.0.0`** — see `package.json` and [docs/packaging.md](docs/packaging.md).
2220

2321
---
@@ -56,7 +54,7 @@ codemap agents init --force
5654

5755
**Environment / flags:** `--root` overrides **`CODEMAP_ROOT`** / **`CODEMAP_TEST_BENCH`**, then **`process.cwd()`**. Indexing a project outside this clone: [docs/benchmark.md § Indexing another project](docs/benchmark.md#indexing-another-project).
5856

59-
**Configuration:** optional **`codemap.config.ts`** (default export object or async factory) or **`codemap.config.json`**. Shape: [codemap.config.example.json](codemap.config.example.json). When developing inside this repo you can use `defineConfig` from `@stainless-code/codemap` or `./src/config`. If you set **`include`**, it **replaces** the default glob list entirely.
57+
**Configuration:** optional **`codemap.config.ts`** (default export object or async factory) or **`codemap.config.json`**. Shape: [codemap.config.example.json](codemap.config.example.json). Runtime validation (**Zod**, strict keys) and API surface: [docs/architecture.md § User config](docs/architecture.md#user-config). When developing inside this repo you can use `defineConfig` from `@stainless-code/codemap` or `./src/config`. If you set **`include`**, it **replaces** the default glob list entirely.
6058

6159
---
6260

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ Technical docs for **[@stainless-code/codemap](https://github.com/stainless-code
44

55
| File | Topic |
66
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
7-
| [architecture.md](./architecture.md) | Schema, layering, CLI, programmatic API, parsers, [Key Files](./architecture.md#key-files) |
7+
| [architecture.md](./architecture.md) | Schema, layering, CLI, programmatic API, [**User config**](./architecture.md#user-config) (Zod), parsers, [Key Files](./architecture.md#key-files) |
88
| [benchmark.md](./benchmark.md) | **[Indexing another project](./benchmark.md#indexing-another-project)** (`CODEMAP_*`, `.env`) · **[benchmark script](./benchmark.md#the-benchmark-script)** (`src/benchmark.ts`) · [`fixtures/minimal/`](../fixtures/minimal/) |
99
| [packaging.md](./packaging.md) | `dist/`, npm **`files`**, **engines**, **[Node vs Bun](./packaging.md#node-vs-bun)**, **[Releases](./packaging.md#releases)** (Changesets) |
1010
| [roadmap.md](./roadmap.md) | Forward-looking backlog (not a `src/` inventory) |
1111
| [why-codemap.md](./why-codemap.md) | Why index + SQL for agents (speed, tokens, accuracy) |
1212

13-
**Cross-cutting:** SQLite, workers, include globs, and JSON config use Bun when available — **[packaging.md § Node vs Bun](./packaging.md#node-vs-bun)** (single table; don’t duplicate elsewhere).
13+
**Cross-cutting:** SQLite, workers, include globs, and **JSON config I/O** use Bun when available — **[packaging.md § Node vs Bun](./packaging.md#node-vs-bun)** (single table; don’t duplicate elsewhere). **Config shape / validation** (Zod, strict keys) lives only in **[architecture.md § User config](./architecture.md#user-config)**.
1414

1515
**Conventions:** one topic per file; relative links; no symbol/file counts or source line numbers (use `codemap query` / `bun run dev query` after indexing). **This repo:** `bun run dev``bun src/index.ts`; **`bun run build`** → tsdown → `dist/`. **Contributors:** `bun run check`, JSDoc on public API — [.github/CONTRIBUTING.md](../.github/CONTRIBUTING.md).

docs/architecture.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru
1010

1111
**`src/sqlite-db.ts`:** Node uses **`better-sqlite3`**; Bun uses **`bun:sqlite`**. Same schema everywhere. **`better-sqlite3`** allows **one SQL statement per `prepare()`**; **`bun:sqlite`** accepts **multiple statements** in one `run()`. On Node, **`runSql()`** splits multi-statement strings on **`;`** and runs each fragment. Do **not** put **`;`** inside **`--` line comments** in **`db.ts`** DDL strings (naive split would break). Details: [packaging.md § Node vs Bun](./packaging.md#node-vs-bun).
1212

13-
**`src/worker-pool.ts`:** Bun `Worker` or Node `worker_threads`. **`src/glob-sync.ts`:** Bun **`Glob`** or **`fast-glob`** for include patterns. **`src/config.ts`:** JSON user config via **`Bun.file`** on Bun, **`readFile` + `JSON.parse`** on Node. Full table: [packaging.md § Node vs Bun](./packaging.md#node-vs-bun).
13+
**`src/worker-pool.ts`:** Bun `Worker` or Node `worker_threads`. **`src/glob-sync.ts`:** Bun **`Glob`** or **`fast-glob`** for include patterns. **`src/config.ts`:** loads **`codemap.config.json`** / **`codemap.config.ts`** (JSON read path: **`Bun.file`** on Bun, **`readFile` + `JSON.parse`** on Node[packaging.md § Node vs Bun](./packaging.md#node-vs-bun)), then validates with **Zod** (`codemapUserConfigSchema`). Details: [User config](#user-config).
1414

1515
**Shipped artifact:** **`dist/`**`package.json` **`bin`** and **`exports`** both point at **`dist/index.mjs`** ([packaging.md](./packaging.md)); tsdown also emits **lazy CLI chunks** (`cmd-index`, `cmd-query`, `cmd-agents`, …) loaded via **`import()`** from **`src/cli/main.ts`**.
1616

@@ -111,6 +111,7 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru
111111
| `parsed-types.ts` | Shared `ParsedFile` shape for workers and adapters |
112112
| `agents-init.ts` | `codemap agents init` — copies `templates/agents``.agents/` |
113113
| `benchmark.ts` | SQL vs traditional timing script — see [benchmark.md § The benchmark script](./benchmark.md#the-benchmark-script) |
114+
| `config.ts` | `codemap.config.*` load path, **Zod** user schema (`codemapUserConfigSchema`), `resolveCodemapConfig` |
114115

115116
## CLI usage
116117

@@ -142,14 +143,22 @@ When specific file paths are passed via `--files`, the indexer skips git diff, g
142143

143144
## Programmatic usage
144145

145-
The npm package exports **`createCodemap`**, **`Codemap`** (`query`, `index`), **`runCodemapIndex`** (advanced), config helpers, **`CodemapDatabase`** (type), adapter types (`LanguageAdapter`, `getAdapterForExtension`, …), and **`ParsedFile`** — see **`src/api.ts`** / **`src/index.ts`** and **`dist/index.d.mts`**. Typical flow:
146+
The npm package exports **`createCodemap`**, **`Codemap`** (`query`, `index`), **`runCodemapIndex`** (advanced), **`codemapUserConfigSchema`**, **`parseCodemapUserConfig`**, **`defineConfig`**, **`CodemapDatabase`** (type), adapter types (`LanguageAdapter`, `getAdapterForExtension`, …), and **`ParsedFile`** — see **`src/api.ts`** / **`src/index.ts`** and **`dist/index.d.mts`**. Typical flow:
146147

147148
1. **`await createCodemap({ root, configFile?, config? })`** — loads `codemap.config.*`, calls **`initCodemap`** and **`configureResolver`**.
148149
2. **`await cm.index({ mode, files?, quiet? })`** — same pipeline as the CLI (incremental / full / targeted).
149150
3. **`cm.query(sql)`** — read-only SQL against `.codemap.db` (opens the DB per call).
150151

151152
**Constraint:** `initCodemap` is global to the process; only one active indexed project at a time.
152153

154+
### User config
155+
156+
Optional **`codemap.config.ts`** (default export: object or async factory) or **`codemap.config.json`** at the project root; **`--config`** points at either. Example shape: [`codemap.config.example.json`](../codemap.config.example.json).
157+
158+
**Validation:** **`codemapUserConfigSchema`** ([Zod](https://zod.dev)) — strict object (unknown keys are rejected). **`defineConfig({ ... })`**, **`parseCodemapUserConfig`**, and **`resolveCodemapConfig`** (CLI and merged `createCodemap({ config })`) all go through the same schema. Invalid config throws **`TypeError`** with a short path/message list.
159+
160+
**Exports:** `codemapUserConfigSchema`, `parseCodemapUserConfig`, `defineConfig`, and **`CodemapUserConfig`** (inferred type) from the package entry — see **`src/config.ts`** / **`dist/index.d.mts`**.
161+
153162
## Schema
154163

155164
**Fingerprints:** incremental runs compare **`files.content_hash`** — SHA-256 hex of raw file bytes from [`src/hash.ts`](../src/hash.ts) (same on Node and Bun). Details in the **`files`** table below.

docs/packaging.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ How **@stainless-code/codemap** is built and published. Hub: [README.md](./READM
1111

1212
Tarballs contain **`dist/`** + **`templates/`** only. **`bun run pack`**, then point the consumer at **`file:…/stainless-code-codemap-*.tgz`**, or use **`file:/path/to/repo`** after build, or **`bun link`**. If **`better-sqlite3`** fails in the consumer, **`npm rebuild better-sqlite3`** (native addon must match that Node).
1313

14-
**Engines** (`package.json`): **Node** `^20.19.0 || >=22.12.0` (matches **`oxc-parser`**; **`better-sqlite3`** is prebuilt for current Node majors only). **Bun** `>=1.0.0`. **Native bindings:** `better-sqlite3`, `lightningcss`, `oxc-parser`, `oxc-resolver` (NAPI); **`fast-glob`** is JS-only.
14+
**Engines** (`package.json`): **Node** `^20.19.0 || >=22.12.0` (matches **`oxc-parser`**; **`better-sqlite3`** is prebuilt for current Node majors only). **Bun** `>=1.0.0`. **Native bindings:** `better-sqlite3`, `lightningcss`, `oxc-parser`, `oxc-resolver` (NAPI); **`fast-glob`** and **`zod`** are JS-only. **`zod`** validates `codemap.config.*` at runtime (**`codemapUserConfigSchema`** in **`src/config.ts`**); see [architecture.md § User config](./architecture.md#user-config).
1515

1616
## Node vs Bun
1717

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"fast-glob": "^3.3.3",
6868
"lightningcss": "^1.32.0",
6969
"oxc-parser": "^0.123.0",
70-
"oxc-resolver": "^11.19.1"
70+
"oxc-resolver": "^11.19.1",
71+
"zod": "^4.3.6"
7172
},
7273
"devDependencies": {
7374
"@changesets/changelog-github": "^0.6.0",

src/config.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,46 @@ import { join } from "node:path";
55

66
import {
77
DEFAULT_INCLUDE_PATTERNS,
8+
defineConfig,
89
loadUserConfig,
10+
parseCodemapUserConfig,
911
resolveCodemapConfig,
1012
type CodemapUserConfig,
1113
} from "./config";
1214

15+
describe("parseCodemapUserConfig / defineConfig", () => {
16+
it("accepts an empty object", () => {
17+
expect(parseCodemapUserConfig({})).toEqual({});
18+
});
19+
20+
it("rejects non-objects", () => {
21+
expect(() => parseCodemapUserConfig(null)).toThrow(TypeError);
22+
expect(() => parseCodemapUserConfig([])).toThrow(TypeError);
23+
expect(() => parseCodemapUserConfig("x")).toThrow(TypeError);
24+
});
25+
26+
it("rejects unknown keys", () => {
27+
expect(() =>
28+
parseCodemapUserConfig({ include: ["**/*.ts"], extra: 1 }),
29+
).toThrow(/Unrecognized key|extra/i);
30+
});
31+
32+
it("rejects wrong array element types", () => {
33+
expect(() =>
34+
parseCodemapUserConfig({ include: ["**/*.ts", 1 as unknown as string] }),
35+
).toThrow(/include/);
36+
});
37+
38+
it("defineConfig validates like parseCodemapUserConfig", () => {
39+
expect(defineConfig({ databasePath: "db.sqlite" })).toEqual({
40+
databasePath: "db.sqlite",
41+
});
42+
expect(() => defineConfig({ bad: true } as CodemapUserConfig)).toThrow(
43+
/Unrecognized key|bad/i,
44+
);
45+
});
46+
});
47+
1348
describe("resolveCodemapConfig", () => {
1449
let dir: string;
1550
beforeEach(() => {
@@ -99,4 +134,13 @@ describe("loadUserConfig", () => {
99134
const cfg = await loadUserConfig(dir, join(dir, "nope.json"));
100135
expect(cfg).toBeUndefined();
101136
});
137+
138+
it("invalid JSON config throws when resolved", async () => {
139+
writeFileSync(
140+
join(dir, "codemap.config.json"),
141+
JSON.stringify({ include: [1, 2] }),
142+
);
143+
const cfg = await loadUserConfig(dir);
144+
expect(() => resolveCodemapConfig(dir, cfg)).toThrow(/include/);
145+
});
102146
});

src/config.ts

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { readFile } from "node:fs/promises";
33
import { join, resolve } from "node:path";
44
import { pathToFileURL } from "node:url";
55

6+
import { z } from "zod";
7+
68
async function readJsonFile(filePath: string): Promise<unknown> {
79
if (typeof Bun !== "undefined") {
810
return Bun.file(filePath).json();
@@ -43,22 +45,53 @@ export const DEFAULT_EXCLUDE_DIR_NAMES = [
4345
] as const;
4446

4547
/**
46-
* User configuration from `codemap.config.ts` / `.json`, CLI, or `createCodemap({ config })`.
48+
* Zod schema for user config (`codemap.config.*`, `defineConfig`, API).
49+
* Unknown keys are rejected (`.strict()`).
4750
*/
48-
export interface CodemapUserConfig {
49-
/** Project root. Defaults via CLI `--root` or `process.cwd()`. */
50-
root?: string;
51-
/** SQLite database path, relative to root or absolute. Default: `<root>/.codemap.db`. */
52-
databasePath?: string;
53-
/** Glob patterns relative to root; replaces {@link DEFAULT_INCLUDE_PATTERNS} when set. */
54-
include?: string[];
55-
/** Directory name segments to skip; replaces {@link DEFAULT_EXCLUDE_DIR_NAMES} when set. */
56-
excludeDirNames?: string[];
57-
/**
58-
* Path to `tsconfig.json` for import alias resolution (oxc-resolver).
59-
* Use `null` to disable. Default: `<root>/tsconfig.json` if the file exists.
60-
*/
61-
tsconfigPath?: string | null;
51+
export const codemapUserConfigSchema = z
52+
.object({
53+
root: z
54+
.string()
55+
.optional()
56+
.describe("Project root. Defaults via CLI `--root` or `process.cwd()`."),
57+
databasePath: z
58+
.string()
59+
.optional()
60+
.describe(
61+
"SQLite database path, relative to root or absolute. Default: `<root>/.codemap.db`.",
62+
),
63+
include: z
64+
.array(z.string())
65+
.optional()
66+
.describe(
67+
"Glob patterns relative to root; replaces default include list when set.",
68+
),
69+
excludeDirNames: z
70+
.array(z.string())
71+
.optional()
72+
.describe(
73+
"Directory name segments to skip; replaces default exclude list when set.",
74+
),
75+
tsconfigPath: z
76+
.union([z.string(), z.null()])
77+
.optional()
78+
.describe(
79+
"Path to `tsconfig.json` for import alias resolution. Use `null` to disable.",
80+
),
81+
})
82+
.strict();
83+
84+
/** Inferred from {@link codemapUserConfigSchema}. */
85+
export type CodemapUserConfig = z.infer<typeof codemapUserConfigSchema>;
86+
87+
function formatCodemapConfigError(error: z.ZodError): string {
88+
return error.issues
89+
.map((issue) => {
90+
const path =
91+
issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
92+
return `${path}: ${issue.message}`;
93+
})
94+
.join("; ");
6295
}
6396

6497
/**
@@ -72,11 +105,26 @@ export interface ResolvedCodemapConfig {
72105
readonly tsconfigPath: string | null;
73106
}
74107

108+
/**
109+
* Runtime validation for {@link CodemapUserConfig} (from JSON, `defineConfig`, or API).
110+
*
111+
* @throws TypeError when the shape is invalid or unknown keys are present.
112+
*/
113+
export function parseCodemapUserConfig(config: unknown): CodemapUserConfig {
114+
const result = codemapUserConfigSchema.safeParse(config);
115+
if (!result.success) {
116+
throw new TypeError(
117+
`Codemap config: ${formatCodemapConfigError(result.error)}`,
118+
);
119+
}
120+
return result.data;
121+
}
122+
75123
/**
76124
* Helper for `export default defineConfig({ ... })` in `codemap.config.ts`.
77125
*/
78126
export function defineConfig(config: CodemapUserConfig): CodemapUserConfig {
79-
return config;
127+
return parseCodemapUserConfig(config);
80128
}
81129

82130
/**
@@ -86,23 +134,24 @@ export function resolveCodemapConfig(
86134
root: string,
87135
user: CodemapUserConfig | undefined,
88136
): ResolvedCodemapConfig {
137+
const parsed = user !== undefined ? parseCodemapUserConfig(user) : undefined;
89138
const absRoot = resolve(root);
90-
const databasePath = user?.databasePath
91-
? resolve(absRoot, user.databasePath)
139+
const databasePath = parsed?.databasePath
140+
? resolve(absRoot, parsed.databasePath)
92141
: join(absRoot, ".codemap.db");
93-
const include = user?.include?.length
94-
? [...user.include]
142+
const include = parsed?.include?.length
143+
? [...parsed.include]
95144
: [...DEFAULT_INCLUDE_PATTERNS];
96145
const excludeDirNames = new Set<string>(
97-
user?.excludeDirNames?.length
98-
? user.excludeDirNames
146+
parsed?.excludeDirNames?.length
147+
? parsed.excludeDirNames
99148
: DEFAULT_EXCLUDE_DIR_NAMES,
100149
);
101150
let tsconfigPath: string | null;
102-
if (user?.tsconfigPath === null) {
151+
if (parsed?.tsconfigPath === null) {
103152
tsconfigPath = null;
104-
} else if (user?.tsconfigPath) {
105-
tsconfigPath = resolve(absRoot, user.tsconfigPath);
153+
} else if (parsed?.tsconfigPath) {
154+
tsconfigPath = resolve(absRoot, parsed.tsconfigPath);
106155
} else {
107156
const d = join(absRoot, "tsconfig.json");
108157
tsconfigPath = existsSync(d) ? d : null;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export type {
1111
ParseContext,
1212
} from "./adapters/types";
1313
export {
14+
codemapUserConfigSchema,
1415
defineConfig,
16+
parseCodemapUserConfig,
1517
type CodemapUserConfig,
1618
type ResolvedCodemapConfig,
1719
} from "./config";

0 commit comments

Comments
 (0)