duckfn-docs-kit 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -24,7 +24,7 @@ npm install duckfn-docs-kit
24
24
  | --- | --- | --- |
25
25
  | `duckfn-docs-kit` | browser | Home-page custom elements (`<dfk-hero>`, `<dfk-features>`, `<dfk-next-steps>`, `<dfk-sql>`), `registerDfkElements()` and the value types their setters accept |
26
26
  | `duckfn-docs-kit/remark` | Node (build) | `remarkVersionPlaceholder`: replaces `{{DUCKFN_VERSION}}` inside `text` / `inlineCode` / `code` nodes |
27
- | `duckfn-docs-kit/sql/remark` | Node (build) | `remarkRunnableSql`: turns fenced `sql run` blocks into `<dfk-sql>` elements |
27
+ | `duckfn-docs-kit/sql/remark` | Node (build) | `remarkRunnableSql`: turns fenced `sql {"type":"duckfn",…}` blocks into `<dfk-sql>` elements |
28
28
  | `duckfn-docs-kit/sql/extensions` | Node (build) | `dfkExtensions()` Docusaurus plugin: preloads a site's DuckDB extensions before the first block runs |
29
29
  | `duckfn-docs-kit/toc-toggle/plugin` | Node (build) | `dfkTocToggle()` Docusaurus plugin: adds the TOC collapse control |
30
30
  | `duckfn-docs-kit/toc-toggle/TocToggle` | browser | The TOC collapse class, for a site that drives it itself |
@@ -97,10 +97,12 @@ styles inside the JS bundle, so they need nothing here.
97
97
 
98
98
  ## Documentation
99
99
 
100
- The user guide lives at <https://shijianjs.github.io/duckfn/docs/docs-kit>. The
101
- conventions this package is written to — retained-mode components, shadow DOM,
102
- SSR safety, the SQL rendering contract — are documented in
103
- [`AGENTS.md`](./AGENTS.md), which ships inside the package.
100
+ The user guide lives at <https://shijianjs.github.io/duckfn/docs/docs-kit>. If you —
101
+ or an AI agent — are *using* this package, read [`AGENTS.md`](./AGENTS.md): it ships
102
+ inside the package and states the contracts (the block metastring, the config
103
+ fields, the traps). The conventions this package is *written to* — retained-mode
104
+ components, shadow DOM, SSR safety — live in `CONVENTIONS.md` in the repository and
105
+ are not published.
104
106
 
105
107
  ## License
106
108
 
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `duckfn-sql-verify` executable.
4
+ *
5
+ * A hand-written wrapper rather than a built entry: Vite's library build emits
6
+ * ESM and does not preserve a shebang, and npm only needs one file with one and
7
+ * an executable bit. `dist/` is built by `prepack`, so the import below always
8
+ * resolves in a published tarball.
9
+ */
10
+ import {cliMain} from '../dist/sql/verify.js';
11
+
12
+ await cliMain(process.argv.slice(2));
package/dist/remark.d.ts CHANGED
@@ -13,7 +13,7 @@ import type { Plugin } from 'unified';
13
13
  */
14
14
  export declare const DEFAULT_VERSION_PLACEHOLDER = "{{DUCKFN_VERSION}}";
