# Typed, Callable `api` Blocks Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Let a `.wrn` page declare a typed API call — `request` / `response` / `error` — and invoke it on demand as `await api.searchUsers({ name })`.

**Architecture:** The parser gains sections inside the existing `api` block. Client-mode blocks compile into the page's browser module as an `api` object whose members call a transport added to the CSR runtime. Type safety is enforced by `tsc` over assertions written into `app/types/wrnexus.generated.d.ts`, not by logic inside the compiler.

**Tech Stack:** Bun, TypeScript, `bun:test`, happy-dom.

**Spec:** `docs/superpowers/specs/2026-08-19-typed-api-block-design.md`

## Global Constraints

- Targets are restricted to this app's `/api/*` routes. Never relax `isSafeApiPath` in `packages/dev-server/src/runtime.ts`.
- A bare block body keeps its current meaning (the legacy, untyped response body). Sections are the typed form. Existing `.wrn` files must not change behaviour.
- `ssr {}` blocks accept `response` and `error` only. `request` inside an `ssr {}` block is a parse error.
- Absent `error {}` means the call rejects. A block must never resolve to `undefined` on failure.
- Declared field types are type-only. No TypeScript may reach the emitted browser module — it is written as `.mjs` and parsed as JavaScript.
- Author-settable request headers are out of scope.
- `bun run format` before every commit; the repo gate is `bun run check:production`.

---

### Task 1: Parse sectioned `api` blocks

**Files:**

- Create: `packages/syntax/src/api-sections.ts`
- Modify: `packages/syntax/src/parser.ts` (the `DataApiBlock` interface at ~line 146; the `api` case inside the mode-block loop at ~line 693)
- Test: `packages/syntax/test/api-block.test.ts`

**Interfaces:**

- Consumes: `readBalancedBraces()` from the existing tokenizer.
- Produces:
  - `interface ApiFieldDecl { name: string; optional: boolean; type: string }`
  - `interface ApiSections { parameters: ApiFieldDecl[]; body: ApiFieldDecl[]; response: string; error: string }`
  - `function parseApiSections(source: string): ApiSections | null` — returns `null` when the body has no sections (legacy form).
  - `DataApiBlock` gains `sections?: ApiSections`. Its existing `body: string` stays and holds the legacy body.

- [ ] **Step 1: Write the failing test**

Create `packages/syntax/test/api-block.test.ts`:

```ts
import { expect, test } from "bun:test";
import { parse } from "../src/index.ts";

const page = (inner: string) => `page Repro {
  client {
${inner}
  }

  view { <main>x</main> }
}
`;

test("parses a sectioned api block into request, response and error", () => {
  const ast = parse(
    page(`    api searchUsers POST /api/users {
      request {
        body {
          name?: string
          age?: number
        }
      }

      response {
        return data.users
      }

      error {
        return []
      }
    }`),
  );

  const block = ast.dataApis[0]!;
  expect(block.name).toBe("searchUsers");
  expect(block.method).toBe("POST");
  expect(block.path).toBe("/api/users");
  expect(block.sections?.body).toEqual([
    { name: "name", optional: true, type: "string" },
    { name: "age", optional: true, type: "number" },
  ]);
  expect(block.sections?.response.trim()).toBe("return data.users");
  expect(block.sections?.error.trim()).toBe("return []");
});

test("a bare body still parses as the legacy response body", () => {
  const ast = parse(
    page(`    api legacyUsers GET /api/users {
      return users.length
    }`),
  );

  const block = ast.dataApis[0]!;
  expect(block.sections).toBeUndefined();
  expect(block.body.trim()).toBe("return users.length");
});

test("GET parameters are parsed as required when not marked optional", () => {
  const ast = parse(
    page(`    api listUsers GET /api/users {
      request {
        parameters {
          team: string
        }
      }

      response {
        return data.users
      }
    }`),
  );

  expect(ast.dataApis[0]!.sections?.parameters).toEqual([
    { name: "team", optional: false, type: "string" },
  ]);
});

test("request inside an ssr block is rejected with a message naming the restriction", () => {
  const source = `page Repro {
  ssr {
    api ssrUsers GET /api/users {
      request {
        parameters {
          team: string
        }
      }

      response {
        return data.users
      }
    }
  }

  view { <main>x</main> }
}
`;

  expect(() => parse(source)).toThrow(/request .*ssr/i);
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `bun test packages/syntax/test/api-block.test.ts`
Expected: FAIL — `parse` does not produce `sections`.

- [ ] **Step 3: Write the section parser**

Create `packages/syntax/src/api-sections.ts`:

```ts
/**
 * Parse the sectioned form of an `api` block body.
 *
 * Returns null when no section keyword is present, which is how the legacy
 * bare-body form stays valid: the caller keeps treating the body as the
 * response expression.
 */
