diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts index 4afc660..1d4a382 100644 --- a/cli/src/commands/init.ts +++ b/cli/src/commands/init.ts @@ -201,6 +201,17 @@ async function initSelfHosted( process.exit(1); } + // 2.5 build D1 for KBDB Base (atomic universal table). Free on Workers Free, no credit card + // (kbdb-base SDD Q4). idempotent: reuse if exists. + let d1DatabaseId = ''; + try { + process.stdout.write(chalk.gray(' → D1 arcrun-kbdb...')); + d1DatabaseId = await cf.ensureD1Database('arcrun-kbdb'); + console.log(chalk.green(' ✓')); + } catch (e) { + console.log(chalk.yellow(`\n ⚠ D1 build failed (${e instanceof Error ? e.message : e}); KBDB Base 暫不可用,可 acr update 重試`)); + } + // 3. 查 workers.dev subdomain(cypher-executor WORKER_SUBDOMAIN 用) let workerSubdomain = ''; try { @@ -212,7 +223,7 @@ async function initSelfHosted( // 4. 下載 repo 部署物(含預編譯 wasm)+ 注入 KV id + wrangler deploy 全部 Worker console.log(chalk.gray('\n → 下載部署物 + 部署 Worker(從 GitHub 拉預編譯 wasm,用你的 CF token 部署)...')); - const deployCtx: DeployContext = { accountId, apiToken: cfApiToken, workerSubdomain, kvNamespaceIds }; + const deployCtx: DeployContext = { accountId, apiToken: cfApiToken, workerSubdomain, kvNamespaceIds, d1DatabaseId }; const deploy = await downloadAndDeploy(deployCtx); const cypherUrl = deploy.cypherExecutorUrl ?? (workerSubdomain ? `https://arcrun-cypher-executor.${workerSubdomain}.workers.dev` : ''); diff --git a/cli/src/commands/recipe.ts b/cli/src/commands/recipe.ts index 1b78b63..793f712 100644 --- a/cli/src/commands/recipe.ts +++ b/cli/src/commands/recipe.ts @@ -6,7 +6,7 @@ import chalk from 'chalk'; import ora from 'ora'; import { readFileSync, existsSync } from 'node:fs'; -import { loadConfig, getCypherExecutorUrl } from '../lib/config.js'; +import { loadConfig, getCypherExecutorUrl, DEFAULT_PUBLIC_LIBRARY_URL } from '../lib/config.js'; import { obtainExposureConsent } from '../lib/exposure-warning.js'; import yaml from 'js-yaml'; @@ -22,6 +22,9 @@ interface RecipeYaml { } interface RecipeDefinition { + uuid?: string; // UUID 身份模型(kbdb-base §7.5.5) + author?: string; + derived_from?: string; canonical_id: string; hash_id: string; display_name?: string; @@ -237,3 +240,151 @@ export async function cmdRecipeDelete(id: string): Promise { process.exit(1); } } + +// ── 公庫互動(kbdb-base §7.5,薄殼:只呼叫 API + 格式化,無業務邏輯)───────────────── + +interface PublicRecipeSummary { + uuid?: string; + canonical_id: string; + author?: string; + display_name?: string; + description?: string; + market_stat?: { success_count: number; failure_count: number } | null; +} + +/** acr recipe search — 搜尋公庫(GET /public-recipes?q=)。落空回創作引導(§7.5.6)。*/ +export async function cmdRecipeSearch(query: string): Promise { + const spinner = ora(`搜尋公庫「${query}」`).start(); + try { + const url = `${DEFAULT_PUBLIC_LIBRARY_URL}/public-recipes?q=${encodeURIComponent(query)}`; + const res = await fetch(url); + const data = await res.json() as + | { found: true; recipes: PublicRecipeSummary[]; count: number } + | { found: false; query: string; hint: string }; + spinner.stop(); + + if (!data.found) { + console.log(chalk.yellow(`\n 公庫無符合「${query}」的 recipe。`)); + console.log(chalk.gray(` ${data.hint}`)); + console.log(chalk.gray(' 做一個:建 recipe YAML → acr recipe push(私庫)→ acr recipe submit-p(投稿成為作者)。\n')); + return; + } + + console.log(chalk.bold(`\n 公庫 recipes(${data.count} 個,同名可多作者)\n`)); + for (const r of data.recipes) { + const s = r.market_stat; + const stat = s ? chalk.gray(` ✓${s.success_count}/✗${s.failure_count}`) : chalk.gray(' (無市場數據)'); + console.log(` • ${chalk.cyan(r.canonical_id.padEnd(20))} ${chalk.magenta('@' + (r.author ?? '?'))}${stat} ${r.display_name ?? ''}`); + if (r.description) console.log(` ${chalk.gray(r.description)}`); + } + console.log(chalk.gray('\n 取用:acr recipe pull [--author=]\n')); + } catch (e) { + spinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`)); + process.exit(1); + } +} + +/** acr recipe pull [--author] — 從公庫取一份 recipe 寫進自己私庫。*/ +export async function cmdRecipePull(canonicalId: string, author?: string): Promise { + const config = loadConfig(); + if (!config.api_key) { + console.error(chalk.red('缺少 API Key,請先執行 acr init')); + process.exit(1); + } + const spinner = ora(`從公庫取 recipe「${canonicalId}」${author ? `(@${author})` : ''}`).start(); + try { + // 1. 從公庫取全文(不指定 author → 公庫回市場最佳版本)。 + const q = author ? `?author=${encodeURIComponent(author)}` : ''; + const pubRes = await fetch(`${DEFAULT_PUBLIC_LIBRARY_URL}/public-recipes/${encodeURIComponent(canonicalId)}${q}`); + const pub = await pubRes.json() as + | { found: true; recipe: RecipeDefinition & { uuid?: string; author?: string }; market_stat?: unknown } + | { found: false; canonical_id: string; hint: string }; + + if (!pub.found) { + spinner.stop(); + console.log(chalk.yellow(`\n 公庫無 recipe「${canonicalId}」。`)); + console.log(chalk.gray(` ${pub.hint}\n`)); + return; + } + + // 2. 寫進自己私庫(POST /recipes,帶 derived_from 溯源 + 種子級同意:pull 公庫公共資料非新暴露)。 + const r = pub.recipe; + const executorUrl = getCypherExecutorUrl(config); + const installRes = await fetch(`${executorUrl}/recipes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Arcrun-API-Key': config.api_key }, + body: JSON.stringify({ + ...r, + derived_from: r.uuid, // 溯源:私庫這份來自公庫哪個 uuid + exposure_consent: { + confirmed_by_human: true, + understood: `pull from public library: ${canonicalId}`, + confirmed_at: new Date().toISOString(), + }, + }), + }); + const inst = await installRes.json() as { success: boolean; recipe?: RecipeDefinition; error?: string }; + if (!inst.success) { + spinner.fail(chalk.red(`寫入私庫失敗:${inst.error ?? '未知錯誤'}`)); + process.exit(1); + } + spinner.succeed(chalk.green(`✓ recipe「${canonicalId}」${author ? `(@${author})` : ''} 已拉進私庫`)); + console.log(chalk.gray(` 在 workflow 用 component: ${canonicalId}\n`)); + } catch (e) { + spinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`)); + process.exit(1); + } +} + +/** acr recipe submit-p — 把私庫某 recipe 投稿到公庫(新增作者版本,需暴露同意)。*/ +export async function cmdRecipeSubmitP(canonicalId: string, author?: string): Promise { + const config = loadConfig(); + if (!config.api_key) { + console.error(chalk.red('缺少 API Key,請先執行 acr init')); + process.exit(1); + } + const executorUrl = getCypherExecutorUrl(config); + + // 1. 從私庫取這份 recipe 全文。 + const myRes = await fetch(`${executorUrl}/recipes/${encodeURIComponent(canonicalId)}`, { + headers: { 'X-Arcrun-API-Key': config.api_key }, + }); + const my = await myRes.json() as { success: boolean; recipe?: RecipeDefinition & { uuid?: string }; error?: string }; + if (!my.success || !my.recipe) { + console.error(chalk.red(`私庫找不到 recipe「${canonicalId}」:${my.error ?? ''}`)); + process.exit(1); + } + + // 2. 投稿到公庫 = 暴露面 → 取得人類明示同意(mindset §6)。 + const consent = await obtainExposureConsent({ + kind: 'recipe', + resourceName: canonicalId, + destination: `公庫(${DEFAULT_PUBLIC_LIBRARY_URL})`, + }); + if (!consent) process.exit(1); + + const spinner = ora(`投稿 recipe「${canonicalId}」到公庫`).start(); + try { + const res = await fetch(`${DEFAULT_PUBLIC_LIBRARY_URL}/recipes/submit`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...my.recipe, + author: author ?? my.recipe.author, + derived_from: my.recipe.derived_from ?? my.recipe.uuid, + submitter: author ?? config.api_key, + exposure_consent: consent, + }), + }); + const data = await res.json() as { success: boolean; recipe?: { uuid?: string; author?: string }; error?: string }; + if (!data.success) { + spinner.fail(chalk.red(`投稿失敗:${data.error ?? '未知錯誤'}`)); + process.exit(1); + } + spinner.succeed(chalk.green(`✓ recipe「${canonicalId}」已投稿公庫(新增作者版本 @${data.recipe?.author ?? '?'})`)); + console.log(chalk.gray(' 別人能搜到並 pull;市場數據累積後決定它被不被選用。\n')); + } catch (e) { + spinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`)); + process.exit(1); + } +} diff --git a/cli/src/index.ts b/cli/src/index.ts index 74d61c8..e2ad091 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -17,7 +17,7 @@ import { cmdPush } from './commands/push.js'; import { cmdRun } from './commands/run.js'; import { cmdValidate } from './commands/validate.js'; import { cmdParts, cmdPartsScaffold, cmdPartsPublish } from './commands/parts.js'; -import { cmdRecipePush, cmdRecipeList, cmdRecipeDelete } from './commands/recipe.js'; +import { cmdRecipePush, cmdRecipeList, cmdRecipeDelete, cmdRecipeSearch, cmdRecipePull, cmdRecipeSubmitP } from './commands/recipe.js'; import { cmdList } from './commands/list.js'; import { cmdLogs } from './commands/logs.js'; import { cmdUpdate } from './commands/update.js'; @@ -121,6 +121,21 @@ recipeCmd .command('delete ') .description('刪除 recipe(canonical_id 或 rec_hash)') .action((id: string) => cmdRecipeDelete(id)); +// 公庫互動(kbdb-base §7.5) +recipeCmd + .command('search ') + .description('搜尋公庫 recipe(同名可多作者,附市場數據)') + .action((query: string) => cmdRecipeSearch(query)); +recipeCmd + .command('pull ') + .description('從公庫取一份 recipe 寫進自己私庫') + .option('--author ', '指定作者版本(不指定取市場最佳)') + .action((canonicalId: string, opts: { author?: string }) => cmdRecipePull(canonicalId, opts.author)); +recipeCmd + .command('submit-p ') + .description('把私庫某 recipe 投稿到公庫(新增作者版本,需暴露同意)') + .option('--author ', '署名作者(預設用 recipe 既有 author)') + .action((canonicalId: string, opts: { author?: string }) => cmdRecipeSubmitP(canonicalId, opts.author)); // acr auth-recipe list / info / scaffold const authRecipeCmd = program.command('auth-recipe').description('第三方服務認證 Recipe(新增服務整合)'); diff --git a/cli/src/lib/cf-api.ts b/cli/src/lib/cf-api.ts index 7e07529..d5af2c7 100644 --- a/cli/src/lib/cf-api.ts +++ b/cli/src/lib/cf-api.ts @@ -144,6 +144,25 @@ export class CfAccountClient { const result = await this.cf<{ subdomain: string }>('/workers/subdomain'); return result.subdomain; } + + // D1 (KBDB Base). Free on Workers Free plan, no credit card (kbdb-base Q4 verified). + async listD1Databases(): Promise> { + const result = await this.cf>('/d1/database?per_page=100'); + const map = new Map(); + for (const db of result) map.set(db.name, db.uuid); + return map; + } + + async ensureD1Database(name: string, existing?: Map): Promise { + const known = existing ?? (await this.listD1Databases()); + const found = known.get(name); + if (found) return found; + const result = await this.cf<{ uuid: string; name: string }>( + '/d1/database', + { method: 'POST', body: JSON.stringify({ name }) }, + ); + return result.uuid; + } } /** AES-GCM 加密 credential(與 cypher-executor credential-injector 解密邏輯對應)*/ diff --git a/cli/src/lib/config.ts b/cli/src/lib/config.ts index 656a1b4..211daa5 100644 --- a/cli/src/lib/config.ts +++ b/cli/src/lib/config.ts @@ -67,6 +67,15 @@ const ENV_MAP: Record = { */ export const DEFAULT_MCP_URL = 'https://mcp.arcrun.dev'; +/** + * 公庫 URL(recipe pull/search/submit-p 的對象,kbdb-base §7.5)。 + * 公庫 = 官方 SaaS cypher(唯一公共真相)。self-hosted 用戶的「私庫」是自己的 cypher + * (getCypherExecutorUrl),但 pull/搜尋/投稿都對著**官方公庫**這個固定 URL。 + * fork 者可用 ARCRUN_PUBLIC_LIBRARY_URL env 覆蓋。 + */ +export const DEFAULT_PUBLIC_LIBRARY_URL = + process.env.ARCRUN_PUBLIC_LIBRARY_URL ?? 'https://cypher.arcrun.dev'; + export function configExists(): boolean { return existsSync(CONFIG_PATH) || findProjectConfig() !== undefined; } diff --git a/cli/src/lib/deploy.ts b/cli/src/lib/deploy.ts index afec5dd..93d0004 100644 --- a/cli/src/lib/deploy.ts +++ b/cli/src/lib/deploy.ts @@ -47,6 +47,7 @@ export interface DeployContext { apiToken: string; workerSubdomain: string; kvNamespaceIds: Record; // title → id + d1DatabaseId?: string; // KBDB Base D1 (arcrun-kbdb); injected into kbdb wrangler.toml } export interface DeployResult { @@ -111,6 +112,23 @@ export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promi } } + // 3.5 KBDB Base: D1 建好後套 migration(建三表 + recipe_stat seed)。 + // 建 D1(cf-api ensureD1Database)只產生空資料庫,schema 要靠這步套。 + // migration 檔來自同一份 tarball(root/kbdb/migrations/0001_base.sql),與 wasm 同源, + // 不依賴本地 CLI 安裝路徑。0001_base.sql 全用 IF NOT EXISTS / INSERT OR IGNORE → 可重複套(idempotent)。 + if (ctx.d1DatabaseId) { + const migPath = join(root, 'kbdb', 'migrations', '0001_base.sql'); + if (existsSync(migPath)) { + try { + await applyD1Migration(ctx, readFileSync(migPath, 'utf8')); + } catch (e) { + failures.push(`D1 migration (${ctx.d1DatabaseId}): ${e instanceof Error ? e.message : String(e)}`); + } + } else { + failures.push(`D1 migration: 部署物缺 kbdb/migrations/0001_base.sql(${migPath})`); + } + } + const cypherExecutorUrl = ctx.workerSubdomain ? `https://arcrun-cypher-executor.${ctx.workerSubdomain}.workers.dev` : undefined; @@ -132,6 +150,30 @@ export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promi }; } +/** + * 對 D1 套 SQL migration(透過 CF API `/d1/database/{id}/query`,非 wrangler)。 + * 用 init 已驗的 ctx.apiToken + accountId;query 端點接受多語句檔,一次送整份 0001_base.sql。 + */ +async function applyD1Migration(ctx: DeployContext, sql: string): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${ctx.accountId}/d1/database/${ctx.d1DatabaseId}/query`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${ctx.apiToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ sql }), + signal: AbortSignal.timeout(60_000), + }); + const json = (await res.json().catch(() => null)) as + | { success?: boolean; errors?: Array<{ message?: string }> } + | null; + if (!res.ok || !json?.success) { + const detail = json?.errors?.map(e => e.message).filter(Boolean).join('; ') || `HTTP ${res.status}`; + throw new Error(detail); + } +} + /** 下載 codeload tarball 解壓到暫存目錄,回傳解壓出的 repo root 路徑。*/ async function downloadRepoTarball(ref: string): Promise { const url = `https://codeload.github.com/${ARCRUN_REPO}/tar.gz/${ref}`; @@ -212,6 +254,14 @@ function injectWranglerConfig(tomlPath: string, ctx: DeployContext): void { ); } + // KBDB Base: inject user's D1 database_id into [[d1_databases]] (placeholder in repo toml) + if (ctx.d1DatabaseId && /database_id\s*=/.test(toml)) { + toml = toml.replace( + /(database_id\s*=\s*")[^"]*(")/, + `$1${ctx.d1DatabaseId}$2`, + ); + } + toml = stripOfficialOnlyBindings(toml); writeFileSync(tomlPath, toml, 'utf8'); diff --git a/cypher-executor/src/actions/webhook-handlers.ts b/cypher-executor/src/actions/webhook-handlers.ts index b4d8809..e24f840 100644 --- a/cypher-executor/src/actions/webhook-handlers.ts +++ b/cypher-executor/src/actions/webhook-handlers.ts @@ -5,6 +5,41 @@ import { graphSchema } from '../lib/schemas'; import { createComponentLoader } from '../lib/component-loader'; import { recordTelemetry } from '../lib/telemetry'; +/** + * kbdb-base §7.1+§7.5.h:一條工作流執行結束後,把這次用到的 recipe 各記一次成功/失敗到 KBDB 市場星數。 + * 判定單位是「工作流執行」(n8n execution):整體成功 → 用到的每個 recipe key +1 成功;整體失敗 → 各 +1 失敗。 + * **key = recipe uuid**(per-uuid,能區分同 canonical 的不同作者版本 §7.5.5;舊資料 fallback canonical_id)。 + * + * fire-and-forget(用 ctx.waitUntil,仿 recordTelemetry):記錄失敗不影響工作流結果。 + * KBDB 端點 POST {KBDB_BASE_URL}/recipe-stats/record { canonical_id, ok, at }—— + * 該欄位名為 canonical_id 但語意已是 recipe key(uuid),KBDB 端只當 stat 的主鍵字串用。 + */ +function recordRecipeStats( + env: Bindings, + recipeKeys: Set, + ok: boolean, + at: number, + ctx?: ExecutionContext, +): void { + if (recipeKeys.size === 0) return; + const base = (env.KBDB_BASE_URL ?? 'https://kbdb.finally.click').replace(/\/$/, ''); + const headers: Record = { 'Content-Type': 'application/json' }; + if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`; + + const promise = Promise.all( + [...recipeKeys].map(key => + fetch(`${base}/recipe-stats/record`, { + method: 'POST', + headers, + body: JSON.stringify({ canonical_id: key, ok, at }), + }).catch(() => undefined), + ), + ).then(() => undefined); + + if (ctx?.waitUntil) ctx.waitUntil(promise); + else void promise; +} + type WebhookRecord = { graph: Record; description: string; @@ -58,6 +93,9 @@ export async function executeWebhookGraph( agent_user_agent: userAgent, }, ctx); + // kbdb-base §7.1:整體成功 → 用到的 recipe 各記成功一次。 + recordRecipeStats(env, executor.usedRecipeKeys, true, Date.now(), ctx); + return { success: true, data: result.data, duration_ms }; } catch (err) { const duration_ms = Date.now() - start; @@ -73,6 +111,12 @@ export async function executeWebhookGraph( agent_user_agent: userAgent, }, ctx); + // kbdb-base §7.1:真錯(非 paused)→ 用到的 recipe 各記失敗一次。 + // paused 是「執行中暫停等 callback」非失敗,不記(resume 後成功才會在那條路徑記成功)。 + if (!isPaused) { + recordRecipeStats(env, executor.usedRecipeKeys, false, Date.now(), ctx); + } + if (err instanceof ExecutionError) { const traceFormatted = err.trace.map(s => ({ node: s.nodeId, diff --git a/cypher-executor/src/graph-executor.ts b/cypher-executor/src/graph-executor.ts index c7d85e3..7f8abbe 100644 --- a/cypher-executor/src/graph-executor.ts +++ b/cypher-executor/src/graph-executor.ts @@ -4,6 +4,7 @@ import { kvSetNodeOutput, kvGetNodeOutput, ExecutionError, WorkflowPaused } from import { injectCredentials } from './actions/credential-injector'; import { tryAuthDispatch } from './actions/auth-dispatcher'; import { expandPromptRecipe } from './lib/recipe-expander'; +import { resolveRecipe } from './routes/recipes'; import { persistPausedRun, isResumablePending, parseRecipeOutput } from './lib/paused-runs'; import { buildMagicVars } from './lib/magic-vars'; import { recordTelemetry } from './lib/telemetry'; @@ -21,6 +22,11 @@ export class GraphExecutor { private apiKey?: string; public recordComponentReference?: (componentId: string, workflowId: string) => Promise; + // kbdb-base §7.1+§7.5.h:本次執行用到的 recipe **key**(uuid 優先,舊資料 fallback canonical_id)。 + // 判定單位是「工作流執行」(n8n execution):執行結束後由 executeWebhookGraph 一次性把這組 key + // 各記成功/失敗到 KBDB 市場星數(per-uuid → 能區分同 canonical 的不同作者版本,§7.5.5)。執行中只收集。 + public readonly usedRecipeKeys = new Set(); + // resumable workflow(SDD: resumable-workflow/design.md) // 暫停時持久化 state 用,需在 execute 進入時設定 private currentGraph?: ExecutionGraph; @@ -286,6 +292,19 @@ export class GraphExecutor { } } + // kbdb-base §7.5.h:收集本次用到的 recipe **uuid**(執行結束後一次性記到 KBDB 市場星數)。 + // 記 per-uuid(非 auth service):投稿/pull 的是 API recipe(帶 uuid),市場數據要能區分 + // 同 canonical 的 Leo 版/John 版(§7.5.5 app-store)。先試 API recipe(有 uuid); + // 無 uuid 的舊資料 fallback canonical_id(向後相容,migration 後自然帶 uuid)。 + if (this.env?.RECIPES) { + try { + const apiRecipe = await resolveRecipe(node.componentId, this.env.RECIPES); + if (apiRecipe) this.usedRecipeKeys.add(apiRecipe.uuid ?? apiRecipe.canonical_id); + } catch { + // 收集失敗不影響執行(成功記錄是輔助資料,非主流程) + } + } + nodeInput = mergedContext; result = await runner(mergedContext); diff --git a/cypher-executor/src/lib/api-recipe-seeds.ts b/cypher-executor/src/lib/api-recipe-seeds.ts index 5705418..8529fa6 100644 --- a/cypher-executor/src/lib/api-recipe-seeds.ts +++ b/cypher-executor/src/lib/api-recipe-seeds.ts @@ -86,9 +86,12 @@ export const API_RECIPE_SEEDS: ApiRecipeSeed[] = [ { canonical_id: 'google_sheets_append', display_name: 'Google Sheets Append', - description: '寫 Sheets。PUT values?valueInputOption=RAW,body 帶 values。auth: google service_account。', + // 壓測階段 12 修正:append 官方 API 是 POST .../values/{range}:append(PUT 是 values.update 覆寫的動詞), + // 種子寫死 PUT 導致每個 self-host 用戶 seed 到壞 recipe(PUT :append → Google 400)。 + // body 形狀屬工作流,泛用種子不寫死欄位 → 由工作流的 _path + body 處理(body_from 機制待 §13.4 補)。 + description: '追加一列到 Sheets。POST .../values/{range}:append?valueInputOption=RAW,body 帶 {values:[[...]]}。auth: google service_account。', endpoint: 'https://sheets.googleapis.com{{_path}}', - method: 'PUT', + method: 'POST', auth_service: 'google_sheets_sa', }, { diff --git a/cypher-executor/src/routes/init-seed.ts b/cypher-executor/src/routes/init-seed.ts index 2be1fb7..b9749f2 100644 --- a/cypher-executor/src/routes/init-seed.ts +++ b/cypher-executor/src/routes/init-seed.ts @@ -20,6 +20,7 @@ import { Hono } from 'hono'; import type { Bindings } from '../types'; import { deriveRecipeHash } from '../lib/hash'; import type { RecipeDefinition, AuthRecipeDefinition } from './recipes'; +import { installRecipeRecord, resolveRecipe } from './recipes'; import { API_RECIPE_SEEDS } from '../lib/api-recipe-seeds'; import { AUTH_RECIPE_SEEDS } from '../lib/auth-recipe-seeds'; @@ -41,8 +42,11 @@ initSeedRouter.post('/init/seed', async (c) => { try { const canonicalId = seed.canonical_id.trim().toLowerCase(); const hashId = await deriveRecipeHash(canonicalId); - const existing = await c.env.RECIPES.get(`recipe:${canonicalId}`, 'json') as RecipeDefinition | null; + // UUID 模型(§7.5.5):種子 author='system'。冪等:已安裝沿用其 uuid,否則新領。 + const existing = await resolveRecipe(canonicalId, c.env.RECIPES); const recipe: RecipeDefinition = { + uuid: existing?.uuid ?? crypto.randomUUID(), + author: existing?.author ?? 'system', canonical_id: canonicalId, hash_id: hashId, display_name: seed.display_name, @@ -54,10 +58,7 @@ initSeedRouter.post('/init/seed', async (c) => { created_at: existing?.created_at ?? now, updated_at: now, }; - await Promise.all([ - c.env.RECIPES.put(`recipe:${canonicalId}`, JSON.stringify(recipe)), - c.env.RECIPES.put(`idx:${hashId}`, canonicalId), - ]); + await installRecipeRecord(c.env.RECIPES, recipe); apiOk++; } catch (e) { apiFail++; diff --git a/cypher-executor/src/routes/recipes.ts b/cypher-executor/src/routes/recipes.ts index 6c0a776..af22f9e 100644 --- a/cypher-executor/src/routes/recipes.ts +++ b/cypher-executor/src/routes/recipes.ts @@ -22,6 +22,12 @@ import type { ExposureConsent } from '../lib/exposure-consent'; export const recipesRouter = new Hono<{ Bindings: Bindings }>(); export interface RecipeDefinition { + // UUID 身份模型(kbdb-base §7.5.5):每個 recipe 一誕生領 uuid = 唯一身份。 + // canonical_id / author / 公私 都是屬性,不是身份。身份(uuid) 與歸屬(author) 分離。 + // 舊 recipe 無 uuid → resolveRecipe / migration 兼容(migration 增量補 uuid,不刪舊 key)。 + uuid?: string; // 唯一身份;舊資料可能缺,讀取時容忍 + author?: string; // 該 uuid 投稿者(誰投誰負責那版市場數據);'system' = init-seed 種子 + derived_from?: string; // 可選溯源:fork 自哪個 uuid(Leo 改 John 版時記 John 的 uuid) canonical_id: string; hash_id: string; // rec_xxxxxxxx display_name?: string; @@ -47,6 +53,36 @@ export interface RecipeDefinition { updated_at: number; } +// ── UUID 身份模型 KV key(kbdb-base §7.5.5)──────────────────────────────────── +// recipe:{uuid} → recipe 本體(唯一身份) +// idx:canonical:{canonical_id} → JSON array of uuid(同 canonical 多作者版本並存,公庫用) +// idx:installed:{canonical_id} → 單一 uuid(本部署執行時用哪個版本;pull/submit 時定) +// idx:{hash_id} → canonical_id(既有 rec_hash 反查,保留) +// 舊資料 recipe:{canonical_id} 不刪,resolveRecipe fallback 讀得到(migration 增量補,不破現況)。 +const kIdxCanonical = (canonicalId: string) => `idx:canonical:${canonicalId}`; +const kIdxInstalled = (canonicalId: string) => `idx:installed:${canonicalId}`; + +/** + * 寫一份 recipe(UUID 身份模型):給定 recipe 已含 uuid → 寫 recipe:{uuid}、 + * 把 uuid 併進 idx:canonical:{canonical_id} 清單、設為本部署 installed(執行時用此版本)、 + * 維護 idx:{hash_id} 反查。private(POST /recipes) 與 public(submit-p) 共用此寫入。 + */ +export async function installRecipeRecord(kv: KVNamespace, recipe: RecipeDefinition): Promise { + const uuid = recipe.uuid!; + const { canonical_id, hash_id } = recipe; + + const listRaw = await kv.get(kIdxCanonical(canonical_id)); + const uuids: string[] = listRaw ? JSON.parse(listRaw) : []; + if (!uuids.includes(uuid)) uuids.push(uuid); + + await Promise.all([ + kv.put(`recipe:${uuid}`, JSON.stringify(recipe)), + kv.put(kIdxCanonical(canonical_id), JSON.stringify(uuids)), + kv.put(kIdxInstalled(canonical_id), uuid), + kv.put(`idx:${hash_id}`, canonical_id), + ]); +} + // POST /recipes — 新增或更新 recipe recipesRouter.post('/recipes', async (c) => { let body: Partial; @@ -63,8 +99,10 @@ recipesRouter.post('/recipes', async (c) => { const hashId = await deriveRecipeHash(canonicalId); const now = Date.now(); - // 讀取現有版本(保留 created_at + 既有同意憑證) - const existing = await c.env.RECIPES.get(`recipe:${canonicalId}`, 'json') as RecipeDefinition | null; + // 私庫(POST /recipes)= 自己地盤,同 canonical 就地更新自己安裝的那份(沿用既有 uuid)。 + // 既有 installed → 沿用其 uuid + created_at;無 → 新領 uuid(首次裝這個 canonical)。 + // 讀取順序:先 UUID 模型(installed→uuid),fallback 舊 key(migration 前的種子)。 + const existing = await resolveRecipe(canonicalId, c.env.RECIPES); // 資料外流警示:recipe 定義資料去向(endpoint)。首次 push 需人類明示同意(公私一視同仁)。 const consentError = checkExposureConsent(body.exposure_consent, existing?.exposure_consent); @@ -73,6 +111,9 @@ recipesRouter.post('/recipes', async (c) => { } const recipe: RecipeDefinition = { + uuid: existing?.uuid ?? crypto.randomUUID(), + author: body.author ?? existing?.author ?? 'local', + derived_from: body.derived_from ?? existing?.derived_from, canonical_id: canonicalId, hash_id: hashId, display_name: body.display_name, @@ -88,16 +129,124 @@ recipesRouter.post('/recipes', async (c) => { updated_at: now, }; - // 寫入兩個 KV key - await Promise.all([ - c.env.RECIPES.put(`recipe:${canonicalId}`, JSON.stringify(recipe)), - c.env.RECIPES.put(`idx:${hashId}`, canonicalId), - ]); - + await installRecipeRecord(c.env.RECIPES, recipe); return c.json({ success: true, recipe }); }); -// GET /recipes/:id — 讀取 recipe(支援 canonical_id 或 rec_hash) +// POST /recipes/submit — 公共庫投稿(submit-p)。kbdb-base SDD §7.2/§7.3。 +// +// 兩套部署模型:self-hosted cypher = 私庫(直接 POST /recipes 寫自己 KV); +// 官方 cypher = 公共庫,外部投稿者把修好的 recipe 送來這個端點。 +// +// app-store / UUID 模型(§7.5.5):submit-p = **新增一個作者版本(領新 uuid)**, +// 不覆蓋同 canonical_id。同 canonical 多作者並存(Leo 版、John 版各自 uuid + 市場數據)。 +// 公共庫 = 暴露面 → 強制 exposure_consent(mindset §6:暴露需人類明示同意)。 +// 投稿者帶的 stat 只當「存證」(誰在何時投了什麼、聲稱打通幾次),寫進 KBDB 一筆 +// recipe_submission entry,**不**併進 recipe-stat 真實計數(避免自報數污染市場數據,§7.3)。 +// 市場信任靠真實使用累積(5.1),不拿投稿者自報數當門檻 → 不造債。 +recipesRouter.post('/recipes/submit', async (c) => { + let body: Partial & { + stat?: { success_count?: number; failure_count?: number }; + submitter?: string; + }; + try { + body = await c.req.json(); + } catch { + return c.json({ success: false, error: 'request body 必須為 JSON' }, 400); + } + + const canonicalId = (body.canonical_id ?? '').trim().toLowerCase(); + if (!canonicalId) return c.json({ success: false, error: 'canonical_id 必填' }, 400); + if (!body.endpoint) return c.json({ success: false, error: 'endpoint 必填' }, 400); + + const hashId = await deriveRecipeHash(canonicalId); + const now = Date.now(); + + // 公共庫投稿一定是暴露 → 需明示同意(無同意直接擋)。投稿是新版本,不沿用既有同意。 + const consentError = checkExposureConsent(body.exposure_consent, undefined); + if (consentError !== null) { + return c.json({ success: false, error: consentError, requires: 'exposure_consent' }, 403); + } + + // app-store 模型:**領新 uuid = 新增作者版本**,不覆蓋既有 canonical(§7.5.5)。 + const recipe: RecipeDefinition = { + uuid: crypto.randomUUID(), + author: body.author ?? body.submitter ?? 'anonymous', + derived_from: body.derived_from, + canonical_id: canonicalId, + hash_id: hashId, + display_name: body.display_name, + description: body.description, + endpoint: body.endpoint, + method: (body.method ?? 'POST').toUpperCase(), + headers: body.headers, + body: body.body, + auth_service: body.auth_service, + credentials_required: body.credentials_required, + exposure_consent: resolveConsentForRecord(body.exposure_consent, undefined), + created_at: now, + updated_at: now, + }; + + // 新增作者版本:寫 recipe:{uuid} + 併進 idx:canonical 清單(同 canonical 多版本並存)。 + // installed 也指向這個新版本(官方部署投稿後預設用最新;market 選擇由 §7.5.5 端點處理)。 + await installRecipeRecord(c.env.RECIPES, recipe); + + // stat 存證:寫一筆 recipe_submission entry 進 KBDB(不當門檻,當法律歸責軌跡)。 + // fire-and-forget:存證失敗不擋投稿成功。 + const kbdbBase = (c.env.KBDB_BASE_URL ?? 'https://kbdb.finally.click').replace(/\/$/, ''); + const evidence = { + content: canonicalId, + entry_type: 'recipe_submission', + metadata_json: JSON.stringify({ + uuid: recipe.uuid, + canonical_id: canonicalId, + author: recipe.author, + submitter: body.submitter ?? 'unknown', + claimed_stat: body.stat ?? null, + submitted_at: now, + }), + }; + const kbdbHeaders: Record = { 'Content-Type': 'application/json' }; + if (c.env.KBDB_INTERNAL_TOKEN) kbdbHeaders['Authorization'] = `Bearer ${c.env.KBDB_INTERNAL_TOKEN}`; + c.executionCtx.waitUntil( + fetch(`${kbdbBase}/entries`, { + method: 'POST', + headers: kbdbHeaders, + body: JSON.stringify(evidence), + }).catch(() => undefined), + ); + + return c.json({ success: true, recipe, evidence_recorded: true }); +}); + +// POST /recipes/migrate-uuid — 一次性 migration:把 migration 前的舊 key recipe:{canonical_id} +// (無 uuid)轉成 UUID 身份模型(§7.5.5)。增量寫、**不刪舊 key**(失敗也不破現況;resolveRecipe +// 本就 fallback 舊 key)。冪等:已有 uuid 的跳過。重跑安全。 +recipesRouter.post('/recipes/migrate-uuid', async (c) => { + const list = await c.env.RECIPES.list({ prefix: 'recipe:' }); + let migrated = 0, skipped = 0; + const errors: string[] = []; + for (const k of list.keys) { + try { + const rec = await c.env.RECIPES.get(k.name, 'json') as RecipeDefinition | null; + if (!rec || !rec.canonical_id) { skipped++; continue; } + if (rec.uuid) { skipped++; continue; } // 已是新模型 + const migrated_recipe: RecipeDefinition = { + ...rec, + uuid: crypto.randomUUID(), + author: rec.author ?? 'system', // 舊種子歸 system + }; + await installRecipeRecord(c.env.RECIPES, migrated_recipe); + migrated++; + } catch (e) { + errors.push(`${k.name}: ${e instanceof Error ? e.message : String(e)}`); + } + } + return c.json({ success: errors.length === 0, migrated, skipped, errors }); +}); + +// GET /recipes/:id — 讀取 recipe(支援 canonical_id / rec_hash / uuid) recipesRouter.get('/recipes/:id', async (c) => { const id = c.req.param('id'); const recipe = await resolveRecipe(id, c.env.RECIPES); @@ -105,42 +254,193 @@ recipesRouter.get('/recipes/:id', async (c) => { return c.json({ success: true, recipe }); }); -// GET /recipes — 列出所有 recipe +// GET /recipes — 列出所有 recipe(本部署 KV 全部版本,含多作者)。 +// prefix recipe: 同時命中 recipe:{uuid}(新)與 recipe:{canonical_id}(migration 前舊 key)。 +// 去重:同 canonical_id 若已有帶 uuid 的版本,捨棄無 uuid 的舊 key 重複項。 recipesRouter.get('/recipes', async (c) => { const list = await c.env.RECIPES.list({ prefix: 'recipe:' }); - const recipes = await Promise.all( - list.keys.map(k => c.env.RECIPES.get(k.name, 'json')) - ); - return c.json({ success: true, recipes: recipes.filter(Boolean), count: recipes.length }); + const all = (await Promise.all( + list.keys.map(k => c.env.RECIPES.get(k.name, 'json') as Promise) + )).filter(Boolean) as RecipeDefinition[]; + + // canonical → 是否已有帶 uuid 的版本 + const hasUuidVersion = new Set(all.filter(r => r.uuid).map(r => r.canonical_id)); + const recipes = all.filter(r => r.uuid || !hasUuidVersion.has(r.canonical_id)); + return c.json({ success: true, recipes, count: recipes.length }); }); -// DELETE /recipes/:id — 刪除 recipe +// ── 公庫只讀端點(kbdb-base §7.5.4,公→私 pull + 瀏覽的後端基礎)────────────────── +// 官方 cypher 開公開只讀(無需 api_key,公庫本就公共)。語意 = 「這是公庫,給 self-hosted pull/瀏覽」, +// 含作者維度 + 市場星數(與內部 /recipes 分開命名,公庫的多作者/排序不污染內部)。 + +/** 從 KBDB 抓 recipe 市場星數(5.1 記的 success/failure)。失敗回 null(端點仍可用,星數缺省)。*/ +async function fetchMarketStat( + env: Bindings, + canonicalId: string, +): Promise<{ success_count: number; failure_count: number } | null> { + try { + const base = (env.KBDB_BASE_URL ?? 'https://kbdb.finally.click').replace(/\/$/, ''); + const headers: Record = {}; + if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`; + const res = await fetch(`${base}/recipe-stats/${encodeURIComponent(canonicalId)}`, { headers }); + if (!res.ok) return null; + const json = await res.json() as { stat?: { success_count?: number; failure_count?: number } }; + if (!json.stat) return null; + return { + success_count: json.stat.success_count ?? 0, + failure_count: json.stat.failure_count ?? 0, + }; + } catch { + return null; + } +} + +// 收集本部署 KV 全部 recipe(去重,與 GET /recipes 同邏輯),給公庫端點共用。 +async function listAllRecipes(kv: KVNamespace): Promise { + const list = await kv.list({ prefix: 'recipe:' }); + const all = (await Promise.all( + list.keys.map(k => kv.get(k.name, 'json') as Promise), + )).filter(Boolean) as RecipeDefinition[]; + const hasUuid = new Set(all.filter(r => r.uuid).map(r => r.canonical_id)); + return all.filter(r => r.uuid || !hasUuid.has(r.canonical_id)); +} + +// GET /public-recipes?q=&limit=&offset= — 搜尋/列出公庫 recipe。 +// 同 canonical_id 回多筆(多作者),各附市場星數,供 CC 依數據選(§7.5.5)。 +// 落空(q 無命中)→ 回 found:false + 創作引導(§7.5.6),不回空陣列乾等。 +recipesRouter.get('/public-recipes', async (c) => { + const q = (c.req.query('q') ?? '').trim().toLowerCase(); + const limit = Math.min(Number(c.req.query('limit') ?? 50), 200); + const offset = Number(c.req.query('offset') ?? 0); + + const all = await listAllRecipes(c.env.RECIPES); + const matched = q + ? all.filter(r => + r.canonical_id.toLowerCase().includes(q) || + (r.display_name ?? '').toLowerCase().includes(q) || + (r.description ?? '').toLowerCase().includes(q)) + : all; + + if (q && matched.length === 0) { + // 落空 = 創作入口(§7.5.6):讓 CC 知道「公庫沒有,可自己做一個成為作者」。 + return c.json({ + found: false, + query: q, + hint: `公庫無符合「${q}」的 recipe。可自行建立並 submit-p 投稿成為作者(app-store 模型)。`, + }); + } + + const page = matched.slice(offset, offset + limit); + const withStats = await Promise.all( + page.map(async r => ({ + uuid: r.uuid, + canonical_id: r.canonical_id, + author: r.author, + display_name: r.display_name, + description: r.description, + market_stat: await fetchMarketStat(c.env, r.uuid ?? r.canonical_id), // §7.5.h per-uuid + })), + ); + return c.json({ found: true, recipes: withStats, count: matched.length }); +}); + +// GET /public-recipes/:canonical_id?author= — 取單一 recipe 全文(pull 用)。 +// 不指定 author → 回市場最佳版本(success_count 最高)。落空 → found:false 創作引導(§7.5.6)。 +recipesRouter.get('/public-recipes/:canonical_id', async (c) => { + const canonicalId = c.req.param('canonical_id').trim().toLowerCase(); + const author = c.req.query('author'); + + const all = await listAllRecipes(c.env.RECIPES); + let versions = all.filter(r => r.canonical_id === canonicalId); + if (author) versions = versions.filter(r => r.author === author); + + if (versions.length === 0) { + return c.json({ + found: false, + canonical_id: canonicalId, + hint: `公庫無 recipe「${canonicalId}」${author ? `(author=${author})` : ''}。可自行建立並 submit-p 投稿成為作者(app-store 模型)。`, + }); + } + + // 多作者 → 選市場最佳(success_count 最高;無 stat 視為 0)。 + // §7.5.h:星數 per-uuid(5.1 記 uuid)→ 能真正區分 Leo 版/John 版。舊資料無 uuid fallback canonical_id。 + let best = versions[0]; + let bestStat: { success_count: number; failure_count: number } | null = null; + let bestScore = -1; + for (const v of versions) { + const stat = await fetchMarketStat(c.env, v.uuid ?? v.canonical_id); + const score = stat?.success_count ?? 0; + if (score > bestScore) { bestScore = score; best = v; bestStat = stat; } + } + return c.json({ found: true, recipe: best, market_stat: bestStat }); +}); + +// DELETE /recipes/:id — 刪除(依 UUID 模型清掉 recipe:{uuid} + installed + canonical 清單裡的該 uuid + 舊 key) recipesRouter.delete('/recipes/:id', async (c) => { const id = c.req.param('id'); const recipe = await resolveRecipe(id, c.env.RECIPES); if (!recipe) return c.json({ success: false, error: `找不到 recipe: ${id}` }, 404); - await Promise.all([ - c.env.RECIPES.delete(`recipe:${recipe.canonical_id}`), + const canonicalId = recipe.canonical_id; + const ops: Promise[] = [ c.env.RECIPES.delete(`idx:${recipe.hash_id}`), - ]); - - return c.json({ success: true, deleted: recipe.canonical_id }); + c.env.RECIPES.delete(`recipe:${canonicalId}`), // 舊 key(若存在) + ]; + if (recipe.uuid) { + ops.push(c.env.RECIPES.delete(`recipe:${recipe.uuid}`)); + // 從 canonical 清單移除此 uuid;若清單空了連 installed 一起清 + const listRaw = await c.env.RECIPES.get(kIdxCanonical(canonicalId)); + const uuids: string[] = listRaw ? JSON.parse(listRaw) : []; + const left = uuids.filter(u => u !== recipe.uuid); + if (left.length > 0) { + ops.push(c.env.RECIPES.put(kIdxCanonical(canonicalId), JSON.stringify(left))); + // installed 若指向被刪的 uuid → 改指剩下第一個 + const installed = await c.env.RECIPES.get(kIdxInstalled(canonicalId)); + if (installed === recipe.uuid) ops.push(c.env.RECIPES.put(kIdxInstalled(canonicalId), left[0])); + } else { + ops.push(c.env.RECIPES.delete(kIdxCanonical(canonicalId))); + ops.push(c.env.RECIPES.delete(kIdxInstalled(canonicalId))); + } + } + await Promise.all(ops); + return c.json({ success: true, deleted: recipe.uuid ?? canonicalId }); }); -/** 用 canonical_id 或 rec_hash 查 recipe */ +/** + * 用 canonical_id / rec_hash / uuid 查 recipe(執行時的解析入口)。 + * UUID 身份模型(§7.5.5)+ 向後相容(migration 前的舊 key): + * 1. id 是 uuid(recipe:{uuid} 直接存在)→ 直接回。 + * 2. rec_xxxxxxxx → idx:{hash} 反查 canonical_id → 再走 canonical 解析。 + * 3. canonical_id → 先查 idx:installed:{canonical_id}(本部署安裝的唯一版本)→ recipe:{uuid}; + * 查不到 fallback 舊 key recipe:{canonical_id}(種子 / migration 前資料)。 + * 執行鏈路(component-loader/auth-dispatcher/credential-injector)都經此 → 不破執行。 + */ export async function resolveRecipe( id: string, kv: KVNamespace, ): Promise { - // rec_xxxxxxxx → 先查 idx 反查 canonical_id + // 1. 直接 uuid(pull / market 指定版本時用) + const direct = await kv.get(`recipe:${id}`, 'json') as RecipeDefinition | null; + if (direct && direct.uuid) return direct; + // direct 命中但無 uuid = 舊 key recipe:{canonical_id}(migration 前)→ 仍可用,但繼續嘗試 installed 拿新版 + // (installed 優先:migration 後新版在 recipe:{uuid},舊 key 為 fallback) + + // 2. rec_hash 反查 canonical_id + let canonicalId = id; if (id.startsWith('rec_')) { - const canonicalId = await kv.get(`idx:${id}`); - if (!canonicalId) return null; - return kv.get(`recipe:${canonicalId}`, 'json'); + const looked = await kv.get(`idx:${id}`); + if (!looked) return direct; // hash 查不到,回 step1 結果(通常 null) + canonicalId = looked; } - // 直接用 canonical_id - return kv.get(`recipe:${id}`, 'json'); + + // 3. canonical → installed uuid → recipe:{uuid};fallback 舊 key + const installedUuid = await kv.get(kIdxInstalled(canonicalId)); + if (installedUuid) { + const byUuid = await kv.get(`recipe:${installedUuid}`, 'json') as RecipeDefinition | null; + if (byUuid) return byUuid; + } + // fallback:舊 key recipe:{canonical_id}(direct 若正是它,已在手上) + return direct ?? (await kv.get(`recipe:${canonicalId}`, 'json')); } // ── Auth Recipe ──────────────────────────────────────────────────────────────── diff --git a/cypher-executor/src/types.ts b/cypher-executor/src/types.ts index 698c309..91b576f 100644 --- a/cypher-executor/src/types.ts +++ b/cypher-executor/src/types.ts @@ -49,6 +49,9 @@ export type Bindings = { SESSION_SIGNING_SECRET?: string; // 用於 HMAC session ID(可選,也可直接用 UUID) // KBDB 整合 KBDB_INTERNAL_TOKEN?: string; + // KBDB Base worker URL(recipe 成功記錄 /recipe-stats/record、fragment 抓取)。 + // 未設 fallback 見各使用點(recipe-expander 預設 kbdb.finally.click)。kbdb-base SDD §7.1。 + KBDB_BASE_URL?: string; // Component Worker subdomain(workers.dev 帳號 subdomain) // 必填:cypher-executor 用此組出 component worker URL(避開同 zone 自循環死鎖,見 P0 #9) // self-hosted fork 必須改 wrangler.toml [vars] 為自己的帳號 subdomain diff --git a/kbdb/migrations/0001_base.sql b/kbdb/migrations/0001_base.sql new file mode 100644 index 0000000..93d87da --- /dev/null +++ b/kbdb/migrations/0001_base.sql @@ -0,0 +1,88 @@ +-- KBDB Base — atomic universal table (3 tables, never changes) +-- SDD: .agents/specs/arcrun/kbdb-base/design.md +-- +-- Plugin model (like PostgreSQL core + PGVector/AGE): +-- - Base = these 3 tables + plain CRUD + D1 LIKE search (D1 only, free, no credit card). +-- - embed module = optional, writes vectors to Vectorize (does NOT alter these tables). +-- - triplet module = separate repo, writes derived records into entry_values (does NOT alter base). +-- "Table never changes": new tech records its output elsewhere, never ALTERs the base. + +-- ============================================================ +-- 1. Three tables +-- ============================================================ + +-- Universal main table: each entry is one atomic datum. +-- entry_type extended for arcrun: 'block' | 'value' | 'template' | 'slot' | 'project' | 'workflow' | 'recipe_stat' +CREATE TABLE IF NOT EXISTS entries ( + id TEXT PRIMARY KEY, + content TEXT, + entry_type TEXT NOT NULL, + owner_id TEXT, -- multi-tenant: namespace (self-hosted) or api_key (SaaS) + + -- tree structure (project -> workflow via parent_id; SDD Q1 decision) + parent_id TEXT, + + -- optional block metadata (harmless plain columns) + page_name TEXT, + refs_json TEXT DEFAULT '[]', + tags_json TEXT DEFAULT '[]', + task_status TEXT, + + -- optional embed bookkeeping (set by optional embed module; base never reads them) + content_hash TEXT, + is_embedded INTEGER DEFAULT 0, + + -- metadata + confidence REAL, + metadata_json TEXT, + + created_at INTEGER DEFAULT (unixepoch()), + updated_at INTEGER DEFAULT (unixepoch()) +); + +-- Template table: defines slots for a virtual table +CREATE TABLE IF NOT EXISTS templates ( + id TEXT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + description TEXT, + slots_json TEXT NOT NULL, -- JSON array, e.g. ["display_name","gender"] + created_by TEXT, -- 'system' | 'ai' | owner_id + created_at INTEGER DEFAULT (unixepoch()), + updated_at INTEGER DEFAULT (unixepoch()) +); + +-- Slot link table: composes multiple entries into one structured record +CREATE TABLE IF NOT EXISTS entry_values ( + id TEXT PRIMARY KEY, + record_id TEXT NOT NULL, + template_id TEXT NOT NULL REFERENCES templates(id), + slot_name TEXT NOT NULL, + entry_id TEXT NOT NULL REFERENCES entries(id), + created_at INTEGER DEFAULT (unixepoch()), + UNIQUE(record_id, slot_name) +); + +-- ============================================================ +-- 2. Indexes +-- ============================================================ + +CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type); +CREATE INDEX IF NOT EXISTS idx_entries_owner ON entries(owner_id); +CREATE INDEX IF NOT EXISTS idx_entries_parent ON entries(parent_id); +CREATE INDEX IF NOT EXISTS idx_entries_page ON entries(page_name); +CREATE INDEX IF NOT EXISTS idx_entries_task ON entries(task_status); +CREATE INDEX IF NOT EXISTS idx_entries_hash ON entries(content_hash); + +CREATE INDEX IF NOT EXISTS idx_ev_record ON entry_values(record_id); +CREATE INDEX IF NOT EXISTS idx_ev_template ON entry_values(template_id); +CREATE INDEX IF NOT EXISTS idx_ev_template_slot ON entry_values(template_id, slot_name); +CREATE INDEX IF NOT EXISTS idx_ev_entry ON entry_values(entry_id); + +-- ============================================================ +-- 3. Seed templates used by arcrun base +-- ============================================================ + +-- recipe_stat: success/failure counters for a recipe (feeds recipe submission, SDD section 7) +INSERT OR IGNORE INTO templates (id, name, description, slots_json, created_by) +VALUES + ('tpl-recipe-stat', 'recipe_stat', 'recipe success/failure counters', '["canonical_id","success_count","failure_count","last_status","last_at"]', 'system'); diff --git a/kbdb/package.json b/kbdb/package.json new file mode 100644 index 0000000..86d461e --- /dev/null +++ b/kbdb/package.json @@ -0,0 +1,20 @@ +{ + "name": "arcrun-kbdb", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "test": "vitest run" + }, + "dependencies": { + "hono": "^4.7.0", + "zod": "~3.23.8" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250219.0", + "typescript": "^5.7.0", + "vitest": "^3.1.0", + "wrangler": "^4.0.0" + } +} diff --git a/kbdb/pnpm-lock.yaml b/kbdb/pnpm-lock.yaml new file mode 100644 index 0000000..273878c --- /dev/null +++ b/kbdb/pnpm-lock.yaml @@ -0,0 +1,1870 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + hono: + specifier: ^4.7.0 + version: 4.12.23 + zod: + specifier: ~3.23.8 + version: 3.23.8 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250219.0 + version: 4.20260607.1 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.1.0 + version: 3.2.6 + wrangler: + specifier: ^4.0.0 + version: 4.98.0(@cloudflare/workers-types@4.20260607.1) + +packages: + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260603.1': + resolution: {integrity: sha512-cEXDWu6V3ZrpmwWkM4OJE9AeXjdAgOY5rh8EHhcBVCuP5rxnzUbPzLtrVOHx0UUUAcCrFq0Xsa6mZKL1VUZsKQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260603.1': + resolution: {integrity: sha512-uBPK4LaWJNbbCYwPnUAehlHbbVulhVZPZsdcAhBPfZhHb3QAuAEPAQepO/P67R3V6Cni4YGx1fLbL8A5wwoaNA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260603.1': + resolution: {integrity: sha512-ht9l6/8Tk7Rp6kA4S9oFZ4X8u0VjnnFdmU/6B3fnABYKREYTKh2RdOqXqXxcp5eNJseireKnWik/hQOPK1CutQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260603.1': + resolution: {integrity: sha512-LJZ6x00rAjSrobV4m0ZW0TpH5ilBbKcWBzlH+y+KOUsIE/CpTuhAzKV43TbSnFLRX5+jrWKiz2v0hO91lPXy6A==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260603.1': + resolution: {integrity: sha512-DvwqkXMAJRPoDN4PxapAwhlz/6ouD+6R1ttbAEK3cWD/QBvFF5STx7Ds/9Irf+rBly3np3uHWkeX+wZnNFEuzA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260607.1': + resolution: {integrity: sha512-TSiusluJ8+5esTMYwxGFuT1SNU/PRzPmt9VMsmAlzjIK0mhc24Zsc1bbGEVH5qyMZ8hrdRtrAPdt2+T8Vph2+Q==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@rollup/rollup-android-arm-eabi@4.61.1': + resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.1': + resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.1': + resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.1': + resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.1': + resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.1': + resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.61.1': + resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.61.1': + resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.61.1': + resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.61.1': + resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.61.1': + resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.1': + resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.1': + resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.1': + resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@vitest/expect@3.2.6': + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + + '@vitest/mocker@3.2.6': + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.6': + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + + '@vitest/runner@3.2.6': + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + + '@vitest/snapshot@3.2.6': + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + + '@vitest/spy@3.2.6': + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + + '@vitest/utils@3.2.6': + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hono@4.12.23: + resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + engines: {node: '>=16.9.0'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + miniflare@4.20260603.0: + resolution: {integrity: sha512-+kMQYB82gC8MPOuojHur3icQsUeZUEJ+Sphuo5rVC3Ri9txBLAW/mH33b9OVrpmkogQeaaqPS4tPtugJZhk5Kw==} + engines: {node: '>=22.0.0'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.61.1: + resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.8.2: + resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.6: + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.6 + '@vitest/ui': 3.2.6 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + workerd@1.20260603.1: + resolution: {integrity: sha512-NPcbhI1++CS+fnELyXtsIR52en+5kwr/OrKeiQeYXGy10HxmPdsQBv9N+DU7hJIOOmBHhOGAAsoGDjyiQ2YCaA==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.98.0: + resolution: {integrity: sha512-cXfFUuF4rMIvE0hiMnXjEAB27ERryaCgquBJdUoPIjFzYYE1rbRdMUkEdQ18qDPUtsPvhJdqxLntixT9OfSzQw==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260603.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + +snapshots: + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260603.1 + + '@cloudflare/workerd-darwin-64@1.20260603.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260603.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260603.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260603.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260603.1': + optional: true + + '@cloudflare/workers-types@4.20260607.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@rollup/rollup-android-arm-eabi@4.61.1': + optional: true + + '@rollup/rollup-android-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-x64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.1': + optional: true + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.15': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@vitest/expect@3.2.6': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.6(vite@7.3.5)': + dependencies: + '@vitest/spy': 3.2.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.5 + + '@vitest/pretty-format@3.2.6': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.6': + dependencies: + '@vitest/utils': 3.2.6 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.6': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + assertion-error@2.0.1: {} + + blake3-wasm@2.1.5: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + cookie@1.1.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + hono@4.12.23: {} + + js-tokens@9.0.1: {} + + kleur@4.1.5: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + miniflare@4.20260603.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260603.1 + ws: 8.20.1 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.61.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.1 + '@rollup/rollup-android-arm64': 4.61.1 + '@rollup/rollup-darwin-arm64': 4.61.1 + '@rollup/rollup-darwin-x64': 4.61.1 + '@rollup/rollup-freebsd-arm64': 4.61.1 + '@rollup/rollup-freebsd-x64': 4.61.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 + '@rollup/rollup-linux-arm-musleabihf': 4.61.1 + '@rollup/rollup-linux-arm64-gnu': 4.61.1 + '@rollup/rollup-linux-arm64-musl': 4.61.1 + '@rollup/rollup-linux-loong64-gnu': 4.61.1 + '@rollup/rollup-linux-loong64-musl': 4.61.1 + '@rollup/rollup-linux-ppc64-gnu': 4.61.1 + '@rollup/rollup-linux-ppc64-musl': 4.61.1 + '@rollup/rollup-linux-riscv64-gnu': 4.61.1 + '@rollup/rollup-linux-riscv64-musl': 4.61.1 + '@rollup/rollup-linux-s390x-gnu': 4.61.1 + '@rollup/rollup-linux-x64-gnu': 4.61.1 + '@rollup/rollup-linux-x64-musl': 4.61.1 + '@rollup/rollup-openbsd-x64': 4.61.1 + '@rollup/rollup-openharmony-arm64': 4.61.1 + '@rollup/rollup-win32-arm64-msvc': 4.61.1 + '@rollup/rollup-win32-ia32-msvc': 4.61.1 + '@rollup/rollup-win32-x64-gnu': 4.61.1 + '@rollup/rollup-win32-x64-msvc': 4.61.1 + fsevents: 2.3.3 + + semver@7.8.2: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@10.2.2: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici@7.24.8: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + vite-node@3.2.4: + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.5 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.5: + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.1 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + vitest@3.2.6: + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.6 + '@vitest/mocker': 3.2.6(vite@7.3.5) + '@vitest/pretty-format': 3.2.6 + '@vitest/runner': 3.2.6 + '@vitest/snapshot': 3.2.6 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.5 + vite-node: 3.2.4 + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + workerd@1.20260603.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260603.1 + '@cloudflare/workerd-darwin-arm64': 1.20260603.1 + '@cloudflare/workerd-linux-64': 1.20260603.1 + '@cloudflare/workerd-linux-arm64': 1.20260603.1 + '@cloudflare/workerd-windows-64': 1.20260603.1 + + wrangler@4.98.0(@cloudflare/workers-types@4.20260607.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260603.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260603.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260607.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.20.1: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.15 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@3.23.8: {} diff --git a/kbdb/src/actions/entry-crud.ts b/kbdb/src/actions/entry-crud.ts new file mode 100644 index 0000000..39a8133 --- /dev/null +++ b/kbdb/src/actions/entry-crud.ts @@ -0,0 +1,117 @@ +// Entry CRUD — atomic data + tree (project/workflow via parent_id). Base, D1 only. +import type { Bindings, Entry } from '../types'; + +function uid(prefix: string): string { + // deterministic-enough unique id without Math.random in hot path is fine here; + // crypto.randomUUID is available in Workers runtime. + return `${prefix}_${crypto.randomUUID()}`; +} + +export interface CreateEntryInput { + content?: string | null; + entry_type: string; + owner_id?: string | null; + parent_id?: string | null; + page_name?: string | null; + refs_json?: string; + tags_json?: string; + task_status?: string | null; + confidence?: number | null; + metadata_json?: string | null; + id?: string; +} + +export async function createEntry(db: D1Database, input: CreateEntryInput): Promise { + const id = input.id ?? uid('e'); + await db + .prepare( + `INSERT INTO entries (id, content, entry_type, owner_id, parent_id, page_name, refs_json, tags_json, task_status, confidence, metadata_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + id, + input.content ?? null, + input.entry_type, + input.owner_id ?? null, + input.parent_id ?? null, + input.page_name ?? null, + input.refs_json ?? '[]', + input.tags_json ?? '[]', + input.task_status ?? null, + input.confidence ?? null, + input.metadata_json ?? null, + ) + .run(); + const row = await getEntry(db, id); + if (!row) throw new Error('createEntry: insert succeeded but row not found'); + return row; +} + +export async function getEntry(db: D1Database, id: string): Promise { + const row = await db.prepare('SELECT * FROM entries WHERE id = ?').bind(id).first(); + return row ?? null; +} + +export interface ListEntriesFilter { + entry_type?: string; + owner_id?: string; + parent_id?: string; + limit?: number; + offset?: number; +} + +export async function listEntries(db: D1Database, f: ListEntriesFilter = {}): Promise { + const conds: string[] = []; + const params: unknown[] = []; + if (f.entry_type) { conds.push('entry_type = ?'); params.push(f.entry_type); } + if (f.owner_id) { conds.push('owner_id = ?'); params.push(f.owner_id); } + if (f.parent_id) { conds.push('parent_id = ?'); params.push(f.parent_id); } + const where = conds.length ? `WHERE ${conds.join(' AND ')}` : ''; + const limit = Math.min(f.limit ?? 100, 1000); + const offset = f.offset ?? 0; + const res = await db + .prepare(`SELECT * FROM entries ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`) + .bind(...params, limit, offset) + .all(); + return res.results ?? []; +} + +export interface UpdateEntryInput { + content?: string | null; + parent_id?: string | null; + page_name?: string | null; + refs_json?: string; + tags_json?: string; + task_status?: string | null; + confidence?: number | null; + metadata_json?: string | null; +} + +export async function updateEntry(db: D1Database, id: string, patch: UpdateEntryInput): Promise { + const cols: string[] = []; + const params: unknown[] = []; + const map: Record = patch as Record; + for (const k of ['content', 'parent_id', 'page_name', 'refs_json', 'tags_json', 'task_status', 'confidence', 'metadata_json']) { + if (k in map && map[k] !== undefined) { cols.push(`${k} = ?`); params.push(map[k]); } + } + if (cols.length === 0) return getEntry(db, id); + cols.push('updated_at = unixepoch()'); + await db.prepare(`UPDATE entries SET ${cols.join(', ')} WHERE id = ?`).bind(...params, id).run(); + return getEntry(db, id); +} + +export async function deleteEntry(db: D1Database, id: string): Promise { + await db.prepare('DELETE FROM entries WHERE id = ?').bind(id).run(); +} + +// D1 LIKE keyword search (base; semantic search is the optional embed module). +export async function searchEntries(db: D1Database, q: string, owner_id?: string, limit = 50): Promise { + const conds = ['content LIKE ?']; + const params: unknown[] = [`%${q}%`]; + if (owner_id) { conds.push('owner_id = ?'); params.push(owner_id); } + const res = await db + .prepare(`SELECT * FROM entries WHERE ${conds.join(' AND ')} ORDER BY updated_at DESC LIMIT ?`) + .bind(...params, Math.min(limit, 200)) + .all(); + return res.results ?? []; +} diff --git a/kbdb/src/actions/recipe-stat.ts b/kbdb/src/actions/recipe-stat.ts new file mode 100644 index 0000000..be86840 --- /dev/null +++ b/kbdb/src/actions/recipe-stat.ts @@ -0,0 +1,63 @@ +// Recipe success/failure records (SDD section 7.1). Stored as an entry per recipe canonical_id. +// This is the "fuel" for submission-with-proof: real 2xx counts beat self-written tests. +import type { Bindings } from '../types'; + +interface RecipeStat { + canonical_id: string; + success_count: number; + failure_count: number; + last_status: string | null; + last_at: number | null; +} + +// One entry per recipe: id = recipestat:{canonical_id}, entry_type='recipe_stat', +// counters live in metadata_json. Atomic upsert via D1. +function statId(canonicalId: string): string { + return `recipestat:${canonicalId}`; +} + +export async function recordRecipeResult(db: D1Database, canonicalId: string, ok: boolean, nowMs: number): Promise { + const id = statId(canonicalId); + const existing = await db.prepare('SELECT metadata_json FROM entries WHERE id = ?').bind(id).first<{ metadata_json: string | null }>(); + + let stat: RecipeStat; + if (existing) { + const prev = existing.metadata_json ? (JSON.parse(existing.metadata_json) as RecipeStat) : emptyStat(canonicalId); + stat = { + canonical_id: canonicalId, + success_count: prev.success_count + (ok ? 1 : 0), + failure_count: prev.failure_count + (ok ? 0 : 1), + last_status: ok ? 'success' : 'failure', + last_at: nowMs, + }; + await db + .prepare('UPDATE entries SET metadata_json = ?, updated_at = unixepoch() WHERE id = ?') + .bind(JSON.stringify(stat), id) + .run(); + } else { + stat = { + canonical_id: canonicalId, + success_count: ok ? 1 : 0, + failure_count: ok ? 0 : 1, + last_status: ok ? 'success' : 'failure', + last_at: nowMs, + }; + await db + .prepare('INSERT INTO entries (id, content, entry_type, metadata_json) VALUES (?, ?, ?, ?)') + .bind(id, canonicalId, 'recipe_stat', JSON.stringify(stat)) + .run(); + } + return stat; +} + +export async function getRecipeStat(db: D1Database, canonicalId: string): Promise { + const row = await db.prepare('SELECT metadata_json FROM entries WHERE id = ?').bind(statId(canonicalId)).first<{ metadata_json: string | null }>(); + if (!row || !row.metadata_json) return emptyStat(canonicalId); + return JSON.parse(row.metadata_json) as RecipeStat; +} + +function emptyStat(canonicalId: string): RecipeStat { + return { canonical_id: canonicalId, success_count: 0, failure_count: 0, last_status: null, last_at: null }; +} + +export type { RecipeStat }; diff --git a/kbdb/src/actions/record-crud.ts b/kbdb/src/actions/record-crud.ts new file mode 100644 index 0000000..4e9664c --- /dev/null +++ b/kbdb/src/actions/record-crud.ts @@ -0,0 +1,120 @@ +// Template + Record CRUD. A "record" = multiple entries composed via a template's slots. +// Base, D1 only. (Ported clean from KBDB; no vectorize/triplet imports.) +import type { Template } from '../types'; +import { createEntry } from './entry-crud'; + +function uid(prefix: string): string { + return `${prefix}_${crypto.randomUUID()}`; +} + +// ---- Templates ---- + +export interface CreateTemplateInput { + name: string; + description?: string | null; + slots: string[]; + created_by?: string | null; + id?: string; +} + +export async function createTemplate(db: D1Database, input: CreateTemplateInput): Promise