15
15
  export interface VersionPlaceholderOptions {
16
- /** The real version string to substitute in, e.g. `0.0.13`. */
16
+ /** The real version string to substitute in, e.g. `0.0.14`. */
17
17
  version: string;
18
18
  /** Override the token if a site uses a different one. */
19
19
  placeholder?: string;
@@ -0,0 +1,29 @@
1
+ import { type RunnableSqlConfig } from './remark';
2
+ /** One runnable block, positioned so a failure can name it. */
3
+ export interface RunnableSqlBlock {
4
+ /** Path relative to the site root, always with forward slashes. */
5
+ file: string;
6
+ /** 1-based line of the opening fence. */
7
+ line: number;
8
+ config: RunnableSqlConfig;
9
+ sql: string;
10
+ }
11
+ export interface CollectRunnableSqlOptions {
12
+ /** Site root the reported paths are relative to, and the base of a relative dir. */
13
+ siteDir: string;
14
+ /**
15
+ * Directories to scan (absolute, or relative to `siteDir`). Missing ones are
16
+ * skipped rather than reported: a site may have no translations.
17
+ */
18
+ contentDirs: readonly string[];
19
+ /** File extensions to scan; both `.md` and `.mdx` are markdown to us. */
20
+ extensions?: readonly string[];
21
+ }
22
+ /** Every runnable block under `contentDirs`, in file order, then line order. */
23
+ export declare function collectRunnableSql(options: CollectRunnableSqlOptions): RunnableSqlBlock[];
24
+ /**
25
+ * Whether a block is expected to fail — the block's own `"expect": "error"`
26
+ * metadata, never its prose or its comments: an expectation that the SQL test
27
+ * suite acts on has to be data, not a string match on a comment.
28
+ */
29
+ export declare function expectsError(config: RunnableSqlConfig): boolean;
@@ -0,0 +1,65 @@
1
+ import { parseRunnableSqlMeta as e } from "./remark.js";
2
+ import { join as t, relative as n, sep as r } from "node:path";
3
+ import { readFileSync as i, readdirSync as a, statSync as o } from "node:fs";
4
+ //#region src/sql/collect.ts
5
+ var s = [".md", ".mdx"];
6
+ function c(e) {
7
+ let { siteDir: a, contentDirs: o, extensions: c = s } = e, d = [];
8
+ for (let e of o) l(t(a, e), c, d);
9
+ let f = [];
10
+ for (let e of d.sort()) for (let t of u(i(e, "utf8"))) f.push({
11
+ ...t,
12
+ file: n(a, e).split(r).join("/")
13
+ });
14
+ return f;
15
+ }
16
+ function l(e, n, r) {
17
+ let i;
18
+ try {
19
+ i = a(e);
20
+ } catch {
21
+ return;
22
+ }
23
+ for (let a of i) {
24
+ let i = t(e, a);
25
+ o(i).isDirectory() ? l(i, n, r) : n.some((e) => a.endsWith(e)) && r.push(i);
26
+ }
27
+ }
28
+ function u(t) {
29
+ let n = t.split("\n"), r = [], i = null, a = [];
30
+ for (let t = 0; t < n.length; t++) {
31
+ let o = n[t] ?? "", s = /^(`{3,}|~{3,})(.*)$/.exec(o);
32
+ if (!s) {
33
+ i && a.push(o);
34
+ continue;
35
+ }
36
+ let [c, l] = [s[1] ?? "", (s[2] ?? "").trim()];
37
+ if (!i) {
38
+ i = {
39
+ marker: c,
40
+ info: l,
41
+ line: t + 1
42
+ }, a = [];
43
+ continue;
44
+ }
45
+ if (!(c.charAt(0) === i.marker.charAt(0) && c.length >= i.marker.length && l === "")) {
46
+ a.push(o);
47
+ continue;
48
+ }
49
+ if (i.info.startsWith("sql")) {
50
+ let t = e(i.info.slice(3).trim());
51
+ t && r.push({
52
+ line: i.line,
53
+ config: t,
54
+ sql: a.join("\n")
55
+ });
56
+ }
57
+ i = null, a = [];
58
+ }
59
+ return r;
60
+ }
61
+ function d(e) {
62
+ return e.expect === "error";
63
+ }
64
+ //#endregion
65
+ export { c as collectRunnableSql, d as expectsError };
@@ -0,0 +1,51 @@
1
+ export interface DuckdbQueryResult {
2
+ numRows: number;
3
+ schema: {
4
+ fields: {
5
+ name: string;
6
+ }[];
7
+ };
8
+ }
9
+ export interface DuckdbConnection {
10
+ query(sql: string): Promise<DuckdbQueryResult>;
11
+ }
12
+ export type WasmPlatform = 'eh' | 'mvp';
13
+ export interface RunnerOptions {
14
+ /**
15
+ * The extension to `LOAD`: a local `.duckdb_extension.wasm` path (served to
16
+ * the worker over a loopback http server) or an absolute `http(s)` URL.
17
+ */
18
+ extension: string;
19
+ /**
20
+ * DuckDB-Wasm platform. Must match how the extension was built — a site
21
+ * serving `duckfn-wasm_eh.duckdb_extension.wasm` runs the `eh` bundle.
22
+ */
23
+ platform?: WasmPlatform;
24
+ /** The engine wasm; defaults to the one shipped beside the worker bundle. */
25
+ engine?: string;
26
+ }
27
+ export interface RunResult {
28
+ rows: number;
29
+ columns: number;
30
+ }
31
+ /**
32
+ * One DuckDB-Wasm instance at a time, driven from Node.
33
+ *
34
+ * `newPage()` is what a docs site does per page load: a fresh instance, a fresh
35
+ * connection, the extension loaded again. Blocks of one page then share state
36
+ * (a table created in one block is visible to the next), while pages stay
37
+ * isolated — which is why the runner is used one page at a time rather than
38
+ * over a single long-lived connection.
39
+ */
40
+ export declare class WasmSqlRunner {
41
+ #private;
42
+ private constructor();
43
+ static create(options: RunnerOptions): Promise<WasmSqlRunner>;
44
+ /** Drops the current instance and starts a fresh page: new instance, new connection. */
45
+ newPage(): Promise<void>;
46
+ /** Runs one block; the caller decides whether a failure is expected. */
47
+ run(sql: string): Promise<RunResult>;
48
+ /** Where the fetched extension was staged, for diagnostics. */
49
+ get stagingDir(): string | null;
50
+ close(): Promise<void>;
51
+ }
@@ -0,0 +1,115 @@
1
+ import { createRequire as e } from "node:module";
2
+ import { basename as t, dirname as n, join as r } from "node:path";
3
+ import { mkdirSync as i, readFileSync as a, rmSync as o } from "node:fs";
4
+ import { createServer as s } from "node:http";
5
+ import { Worker as c } from "node:worker_threads";
6
+ import { homedir as l } from "node:os";
7
+ //#region src/sql/nodeRunner.ts
8
+ var u = e(import.meta.url), d = "@duckdb/duckdb-wasm/dist/duckdb-node.cjs", f = class {
9
+ #e;
10
+ #t = /* @__PURE__ */ new Map();
11
+ onmessage = null;
12
+ onerror = null;
13
+ onclose = null;
14
+ constructor(e) {
15
+ this.#e = new c(u.resolve(d), { workerData: {
16
+ mod: e,
17
+ name: "",
18
+ type: ""
19
+ } }), this.#e.on("message", (e) => this.#n("message", e)), this.#e.on("error", (e) => this.#n("error", e)), this.#e.on("exit", () => this.#n("close"));
20
+ }
21
+ #n(e, t) {
22
+ let n = {
23
+ type: e,
24
+ data: t,
25
+ target: this,
26
+ currentTarget: this
27
+ }, r = this[`on${e}`];
28
+ typeof r == "function" && r(n);
29
+ for (let t of this.#t.get(e) ?? []) t(n);
30
+ }
31
+ addEventListener(e, t) {
32
+ this.#t.set(e, [...this.#t.get(e) ?? [], t]);
33
+ }
34
+ removeEventListener(e, t) {
35
+ this.#t.set(e, (this.#t.get(e) ?? []).filter((e) => e !== t));
36
+ }
37
+ postMessage(e, t) {
38
+ this.#e.postMessage(e, t ?? []);
39
+ }
40
+ terminate() {
41
+ return this.#e.terminate();
42
+ }
43
+ }, p = class e {
44
+ #e = null;
45
+ #t = null;
46
+ #n = null;
47
+ #r = null;
48
+ #i;
49
+ #a;
50
+ #o;
51
+ constructor(e, t, n) {
52
+ this.#i = e, this.#a = t, this.#o = n;
53
+ }
54
+ static async create(t) {
55
+ let i = t.platform ?? "eh", a = u.resolve(`@duckdb/duckdb-wasm/dist/duckdb-node-${i}.worker.cjs`), o = t.engine ?? r(n(a), `duckdb-${i}.wasm`), s = /^https?:\/\//i.test(t.extension) ? null : await m(t.extension), c = s ? s.url : t.extension, l = new e(c, o, a);
56
+ return l.#n = s ? s.server : null, l.#r = s ? h(s.stagingHost, s.stagingSegment) : null, l;
57
+ }
58
+ async newPage() {
59
+ this.#e && (await this.#e.terminate(), this.#e = null, this.#t = null);
60
+ let e = await import(
61
+ /* @vite-ignore */
62
+ d
63
+ ), t = new e.AsyncDuckDB(new e.ConsoleLogger(), new f(this.#o));
64
+ await t.instantiate(this.#a, null), await t.open({ allowUnsignedExtensions: !0 });
65
+ let n = await t.connect();
66
+ await n.query(`LOAD '${this.#i}'`), this.#e = t, this.#t = n;
67
+ }
68
+ async run(e) {
69
+ let t = this.#t;
70
+ if (!t) throw Error("sql/verify: no page is open — call newPage() first");
71
+ let n = await t.query(e);
72
+ return {
73
+ rows: n.numRows,
74
+ columns: n.schema.fields.length
75
+ };
76
+ }
77
+ get stagingDir() {
78
+ return this.#r;
79
+ }
80
+ async close() {
81
+ this.#e && (await this.#e.terminate(), this.#e = null, this.#t = null), this.#n &&= (await new Promise((e) => this.#n?.close(() => e())), null);
82
+ }
83
+ };
84
+ async function m(e) {
85
+ let n = a(e), r = t(e), i = g(e), o = s((e, t) => {
86
+ t.writeHead(200, {
87
+ "content-type": "application/octet-stream",
88
+ "content-length": n.length
89
+ }), t.end(n);
90
+ }), c = process.platform === "win32" ? 80 : 0;
91
+ await new Promise((e, t) => {
92
+ o.once("error", (e) => {
93
+ t(/* @__PURE__ */ Error(`sql/verify: cannot serve the extension on port ${c} (${e.code}): a port in the URL would put a colon into DuckDB's staging path, which is not a legal Windows path — free port 80, or load the extension from an http(s) URL with --extension`));
94
+ }), o.listen(c, "localhost", e);
95
+ });
96
+ let l = o.address(), u = typeof l == "object" && l ? l.port : c, d = u === 80 ? "localhost" : `localhost:${u}`;
97
+ return {
98
+ server: o,
99
+ url: `http://${d}/${i}/${r}`,
100
+ stagingHost: d,
101
+ stagingSegment: i
102
+ };
103
+ }
104
+ function h(e, t) {
105
+ let n = r(l(), ".duckdb", "extensions", e, t);
106
+ return o(n, {
107
+ recursive: !0,
108
+ force: !0
109
+ }), i(n, { recursive: !0 }), n;
110
+ }
111
+ function g(e) {
112
+ return t(e).split(".")[0] ?? "";
113
+ }
114
+ //#endregion
115
+ export { p as WasmSqlRunner };
@@ -36,6 +36,17 @@ export interface RunnableSqlConfig {
36
36
  * iframe, so scripts run with an opaque origin.
37
37
  */
38
38
  show?: 'table' | 'html' | 'iframe' | 'svg' | 'text';
39
+ /**
40
+ * What this block is expected to do when the docs' own SQL test suite runs it
41
+ * (`duckfn-docs-kit/sql/verify`). Defaults to `'ok'`; `'error'` marks a block
42
+ * that demonstrates a failure — the suite then *requires* it to fail, and
43
+ * reports it when it unexpectedly succeeds instead.
44
+ *
45
+ * This is the only source of truth for the expectation: the prose around a
46
+ * block, and a `-- error: …` comment inside it, are there for readers, and
47
+ * neither is machine-checked.
48
+ */
49
+ expect?: 'ok' | 'error';
39
50
  /**
40
51
  * The column holding the markup, for the preview renderers. A single-column
41
52
  * result is unambiguous and is used as-is.
@@ -85,4 +96,13 @@ export interface RunnableSqlOptions {
85
96
  }
86
97
  /** The custom element the plugin emits; must match `register.ts`. */
87
98
  export declare const DFK_SQL_TAG = "dfk-sql";
99
+ /**
100
+ * Parse the metastring; `null` means "not a runnable block, leave it alone".
101
+ *
102
+ * Exported because the block contract has two consumers: this plugin, which
103
+ * turns a block into `<dfk-sql>` at build time, and `sql/verify` (via
104
+ * `sql/collect`), which runs those same blocks in CI. Both have to agree on
105
+ * what counts as runnable, so there is one parser.
106
+ */
107
+ export declare function parseRunnableSqlMeta(meta: string | null | undefined): RunnableSqlConfig | null;
88
108
  export declare const remarkRunnableSql: Plugin<[RunnableSqlOptions?]>;
@@ -66,4 +66,4 @@ var a = (e = {}) => (e) => {
66
66
  n(e);
67
67
  };
68
68
  //#endregion
69
- export { e as DFK_SQL_TAG, a as remarkRunnableSql };
69
+ export { e as DFK_SQL_TAG, t as parseRunnableSqlMeta, a as remarkRunnableSql };
@@ -0,0 +1,61 @@
1
+ import { type WasmPlatform } from './nodeRunner';
2
+ export interface VerifyOptions {
3
+ /** Docs site root; defaults to the working directory. */
4
+ siteDir?: string;
5
+ /** Content directories relative to the site root; defaults to the site layout. */
6
+ contentDirs?: readonly string[];
7
+ /**
8
+ * The extension to `LOAD`: a path, or an absolute `http(s)` URL. Defaults to
9
+ * the single file under `<siteDir>/static/duckdb-extensions/`.
10
+ */
11
+ extension?: string;
12
+ /** DuckDB-Wasm platform, which must match the extension build. */
13
+ platform?: WasmPlatform;
14
+ /** Engine wasm override, for pinning a specific DuckDB-Wasm build. */
15
+ engine?: string;
16
+ /** Per-block timeout in milliseconds; a hang is reported instead of blocking CI. */
17
+ timeoutMs?: number;
18
+ /**
19
+ * Directory the blocks run in. Defaults to a fresh temporary directory that
20
+ * is removed afterwards: a block may `COPY … TO 'a.csv'`, and on Node
21
+ * DuckDB's file system is the real one, relative to the working directory.
22
+ */
23
+ workingDir?: string;
24
+ /** Write the full result list here as JSON. */
25
+ reportFile?: string;
26
+ }
27
+ /** What happened to a block, judged against what it declared. */
28
+ export type BlockOutcome =
29
+ /** Ran, and was expected to run. */
30
+ 'ok'
31
+ /** Failed, and declared `"expect": "error"`. */
32
+ | 'error-as-expected'
33
+ /** Failed, but was expected to run. */
34
+ | 'unexpected-error'
35
+ /** Ran, but declared `"expect": "error"`. */
36
+ | 'unexpected-success';
37
+ export interface BlockResult {
38
+ file: string;
39
+ line: number;
40
+ outcome: BlockOutcome;
41
+ detail: string;
42
+ }
43
+ export interface VerifyReport {
44
+ blocks: BlockResult[];
45
+ /** Blocks that behaved as declared: they ran, or they failed as declared. */
46
+ asDeclared: BlockResult[];
47
+ /** Blocks that did not behave as declared — the ones that should fail CI. */
48
+ unexpected: BlockResult[];
49
+ }
50
+ /** Runs every runnable block of the site and returns the outcome of each. */
51
+ export declare function verifySqlDocs(options?: VerifyOptions): Promise<VerifyReport>;
52
+ export interface CliOptions extends VerifyOptions {
53
+ quiet?: boolean;
54
+ }
55
+ /**
56
+ * `duckfn-sql-verify` — the command line around {@link verifySqlDocs}.
57
+ *
58
+ * Exit code 1 when a block did not behave as it declared, so a docs site can
59
+ * wire it straight into `npm test`.
60
+ */
61
+ export declare function cliMain(argv: readonly string[]): Promise<void>;
@@ -0,0 +1,148 @@
1
+ import { collectRunnableSql as e, expectsError as t } from "./collect.js";
2
+ import { WasmSqlRunner as n } from "./nodeRunner.js";
3
+ import { join as r, resolve as i } from "node:path";
4
+ import { mkdirSync as a, mkdtempSync as o, readdirSync as s, rmSync as c, statSync as l, writeFileSync as u } from "node:fs";
5
+ import { tmpdir as d } from "node:os";
6
+ //#region src/sql/verify.ts
7
+ var f = ["docs", "i18n"], p = 3e4;
8
+ async function m(t = {}) {
9
+ let n = i(t.siteDir ?? process.cwd()), s = e({
10
+ siteDir: n,
11
+ contentDirs: t.contentDirs ?? v(n)
12
+ }), l = t.extension, f = l ? /^https?:\/\//i.test(l) ? l : i(l) : y(n), m = t.timeoutMs ?? p, g = t.workingDir ? i(t.workingDir) : o(r(d(), "duckfn-sql-verify-"));
13
+ a(g, { recursive: !0 });
14
+ let _ = process.cwd();
15
+ process.chdir(g);
16
+ let b;
17
+ try {
18
+ b = await h(s, f, t, m);
19
+ } finally {
20
+ process.chdir(_), t.workingDir || c(g, {
21
+ recursive: !0,
22
+ force: !0
23
+ });
24
+ }
25
+ let x = {
26
+ blocks: b,
27
+ asDeclared: b.filter((e) => e.outcome === "ok" || e.outcome === "error-as-expected"),
28
+ unexpected: b.filter((e) => e.outcome === "unexpected-error" || e.outcome === "unexpected-success")
29
+ };
30
+ return t.reportFile && u(t.reportFile, `${JSON.stringify(x, null, 2)}\n`), x;
31
+ }
32
+ async function h(e, t, r, i) {
33
+ let a = await n.create({
34
+ extension: t,
35
+ platform: r.platform,
36
+ engine: r.engine
37
+ }), o = [];
38
+ try {
39
+ let t = null;
40
+ for (let n of e) n.file !== t && (t = n.file, await a.newPage()), o.push(await g(a, n, i));
41
+ } finally {
42
+ await a.close();
43
+ }
44
+ return o;
45
+ }
46
+ async function g(e, n, r) {
47
+ let i = t(n.config);
48
+ try {
49
+ let t = await _(e.run(n.sql), r);
50
+ return {
51
+ file: n.file,
52
+ line: n.line,
53
+ outcome: i ? "unexpected-success" : "ok",
54
+ detail: i ? `declared "expect": "error" but succeeded (${t.rows}×${t.columns})` : `ok (${t.rows}×${t.columns})`
55
+ };
56
+ } catch (e) {
57
+ let t = String(e?.message ?? e).split("\n")[0] ?? "";
58
+ return {
59
+ file: n.file,
60
+ line: n.line,
61
+ outcome: i ? "error-as-expected" : "unexpected-error",
62
+ detail: t
63
+ };
64
+ }
65
+ }
66
+ function _(e, t) {
67
+ let n, r = new Promise((e, r) => {
68
+ n = setTimeout(() => r(/* @__PURE__ */ Error(`timed out after ${t}ms`)), t);
69
+ });
70
+ return Promise.race([e, r]).finally(() => clearTimeout(n));
71
+ }
72
+ function v(e) {
73
+ let t = [];
74
+ b(r(e, f[0])) && t.push(f[0]);
75
+ let n = r(e, f[1]);
76
+ if (b(n)) for (let e of s(n)) b(r(n, e, "docusaurus-plugin-content-docs", "current")) && t.push(r("i18n", e, "docusaurus-plugin-content-docs", "current"));
77
+ return t.length > 0 ? t : ["."];
78
+ }
79
+ function y(e) {
80
+ let t = r(e, "static", "duckdb-extensions"), n = b(t) ? s(t).filter((e) => e.endsWith(".duckdb_extension.wasm")) : [];
81
+ if (n.length === 0) throw Error(`sql/verify: no extension found in ${t} — pass --extension <file|url>, or let the site's extension preload plugin fetch it first`);
82
+ if (n.length > 1) throw Error(`sql/verify: several extensions found in ${t} (${n.join(", ")}) — pass --extension to pick one`);
83
+ return r(t, n[0]);
84
+ }
85
+ function b(e) {
86
+ return l(e, { throwIfNoEntry: !1 })?.isDirectory() ?? !1;
87
+ }
88
+ async function x(e) {
89
+ let t = C(e);
90
+ if (t.help) {
91
+ process.stdout.write(S);
92
+ return;
93
+ }
94
+ let n = Date.now(), r = await m(t), i = ((Date.now() - n) / 1e3).toFixed(1), a = r.blocks.filter((e) => e.outcome === "error-as-expected").length;
95
+ if (t.quiet || (process.stdout.write(`\n${r.blocks.length} block(s) in ${i}s\n`), process.stdout.write(` ${r.asDeclared.length} as declared (${a} erroring on purpose), ${r.unexpected.length} unexpected\n`)), r.unexpected.length > 0) {
96
+ process.stdout.write("unexpected behaviour:\n");
97
+ for (let e of r.unexpected) process.stdout.write(`- ${e.file}:${e.line} [${e.outcome}] :: ${e.detail}\n`);
98
+ process.exitCode = 1;
99
+ }
100
+ }
101
+ var S = "Usage: duckfn-sql-verify [options]\n\nRuns every runnable SQL block of a duckfn docs site in DuckDB-Wasm, and checks\nthat each one behaves as its own metadata declares (\"expect\": \"error\" for a\nblock that demonstrates a failure).\n\n --site <dir> Docs site root (default: the working directory)\n --content <dir> Content directory, relative to the site root (repeatable;\n default: docs/ plus every i18n/<locale>/… translation)\n --extension <path> The extension to preload: a .duckdb_extension.wasm path,\n or an absolute http(s) URL (default: the single file under\n static/duckdb-extensions/)\n --platform <eh|mvp> DuckDB-Wasm bundle, which must match the extension build\n (default: eh)\n --engine <path> Engine wasm override\n --timeout <ms> Per-block timeout (default: 30000)\n --working-dir <dir> Directory the blocks run in (default: a temporary one)\n --report <file> Write the full result list as JSON\n --quiet Only report unexpected behaviour\n --help Show this help\n";
102
+ function C(e) {
103
+ let t = {}, n = [];
104
+ for (let r = 0; r < e.length; r++) {
105
+ let i = e[r], a = () => {
106
+ let t = e[++r];
107
+ if (t === void 0) throw Error(`sql/verify: ${i} needs a value`);
108
+ return t;
109
+ };
110
+ switch (i) {
111
+ case "--site":
112
+ t.siteDir = a();
113
+ break;
114
+ case "--content":
115
+ n.push(a());
116
+ break;
117
+ case "--extension":
118
+ t.extension = a();
119
+ break;
120
+ case "--platform":
121
+ t.platform = a();
122
+ break;
123
+ case "--engine":
124
+ t.engine = a();
125
+ break;
126
+ case "--timeout":
127
+ t.timeoutMs = Number(a());
128
+ break;
129
+ case "--working-dir":
130
+ t.workingDir = a();
131
+ break;
132
+ case "--report":
133
+ t.reportFile = a();
134
+ break;
135
+ case "--quiet":
136
+ t.quiet = !0;
137
+ break;
138
+ case "--help":
139
+ case "-h":
140
+ t.help = !0;
141
+ break;
142
+ default: throw Error(`sql/verify: unknown option ${i}`);
143
+ }
144
+ }
145
+ return n.length > 0 && (t.contentDirs = n), t;
146
+ }
147
+ //#endregion
148
+ export { x as cliMain, m as verifySqlDocs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duckfn-docs-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Shared Docusaurus building blocks (TOC toggle, home-page web components, brand tokens, remark version placeholder) for duckfn-family extension docs sites.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -24,6 +24,9 @@
24
24
  "type": "module",
25
25
  "main": "./dist/index.js",
26
26
  "types": "./dist/index.d.ts",
27
+ "bin": {
28
+ "duckfn-sql-verify": "./bin/sql-verify.mjs"
29
+ },
27
30
  "exports": {
28
31
  ".": {
29
32
  "types": "./dist/index.d.ts",
@@ -38,6 +41,7 @@
38
41
  "files": [
39
42
  "dist",
40
43
  "src",
44
+ "bin",
41
45
  "AGENTS.md"
42
46
  ],
43
47
  "scripts": {
package/src/remark.ts CHANGED
@@ -15,7 +15,7 @@ import type {Plugin} from 'unified';
15
15
  export const DEFAULT_VERSION_PLACEHOLDER = '{{DUCKFN_VERSION}}';
16
16
 
17
17
  export interface VersionPlaceholderOptions {
18
- /** The real version string to substitute in, e.g. `0.0.13`. */
18
+ /** The real version string to substitute in, e.g. `0.0.14`. */
19
19
  version: string;
20
20
  /** Override the token if a site uses a different one. */
21
21
  placeholder?: string;
package/src/sql/DfkSql.ts CHANGED
@@ -18,7 +18,7 @@ import {el, HTMLElementBase} from '../dom';
18
18
  * Everything but the result is in the shadow root. The result is a slotted
19
19
  * light-DOM sibling because VTable injects a *document-level* stylesheet that a
20
20
  * shadow boundary could not host — and it is only created when a query runs,
21
- * long after hydration, so the light DOM still starts empty (AGENTS.md rule 11).
21
+ * long after hydration, so the light DOM still starts empty (CONVENTIONS.md rule 11).
22
22
  * The editor has no such problem: it sits in this shadow root, so CodeMirror's
23
23
  * style-mod resolves the root to the same tree its styles are used in.
24
24
  *
@@ -30,7 +30,7 @@ import {el, HTMLElementBase} from '../dom';
30
30
  * which a renderer builds, so the component keeps the node (and its state) and
31
31
  * hands it over through `RenderContext.fullscreenButton`.
32
32
  *
33
- * Content entry is an **attribute seed** (see AGENTS.md rule 5 exception): the
33
+ * Content entry is an **attribute seed** (see CONVENTIONS.md rule 5 exception): the
34
34
  * `config` / `sql` attributes are read once in `connectedCallback` because the
35
35
  * remark-generated JSX cannot hand content through a ref setter. Reading once
36
36
  * to initialise is not an attribute→render loop, so the retained-mode contract