clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

docs: raise JSR score and make the package README user-facing

Dami committed 8397e65
- add a package description to deno.json (hasDescription)
- add module-level @module docs to all 7 JSR entrypoints (allEntrypointsDocs)
- add JSDoc to the exported symbols across package/testing/* (documented
  symbols was ~10%)
- rewrite the root README to lead with `create` and what a generated project
  looks like, drop the stale 'nothing published yet' + repo-internal map, and
  fix links to removed files

Provenance was already recorded for 0.1.0 (sigstore transparency log); the score
API just lagged. multipleRuntimesCompatible is left false on purpose — this is a
Deno-only CLI/toolkit and claiming Node/browser support would be dishonest.
commit
8397e65635abc9e798962c242693e3424b2cd4b8
parent
57718ef68ac0a21ac3517ff12688101b523919a5
author
Dami <dami.etoile@gmail.com>
15 files changed, +217 -110
MREADME.md184++++++++++++------------------
Mdeno.json1+
Mpackage/commands/build.ts8+
Mpackage/commands/dev.ts7+
Mpackage/commands/doctor.ts8+
Mpackage/commands/preview.ts7+
Mpackage/commands/run_tests.ts7+
Mpackage/create.ts7+
Mpackage/testing/browser_error.ts5+
Mpackage/testing/convergence.ts9+
Mpackage/testing/fixture.ts56+++++++++
Mpackage/testing/mod.ts8+
Mpackage/testing/readiness.ts2+
Mpackage/testing/safety.ts9+
Mpackage/testing/trace.ts9+
M README.md +74 -110
@@ -1,33 +1,13 @@
1-# lofi1+# @nzip/lofi22 3-**A local-first mobile PWA meta-framework, built as an honest prototype.**4-5-Write a schema, build Preact islands, ship an installable offline-capable app. The UI always6-hydrates from local data, identity never waits on a server, and the sync module is the only network7-surface an app developer ever sees.3+**A local-first mobile PWA meta-framework for Deno.** Declare a schema, build Preact islands, ship4+an installable offline-capable app. The UI always hydrates from local data, identity never waits on5+a server, and the sync module is the only network surface you touch.86 97 Built on [Jazz 2](https://jazz.tools) (CRDT data + sync) · [Preact](https://preactjs.com) islands in108 [Astro](https://astro.build) · the [Deno](https://deno.com) toolchain.119 12-> **Status: working prototype, pre-release.** Nothing is published yet. M0 (DevX contract) and M113-> (feasibility) are complete; all three M2 source layers are merged on `m2` and undergoing combined14-> promotion review. See the [roadmap](#roadmap).15-16-## Why lofi17-18-- **Local-first without the plumbing.** Product code touches schema, config, islands, styles, and19- tests — never providers, workers, transports, or service-worker internals20- ([DX-AUTHOR-01, DX-LEAK-01](docs/devx-contract.md#measurable-promises)).21-- **Honest by construction.** Durable storage never silently degrades to memory; diagnostics only22- claim what the underlying APIs can actually observe; identity wording matches real custody and23- recovery semantics.24-- **Mobile PWA first.** Platform floors (Android Chrome 148+, iOS Safari 16.4+) are enforced boot25- gates, not advisory footnotes. Unsupported browsers get an explicit answer, never silent data26- loss.27-28-## The north star29-30-When every milestone below has landed, this is the entire onboarding experience:10+## Quick start3111 3212 ```sh3313 deno run -A jsr:@nzip/lofi/create my-app
@@ -35,105 +15,89 @@ cd my-app
3515 deno task dev3616 ```3717 38-From there a developer makes a retained local write, reloads it, works offline, runs checks, builds,39-previews, and opens the same stable secure origin on a physical phone — with Deno as the only40-required global runtime and a command surface of exactly seven verbs: `create`, `dev`, `doctor`,41-`check`, `test`, `build`, `preview`.18+That's the whole onboarding. You now have an installable, offline-capable local-first PWA — make a19+write, reload it, go offline, and the UI keeps working because it reads from durable local storage,20+not the network.4221 43-## Try it today (checkout journey)22+> Requires **Deno 2.9+**, the only global runtime. With no `.env` the app runs local-only (stated23+> explicitly at boot); for managed sync, copy `.env.example` and set the public `JAZZ_APP_ID` /24+> `JAZZ_SERVER_URL` pair. Server-only secrets never reach client output.4425 45-The `m2` branch contains the accepted source generator, package-owned commands, development46-inspector, and browser-testing helpers. The package is not published until explicitly authorized and47-the registry-backed smoke passes, so the currently available journey runs from a checkout:26+## What a generated project looks like4827 49-```sh50-git clone https://github.com/FelineStateMachine/lofi.git51-cd lofi52-deno task dev53-```54-55-- **Deno 2.9+** is the only required global runtime.56-- **No `.env` means local-only mode**, stated explicitly at boot. For cloud sync, copy57- `.env.example` and set the public `JAZZ_APP_ID`/`JAZZ_SERVER_URL` pair. Server-only secrets never58- reach client output — this is tested, not hoped.59-- From `deno task dev` you should reach a retained local write in under 60 seconds60- ([DX-PROTOTYPE-01](docs/devx-contract.md#measurable-promises)).61-62-### Command surface63-64-| Command | What it does today |65-| ------------------- | ------------------------------------------------------------------------ |66-| `deno task dev` | Serves the prototype; prints URL plus storage, identity, and sync state. |67-| `deno task check` | Format, lint, type, Astro, env-contract, and secret-leak checks. |68-| `deno task test` | Deterministic local-first tests (no hand-timed sleeps). |69-| `deno task build` | Production build through the supported Deno path. |70-| `deno task preview` | Serves the production build. |71-72-`create` and `doctor` have graduated through the source-backed generated-project golden journey and73-clean-room review. The public JSR invocation remains unavailable until publication and its74-registry-backed smoke pass; the output contracts are specified in the75-[DevX contract](docs/devx-contract.md#canonical-command-surface-and-output-contract).76-77-## How this repository works78-79-Development here is **contract-driven**: the product is a set of falsifiable promises, and code80-graduates only by producing evidence against them.81-82-1. **[The DevX contract](docs/devx-contract.md)** defines measurable promises with IDs (like83- `DX-LOCAL-01`), budgets, and statuses (`proposed → validated / revised / rejected`).84-2. **[Decision records](docs/decisions/)** capture each experiment: question, exact versions,85- control, procedure, retained evidence, and the resulting contract delta. A spike is complete when86- it supports a decision — including a negative one — not when a demo renders once.87-3. **[Milestones](https://github.com/FelineStateMachine/lofi/milestones)** gate scope. Issues carry88- `area:*`, `type:*`, and `priority:p0–p2` labels; p0 blocks the golden path.89-90-Notable honest outcomes so far: the Jazz 2 alpha passkey-backup ceremony was **rejected** on91-security grounds rather than demoed92-([decision 0004](docs/decisions/0004-reject-alpha53-passkey-backup.md)), and live transport state is93-reported as _unavailable_ rather than faked, because the pinned API exposes no supported signal.94-95-## Roadmap96-97-| Milestone | Goal | Status |98-| ------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------- |99-| **M0 — DevX contract** | Define the golden path, command surface, environment contract, and measurable acceptance criteria. | ✅ Done |100-| **M1 — Feasibility spikes** | Prove Jazz 2, Preact, OPFS, identity, Astro islands, and Deno behavior with go/no-go decisions. | ✅ Done |101-| **M2 — DevX vertical slice** | `create`, `dev`, `doctor`, diagnostics, inspector, and testing workflows around a reference app. | 🧪 Promotion review |102-| **M3 — Mobile PWA hardening** | Install, persistence, offline cold start, lifecycle recovery, and physical-device validation. | ⏳ Queued |103-| **M4 — Runtime API extraction** | Extract remaining proven runtime seams as `@nzip/lofi` subpaths; validate them through a second app. | ⏳ Queued |104-105-The definition of done for the whole prototype is the [north star](#the-north-star) journey passing106-its contract budgets end-to-end, on real devices.107-108-## Repository map28+A new project is split into two zones — what you own, and the runtime you don't touch:10929 11030 ```text111-lofi/112-├── docs/113-│ ├── devx-contract.md # The product contract: promises, budgets, statuses114-│ └── decisions/ # Evidence-backed decision records (ADR-style)115-├── apps/reference/ # The integrated app graduated from M1 and reshaped in M2116-├── package/ # The single @nzip/lofi package source and generated-project commands117-├── spikes/ # M1 feasibility experiments, kept for evidence118-├── tools/ # Deno tasks: env contract, secret scanning, runners119-├── init.md # Original research plan — input, not the contract120-├── deno.json # Workspace tasks and pinned toolchain121-└── jsr.json # One package name, version, export map, and publish manifest31+my-app/32+├── deno.json # tasks + pinned toolchain33+├── astro.config.ts34+├── public/ # manifest, service worker, icons35+├── src/36+│ ├── schema.ts # ← your data model37+│ ├── permissions.ts # ← your access policy38+│ ├── app.ts # ← your app configuration39+│ ├── pages/ # ← your Astro shell40+│ ├── islands/ # ← your Preact UI (starts as a minimal task list to replace)41+│ ├── styles/42+│ └── _lofi/ # runtime: storage, identity, sync, lifecycle, diagnostics — don't edit43+└── tests/ # your tests + the @nzip/lofi/testing contract12244 ```12345 46+You write `schema.ts`, `permissions.ts`, islands, and styles. Everything under `src/_lofi/` is the47+framework runtime; product work never needs to touch it.48+49+## Commands50+51+Every generated project ships this task surface (`deno task <name>`):52+53+| Command | What it does |54+| --------------------------------------- | ----------------------------------------------------------- |55+| `dev` | Astro dev server; prints storage, identity, and sync state. |56+| `doctor` | Value-free readiness report — no secrets, no faked claims. |57+| `test` | Deterministic local-first tests (no hand-timed sleeps). |58+| `build` | Static production build into `dist/`. |59+| `preview` | Serves the production build locally. |60+| `deploy` / `deploy:create` | Host the static build on Deno Deploy. |61+| `schema:validate` / `schema:deploy` | Validate and publish your Jazz schema. |62+| `migrations:create` / `migrations:push` | Author and push schema migrations. |63+64+## Testing local-first behavior65+66+`@nzip/lofi/testing` provides Playwright-backed helpers for the parts that are genuinely hard to67+test — offline and multi-client behavior: a two-client fixture, concurrent-offline convergence,68+readiness waits without arbitrary sleeps, and value-free (secret-free) failure artifacts. Every69+project includes a worked example at `tests/convergence_e2e_test.ts`.70+71+## Why lofi72+73+- **Local-first without the plumbing.** Product code touches schema, config, islands, styles, and74+ tests — never providers, workers, transports, or service-worker internals.75+- **Honest by construction.** Durable storage never silently degrades to memory; diagnostics only76+ claim what the underlying APIs can actually observe; identity wording matches real custody and77+ recovery semantics.78+- **Mobile PWA first.** Platform floors (Android Chrome 148+, iOS Safari 16.4+) are enforced boot79+ gates, not advisory footnotes. Unsupported browsers get an explicit answer, never silent data80+ loss.81+12482 ## Stack12583 12684 | Layer | Choice | Pinned at |12785 | ---------- | --------------------------------------------- | ------------------- |128-| Data/sync | jazz-tools 2.0 alpha (exact pin), OPFS | `2.0.0-alpha.53` |86+| Data/sync | jazz-tools 2.0 alpha, OPFS | `2.0.0-alpha.53` |12987 | UI runtime | Preact islands, `client:only`, thin adapter | Preact 10 / Astro 7 |13088 | Shell | Astro, fully prerendered, static-through-Deno | Astro 7 |13189 | Toolchain | Deno tasks + npm-compat | Deno 2.9 |13290 133-Version pins are deliberate: the data layer is an alpha, so every bump is a reviewed decision, and134-`docs/decisions/` records what each pin was verified to do.91+Version pins are deliberate: the data layer is an alpha, so every bump is a reviewed decision.92+93+## Status94+95+Early release, pre-1.0. Passkey backup and recovery are intentionally absent — the pinned Jazz alpha96+exposes a rejected credential design — so a fresh project uses a device-local identity and does not97+imply recoverability.13598 136-## Contributing99+---137100 138-See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow: how promises, evidence, and decision139-records fit together, what `deno task check` enforces, and how issues and milestones are organized.101+This repository is also the framework's development monorepo: `package/` is the `@nzip/lofi` source102+and `apps/reference/` is the reference app the generator is validated against. See103+[CONTRIBUTING.md](CONTRIBUTING.md) and the [DevX contract](docs/devx-contract.md).
M deno.json +1 -0
M package/commands/build.ts +8 -0
M package/commands/dev.ts +7 -0
M package/commands/doctor.ts +8 -0
M package/commands/preview.ts +7 -0
M package/commands/run_tests.ts +7 -0
M package/create.ts +7 -0
M package/testing/browser_error.ts +5 -0
M package/testing/convergence.ts +9 -0
@@ -1,12 +1,14 @@
11 import type { BrowserTestClient } from "./fixture.ts";22 import type { ValueFreeState } from "./safety.ts";33 4+/** Minimal client contract the convergence runner needs: offline state and toggles. */45 export interface OfflineTestClient {56 readonly offline: boolean;67 goOffline(): Promise<void>;78 goOnline(): Promise<void>;89 }910 11+/** Minimal two-client fixture contract required to drive an offline scenario. */1012 export interface OfflineTestFixture<Client extends OfflineTestClient> {1113 readonly clients: readonly [Client, Client];1214 readonly first: Client;
@@ -19,6 +21,11 @@ export interface OfflineTestFixture<Client extends OfflineTestClient> {
1921 ): Promise<unknown>;2022 }2123 24+/**25+ * App-owned definition of a concurrent-offline convergence test: the two edits26+ * plus the hooks that assert readiness, apply and locally verify each edit, and27+ * confirm convergence after reconnection.28+ */2229 export interface ConcurrentOfflineScenario<2330 Edit,2431 Client extends OfflineTestClient = BrowserTestClient,
@@ -44,9 +51,11 @@ export interface ConcurrentOfflineScenario<
4451 readonly failureLabel?: string;4552 }4653 54+/** Thrown when a convergence scenario fails, naming the stage that failed via {@link stage}. */4755 export class ConvergenceScenarioError extends Error {4856 override readonly name = "ConvergenceScenarioError";4957 58+ /** @param stage the lifecycle stage that was in progress when the scenario failed */5059 constructor(readonly stage: string, options?: ErrorOptions) {5160 super(`Concurrent offline convergence failed during ${stage}`, options);5261 }
M package/testing/fixture.ts +56 -0
@@ -18,13 +18,19 @@ import {
1818 import { sanitizeTraceArchive } from "./trace.ts";1919 2020 type MemoryStorageState = Awaited<ReturnType<BrowserContext["storageState"]>>;21+/**22+ * Playwright context options with `storageState` forbidden, so identity never23+ * leaves memory via an on-disk state file.24+ */2125 export type SafeContextOptions = Omit<BrowserContextOptions, "storageState"> & {2226 /** Identity state is managed in memory by the fixture. File paths are forbidden. */2327 storageState?: never;2428 };2529 30+/** Identifies which of the fixture's two clients a value belongs to. */2631 export type ClientName = "first" | "second";2732 33+/** A redacted console, page-error, or failed-request record captured from a client. */2834 export interface BrowserDiagnostic {2935 readonly client: ClientName;3036 readonly kind: "console" | "page-error" | "request-failed";
@@ -33,6 +39,11 @@ export interface BrowserDiagnostic {
3339 readonly url?: string;3440 }3541 42+/**43+ * How the two clients obtain identity: `shared` clones the primary's prepared44+ * state (in memory) into the second client, `isolated` prepares each client on45+ * its own.46+ */3647 export type IdentityOptions =3748 | {3849 readonly mode: "shared";
@@ -45,6 +56,7 @@ export type IdentityOptions =
4556 readonly prepare?: (client: BrowserTestClient) => Promise<void>;4657 };4758 59+/** Configures where and how redacted failure artifacts are written on capture. */4860 export interface FailureArtifactOptions {4961 readonly directory: string;5062 /** Literal credential values to remove from retained diagnostics. */
@@ -53,6 +65,7 @@ export interface FailureArtifactOptions {
5365 readonly mask?: (client: BrowserTestClient) => readonly Locator[];5466 }5567 68+/** Options for constructing a {@link TwoClientFixture} via {@link createTwoClientFixture}. */5669 export interface TwoClientFixtureOptions {5770 readonly baseURL: string;5871 readonly identity: IdentityOptions;
@@ -63,6 +76,7 @@ export interface TwoClientFixtureOptions {
6376 readonly traceOnFailure?: boolean;6477 }6578 79+/** Re-exported so callers can catch a missing-Chromium launch failure. */6680 export { BrowserUnavailableError };6781 6882 function validateContextOptions(options: SafeContextOptions | undefined): void {
@@ -73,6 +87,11 @@ function validateContextOptions(options: SafeContextOptions | undefined): void {
7387 }7488 }7589 90+/**91+ * One browser client (context + page) whose identity and IndexedDB stay in92+ * memory. Records redacted diagnostics and can go offline/online, restart, and93+ * capture a sanitized trace.94+ */7695 export class BrowserTestClient {7796 readonly #diagnostics: BrowserDiagnostic[] = [];7897 #context: BrowserContext;
@@ -94,22 +113,27 @@ export class BrowserTestClient {
94113 this.#page = page;95114 }96115 116+ /** The client's current Playwright browser context. */97117 get context(): BrowserContext {98118 return this.#context;99119 }100120 121+ /** The client's current Playwright page. */101122 get page(): Page {102123 return this.#page;103124 }104125 126+ /** Whether the client's network is currently forced offline. */105127 get offline(): boolean {106128 return this.#offline;107129 }108130 131+ /** Redacted diagnostics collected from the client's pages so far. */109132 get diagnostics(): readonly BrowserDiagnostic[] {110133 return this.#diagnostics;111134 }112135 136+ /** Attach diagnostic listeners and, if enabled, begin failure tracing. */113137 async startRecording(): Promise<void> {114138 if (this.#recording || this.#closed) return;115139 this.#attachDiagnostics(this.#page);
@@ -119,11 +143,13 @@ export class BrowserTestClient {
119143 this.#recording = true;120144 }121145 146+ /** Force the client's network offline. */122147 async goOffline(): Promise<void> {123148 await this.#context.setOffline(true);124149 this.#offline = true;125150 }126151 152+ /** Restore the client's network to online. */127153 async goOnline(): Promise<void> {128154 await this.#context.setOffline(false);129155 this.#offline = false;
@@ -168,6 +194,10 @@ export class BrowserTestClient {
168194 return this.#page;169195 }170196 197+ /**198+ * Stop tracing and write a sanitized trace archive to `path`. Returns false199+ * when tracing was not active; removes the archive if sanitization fails.200+ */171201 async captureTrace(path: string): Promise<boolean> {172202 if (!this.#recording || !this.traceOnFailure) return false;173203 await this.#context.tracing.stop({ path });
@@ -181,6 +211,7 @@ export class BrowserTestClient {
181211 return true;182212 }183213 214+ /** Discard any pending trace and close the client's context. Idempotent. */184215 async close(): Promise<void> {185216 if (this.#closed) return;186217 this.#closed = true;
@@ -222,12 +253,19 @@ export class BrowserTestClient {
222253 }223254 }224255 256+/** The directory and file paths produced by a failure capture. */225257 export interface FailureArtifacts {226258 readonly directory: string;227259 readonly files: readonly string[];228260 }229261 262+/**263+ * Coordinates a pair of {@link BrowserTestClient}s sharing one browser, with264+ * helpers to take both clients offline/online and to capture redacted,265+ * value-free failure artifacts.266+ */230267 export class TwoClientFixture {268+ /** The fixture's two clients, ordered `[first, second]`. */231269 readonly clients: readonly [BrowserTestClient, BrowserTestClient];232270 #closed = false;233271
@@ -241,22 +279,31 @@ export class TwoClientFixture {
241279 this.clients = [first, second];242280 }243281 282+ /** The first client. */244283 get first(): BrowserTestClient {245284 return this.clients[0];246285 }247286 287+ /** The second client. */248288 get second(): BrowserTestClient {249289 return this.clients[1];250290 }251291 292+ /** Take both clients offline. */252293 async goOffline(): Promise<void> {253294 await Promise.all(this.clients.map((client) => client.goOffline()));254295 }255296 297+ /** Restore both clients to online. */256298 async goOnline(): Promise<void> {257299 await Promise.all(this.clients.map((client) => client.goOnline()));258300 }259301 302+ /**303+ * Write redacted screenshots, traces, diagnostics, and (optionally) a304+ * value-free state-shape snapshot under a label-named directory. Returns305+ * undefined when no artifact directory was configured.306+ */260307 async captureFailure(261308 label: string,262309 snapshot?: (client: BrowserTestClient) => Promise<ValueFreeState>,
@@ -299,6 +346,10 @@ export class TwoClientFixture {
299346 return { directory, files };300347 }301348 349+ /**350+ * Close both clients and, when the fixture owns it, the browser. Idempotent;351+ * rethrows the first client close failure.352+ */302353 async close(): Promise<void> {303354 if (this.#closed) return;304355 this.#closed = true;
@@ -309,6 +360,11 @@ export class TwoClientFixture {
309360 }310361 }311362 363+/**364+ * Launch (or reuse) a browser and build a {@link TwoClientFixture}: opens both365+ * clients at `baseURL`, applies the chosen identity mode with state kept in366+ * memory, and starts recording. Cleans up on any setup failure.367+ */312368 export async function createTwoClientFixture(313369 options: TwoClientFixtureOptions,314370 ): Promise<TwoClientFixture> {
M package/testing/mod.ts +8 -0
M package/testing/readiness.ts +2 -0
M package/testing/safety.ts +9 -0
M package/testing/trace.ts +9 -0