export interface ApiFieldDecl {
  name: string;
  optional: boolean;
  type: string;
}

export interface ApiSections {
  parameters: ApiFieldDecl[];
  body: ApiFieldDecl[];
  response: string;
  error: string;
}

const SECTION_NAMES = ["request", "response", "error"] as const;

/** Slice the balanced `{ ... }` that follows `keyword`, or null when absent. */
function sectionBody(source: string, keyword: string): string | null {
  const match = new RegExp(`(^|[^A-Za-z0-9_$])${keyword}\\s*\\{`).exec(source);
  if (!match) return null;

  const open = source.indexOf("{", match.index + match[1]!.length);
  let depth = 0;

  for (let index = open; index < source.length; index++) {
    const character = source[index];
    if (character === "{") depth++;
    else if (character === "}") {
      depth--;
      if (depth === 0) return source.slice(open + 1, index);
    }
  }

  throw new Error(`Unclosed "${keyword}" section in an api block`);
}

/** `name?: string` -> { name, optional, type }. Blank lines and comments are skipped. */
function parseFields(source: string): ApiFieldDecl[] {
  const fields: ApiFieldDecl[] = [];

  for (const rawLine of source.split("\n")) {
    const line = rawLine.trim().replace(/,$/, "");
    if (!line || line.startsWith("//")) continue;

    const match = /^([A-Za-z_$][A-Za-z0-9_$]*)(\?)?\s*:\s*(.+)$/.exec(line);
    if (!match) {
      throw new Error(`Expected "name: type" in an api request section, got "${line}"`);
    }

    fields.push({ name: match[1]!, optional: match[2] === "?", type: match[3]!.trim() });
  }

  return fields;
}

export function parseApiSections(source: string): ApiSections | null {
  const present = SECTION_NAMES.some((name) => sectionBody(source, name) !== null);
  if (!present) return null;

  const request = sectionBody(source, "request");

  return {
    parameters: request ? parseFields(sectionBody(request, "parameters") ?? "") : [],
    body: request ? parseFields(sectionBody(request, "body") ?? "") : [],
    response: sectionBody(source, "response") ?? "",
    error: sectionBody(source, "error") ?? "",
  };
}

