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.
- package/AGENTS.md +1 -1
- package/README.md +36 -29
- package/docs/spec/example.md +1187 -0
- package/docs/spec/unified-theory.md +1 -1
- package/package.json +20 -6
- package/src/cli/dev-app-runner.ts +6 -2
- package/src/cli/dev.ts +19 -10
- package/src/cli/doc-drift.ts +54 -21
- package/src/console/index.ts +25 -13
- package/src/console/server/app.ts +44 -7
- package/src/console/server/claim.test.ts +7 -4
- package/src/console/server/claim.ts +2 -7
- package/src/console/server/flows.ts +2 -6
- package/src/console/server/index.ts +2 -1
- package/src/console/server/lazy-panels.test.ts +27 -0
- package/src/console/server/panel-load.ts +28 -0
- package/src/console/server/plugin.ts +1 -1
- package/src/console/server/plugins.ts +7 -6
- package/src/console/server/public-flows.ts +12 -0
- package/src/console/server/state.ts +159 -122
- package/src/console/server/store.ts +13 -10
- package/src/console/ui/dist/assets/index-B71Yl_SS.js +10 -0
- package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
- package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
- package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
- package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
- package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
- package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
- package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
- package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
- package/src/console/ui/dist/assets/panel-overview-Bd48d9km.js +1 -0
- package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BwsWqKeB.js +1 -0
- package/src/console/ui/dist/assets/panel-signals-9najbZY2.js +1 -0
- package/src/console/ui/dist/assets/panel-store-OHkP2pDp.js +1 -0
- package/src/console/ui/dist/assets/panel-traces-tn2JoY8U.js +1 -0
- package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
- package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
- package/src/console/ui/dist/assets/style-Cnl7WLya.css +3 -0
- package/src/console/ui/dist/index.html +14 -0
- package/src/docs-origin.ts +19 -0
- package/src/drivers/index.ts +1 -6
- package/src/drivers/vault-sops.ts +20 -1
- package/src/kernel/app.ts +1 -1
- package/src/kernel/boot-bind/ai.ts +31 -0
- package/src/kernel/boot-bind/channel.ts +27 -0
- package/src/kernel/boot-bind/clock.ts +74 -0
- package/src/kernel/boot-bind/gate.ts +28 -0
- package/src/kernel/boot-bind/runs.ts +28 -0
- package/src/kernel/boot-bind/signal.ts +68 -0
- package/src/kernel/boot-bind/store.ts +47 -0
- package/src/kernel/boot-bind/vault.ts +36 -0
- package/src/kernel/boot.test.ts +85 -1
- package/src/kernel/boot.ts +250 -212
- package/src/kernel/errors.registry.test.ts +4 -2
- package/src/kernel/errors.ts +3 -1
- package/src/kernel/fx.test.ts +2 -2
- package/src/kernel/index.ts +2 -0
- package/src/mcp/data.ts +1 -0
- package/src/mcp/docs-index.ts +252 -0
- package/src/mcp/docs-mcp.test.ts +176 -0
- package/src/mcp/docs-server.ts +233 -0
- package/src/mcp/docs-tools.ts +143 -0
- package/src/mcp/index.ts +29 -5
- package/src/mcp/protocol.ts +2 -1
- package/src/release/exports.test.ts +71 -0
- package/src/release/exports.ts +156 -0
- package/src/release/index.ts +21 -0
- package/src/release/limits.ts +9 -0
- package/src/release/measure.exports.test.ts +82 -0
- package/src/release/measure.ts +297 -14
- package/src/release/publish.ts +14 -3
- package/src/release/readme.test.ts +61 -0
- package/src/release/readme.ts +13 -0
- package/src/runtime/index.ts +1 -0
- package/src/runtime/security.test.ts +6 -1
- package/src/runtime/types.ts +6 -0
- package/src/term.test.ts +51 -0
- package/src/term.ts +158 -0
- package/src/test/create-test-app.ts +2 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only docs MCP tools — search and fetch documentation pages.
|
|
3
|
+
*
|
|
4
|
+
* Every return goes through {@link asData}. No write tools, no auth scopes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { asData, type McpDataEnvelope } from "./data.ts";
|
|
8
|
+
import type { DocsIndex } from "./docs-index.ts";
|
|
9
|
+
|
|
10
|
+
/** Result of a docs tool call. */
|
|
11
|
+
export type DocsToolCallResult =
|
|
12
|
+
| { readonly ok: true; readonly data: McpDataEnvelope }
|
|
13
|
+
| {
|
|
14
|
+
readonly ok: false;
|
|
15
|
+
readonly code: "invalid" | "not-found";
|
|
16
|
+
readonly message: string;
|
|
17
|
+
readonly data: McpDataEnvelope;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Tool descriptor exposed via `tools/list`. */
|
|
21
|
+
export interface DocsToolDescriptor {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly description: string;
|
|
24
|
+
readonly inputSchema: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Docs MCP tool catalogue. */
|
|
28
|
+
export const DOCS_MCP_TOOLS: readonly DocsToolDescriptor[] = [
|
|
29
|
+
{
|
|
30
|
+
name: "oke.docs.search",
|
|
31
|
+
description:
|
|
32
|
+
"Search okengine documentation pages by keyword. Returns slug, title, url, and excerpt.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
query: { type: "string", description: "Search query" },
|
|
37
|
+
limit: {
|
|
38
|
+
type: "number",
|
|
39
|
+
description: "Max results (default 20)",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["query"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "oke.docs.get",
|
|
48
|
+
description:
|
|
49
|
+
"Fetch a documentation page by slug or content path. Returns source-identical markdown (YAML frontmatter stripped).",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
slug: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description:
|
|
56
|
+
"Page slug (e.g. get-started/introduction) or path (…/introduction.mdx)",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
required: ["slug"],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create the docs tool runtime bound to an index.
|
|
67
|
+
*
|
|
68
|
+
* @param index - Loaded docs catalogue
|
|
69
|
+
*/
|
|
70
|
+
export function createDocsToolRuntime(index: DocsIndex): {
|
|
71
|
+
readonly listTools: () => readonly DocsToolDescriptor[];
|
|
72
|
+
readonly callTool: (
|
|
73
|
+
name: string,
|
|
74
|
+
args: Record<string, unknown>,
|
|
75
|
+
) => DocsToolCallResult;
|
|
76
|
+
} {
|
|
77
|
+
return {
|
|
78
|
+
listTools: () => DOCS_MCP_TOOLS,
|
|
79
|
+
callTool(name, args) {
|
|
80
|
+
if (name === "oke.docs.search") {
|
|
81
|
+
const query = args.query;
|
|
82
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
code: "invalid",
|
|
86
|
+
message: "query must be a non-empty string",
|
|
87
|
+
data: asData({ reason: "invalid-query" }, "error"),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const limit =
|
|
91
|
+
typeof args.limit === "number" && Number.isFinite(args.limit)
|
|
92
|
+
? Math.max(1, Math.min(100, Math.floor(args.limit)))
|
|
93
|
+
: 20;
|
|
94
|
+
const hits = index.search(query, limit);
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
data: asData({ query, hits }, "docs"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (name === "oke.docs.get") {
|
|
102
|
+
const slug = args.slug;
|
|
103
|
+
if (typeof slug !== "string" || slug.trim().length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
code: "invalid",
|
|
107
|
+
message: "slug must be a non-empty string",
|
|
108
|
+
data: asData({ reason: "invalid-slug" }, "error"),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const page = index.get(slug.trim());
|
|
112
|
+
if (!page) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
code: "not-found",
|
|
116
|
+
message: `docs page not found: ${slug}`,
|
|
117
|
+
data: asData({ slug }, "error"),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
data: asData(
|
|
123
|
+
{
|
|
124
|
+
slug: page.slug,
|
|
125
|
+
path: page.path,
|
|
126
|
+
title: page.title,
|
|
127
|
+
url: page.url,
|
|
128
|
+
body: page.body,
|
|
129
|
+
},
|
|
130
|
+
"docs",
|
|
131
|
+
),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
code: "not-found",
|
|
138
|
+
message: `unknown tool: ${name}`,
|
|
139
|
+
data: asData({ tool: name }, "error"),
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP AI
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* actions to agents — read-only by default, write tools gated by fresh
|
|
6
|
-
* human confirmation, every return wrapped as inert data.
|
|
2
|
+
* MCP AI surfaces (console §10.3 · unified-theory §25):
|
|
3
|
+
* - App MCP `:6535` — Manifest, schemas, effects, traces, Console-safe actions
|
|
4
|
+
* - Docs MCP `:6536` — read-only documentation search / fetch
|
|
7
5
|
*
|
|
8
6
|
* Subpath: `okengine/mcp`
|
|
9
7
|
* @module
|
|
@@ -79,3 +77,29 @@ export {
|
|
|
79
77
|
type McpServerHandle,
|
|
80
78
|
type ServeMcpOptions,
|
|
81
79
|
} from "./server.ts";
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
defaultDocsContentDir,
|
|
83
|
+
loadDocsIndex,
|
|
84
|
+
parseDocsFrontmatter,
|
|
85
|
+
stripYamlFrontmatter,
|
|
86
|
+
type DocsIndex,
|
|
87
|
+
type DocsPage,
|
|
88
|
+
type DocsSearchHit,
|
|
89
|
+
} from "./docs-index.ts";
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
createDocsToolRuntime,
|
|
93
|
+
DOCS_MCP_TOOLS,
|
|
94
|
+
type DocsToolCallResult,
|
|
95
|
+
type DocsToolDescriptor,
|
|
96
|
+
} from "./docs-tools.ts";
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
createDocsMcpServer,
|
|
100
|
+
serveDocsMcp,
|
|
101
|
+
type CreateDocsMcpServerOptions,
|
|
102
|
+
type DocsMcpServer,
|
|
103
|
+
type DocsMcpServerHandle,
|
|
104
|
+
type ServeDocsMcpOptions,
|
|
105
|
+
} from "./docs-server.ts";
|
package/src/mcp/protocol.ts
CHANGED
|
@@ -126,7 +126,8 @@ export interface McpInitializeResult {
|
|
|
126
126
|
readonly tools: { readonly listChanged: false };
|
|
127
127
|
};
|
|
128
128
|
readonly serverInfo: {
|
|
129
|
-
|
|
129
|
+
/** App MCP (`okengine-mcp`) or docs MCP (`okengine-docs-mcp`). */
|
|
130
|
+
readonly name: "okengine-mcp" | "okengine-docs-mcp";
|
|
130
131
|
readonly version: string;
|
|
131
132
|
};
|
|
132
133
|
/** Cryptographically random transport session id. */
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Published-export discovery for bundle budgets.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
exportBudgetGroup,
|
|
9
|
+
exportBudgetLabel,
|
|
10
|
+
isMeasurableDriverFile,
|
|
11
|
+
listDriverModules,
|
|
12
|
+
resolveExportBudgetTargets,
|
|
13
|
+
} from "./exports.ts";
|
|
14
|
+
|
|
15
|
+
const ROOT = resolve(import.meta.dir, "../..");
|
|
16
|
+
|
|
17
|
+
describe("export budget targets", () => {
|
|
18
|
+
test("isMeasurableDriverFile excludes types, tests, barrel, conformance", () => {
|
|
19
|
+
expect(isMeasurableDriverFile("postgres.ts")).toBe(true);
|
|
20
|
+
expect(isMeasurableDriverFile("index.ts")).toBe(false);
|
|
21
|
+
expect(isMeasurableDriverFile("types.ts")).toBe(false);
|
|
22
|
+
expect(isMeasurableDriverFile("signal-types.ts")).toBe(false);
|
|
23
|
+
expect(isMeasurableDriverFile("conformance.ts")).toBe(false);
|
|
24
|
+
expect(isMeasurableDriverFile("conformance.test.ts")).toBe(false);
|
|
25
|
+
expect(isMeasurableDriverFile("ai-providers.test.ts")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("exportBudgetLabel / group use short names", () => {
|
|
29
|
+
expect(exportBudgetLabel(".")).toBe("okengine");
|
|
30
|
+
expect(exportBudgetLabel("./channel")).toBe("channel");
|
|
31
|
+
expect(exportBudgetLabel("./drivers/postgres")).toBe("postgres");
|
|
32
|
+
expect(exportBudgetGroup(".")).toBe("exports");
|
|
33
|
+
expect(exportBudgetGroup("./channel")).toBe("exports");
|
|
34
|
+
expect(exportBudgetGroup("./drivers")).toBe("drivers");
|
|
35
|
+
expect(exportBudgetGroup("./drivers/postgres")).toBe("drivers");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("every non-glob package export and every driver module is covered", async () => {
|
|
39
|
+
const pkg = (await Bun.file(join(ROOT, "package.json")).json()) as {
|
|
40
|
+
exports: Record<string, string>;
|
|
41
|
+
};
|
|
42
|
+
const targets = await resolveExportBudgetTargets();
|
|
43
|
+
const bySubpath = new Map(targets.map((t) => [t.subpath, t]));
|
|
44
|
+
|
|
45
|
+
for (const [subpath, entryRel] of Object.entries(pkg.exports)) {
|
|
46
|
+
if (subpath.endsWith("/*")) continue;
|
|
47
|
+
const target = bySubpath.get(subpath);
|
|
48
|
+
expect(target).toBeDefined();
|
|
49
|
+
expect(target!.id).toBe(`export:${subpath}`);
|
|
50
|
+
expect(target!.label).toBe(exportBudgetLabel(subpath));
|
|
51
|
+
expect(target!.group).toBe(exportBudgetGroup(subpath));
|
|
52
|
+
expect(target!.entry).toBe(resolve(ROOT, entryRel));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const drivers = await listDriverModules();
|
|
56
|
+
expect(drivers.length).toBeGreaterThan(0);
|
|
57
|
+
for (const file of drivers) {
|
|
58
|
+
const name = file.replace(/\.ts$/, "");
|
|
59
|
+
const subpath = `./drivers/${name}`;
|
|
60
|
+
const target = bySubpath.get(subpath);
|
|
61
|
+
expect(target).toBeDefined();
|
|
62
|
+
expect(target!.id).toBe(`export:${subpath}`);
|
|
63
|
+
expect(target!.label).toBe(name);
|
|
64
|
+
expect(target!.group).toBe("drivers");
|
|
65
|
+
expect(target!.entry).toBe(join(ROOT, "src/drivers", file));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// No duplicates.
|
|
69
|
+
expect(new Set(targets.map((t) => t.id)).size).toBe(targets.length);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve published `package.json` exports into measurable bundle entries.
|
|
3
|
+
*
|
|
4
|
+
* Expands `./drivers/*` to concrete driver modules (tree-shaken subpaths).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdir } from "node:fs/promises";
|
|
8
|
+
import { basename, join, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
const ROOT = resolve(import.meta.dir, "../..");
|
|
11
|
+
const DRIVERS_DIR = join(ROOT, "src/drivers");
|
|
12
|
+
|
|
13
|
+
/** Report / snapshot group for a budget sample. */
|
|
14
|
+
export type BudgetGroup = "core" | "exports" | "drivers";
|
|
15
|
+
|
|
16
|
+
/** One published subpath ready for gzip measurement. */
|
|
17
|
+
export interface ExportBudgetTarget {
|
|
18
|
+
/** Sample id — `export:<subpath>`. */
|
|
19
|
+
readonly id: string;
|
|
20
|
+
/** Package subpath (`.` or `./store`, `./drivers/postgres`, …). */
|
|
21
|
+
readonly subpath: string;
|
|
22
|
+
/** Short human label (`channel`, `postgres`, `okengine`, …). */
|
|
23
|
+
readonly label: string;
|
|
24
|
+
/** Report section. */
|
|
25
|
+
readonly group: BudgetGroup;
|
|
26
|
+
/** Absolute path to the TypeScript entry. */
|
|
27
|
+
readonly entry: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Short display name for a package subpath.
|
|
32
|
+
*
|
|
33
|
+
* @param subpath - `.` / `./channel` / `./drivers/postgres`
|
|
34
|
+
*/
|
|
35
|
+
export function exportBudgetLabel(subpath: string): string {
|
|
36
|
+
if (subpath === ".") return "okengine";
|
|
37
|
+
if (subpath.startsWith("./drivers/")) {
|
|
38
|
+
return subpath.slice("./drivers/".length);
|
|
39
|
+
}
|
|
40
|
+
if (subpath.startsWith("./")) return subpath.slice(2);
|
|
41
|
+
return subpath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Which report section a published export belongs to.
|
|
46
|
+
*
|
|
47
|
+
* @param subpath - Package export subpath
|
|
48
|
+
*/
|
|
49
|
+
export function exportBudgetGroup(subpath: string): BudgetGroup {
|
|
50
|
+
if (subpath === "./drivers" || subpath.startsWith("./drivers/")) {
|
|
51
|
+
return "drivers";
|
|
52
|
+
}
|
|
53
|
+
return "exports";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface PackageExports {
|
|
57
|
+
readonly exports?: Readonly<Record<string, string>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a driver filename is a measurable protocol module (not types/tests).
|
|
62
|
+
*
|
|
63
|
+
* @param name - Basename under `src/drivers/`
|
|
64
|
+
*/
|
|
65
|
+
export function isMeasurableDriverFile(name: string): boolean {
|
|
66
|
+
if (!name.endsWith(".ts")) return false;
|
|
67
|
+
if (name === "index.ts") return false;
|
|
68
|
+
if (name === "conformance.ts") return false;
|
|
69
|
+
if (name.endsWith(".test.ts")) return false;
|
|
70
|
+
if (name.endsWith("-types.ts")) return false;
|
|
71
|
+
if (name === "types.ts") return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Discover concrete `okengine/drivers/*` modules under `src/drivers/`.
|
|
77
|
+
*
|
|
78
|
+
* @param driversDir - Absolute drivers directory
|
|
79
|
+
*/
|
|
80
|
+
export async function listDriverModules(
|
|
81
|
+
driversDir = DRIVERS_DIR,
|
|
82
|
+
): Promise<readonly string[]> {
|
|
83
|
+
const entries = await readdir(driversDir);
|
|
84
|
+
return entries.filter(isMeasurableDriverFile).sort((a, b) => a.localeCompare(b));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve every published export into a stable, sorted measurement target list.
|
|
89
|
+
*
|
|
90
|
+
* @param options - Override package.json / drivers dir (tests)
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveExportBudgetTargets(options?: {
|
|
93
|
+
readonly root?: string;
|
|
94
|
+
readonly pkg?: PackageExports;
|
|
95
|
+
readonly driversDir?: string;
|
|
96
|
+
}): Promise<readonly ExportBudgetTarget[]> {
|
|
97
|
+
const root = options?.root ?? ROOT;
|
|
98
|
+
const pkg =
|
|
99
|
+
options?.pkg ??
|
|
100
|
+
((await Bun.file(join(root, "package.json")).json()) as PackageExports);
|
|
101
|
+
const exportsMap = pkg.exports ?? {};
|
|
102
|
+
const driversDir = options?.driversDir ?? join(root, "src/drivers");
|
|
103
|
+
|
|
104
|
+
const targets: ExportBudgetTarget[] = [];
|
|
105
|
+
|
|
106
|
+
for (const [subpath, entryRel] of Object.entries(exportsMap)) {
|
|
107
|
+
if (subpath.endsWith("/*")) {
|
|
108
|
+
if (subpath !== "./drivers/*") {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`unsupported export glob ${subpath}: only ./drivers/* is expanded`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const modules = await listDriverModules(driversDir);
|
|
114
|
+
for (const file of modules) {
|
|
115
|
+
const name = basename(file, ".ts");
|
|
116
|
+
const driverSubpath = `./drivers/${name}`;
|
|
117
|
+
targets.push(targetFor(driverSubpath, join(driversDir, file)));
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof entryRel !== "string") {
|
|
123
|
+
throw new Error(`export ${subpath} must map to a string path`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
targets.push(targetFor(subpath, resolve(root, entryRel)));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
targets.sort((a, b) => {
|
|
130
|
+
const g = groupOrder(a.group) - groupOrder(b.group);
|
|
131
|
+
if (g !== 0) return g;
|
|
132
|
+
// Barrel `drivers` before concrete modules.
|
|
133
|
+
if (a.subpath === "./drivers") return -1;
|
|
134
|
+
if (b.subpath === "./drivers") return 1;
|
|
135
|
+
if (a.subpath === ".") return -1;
|
|
136
|
+
if (b.subpath === ".") return 1;
|
|
137
|
+
return a.label.localeCompare(b.label);
|
|
138
|
+
});
|
|
139
|
+
return targets;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function targetFor(subpath: string, entry: string): ExportBudgetTarget {
|
|
143
|
+
return {
|
|
144
|
+
id: `export:${subpath}`,
|
|
145
|
+
subpath,
|
|
146
|
+
label: exportBudgetLabel(subpath),
|
|
147
|
+
group: exportBudgetGroup(subpath),
|
|
148
|
+
entry,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function groupOrder(group: BudgetGroup): number {
|
|
153
|
+
if (group === "exports") return 0;
|
|
154
|
+
if (group === "drivers") return 1;
|
|
155
|
+
return 2;
|
|
156
|
+
}
|
package/src/release/index.ts
CHANGED
|
@@ -2,29 +2,50 @@
|
|
|
2
2
|
* Release engineering — budgets, snapshots, published numbers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export {
|
|
6
|
+
exportBudgetGroup,
|
|
7
|
+
exportBudgetLabel,
|
|
8
|
+
isMeasurableDriverFile,
|
|
9
|
+
listDriverModules,
|
|
10
|
+
resolveExportBudgetTargets,
|
|
11
|
+
type BudgetGroup,
|
|
12
|
+
type ExportBudgetTarget,
|
|
13
|
+
} from "./exports.ts";
|
|
14
|
+
|
|
5
15
|
export {
|
|
6
16
|
CLIENT_BUDGET_BYTES,
|
|
7
17
|
COLD_START_BUDGET_MS,
|
|
8
18
|
CONSOLE_BUDGET_BYTES,
|
|
19
|
+
EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES,
|
|
20
|
+
EXPORT_REGRESSION_TOLERANCE_RATIO,
|
|
9
21
|
KERNEL_EDGE_BUDGET_BYTES,
|
|
10
22
|
ROUTING_P99_BUDGET_MS,
|
|
11
23
|
} from "./limits.ts";
|
|
12
24
|
|
|
13
25
|
export {
|
|
14
26
|
budgetsPass,
|
|
27
|
+
exportRegressionLimitBytes,
|
|
28
|
+
formatBudgetsMarkdown,
|
|
15
29
|
formatBudgetsReport,
|
|
30
|
+
loadPreviousBudgetValues,
|
|
16
31
|
measureAllBudgets,
|
|
17
32
|
measureClientGzipBytes,
|
|
18
33
|
measureColdStartMedianMs,
|
|
19
34
|
measureConsoleInitialGzipBytes,
|
|
35
|
+
measureEntryGzipBytes,
|
|
36
|
+
measureExportGzipBytes,
|
|
20
37
|
measureKernelEdgeGzipBytes,
|
|
21
38
|
measureRoutingP99Ms,
|
|
39
|
+
type BudgetGate,
|
|
22
40
|
type BudgetSample,
|
|
23
41
|
type BudgetsSnapshot,
|
|
24
42
|
} from "./measure.ts";
|
|
25
43
|
|
|
26
44
|
export {
|
|
27
45
|
BUDGETS_JSON,
|
|
46
|
+
BUDGETS_MARKDOWN,
|
|
28
47
|
publishBudgets,
|
|
29
48
|
type PublishBudgetsOptions,
|
|
30
49
|
} from "./publish.ts";
|
|
50
|
+
|
|
51
|
+
export { BUDGETS_MD } from "./readme.ts";
|
package/src/release/limits.ts
CHANGED
|
@@ -18,3 +18,12 @@ export const COLD_START_BUDGET_MS = 75;
|
|
|
18
18
|
|
|
19
19
|
/** p99 HTTP routing overhead (match only). */
|
|
20
20
|
export const ROUTING_P99_BUDGET_MS = 1;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Regression tolerance for published-export gzip samples.
|
|
24
|
+
* Allowed growth = max(floor bytes, ratio × previous).
|
|
25
|
+
*/
|
|
26
|
+
export const EXPORT_REGRESSION_TOLERANCE_RATIO = 0.02;
|
|
27
|
+
|
|
28
|
+
/** Minimum allowed growth (bytes) before a subpath export fails regression. */
|
|
29
|
+
export const EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES = 256;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export gzip measurement + regression limit helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
exportRegressionLimitBytes,
|
|
9
|
+
formatBudgetsReport,
|
|
10
|
+
measureExportGzipBytes,
|
|
11
|
+
type BudgetsSnapshot,
|
|
12
|
+
} from "./measure.ts";
|
|
13
|
+
import {
|
|
14
|
+
EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES,
|
|
15
|
+
EXPORT_REGRESSION_TOLERANCE_RATIO,
|
|
16
|
+
} from "./limits.ts";
|
|
17
|
+
|
|
18
|
+
describe("export gzip budgets", () => {
|
|
19
|
+
test("measureExportGzipBytes(./client) retains the runtime graph", async () => {
|
|
20
|
+
const entry = resolve(import.meta.dir, "../client/index.ts");
|
|
21
|
+
const size = await measureExportGzipBytes(entry);
|
|
22
|
+
// Must not collapse to an empty re-export stub (~100 B).
|
|
23
|
+
expect(size).toBeGreaterThan(1024);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("exportRegressionLimitBytes uses max(floor, 2% of previous)", () => {
|
|
27
|
+
expect(exportRegressionLimitBytes(1000)).toBe(
|
|
28
|
+
1000 + EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES,
|
|
29
|
+
);
|
|
30
|
+
const large = 100_000;
|
|
31
|
+
expect(exportRegressionLimitBytes(large)).toBe(
|
|
32
|
+
large + Math.ceil(large * EXPORT_REGRESSION_TOLERANCE_RATIO),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("formatBudgetsReport groups Core / Exports / Drivers with short names", () => {
|
|
37
|
+
const snapshot: BudgetsSnapshot = {
|
|
38
|
+
measuredAt: "2026-07-25T00:00:00.000Z",
|
|
39
|
+
version: "0.0.0",
|
|
40
|
+
budgets: [
|
|
41
|
+
{
|
|
42
|
+
id: "kernelEdgeGzipBytes",
|
|
43
|
+
label: "Kernel (edge profile)",
|
|
44
|
+
value: 100,
|
|
45
|
+
limit: 200,
|
|
46
|
+
unit: "bytes",
|
|
47
|
+
gate: "absolute",
|
|
48
|
+
group: "core",
|
|
49
|
+
ok: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "export:./channel",
|
|
53
|
+
label: "channel",
|
|
54
|
+
value: 100,
|
|
55
|
+
limit: 200,
|
|
56
|
+
unit: "bytes",
|
|
57
|
+
gate: "regression",
|
|
58
|
+
group: "exports",
|
|
59
|
+
ok: true,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "export:./drivers/postgres",
|
|
63
|
+
label: "postgres",
|
|
64
|
+
value: 100,
|
|
65
|
+
limit: 200,
|
|
66
|
+
unit: "bytes",
|
|
67
|
+
gate: "regression",
|
|
68
|
+
group: "drivers",
|
|
69
|
+
ok: true,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
const report = formatBudgetsReport(snapshot);
|
|
74
|
+
expect(report).toContain("\nCore\n");
|
|
75
|
+
expect(report).toContain("\nExports\n");
|
|
76
|
+
expect(report).toContain("\nDrivers\n");
|
|
77
|
+
expect(report).toContain("[ok] channel:");
|
|
78
|
+
expect(report).toContain("[ok] postgres:");
|
|
79
|
+
expect(report).not.toContain("regression Export");
|
|
80
|
+
expect(report).not.toContain("./channel");
|
|
81
|
+
});
|
|
82
|
+
});
|