okengine 0.2.8 → 0.3.4

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 (180) hide show
  1. package/AGENTS.md +17 -15
  2. package/README.md +59 -31
  3. package/package.json +11 -16
  4. package/site/content/docs/ai/llms-txt.mdx +54 -0
  5. package/site/content/docs/ai/mcp.mdx +123 -0
  6. package/site/content/docs/ai/meta.json +5 -0
  7. package/site/content/docs/ai/skills.mdx +53 -0
  8. package/site/content/docs/console/access.mdx +29 -0
  9. package/site/content/docs/console/ai.mdx +35 -0
  10. package/site/content/docs/console/architecture.mdx +35 -0
  11. package/site/content/docs/console/channels.mdx +37 -0
  12. package/site/content/docs/console/clock.mdx +31 -0
  13. package/site/content/docs/console/flows.mdx +31 -0
  14. package/site/content/docs/console/gates.mdx +35 -0
  15. package/site/content/docs/console/manifest-diff.mdx +34 -0
  16. package/site/content/docs/console/meta.json +23 -0
  17. package/site/content/docs/console/overview.mdx +40 -0
  18. package/site/content/docs/console/plugins.mdx +41 -0
  19. package/site/content/docs/console/privacy.mdx +32 -0
  20. package/site/content/docs/console/runs.mdx +40 -0
  21. package/site/content/docs/console/signals.mdx +31 -0
  22. package/site/content/docs/console/store.mdx +32 -0
  23. package/site/content/docs/console/tenancy.mdx +32 -0
  24. package/site/content/docs/console/traces.mdx +34 -0
  25. package/site/content/docs/console/vault.mdx +37 -0
  26. package/site/content/docs/elements/ai.mdx +180 -0
  27. package/site/content/docs/elements/channel.mdx +167 -0
  28. package/site/content/docs/elements/clock.mdx +182 -0
  29. package/site/content/docs/elements/flow.mdx +288 -0
  30. package/site/content/docs/elements/gate.mdx +171 -0
  31. package/site/content/docs/elements/meta.json +5 -0
  32. package/site/content/docs/elements/signal.mdx +171 -0
  33. package/site/content/docs/elements/store.mdx +320 -0
  34. package/site/content/docs/elements/vault.mdx +263 -0
  35. package/site/content/docs/get-started/basic-usage.mdx +124 -0
  36. package/site/content/docs/get-started/comparison.mdx +65 -0
  37. package/site/content/docs/get-started/installation.mdx +113 -0
  38. package/site/content/docs/get-started/introduction.mdx +123 -0
  39. package/site/content/docs/get-started/meta.json +5 -0
  40. package/site/content/docs/index.mdx +63 -0
  41. package/site/content/docs/meta.json +5 -0
  42. package/site/content/docs/plugins/compression.mdx +60 -0
  43. package/site/content/docs/plugins/cors.mdx +98 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +93 -0
  46. package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
  47. package/site/content/docs/plugins/meta.json +15 -0
  48. package/site/content/docs/plugins/security-headers.mdx +136 -0
  49. package/site/content/docs/reference/cli.md +101 -0
  50. package/site/content/docs/reference/configuration.mdx +159 -0
  51. package/site/content/docs/reference/environment-variables.mdx +87 -0
  52. package/site/content/docs/reference/errors.mdx +80 -0
  53. package/site/content/docs/reference/fx.mdx +117 -0
  54. package/site/content/docs/reference/meta.json +5 -0
  55. package/site/content/docs/reference/plugins.mdx +249 -0
  56. package/site/content/docs/reference/security.md +63 -0
  57. package/src/auth/auth.test.ts +3 -0
  58. package/src/cli/ask-dev-mode.ts +1 -1
  59. package/src/cli/db.ts +87 -17
  60. package/src/cli/dev-db-push.test.ts +32 -2
  61. package/src/cli/dev-schema-sync.test.ts +66 -0
  62. package/src/cli/dev-schema-sync.ts +139 -0
  63. package/src/cli/dev.test.ts +123 -1
  64. package/src/cli/dev.ts +150 -8
  65. package/src/cli/doc-staleness.test.ts +4 -4
  66. package/src/cli/docker-cli.test.ts +20 -0
  67. package/src/cli/docker.ts +10 -0
  68. package/src/cli/drizzle-env.test.ts +67 -0
  69. package/src/cli/drizzle-env.ts +78 -0
  70. package/src/cli/ensure-drizzle-config.ts +50 -0
  71. package/src/cli/hero-meta.test.ts +1 -1
  72. package/src/cli/load-config.ts +6 -0
  73. package/src/cli/mode.ts +24 -4
  74. package/src/cli/openbao-bootstrap.test.ts +149 -0
  75. package/src/cli/openbao-bootstrap.ts +314 -0
  76. package/src/cli/openbao-restart.integration.test.ts +136 -0
  77. package/src/cli/ports.test.ts +7 -5
  78. package/src/cli/ports.ts +6 -2
  79. package/src/cli/resolve-dev-sql-env.test.ts +48 -0
  80. package/src/cli/resolve-dev-sql-env.ts +42 -0
  81. package/src/cli/stack.ts +7 -4
  82. package/src/cli/vault-cmd.ts +63 -0
  83. package/src/client/types.ts +7 -1
  84. package/src/compiler/extract.test.ts +40 -0
  85. package/src/compiler/extract.ts +123 -1
  86. package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
  87. package/src/compiler/fixtures/skyport.expected.json +1 -1
  88. package/src/compiler/response.ts +12 -0
  89. package/src/config/define-config.test.ts +4 -6
  90. package/src/config/index.ts +2 -15
  91. package/src/console/server/app.ts +2 -0
  92. package/src/console/server/vault.ts +21 -6
  93. package/src/docker/compose.ts +112 -16
  94. package/src/docker/derive.ts +7 -1
  95. package/src/docker/docker.test.ts +103 -0
  96. package/src/docker/index.ts +11 -1
  97. package/src/docker/recipes/index.ts +3 -2
  98. package/src/docker/recipes/openbao.ts +47 -0
  99. package/src/docker/recipes/redis.ts +5 -1
  100. package/src/docker/recipes/rustfs.ts +2 -3
  101. package/src/docker/stack-id.test.ts +43 -8
  102. package/src/docker/stack-id.ts +99 -20
  103. package/src/docker/stack.ts +36 -4
  104. package/src/docker/types.ts +3 -0
  105. package/src/docs-origin.ts +4 -4
  106. package/src/drivers/drizzle-dialect.test.ts +20 -0
  107. package/src/drivers/drizzle-dialect.ts +37 -0
  108. package/src/drivers/index.ts +1 -2
  109. package/src/drivers/memory.ts +278 -39
  110. package/src/drivers/s3.ts +10 -1
  111. package/src/drivers/vault-driver-removal.test.ts +55 -0
  112. package/src/drivers/vault-openbao.test.ts +97 -0
  113. package/src/drivers/vault-openbao.ts +102 -35
  114. package/src/drivers/vault-types.ts +3 -10
  115. package/src/elements/store/declare.ts +4 -1
  116. package/src/elements/store/resource-list-docs.fixture.ts +56 -0
  117. package/src/elements/store/resource-list-docs.test.ts +79 -0
  118. package/src/elements/store/resource.test.ts +253 -0
  119. package/src/elements/store/resource.ts +786 -0
  120. package/src/elements/store/sql-condition.test.ts +132 -0
  121. package/src/elements/store/sql-condition.ts +284 -46
  122. package/src/elements/store/sql-session.test.ts +86 -1
  123. package/src/elements/store/sql-session.ts +187 -27
  124. package/src/elements/store/table.ts +34 -4
  125. package/src/elements/store.ts +16 -0
  126. package/src/elements/vault/runtime.ts +1 -1
  127. package/src/elements/vault.test.ts +1 -28
  128. package/src/elements/vault.ts +1 -1
  129. package/src/kernel/app.ts +59 -25
  130. package/src/kernel/boot-bind/channel.test.ts +60 -0
  131. package/src/kernel/boot-bind/channel.ts +64 -2
  132. package/src/kernel/boot-bind/store.test.ts +10 -1
  133. package/src/kernel/boot-bind/store.ts +49 -2
  134. package/src/kernel/boot.test.ts +0 -1
  135. package/src/kernel/boot.ts +1 -1
  136. package/src/kernel/edge.test.ts +68 -0
  137. package/src/kernel/errors.registry.test.ts +1 -1
  138. package/src/kernel/flow.ts +8 -0
  139. package/src/kernel/fx.test.ts +23 -3
  140. package/src/kernel/fx.ts +115 -18
  141. package/src/kernel/hooks.test.ts +33 -0
  142. package/src/kernel/hooks.ts +22 -0
  143. package/src/kernel/index.ts +7 -0
  144. package/src/kernel/on.ts +44 -3
  145. package/src/kernel/plugin.ts +33 -3
  146. package/src/kernel/registry-isolation.test.ts +74 -0
  147. package/src/kernel/registry.ts +22 -1
  148. package/src/kernel/triggers.ts +59 -0
  149. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  150. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  151. package/src/manifest/index.ts +1 -1
  152. package/src/manifest/types.ts +1 -1
  153. package/src/manifest/validate.ts +2 -2
  154. package/src/plugins/compression.test.ts +127 -0
  155. package/src/plugins/compression.ts +94 -0
  156. package/src/plugins/config-source.test.ts +204 -0
  157. package/src/plugins/config-source.ts +209 -0
  158. package/src/plugins/cors.test.ts +172 -0
  159. package/src/plugins/cors.ts +153 -0
  160. package/src/plugins/csrf.test.ts +102 -0
  161. package/src/plugins/csrf.ts +86 -0
  162. package/src/plugins/headers.ts +54 -0
  163. package/src/plugins/index.ts +26 -0
  164. package/src/plugins/ip-allowlist.test.ts +139 -0
  165. package/src/plugins/ip-allowlist.ts +129 -0
  166. package/src/plugins/maintenance-mode.test.ts +91 -0
  167. package/src/plugins/maintenance-mode.ts +85 -0
  168. package/src/plugins/security-headers.test.ts +243 -0
  169. package/src/plugins/security-headers.ts +255 -0
  170. package/src/release/measure.ts +1 -2
  171. package/src/test/create-test-app.ts +14 -2
  172. package/docs/spec/console.md +0 -762
  173. package/docs/spec/example.md +0 -1374
  174. package/docs/spec/four-applications.md +0 -1376
  175. package/docs/spec/unified-theory.md +0 -498
  176. package/src/cli/doc-drift.test.ts +0 -147
  177. package/src/cli/doc-drift.ts +0 -401
  178. package/src/cli/doctor-diff-examples.ts +0 -90
  179. package/src/drivers/vault-sops.ts +0 -246
  180. /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
