From d73568f6c4885dc23dc7b04816a1cc28d9a94b59 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 25 Jun 2026 14:05:41 +0300 Subject: [PATCH 1/3] Text (Markdown) lens, canvas polish, live-catalog validation + BFF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Text lens: a data-driven Markdown card (title_column + text_column rendered via react-markdown; raw HTML off → XSS-safe; links open new-tab). Wired across core catalog + spec enums + web/Ink renderers; TUI shows raw source. - Canvas polish: fix the heavy drag-zoom (translate-only, not CSS.Transform's scaleX/scaleY) in the mixed-span grid; cap long card content with internal vertical scroll (max-h + overscroll-contain) so tall tiles don't dominate. - Client.queries() catalog listing (og.queries.list); validate resolves cell refs/params against the live catalog; catalog command surfaces server queries. - BFF: the browser holds no token for the same-origin /og proxy (readToken short-circuits proxy mode); the Node proxy owns auth injection. - Docs refresh (README / AGENTS / CLAUDE / canon). Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 6 +- CLAUDE.md | 6 +- README.md | 6 +- dash-books-canon.md | 20 +- package.json | 2 +- packages/cli/src/commands/catalog.ts | 4 + packages/cli/src/commands/validate.ts | 258 ++++++++- packages/cli/src/source.ts | 3 +- packages/client/src/http.test.ts | 47 ++ packages/client/src/http.ts | 61 +- packages/client/src/index.ts | 4 + packages/core/package.json | 2 +- packages/core/src/catalog/index.test.ts | 16 +- packages/core/src/catalog/index.ts | 14 + packages/core/src/catalog/lenses/text.ts | 26 + packages/core/src/index.ts | 2 +- packages/core/src/spec/index.ts | 2 + packages/tui/src/components/Text.tsx | 35 ++ packages/tui/src/registry.ts | 3 +- packages/web/package.json | 1 + packages/web/src/App.test.ts | 8 + packages/web/src/App.tsx | 11 +- packages/web/src/components/Text.tsx | 60 ++ packages/web/src/config.ts | 21 +- packages/web/src/registry.ts | 3 +- packages/web/vite.config.ts | 22 +- pnpm-lock.yaml | 673 +++++++++++++++++++++++ scripts/server-demo.sh | 8 +- 28 files changed, 1254 insertions(+), 70 deletions(-) create mode 100644 packages/core/src/catalog/lenses/text.ts create mode 100644 packages/tui/src/components/Text.tsx create mode 100644 packages/web/src/components/Text.tsx diff --git a/AGENTS.md b/AGENTS.md index 709ffc4..cea4df6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t Turn an OmniGraph graph database into a **read-and-act dashboard you describe in one YAML file** — rendered identically in a terminal and a browser. -Normally you inspect a graph by writing queries and reading JSON, or by building a bespoke UI. A notebook is the layer between: a YAML file that *declares what slices of the graph to show and what actions to allow*, not code. Each cell is a typed lens (`Table`/`Path`/`Subgraph`/`ActionList`) fed by a structured query, or a control (`Select`/`Toggle`/`Button`) that filters state or mutates the graph. See `examples/company-server.notebook.yaml`: a status filter, a decisions table, a `Signal → Decision → Actor` path, an ego subgraph, and a clause list with inline Approve/Reject buttons — no UI code anywhere. +Normally you inspect a graph by writing queries and reading JSON, or by building a bespoke UI. A notebook is the layer between: a YAML file that *declares what slices of the graph to show and what actions to allow*, not code. Each data cell is a typed lens (`Table`/`Path`/`Subgraph`/`ActionList`/`Timeline`/`Card`/`Quote`/`Text`) fed by `query.ref` to a server-owned `.gq` catalog query, or a control (`Select`/`Toggle`/`Button`) that filters state or dispatches actions. See `examples/company-server.notebook.yaml`: a clause review list with inline Approve/Reject buttons, a decisions table, and a `Signal → Decision` path — no UI code anywhere. Two bets make it work: @@ -30,7 +30,7 @@ pnpm --filter @modernrelay/notebook- test -- # single test file/n pnpm tui examples/company-server.notebook.yaml # Ink TUI, server mode — server URL + graph id # come from the notebook (run server-demo.sh first) pnpm --filter @modernrelay/notebook-web dev # Vite dev server at 127.0.0.1:5173 - # add ?mode=server&server=/og (same-origin proxy) + # add ?server=/og&graph=company (same-origin proxy) pnpm --filter @modernrelay/notebook-web build # tsc + vite production build scripts/server-demo.sh # build omnigraph v0.7.0 CLI/server, boot a local @@ -48,7 +48,7 @@ The TUI consumes built `dist/` from sibling workspace packages — **always run | Package | Role | |---|---| | `@modernrelay/notebook-core` | The engine — start here. One package, three internal modules: `spec` (Zod schemas + YAML parser, query model — `ref`/`rawGq` — mutation specs), `catalog` (component+action definitions shared by both renderers; `assembleLensSpec` / `assembleControlSpec` produce json-render specs), `runtime` (capability-aware execution, state mirror, dependency invalidation, action dispatch, mutation lifecycle, optimistic reconciliation). The `@json-render/core` analog. | -| `@modernrelay/notebook-client` | **The only data source.** `ServerSource` + `translate` (structured DSL → `.gq`) + a `Client` facade over the `@modernrelay/omnigraph` SDK (`/query` + `/mutate`, graph-scoped). | +| `@modernrelay/notebook-client` | **The only data source.** `ServerSource` + a `Client` facade over the `@modernrelay/omnigraph` SDK (`/queries/{name}` + `/query` escape hatch + `/mutate`, graph-scoped). `translateMutation` exists only for the interim `set_field` write path. | | `@modernrelay/notebook-tui` | Ink renderer + CLI entry (`bin/omnigraph-tui.js`); host shell for terminal. | | `@modernrelay/notebook-web` | Vite + React + Tailwind renderer; host shell for browser. | | `@modernrelay/notebook` (`packages/cli`) | The published front-door CLI. Bundles every `@modernrelay/notebook-*` lib (tsup, `noExternal`) and ships the built web SPA in `web-dist/`. Subcommands: `view` (browser — static server + `/og` BFF proxy with server-side token injection, reusing `web/src/config.ts`'s URL-param contract), `tui` (calls `@modernrelay/notebook-tui` `main`), `validate`/`render`/`catalog`/`schema` (agent-DX, JSON out; schema via Zod 4 `z.toJSONSchema`). The workspace root is the private `notebook-workspace`; `@modernrelay/notebook` is the CLI, not the root. | diff --git a/CLAUDE.md b/CLAUDE.md index 7da9689..b4bfad5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Turn an OmniGraph graph database into a **read-and-act dashboard you describe in one YAML file** — rendered identically in a terminal and a browser. -Normally you inspect a graph by writing queries and reading JSON, or by building a bespoke UI. A notebook is the layer between: a YAML file that *declares what slices of the graph to show and what actions to allow*, not code. Each cell is a typed lens (`Table`/`Path`/`Subgraph`/`ActionList`) fed by a structured query, or a control (`Select`/`Toggle`/`Button`) that filters state or mutates the graph. See `examples/company-server.notebook.yaml`: a status filter, a decisions table, a `Signal → Decision → Actor` path, an ego subgraph, and a clause list with inline Approve/Reject buttons — no UI code anywhere. +Normally you inspect a graph by writing queries and reading JSON, or by building a bespoke UI. A notebook is the layer between: a YAML file that *declares what slices of the graph to show and what actions to allow*, not code. Each data cell is a typed lens (`Table`/`Path`/`Subgraph`/`ActionList`/`Timeline`/`Card`/`Quote`/`Text`) fed by `query.ref` to a server-owned `.gq` catalog query, or a control (`Select`/`Toggle`/`Button`) that filters state or dispatches actions. See `examples/company-server.notebook.yaml`: a clause review list with inline Approve/Reject buttons, a decisions table, and a `Signal → Decision` path — no UI code anywhere. Two bets make it work: @@ -30,7 +30,7 @@ pnpm --filter @modernrelay/notebook- test -- # single test file/n pnpm tui examples/company-server.notebook.yaml # Ink TUI, server mode — server URL + graph id # come from the notebook (run server-demo.sh first) pnpm --filter @modernrelay/notebook-web dev # Vite dev server at 127.0.0.1:5173 - # add ?mode=server&server=/og (same-origin proxy) + # add ?server=/og&graph=company (same-origin proxy) pnpm --filter @modernrelay/notebook-web build # tsc + vite production build scripts/server-demo.sh # build omnigraph v0.7.0 CLI/server, boot a local @@ -48,7 +48,7 @@ The TUI consumes built `dist/` from sibling workspace packages — **always run | Package | Role | |---|---| | `@modernrelay/notebook-core` | The engine — start here. One package, three internal modules: `spec` (Zod schemas + YAML parser, query model — `ref`/`rawGq` — mutation specs), `catalog` (component+action definitions shared by both renderers; `assembleLensSpec` / `assembleControlSpec` produce json-render specs), `runtime` (capability-aware execution, state mirror, dependency invalidation, action dispatch, mutation lifecycle, optimistic reconciliation). The `@json-render/core` analog. | -| `@modernrelay/notebook-client` | **The only data source.** `ServerSource` + `translate` (structured DSL → `.gq`) + a `Client` facade over the `@modernrelay/omnigraph` SDK (`/query` + `/mutate`, graph-scoped). | +| `@modernrelay/notebook-client` | **The only data source.** `ServerSource` + a `Client` facade over the `@modernrelay/omnigraph` SDK (`/queries/{name}` + `/query` escape hatch + `/mutate`, graph-scoped). `translateMutation` exists only for the interim `set_field` write path. | | `@modernrelay/notebook-tui` | Ink renderer + CLI entry (`bin/omnigraph-tui.js`); host shell for terminal. | | `@modernrelay/notebook-web` | Vite + React + Tailwind renderer; host shell for browser. | | `@modernrelay/notebook` (`packages/cli`) | The published front-door CLI. Bundles every `@modernrelay/notebook-*` lib (tsup, `noExternal`) and ships the built web SPA in `web-dist/`. Subcommands: `view` (browser — static server + `/og` BFF proxy with server-side token injection, reusing `web/src/config.ts`'s URL-param contract), `tui` (calls `@modernrelay/notebook-tui` `main`), `validate`/`render`/`catalog`/`schema` (agent-DX, JSON out; schema via Zod 4 `z.toJSONSchema`). The workspace root is the private `notebook-workspace`; `@modernrelay/notebook` is the CLI, not the root. | diff --git a/README.md b/README.md index dfe46ed..88df51e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @modernrelay/notebook -Notebook UI for [OmniGraph](https://github.com/ModernRelay/omnigraph). Each notebook cell is a typed *lens primitive* (Table, Path, Subgraph) rendered from a structured query — not a generic graph viewer. +Notebook UI for [OmniGraph](https://github.com/ModernRelay/omnigraph). Each notebook cell is a typed *lens primitive* (Table, Path, Subgraph) rendered from a server-owned catalog query — not a generic graph viewer. One catalog of components, two renderers (terminal and web), one server-backed runtime. @@ -8,7 +8,7 @@ One catalog of components, two renderers (terminal and web), one server-backed r Turn an OmniGraph graph database into a **read-and-act dashboard you describe in one YAML file** — rendered identically in a terminal and a browser. -Normally you inspect a graph by writing queries and reading JSON, or by building a bespoke UI. A notebook is the layer between: a YAML file that *declares what slices of the graph to show and what actions to allow*, not code. Each cell is a typed lens (`Table`/`Path`/`Subgraph`/`ActionList`) fed by a structured query, or a control (`Select`/`Toggle`/`Button`) that filters state or mutates the graph. See `examples/company-server.notebook.yaml`: a status filter, a decisions table, a `Signal → Decision → Actor` path, an ego subgraph, and a clause list with inline Approve/Reject buttons — no UI code anywhere. +Normally you inspect a graph by writing queries and reading JSON, or by building a bespoke UI. A notebook is the layer between: a YAML file that *declares what slices of the graph to show and what actions to allow*, not code. Each data cell is a typed lens (`Table`/`Path`/`Subgraph`/`ActionList`/`Timeline`/`Card`/`Quote`/`Text`) fed by `query.ref` to a server-owned `.gq` catalog query, or a control (`Select`/`Toggle`/`Button`) that filters state or dispatches actions. See `examples/company-server.notebook.yaml`: a clause review list with inline Approve/Reject buttons, a decisions table, and a `Signal → Decision` path — no UI code anywhere. Two bets make it work: @@ -71,7 +71,7 @@ pnpm --filter @modernrelay/notebook build # bundle the CLI (tsup) + web-d | Package | Purpose | |---|---| -| `@modernrelay/notebook-core` | The engine — start here. Three modules behind one entry: `spec` (Zod YAML schemas + query DSL), `catalog` (`lensComponents`/`lensActions` + `assembleLensSpec`), `runtime` (capability-aware execution, state, mutations). The `@json-render/core` analog. | +| `@modernrelay/notebook-core` | The engine — start here. Three modules behind one entry: `spec` (Zod YAML schemas + `ref`/`rawGq` query model), `catalog` (`lensComponents`/`lensActions` + `assembleLensSpec`), `runtime` (capability-aware execution, state, mutations). The `@json-render/core` analog. | | `@modernrelay/notebook-client` | The only data source — `ServerSource` + a `Client` facade over the `@modernrelay/omnigraph` SDK. | | `@modernrelay/notebook-tui` | Ink renderer + the `omnigraph-tui` binary. | | `@modernrelay/notebook-web` | Vite + React + Tailwind v4 SPA. | diff --git a/dash-books-canon.md b/dash-books-canon.md index f666b67..615d18f 100644 --- a/dash-books-canon.md +++ b/dash-books-canon.md @@ -11,7 +11,7 @@ one typed result contract in a browser and a terminal.** A "dash-book" is a notebook: a YAML document whose cells are typed **lenses** (`Table`, `Path`, -`Subgraph`, `ActionList`, `Timeline`, `Card`, `Quote`) and **controls** (`Button`, `Toggle`, `Select`). You declare *what slice of +`Subgraph`, `ActionList`, `Timeline`, `Card`, `Quote`, `Text`) and **controls** (`Button`, `Toggle`, `Select`). You declare *what slice of the graph to show and what actions to allow*; you never write UI code. The same YAML drives a React/Tailwind web renderer and an Ink terminal renderer from one shared result contract. The browser is the first-class rich renderer; the terminal is a useful degradation over the same data and action @@ -68,11 +68,10 @@ json-render spec each renderer draws. - **0.7 cluster-only.** omnigraph-server 0.7.0+ serves every graph under `/graphs/{graph}/…`, so server mode requires a graph id. Connection today is ad-hoc: `--server ` / `--graph` / `--token` (+ a few env vars). -- **Interim wart — client still generates queries.** Cells still carry the structured query DSL - (`query.fixture` = `nodes`/`path`/`ego`), which `ServerSource` compiles to ad-hoc `.gq` via - `translate.ts` (incl. ego decomposition + identifier-sanitizing regexes) and ships as - `og.query({ query })`. This is the thing §4 removes. The field is still named `fixture` for - historical reasons; that rename rides along with the §4 work. +- **Named-query reads by default.** Cells bind to server-owned catalog queries with `query.ref`; + `ServerSource` invokes them via `og.queries.invoke`. The removed `nodes`/`path`/`ego` DSL no + longer exists. Inline `rawGq` is a capability-gated escape hatch, off by default. The remaining + client-side `.gq` compiler is only the deferred interim `set_field` mutation path. --- @@ -264,8 +263,9 @@ strict, single-version schema is safe; no version negotiation or legacy-v1 suppo `Authorization`/`Proxy-Authorization` and injects only the server-side token; the browser holds no default token (no `devtoken`); the `Client` reads no env (resolution lives only in the operator resolver — `OMNIGRAPH_TOKEN`/`OMNIGRAPH_GRAPH_ID` are gone). -- [~] `notebook validate` parses + capability-checks; resolving `ref`/params against the live catalog - (`og.queries.list()`) is still TODO (needs a reachable server). +- [x] `notebook validate` parses + capability-checks and resolves `ref`/literal params against the + live catalog (`og.queries.list()`); dynamic `$state` params are checked for presence, with + literal defaults type-checked when present. - [x] Strict schema; rejects stale fixture-mode keys (internal tool — no version support). - [x] Operator-config connection client (shared `@modernrelay/notebook-client/node` resolver: config.yaml + credentials, named servers, keyed tokens, profiles; browser uses the `view` proxy). @@ -299,6 +299,10 @@ the auto tier and output-binding validation; until it ships, v1 uses author-decl - [x] **Quote lens.** Renders rows as a blockquote feed — `text_column` + a `source_column · meta…` citation (`refs/r2.jpg`) — for highlights/annotations/comments. Utterance-centric, distinct from Timeline (event feed). Replaces the cramped 2-column highlights table; web + Ink renderers. + - [x] **Text lens.** Renders the first row's `text_column` as **Markdown** (`title_column` optional + heading) — a node's definition/notes/body as a prose block. Web uses `react-markdown` (raw HTML + off → XSS-safe; links open in a new tab); the TUI shows the raw Markdown source. Distinct from + Card (labeled fields) and Quote (citation feed). - [x] **Interactive arrange (Tier 1).** An "Edit layout" toggle lets you drag-reorder cells (a handle; `@dnd-kit` sortable) and drag a cell's right edge to resize its column span (1–6; raw pointer events). It's a **browser-local override** of the declared order/`width`, persisted to diff --git a/package.json b/package.json index 8d66c0e..d13d1d1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "notebook-workspace", "version": "0.0.1", "private": true, - "description": "Monorepo for @modernrelay/notebook — typed lens primitives over an omnigraph fixture or live cluster, rendered to terminal and web from one json-render catalog. The published CLI lives in packages/cli.", + "description": "Monorepo for @modernrelay/notebook — typed lens primitives over server-owned omnigraph catalog queries, rendered to terminal and web from one json-render catalog. The published CLI lives in packages/cli.", "license": "Apache-2.0", "packageManager": "pnpm@10.30.3", "engines": { diff --git a/packages/cli/src/commands/catalog.ts b/packages/cli/src/commands/catalog.ts index e5affd5..9987ed0 100644 --- a/packages/cli/src/commands/catalog.ts +++ b/packages/cli/src/commands/catalog.ts @@ -7,6 +7,8 @@ import { TableAuthorPropsSchema, TimelineAuthorPropsSchema, CardAuthorPropsSchema, + QuoteAuthorPropsSchema, + TextAuthorPropsSchema, } from "@modernrelay/notebook-core"; import type { ZodType } from "zod"; import { z } from "zod"; @@ -22,6 +24,8 @@ const AUTHOR_PROPS: Record = { ActionList: ActionListAuthorPropsSchema, Timeline: TimelineAuthorPropsSchema, Card: CardAuthorPropsSchema, + Quote: QuoteAuthorPropsSchema, + Text: TextAuthorPropsSchema, }; /** diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 40e27b8..03f31d6 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -1,6 +1,14 @@ import { parseArgs } from "node:util"; -import { validateNotebookCompatibility } from "@modernrelay/notebook-core"; +import { + validateNotebookCompatibility, + type Cell, + type Notebook, +} from "@modernrelay/notebook-core"; +import type { + ParamDescriptor, + QueriesOutput, +} from "@modernrelay/notebook-client"; import { z } from "zod"; import { SOURCE_OPTIONS, sourceOptionsFrom } from "../args.js"; @@ -13,11 +21,12 @@ interface ValidateResult { } /** - * Parse a notebook and (when a source can be built) capability-check it. Emits a - * structured `{ ok, errors[], warnings? }` with `--json`, or a human summary. - * Exit 0 = valid, 1 = invalid, 2 = usage. + * Parse a notebook, resolve the operator-config-backed source, capability-check + * it, and validate named query refs against the live catalog. Emits a structured + * `{ ok, errors[], warnings? }` with `--json`, or a human summary. Exit 0 = + * valid, 1 = invalid, 2 = usage. */ -export function validateCommand(argv: string[]): number { +export async function validateCommand(argv: string[]): Promise { const { positionals, values } = parseArgs({ args: argv, allowPositionals: true, @@ -40,40 +49,237 @@ export function validateCommand(argv: string[]): number { return 1; } - // Structural parse passed. Capability-check against the source when the - // notebook (or a --server flag) points at an omnigraph-server. + // Structural parse passed. Resolve the same operator-config-backed source + // path as view/render, then capability-check and validate catalog refs. const result: ValidateResult = { ok: true, errors: [] }; const sourceOpts = sourceOptionsFrom(values); - const hasSource = Boolean(loaded.notebook.server || sourceOpts.server); - if (hasSource) { - try { - const { source } = buildSource(loaded, sourceOpts); - const compat = validateNotebookCompatibility( - loaded.notebook, - source.capabilities(), - ); - if (compat.warnings.length > 0) result.warnings = compat.warnings; - if (compat.errors.length > 0) { - result.ok = false; - result.errors = compat.errors.map((message) => ({ + try { + const { source, client } = buildSource(loaded, sourceOpts); + const compat = validateNotebookCompatibility( + loaded.notebook, + source.capabilities(), + ); + if (compat.warnings.length > 0) result.warnings = compat.warnings; + if (compat.errors.length > 0) { + pushErrors( + result, + compat.errors.map((message) => ({ path: "", message, code: "incompatible", - })); - } - } catch (err) { - result.ok = false; - result.errors.push({ + })), + ); + } + + if (loaded.notebook.cells.some((cell) => cell.query?.ref !== undefined)) { + const catalog = await client.queries(); + pushErrors(result, validateCatalogRefs(loaded.notebook, catalog)); + } + } catch (err) { + pushErrors(result, [ + { path: "", message: err instanceof Error ? err.message : String(err), code: "source", - }); - } + }, + ]); } emit(result, asJson); return result.ok ? 0 : 1; } +function pushErrors( + result: ValidateResult, + errors: ValidateResult["errors"], +): void { + if (errors.length === 0) return; + result.ok = false; + result.errors.push(...errors); +} + +function validateCatalogRefs( + notebook: Notebook, + catalog: QueriesOutput, +): ValidateResult["errors"] { + const errors: ValidateResult["errors"] = []; + const queries = new Map(catalog.queries.map((query) => [query.name, query])); + + notebook.cells.forEach((cell, index) => { + const ref = cell.query?.ref; + if (ref === undefined) return; + const entry = queries.get(ref); + const basePath = `cells/${index}/query`; + if (entry === undefined) { + errors.push({ + path: `${basePath}/ref`, + message: `catalog query '${ref}' does not exist or is not exposed`, + code: "unknown_ref", + }); + return; + } + if (entry.mutation) { + errors.push({ + path: `${basePath}/ref`, + message: `catalog ref '${ref}' is a stored mutation; data cells require read queries`, + code: "wrong_query_kind", + }); + } + errors.push(...validateParams(cell, index, entry.params)); + }); + + return errors; +} + +function validateParams( + cell: Cell, + index: number, + descriptors: ParamDescriptor[], +): ValidateResult["errors"] { + const errors: ValidateResult["errors"] = []; + const basePath = `cells/${index}/query/params`; + const params = cell.query?.params ?? {}; + const supplied = new Set(Object.keys(params)); + const byName = new Map(descriptors.map((param) => [param.name, param])); + + for (const [key, value] of Object.entries(params)) { + const param = byName.get(key); + if (param === undefined) { + errors.push({ + path: `${basePath}/${escapeJsonPointer(key)}`, + message: `unknown param '${key}' for catalog query '${cell.query?.ref ?? ""}'`, + code: "unknown_param", + }); + continue; + } + const literal = literalForValidation(value); + if (literal.kind === "dynamic") continue; + const message = validateParamValue(literal.value, param); + if (message !== null) { + errors.push({ + path: `${basePath}/${escapeJsonPointer(key)}`, + message, + code: "invalid_param", + }); + } + } + + for (const param of descriptors) { + if (!param.nullable && !supplied.has(param.name)) { + errors.push({ + path: `${basePath}/${escapeJsonPointer(param.name)}`, + message: `missing required param '${param.name}'`, + code: "missing_param", + }); + } + } + + return errors; +} + +function literalForValidation( + value: unknown, +): { kind: "literal"; value: unknown } | { kind: "dynamic" } { + if (isRecord(value) && "$state" in value) { + return "default" in value + ? { kind: "literal", value: value.default } + : { kind: "dynamic" }; + } + return containsStateExpr(value) + ? { kind: "dynamic" } + : { kind: "literal", value }; +} + +function containsStateExpr(value: unknown): boolean { + if (!value || typeof value !== "object") return false; + if (Array.isArray(value)) return value.some(containsStateExpr); + const record = value as Record; + return "$state" in record || Object.values(record).some(containsStateExpr); +} + +function validateParamValue( + value: unknown, + param: ParamDescriptor, +): string | null { + if (value === null || value === undefined) { + return param.nullable + ? null + : `param '${param.name}' is required and must be ${describeParam(param)}`; + } + switch (param.kind) { + case "string": + case "bigint": + case "date": + case "datetime": + case "blob": + return typeof value === "string" + ? null + : `param '${param.name}' must be ${describeParam(param)}`; + case "bool": + return typeof value === "boolean" + ? null + : `param '${param.name}' must be ${describeParam(param)}`; + case "int": + return typeof value === "number" && Number.isInteger(value) + ? null + : `param '${param.name}' must be ${describeParam(param)}`; + case "float": + return typeof value === "number" && Number.isFinite(value) + ? null + : `param '${param.name}' must be ${describeParam(param)}`; + case "vector": + if ( + !Array.isArray(value) || + !value.every((item) => typeof item === "number") + ) { + return `param '${param.name}' must be ${describeParam(param)}`; + } + if ( + typeof param.vector_dim === "number" && + param.vector_dim > 0 && + value.length !== param.vector_dim + ) { + return `param '${param.name}' must have ${param.vector_dim} vector dimensions`; + } + return null; + case "list": + if (!Array.isArray(value)) { + return `param '${param.name}' must be ${describeParam(param)}`; + } + if (param.item_kind === undefined || param.item_kind === null) return null; + for (const item of value) { + const itemParam: ParamDescriptor = { + ...param, + kind: param.item_kind, + item_kind: undefined, + nullable: false, + }; + const message = validateParamValue(item, itemParam); + if (message !== null) { + return `param '${param.name}' items must be ${param.item_kind}`; + } + } + return null; + } +} + +function describeParam(param: ParamDescriptor): string { + if (param.kind === "list" && param.item_kind) { + return `list<${param.item_kind}>${param.nullable ? " or null" : ""}`; + } + if (param.kind === "vector" && param.vector_dim) { + return `vector[${param.vector_dim}]${param.nullable ? " or null" : ""}`; + } + return `${param.kind}${param.nullable ? " or null" : ""}`; +} + +function escapeJsonPointer(segment: string): string { + return segment.replace(/~/g, "~0").replace(/\//g, "~1"); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + function toParseError(err: unknown): ValidateResult { if (err instanceof z.ZodError) { return { diff --git a/packages/cli/src/source.ts b/packages/cli/src/source.ts index 821f369..a3d819e 100644 --- a/packages/cli/src/source.ts +++ b/packages/cli/src/source.ts @@ -79,6 +79,7 @@ export function resolveConnection( export interface BuiltSource { source: Source; + client: Client; connection: Connection; } @@ -100,5 +101,5 @@ export function buildSource( ...(connection.branch !== undefined ? { branch: connection.branch } : {}), ...(opts.allowRawGq ? { allowRawGq: true } : {}), }); - return { source, connection }; + return { source, client, connection }; } diff --git a/packages/client/src/http.test.ts b/packages/client/src/http.test.ts index 6dd2c58..77ad6e6 100644 --- a/packages/client/src/http.test.ts +++ b/packages/client/src/http.test.ts @@ -89,6 +89,53 @@ describe("Client (SDK-backed facade)", () => { }); }); + it("reshapes /queries catalog entries to snake_case", async () => { + const client = clientWith( + fetchReturning( + jsonResponse({ + queries: [ + { + name: "decisions_by_urgency", + tool_name: "decisions_by_urgency", + mutation: false, + description: "Decisions", + instruction: null, + params: [ + { + name: "status", + kind: "string", + nullable: true, + item_kind: null, + vector_dim: null, + }, + ], + }, + ], + }), + ), + ); + await expect(client.queries()).resolves.toEqual({ + queries: [ + { + name: "decisions_by_urgency", + tool_name: "decisions_by_urgency", + mutation: false, + description: "Decisions", + instruction: null, + params: [ + { + name: "status", + kind: "string", + nullable: true, + item_kind: null, + vector_dim: null, + }, + ], + }, + ], + }); + }); + it("wraps a 401 as OmnigraphHttpError matching the permission classifier", async () => { const client = clientWith( fetchReturning(jsonResponse({ error: "bad token", code: "unauthorized" }, 401)), diff --git a/packages/client/src/http.ts b/packages/client/src/http.ts index ba298da..e9a4352 100644 --- a/packages/client/src/http.ts +++ b/packages/client/src/http.ts @@ -3,7 +3,7 @@ * * The SDK owns the HTTP transport, the OpenAPI-faithful types, and typed * error classes. This `Client` keeps colombo's stable, snake_case surface - * (`query`/`mutate`/`branches`/`healthz` + `OmnigraphHttpError`) so the + * (`query`/`queries`/`mutate`/`branches`/`healthz` + `OmnigraphHttpError`) so the * `ServerSource` adapter, its tests, and the web error-classifier are * unaffected by the SDK swap. It reshapes the SDK's camelCase responses * back to colombo's shapes and re-wraps thrown SDK errors as @@ -17,6 +17,7 @@ import { type QueryInput as SdkQueryInput, type MutationInput as SdkMutationInput, type Read as SdkRead, + type Queries as SdkQueries, } from "@modernrelay/omnigraph"; export interface ClientOptions { @@ -73,6 +74,39 @@ export interface BranchListOutput { branches: string[]; } +export type ParamKind = + | "string" + | "bool" + | "int" + | "bigint" + | "float" + | "date" + | "datetime" + | "blob" + | "vector" + | "list"; + +export interface ParamDescriptor { + name: string; + kind: ParamKind; + nullable: boolean; + item_kind?: ParamKind | null; + vector_dim?: number | null; +} + +export interface QueryCatalogEntry { + name: string; + tool_name: string; + mutation: boolean; + description?: string | null; + instruction?: string | null; + params: ParamDescriptor[]; +} + +export interface QueriesOutput { + queries: QueryCatalogEntry[]; +} + export class OmnigraphHttpError extends Error { constructor( public status: number, @@ -176,6 +210,31 @@ export class Client { } } + async queries(): Promise { + this.requireGraph("/queries"); + try { + const r: SdkQueries = await this.og.queries.list(); + return { + queries: r.queries.map((q) => ({ + name: q.name, + tool_name: q.toolName, + mutation: q.mutation, + ...(q.description !== undefined ? { description: q.description } : {}), + ...(q.instruction !== undefined ? { instruction: q.instruction } : {}), + params: q.params.map((p) => ({ + name: p.name, + kind: p.kind, + nullable: p.nullable, + ...(p.itemKind !== undefined ? { item_kind: p.itemKind } : {}), + ...(p.vectorDim !== undefined ? { vector_dim: p.vectorDim } : {}), + })), + })), + }; + } catch (e) { + throw toHttpError(e, "/queries"); + } + } + async mutate(body: MutateInput, signal?: AbortSignal): Promise { this.requireGraph("/mutate"); try { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index de5a626..2b22359 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -7,6 +7,10 @@ export { type MutateInput, type ChangeOutput, type BranchListOutput, + type ParamDescriptor, + type ParamKind, + type QueriesOutput, + type QueryCatalogEntry, } from "./http.js"; export { diff --git a/packages/core/package.json b/packages/core/package.json index a49a857..a6ae204 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,7 @@ "name": "@modernrelay/notebook-core", "version": "0.0.1", "private": true, - "description": "Notebook engine: Zod YAML schemas + query DSL, the renderer-agnostic component/action catalog, and the capability-aware runtime. The @json-render/core analog — start here.", + "description": "Notebook engine: Zod YAML schemas + ref/rawGq query model, the renderer-agnostic component/action catalog, and the capability-aware runtime. The @json-render/core analog — start here.", "type": "module", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/core/src/catalog/index.test.ts b/packages/core/src/catalog/index.test.ts index 381b193..312caae 100644 --- a/packages/core/src/catalog/index.test.ts +++ b/packages/core/src/catalog/index.test.ts @@ -17,7 +17,7 @@ const fakeResult: QueryResult = { }; describe("lensComponents", () => { - it("registers all ten components (7 lenses + 3 controls)", () => { + it("registers all eleven components (8 lenses + 3 controls)", () => { expect(Object.keys(lensComponents).sort()).toEqual([ "ActionList", "Button", @@ -27,6 +27,7 @@ describe("lensComponents", () => { "Select", "Subgraph", "Table", + "Text", "Timeline", "Toggle", ]); @@ -68,6 +69,19 @@ describe("assembleLensSpec", () => { ); }); + it("builds a Text spec with rows merged in", () => { + const spec = assembleLensSpec( + "t1", + "Text", + { title_column: "from", text_column: "p1" }, + fakeResult, + ); + expect(spec.elements["t1"]?.type).toBe("Text"); + expect((spec.elements["t1"]?.props as { rows: unknown[] }).rows).toEqual( + fakeResult.rows, + ); + }); + it("rejects malformed author props", () => { expect(() => assembleLensSpec("bad", "Table", { columns: [] }, fakeResult), diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts index 4264d9c..95cfa2a 100644 --- a/packages/core/src/catalog/index.ts +++ b/packages/core/src/catalog/index.ts @@ -50,6 +50,13 @@ import { type QuoteAuthorProps, type QuoteRuntimeProps, } from "./lenses/quote.js"; +import { + TextAuthorPropsSchema, + TextRuntimePropsSchema, + TextDescription, + type TextAuthorProps, + type TextRuntimeProps, +} from "./lenses/text.js"; import { ButtonRuntimePropsSchema, ButtonDescription, @@ -70,6 +77,7 @@ export * from "./lenses/action_list.js"; export * from "./lenses/timeline.js"; export * from "./lenses/card.js"; export * from "./lenses/quote.js"; +export * from "./lenses/text.js"; export * from "./lenses/button.js"; export * from "./lenses/toggle.js"; export * from "./lenses/select.js"; @@ -107,6 +115,7 @@ export const lensComponents = { Timeline: { props: TimelineRuntimePropsSchema, description: TimelineDescription }, Card: { props: CardRuntimePropsSchema, description: CardDescription }, Quote: { props: QuoteRuntimePropsSchema, description: QuoteDescription }, + Text: { props: TextRuntimePropsSchema, description: TextDescription }, Button: { props: ButtonRuntimePropsSchema, description: ButtonDescription }, Toggle: { props: ToggleRuntimePropsSchema, description: ToggleDescription }, Select: { props: SelectRuntimePropsSchema, description: SelectDescription }, @@ -253,5 +262,10 @@ function buildRuntimeProps( const runtime: QuoteRuntimeProps = { ...author, rows: result.rows }; return runtime as unknown as Record; } + case "Text": { + const author: TextAuthorProps = TextAuthorPropsSchema.parse(authorProps); + const runtime: TextRuntimeProps = { ...author, rows: result.rows }; + return runtime as unknown as Record; + } } } diff --git a/packages/core/src/catalog/lenses/text.ts b/packages/core/src/catalog/lenses/text.ts new file mode 100644 index 0000000..2be7360 --- /dev/null +++ b/packages/core/src/catalog/lenses/text.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +/** + * Prose card: renders the first result row's text column as **Markdown** — a + * node's definition, notes, or body, read as a formatted block. Pair with a + * single-node query (often bound to a selection via $state). Distinct from Card + * (a labeled key:value field list) and Quote (a one-line citation feed): Text is + * one featured prose block. + */ +export const TextAuthorPropsSchema = z.object({ + /** Column used as a heading above the prose (e.g. the node's name). */ + title_column: z.string().optional(), + /** Column holding the Markdown body (the dominant element). */ + text_column: z.string().optional(), + /** Shown when the query returns no row (e.g. nothing selected yet). */ + empty_text: z.string().optional(), +}); +export type TextAuthorProps = z.infer; + +export const TextRuntimePropsSchema = TextAuthorPropsSchema.extend({ + rows: z.array(z.record(z.string(), z.unknown())), +}); +export type TextRuntimeProps = z.infer; + +export const TextDescription = + "Prose card — renders the first result row's text column as Markdown (a node's definition/notes/body), with an optional title heading. Drive it with a single-node query, often bound to a selection via $state. Distinct from Card (labeled fields) and Quote (citation feed)."; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e188709..d6d2cd7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,6 @@ // @modernrelay/notebook-core — the notebook engine and front door. // Three internal modules, one public surface: -// spec/ Zod schemas + YAML parser for the notebook wire format + query DSL +// spec/ Zod schemas + YAML parser for the notebook wire format + ref/rawGq query model // catalog/ renderer-agnostic component/action definitions + spec assembler // runtime/ capability-aware execution, state, dependency invalidation, mutations export * from "./spec/index.js"; diff --git a/packages/core/src/spec/index.ts b/packages/core/src/spec/index.ts index 899567a..696bb0a 100644 --- a/packages/core/src/spec/index.ts +++ b/packages/core/src/spec/index.ts @@ -10,6 +10,7 @@ export const LensKind = z.enum([ "Timeline", "Card", "Quote", + "Text", ]); export type LensKind = z.infer; @@ -26,6 +27,7 @@ export const ComponentKind = z.enum([ "Timeline", "Card", "Quote", + "Text", "Button", "Toggle", "Select", diff --git a/packages/tui/src/components/Text.tsx b/packages/tui/src/components/Text.tsx new file mode 100644 index 0000000..43dd9a0 --- /dev/null +++ b/packages/tui/src/components/Text.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Box, Text as InkText } from "ink"; +import type { TextRuntimeProps } from "@modernrelay/notebook-core"; + +interface ComponentCtx

{ + props: P; +} + +function asText(v: unknown): string { + if (v === null || v === undefined) return ""; + return typeof v === "object" ? JSON.stringify(v) : String(v); +} + +// Degraded fallback: the terminal renders the raw Markdown source (still +// readable) — no Markdown formatting in Ink. +export function Text({ + props: p, +}: ComponentCtx): React.ReactElement { + const row = p.rows[0]; + const body = row ? asText(row[p.text_column ?? ""]) : ""; + if (!row || body.trim() === "") { + return ( + + {p.empty_text ?? "(no text)"} + + ); + } + const title = p.title_column ? asText(row[p.title_column]) : ""; + return ( + + {title && {title}} + {body} + + ); +} diff --git a/packages/tui/src/registry.ts b/packages/tui/src/registry.ts index 24eb914..d29fc44 100644 --- a/packages/tui/src/registry.ts +++ b/packages/tui/src/registry.ts @@ -8,6 +8,7 @@ import { ActionList } from "./components/ActionList.js"; import { Timeline } from "./components/Timeline.js"; import { Card } from "./components/Card.js"; import { Quote } from "./components/Quote.js"; +import { Text } from "./components/Text.js"; import { Button } from "./components/Button.js"; import { Toggle } from "./components/Toggle.js"; import { Select } from "./components/Select.js"; @@ -18,7 +19,7 @@ const catalog = defineCatalog(schema, { }); const { registry: inkRegistry } = defineRegistry(catalog, { - components: { Table, Path, Subgraph, ActionList, Timeline, Card, Quote, Button, Toggle, Select }, + components: { Table, Path, Subgraph, ActionList, Timeline, Card, Quote, Text, Button, Toggle, Select }, actions: { setState: async (params, setState) => { const { statePath, value } = params as { statePath: string; value: unknown }; diff --git a/packages/web/package.json b/packages/web/package.json index 24b248a..3fa4f68 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -28,6 +28,7 @@ "lucide-react": "^1.20.0", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-markdown": "^10.1.0", "tailwind-merge": "^3.6.0" }, "devDependencies": { diff --git a/packages/web/src/App.test.ts b/packages/web/src/App.test.ts index 7f4b423..04b402d 100644 --- a/packages/web/src/App.test.ts +++ b/packages/web/src/App.test.ts @@ -23,6 +23,14 @@ describe("buildConfig", () => { expect(config.source.capabilities().rawGq).toBe(true); }); + it("does not reuse a browser token for the same-origin /og proxy", async () => { + const storage = stubWindow( + "http://127.0.0.1:5173/?server=http://127.0.0.1:5173/og&token=tok&graph=acme", + ); + await buildConfig(); + expect(storage.has("omnigraph_token")).toBe(false); + }); + it("loads a notebook from the ?notebook= URL", async () => { stubWindow("http://127.0.0.1:5173/?notebook=/dash/notebook.yaml&graph=acme"); const fetch = vi.fn(async (input: URL | string) => { diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 6f88ac4..0769b8d 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -450,7 +450,9 @@ function CellCard({

)} + {/* Cap long content and scroll it inside the card (header stays fixed); + overscroll-contain keeps the page from scrolling at the card's end. */}
{cell.error !== null && ( diff --git a/packages/web/src/components/Text.tsx b/packages/web/src/components/Text.tsx new file mode 100644 index 0000000..01ac6f1 --- /dev/null +++ b/packages/web/src/components/Text.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import Markdown from "react-markdown"; +import type { TextRuntimeProps } from "@modernrelay/notebook-core"; + +interface ComponentCtx

{ + props: P; +} + +function asText(v: unknown): string { + if (v === null || v === undefined) return ""; + return typeof v === "object" ? JSON.stringify(v) : String(v); +} + +// Prose typography via Tailwind child-selectors (no typography plugin). Tailwind +// emits these literal arbitrary variants; the markdown output inherits them. +const PROSE = + "max-w-prose break-words text-sm leading-relaxed text-foreground " + + "[&_p]:my-2 [&_a]:text-primary [&_a]:underline " + + "[&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:text-lg [&_h1]:font-semibold " + + "[&_h2]:mt-4 [&_h2]:mb-2 [&_h2]:text-base [&_h2]:font-semibold " + + "[&_h3]:mt-3 [&_h3]:mb-1.5 [&_h3]:font-semibold " + + "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 " + + "[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs " + + "[&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-muted [&_pre]:p-3 [&_pre_code]:bg-transparent [&_pre_code]:p-0 " + + "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground"; + +export function Text({ + props: p, +}: ComponentCtx): React.ReactElement { + const row = p.rows[0]; + const body = row ? asText(row[p.text_column ?? ""]) : ""; + if (!row || body.trim() === "") { + return ( +

+ {p.empty_text ?? "(no text)"} +

+ ); + } + const title = p.title_column ? asText(row[p.title_column]) : ""; + return ( +
+ {title && ( +

{title}

+ )} +
+ {/* No rehype-raw → raw HTML in the text is ignored (XSS-safe). Links + open in a new tab. */} + ; + }, + }} + > + {body} + +
+
+ ); +} diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts index be7c837..3808638 100644 --- a/packages/web/src/config.ts +++ b/packages/web/src/config.ts @@ -10,7 +10,11 @@ export interface AppConfig { label: string; } -function readToken(): string | undefined { +function readToken(server: string): string | undefined { + // In BFF/proxy mode the browser must hold no default graph token; the Node + // proxy owns auth injection and strips any browser-supplied Authorization. + if (isSameOriginOgProxy(server)) return undefined; + // Only an explicit `?token=` (persisted for direct, non-proxy server mode). // No default token: through the `view` BFF the proxy injects the server-side // token and strips any client-supplied Authorization, so the browser holds @@ -62,7 +66,7 @@ export async function buildConfig(): Promise { } const client = new Client({ baseUrl: server, - token: readToken(), + token: readToken(server), graphId: graph, }); return { @@ -80,6 +84,19 @@ function isTruthyParam(v: string | null): boolean { return v !== null && v !== "" && v !== "0" && v !== "false"; } +function isSameOriginOgProxy(server: string): boolean { + try { + const url = new URL(server); + const path = url.pathname.replace(/\/$/, ""); + return ( + url.origin === window.location.origin && + (path === "/og" || path.startsWith("/og/")) + ); + } catch { + return false; + } +} + async function fetchText(url: URL): Promise { const res = await fetch(url); if (!res.ok) throw new Error(`${url.toString()} returned ${res.status}`); diff --git a/packages/web/src/registry.ts b/packages/web/src/registry.ts index d60cf68..874dbaf 100644 --- a/packages/web/src/registry.ts +++ b/packages/web/src/registry.ts @@ -9,6 +9,7 @@ import { ActionList } from "./components/ActionList.js"; import { Timeline } from "./components/Timeline.js"; import { Card } from "./components/Card.js"; import { Quote } from "./components/Quote.js"; +import { Text } from "./components/Text.js"; import { Button } from "./components/Button.js"; import { Toggle } from "./components/Toggle.js"; import { Select } from "./components/Select.js"; @@ -21,7 +22,7 @@ const catalog = defineCatalog(schema, { // React's SetState is `(updater: (prev) => next) => void`. We use the // executor's setAtPointer helper to write at JSON-pointer paths immutably. const { registry: webRegistry } = defineRegistry(catalog, { - components: { Table, Path, Subgraph, ActionList, Timeline, Card, Quote, Button, Toggle, Select }, + components: { Table, Path, Subgraph, ActionList, Timeline, Card, Quote, Text, Button, Toggle, Select }, actions: { setState: async (params, setState) => { const { statePath, value } = params as { diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index e4bb8a9..5584c45 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -29,18 +29,18 @@ export default defineConfig({ target: process.env.OMNIGRAPH_PROXY_TARGET ?? "http://127.0.0.1:8080", changeOrigin: true, rewrite: (p) => p.replace(/^\/og/, ""), - ...(process.env.OMNIGRAPH_PROXY_TOKEN - ? { - configure: (proxy) => { - proxy.on("proxyReq", (proxyReq) => { - proxyReq.setHeader( - "authorization", - `Bearer ${process.env.OMNIGRAPH_PROXY_TOKEN}`, - ); - }); - }, + configure: (proxy) => { + proxy.on("proxyReq", (proxyReq) => { + proxyReq.removeHeader("authorization"); + proxyReq.removeHeader("proxy-authorization"); + if (process.env.OMNIGRAPH_PROXY_TOKEN) { + proxyReq.setHeader( + "authorization", + `Bearer ${process.env.OMNIGRAPH_PROXY_TOKEN}`, + ); } - : {}), + }); + }, }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 668fc40..4ad86f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,6 +174,9 @@ importers: react-dom: specifier: ^19.2.6 version: 19.2.6(react@19.2.6) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.14)(react@19.2.6) tailwind-merge: specifier: ^3.6.0 version: 3.6.0 @@ -835,12 +838,27 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@25.6.2': resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} @@ -852,6 +870,15 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -922,6 +949,9 @@ packages: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -932,6 +962,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -940,6 +973,18 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -967,6 +1012,9 @@ packages: resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -997,10 +1045,20 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1027,6 +1085,9 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1034,6 +1095,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1058,6 +1122,15 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} @@ -1084,15 +1157,34 @@ packages: react-devtools-core: optional: true + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ci@2.0.0: resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} engines: {node: '>=20'} hasBin: true + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1186,6 +1278,9 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lucide-react@1.20.0: resolution: {integrity: sha512-jhXLeC/7m0/tjL1nzMdKk6x256zWA6AtbhTVreHOiKPoeX2d6MK4FbyIQPpVq0E6iPWBisyy1TW+pEge/uMEuQ==} peerDependencies: @@ -1199,6 +1294,93 @@ packages: engines: {node: '>= 20'} hasBin: true + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1228,6 +1410,9 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1271,11 +1456,20 @@ packages: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + react-dom@19.2.6: resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: react: ^19.2.6 + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-reconciler@0.33.0: resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} engines: {node: '>=0.10.0'} @@ -1290,6 +1484,12 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + reselect@5.2.0: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} @@ -1332,6 +1532,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -1350,10 +1553,19 @@ packages: resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1406,6 +1618,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -1446,11 +1664,35 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.0.11: resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1571,6 +1813,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alcalzone/ansi-tokenize@0.2.5': @@ -2003,10 +2248,28 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.9': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@25.6.2': dependencies: undici-types: 7.19.2 @@ -2019,6 +2282,12 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.2': {} + '@vitejs/plugin-react@6.0.1(vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.8.4))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 @@ -2081,6 +2350,8 @@ snapshots: auto-bind@5.0.1: {} + bail@2.0.2: {} + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -2088,10 +2359,20 @@ snapshots: cac@6.7.14: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -2117,6 +2398,8 @@ snapshots: dependencies: convert-to-spaces: 2.0.1 + comma-separated-tokens@2.0.3: {} + commander@4.1.1: {} confbox@0.1.8: {} @@ -2133,8 +2416,18 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + emoji-regex@10.6.0: {} enhanced-resolve@5.21.2: @@ -2179,12 +2472,16 @@ snapshots: escape-string-regexp@2.0.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 expect-type@1.3.0: {} + extend@3.0.2: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -2202,6 +2499,32 @@ snapshots: graceful-fs@4.2.11: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-url-attributes@3.0.1: {} + indent-string@5.0.0: {} ink-testing-library@4.0.0(@types/react@19.2.14): @@ -2242,12 +2565,27 @@ snapshots: - bufferutil - utf-8-validate + inline-style-parser@0.2.7: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.6.0 + is-hexadecimal@2.0.1: {} + is-in-ci@2.0.0: {} + is-plain-obj@4.1.0: {} + jiti@2.7.0: {} joycon@3.1.1: {} @@ -2307,6 +2645,8 @@ snapshots: load-tsconfig@0.2.5: {} + longest-streak@3.1.0: {} + lucide-react@1.20.0(react@19.2.6): dependencies: react: 19.2.6 @@ -2317,6 +2657,228 @@ snapshots: marked@17.0.6: {} + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.2 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + mimic-fn@2.1.0: {} mlly@1.8.2: @@ -2344,6 +2906,16 @@ snapshots: dependencies: mimic-fn: 2.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + patch-console@2.0.0: {} pathe@2.0.3: {} @@ -2374,11 +2946,31 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + property-information@7.2.0: {} + react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 scheduler: 0.27.0 + react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.6): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.6 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-reconciler@0.33.0(react@19.2.6): dependencies: react: 19.2.6 @@ -2388,6 +2980,23 @@ snapshots: readdirp@4.1.2: {} + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + reselect@5.2.0: {} resolve-from@5.0.0: {} @@ -2464,6 +3073,8 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -2483,10 +3094,23 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -2530,6 +3154,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-interface-checker@0.1.13: {} tslib@2.8.1: {} @@ -2572,10 +3200,53 @@ snapshots: undici-types@7.19.2: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + use-sync-external-store@1.6.0(react@19.2.6): dependencies: react: 19.2.6 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@8.0.11(@types/node@25.6.2)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.8.4): dependencies: lightningcss: 1.32.0 @@ -2639,3 +3310,5 @@ snapshots: yoga-layout@3.2.1: {} zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/scripts/server-demo.sh b/scripts/server-demo.sh index ad6aa3a..050275f 100755 --- a/scripts/server-demo.sh +++ b/scripts/server-demo.sh @@ -17,8 +17,8 @@ # After this script is done: # pnpm tui examples/company-server.notebook.yaml # TUI (direct, no CORS) # -# pnpm --filter @omnigraph/web dev # web, then open: -# http://127.0.0.1:5173/?mode=server&server=/og # via the Vite /og proxy +# pnpm --filter @modernrelay/notebook-web dev # web, then open: +# http://127.0.0.1:5173/?server=/og&graph=company # via the Vite /og proxy # (omnigraph-server 0.7.0 no longer sets CORS headers, so the browser must # talk same-origin; the dev proxy in vite.config.ts forwards /og → :8080.) # @@ -116,8 +116,8 @@ Run the TUI: pnpm tui examples/company-server.notebook.yaml Run the web app (same-origin via the Vite /og proxy): - pnpm --filter @omnigraph/web dev - open 'http://127.0.0.1:5173/?mode=server&server=/og' + pnpm --filter @modernrelay/notebook-web dev + open 'http://127.0.0.1:5173/?server=/og&graph=${GRAPH_ID}' Probe the persisted clause status (after pressing Approve in the UI): curl -s -H "Content-Type: application/json" \\ From 41c24d81a670fb8751852d4a0667a0da442f5d4e Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 25 Jun 2026 17:17:34 +0300 Subject: [PATCH 2/3] validate: clearer message when the catalog server is unreachable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the live-catalog fetch in its own try/catch so a network/server-down failure reports `catalog_unreachable` ("catalog validation requires a reachable server: …") rather than a generic `source` error — config/source errors stay distinct. Cosmetic; behavior unchanged (still exits 1). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/src/commands/validate.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 03f31d6..c2b6242 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -72,8 +72,22 @@ export async function validateCommand(argv: string[]): Promise { } if (loaded.notebook.cells.some((cell) => cell.query?.ref !== undefined)) { - const catalog = await client.queries(); - pushErrors(result, validateCatalogRefs(loaded.notebook, catalog)); + try { + const catalog = await client.queries(); + pushErrors(result, validateCatalogRefs(loaded.notebook, catalog)); + } catch (err) { + // Distinguish "server unreachable" from a config/source error so the + // message says what's actually needed (catalog refs need a live server). + pushErrors(result, [ + { + path: "", + message: `catalog validation requires a reachable server: ${ + err instanceof Error ? err.message : String(err) + }`, + code: "catalog_unreachable", + }, + ]); + } } } catch (err) { pushErrors(result, [ From 2d5eb539134f9c7ec64fc1003369de13aa37acac Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 25 Jun 2026 17:42:59 +0300 Subject: [PATCH 3/3] Address Greptile P1s on PR #8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Text/Quote `text_column` is now required (z.string().min(1)) — a Text/Quote cell with rows but no text column was silently rendering the empty fallback instead of failing validation. (#3) - validate: warn when a *required* (non-nullable) catalog param is bound to $state without a default — it resolves at runtime, which validate can't see, so flag it rather than passing silently. (#2) - validate: document that the server-bound checks are intentional — structural parse already runs offline above; the meaningful ref/param validation needs a resolvable source by design. (#1, accepted-by-design) Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/src/commands/validate.ts | 39 +++++++++++++++++------ packages/core/src/catalog/lenses/quote.ts | 4 +-- packages/core/src/catalog/lenses/text.ts | 4 +-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index c2b6242..9b33796 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -49,8 +49,12 @@ export async function validateCommand(argv: string[]): Promise { return 1; } - // Structural parse passed. Resolve the same operator-config-backed source - // path as view/render, then capability-check and validate catalog refs. + // Structural parse passed (errors already returned above, offline). The + // remaining checks are intentionally server-bound: a dash-book's value is that + // its cells resolve against the server-owned catalog, so `validate` resolves + // the same operator-config source as view/render, capability-checks, and + // validates catalog refs/params live. A notebook with no resolvable source + // fails here by design — the meaningful validation can't run without one. const result: ValidateResult = { ok: true, errors: [] }; const sourceOpts = sourceOptionsFrom(values); try { @@ -74,7 +78,11 @@ export async function validateCommand(argv: string[]): Promise { if (loaded.notebook.cells.some((cell) => cell.query?.ref !== undefined)) { try { const catalog = await client.queries(); - pushErrors(result, validateCatalogRefs(loaded.notebook, catalog)); + const { errors, warnings } = validateCatalogRefs(loaded.notebook, catalog); + pushErrors(result, errors); + if (warnings.length > 0) { + result.warnings = [...(result.warnings ?? []), ...warnings]; + } } catch (err) { // Distinguish "server unreachable" from a config/source error so the // message says what's actually needed (catalog refs need a live server). @@ -114,8 +122,9 @@ function pushErrors( function validateCatalogRefs( notebook: Notebook, catalog: QueriesOutput, -): ValidateResult["errors"] { +): { errors: ValidateResult["errors"]; warnings: string[] } { const errors: ValidateResult["errors"] = []; + const warnings: string[] = []; const queries = new Map(catalog.queries.map((query) => [query.name, query])); notebook.cells.forEach((cell, index) => { @@ -138,18 +147,21 @@ function validateCatalogRefs( code: "wrong_query_kind", }); } - errors.push(...validateParams(cell, index, entry.params)); + const params = validateParams(cell, index, entry.params); + errors.push(...params.errors); + warnings.push(...params.warnings); }); - return errors; + return { errors, warnings }; } function validateParams( cell: Cell, index: number, descriptors: ParamDescriptor[], -): ValidateResult["errors"] { +): { errors: ValidateResult["errors"]; warnings: string[] } { const errors: ValidateResult["errors"] = []; + const warnings: string[] = []; const basePath = `cells/${index}/query/params`; const params = cell.query?.params ?? {}; const supplied = new Set(Object.keys(params)); @@ -166,7 +178,16 @@ function validateParams( continue; } const literal = literalForValidation(value); - if (literal.kind === "dynamic") continue; + if (literal.kind === "dynamic") { + // Resolves from $state at runtime; validate can't see runtime state. Warn + // (don't fail) when a *required* param is bound this way without a default. + if (!param.nullable) { + warnings.push( + `param '${key}' for catalog query '${cell.query?.ref ?? ""}' is required and resolves from $state at runtime; validate can't confirm it will be set`, + ); + } + continue; + } const message = validateParamValue(literal.value, param); if (message !== null) { errors.push({ @@ -187,7 +208,7 @@ function validateParams( } } - return errors; + return { errors, warnings }; } function literalForValidation( diff --git a/packages/core/src/catalog/lenses/quote.ts b/packages/core/src/catalog/lenses/quote.ts index 356acb8..925c2af 100644 --- a/packages/core/src/catalog/lenses/quote.ts +++ b/packages/core/src/catalog/lenses/quote.ts @@ -8,8 +8,8 @@ import { z } from "zod"; * the source is a small caption. */ export const QuoteAuthorPropsSchema = z.object({ - /** Column holding the quotation text (the dominant element). */ - text_column: z.string().optional(), + /** Column holding the quotation text — required (the dominant element). */ + text_column: z.string().min(1), /** Column holding the source citation (e.g. an artifact title). */ source_column: z.string().optional(), /** diff --git a/packages/core/src/catalog/lenses/text.ts b/packages/core/src/catalog/lenses/text.ts index 2be7360..9d5ff09 100644 --- a/packages/core/src/catalog/lenses/text.ts +++ b/packages/core/src/catalog/lenses/text.ts @@ -10,8 +10,8 @@ import { z } from "zod"; export const TextAuthorPropsSchema = z.object({ /** Column used as a heading above the prose (e.g. the node's name). */ title_column: z.string().optional(), - /** Column holding the Markdown body (the dominant element). */ - text_column: z.string().optional(), + /** Column holding the Markdown body — required (a Text card's reason to exist). */ + text_column: z.string().min(1), /** Shown when the query returns no row (e.g. nothing selected yet). */ empty_text: z.string().optional(), });