@kindgi/handler-runtime 0.0.0-bootstrap.0 → 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.
Files changed (68) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +218 -2
  3. package/dist/build-extensions.d.ts +75 -0
  4. package/dist/build-extensions.d.ts.map +1 -0
  5. package/dist/build-extensions.js +27 -0
  6. package/dist/build-extensions.js.map +1 -0
  7. package/dist/discovery.d.ts +19 -0
  8. package/dist/discovery.d.ts.map +1 -0
  9. package/dist/discovery.js +81 -0
  10. package/dist/discovery.js.map +1 -0
  11. package/dist/entrypoint.d.ts +10 -0
  12. package/dist/entrypoint.d.ts.map +1 -0
  13. package/dist/entrypoint.js +24 -0
  14. package/dist/entrypoint.js.map +1 -0
  15. package/dist/handler-runner.d.ts +127 -0
  16. package/dist/handler-runner.d.ts.map +1 -0
  17. package/dist/handler-runner.js +318 -0
  18. package/dist/handler-runner.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +7 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/kindgi-index-main.d.ts +2 -0
  24. package/dist/kindgi-index-main.d.ts.map +1 -0
  25. package/dist/kindgi-index-main.js +15 -0
  26. package/dist/kindgi-index-main.js.map +1 -0
  27. package/dist/kindgi-index.d.ts +316 -0
  28. package/dist/kindgi-index.d.ts.map +1 -0
  29. package/dist/kindgi-index.js +1201 -0
  30. package/dist/kindgi-index.js.map +1 -0
  31. package/dist/pack-env.d.ts +66 -0
  32. package/dist/pack-env.d.ts.map +1 -0
  33. package/dist/pack-env.js +97 -0
  34. package/dist/pack-env.js.map +1 -0
  35. package/dist/pack-service/index.d.ts +7 -0
  36. package/dist/pack-service/index.d.ts.map +1 -0
  37. package/dist/pack-service/index.js +6 -0
  38. package/dist/pack-service/index.js.map +1 -0
  39. package/dist/pack-service/main.d.ts +47 -0
  40. package/dist/pack-service/main.d.ts.map +1 -0
  41. package/dist/pack-service/main.js +201 -0
  42. package/dist/pack-service/main.js.map +1 -0
  43. package/dist/pack-service/service.d.ts +61 -0
  44. package/dist/pack-service/service.d.ts.map +1 -0
  45. package/dist/pack-service/service.js +341 -0
  46. package/dist/pack-service/service.js.map +1 -0
  47. package/dist/pack-service/supervisor.d.ts +138 -0
  48. package/dist/pack-service/supervisor.d.ts.map +1 -0
  49. package/dist/pack-service/supervisor.js +424 -0
  50. package/dist/pack-service/supervisor.js.map +1 -0
  51. package/dist/protocol.d.ts +104 -0
  52. package/dist/protocol.d.ts.map +1 -0
  53. package/dist/protocol.js +116 -0
  54. package/dist/protocol.js.map +1 -0
  55. package/package.json +87 -4
  56. package/src/build-extensions.ts +100 -0
  57. package/src/discovery.ts +89 -0
  58. package/src/entrypoint.ts +23 -0
  59. package/src/handler-runner.ts +500 -0
  60. package/src/index.ts +66 -0
  61. package/src/kindgi-index-main.ts +17 -0
  62. package/src/kindgi-index.ts +1605 -0
  63. package/src/pack-env.ts +148 -0
  64. package/src/pack-service/index.ts +17 -0
  65. package/src/pack-service/main.ts +246 -0
  66. package/src/pack-service/service.ts +478 -0
  67. package/src/pack-service/supervisor.ts +600 -0
  68. package/src/protocol.ts +214 -0