@@ -1,147 +0,0 @@
1
- /**
2
- * Doc-drift parser / containment unit tests.
3
- */
4
-
5
- import { describe, expect, test } from "bun:test";
6
- import {
7
- checkDocDrift,
8
- checkMermaidSyntax,
9
- normalizeTs,
10
- parseClaimedFences,
11
- parseMermaidFences,
12
- } from "./doc-drift.ts";
13
-
14
- describe("doc-drift", () => {
15
- test("normalizeTs collapses whitespace (indent-insensitive)", () => {
16
- expect(normalizeTs("a \n b\t\n")).toBe("a b");
17
- expect(normalizeTs(" const t = 1;\n const u = 2;")).toBe("const t = 1; const u = 2;");
18
- });
19
-
20
- test("parseClaimedFences picks headed typescript only", () => {
21
- const md = `# 1 · BASIC — Notes
22
-
23
- ### \`oke.config.ts\`
24
-
25
- \`\`\`typescript
26
- export default 1;
27
- \`\`\`
28
-
29
- Unheaded illustration:
30
-
31
- \`\`\`typescript
32
- const t = 1;
33
- \`\`\`
34
-
35
- # 2 · INTERMEDIATE — Linkly
36
-
37
- ### \`src/gates.ts\` — gates
38
-
39
- \`\`\`ts
40
- export const g = 1;
41
- \`\`\`
42
-
43
- # REFERENCE
44
- `;
45
- const fences = parseClaimedFences(md);
46
- expect(fences).toHaveLength(2);
47
- expect(fences[0]).toMatchObject({
48
- app: "notes",
49
- relPath: "oke.config.ts",
50
- body: "export default 1;",
51
- });
52
- expect(fences[1]).toMatchObject({
53
- app: "linkly",
54
- relPath: "src/gates.ts",
55
- body: "export const g = 1;",
56
- });
57
- });
58
-
59
- test("parseClaimedFences accepts examples/<app>/… headings without app sections", () => {
60
- const md = `## Quick start
61
-
62
- ### \`examples/notes/src/app.ts\`
63
-
64
- \`\`\`typescript
65
- export const app = 1;
66
- \`\`\`
67
-
68
- ### \`examples/notes/src/flows/notes/index.ts\`
69
-
70
- \`\`\`ts
71
- export const create = 2;
72
- \`\`\`
73
-
74
- Unheaded:
75
-
76
- \`\`\`typescript
77
- const skip = true;
78
- \`\`\`
79
- `;
80
- const fences = parseClaimedFences(md);
81
- expect(fences).toHaveLength(2);
82
- expect(fences[0]).toMatchObject({
83
- app: "notes",
84
- relPath: "src/app.ts",
85
- body: "export const app = 1;",
86
- });
87
- expect(fences[1]).toMatchObject({
88
- app: "notes",
89
- relPath: "src/flows/notes/index.ts",
90
- body: "export const create = 2;",
91
- });
92
- });
93
-
94
- test("checkDocDrift reports missing files", async () => {
95
- const { ok, failures } = await checkDocDrift(
96
- [
97
- {
98
- app: "notes",
99
- relPath: "does-not-exist.ts",
100
- body: "x",
101
- headingLine: 1,
102
- },
103
- ],
104
- import.meta.dir,
105
- );
106
- expect(ok).toBe(false);
107
- expect(failures[0]).toContain("does not contain claimed fence");
108
- });
109
-
110
- test("parseMermaidFences extracts mermaid blocks", () => {
111
- const md = `## Architecture
112
-
113
- \`\`\`mermaid
114
- flowchart LR
115
- A --> B
116
- \`\`\`
117
-
118
- \`\`\`typescript
119
- const skip = true;
120
- \`\`\`
121
- `;
122
- const fences = parseMermaidFences(md);
123
- expect(fences).toHaveLength(1);
124
- expect(fences[0]!.body).toContain("flowchart LR");
125
- });
126
-
127
- test("checkMermaidSyntax accepts valid flowchart", async () => {
128
- const { ok, failures } = await checkMermaidSyntax([
129
- {
130
- body: `flowchart TD
131
- code["your code"] -->|compile| manifest["manifest.oke.json"]
132
- manifest --> client["typed client (+ live queries)"]`,
133
- startLine: 1,
134
- },
135
- ]);
136
- expect(ok).toBe(true);
137
- expect(failures).toEqual([]);
138
- });
139
-
140
- test("checkMermaidSyntax rejects invalid syntax", async () => {
141
- const { ok, failures } = await checkMermaidSyntax([
142
- { body: "flowchart LR\n A-->", startLine: 9 },
143
- ]);
144
- expect(ok).toBe(false);
145
- expect(failures[0]).toContain("mermaid: invalid syntax (line 9)");
146
- });
147
- });
@@ -1,401 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Doc-drift checker — claimed TypeScript fences in markdown docs must be
4
- * contained (whitespace-normalized) under `examples/<app>/`.
5
- *
6
- * Claimed fences are those under a heading like `### \`path/to/file.ts\``
7
- * (relative, under an app section in four-applications.md) or
8
- * `### \`examples/<app>/path/to/file.ts\`` (absolute from repo root — used by
9
- * README.md). Unheaded fences are illustrations and are ignored.
10
- *
11
- * Also validates ```mermaid fences with Mermaid's own parser (happy-dom
12
- * provides the DOM surface Bun needs for sanitize hooks).
13
- *
14
- * Usage:
15
- * bun src/cli/doc-drift.ts
16
- * bun src/cli/doc-drift.ts docs/spec/four-applications.md README.md
17
- * bun src/cli/doc-drift.ts site/content/docs/learn/notes.md
18
- *
19
- * Default paths: four-applications.md, README.md, and every markdown/MDX file
20
- * under site/content/docs (claimed fences only — prose-only pages skip).
21
- */
22
-
23
- import { resolve } from "node:path";
24
- import { Window } from "happy-dom";
25
-
26
- const ROOT = resolve(import.meta.dir, "../..");
27
-
28
- /**
29
- * Discover site content markdown that may contain claimed fences.
30
- *
31
- * @param root - Repo root
32
- */
33
- async function discoverSiteDocs(root: string): Promise<string[]> {
34
- const docsDir = resolve(root, "site/content/docs");
35
- const out: string[] = [];
36
- try {
37
- const glob = new Bun.Glob("**/*.{md,mdx}");
38
- for await (const rel of glob.scan({ cwd: docsDir, onlyFiles: true })) {
39
- out.push(resolve(docsDir, rel));
40
- }
41
- } catch {
42
- // site/ not present yet — skip
43
- }
44
- return out.sort();
45
- }
46
-
47
- /** Core docs that must contain claimed fences. */
48
- const REQUIRED_DOCS: readonly string[] = [
49
- resolve(ROOT, "docs/spec/four-applications.md"),
50
- resolve(ROOT, "README.md"),
51
- ];
52
-
53
- let mermaidReady: Promise<typeof import("mermaid").default> | undefined;
54
-
55
- /**
56
- * Install a minimal DOM so Mermaid's sanitize hooks can run under Bun.
57
- */
58
- function ensureDom(): void {
59
- const g = globalThis as typeof globalThis & { document?: unknown };
60
- if (g.document !== undefined) return;
61
- const window = new Window({ url: "https://localhost/" });
62
- Object.assign(g, {
63
- window,
64
- document: window.document,
65
- HTMLElement: window.HTMLElement,
66
- Element: window.Element,
67
- Node: window.Node,
68
- DOMParser: window.DOMParser,
69
- navigator: window.navigator,
70
- getComputedStyle: window.getComputedStyle.bind(window),
71
- });
72
- }
73
-
74
- /**
75
- * Lazy-load Mermaid after the DOM polyfill is in place.
76
- */
77
- async function loadMermaid(): Promise<typeof import("mermaid").default> {
78
- ensureDom();
79
- mermaidReady ??= import("mermaid").then((m) => m.default);
80
- return mermaidReady;
81
- }
82
-
83
- /** App section heading → example package directory name. */
84
- const APP_SECTIONS: ReadonlyArray<{ readonly re: RegExp; readonly app: string }> = [
85
- { re: /^# 1 · BASIC — Notes\b/m, app: "notes" },
86
- { re: /^# 2 · INTERMEDIATE — Linkly\b/m, app: "linkly" },
87
- { re: /^# 3 · ADVANCED — Provisions\b/m, app: "provisions" },
88
- { re: /^# 4 · COMPLEX — Skyport\b/m, app: "skyport" },
89
- ];
90
-
91
- /**
92
- * Normalize TypeScript source for containment comparison.
93
- *
94
- * @param text - Raw source
95
- */
96
- export function normalizeTs(text: string): string {
97
- // Collapse all whitespace so claimed fences match sources regardless of
98
- // indent (e.g. oxfmt-formatted bodies inside `test(...)` callbacks).
99
- return text.replace(/\s+/g, " ").trim();
100
- }
101
-
102
- /** One claimed fence extracted from a markdown document. */
103
- export interface ClaimedFence {
104
- readonly app: string;
105
- readonly relPath: string;
106
- readonly body: string;
107
- readonly headingLine: number;
108
- }
109
-
110
- /** One ```mermaid fence extracted from a markdown document. */
111
- export interface MermaidFence {
112
- readonly body: string;
113
- readonly startLine: number;
114
- }
115
-
116
- /**
117
- * Extract every fenced Mermaid diagram from a markdown document.
118
- *
119
- * @param markdown - Spec or README document
120
- */
121
- export function parseMermaidFences(markdown: string): MermaidFence[] {
122
- const lines = markdown.replace(/\r\n/g, "\n").split("\n");
123
- const fences: MermaidFence[] = [];
124
-
125
- for (let i = 0; i < lines.length; i++) {
126
- if (!/^```mermaid\s*$/.test(lines[i]!)) continue;
127
- const startLine = i + 1;
128
- i++;
129
- const bodyLines: string[] = [];
130
- while (i < lines.length && !lines[i]!.startsWith("```")) {
131
- bodyLines.push(lines[i]!);
132
- i++;
133
- }
134
- if (i >= lines.length || !lines[i]!.startsWith("```")) {
135
- throw new Error(`doc-drift: unclosed mermaid fence (line ${startLine})`);
136
- }
137
- fences.push({ body: bodyLines.join("\n").trim(), startLine });
138
- }
139
-
140
- return fences;
141
- }
142
-
143
- /**
144
- * Validate Mermaid fence bodies with Mermaid's parser.
145
- *
146
- * @param fences - Extracted mermaid fences
147
- */
148
- export async function checkMermaidSyntax(
149
- fences: readonly MermaidFence[],
150
- ): Promise<{ readonly ok: boolean; readonly failures: readonly string[] }> {
151
- if (fences.length === 0) {
152
- return { ok: true, failures: [] };
153
- }
154
-
155
- const mermaid = await loadMermaid();
156
- const failures: string[] = [];
157
-
158
- for (const fence of fences) {
159
- try {
160
- const result = await mermaid.parse(fence.body);
161
- if (!result) {
162
- failures.push(`mermaid: parse returned false (line ${fence.startLine})`);
163
- }
164
- } catch (err) {
165
- const detail = err instanceof Error ? err.message : String(err);
166
- failures.push(`mermaid: invalid syntax (line ${fence.startLine}): ${detail.split("\n")[0]}`);
167
- }
168
- }
169
-
170
- return { ok: failures.length === 0, failures };
171
- }
172
-
173
- /**
174
- * Read the typescript/ts fence immediately after a heading line.
175
- *
176
- * @param lines - Document lines
177
- * @param headingIndex - Index of the `### \`…\`` heading
178
- * @param relPath - Path shown in the heading (for error messages)
179
- */
180
- function readFenceAfterHeading(
181
- lines: readonly string[],
182
- headingIndex: number,
183
- relPath: string,
184
- ): { readonly body: string; readonly endIndex: number } | undefined {
185
- let j = headingIndex + 1;
186
- while (j < lines.length && lines[j]!.trim() === "") j++;
187
- const open = lines[j];
188
- if (!open || !/^```(?:typescript|ts)\s*$/.test(open)) return undefined;
189
-
190
- j++;
191
- const bodyLines: string[] = [];
192
- while (j < lines.length && !lines[j]!.startsWith("```")) {
193
- bodyLines.push(lines[j]!);
194
- j++;
195
- }
196
- if (j >= lines.length || !lines[j]!.startsWith("```")) {
197
- throw new Error(
198
- `doc-drift: unclosed fence after ### \`${relPath}\` (line ${headingIndex + 1})`,
199
- );
200
- }
201
-
202
- return { body: bodyLines.join("\n"), endIndex: j };
203
- }
204
-
205
- /**
206
- * Parse claimed fences from a markdown document.
207
- *
208
- * Supports (1) app-section context + relative `### \`src/…\`` headings
209
- * (four-applications.md) and (2) absolute `### \`examples/<app>/…\`` headings
210
- * (README.md). Stops at `# REFERENCE` / `# 5 ·` as in the four-apps spec.
211
- *
212
- * @param markdown - Spec or README document
213
- */
214
- export function parseClaimedFences(markdown: string): ClaimedFence[] {
215
- const lines = markdown.replace(/\r\n/g, "\n").split("\n");
216
- const fences: ClaimedFence[] = [];
217
- let app: string | undefined;
218
-
219
- for (let i = 0; i < lines.length; i++) {
220
- const line = lines[i]!;
221
-
222
- if (/^# REFERENCE\b/.test(line) || /^# 5 ·/.test(line)) {
223
- break;
224
- }
225
-
226
- for (const section of APP_SECTIONS) {
227
- if (section.re.test(line)) {
228
- app = section.app;
229
- break;
230
- }
231
- }
232
-
233
- const heading = /^### `([^`]+)`/.exec(line);
234
- if (!heading) continue;
235
-
236
- const claimedPath = heading[1]!;
237
- const examplesMatch = /^examples\/([^/]+)\/(.+)$/.exec(claimedPath);
238
-
239
- let fenceApp: string | undefined;
240
- let relPath: string;
241
-
242
- if (examplesMatch) {
243
- fenceApp = examplesMatch[1]!;
244
- relPath = examplesMatch[2]!;
245
- } else if (app) {
246
- fenceApp = app;
247
- relPath = claimedPath;
248
- } else {
249
- continue;
250
- }
251
-
252
- const fence = readFenceAfterHeading(lines, i, claimedPath);
253
- if (!fence) continue;
254
-
255
- fences.push({
256
- app: fenceApp,
257
- relPath,
258
- body: fence.body,
259
- headingLine: i + 1,
260
- });
261
- i = fence.endIndex;
262
- }
263
-
264
- return fences;
265
- }
266
-
267
- /**
268
- * Whether `expected` is contained in a file, or any file under a directory path.
269
- *
270
- * @param targetPath - File or directory (`tests/`)
271
- * @param expected - Normalized fence body
272
- */
273
- async function containsFence(targetPath: string, expected: string): Promise<boolean> {
274
- const direct = Bun.file(targetPath);
275
- if (await direct.exists()) {
276
- const st = await direct.stat();
277
- if (st.isFile()) {
278
- return normalizeTs(await direct.text()).includes(expected);
279
- }
280
- }
281
-
282
- // Directory claim (`### \`tests/\``) — any nested file may contain the fence.
283
- const dirPath = targetPath.endsWith("/") ? targetPath.slice(0, -1) : targetPath;
284
- try {
285
- const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx}");
286
- for await (const rel of glob.scan({ cwd: dirPath, onlyFiles: true })) {
287
- const text = await Bun.file(resolve(dirPath, rel)).text();
288
- if (normalizeTs(text).includes(expected)) return true;
289
- }
290
- } catch {
291
- return false;
292
- }
293
- return false;
294
- }
295
-
296
- /**
297
- * Check that every claimed fence is contained in its example file.
298
- *
299
- * @param fences - Claimed fences
300
- * @param root - Repo root
301
- */
302
- export async function checkDocDrift(
303
- fences: readonly ClaimedFence[],
304
- root = ROOT,
305
- ): Promise<{ readonly ok: boolean; readonly failures: readonly string[] }> {
306
- const failures: string[] = [];
307
-
308
- for (const fence of fences) {
309
- const targetPath = resolve(root, "examples", fence.app, fence.relPath);
310
- const expected = normalizeTs(fence.body);
311
- const contained = await containsFence(targetPath, expected);
312
- if (!contained) {
313
- const kind = fence.relPath.endsWith("/") ? "directory" : "file";
314
- failures.push(
315
- `${fence.app}: ${kind} ${fence.relPath} does not contain claimed fence (spec line ${fence.headingLine})`,
316
- );
317
- }
318
- }
319
-
320
- return { ok: failures.length === 0, failures };
321
- }
322
-
323
- /**
324
- * Run doc-drift over one or more markdown files.
325
- *
326
- * Site content under `site/` may have zero claimed fences (prose-only pages);
327
- * only `docs/spec/four-applications.md` and `README.md` require at least one.
328
- *
329
- * @param paths - Absolute or repo-relative markdown paths
330
- */
331
- export async function runDocDrift(
332
- paths: readonly string[],
333
- ): Promise<{ readonly ok: boolean; readonly messages: readonly string[] }> {
334
- const messages: string[] = [];
335
- let ok = true;
336
- let total = 0;
337
- let mermaidTotal = 0;
338
- let checkedFiles = 0;
339
-
340
- for (const path of paths) {
341
- const abs = resolve(ROOT, path);
342
- const label = abs.startsWith(ROOT + "/") ? abs.slice(ROOT.length + 1) : abs;
343
- const requireClaimed = !label.startsWith("site/");
344
- const markdown = await Bun.file(abs).text();
345
- const fences = parseClaimedFences(markdown);
346
- if (fences.length === 0) {
347
- if (requireClaimed) {
348
- messages.push(`doc-drift: no claimed fences found in ${label}`);
349
- ok = false;
350
- }
351
- } else {
352
- checkedFiles++;
353
- const result = await checkDocDrift(fences);
354
- total += fences.length;
355
- if (!result.ok) {
356
- ok = false;
357
- messages.push(`doc-drift: ${result.failures.length} failure(s) in ${label}`);
358
- for (const f of result.failures) messages.push(` · ${f}`);
359
- } else {
360
- messages.push(`doc-drift: ok — ${fences.length} claimed fence(s) in ${label}`);
361
- }
362
- }
363
-
364
- const mermaidFences = parseMermaidFences(markdown);
365
- if (mermaidFences.length > 0) {
366
- mermaidTotal += mermaidFences.length;
367
- const mermaidResult = await checkMermaidSyntax(mermaidFences);
368
- if (!mermaidResult.ok) {
369
- ok = false;
370
- messages.push(`doc-drift: ${mermaidResult.failures.length} mermaid failure(s) in ${label}`);
371
- for (const f of mermaidResult.failures) messages.push(` · ${f}`);
372
- } else {
373
- messages.push(`doc-drift: ok — ${mermaidFences.length} mermaid fence(s) in ${label}`);
374
- }
375
- }
376
- }
377
-
378
- if (ok && checkedFiles > 1) {
379
- const mermaidNote = mermaidTotal > 0 ? ` · ${mermaidTotal} mermaid fence(s)` : "";
380
- messages.push(
381
- `doc-drift: ok — ${total} claimed fence(s) across ${checkedFiles} file(s)${mermaidNote}`,
382
- );
383
- }
384
-
385
- return { ok, messages };
386
- }
387
-
388
- async function main(): Promise<number> {
389
- const args = process.argv.slice(2);
390
- const paths = args.length > 0 ? args : [...REQUIRED_DOCS, ...(await discoverSiteDocs(ROOT))];
391
- const { ok, messages } = await runDocDrift(paths);
392
- for (const m of messages) {
393
- if (ok) console.log(m);
394
- else console.error(m);
395
- }
396
- return ok ? 0 : 1;
397
- }
398
-
399
- if (import.meta.main) {
400
- process.exit(await main());
401
- }
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * CI gate: `oke doctor --diff` across the four reference apps.
4
- *
5
- * Extracts Manifests from working-tree sources and from the git merge-base,
6
- * then fails on undeclared contract breaks.
7
- */
8
-
9
- import { resolve } from "node:path";
10
- import { extractManifest, type SourceFile } from "../compiler/extract.ts";
11
- import { undeclaredContractBreaks } from "../manifest/undeclared.ts";
12
- import { gitMergeBase, gitShow } from "./doctor-diff.ts";
13
-
14
- const ROOT = resolve(import.meta.dir, "../..");
15
- const APPS = ["notes", "linkly", "provisions", "skyport"] as const;
16
-
17
- /**
18
- * Run undeclared-break checks for every example app.
19
- *
20
- * @returns Exit code
21
- */
22
- export async function runDoctorDiffExamples(): Promise<number> {
23
- const mergeBase = await gitMergeBase(ROOT);
24
- let failed = 0;
25
-
26
- for (const app of APPS) {
27
- const rootDir = resolve(ROOT, `examples/${app}`);
28
- const after = await extractManifest({ rootDir });
29
- const extracted = mergeBase
30
- ? await extractAtRevision(ROOT, mergeBase, `examples/${app}`)
31
- : after;
32
- // No sources at merge-base → empty baseline (additions are no-impact).
33
- const before = extracted ?? { oke: after.oke, app: after.app };
34
-
35
- const undeclared = undeclaredContractBreaks(before, after);
36
- if (undeclared.length === 0) {
37
- console.log(`oke doctor --diff examples/${app}: ok`);
38
- continue;
39
- }
40
- failed++;
41
- console.log(
42
- `oke doctor --diff examples/${app}: ${undeclared.length} undeclared contract-breaking change(s)`,
43
- );
44
- for (const c of undeclared) {
45
- console.log(` [contract-breaking] ${c.path}: ${c.summary}`);
46
- }
47
- }
48
-
49
- if (failed > 0) {
50
- console.log("Acknowledge intentional breaks with `breaking: true` on the owning flow.");
51
- }
52
- return failed > 0 ? 1 : 0;
53
- }
54
-
55
- /**
56
- * Extract a Manifest from sources as they existed at a git revision.
57
- *
58
- * @param repoRoot - Repository root
59
- * @param rev - Commit-ish
60
- * @param appRel - App directory relative to repo root (e.g. `examples/notes`)
61
- */
62
- async function extractAtRevision(
63
- repoRoot: string,
64
- rev: string,
65
- appRel: string,
66
- ): Promise<Awaited<ReturnType<typeof extractManifest>> | null> {
67
- const workingRoot = resolve(repoRoot, appRel);
68
- // Bun.Glob (global) — bare `import … from "bun"` is rejected by JSR.
69
- const glob = new Bun.Glob("**/*.{ts,tsx}");
70
- const files: SourceFile[] = [];
71
-
72
- for await (const rel of glob.scan({
73
- cwd: workingRoot,
74
- onlyFiles: true,
75
- })) {
76
- if (rel.includes("node_modules/") || rel.endsWith(".test.ts")) continue;
77
- const repoPath = `${appRel}/${rel}`;
78
- const source = await gitShow(repoRoot, rev, repoPath);
79
- if (source === null) continue;
80
- files.push({ path: rel, source });
81
- }
82
-
83
- if (files.length === 0) return null;
84
- files.sort((a, b) => a.path.localeCompare(b.path));
85
- return extractManifest({ files });
86
- }
87
-
88
- if (import.meta.main) {
89
- process.exit(await runDoctorDiffExamples());
90
- }