/** True when the block declares a `request` section. */
export function hasRequestSection(source: string): boolean {
  return sectionBody(source, "request") !== null;
}
```

- [ ] **Step 4: Wire it into the parser**

In `packages/syntax/src/parser.ts`, add the import at the top:

```ts
import { parseApiSections, hasRequestSection, type ApiSections } from "./api-sections.ts";
```

Extend the interface at ~line 146:

```ts
export interface DataApiBlock {
  mode: DataMode;
  name: string;
  method: string;
  path: string;
  /** Legacy bare body. Empty string when `sections` is set. */
  body: string;
  /** Present only for the sectioned, typed form. */
  sections?: ApiSections;
}
```

Replace the `dataApis.push` at ~line 699:

```ts
const sections = parseApiSections(body);
if (sections && mode !== "client" && hasRequestSection(body)) {
  throw new ParseError(
    `An ssr api block cannot declare "request": there is no caller at render time to supply it. Use a client block, or a server function.`,
  );
}
dataApis.push({
  mode,
  name,
  method,
  path,
  body: sections ? "" : body,
  ...(sections ? { sections } : {}),
});
```

- [ ] **Step 5: Run tests to verify they pass**

Run: `bun test packages/syntax`
Expected: PASS, including the pre-existing syntax suite (the legacy-body test is the regression guard).

- [ ] **Step 6: Commit**

```bash
bun run format
git add packages/syntax/src/api-sections.ts packages/syntax/src/parser.ts packages/syntax/test/api-block.test.ts
git commit -m "feat(syntax): parse sectioned api blocks"
```

---

### Task 2: `callApi` transport in the CSR runtime

**Files:**

- Modify: `packages/csr/src/reactive-runtime.ts` (add the function near `callServerFunction`; expose it on the client context beside `server`)
- Test: `packages/csr/test/api-call.test.ts`

**Interfaces:**

- Consumes: the CSRF lookup already in `reactive-runtime.ts` (`wrn-csrf` cookie, `wrnexus-csrf` meta).
- Produces: `context.callApi(path, method, input)` — resolves to the parsed JSON payload for 2xx; rejects with an `Error` carrying `status`, `message`, and `data` otherwise. Task 3 calls this.

**Note:** `REACTIVE_RUNTIME` is a template literal. Backticks inside it terminate the string — use plain quotes in any code or comment you add.

- [ ] **Step 1: Write the failing test**

Create `packages/csr/test/api-call.test.ts`:

```ts
import { expect, test, beforeEach } from "bun:test";
import { Window } from "happy-dom";
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
import { restoreGlobalsAfterAll } from "./global-restore.ts";

const REPLACED_GLOBALS = ["window", "document", "location", "fetch"];
restoreGlobalsAfterAll(REPLACED_GLOBALS);

beforeEach(() => {
  for (const name of REPLACED_GLOBALS) delete (globalThis as Record<string, unknown>)[name];
});

interface Call {
  url: string;
  init: RequestInit;
}

/** Mount the runtime with a recording fetch and return its callApi plus the calls made. */
function harness(response: { status: number; payload: unknown }) {
  const win = new Window() as unknown as Window & Record<string, unknown>;
  win.document.body.innerHTML = `<div data-scope="x: 1"></div>`;
  const calls: Call[] = [];

  (globalThis as Record<string, unknown>).window = win;
  (globalThis as Record<string, unknown>).document = win.document;
  (globalThis as Record<string, unknown>).location = win.location;
  (globalThis as Record<string, unknown>).fetch = (url: string, init: RequestInit) => {
    calls.push({ url, init });
    return Promise.resolve({
      ok: response.status >= 200 && response.status < 300,
      status: response.status,
      json: () => Promise.resolve(response.payload),
    });
  };

  (0, eval)(REACTIVE_RUNTIME);
  const callApi = (win as unknown as { __wrnexusCallApi: Function }).__wrnexusCallApi;
  return { callApi, calls, win };
}

test("GET builds a query string and omits undefined fields", async () => {
  const { callApi, calls } = harness({ status: 200, payload: { users: [] } });

  await callApi("/api/users", "GET", { name: "Ajay", age: undefined });

  expect(calls[0]!.url).toBe("/api/users?name=Ajay");
  expect(calls[0]!.init.method).toBe("GET");
  expect(calls[0]!.init.body).toBeUndefined();
});

test("POST sends a JSON body", async () => {
  const { callApi, calls } = harness({ status: 200, payload: { ok: true } });

  await callApi("/api/users", "POST", { name: "Ajay" });

  expect(calls[0]!.url).toBe("/api/users");
  expect(calls[0]!.init.body).toBe(JSON.stringify({ name: "Ajay" }));
  expect((calls[0]!.init.headers as Record<string, string>)["content-type"]).toBe(
    "application/json",
  );
});

test("a non-GET request carries the CSRF token from the cookie", async () => {
  const { callApi, calls, win } = harness({ status: 200, payload: {} });
  win.document.cookie = "wrn-csrf=token-123";

  await callApi("/api/users", "POST", {});

  expect((calls[0]!.init.headers as Record<string, string>)["x-csrf-token"]).toBe("token-123");
});

test("a 2xx resolves to the parsed payload", async () => {
  const { callApi } = harness({ status: 200, payload: { users: [{ name: "Ajay" }] } });

  expect(await callApi("/api/users", "GET", {})).toEqual({ users: [{ name: "Ajay" }] });
});

