diff --git a/AGENTS.md b/AGENTS.md
index 52336be6..237fd7f5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,14 +12,13 @@ Altertable CLI — a TypeScript/Bun command-line tool for querying and managing
| ---------------------- | --------------------------------------------------------- |
| `cli/src/` | CLI commands, HTTP clients, formatting, config |
| `cli/src/**/*.test.ts` | Colocated Bun unit tests for CLI logic |
-| `data-app/runtime/` | Shared data app package, contracts, components, unit tests |
| `data-app/starter/` | Runnable getting-started app copied by `app create` |
-| `data-app/tests/` | Browser tests and fixtures for the starter and runtime |
+| `data-app/tests/` | Browser tests and fixtures consuming the published package |
| `tests/` | Black-box end-user CLI tests run through `bin/altertable` |
| `specs/` | Client API specs (submodule — read-only from this repo) |
| `bin/altertable` | Dev launcher — do not edit |
-For data app runtime, starter, distribution, and browser work, follow [data-app/AGENTS.md](data-app/AGENTS.md).
+For data app starter, distribution, and browser work, follow [data-app/AGENTS.md](data-app/AGENTS.md).
## Start here
@@ -33,13 +32,11 @@ git submodule update --init --recursive # first checkout only
For data app changes, run the focused checks first from the repository root:
```fish
-bun run --cwd data-app/runtime typecheck
-bun run --cwd data-app/runtime test
bun run --cwd data-app/starter build
bun run --cwd cli data-app:test:browser
```
-The starter build requires its local runtime; see [data-app/README.md](data-app/README.md)
+The starter build requires its published package dependencies; see [data-app/README.md](data-app/README.md)
for setup and browser prerequisites. Run `bun run --cwd cli data-app:check` to check all
three data app projects together. Use `./scripts/verify.sh --quick` after focused checks,
then default `./scripts/verify.sh` before opening a PR.
diff --git a/COMMANDS.md b/COMMANDS.md
index df07c000..b42fe78e 100644
--- a/COMMANDS.md
+++ b/COMMANDS.md
@@ -425,7 +425,7 @@ altertable app create|dev|build|check|upgrade
- `dev` — Preview a data app locally with lakehouse access.
- `build` — Typecheck and build a data app without Altertable credentials.
- `check` — Validate a data app's format, lint, types, contracts, build, and client credential boundary.
-- `upgrade` — Update an unmodified data app runtime to the CLI's current version.
+- `upgrade` — Update the data app package to the CLI's tested version.
**Examples**
@@ -484,14 +484,12 @@ altertable app dev [options]
| --- | --- |
| `--dir
` | App directory (default: current directory). |
| `--port ` | Local dev server port (1–65535; default: app setting). |
-| `--watch-runtime` | Upgrade generated runtime on source changes and restart preview. |
**Examples**
```bash
altertable app dev
altertable app dev --port 3022
-altertable app dev --watch-runtime
altertable --profile staging app dev --dir ./my-app
```
@@ -544,7 +542,7 @@ altertable app check --lakehouse
#### `altertable app upgrade`
-Update an unmodified data app runtime to the CLI's current version.
+Update the data app package to the CLI's tested version.
**Usage**
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index ba2be584..eb09a30e 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -231,4 +231,4 @@ When bumping the `specs/` submodule, extend the mapped tests before merge.
## Data apps
-The runnable starter and shared runtime live in [`data-app/`](data-app/README.md). That guide covers source ownership, the copy allowlist, CLI embedding, runtime upgrades, and browser tests.
+The runnable starter and package-consumer browser tests live in [`data-app/`](data-app/README.md). That guide covers starter embedding, npm package pins, package upgrades, and verification. Runtime development and releases belong to [the Data App repository](https://github.com/altertable-ai/data-app).
diff --git a/cli-reference.json b/cli-reference.json
index 743b3a05..b4f71b70 100644
--- a/cli-reference.json
+++ b/cli-reference.json
@@ -897,22 +897,11 @@
"repeatable": false,
"scope": "command",
"values": []
- },
- {
- "name": "watch-runtime",
- "aliases": [],
- "type": "boolean",
- "description": "Upgrade generated runtime on source changes and restart preview.",
- "required": false,
- "repeatable": false,
- "scope": "command",
- "values": []
}
],
"examples": [
"altertable app dev",
"altertable app dev --port 3022",
- "altertable app dev --watch-runtime",
"altertable --profile staging app dev --dir ./my-app"
],
"subcommands": []
@@ -984,7 +973,7 @@
{
"id": "altertable-app-upgrade",
"command": "altertable app upgrade",
- "description": "Update an unmodified data app runtime to the CLI's current version.",
+ "description": "Update the data app package to the CLI's tested version.",
"usage": [
"altertable app upgrade [options]"
],
diff --git a/cli/AGENTS.md b/cli/AGENTS.md
index 1832689f..68b85b7e 100644
--- a/cli/AGENTS.md
+++ b/cli/AGENTS.md
@@ -51,9 +51,8 @@ bun test "$PWD"/tests/integration.e2e.ts
## Conventions
-- For generated app authoring, edit [the starter app guide](../data-app/starter/AGENTS.md). Keep it a task router into focused authoring guides and the runtime API map. Types describe props; reserve JSDoc for constraints, ownership, security boundaries, and surprising behavior.
-- When changing a runtime primitive, preserve semantic ownership: layout spacing belongs to `AppLayout`, `Stack`, and `TabPanels`; filters and date bounds belong to their variable and control contracts; request states belong to `DataSection`. Add a prop or slot when app authors repeatedly need the same custom wrapper.
-- Add recurring icons to `../data-app/runtime/src/react/ui/icons.ts` with a semantic name and fixed optical size. Keep toolbar and presentation button variants explicit in their owning components. Verify the generated app in both themes and at desktop and phone widths.
+- For generated app authoring, edit [the starter app guide](../data-app/starter/AGENTS.md). Keep it a task router into focused authoring guides and the installed package docs. Types describe props; reserve JSDoc for constraints, ownership, security boundaries, and surprising behavior.
+- Runtime contracts and UI components are maintained in [the Data App repository](https://github.com/altertable-ai/data-app). This repository owns the starter and its integration with CLI commands.
- Declare and export each command immediately after its imports; keep supporting helpers and types below it.
- Import command types and `defineArgs` from `src/lib/command.ts`; its metadata drives parsing, help, completion, and generated documentation.
diff --git a/cli/package.json b/cli/package.json
index b0ad54e6..4a2c9372 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -44,7 +44,6 @@
"pack:check": "bun run build && bun pm pack --dry-run",
"knip": "knip --no-config-hints",
"knip:production": "knip --production --no-config-hints",
- "data-app:setup": "bun run scripts/package-data-app.ts --setup",
"data-app:check": "bun run scripts/check-data-app.ts",
"data-app:test:browser": "cd ../data-app/tests && bun run test"
},
diff --git a/cli/scripts/check-data-app.ts b/cli/scripts/check-data-app.ts
index fe6f3b14..e51aa073 100644
--- a/cli/scripts/check-data-app.ts
+++ b/cli/scripts/check-data-app.ts
@@ -1,17 +1,11 @@
import { join } from "node:path";
import { dataAppDirectory } from "@/commands/app/lib/distribution.ts";
-import { setupDataApp } from "@/../scripts/package-data-app.ts";
-await setupDataApp();
-for (const project of ["runtime", "starter", "tests"]) {
+for (const project of ["starter", "tests"]) {
const commands = [
["install", "--frozen-lockfile"],
...["typecheck", "lint", "format:check"].map((name) => ["run", name]),
- ...(project === "runtime"
- ? [["test", "tests"]]
- : project === "starter"
- ? [["run", "build"]]
- : []),
+ ...(project === "starter" ? [["run", "build"]] : []),
];
for (const args of commands) {
const child = Bun.spawn([process.execPath, ...args], {
diff --git a/cli/scripts/package-data-app.ts b/cli/scripts/package-data-app.ts
index c7b102e8..ad5e221b 100644
--- a/cli/scripts/package-data-app.ts
+++ b/cli/scripts/package-data-app.ts
@@ -1,18 +1,10 @@
-import { mkdir, writeFile } from "node:fs/promises";
-import { dirname, join } from "node:path";
import type { BunPlugin } from "bun";
-import { upgradeApp } from "@/commands/app/upgrade.ts";
-import {
- dataAppDirectory,
- readDataAppPayload,
- runtimeIntegrity,
- runtimePath,
-} from "@/commands/app/lib/distribution.ts";
+import { readDataAppPayload } from "@/commands/app/lib/distribution.ts";
-/** Both npm and native releases embed the same deterministic source payload. */
+/** Both npm and native releases embed the same deterministic starter template. */
export function dataAppPlugin(): BunPlugin {
return {
- name: "data-app-payload",
+ name: "data-app-starter",
setup(build) {
build.onLoad({ filter: /[/\\]commands[/\\]app[/\\]lib[/\\]payload\.ts$/ }, async () => ({
contents: `export const dataAppPayload = ${JSON.stringify(await readDataAppPayload())};`,
@@ -21,26 +13,3 @@ export function dataAppPlugin(): BunPlugin {
},
};
}
-
-export async function setupDataApp(): Promise {
- const payload = await readDataAppPayload();
- const directory = join(dataAppDirectory, "starter", runtimePath);
- if (await Bun.file(join(directory, "integrity.json")).exists()) {
- await upgradeApp(join(dataAppDirectory, "starter"), { runtimeFiles: payload.runtime });
- return;
- }
- for (const [name, content] of Object.entries(payload.runtime)) {
- await mkdir(dirname(join(directory, name)), { recursive: true });
- await writeFile(join(directory, name), content);
- }
- await writeFile(
- join(directory, "integrity.json"),
- `${JSON.stringify(runtimeIntegrity(payload.runtime), null, 2)}\n`,
- );
-}
-
-if (import.meta.main) {
- if (!Bun.argv.includes("--setup"))
- throw new Error("Usage: bun run scripts/package-data-app.ts --setup");
- await setupDataApp();
-}
diff --git a/cli/scripts/smoke-data-app.ts b/cli/scripts/smoke-data-app.ts
index fc998aef..bec4b383 100644
--- a/cli/scripts/smoke-data-app.ts
+++ b/cli/scripts/smoke-data-app.ts
@@ -2,7 +2,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
-/** Exercise the shipped CLI outside the checkout, including its embedded app sources. */
+/** Exercise the shipped CLI outside the checkout, including its embedded starter and published runtime. */
export async function smokeDataApp(command: string[], scaffoldOnly = false): Promise {
const directory = await mkdtemp(join(tmpdir(), "altertable-packaged-app-"));
const app = join(directory, "app");
@@ -24,10 +24,21 @@ export async function smokeDataApp(command: string[], scaffoldOnly = false): Pro
[...command, "app", "create", "package-smoke", "--dir", app, "--without-profile"],
directory,
);
- if (!(await Bun.file(join(app, ".altertable/runtime/src/server/index.ts")).exists()))
- throw new Error("Packaged runtime is missing");
+ const manifest = await Bun.file(join(app, "package.json")).json();
+ if (!/^\d+\.\d+\.\d+$/.test(manifest.dependencies?.["@altertable/data-app"] ?? ""))
+ throw new Error("Packaged starter must pin a published runtime");
+ if (await Bun.file(join(app, ".altertable/runtime/package.json")).exists())
+ throw new Error("Packaged starter must not vendor the runtime");
if (scaffoldOnly) return;
await run([process.execPath, "install", "--frozen-lockfile", "--ignore-scripts"], app);
+ await run(
+ [
+ process.execPath,
+ "-e",
+ 'import { createDataHandler } from "@altertable/data-app/server"; import { localLakehouse } from "@altertable/data-app/server/bun"; if (typeof createDataHandler !== "function" || typeof localLakehouse !== "function") process.exit(1);',
+ ],
+ app,
+ );
await run([...command, "app", "check", "--dir", app], directory);
} finally {
await rm(directory, { recursive: true, force: true });
diff --git a/cli/src/commands/app/check.test.ts b/cli/src/commands/app/check.test.ts
index 8ba6454a..92dca45a 100644
--- a/cli/src/commands/app/check.test.ts
+++ b/cli/src/commands/app/check.test.ts
@@ -8,6 +8,20 @@ let directory: string;
beforeEach(() => {
directory = mkdtempSync(join(tmpdir(), "altertable-app-check-"));
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), { recursive: true });
+ writeFileSync(
+ join(directory, "node_modules/@altertable/data-app/package.json"),
+ JSON.stringify({
+ name: "@altertable/data-app",
+ type: "module",
+ exports: {
+ "./appearance": "./dist/core/appearance.js",
+ "./server": "./dist/server/index.js",
+ "./server/bun": "./dist/server/local.js",
+ },
+ }),
+ );
});
afterEach(() => {
rmSync(directory, { recursive: true, force: true });
@@ -16,8 +30,10 @@ afterEach(() => {
describe("data app contract", () => {
test("rejects unbounded or unchecked operations in the app process", async () => {
mkdirSync(join(directory, "src"));
- mkdirSync(join(directory, ".altertable/runtime/src/core"), { recursive: true });
- mkdirSync(join(directory, ".altertable/runtime/src/server"), { recursive: true });
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), {
+ recursive: true,
+ });
writeFileSync(
join(directory, "app.json"),
JSON.stringify({
@@ -26,7 +42,7 @@ describe("data app contract", () => {
}),
);
writeFileSync(
- join(directory, ".altertable/runtime/src/core/appearance.ts"),
+ join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
"export function parseAppearance() {}",
);
writeFileSync(
@@ -85,11 +101,11 @@ describe("data app contract", () => {
test("browser bundles reject value imports of operations but allow type imports", async () => {
mkdirSync(join(directory, "src"));
- mkdirSync(join(directory, ".altertable/runtime/src/core"), { recursive: true });
- mkdirSync(join(directory, ".altertable/runtime/src/server"), { recursive: true });
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), { recursive: true });
writeFileSync(join(directory, "app.json"), JSON.stringify({ schemaVersion: 1, title: "Test" }));
writeFileSync(
- join(directory, ".altertable/runtime/src/core/appearance.ts"),
+ join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
"export function parseAppearance() {}",
);
writeFileSync(
@@ -113,24 +129,24 @@ test("browser bundles reject value imports of operations but allow type imports"
await checkAppProject(directory);
});
-test("lakehouse checks execute every declared input through the current runtime layout", async () => {
+test("lakehouse checks execute every declared input through public package exports", async () => {
mkdirSync(join(directory, "src"));
- mkdirSync(join(directory, ".altertable/runtime/src/core"), { recursive: true });
- mkdirSync(join(directory, ".altertable/runtime/src/server"), { recursive: true });
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
+ mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), { recursive: true });
writeFileSync(
join(directory, "app.json"),
JSON.stringify({ schemaVersion: 1, title: "Live check" }),
);
writeFileSync(
- join(directory, ".altertable/runtime/src/core/appearance.ts"),
+ join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
"export function parseAppearance() {}",
);
writeFileSync(
- join(directory, ".altertable/runtime/src/server/local.ts"),
+ join(directory, "node_modules/@altertable/data-app/dist/server/local.js"),
"export function localLakehouse() { return {}; }",
);
writeFileSync(
- join(directory, ".altertable/runtime/src/server/index.ts"),
+ join(directory, "node_modules/@altertable/data-app/dist/server/index.js"),
`
export function createDataHandler(operations) {
return async (request) => {
@@ -157,3 +173,44 @@ test("lakehouse checks execute every declared input through the current runtime
await checkAppProject(directory, {});
expect(readFileSync(join(directory, "executed.txt"), "utf8")).toBe("12");
});
+
+test.each(["@altertable/data-app/server", "@altertable/data-app/server/bun"])(
+ "browser bundles reject the published %s entry even without credential strings",
+ async (entry) => {
+ mkdirSync(join(directory, "src"));
+ writeFileSync(
+ join(directory, "app.json"),
+ JSON.stringify({ schemaVersion: 1, title: "Boundary" }),
+ );
+ writeFileSync(
+ join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
+ "export function parseAppearance() {}",
+ );
+ writeFileSync(
+ join(directory, "node_modules/@altertable/data-app/dist/server/index.js"),
+ "export const secretFreeHandler = 1;",
+ );
+ writeFileSync(
+ join(directory, "node_modules/@altertable/data-app/dist/server/local.js"),
+ "export const secretFreeHandler = 1;",
+ );
+ writeFileSync(
+ join(directory, "src/operations.ts"),
+ "export const operations = { test: { checks: [{}], input: v => v, output: v => v, run: async () => ({}), policy: { maxQueryRows: 1, maxDurationMs: 1000 } } };",
+ );
+ writeFileSync(
+ join(directory, "src/index.html"),
+ '',
+ );
+ writeFileSync(
+ join(directory, "src/main.ts"),
+ `import { secretFreeHandler } from "${entry}"; console.log(secretFreeHandler);`,
+ );
+ expect(checkAppProject(directory)).rejects.toThrow("Data app validation failed.");
+ writeFileSync(
+ join(directory, "src/main.ts"),
+ `import type { secretFreeHandler } from "${entry}"; console.log("browser safe");`,
+ );
+ await checkAppProject(directory);
+ },
+);
diff --git a/cli/src/commands/app/check.ts b/cli/src/commands/app/check.ts
index 0cfe6819..863a4e7e 100644
--- a/cli/src/commands/app/check.ts
+++ b/cli/src/commands/app/check.ts
@@ -13,7 +13,7 @@ import {
requireAppScripts,
runAppCommand,
} from "@/commands/app/lib/run.ts";
-import { currentRuntimeIntegrity, installedRuntimeIntegrity } from "@/commands/app/lib/runtime.ts";
+import { requirePublishedDataApp } from "@/commands/app/lib/package.ts";
import projectCheckScript from "@/commands/app/lib/project-check.js.txt";
type AppManifest = {
@@ -42,17 +42,8 @@ export const appCheckCommand = defineCommand({
requireAppScripts(directory, ["format:check", "lint", "typecheck", "build"]);
const manifest = await readManifest(directory);
if (args.lakehouse) await checkAppScope(directory, execution.profile);
- const installed = await installedRuntimeIntegrity(directory);
- const current = currentRuntimeIntegrity();
- if (
- installed.version !== current.version ||
- Object.entries(current.sha256).some(([name, checksum]) => installed.sha256[name] !== checksum)
- ) {
- throw new ConfigurationError(
- `Data app runtime ${installed.version} is outdated. Run \`altertable app upgrade\`.`,
- );
- }
- if ((await runAppCommand("install", directory)) !== 0) return { exitCode: EXIT_GENERIC };
+ await requirePublishedDataApp(directory);
+ if ((await runAppCommand("install-frozen", directory)) !== 0) return { exitCode: EXIT_GENERIC };
if ((await runAppCommand("format:check", directory)) !== 0) return { exitCode: EXIT_GENERIC };
if ((await runAppCommand("lint", directory)) !== 0) return { exitCode: EXIT_GENERIC };
if ((await runAppCommand("typecheck", directory)) !== 0) return { exitCode: EXIT_GENERIC };
diff --git a/cli/src/commands/app/create.test.ts b/cli/src/commands/app/create.test.ts
index 6e105e99..98df0125 100644
--- a/cli/src/commands/app/create.test.ts
+++ b/cli/src/commands/app/create.test.ts
@@ -1,14 +1,10 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
-import { join } from "node:path";
+import { join, posix } from "node:path";
+import { readDataAppPayload } from "@/commands/app/lib/distribution.ts";
import { runCommandWithTestRuntime } from "@/test-utils/cli.ts";
-import { upgradeApp } from "@/commands/app/upgrade.ts";
-import {
- currentRuntimeIntegrity,
- installedRuntimeIntegrity,
- readRuntimeSource,
-} from "@/commands/app/lib/runtime.ts";
+import { recommendedDataAppVersion } from "@/commands/app/lib/package.ts";
import { configSet, ensureProfileExists, setActiveProfile } from "@/lib/profile-store.ts";
let home: string;
@@ -32,7 +28,7 @@ afterEach(() => {
});
describe("app create", () => {
- test("creates a self-contained offline project when explicitly requested", async () => {
+ test("scaffolds a pinned package project without registry access", async () => {
const directory = join(home, "product-pulse");
const result = await runCommandWithTestRuntime([
"app",
@@ -72,13 +68,20 @@ describe("app create", () => {
scope: { organization: "Your organization", environment: "your environment" },
});
expect(readFileSync(join(directory, "bun.lock"), "utf8")).toContain('"name": "product-pulse"');
- expect(await installedRuntimeIntegrity(directory)).toEqual(currentRuntimeIntegrity());
+ expect(
+ JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).dependencies[
+ "@altertable/data-app"
+ ],
+ ).toBe(recommendedDataAppVersion());
+ expect(existsSync(join(directory, ".altertable/runtime"))).toBe(false);
const paths = JSON.parse(result.stdout[0]!).files as string[];
- expect(paths).toContain(".altertable/runtime/src/react/ui/PlayStory.tsx");
+ expect(paths.some((path) => path.startsWith(".altertable/"))).toBe(false);
expect(paths).toContain("src/App.tsx");
expect(paths).toContain(".oxlintrc.json");
expect(paths).toContain("docs/data.md");
- expect(paths).toContain(".altertable/runtime/README.md");
+ expect(readFileSync(join(directory, "AGENTS.md"), "utf8")).toContain(
+ "node_modules/@altertable/data-app/AGENTS.md",
+ );
expect(readFileSync(join(directory, "src/App.tsx"), "utf8")).toContain(
"Connectivity-only screen",
);
@@ -88,19 +91,6 @@ describe("app create", () => {
expect(readFileSync(join(directory, "AGENTS.md"), "utf8")).toContain(
"connectivity screen is scaffolding, not an example analysis",
);
- expect(Bun.spawnSync(["git", "init", "--quiet"], { cwd: directory }).exitCode).toBe(0);
- const ignored = Bun.spawnSync(
- [
- "git",
- "-c",
- "core.excludesFile=/dev/null",
- "check-ignore",
- "--no-index",
- ".altertable/runtime/package.json",
- ],
- { cwd: directory },
- );
- expect(ignored.exitCode).toBe(1);
expect(paths.some((path) => /tests|fixtures|node_modules|\.txt$/.test(path))).toBe(false);
});
@@ -264,7 +254,7 @@ describe("app create", () => {
});
});
- test("installs the generated app with its frozen lockfile and checks the project", async () => {
+ test("installs the generated app with its frozen lockfile and checks authoring links and the project", async () => {
const directory = join(home, "first-check");
await runCommandWithTestRuntime([
"app",
@@ -280,6 +270,22 @@ describe("app create", () => {
stderr: "pipe",
});
expect(install.exitCode).toBe(0);
+ expect(
+ JSON.parse(
+ readFileSync(join(directory, "node_modules/@altertable/data-app/package.json"), "utf8"),
+ ).version,
+ ).toBe(recommendedDataAppVersion());
+ const { starter } = await readDataAppPayload();
+ for (const name of Object.keys(starter)) {
+ if (!name.endsWith(".md")) continue;
+ const content = readFileSync(join(directory, name), "utf8");
+ for (const match of content.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
+ const target = match[1]!.split("#")[0]!;
+ if (!target || /^[a-z]+:\/\//i.test(target)) continue;
+ const path = posix.normalize(posix.join(posix.dirname(name), target));
+ expect(existsSync(join(directory, path)), `${name} links to missing ${path}`).toBe(true);
+ }
+ }
const result = await runCommandWithTestRuntime(["app", "check", "--dir", directory], {
debug: false,
json: false,
@@ -368,131 +374,30 @@ describe("app create", () => {
expect(JSON.parse(result.stdout[0]!)).toMatchObject({ name: "agent-app", directory });
});
- test("upgrade preserves app-owned changes and refuses a modified runtime", async () => {
- const directory = join(home, "upgrade-app");
+ test("upgrade reports the tested package version without changing a current app", async () => {
+ const directory = join(home, "current-app");
await runCommandWithTestRuntime([
"app",
"create",
- "upgrade-app",
+ "current-app",
"--dir",
directory,
"--without-profile",
]);
const operations = join(directory, "src/operations.ts");
writeFileSync(operations, `${readFileSync(operations, "utf8")}\n// App-specific change.\n`);
- const current = await runCommandWithTestRuntime(["app", "upgrade", "--dir", directory], {
- debug: false,
- json: false,
- agent: false,
- });
- expect(current.stdout.join("\n")).toContain("already current");
- expect(readFileSync(operations, "utf8")).toContain("App-specific change");
-
for (const mode of [
{ debug: false, json: true, agent: false },
{ debug: false, json: false, agent: true },
]) {
- const output = await runCommandWithTestRuntime(["app", "upgrade", "--dir", directory], mode);
- expect(JSON.parse(output.stdout[0]!)).toEqual({
+ const result = await runCommandWithTestRuntime(["app", "upgrade", "--dir", directory], mode);
+ expect(JSON.parse(result.stdout[0]!)).toEqual({
directory,
upgraded: false,
- runtimeVersion: currentRuntimeIntegrity().version,
+ runtimeVersion: recommendedDataAppVersion(),
nextSteps: [],
});
}
-
- const runtime = join(directory, ".altertable/runtime/src/server/index.ts");
- writeFileSync(runtime, `${readFileSync(runtime, "utf8")}\n// Local edit.\n`);
- expect(runCommandWithTestRuntime(["app", "upgrade", "--dir", directory])).rejects.toThrow(
- "was modified",
- );
- expect(readFileSync(runtime, "utf8")).toContain("Local edit");
- });
-
- test("source watch upgrade updates integrity and stops on generated edits", async () => {
- const directory = join(home, "watched-app");
- await runCommandWithTestRuntime([
- "app",
- "create",
- "watched-app",
- "--dir",
- directory,
- "--without-profile",
- ]);
- const files = await readRuntimeSource();
- files["src/core/format.ts"] += "\n// Changed source.\n";
- expect(await upgradeApp(directory, { runtimeFiles: files })).toBe(true);
- expect(
- readFileSync(join(directory, ".altertable/runtime/src/core/format.ts"), "utf8"),
- ).toContain("Changed source");
- expect((await installedRuntimeIntegrity(directory)).sha256).toEqual(
- currentRuntimeIntegrity(files).sha256,
- );
-
- const generated = join(directory, ".altertable/runtime/src/core/format.ts");
- writeFileSync(generated, `${readFileSync(generated, "utf8")}\n// App edit.\n`);
- expect(upgradeApp(directory, { runtimeFiles: await readRuntimeSource() })).rejects.toThrow(
- "format.ts was modified",
- );
- });
-
- test("invalid lockfile leaves the installed runtime unchanged", async () => {
- const directory = join(home, "invalid-lock-app");
- await runCommandWithTestRuntime([
- "app",
- "create",
- "invalid-lock-app",
- "--dir",
- directory,
- "--without-profile",
- ]);
- const integrityPath = join(directory, ".altertable/runtime/integrity.json");
- const integrity = JSON.parse(readFileSync(integrityPath, "utf8")) as { version: string };
- integrity.version = "0.1.0";
- writeFileSync(integrityPath, `${JSON.stringify(integrity, null, 2)}\n`);
- const runtimePath = join(directory, ".altertable/runtime/src/server/index.ts");
- const beforeRuntime = readFileSync(runtimePath, "utf8");
- const beforeIntegrity = readFileSync(integrityPath, "utf8");
- const lockPath = join(directory, "bun.lock");
- const validLock = readFileSync(lockPath, "utf8");
- rmSync(lockPath);
- expect(upgradeApp(directory)).rejects.toThrow("valid bun.lock");
- expect(readFileSync(integrityPath, "utf8")).toBe(beforeIntegrity);
- writeFileSync(lockPath, "{ invalid lockfile");
-
- expect(upgradeApp(directory)).rejects.toThrow("valid bun.lock");
- expect(readFileSync(runtimePath, "utf8")).toBe(beforeRuntime);
- expect(readFileSync(integrityPath, "utf8")).toBe(beforeIntegrity);
- writeFileSync(lockPath, validLock);
- expect(await upgradeApp(directory)).toBe(true);
- });
-
- test("upgrade rolls back a failure after applying runtime files", async () => {
- const directory = join(home, "rollback-app");
- await runCommandWithTestRuntime([
- "app",
- "create",
- "rollback-app",
- "--dir",
- directory,
- "--without-profile",
- ]);
- const integrityPath = join(directory, ".altertable/runtime/integrity.json");
- const integrity = JSON.parse(readFileSync(integrityPath, "utf8")) as { version: string };
- integrity.version = "0.1.0";
- writeFileSync(integrityPath, `${JSON.stringify(integrity, null, 2)}\n`);
- const paths = [integrityPath, join(directory, "package.json"), join(directory, "bun.lock")];
- const before = paths.map((path) => readFileSync(path, "utf8"));
-
- expect(
- upgradeApp(directory, {
- afterApply(path) {
- if (path === join(directory, ".altertable/runtime"))
- throw new Error("Injected write failure");
- },
- }),
- ).rejects.toThrow("Injected write failure");
- expect(paths.map((path) => readFileSync(path, "utf8"))).toEqual(before);
- expect(await upgradeApp(directory)).toBe(true);
+ expect(readFileSync(operations, "utf8")).toContain("App-specific change");
});
});
diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts
index 59f11ada..c21d0001 100644
--- a/cli/src/commands/app/dev.ts
+++ b/cli/src/commands/app/dev.ts
@@ -1,10 +1,7 @@
import { checkAppScope } from "@/commands/app/lib/scope.ts";
-import { watch } from "node:fs";
import { defineCommand } from "@/lib/command.ts";
-import { CliError, ConfigurationError, EXIT_GENERIC } from "@/lib/errors.ts";
+import { CliError, EXIT_GENERIC } from "@/lib/errors.ts";
import { startAppDevProxy } from "@/commands/app/lib/dev-proxy.ts";
-import { readRuntimeSource, runtimeSourceDirectory } from "@/commands/app/lib/runtime.ts";
-import { upgradeApp } from "@/commands/app/upgrade.ts";
import {
appDirectory,
assertAppOutputMode,
@@ -19,31 +16,19 @@ export const appDevCommand = defineCommand({
examples: [
"altertable app dev",
"altertable app dev --port 3022",
- "altertable app dev --watch-runtime",
"altertable --profile staging app dev --dir ./my-app",
],
},
args: {
dir: { type: "string", description: "App directory (default: current directory)." },
port: { type: "string", description: "Local dev server port (1–65535; default: app setting)." },
- "watch-runtime": {
- type: "boolean",
- description: "Upgrade generated runtime on source changes and restart preview.",
- },
},
- async run({ args, execution, runtime, sink }) {
+ async run({ args, execution, runtime }) {
assertAppOutputMode(runtime.context.json, runtime.context.agent);
const port = appPort(args.port);
const directory = appDirectory(args.dir);
requireAppScripts(directory, ["dev"]);
await checkAppScope(directory, execution.profile);
- if (args["watch-runtime"]) {
- try {
- await upgradeApp(directory, { runtimeFiles: await readRuntimeSource() });
- } catch (error) {
- throw new ConfigurationError(`Runtime source watch stopped: ${errorMessage(error)}`);
- }
- }
const installExitCode = await runAppCommand("install", directory);
if (installExitCode !== 0) return { exitCode: EXIT_GENERIC };
const proxy = startAppDevProxy(execution);
@@ -52,9 +37,7 @@ export const appDevCommand = defineCommand({
...proxy.environment,
...(port ? { PORT: port } : {}),
};
- const exitCode = args["watch-runtime"]
- ? await runWithRuntimeWatch(directory, environment, sink.writeHuman.bind(sink))
- : await runAppCommand("dev", directory, environment);
+ const exitCode = await runAppCommand("dev", directory, environment);
return { exitCode: exitCode === 0 ? 0 : EXIT_GENERIC };
} finally {
await proxy.stop();
@@ -62,84 +45,6 @@ export const appDevCommand = defineCommand({
},
});
-function errorMessage(error: unknown): string {
- return error instanceof Error ? error.message : String(error);
-}
-
-async function runWithRuntimeWatch(
- directory: string,
- environment: Record,
- announce: (message: string) => void,
-): Promise {
- let changed = false;
- let wake: (() => void) | undefined;
- let timer: ReturnType | undefined;
- let watchedRoots = new Set(
- Object.keys(await readRuntimeSource()).map((name) => name.split("/")[0]),
- );
- let watcher: ReturnType;
- try {
- watcher = watch(runtimeSourceDirectory, { recursive: true }, (_, filename) => {
- if (filename && !watchedRoots.has(filename.replaceAll("\\", "/").split("/")[0])) return;
- if (timer) clearTimeout(timer);
- timer = setTimeout(() => {
- changed = true;
- wake?.();
- }, 200);
- });
- } catch (error) {
- throw new ConfigurationError(
- `Cannot watch runtime source at ${runtimeSourceDirectory}: ${errorMessage(error)}`,
- );
- }
- function waitForChange(): Promise {
- if (changed) {
- changed = false;
- return Promise.resolve();
- }
- return new Promise((resolve) => {
- wake = () => {
- wake = undefined;
- changed = false;
- resolve();
- };
- });
- }
- try {
- announce(`Watching runtime source at ${runtimeSourceDirectory}.`);
- while (true) {
- const controller = new AbortController();
- const preview = runAppCommand("dev", directory, environment, controller.signal);
- const result = await Promise.race([
- preview.then((exitCode) => ({ kind: "exit" as const, exitCode })),
- waitForChange().then(() => ({ kind: "change" as const })),
- ]);
- if (result.kind === "exit") return result.exitCode;
- controller.abort();
- await preview;
- try {
- const files = await readRuntimeSource();
- watchedRoots = new Set(Object.keys(files).map((name) => name.split("/")[0]));
- const upgraded = await upgradeApp(directory, { runtimeFiles: files });
- if (upgraded) {
- const installExitCode = await runAppCommand("install", directory);
- if (installExitCode !== 0) return installExitCode;
- }
- announce(
- upgraded
- ? "Runtime upgraded; restarting preview."
- : "Runtime source unchanged; restarting preview.",
- );
- } catch (error) {
- throw new ConfigurationError(`Runtime source watch stopped: ${errorMessage(error)}`);
- }
- }
- } finally {
- if (timer) clearTimeout(timer);
- watcher.close();
- }
-}
-
function appPort(value: unknown): string | undefined {
if (value === undefined) return undefined;
if (typeof value !== "string" || !/^[1-9]\d{0,4}$/.test(value) || Number(value) > 65535) {
diff --git a/cli/src/commands/app/lib/distribution.test.ts b/cli/src/commands/app/lib/distribution.test.ts
index 8df2dc9f..71997606 100644
--- a/cli/src/commands/app/lib/distribution.test.ts
+++ b/cli/src/commands/app/lib/distribution.test.ts
@@ -1,13 +1,14 @@
import { afterEach, expect, test } from "bun:test";
-import { cp, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
+import { cp, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, posix } from "node:path";
import {
createAppFiles,
dataAppDirectory,
readDataAppPayload,
- readRuntimeSource,
+ starterFiles,
} from "@/commands/app/lib/distribution.ts";
+import { recommendedDataAppVersion } from "@/commands/app/lib/package.ts";
const directories: string[] = [];
afterEach(async () => {
@@ -16,12 +17,12 @@ afterEach(async () => {
);
});
-test("distribution is deterministic, source-based, and excludes development files", async () => {
+test("distribution embeds a deterministic starter with an exact package pin and no runtime", async () => {
const payload = await readDataAppPayload();
expect(await readDataAppPayload()).toEqual(payload);
- expect(Object.keys(payload.runtime)).toContain("src/react/ui/PlayStory.tsx");
+ expect(Object.keys(payload)).toEqual(["starter"]);
expect(Object.keys(payload.starter)).toContain("src/App.tsx");
- for (const path of [...Object.keys(payload.runtime), ...Object.keys(payload.starter)]) {
+ for (const path of Object.keys(payload.starter)) {
expect(path).not.toMatch(/(^|\/)(tests|fixtures|node_modules|dist|\.altertable)(\/|$)|\.txt$/);
}
const identity = {
@@ -34,53 +35,51 @@ test("distribution is deterministic, source-based, and excludes development file
title: identity.title,
scope: identity.scope,
});
+ expect(JSON.parse(files["package.json"]!).dependencies["@altertable/data-app"]).toBe(
+ recommendedDataAppVersion(),
+ );
expect(files["src/App.tsx"]).toBe(payload.starter["src/App.tsx"]);
- expect(files["src/server.ts"]).toBe(payload.starter["src/server.ts"]);
- expect(files[".oxlintrc.json"]).toBe(payload.starter[".oxlintrc.json"]);
+ expect(files["src/server.ts"]).toContain("@altertable/data-app/server/bun");
+ expect(files["src/main.tsx"]).toContain("@altertable/data-app/react/styles.css");
expect(files["bun.lock"]).toContain('"name": "example"');
});
-test("new runtime source files enter the payload without a manual inventory", async () => {
- const directory = await mkdtemp(join(tmpdir(), "runtime-distribution-"));
+async function fixture() {
+ const directory = await mkdtemp(join(tmpdir(), "starter-distribution-"));
directories.push(directory);
- await cp(join(dataAppDirectory, "runtime", "src"), join(directory, "src"), { recursive: true });
- await writeFile(
- join(directory, "package.json"),
- JSON.stringify({ files: ["package.json", "src"] }),
- );
- await writeFile(join(directory, "src/new.ts"), "export const added = true;\n");
- expect((await readRuntimeSource(directory))["src/new.ts"]).toContain("added");
- await writeFile(join(directory, "src/new.test.ts"), "test artifact");
- expect(readRuntimeSource(directory)).rejects.toThrow("Development artifact");
-});
+ for (const name of starterFiles)
+ await cp(join(dataAppDirectory, "starter", name), join(directory, name), { recursive: true });
+ return directory;
+}
-test("distribution rejects paths outside its source and symlinks", async () => {
- const directory = await mkdtemp(join(tmpdir(), "runtime-distribution-"));
- directories.push(directory);
- await writeFile(join(directory, "package.json"), JSON.stringify({ files: ["../secret"] }));
- expect(readRuntimeSource(directory)).rejects.toThrow("Invalid distribution path");
- await mkdir(join(directory, "src"));
- await writeFile(join(directory, "package.json"), JSON.stringify({ files: ["src"] }));
+test("distribution rejects symlinks and development artifacts inside starter source", async () => {
+ const directory = await fixture();
await symlink(join(directory, "package.json"), join(directory, "src/link.ts"));
- expect(readRuntimeSource(directory)).rejects.toThrow("Symlink");
+ // The payload root owns a starter directory; keep its allowlist identical to production.
+ const root = await mkdtemp(join(tmpdir(), "starter-payload-"));
+ directories.push(root);
+ await cp(directory, join(root, "starter"), { recursive: true, dereference: false });
+ expect(readDataAppPayload(root)).rejects.toThrow("Symlink");
+ await rm(join(root, "starter/src/link.ts"));
+ await writeFile(join(root, "starter/src/probe.test.ts"), "test artifact");
+ expect(readDataAppPayload(root)).rejects.toThrow("Development artifact");
});
-test("generated authoring routes and runtime API links resolve inside the shipped project", async () => {
- const payload = await readDataAppPayload();
- const files = createAppFiles(payload, {
- name: "documentation-test",
- title: "Documentation test",
+test("app-owned authoring links resolve within the generated files", async () => {
+ const files = createAppFiles(await readDataAppPayload(), {
+ name: "docs-test",
+ title: "Docs",
scope: { organization: "Test", environment: "test" },
});
- expect(files["docs/data.md"]).toBeDefined();
- expect(files["docs/views.md"]).toBeDefined();
- expect(files[".altertable/runtime/README.md"]).toBeDefined();
+ expect(files["AGENTS.md"]).toContain("node_modules/@altertable/data-app/AGENTS.md");
for (const [name, content] of Object.entries(files)) {
if (!name.endsWith(".md")) continue;
for (const match of content.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
const target = match[1]!.split("#")[0]!;
if (!target || /^[a-z]+:\/\//i.test(target)) continue;
const path = posix.normalize(posix.join(posix.dirname(name), target));
+ // Package documentation is validated after installation in the app creation test.
+ if (path.startsWith("node_modules/@altertable/data-app/")) continue;
expect(files[path], `${name} links to missing ${path}`).toBeDefined();
}
}
diff --git a/cli/src/commands/app/lib/distribution.ts b/cli/src/commands/app/lib/distribution.ts
index d44fecb3..d7801016 100644
--- a/cli/src/commands/app/lib/distribution.ts
+++ b/cli/src/commands/app/lib/distribution.ts
@@ -1,4 +1,3 @@
-import { createHash } from "node:crypto";
import { lstat, readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -18,10 +17,8 @@ export const starterFiles = [
".oxlintrc.json",
".oxfmtrc.json",
] as const;
-export const runtimePath = ".altertable/runtime";
export const dataAppDirectory = fileURLToPath(new URL("../../../../../data-app/", import.meta.url));
-export const runtimeSourceDirectory = join(dataAppDirectory, "runtime");
-export type DataAppPayload = { starter: Record; runtime: Record };
+export type DataAppPayload = { starter: Record };
function assertRelativePath(path: string): void {
if (
@@ -56,39 +53,9 @@ async function readFiles(
return files;
}
-export async function readRuntimeSource(
- directory = runtimeSourceDirectory,
-): Promise> {
- const manifest = JSON.parse(await readFile(join(directory, "package.json"), "utf8")) as {
- files?: unknown;
- };
- if (!Array.isArray(manifest.files) || !manifest.files.every((file) => typeof file === "string")) {
- throw new Error("Runtime package.json needs a files allowlist.");
- }
- return readFiles(directory, manifest.files);
-}
-
+/** Release builds embed only the starter; its runtime is installed from npm. */
export async function readDataAppPayload(directory = dataAppDirectory): Promise {
- const [starter, runtime] = await Promise.all([
- readFiles(join(directory, "starter"), starterFiles),
- readRuntimeSource(join(directory, "runtime")),
- ]);
- return { starter, runtime };
-}
-
-export function runtimeIntegrity(files: Record): {
- version: string;
- sha256: Record;
-} {
- return {
- version: (JSON.parse(files["package.json"]!) as { version: string }).version,
- sha256: Object.fromEntries(
- Object.entries(files).map(([name, content]) => [
- name,
- createHash("sha256").update(content).digest("hex"),
- ]),
- ),
- };
+ return { starter: await readFiles(join(directory, "starter"), starterFiles) };
}
export function createAppFiles(
@@ -102,7 +69,6 @@ export function createAppFiles(
const files = { ...payload.starter };
const packageJson = JSON.parse(files["package.json"]!);
packageJson.name = identity.name;
- packageJson.dependencies["@altertable/data-app"] = `file:${runtimePath}`;
files["package.json"] = `${JSON.stringify(packageJson, null, 2)}\n`;
const app = JSON.parse(files["app.json"]!);
app.title = identity.title;
@@ -114,9 +80,5 @@ export function createAppFiles(
`"name": ${oldName}`,
`"name": ${JSON.stringify(identity.name)}`,
);
- for (const [name, content] of Object.entries(payload.runtime))
- files[`${runtimePath}/${name}`] = content;
- files[`${runtimePath}/integrity.json`] =
- `${JSON.stringify(runtimeIntegrity(payload.runtime), null, 2)}\n`;
return files;
}
diff --git a/cli/src/commands/app/lib/package.test.ts b/cli/src/commands/app/lib/package.test.ts
new file mode 100644
index 00000000..da177772
--- /dev/null
+++ b/cli/src/commands/app/lib/package.test.ts
@@ -0,0 +1,53 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { dataAppPackage, requirePublishedDataApp } from "@/commands/app/lib/package.ts";
+
+let directory: string;
+beforeEach(async () => {
+ directory = await mkdtemp(join(tmpdir(), "app-package-check-"));
+});
+afterEach(async () => {
+ await rm(directory, { recursive: true, force: true });
+});
+
+async function fixture(reference: string, lockedReference = reference, version = "0.59.0") {
+ await writeFile(
+ join(directory, "package.json"),
+ JSON.stringify({ dependencies: { [dataAppPackage]: reference } }),
+ );
+ await writeFile(
+ join(directory, "bun.lock"),
+ JSON.stringify({
+ lockfileVersion: 2,
+ workspaces: { "": { dependencies: { [dataAppPackage]: lockedReference } } },
+ packages: { [dataAppPackage]: [`${dataAppPackage}@${version}`, "", {}, "integrity"] },
+ }),
+ );
+}
+
+test.each([
+ ["file:.altertable/runtime", "published"],
+ ["file:../local-runtime", "published"],
+ ["github:altertable-ai/data-app", "published"],
+])("check rejects %s before installing or modifying the lockfile", async (reference, message) => {
+ await fixture(reference!);
+ const before = await readFile(join(directory, "bun.lock"), "utf8");
+ const failure = await requirePublishedDataApp(directory).catch((error: unknown) => error);
+ expect(failure).toMatchObject({ message: expect.stringContaining(message!) });
+ expect(await readFile(join(directory, "bun.lock"), "utf8")).toBe(before);
+});
+
+test("check rejects a stale lockfile without resolving a replacement", async () => {
+ await fixture("0.59.1", "0.59.0");
+ const before = await readFile(join(directory, "bun.lock"), "utf8");
+ const failure = await requirePublishedDataApp(directory).catch((error: unknown) => error);
+ expect(failure).toMatchObject({ message: expect.stringContaining("matching package.json") });
+ expect(await readFile(join(directory, "bun.lock"), "utf8")).toBe(before);
+});
+
+test("check accepts an older registry package independent of the CLI starter pin", async () => {
+ await fixture("^0.59.0");
+ await requirePublishedDataApp(directory);
+});
diff --git a/cli/src/commands/app/lib/package.ts b/cli/src/commands/app/lib/package.ts
new file mode 100644
index 00000000..41e20380
--- /dev/null
+++ b/cli/src/commands/app/lib/package.ts
@@ -0,0 +1,169 @@
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { ConfigurationError } from "@/lib/errors.ts";
+import { isRecord } from "@/lib/object.ts";
+import { dataAppPayload } from "@/commands/app/lib/payload.ts";
+
+export const dataAppPackage = "@altertable/data-app";
+
+type AppPackage = {
+ dependencies?: Record;
+ devDependencies?: Record;
+};
+
+export function recommendedDataAppVersion(): string {
+ const version = JSON.parse(dataAppPayload.starter["package.json"]!).dependencies[dataAppPackage];
+ if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version)) {
+ throw new ConfigurationError("The CLI starter must pin a published data app version.");
+ }
+ return version;
+}
+
+export async function readAppPackage(
+ directory: string,
+): Promise<{ source: string; manifest: AppPackage }> {
+ try {
+ const source = await readFile(join(directory, "package.json"), "utf8");
+ const manifest: unknown = JSON.parse(source);
+ if (
+ !isRecord(manifest) ||
+ !isRecord(manifest.dependencies) ||
+ Object.values(manifest.dependencies).some((value) => typeof value !== "string")
+ )
+ throw new Error();
+ return { source, manifest: manifest as AppPackage };
+ } catch {
+ throw new ConfigurationError(`Cannot read a valid package.json in ${directory}.`);
+ }
+}
+
+export async function requirePublishedDataApp(directory: string): Promise {
+ const { manifest } = await readAppPackage(directory);
+ const reference = manifest.dependencies?.[dataAppPackage];
+ if (!reference || !/^[\d~^<>=*]/.test(reference)) {
+ throw new ConfigurationError(
+ `package.json needs a published ${dataAppPackage} version or range.`,
+ );
+ }
+ let lock: string;
+ try {
+ lock = await readFile(join(directory, "bun.lock"), "utf8");
+ } catch {
+ throw new ConfigurationError(`Cannot read a valid bun.lock in ${directory}.`);
+ }
+ const { packages } = readAppLock(lock, directory, reference);
+ const version = lockedVersion(packages, dataAppPackage);
+ if (!version || !Bun.semver.satisfies(version, reference)) {
+ throw new ConfigurationError(
+ "bun.lock does not resolve the declared data app package. Run bun install before checking.",
+ );
+ }
+}
+
+export function readAppLock(
+ source: string,
+ directory: string,
+ reference?: string,
+): {
+ packages: Record;
+} {
+ try {
+ const value = parseJsonc(source);
+ const workspaces = isRecord(value) && value.workspaces;
+ const root = isRecord(workspaces) && workspaces[""];
+ const dependencies = isRecord(root) && root.dependencies;
+ if (
+ !isRecord(value) ||
+ !isRecord(value.packages) ||
+ !isRecord(dependencies) ||
+ (reference !== undefined && dependencies[dataAppPackage] !== reference)
+ )
+ throw new Error();
+ return { packages: value.packages };
+ } catch {
+ throw new ConfigurationError(
+ `Cannot read a valid bun.lock matching package.json in ${directory}.`,
+ );
+ }
+}
+
+export function lockedVersion(packages: Record, name: string): string | undefined {
+ const entry = packages[name];
+ if (!Array.isArray(entry) || typeof entry[0] !== "string") return;
+ const version = entry[0].slice(`${name}@`.length);
+ return entry[0].startsWith(`${name}@`) && /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version)
+ ? version
+ : undefined;
+}
+
+export function recommendedDataAppPeers(): Record {
+ const { packages } = readAppLock(
+ dataAppPayload.starter["bun.lock"]!,
+ "CLI starter",
+ recommendedDataAppVersion(),
+ );
+ const entry = packages[dataAppPackage];
+ const metadata =
+ Array.isArray(entry) &&
+ entry.find((value) => isRecord(value) && isRecord(value.peerDependencies));
+ if (
+ !isRecord(metadata) ||
+ !isRecord(metadata.peerDependencies) ||
+ Object.values(metadata.peerDependencies).some((value) => typeof value !== "string")
+ ) {
+ throw new ConfigurationError("The CLI starter lockfile needs data app peer dependencies.");
+ }
+ return metadata.peerDependencies as Record;
+}
+
+function parseJsonc(source: string): unknown {
+ let withoutComments = "";
+ let inString = false;
+ let escaped = false;
+ for (let index = 0; index < source.length; index++) {
+ const character = source[index]!;
+ const next = source[index + 1];
+ if (inString) {
+ withoutComments += character;
+ if (escaped) escaped = false;
+ else if (character === "\\") escaped = true;
+ else if (character === '"') inString = false;
+ } else if (character === '"') {
+ inString = true;
+ withoutComments += character;
+ } else if (character === "/" && next === "/") {
+ while (index < source.length && source[index] !== "\n") index++;
+ withoutComments += "\n";
+ } else if (character === "/" && next === "*") {
+ index += 2;
+ while (index < source.length && !(source[index] === "*" && source[index + 1] === "/"))
+ index++;
+ if (index >= source.length) throw new Error("Unterminated JSONC comment");
+ index++;
+ withoutComments += " ";
+ } else {
+ withoutComments += character;
+ }
+ }
+ if (inString) throw new Error("Unterminated JSONC string");
+ let withoutTrailingCommas = "";
+ inString = false;
+ escaped = false;
+ for (let index = 0; index < withoutComments.length; index++) {
+ const character = withoutComments[index]!;
+ if (inString) {
+ withoutTrailingCommas += character;
+ if (escaped) escaped = false;
+ else if (character === "\\") escaped = true;
+ else if (character === '"') inString = false;
+ } else if (character === '"') {
+ inString = true;
+ withoutTrailingCommas += character;
+ } else if (character === "," && /^[\s]*[}\]]/.test(withoutComments.slice(index + 1))) {
+ continue;
+ } else {
+ withoutTrailingCommas += character;
+ }
+ }
+ return JSON.parse(withoutTrailingCommas) as unknown;
+}
diff --git a/cli/src/commands/app/lib/payload.ts b/cli/src/commands/app/lib/payload.ts
index 4cfdde15..396b8a1b 100644
--- a/cli/src/commands/app/lib/payload.ts
+++ b/cli/src/commands/app/lib/payload.ts
@@ -1,4 +1,4 @@
import { readDataAppPayload } from "@/commands/app/lib/distribution.ts";
-// Source execution reads canonical projects. Release builds replace this module with literal data.
+// Source execution reads the starter template. Release builds replace this module with literal data.
export const dataAppPayload = await readDataAppPayload();
diff --git a/cli/src/commands/app/lib/project-check.js.txt b/cli/src/commands/app/lib/project-check.js.txt
index a1038a53..7e681d1b 100644
--- a/cli/src/commands/app/lib/project-check.js.txt
+++ b/cli/src/commands/app/lib/project-check.js.txt
@@ -7,7 +7,7 @@ const appFile = (path) => pathToFileURL(join(process.cwd(), path)).href;
async function check() {
const manifest = JSON.parse(await readFile("app.json", "utf8"));
try {
- const { parseAppearance } = await import(appFile(".altertable/runtime/src/core/appearance.ts"));
+ const { parseAppearance } = await import("@altertable/data-app/appearance");
parseAppearance(manifest.appearance);
} catch {
throw new Error("app.json has invalid appearance settings.");
@@ -63,12 +63,15 @@ async function check() {
plugins: [{
name: "data-app-browser-boundary",
setup(build) {
- build.onLoad({ filter: /(?:operations|server|index|handler|local)\.[cm]?[jt]sx?$/ }, ({ path }) => {
+ build.onResolve({ filter: /^@altertable\/data-app\/server(?:\/|$)/ }, () => {
+ throw new Error("Browser code cannot import server transport. Use import type for server types.");
+ });
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, ({ path }) => {
const normalized = path.replaceAll("\\", "/");
const root = process.cwd().replaceAll("\\", "/");
if (normalized === `${root}/src/operations.ts` ||
normalized === `${root}/src/server.ts` ||
- /(?:\.altertable\/runtime|node_modules\/@altertable\/data-app)\/src\/server\/(?:index|handler|local)\.ts$/.test(normalized)) {
+ /node_modules\/@altertable\/data-app\/(?:dist\/(?:server|local)\.js|src\/server\/[^/]+\.[jt]s)$/.test(normalized)) {
throw new Error("Browser code cannot import server operations or transport. Use import type for operation types.");
}
});
@@ -79,8 +82,8 @@ async function check() {
}
if (process.argv[1] !== "--lakehouse") return;
- const { createDataHandler } = await import(appFile(".altertable/runtime/src/server/index.ts"));
- const { localLakehouse } = await import(appFile(".altertable/runtime/src/server/local.ts"));
+ const { createDataHandler } = await import("@altertable/data-app/server");
+ const { localLakehouse } = await import("@altertable/data-app/server/bun");
const handle = createDataHandler(operations, async () => ({
lakehouse: localLakehouse(process.env),
canDiscloseSql: true,
diff --git a/cli/src/commands/app/lib/run.ts b/cli/src/commands/app/lib/run.ts
index 11039d8b..95993505 100644
--- a/cli/src/commands/app/lib/run.ts
+++ b/cli/src/commands/app/lib/run.ts
@@ -4,7 +4,7 @@ import { CliError, ConfigurationError } from "@/lib/errors.ts";
import { copyProcessEnv } from "@/lib/env.ts";
export type AppScript = "typecheck" | "lint" | "format:check" | "dev" | "build";
-export type AppCommand = "install" | AppScript;
+export type AppCommand = "install" | "install-frozen" | AppScript;
export function appDirectory(value: unknown): string {
if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
@@ -57,7 +57,12 @@ export async function runAppCommand(
// command and supports `bun` calls inside the project's scripts.
env.BUN_BE_BUN = "1";
- const arguments_ = command === "install" ? ["install"] : ["run", command];
+ const arguments_ =
+ command === "install"
+ ? ["install"]
+ : command === "install-frozen"
+ ? ["install", "--frozen-lockfile"]
+ : ["run", command];
const child = Bun.spawn([process.execPath, ...arguments_], {
cwd: directory,
env,
diff --git a/cli/src/commands/app/lib/runtime.ts b/cli/src/commands/app/lib/runtime.ts
deleted file mode 100644
index acc535b4..00000000
--- a/cli/src/commands/app/lib/runtime.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { createHash } from "node:crypto";
-import { readFile } from "node:fs/promises";
-import { join } from "node:path";
-import { ConfigurationError } from "@/lib/errors.ts";
-import { dataAppPayload } from "@/commands/app/lib/payload.ts";
-import { runtimeIntegrity, runtimePath } from "@/commands/app/lib/distribution.ts";
-
-export {
- runtimePath,
- runtimeSourceDirectory,
- readRuntimeSource,
-} from "@/commands/app/lib/distribution.ts";
-export const runtimeFiles = dataAppPayload.runtime;
-
-type RuntimeIntegrity = { version: string; sha256: Record };
-export type InstalledRuntime = RuntimeIntegrity;
-function hash(content: string): string {
- return createHash("sha256").update(content).digest("hex");
-}
-
-export function currentRuntimeIntegrity(
- files: Record = runtimeFiles,
-): RuntimeIntegrity {
- return runtimeIntegrity(files);
-}
-
-export async function installedRuntimeIntegrity(directory: string): Promise {
- const path = runtimePath;
- let integrity: RuntimeIntegrity;
- try {
- integrity = JSON.parse(
- await readFile(join(directory, path, "integrity.json"), "utf8"),
- ) as RuntimeIntegrity;
- } catch {
- throw new ConfigurationError(
- "Data app runtime has no integrity record. Restore it from the original generated project.",
- );
- }
- if (
- typeof integrity.version !== "string" ||
- !integrity.sha256 ||
- typeof integrity.sha256 !== "object" ||
- !Object.keys(integrity.sha256).length
- ) {
- throw new ConfigurationError("Data app runtime has an invalid integrity record.");
- }
- for (const [name, checksum] of Object.entries(integrity.sha256)) {
- if (name.split("/").some((part) => !part || part === "." || part === "..")) {
- throw new ConfigurationError("Data app runtime has an invalid integrity record.");
- }
- let content: string;
- try {
- content = await readFile(join(directory, path, name), "utf8");
- } catch {
- throw new ConfigurationError(`Data app runtime is missing ${name}.`);
- }
- if (hash(content) !== checksum) {
- throw new ConfigurationError(
- `Data app runtime ${name} was modified. Keep app code outside ${path}/.`,
- );
- }
- }
- return integrity;
-}
diff --git a/cli/src/commands/app/upgrade.test.ts b/cli/src/commands/app/upgrade.test.ts
index e8cf08ba..b57d32c6 100644
--- a/cli/src/commands/app/upgrade.test.ts
+++ b/cli/src/commands/app/upgrade.test.ts
@@ -2,12 +2,12 @@ import { afterEach, expect, test } from "bun:test";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
+import { createAppFiles, readDataAppPayload } from "@/commands/app/lib/distribution.ts";
import {
- createAppFiles,
- readDataAppPayload,
- runtimeIntegrity,
-} from "@/commands/app/lib/distribution.ts";
-import { installedRuntimeIntegrity } from "@/commands/app/lib/runtime.ts";
+ dataAppPackage,
+ readAppLock,
+ recommendedDataAppVersion,
+} from "@/commands/app/lib/package.ts";
import { upgradeApp } from "@/commands/app/upgrade.ts";
const directories: string[] = [];
@@ -16,163 +16,165 @@ afterEach(async () => {
directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
);
});
+
async function fixture() {
- const directory = await mkdtemp(join(tmpdir(), "runtime-upgrade-"));
+ const directory = await mkdtemp(join(tmpdir(), "package-upgrade-"));
directories.push(directory);
- const payload = await readDataAppPayload();
- for (const [name, content] of Object.entries(
- createAppFiles(payload, {
- name: "upgrade-test",
- title: "Test",
- scope: { organization: "Test", environment: "test" },
- }),
- )) {
+ const files = createAppFiles(await readDataAppPayload(), {
+ name: "upgrade-test",
+ title: "Test",
+ scope: { organization: "Test", environment: "test" },
+ });
+ for (const [name, content] of Object.entries(files)) {
await mkdir(dirname(join(directory, name)), { recursive: true });
await writeFile(join(directory, name), content);
}
- return { directory, files: { ...payload.runtime } };
+ return directory;
}
+
async function snapshot(directory: string) {
return Promise.all(
- [
- "package.json",
- "bun.lock",
- ".altertable/runtime/integrity.json",
- ".altertable/runtime/package.json",
- ].map((name) => readFile(join(directory, name), "utf8")),
+ ["package.json", "bun.lock", "src/App.tsx"].map((name) =>
+ readFile(join(directory, name), "utf8"),
+ ),
);
}
-test("dependency changes regenerate the lock and support a frozen installation", async () => {
- const { directory, files } = await fixture();
- const pkg = JSON.parse(files["package.json"]!);
- pkg.dependencies["@floating-ui/core"] = "1.7.5";
- files["package.json"] = JSON.stringify(pkg);
- const originalApp = await readFile(join(directory, "src/App.tsx"), "utf8");
- const before = await readFile(join(directory, "bun.lock"), "utf8");
- expect(await upgradeApp(directory, { runtimeFiles: files })).toBe(true);
- expect(await readFile(join(directory, "bun.lock"), "utf8")).not.toBe(before);
- const child = Bun.spawn([process.execPath, "install", "--frozen-lockfile", "--ignore-scripts"], {
- cwd: directory,
- stdout: "pipe",
- stderr: "pipe",
- });
- const stderr = await new Response(child.stderr).text();
- expect(await child.exited, stderr).toBe(0);
- expect(await readFile(join(directory, "src/App.tsx"), "utf8")).toBe(originalApp);
- delete pkg.dependencies["@floating-ui/core"];
- files["package.json"] = JSON.stringify(pkg);
- expect(await upgradeApp(directory, { runtimeFiles: files })).toBe(true);
- expect(await installedRuntimeIntegrity(directory)).toEqual(runtimeIntegrity(files));
-}, 30_000);
+async function expectUpgradeFailure(operation: () => Promise, message: string) {
+ const failure = await operation().catch((error: unknown) => error);
+ expect(failure).toMatchObject({ message: expect.stringContaining(message) });
+}
-test("failed dependency resolution rolls back runtime, added peer, and lockfile", async () => {
- const { directory, files } = await fixture();
- const app = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
- delete app.dependencies["react-dom"];
- await writeFile(join(directory, "package.json"), JSON.stringify(app));
- files["src/core/format.ts"] += "\n// changed\n";
+async function updateManifest(
+ directory: string,
+ edit: (pkg: { dependencies: Record; scripts: Record }) => void,
+) {
+ const pkg = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
+ edit(pkg);
+ await writeFile(join(directory, "package.json"), JSON.stringify(pkg));
+ const { packages } = readAppLock(await readFile(join(directory, "bun.lock"), "utf8"), directory);
+ await writeFile(
+ join(directory, "bun.lock"),
+ JSON.stringify({
+ lockfileVersion: 2,
+ configVersion: 1,
+ workspaces: { "": { dependencies: pkg.dependencies } },
+ packages,
+ }),
+ );
+}
+
+async function resolveLock(directory: string) {
+ const payload = await readDataAppPayload();
+ await writeFile(join(directory, "bun.lock"), payload.starter["bun.lock"]!);
+}
+
+test("an exact current pin is a no-op", async () => {
+ const directory = await fixture();
const before = await snapshot(directory);
- const failure = await upgradeApp(directory, {
- runtimeFiles: files,
- async resolveLock(path) {
- expect(
- JSON.parse(await readFile(join(path, "package.json"), "utf8")).dependencies["react-dom"],
- ).toBe("19.3.0");
- await writeFile(join(path, "bun.lock"), "partial lock");
- throw new Error("resolution failed");
- },
- }).catch((error: unknown) => error);
- expect(failure).toMatchObject({ message: "resolution failed" });
+ expect(await upgradeApp(directory)).toBe(false);
expect(await snapshot(directory)).toEqual(before);
});
-test("an incompatible new peer requirement fails before mutation", async () => {
- const { directory, files } = await fixture();
- const pkg = JSON.parse(files["package.json"]!);
- pkg.peerDependencies.react = ">=99";
- files["package.json"] = JSON.stringify(pkg);
+test("failed resolution restores the manifest and lockfile", async () => {
+ const directory = await fixture();
+ await updateManifest(directory, (pkg) => {
+ pkg.dependencies[dataAppPackage] = `^${recommendedDataAppVersion()}`;
+ });
const before = await snapshot(directory);
- const failure = await upgradeApp(directory, { runtimeFiles: files }).catch(
- (error: unknown) => error,
+ await expectUpgradeFailure(
+ () =>
+ upgradeApp(directory, {
+ async resolveLock(path) {
+ await writeFile(join(path, "bun.lock"), "partial lock");
+ throw new Error("resolution failed");
+ },
+ }),
+ "resolution failed",
);
- expect(failure).toMatchObject({ message: expect.stringContaining("Runtime needs react@>=99") });
expect(await snapshot(directory)).toEqual(before);
});
-test("removed runtime files trigger an upgrade even without a version change", async () => {
- const { directory, files } = await fixture();
- delete files["src/assets.d.ts"];
- expect(await upgradeApp(directory, { runtimeFiles: files })).toBe(true);
- expect(await Bun.file(join(directory, ".altertable/runtime/src/assets.d.ts")).exists()).toBe(
- false,
+test("a stale resolved lockfile restores the manifest and lockfile", async () => {
+ const directory = await fixture();
+ await updateManifest(directory, (pkg) => {
+ pkg.dependencies[dataAppPackage] = `^${recommendedDataAppVersion()}`;
+ });
+ const before = await snapshot(directory);
+ await expectUpgradeFailure(
+ () => upgradeApp(directory, { resolveLock: async () => {} }),
+ "matching package.json",
);
+ expect(await snapshot(directory)).toEqual(before);
});
-test("legacy generated apps migrate internal paths while keeping app source and public exports", async () => {
- const { directory, files } = await fixture();
- const legacy = Object.fromEntries(
- Object.entries(files).map(([name, content]) => [name.replace(/^src\//, ""), content]),
- );
- const pkg = JSON.parse(legacy["package.json"]!);
- pkg.version = "0.57.0";
- pkg.exports = Object.fromEntries(
- Object.entries(pkg.exports).map(([name, path]) => [name, String(path).replace("./src/", "./")]),
- );
- legacy["package.json"] = JSON.stringify(pkg);
- await rm(join(directory, ".altertable/runtime"), { recursive: true });
- for (const [name, content] of Object.entries(legacy)) {
- await mkdir(dirname(join(directory, ".altertable/runtime", name)), { recursive: true });
- await writeFile(join(directory, ".altertable/runtime", name), content);
- }
- await writeFile(
- join(directory, ".altertable/runtime/integrity.json"),
- JSON.stringify(runtimeIntegrity(legacy)),
- );
- const appSource = await readFile(join(directory, "src/App.tsx"), "utf8");
- expect(await upgradeApp(directory, { runtimeFiles: files })).toBe(true);
- expect(await Bun.file(join(directory, ".altertable/runtime/server.ts")).exists()).toBe(false);
- expect(await Bun.file(join(directory, ".altertable/runtime/src/server/index.ts")).exists()).toBe(
- true,
- );
- expect(await readFile(join(directory, "src/App.tsx"), "utf8")).toBe(appSource);
- expect(await installedRuntimeIntegrity(directory)).toEqual(runtimeIntegrity(files));
+test("incompatible peers fail before mutation", async () => {
+ const directory = await fixture();
+ await updateManifest(directory, (pkg) => {
+ pkg.dependencies.react = "^18.0.0";
+ });
+ const before = await snapshot(directory);
+ await expectUpgradeFailure(() => upgradeApp(directory), "Data app package needs react@");
+ expect(await snapshot(directory)).toEqual(before);
});
-test("missing peers are installed from starter defaults without replacing app customizations", async () => {
- const { directory, files } = await fixture();
- const app = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
- delete app.dependencies["react-dom"];
- app.scripts.custom = "echo user-owned";
- await writeFile(join(directory, "package.json"), JSON.stringify(app));
- expect(await upgradeApp(directory, { runtimeFiles: files })).toBe(true);
+test("missing peers use starter defaults while preserving custom scripts", async () => {
+ const directory = await fixture();
+ await updateManifest(directory, (pkg) => {
+ delete pkg.dependencies["react-dom"];
+ pkg.scripts.custom = "echo user-owned";
+ });
+ await upgradeApp(directory, { resolveLock });
expect(JSON.parse(await readFile(join(directory, "package.json"), "utf8"))).toMatchObject({
dependencies: { "react-dom": "19.3.0" },
scripts: { custom: "echo user-owned" },
});
-}, 30_000);
-
-test.each(["^19.0.0", ">=19"])("compatible locked peer range %s is preserved", async (range) => {
- const { directory, files } = await fixture();
- const app = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
- app.dependencies.react = range;
- const source = JSON.stringify(app);
- await writeFile(join(directory, "package.json"), source);
- files["src/core/format.ts"] += "\n// source change\n";
- expect(await upgradeApp(directory, { runtimeFiles: files })).toBe(true);
- expect(await readFile(join(directory, "package.json"), "utf8")).toBe(source);
});
-test("unchanged peer requirements still reject incompatible app dependencies", async () => {
- const { directory, files } = await fixture();
- const app = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
- app.dependencies.react = "^18.0.0";
- await writeFile(join(directory, "package.json"), JSON.stringify(app));
- files["src/core/format.ts"] += "\n// source change\n";
- const before = await snapshot(directory);
- const failure = await upgradeApp(directory, { runtimeFiles: files }).catch(
- (error: unknown) => error,
+test("registry upgrades pin a compatible range and preserve unrelated dependencies and app files", async () => {
+ const directory = await fixture();
+ const pkg = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
+ pkg.dependencies[dataAppPackage] = `^${recommendedDataAppVersion()}`;
+ pkg.dependencies.yaml = "2.9.0";
+ pkg.scripts.custom = "echo user-owned";
+ await writeFile(join(directory, "package.json"), JSON.stringify(pkg));
+ const { packages } = readAppLock(await readFile(join(directory, "bun.lock"), "utf8"), directory);
+
+ await writeFile(
+ join(directory, "bun.lock"),
+ JSON.stringify({
+ lockfileVersion: 2,
+ configVersion: 1,
+ workspaces: { "": { dependencies: pkg.dependencies } },
+ packages,
+ }),
+ );
+ const original = await readFile(join(directory, "src/App.tsx"), "utf8");
+ await upgradeApp(directory);
+ expect(JSON.parse(await readFile(join(directory, "package.json"), "utf8"))).toMatchObject({
+ dependencies: { yaml: "2.9.0", [dataAppPackage]: recommendedDataAppVersion() },
+ scripts: { custom: "echo user-owned" },
+ });
+ expect(await readFile(join(directory, "src/App.tsx"), "utf8")).toBe(original);
+}, 60_000);
+
+test("a newer installed package is never downgraded", async () => {
+ const directory = await fixture();
+ const pkg = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
+ pkg.dependencies[dataAppPackage] = "9.0.0";
+ await writeFile(join(directory, "package.json"), JSON.stringify(pkg));
+ const { packages } = readAppLock(await readFile(join(directory, "bun.lock"), "utf8"), directory);
+ packages[dataAppPackage] = [`${dataAppPackage}@9.0.0`, "", {}, "integrity"];
+ await writeFile(
+ join(directory, "bun.lock"),
+ JSON.stringify({
+ lockfileVersion: 2,
+ configVersion: 1,
+ workspaces: { "": { dependencies: pkg.dependencies } },
+ packages,
+ }),
);
- expect(failure).toMatchObject({ message: expect.stringContaining("Runtime needs react@>=19") });
+ const before = await snapshot(directory);
+ expect(await upgradeApp(directory)).toBe(false);
expect(await snapshot(directory)).toEqual(before);
});
diff --git a/cli/src/commands/app/upgrade.ts b/cli/src/commands/app/upgrade.ts
index a8205458..9fef1874 100644
--- a/cli/src/commands/app/upgrade.ts
+++ b/cli/src/commands/app/upgrade.ts
@@ -1,29 +1,34 @@
-import { access, cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
-import { dirname, join } from "node:path";
+import { readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
import { defineCommand } from "@/lib/command.ts";
import { ConfigurationError } from "@/lib/errors.ts";
import { dataAppPayload } from "@/commands/app/lib/payload.ts";
-import { copyProcessEnv } from "@/lib/env.ts";
-import { isRecord } from "@/lib/object.ts";
import { appDirectory } from "@/commands/app/lib/run.ts";
+import { copyProcessEnv } from "@/lib/env.ts";
import {
- currentRuntimeIntegrity,
- installedRuntimeIntegrity,
- runtimeFiles,
- runtimePath,
-} from "@/commands/app/lib/runtime.ts";
+ dataAppPackage,
+ lockedVersion,
+ readAppLock,
+ readAppPackage,
+ recommendedDataAppPeers,
+ recommendedDataAppVersion,
+} from "@/commands/app/lib/package.ts";
export const appUpgradeCommand = defineCommand({
metadata: {
name: "upgrade",
- description: "Update an unmodified data app runtime to the CLI's current version.",
+ description: "Update the data app package to the CLI's tested version.",
examples: ["altertable app upgrade", "altertable app upgrade --dir ./my-app"],
},
args: { dir: { type: "string", description: "App directory (default: current directory)." } },
async run({ args, sink }) {
const directory = appDirectory(args.dir);
const upgraded = await upgradeApp(directory);
- const version = currentRuntimeIntegrity().version;
+ const { packages } = readAppLock(
+ await readFile(join(directory, "bun.lock"), "utf8"),
+ directory,
+ );
+ const version = lockedVersion(packages, dataAppPackage)!;
if (sink.json) {
sink.writeJson({
directory,
@@ -39,250 +44,109 @@ export const appUpgradeCommand = defineCommand({
} else {
sink.writeHuman(
upgraded
- ? `Updated data app runtime to ${version}. Run \`altertable app check\`, then restart any running \`altertable app dev\` server.`
- : `Data app runtime ${version} is already current.`,
+ ? `Updated data app package to ${version}. Run \`altertable app check\`, then restart any running \`altertable app dev\` server.`
+ : `Data app package ${version} is already current.`,
);
}
},
});
type UpgradeOptions = {
- afterApply?: (path: string) => void | Promise;
- runtimeFiles?: Record;
resolveLock?: (directory: string) => Promise;
};
-type Applied = { destination: string; backup: string | null };
-
-function parseJsonc(source: string): unknown {
- let withoutComments = "";
- let inString = false;
- let escaped = false;
- for (let index = 0; index < source.length; index++) {
- const character = source[index]!;
- const next = source[index + 1];
- if (inString) {
- withoutComments += character;
- if (escaped) escaped = false;
- else if (character === "\\") escaped = true;
- else if (character === '"') inString = false;
- } else if (character === '"') {
- inString = true;
- withoutComments += character;
- } else if (character === "/" && next === "/") {
- while (index < source.length && source[index] !== "\n") index++;
- withoutComments += "\n";
- } else if (character === "/" && next === "*") {
- index += 2;
- while (index < source.length && !(source[index] === "*" && source[index + 1] === "/"))
- index++;
- if (index >= source.length) throw new Error("Unterminated JSONC comment");
- index++;
- withoutComments += " ";
- } else {
- withoutComments += character;
- }
- }
- if (inString) throw new Error("Unterminated JSONC string");
- let withoutTrailingCommas = "";
- inString = false;
- escaped = false;
- for (let index = 0; index < withoutComments.length; index++) {
- const character = withoutComments[index]!;
- if (inString) {
- withoutTrailingCommas += character;
- if (escaped) escaped = false;
- else if (character === "\\") escaped = true;
- else if (character === '"') inString = false;
- } else if (character === '"') {
- inString = true;
- withoutTrailingCommas += character;
- } else if (character === "," && /^[\s]*[}\]]/.test(withoutComments.slice(index + 1))) {
- continue;
- } else {
- withoutTrailingCommas += character;
- }
- }
- return JSON.parse(withoutTrailingCommas) as unknown;
-}
-
-async function exists(path: string): Promise {
- try {
- await access(path);
- return true;
- } catch {
- return false;
- }
-}
-
-function readLock(lock: string, directory: string): Record {
- let value: unknown;
- try {
- value = parseJsonc(lock);
- } catch {
- throw new ConfigurationError(`Cannot read a valid bun.lock in ${directory}.`);
- }
- const workspaces = isRecord(value) && value.workspaces;
- const root = isRecord(workspaces) && workspaces[""];
- const dependencies = isRecord(root) && root.dependencies;
- if (
- !isRecord(value) ||
- !isRecord(value.packages) ||
- !isRecord(dependencies) ||
- !Object.values(dependencies).some((reference) => reference === `file:${runtimePath}`)
- ) {
- throw new ConfigurationError("bun.lock does not reference the generated runtime.");
- }
- return value.packages;
-}
-/** Upgrade from validated inputs. Staged replacements are swapped into place and rolled back on failure. */
+/** Update only the package manifest and lockfile; restore both if resolution fails. */
export async function upgradeApp(
directory: string,
options: UpgradeOptions = {},
): Promise {
- const files: Record = options.runtimeFiles ?? runtimeFiles;
- const installed = await installedRuntimeIntegrity(directory);
- const current = currentRuntimeIntegrity(files);
- const runtimeUnchanged =
- installed.version === current.version &&
- Object.keys(installed.sha256).length === Object.keys(current.sha256).length &&
- Object.entries(current.sha256).every(([name, checksum]) => installed.sha256[name] === checksum);
-
- const lockPath = join(directory, "bun.lock");
+ const version = recommendedDataAppVersion();
+ const { source: originalPackage, manifest } = await readAppPackage(directory);
+ const reference = manifest.dependencies?.[dataAppPackage];
+ if (!reference || !/^[\d~^<>=*]/.test(reference)) {
+ throw new ConfigurationError(
+ `package.json must reference the published ${dataAppPackage} package.`,
+ );
+ }
let lock: string;
try {
- lock = await readFile(lockPath, "utf8");
+ lock = await readFile(join(directory, "bun.lock"), "utf8");
} catch {
throw new ConfigurationError(`Cannot read a valid bun.lock in ${directory}.`);
}
- const lockedPackages = readLock(lock, directory);
- const packagePath = join(directory, "package.json");
- const packageSource = await readFile(packagePath, "utf8");
- const appPackage = JSON.parse(packageSource) as PackageManifest;
- if (appPackage.dependencies?.["@altertable/data-app"] !== `file:${runtimePath}`) {
- throw new ConfigurationError("package.json does not reference the generated runtime.");
+ const { packages } = readAppLock(lock, directory, reference);
+ const installedVersion = lockedVersion(packages, dataAppPackage);
+ if (!installedVersion || !Bun.semver.satisfies(installedVersion, reference)) {
+ throw new ConfigurationError(
+ "bun.lock does not resolve the declared data app package. Run bun install before upgrading.",
+ );
}
- const previousPackage = JSON.parse(
- await readFile(join(directory, runtimePath, "package.json"), "utf8"),
- ) as PackageManifest;
- const nextPackage = JSON.parse(files["package.json"]!) as PackageManifest;
- const defaults = JSON.parse(dataAppPayload.starter["package.json"]!) as PackageManifest;
- let packageChanged = false;
- for (const [name, range] of Object.entries(nextPackage.peerDependencies ?? {})) {
- const existing = appPackage.dependencies?.[name] ?? appPackage.devDependencies?.[name];
- const locked = lockedPackages[name];
- const locator = Array.isArray(locked) && typeof locked[0] === "string" ? locked[0] : "";
- const resolvedVersion = locator.startsWith(`${name}@`) ? locator.slice(name.length + 1) : "";
- // A lockfile version proves compatibility for user-authored ranges such as ^19.
+ // A newer package installed intentionally must never be downgraded by an older CLI.
+ if (Bun.semver.order(installedVersion, version) > 0) return false;
+ const defaults = JSON.parse(dataAppPayload.starter["package.json"]!).dependencies as Record<
+ string,
+ string
+ >;
+ let packageChanged = reference !== version;
+ for (const [name, range] of Object.entries(recommendedDataAppPeers())) {
+ const existing = manifest.dependencies?.[name] ?? manifest.devDependencies?.[name];
+ const resolved = lockedVersion(packages, name);
if (
existing &&
- (satisfies(existing, range) ||
- (satisfies(resolvedVersion, existing) && satisfies(resolvedVersion, range)))
+ resolved &&
+ Bun.semver.satisfies(resolved, existing) &&
+ Bun.semver.satisfies(resolved, range)
)
continue;
if (existing)
throw new ConfigurationError(
- `Runtime needs ${name}@${range}; app declares ${existing}. Update package.json before upgrading.`,
+ `Data app package needs ${name}@${range}; app declares ${existing}. Update package.json and bun.lock before upgrading.`,
);
- const version = defaults.dependencies?.[name];
- if (!version || !satisfies(version, range))
- throw new ConfigurationError(
- `Runtime needs ${name}@${range}. Add a compatible dependency to package.json before upgrading.`,
- );
- appPackage.dependencies ??= {};
- appPackage.dependencies[name] = version;
+ const fallback = defaults[name];
+ if (!fallback || !Bun.semver.satisfies(fallback, range))
+ throw new ConfigurationError(`Add ${name}@${range} before upgrading.`);
+ manifest.dependencies![name] = fallback;
packageChanged = true;
}
- if (runtimeUnchanged && !packageChanged) return false;
- const resolveDependencies =
- packageChanged ||
- JSON.stringify(previousPackage.dependencies) !== JSON.stringify(nextPackage.dependencies) ||
- JSON.stringify(previousPackage.peerDependencies) !==
- JSON.stringify(nextPackage.peerDependencies);
- const transaction = await mkdtemp(join(directory, ".altertable", ".upgrade-"));
- const applied: Applied[] = [];
- let preserveBackup = false;
+ if (!packageChanged) return false;
+ manifest.dependencies![dataAppPackage] = version;
+ const nextPackage = `${JSON.stringify(manifest, null, 2)}\n`;
try {
- const target = join(directory, runtimePath);
- const stagedRuntime = join(transaction, "stage-runtime");
- if (await exists(target)) await cp(target, stagedRuntime, { recursive: true });
- else await mkdir(stagedRuntime);
- for (const [name, content] of Object.entries(files)) {
- const path = join(stagedRuntime, name);
- await mkdir(dirname(path), { recursive: true });
- await writeFile(path, content);
- }
- for (const name of Object.keys(installed.sha256)) {
- if (!(name in files)) await rm(join(stagedRuntime, name));
- }
- await writeFile(join(stagedRuntime, "integrity.json"), `${JSON.stringify(current, null, 2)}\n`);
- async function apply(staged: string, destination: string, name: string): Promise {
- const backup = join(transaction, "backup", name);
- const hadOriginal = await exists(destination);
- if (hadOriginal) {
- await mkdir(dirname(backup), { recursive: true });
- await rename(destination, backup);
- }
- applied.push({ destination, backup: hadOriginal ? backup : null });
- await rename(staged, destination);
- await options.afterApply?.(destination);
- }
- await apply(stagedRuntime, target, "runtime");
- if (resolveDependencies) {
- const stagedPackage = join(transaction, "package.json");
- await writeFile(
- stagedPackage,
- packageChanged ? `${JSON.stringify(appPackage, null, 2)}\n` : packageSource,
- );
- await apply(stagedPackage, packagePath, "package.json");
- const stagedLock = join(transaction, "bun.lock");
- await writeFile(stagedLock, lock);
- await apply(stagedLock, lockPath, "bun.lock");
- await (options.resolveLock ?? resolveLockfile)(directory);
- readLock(await readFile(lockPath, "utf8"), directory);
- // Bun update may rewrite the root manifest; preserve all app-owned fields and formatting.
- await writeFile(
- packagePath,
- packageChanged ? `${JSON.stringify(appPackage, null, 2)}\n` : packageSource,
+ await writeFile(join(directory, "package.json"), nextPackage);
+ await (options.resolveLock ?? resolveLockfile)(directory);
+ const resolved = readAppLock(
+ await readFile(join(directory, "bun.lock"), "utf8"),
+ directory,
+ version,
+ );
+ if (lockedVersion(resolved.packages, dataAppPackage) !== version)
+ throw new ConfigurationError(
+ "Dependency resolution did not lock the tested data app version.",
);
+ for (const [name, range] of Object.entries(recommendedDataAppPeers())) {
+ const peerVersion = lockedVersion(resolved.packages, name);
+ if (!peerVersion || !Bun.semver.satisfies(peerVersion, range))
+ throw new ConfigurationError(
+ `Resolved ${name} is incompatible with data app's ${range} peer requirement.`,
+ );
}
+ // Bun may rewrite the root manifest; preserve all unrelated app-owned fields.
+ await writeFile(join(directory, "package.json"), nextPackage);
} catch (error) {
- const rollbackFailures: string[] = [];
- for (const replacement of applied.reverse()) {
- try {
- await rm(replacement.destination, { recursive: true, force: true });
- if (replacement.backup) await rename(replacement.backup, replacement.destination);
- } catch {
- rollbackFailures.push(replacement.destination);
- }
- }
- if (rollbackFailures.length) {
- preserveBackup = true;
+ const restored = await Promise.allSettled([
+ writeFile(join(directory, "package.json"), originalPackage),
+ writeFile(join(directory, "bun.lock"), lock),
+ ]);
+ if (restored.some((result) => result.status === "rejected"))
throw new ConfigurationError(
- `Upgrade failed and rollback was incomplete. Backups remain at ${transaction}.`,
+ "Upgrade failed and could not restore package.json and bun.lock.",
+ { cause: error },
);
- }
throw error;
- } finally {
- if (!preserveBackup) await rm(transaction, { recursive: true, force: true });
}
return true;
}
-type PackageManifest = {
- dependencies?: Record;
- devDependencies?: Record;
- peerDependencies?: Record;
-};
-
-function satisfies(version: string, range: string): boolean {
- try {
- return Bun.semver.satisfies(version, range);
- } catch {
- return false;
- }
-}
-
async function resolveLockfile(directory: string): Promise {
const env = copyProcessEnv();
for (const key of Object.keys(env)) if (key.startsWith("ALTERTABLE_")) delete env[key];
@@ -302,6 +166,6 @@ async function resolveLockfile(directory: string): Promise {
]);
if (code !== 0)
throw new ConfigurationError(
- `Could not resolve runtime dependencies; upgrade rolled back.\n${stderr || stdout}`,
+ `Could not resolve data app dependencies; upgrade rolled back.\n${stderr || stdout}`,
);
}
diff --git a/data-app/AGENTS.md b/data-app/AGENTS.md
index 7fe7fef4..3cfe60bf 100644
--- a/data-app/AGENTS.md
+++ b/data-app/AGENTS.md
@@ -2,15 +2,13 @@
| Change | Source and guidance | Focused check |
| --- | --- | --- |
-| Runtime contract, transport, or formatting | `runtime/src/`; [API map](runtime/README.md#entry-points) | Runtime typecheck and `runtime/tests/` |
-| UI component or behavior | [UI map](runtime/README.md#find-ui-by-task); styles beside components | Runtime checks; relevant browser scenarios in `tests/` |
-| Generated app defaults or author guidance | `starter/`; [app authoring router](starter/AGENTS.md) | Starter checks and CLI app creation tests |
-| Copied files or runtime upgrades | `../cli/src/commands/app/lib/distribution.ts`, `../cli/src/commands/app/upgrade.ts` | Distribution and upgrade tests; packaged-app smoke |
-
-See [development commands](README.md#develop). Run `bun run --cwd cli data-app:check` from the repository root for source checks. Run browser scenarios from `data-app/tests/` with `bun run test`.
-
-Keep `runtime/src/core/`, `client/`, and `server/` free of React and UI imports; the boundary test enforces this. `runtime/src/react/index.ts` exports the public React API; implementations live in focused modules beside `react/ui/`. Keep component styles beside their source. Source-specific SQL and metric definitions belong in apps.
-
-JSDoc should explain constraints, ownership, units, security boundaries, or surprising behavior. Let names and types describe obvious props and functions. Put task routing in the API map and app guides.
-
-Canonical `runtime/` is tracked. The repository starter's `.altertable/runtime/` is ignored and recreated by setup; generated user apps commit their vendored runtime. Runtime `package.json` lists distributed files, while the CLI distribution manifest lists starter files. Keep routing links valid in the generated artifact.
+| Package contracts, transport, or UI | [Data App repository](https://github.com/altertable-ai/data-app) | Package repository checks |
+| Generated app defaults or author guidance | `starter/`; [app authoring router](starter/AGENTS.md) | Starter checks and CLI creation tests |
+| Scaffold distribution or package upgrades | `../cli/src/commands/app/lib/distribution.ts`, `../cli/src/commands/app/upgrade.ts` | Distribution, upgrade, and packaged-app tests |
+| Consumer integration or browser behavior | `tests/`, `starter/fixtures/` | Browser tests against the published package |
+
+See [development commands](README.md#develop). Keep SQL and metric definitions in app-owned source.
+Import only public package exports; local Bun serving uses `/server/bun`, and each browser entry
+imports `/react/styles.css`. Keep starter and browser-test package pins aligned, commit their
+lockfiles, and preserve documentation links to installed package docs. Do not copy runtime source
+into this repository or generated apps.
diff --git a/data-app/README.md b/data-app/README.md
index 87cc3bca..e2687f4d 100644
--- a/data-app/README.md
+++ b/data-app/README.md
@@ -1,10 +1,9 @@
# Data app development
-`runtime/` is the private `@altertable/data-app` package. `starter/` is the actual getting-started application. `tests/` runs browser scenarios against the starter and its separate component fixtures.
-
-The [runtime API map](runtime/README.md) routes readers to public entries and UI source. The [contributor router](AGENTS.md) identifies source and checks for each change.
-
-The package separates React-free `core/`, `client/`, and `server/` source from `react/`, which owns hooks, the `DataApp` shell, and UI. Generated apps keep their question, SQL, validation, exploration context, and view in `src/`.
+`starter/` is the application copied by `altertable app create`. It pins the published
+[`@altertable/data-app`](https://github.com/altertable-ai/data-app) package and ships a frozen
+lockfile. Package source, API documentation, unit tests, and releases belong to that repository.
+`tests/` exercises the starter and component fixtures against the installed npm package.
## Develop
@@ -12,49 +11,52 @@ From the repository root, using Fish:
```fish
bun install --cwd cli --frozen-lockfile
-bun run --cwd cli data-app:setup
-bun install --cwd data-app/runtime --frozen-lockfile
bun install --cwd data-app/starter --frozen-lockfile
-./bin/altertable app dev --dir data-app/starter --watch-runtime
+./bin/altertable app dev --dir data-app/starter
```
-Use a configured profile for live data. `--watch-runtime` watches canonical runtime source, validates the installed copy, upgrades it, and restarts the app. Edit runtime source in `runtime/src/`; the starter's `.altertable/runtime/` is generated and ignored. The starter's connection check executes a bounded query before showing Connected.
+Use a configured profile for live data. The starter executes a bounded connection query before
+showing Connected. App queries, data context, views, and appearance remain app-owned.
## Ownership and distribution
-- `runtime/package.json` declares public exports and a `files` allowlist. Tests and development configuration live outside that list.
-- `runtime/.oxlintrc.json` checks runtime React code; `starter/.oxlintrc.json` ships with each new app so `app check` enforces React Compiler, hooks, and accessibility correctness rules on app-owned source.
-- Generated apps use package `#app/*` and `#config` imports. Oxlint's built-in `no-restricted-imports` rule rejects relative source imports without a custom plugin.
-- `cli/src/commands/app/lib/distribution.ts` declares the starter copy allowlist. `src/`, app configuration, lockfile, and authoring docs ship. Browser fixtures, tests, dependencies, and build outputs do not.
-- `createAppFiles` changes JSON identity fields and the lockfile root name. Application code reads `app.json`; source code has no template tokens.
-- `cli/scripts/package-data-app.ts` supplies the Bun build plugin. It replaces the source payload loader with literal file data. The npm bundle embeds this payload; native releases compile that same bundle. Installed CLIs never read this repository to create an app.
-- Generated apps commit `.altertable/runtime/` because it is a required `file:` dependency; only this repository's starter copy is ignored. Users own their generated `src/`, `app.json`, package manifest, and docs. The CLI owns `.altertable/runtime/` and records its source checksums. Public import paths remain `@altertable/data-app/...`.
+The CLI embeds only the starter allowlist in `cli/src/commands/app/lib/distribution.ts`.
+Both npm and native CLI builds can scaffold an app without reading this checkout or accessing
+the network. Installing the generated app needs registry access or a populated Bun cache.
+Generated projects commit their source, package manifest, and lockfile; they do not vendor runtime
+source. Public imports use `@altertable/data-app/...`.
-The runtime owns its implementation dependencies. The starter owns React, ReactDOM, and its authoring tools. When runtime dependencies change, refresh the starter lockfile after setup:
+The starter owns React, ReactDOM, and its authoring tools. The package owns its implementation
+dependencies. Its stylesheet is imported explicitly by the browser entry. The app's `AGENTS.md`
+points to the installed package guide and docs; dependency guides are not assumed to load automatically.
-```fish
-cd data-app/starter
-bun update @altertable/data-app --lockfile-only --ignore-scripts
-```
-
-Commit both project lockfiles when their respective dependencies change. Runtime code changes alone need no lockfile update. Keep exported API changes compatible with existing apps, or explain the required application migration explicitly.
+To adopt a package release, update its exact pin in both `starter/package.json` and
+`tests/package.json`, regenerate both lockfiles, and run the checks below. Never resolve `latest`
+during app creation. Develop package changes in the package repository; the CLI has no runtime
+source watcher.
## Upgrades
-`app upgrade` validates installed checksums before replacing managed files, including removal of obsolete runtime files. It preserves app source. Dependency changes trigger a targeted Bun lockfile update without installing packages or running lifecycle scripts. Missing peers can be seeded from the starter's pinned dependencies; incompatible new peer requirements stop with an actionable error. A failed update restores runtime, package manifest, and lockfile. Dependency resolution can require the network or a populated Bun cache. Restart running previews after an upgrade.
-
-The package is still private and bundled with the CLI. New generated apps use `@altertable/data-app` imports; the CLI upgrades the managed runtime files without changing app-owned source.
+`app upgrade` updates the app manifest and lockfile to the CLI's tested package version,
+preserving unrelated dependencies and app code. A newer installed package is not downgraded.
+Peer incompatibilities stop with an actionable error; missing peers are seeded from starter defaults.
+Upgrades resolve only the lockfile and restore the original manifest and lockfile if resolution
+fails. Restart running previews after an upgrade. Apps must already use the published package;
+`app upgrade` does not migrate local runtime copies or rewrite app source.
## Verify
```fish
bun run --cwd cli data-app:check
-cd data-app/tests
-bun install --frozen-lockfile
-bunx playwright install chromium
-bun run test
+bun install --cwd data-app/tests --frozen-lockfile
+bun run --cwd cli data-app:test:browser
+./scripts/verify.sh --quick
+./scripts/verify.sh
```
-Source checks cover every runtime module and its contract, transport, search, formatting, and public component composition tests. Starter checks cover types, lint, formatting, and a production build. Browser tests cover connection states, retry, stale success, context, theme persistence, and Present navigation at desktop and phone sizes.
-
-CLI tests cover distribution safety, creation, upgrades, dependency resolution, and rollback. Release smoke checks generate an app outside the checkout, install with a frozen lockfile, and run `app check` using the packaged CLI. The minimum CLI runtime compatibility job checks scaffolding only; building data apps uses the repository's current Bun toolchain.
+Install Playwright Chromium with `bunx playwright install chromium` from `data-app/tests/`
+if needed. Browser checks cover connection and request states, theme, context, and presentation
+at phone and desktop widths. CLI tests cover offline scaffolding, npm consumption, package upgrades,
+peer checks, and failed resolution. Release smoke checks create an app outside the checkout, install its
+frozen lockfile, and run `app check` with the packaged CLI. The minimum Bun compatibility job
+checks scaffolding only; app builds use the repository's current toolchain.
diff --git a/data-app/runtime/.oxfmtrc.json b/data-app/runtime/.oxfmtrc.json
deleted file mode 100644
index e9c09693..00000000
--- a/data-app/runtime/.oxfmtrc.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "$schema": "./node_modules/oxfmt/configuration_schema.json",
- "ignorePatterns": ["dist/**", "bun.lock"]
-}
diff --git a/data-app/runtime/.oxlintrc.json b/data-app/runtime/.oxlintrc.json
deleted file mode 100644
index efd27967..00000000
--- a/data-app/runtime/.oxlintrc.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "$schema": "./node_modules/oxlint/configuration_schema.json",
- "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y"],
- "categories": {
- "correctness": "error"
- },
- "options": {
- "typeAware": true
- },
- "rules": {
- "react/unsupported-syntax": "error"
- }
-}
diff --git a/data-app/runtime/README.md b/data-app/runtime/README.md
deleted file mode 100644
index bfa4c7d9..00000000
--- a/data-app/runtime/README.md
+++ /dev/null
@@ -1,196 +0,0 @@
-# Data app runtime APIs
-
-Start with the task below, then read the linked types. Import through `@altertable/data-app/`; paths under `src/` are implementation details and can move during upgrades.
-
-`src/core/` contains shared contracts and pure data logic. `src/client/` and `src/server/` depend on core without importing React or UI code. `src/react/` owns hooks and `src/react/ui/` owns components and styles. Apps import the package entries below rather than those source folders.
-
-## Entry points
-
-| Task | Package entry | Start here |
-| --- | --- | --- |
-| Define bounded operations, live checks, and date ranges | `/contract` | [defineOperation, defineDateRangeContract, parsers](src/core/contract.ts) |
-| Author app identity and scope | `/config` | [DataAppConfig](src/core/config.ts) |
-| Mount the browser app, load data, and compose its UI | `/react` | [mountDataApp, createDataHooks, DataApp, Grid](src/react/index.ts) |
-| Call operations without React | `/client` | [createDataClient](src/client/index.ts) |
-| Run locally or host a server | `/server` | [serveLocalApp, localLakehouse, createDataHandler](src/server/index.ts) |
-| Set brand tokens and viewer theme | `/appearance` | [parseAppearance, createThemeController](src/core/appearance.ts) |
-| Format dates, numbers, ratios, currency, and plural forms | `/format` | [Formatting functions](src/core/format.ts) |
-
-## Find UI by task
-
-All of these APIs are exported from `/react`. Each component's stylesheet lives beside its implementation.
-
-| Task | Start here | Related APIs |
-| --- | --- | --- |
-| Primary request and page shell | [DataApp](src/react/ui/DataApp.tsx) | AppLayout, AppHeader, AppToolbar, AppFooter, AppScope, ThemeToggle |
-| Initial connection check | [GettingStarted](src/react/ui/GettingStarted.tsx) | Pair with `connectionCheck()` from `/contract` |
-| Arrange content | [Grid](src/react/ui/Grid.tsx), [Stack](src/react/ui/Stack.tsx) | StorySection |
-| Show a key number | [MetricWidget](src/react/ui/MetricWidget.tsx) | ComparisonVisual |
-| Show charts and collections | [VisualizationWidget](src/react/ui/VisualizationWidget.tsx), [TableWidget](src/react/ui/TableWidget.tsx) | DataTable, Ranking, Breakdown, chartColor |
-| Handle a request's loading, error, and stale data | [DataSection](src/react/ui/DataSection.tsx) | DataBoundary, DataViewToast, EmptyState, StatusPanel, Skeleton |
-| Show freshness and refresh | [UpdatedAt](src/react/ui/UpdatedAt.tsx), [AppToolbar](src/react/ui/AppToolbar.tsx) | RefreshRegion, LiveControl |
-| Bind filters to the URL | [variables](src/react/ui/variables.ts), [DateRangePicker](src/react/ui/DateRangePicker.tsx) | Combobox, PeriodSummary, Tabs, useViewTab |
-| Search a loaded collection | [searchItems](src/react/ui/searchItems.ts), [SearchMatch](src/react/ui/SearchMatch.tsx) | SearchField |
-| Explain context, glossary, and queries | [AboutData](src/react/ui/AboutData.tsx), [DataContext](src/react/ui/data-context.ts) | [GlossaryDefinition](src/react/ui/GlossaryDefinition.tsx), GlossaryExplanation, [defineDataIdentifiers](src/react/ui/data-identifiers.tsx) |
-| Present loaded findings | [PlayStory](src/react/ui/PlayStory.tsx) | StoryStep |
-| Build custom controls and overlays | [Button](src/react/ui/Button.tsx), [Sheet](src/react/ui/Sheet.tsx) | IconButton, Tooltip, HelpPopover, Kbd |
-
-## Contracts
-
-| Definition | Runtime owns |
-| --- | --- |
-| `defineOperation` | Input/output validation, check inputs, query limits and cancellation; `query(name, sql)` accepts registered names and records executed evidence. |
-| `defineDataView` | URL variables, operation input, emptiness and the primary date binding. `useView` connects the result and controls to `DataApp`. |
-| `DataApp` | Header, variable bar, refresh state, stale-result notice and dimming, default inspection empty states. |
-| `view.content` | One loading/ready layout. `result.select` never evaluates loading data; `result.metric` binds comparisons to the displayed input. |
-| `context.metric` | Label, numeric format, glossary evidence and optional direction of improvement. |
-| `WidgetViewTabs` | Valid, unique selection IDs and a required empty state per tab. |
-
-SQL and business definitions belong to the app. Hosted adapters authorize every request; local development uses the CLI proxy. Browser/server boundaries and managed runtime integrity are checked by `app check`. SQL disclosure also requires server permission.
-
-A measured zero and unavailable data have different meanings. Metric readings use `null` for an unavailable previous value. The app defines whether a result is empty. `Breakdown` shows parts of a total; `Ranking` scales against its largest value. Percent formats accept ratios.
-
-## Ownership
-
-In a generated app, commit `.altertable/runtime/`, including `integrity.json`, with `package.json` and `bun.lock`. The package is a vendored `file:` dependency required by a fresh clone. Read these files to discover APIs; keep application customizations in the app's `src/`. `altertable app upgrade` replaces unmodified runtime files and verifies their checksums.
-
-In the CLI repository, edit the canonical `data-app/runtime/` package. Its sibling starter's `.altertable/runtime/` copy is ignored and recreated by `data-app:setup`.
-
-## Bind a view
-
-```tsx
-import { createDataClient } from "@altertable/data-app/client";
-import { createDataHooks, dateRangeVariable, DataApp, Grid, MetricWidget, VisualizationWidget, Ranking } from "@altertable/data-app/react";
-import type { operations } from "#app/operations.ts";
-import { calendar } from "#app/contracts.ts";
-import { dataContext, actions } from "#app/data-context.tsx";
-import config from "#config";
-
-const period = dateRangeVariable({
- key: "period", contract: calendar, comparison: true,
- defaultValue: { kind: "preset", id: "last-30" },
-});
-const { defineDataView, useView } = createDataHooks(createDataClient());
-const activityView = defineDataView({
- operation: "activity",
- variables: { period },
- input: ({ period }) => period,
- date: { variable: "period", input: (input) => input },
- isEmpty: (data) => data.features.length === 0,
- empty: { title: "No activity in this range" },
-});
-const featureEvidence = dataContext.evidence({
- id: "feature-use", queryNames: [dataContext.queryNames.activity],
-});
-const content = activityView.content((result) => (
-
- ({
- current: data.count, previous: data.previousCount,
- }))} />
- data.features)}
- isEmpty={(features) => features.length === 0}
- empty={{ title: "No features" }}
- skeleton={{ variant: "ranking", rows: 6 }}
- >
- {(features) => }
-
-
-));
-function App() {
- const activity = useView(activityView);
- return ;
-}
-```
-
-The `date` binding identifies the controlling variable and extracts its range from the operation input. Nested inputs use, for example, `input: (input) => input.period`. The runtime rejects mappings that silently change the selected range or comparison. Non-date views supply `describeInput`; date views can override it when other inputs also need describing.
-
-The shared calendar lives in a browser-safe module:
-
-```ts
-import { defineDateRangeContract } from "@altertable/data-app/contract";
-export const calendar = defineDateRangeContract({
- minDate: "2026-01-01", maxRangeDays: 90, timeZone: "UTC",
-});
-// Server operation: input: calendar.parseRequest
-```
-
-`useView` generates controls for date, text and fixed-option select variables; custom controls use `result.variables.bind(name)`. `input` chooses which variables reach the operation, so local search can stay local. The callback in `view.content` receives the displayed result, including its original input during refreshes and failures. Hooks belong in the enclosing component.
-
-`TableWidget` also accepts `reading={result.select((data) => data.rows)}` and optional `skeletonRows`. Columns and empty states are declared once for both loading and ready layouts. For bounded results already loaded in the app, pass `pagination={{ pageSize: 8 }}` to page the rows after local search; the footer counts only the supplied rows. `limit` remains a separate, mutually exclusive display cap. Large catalogs need query-backed pagination with a stable sort and total count.
-
-Bound `VisualizationWidget` and `TableWidget` calls require `evidence` from `context.evidence(...)`. A bound `MetricWidget` gets evidence from its metric definition. Evidence must name at least one glossary entry or query. Static widgets may omit it.
-
-`MetricWidget` and `ComparisonVisual` both accept the same `metric` and `reading`. The comparison is enabled by the displayed result's range. The definition supplies formatting and evidence; a reading cannot override those or provide a second value. `favorableDirection` is optional; changes are neutral until the author defines whether up or down is favorable.
-
-`defineDataContent` remains available for manually managed requests. Its optional `{ date: (input) => rangeRequest }` binds comparison readings. `DataSection` handles independent requests. Low-level widgets, tabs and layout components remain available for custom interfaces.
-
-## Execute named queries
-
-```ts
-const queries = defineQueryNames({ activity: "feature-activity" });
-const activity = defineOperation({
- queryNames: queries,
- input: calendar.parseRequest,
- output: parseActivity,
- checks: [checkInput],
- policy: { maxQueryRows: 100, maxDurationMs: 15000, exposeSql: true },
- async run({ query }, input) {
- const result = await query(queries.activity, buildActivitySql(input));
- return parseActivityRows(result);
- },
-});
-```
-
-`query` inherits the operation's limit and cancellation signal; `{ limit }` can lower a particular query's bound. Names are checked by TypeScript and at runtime. The server records the SQL and query ID when execution occurs, so evidence does not need a separate result field. Browser modules import operation types with `import type`; they never import server implementations.
-
-## Bind evidence
-
-```tsx
-const queries = defineQueryNames({ activity: "feature-activity" });
-// In the server operation: queryNames: queries
-const identifiers = defineDataIdentifiers({
- tables: { events: { catalog: "product_analytics", schema: "analytics", name: "events" } },
- columns: { identity: { table: "events", name: "identity_uuid" } },
-});
-const { DataIdentifier } = identifiers;
-const context = createDataContext(queries)({
- identifiers: identifiers.definitions,
- description: <>Explore activity in .>,
- glossary: {
- identities: {
- term: "Tracked identities",
- definition: <>Distinct values.>,
- queryNames: [queries.activity],
- },
- },
-});
-const evidence = context.evidence({
- id: "identities",
- glossaryIds: ["identities"],
- queryNames: [queries.activity],
-});
-const actions = context.metric({
- id: "actions",
- glossaryId: "identities",
- label: "Tracked identities",
- format: { kind: "count" },
-});
-const step = context.storyStep({
- id: "activity",
- headline: "What people do",
- visual: ,
- queryNames: [queries.activity],
-});
-```
-
-Import `defineQueryNames` from `/contract` and the context/identifier factories from `/ui`. Use the same registry in `defineOperation({ queryNames: queries, ... })`. Unknown glossary/query references fail type checks and registry validation; the server also validates returned query names. Physical source identity stays in the identifier registry for future linking.
-
-## API migration
-
-- Views that previously inferred their date variable now declare `date: { variable: "period", input: (input) => input }`, or supply `describeInput` for a non-date view.
-- Numeric metrics use `value={count} format={{ kind: "count" }}`. Custom formatted JSX or strings use `content={...}` instead of `value`.
-- Supply `empty` to secondary `DataSection` requests or pass a bound `useView` result. A primary `DataApp` accepts it either from `useView` or as an explicit prop.
-- For alternate views of the same bound result, pass `views={[{ id, label, render }]}` and `viewLabel` to `VisualizationWidget`. Its required `isEmpty` and `empty` apply to the whole result; the widget owns selection. Use `WidgetViewTabs` directly only when views have independent empty states.
-- Variable URL keys cannot use `view`, `about`, `tab`, `present`, or `step`.
diff --git a/data-app/runtime/bun.lock b/data-app/runtime/bun.lock
deleted file mode 100644
index 43f40e85..00000000
--- a/data-app/runtime/bun.lock
+++ /dev/null
@@ -1,237 +0,0 @@
-{
- "lockfileVersion": 2,
- "configVersion": 1,
- "workspaces": {
- "": {
- "name": "@altertable/data-app-runtime",
- "dependencies": {
- "@floating-ui/react": "0.27.20",
- "@internationalized/date": "3.12.4",
- "@tanstack/react-query": "5.104.0",
- "fuzzysort": "4.0.2",
- "lucide-react": "1.48.0",
- "react-aria-components": "1.21.1",
- },
- "devDependencies": {
- "@types/bun": "1.4.2",
- "@types/react": "19.3.0",
- "@types/react-dom": "19.3.0",
- "oxfmt": "0.70.0",
- "oxlint": "1.85.0",
- "oxlint-tsgolint": "7.0.2003",
- "react": "19.3.0",
- "react-dom": "19.3.0",
- "typescript": "7.0.2",
- },
- "peerDependencies": {
- "react": ">=19",
- "react-dom": ">=19",
- },
- },
- },
- "packages": {
- "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
-
- "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
-
- "@floating-ui/react": ["@floating-ui/react@0.27.20", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw=="],
-
- "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
-
- "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
-
- "@internationalized/date": ["@internationalized/date@3.12.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ=="],
-
- "@internationalized/number": ["@internationalized/number@3.6.8", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig=="],
-
- "@internationalized/string": ["@internationalized/string@3.2.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA=="],
-
- "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.70.0", "", { "os": "android", "cpu": "arm" }, "sha512-Xd7YO4/T2axEj6FTLcj4Why3mTBqFMg+x24xtorT4Lb2+1g82090GH0a/4U1m0pABGYiix2bq1pqkYrmV3f0Sw=="],
-
- "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.70.0", "", { "os": "android", "cpu": "arm64" }, "sha512-x9rlMYyKXdgKdYyUJzGsK1ZV8P4di/J32ipzcS6Jet6p9r9UAh28neXIMtdlSaJJycdi61Z4YkcLKLpk8ueFjg=="],
-
- "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.70.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IUTUPvrBVYy7POh4stXzRdz4IVC/1QSaviCWoyenSlOhGu0X9j5K07vCTM9biLjAA2Zs31l0Rj5vvRpj9n95wA=="],
-
- "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.70.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vw745q870oTd6J517O24asoX4/E+eK0nxYIFoedSLqgJ+nI5En7+ZS82iZSHZ69zevQrnOXiyHP01dA+t8xD8w=="],
-
- "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.70.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-NO14EgSM9dFkcg+MfGPxvsKqXYs9LKaxPrOKXpv1R0rLokGGFDcCq6dBMq18dE4wlpFOovX0UZY2uh1P30O7QA=="],
-
- "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.70.0", "", { "os": "linux", "cpu": "arm" }, "sha512-139OEhHarj9CYoJ/i9gXlPv4KLBGtLj2toseOWYFf09QwlhklaZk+wW3aOvlqoeZtuayvkoSNVWra7WJ21s3VQ=="],
-
- "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.70.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GEh2PY3IWTE0M24eNhTduountANSbWyDmMnzFSQE/nGg/bjPugbUgiGuFu+xdqcQSd/HKSwH80/F2yVVD48yhA=="],
-
- "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.70.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-En5i+UJmZSPxuSf47F2Hl5YOzKB0bicQLnGQkeTCMQ35cWLtbrSwACJKfiLqRZrk05DwSnsJkhBRaM3OURtIaA=="],
-
- "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.70.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WWOoV5W9Im3flVwOVrWn/2DUlOF8v5vcCip+kcNuaMpulRCh6nzzt1Su2vcL2F908YJIXNV3HvegbBHuyLwKHg=="],
-
- "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.70.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-YUouneIqW+5n7aE8xx/zeZ6/utr/KH7oykcGoFyd8Uz8uh591T1oKlnoWA3BsRq/ZR42oY1w4MUYvS/0e/MQOA=="],
-
- "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.70.0", "", { "os": "linux", "cpu": "none" }, "sha512-iEnMf21S5aGVa4hViDGY8sAQ/AHyCu2JPyrQF8P06wtHhSkD1YJBeT4m/KiGewgf7+a5XCYSCRIPcRQa1xwEoQ=="],
-
- "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.70.0", "", { "os": "linux", "cpu": "none" }, "sha512-91Sdniaj20fQzyMeCxMDzTP4c9s4RB8dGQ308xHhDR0n6U7+1Xq7N9klE7mfXq8iV3lRmIGSXi5X23Hn/0XX/g=="],
-
- "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.70.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-uUV30M6E+2TKKGMaKiwfeL4RZrviHXlUxsrYJ/jFBb+1EZy+pnFT+hF73eeWdzh5NqPOAnw0iiMAIqjqiLZPFg=="],
-
- "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.70.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ivMcX6kNDPhqtbOaBt/ItFlLlTlXNHLgRuNmxP6Na6UuYXRT10llpJcAPbGeRgjjb3Qzv4jwPp3fB0hui50WNQ=="],
-
- "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.70.0", "", { "os": "linux", "cpu": "x64" }, "sha512-w+S+fERxYmlZSyZlJK/U292FjyBoH8cCEj21/tYJX6atX5kNSn+HDkhlFQKT2zcMwUW0uAtUL/bOrlwJZwqRdA=="],
-
- "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.70.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Zlom1Xkx257R8bk4ZI4zJsrGno2opknz1+5v5baka3nn4FPyvNSdh8JUL4CdN1S1OWRMvJ9UJQ+RfIqGGCUEfA=="],
-
- "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.70.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-FQgPW5R17vzt7cgrJ8eG/dqX00o2xHsqFeLfw4xzA9FRHpN/DjFo9YDonvIIXGxiEuS9F/jZPnGO+KHNKuCo4Q=="],
-
- "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.70.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZfZublNhZ+XBndMiXhkiLlPE+XyGRDa0CweeTL6t1fZypfCh1LTg7e5CvnOeTBunq15MskOcRempumSPGAaCQA=="],
-
- "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.70.0", "", { "os": "win32", "cpu": "x64" }, "sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg=="],
-
- "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2003", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w=="],
-
- "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2003", "", { "os": "darwin", "cpu": "x64" }, "sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg=="],
-
- "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2003", "", { "os": "linux", "cpu": "arm64" }, "sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw=="],
-
- "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2003", "", { "os": "linux", "cpu": "x64" }, "sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw=="],
-
- "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2003", "", { "os": "win32", "cpu": "arm64" }, "sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q=="],
-
- "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2003", "", { "os": "win32", "cpu": "x64" }, "sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA=="],
-
- "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.85.0", "", { "os": "android", "cpu": "arm" }, "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg=="],
-
- "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.85.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA=="],
-
- "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.85.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA=="],
-
- "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.85.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw=="],
-
- "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.85.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg=="],
-
- "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.85.0", "", { "os": "linux", "cpu": "arm" }, "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw=="],
-
- "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.85.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g=="],
-
- "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.85.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg=="],
-
- "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.85.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw=="],
-
- "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.85.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw=="],
-
- "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.85.0", "", { "os": "linux", "cpu": "none" }, "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg=="],
-
- "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.85.0", "", { "os": "linux", "cpu": "none" }, "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA=="],
-
- "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.85.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ=="],
-
- "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.85.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg=="],
-
- "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.85.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA=="],
-
- "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.85.0", "", { "os": "none", "cpu": "arm64" }, "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA=="],
-
- "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.85.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA=="],
-
- "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.85.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A=="],
-
- "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.85.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA=="],
-
- "@react-types/shared": ["@react-types/shared@3.36.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag=="],
-
- "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
-
- "@tanstack/query-core": ["@tanstack/query-core@5.104.0", "", {}, "sha512-JrC2r/JQlXt7khBSdUpsgxNvybzOg+aITa+ARRMlP2AFo93Y8vIqz077rp+e61mJetSZ0T6AJbwW1JHer1vrPQ=="],
-
- "@tanstack/react-query": ["@tanstack/react-query@5.104.0", "", { "dependencies": { "@tanstack/query-core": "5.104.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-e1TZmDCQnWIfiDVryIHeA6Idj+Lfx1hORLOhXS/l5wcgvtVD4yYqbTDl378sGQKIi2cSgb0TEMC9cKK8GGoJEw=="],
-
- "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="],
-
- "@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="],
-
- "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
-
- "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
-
- "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
-
- "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
-
- "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
-
- "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
-
- "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
-
- "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
-
- "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
-
- "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
-
- "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
-
- "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
-
- "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
-
- "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
-
- "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
-
- "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
-
- "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
-
- "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
-
- "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
-
- "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
-
- "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
-
- "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
-
- "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
-
- "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
-
- "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
-
- "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
-
- "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
-
- "fuzzysort": ["fuzzysort@4.0.2", "", {}, "sha512-3c7yD6rK1EXIsqpcu1ZVzXJugODWNMzvUij6yTiziaAHrpNS8kzFOI9mJLE8pmG4K9JOaVgzQRgklfOEk8f4Tg=="],
-
- "lucide-react": ["lucide-react@1.48.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ=="],
-
- "oxfmt": ["oxfmt@0.70.0", "", { "dependencies": { "tinypool": "2.1.2" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.70.0", "@oxfmt/binding-android-arm64": "0.70.0", "@oxfmt/binding-darwin-arm64": "0.70.0", "@oxfmt/binding-darwin-x64": "0.70.0", "@oxfmt/binding-freebsd-x64": "0.70.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.70.0", "@oxfmt/binding-linux-arm-musleabihf": "0.70.0", "@oxfmt/binding-linux-arm64-gnu": "0.70.0", "@oxfmt/binding-linux-arm64-musl": "0.70.0", "@oxfmt/binding-linux-ppc64-gnu": "0.70.0", "@oxfmt/binding-linux-riscv64-gnu": "0.70.0", "@oxfmt/binding-linux-riscv64-musl": "0.70.0", "@oxfmt/binding-linux-s390x-gnu": "0.70.0", "@oxfmt/binding-linux-x64-gnu": "0.70.0", "@oxfmt/binding-linux-x64-musl": "0.70.0", "@oxfmt/binding-openharmony-arm64": "0.70.0", "@oxfmt/binding-win32-arm64-msvc": "0.70.0", "@oxfmt/binding-win32-ia32-msvc": "0.70.0", "@oxfmt/binding-win32-x64-msvc": "0.70.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-IsHxZ4y0wQLLMhnrJblBJgZsLDzfULrJnAw5j/QqsTlMa/m3AqsbToi+W71uhBGaqlqq/PbbjvHc09TJwdv3Tw=="],
-
- "oxlint": ["oxlint@1.85.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.85.0", "@oxlint/binding-android-arm64": "1.85.0", "@oxlint/binding-darwin-arm64": "1.85.0", "@oxlint/binding-darwin-x64": "1.85.0", "@oxlint/binding-freebsd-x64": "1.85.0", "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", "@oxlint/binding-linux-arm-musleabihf": "1.85.0", "@oxlint/binding-linux-arm64-gnu": "1.85.0", "@oxlint/binding-linux-arm64-musl": "1.85.0", "@oxlint/binding-linux-ppc64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-musl": "1.85.0", "@oxlint/binding-linux-s390x-gnu": "1.85.0", "@oxlint/binding-linux-x64-gnu": "1.85.0", "@oxlint/binding-linux-x64-musl": "1.85.0", "@oxlint/binding-openharmony-arm64": "1.85.0", "@oxlint/binding-win32-arm64-msvc": "1.85.0", "@oxlint/binding-win32-ia32-msvc": "1.85.0", "@oxlint/binding-win32-x64-msvc": "1.85.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg=="],
-
- "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2003", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2003", "@oxlint-tsgolint/darwin-x64": "7.0.2003", "@oxlint-tsgolint/linux-arm64": "7.0.2003", "@oxlint-tsgolint/linux-x64": "7.0.2003", "@oxlint-tsgolint/win32-arm64": "7.0.2003", "@oxlint-tsgolint/win32-x64": "7.0.2003" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ=="],
-
- "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
-
- "react-aria": ["react-aria@3.52.1", "", { "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/number": "^3.6.8", "@internationalized/string": "^3.2.10", "@react-types/shared": "^3.36.1", "@swc/helpers": "^0.5.0", "aria-hidden": "^1.2.3", "clsx": "^2.0.0", "react-stately": "3.50.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-fdZZruC9/x/joCg0mhKGs5aHpwrXLCSZ4GOJmhhYyiE0ffyEsk9MLFt9LCOCF9tn8ErTD+UDQP4oDUSlZkmpCg=="],
-
- "react-aria-components": ["react-aria-components@1.21.1", "", { "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/string": "^3.2.10", "@react-types/shared": "^3.36.1", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "3.52.1", "react-stately": "3.50.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-J88WflYY0z+EhLX0ce+GjFlQ3ag3ubIgfUTjpKSrBQBu92Xtl4Ub9o118HItUddtbk1Ido0jzyPy94/klZy1hg=="],
-
- "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
-
- "react-stately": ["react-stately@3.50.0", "", { "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/number": "^3.6.8", "@internationalized/string": "^3.2.10", "@react-types/shared": "^3.36.1", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-TnckvpDQGU0672wEaLZqdzvhuSeXWjgW6vijMWxiKOxc8CosQUfPGISdcZyfHqjX3XMjEtRxj4w9HumvX4h4mw=="],
-
- "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
-
- "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
-
- "tinypool": ["tinypool@2.1.2", "", {}, "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww=="],
-
- "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
- "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
-
- "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
-
- "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="],
- }
-}
diff --git a/data-app/runtime/package.json b/data-app/runtime/package.json
deleted file mode 100644
index fdab5c7d..00000000
--- a/data-app/runtime/package.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "name": "@altertable/data-app",
- "version": "0.59.1",
- "private": true,
- "files": [
- "src",
- "package.json",
- "README.md"
- ],
- "type": "module",
- "exports": {
- "./contract": "./src/core/contract.ts",
- "./config": "./src/core/config.ts",
- "./format": "./src/core/format.ts",
- "./appearance": "./src/core/appearance.ts",
- "./client": "./src/client/index.ts",
- "./server": "./src/server/index.ts",
- "./react": "./src/react/index.ts"
- },
- "scripts": {
- "typecheck": "tsc --noEmit",
- "lint": "oxlint --type-aware src tests",
- "format": "oxfmt src tests",
- "format:check": "oxfmt --check src tests",
- "test": "bun test tests"
- },
- "dependencies": {
- "@floating-ui/react": "0.27.20",
- "@internationalized/date": "3.12.4",
- "@tanstack/react-query": "5.104.0",
- "fuzzysort": "4.0.2",
- "lucide-react": "1.48.0",
- "react-aria-components": "1.21.1"
- },
- "devDependencies": {
- "@types/bun": "1.4.2",
- "@types/react": "19.3.0",
- "@types/react-dom": "19.3.0",
- "oxfmt": "0.70.0",
- "oxlint": "1.85.0",
- "oxlint-tsgolint": "7.0.2003",
- "react": "19.3.0",
- "react-dom": "19.3.0",
- "typescript": "7.0.2"
- },
- "peerDependencies": {
- "react": ">=19",
- "react-dom": ">=19"
- }
-}
diff --git a/data-app/runtime/src/assets.d.ts b/data-app/runtime/src/assets.d.ts
deleted file mode 100644
index cbe652db..00000000
--- a/data-app/runtime/src/assets.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module "*.css";
diff --git a/data-app/runtime/src/client/index.ts b/data-app/runtime/src/client/index.ts
deleted file mode 100644
index 5dd78dcd..00000000
--- a/data-app/runtime/src/client/index.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import type { DataOperations, DisclosedQuery } from "../core/contract.ts";
-
-export type InputOf = T extends { input: (value: unknown) => infer Input } ? Input : never;
-export type OutputOf = T extends { output: (value: unknown) => infer Output } ? Output : never;
-
-/**
- * Parsed operation data and query evidence. `queries` is present only when SQL disclosure is
- * allowed.
- */
-export type DataResponse