- commit
- b13c4245c832c18628b2ff9175dbba3bef992795
- parent
- f6ce64d72ca8a6cbf579c02a7419480b3ec06f11
- author
- Dami <dami.etoile@gmail.com>
@@ -16,6 +16,16 @@ if (Boolean(cloudAppId) !== Boolean(cloudServerUrl)) {
1616 "Cloud mode requires both JAZZ_APP_ID and JAZZ_SERVER_URL; set the missing name or remove the partial pair.",1717 );1818 }19+20+// Jazz's managed Vite plugin reads the VITE_ pair when selecting the server21+// that receives schema, migration, and permission publication. The reference22+// runtime still receives explicit build constants below, so only this public23+// pair enters Vite's client-visible namespace and server secrets remain private.24+if (cloudAppId && cloudServerUrl) {25+ process.env.VITE_JAZZ_APP_ID = cloudAppId;26+ process.env.VITE_JAZZ_SERVER_URL = cloudServerUrl;27+}28+1929 export default defineConfig({2030 output: "static",2131 integrations: [preact({ compat: true, devtools: true })],@@ -3,7 +3,9 @@ import { app as schema } from "./schema.ts";
33 /** Author-owned composition. Vendor URLs and browser drivers remain in generated runtime code. */44 export const referenceApp = {55 name: "lofi checklist",6- databaseName: "lofi-reference",6+ // Keep the M1 OPFS namespace so the notes-to-tasks migration can open and7+ // transform existing device data instead of presenting an empty database.8+ databaseName: "lofi-prototype",79 schema,810 storage: "durable" as const,911 identity: "device-local" as const,No content changes.
No content changes.
No content changes.
@@ -23,14 +23,17 @@
2323 "tasks": {2424 "check:env": "deno run --allow-env=JAZZ_APP_ID,JAZZ_SERVER_URL,JAZZ_ADMIN_SECRET,BACKEND_SECRET --allow-read=.env tools/check_env.ts",2525 "check:secrets": "deno run --allow-env=JAZZ_ADMIN_SECRET,BACKEND_SECRET --allow-read --allow-run=git tools/check_secret_leaks.ts",26+ "check:migrations": "deno run --allow-read=apps/reference/src/migrations --allow-run tools/check_migrations.ts",2627 "check:astro": "LOFI_SKIP_JAZZ_MANAGED=1 JAZZ_ADMIN_SECRET= BACKEND_SECRET= deno run -A npm:astro@7.0.9 check --root apps/reference",27- "check": "deno fmt --check && deno lint && deno check tools/*.ts migrations/*.ts spikes/jazz2-baseline/*.ts spikes/jazz2-baseline/src/*.ts apps/reference/*.ts apps/reference/src/**/*.ts apps/reference/src/**/*.tsx && deno task check:astro && deno task check:env && deno task check:secrets",28+ "check": "deno fmt --check && deno lint && deno check tools/*.ts spikes/jazz2-baseline/*.ts spikes/jazz2-baseline/src/*.ts apps/reference/*.ts apps/reference/src/**/*.ts apps/reference/src/**/*.tsx && deno task check:astro && deno task check:env && deno task check:secrets && deno task check:migrations && deno task test",2829 "test:unit": "deno test --allow-read --allow-write=. --allow-run=git tools apps/reference/src/_lofi apps/reference/tests",2930 "test": "deno task test:unit",3031 "test:browser:install": "deno run -A npm:playwright@1.61.1 install chromium",3132 "test:golden:checkout": "deno run -A tools/golden_path_checkout.ts",3233 "dev": "deno run --allow-env=JAZZ_APP_ID,JAZZ_SERVER_URL,JAZZ_ADMIN_SECRET,BACKEND_SECRET --allow-read=.env,apps/reference/.env --allow-write=apps/reference/.env --allow-run tools/run_reference.ts dev",3334 "build": "deno run --allow-env=JAZZ_APP_ID,JAZZ_SERVER_URL,JAZZ_ADMIN_SECRET,BACKEND_SECRET --allow-read=.env,apps/reference/.env,apps/reference/dist --allow-write=apps/reference/.env,apps/reference/dist --allow-run tools/run_reference.ts build",35+ "schema:validate": "deno run -A npm:jazz-tools@2.0.0-alpha.53 validate --schema-dir apps/reference/src",36+ "schema:deploy": "deno run --env-file=.env -A npm:jazz-tools@2.0.0-alpha.53 deploy --schema-dir apps/reference/src --migrations-dir apps/reference/src/migrations",3437 "preview": "deno run --allow-net --allow-read=apps/reference/dist tools/preview.ts",3538 "spike:jazz2:dev": "deno run --allow-read=spikes/jazz2-baseline/.env --allow-write=spikes/jazz2-baseline/.env --allow-run tools/run_jazz2_spike.ts local",3639 "spike:jazz2:cloud": "deno run --allow-read=spikes/jazz2-baseline/.env --allow-write=spikes/jazz2-baseline/.env --allow-run tools/run_jazz2_spike.ts cloud",@@ -295,3 +295,9 @@ it may merge into `dev`.
295295 Layer 1 does not close #11: checkout-mode evidence is necessary, but the generated-project journey296296 cannot pass until Layer 2 implements the public create command. Remaining abstraction leaks are297297 tracked in `docs/m2-abstraction-leaks.md` rather than hidden behind premature package APIs.298+299+Layer 1 keeps M1's `lofi-prototype-<appId>` OPFS namespace so existing device rows remain visible to300+the reviewed notes-to-tasks lens. Jazz discovers that lens and its snapshots beside `schema.ts` in301+`apps/reference/src/migrations`. `deno task check:migrations` couples the current schema hash,302+incoming migration edge, and snapshot; `deno task schema:deploy` is the explicit cloud publication303+step for the schema, migration, and permissions bundle.@@ -0,0 +1,143 @@
1+import { schema as s } from "jazz-tools";2+import { referenceApp } from "../apps/reference/src/app.ts";3+import migration from "../apps/reference/src/migrations/20260715T194947-notes-to-tasks-6c62fec42c35-ff85ac1d97ee.ts";4+5+const SCHEMA_DIR = "apps/reference/src";6+const MIGRATIONS_DIR = `${SCHEMA_DIR}/migrations`;7+const SNAPSHOTS_DIR = `${MIGRATIONS_DIR}/snapshots`;8+const JAZZ_TOOLS = "npm:jazz-tools@2.0.0-alpha.53";9+10+function fail(message: string): never {11+ throw new Error(`migration contract: ${message}`);12+}13+14+async function jazz(args: string[]): Promise<string> {15+ const output = await new Deno.Command(Deno.execPath(), {16+ args: ["run", "-A", JAZZ_TOOLS, ...args, "--schema-dir", SCHEMA_DIR],17+ stdout: "piped",18+ stderr: "piped",19+ }).output();20+ if (!output.success) {21+ const error = new TextDecoder().decode(output.stderr).trim();22+ fail(`Jazz CLI failed: ${error || `exit ${output.code}`}`);23+ }24+ return new TextDecoder().decode(output.stdout).trim();25+}26+27+async function jazzSnapshot(hash: string): Promise<unknown> {28+ const output = await new Deno.Command(Deno.execPath(), {29+ args: [30+ "run",31+ "-A",32+ JAZZ_TOOLS,33+ "schema",34+ "export",35+ "--schema-hash",36+ hash,37+ "--migrations-dir",38+ MIGRATIONS_DIR,39+ ],40+ stdout: "piped",41+ stderr: "piped",42+ }).output();43+ if (!output.success) {44+ const error = new TextDecoder().decode(output.stderr).trim();45+ fail(`snapshot ${hash} does not hash to its filename: ${error || `exit ${output.code}`}`);46+ }47+ return JSON.parse(new TextDecoder().decode(output.stdout));48+}49+50+function hashField(source: string, field: "fromHash" | "toHash"): string {51+ const match = source.match(new RegExp(`${field}:\\s*["']([0-9a-f]{12})["']`));52+ if (!match) fail(`reviewed migration is missing a 12-character ${field}`);53+ return match[1];54+}55+56+const hashOutput = await jazz(["schema", "hash"]);57+const currentHash = hashOutput.match(/Current schema hash:\s*([0-9a-f]{12})/)?.[1];58+if (!currentHash) fail(`could not read the current hash from Jazz CLI output: ${hashOutput}`);59+60+const migrationNames: string[] = [];61+for await (const entry of Deno.readDir(MIGRATIONS_DIR)) {62+ if (entry.isFile && entry.name.endsWith(".ts")) migrationNames.push(entry.name);63+}64+if (migrationNames.length === 0) fail(`no reviewed migrations found in ${MIGRATIONS_DIR}`);65+66+let currentEdgeFound = false;67+for (const name of migrationNames.sort()) {68+ const source = await Deno.readTextFile(`${MIGRATIONS_DIR}/${name}`);69+ const fromHash = hashField(source, "fromHash");70+ const toHash = hashField(source, "toHash");71+ if (!name.endsWith(`-${fromHash}-${toHash}.ts`)) {72+ fail(`${name} does not encode its declared ${fromHash} -> ${toHash} edge`);73+ }74+ for (const hash of [fromHash, toHash]) {75+ const snapshotNames = [];76+ for await (const entry of Deno.readDir(SNAPSHOTS_DIR)) {77+ if (entry.isFile && entry.name.endsWith(`-${hash}.json`)) snapshotNames.push(entry.name);78+ }79+ if (snapshotNames.length !== 1) {80+ fail(`${name} requires exactly one ${hash} snapshot; found ${snapshotNames.length}`);81+ }82+ const snapshot = JSON.parse(83+ await Deno.readTextFile(`${SNAPSHOTS_DIR}/${snapshotNames[0]}`),84+ );85+ if (JSON.stringify(await jazzSnapshot(hash)) !== JSON.stringify(snapshot)) {86+ fail(`${snapshotNames[0]} does not round-trip through Jazz's hash loader`);87+ }88+ }89+ if (toHash === currentHash) currentEdgeFound = true;90+}91+if (!currentEdgeFound) {92+ fail(`current schema ${currentHash} has no reviewed incoming migration edge`);93+}94+95+const currentSnapshots: string[] = [];96+for await (const entry of Deno.readDir(SNAPSHOTS_DIR)) {97+ if (entry.isFile && entry.name.endsWith(`-${currentHash}.json`)) {98+ currentSnapshots.push(entry.name);99+ }100+}101+if (currentSnapshots.length !== 1) {102+ fail(`current schema ${currentHash} must have exactly one snapshot`);103+}104+105+const currentSnapshot = JSON.parse(106+ await Deno.readTextFile(`${SNAPSHOTS_DIR}/${currentSnapshots[0]}`),107+);108+if (JSON.stringify(referenceApp.schema.wasmSchema) !== JSON.stringify(currentSnapshot)) {109+ fail(`current snapshot ${currentSnapshots[0]} does not match schema.ts`);110+}111+112+const previousSnapshotName = [];113+for await (const entry of Deno.readDir(SNAPSHOTS_DIR)) {114+ if (entry.isFile && entry.name.endsWith("-6c62fec42c35.json")) {115+ previousSnapshotName.push(entry.name);116+ }117+}118+if (previousSnapshotName.length !== 1) fail("the M1 source snapshot is missing or ambiguous");119+const previousSnapshot = JSON.parse(120+ await Deno.readTextFile(`${SNAPSHOTS_DIR}/${previousSnapshotName[0]}`),121+);122+if (JSON.stringify(s.defineApp(migration.from).wasmSchema) !== JSON.stringify(previousSnapshot)) {123+ fail("the reviewed lens source does not match the M1 snapshot");124+}125+if (JSON.stringify(s.defineApp(migration.to).wasmSchema) !== JSON.stringify(currentSnapshot)) {126+ fail("the reviewed lens target does not match the current snapshot");127+}128+129+const expectedForward = [{130+ table: "tasks",131+ renamedFrom: "notes",132+ operations: [133+ { type: "rename", column: "body", value: "text" },134+ { type: "introduce", column: "completed", sqlType: "BOOLEAN", value: false },135+ ],136+}];137+if (JSON.stringify(migration.forward) !== JSON.stringify(expectedForward)) {138+ fail("the reviewed lens must rename notes/body and add completed=false");139+}140+141+console.log(142+ `migration contract: ${migrationNames.length} reviewed edge(s), current schema ${currentHash}`,143+);