@@ -0,0 +1,1201 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+ /**
4
+ * `kindgi-index` — the pack indexer, which writes a pack's `index.json`.
5
+ * `kindgi dev` runs it over the dev bundles; a pack image's indexer
6
+ * stage runs it (process entry `kindgi-index-main`, bundled as
7
+ * `dist/kindgi-index.mjs`) over the image's bundles, and `kindgi build`
8
+ * runs the same bundle locally, so the two indexes compare.
9
+ *
10
+ * The indexer:
11
+ *
12
+ * 1. Loads `kindgi.config.ts` (or `.js` / `.mjs`) at the pack root
13
+ * to read `pack.id` / `pack.version` + optional `discovery` glob
14
+ * overrides.
15
+ * 2. Discovers files under the four default folders — `tools/`,
16
+ * `guardrails/`, `agents/`, `flows/` — using the config's
17
+ * discovery patterns; or, indexing a build, takes the files a
18
+ * bundle map lists (`bundleMap`), classified by the same patterns.
19
+ * 3. Dynamically imports each file (from its bundle, given a map).
20
+ * Reads the `default` export
21
+ * (accepting either the raw primitive shape or a `Result`-wrapped
22
+ * envelope from `defineTool` / `defineGuardrail` / etc.).
23
+ * 4. Kind-maps the discovered file by its default folder (primary) or
24
+ * by structural shape of the default export (fallback for
25
+ * custom-pattern files).
26
+ * 5. For every Zod-typed schema (`tool.inputZod`, `tool.outputZod`,
27
+ * `check.configZod`) calls `@kindgi/schema.toJSONSchemaSync` to
28
+ * derive the JSON Schema wire form. When JSON Schema is authored
29
+ * directly, it is emitted verbatim.
30
+ * 6. Assembles a `v: 1` `index.json` envelope and writes it
31
+ * atomically (write-to-tmp, then rename) to the configured output
32
+ * path.
33
+ *
34
+ * ## Fail loud at every boundary
35
+ *
36
+ * Missing config, missing default export, kind mismatch, Zod conversion
37
+ * failure, filesystem write error — each is a distinct typed
38
+ * `IndexerError` variant with the file path attached. No silent skips.
39
+ *
40
+ * ## Determinism
41
+ *
42
+ * The integrity gate (locally verify the server's build) diffs an indexer output computed on the developer's
43
+ * laptop against the server's output. Byte-identical inputs must
44
+ * produce byte-identical outputs. All list fields are sorted
45
+ * lexicographically by id; JSON serialization uses compact stable
46
+ * output; `publishedAt` is accepted as an explicit `opts.publishedAt`
47
+ * for reproducible builds (the pack build passes it) and defaults to
48
+ * `new Date()` only for local dev (documented in the README).
49
+ */
50
+ import * as fs from 'node:fs/promises';
51
+ import * as path from 'node:path';
52
+ import { fileURLToPath, pathToFileURL } from 'node:url';
53
+ import { loadFlow } from '@kindgi/flow';
54
+ import { isZodSchema, loadZodConverterSync, toJSONSchemaSync } from '@kindgi/schema';
55
+ import { parse as parseToml } from 'smol-toml';
56
+ import { TEST_FILE_REGEX, globStaticPrefix, globToRegex } from './discovery.js';
57
+ import { resolvePackEnv } from './pack-env.js';
58
+ // -----------------------------------------------------------------------
59
+ // Public envelope + shape types
60
+ // -----------------------------------------------------------------------
61
+ /**
62
+ * Envelope version emitted on `index.json`. Bump on breaking change;
63
+ * additive fields on an existing shape don't bump. Consumers (the
64
+ * controller) MUST refuse an envelope version they
65
+ * don't recognize.
66
+ */
67
+ export const INDEX_ENVELOPE_VERSION = 1;
68
+ /** The language of a loaded config's pack code. */
69
+ export function packLanguage(config) {
70
+ return config.language ?? 'node';
71
+ }
72
+ /**
73
+ * Default discovery patterns. Extensions
74
+ * `.ts` / `.js` / `.mjs` are all accepted — a pack build may transpile
75
+ * `.ts` to `.js` before the indexer runs inside the Dockerfile's indexer
76
+ * stage, but the indexer accepts either shape so local development (no
77
+ * bundle) and CI-built images (post-bundle) share the same code path.
78
+ */
79
+ export const DEFAULT_DISCOVERY = {
80
+ tools: 'tools/**/*.{ts,js,mjs}',
81
+ guardrails: 'guardrails/**/*.{ts,js,mjs}',
82
+ agents: 'agents/**/*.{ts,js,mjs}',
83
+ flows: 'flows/**/*.{ts,js,mjs}',
84
+ };
85
+ /**
86
+ * Default discovery patterns of a Python pack (`python -m kindgi.pack index`
87
+ * reads the same keys from `[tool.kindgi.discovery]`).
88
+ */
89
+ export const DEFAULT_PYTHON_DISCOVERY = {
90
+ tools: 'tools/**/*.py',
91
+ guardrails: 'guardrails/**/*.py',
92
+ agents: 'agents/**/*.py',
93
+ flows: 'flows/**/*.py',
94
+ };
95
+ /** A config's discovery patterns with the language's defaults filled in. */
96
+ export function resolveDiscovery(discovery, language = 'node') {
97
+ const defaults = language === 'python' ? DEFAULT_PYTHON_DISCOVERY : DEFAULT_DISCOVERY;
98
+ return { ...defaults, ...(discovery ?? {}) };
99
+ }
100
+ /**
101
+ * Envelope version for the flow payload embedded inside `index.json`.
102
+ * Distinct from the top-level `v` envelope so the flow substrate can
103
+ * evolve its own shape independently — matches the `packages/flow`
104
+ * schema-version convention (currently 1.8.0, which added input and
105
+ * output mappings). Additive fields don't bump this.
106
+ */
107
+ export const KERNEL_PAYLOAD_VERSION = 1;
108
+ // -----------------------------------------------------------------------
109
+ // Entry point
110
+ // -----------------------------------------------------------------------
111
+ /**
112
+ * Run the indexer stage. Never throws — every failure is captured as a
113
+ * typed `IndexerError`.
114
+ */
115
+ export async function runIndexer(opts) {
116
+ const packDir = path.resolve(opts.packDir);
117
+ const importModule = opts.importModule ?? defaultImportModule;
118
+ const configResult = await loadConfig(packDir, opts.configPath, importModule);
119
+ if (configResult.kind === 'err')
120
+ return configResult;
121
+ const config = configResult.value;
122
+ if (packLanguage(config) !== 'node') {
123
+ return {
124
+ kind: 'err',
125
+ error: {
126
+ code: 'language-mismatch',
127
+ message: `${packDir} is a ${packLanguage(config)} pack; index it with its own indexer (python -m kindgi.pack index)`,
128
+ },
129
+ };
130
+ }
131
+ const env = resolvePackEnv(config.env);
132
+ if (env.kind === 'err') {
133
+ return {
134
+ kind: 'err',
135
+ error: { code: 'config-invalid', field: 'env', message: `kindgi.config: ${env.message}` },
136
+ };
137
+ }
138
+ const discovery = resolveDiscovery(config.discovery);
139
+ const bundleMap = opts.bundleMap;
140
+ const moduleRoot = path.resolve(opts.moduleRoot ?? packDir);
141
+ const discovered = bundleMap !== undefined
142
+ ? discoverInBundleMap(bundleMap, discovery)
143
+ : await discoverFiles(packDir, discovery);
144
+ if (discovered.length === 0) {
145
+ return {
146
+ kind: 'err',
147
+ error: {
148
+ code: 'discovery-empty',
149
+ message: bundleMap !== undefined
150
+ ? 'The bundle map lists no file the discovery patterns match. Check kindgi.config.ts discovery patterns.'
151
+ : `Indexer discovered zero files under ${packDir}. Check kindgi.config.ts discovery patterns.`,
152
+ },
153
+ };
154
+ }
155
+ const zodConverter = loadZodConverterSync();
156
+ const tools = [];
157
+ const guardrails = [];
158
+ const agents = [];
159
+ const flows = [];
160
+ const fileErrors = [];
161
+ for (const { relPath, expectedKind } of discovered) {
162
+ const bundle = bundleMap?.[relPath];
163
+ const absPath = bundle !== undefined ? path.resolve(moduleRoot, bundle) : path.join(packDir, relPath);
164
+ const fileUrl = pathToFileURL(absPath).href;
165
+ let module_;
166
+ try {
167
+ module_ = await importModule(fileUrl);
168
+ }
169
+ catch (cause) {
170
+ fileErrors.push({
171
+ code: 'file-import-failed',
172
+ message: `Failed to import ${relPath}: ${stringifyError(cause)}`,
173
+ filePath: relPath,
174
+ cause: serializeCause(cause),
175
+ });
176
+ continue;
177
+ }
178
+ const defExport = extractDefaultExport(module_);
179
+ if (defExport === undefined) {
180
+ fileErrors.push({
181
+ code: 'no-default-export',
182
+ message: `File ${relPath} has no default export`,
183
+ filePath: relPath,
184
+ });
185
+ continue;
186
+ }
187
+ const errResult = detectResultError(defExport);
188
+ if (errResult !== undefined) {
189
+ fileErrors.push({
190
+ code: 'manifest-validation-failed',
191
+ message: `File ${relPath} default export is a Result-wrapped error: ${resultErrorMessage(errResult)}`,
192
+ filePath: relPath,
193
+ cause: errResult,
194
+ });
195
+ continue;
196
+ }
197
+ const unwrapped = unwrapResult(defExport);
198
+ const detected = detectKind(unwrapped);
199
+ if (detected === undefined) {
200
+ fileErrors.push({
201
+ code: 'ambiguous-kind',
202
+ message: `File ${relPath} default export does not match any primitive shape (tool / guardrail / agent / flow)`,
203
+ filePath: relPath,
204
+ });
205
+ continue;
206
+ }
207
+ if (expectedKind !== undefined && detected !== expectedKind) {
208
+ fileErrors.push({
209
+ code: 'kind-mismatch',
210
+ message: `File ${relPath} is under the default folder for kind '${expectedKind}' but default-exports a '${detected}'`,
211
+ filePath: relPath,
212
+ });
213
+ continue;
214
+ }
215
+ switch (detected) {
216
+ case 'tool': {
217
+ const built = buildTool(unwrapped, relPath, zodConverter);
218
+ if (built.kind === 'err') {
219
+ fileErrors.push(built.error);
220
+ continue;
221
+ }
222
+ tools.push(built.value);
223
+ break;
224
+ }
225
+ case 'guardrail': {
226
+ const built = buildGuardrail(unwrapped, relPath, zodConverter);
227
+ if (built.kind === 'err') {
228
+ fileErrors.push(built.error);
229
+ continue;
230
+ }
231
+ guardrails.push(built.value);
232
+ break;
233
+ }
234
+ case 'agent': {
235
+ const built = buildAgent(unwrapped, relPath);
236
+ if (built.kind === 'err') {
237
+ fileErrors.push(built.error);
238
+ continue;
239
+ }
240
+ agents.push(built.value);
241
+ break;
242
+ }
243
+ case 'flow': {
244
+ const built = buildGraph(unwrapped, relPath);
245
+ if (built.kind === 'err') {
246
+ fileErrors.push(built.error);
247
+ continue;
248
+ }
249
+ flows.push(built.value);
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ const outputPath = opts.outputPath !== undefined
255
+ ? path.resolve(opts.outputPath)
256
+ : path.join(packDir, 'index.json');
257
+ const artifactVersion = opts.artifactVersion ?? (await computeAutoArtifactVersion(outputPath));
258
+ const publishedAt = opts.publishedAt ?? new Date().toISOString();
259
+ const index = {
260
+ v: INDEX_ENVELOPE_VERSION,
261
+ packId: config.pack.id,
262
+ packVersion: config.pack.version,
263
+ artifactVersion,
264
+ publishedAt,
265
+ tools: sortById(tools),
266
+ guardrails: sortById(guardrails),
267
+ agents: sortById(agents),
268
+ flows: sortById(flows),
269
+ ...(env.value !== undefined && { env: env.value }),
270
+ };
271
+ const written = await atomicWriteJson(outputPath, index);
272
+ if (written.kind === 'err')
273
+ return written;
274
+ return {
275
+ kind: 'ok',
276
+ value: {
277
+ packId: config.pack.id,
278
+ packVersion: config.pack.version,
279
+ artifactVersion,
280
+ publishedAt,
281
+ counts: {
282
+ tools: tools.length,
283
+ guardrails: guardrails.length,
284
+ agents: agents.length,
285
+ flows: flows.length,
286
+ },
287
+ outputPath,
288
+ fileErrors,
289
+ },
290
+ };
291
+ }
292
+ // -----------------------------------------------------------------------
293
+ // CLI entry
294
+ // -----------------------------------------------------------------------
295
+ /**
296
+ * The `kindgi-index` command (process entry: `kindgi-index-main`) — a
297
+ * pack image's indexer stage runs it over the image's bundles. Args:
298
+ *
299
+ * --pack-dir <path> root of the pack (required)
300
+ * --config <path> explicit config file (optional)
301
+ * --output <path> explicit output path (optional)
302
+ * --bundle-map <path> index a build: the bundle map (JSON, optional)
303
+ * --module-root <path> where the bundle map's paths resolve (optional)
304
+ * --artifact-version <str> pin artifact version (optional)
305
+ * --published-at <iso> pin publishedAt (optional; enables reproducible builds)
306
+ * --help
307
+ * --version
308
+ */
309
+ export async function main(argv) {
310
+ const parsed = parseArgs(argv);
311
+ if (parsed.kind === 'help') {
312
+ process.stdout.write(`${HELP_TEXT}\n`);
313
+ return 0;
314
+ }
315
+ if (parsed.kind === 'version') {
316
+ process.stdout.write('kindgi-index v0.0.0\n');
317
+ return 0;
318
+ }
319
+ if (parsed.kind === 'err') {
320
+ process.stderr.write(`kindgi-index: ${parsed.error}\n`);
321
+ return 2;
322
+ }
323
+ let options = parsed.value.options;
324
+ if (parsed.value.bundleMapPath !== undefined) {
325
+ const bundleMap = await readBundleMap(parsed.value.bundleMapPath);
326
+ if (bundleMap.kind === 'err') {
327
+ process.stderr.write(`kindgi-index: ${bundleMap.message}\n`);
328
+ return 2;
329
+ }
330
+ options = { ...options, bundleMap: bundleMap.value };
331
+ }
332
+ const outcome = await runIndexer(options);
333
+ if (outcome.kind === 'err') {
334
+ process.stderr.write(`kindgi-index: ${outcome.error.code}: ${outcome.error.message}\n`);
335
+ return 1;
336
+ }
337
+ process.stdout.write(`${JSON.stringify(outcome.value, null, 2)}\n`);
338
+ return 0;
339
+ }
340
+ export const HELP_TEXT = `kindgi-index — build-time pack manifest indexer
341
+
342
+ Usage:
343
+ kindgi-index --pack-dir <path> [--config <path>] [--output <path>]
344
+ [--bundle-map <path> [--module-root <path>]]
345
+ [--artifact-version <str>] [--published-at <iso>]
346
+
347
+ Options:
348
+ --pack-dir <path> Root of the pack (required).
349
+ --config <path> Explicit kindgi.config.* path.
350
+ --output <path> Output path for index.json (default: <packDir>/index.json).
351
+ --bundle-map <path> Index a build: JSON mapping each source path to its bundle.
352
+ --module-root <path> Where the bundle map's paths resolve (default: --pack-dir).
353
+ --artifact-version <str> Pin artifact version (default: YYYYMMDD.N auto).
354
+ --published-at <iso> Pin publishedAt for reproducible builds.
355
+ --help Show this help.
356
+ --version Print version and exit.`;
357
+ function parseArgs(argv) {
358
+ let packDir;
359
+ let configPath;
360
+ let outputPath;
361
+ let artifactVersion;
362
+ let publishedAt;
363
+ let bundleMapPath;
364
+ let moduleRoot;
365
+ let i = 0;
366
+ while (i < argv.length) {
367
+ const arg = argv[i];
368
+ if (arg === undefined)
369
+ break;
370
+ if (arg === '--help' || arg === '-h')
371
+ return { kind: 'help' };
372
+ if (arg === '--version' || arg === '-v')
373
+ return { kind: 'version' };
374
+ const takeValue = () => {
375
+ const value = argv[i + 1];
376
+ if (value === undefined)
377
+ return { err: `${arg} requires a value` };
378
+ return value;
379
+ };
380
+ if (arg === '--pack-dir') {
381
+ const value = takeValue();
382
+ if (typeof value !== 'string')
383
+ return { kind: 'err', error: value.err };
384
+ packDir = value;
385
+ i += 2;
386
+ continue;
387
+ }
388
+ if (arg === '--config') {
389
+ const value = takeValue();
390
+ if (typeof value !== 'string')
391
+ return { kind: 'err', error: value.err };
392
+ configPath = value;
393
+ i += 2;
394
+ continue;
395
+ }
396
+ if (arg === '--output') {
397
+ const value = takeValue();
398
+ if (typeof value !== 'string')
399
+ return { kind: 'err', error: value.err };
400
+ outputPath = value;
401
+ i += 2;
402
+ continue;
403
+ }
404
+ if (arg === '--artifact-version') {
405
+ const value = takeValue();
406
+ if (typeof value !== 'string')
407
+ return { kind: 'err', error: value.err };
408
+ artifactVersion = value;
409
+ i += 2;
410
+ continue;
411
+ }
412
+ if (arg === '--published-at') {
413
+ const value = takeValue();
414
+ if (typeof value !== 'string')
415
+ return { kind: 'err', error: value.err };
416
+ publishedAt = value;
417
+ i += 2;
418
+ continue;
419
+ }
420
+ if (arg === '--bundle-map') {
421
+ const value = takeValue();
422
+ if (typeof value !== 'string')
423
+ return { kind: 'err', error: value.err };
424
+ bundleMapPath = value;
425
+ i += 2;
426
+ continue;
427
+ }
428
+ if (arg === '--module-root') {
429
+ const value = takeValue();
430
+ if (typeof value !== 'string')
431
+ return { kind: 'err', error: value.err };
432
+ moduleRoot = value;
433
+ i += 2;
434
+ continue;
435
+ }
436
+ return { kind: 'err', error: `Unknown argument: ${arg}` };
437
+ }
438
+ if (packDir === undefined) {
439
+ return { kind: 'err', error: '--pack-dir is required' };
440
+ }
441
+ if (moduleRoot !== undefined && bundleMapPath === undefined) {
442
+ return { kind: 'err', error: '--module-root goes with --bundle-map' };
443
+ }
444
+ const options = {
445
+ packDir,
446
+ ...(configPath !== undefined && { configPath }),
447
+ ...(outputPath !== undefined && { outputPath }),
448
+ ...(artifactVersion !== undefined && { artifactVersion }),
449
+ ...(publishedAt !== undefined && { publishedAt }),
450
+ ...(moduleRoot !== undefined && { moduleRoot }),
451
+ };
452
+ return {
453
+ kind: 'ok',
454
+ value: { options, ...(bundleMapPath !== undefined && { bundleMapPath }) },
455
+ };
456
+ }
457
+ /**
458
+ * A bundle map file: a JSON object of source path → bundle path, both
459
+ * strings (`kindgi build` writes `dist/bundle-map.json`).
460
+ */
461
+ export async function readBundleMap(file) {
462
+ let parsed;
463
+ try {
464
+ parsed = JSON.parse(await fs.readFile(file, 'utf8'));
465
+ }
466
+ catch (cause) {
467
+ return { kind: 'err', message: `Cannot read the bundle map ${file}: ${stringifyError(cause)}` };
468
+ }
469
+ if (parsed === null ||
470
+ typeof parsed !== 'object' ||
471
+ Array.isArray(parsed) ||
472
+ !Object.values(parsed).every((v) => typeof v === 'string' && v !== '')) {
473
+ return {
474
+ kind: 'err',
475
+ message: `The bundle map ${file} must be a JSON object of source path → bundle path.`,
476
+ };
477
+ }
478
+ return { kind: 'ok', value: parsed };
479
+ }
480
+ // -----------------------------------------------------------------------
481
+ // Config loading
482
+ // -----------------------------------------------------------------------
483
+ /** Config file names looked up at a pack root, in order. */
484
+ export const KINDGI_CONFIG_FILENAMES = [
485
+ 'kindgi.config.ts',
486
+ // `.mts` is always ESM — the config name for a CommonJS app, where a
487
+ // `.ts` config would be reparsed with a Node warning.
488
+ 'kindgi.config.mts',
489
+ 'kindgi.config.mjs',
490
+ 'kindgi.config.js',
491
+ 'kindgi.config.cjs',
492
+ ];
493
+ /**
494
+ * Load and minimally validate a pack's `kindgi.config.*` — the one
495
+ * loader behind the indexer and any tooling that reads pack config.
496
+ * Never throws:
497
+ * `config-not-found` when no config file exists, `config-parse-failed`
498
+ * (with the underlying cause) when it exists but cannot be imported or
499
+ * lacks `pack.id` / `pack.version`. Imports are cache-busted by mtime so
500
+ * a long-running process sees edits.
501
+ */
502
+ export async function loadKindgiConfig(packDir, options = {}) {
503
+ return loadConfig(path.resolve(packDir), options.configPath, options.importModule ?? defaultImportModule);
504
+ }
505
+ /** `pyproject.toml` — a Python pack keeps its config in the `[tool.kindgi]` table. */
506
+ export const PYPROJECT_FILENAME = 'pyproject.toml';
507
+ /**
508
+ * Where a pack's config lives: the first `kindgi.config.*` at `packDir`
509
+ * (`KINDGI_CONFIG_FILENAMES` order), else a `pyproject.toml` with a
510
+ * `[tool.kindgi]` table. `undefined` when neither — a `pyproject.toml`
511
+ * without the table is a Python project, not a pack.
512
+ */
513
+ export async function findKindgiConfig(packDir) {
514
+ for (const name of KINDGI_CONFIG_FILENAMES) {
515
+ const candidate = path.join(packDir, name);
516
+ if (await exists(candidate))
517
+ return { path: candidate, format: 'module' };
518
+ }
519
+ const pyproject = path.join(packDir, PYPROJECT_FILENAME);
520
+ let text;
521
+ try {
522
+ text = await fs.readFile(pyproject, 'utf8');
523
+ }
524
+ catch {
525
+ return undefined;
526
+ }
527
+ try {
528
+ const tool = parseToml(text).tool;
529
+ return isObject(tool) && 'kindgi' in tool
530
+ ? { path: pyproject, format: 'pyproject' }
531
+ : undefined;
532
+ }
533
+ catch {
534
+ // Unparsable: a pack only if it clearly meant to be one (the loader reports the error).
535
+ return /^\s*\[tool\.kindgi[\].]/m.test(text)
536
+ ? { path: pyproject, format: 'pyproject' }
537
+ : undefined;
538
+ }
539
+ }
540
+ async function exists(file) {
541
+ try {
542
+ await fs.access(file);
543
+ return true;
544
+ }
545
+ catch {
546
+ return false;
547
+ }
548
+ }
549
+ async function loadConfig(packDir, explicitPath, importModule) {
550
+ let file;
551
+ if (explicitPath !== undefined) {
552
+ const resolved = path.resolve(explicitPath);
553
+ if (await exists(resolved)) {
554
+ const format = path.basename(resolved) === PYPROJECT_FILENAME ? 'pyproject' : 'module';
555
+ file = { path: resolved, format };
556
+ }
557
+ }
558
+ else {
559
+ file = await findKindgiConfig(packDir);
560
+ }
561
+ if (file === undefined) {
562
+ return {
563
+ kind: 'err',
564
+ error: {
565
+ code: 'config-not-found',
566
+ message: `No kindgi.config.{ts,mts,mjs,js,cjs}, or pyproject.toml with a [tool.kindgi] table, found at pack root ${packDir}`,
567
+ },
568
+ };
569
+ }
570
+ const read = file.format === 'pyproject'
571
+ ? await readPyprojectTable(file.path)
572
+ : await importConfigModule(file.path, importModule);
573
+ if (read.kind === 'err')
574
+ return read;
575
+ const checked = checkConfig(read.value, file.path);
576
+ if (checked.kind === 'err')
577
+ return checked;
578
+ const record = checked.value;
579
+ return {
580
+ kind: 'ok',
581
+ value: file.format === 'pyproject' && record.language === undefined
582
+ ? { ...record, language: 'python' }
583
+ : record,
584
+ };
585
+ }
586
+ function parseFailed(filePath, message, cause) {
587
+ return {
588
+ kind: 'err',
589
+ error: {
590
+ code: 'config-parse-failed',
591
+ message,
592
+ filePath,
593
+ ...(cause !== undefined && { cause: serializeCause(cause) }),
594
+ },
595
+ };
596
+ }
597
+ async function importConfigModule(filePath, importModule) {
598
+ let module_;
599
+ try {
600
+ module_ = await importModule(pathToFileURL(filePath).href);
601
+ }
602
+ catch (cause) {
603
+ return parseFailed(filePath, `Failed to import ${filePath}: ${stringifyError(cause)}`, cause);
604
+ }
605
+ const defExport = extractDefaultExport(module_);
606
+ if (defExport === undefined || typeof defExport !== 'object' || defExport === null) {
607
+ return parseFailed(filePath, `Config file ${filePath} has no default export (or it is not an object)`);
608
+ }
609
+ return { kind: 'ok', value: defExport };
610
+ }
611
+ async function readPyprojectTable(filePath) {
612
+ let document;
613
+ try {
614
+ document = parseToml(await fs.readFile(filePath, 'utf8'));
615
+ }
616
+ catch (cause) {
617
+ return parseFailed(filePath, `Failed to read ${filePath}: ${stringifyError(cause)}`, cause);
618
+ }
619
+ const tool = document.tool;
620
+ const table = isObject(tool) ? tool.kindgi : undefined;
621
+ if (!isObject(table)) {
622
+ return {
623
+ kind: 'err',
624
+ error: {
625
+ code: 'config-not-found',
626
+ message: `${filePath} has no [tool.kindgi] table`,
627
+ filePath,
628
+ },
629
+ };
630
+ }
631
+ return { kind: 'ok', value: table };
632
+ }
633
+ /** The checks every config passes, whatever file it came from. */
634
+ function checkConfig(record, filePath) {
635
+ const pack = record.pack;
636
+ if (typeof pack !== 'object' || pack === null) {
637
+ return parseFailed(filePath, `Config file ${filePath}: 'pack' field is missing or not an object`);
638
+ }
639
+ const packRec = pack;
640
+ if (typeof packRec.id !== 'string' || packRec.id.length === 0) {
641
+ return parseFailed(filePath, `Config file ${filePath}: 'pack.id' is missing or not a non-empty string`);
642
+ }
643
+ if (typeof packRec.version !== 'string' || packRec.version.length === 0) {
644
+ return parseFailed(filePath, `Config file ${filePath}: 'pack.version' is missing or not a non-empty string`);
645
+ }
646
+ if (record.language !== undefined && record.language !== 'node' && record.language !== 'python') {
647
+ return parseFailed(filePath, `Config file ${filePath}: 'language' must be "node" or "python"`);
648
+ }
649
+ return { kind: 'ok', value: record };
650
+ }
651
+ const DEFAULT_FOLDERS = {
652
+ tool: 'tools',
653
+ guardrail: 'guardrails',
654
+ agent: 'agents',
655
+ flow: 'flows',
656
+ };
657
+ async function discoverFiles(packDir, discovery) {
658
+ const seen = new Map();
659
+ for (const { kind, pattern } of kindPatterns(discovery)) {
660
+ for (const rel of await globMatch(packDir, pattern))
661
+ classifyDiscovered(seen, rel, kind);
662
+ }
663
+ return sortedDiscovered(seen);
664
+ }
665
+ /** A bundle map's source paths, classified as `discoverFiles` classifies what it finds. */
666
+ function discoverInBundleMap(bundleMap, discovery) {
667
+ const sources = Object.keys(bundleMap).sort();
668
+ const seen = new Map();
669
+ for (const { kind, pattern } of kindPatterns(discovery)) {
670
+ const regex = globToRegex(pattern);
671
+ for (const rel of sources)
672
+ if (regex.test(rel))
673
+ classifyDiscovered(seen, rel, kind);
674
+ }
675
+ return sortedDiscovered(seen);
676
+ }
677
+ function kindPatterns(discovery) {
678
+ return [
679
+ { kind: 'tool', pattern: discovery.tools },
680
+ { kind: 'guardrail', pattern: discovery.guardrails },
681
+ { kind: 'agent', pattern: discovery.agents },
682
+ { kind: 'flow', pattern: discovery.flows },
683
+ ];
684
+ }
685
+ /** Record `rel`, matched by `kind`'s pattern, unless it's a test file or already seen. */
686
+ function classifyDiscovered(seen, rel, kind) {
687
+ // Skip test / spec files — they live next to the primitive
688
+ // (`tools/echo/index.ts` + `tools/echo/index.test.ts`)
689
+ // and would break the indexer if imported as a primitive
690
+ // module. Matches conventional test-file suffixes across
691
+ // vitest / jest / node:test.
692
+ if (TEST_FILE_REGEX.test(rel))
693
+ return;
694
+ if (seen.has(rel))
695
+ return;
696
+ const firstSegment = rel.split('/')[0];
697
+ const inDefaultFolder = firstSegment === DEFAULT_FOLDERS[kind];
698
+ seen.set(rel, inDefaultFolder ? kind : undefined);
699
+ }
700
+ function sortedDiscovered(seen) {
701
+ const out = [];
702
+ for (const [relPath, expectedKind] of seen)
703
+ out.push({ relPath, expectedKind });
704
+ out.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
705
+ return out;
706
+ }
707
+ /**
708
+ * Minimal glob matcher supporting `**` (any depth), `*` (single-segment
709
+ * wildcard), and `{a,b,c}` alternation. Sufficient for the default
710
+ * discovery patterns and typical custom overrides. Negations and
711
+ * extglob patterns are not supported.
712
+ */
713
+ async function globMatch(root, pattern) {
714
+ const regex = globToRegex(pattern);
715
+ const matches = [];
716
+ // Walk only the pattern's static prefix — a pack embedded in a large
717
+ // app (`kindgi/tools/**`) must not crawl the whole host repo.
718
+ await walk(root, globStaticPrefix(pattern), matches, regex);
719
+ return matches;
720
+ }
721
+ async function walk(root, rel, matches, regex) {
722
+ const abs = rel === '' ? root : path.join(root, rel);
723
+ let entries;
724
+ try {
725
+ entries = await fs.readdir(abs, { withFileTypes: true });
726
+ }
727
+ catch {
728
+ return;
729
+ }
730
+ for (const entry of entries) {
731
+ if (entry.name === 'node_modules' || entry.name.startsWith('.'))
732
+ continue;
733
+ const childRel = rel === '' ? entry.name : `${rel}/${entry.name}`;
734
+ if (entry.isDirectory()) {
735
+ await walk(root, childRel, matches, regex);
736
+ }
737
+ else if (entry.isFile()) {
738
+ if (regex.test(childRel))
739
+ matches.push(childRel);
740
+ }
741
+ }
742
+ }
743
+ // -----------------------------------------------------------------------
744
+ // Module import + default export extraction
745
+ // -----------------------------------------------------------------------
746
+ /**
747
+ * Cache-busted dynamic import. Node's module cache is keyed by URL, so
748
+ * a plain `import(fileUrl)` inside a long-running process (a dev watch
749
+ * loop) returns the *originally loaded* module even after the author
750
+ * edits the file on disk — the indexer would then re-emit stale
751
+ * manifests every watch tick.
752
+ *
753
+ * Fix: append `?v=<mtimeMs>` to the URL. Node treats each distinct URL
754
+ * as a distinct module entry, so an edited file (with a newer mtime)
755
+ * triggers a fresh load; an unchanged file keeps the same URL and hits
756
+ * the cache — no wasted work, no memory churn.
757
+ *
758
+ * Best-effort: if `fs.stat` fails (e.g. an unusual URL scheme or a
759
+ * race), fall back to the plain `import()` so the indexer still
760
+ * completes — logged behavior remains the same, just without cache
761
+ * invalidation.
762
+ */
763
+ async function defaultImportModule(fileUrl) {
764
+ try {
765
+ const abs = fileURLToPath(fileUrl);
766
+ const { mtimeMs } = await fs.stat(abs);
767
+ return await import(`${fileUrl}?v=${mtimeMs}`);
768
+ }
769
+ catch {
770
+ return await import(fileUrl);
771
+ }
772
+ }
773
+ function extractDefaultExport(module_) {
774
+ if (module_ === null || typeof module_ !== 'object')
775
+ return undefined;
776
+ const record = module_;
777
+ return record.default;
778
+ }
779
+ // -----------------------------------------------------------------------
780
+ // Result-wrapper handling
781
+ // -----------------------------------------------------------------------
782
+ function detectResultError(value) {
783
+ if (value === null || typeof value !== 'object')
784
+ return undefined;
785
+ const record = value;
786
+ if (record.kind === 'err' && 'error' in record)
787
+ return record.error;
788
+ return undefined;
789
+ }
790
+ function unwrapResult(value) {
791
+ if (value === null || typeof value !== 'object')
792
+ return value;
793
+ const record = value;
794
+ if (record.kind === 'ok' && 'value' in record)
795
+ return record.value;
796
+ return value;
797
+ }
798
+ function resultErrorMessage(err) {
799
+ if (err === null || typeof err !== 'object')
800
+ return String(err);
801
+ const rec = err;
802
+ if (typeof rec.message === 'string')
803
+ return rec.message;
804
+ return JSON.stringify(err);
805
+ }
806
+ // -----------------------------------------------------------------------
807
+ // Structural kind detection
808
+ // -----------------------------------------------------------------------
809
+ function detectKind(value) {
810
+ if (value === null || typeof value !== 'object')
811
+ return undefined;
812
+ const rec = value;
813
+ if (typeof rec.handler === 'function' && ('input' in rec || 'inputZod' in rec)) {
814
+ return 'tool';
815
+ }
816
+ const isGuardrailAction = typeof rec.action === 'object' &&
817
+ rec.action !== null &&
818
+ 'on-violation' in rec.action;
819
+ if (isGuardrailAction && 'kind' in rec)
820
+ return 'guardrail';
821
+ if (typeof rec.instructions === 'string' && Array.isArray(rec.tools))
822
+ return 'agent';
823
+ if (Array.isArray(rec.nodes) && Array.isArray(rec.edges))
824
+ return 'flow';
825
+ return undefined;
826
+ }
827
+ // -----------------------------------------------------------------------
828
+ // Manifest builders
829
+ // -----------------------------------------------------------------------
830
+ function buildTool(raw, relPath, converter) {
831
+ const rec = raw;
832
+ if (typeof rec.id !== 'string') {
833
+ return manifestErr(relPath, `'id' is missing or not a string`);
834
+ }
835
+ const inputWire = resolveSchema(rec, 'input', 'inputZod', relPath, converter);
836
+ if (inputWire.kind === 'err')
837
+ return inputWire;
838
+ const outputWire = resolveSchema(rec, 'output', 'outputZod', relPath, converter);
839
+ if (outputWire.kind === 'err')
840
+ return outputWire;
841
+ const tool = {
842
+ id: rec.id,
843
+ ...(typeof rec.description === 'string' && { description: rec.description }),
844
+ ...(typeof rec.version === 'string' && { version: rec.version }),
845
+ input: inputWire.value,
846
+ output: outputWire.value,
847
+ ...(Array.isArray(rec.effects) && {
848
+ effects: rec.effects,
849
+ }),
850
+ ...(Array.isArray(rec.needs) && {
851
+ needs: rec.needs,
852
+ }),
853
+ ...(isObject(rec.needsSpec) && {
854
+ needsSpec: rec.needsSpec,
855
+ }),
856
+ ...(typeof rec.sandbox === 'string' && { sandbox: rec.sandbox }),
857
+ ...(isObject(rec.limits) && {
858
+ limits: rec.limits,
859
+ }),
860
+ ...(isObject(rec.network) && {
861
+ network: rec.network,
862
+ }),
863
+ ...(isObject(rec.spec) && { spec: rec.spec }),
864
+ ...(typeof rec.mutating === 'boolean' && { mutating: rec.mutating }),
865
+ modulePath: normalizeModulePath(relPath),
866
+ };
867
+ return { kind: 'ok', value: tool };
868
+ }
869
+ function buildGuardrail(raw, relPath, converter) {
870
+ const rec = raw;
871
+ if (typeof rec.id !== 'string') {
872
+ return manifestErr(relPath, `'id' is missing or not a string`);
873
+ }
874
+ if (typeof rec.kind !== 'string') {
875
+ return manifestErr(relPath, `'kind' is missing or not a string`);
876
+ }
877
+ if (!isObject(rec.action)) {
878
+ return manifestErr(relPath, `'action' is missing or not an object`);
879
+ }
880
+ // configSchema resolution: look in three places, in priority order:
881
+ // 1. guardrail.configZod (top-level, DefinedCheck-style)
882
+ // 2. guardrail.check.configZod (nested inside the check object)
883
+ // 3. guardrail.configSchema / guardrail.check.configSchema (already JSON Schema)
884
+ const check = isObject(rec.check) ? rec.check : undefined;
885
+ const zodConfig = (isZodSchema(rec.configZod) ? rec.configZod : undefined) ??
886
+ (check && isZodSchema(check.configZod) ? check.configZod : undefined);
887
+ let configSchema;
888
+ if (zodConfig !== undefined) {
889
+ const converted = toJSONSchemaSync(zodConfig, converter, 'input');
890
+ if (converted.kind === 'err') {
891
+ return {
892
+ kind: 'err',
893
+ error: {
894
+ code: 'zod-conversion-failed',
895
+ message: `Guardrail ${rec.id} configSchema Zod conversion failed: ${converted.error.message}`,
896
+ filePath: relPath,
897
+ field: 'configSchema',
898
+ cause: converted.error.cause,
899
+ },
900
+ };
901
+ }
902
+ configSchema = converted.value;
903
+ }
904
+ else if (isObject(rec.configSchema)) {
905
+ configSchema = rec.configSchema;
906
+ }
907
+ else if (check !== undefined && isObject(check.configSchema)) {
908
+ configSchema = check.configSchema;
909
+ }
910
+ else if (check !== undefined && isObject(check.configJsonSchema)) {
911
+ // `defineCheck` (guardrails pkg) already stores the converted JSON
912
+ // Schema on `configJsonSchema` — accept that shape without a second
913
+ // conversion pass.
914
+ configSchema = check.configJsonSchema;
915
+ }
916
+ const checkId = typeof rec.check === 'string'
917
+ ? rec.check
918
+ : check !== undefined && typeof check.id === 'string'
919
+ ? check.id
920
+ : undefined;
921
+ const guardrail = {
922
+ id: rec.id,
923
+ ...(typeof rec.name === 'string' && { name: rec.name }),
924
+ kind: rec.kind,
925
+ action: rec.action,
926
+ ...(typeof rec.severity === 'string' && { severity: rec.severity }),
927
+ ...(isObject(rec.scope) && {
928
+ scope: rec.scope,
929
+ }),
930
+ checkModulePath: normalizeModulePath(relPath),
931
+ ...(checkId !== undefined && { checkId }),
932
+ ...(configSchema !== undefined && { configSchema }),
933
+ ...(isObject(rec.config) && { config: rec.config }),
934
+ ...(typeof rec.sandbox === 'string' && { sandbox: rec.sandbox }),
935
+ ...(isObject(rec.limits) && {
936
+ limits: rec.limits,
937
+ }),
938
+ ...(isObject(rec.network) && {
939
+ network: rec.network,
940
+ }),
941
+ };
942
+ return { kind: 'ok', value: guardrail };
943
+ }
944
+ function buildAgent(raw, relPath) {
945
+ const rec = raw;
946
+ if (typeof rec.id !== 'string') {
947
+ return manifestErr(relPath, `'id' is missing or not a string`);
948
+ }
949
+ if (typeof rec.version !== 'string') {
950
+ return manifestErr(relPath, `'version' is missing or not a string`);
951
+ }
952
+ if (typeof rec.name !== 'string') {
953
+ return manifestErr(relPath, `'name' is missing or not a string`);
954
+ }
955
+ if (typeof rec.instructions !== 'string') {
956
+ return manifestErr(relPath, `'instructions' is missing or not a string`);
957
+ }
958
+ if (!Array.isArray(rec.capabilities)) {
959
+ return manifestErr(relPath, `'capabilities' is missing or not an array`);
960
+ }
961
+ if (!Array.isArray(rec.tools)) {
962
+ return manifestErr(relPath, `'tools' is missing or not an array`);
963
+ }
964
+ const agent = {
965
+ id: rec.id,
966
+ version: rec.version,
967
+ name: rec.name,
968
+ instructions: rec.instructions,
969
+ capabilities: rec.capabilities,
970
+ tools: rec.tools,
971
+ ...optionalAgentFields(rec),
972
+ modulePath: normalizeModulePath(relPath),
973
+ };
974
+ return { kind: 'ok', value: agent };
975
+ }
976
+ /** The agent's optional fields, carried when present with the right shape. */
977
+ function optionalAgentFields(rec) {
978
+ const out = {};
979
+ for (const key of ['retrieval', 'guardrails', 'parameters', 'tags']) {
980
+ if (Array.isArray(rec[key]))
981
+ out[key] = rec[key];
982
+ }
983
+ for (const key of ['budget', 'conversationPolicy', 'output', 'toolErrors']) {
984
+ if (isObject(rec[key]))
985
+ out[key] = rec[key];
986
+ }
987
+ for (const key of ['preferredProvider', 'preferredModel', 'description']) {
988
+ if (typeof rec[key] === 'string')
989
+ out[key] = rec[key];
990
+ }
991
+ return out;
992
+ }
993
+ /**
994
+ * Validate a discovered flow with the same loader the API uses, so a
995
+ * broken flow fails indexing with the loader's message instead of
996
+ * failing later at publish or run time, and carry every field it has.
997
+ */
998
+ function buildGraph(raw, relPath) {
999
+ const loaded = loadFlow(raw);
1000
+ if (loaded.kind === 'err')
1001
+ return manifestErr(relPath, loaded.error.message);
1002
+ const f = loaded.value;
1003
+ const flow = {
1004
+ id: f.id,
1005
+ version: f.version,
1006
+ ...(f.name !== undefined && { name: f.name }),
1007
+ ...(f.description !== undefined && { description: f.description }),
1008
+ nodes: f.nodes,
1009
+ edges: f.edges,
1010
+ ...(f.maxParallelism !== undefined && { maxParallelism: f.maxParallelism }),
1011
+ ...(f.metadata !== undefined && { metadata: f.metadata }),
1012
+ ...(f.output !== undefined && { output: f.output }),
1013
+ kernelPayloadVersion: KERNEL_PAYLOAD_VERSION,
1014
+ modulePath: normalizeModulePath(relPath),
1015
+ };
1016
+ return { kind: 'ok', value: flow };
1017
+ }
1018
+ function manifestErr(relPath, msg) {
1019
+ return {
1020
+ kind: 'err',
1021
+ error: {
1022
+ code: 'manifest-validation-failed',
1023
+ message: `${relPath}: ${msg}`,
1024
+ filePath: relPath,
1025
+ },
1026
+ };
1027
+ }
1028
+ // -----------------------------------------------------------------------
1029
+ // Schema resolution — Zod → JSON Schema pass
1030
+ // -----------------------------------------------------------------------
1031
+ function resolveSchema(rec, wireField, zodField, relPath, converter) {
1032
+ // A tool's `input` is what a caller sends; its `output`, what it returns.
1033
+ const side = wireField;
1034
+ const zodValue = rec[zodField];
1035
+ if (zodValue !== undefined && isZodSchema(zodValue)) {
1036
+ const converted = toJSONSchemaSync(zodValue, converter, side);
1037
+ if (converted.kind === 'err') {
1038
+ return {
1039
+ kind: 'err',
1040
+ error: {
1041
+ code: 'zod-conversion-failed',
1042
+ message: `${relPath}: Zod conversion for '${zodField}' failed: ${converted.error.message}`,
1043
+ filePath: relPath,
1044
+ field: zodField,
1045
+ cause: converted.error.cause,
1046
+ },
1047
+ };
1048
+ }
1049
+ return { kind: 'ok', value: converted.value };
1050
+ }
1051
+ const wireValue = rec[wireField];
1052
+ // `wireValue` may itself be a Zod schema (author-time authored with Zod
1053
+ // but not yet run through defineTool). Convert lazily in that case.
1054
+ if (isZodSchema(wireValue)) {
1055
+ const converted = toJSONSchemaSync(wireValue, converter, side);
1056
+ if (converted.kind === 'err') {
1057
+ return {
1058
+ kind: 'err',
1059
+ error: {
1060
+ code: 'zod-conversion-failed',
1061
+ message: `${relPath}: Zod conversion for '${wireField}' failed: ${converted.error.message}`,
1062
+ filePath: relPath,
1063
+ field: wireField,
1064
+ cause: converted.error.cause,
1065
+ },
1066
+ };
1067
+ }
1068
+ return { kind: 'ok', value: converted.value };
1069
+ }
1070
+ if (!isObject(wireValue)) {
1071
+ return {
1072
+ kind: 'err',
1073
+ error: {
1074
+ code: 'manifest-validation-failed',
1075
+ message: `${relPath}: '${wireField}' is missing or not an object`,
1076
+ filePath: relPath,
1077
+ field: wireField,
1078
+ },
1079
+ };
1080
+ }
1081
+ return { kind: 'ok', value: wireValue };
1082
+ }
1083
+ // -----------------------------------------------------------------------
1084
+ // Output serialization — atomic write + auto artifact version
1085
+ // -----------------------------------------------------------------------
1086
+ async function atomicWriteJson(outputPath, index) {
1087
+ const tmpPath = `${outputPath}.tmp-${process.pid}-${Date.now()}`;
1088
+ const serialized = `${stableStringify(index)}\n`;
1089
+ try {
1090
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
1091
+ await fs.writeFile(tmpPath, serialized, 'utf8');
1092
+ await fs.rename(tmpPath, outputPath);
1093
+ }
1094
+ catch (cause) {
1095
+ // Best-effort tmp cleanup — swallow errors here (the original write
1096
+ // error is what the caller needs).
1097
+ try {
1098
+ await fs.unlink(tmpPath);
1099
+ }
1100
+ catch {
1101
+ // ignore
1102
+ }
1103
+ return {
1104
+ kind: 'err',
1105
+ error: {
1106
+ code: 'output-write-failed',
1107
+ message: `Failed to write ${outputPath}: ${stringifyError(cause)}`,
1108
+ filePath: outputPath,
1109
+ cause: serializeCause(cause),
1110
+ },
1111
+ };
1112
+ }
1113
+ return { kind: 'ok', value: undefined };
1114
+ }
1115
+ /**
1116
+ * Stable JSON serializer — keys sorted at every level. Guarantees
1117
+ * byte-identical output for byte-identical semantic input, which is the
1118
+ * property the local integrity gate depends on.
1119
+ */
1120
+ function stableStringify(value) {
1121
+ return JSON.stringify(canonicalize(value), null, 2);
1122
+ }
1123
+ function canonicalize(value) {
1124
+ if (value === null || typeof value !== 'object')
1125
+ return value;
1126
+ if (Array.isArray(value))
1127
+ return value.map(canonicalize);
1128
+ const rec = value;
1129
+ const out = {};
1130
+ for (const key of Object.keys(rec).sort()) {
1131
+ out[key] = canonicalize(rec[key]);
1132
+ }
1133
+ return out;
1134
+ }
1135
+ async function computeAutoArtifactVersion(outputPath) {
1136
+ const today = new Date();
1137
+ const yyyymmdd = today.getUTCFullYear().toString().padStart(4, '0') +
1138
+ (today.getUTCMonth() + 1).toString().padStart(2, '0') +
1139
+ today.getUTCDate().toString().padStart(2, '0');
1140
+ let n = 1;
1141
+ try {
1142
+ const existing = await fs.readFile(outputPath, 'utf8');
1143
+ const parsed = JSON.parse(existing);
1144
+ if (typeof parsed.artifactVersion === 'string') {
1145
+ const match = /^(\d{8})\.(\d+)$/.exec(parsed.artifactVersion);
1146
+ const dayPart = match?.[1];
1147
+ const nPart = match?.[2];
1148
+ if (dayPart === yyyymmdd && nPart !== undefined) {
1149
+ const parsedN = Number.parseInt(nPart, 10);
1150
+ if (Number.isFinite(parsedN))
1151
+ n = parsedN + 1;
1152
+ }
1153
+ }
1154
+ }
1155
+ catch {
1156
+ // No prior index.json — start at .1
1157
+ }
1158
+ return `${yyyymmdd}.${n}`;
1159
+ }
1160
+ // -----------------------------------------------------------------------
1161
+ // Utilities
1162
+ // -----------------------------------------------------------------------
1163
+ function isObject(v) {
1164
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
1165
+ }
1166
+ function sortById(items) {
1167
+ return [...items].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
1168
+ }
1169
+ function stringifyError(err) {
1170
+ if (err instanceof Error)
1171
+ return `${err.name}: ${err.message}`;
1172
+ return String(err);
1173
+ }
1174
+ function serializeCause(err) {
1175
+ if (err instanceof Error) {
1176
+ return { name: err.name, message: err.message, stack: err.stack };
1177
+ }
1178
+ if (err === undefined)
1179
+ return null;
1180
+ if (err === null ||
1181
+ typeof err === 'string' ||
1182
+ typeof err === 'number' ||
1183
+ typeof err === 'boolean') {
1184
+ return err;
1185
+ }
1186
+ try {
1187
+ JSON.stringify(err);
1188
+ return err;
1189
+ }
1190
+ catch {
1191
+ return String(err);
1192
+ }
1193
+ }
1194
+ function normalizeModulePath(relPath) {
1195
+ // Forward slashes only, no leading './'
1196
+ let normalized = relPath.replace(/\\/g, '/');
1197
+ if (normalized.startsWith('./'))
1198
+ normalized = normalized.slice(2);
1199
+ return normalized;
1200
+ }
1201
+ //# sourceMappingURL=kindgi-index.js.map