okengine 0.1.6 → 0.2.2

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 (80) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +36 -29
  3. package/docs/spec/example.md +1187 -0
  4. package/docs/spec/unified-theory.md +1 -1
  5. package/package.json +20 -6
  6. package/src/cli/dev-app-runner.ts +6 -2
  7. package/src/cli/dev.ts +19 -10
  8. package/src/cli/doc-drift.ts +54 -21
  9. package/src/console/index.ts +25 -13
  10. package/src/console/server/app.ts +44 -7
  11. package/src/console/server/claim.test.ts +7 -4
  12. package/src/console/server/claim.ts +2 -7
  13. package/src/console/server/flows.ts +2 -6
  14. package/src/console/server/index.ts +2 -1
  15. package/src/console/server/lazy-panels.test.ts +27 -0
  16. package/src/console/server/panel-load.ts +28 -0
  17. package/src/console/server/plugin.ts +1 -1
  18. package/src/console/server/plugins.ts +7 -6
  19. package/src/console/server/public-flows.ts +12 -0
  20. package/src/console/server/state.ts +159 -122
  21. package/src/console/server/store.ts +13 -10
  22. package/src/console/ui/dist/assets/index-B71Yl_SS.js +10 -0
  23. package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
  24. package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
  25. package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
  26. package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
  27. package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
  28. package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
  29. package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
  30. package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
  31. package/src/console/ui/dist/assets/panel-overview-Bd48d9km.js +1 -0
  32. package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
  33. package/src/console/ui/dist/assets/panel-runs-BwsWqKeB.js +1 -0
  34. package/src/console/ui/dist/assets/panel-signals-9najbZY2.js +1 -0
  35. package/src/console/ui/dist/assets/panel-store-OHkP2pDp.js +1 -0
  36. package/src/console/ui/dist/assets/panel-traces-tn2JoY8U.js +1 -0
  37. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
  38. package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
  39. package/src/console/ui/dist/assets/style-Cnl7WLya.css +3 -0
  40. package/src/console/ui/dist/index.html +14 -0
  41. package/src/docs-origin.ts +19 -0
  42. package/src/drivers/index.ts +1 -6
  43. package/src/drivers/vault-sops.ts +20 -1
  44. package/src/kernel/app.ts +1 -1
  45. package/src/kernel/boot-bind/ai.ts +31 -0
  46. package/src/kernel/boot-bind/channel.ts +27 -0
  47. package/src/kernel/boot-bind/clock.ts +74 -0
  48. package/src/kernel/boot-bind/gate.ts +28 -0
  49. package/src/kernel/boot-bind/runs.ts +28 -0
  50. package/src/kernel/boot-bind/signal.ts +68 -0
  51. package/src/kernel/boot-bind/store.ts +47 -0
  52. package/src/kernel/boot-bind/vault.ts +36 -0
  53. package/src/kernel/boot.test.ts +85 -1
  54. package/src/kernel/boot.ts +250 -212
  55. package/src/kernel/errors.registry.test.ts +4 -2
  56. package/src/kernel/errors.ts +3 -1
  57. package/src/kernel/fx.test.ts +2 -2
  58. package/src/kernel/index.ts +2 -0
  59. package/src/mcp/data.ts +1 -0
  60. package/src/mcp/docs-index.ts +252 -0
  61. package/src/mcp/docs-mcp.test.ts +176 -0
  62. package/src/mcp/docs-server.ts +233 -0
  63. package/src/mcp/docs-tools.ts +143 -0
  64. package/src/mcp/index.ts +29 -5
  65. package/src/mcp/protocol.ts +2 -1
  66. package/src/release/exports.test.ts +71 -0
  67. package/src/release/exports.ts +156 -0
  68. package/src/release/index.ts +21 -0
  69. package/src/release/limits.ts +9 -0
  70. package/src/release/measure.exports.test.ts +82 -0
  71. package/src/release/measure.ts +297 -14
  72. package/src/release/publish.ts +14 -3
  73. package/src/release/readme.test.ts +61 -0
  74. package/src/release/readme.ts +13 -0
  75. package/src/runtime/index.ts +1 -0
  76. package/src/runtime/security.test.ts +6 -1
  77. package/src/runtime/types.ts +6 -0
  78. package/src/term.test.ts +51 -0
  79. package/src/term.ts +158 -0
  80. package/src/test/create-test-app.ts +2 -0
