diff --git a/.changeset/agent-readable-docs.md b/.changeset/agent-readable-docs.md new file mode 100644 index 0000000..eaddf65 --- /dev/null +++ b/.changeset/agent-readable-docs.md @@ -0,0 +1,6 @@ +--- +"@triplex-build/triplex": patch +--- + +Describe Triplex as the database that remembers why, and link the agent modeling guide and the +`llms.txt` documentation index from the package README. diff --git a/README.md b/README.md index 68a940d..8b53d73 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,14 @@ > FoundationDB are experimental. See [Current state](docs/current-state.md) for the exact maturity > contract. -An Effect-native fact database for applications that have to explain themselves. +**The database that remembers why.** Triplex is an embedded fact database for TypeScript +back-office systems, built on Effect. Every write records who made it, which versioned rules +governed it, and when it was true, so audit questions and open work become queries instead of +investigations. [Documentation](https://triplex.build) · +[Modeling guide for agents](https://triplex.build/agents) · +[`llms.txt`](https://triplex.build/llms.txt) · [GitHub](https://github.com/bjacobso/triplex) · [Security](SECURITY.md) @@ -572,6 +577,7 @@ Public exports resolve only to built `dist` files. | Document | Purpose | | ------------------------------------------------------------- | ------------------------------------------------------------ | | [Getting started](docs/getting-started.md) | First runnable program, output, and persistence | +| [Modeling a back-office domain](docs/agents.md) | Prescriptive guide for agents: mapping, rules, anti-patterns | | [Playground](docs/playground.md) | Local in-browser database with selectable domains | | [Core concepts](docs/concepts.md) | Facts, time, configuration, derivation, and ownership | | [Configuration walkthrough](docs/configuration-versioning.md) | Publish, promote, pin, inspect, and roll back config | diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index d56e6ae..d96b3f8 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,22 +1,79 @@ -import { dirname, resolve } from "node:path"; +import { resolve } from "node:path"; -import { transformerTwoslash } from "@shikijs/vitepress-twoslash"; import tailwindcss from "@tailwindcss/vite"; -import ts from "typescript"; -import { defineConfig } from "vitepress"; +import { type DefaultTheme, defineConfig } from "vitepress"; import { workspaceAliases } from "../../vitest.workspace-aliases.js"; +import { llmsDevServer, writeLlmsFiles } from "./llms.js"; + +const srcDir = resolve(import.meta.dirname, ".."); + +const sidebar: DefaultTheme.SidebarItem[] = [ + { + text: "Start", + items: [ + { text: "Getting started", link: "/getting-started" }, + { text: "Modeling a back-office domain", link: "/agents" }, + { text: "Playground", link: "/playground" }, + { text: "Core concepts", link: "/concepts" }, + { text: "Current state", link: "/current-state" }, + ], + }, + { + text: "Learn", + items: [ + { text: "Datalog", link: "/datalog" }, + { text: "Configuration walkthrough", link: "/configuration-versioning" }, + { text: "Derivations", link: "/derivations" }, + { text: "Provenance", link: "/provenance" }, + ], + }, + { + text: "Reference", + items: [ + { text: "Configuration", link: "/configuration" }, + { text: "Operational primitives", link: "/operational-primitives" }, + { text: "Configuration HTTP API", link: "/http-api" }, + { text: "Performance", link: "/performance" }, + { + text: "Architecture", + link: "https://github.com/bjacobso/triplex/blob/main/ARCHITECTURE.md", + }, + ], + }, + { + text: "Operate", + items: [ + { text: "CLI and dashboard", link: "/tools" }, + { text: "Data explorer", link: "/explorer" }, + { text: "Host integration", link: "/host-integration" }, + { text: "Troubleshooting and FAQ", link: "/troubleshooting" }, + { text: "Releasing", link: "/releasing" }, + ], + }, + { + text: "Project", + items: [ + { text: "Custom runtimes", link: "/custom-runtimes" }, + { text: "Roadmap", link: "/roadmap" }, + ], + }, +]; export default defineConfig({ lang: "en-US", title: "Triplex", titleTemplate: ":title · Triplex", - description: "A temporal fact database for TypeScript, built on Effect.", + description: + "The database that remembers why: an embedded fact database for TypeScript back-office systems, built on Effect.", cleanUrls: true, + // Snippet outputs are included into pages, never published as pages themselves. + srcExclude: ["snippets/**"], lastUpdated: true, outDir: resolve(import.meta.dirname, "../../dist"), + buildEnd: (site) => writeLlmsFiles(srcDir, site.outDir, sidebar), vite: { - plugins: [tailwindcss()], + plugins: [tailwindcss(), llmsDevServer(srcDir, sidebar)], resolve: { alias: workspaceAliases() }, }, head: [ @@ -24,43 +81,24 @@ export default defineConfig({ ["meta", { name: "theme-color", content: "#0b1020" }], ["meta", { property: "og:type", content: "website" }], ["meta", { property: "og:site_name", content: "Triplex" }], + // Machine-readable entry points for coding agents: https://llmstxt.org + ["link", { rel: "alternate", type: "text/plain", href: "/llms.txt", title: "llms.txt" }], + [ + "link", + { rel: "alternate", type: "text/plain", href: "/llms-full.txt", title: "llms-full.txt" }, + ], ], markdown: { theme: { light: "tokyo-night", dark: "tokyo-night" }, languages: ["js", "json", "sh", "sql", "ts"], - codeTransformers: [ - transformerTwoslash({ - throws: true, - twoslashOptions: { - tsModule: ts, - tsLibDirectory: dirname(ts.getDefaultLibFilePath({})), - vfsRoot: resolve(import.meta.dirname, "../snippets/home"), - cache: process.env.NODE_ENV === "production", - fsCache: process.env.NODE_ENV === "production", - compilerOptions: { - target: ts.ScriptTarget.ES2024, - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Bundler, - allowImportingTsExtensions: true, - noEmit: true, - strict: true, - baseUrl: resolve(import.meta.dirname, "../.."), - paths: { - "@triplex-build/triplex": ["packages/core/src/index.ts"], - "@triplex-build/triplex/config": ["packages/core/src/config/index.ts"], - }, - }, - }, - }), - ], }, themeConfig: { logo: { src: "/mark.svg", alt: "Triplex" }, siteTitle: "Triplex", nav: [ { text: "Get started", link: "/getting-started" }, + { text: "Modeling guide", link: "/agents" }, { text: "Playground", link: "/playground" }, - { text: "Explorer", link: "/explorer" }, { text: "Concepts", link: "/concepts" }, { text: "Reference", @@ -73,55 +111,7 @@ export default defineConfig({ }, { text: "Operate", link: "/tools" }, ], - sidebar: [ - { - text: "Start", - items: [ - { text: "Getting started", link: "/getting-started" }, - { text: "Playground", link: "/playground" }, - { text: "Core concepts", link: "/concepts" }, - { text: "Current state", link: "/current-state" }, - ], - }, - { - text: "Learn", - items: [ - { text: "Datalog", link: "/datalog" }, - { text: "Configuration walkthrough", link: "/configuration-versioning" }, - { text: "Derivations", link: "/derivations" }, - { text: "Provenance", link: "/provenance" }, - ], - }, - { - text: "Reference", - items: [ - { text: "Configuration", link: "/configuration" }, - { text: "Operational primitives", link: "/operational-primitives" }, - { text: "Configuration HTTP API", link: "/http-api" }, - { text: "Performance", link: "/performance" }, - { - text: "Architecture", - link: "https://github.com/bjacobso/triplex/blob/main/ARCHITECTURE.md", - }, - ], - }, - { - text: "Operate", - items: [ - { text: "CLI and dashboard", link: "/tools" }, - { text: "Host integration", link: "/host-integration" }, - { text: "Troubleshooting and FAQ", link: "/troubleshooting" }, - { text: "Releasing", link: "/releasing" }, - ], - }, - { - text: "Project", - items: [ - { text: "Custom runtimes", link: "/custom-runtimes" }, - { text: "Roadmap", link: "/roadmap" }, - ], - }, - ], + sidebar, search: { provider: "local", options: { detailedView: true }, diff --git a/docs/.vitepress/llms.ts b/docs/.vitepress/llms.ts new file mode 100644 index 0000000..0cf73c5 --- /dev/null +++ b/docs/.vitepress/llms.ts @@ -0,0 +1,264 @@ +/** + * Machine-readable documentation for coding agents (https://llmstxt.org). + * + * - `/llms.txt` indexes every guide in sidebar order with a one-line summary. + * - `/llms-full.txt` concatenates every guide into one Markdown document. + * - `/.md` serves each page as plain Markdown, with code includes resolved. + * + * The same renderer backs the production build and the dev server, so links on + * the homepage work in both. + */ +import { readFile, readdir, writeFile } from "node:fs/promises"; +import { dirname, extname, join, resolve } from "node:path"; + +import type { IncomingMessage, ServerResponse } from "node:http"; + +import type { DefaultTheme } from "vitepress"; + +export const siteUrl = "https://triplex.build"; + +const summary = + "The database that remembers why: an embedded fact database for TypeScript back-office " + + "systems. Every write records who made it, which versioned rules governed it, and when it was " + + "true, so audit questions and open work become queries."; + +const keyFacts = `Install with \`npm install @triplex-build/triplex effect@4.0.0-rc.112\`. Triplex is pre-1.0, +requires Effect 4 (\`effect@4.0.0-rc.112\`; Effect 3 is not compatible), is ESM-only, and targets +Node.js 22+ plus modern browsers and edge runtimes for the core. In-memory and SQLite storage are +supported, PostgreSQL is a production candidate, and Cloudflare and FoundationDB are experimental. + +Coding agents modeling a back-office domain should read the modeling guide first. Every page is +available as Markdown by appending \`.md\` to its URL. Code on the homepage and in the modeling +guide is type-checked and executed in CI.`; + +interface Page { + readonly route: string; + readonly file: string; + readonly title: string; + readonly description: string; + readonly markdown: string; +} + +const routeOf = (file: string) => { + const route = `/${file.replace(/\.md$/, "")}`; + return route === "/index" ? "/" : route; +}; + +const markdownUrl = (route: string) => `${siteUrl}${route === "/" ? "/index" : route}.md`; + +const frontmatter = (source: string) => { + const match = /^---\n([\s\S]*?)\n---\n/.exec(source); + if (!match) return { data: {} as Record, body: source }; + const data: Record = {}; + for (const line of (match[1] ?? "").split("\n")) { + const field = /^([a-zA-Z]+):\s*(.*)$/.exec(line); + if (field?.[1] !== undefined) data[field[1]] = (field[2] ?? "").replace(/^["']|["']$/g, ""); + } + return { data, body: source.slice(match[0].length) }; +}; + +const languageOf = (path: string) => { + const extension = extname(path).slice(1); + return extension === "mts" || extension === "cts" ? "ts" : extension; +}; + +const regionOf = (source: string, region: string | undefined) => { + const lines = source.split("\n"); + const marker = /^\s*\/\/ #(?:end)?region\b/; + if (region === undefined) return lines.filter((line) => !marker.test(line)).join("\n"); + const start = lines.findIndex((line) => line.trim() === `// #region ${region}`); + const end = lines.findIndex((line) => line.trim() === `// #endregion ${region}`); + if (start === -1 || end === -1) throw new Error(`Missing snippet region "${region}"`); + return lines + .slice(start + 1, end) + .filter((line) => !marker.test(line)) + .join("\n"); +}; + +/** Converts the small, controlled HTML vocabulary used by the homepage into Markdown. */ +const htmlToMarkdown = (html: string) => + html + .replace(/]*>([\s\S]*?)<\/h\1>/g, (_, level: string, text: string) => { + return `\n${"#".repeat(Number(level))} ${text.trim()}\n`; + }) + .replace(/
  • ([\s\S]*?)<\/li>/g, "- $1") + .replace(/]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g, "[$2]($1)") + .replace(/([\s\S]*?)<\/code>/g, "`$1`") + .replace(/([\s\S]*?)<\/strong>/g, "**$1**") + .replace(/([\s\S]*?)<\/em>/g, "*$1*") + .replace(/]*>([\s\S]*?)<\/p>/g, "\n$1\n") + .replace(//g, "; ") + .replace(/]*><\/span>/g, "") + .replace(/<\/?[a-zA-Z][^>]*>/g, "") + .replace(/&/g, "&") + .replace(/\n{3,}/g, "\n\n"); + +const renderMarkdown = async (srcDir: string, file: string, body: string) => { + const path = join(srcDir, file); + let markdown = body; + + // `` + for (const match of markdown.matchAll(//g)) { + const included = await readFile(resolve(dirname(path), match[1] ?? ""), "utf8"); + markdown = markdown.replace(match[0], () => included.trim()); + } + + // `<<< @/snippets/file.ts#region` code imports, where `@` is the docs source root. + for (const match of markdown.matchAll(/^<<< @\/([^\s#{]+)(?:#([\w-]+))?.*$/gm)) { + const snippetPath = join(srcDir, match[1] ?? ""); + const code = regionOf(await readFile(snippetPath, "utf8"), match[2]).trim(); + markdown = markdown.replace( + match[0], + () => `\`\`\`${languageOf(snippetPath)}\n${code}\n\`\`\``, + ); + } + + markdown = markdown.replace(/^```(ts|typescript)\s+check\s*$/gm, "```$1"); + if (/<(div|section|header|article|aside|nav)\b/.test(markdown)) { + markdown = htmlToMarkdown(markdown); + } + // VitePress custom containers are not standard Markdown. + markdown = markdown.replace(/^::: ?(\w+)(.*)$/gm, (_, kind: string, title: string) => { + return `> **${(title.trim() || kind).replace(/^\w/, (c) => c.toUpperCase())}**`; + }); + markdown = markdown.replace(/^:::$/gm, ""); + return `${markdown.trim()}\n`; +}; + +const firstParagraph = (markdown: string) => { + const paragraphs = markdown + .replace(/^#.*$/gm, "") + .split(/\n\s*\n/) + .map((paragraph) => paragraph.replace(/\s+/g, " ").trim()) + .filter((paragraph) => /^[A-Z`*[]/.test(paragraph) && !paragraph.startsWith("```")); + const text = paragraphs[0] ?? ""; + const sentence = /^(.+?[.!?])(\s|$)/.exec(text)?.[1] ?? text; + return sentence.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); +}; + +const loadPages = async (srcDir: string): Promise => { + const files = (await readdir(srcDir)).filter((file) => file.endsWith(".md")).sort(); + return Promise.all( + files.map(async (file) => { + const { data, body } = frontmatter(await readFile(join(srcDir, file), "utf8")); + const markdown = await renderMarkdown(srcDir, file, body); + const heading = /^# (.+)$/m.exec(markdown)?.[1]?.trim(); + return { + route: routeOf(file), + file, + title: heading ?? data["title"] ?? file, + description: data["description"] ?? firstParagraph(markdown), + markdown, + }; + }), + ); +}; + +const sidebarGroups = (sidebar: DefaultTheme.SidebarItem[]) => + sidebar.map((group) => ({ + text: group.text ?? "Guides", + routes: (group.items ?? []) + .map((item) => item.link) + .filter((link): link is string => link !== undefined && link.startsWith("/")), + })); + +export const renderLlmsFiles = async ( + srcDir: string, + sidebar: DefaultTheme.SidebarItem[], +): Promise> => { + const pages = await loadPages(srcDir); + const byRoute = new Map(pages.map((page) => [page.route, page])); + const groups = sidebarGroups(sidebar); + const listed = new Set(groups.flatMap((group) => group.routes)); + const unlisted = pages.filter((page) => page.route !== "/" && !listed.has(page.route)); + + const entry = (page: Page) => + `- [${page.title}](${markdownUrl(page.route)})${page.description ? `: ${page.description}` : ""}`; + + const index = [`# Triplex`, "", `> ${summary}`, "", keyFacts, ""]; + const home = byRoute.get("/"); + if (home) index.push("## Overview", "", entry({ ...home, title: "Homepage" }), ""); + for (const group of groups) { + const entries = group.routes.flatMap((route) => { + const page = byRoute.get(route); + return page ? [entry(page)] : []; + }); + if (entries.length > 0) index.push(`## ${group.text}`, "", ...entries, ""); + } + if (unlisted.length > 0) index.push("## Optional", "", ...unlisted.map(entry), ""); + + const ordered = [ + ...(home ? [home] : []), + ...groups.flatMap((group) => group.routes.flatMap((route) => byRoute.get(route) ?? [])), + ...unlisted, + ]; + const full = [ + `# Triplex documentation\n\n> ${summary}\n\n${keyFacts}\n`, + ...[...new Map(ordered.map((page) => [page.route, page])).values()].map( + (page) => `\n\n${page.markdown}`, + ), + ].join("\n---\n\n"); + + const files = new Map([ + ["llms.txt", `${index.join("\n").trim()}\n`], + ["llms-full.txt", full], + ]); + for (const page of pages) files.set(page.file, page.markdown); + return files; +}; + +/** Writes the agent-readable files next to the built HTML. */ +export const writeLlmsFiles = async ( + srcDir: string, + outDir: string, + sidebar: DefaultTheme.SidebarItem[], +) => { + const files = await renderLlmsFiles(srcDir, sidebar); + await Promise.all( + [...files].map(([name, content]) => writeFile(join(outDir, name), content, "utf8")), + ); + console.log(`Wrote llms.txt, llms-full.txt, and ${files.size - 2} Markdown pages.`); +}; + +/** The slice of Vite's dev server this plugin uses; `vite` is not a direct dependency. */ +interface DevServer { + readonly middlewares: { + use( + handler: ( + request: IncomingMessage, + response: ServerResponse, + next: (error?: unknown) => void, + ) => void, + ): void; + }; +} + +/** Serves the same files from the dev server. */ +export const llmsDevServer = (srcDir: string, sidebar: DefaultTheme.SidebarItem[]) => ({ + name: "triplex:llms", + apply: "serve" as const, + configureServer(server: DevServer) { + server.middlewares.use((request, response, next) => { + const [pathname = "", query] = (request.url ?? "").split("?"); + const path = decodeURIComponent(pathname).replace(/^\//, ""); + const isLlms = /^llms(-full)?\.txt$/.test(path); + // The dev client imports pages as `/.md` modules; only answer plain fetches. + const isPageMarkdown = + /^[\w-]+\.md$/.test(path) && + query === undefined && + request.headers["sec-fetch-dest"] !== "script"; + if (!isLlms && !isPageMarkdown) return next(); + renderLlmsFiles(srcDir, sidebar) + .then((files) => { + const content = files.get(path); + if (content === undefined) return next(); + response.setHeader( + "Content-Type", + `${path.endsWith(".md") ? "text/markdown" : "text/plain"}; charset=utf-8`, + ); + response.end(content); + }) + .catch(next); + }); + }, +}); diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index ee78ffb..02cbcfc 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -15,7 +15,6 @@ --vp-code-tab-text-color: #aeb8d2; --vp-code-tab-hover-text-color: #f3f5ff; --vp-code-tab-active-text-color: #f3f5ff; - --twoslash-popup-bg: var(--vp-code-block-bg); --vp-nav-height: 72px; --vp-layout-top-height: 32px; } @@ -156,6 +155,105 @@ body { color: var(--vp-c-brand-1); } +.triplex-home__intro div[class*="language-"] { + max-width: 560px; + margin: 24px 0 0; + border: 1px solid var(--vp-c-divider); + border-radius: 7px; +} + +.triplex-home__intro .triplex-home__status { + max-width: 760px; + margin: 18px 0 0; + color: var(--vp-c-text-2); + font-size: 0.85rem; + line-height: 1.6; +} + +.triplex-home__agents { + margin-top: 40px; + padding: 20px 24px; + border: 1px solid color-mix(in srgb, var(--vp-c-brand-1) 35%, var(--vp-c-divider)); + border-radius: 7px; + background: var(--vp-c-brand-soft); +} + +.triplex-home__agents h2 { + margin: 0 0 10px; + padding: 0; + border: 0; + font-size: 1.05rem; + letter-spacing: -0.01em; +} + +.triplex-home__agents ul { + margin: 0; + padding-left: 1.1em; +} + +.triplex-home__agents li { + margin: 6px 0; + color: var(--vp-c-text-2); + font-size: 0.95rem; + line-height: 1.6; +} + +.triplex-home__agents li strong { + color: var(--vp-c-text-1); +} + +.triplex-home__section table { + display: table; + width: 100%; + margin: 0; + font-size: 0.9rem; +} + +.triplex-home__section th { + white-space: nowrap; +} + +.triplex-home__section div[class*="language-"] + table { + margin-top: 24px; +} + +.triplex-home__section td, +.triplex-home__section th { + vertical-align: top; +} + +.triplex-home__timeline { + overflow-x: auto; +} + +.triplex-home__timeline table { + font-size: 0.82rem; +} + +.triplex-home__timeline td:first-child { + text-align: right; +} + +.triplex-home__timeline td, +.triplex-home__timeline th { + padding: 8px 12px; + white-space: nowrap; +} + +.triplex-home .triplex-home__output-label { + margin: 18px 0 8px; + color: var(--vp-c-text-2); + font-family: var(--vp-font-family-mono); + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.triplex-home .triplex-home__output-label code { + font-size: inherit; + text-transform: none; +} + .triplex-home__link-icon { flex: none; margin-left: 0.375em; @@ -225,20 +323,21 @@ body { } .triplex-home__example { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 2.2fr); - align-items: start; - gap: 28px; - margin-top: 32px; + margin-top: 36px; } .triplex-home__example + .triplex-home__example { - padding-top: 32px; + padding-top: 36px; border-top: 1px solid var(--vp-c-divider); } +.triplex-home__example-description { + max-width: 760px; + margin-bottom: 20px; +} + .triplex-home__example-description p { - margin: 0 0 16px; + margin: 0 0 12px; font-size: 0.95rem; } @@ -252,18 +351,14 @@ body { min-width: 0; } -.triplex-home__snippet div[class*="language-"] { +.triplex-home__snippet div[class*="language-"], +.triplex-home__section > div[class*="language-"] { margin: 0; border: 1px solid var(--vp-c-divider); border-radius: 7px; box-shadow: none; } -.twoslash-floating .twoslash-popup-code { - max-height: min(60vh, 420px); - overflow: auto; -} - .triplex-home__guides { display: flex; flex-wrap: wrap; @@ -306,7 +401,11 @@ body { } @media (max-width: 800px) { - .triplex-home__example, + .triplex-home__section table { + display: block; + overflow-x: auto; + } + .triplex-home__principles { grid-template-columns: 1fr; gap: 20px; diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index ebabef3..e50490d 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,11 +1,9 @@ import "@fontsource-variable/ibm-plex-sans"; import "@fontsource/ibm-plex-mono/400.css"; -import TwoslashFloatingVue from "@shikijs/vitepress-twoslash/client"; import type { Theme } from "vitepress"; import DefaultTheme from "vitepress/theme-without-fonts"; import { h } from "vue"; -import "@shikijs/vitepress-twoslash/style.css"; import "./custom.css"; import Playground from "./Playground.vue"; import TriplexExplorer from "./TriplexExplorer.vue"; @@ -23,14 +21,5 @@ export default { enhanceApp({ app }) { app.component("TriplexPlayground", Playground); app.component("TriplexExplorer", TriplexExplorer); - app.use(TwoslashFloatingVue, { - themes: { - twoslash: { - flip: true, - triggers: ["hover", "click"], - popperTriggers: ["hover"], - }, - }, - }); }, } satisfies Theme; diff --git a/docs/agents.md b/docs/agents.md new file mode 100644 index 0000000..a8eb2a4 --- /dev/null +++ b/docs/agents.md @@ -0,0 +1,201 @@ +--- +description: A prescriptive guide for coding agents modeling a back-office domain with Triplex. It maps domain concepts to primitives and lists the rules every command should follow. +--- + +# Modeling a back-office domain + +This guide is for coding agents, and the people directing them, who are deciding whether and how +to model a back-office domain in Triplex. It is prescriptive on purpose: follow the rules unless +you have a specific reason not to, and each section links to the reference behind it. + +Every code block on this page comes from the +[site-safety scenario](https://github.com/bjacobso/triplex/tree/main/docs/snippets/home/site-safety). +CI type-checks and runs that scenario on every change. This page is also available as +[Markdown](/agents.md), and the full documentation is available as [`/llms-full.txt`](/llms-full.txt). + +## Decide whether Triplex fits + +Use Triplex when the domain has one or more of these properties: + +- People ask what was true, or what the system believed, at an earlier date. +- Policies, schemas, or forms change, and earlier decisions must still be explained under the + rules that applied then. +- Work follows from the data: missing evidence, expiring certifications, cases waiting for review. +- Several actors write, whether people, services, or agents, and every change must be attributed + and safe to retry. + +Do not use it for high-volume telemetry, blobs, caches, queues, or tables that never need history +or provenance. Those belong in your existing database, and Triplex can live +[beside it](/host-integration). + +Check these constraints with the people you work for before adopting it: + +| Constraint | Current contract | +| -------------- | ---------------------------------------------------------------------------- | +| Maturity | Pre-1.0. See [Current state](/current-state) | +| Effect | Requires `effect@4.0.0-rc.112`. Effect 3 is not compatible | +| Runtime | ESM-only, Node.js 22+, modern browsers and edge runtimes for the core | +| Durable stores | SQLite is supported. PostgreSQL is a production candidate | +| Host duties | Authentication, authorization, task lifecycle, timers, and retries are yours | + +## Map the domain + +| In your domain | In Triplex | +| --------------------------------------- | -------------------------------------------------------------------- | +| Things: workers, sites, cases, accounts | Stable `EntityId`s and an `EntityType` per kind | +| Properties and relationships | Global `Attribute` keys, with `Attribute.ref` for relationships | +| Schemas, policies, forms | Config nodes committed as an immutable release, pointed to by `live` | +| User and system actions | One `transact` call per command | +| Evidence that starts or expires | `validFrom` and `validTo` on the assertion | +| Corrections | Retract and assert in one transaction | +| Obligations and to-dos | A `Derivation` whose candidates you reconcile | +| Tickets, notifications, SLAs | Your code, driven by reconciliation diffs | +| Audit questions | A temporal basis, command receipts, and the journal | + +## 1. Name entities and attributes + +- Give every entity a stable, typed ID such as `worker:maria` or `case:01J9…`. Construct IDs once + at the application boundary with `EntityId.make`. Don't cast strings deep inside domain code. +- Attribute keys are global keywords of the form `:namespace/name`, matching + `^:[a-z][a-z0-9_-]*/[a-z][a-z0-9_-]*$`. An attribute owns its identity and value type. +- An entity type owns how it uses an attribute: `required`, `cardinality` (`"one"` or `"many"`), + and `unique`. The same attribute can be required in one type and optional in another. +- Model relationships as `Attribute.ref(key, TargetType)`. There are no join tables. Datalog joins + on shared values. +- Available value types: `text`, `number`, `integer`, `boolean`, `date`, `instant`, `enumOf`, + and `ref`. +- Set `entityType` on every assertion so type-level reads and constraints can find the entity. + +<<< @/snippets/home/site-safety/model.ts + +Read more in [Typed configuration](/configuration#ontology-dsl). + +## 2. Publish rules as releases + +- Commit schemas, policies, and forms together with `config.commit({ label, objects, ref: "live" })`. + Each release is immutable and content-addressed. +- To change the rules, publish a new release. Never edit an old one. Moving `live` back + to an earlier release doesn't roll back data. +- At command time, resolve the release you intend to enforce and keep its snapshot for the whole + command. + +Read more in [Releases, refs, and rollback](/configuration-versioning). + +## 3. Write each command as one transaction + +Follow these rules for every write: + +1. **One application command is one `transact` call.** Everything the command changes commits or + rolls back together. +2. **Always set `actor` and a deterministic `commandId`.** Derive the command ID from the request + so that a retry reuses it. Command IDs are atomically unique per database, and a duplicate fails + with `CommandAlreadyCommittedError` instead of applying twice. +3. **Set `correlationId` and `causationId`.** Use the request or case for correlation and the + triggering command or event for causation. +4. **Pin `configSnapshot` and pass `enforce` from the same release.** Pinning alone only records + which rules applied. Constraints are checked only when you also pass them with `enforce`. +5. **Put business time on the assertion.** Use `validFrom` and `validTo` when the dates define when + the fact holds. Intervals are half-open: `[validFrom, validTo)`. +6. **Correct facts; don't delete them.** Retract the wrong assertion and assert the correct one in + the same transaction. For facts that other writers may also change, add a + `{ _tag: "TripleLive", id }` precondition so a concurrent change fails with a conflict instead + of being lost. + +<<< @/snippets/home/site-safety/record.ts#place + +A retry with the same command ID returns the original receipt, and the journal gains no entry: + +<<< @/snippets/home/site-safety/retry.ts + +Read more in [Operational primitives](/operational-primitives) and +[Troubleshooting](/troubleshooting#what-should-happen-on-a-duplicate-command-id). + +## 4. Model obligations as derivations + +- Write the obligation as a Datalog query. Positive patterns describe the situation, and a `not` + clause describes the missing evidence. Every variable in `not` must be bound outside it. +- Set `identity` to the variables that name one obligation, such as worker and site. Candidate IDs + are then stable across re-evaluations. +- Use `Derivation.evaluate` for reads and previews, and + `Derivation.Materialization.materialize` for durable, freshness-tracked runs. +- Pass the difference from `Derivation.reconcile` (`added`, `removed`, `changed`) to your own task + system. Triplex decides what should be open; your code decides what "open" means. +- Wake up at `nextTemporalBoundary` to catch expiring or future-effective evidence. Don't poll. +- Follow the journal with `triples.transactions({ after })` and a `ConsumerCheckpoint`. Advance + the checkpoint only after downstream effects succeed. +- A `stale` or `unmaterialized` result doesn't mean there is no work. Rematerialize before + treating an empty result as current. + +<<< @/snippets/home/site-safety/open-work.ts + +The +[compliance host example](https://github.com/bjacobso/triplex/tree/main/examples/compliance-host) +adds the complete loop: journal consumer, durable task occurrences, a hypothetical preview, and a +scheduler that reopens work at expiry. Read more in [Derivations](/derivations). + +## 5. Answer audit questions + +- **What did we believe then about what was true then?** Set both `recordedAt` and `validAt`. + Setting only one leaves the other at "now". +- **What did we know when we made a decision?** Use the decision's receipt `instant` as + `recordedAt`. Find the receipt with `transactionByCommand`. +- **Who changed this entity, and why?** Page `transactionsForEntity(entityId)`, newest first. + Each record carries its actor, command, correlation, causation, release, and typed changes. +- **What was retracted?** `history(entityId)` includes retracted assertions. `entity` and `match` + show only facts visible at the basis you ask for. + +<<< @/snippets/home/site-safety/what-we-knew.ts + +## 6. Choose a runtime + +Construct one layer per database lifetime, at the application boundary. Building an in-memory +layer again creates a new empty database, and building a SQL layer per request defeats connection +sharing. + +<<< @/snippets/home/layers.ts + +On PostgreSQL, Triplex can join your application's own Effect SQL transaction, so your rows, +Triplex facts, the journal, and an outbox row commit together. See +[Host integration](/host-integration#choose-a-runtime). + +## Anti-patterns + +| Don't | Do instead | +| ------------------------------------------------------------------- | ------------------------------------------------------------------ | +| Generate a new random `commandId` on every retry | Derive it from the request; handle `CommandAlreadyCommittedError` | +| Pin `configSnapshot` and assume constraints were checked | Also pass `enforce` built from that snapshot | +| Retract a fact in one command and assert its replacement in another | Do both in one transaction | +| Store a date attribute when the date says when the fact holds | Use `validFrom` and `validTo` | +| Re-run derivations on a fixed timer | Follow the journal and wake up at `nextTemporalBoundary` | +| Treat an empty `stale` or `unmaterialized` result as "no work" | Check freshness and rematerialize | +| Put task status, assignees, or SLAs in the derivation | Keep the task lifecycle in host code, driven by reconciliation | +| Call `queryAll` on request paths | Use `query` and follow `nextCursor` (100 rows per page by default) | +| Encode authorization as Datalog over the same store | Authorize in the host before providing `Triples` | + +## Inspect with the CLI + +The CLI is non-interactive and prints one JSON envelope per command: +`{ "ok": true, "command": ..., "data": ... }` on success, and `{ "ok": false, "error": ... }` on +stderr with a nonzero exit on failure. Writes require `meta.actor` and `meta.commandId`. + +```sh +npm install --global @triplex-build/triplex-cli +triplex --sqlite ./app.db describe # capability manifest +triplex --sqlite ./app.db entity types +triplex --sqlite ./app.db entity facts worker:maria +triplex --sqlite ./app.db entity history worker:maria --limit 25 +triplex --sqlite ./app.db query explain --input query.json +triplex --sqlite ./app.db journal receipt placement:create:maria-harbor +triplex --sqlite ./app.db config release --ref live +``` + +The unscoped `triplex` package on npm is an unrelated project. See [CLI and dashboard](/tools) for +the full command set. + +## Next + +- [Getting started](/getting-started) to run a first program with expected output +- [Core concepts](/concepts) for facts, retractions, and the two clocks +- [Datalog](/datalog) for joins, negation, aggregation, recursion, and pagination +- [Host integration](/host-integration) for the full responsibility boundary +- [Troubleshooting](/troubleshooting) for empty queries, missing services, and stale projections diff --git a/docs/index.md b/docs/index.md index fecd2bc..f0dd762 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,170 +1,263 @@ --- layout: home -title: Triplex +title: Triplex · The database that remembers why titleTemplate: false -description: A temporal fact database for TypeScript, built on Effect. +description: An embedded fact database for TypeScript back-office systems. Every write records who made it, which versioned rules governed it, and when it was true. sidebar: false aside: false pageClass: triplex-index --- -::: warning Pre-1.0 release candidate -The seven public `@triplex-build` packages are available from npm. They require -`effect@4.0.0-rc.112`; Effect 3 is not compatible. KV and SQLite are the -supported baseline; PostgreSQL is a production candidate, while Cloudflare and FoundationDB are -experimental. [Read the exact maturity contract](/current-state). -::: - diff --git a/docs/snippets/home/facts.ts b/docs/snippets/home/facts.ts deleted file mode 100644 index ac8df20..0000000 --- a/docs/snippets/home/facts.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Effect } from "effect"; -import { EntityId, KvTriples, Triples, ref } from "@triplex-build/triplex"; - -const student = EntityId.make("student:ada"); -const quiz = EntityId.make("quiz:logic-1"); -const submission = EntityId.make("submission:ada:logic-1"); - -export const submitQuiz = Effect.gen(function* () { - const triples = yield* Triples; - - return yield* triples.transact( - [ - { - op: "assert", - entityId: submission, - entityType: "Submission", - attribute: ":submission/student", - value: ref(student), - }, - { - op: "assert", - entityId: submission, - entityType: "Submission", - attribute: ":submission/quiz", - value: ref(quiz), - }, - ], - { - actor: "teacher:grace", - commandId: "submit:ada:logic-1", - configSnapshot: "config:learning-2026.1", - }, - ); -}).pipe(Effect.provide(KvTriples.layer)); diff --git a/docs/snippets/home/layers.ts b/docs/snippets/home/layers.ts new file mode 100644 index 0000000..3d0259e --- /dev/null +++ b/docs/snippets/home/layers.ts @@ -0,0 +1,16 @@ +import { Layer } from "effect"; +import { KvTriples } from "@triplex-build/triplex"; +import { ConfigStore } from "@triplex-build/triplex/config"; +import { PgTriples } from "@triplex-build/triplex-postgres"; +import { SqliteTriples } from "@triplex-build/triplex-sqlite"; + +// The same program runs against any of these. Only the provided layer changes. +export const InMemory = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer)); + +export const Sqlite = ConfigStore.layer.pipe( + Layer.provideMerge(SqliteTriples.layer({ filename: "app.db" })), +); + +export const Postgres = ConfigStore.layer.pipe( + Layer.provideMerge(PgTriples.layerFromUrl(process.env["DATABASE_URL"] ?? "")), +); diff --git a/docs/snippets/home/ontology.ts b/docs/snippets/home/ontology.ts deleted file mode 100644 index 0a881cd..0000000 --- a/docs/snippets/home/ontology.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Effect } from "effect"; -import { Attribute, ConfigStore, EntityType } from "@triplex-build/triplex/config"; - -const StudentName = Attribute.text(":student/name"); - -const Student = EntityType.make("Student", { - attributes: { - name: Attribute.use(StudentName, { - required: true, - unique: true, - }), - }, -}); - -export const publishSchema = Effect.gen(function* () { - const config = yield* ConfigStore.ConfigStore; - - return yield* config.commit({ - label: "learning-2026.1", - objects: yield* Student.nodes, - ref: "live", - }); -}); diff --git a/docs/snippets/home/query.ts b/docs/snippets/home/query.ts deleted file mode 100644 index a199966..0000000 --- a/docs/snippets/home/query.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Effect } from "effect"; -import { Triples } from "@triplex-build/triplex"; - -export const openGradingTasks = Effect.gen(function* () { - const triples = yield* Triples; - - return yield* triples.query({ - find: ["?student", "?quiz"], - where: [ - ["?submission", ":submission/student", "?student"], - ["?submission", ":submission/quiz", "?quiz"], - ["not", ["?submission", ":submission/grade", "?grade"]], - ], - }); -}); diff --git a/docs/snippets/home/site-safety/model.ts b/docs/snippets/home/site-safety/model.ts new file mode 100644 index 0000000..0dfa0d8 --- /dev/null +++ b/docs/snippets/home/site-safety/model.ts @@ -0,0 +1,52 @@ +import { Effect } from "effect"; +import { EntityId } from "@triplex-build/triplex"; +import { Attribute, ConfigStore, EntityType } from "@triplex-build/triplex/config"; + +// Attributes own global identity and value type. +export const SiteName = Attribute.text(":site/name"); +export const Site = EntityType.make("Site", { + attributes: { name: Attribute.use(SiteName, { required: true, unique: true }) }, +}); + +export const WorkerName = Attribute.text(":worker/name"); +export const TrainedFor = Attribute.ref(":worker/trained-for", Site); +export const Worker = EntityType.make("Worker", { + attributes: { + name: Attribute.use(WorkerName, { required: true }), + trainedFor: Attribute.use(TrainedFor, { cardinality: "many" }), + }, +}); + +export const PlacementWorker = Attribute.ref(":placement/worker", Worker); +export const PlacementSite = Attribute.ref(":placement/site", Site); +export const Placement = EntityType.make("Placement", { + attributes: { + worker: Attribute.use(PlacementWorker, { required: true }), + site: Attribute.use(PlacementSite, { required: true }), + }, +}); + +// hr-2026.2 changes the rules: every placement now needs a supervisor. +export const PlacementSupervisor = Attribute.text(":placement/supervisor"); +export const PlacementV2 = EntityType.make("Placement", { + attributes: { + worker: Attribute.use(PlacementWorker, { required: true }), + site: Attribute.use(PlacementSite, { required: true }), + supervisor: Attribute.use(PlacementSupervisor, { required: true }), + }, +}); + +export const maria = EntityId.make("worker:maria"); +export const harbor = EntityId.make("site:harbor"); +export const placement = EntityId.make("placement:maria-harbor"); + +// Entity types own usage rules. Publishing them creates an immutable release. +export const publish = (label: string, placementType: typeof Placement | typeof PlacementV2) => + Effect.gen(function* () { + const config = yield* ConfigStore.ConfigStore; + return yield* config.commit({ + label, + objects: [...(yield* Site.nodes), ...(yield* Worker.nodes), ...(yield* placementType.nodes)], + ref: "live", + }); + }); diff --git a/docs/snippets/home/site-safety/open-work.ts b/docs/snippets/home/site-safety/open-work.ts new file mode 100644 index 0000000..6d9322b --- /dev/null +++ b/docs/snippets/home/site-safety/open-work.ts @@ -0,0 +1,37 @@ +import { Effect } from "effect"; +import { Triples } from "@triplex-build/triplex"; +import * as Derivation from "@triplex-build/triplex/derivation"; +import { PlacementSite, PlacementWorker, TrainedFor } from "./model.ts"; + +// Obligation: every placed worker holds current training for that site. +export const siteSafety = (configSnapshot: string) => + Derivation.make({ + name: "task.site-safety-training", + query: { + find: ["?worker", "?site"], + where: [ + ["?placement", PlacementWorker.key, "?worker"], + ["?placement", PlacementSite.key, "?site"], + ["not", ["?worker", TrainedFor.key, "?site"]], + ], + }, + identity: ["?worker", "?site"], + configSnapshot, + }); + +const day = (instant: number) => new Date(instant).toISOString().slice(0, 10); + +// Which training tasks should be open on a given day, and when will that change? +export const openWorkOn = (configSnapshot: string, validAt: number) => + Effect.gen(function* () { + const triples = yield* Triples; + const definition = yield* siteSafety(configSnapshot); + const { candidates, nextTemporalBoundary } = yield* Derivation.evaluate(triples, definition, { + basis: { validAt }, + }); + return { + on: day(validAt), + open: candidates.map((task) => task.result), + recheckOn: nextTemporalBoundary === undefined ? null : day(nextTemporalBoundary), + }; + }); diff --git a/docs/snippets/home/site-safety/output/open-work.json b/docs/snippets/home/site-safety/output/open-work.json new file mode 100644 index 0000000..3d01a3f --- /dev/null +++ b/docs/snippets/home/site-safety/output/open-work.json @@ -0,0 +1,13 @@ +[ + { + "on": "2026-03-03", + "open": [{ "?worker": "worker:maria", "?site": "site:harbor" }], + "recheckOn": "2026-03-05" + }, + { "on": "2026-04-01", "open": [], "recheckOn": "2026-06-01" }, + { + "on": "2026-06-01", + "open": [{ "?worker": "worker:maria", "?site": "site:harbor" }], + "recheckOn": null + } +] diff --git a/docs/snippets/home/site-safety/output/rejected.json b/docs/snippets/home/site-safety/output/rejected.json new file mode 100644 index 0000000..b59ffaa --- /dev/null +++ b/docs/snippets/home/site-safety/output/rejected.json @@ -0,0 +1 @@ +{ "error": "ConstraintViolationError", "violations": ["Placement requires :placement/supervisor"] } diff --git a/docs/snippets/home/site-safety/output/timeline.md b/docs/snippets/home/site-safety/output/timeline.md new file mode 100644 index 0000000..61e8ebd --- /dev/null +++ b/docs/snippets/home/site-safety/output/timeline.md @@ -0,0 +1,8 @@ +| Position | Actor | Command | Changes (+ assert, − retract; valid time) | +| -------- | ---------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `triplex/config-store` | — | Release `hr-2026.1`; `live` moved to it | +| 2 | `user:hr` | `onboard:maria` | + `:site/name` `"Harbor"` from 2026-01-05
    + `:worker/name` `"Maria"` from 2026-01-05 | +| 3 | `user:trainer` | `training:record:maria-harbor` | + `:worker/trained-for` `site:harbor` [2026-03-01, 2026-06-01) | +| 4 | `user:dana` | `placement:create:maria-harbor` | + `:placement/worker` `worker:maria` from 2026-03-02
    + `:placement/site` `site:harbor` from 2026-03-02 | +| 5 | `triplex/config-store` | — | Release `hr-2026.2`; `live` moved to it | +| 6 | `user:auditor` | `training:correct:maria-harbor` | − `:worker/trained-for` `site:harbor` [2026-03-01, 2026-06-01)
    + `:worker/trained-for` `site:harbor` [2026-03-05, 2026-06-01) | diff --git a/docs/snippets/home/site-safety/output/what-we-knew.json b/docs/snippets/home/site-safety/output/what-we-knew.json new file mode 100644 index 0000000..0e35098 --- /dev/null +++ b/docs/snippets/home/site-safety/output/what-we-knew.json @@ -0,0 +1 @@ +{ "knownAtDecision": true, "knownToday": false } diff --git a/docs/snippets/home/site-safety/output/which-rules.json b/docs/snippets/home/site-safety/output/which-rules.json new file mode 100644 index 0000000..6561f41 --- /dev/null +++ b/docs/snippets/home/site-safety/output/which-rules.json @@ -0,0 +1,6 @@ +{ + "actor": "user:dana", + "correlationId": "request:7f3a", + "governedBy": "hr-2026.1", + "liveToday": "hr-2026.2" +} diff --git a/docs/snippets/home/site-safety/record.ts b/docs/snippets/home/site-safety/record.ts new file mode 100644 index 0000000..cbd7045 --- /dev/null +++ b/docs/snippets/home/site-safety/record.ts @@ -0,0 +1,113 @@ +import { Effect } from "effect"; +import { Triples } from "@triplex-build/triplex"; +import { GraphConstraint, type InMemoryConfigStore } from "@triplex-build/triplex/config"; +import { Placement, Site, TrainedFor, Worker, harbor, maria, placement } from "./model.ts"; + +export const JAN_5 = Date.UTC(2026, 0, 5); +export const MAR_1 = Date.UTC(2026, 2, 1); +export const MAR_2 = Date.UTC(2026, 2, 2); +export const MAR_5 = Date.UTC(2026, 2, 5); +export const JUN_1 = Date.UTC(2026, 5, 1); + +export const onboard = Effect.gen(function* () { + const triples = yield* Triples; + return yield* triples.transact( + [ + { + op: "assert", + entityId: harbor, + entityType: "Site", + ...Site.attributes.name.assertion("Harbor", { validFrom: JAN_5 }), + }, + { + op: "assert", + entityId: maria, + entityType: "Worker", + ...Worker.attributes.name.assertion("Maria", { validFrom: JAN_5 }), + }, + ], + { actor: "user:hr", commandId: "onboard:maria" }, + ); +}); + +// Evidence holds for a window of business time: [validFrom, validTo). +export const recordTraining = Effect.gen(function* () { + const triples = yield* Triples; + return yield* triples.transact( + [ + { + op: "assert", + entityId: maria, + entityType: "Worker", + attribute: TrainedFor.key, + value: TrainedFor.encode(harbor), + validFrom: MAR_1, + validTo: JUN_1, + }, + ], + { actor: "user:trainer", commandId: "training:record:maria-harbor" }, + ); +}); + +// #region place +// One application command is one atomic transaction that records who acted, +// which command it was, and the release whose rules it was checked against. +export const placeWorker = (release: InMemoryConfigStore.ConfigSnapshot) => + Effect.gen(function* () { + const triples = yield* Triples; + const rules = yield* GraphConstraint.collect(release.root); + return yield* triples.transact( + [ + { + op: "assert", + entityId: placement, + entityType: "Placement", + ...Placement.attributes.worker.assertion(maria, { validFrom: MAR_2 }), + }, + { + op: "assert", + entityId: placement, + entityType: "Placement", + ...Placement.attributes.site.assertion(harbor, { validFrom: MAR_2 }), + }, + ], + { + actor: "user:dana", + commandId: "placement:create:maria-harbor", // unique: a retry can never apply twice + correlationId: "request:7f3a", + configSnapshot: release.id, // the rules this write was governed by + enforce: GraphConstraint.enforcement(rules), // required/unique/ref checks, atomically + }, + ); + }); +// #endregion place + +// A correction never deletes history. It retracts the recorded belief and +// asserts what is now known, in one attributed transaction. +export const correctTraining = Effect.gen(function* () { + const triples = yield* Triples; + const [wrong] = yield* triples.match( + { entityId: maria, attribute: TrainedFor.key }, + { validAt: MAR_1 }, + ); + if (wrong === undefined) return yield* Effect.die("no training evidence to correct"); + return yield* triples.transact( + [ + { op: "retract", id: wrong.id }, + { + op: "assert", + entityId: maria, + entityType: "Worker", + attribute: TrainedFor.key, + value: TrainedFor.encode(harbor), + validFrom: MAR_5, + validTo: JUN_1, + }, + ], + { + actor: "user:auditor", + commandId: "training:correct:maria-harbor", + causationId: "audit:2026-q1", + }, + ); +}); diff --git a/docs/snippets/home/site-safety/retry.ts b/docs/snippets/home/site-safety/retry.ts new file mode 100644 index 0000000..d5ad62d --- /dev/null +++ b/docs/snippets/home/site-safety/retry.ts @@ -0,0 +1,18 @@ +import { Effect } from "effect"; +import { Triples } from "@triplex-build/triplex"; +import type { InMemoryConfigStore } from "@triplex-build/triplex/config"; +import { placeWorker } from "./record.ts"; + +// A retried command must never apply twice. The duplicate is rejected with the +// original transaction, so the caller can answer with the first receipt. +export const placeWorkerOnce = (release: InMemoryConfigStore.ConfigSnapshot) => + placeWorker(release).pipe( + Effect.map((result) => result.position), + Effect.catchTag("CommandAlreadyCommittedError", (duplicate) => + Effect.gen(function* () { + const triples = yield* Triples; + const receipt = yield* triples.transactionByCommand(duplicate.commandId); + return receipt?.position; + }), + ), + ); diff --git a/docs/snippets/home/site-safety/run.ts b/docs/snippets/home/site-safety/run.ts new file mode 100644 index 0000000..5d9a098 --- /dev/null +++ b/docs/snippets/home/site-safety/run.ts @@ -0,0 +1,152 @@ +/** + * Runs the homepage site-safety scenario end to end and writes every output the + * homepage displays. `--check` fails instead of writing when an output drifts, + * so the published answers are always produced by the published code. + */ +import { execFileSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import { Effect, Layer } from "effect"; +import { EntityId, KvTriples, Triples, type TransactionChange } from "@triplex-build/triplex"; +import { ConfigStore, GraphConstraint } from "@triplex-build/triplex/config"; +import { ContentId } from "@triplex-build/triplex/content"; + +import { Placement, PlacementV2, harbor, maria, publish } from "./model.ts"; +import { JUN_1, MAR_2, correctTraining, onboard, placeWorker, recordTraining } from "./record.ts"; +import { openWorkOn } from "./open-work.ts"; +import { placeWorkerOnce } from "./retry.ts"; +import { governingRules } from "./which-rules.ts"; +import { wasMariaTrained } from "./what-we-knew.ts"; + +const day = (instant: number) => new Date(instant).toISOString().slice(0, 10); + +// `+` asserts and `−` retracts; valid time is the half-open interval [validFrom, validTo). +const describe = (change: TransactionChange) => { + const value = + change.value?.type === "ref" ? change.value.value : JSON.stringify(change.value?.value); + const from = change.validFrom === undefined ? "" : day(change.validFrom); + const valid = change.validTo === undefined ? `from ${from}` : `[${from}, ${day(change.validTo)})`; + return `${change.op === "assert" ? "+" : "−"} \`${change.attribute}\` \`${value}\` ${valid}`; +}; + +// Recorded time comes from the wall clock; keep each step on a distinct instant. +const tick = Effect.sleep("5 millis"); + +const scenario = Effect.gen(function* () { + const triples = yield* Triples; + const release = yield* publish("hr-2026.1", Placement); + yield* tick; + yield* onboard; + yield* tick; + yield* recordTraining; + yield* tick; + const placed = yield* placeWorker(release.snapshot); + yield* tick; + const retried = yield* placeWorkerOnce(release.snapshot); + if (retried !== placed.position) + throw new Error("a retried command must return its first receipt"); + yield* tick; + const next = yield* publish("hr-2026.2", PlacementV2); + yield* tick; + // The same kind of command, checked against today's rules, is rejected atomically. + const rejected = yield* triples + .transact( + [ + { + op: "assert", + entityId: EntityId.make("placement:maria-pier"), + entityType: "Placement", + ...PlacementV2.attributes.worker.assertion(maria, { validFrom: MAR_2 }), + }, + { + op: "assert", + entityId: EntityId.make("placement:maria-pier"), + entityType: "Placement", + ...PlacementV2.attributes.site.assertion(harbor, { validFrom: MAR_2 }), + }, + ], + { + actor: "user:dana", + commandId: "placement:create:maria-pier", + configSnapshot: next.snapshot.id, + enforce: GraphConstraint.enforcement(yield* GraphConstraint.collect(next.snapshot.root)), + }, + ) + .pipe(Effect.flip); + yield* tick; + yield* correctTraining; + + const config = yield* ConfigStore.ConfigStore; + const journal = yield* triples.transactions({ after: 0, limit: 1_000 }); + const rows = [ + "| Position | Actor | Command | Changes (+ assert, − retract; valid time) |", + "| --- | --- | --- | --- |", + ]; + for (const transaction of journal.transactions) { + const pinned = transaction.configSnapshot; + const changed = + transaction.actor === "triplex/config-store" && + pinned !== undefined && + ContentId.isContentId(pinned) + ? `Release \`${(yield* config.snapshotById(pinned))?.label}\`; \`live\` moved to it` + : transaction.changes.map(describe).join("
    "); + rows.push( + `| ${transaction.position} | \`${transaction.actor}\` | ${transaction.commandId === undefined ? "—" : `\`${transaction.commandId}\``} | ${changed} |`, + ); + } + const snapshot = release.snapshot.id; + return { + timeline: `${rows.join("\n")}\n`, + "what-we-knew": yield* wasMariaTrained, + "which-rules": yield* governingRules, + rejected: { + error: rejected._tag, + ...(rejected._tag === "ConstraintViolationError" + ? { violations: [...new Set(rejected.violations.map((violation) => violation.message))] } + : {}), + }, + "open-work": [ + yield* openWorkOn(snapshot, Date.UTC(2026, 2, 3)), + yield* openWorkOn(snapshot, Date.UTC(2026, 3, 1)), + yield* openWorkOn(snapshot, JUN_1), + ], + }; +}); + +const AppLayer = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer)); +const outputs = await Effect.runPromise(scenario.pipe(Effect.provide(AppLayer))); + +const check = process.argv.includes("--check"); +const files: Record = { + "timeline.md": outputs.timeline, + "what-we-knew.json": JSON.stringify(outputs["what-we-knew"]), + "which-rules.json": JSON.stringify(outputs["which-rules"]), + "rejected.json": JSON.stringify(outputs.rejected), + "open-work.json": JSON.stringify(outputs["open-work"]), +}; + +// Format exactly as the repository formatter would, so `pnpm format` never fights `--check`. +const staging = await mkdtemp(join(tmpdir(), "triplex-home-")); +for (const [name, body] of Object.entries(files)) await writeFile(join(staging, name), body); +execFileSync(resolve(import.meta.dirname, "../../../../node_modules/.bin/oxfmt"), [staging], { + stdio: "ignore", +}); + +const outputDirectory = resolve(import.meta.dirname, "output"); +const drift: string[] = []; +for (const name of Object.keys(files)) { + const expected = await readFile(join(staging, name), "utf8"); + const path = join(outputDirectory, name); + if (!check) await writeFile(path, expected); + else if ((await readFile(path, "utf8").catch(() => "")) !== expected) drift.push(path); +} +await rm(staging, { recursive: true, force: true }); + +if (drift.length > 0) { + console.error("Homepage outputs are stale. Run `pnpm docs:outputs` and commit:"); + for (const path of drift) console.error(` ${path}`); + process.exit(1); +} +console.log(check ? "Homepage outputs match the scenario." : "Wrote homepage outputs."); diff --git a/docs/snippets/home/site-safety/what-we-knew.ts b/docs/snippets/home/site-safety/what-we-knew.ts new file mode 100644 index 0000000..32e488d --- /dev/null +++ b/docs/snippets/home/site-safety/what-we-knew.ts @@ -0,0 +1,23 @@ +import { Effect } from "effect"; +import { Triples, type TemporalBasis } from "@triplex-build/triplex"; +import { TrainedFor, maria } from "./model.ts"; +import { MAR_2 } from "./record.ts"; + +// Was Maria trained for Harbor on the day she started there? +export const wasMariaTrained = Effect.gen(function* () { + const triples = yield* Triples; + const decision = yield* triples.transactionByCommand("placement:create:maria-harbor"); + if (decision === null) return yield* Effect.die("placement was never recorded"); + + const trained = (basis: TemporalBasis) => + triples + .match({ entityId: maria, attribute: TrainedFor.key }, basis) + .pipe(Effect.map((facts) => facts.length > 0)); + + return { + // What the database believed at the instant Dana made the placement. + knownAtDecision: yield* trained({ validAt: MAR_2, recordedAt: decision.instant }), + // What the database knows now, after the auditor's correction. + knownToday: yield* trained({ validAt: MAR_2 }), + }; +}); diff --git a/docs/snippets/home/site-safety/which-rules.ts b/docs/snippets/home/site-safety/which-rules.ts new file mode 100644 index 0000000..5b2202c --- /dev/null +++ b/docs/snippets/home/site-safety/which-rules.ts @@ -0,0 +1,21 @@ +import { Effect } from "effect"; +import { Triples } from "@triplex-build/triplex"; +import { ConfigStore, ContentId } from "@triplex-build/triplex/config"; + +// Who placed Maria, and under which version of the rules? +export const governingRules = Effect.gen(function* () { + const triples = yield* Triples; + const config = yield* ConfigStore.ConfigStore; + const receipt = yield* triples.transactionByCommand("placement:create:maria-harbor"); + const pinned = receipt?.configSnapshot; + if (pinned === undefined || !ContentId.isContentId(pinned)) { + return yield* Effect.die("placement was not pinned to a release"); + } + + return { + actor: receipt?.actor, + correlationId: receipt?.correlationId, + governedBy: (yield* config.snapshotById(pinned))?.label, + liveToday: (yield* config.resolveRef("live"))?.label, + }; +}); diff --git a/docs/snippets/tsconfig.json b/docs/snippets/tsconfig.json index 61d45f5..6eb8c02 100644 --- a/docs/snippets/tsconfig.json +++ b/docs/snippets/tsconfig.json @@ -7,8 +7,15 @@ "paths": { "@triplex-build/triplex": ["packages/core/src/index.ts"], "@triplex-build/triplex/config": ["packages/core/src/config/index.ts"], - "@triplex-build/triplex-sqlite": ["packages/sqlite/src/index.ts"] - } + "@triplex-build/triplex-sqlite": ["packages/sqlite/src/index.ts"], + "@triplex-build/triplex/content": ["packages/core/src/content/index.ts"], + "@triplex-build/triplex/derivation": ["packages/core/src/derivation/index.ts"], + "@triplex-build/triplex-postgres": ["packages/postgres/src/index.ts"], + "@triplex-build/triplex/internal": ["packages/core/src/internal.ts"], + "@triplex-build/triplex-sql": ["packages/sql/src/index.ts"] + }, + "allowImportingTsExtensions": true, + "types": ["node"] }, "include": ["**/*.ts"] } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c0480e1..07d9ed4 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,3 +1,7 @@ +--- +description: Answers to common failures, including npm 404s, missing Effect services, empty queries, 100-row pages, which clock to set, duplicate command IDs, unenforced constraints, and stale projections. +--- + # Troubleshooting and FAQ ## Why does npm return 404 for a Triplex package? diff --git a/e2e/docs-home.spec.ts b/e2e/docs-home.spec.ts index 55f4972..ae04d4f 100644 --- a/e2e/docs-home.spec.ts +++ b/e2e/docs-home.spec.ts @@ -12,28 +12,34 @@ for (const colorScheme of ["light", "dark"] as const) { } else { await expect(root).not.toHaveClass(/dark/); } - await expect(page.getByRole("heading", { level: 1, name: "Triplex" })).toBeVisible(); + await expect( + page.getByRole("heading", { level: 1, name: "The database that remembers why." }), + ).toBeVisible(); const content = page.locator("#VPContent"); await expect(content.getByRole("link", { name: /Get started/ })).toHaveAttribute( "href", "/getting-started", ); - await expect(content.getByRole("link", { name: "Try the playground" })).toHaveAttribute( + await expect(content.getByRole("link", { name: "Modeling guide for agents" })).toHaveAttribute( "href", - "/playground", + "/agents", ); + await expect( + page.getByRole("heading", { name: "Reading this as a coding agent?" }), + ).toBeVisible(); + + // The journal table and every question are rendered from verified scenario outputs. + const timeline = page.locator(".triplex-home__timeline table"); + await expect(timeline.locator("tbody tr")).toHaveCount(6); + await expect(timeline).toContainText("placement:create:maria-harbor"); const examples = page.locator('.triplex-home__snippet div[class*="language-"]'); - await expect(examples).toHaveCount(3); + await expect(examples).toHaveCount(8); for (const example of await examples.all()) { await expect(example).toBeVisible(); } + await expect(content).toContainText('"knownAtDecision": true'); + await expect(content).toContainText('"governedBy": "hr-2026.1"'); await expect(page.getByRole("tab")).toHaveCount(0); - await expect( - page.getByRole("heading", { name: "Your application should be able to explain itself." }), - ).toBeVisible(); - await expect( - page.getByRole("heading", { name: "Version the rules alongside the facts." }), - ).toBeVisible(); await expect(page).toHaveScreenshot(`home-${colorScheme}.png`, { animations: "disabled", @@ -46,9 +52,28 @@ test("home examples remain visible on mobile without page overflow", async ({ pa await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/"); const examples = page.locator('.triplex-home__snippet div[class*="language-"]'); - await expect(examples).toHaveCount(3); + await expect(examples).toHaveCount(8); for (const example of await examples.all()) { await expect(example).toBeVisible(); } expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); }); + +test("agent-readable documentation is served as plain text", async ({ request }) => { + const index = await request.get("/llms.txt"); + expect(index.ok()).toBe(true); + const body = await index.text(); + expect(body).toMatch(/^# Triplex\n\n> The database that remembers why/); + expect(body).toContain("(https://triplex.build/agents.md)"); + + const guide = await request.get("/agents.md"); + expect(guide.headers()["content-type"]).toContain("text/markdown"); + const markdown = await guide.text(); + expect(markdown).toMatch(/^# Modeling a back-office domain/); + // Code imports are resolved into fenced code rather than left as VitePress directives. + expect(markdown).toContain('commandId: "placement:create:maria-harbor"'); + expect(markdown).not.toContain("<<<"); + + const full = await request.get("/llms-full.txt"); + expect(await full.text()).toContain("# Troubleshooting and FAQ"); +}); diff --git a/e2e/docs-home.spec.ts-snapshots/home-dark.png b/e2e/docs-home.spec.ts-snapshots/home-dark.png index e3767ed..13f40a3 100644 Binary files a/e2e/docs-home.spec.ts-snapshots/home-dark.png and b/e2e/docs-home.spec.ts-snapshots/home-dark.png differ diff --git a/e2e/docs-home.spec.ts-snapshots/home-light.png b/e2e/docs-home.spec.ts-snapshots/home-light.png index ad06948..8ef3c88 100644 Binary files a/e2e/docs-home.spec.ts-snapshots/home-light.png and b/e2e/docs-home.spec.ts-snapshots/home-light.png differ diff --git a/package.json b/package.json index 10a4528..4f8a65f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "triplex-monorepo", "version": "0.1.0", "private": true, - "description": "An Effect-native fact database with Datalog and typed, content-addressed configuration.", + "description": "The database that remembers why: an embedded, bitemporal fact database for TypeScript back-office systems, built on Effect.", "author": "Ben Jacobson", "type": "module", "scripts": { @@ -12,7 +12,8 @@ "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm docs:check && pnpm test && pnpm build", "dev": "turbo watch build", "docs:build": "vitepress build docs", - "docs:check": "node scripts/check-markdown-code.mjs && tsc --noEmit -p docs/snippets/tsconfig.json", + "docs:check": "node scripts/check-markdown-code.mjs && tsc --noEmit -p docs/snippets/tsconfig.json && pnpm docs:outputs --check", + "docs:outputs": "tsx --tsconfig docs/snippets/tsconfig.json docs/snippets/home/site-safety/run.ts", "docs:deploy": "alchemy deploy --stage prod", "docs:dev": "vitepress dev docs", "docs:plan": "alchemy plan --stage prod", @@ -47,7 +48,6 @@ "@fontsource-variable/ibm-plex-sans": "catalog:", "@fontsource/ibm-plex-mono": "catalog:", "@playwright/test": "catalog:", - "@shikijs/vitepress-twoslash": "catalog:", "@tailwindcss/vite": "catalog:", "@types/node": "catalog:", "alchemy": "catalog:", diff --git a/packages/core/README.md b/packages/core/README.md index c41d941..fab8542 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,6 +1,12 @@ # @triplex-build/triplex -An Effect-native fact database with Datalog and typed, content-addressed configuration. +The database that remembers why: an embedded fact database for TypeScript back-office systems, +built on Effect. Every write records who made it, which versioned rules governed it, and when it +was true, so audit questions and open work become queries. + +Coding agents should start with the +[modeling guide](https://triplex.build/agents) or the Markdown documentation index at +[`triplex.build/llms.txt`](https://triplex.build/llms.txt). The platform-neutral core includes temporal triples, an in-memory ordered-KV hexastore, Datalog querying, subscriptions, entity snapshots, configuration releases, diff --git a/packages/core/package.json b/packages/core/package.json index e434258..8786d9f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@triplex-build/triplex", "version": "0.2.0", - "description": "An Effect-native fact database with Datalog and typed, content-addressed configuration.", + "description": "The database that remembers why: an embedded, bitemporal fact database for TypeScript back-office systems, built on Effect.", "keywords": [ "configuration", "content-addressed", diff --git a/packages/testkit/test/fixtures.test.ts b/packages/testkit/test/fixtures.test.ts index 2c72f1b..5fc6636 100644 --- a/packages/testkit/test/fixtures.test.ts +++ b/packages/testkit/test/fixtures.test.ts @@ -22,7 +22,8 @@ describe("database-testkit", () => { }); describe("Triples conformance (in-memory KV)", () => { - it("passes the full conformance suite", async () => { + // Runs the entire corpus serially in one test, which has outgrown Vitest's 5s default on CI. + it("passes the full conformance suite", { timeout: 30_000 }, async () => { await Effect.runPromise(makeTriplesConformanceSuite().pipe(Effect.provide(KvTriples.layer))); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2c44ee..72b89c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,9 +42,6 @@ catalogs: '@playwright/test': specifier: 1.63.0 version: 1.63.0 - '@shikijs/vitepress-twoslash': - specifier: 4.4.2 - version: 4.4.2 '@tailwindcss/vite': specifier: 4.3.3 version: 4.3.3 @@ -132,9 +129,6 @@ importers: '@playwright/test': specifier: 'catalog:' version: 1.63.0 - '@shikijs/vitepress-twoslash': - specifier: 'catalog:' - version: 4.4.2(typescript@6.0.2) '@tailwindcss/vite': specifier: 'catalog:' version: 4.3.3(vite@8.2.2(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.19.0)(yaml@2.9.0)) @@ -1422,9 +1416,6 @@ packages: '@floating-ui/core@1.8.0': resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.1.1': - resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==} - '@floating-ui/dom@1.8.0': resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} @@ -2712,50 +2703,26 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@shikijs/core@4.4.2': - resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} - engines: {node: '>=20'} - '@shikijs/core@4.4.3': resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.4.2': - resolution: {integrity: sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==} - engines: {node: '>=20'} - '@shikijs/engine-javascript@4.4.3': resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.4.2': - resolution: {integrity: sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==} - engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.4.3': resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} engines: {node: '>=20'} - '@shikijs/langs@4.4.2': - resolution: {integrity: sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==} - engines: {node: '>=20'} - '@shikijs/langs@4.4.3': resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} engines: {node: '>=20'} - '@shikijs/primitive@4.4.2': - resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} - engines: {node: '>=20'} - '@shikijs/primitive@4.4.3': resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} engines: {node: '>=20'} - '@shikijs/themes@4.4.2': - resolution: {integrity: sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==} - engines: {node: '>=20'} - '@shikijs/themes@4.4.3': resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} engines: {node: '>=20'} @@ -2764,24 +2731,10 @@ packages: resolution: {integrity: sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==} engines: {node: '>=20'} - '@shikijs/twoslash@4.4.2': - resolution: {integrity: sha512-JjPKNbYCZn5+DVOxDZJ0ZruFPBM7m2JQBp4WbLh4tU2y3ed93wBCaQhKwMyfTFIsDzHks22szAkUUd2+jIPo/w==} - engines: {node: '>=20'} - peerDependencies: - typescript: '>=5.5.0' - - '@shikijs/types@4.4.2': - resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} - engines: {node: '>=20'} - '@shikijs/types@4.4.3': resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} engines: {node: '>=20'} - '@shikijs/vitepress-twoslash@4.4.2': - resolution: {integrity: sha512-dBrtLV5YyDK52FVyiZ3TORnKohZlYznGGbLn/mfkHQMJDV8dzca4K5t5B5VBN6Fp6QYUwc9FZxXo3p+KDAtFjQ==} - engines: {node: '>=20'} - '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2975,9 +2928,6 @@ 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==} @@ -3008,9 +2958,6 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} @@ -3035,11 +2982,6 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript/vfs@1.6.4': - resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} - peerDependencies: - typescript: '*' - '@ungap/structured-clone@1.4.0': resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} @@ -3079,12 +3021,6 @@ packages: '@vitest/utils@4.1.11': resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} - '@volar/language-core@2.4.28': - resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} - - '@volar/source-map@2.4.28': - resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} - '@vue/compiler-core@3.5.40': resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} @@ -3106,9 +3042,6 @@ packages: '@vue/devtools-shared@8.2.1': resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} - '@vue/language-core@3.3.11': - resolution: {integrity: sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==} - '@vue/reactivity@3.5.40': resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} @@ -3235,9 +3168,6 @@ packages: ws: optional: true - alien-signals@3.2.1: - resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} - ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -3273,9 +3203,6 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -3437,9 +3364,6 @@ packages: 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==} - chevrotain@10.5.0: resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} @@ -3538,9 +3462,6 @@ packages: supports-color: optional: true - decode-named-character-reference@1.3.0: - resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -3614,10 +3535,6 @@ packages: resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -3657,10 +3574,6 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -3735,15 +3648,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - floating-vue@5.2.2: - resolution: {integrity: sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==} - peerDependencies: - '@nuxt/kit': ^3.2.0 - vue: ^3.2.0 - peerDependenciesMeta: - '@nuxt/kit': - optional: true - focus-trap@8.2.2: resolution: {integrity: sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==} @@ -4107,9 +4011,6 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - linkify-it@5.0.2: - resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -4122,16 +4023,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -4141,119 +4035,28 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} - markdown-it@14.3.1: - resolution: {integrity: sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==} - hasBin: true - - markdown-table@3.0.4: - resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - - mdast-util-find-and-replace@3.0.2: - resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - - mdast-util-from-markdown@2.0.3: - resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} - - mdast-util-gfm-autolink-literal@2.0.1: - resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - - mdast-util-gfm-footnote@2.1.0: - resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} - - mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - - mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - - mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - - mdast-util-gfm@3.1.0: - resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - - 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==} - - mdurl@2.1.0: - resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - 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==} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -4315,9 +4118,6 @@ packages: msgpackr@2.1.0: resolution: {integrity: sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ==} - muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nan@2.28.0: resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} @@ -4352,9 +4152,6 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - ohash@2.0.12: - resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4399,9 +4196,6 @@ packages: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-expression-matcher@1.6.2: resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} engines: {node: '>=14.0.0'} @@ -4566,10 +4360,6 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - pure-rand@8.4.2: resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} @@ -4732,10 +4522,6 @@ packages: resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} - shiki@4.4.2: - resolution: {integrity: sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==} - engines: {node: '>=20'} - shiki@4.4.3: resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} engines: {node: '>=20'} @@ -4964,19 +4750,6 @@ packages: tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - twoslash-protocol@0.3.9: - resolution: {integrity: sha512-9/iwp+CXOnjFMPQuPL5PkuRbZnDoNpBvtJCLs9t8kDYkL3YHujbvnHfZA1i5fApDftVEdBw+T/4F+dH5kIzpYQ==} - - twoslash-vue@0.3.9: - resolution: {integrity: sha512-2zO1u4iPhZz9k7ysuDaJL1FUn4SHzm78ZF6/0Q4yFR2VP3NW0JwRpw/4cWqEM6ye3pDf8Zr9lVPBvsX4uK315A==} - peerDependencies: - typescript: ^5.5.0 || ^6.0.0 - - twoslash@0.3.9: - resolution: {integrity: sha512-rDclk+OtzuTX+tnea7DYLCkqGQ3eP0IyfD+kzUJ7t46X/NzlaxwrhecmEBNuSCuEn3V+n1PhcjUUQQ7gUJzX5Q==} - peerDependencies: - typescript: ^5.5.0 || ^6.0.0 - type-fest@5.9.0: resolution: {integrity: sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==} engines: {node: '>=20'} @@ -4986,9 +4759,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ulidx@2.4.1: resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==} engines: {node: '>=16'} @@ -5159,11 +4929,6 @@ packages: jsdom: optional: true - vue-resize@2.0.0-alpha.1: - resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==} - peerDependencies: - vue: ^3.0.0 - vue@3.5.40: resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} peerDependencies: @@ -6049,10 +5814,6 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.1.1': - dependencies: - '@floating-ui/core': 1.8.0 - '@floating-ui/dom@1.8.0': dependencies: '@floating-ui/core': 1.8.0 @@ -6993,14 +6754,6 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@shikijs/core@4.4.2': - dependencies: - '@shikijs/primitive': 4.4.2 - '@shikijs/types': 4.4.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - hast-util-to-html: 9.0.5 - '@shikijs/core@4.4.3': dependencies: '@shikijs/primitive': 4.4.3 @@ -7009,52 +6762,27 @@ snapshots: '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.4.2': - dependencies: - '@shikijs/types': 4.4.2 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - '@shikijs/engine-javascript@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.4.2': - dependencies: - '@shikijs/types': 4.4.2 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.4.2': - dependencies: - '@shikijs/types': 4.4.2 - '@shikijs/langs@4.4.3': dependencies: '@shikijs/types': 4.4.3 - '@shikijs/primitive@4.4.2': - dependencies: - '@shikijs/types': 4.4.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - '@shikijs/primitive@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/themes@4.4.2': - dependencies: - '@shikijs/types': 4.4.2 - '@shikijs/themes@4.4.3': dependencies: '@shikijs/types': 4.4.3 @@ -7064,45 +6792,11 @@ snapshots: '@shikijs/core': 4.4.3 '@shikijs/types': 4.4.3 - '@shikijs/twoslash@4.4.2(typescript@6.0.2)': - dependencies: - '@shikijs/core': 4.4.2 - '@shikijs/types': 4.4.2 - twoslash: 0.3.9(typescript@6.0.2) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@shikijs/types@4.4.2': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/vitepress-twoslash@4.4.2(typescript@6.0.2)': - dependencies: - '@shikijs/twoslash': 4.4.2(typescript@6.0.2) - floating-vue: 5.2.2(vue@3.5.40(typescript@6.0.2)) - lz-string: 1.5.0 - magic-string: 1.2.3 - markdown-it: 14.3.1 - mdast-util-from-markdown: 2.0.3 - mdast-util-gfm: 3.1.0 - mdast-util-to-hast: 13.2.1 - ohash: 2.0.12 - shiki: 4.4.2 - twoslash: 0.3.9(typescript@6.0.2) - twoslash-vue: 0.3.9(typescript@6.0.2) - vue: 3.5.40(typescript@6.0.2) - transitivePeerDependencies: - - '@nuxt/kit' - - supports-color - - typescript - '@shikijs/vscode-textmate@10.0.2': {} '@sindresorhus/is@7.2.0': {} @@ -7275,10 +6969,6 @@ 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/docker-modem@3.0.6': @@ -7313,8 +7003,6 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/ms@2.1.0': {} - '@types/node@18.19.130': dependencies: undici-types: 5.26.5 @@ -7344,13 +7032,6 @@ snapshots: dependencies: '@types/node': 25.9.1 - '@typescript/vfs@1.6.4(typescript@6.0.2)': - dependencies: - debug: 4.4.3 - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - '@ungap/structured-clone@1.4.0': {} '@vitejs/plugin-vue@6.0.8(vite@8.2.2(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.19.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.2))': @@ -7400,12 +7081,6 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.1 - '@volar/language-core@2.4.28': - dependencies: - '@volar/source-map': 2.4.28 - - '@volar/source-map@2.4.28': {} - '@vue/compiler-core@3.5.40': dependencies: '@babel/parser': 7.29.8 @@ -7449,16 +7124,6 @@ snapshots: '@vue/devtools-shared@8.2.1': {} - '@vue/language-core@3.3.11': - dependencies: - '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.40 - '@vue/shared': 3.5.42 - alien-signals: 3.2.1 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - picomatch: 4.0.5 - '@vue/reactivity@3.5.40': dependencies: '@vue/shared': 3.5.40 @@ -7567,8 +7232,6 @@ snapshots: - utf-8-validate - vitest - alien-signals@3.2.1: {} - ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -7611,8 +7274,6 @@ snapshots: - bare-buffer - react-native-b4a - argparse@2.0.1: {} - asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -7740,8 +7401,6 @@ snapshots: character-entities-legacy@3.0.0: {} - character-entities@2.0.2: {} - chevrotain@10.5.0: dependencies: '@chevrotain/cst-dts-gen': 10.5.0 @@ -7835,10 +7494,6 @@ snapshots: dependencies: ms: 2.1.3 - decode-named-character-reference@1.3.0: - dependencies: - character-entities: 2.0.2 - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -7908,8 +7563,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 - entities@4.5.0: {} - entities@7.0.1: {} entities@8.0.0: {} @@ -7982,8 +7635,6 @@ snapshots: escape-string-regexp@2.0.0: {} - escape-string-regexp@5.0.0: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -8058,12 +7709,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - floating-vue@5.2.2(vue@3.5.40(typescript@6.0.2)): - dependencies: - '@floating-ui/dom': 1.1.1 - vue: 3.5.40(typescript@6.0.2) - vue-resize: 2.0.0-alpha.1(vue@3.5.40(typescript@6.0.2)) - focus-trap@8.2.2: dependencies: tabbable: 6.5.0 @@ -8384,10 +8029,6 @@ snapshots: lilconfig@2.1.0: {} - linkify-it@5.0.2: - dependencies: - uc.micro: 2.1.0 - lodash.camelcase@4.3.0: {} lodash@4.17.21: {} @@ -8396,12 +8037,8 @@ snapshots: long@5.3.2: {} - longest-streak@3.1.0: {} - lru-cache@10.4.3: {} - lz-string@1.5.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8412,103 +8049,6 @@ snapshots: mark.js@8.11.1: {} - markdown-it@14.3.1: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.2 - mdurl: 2.1.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - markdown-table@3.0.4: {} - - mdast-util-find-and-replace@3.0.2: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - 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-gfm-autolink-literal@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.2 - micromark-util-character: 2.1.1 - - mdast-util-gfm-footnote@2.1.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-strikethrough@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-table@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@2.0.0: - dependencies: - '@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-gfm@3.1.0: - dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 - 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.5 @@ -8521,159 +8061,25 @@ snapshots: 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 - - mdurl@2.1.0: {} - merge2@1.4.1: {} - 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 - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8735,8 +8141,6 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.4 - muggle-string@0.4.1: {} - nan@2.28.0: optional: true @@ -8761,8 +8165,6 @@ snapshots: obug@2.1.4: {} - ohash@2.0.12: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -8837,8 +8239,6 @@ snapshots: patch-console@2.0.0: {} - path-browserify@1.0.1: {} - path-expression-matcher@1.6.2: {} path-key@3.1.1: {} @@ -9002,8 +8402,6 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 - punycode.js@2.3.1: {} - pure-rand@8.4.2: {} quansync@1.0.0: {} @@ -9308,17 +8706,6 @@ snapshots: shell-quote@1.10.0: {} - shiki@4.4.2: - dependencies: - '@shikijs/core': 4.4.2 - '@shikijs/engine-javascript': 4.4.2 - '@shikijs/engine-oniguruma': 4.4.2 - '@shikijs/langs': 4.4.2 - '@shikijs/themes': 4.4.2 - '@shikijs/types': 4.4.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - shiki@4.4.3: dependencies: '@shikijs/core': 4.4.3 @@ -9632,33 +9019,12 @@ snapshots: tweetnacl@0.14.5: {} - twoslash-protocol@0.3.9: {} - - twoslash-vue@0.3.9(typescript@6.0.2): - dependencies: - '@vue/language-core': 3.3.11 - twoslash: 0.3.9(typescript@6.0.2) - twoslash-protocol: 0.3.9 - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - twoslash@0.3.9(typescript@6.0.2): - dependencies: - '@typescript/vfs': 1.6.4(typescript@6.0.2) - twoslash-protocol: 0.3.9 - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - type-fest@5.9.0: dependencies: tagged-tag: 1.0.0 typescript@6.0.2: {} - uc.micro@2.1.0: {} - ulidx@2.4.1: dependencies: layerr: 3.0.0 @@ -9818,10 +9184,6 @@ snapshots: transitivePeerDependencies: - msw - vue-resize@2.0.0-alpha.1(vue@3.5.40(typescript@6.0.2)): - dependencies: - vue: 3.5.40(typescript@6.0.2) - vue@3.5.40(typescript@6.0.2): dependencies: '@vue/compiler-dom': 3.5.40 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 621f4a3..75135a7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,7 +25,6 @@ catalog: "@fontsource-variable/ibm-plex-sans": 5.3.0 "@fontsource/ibm-plex-mono": 5.3.0 "@playwright/test": 1.63.0 - "@shikijs/vitepress-twoslash": 4.4.2 "@foldkit/ui": 0.157.0 "@foldkit/vite-plugin": 0.20.1 "@tailwindcss/vite": 4.3.3
    +
    -

    Triplex

    -

    A temporal fact database for TypeScript.

    -

    - Store facts. Query relationships. Derive state. Keep the complete history.
    - Bitemporal facts, Datalog, and typed, content-addressed configuration—built on Effect. -

    +

    Triplex

    +

    The database that remembers why.

    +

    An embedded fact database for TypeScript back-office systems.

    +

    Every write records who made it, which versioned rules governed it, and when it was true. Audit questions and open work become queries instead of investigations.

    + +```sh +npm install @triplex-build/triplex effect@4.0.0-rc.112 +``` +
    +

    Pre-1.0 · Built on Effect 4 (effect@4.0.0-rc.112; Effect 3 is not compatible) · In-memory and SQLite supported · PostgreSQL is a production candidate · Maturity contract

    -
    + + +
    -

    Why Triplex?

    -

    Your application should be able to explain itself.

    -

    Current state tells you what is true now. Explaining why often means piecing together database rows, audit logs, policy versions, and background jobs. Those systems rarely share the same view of time.

    -

    Triplex connects facts, history, configuration, and derived state through a shared identity model and provenance chain. You can trace an answer back to the data and the versioned rules behind it.

    -
    -
    -
    -

    What did we know?

    -

    Read facts as they were recorded at a point in time, or as they were valid in the world. Corrections preserve the assertion history.

    -
    -
    -

    Which rules applied?

    -

    Pin a write to an immutable configuration release. Keep the exact schema and policy version available after the live configuration changes.

    -
    -
    -

    What needs to happen?

    -

    Query relationships and missing evidence to derive work. Your application decides how to turn those results into tasks and workflows.

    +

    The problem

    +

    “Why?” is the most expensive question a back-office system gets asked.

    +

    An auditor asks: was Maria trained for the Harbor site on the day she started there? In a typical stack, the evidence is spread across systems that never shared an identity model or a clock.

    -
    -

    Built for domains where history and rules matter: compliance, onboarding, eligibility, entitlements, and agent-driven workflows.

    + +| Where the evidence usually lives | What it can no longer tell you | +| -------------------------------- | ----------------------------------------------------------------------- | +| Application database | The training row was corrected in place, so the original belief is gone | +| Config service or repository | The placement rules have changed since. Which version applied? | +| Audit log | Who acted, but not the facts they saw or the rules they were held to | +| Job queue | Whether a training task should have existed, and when it should reopen | + +

    Triplex keeps facts, versioned rules, the causal journal, and derived work in one store with one identity model and one clock. The auditor’s question becomes a query.

    -
    +
    -

    In-memory data explorer

    -

    Browse Triplex like a spreadsheet.

    -

    Open a real browser-local database, switch between configuration-managed and runtime-discovered entity types, edit typed facts as a form or JSON, and inspect the semantic diff before committing an attributed transaction.

    -Launch the explorer -
    -
    -
    -

    Grid-first

    -

    Scan entity types, rows, and reflected attributes in a dense Airtable-like view with stable cursor pagination.

    +

    One scenario, start to finish

    +

    Maria, the Harbor site, and a certificate that was wrong.

    +

    Maria is trained for Harbor from March 1 to June 1 and placed there on March 2 under release hr-2026.1. The rules then change: hr-2026.2 requires a supervisor on every placement. Later, an auditor finds that the certificate actually began on March 5.

    +

    After the scenario runs, the database’s own journal reads:

    -
    -

    JSON when you need it

    -

    Drop into typed JSON for exact intervals, multiple values, blobs, or attributes that have not been configured yet.

    + +
    + + +
    -
    -

    Review before commit

    -

    Compare current and proposed facts by attribute, then write the entire change as one Triplex transaction.

    + +
    +
    +

    00 · Record

    +

    One command, one atomic transaction.

    +

    Dana’s placement records who acted, a command ID that makes retries safe, a correlation ID, and the release that governed the write. The constraints come from that same release and are checked in the same transaction.

    +

    Nothing is overwritten. The auditor’s correction retracts the old belief and asserts the new one, and the journal keeps both.

    +Transactions and the journal
    +
    + +<<< @/snippets/home/site-safety/record.ts#place +
    +
    -
    +
    -

    Facts → queries → work

    -

    From a submission to an open grading task.

    -

    A student submits a quiz. Until a grade exists, that submission implies work. Here is how the facts and query fit together.

    +

    Three questions, three queries

    +

    Ask what you knew, which rules applied, and what happens next.

    +

    Each answer below is the real output of the code beside it.

    -
    +
    -

    01 · Facts

    -

    Record what happened.

    -

    Assert the submission’s relationships in one atomic transaction, with the actor, command, and governing configuration recorded alongside it.

    -Explore transactions +

    01 · What did we know?

    +

    Two clocks: when it was true, and when you learned it.

    +

    validAt is the day in question. recordedAt is the instant of Dana’s receipt. At that instant, the record said Maria was trained. Today, after the correction, it says she was not.

    +

    Both answers can be proven. Dana acted in good faith on a record that was later corrected.

    +Recorded time and valid time
    -<<< @/snippets/home/facts.ts{ts twoslash} +<<< @/snippets/home/site-safety/what-we-knew.ts + +

    Output

    + +<<< @/snippets/home/site-safety/output/what-we-knew.json
    -
    +
    -

    02 · Datalog

    -

    Ask what still needs doing.

    -

    Join facts by shared variables and find submissions without a grade. Once a grade is present, the submission no longer matches.

    -

    A derivation can track changes to these results and retain their source provenance, so your application can reconcile its grading tasks.

    -Explore derived facts +

    02 · Which rules applied?

    +

    Every receipt is pinned to an immutable release.

    +

    The live ref has moved to hr-2026.2, but Dana’s placement still points at hr-2026.1, the content-addressed release that governed it.

    +

    The same kind of command under today’s rules is rejected atomically. The rejected command writes no facts, no receipt, and no journal entry.

    +Releases, refs, and rollback
    -<<< @/snippets/home/query.ts{ts twoslash} +<<< @/snippets/home/site-safety/which-rules.ts -
    -
    +

    Output

    -
    +<<< @/snippets/home/site-safety/output/which-rules.json + +

    The same command under hr-2026.2

    + +<<< @/snippets/home/site-safety/output/rejected.json -
    -
    -

    Configuration & versioning

    -

    Version the rules alongside the facts.

    -

    Schemas, forms, policies, and routines change. Triplex stores their typed configuration graph as immutable, content-addressed releases, so a new deployment does not erase the rules that governed an earlier decision.

    + -
    +
    -

    03 · Typed configuration

    -

    Define once. Publish a release.

    -

    Attributes define identity and value type. Entity types define how those attributes are used, including requiredness and uniqueness.

    -

    Commit those definitions with your application’s other configuration into one release, and point live at its snapshot.

    -Explore configuration +

    03 · What needs to happen?

    +

    Open work is a query over facts.

    +

    The obligation is a Datalog derivation: a placed worker without current training for that site.

    +

    On March 3, the corrected record exposes a gap, so the task is open. On April 1 nothing is open, and the evaluation reports when that can next change. On June 1 the training expires and the task reopens, even though nobody wrote anything.

    +

    recheckOn comes from nextTemporalBoundary. Schedule a wake-up for that date instead of polling.

    +Derivations and reconciliation
    -<<< @/snippets/home/ontology.ts{ts twoslash} +<<< @/snippets/home/site-safety/open-work.ts + +

    Output for March 3, April 1, and June 1

    + +<<< @/snippets/home/site-safety/output/open-work.json
    +
    + +
    +
    +

    Derived work

    +

    Stop enqueuing obligations. Reconcile them.

    +

    Most back-office bugs come down to a task that should exist but doesn’t, or one that exists but shouldn’t. Event handlers create work one step at a time and drift out of sync with the data. A derivation states what should be true, and reconciliation reports the difference.

    +
    + +| | Event handlers that enqueue tasks | Triplex derivations | +| ----------------- | --------------------------------------- | ------------------------------------------------------------------ | +| Source of truth | Whatever each handler remembered to do | A Datalog query over facts, pinned to a release | +| Late corrections | Missed unless someone re-runs a handler | Appear on the next evaluation, even for past dates | +| Expiring evidence | A separate cron job per rule | `nextTemporalBoundary` says exactly when to re-evaluate | +| Duplicates | Hand-built idempotency | Stable, content-addressed candidate IDs | +| Explanation | Log lines | Each candidate carries its source facts and assertion transactions | +| Change | Diff it yourself | `Derivation.reconcile` returns `added`, `removed`, and `changed` |
    -

    Immutable releases

    -

    Content determines identity. Equal nodes deduplicate, and unchanged revisions are shared between releases. Each snapshot preserves an exact configuration graph.

    +

    Triplex owns

    +

    Facts, history, and the temporal read basis. Atomic transactions, command receipts, and the ordered journal. Release identity and constraint enforcement. Derivation candidates, provenance, and reconciliation diffs. Consumer checkpoints and wake-up boundaries.

    -

    Movable refs

    -

    Names such as test and live point to snapshots. Promote a release or point back to an earlier one without copying configuration. Moving a ref does not undo operational writes.

    +

    Your application owns

    +

    Domain vocabulary and entity IDs. Authentication, authorization, and HTTP handlers. What a release contains and when live moves. What each reconciliation diff means: a ticket, an email, a workflow. Timer delivery and retries.

    -

    Pinned provenance

    -

    Attach the release’s snapshot ID to a transaction with configSnapshot. Later, inspect the exact version that governed the write, even after live moves.

    +

    See it end to end

    +

    The compliance host example adds durable task occurrences, a journal consumer with a checkpoint, a hypothetical preview, and a scheduler that reopens work at expiry. Read Host integration for the full boundary.

    +
    +
    +
    + +
    +
    +

    Modeling

    +

    From a back-office domain to Triplex primitives.

    +

    Most back-office systems are built from the same parts. This is where each part goes.

    + +| In your domain | In Triplex | In the scenario | +| --------------------------------------- | ---------------------------------------------------------------------- | -------------------------------- | +| Things: workers, sites, cases, accounts | Stable `EntityId`s and an `EntityType` per kind | `worker:maria`, `Worker` | +| Properties and relationships | Global `Attribute` keys, with `Attribute.ref` for relationships | `:placement/site` | +| Schemas, policies, forms | Config nodes committed as an immutable release, pointed to by `live` | `hr-2026.1`, `hr-2026.2` | +| User and system actions | One `transact` per command with `actor`, `commandId`, `configSnapshot` | `placement:create:maria-harbor` | +| Evidence that starts or expires | `validFrom` and `validTo` on the assertion | Training valid March 1 to June 1 | +| Corrections | Retract and assert in one transaction; history is kept | The auditor’s fix | +| Obligations and to-dos | A `Derivation` whose candidates you reconcile | `task.site-safety-training` | +| Tickets, notifications, SLAs | Your code, driven by reconciliation diffs and wake-up boundaries | Host-owned | +| Audit questions | A temporal basis, command receipts, and the journal | Questions 01 and 02 | + +

    Read the full modeling guide

    +
    + +
    +
    +

    Deployment

    +

    A library, not another server.

    +

    Triplex runs inside your process as Effect layers. Switching storage changes the layer you provide, and the rest of the program stays the same. On PostgreSQL, Triplex can share your application’s transaction, so your own rows, Triplex facts, the journal, and an outbox row commit or roll back together.

    +<<< @/snippets/home/layers.ts + +| Runtime | Package | Status | +| -------------------------- | --------------------------------- | ------------------------------- | +| In-memory KV | `@triplex-build/triplex` | Supported: tests, browser, edge | +| SQLite | `@triplex-build/triplex-sqlite` | Supported durable baseline | +| PostgreSQL | `@triplex-build/triplex-postgres` | Pre-1.0 production candidate | +| Cloudflare Durable Objects | private workspace package | Experimental | +| FoundationDB | private workspace package | Experimental | + +

    Also in the box: a JSON-first CLI for agents, a browser data explorer, a configuration-derived HTTP API with OpenAPI, and an in-browser playground.

    +
    + +
    +
    +

    Fit

    +

    When to use Triplex, and when not to.

    +
    +
    +
    +

    Good fit

    +

    Compliance and onboarding, eligibility and entitlements, policy engines, case management, and back-office workflows. Any domain where “as of when?” and “under which rules?” come up regularly. Also agent-driven systems that need a durable world model with an attributed record of every change.

    +
    +
    +

    Not a fit

    +

    High-volume telemetry, blob storage, caches, and queues. Also workloads that only need a relational table and never ask about history or provenance. Triplex is a system of record for facts, not a replacement for every table.

    +
    +
    +

    How it relates

    +

    Datomic and XTDB share the model of immutable facts, Datalog, and time travel; Triplex embeds in TypeScript and adds versioned rules and reconciled work. Event sourcing gives you history, but you hand-write every projection. Workflow engines run durable code; Triplex decides what work should exist, and the two compose.

    +
    +