test("a non-2xx rejects with status, message and data", async () => {
  const { callApi } = harness({ status: 400, payload: { error: "Bad filter" } });

  const failure = await callApi("/api/users", "GET", {}).catch(
    (error: Error & { status?: number; data?: unknown }) => error,
  );

  expect(failure.status).toBe(400);
  expect(failure.message).toContain("Bad filter");
  expect(failure.data).toEqual({ error: "Bad filter" });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `bun test packages/csr/test/api-call.test.ts`
Expected: FAIL — `__wrnexusCallApi` is undefined.

- [ ] **Step 3: Implement the transport**

In `packages/csr/src/reactive-runtime.ts`, add this beside `callServerFunction` (no backticks):

```js
/*
 * Transport for compiled api blocks.
 *
 * Only the request and the failure shape live here. A block's response and
 * error bodies are page code, so they are emitted into the browser module
 * and applied by the caller.
 */
function readCsrfToken() {
  var meta = document.querySelector('meta[name="wrnexus-csrf"]');
  if (meta) return meta.getAttribute("content") || "";
  var match = /(?:^|;\s*)wrn-csrf=([^;]+)/.exec(document.cookie || "");
  return match ? decodeURIComponent(match[1]) : "";
}

function wrnexusCallApi(path, method, input) {
  var verb = String(method || "GET").toUpperCase();
  var values = input || {};
  var url = path;
  var headers = { accept: "application/json" };
  var init = { method: verb, credentials: "same-origin", headers: headers };

  if (verb === "GET" || verb === "HEAD") {
    var query = [];
    Object.keys(values).forEach(function (key) {
      var value = values[key];
      // An omitted filter must not become "name=undefined".
      if (value === undefined || value === null || value === "") return;
      query.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(value)));
    });
    if (query.length) url = path + "?" + query.join("&");
  } else {
    headers["content-type"] = "application/json";
    headers["x-csrf-token"] = readCsrfToken();
    init.body = JSON.stringify(values);
  }

  return fetch(url, init).then(function (response) {
    return response.json().then(
      function (data) {
        if (response.ok) return data;
        var message =
          data && data.error ? String(data.error) : "Request failed with " + response.status;
        var failure = new Error(message);
        failure.status = response.status;
        failure.data = data;
        throw failure;
      },
      function () {
        var failure = new Error("Response was not valid JSON");
        failure.status = response.status;
        failure.data = undefined;
        throw failure;
      },
    );
  });
}

window.__wrnexusCallApi = wrnexusCallApi;
```

Then expose it on the client context. Find where `server: serverProxy` is placed on the context object (search for `server: serverProxy`) and add alongside it:

```js
        callApi: wrnexusCallApi,
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `bun test packages/csr`
Expected: PASS — the new file plus the existing suite.

- [ ] **Step 5: Check the runtime size budget**

Run: `bun scripts/lib/measure-runtime-size.ts`
Expected: `reactive-runtime.ts` under the 50,500 budget in `scripts/security-performance-audit.mjs`. If it exceeds, raise the budget in that file **in this commit** with a one-line note saying what bought the bytes — do not leave the gate red for the next task.

- [ ] **Step 6: Commit**

```bash
bun run format
git add packages/csr/src/reactive-runtime.ts packages/csr/test/api-call.test.ts scripts/security-performance-audit.mjs
git commit -m "feat(csr): add the api block transport"
```

---

### Task 3: Compile client-mode blocks into the browser module

**Files:**

- Modify: `packages/compiler/src/client-codegen.ts` (the `__wrnexusCreateClientFunctions` template at ~line 361)
- Test: `packages/compiler/test/api-block-codegen.test.ts`

**Interfaces:**

- Consumes: `DataApiBlock.sections` from Task 1; `context.callApi` from Task 2.
- Produces: the emitted module declares `const api = { <name>: async (input) => ... }` inside `__wrnexusCreateClientFunctions`, so client function bodies can call `await api.searchUsers({...})`.

- [ ] **Step 1: Write the failing test**

Create `packages/compiler/test/api-block-codegen.test.ts`:

```ts
import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generateTargets } from "../src/targets.ts";

function browserModule(inner: string): string {
  return generateTargets(
    parse(`page Repro {
  client {
${inner}
  }

  functions {
    client async function run(): Promise<void> {
      const users = await api.searchUsers({ name: "Ajay" })
      console.log(users)
    }
  }

  view { <main><button @click="run()">go</button></main> }
}
`),
  ).browser;
}

const BLOCK = `    api searchUsers POST /api/users {
      request {
        body {
          name?: string
          age?: number
        }
      }

      response {
        return data.users
      }

      error {
        return []
      }
    }`;

test("emits an api member that calls the transport with the block's path and method", () => {
  const generated = browserModule(BLOCK);

  expect(generated).toContain("const api =");
  expect(generated).toContain("searchUsers");
  expect(generated).toContain('"/api/users"');
  expect(generated).toContain('"POST"');
});

test("declared field types never reach the browser module", () => {
  // The artifact is written as .mjs and parsed as JavaScript.
  const generated = browserModule(BLOCK);

  expect(generated).not.toContain("name?: string");
  expect(generated).not.toContain("age?: number");
});

test("the emitted module is valid JavaScript", () => {
  const generated = browserModule(BLOCK);

  expect(() => {
    new Function(generated.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, ""));
  }).not.toThrow();
});