@@ -2,18 +2,55 @@
2
2
  * Budget measurement helpers — shared by gates and the budgets.json publisher.
3
3
  */
4
4
 
5
- import { readdir } from "node:fs/promises";
6
- import { join } from "node:path";
5
+ import { mkdtemp, readdir, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { join, resolve } from "node:path";
7
8
  import { createRouter } from "../kernel/router.ts";
9
+ import {
10
+ type BudgetGroup,
11
+ resolveExportBudgetTargets,
12
+ } from "./exports.ts";
8
13
  import {
9
14
  CLIENT_BUDGET_BYTES,
10
15
  COLD_START_BUDGET_MS,
11
16
  CONSOLE_BUDGET_BYTES,
17
+ EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES,
18
+ EXPORT_REGRESSION_TOLERANCE_RATIO,
12
19
  KERNEL_EDGE_BUDGET_BYTES,
13
20
  ROUTING_P99_BUDGET_MS,
14
21
  } from "./limits.ts";
15
22
 
16
23
  const ROOT = `${import.meta.dir}/../..`;
24
+ const BUDGETS_JSON_PATH = resolve(ROOT, "budgets.json");
25
+
26
+ /**
27
+ * Deps measured separately from OKE code in export samples.
28
+ * Edge/client absolute gates keep the historical smaller external list.
29
+ */
30
+ const BUILD_EXTERNALS = ["@duckdb/node-api", "@duckdb/*"] as const;
31
+
32
+ /** Heavy optional / peer packages — not counted in export:* gzip samples. */
33
+ const EXPORT_BUILD_EXTERNALS = [
34
+ ...BUILD_EXTERNALS,
35
+ "age-encryption",
36
+ "sently",
37
+ "sently/*",
38
+ "ajv",
39
+ "ajv/*",
40
+ "ajv-formats",
41
+ "oxc-parser",
42
+ "zod",
43
+ ] as const;
44
+
45
+ /** How a sample is gated. */
46
+ export type BudgetGate = "absolute" | "regression";
47
+
48
+ /** Report section headings. */
49
+ const GROUP_HEADINGS: Readonly<Record<BudgetGroup, string>> = {
50
+ core: "Core",
51
+ exports: "Exports",
52
+ drivers: "Drivers",
53
+ };
17
54
 
18
55
  /** One measured budget sample. */
19
56
  export interface BudgetSample {
@@ -23,11 +60,15 @@ export interface BudgetSample {
23
60
  readonly label: string;
24
61
  /** Measured value. */
25
62
  readonly value: number;
26
- /** Hard limit (exclusive upper bound). */
63
+ /** Upper bound used for `ok` (absolute cap or regression ceiling). */
27
64
  readonly limit: number;
28
65
  /** Unit for display / snapshot. */
29
66
  readonly unit: "bytes" | "ms";
30
- /** True when `value < limit`. */
67
+ /** Gate mode. */
68
+ readonly gate: BudgetGate;
69
+ /** Report section. */
70
+ readonly group: BudgetGroup;
71
+ /** True when under the gate's limit (or first baseline for regression). */
31
72
  readonly ok: boolean;
32
73
  }
33
74
 
@@ -41,18 +82,69 @@ export interface BudgetsSnapshot {
41
82
  readonly budgets: readonly BudgetSample[];
42
83
  }
43
84
 
85
+ /**
86
+ * Gzip size of all JS artifacts from a Bun.build result.
87
+ *
88
+ * @param result - Successful build
89
+ * @param label - Error context
90
+ */
91
+ async function gzipBuildOutputs(
92
+ result: Awaited<ReturnType<typeof Bun.build>>,
93
+ label: string,
94
+ ): Promise<number> {
95
+ if (!result.success) {
96
+ throw new Error(
97
+ `${label} build failed:\n${result.logs.map(String).join("\n")}`,
98
+ );
99
+ }
100
+ let total = 0;
101
+ for (const artifact of result.outputs) {
102
+ const path = artifact.path;
103
+ if (path && !/\.(m?js|cjs)$/.test(path) && artifact.kind === "asset") {
104
+ continue;
105
+ }
106
+ const raw = await artifact.arrayBuffer();
107
+ if (raw.byteLength === 0) continue;
108
+ total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
109
+ }
110
+ if (total <= 0) throw new Error(`${label} build produced no JS output`);
111
+ return total;
112
+ }
113
+
114
+ /**
115
+ * Gzip size of a minified ESM bundle for one entry file.
116
+ *
117
+ * @param entry - Absolute path to the TypeScript entry
118
+ * @param label - Error context
119
+ * @param external - Packages left external (not inlined)
120
+ */
121
+ export async function measureEntryGzipBytes(
122
+ entry: string,
123
+ label = entry,
124
+ external: readonly string[] = BUILD_EXTERNALS,
125
+ ): Promise<number> {
126
+ const result = await Bun.build({
127
+ entrypoints: [entry],
128
+ minify: true,
129
+ target: "bun",
130
+ format: "esm",
131
+ external: [...external],
132
+ });
133
+ return gzipBuildOutputs(result, label);
134
+ }
135
+
44
136
  /**
45
137
  * Gzip size of the kernel edge profile bundle.
46
138
  */
47
139
  export async function measureKernelEdgeGzipBytes(): Promise<number> {
48
140
  const entry = `${import.meta.dir}/../kernel/budget-entry.ts`;
141
+ // Browser target matches the historical edge-profile gate.
49
142
  const result = await Bun.build({
50
143
  entrypoints: [entry],
51
144
  minify: true,
52
145
  target: "browser",
53
146
  format: "esm",
54
- // Optional native drivers stay out of the edge profile.
55
- external: ["@duckdb/node-api", "@duckdb/*"],
147
+ external: [...BUILD_EXTERNALS],
56
148
  });
57
149
  if (!result.success) {
58
150
  throw new Error(
@@ -87,6 +179,33 @@ export async function measureClientGzipBytes(): Promise<number> {
87
179
  return Bun.gzipSync(new Uint8Array(raw)).byteLength;
88
180
  }
89
181
 
182
+ /**
183
+ * Gzip size of one published package export entry.
184
+ *
185
+ * Bundles through a temporary `import *` anchor so minify cannot drop the
186
+ * re-export graph (direct entry bundles collapse barrels to ~empty stubs).
187
+ *
188
+ * @param entry - Absolute path to the export entry
189
+ */
190
+ export async function measureExportGzipBytes(entry: string): Promise<number> {
191
+ const dir = await mkdtemp(join(tmpdir(), "oke-export-budget-"));
192
+ const anchor = join(dir, "anchor.ts");
193
+ await Bun.write(
194
+ anchor,
195
+ `import * as mod from ${JSON.stringify(entry)};\n` +
196
+ `export function __okeExportBudgetAnchor(){return mod;}\n`,
197
+ );
198
+ try {
199
+ return await measureEntryGzipBytes(
200
+ anchor,
201
+ `export ${entry}`,
202
+ EXPORT_BUILD_EXTERNALS,
203
+ );
204
+ } finally {
205
+ await rm(dir, { recursive: true, force: true });
206
+ }
207
+ }
208
+
90
209
  /** One Console asset's gzip size. */
91
210
  export interface ConsoleAssetGzip {
92
211
  /** File name under `dist/` or `dist/assets/`. */
@@ -253,12 +372,49 @@ export function measureRoutingP99Ms(): number {
253
372
  }
254
373
 
255
374
  /**
256
- * Measure every published budget.
375
+ * Regression ceiling from a previous byte sample.
376
+ *
377
+ * @param previous - Last committed value
378
+ */
379
+ export function exportRegressionLimitBytes(previous: number): number {
380
+ const allowed = Math.max(
381
+ EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES,
382
+ Math.ceil(previous * EXPORT_REGRESSION_TOLERANCE_RATIO),
383
+ );
384
+ return previous + allowed;
385
+ }
386
+
387
+ /**
388
+ * Load previous sample values from a budgets snapshot file (if present).
389
+ *
390
+ * @param path - Absolute path to budgets.json
391
+ */
392
+ export async function loadPreviousBudgetValues(
393
+ path = BUDGETS_JSON_PATH,
394
+ ): Promise<ReadonlyMap<string, number>> {
395
+ const file = Bun.file(path);
396
+ if (!(await file.exists())) return new Map();
397
+ try {
398
+ const snap = (await file.json()) as BudgetsSnapshot;
399
+ const map = new Map<string, number>();
400
+ for (const b of snap.budgets ?? []) {
401
+ map.set(b.id, b.value);
402
+ }
403
+ return map;
404
+ } catch {
405
+ return new Map();
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Measure every published budget (absolute AGENTS caps + export regressions).
257
411
  */
258
412
  export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
259
413
  const pkg = (await Bun.file(`${ROOT}/package.json`).json()) as {
260
414
  version: string;
261
415
  };
416
+ const previous = await loadPreviousBudgetValues();
417
+
262
418
  // Cold start alone first — parallel gzip work contends for CPU on CI and
263
419
  // falsely inflates the wall-clock probe.
264
420
  const coldStartMedianMs = await measureColdStartMedianMs();
@@ -277,6 +433,8 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
277
433
  kernelEdgeGzipBytes,
278
434
  KERNEL_EDGE_BUDGET_BYTES,
279
435
  "bytes",
436
+ "absolute",
437
+ "core",
280
438
  ),
281
439
  sample(
282
440
  "clientGzipBytes",
@@ -284,6 +442,8 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
284
442
  clientGzipBytes,
285
443
  CLIENT_BUDGET_BYTES,
286
444
  "bytes",
445
+ "absolute",
446
+ "core",
287
447
  ),
288
448
  sample(
289
449
  "consoleInitialGzipBytes",
@@ -291,6 +451,8 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
291
451
  consoleInitialGzipBytes,
292
452
  CONSOLE_BUDGET_BYTES,
293
453
  "bytes",
454
+ "absolute",
455
+ "core",
294
456
  ),
295
457
  sample(
296
458
  "coldStartMedianMs",
@@ -298,6 +460,8 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
298
460
  coldStartMedianMs,
299
461
  COLD_START_BUDGET_MS,
300
462
  "ms",
463
+ "absolute",
464
+ "core",
301
465
  ),
302
466
  sample(
303
467
  "routingP99Ms",
@@ -305,9 +469,30 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
305
469
  routingP99Ms,
306
470
  ROUTING_P99_BUDGET_MS,
307
471
  "ms",
472
+ "absolute",
473
+ "core",
308
474
  ),
309
475
  ];
310
476
 
477
+ const exportTargets = await resolveExportBudgetTargets();
478
+ const exportSizes = await Promise.all(
479
+ exportTargets.map(async (t) => ({
480
+ target: t,
481
+ bytes: await measureExportGzipBytes(t.entry),
482
+ })),
483
+ );
484
+ for (const { target, bytes } of exportSizes) {
485
+ budgets.push(
486
+ regressionSample(
487
+ target.id,
488
+ target.label,
489
+ target.group,
490
+ bytes,
491
+ previous,
492
+ ),
493
+ );
494
+ }
495
+
311
496
  return {
312
497
  measuredAt: new Date().toISOString(),
313
498
  version: pkg.version,
@@ -316,7 +501,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
316
501
  }
317
502
 
318
503
  /**
319
- * Format a snapshot for CI logs.
504
+ * Format a snapshot for CI logs (Core / Exports / Drivers sections).
320
505
  *
321
506
  * @param snapshot - Measured budgets
322
507
  */
@@ -324,11 +509,54 @@ export function formatBudgetsReport(snapshot: BudgetsSnapshot): string {
324
509
  const lines = [
325
510
  `okengine budgets v${snapshot.version} @ ${snapshot.measuredAt}`,
326
511
  ];
327
- for (const b of snapshot.budgets) {
328
- const flag = b.ok ? "ok" : "FAIL";
329
- lines.push(
330
- ` [${flag}] ${b.label}: ${formatValue(b.value, b.unit)} < ${formatValue(b.limit, b.unit)}`,
331
- );
512
+ const order: readonly BudgetGroup[] = ["core", "exports", "drivers"];
513
+ for (const group of order) {
514
+ const rows = snapshot.budgets.filter((b) => b.group === group);
515
+ if (rows.length === 0) continue;
516
+ lines.push("");
517
+ lines.push(GROUP_HEADINGS[group]);
518
+ for (const b of rows) {
519
+ const flag = b.ok ? "ok" : "FAIL";
520
+ lines.push(
521
+ ` [${flag}] ${b.label}: ${formatValue(b.value, b.unit)} < ${formatValue(b.limit, b.unit)}`,
522
+ );
523
+ }
524
+ }
525
+ return `${lines.join("\n")}\n`;
526
+ }
527
+
528
+ /**
529
+ * Full markdown document for [`BUDGETS.md`](../../BUDGETS.md).
530
+ * Written by `bun run budgets` — do not edit the tables by hand.
531
+ *
532
+ * @param snapshot - Measured budgets
533
+ */
534
+ export function formatBudgetsMarkdown(snapshot: BudgetsSnapshot): string {
535
+ const lines = [
536
+ "# Budgets",
537
+ "",
538
+ "Published numbers from [`budgets.json`](budgets.json). Refresh with `bun run budgets`.",
539
+ "",
540
+ `_okengine v${snapshot.version} · measured ${snapshot.measuredAt}_`,
541
+ "",
542
+ "Core rows are absolute AGENTS caps. Exports and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes peers/optionals (`zod`, `age-encryption`, `sently`, `oxc-parser`, `ajv`).",
543
+ ];
544
+ const order: readonly BudgetGroup[] = ["core", "exports", "drivers"];
545
+ for (const group of order) {
546
+ const rows = snapshot.budgets.filter((b) => b.group === group);
547
+ if (rows.length === 0) continue;
548
+ const limitCol = group === "core" ? "Limit" : "Ceiling";
549
+ lines.push("");
550
+ lines.push(`## ${GROUP_HEADINGS[group]}`);
551
+ lines.push("");
552
+ lines.push(`| | Measured | ${limitCol} |`);
553
+ lines.push("|---|---|---|");
554
+ for (const b of rows) {
555
+ const mark = b.ok ? "" : " **FAIL**";
556
+ lines.push(
557
+ `| ${b.label}${mark} | ${formatValue(b.value, b.unit)} | ${formatValue(b.limit, b.unit)} |`,
558
+ );
559
+ }
332
560
  }
333
561
  return `${lines.join("\n")}\n`;
334
562
  }
@@ -348,10 +576,65 @@ function sample(
348
576
  value: number,
349
577
  limit: number,
350
578
  unit: BudgetSample["unit"],
579
+ gate: BudgetGate,
580
+ group: BudgetGroup,
351
581
  ): BudgetSample {
352
582
  const rounded =
353
583
  unit === "bytes" ? Math.round(value) : Math.round(value * 1000) / 1000;
354
- return { id, label, value: rounded, limit, unit, ok: rounded < limit };
584
+ return {
585
+ id,
586
+ label,
587
+ value: rounded,
588
+ limit,
589
+ unit,
590
+ gate,
591
+ group,
592
+ ok: rounded < limit,
593
+ };
594
+ }
595
+
596
+ /**
597
+ * Byte sample gated against the previous committed value (+ tolerance).
598
+ * First appearance records a baseline (`ok: true`, `limit` = value + floor).
599
+ *
600
+ * @param id - Sample id
601
+ * @param label - Human label
602
+ * @param group - Report section
603
+ * @param value - Measured gzip bytes
604
+ * @param previous - Prior snapshot values by id
605
+ */
606
+ function regressionSample(
607
+ id: string,
608
+ label: string,
609
+ group: BudgetGroup,
610
+ value: number,
611
+ previous: ReadonlyMap<string, number>,
612
+ ): BudgetSample {
613
+ const rounded = Math.round(value);
614
+ const prior = previous.get(id);
615
+ if (prior === undefined) {
616
+ return {
617
+ id,
618
+ label,
619
+ value: rounded,
620
+ limit: rounded + EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES,
621
+ unit: "bytes",
622
+ gate: "regression",
623
+ group,
624
+ ok: true,
625
+ };
626
+ }
627
+ const limit = exportRegressionLimitBytes(prior);
628
+ return {
629
+ id,
630
+ label,
631
+ value: rounded,
632
+ limit,
633
+ unit: "bytes",
634
+ gate: "regression",
635
+ group,
636
+ ok: rounded < limit,
637
+ };
355
638
  }
356
639
 
357
640
  function formatValue(value: number, unit: BudgetSample["unit"]): string {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Measure every published budget, print the report, write `budgets.json`.
3
+ * Measure every published budget, print the report, write `budgets.json`
4
+ * and sync `BUDGETS.md`.
4
5
  *
5
6
  * Exit 1 on any regression. CI runs this as a gate; the snapshot is written
6
7
  * locally and committed with releases — not auto-pushed from Actions.
@@ -9,26 +10,32 @@
9
10
  import { resolve } from "node:path";
10
11
  import {
11
12
  budgetsPass,
13
+ formatBudgetsMarkdown,
12
14
  formatBudgetsReport,
13
15
  measureAllBudgets,
14
16
  type BudgetsSnapshot,
15
17
  } from "./measure.ts";
18
+ import { BUDGETS_MD } from "./readme.ts";
16
19
 
17
20
  const ROOT = resolve(import.meta.dir, "../..");
18
21
  /** Snapshot path at the repository root. */
19
22
  export const BUDGETS_JSON = resolve(ROOT, "budgets.json");
23
+ /** Generated markdown report (`BUDGETS.md`). */
24
+ export const BUDGETS_MARKDOWN = BUDGETS_MD;
20
25
 
21
26
  /** Options for {@link publishBudgets}. */
22
27
  export interface PublishBudgetsOptions {
23
28
  /** Destination path (defaults to repo-root `budgets.json`). */
24
29
  readonly outPath?: string;
25
- /** Skip writing the snapshot file. */
30
+ /** Markdown report path (defaults to repo-root `BUDGETS.md`). */
31
+ readonly markdownPath?: string;
32
+ /** Skip writing the snapshot and markdown report. */
26
33
  readonly dryRun?: boolean;
27
34
  readonly write?: (text: string) => void;
28
35
  }
29
36
 
30
37
  /**
31
- * Measure, print, and optionally persist the budgets snapshot.
38
+ * Measure, print, and optionally persist the budgets snapshot + markdown.
32
39
  *
33
40
  * @param options - Output / dry-run
34
41
  * @returns Exit code (1 when any budget fails)
@@ -44,6 +51,10 @@ export async function publishBudgets(
44
51
  const path = options.outPath ?? BUDGETS_JSON;
45
52
  await Bun.write(path, `${JSON.stringify(snapshot, null, 2)}\n`);
46
53
  write(`wrote ${path}\n`);
54
+
55
+ const markdownPath = options.markdownPath ?? BUDGETS_MARKDOWN;
56
+ await Bun.write(markdownPath, formatBudgetsMarkdown(snapshot));
57
+ write(`wrote ${markdownPath}\n`);
47
58
  }
48
59
 
49
60
  return { code: budgetsPass(snapshot) ? 0 : 1, snapshot };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Generated budgets markdown document.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { formatBudgetsMarkdown, type BudgetsSnapshot } from "./measure.ts";
7
+ import { BUDGETS_MD } from "./readme.ts";
8
+
9
+ describe("budgets markdown", () => {
10
+ test("BUDGETS_MD points at root BUDGETS.md", () => {
11
+ expect(BUDGETS_MD.endsWith("/BUDGETS.md")).toBe(true);
12
+ });
13
+
14
+ test("formatBudgetsMarkdown is a standalone document with short names", () => {
15
+ const snapshot: BudgetsSnapshot = {
16
+ measuredAt: "2026-07-25T00:00:00.000Z",
17
+ version: "0.1.6",
18
+ budgets: [
19
+ {
20
+ id: "kernelEdgeGzipBytes",
21
+ label: "Kernel (edge profile)",
22
+ value: 8969,
23
+ limit: 15360,
24
+ unit: "bytes",
25
+ gate: "absolute",
26
+ group: "core",
27
+ ok: true,
28
+ },
29
+ {
30
+ id: "export:./channel",
31
+ label: "channel",
32
+ value: 6144,
33
+ limit: 6400,
34
+ unit: "bytes",
35
+ gate: "regression",
36
+ group: "exports",
37
+ ok: true,
38
+ },
39
+ {
40
+ id: "export:./drivers/postgres",
41
+ label: "postgres",
42
+ value: 1413,
43
+ limit: 1669,
44
+ unit: "bytes",
45
+ gate: "regression",
46
+ group: "drivers",
47
+ ok: true,
48
+ },
49
+ ],
50
+ };
51
+ const md = formatBudgetsMarkdown(snapshot);
52
+ expect(md.startsWith("# Budgets\n")).toBe(true);
53
+ expect(md).toContain("## Core");
54
+ expect(md).toContain("## Exports");
55
+ expect(md).toContain("## Drivers");
56
+ expect(md).toContain("| channel |");
57
+ expect(md).toContain("| postgres |");
58
+ expect(md).toContain("[`budgets.json`](budgets.json)");
59
+ expect(md).not.toContain("./channel");
60
+ });
61
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Path helpers for the generated budgets markdown document.
3
+ *
4
+ * The tables live in `BUDGETS.md` at the repo root (rewritten by
5
+ * `bun run budgets`). README only links there.
6
+ */
7
+
8
+ import { resolve } from "node:path";
9
+
10
+ const ROOT = resolve(import.meta.dir, "../..");
11
+
12
+ /** Generated budgets report (markdown tables). */
13
+ export const BUDGETS_MD = resolve(ROOT, "BUDGETS.md");
@@ -30,6 +30,7 @@ export {
30
30
  export {
31
31
  APP_PORT,
32
32
  CONSOLE_PORT,
33
+ DOCS_MCP_PORT,
33
34
  MCP_PORT,
34
35
  type FetchApp,
35
36
  type PasswordAlgorithm,
@@ -123,7 +123,7 @@ describe("secureFetch", () => {
123
123
  });
124
124
  });
125
125
 
126
- describe("Host/Origin entry points (6530 / 6533 / 6535)", () => {
126
+ describe("Host/Origin entry points (6530 / 6533 / 6535 / 6536)", () => {
127
127
  test("every HTTP surface routes through checkRequestSecurity", async () => {
128
128
  /**
129
129
  * Entry-point map (source of truth for §10.1):
@@ -132,6 +132,7 @@ describe("Host/Origin entry points (6530 / 6533 / 6535)", () => {
132
132
  * and checkRequestSecurity before WebSocket upgrade on /console/live
133
133
  * - MCP :6535 → mcp/server.ts → checkRequestSecurity (same primitive;
134
134
  * secureFetch is a thin wrapper around it)
135
+ * - Docs MCP :6536 → mcp/docs-server.ts → checkRequestSecurity
135
136
  */
136
137
  const surfaces: ReadonlyArray<{
137
138
  readonly file: string;
@@ -153,6 +154,10 @@ describe("Host/Origin entry points (6530 / 6533 / 6535)", () => {
153
154
  file: `${import.meta.dir}/../mcp/server.ts`,
154
155
  mustInclude: ["checkRequestSecurity", "forbiddenResponse"],
155
156
  },
157
+ {
158
+ file: `${import.meta.dir}/../mcp/docs-server.ts`,
159
+ mustInclude: ["checkRequestSecurity", "forbiddenResponse"],
160
+ },
156
161
  ];
157
162
 
158
163
  for (const surface of surfaces) {
@@ -17,6 +17,12 @@ export const CONSOLE_PORT = 6533;
17
17
  /** Default MCP listen port (O·K·E mnemonic). */
18
18
  export const MCP_PORT = 6535;
19
19
 
20
+ /**
21
+ * Default docs MCP listen port (read-only documentation search/fetch).
22
+ * Distinct from {@link MCP_PORT} — serves docs content, not a live Manifest.
23
+ */
24
+ export const DOCS_MCP_PORT = 6536;
25
+
20
26
  /**
21
27
  * Minimal app surface a runtime can serve.
22
28
  * Satisfied by {@link import("../kernel/app.ts").OkeApp}.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Terminal chrome — color off stays plain; claim/banner keep structure.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ formatAppReadyLine,
8
+ formatClaimNote,
9
+ formatDevBanner,
10
+ formatServiceLine,
11
+ formatStatusLine,
12
+ termStyle,
13
+ } from "./term.ts";
14
+
15
+ describe("term", () => {
16
+ test("termStyle(false) strips ANSI", () => {
17
+ const s = termStyle(false);
18
+ expect(s.cyan).toBe("");
19
+ expect(s.bold).toBe("");
20
+ });
21
+
22
+ test("formatDevBanner is readable without color", () => {
23
+ const out = formatDevBanner({ color: false });
24
+ expect(out).toContain("oke dev");
25
+ expect(out).toContain("Starting");
26
+ expect(out).toContain("watching");
27
+ expect(out).not.toMatch(/\u001b\[/);
28
+ });
29
+
30
+ test("formatServiceLine / formatAppReadyLine include URL", () => {
31
+ expect(formatServiceLine("Console", "http://127.0.0.1:6533", false)).toContain(
32
+ "http://127.0.0.1:6533",
33
+ );
34
+ expect(formatAppReadyLine("http://127.0.0.1:6530", false)).toContain(
35
+ "http://127.0.0.1:6530",
36
+ );
37
+ });
38
+
39
+ test("formatClaimNote embeds code and ownership line", () => {
40
+ const code = "aabbccddeeff00112233445566778899";
41
+ const out = formatClaimNote(code, false);
42
+ expect(out).toContain(code);
43
+ expect(out).toContain("Claim code");
44
+ expect(out).toContain("owns the server");
45
+ expect(out).not.toMatch(/\u001b\[/);
46
+ });
47
+
48
+ test("formatStatusLine keeps message", () => {
49
+ expect(formatStatusLine("stack up (sql)", false)).toContain("stack up");
50
+ });
51
+ });