@kurotako/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,435 @@
1
+ import { IrIssue, IR, SourceIR } from '@kurotako/ir';
2
+
3
+ /**
4
+ * `TakoError` hierarchy. Every failure in the pipeline is fail-fast and carries
5
+ * enough context to name the offending source or generator. The CLI maps any
6
+ * `TakoError` to a formatted message + non-zero exit; a non-`TakoError` throw is
7
+ * a bug and surfaces as a stack trace.
8
+ *
9
+ * Error table: `backlog/features/core-pipeline/technical.md` §Error model.
10
+ */
11
+
12
+ declare class TakoError extends Error {
13
+ readonly code: string;
14
+ constructor(code: string, message: string, options?: {
15
+ cause?: unknown;
16
+ });
17
+ }
18
+ declare class NamespaceMismatchError extends TakoError {
19
+ readonly namespace: string;
20
+ readonly returned: string;
21
+ constructor(namespace: string, returned: string);
22
+ }
23
+ declare class IrValidationError extends TakoError {
24
+ readonly issues: IrIssue[];
25
+ readonly namespace?: string;
26
+ constructor(issues: IrIssue[], namespace?: string);
27
+ }
28
+ declare class DuplicateNamespaceError extends TakoError {
29
+ readonly namespace: string;
30
+ constructor(namespace: string);
31
+ }
32
+ declare class UnknownDependencyError extends TakoError {
33
+ readonly generator: string;
34
+ readonly missing: string;
35
+ constructor(generator: string, missing: string);
36
+ }
37
+ declare class InvalidDependencyError extends TakoError {
38
+ readonly generator: string;
39
+ readonly dependency: string;
40
+ constructor(generator: string, dependency: string);
41
+ }
42
+ declare class DependencyCycleError extends TakoError {
43
+ readonly cycle: string[];
44
+ constructor(cycle: string[]);
45
+ }
46
+ declare class OutputCollisionError extends TakoError {
47
+ readonly path: string;
48
+ readonly generators: [string, string];
49
+ constructor(path: string, generators: [string, string], hint?: string);
50
+ }
51
+ declare class InvalidOutputPathError extends TakoError {
52
+ readonly path: string;
53
+ readonly generator: string;
54
+ constructor(path: string, generator: string);
55
+ }
56
+ declare class UnsupportedOutputModeError extends TakoError {
57
+ readonly mode: string;
58
+ constructor(mode: string);
59
+ }
60
+ declare class OutputPeerConflictError extends TakoError {
61
+ readonly namespace: string;
62
+ readonly package: string;
63
+ readonly ranges: string[];
64
+ readonly generators: string[];
65
+ constructor(namespace: string, pkg: string, ranges: string[], generators: string[]);
66
+ }
67
+ declare class PackageBuildError extends TakoError {
68
+ readonly namespace: string;
69
+ constructor(namespace: string, options?: {
70
+ cause?: unknown;
71
+ });
72
+ }
73
+ declare class MissingPackageWorkspaceFilesError extends TakoError {
74
+ readonly workspaceRoot: string;
75
+ readonly missing: string[];
76
+ constructor(workspaceRoot: string, missing: string[]);
77
+ }
78
+ declare class OutputNotGeneratedError extends TakoError {
79
+ readonly path: string;
80
+ constructor(path: string);
81
+ }
82
+ declare class PackageInstallError extends TakoError {
83
+ readonly pm: string;
84
+ constructor(pm: string, options?: {
85
+ cause?: unknown;
86
+ });
87
+ }
88
+ declare class DriverError extends TakoError {
89
+ readonly role: 'parser' | 'generator';
90
+ readonly driverName: string;
91
+ readonly namespace?: string;
92
+ constructor(role: 'parser' | 'generator', driverName: string, options?: {
93
+ cause?: unknown;
94
+ namespace?: string;
95
+ });
96
+ }
97
+ declare class HookError extends TakoError {
98
+ readonly hook: string;
99
+ constructor(hook: string, options?: {
100
+ cause?: unknown;
101
+ });
102
+ }
103
+
104
+ /**
105
+ * The Writer seam. `run()` aggregates a virtual tree and hands it to the writer
106
+ * selected by `output.mode`. Mode A (`directoryWriter`) and mode B
107
+ * (`packageWriter`) both live in this directory.
108
+ */
109
+
110
+ interface WriteInput {
111
+ files: VirtualFile[];
112
+ output: OutputConfig;
113
+ /**
114
+ * Generator short name -> artifact. Mode B reads `peerDependencies` from it;
115
+ * mode A ignores it. Optional so a bare `directoryWriter.write({ files,
116
+ * output })` call still type-checks.
117
+ */
118
+ artifacts?: Record<string, GeneratorArtifact>;
119
+ /** Mode B logs the manual install command here when no pm is resolved. */
120
+ logger?: Logger;
121
+ }
122
+ /**
123
+ * One file a `write()` would emit, resolved to its absolute on-disk path with
124
+ * the exact bytes it would serialise (banner already applied by the caller, as
125
+ * for `write`). `plan()` computes these without any disk I/O; `write()` is
126
+ * `plan()` plus materialisation, so the two never drift.
127
+ */
128
+ interface PlannedFile {
129
+ /** Absolute. */
130
+ path: string;
131
+ content: string;
132
+ }
133
+ interface Writer {
134
+ write(input: WriteInput): Promise<string[]>;
135
+ /**
136
+ * Same layout as `write()`, no disk I/O: the exact set of files `write()`
137
+ * would produce (mode B: `<pkgDir>/src/…` remap + synthesized manifest, but
138
+ * no `dist/`, no `pm install`).
139
+ */
140
+ plan(input: WriteInput): Promise<PlannedFile[]>;
141
+ }
142
+
143
+ /**
144
+ * Public type surface of `@kurotako/core`. Runtime-code free: every export here
145
+ * is a type or an interface. The orchestrator (`run.ts`), the error hierarchy
146
+ * (`errors.ts`) and the writer seam (`writer/`) build on these.
147
+ *
148
+ * Product decisions: `backlog/features/core-pipeline/technical.md`. The IR types
149
+ * come from `@kurotako/ir`; core owns config, driver contracts, contexts,
150
+ * artifacts, hooks, the logger and the `run()` option / result shapes.
151
+ */
152
+
153
+ /**
154
+ * Structured logger. Core ships a no-op default (`logger.ts`); the CLI injects a
155
+ * real one. Contexts receive a child logger tagged with the namespace /
156
+ * generator name.
157
+ */
158
+ interface Logger {
159
+ debug(msg: string, meta?: unknown): void;
160
+ info(msg: string, meta?: unknown): void;
161
+ warn(msg: string, meta?: unknown): void;
162
+ error(msg: string, meta?: unknown): void;
163
+ }
164
+ /**
165
+ * The already-resolved, already-validated configuration `run()` consumes.
166
+ * Construction, file format and driver-option validation belong to
167
+ * `@kurotako/config`; core only declares the shape it needs.
168
+ */
169
+ interface ResolvedConfig {
170
+ /**
171
+ * Absolute path of the directory holding the config file. Anchor for relative
172
+ * output paths and for `ParseContext.cwd`.
173
+ */
174
+ rootDir: string;
175
+ /** Key === namespace (ADR-0003). */
176
+ sources: Record<string, SourceConfig>;
177
+ /** Key === `Generator.name` (short name). */
178
+ generators: Record<string, GeneratorConfig>;
179
+ outputs: OutputConfig[];
180
+ hooks?: Hooks;
181
+ }
182
+ interface SourceConfig {
183
+ parser: Parser;
184
+ /** Opaque seam kept for `--emit` / debugging; core does not read it. */
185
+ options?: unknown;
186
+ }
187
+ interface GeneratorConfig {
188
+ generator: Generator;
189
+ /** Opaque seam kept for `--emit` / debugging; core does not read it. */
190
+ options?: unknown;
191
+ /** Restrict this generator to a subset of namespaces; default = all. */
192
+ namespaces?: string[];
193
+ }
194
+ interface OutputConfig {
195
+ /** Default `'dir'`. */
196
+ mode?: 'dir' | 'package';
197
+ /** Mode A; resolved absolute by config-system. */
198
+ dir?: string;
199
+ /** Mode B. */
200
+ packagesDir?: string;
201
+ /** Mode B (required for mode B — config-system enforces). */
202
+ scope?: string;
203
+ /** Mode B, optional — consumed by output-modes. */
204
+ packageManager?: 'bun' | 'pnpm' | 'yarn' | 'npm';
205
+ /** Restrict this destination to a subset of `config.generators`; default = all. */
206
+ generators?: string[];
207
+ }
208
+ interface Parser {
209
+ name: string;
210
+ parse(ctx: ParseContext): Promise<SourceIR> | SourceIR;
211
+ /**
212
+ * Metadata for `cli --watch` — the set of paths a watcher should observe.
213
+ * `run()` never calls it.
214
+ */
215
+ watchPaths?(ctx: ParseContext): string[] | Promise<string[]>;
216
+ /**
217
+ * The directory this source is anchored at, for toolchain-dependency
218
+ * resolution. Already curried (options bound). `run()` calls it before
219
+ * `parse()` and passes the result as `ParseContext.anchorDir`. Return
220
+ * `undefined` (or omit the hook) to anchor on `rootDir`. Must not throw for an
221
+ * ordinary "not found" case — a bad path is the parser's problem to surface
222
+ * during `parse()`.
223
+ */
224
+ anchor?(rootDir: string): string | undefined | Promise<string | undefined>;
225
+ }
226
+ interface ParseContext {
227
+ namespace: string;
228
+ /** Absolute; the config-file directory. Base for `options.schema` and output paths. */
229
+ cwd: string;
230
+ /**
231
+ * Absolute; the directory this source is anchored at — where its schema lives.
232
+ * A parser resolves the source's own toolchain dependencies (`@prisma/internals`
233
+ * and equivalents) from here, letting Node walk up `node_modules` to `cwd` and
234
+ * beyond. Absent (⇒ treat as `cwd`) when the parser declares no `anchor` hook.
235
+ */
236
+ anchorDir?: string;
237
+ logger: Logger;
238
+ }
239
+ interface Generator {
240
+ name: string;
241
+ /** Hard dependency: absent from the config => error. Constrains order. */
242
+ dependsOn?: string[];
243
+ /** Optional dependency: used if present, else ignored. Constrains order. */
244
+ optionalDependsOn?: string[];
245
+ generate(ctx: GenerateContext): Promise<GenOutput> | GenOutput;
246
+ }
247
+ interface GenerateContext {
248
+ /** Namespace-filtered deep clone of the merged IR. */
249
+ ir: IR;
250
+ /** Only declared deps (`dependsOn ∪ optionalDependsOn`) that actually ran. */
251
+ dependencies: Record<string, GeneratorArtifact>;
252
+ logger: Logger;
253
+ }
254
+ interface GenOutput {
255
+ files: VirtualFile[];
256
+ artifact: GeneratorArtifact;
257
+ }
258
+ interface VirtualFile {
259
+ /**
260
+ * POSIX, relative to the output root. The generator owns the
261
+ * `<namespace>/<generatorName>/` prefix (one sub-tree per generator; core
262
+ * synthesizes `<namespace>/index.ts`).
263
+ */
264
+ path: string;
265
+ content: string;
266
+ }
267
+ interface GeneratorArtifact {
268
+ /** Key === `${namespace}.${entity}`. */
269
+ entities: Record<string, EntitySymbols>;
270
+ /**
271
+ * Package -> semver range the emitted code imports. Mode B: core aggregates
272
+ * per namespace (output-modes).
273
+ */
274
+ peerDependencies?: Record<string, string>;
275
+ /** Generator-defined; the consumer casts to the producer's published type. */
276
+ extra?: unknown;
277
+ }
278
+ interface EntitySymbols {
279
+ /** Module specifier a sibling generator imports from. */
280
+ module: string;
281
+ /** Role -> exported identifier, e.g. `{ schema: "UserSchema", type: "User" }`. */
282
+ symbols: Record<string, string>;
283
+ }
284
+ interface Hooks {
285
+ afterEmit?(ctx: AfterEmitContext): Promise<void> | void;
286
+ }
287
+ interface AfterEmitContext {
288
+ /** Absolute; the directory the Writer just populated. */
289
+ outputDir: string;
290
+ /** Absolute paths actually written, sorted. */
291
+ files: string[];
292
+ logger: Logger;
293
+ }
294
+ interface RunOptions {
295
+ /** Default: no-op. */
296
+ logger?: Logger;
297
+ /** Cooperative cancellation between steps (watch mode). */
298
+ signal?: AbortSignal;
299
+ /** Default `true`; `false` => run everything, skip the Writer. */
300
+ write?: boolean;
301
+ /**
302
+ * Default `false`. `true` => run everything up to (not including) emission,
303
+ * then ask each output's Writer for the files a `generate` would write
304
+ * (absolute path + exact bytes) via `Writer.plan()`, returned as
305
+ * `RunResult.plan`. No disk I/O, `afterEmit` does not fire. Wins over
306
+ * `write`: `{ plan: true, write: true }` still writes nothing.
307
+ */
308
+ plan?: boolean;
309
+ }
310
+ interface RunResult {
311
+ /** Merged, validated IR (for `--emit-ir`, drift-guard). */
312
+ ir: IR;
313
+ /** Generator short names, in execution order. */
314
+ order: string[];
315
+ /** Aggregated virtual tree, sorted by path. */
316
+ files: VirtualFile[];
317
+ /** Generator short name -> its artifact. */
318
+ artifacts: Record<string, GeneratorArtifact>;
319
+ /** One entry per `config.outputs[]`, in order; `[]` when `write: false`. */
320
+ written: {
321
+ output: OutputConfig;
322
+ files: string[];
323
+ }[];
324
+ /**
325
+ * Present iff `opts.plan === true`: the files a fresh `generate` would write
326
+ * across every `config.outputs[]` entry, absolute paths, sorted by `path`.
327
+ * Basis of `tako check` (drift-guard).
328
+ */
329
+ plan?: PlannedFile[];
330
+ }
331
+
332
+ /**
333
+ * The no-op `Logger` default and a `childLogger` wrapper that merges a
334
+ * `{ namespace }` / `{ generator }` tag into every call's `meta`.
335
+ */
336
+
337
+ /** Default logger: swallows everything. The CLI injects a real one. */
338
+ declare const noopLogger: Logger;
339
+ /**
340
+ * Wrap `base` so every message carries `prefixMeta` (e.g. `{ namespace }` or
341
+ * `{ generator }`) merged into its `meta` argument.
342
+ */
343
+ declare function childLogger(base: Logger, prefixMeta: Record<string, unknown>): Logger;
344
+
345
+ /**
346
+ * `run()` — the single public entry point. Sequential, fail-fast: parse ->
347
+ * merge -> order -> generate -> collect -> write -> afterEmit. `opts.signal` is
348
+ * checked at each step boundary; `opts.write === false` runs everything but
349
+ * skips the Writer (basis of `--dry-run`). `opts.plan === true` also stops
350
+ * before emission but calls `Writer.plan()` per output and returns the planned
351
+ * tree as `RunResult.plan` — no disk I/O, no `afterEmit` (basis of `tako check`
352
+ * / drift-guard); it wins over `opts.write`.
353
+ *
354
+ * Steps 5b/5c (synthesize root barrels, apply banner) are added to this file by
355
+ * the output-modes feature; they are not part of the core-pipeline tasks.
356
+ */
357
+
358
+ declare function run(config: ResolvedConfig, opts?: RunOptions): Promise<RunResult>;
359
+
360
+ /**
361
+ * The generated-file banner. `run.ts` calls `applyBanner` once, after barrel
362
+ * synthesis and before the writer, so every `.ts` file — generator output and
363
+ * synthesized barrels alike — carries the marker. `.json` files have no comment
364
+ * syntax: `packageWriter` sets a `"//"` key on `package.json` instead.
365
+ *
366
+ * Design: `backlog/features/output-modes/technical.md` §Banner.
367
+ */
368
+
369
+ declare const BANNER = "// Generated by tako. Do not edit.\n";
370
+ declare const GITATTRIBUTES = "* linguist-generated=true\n";
371
+ /**
372
+ * Prepend `BANNER` to every `.ts` / `.tsx` file (and `tsconfig.json`). Pure,
373
+ * and idempotent-safe: a file that already starts with the banner is left
374
+ * untouched. `package.json` and other `.json` files pass through unchanged.
375
+ */
376
+ declare function applyBanner(files: VirtualFile[]): VirtualFile[];
377
+
378
+ /**
379
+ * Root-barrel synthesis. Each generator owns `<namespace>/<generatorName>/` and
380
+ * emits its own barrel there; `tako` synthesizes `<namespace>/index.ts` so
381
+ * `import … from '<scope>/<namespace>'` resolves regardless of how many
382
+ * generators ran. Mode-independent — mode A and mode B both get the barrel.
383
+ *
384
+ * Design: `backlog/features/output-modes/technical.md` §New orchestration step.
385
+ */
386
+
387
+ /**
388
+ * One `VirtualFile { path: '<ns>/index.ts' }` per namespace present in `files`,
389
+ * its content one sorted `export * from './<generatorName>';` line per
390
+ * generator that contributed a file under `<ns>/<generatorName>/`. A
391
+ * single-generator namespace still gets a barrel.
392
+ *
393
+ * When `artifactsByGenerator` is supplied, `logger?.warn(...)` fires if the same
394
+ * exported identifier appears in two contributing artifacts for one namespace
395
+ * (an ambiguous star re-export TypeScript/ESM silently drops). Never throws.
396
+ */
397
+ declare function synthesizeRootBarrels(files: VirtualFile[], artifactsByGenerator?: Record<string, GeneratorArtifact>, logger?: Logger): VirtualFile[];
398
+
399
+ declare const directoryWriter: Writer;
400
+
401
+ declare const packageWriter: Writer;
402
+
403
+ declare function selectWriter(output: OutputConfig): Writer;
404
+
405
+ /**
406
+ * Namespace -> (package -> semver range). Per namespace, union the
407
+ * `peerDependencies` of every generator that emitted a file under
408
+ * `<namespace>/<generatorName>/`. Identical ranges de-duplicate; the same
409
+ * package with two different ranges from two generators throws
410
+ * `OutputPeerConflictError` (fail-fast). Namespace and package keys are sorted.
411
+ */
412
+ declare function collectPeerDependencies(artifactsByGenerator: Record<string, GeneratorArtifact>, files: VirtualFile[]): Record<string, Record<string, string>>;
413
+
414
+ type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm';
415
+ /**
416
+ * Resolve the package manager to run `install` with, in order:
417
+ * 1. `configured` (from `output.packageManager`) — used verbatim;
418
+ * 2. lockfile walk-up from `startDir` (`bun.lock` / `bun.lockb` -> `bun`,
419
+ * `pnpm-lock.yaml` -> `pnpm`, `yarn.lock` -> `yarn`,
420
+ * `package-lock.json` -> `npm`), stopping at a `.git` marker or the root;
421
+ * 3. nearest ancestor `package.json` `packageManager` field (name before `@`);
422
+ * 4. `null` — do not guess.
423
+ */
424
+ declare function resolvePackageManager(opts: {
425
+ configured?: PackageManager;
426
+ startDir: string;
427
+ }): PackageManager | null;
428
+ /**
429
+ * Run `<pm> install` in `cwd`. No `--frozen-lockfile` — the generated packages
430
+ * are new, the lockfile must change. A non-zero exit becomes
431
+ * `PackageInstallError { pm, cause }`.
432
+ */
433
+ declare function runInstall(pm: PackageManager, cwd: string): Promise<void>;
434
+
435
+ export { type AfterEmitContext, BANNER, DependencyCycleError, DriverError, DuplicateNamespaceError, type EntitySymbols, GITATTRIBUTES, type GenOutput, type GenerateContext, type Generator, type GeneratorArtifact, type GeneratorConfig, HookError, type Hooks, InvalidDependencyError, InvalidOutputPathError, IrValidationError, type Logger, MissingPackageWorkspaceFilesError, NamespaceMismatchError, OutputCollisionError, type OutputConfig, OutputNotGeneratedError, OutputPeerConflictError, PackageBuildError, PackageInstallError, type PackageManager, type ParseContext, type Parser, type PlannedFile, type ResolvedConfig, type RunOptions, type RunResult, type SourceConfig, TakoError, UnknownDependencyError, UnsupportedOutputModeError, type VirtualFile, type WriteInput, type Writer, applyBanner, childLogger, collectPeerDependencies, directoryWriter, noopLogger, packageWriter, resolvePackageManager, run, runInstall, selectWriter, synthesizeRootBarrels };