okengine 0.2.8 → 0.3.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 +17 -15
- package/README.md +57 -29
- package/package.json +11 -16
- package/site/content/docs/ai/llms-txt.mdx +54 -0
- package/site/content/docs/ai/mcp.mdx +123 -0
- package/site/content/docs/ai/meta.json +5 -0
- package/site/content/docs/ai/skills.mdx +53 -0
- package/site/content/docs/console/access.mdx +29 -0
- package/site/content/docs/console/ai.mdx +35 -0
- package/site/content/docs/console/architecture.mdx +35 -0
- package/site/content/docs/console/channels.mdx +37 -0
- package/site/content/docs/console/clock.mdx +31 -0
- package/site/content/docs/console/flows.mdx +31 -0
- package/site/content/docs/console/gates.mdx +35 -0
- package/site/content/docs/console/manifest-diff.mdx +34 -0
- package/site/content/docs/console/meta.json +23 -0
- package/site/content/docs/console/overview.mdx +40 -0
- package/site/content/docs/console/plugins.mdx +41 -0
- package/site/content/docs/console/privacy.mdx +32 -0
- package/site/content/docs/console/runs.mdx +40 -0
- package/site/content/docs/console/signals.mdx +31 -0
- package/site/content/docs/console/store.mdx +32 -0
- package/site/content/docs/console/tenancy.mdx +32 -0
- package/site/content/docs/console/traces.mdx +34 -0
- package/site/content/docs/console/vault.mdx +37 -0
- package/site/content/docs/elements/ai.mdx +180 -0
- package/site/content/docs/elements/channel.mdx +167 -0
- package/site/content/docs/elements/clock.mdx +182 -0
- package/site/content/docs/elements/flow.mdx +288 -0
- package/site/content/docs/elements/gate.mdx +171 -0
- package/site/content/docs/elements/meta.json +5 -0
- package/site/content/docs/elements/signal.mdx +171 -0
- package/site/content/docs/elements/store.mdx +320 -0
- package/site/content/docs/elements/vault.mdx +263 -0
- package/site/content/docs/get-started/basic-usage.mdx +124 -0
- package/site/content/docs/get-started/comparison.mdx +65 -0
- package/site/content/docs/get-started/installation.mdx +113 -0
- package/site/content/docs/get-started/introduction.mdx +123 -0
- package/site/content/docs/get-started/meta.json +5 -0
- package/site/content/docs/index.mdx +63 -0
- package/site/content/docs/meta.json +5 -0
- package/site/content/docs/plugins/compression.mdx +60 -0
- package/site/content/docs/plugins/cors.mdx +92 -0
- package/site/content/docs/plugins/csrf.mdx +96 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
- package/site/content/docs/plugins/meta.json +15 -0
- package/site/content/docs/plugins/security-headers.mdx +136 -0
- package/site/content/docs/reference/cli.md +101 -0
- package/site/content/docs/reference/configuration.mdx +159 -0
- package/site/content/docs/reference/environment-variables.mdx +87 -0
- package/site/content/docs/reference/errors.mdx +80 -0
- package/site/content/docs/reference/fx.mdx +117 -0
- package/site/content/docs/reference/meta.json +5 -0
- package/site/content/docs/reference/plugins.mdx +249 -0
- package/site/content/docs/reference/security.md +63 -0
- package/src/auth/auth.test.ts +3 -0
- package/src/cli/ask-dev-mode.ts +1 -1
- package/src/cli/db.ts +87 -17
- package/src/cli/dev-db-push.test.ts +32 -2
- package/src/cli/dev-schema-sync.test.ts +66 -0
- package/src/cli/dev-schema-sync.ts +139 -0
- package/src/cli/dev.test.ts +123 -1
- package/src/cli/dev.ts +150 -8
- package/src/cli/doc-staleness.test.ts +4 -4
- package/src/cli/docker-cli.test.ts +20 -0
- package/src/cli/docker.ts +10 -0
- package/src/cli/drizzle-env.test.ts +67 -0
- package/src/cli/drizzle-env.ts +78 -0
- package/src/cli/ensure-drizzle-config.ts +50 -0
- package/src/cli/hero-meta.test.ts +1 -1
- package/src/cli/load-config.ts +6 -0
- package/src/cli/mode.ts +24 -4
- package/src/cli/openbao-bootstrap.test.ts +147 -0
- package/src/cli/openbao-bootstrap.ts +280 -0
- package/src/cli/openbao-restart.integration.test.ts +136 -0
- package/src/cli/ports.test.ts +7 -5
- package/src/cli/ports.ts +6 -2
- package/src/cli/resolve-dev-sql-env.test.ts +48 -0
- package/src/cli/resolve-dev-sql-env.ts +42 -0
- package/src/cli/stack.ts +7 -4
- package/src/cli/vault-cmd.ts +63 -0
- package/src/client/types.ts +7 -1
- package/src/compiler/extract.test.ts +40 -0
- package/src/compiler/extract.ts +123 -1
- package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/response.ts +12 -0
- package/src/config/define-config.test.ts +4 -6
- package/src/config/index.ts +2 -15
- package/src/console/server/app.ts +2 -0
- package/src/console/server/vault.ts +21 -6
- package/src/docker/compose.ts +112 -16
- package/src/docker/derive.ts +7 -1
- package/src/docker/docker.test.ts +103 -0
- package/src/docker/index.ts +11 -1
- package/src/docker/recipes/index.ts +3 -2
- package/src/docker/recipes/openbao.ts +47 -0
- package/src/docker/recipes/redis.ts +5 -1
- package/src/docker/recipes/rustfs.ts +2 -3
- package/src/docker/stack-id.test.ts +43 -8
- package/src/docker/stack-id.ts +99 -20
- package/src/docker/stack.ts +36 -4
- package/src/docker/types.ts +3 -0
- package/src/docs-origin.ts +4 -4
- package/src/drivers/drizzle-dialect.test.ts +20 -0
- package/src/drivers/drizzle-dialect.ts +37 -0
- package/src/drivers/index.ts +1 -2
- package/src/drivers/memory.ts +278 -39
- package/src/drivers/s3.ts +10 -1
- package/src/drivers/vault-driver-removal.test.ts +55 -0
- package/src/drivers/vault-openbao.test.ts +97 -0
- package/src/drivers/vault-openbao.ts +102 -35
- package/src/drivers/vault-types.ts +3 -10
- package/src/elements/store/declare.ts +4 -1
- package/src/elements/store/resource-list-docs.fixture.ts +56 -0
- package/src/elements/store/resource-list-docs.test.ts +79 -0
- package/src/elements/store/resource.test.ts +253 -0
- package/src/elements/store/resource.ts +786 -0
- package/src/elements/store/sql-condition.test.ts +132 -0
- package/src/elements/store/sql-condition.ts +284 -46
- package/src/elements/store/sql-session.test.ts +86 -1
- package/src/elements/store/sql-session.ts +187 -27
- package/src/elements/store/table.ts +34 -4
- package/src/elements/store.ts +16 -0
- package/src/elements/vault/runtime.ts +1 -1
- package/src/elements/vault.test.ts +1 -28
- package/src/elements/vault.ts +1 -1
- package/src/kernel/app.ts +59 -25
- package/src/kernel/boot-bind/channel.test.ts +60 -0
- package/src/kernel/boot-bind/channel.ts +64 -2
- package/src/kernel/boot-bind/store.test.ts +10 -1
- package/src/kernel/boot-bind/store.ts +49 -2
- package/src/kernel/boot.test.ts +0 -1
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/edge.test.ts +68 -0
- package/src/kernel/errors.registry.test.ts +1 -1
- package/src/kernel/flow.ts +8 -0
- package/src/kernel/fx.test.ts +23 -3
- package/src/kernel/fx.ts +115 -18
- package/src/kernel/hooks.test.ts +33 -0
- package/src/kernel/hooks.ts +22 -0
- package/src/kernel/index.ts +7 -0
- package/src/kernel/on.ts +44 -3
- package/src/kernel/plugin.ts +33 -3
- package/src/kernel/registry-isolation.test.ts +74 -0
- package/src/kernel/registry.ts +22 -1
- package/src/kernel/triggers.ts +59 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/manifest/index.ts +1 -1
- package/src/manifest/types.ts +1 -1
- package/src/manifest/validate.ts +2 -2
- package/src/plugins/compression.test.ts +127 -0
- package/src/plugins/compression.ts +94 -0
- package/src/plugins/config-source.test.ts +204 -0
- package/src/plugins/config-source.ts +209 -0
- package/src/plugins/cors.test.ts +138 -0
- package/src/plugins/cors.ts +129 -0
- package/src/plugins/csrf.test.ts +102 -0
- package/src/plugins/csrf.ts +86 -0
- package/src/plugins/headers.ts +54 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/ip-allowlist.test.ts +105 -0
- package/src/plugins/ip-allowlist.ts +76 -0
- package/src/plugins/maintenance-mode.test.ts +91 -0
- package/src/plugins/maintenance-mode.ts +85 -0
- package/src/plugins/security-headers.test.ts +243 -0
- package/src/plugins/security-headers.ts +255 -0
- package/src/release/measure.ts +1 -2
- package/src/test/create-test-app.ts +14 -2
- package/docs/spec/console.md +0 -762
- package/docs/spec/example.md +0 -1374
- package/docs/spec/four-applications.md +0 -1376
- package/docs/spec/unified-theory.md +0 -498
- package/src/cli/doc-drift.test.ts +0 -147
- package/src/cli/doc-drift.ts +0 -401
- package/src/cli/doctor-diff-examples.ts +0 -90
- package/src/drivers/vault-sops.ts +0 -246
- /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
|
-
});
|
package/src/cli/doc-drift.ts
DELETED
|
@@ -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
|
-
}
|