test("a block without an error section still emits its response body", () => {
  const generated = browserModule(`    api plainUsers GET /api/users {
      request {
        parameters {
          team: string
        }
      }

      response {
        return data.users
      }
    }`);

  expect(generated).toContain("plainUsers");
  expect(generated).toContain("data.users");
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `bun test packages/compiler/test/api-block-codegen.test.ts`
Expected: FAIL — no `const api =` in the generated module.

- [ ] **Step 3: Emit the api object**

In `packages/compiler/src/client-codegen.ts`, add this helper above `generateBrowserModule`:

```ts
/**
 * Client-mode api blocks become members of an `api` object in client scope.
 *
 * Only the response and error bodies are emitted; the declared field types are
 * type-only and are consumed by the types generator instead. Anything
 * TypeScript reaching this module would be a syntax error in the .mjs artifact.
 */
function apiBindings(ast: PageAst): string {
  const members = ast.dataApis
    .filter((block) => block.mode === "client" && block.sections)
    .map((block) => {
      const sections = block.sections!;
      const response = sections.response.trim() || "return data;";
      const error = sections.error.trim();
      const failure = error
        ? `(error) => { const status = error.status; const message = error.message; const data = error.data; ${error} }`
        : `(error) => { throw error; }`;

      return `    ${JSON.stringify(block.name)}: async (input) => context.callApi(${JSON.stringify(
        block.path,
      )}, ${JSON.stringify(block.method)}, input).then((data) => { ${response} }).catch(${failure})`;
    });

  return members.length ? `const api = {\n${members.join(",\n")}\n  };` : "";
}
```

Then insert its output into the `__wrnexusCreateClientFunctions` template, immediately after `const refs = context.refs;`:

```ts
  const refs = context.refs;
  ${apiBindings(ast)}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `bun test packages/compiler`
Expected: PASS, including the existing compiler suite.

- [ ] **Step 5: Commit**

```bash
bun run format
git add packages/compiler/src/client-codegen.ts packages/compiler/test/api-block-codegen.test.ts
git commit -m "feat(compiler): compile client api blocks into the browser module"
```

---

### Task 4: Generate the type assertions

**Files:**

- Modify: `packages/cli/src/types.ts` (the generated namespace at ~line 209-221)
- Test: `packages/cli/test/api-block-types.test.ts`

**Interfaces:**

- Consumes: `DataApiBlock.sections` from Task 1; the existing `ApiRoute`, `ApiContracts`, and `ApiContract` types this file already emits.
- Produces: `ApiInput`, `ApiOutput`, `AssertAssignable`, and one `__wrn_api_check_<name>` type per sectioned block, all inside `declare namespace WRNexusGenerated`.

- [ ] **Step 1: Write the failing test**

Create `packages/cli/test/api-block-types.test.ts`:

```ts
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { generateApplicationTypes } from "../src/types.ts";

const roots: string[] = [];
afterEach(() => {
  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

/** Minimal app with one typed endpoint and one page that calls it. */
function fixture(block: string): string {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-api-types-"));
  roots.push(root);
  mkdirSync(join(root, "app/pages"), { recursive: true });
  mkdirSync(join(root, "app/api"), { recursive: true });
  writeFileSync(
    join(root, "app/api/users.ts"),
    `export const POST = async () => Response.json({ users: [] });\n`,
  );
  writeFileSync(
    join(root, "app/pages/search.wrn"),
    `page Search {\n  client {\n${block}\n  }\n\n  view { <main>x</main> }\n}\n`,
  );
  return root;
}

const BLOCK = `    api searchUsers POST /api/users {
      request {
        body {
          name?: string
          age?: number
        }
      }

      response {
        return data.users
      }
    }`;

test("emits the ApiInput, ApiOutput and AssertAssignable helpers", () => {
  const root = fixture(BLOCK);
  generateApplicationTypes(root);
  const generated = readFileSync(join(root, "app/types/wrnexus.generated.d.ts"), "utf8");

  expect(generated).toContain("type AssertAssignable<");
  expect(generated).toContain('type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"]');
  expect(generated).toContain(
    'type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"]',
  );
});

test("emits one assertion per sectioned block, naming its route and method", () => {
  const root = fixture(BLOCK);
  generateApplicationTypes(root);
  const generated = readFileSync(join(root, "app/types/wrnexus.generated.d.ts"), "utf8");

  expect(generated).toContain("__wrn_api_check_searchUsers");
  expect(generated).toContain('ApiInput<"/api/users", "POST">');
  expect(generated).toContain("name?: string");
  expect(generated).toContain("age?: number");
});

test("a legacy bare-body block produces no assertion", () => {
  const root = fixture(`    api legacyUsers GET /api/users {
      return users.length
    }`);
  generateApplicationTypes(root);
  const generated = readFileSync(join(root, "app/types/wrnexus.generated.d.ts"), "utf8");

  expect(generated).not.toContain("__wrn_api_check_legacyUsers");
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `bun test packages/cli/test/api-block-types.test.ts`
Expected: FAIL — none of the helpers are emitted.

- [ ] **Step 3: Emit the helpers and assertions**

In `packages/cli/src/types.ts`, add above the function that builds the namespace:

```ts
/**
 * Type assertions for sectioned api blocks.
 *
 * Enforcement lives here rather than in the compiler because this file is under
 * `app/` and is therefore compiled by the project's own tsc, while generated
 * build artifacts are not type-checked at all.
 */
function apiBlockAssertions(pages: { path: string; ast: PageAst }[]): string {
  const lines: string[] = [];

  for (const page of pages) {
    for (const block of page.ast.dataApis) {
      if (!block.sections) continue;

      const fields = [...block.sections.parameters, ...block.sections.body];
      const shape = fields.length
        ? `{ ${fields.map((f) => `${f.name}${f.optional ? "?" : ""}: ${f.type}`).join("; ")} }`
        : "Record<string, never>";

      lines.push(
        `  type __wrn_api_check_${block.name} = AssertAssignable<${shape}, ApiInput<${JSON.stringify(
          block.path,
        )}, ${JSON.stringify(block.method)}>>;`,
      );
    }
  }

  return lines.join("\n");
}
```

Add the helper types and the assertions into the generated namespace, next to the existing `generatedContractMap` calls:

```ts
  type AssertAssignable<Actual, Expected> = [Actual] extends [Expected] ? true : never;
  type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
  type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
${apiBlockAssertions(pages)}
```

`generateApplicationTypes` does not retain page ASTs — line ~111 parses component `.wrn` files
only. Collect them with the `files()` helper this module already uses (see its use at ~line 296):

```ts
const pageAsts = files(join(root, "app"), (path) => extname(path) === ".wrn").map((file) => ({
  path: file,
  ast: parse(readFileSync(file, "utf8")),
}));
```

`parse`, `files`, `join`, `extname`, and `readFileSync` are all already imported by this module.
`PageAst` comes from `@wrnexus/syntax`; add it to the existing type import if absent.

- [ ] **Step 4: Warn for routes with no contract**

Where a block's path has no entry in `apiContracts`, print once per route:

```ts
console.warn(
  `[wrnexus] api block "${block.name}" targets ${block.path}, which has no defineEndpoint contract — its declared types are not checked.`,
);
```

- [ ] **Step 5: Run tests to verify they pass**

Run: `bun test packages/cli`
Expected: PASS.

- [ ] **Step 6: Verify the assertion actually bites**

Regenerate types for the example app and confirm a wrong field fails `tsc`, rather than trusting the string match:

```bash
bun run --cwd examples/basic-app wrnexus generate types
bun run typecheck
```

Expected: PASS. Then temporarily add a field the endpoint does not accept to a block in `examples/basic-app`, regenerate, and confirm `bun run typecheck` FAILS. Revert the temporary change.

- [ ] **Step 7: Commit**

```bash
bun run format
git add packages/cli/src/types.ts packages/cli/test/api-block-types.test.ts
git commit -m "feat(cli): generate type assertions for api blocks"
```

---

### Task 5: SSR-mode `response` and `error` sections

**Files:**

- Modify: `packages/compiler/src/codegen.ts` (`apiBindingMap` at ~line 932, and `dataBody`)
- Test: `packages/compiler/test/api-block-ssr.test.ts`

**Interfaces:**

- Consumes: `DataApiBlock.sections` from Task 1.
- Produces: no new exports. An `ssr` block with sections evaluates `response` with the payload bound to `data`; a failure runs `error`.

- [ ] **Step 1: Write the failing test**

Create `packages/compiler/test/api-block-ssr.test.ts`:

```ts
import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generate } from "../src/codegen.ts";

function serverModule(inner: string): string {
  return generate(
    parse(`page Repro {
  ssr {
${inner}
  }

  view { <main><p api="ssrUsers">loading</p></main> }
}
`),
  );
}

test("a sectioned ssr block binds the payload to data", () => {
  const generated = serverModule(`    api ssrUsers GET /api/users {
      response {
        return data.users.length
      }
    }`);

  expect(generated).toContain("data.users.length");
});

test("a legacy ssr block is unchanged", () => {
  const generated = serverModule(`    api ssrUsers GET /api/users {
      return users.length
    }`);

  expect(generated).toContain("users.length");
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `bun test packages/compiler/test/api-block-ssr.test.ts`
Expected: FAIL — the sectioned body is not read.

- [ ] **Step 3: Read sections in `apiBindingMap`**

In `packages/compiler/src/codegen.ts`, change the `body` assignment inside `apiBindingMap`:

```ts
const sectioned = block.sections;
bindings.set(block.name, {
  mode: block.mode,
  method: block.method,
  path: apiRoutePath(block.path),
  // A sectioned block binds the payload to `data`; the legacy form keeps
  // the `with ($data)` injection, which cannot be typed.
  body: sectioned
    ? `const data = $data; ${sectioned.response.trim() || "return data;"}`
    : dataBody(block.body),
  helpers: modeHelpers(ast, block.mode, sharedHelpers),
});
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `bun test packages/compiler`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
bun run format
git add packages/compiler/src/codegen.ts packages/compiler/test/api-block-ssr.test.ts
git commit -m "feat(compiler): support sections in ssr api blocks"
```

---

### Task 6: End-to-end verification in a real browser

**Files:**

- Create: `examples/basic-app/app/api/directory.ts`
- Create: `examples/basic-app/app/pages/api-block-demo.wrn`
- Test: exercised through the running dev server; no unit test file

**Interfaces:**

- Consumes: everything from Tasks 1-5.
- Produces: a demo page that stays in the repo as the worked example.

**Why this task exists:** this repository has repeatedly shipped features whose tests passed while the feature did not work — the island runtime that 404'd, the JSX pragma test asserting generated text, three tests that would have survived deleting the code they guarded. Browser verification is part of done.

- [ ] **Step 1: Add the endpoint**

Create `examples/basic-app/app/api/directory.ts`:

```ts
import { defineEndpoint } from "@wrnexus/core";
import type { Context } from "@wrnexus/core";

const ALL = [
  { name: "Ajay", designation: "UI" },
  { name: "Asha", designation: "Backend" },
  { name: "Chen", designation: "UI" },
];

export const POST = async (ctx: Context) => {
  const body = (await ctx.req.json().catch(() => ({}))) as { name?: string };
  const needle = String(body.name ?? "").toLowerCase();
  return Response.json({ users: ALL.filter((user) => user.name.toLowerCase().includes(needle)) });
};
```

- [ ] **Step 2: Add the page**

Create `examples/basic-app/app/pages/api-block-demo.wrn`:

```wrn
page ApiBlockDemo {
  state nameFilter = "a"
  state found = ""
  state failed = ""

  client {
    api searchDirectory POST /api/directory {
      request {
        body {
          name?: string
        }
      }

      response {
        return data.users
      }

      error {
        return []
      }
    }
  }

  functions {
    client async function search(): Promise<void> {
      const users = await api.searchDirectory({ name: nameFilter })
      found = users.map((user) => user.name).join(", ")
    }
  }

  view {
    <main>
      <button @click="search()">Search</button>
      <p class="found" data-text="found">{found}</p>
      <p class="failed" data-text="failed">{failed}</p>
    </main>
  }
}
```

- [ ] **Step 3: Start the dev server**

```bash
bun run --cwd examples/basic-app dev -- --port=3480
```

Wait for `WrNexus — http://localhost:3480`, then confirm the page responds:

```bash
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3480/api-block-demo
```

Expected: `200`.

- [ ] **Step 4: Drive it in the browser**

Open `http://localhost:3480/api-block-demo`, click Search, and read the page. Expected: `.found` contains `Ajay, Asha` (both match "a"; Chen does not). Confirm in the network panel that exactly one `POST /api/directory` was made and that it carried an `x-csrf-token` header.

- [ ] **Step 5: Verify the failure path**

Temporarily change the block's path to `/api/directory-missing`, reload, and click Search. Expected: no exception in the console and `.found` empty, because the `error` section returned `[]`. Restore the path.

- [ ] **Step 6: Verify the type gate**

```bash
bun run --cwd examples/basic-app wrnexus generate types
bun run typecheck
```

Expected: PASS. Then add `nope?: string` to the block's `body`, regenerate, and confirm `typecheck` FAILS naming `__wrn_api_check_searchDirectory`. Remove it.

- [ ] **Step 7: Check whether errors appear inline in the editor**

The spec assumes the language server surfaces the generated assertion's failure inside the `.wrn`
file. That is an assumption, not a requirement. With a deliberately wrong field in place, open the
page in VS Code and note whether the error appears on the block or only in
`wrnexus.generated.d.ts`. If only the latter, record it as follow-up work — do not expand this
plan's scope to fix it.

- [ ] **Step 8: Run the full gate**

```bash
bun run format
bun test
bun run typecheck
bun run --cwd editors/vscode build
bun run check:production
```

Expected: all pass. The editor bundles must be rebuilt because `packages/compiler` and `packages/syntax` changed; `check:editor-compiler` fails on a stale bundle.

- [ ] **Step 9: Commit**

```bash
git add examples/basic-app editors/vscode/src
git commit -m "feat(examples): worked example for typed api blocks"
```

---

## Notes for the executor

- **`REACTIVE_RUNTIME` is a template literal.** A backtick in code or a comment you add to it terminates the string and produces a confusing parse error elsewhere in the file. Use plain quotes.
- **Do not use `node -e` or shell heredocs to write regexes** into these files; escaping mangles them silently. Use the editing tools.
- **The public API surface is gated.** Adding an export to a package makes `check:public-api` fail until you run `bun run generate:public-api` and confirm the diff is additive only.
- **If a test would still pass with the code it guards deleted, it is not a test.** Delete the implementation, watch it fail, restore it.
