var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res, err) => function __init() {
if (err) throw err[0];
try {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
} catch (e) {
throw err = [e], e;
}
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// kbdb/src/actions/relation-orphans.ts
var relation_orphans_exports = {};
__export(relation_orphans_exports, {
scanRelationOrphans: () => scanRelationOrphans
});
async function scanRelationOrphans(db, limit = 200) {
const cap = Math.min(Math.max(limit, 1), 1e3);
const res = await db.prepare(
`SELECT relation_id, role, missing_id FROM (
SELECT r.id AS relation_id, 'src' AS role, r.src_id AS missing_id
FROM entries r LEFT JOIN entries t ON t.id = r.src_id
WHERE r.src_id IS NOT NULL AND t.id IS NULL
UNION ALL
SELECT r.id, 'rel', r.rel_id
FROM entries r LEFT JOIN entries t ON t.id = r.rel_id
WHERE r.rel_id IS NOT NULL AND t.id IS NULL
UNION ALL
SELECT r.id, 'dst', r.dst_id
FROM entries r LEFT JOIN entries t ON t.id = r.dst_id
WHERE r.dst_id IS NOT NULL AND t.id IS NULL
) LIMIT ?`
).bind(cap + 1).all();
const rows = res.results ?? [];
const truncated = rows.length > cap;
const orphans = truncated ? rows.slice(0, cap) : rows;
return { orphans, count: orphans.length, truncated };
}
var init_relation_orphans = __esm({
"kbdb/src/actions/relation-orphans.ts"() {
"use strict";
}
});
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/compose.js
var compose = (middleware, onError, onNotFound) => {
return (context, next) => {
let index = -1;
return dispatch(0);
async function dispatch(i) {
if (i <= index) {
throw new Error("next() called multiple times");
}
index = i;
let res;
let isError = false;
let handler;
if (middleware[i]) {
handler = middleware[i][0][0];
context.req.routeIndex = i;
} else {
handler = i === middleware.length && next || void 0;
}
if (handler) {
try {
res = await handler(context, () => dispatch(i + 1));
} catch (err) {
if (err instanceof Error && onError) {
context.error = err;
res = await onError(err, context);
isError = true;
} else {
throw err;
}
}
} else {
if (context.finalized === false && onNotFound) {
res = await onNotFound(context);
}
}
if (res && (context.finalized === false || isError)) {
context.res = res;
}
return context;
}
};
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/request/constants.js
var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/body.js
var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
const { all = false, dot = false } = options;
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
const contentType = headers.get("Content-Type");
if (contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded")) {
return parseFormData(request, { all, dot });
}
return {};
};
async function parseFormData(request, options) {
const formData = await request.formData();
if (formData) {
return convertFormDataToBodyData(formData, options);
}
return {};
}
function convertFormDataToBodyData(formData, options) {
const form = /* @__PURE__ */ Object.create(null);
formData.forEach((value, key) => {
const shouldParseAllValues = options.all || key.endsWith("[]");
if (!shouldParseAllValues) {
form[key] = value;
} else {
handleParsingAllValues(form, key, value);
}
});
if (options.dot) {
Object.entries(form).forEach(([key, value]) => {
const shouldParseDotValues = key.includes(".");
if (shouldParseDotValues) {
handleParsingNestedValues(form, key, value);
delete form[key];
}
});
}
return form;
}
var handleParsingAllValues = (form, key, value) => {
if (form[key] !== void 0) {
if (Array.isArray(form[key])) {
;
form[key].push(value);
} else {
form[key] = [form[key], value];
}
} else {
if (!key.endsWith("[]")) {
form[key] = value;
} else {
form[key] = [value];
}
}
};
var handleParsingNestedValues = (form, key, value) => {
if (/(?:^|\.)__proto__\./.test(key)) {
return;
}
let nestedForm = form;
const keys = key.split(".");
keys.forEach((key2, index) => {
if (index === keys.length - 1) {
nestedForm[key2] = value;
} else {
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
nestedForm[key2] = /* @__PURE__ */ Object.create(null);
}
nestedForm = nestedForm[key2];
}
});
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/url.js
var splitPath = (path) => {
const paths = path.split("/");
if (paths[0] === "") {
paths.shift();
}
return paths;
};
var splitRoutingPath = (routePath) => {
const { groups, path } = extractGroupsFromPath(routePath);
const paths = splitPath(path);
return replaceGroupMarks(paths, groups);
};
var extractGroupsFromPath = (path) => {
const groups = [];
path = path.replace(/\{[^}]+\}/g, (match2, index) => {
const mark = `@${index}`;
groups.push([mark, match2]);
return mark;
});
return { groups, path };
};
var replaceGroupMarks = (paths, groups) => {
for (let i = groups.length - 1; i >= 0; i--) {
const [mark] = groups[i];
for (let j = paths.length - 1; j >= 0; j--) {
if (paths[j].includes(mark)) {
paths[j] = paths[j].replace(mark, groups[i][1]);
break;
}
}
}
return paths;
};
var patternCache = {};
var getPattern = (label, next) => {
if (label === "*") {
return "*";
}
const match2 = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
if (match2) {
const cacheKey = `${label}#${next}`;
if (!patternCache[cacheKey]) {
if (match2[2]) {
patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [cacheKey, match2[1], new RegExp(`^${match2[2]}(?=/${next})`)] : [label, match2[1], new RegExp(`^${match2[2]}$`)];
} else {
patternCache[cacheKey] = [label, match2[1], true];
}
}
return patternCache[cacheKey];
}
return null;
};
var tryDecode = (str, decoder) => {
try {
return decoder(str);
} catch {
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
try {
return decoder(match2);
} catch {
return match2;
}
});
}
};
var tryDecodeURI = (str) => tryDecode(str, decodeURI);
var getPath = (request) => {
const url = request.url;
const start = url.indexOf("/", url.indexOf(":") + 4);
let i = start;
for (; i < url.length; i++) {
const charCode = url.charCodeAt(i);
if (charCode === 37) {
const queryIndex = url.indexOf("?", i);
const hashIndex = url.indexOf("#", i);
const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
const path = url.slice(start, end);
return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path);
} else if (charCode === 63 || charCode === 35) {
break;
}
}
return url.slice(start, i);
};
var getPathNoStrict = (request) => {
const result = getPath(request);
return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
};
var mergePath = (base, sub, ...rest) => {
if (rest.length) {
sub = mergePath(sub, ...rest);
}
return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
};
var checkOptionalParameter = (path) => {
if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) {
return null;
}
const segments = path.split("/");
const results = [];
let basePath = "";
segments.forEach((segment) => {
if (segment !== "" && !/\:/.test(segment)) {
basePath += "/" + segment;
} else if (/\:/.test(segment)) {
if (/\?/.test(segment)) {
if (results.length === 0 && basePath === "") {
results.push("/");
} else {
results.push(basePath);
}
const optionalSegment = segment.replace("?", "");
basePath += "/" + optionalSegment;
results.push(basePath);
} else {
basePath += "/" + segment;
}
}
});
return results.filter((v, i, a) => a.indexOf(v) === i);
};
var _decodeURI = (value) => {
if (!/[%+]/.test(value)) {
return value;
}
if (value.indexOf("+") !== -1) {
value = value.replace(/\+/g, " ");
}
return value.indexOf("%") !== -1 ? tryDecode(value, decodeURIComponent_) : value;
};
var _getQueryParam = (url, key, multiple) => {
let encoded;
if (!multiple && key && !/[%+]/.test(key)) {
let keyIndex2 = url.indexOf("?", 8);
if (keyIndex2 === -1) {
return void 0;
}
if (!url.startsWith(key, keyIndex2 + 1)) {
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
}
while (keyIndex2 !== -1) {
const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
if (trailingKeyCode === 61) {
const valueIndex = keyIndex2 + key.length + 2;
const endIndex = url.indexOf("&", valueIndex);
return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex));
} else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) {
return "";
}
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
}
encoded = /[%+]/.test(url);
if (!encoded) {
return void 0;
}
}
const results = {};
encoded ??= /[%+]/.test(url);
let keyIndex = url.indexOf("?", 8);
while (keyIndex !== -1) {
const nextKeyIndex = url.indexOf("&", keyIndex + 1);
let valueIndex = url.indexOf("=", keyIndex);
if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) {
valueIndex = -1;
}
let name = url.slice(
keyIndex + 1,
valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex
);
if (encoded) {
name = _decodeURI(name);
}
keyIndex = nextKeyIndex;
if (name === "") {
continue;
}
let value;
if (valueIndex === -1) {
value = "";
} else {
value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex);
if (encoded) {
value = _decodeURI(value);
}
}
if (multiple) {
if (!(results[name] && Array.isArray(results[name]))) {
results[name] = [];
}
;
results[name].push(value);
} else {
results[name] ??= value;
}
}
return key ? results[key] : results;
};
var getQueryParam = _getQueryParam;
var getQueryParams = (url, key) => {
return _getQueryParam(url, key, true);
};
var decodeURIComponent_ = decodeURIComponent;
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/request.js
var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
var HonoRequest = class {
/**
* `.raw` can get the raw Request object.
*
* @see {@link https://hono.dev/docs/api/request#raw}
*
* @example
* ```ts
* // For Cloudflare Workers
* app.post('/', async (c) => {
* const metadata = c.req.raw.cf?.hostMetadata?
* ...
* })
* ```
*/
raw;
#validatedData;
// Short name of validatedData
#matchResult;
routeIndex = 0;
/**
* `.path` can get the pathname of the request.
*
* @see {@link https://hono.dev/docs/api/request#path}
*
* @example
* ```ts
* app.get('/about/me', (c) => {
* const pathname = c.req.path // `/about/me`
* })
* ```
*/
path;
bodyCache = {};
constructor(request, path = "/", matchResult = [[]]) {
this.raw = request;
this.path = path;
this.#matchResult = matchResult;
this.#validatedData = {};
}
param(key) {
return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
}
#getDecodedParam(key) {
const paramKey = this.#matchResult[0][this.routeIndex][1][key];
const param = this.#getParamValue(paramKey);
return param && /\%/.test(param) ? tryDecodeURIComponent(param) : param;
}
#getAllDecodedParams() {
const decoded = {};
const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
for (const key of keys) {
const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
if (value !== void 0) {
decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value;
}
}
return decoded;
}
#getParamValue(paramKey) {
return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey;
}
query(key) {
return getQueryParam(this.url, key);
}
queries(key) {
return getQueryParams(this.url, key);
}
header(name) {
if (name) {
return this.raw.headers.get(name) ?? void 0;
}
const headerData = {};
this.raw.headers.forEach((value, key) => {
headerData[key] = value;
});
return headerData;
}
async parseBody(options) {
return parseBody(this, options);
}
#cachedBody = (key) => {
const { bodyCache, raw: raw2 } = this;
const cachedBody = bodyCache[key];
if (cachedBody) {
return cachedBody;
}
const anyCachedKey = Object.keys(bodyCache)[0];
if (anyCachedKey) {
return bodyCache[anyCachedKey].then((body) => {
if (anyCachedKey === "json") {
body = JSON.stringify(body);
}
return new Response(body)[key]();
});
}
return bodyCache[key] = raw2[key]();
};
/**
* `.json()` can parse Request body of type `application/json`
*
* @see {@link https://hono.dev/docs/api/request#json}
*
* @example
* ```ts
* app.post('/entry', async (c) => {
* const body = await c.req.json()
* })
* ```
*/
json() {
return this.#cachedBody("text").then((text) => JSON.parse(text));
}
/**
* `.text()` can parse Request body of type `text/plain`
*
* @see {@link https://hono.dev/docs/api/request#text}
*
* @example
* ```ts
* app.post('/entry', async (c) => {
* const body = await c.req.text()
* })
* ```
*/
text() {
return this.#cachedBody("text");
}
/**
* `.arrayBuffer()` parse Request body as an `ArrayBuffer`
*
* @see {@link https://hono.dev/docs/api/request#arraybuffer}
*
* @example
* ```ts
* app.post('/entry', async (c) => {
* const body = await c.req.arrayBuffer()
* })
* ```
*/
arrayBuffer() {
return this.#cachedBody("arrayBuffer");
}
/**
* `.bytes()` parses the request body as a `Uint8Array`.
*
* @see {@link https://hono.dev/docs/api/request#bytes}
*
* @example
* ```ts
* app.post('/entry', async (c) => {
* const body = await c.req.bytes()
* })
* ```
*/
bytes() {
return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer));
}
/**
* Parses the request body as a `Blob`.
* @example
* ```ts
* app.post('/entry', async (c) => {
* const body = await c.req.blob();
* });
* ```
* @see https://hono.dev/docs/api/request#blob
*/
blob() {
return this.#cachedBody("blob");
}
/**
* Parses the request body as `FormData`.
* @example
* ```ts
* app.post('/entry', async (c) => {
* const body = await c.req.formData();
* });
* ```
* @see https://hono.dev/docs/api/request#formdata
*/
formData() {
return this.#cachedBody("formData");
}
/**
* Adds validated data to the request.
*
* @param target - The target of the validation.
* @param data - The validated data to add.
*/
addValidatedData(target, data) {
this.#validatedData[target] = data;
}
valid(target) {
return this.#validatedData[target];
}
/**
* `.url()` can get the request url strings.
*
* @see {@link https://hono.dev/docs/api/request#url}
*
* @example
* ```ts
* app.get('/about/me', (c) => {
* const url = c.req.url // `http://localhost:8787/about/me`
* ...
* })
* ```
*/
get url() {
return this.raw.url;
}
/**
* `.method()` can get the method name of the request.
*
* @see {@link https://hono.dev/docs/api/request#method}
*
* @example
* ```ts
* app.get('/about/me', (c) => {
* const method = c.req.method // `GET`
* })
* ```
*/
get method() {
return this.raw.method;
}
get [GET_MATCH_RESULT]() {
return this.#matchResult;
}
/**
* `.matchedRoutes()` can return a matched route in the handler
*
* @deprecated
*
* Use matchedRoutes helper defined in "hono/route" instead.
*
* @see {@link https://hono.dev/docs/api/request#matchedroutes}
*
* @example
* ```ts
* app.use('*', async function logger(c, next) {
* await next()
* c.req.matchedRoutes.forEach(({ handler, method, path }, i) => {
* const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]')
* console.log(
* method,
* ' ',
* path,
* ' '.repeat(Math.max(10 - path.length, 0)),
* name,
* i === c.req.routeIndex ? '<- respond from here' : ''
* )
* })
* })
* ```
*/
get matchedRoutes() {
return this.#matchResult[0].map(([[, route]]) => route);
}
/**
* `routePath()` can retrieve the path registered within the handler
*
* @deprecated
*
* Use routePath helper defined in "hono/route" instead.
*
* @see {@link https://hono.dev/docs/api/request#routepath}
*
* @example
* ```ts
* app.get('/posts/:id', (c) => {
* return c.json({ path: c.req.routePath })
* })
* ```
*/
get routePath() {
return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path;
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/html.js
var HtmlEscapedCallbackPhase = {
Stringify: 1,
BeforeStream: 2,
Stream: 3
};
var raw = (value, callbacks) => {
const escapedString = new String(value);
escapedString.isEscaped = true;
escapedString.callbacks = callbacks;
return escapedString;
};
var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
if (typeof str === "object" && !(str instanceof String)) {
if (!(str instanceof Promise)) {
str = str.toString();
}
if (str instanceof Promise) {
str = await str;
}
}
const callbacks = str.callbacks;
if (!callbacks?.length) {
return Promise.resolve(str);
}
if (buffer) {
buffer[0] += str;
} else {
buffer = [str];
}
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then(
(res) => Promise.all(
res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))
).then(() => buffer[0])
);
if (preserveCallbacks) {
return raw(await resStr, callbacks);
} else {
return resStr;
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/context.js
var TEXT_PLAIN = "text/plain; charset=UTF-8";
var setDefaultContentType = (contentType, headers) => {
return {
"Content-Type": contentType,
...headers
};
};
var createResponseInstance = (body, init) => new Response(body, init);
var Context = class {
#rawRequest;
#req;
/**
* `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers.
*
* @see {@link https://hono.dev/docs/api/context#env}
*
* @example
* ```ts
* // Environment object for Cloudflare Workers
* app.get('*', async c => {
* const counter = c.env.COUNTER
* })
* ```
*/
env = {};
#var;
finalized = false;
/**
* `.error` can get the error object from the middleware if the Handler throws an error.
*
* @see {@link https://hono.dev/docs/api/context#error}
*
* @example
* ```ts
* app.use('*', async (c, next) => {
* await next()
* if (c.error) {
* // do something...
* }
* })
* ```
*/
error;
#status;
#executionCtx;
#res;
#layout;
#renderer;
#notFoundHandler;
#preparedHeaders;
#matchResult;
#path;
/**
* Creates an instance of the Context class.
*
* @param req - The Request object.
* @param options - Optional configuration options for the context.
*/
constructor(req, options) {
this.#rawRequest = req;
if (options) {
this.#executionCtx = options.executionCtx;
this.env = options.env;
this.#notFoundHandler = options.notFoundHandler;
this.#path = options.path;
this.#matchResult = options.matchResult;
}
}
/**
* `.req` is the instance of {@link HonoRequest}.
*/
get req() {
this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult);
return this.#req;
}
/**
* @see {@link https://hono.dev/docs/api/context#event}
* The FetchEvent associated with the current request.
*
* @throws Will throw an error if the context does not have a FetchEvent.
*/
get event() {
if (this.#executionCtx && "respondWith" in this.#executionCtx) {
return this.#executionCtx;
} else {
throw Error("This context has no FetchEvent");
}
}
/**
* @see {@link https://hono.dev/docs/api/context#executionctx}
* The ExecutionContext associated with the current request.
*
* @throws Will throw an error if the context does not have an ExecutionContext.
*/
get executionCtx() {
if (this.#executionCtx) {
return this.#executionCtx;
} else {
throw Error("This context has no ExecutionContext");
}
}
/**
* @see {@link https://hono.dev/docs/api/context#res}
* The Response object for the current request.
*/
get res() {
return this.#res ||= createResponseInstance(null, {
headers: this.#preparedHeaders ??= new Headers()
});
}
/**
* Sets the Response object for the current request.
*
* @param _res - The Response object to set.
*/
set res(_res) {
if (this.#res && _res) {
_res = createResponseInstance(_res.body, _res);
for (const [k, v] of this.#res.headers.entries()) {
if (k === "content-type") {
continue;
}
if (k === "set-cookie") {
const cookies = this.#res.headers.getSetCookie();
_res.headers.delete("set-cookie");
for (const cookie of cookies) {
_res.headers.append("set-cookie", cookie);
}
} else {
_res.headers.set(k, v);
}
}
}
this.#res = _res;
this.finalized = true;
}
/**
* `.render()` can create a response within a layout.
*
* @see {@link https://hono.dev/docs/api/context#render-setrenderer}
*
* @example
* ```ts
* app.get('/', (c) => {
* return c.render('Hello!')
* })
* ```
*/
render = (...args) => {
this.#renderer ??= (content) => this.html(content);
return this.#renderer(...args);
};
/**
* Sets the layout for the response.
*
* @param layout - The layout to set.
* @returns The layout function.
*/
setLayout = (layout) => this.#layout = layout;
/**
* Gets the current layout for the response.
*
* @returns The current layout function.
*/
getLayout = () => this.#layout;
/**
* `.setRenderer()` can set the layout in the custom middleware.
*
* @see {@link https://hono.dev/docs/api/context#render-setrenderer}
*
* @example
* ```tsx
* app.use('*', async (c, next) => {
* c.setRenderer((content) => {
* return c.html(
*
*
* {content}
*
*
* )
* })
* await next()
* })
* ```
*/
setRenderer = (renderer) => {
this.#renderer = renderer;
};
/**
* `.header()` can set headers.
*
* @see {@link https://hono.dev/docs/api/context#header}
*
* @example
* ```ts
* app.get('/welcome', (c) => {
* // Set headers
* c.header('X-Message', 'Hello!')
* c.header('Content-Type', 'text/plain')
*
* return c.body('Thank you for coming')
* })
* ```
*/
header = (name, value, options) => {
if (this.finalized) {
this.#res = createResponseInstance(this.#res.body, this.#res);
}
const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers();
if (value === void 0) {
headers.delete(name);
} else if (options?.append) {
headers.append(name, value);
} else {
headers.set(name, value);
}
};
status = (status) => {
this.#status = status;
};
/**
* `.set()` can set the value specified by the key.
*
* @see {@link https://hono.dev/docs/api/context#set-get}
*
* @example
* ```ts
* app.use('*', async (c, next) => {
* c.set('message', 'Hono is hot!!')
* await next()
* })
* ```
*/
set = (key, value) => {
this.#var ??= /* @__PURE__ */ new Map();
this.#var.set(key, value);
};
/**
* `.get()` can use the value specified by the key.
*
* @see {@link https://hono.dev/docs/api/context#set-get}
*
* @example
* ```ts
* app.get('/', (c) => {
* const message = c.get('message')
* return c.text(`The message is "${message}"`)
* })
* ```
*/
get = (key) => {
return this.#var ? this.#var.get(key) : void 0;
};
/**
* `.var` can access the value of a variable.
*
* @see {@link https://hono.dev/docs/api/context#var}
*
* @example
* ```ts
* const result = c.var.client.oneMethod()
* ```
*/
// c.var.propName is a read-only
get var() {
if (!this.#var) {
return {};
}
return Object.fromEntries(this.#var);
}
#newResponse(data, arg, headers) {
const responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders ?? new Headers();
if (typeof arg === "object" && "headers" in arg) {
const argHeaders = arg.headers instanceof Headers ? arg.headers : new Headers(arg.headers);
for (const [key, value] of argHeaders) {
if (key.toLowerCase() === "set-cookie") {
responseHeaders.append(key, value);
} else {
responseHeaders.set(key, value);
}
}
}
if (headers) {
for (const [k, v] of Object.entries(headers)) {
if (typeof v === "string") {
responseHeaders.set(k, v);
} else {
responseHeaders.delete(k);
for (const v2 of v) {
responseHeaders.append(k, v2);
}
}
}
}
const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
return createResponseInstance(data, { status, headers: responseHeaders });
}
newResponse = (...args) => this.#newResponse(...args);
/**
* `.body()` can return the HTTP response.
* You can set headers with `.header()` and set HTTP status code with `.status`.
* This can also be set in `.text()`, `.json()` and so on.
*
* @see {@link https://hono.dev/docs/api/context#body}
*
* @example
* ```ts
* app.get('/welcome', (c) => {
* // Set headers
* c.header('X-Message', 'Hello!')
* c.header('Content-Type', 'text/plain')
* // Set HTTP status code
* c.status(201)
*
* // Return the response body
* return c.body('Thank you for coming')
* })
* ```
*/
body = (data, arg, headers) => this.#newResponse(data, arg, headers);
/**
* `.text()` can render text as `Content-Type:text/plain`.
*
* @see {@link https://hono.dev/docs/api/context#text}
*
* @example
* ```ts
* app.get('/say', (c) => {
* return c.text('Hello!')
* })
* ```
*/
text = (text, arg, headers) => {
return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(
text,
arg,
setDefaultContentType(TEXT_PLAIN, headers)
);
};
/**
* `.json()` can render JSON as `Content-Type:application/json`.
*
* @see {@link https://hono.dev/docs/api/context#json}
*
* @example
* ```ts
* app.get('/api', (c) => {
* return c.json({ message: 'Hello!' })
* })
* ```
*/
json = (object, arg, headers) => {
return this.#newResponse(
JSON.stringify(object),
arg,
setDefaultContentType("application/json", headers)
);
};
html = (html, arg, headers) => {
const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
};
/**
* `.redirect()` can Redirect, default status code is 302.
*
* @see {@link https://hono.dev/docs/api/context#redirect}
*
* @example
* ```ts
* app.get('/redirect', (c) => {
* return c.redirect('/')
* })
* app.get('/redirect-permanently', (c) => {
* return c.redirect('/', 301)
* })
* ```
*/
redirect = (location, status) => {
const locationString = String(location);
this.header(
"Location",
// Multibyes should be encoded
// eslint-disable-next-line no-control-regex
!/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString)
);
return this.newResponse(null, status ?? 302);
};
/**
* `.notFound()` can return the Not Found Response.
*
* @see {@link https://hono.dev/docs/api/context#notfound}
*
* @example
* ```ts
* app.get('/notfound', (c) => {
* return c.notFound()
* })
* ```
*/
notFound = () => {
this.#notFoundHandler ??= () => createResponseInstance();
return this.#notFoundHandler(this);
};
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router.js
var METHOD_NAME_ALL = "ALL";
var METHOD_NAME_ALL_LOWERCASE = "all";
var METHODS = ["get", "post", "put", "delete", "options", "patch"];
var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built.";
var UnsupportedPathError = class extends Error {
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/constants.js
var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/hono-base.js
var notFoundHandler = (c) => {
return c.text("404 Not Found", 404);
};
var errorHandler = (err, c) => {
if ("getResponse" in err) {
const res = err.getResponse();
return c.newResponse(res.body, res);
}
console.error(err);
return c.text("Internal Server Error", 500);
};
var Hono = class _Hono {
get;
post;
put;
delete;
options;
patch;
all;
on;
use;
/*
This class is like an abstract class and does not have a router.
To use it, inherit the class and implement router in the constructor.
*/
router;
getPath;
// Cannot use `#` because it requires visibility at JavaScript runtime.
_basePath = "/";
#path = "/";
routes = [];
constructor(options = {}) {
const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE];
allMethods.forEach((method) => {
this[method] = (args1, ...args) => {
if (typeof args1 === "string") {
this.#path = args1;
} else {
this.#addRoute(method, this.#path, args1);
}
args.forEach((handler) => {
this.#addRoute(method, this.#path, handler);
});
return this;
};
});
this.on = (method, path, ...handlers) => {
for (const p of [path].flat()) {
this.#path = p;
for (const m of [method].flat()) {
handlers.map((handler) => {
this.#addRoute(m.toUpperCase(), this.#path, handler);
});
}
}
return this;
};
this.use = (arg1, ...handlers) => {
if (typeof arg1 === "string") {
this.#path = arg1;
} else {
this.#path = "*";
handlers.unshift(arg1);
}
handlers.forEach((handler) => {
this.#addRoute(METHOD_NAME_ALL, this.#path, handler);
});
return this;
};
const { strict, ...optionsWithoutStrict } = options;
Object.assign(this, optionsWithoutStrict);
this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict;
}
#clone() {
const clone = new _Hono({
router: this.router,
getPath: this.getPath
});
clone.errorHandler = this.errorHandler;
clone.#notFoundHandler = this.#notFoundHandler;
clone.routes = this.routes;
return clone;
}
#notFoundHandler = notFoundHandler;
// Cannot use `#` because it requires visibility at JavaScript runtime.
errorHandler = errorHandler;
/**
* `.route()` allows grouping other Hono instance in routes.
*
* @see {@link https://hono.dev/docs/api/routing#grouping}
*
* @param {string} path - base Path
* @param {Hono} app - other Hono instance
* @returns {Hono} routed Hono instance
*
* @example
* ```ts
* const app = new Hono()
* const app2 = new Hono()
*
* app2.get("/user", (c) => c.text("user"))
* app.route("/api", app2) // GET /api/user
* ```
*/
route(path, app2) {
const subApp = this.basePath(path);
app2.routes.map((r) => {
let handler;
if (app2.errorHandler === errorHandler) {
handler = r.handler;
} else {
handler = async (c, next) => (await compose([], app2.errorHandler)(c, () => r.handler(c, next))).res;
handler[COMPOSED_HANDLER] = r.handler;
}
subApp.#addRoute(r.method, r.path, handler, r.basePath);
});
return this;
}
/**
* `.basePath()` allows base paths to be specified.
*
* @see {@link https://hono.dev/docs/api/routing#base-path}
*
* @param {string} path - base Path
* @returns {Hono} changed Hono instance
*
* @example
* ```ts
* const api = new Hono().basePath('/api')
* ```
*/
basePath(path) {
const subApp = this.#clone();
subApp._basePath = mergePath(this._basePath, path);
return subApp;
}
/**
* `.onError()` handles an error and returns a customized Response.
*
* @see {@link https://hono.dev/docs/api/hono#error-handling}
*
* @param {ErrorHandler} handler - request Handler for error
* @returns {Hono} changed Hono instance
*
* @example
* ```ts
* app.onError((err, c) => {
* console.error(`${err}`)
* return c.text('Custom Error Message', 500)
* })
* ```
*/
onError = (handler) => {
this.errorHandler = handler;
return this;
};
/**
* `.notFound()` allows you to customize a Not Found Response.
*
* @see {@link https://hono.dev/docs/api/hono#not-found}
*
* @param {NotFoundHandler} handler - request handler for not-found
* @returns {Hono} changed Hono instance
*
* @example
* ```ts
* app.notFound((c) => {
* return c.text('Custom 404 Message', 404)
* })
* ```
*/
notFound = (handler) => {
this.#notFoundHandler = handler;
return this;
};
/**
* `.mount()` allows you to mount applications built with other frameworks into your Hono application.
*
* @see {@link https://hono.dev/docs/api/hono#mount}
*
* @param {string} path - base Path
* @param {Function} applicationHandler - other Request Handler
* @param {MountOptions} [options] - options of `.mount()`
* @returns {Hono} mounted Hono instance
*
* @example
* ```ts
* import { Router as IttyRouter } from 'itty-router'
* import { Hono } from 'hono'
* // Create itty-router application
* const ittyRouter = IttyRouter()
* // GET /itty-router/hello
* ittyRouter.get('/hello', () => new Response('Hello from itty-router'))
*
* const app = new Hono()
* app.mount('/itty-router', ittyRouter.handle)
* ```
*
* @example
* ```ts
* const app = new Hono()
* // Send the request to another application without modification.
* app.mount('/app', anotherApp, {
* replaceRequest: (req) => req,
* })
* ```
*/
mount(path, applicationHandler, options) {
let replaceRequest;
let optionHandler;
if (options) {
if (typeof options === "function") {
optionHandler = options;
} else {
optionHandler = options.optionHandler;
if (options.replaceRequest === false) {
replaceRequest = (request) => request;
} else {
replaceRequest = options.replaceRequest;
}
}
}
const getOptions = optionHandler ? (c) => {
const options2 = optionHandler(c);
return Array.isArray(options2) ? options2 : [options2];
} : (c) => {
let executionContext = void 0;
try {
executionContext = c.executionCtx;
} catch {
}
return [c.env, executionContext];
};
replaceRequest ||= (() => {
const mergedPath = mergePath(this._basePath, path);
const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
return (request) => {
const url = new URL(request.url);
url.pathname = this.getPath(request).slice(pathPrefixLength) || "/";
return new Request(url, request);
};
})();
const handler = async (c, next) => {
const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c));
if (res) {
return res;
}
await next();
};
this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler);
return this;
}
#addRoute(method, path, handler, baseRoutePath) {
method = method.toUpperCase();
path = mergePath(this._basePath, path);
const r = {
basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
path,
method,
handler
};
this.router.add(method, path, [handler, r]);
this.routes.push(r);
}
#handleError(err, c) {
if (err instanceof Error) {
return this.errorHandler(err, c);
}
throw err;
}
#dispatch(request, executionCtx, env, method) {
if (method === "HEAD") {
return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
}
const path = this.getPath(request, { env });
const matchResult = this.router.match(method, path);
const c = new Context(request, {
path,
matchResult,
env,
executionCtx,
notFoundHandler: this.#notFoundHandler
});
if (matchResult[0].length === 1) {
let res;
try {
res = matchResult[0][0][0][0](c, async () => {
c.res = await this.#notFoundHandler(c);
});
} catch (err) {
return this.#handleError(err, c);
}
return res instanceof Promise ? res.then(
(resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))
).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c);
}
const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
return (async () => {
try {
const context = await composed(c);
if (!context.finalized) {
throw new Error(
"Context is not finalized. Did you forget to return a Response object or `await next()`?"
);
}
return context.res;
} catch (err) {
return this.#handleError(err, c);
}
})();
}
/**
* `.fetch()` will be entry point of your app.
*
* @see {@link https://hono.dev/docs/api/hono#fetch}
*
* @param {Request} request - request Object of request
* @param {Env} Env - env Object
* @param {ExecutionContext} - context of execution
* @returns {Response | Promise} response of request
*
*/
fetch = (request, ...rest) => {
return this.#dispatch(request, rest[1], rest[0], request.method);
};
/**
* `.request()` is a useful method for testing.
* You can pass a URL or pathname to send a GET request.
* app will return a Response object.
* ```ts
* test('GET /hello is ok', async () => {
* const res = await app.request('/hello')
* expect(res.status).toBe(200)
* })
* ```
* @see https://hono.dev/docs/api/hono#request
*/
request = (input, requestInit, Env, executionCtx) => {
if (input instanceof Request) {
return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx);
}
input = input.toString();
return this.fetch(
new Request(
/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`,
requestInit
),
Env,
executionCtx
);
};
/**
* `.fire()` automatically adds a global fetch event listener.
* This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers.
* @deprecated
* Use `fire` from `hono/service-worker` instead.
* ```ts
* import { Hono } from 'hono'
* import { fire } from 'hono/service-worker'
*
* const app = new Hono()
* // ...
* fire(app)
* ```
* @see https://hono.dev/docs/api/hono#fire
* @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
* @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/
*/
fire = () => {
addEventListener("fetch", (event) => {
event.respondWith(this.#dispatch(event.request, event, void 0, event.request.method));
});
};
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router/reg-exp-router/matcher.js
var emptyParam = [];
function match(method, path) {
const matchers = this.buildAllMatchers();
const match2 = ((method2, path2) => {
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
const staticMatch = matcher[2][path2];
if (staticMatch) {
return staticMatch;
}
const match3 = path2.match(matcher[0]);
if (!match3) {
return [[], emptyParam];
}
const index = match3.indexOf("", 1);
return [matcher[1][index], match3];
});
this.match = match2;
return match2(method, path);
}
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router/reg-exp-router/node.js
var LABEL_REG_EXP_STR = "[^/]+";
var ONLY_WILDCARD_REG_EXP_STR = ".*";
var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
var PATH_ERROR = /* @__PURE__ */ Symbol();
var regExpMetaChars = new Set(".\\+*[^]$()");
function compareKey(a, b) {
if (a.length === 1) {
return b.length === 1 ? a < b ? -1 : 1 : -1;
}
if (b.length === 1) {
return 1;
}
if (a === ONLY_WILDCARD_REG_EXP_STR || a === TAIL_WILDCARD_REG_EXP_STR) {
return 1;
} else if (b === ONLY_WILDCARD_REG_EXP_STR || b === TAIL_WILDCARD_REG_EXP_STR) {
return -1;
}
if (a === LABEL_REG_EXP_STR) {
return 1;
} else if (b === LABEL_REG_EXP_STR) {
return -1;
}
return a.length === b.length ? a < b ? -1 : 1 : b.length - a.length;
}
var Node = class _Node {
#index;
#varIndex;
#children = /* @__PURE__ */ Object.create(null);
insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
if (tokens.length === 0) {
if (this.#index !== void 0) {
throw PATH_ERROR;
}
if (pathErrorCheckOnly) {
return;
}
this.#index = index;
return;
}
const [token, ...restTokens] = tokens;
const pattern = token === "*" ? restTokens.length === 0 ? ["", "", ONLY_WILDCARD_REG_EXP_STR] : ["", "", LABEL_REG_EXP_STR] : token === "/*" ? ["", "", TAIL_WILDCARD_REG_EXP_STR] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
let node;
if (pattern) {
const name = pattern[1];
let regexpStr = pattern[2] || LABEL_REG_EXP_STR;
if (name && pattern[2]) {
if (regexpStr === ".*") {
throw PATH_ERROR;
}
regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:");
if (/\((?!\?:)/.test(regexpStr)) {
throw PATH_ERROR;
}
}
node = this.#children[regexpStr];
if (!node) {
if (Object.keys(this.#children).some(
(k) => k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR
)) {
throw PATH_ERROR;
}
if (pathErrorCheckOnly) {
return;
}
node = this.#children[regexpStr] = new _Node();
if (name !== "") {
node.#varIndex = context.varIndex++;
}
}
if (!pathErrorCheckOnly && name !== "") {
paramMap.push([name, node.#varIndex]);
}
} else {
node = this.#children[token];
if (!node) {
if (Object.keys(this.#children).some(
(k) => k.length > 1 && k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR
)) {
throw PATH_ERROR;
}
if (pathErrorCheckOnly) {
return;
}
node = this.#children[token] = new _Node();
}
}
node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
}
buildRegExpStr() {
const childKeys = Object.keys(this.#children).sort(compareKey);
const strList = childKeys.map((k) => {
const c = this.#children[k];
return (typeof c.#varIndex === "number" ? `(${k})@${c.#varIndex}` : regExpMetaChars.has(k) ? `\\${k}` : k) + c.buildRegExpStr();
});
if (typeof this.#index === "number") {
strList.unshift(`#${this.#index}`);
}
if (strList.length === 0) {
return "";
}
if (strList.length === 1) {
return strList[0];
}
return "(?:" + strList.join("|") + ")";
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router/reg-exp-router/trie.js
var Trie = class {
#context = { varIndex: 0 };
#root = new Node();
insert(path, index, pathErrorCheckOnly) {
const paramAssoc = [];
const groups = [];
for (let i = 0; ; ) {
let replaced = false;
path = path.replace(/\{[^}]+\}/g, (m) => {
const mark = `@\\${i}`;
groups[i] = [mark, m];
i++;
replaced = true;
return mark;
});
if (!replaced) {
break;
}
}
const tokens = path.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
for (let i = groups.length - 1; i >= 0; i--) {
const [mark] = groups[i];
for (let j = tokens.length - 1; j >= 0; j--) {
if (tokens[j].indexOf(mark) !== -1) {
tokens[j] = tokens[j].replace(mark, groups[i][1]);
break;
}
}
}
this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
return paramAssoc;
}
buildRegExp() {
let regexp = this.#root.buildRegExpStr();
if (regexp === "") {
return [/^$/, [], []];
}
let captureIndex = 0;
const indexReplacementMap = [];
const paramReplacementMap = [];
regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => {
if (handlerIndex !== void 0) {
indexReplacementMap[++captureIndex] = Number(handlerIndex);
return "$()";
}
if (paramIndex !== void 0) {
paramReplacementMap[Number(paramIndex)] = ++captureIndex;
return "";
}
return "";
});
return [new RegExp(`^${regexp}`), indexReplacementMap, paramReplacementMap];
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router/reg-exp-router/router.js
var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
function buildWildcardRegExp(path) {
return wildcardRegExpCache[path] ??= new RegExp(
path === "*" ? "" : `^${path.replace(
/\/\*$|([.\\+*[^\]$()])/g,
(_, metaChar) => metaChar ? `\\${metaChar}` : "(?:|/.*)"
)}$`
);
}
function clearWildcardRegExpCache() {
wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
}
function buildMatcherFromPreprocessedRoutes(routes) {
const trie = new Trie();
const handlerData = [];
if (routes.length === 0) {
return nullMatcher;
}
const routesWithStaticPathFlag = routes.map(
(route) => [!/\*|\/:/.test(route[0]), ...route]
).sort(
([isStaticA, pathA], [isStaticB, pathB]) => isStaticA ? 1 : isStaticB ? -1 : pathA.length - pathB.length
);
const staticMap = /* @__PURE__ */ Object.create(null);
for (let i = 0, j = -1, len = routesWithStaticPathFlag.length; i < len; i++) {
const [pathErrorCheckOnly, path, handlers] = routesWithStaticPathFlag[i];
if (pathErrorCheckOnly) {
staticMap[path] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
} else {
j++;
}
let paramAssoc;
try {
paramAssoc = trie.insert(path, j, pathErrorCheckOnly);
} catch (e) {
throw e === PATH_ERROR ? new UnsupportedPathError(path) : e;
}
if (pathErrorCheckOnly) {
continue;
}
handlerData[j] = handlers.map(([h, paramCount]) => {
const paramIndexMap = /* @__PURE__ */ Object.create(null);
paramCount -= 1;
for (; paramCount >= 0; paramCount--) {
const [key, value] = paramAssoc[paramCount];
paramIndexMap[key] = value;
}
return [h, paramIndexMap];
});
}
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
for (let i = 0, len = handlerData.length; i < len; i++) {
for (let j = 0, len2 = handlerData[i].length; j < len2; j++) {
const map = handlerData[i][j]?.[1];
if (!map) {
continue;
}
const keys = Object.keys(map);
for (let k = 0, len3 = keys.length; k < len3; k++) {
map[keys[k]] = paramReplacementMap[map[keys[k]]];
}
}
}
const handlerMap = [];
for (const i in indexReplacementMap) {
handlerMap[i] = handlerData[indexReplacementMap[i]];
}
return [regexp, handlerMap, staticMap];
}
function findMiddleware(middleware, path) {
if (!middleware) {
return void 0;
}
for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
if (buildWildcardRegExp(k).test(path)) {
return [...middleware[k]];
}
}
return void 0;
}
var RegExpRouter = class {
name = "RegExpRouter";
#middleware;
#routes;
constructor() {
this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
}
add(method, path, handler) {
const middleware = this.#middleware;
const routes = this.#routes;
if (!middleware || !routes) {
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
}
if (!middleware[method]) {
;
[middleware, routes].forEach((handlerMap) => {
handlerMap[method] = /* @__PURE__ */ Object.create(null);
Object.keys(handlerMap[METHOD_NAME_ALL]).forEach((p) => {
handlerMap[method][p] = [...handlerMap[METHOD_NAME_ALL][p]];
});
});
}
if (path === "/*") {
path = "*";
}
const paramCount = (path.match(/\/:/g) || []).length;
if (/\*$/.test(path)) {
const re = buildWildcardRegExp(path);
if (method === METHOD_NAME_ALL) {
Object.keys(middleware).forEach((m) => {
middleware[m][path] ||= findMiddleware(middleware[m], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
});
} else {
middleware[method][path] ||= findMiddleware(middleware[method], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
}
Object.keys(middleware).forEach((m) => {
if (method === METHOD_NAME_ALL || method === m) {
Object.keys(middleware[m]).forEach((p) => {
re.test(p) && middleware[m][p].push([handler, paramCount]);
});
}
});
Object.keys(routes).forEach((m) => {
if (method === METHOD_NAME_ALL || method === m) {
Object.keys(routes[m]).forEach(
(p) => re.test(p) && routes[m][p].push([handler, paramCount])
);
}
});
return;
}
const paths = checkOptionalParameter(path) || [path];
for (let i = 0, len = paths.length; i < len; i++) {
const path2 = paths[i];
Object.keys(routes).forEach((m) => {
if (method === METHOD_NAME_ALL || method === m) {
routes[m][path2] ||= [
...findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || []
];
routes[m][path2].push([handler, paramCount - len + i + 1]);
}
});
}
}
match = match;
buildAllMatchers() {
const matchers = /* @__PURE__ */ Object.create(null);
Object.keys(this.#routes).concat(Object.keys(this.#middleware)).forEach((method) => {
matchers[method] ||= this.#buildMatcher(method);
});
this.#middleware = this.#routes = void 0;
clearWildcardRegExpCache();
return matchers;
}
#buildMatcher(method) {
const routes = [];
let hasOwnRoute = method === METHOD_NAME_ALL;
[this.#middleware, this.#routes].forEach((r) => {
const ownRoute = r[method] ? Object.keys(r[method]).map((path) => [path, r[method][path]]) : [];
if (ownRoute.length !== 0) {
hasOwnRoute ||= true;
routes.push(...ownRoute);
} else if (method !== METHOD_NAME_ALL) {
routes.push(
...Object.keys(r[METHOD_NAME_ALL]).map((path) => [path, r[METHOD_NAME_ALL][path]])
);
}
});
if (!hasOwnRoute) {
return null;
} else {
return buildMatcherFromPreprocessedRoutes(routes);
}
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router/smart-router/router.js
var SmartRouter = class {
name = "SmartRouter";
#routers = [];
#routes = [];
constructor(init) {
this.#routers = init.routers;
}
add(method, path, handler) {
if (!this.#routes) {
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
}
this.#routes.push([method, path, handler]);
}
match(method, path) {
if (!this.#routes) {
throw new Error("Fatal error");
}
const routers = this.#routers;
const routes = this.#routes;
const len = routers.length;
let i = 0;
let res;
for (; i < len; i++) {
const router = routers[i];
try {
for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
router.add(...routes[i2]);
}
res = router.match(method, path);
} catch (e) {
if (e instanceof UnsupportedPathError) {
continue;
}
throw e;
}
this.match = router.match.bind(router);
this.#routers = [router];
this.#routes = void 0;
break;
}
if (i === len) {
throw new Error("Fatal error");
}
this.name = `SmartRouter + ${this.activeRouter.name}`;
return res;
}
get activeRouter() {
if (this.#routes || this.#routers.length !== 1) {
throw new Error("No active router has been determined yet.");
}
return this.#routers[0];
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router/trie-router/node.js
var emptyParams = /* @__PURE__ */ Object.create(null);
var hasChildren = (children) => {
for (const _ in children) {
return true;
}
return false;
};
var Node2 = class _Node2 {
#methods;
#children;
#patterns;
#order = 0;
#params = emptyParams;
constructor(method, handler, children) {
this.#children = children || /* @__PURE__ */ Object.create(null);
this.#methods = [];
if (method && handler) {
const m = /* @__PURE__ */ Object.create(null);
m[method] = { handler, possibleKeys: [], score: 0 };
this.#methods = [m];
}
this.#patterns = [];
}
insert(method, path, handler) {
this.#order = ++this.#order;
let curNode = this;
const parts = splitRoutingPath(path);
const possibleKeys = [];
for (let i = 0, len = parts.length; i < len; i++) {
const p = parts[i];
const nextP = parts[i + 1];
const pattern = getPattern(p, nextP);
const key = Array.isArray(pattern) ? pattern[0] : p;
if (key in curNode.#children) {
curNode = curNode.#children[key];
if (pattern) {
possibleKeys.push(pattern[1]);
}
continue;
}
curNode.#children[key] = new _Node2();
if (pattern) {
curNode.#patterns.push(pattern);
possibleKeys.push(pattern[1]);
}
curNode = curNode.#children[key];
}
curNode.#methods.push({
[method]: {
handler,
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
score: this.#order
}
});
return curNode;
}
#pushHandlerSets(handlerSets, node, method, nodeParams, params) {
for (let i = 0, len = node.#methods.length; i < len; i++) {
const m = node.#methods[i];
const handlerSet = m[method] || m[METHOD_NAME_ALL];
const processedSet = {};
if (handlerSet !== void 0) {
handlerSet.params = /* @__PURE__ */ Object.create(null);
handlerSets.push(handlerSet);
if (nodeParams !== emptyParams || params && params !== emptyParams) {
for (let i2 = 0, len2 = handlerSet.possibleKeys.length; i2 < len2; i2++) {
const key = handlerSet.possibleKeys[i2];
const processed = processedSet[handlerSet.score];
handlerSet.params[key] = params?.[key] && !processed ? params[key] : nodeParams[key] ?? params?.[key];
processedSet[handlerSet.score] = true;
}
}
}
}
}
search(method, path) {
const handlerSets = [];
this.#params = emptyParams;
const curNode = this;
let curNodes = [curNode];
const parts = splitPath(path);
const curNodesQueue = [];
const len = parts.length;
let partOffsets = null;
for (let i = 0; i < len; i++) {
const part = parts[i];
const isLast = i === len - 1;
const tempNodes = [];
for (let j = 0, len2 = curNodes.length; j < len2; j++) {
const node = curNodes[j];
const nextNode = node.#children[part];
if (nextNode) {
nextNode.#params = node.#params;
if (isLast) {
if (nextNode.#children["*"]) {
this.#pushHandlerSets(handlerSets, nextNode.#children["*"], method, node.#params);
}
this.#pushHandlerSets(handlerSets, nextNode, method, node.#params);
} else {
tempNodes.push(nextNode);
}
}
for (let k = 0, len3 = node.#patterns.length; k < len3; k++) {
const pattern = node.#patterns[k];
const params = node.#params === emptyParams ? {} : { ...node.#params };
if (pattern === "*") {
const astNode = node.#children["*"];
if (astNode) {
this.#pushHandlerSets(handlerSets, astNode, method, node.#params);
astNode.#params = params;
tempNodes.push(astNode);
}
continue;
}
const [key, name, matcher] = pattern;
if (!part && !(matcher instanceof RegExp)) {
continue;
}
const child = node.#children[key];
if (matcher instanceof RegExp) {
if (partOffsets === null) {
partOffsets = new Array(len);
let offset = path[0] === "/" ? 1 : 0;
for (let p = 0; p < len; p++) {
partOffsets[p] = offset;
offset += parts[p].length + 1;
}
}
const restPathString = path.substring(partOffsets[i]);
const m = matcher.exec(restPathString);
if (m) {
params[name] = m[0];
this.#pushHandlerSets(handlerSets, child, method, node.#params, params);
if (hasChildren(child.#children)) {
child.#params = params;
const componentCount = m[0].match(/\//)?.length ?? 0;
const targetCurNodes = curNodesQueue[componentCount] ||= [];
targetCurNodes.push(child);
}
continue;
}
}
if (matcher === true || matcher.test(part)) {
params[name] = part;
if (isLast) {
this.#pushHandlerSets(handlerSets, child, method, params, node.#params);
if (child.#children["*"]) {
this.#pushHandlerSets(
handlerSets,
child.#children["*"],
method,
params,
node.#params
);
}
} else {
child.#params = params;
tempNodes.push(child);
}
}
}
}
const shifted = curNodesQueue.shift();
curNodes = shifted ? tempNodes.concat(shifted) : tempNodes;
}
if (handlerSets.length > 1) {
handlerSets.sort((a, b) => {
return a.score - b.score;
});
}
return [handlerSets.map(({ handler, params }) => [handler, params])];
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/router/trie-router/router.js
var TrieRouter = class {
name = "TrieRouter";
#node;
constructor() {
this.#node = new Node2();
}
add(method, path, handler) {
const results = checkOptionalParameter(path);
if (results) {
for (let i = 0, len = results.length; i < len; i++) {
this.#node.insert(method, results[i], handler);
}
return;
}
this.#node.insert(method, path, handler);
}
match(method, path) {
return this.#node.search(method, path);
}
};
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/hono.js
var Hono2 = class extends Hono {
/**
* Creates an instance of the Hono class.
*
* @param options - Optional configuration options for the Hono instance.
*/
constructor(options = {}) {
super(options);
this.router = options.router ?? new SmartRouter({
routers: [new RegExpRouter(), new TrieRouter()]
});
}
};
// kbdb/src/actions/library-predicate.ts
var UNLABELLED_LIBRARY = "general";
function libraryOf(expr) {
return `COALESCE(NULLIF(${expr}, ''), '${UNLABELLED_LIBRARY}')`;
}
function libraryOfValue(value) {
const v = (value ?? "").trim();
return v === "" ? UNLABELLED_LIBRARY : v;
}
var ENTRY_LIBRARY_EXPR = "json_extract(metadata_json, '$.library')";
var ENTRY_LIBRARY = libraryOf(ENTRY_LIBRARY_EXPR);
var ENTRY_UNLABELLED = `(${ENTRY_LIBRARY_EXPR} IS NULL OR ${ENTRY_LIBRARY_EXPR} = '')`;
function parseLibraryList(raw2) {
if (!raw2) return void 0;
const libs = raw2.split(",").map((s) => s.trim()).filter(Boolean);
return libs.length > 0 ? libs : void 0;
}
// kbdb/src/actions/entity-canon.ts
var NOTE_EXT = /\.(md|markdown|mdx|txt|org)$/i;
function unwrapWhole(t) {
const code = /^(`+)([\s\S]*?)(`+)$/.exec(t);
if (code) {
const inner = code[2].trim();
if (inner && !inner.includes("`")) return inner;
}
if (t.startsWith("[[") && t.endsWith("]]") && t.length > 4) {
const inner = t.slice(2, -2).trim();
if (inner && !inner.includes("[[") && !inner.includes("]]")) return inner;
}
return t;
}
function canonicalEntity(raw2) {
if (typeof raw2 !== "string") return raw2;
const fallback = raw2.trim();
let t = raw2.normalize("NFC").trim();
if (!t) return fallback;
for (let i = 0; i < 4; i++) {
const before = t;
t = unwrapWhole(t);
if (t === before) break;
}
if (NOTE_EXT.test(t)) {
t = t.replace(NOTE_EXT, "");
const cut = Math.max(t.lastIndexOf("/"), t.lastIndexOf("\\"));
if (cut >= 0) t = t.slice(cut + 1);
}
t = t.replace(/\s+/g, " ").trim();
return t || fallback;
}
var ENTITY_SLOTS = ["subject", "object"];
function isTripletShaped(slots) {
return slots.includes("subject") && slots.includes("predicate") && slots.includes("object");
}
function canonicalizeEntityValues(slots, values) {
if (!isTripletShaped(slots)) return values;
let touched = false;
const out = { ...values };
for (const slot of ENTITY_SLOTS) {
const v = out[slot];
if (typeof v !== "string") continue;
const c = canonicalEntity(v);
if (c !== v) {
out[slot] = c;
touched = true;
}
}
return touched ? out : values;
}
// kbdb/src/actions/record-crud.ts
function uid(prefix) {
return `${prefix}_${crypto.randomUUID()}`;
}
var SYS_ROOT = "sys_root";
var SYS_BELONGS = "sys_belongs";
var SYS_FIELD_OF = "sys_field_of";
function fieldEntryId(templateId, slot) {
return `fld_${templateId}_${slot}`;
}
async function ensureAnchors(db) {
await db.prepare(
`INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES
('${SYS_ROOT}', 'root', 'system', NULL),
('${SYS_BELONGS}', 'belongs', 'system', NULL),
('${SYS_FIELD_OF}', 'field_of', 'system', NULL)`
).run();
}
async function ensureFieldEntries(db, templateId, slots) {
for (const slot of slots) {
const fid = fieldEntryId(templateId, slot);
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type) VALUES (?, ?, 'field')`).bind(fid, slot).run();
await db.prepare(
`INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, '${SYS_FIELD_OF}', ?)`
).bind(`relf_${templateId}_${slot}`, fid, templateId).run();
}
}
async function createTemplate(db, input) {
const id = input.id ?? uid("tpl");
await db.prepare(`INSERT INTO templates (id, name, description, slots_json, created_by) VALUES (?, ?, ?, ?, ?)`).bind(id, input.name, input.description ?? null, JSON.stringify(input.slots), input.created_by ?? null).run();
await ensureAnchors(db);
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type) VALUES (?, ?, 'sheet')`).bind(id, input.name).run();
await db.prepare(
`INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, '${SYS_BELONGS}', '${SYS_ROOT}')`
).bind(`relb_${id}`, id).run();
await ensureFieldEntries(db, id, input.slots);
const row = await getTemplate(db, id);
if (!row) throw new Error("createTemplate: row not found after insert");
return row;
}
async function getTemplate(db, idOrName) {
const row = await db.prepare("SELECT * FROM templates WHERE id = ? OR name = ? LIMIT 1").bind(idOrName, idOrName).first();
return row ?? null;
}
async function listTemplates(db) {
const res = await db.prepare("SELECT * FROM templates ORDER BY created_at DESC").all();
return res.results ?? [];
}
async function updateTemplate(db, id, patch) {
const cols = [];
const params = [];
if (patch.description !== void 0) {
cols.push("description = ?");
params.push(patch.description);
}
if (patch.slots !== void 0) {
cols.push("slots_json = ?");
params.push(JSON.stringify(patch.slots));
}
if (cols.length === 0) return getTemplate(db, id);
cols.push("updated_at = unixepoch()");
await db.prepare(`UPDATE templates SET ${cols.join(", ")} WHERE id = ?`).bind(...params, id).run();
if (patch.slots !== void 0) await ensureFieldEntries(db, id, patch.slots);
return getTemplate(db, id);
}
function derivedCellIds(recordId, slot) {
return { value: `${recordId}~v~${slot}`, relation: `${recordId}~r~${slot}` };
}
async function loadReferencedEntries(db, entryIds, recordOwnerId) {
const ids = [...new Set(Object.values(entryIds))];
if (ids.length === 0) return /* @__PURE__ */ new Map();
const rows = [];
for (let i = 0; i < ids.length; i += 90) {
const chunk = ids.slice(i, i + 90);
const res = await db.prepare(`SELECT id, content, owner_id FROM entries WHERE id IN (${chunk.map(() => "?").join(",")})`).bind(...chunk).all();
rows.push(...res.results ?? []);
}
const found = new Map(rows.map((r) => [r.id, r]));
const missing = ids.filter((id) => !found.has(id));
if (missing.length > 0) throw new Error(`entry not found: ${missing.join(", ")}`);
if (recordOwnerId != null) {
const foreign = rows.filter((r) => r.owner_id != null && r.owner_id !== recordOwnerId);
if (foreign.length > 0) {
throw new Error(
`entry owner mismatch: ${foreign.map((r) => `${r.id}(${r.owner_id})`).join(", ")} != ${recordOwnerId}`
);
}
}
return new Map(rows.map((r) => [r.id, r.content]));
}
async function recordBelongs(db, recordId) {
const row = await db.prepare(`SELECT dst_id FROM entries WHERE src_id = ? AND rel_id = '${SYS_BELONGS}' AND dst_id != '${SYS_ROOT}' LIMIT 1`).bind(recordId).first();
return row ?? null;
}
async function insertCellRelation(db, recordId, templateId, slot, dstEntryId, ownerId) {
await db.prepare(
`INSERT INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, ?, ?, ?)`
).bind(uid("relv"), ownerId, recordId, fieldEntryId(templateId, slot), dstEntryId).run();
}
async function createRecord(db, input) {
const tpl = await getTemplate(db, input.template);
if (!tpl) throw new Error(`template not found: ${input.template}`);
const slots = JSON.parse(tpl.slots_json);
const recordId = input.record_id ?? uid("rec");
const values = canonicalizeEntityValues(slots, input.values ?? {});
const entryIds = input.entry_ids ?? {};
const refSlots = Object.keys(entryIds);
const ownerId = input.owner_id ?? null;
const both = refSlots.filter((s) => s in values);
if (both.length > 0) throw new Error(`slot given both value and entry_id: ${both.join(", ")}`);
const unknown = refSlots.filter((s) => !slots.includes(s));
if (unknown.length > 0) throw new Error(`slot not in template: ${unknown.join(", ")}`);
const referenced = await loadReferencedEntries(db, entryIds, ownerId);
const mapTracked = isMapTrackedTemplate(tpl.name);
const mapBefore = mapTracked && input.record_id ? (await getRecord(db, input.record_id))?.values ?? null : null;
await db.prepare(`INSERT OR IGNORE INTO entries (id, entry_type, owner_id) VALUES (?, 'record', ?)`).bind(recordId, ownerId).run();
await db.prepare(
`INSERT OR IGNORE INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, ?, '${SYS_BELONGS}', ?)`
).bind(`relb_${recordId}_${tpl.id}`, ownerId, recordId, tpl.id).run();
const writtenSlots = slots.filter((s) => s in entryIds || s in values);
await ensureFieldEntries(db, tpl.id, writtenSlots);
for (const slot of writtenSlots) {
if (slot in entryIds) {
await insertCellRelation(db, recordId, tpl.id, slot, entryIds[slot], ownerId);
continue;
}
if (input.derived_cell_ids) {
const ids = derivedCellIds(recordId, slot);
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES (?, ?, 'value', ?)`).bind(ids.value, values[slot], ownerId).run();
await db.prepare(`INSERT OR IGNORE INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, ?, ?, ?)`).bind(ids.relation, ownerId, recordId, fieldEntryId(tpl.id, slot), ids.value).run();
continue;
}
const entry = await createEntry(db, {
content: values[slot],
entry_type: "value",
owner_id: ownerId
});
await insertCellRelation(db, recordId, tpl.id, slot, entry.id, ownerId);
}
const out = { ...values };
for (const [slot, entryId] of Object.entries(entryIds)) out[slot] = referenced.get(entryId) ?? "";
if (mapTracked) {
const stored = Object.fromEntries(Object.entries(out).filter(([slot]) => slots.includes(slot)));
const mapAfter = input.record_id ? (await getRecord(db, recordId))?.values ?? stored : stored;
await noteRecordWrite(db, tpl.name, ownerId, mapBefore, mapAfter);
}
return { record_id: recordId, template_id: tpl.id, values: out, owner_id: ownerId };
}
async function updateRecord(db, recordId, values) {
const belongs = await recordBelongs(db, recordId);
if (!belongs) return null;
const templateId = belongs.dst_id;
const tpl = await getTemplate(db, templateId);
const mapBefore = tpl && isMapTrackedTemplate(tpl.name) ? await getRecord(db, recordId) : null;
const cellRes = await db.prepare(
`SELECT f.content AS slot_name, r.dst_id AS entry_id
FROM entries r JOIN entries f ON r.rel_id = f.id
WHERE r.src_id = ? AND r.rel_id != '${SYS_BELONGS}'`
).bind(recordId).all();
const cells = cellRes.results ?? [];
const slotToEntries = /* @__PURE__ */ new Map();
for (const c of cells) {
const list = slotToEntries.get(c.slot_name) ?? [];
list.push(c.entry_id);
slotToEntries.set(c.slot_name, list);
}
const identity = await db.prepare("SELECT owner_id FROM entries WHERE id = ?").bind(recordId).first();
const recordOwnerId = identity?.owner_id ?? null;
const allowed = tpl ? JSON.parse(tpl.slots_json) : [...slotToEntries.keys()];
const canon = canonicalizeEntityValues(allowed, values);
for (const [slot, content] of Object.entries(canon)) {
if (!allowed.includes(slot)) {
throw new Error(`slot not in template: ${slot}`);
}
const entryIds = slotToEntries.get(slot);
if (entryIds && entryIds.length > 0) {
for (const entryId of entryIds) {
await db.prepare(`UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?`).bind(content, entryId).run();
}
} else {
await ensureFieldEntries(db, templateId, [slot]);
const entry = await createEntry(db, { content, entry_type: "value", owner_id: recordOwnerId });
await insertCellRelation(db, recordId, templateId, slot, entry.id, recordOwnerId);
}
}
const after = await getRecord(db, recordId);
if (tpl && mapBefore) await noteRecordWrite(db, tpl.name, recordOwnerId, mapBefore.values, after?.values ?? null);
return after;
}
async function getRecord(db, recordId) {
const belongs = await recordBelongs(db, recordId);
if (!belongs) return null;
const res = await db.prepare(
`SELECT f.content AS slot, v.content AS content
FROM entries r
JOIN entries f ON r.rel_id = f.id
JOIN entries v ON r.dst_id = v.id
WHERE r.src_id = ? AND r.rel_id != '${SYS_BELONGS}'`
).bind(recordId).all();
const values = {};
for (const r of res.results ?? []) values[r.slot] = r.content;
const identity = await db.prepare("SELECT owner_id FROM entries WHERE id = ?").bind(recordId).first();
return { record_id: recordId, template_id: belongs.dst_id, values, owner_id: identity?.owner_id ?? null };
}
async function countSheetMembers(db, sheetId, owner_id, offset, limit, got) {
if (offset === 0 && got < limit) return got;
const row = owner_id ? await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/)
`SELECT COUNT(*) AS total FROM entries WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? AND +owner_id = ?`
).bind(sheetId, owner_id).first() : await db.prepare(
// kbdb-sql-ok:同上
`SELECT COUNT(*) AS total FROM entries WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ?`
).bind(sheetId).first();
return row?.total ?? 0;
}
async function searchByTemplatePage(db, template, owner_id, limit = 100, offset = 0) {
const tpl = await getTemplate(db, template);
if (!tpl) return { records: [], total: 0 };
const cap = Math.min(Math.max(limit, 1), 500);
const skip = Math.max(offset, 0);
const res = owner_id ? await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/);本次 checkout 開在 worktree /private/tmp/wt-graph-first-44/,hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到,與 962d863/5919c6b 記載的是同一個假警報
`SELECT src_id AS record_id FROM entries
WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? AND owner_id = ?
ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?`
).bind(tpl.id, owner_id, cap, skip).all() : await db.prepare(
// kbdb-sql-ok:同上(worktree 路徑假警報)
`SELECT src_id AS record_id FROM entries
WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ?
ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?`
).bind(tpl.id, cap, skip).all();
const ids = (res.results ?? []).map((r) => r.record_id);
const total = await countSheetMembers(db, tpl.id, owner_id, skip, cap, ids.length);
if (ids.length === 0) return { records: [], total };
const byId = /* @__PURE__ */ new Map();
for (const id of ids) byId.set(id, { record_id: id, template_id: tpl.id, values: {}, owner_id: null });
for (let i = 0; i < ids.length; i += 90) {
const chunk = ids.slice(i, i + 90);
const placeholders = chunk.map(() => "?").join(",");
const [cellRes, identRes] = await Promise.all([
db.prepare(
`SELECT r.src_id AS record_id, f.content AS slot, v.content AS content
FROM entries r
JOIN entries f ON r.rel_id = f.id
JOIN entries v ON r.dst_id = v.id
WHERE r.src_id IN (${placeholders}) AND r.rel_id != '${SYS_BELONGS}'`
).bind(...chunk).all(),
db.prepare(`SELECT id, owner_id FROM entries WHERE id IN (${placeholders})`).bind(...chunk).all()
]);
for (const r of cellRes.results ?? []) {
const rec = byId.get(r.record_id);
if (rec) rec.values[r.slot] = r.content;
}
for (const r of identRes.results ?? []) {
const rec = byId.get(r.id);
if (rec) rec.owner_id = r.owner_id;
}
}
return { records: ids.map((id) => byId.get(id)).filter((r) => !!r), total };
}
async function deleteRecord(db, recordId) {
const belongs = await recordBelongs(db, recordId);
if (!belongs) return false;
const mapTpl = await getTemplate(db, belongs.dst_id);
const mapBefore = mapTpl && isMapTrackedTemplate(mapTpl.name) ? await getRecord(db, recordId) : null;
const cellRes = await db.prepare(`SELECT dst_id FROM entries WHERE src_id = ? AND rel_id != '${SYS_BELONGS}'`).bind(recordId).all();
const dsts = (cellRes.results ?? []).map((r) => r.dst_id);
await db.prepare(`DELETE FROM entries WHERE src_id = ?`).bind(recordId).run();
await db.prepare(
`DELETE FROM entries WHERE id = ?1 AND entry_type = 'record'
AND NOT EXISTS (SELECT 1 FROM entries WHERE dst_id = ?1)`
).bind(recordId).run();
for (const dst of dsts) {
await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/);worktree 開在 matrix/arcrun-wt-218/,hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到——與本檔既有註解記載的同一個假警報
`DELETE FROM entries WHERE id = ?1
AND entry_type NOT IN ('sheet', 'field', 'system')
AND NOT EXISTS (SELECT 1 FROM entries WHERE dst_id = ?1)
AND NOT EXISTS (SELECT 1 FROM entries WHERE src_id = ?1)`
).bind(dst).run();
}
if (mapTpl && mapBefore) await noteRecordWrite(db, mapTpl.name, mapBefore.owner_id, mapBefore.values, null);
return true;
}
// kbdb/src/actions/library-map-store.ts
var LIBRARY_MAP_TEMPLATE_ID = "tpl-library-map";
var LIBRARY_MAP_TEMPLATE_NAME = "library_map";
var LIBRARY_MAP_SLOTS = [
"library",
"narrative",
"top_entities",
"relation_profile",
"bridges",
"triplet_count",
"commit_hash",
"status",
"entry_count"
];
var DEFAULT_TRIPLET_TEMPLATE = "triplet";
var PORTAL_LIBRARY_TEMPLATE = "portal_library";
var ENTITY_SKETCH_SIZE = 128;
var PREDICATE_SKETCH_SIZE = 100;
var LIBRARY_MAP_TEMPLATE_DESCRIPTION = "per-library map block\uFF08\u85CF\u66F8\u5730\u5716\uFF1Atop_entities\uFF0Frelation_profile\uFF0Fbridges\uFF0Ftriplet_count \u7531 graph \u6A5F\u68B0\u5C0E\u51FA\uFF1Bnarrative \u7531 LLM \u8457\u4F5C\uFF0C\u7B97\u4E0D\u51FA\u4F86\u3001\u4E1F\u4E86\u56DE\u4E0D\u4F86\uFF1BArcrun#39\uFF0F#44\uFF09";
var NOT_KNOWLEDGE_ENTRY_TYPES = [
"value",
"record",
"sheet",
"field",
"system",
"relation",
"execution_log",
"execution_log_usage",
"execution_log_retention_config",
"credential",
"embed_backfill_usage",
"kbdb_maintenance_usage",
"recipe_stat"
];
function mapContent(library, narrative, coreNames) {
const core = coreNames.length ? coreNames.join("\u3001") : "\uFF08\u5C1A\u7121 entities\uFF09";
const text = narrative?.trim();
if (!text) return `${library}\u3002\u6838\u5FC3\uFF1A${core}`;
const sep = /[。..!!??;;]$/.test(text) ? "" : "\u3002";
return `${library}\uFF1A${text}${sep}\u6838\u5FC3\uFF1A${core}`;
}
async function ensureLibraryMapTemplate(db) {
const existing = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME);
if (existing) {
const slots = JSON.parse(existing.slots_json);
const missing = LIBRARY_MAP_SLOTS.filter((s) => !slots.includes(s));
const patch = {};
if (existing.description !== LIBRARY_MAP_TEMPLATE_DESCRIPTION) patch.description = LIBRARY_MAP_TEMPLATE_DESCRIPTION;
if (missing.length) patch.slots = [...slots, ...missing];
if (patch.description !== void 0 || patch.slots !== void 0) await updateTemplate(db, existing.id, patch);
return;
}
try {
await createTemplate(db, {
id: LIBRARY_MAP_TEMPLATE_ID,
name: LIBRARY_MAP_TEMPLATE_NAME,
description: LIBRARY_MAP_TEMPLATE_DESCRIPTION,
slots: LIBRARY_MAP_SLOTS,
created_by: "system"
});
} catch {
if (!await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME)) throw new Error("ensureLibraryMapTemplate failed");
}
}
async function ensureTripletLibrarySlot(db, tripletTemplate) {
const tpl = await getTemplate(db, tripletTemplate);
if (!tpl) throw new Error(`triplet template not found: ${tripletTemplate}`);
const slots = JSON.parse(tpl.slots_json);
if (slots.includes("library")) return false;
await updateTemplate(db, tpl.id, { slots: [...slots, "library"] });
return true;
}
var templateReady = /* @__PURE__ */ new WeakMap();
var adoptedOwners = /* @__PURE__ */ new WeakMap();
var knownMaps = /* @__PURE__ */ new WeakMap();
var adoptionJobs = /* @__PURE__ */ new WeakMap();
function setFor(store, db) {
let s = store.get(db);
if (!s) {
s = /* @__PURE__ */ new Set();
store.set(db, s);
}
return s;
}
function ensureTemplateOnce(db) {
let p = templateReady.get(db);
if (!p) {
p = ensureLibraryMapTemplate(db).catch((e) => {
templateReady.delete(db);
throw e;
});
templateReady.set(db, p);
}
return p;
}
var writeSeqCounter = 0;
function nextWriteSeq() {
writeSeqCounter += 1;
return writeSeqCounter;
}
var MAP_PAGE_PREFIX = "library-map@";
var MAP_PAGE_UPPER_ALL = "library-mapA";
var LIBRARY_MAP_ID_PREFIX = "lmap";
var NOT_LIBRARY_MAP_ROW = `(substr(id, 1, 4) != '${LIBRARY_MAP_ID_PREFIX}' AND substr(id, 1, 9) != 'relb_${LIBRARY_MAP_ID_PREFIX}')`;
var NOT_LIBRARY_MAP_CELL = `(substr(id, 1, 4) != '${LIBRARY_MAP_ID_PREFIX}' OR instr(id, '~') = 0)`;
async function hexDigest(input, chars) {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, chars);
}
function ownerKey(owner) {
return owner || "";
}
async function ownerHash(owner) {
return hexDigest(`owner ${ownerKey(owner)}`, 16);
}
async function mapIdentity(owner, library) {
const oh = await ownerHash(owner);
const mh = await hexDigest(`map ${ownerKey(owner)} ${library}`, 32);
return { id: `lmap_${mh}`, pageName: `${MAP_PAGE_PREFIX}${oh}:${library}` };
}
async function adoptionMarkerId(owner) {
return `lmapo_${await hexDigest(`adopted ${ownerKey(owner)}`, 32)}`;
}
var cellId = (mapId, slot) => derivedCellIds(mapId, slot).value;
function parseMeta(raw2) {
if (!raw2) return null;
try {
const v = JSON.parse(raw2);
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
} catch {
return null;
}
}
function countedLibraryOf(e) {
if (!e) return null;
if (e.src_id != null) return null;
if (NOT_KNOWLEDGE_ENTRY_TYPES.includes(e.entry_type)) return null;
const meta = parseMeta(e.metadata_json);
if (e.entry_type === "block" && meta?.kind === "library_map") return null;
const lib = meta?.library;
return libraryOfValue(lib === void 0 || lib === null ? null : String(lib));
}
function countedEntrySql() {
const types = NOT_KNOWLEDGE_ENTRY_TYPES.map((t) => `'${t}'`).join(", ");
return `src_id IS NULL
AND entry_type NOT IN (${types})
AND NOT (entry_type = 'block' AND COALESCE(json_extract(metadata_json, '$.kind'), '') = 'library_map')`;
}
function tripletContribution(values) {
if (!values) return null;
const status = values.status;
if (status !== void 0 && status !== null && status !== "active") return null;
return {
library: libraryOfValue(values.library),
subject: values.subject ?? null,
object: values.object ?? null,
predicate: values.predicate ?? null
};
}
function sameContribution(a, b) {
return a.library === b.library && a.subject === b.subject && a.object === b.object && a.predicate === b.predicate;
}
function parseArray(raw2) {
if (!raw2) return [];
try {
const v = JSON.parse(raw2);
return Array.isArray(v) ? v : [];
} catch {
return [];
}
}
function entitiesToPairs(raw2) {
const out = [];
for (const e of parseArray(raw2)) {
const r = e;
if (typeof r?.name === "string" && typeof r.degree === "number") out.push([r.name, r.degree]);
}
return out;
}
function relationsToPairs(raw2) {
const out = [];
for (const e of parseArray(raw2)) {
const r = e;
if (typeof r?.predicate === "string" && typeof r.count === "number") out.push([r.predicate, r.count]);
}
return out;
}
function sortPairs(pairs) {
pairs.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
}
function bump(pairs, key, delta, capacity) {
const i = pairs.findIndex((p) => p[0] === key);
if (i >= 0) {
pairs[i][1] += delta;
if (pairs[i][1] <= 0) pairs.splice(i, 1);
return;
}
if (delta <= 0) return;
if (pairs.length < capacity) {
pairs.push([key, delta]);
return;
}
let min = 0;
for (let j = 1; j < pairs.length; j++) {
if (pairs[j][1] < pairs[min][1] || pairs[j][1] === pairs[min][1] && pairs[j][0] > pairs[min][0]) min = j;
}
pairs[min] = [key, pairs[min][1] + delta];
}
function top3Key(raw2) {
return entitiesToPairs(raw2).slice(0, 3).map((p) => p[0]).join(" ");
}
async function bumpCounter(db, id, delta) {
await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/);worktree 路徑造成的假警報同 library-map.ts 既有註解
`UPDATE entries
SET content = CAST(MAX(0, CAST(COALESCE(NULLIF(content, ''), '0') AS INTEGER) + ?) AS TEXT),
updated_at = unixepoch()
WHERE id = ?`
).bind(delta, id).run();
}
async function compareAndSetCell(db, id, mutate) {
for (let attempt = 0; attempt < 5; attempt++) {
const row = await db.prepare("SELECT content FROM entries WHERE id = ?").bind(id).first();
if (!row) return null;
const next = mutate(row.content);
if (next === row.content) return { before: row.content, after: next };
const res = await db.prepare("UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ? AND content IS ?").bind(next, id, row.content).run();
const changes = res?.meta?.changes;
if (changes === void 0 || changes > 0) return { before: row.content, after: next };
}
throw new Error(`library map cell stayed contended after 5 attempts: ${id}`);
}
async function readCellsById(db, ids) {
const out = /* @__PURE__ */ new Map();
for (let i = 0; i < ids.length; i += 90) {
const chunk = ids.slice(i, i + 90);
const res = await db.prepare(`SELECT id, content, updated_at FROM entries WHERE id IN (${chunk.map(() => "?").join(",")})`).bind(...chunk).all();
for (const r of res.results ?? []) out.set(r.id, { content: r.content, updated_at: r.updated_at });
}
return out;
}
async function refreshMapSentence(db, mapId, library) {
const cells = await readCellsById(db, [cellId(mapId, "narrative"), cellId(mapId, "top_entities")]);
const narrative = cells.get(cellId(mapId, "narrative"))?.content ?? "";
const top = entitiesToPairs(cells.get(cellId(mapId, "top_entities"))?.content ?? null).slice(0, 3).map((p) => p[0]);
const content = mapContent(library, narrative, top);
await db.prepare("UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ? AND content IS NOT ?").bind(content, mapId, content).run();
}
function defaultMapValues(library) {
return {
library,
narrative: "",
top_entities: "[]",
relation_profile: "[]",
bridges: "[]",
triplet_count: "0",
commit_hash: "",
status: "active",
entry_count: "0"
};
}
async function createMapRecord(db, owner, library, seed) {
await ensureTemplateOnce(db);
const ident = await mapIdentity(owner, library);
const values = { ...defaultMapValues(library), ...seed };
const top = entitiesToPairs(values.top_entities).slice(0, 3).map((p) => p[0]);
await db.prepare(
// kbdb-sql-ok:同上
`INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id, page_name, metadata_json)
VALUES (?, ?, 'block', ?, ?, ?)`
).bind(ident.id, mapContent(library, values.narrative, top), owner, ident.pageName, JSON.stringify({ kind: "library_map", library })).run();
await createRecord(db, {
template: LIBRARY_MAP_TEMPLATE_NAME,
record_id: ident.id,
values,
owner_id: owner,
derived_cell_ids: true
});
return ident.id;
}
async function ensureMapRecord(db, owner, library) {
const ident = await mapIdentity(owner, library);
const known = setFor(knownMaps, db);
if (known.has(ident.id)) return ident.id;
const lastSlot = LIBRARY_MAP_SLOTS[LIBRARY_MAP_SLOTS.length - 1];
const hit = await db.prepare("SELECT id FROM entries WHERE id = ?").bind(cellId(ident.id, lastSlot)).first();
if (!hit) await createMapRecord(db, owner, library, {});
known.add(ident.id);
return ident.id;
}
async function putStoredMap(db, owner, library, values) {
const mapId = await createMapRecord(db, owner, library, values);
for (const [slot, content] of Object.entries(values)) {
await db.prepare("UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ? AND content IS NOT ?").bind(content, cellId(mapId, slot), content).run();
}
await refreshMapSentence(db, mapId, library);
setFor(knownMaps, db).add(mapId);
return mapId;
}
async function putNarrative(db, owner, library, narrative) {
await ensureOwnerAdopted(db, owner, nextWriteSeq());
const mapId = await ensureMapRecord(db, owner, library);
await db.prepare("UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?").bind(narrative, cellId(mapId, "narrative")).run();
await refreshMapSentence(db, mapId, library);
}
function ownerClause(column, owner) {
if (owner === void 0) return { sql: "", params: [] };
if (owner === null) return { sql: ` AND +${column} IS NULL`, params: [] };
return { sql: ` AND +${column} = ?`, params: [owner] };
}
var TRIPLET_SUMMARY_SLOTS = ["subject", "object", "predicate", "status", "library", "source_uri"];
function tripletCellsSql(ownerSql) {
const cols = TRIPLET_SUMMARY_SLOTS.map((s) => `MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_${s}' THEN v.content END) AS ${s}`).join(",\n ");
const rels = TRIPLET_SUMMARY_SLOTS.map((s) => `'fld_' || b.dst_id || '_${s}'`).join(", ");
return `SELECT b.src_id AS rid,
${cols}
FROM entries b
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id IN (${rels})
LEFT JOIN entries v ON v.id = r.dst_id
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerSql}
GROUP BY b.src_id`;
}
async function aggregateTripletSummaries(db, tripletTemplateId, owner, opts = {}) {
const oc = ownerClause("b.owner_id", owner);
const withPrefix = !!(opts.sourcePrefix && opts.library);
const labelled = withPrefix ? `CASE WHEN t.library IS NULL AND t.source_uri LIKE ? || '%' THEN ? ELSE ${libraryOf("t.library")} END` : libraryOf("t.library");
const labelParams = withPrefix ? [opts.sourcePrefix, opts.library] : [];
const filter = opts.library ? "WHERE library = ?" : "";
const filterParams = opts.library ? [opts.library] : [];
const head = `WITH t AS (${tripletCellsSql(oc.sql)}),
act AS (SELECT subject, object, predicate, ${labelled} AS library FROM t WHERE COALESCE(t.status, 'active') = 'active'),
lib AS (SELECT * FROM act ${filter})`;
const params = [tripletTemplateId, ...oc.params, ...labelParams, ...filterParams];
const [countRes, entRes, relRes] = await Promise.all([
db.prepare(`${head} SELECT library, COUNT(*) AS n FROM lib GROUP BY library`).bind(...params).all(),
db.prepare(
// kbdb-sql-ok:同上
`${head},
ent AS (SELECT subject AS name, library FROM lib WHERE subject IS NOT NULL
UNION ALL SELECT object AS name, library FROM lib WHERE object IS NOT NULL),
deg AS (SELECT library, name, COUNT(*) AS n FROM ent GROUP BY library, name),
ranked AS (SELECT library, name, n, ROW_NUMBER() OVER (PARTITION BY library ORDER BY n DESC, name ASC) AS rn FROM deg)
SELECT library, name, n FROM ranked WHERE rn <= ? ORDER BY library, rn`
).bind(...params, ENTITY_SKETCH_SIZE).all(),
db.prepare(
// kbdb-sql-ok:同上
`${head},
deg AS (SELECT library, predicate AS name, COUNT(*) AS n FROM lib WHERE predicate IS NOT NULL GROUP BY library, predicate),
ranked AS (SELECT library, name, n, ROW_NUMBER() OVER (PARTITION BY library ORDER BY n DESC, name ASC) AS rn FROM deg)
SELECT library, name, n FROM ranked WHERE rn <= ? ORDER BY library, rn`
).bind(...params, PREDICATE_SKETCH_SIZE).all()
]);
const out = /* @__PURE__ */ new Map();
const get = (library) => {
const key = String(library);
let s = out.get(key);
if (!s) {
s = { count: 0, entities: [], relations: [] };
out.set(key, s);
}
return s;
};
for (const r of countRes.results ?? []) get(r.library).count = r.n;
for (const r of entRes.results ?? []) get(r.library).entities.push({ name: r.name, degree: r.n });
for (const r of relRes.results ?? []) get(r.library).relations.push({ predicate: r.name, count: r.n });
return out;
}
async function aggregateEntryCounts(db, owner, library) {
const params = owner === null ? [] : [owner];
if (library) params.push(library);
const res = await db.prepare(
// kbdb-sql-ok:同上
`SELECT ${ENTRY_LIBRARY} AS library, COUNT(*) AS n
FROM entries
WHERE ${owner === null ? "owner_id IS NULL" : "owner_id = ?"}
AND ${countedEntrySql()}
${library ? `AND ${ENTRY_LIBRARY} = ?` : ""}
GROUP BY ${ENTRY_LIBRARY}`
).bind(...params).all();
const out = /* @__PURE__ */ new Map();
for (const r of res.results ?? []) out.set(String(r.library), r.n);
return out;
}
async function portalLibraryNames(db, owner) {
const tpl = await getTemplate(db, PORTAL_LIBRARY_TEMPLATE);
if (!tpl) return [];
const oc = ownerClause("b.owner_id", owner);
const res = await db.prepare(
// kbdb-sql-ok:同上
`SELECT MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_name' THEN v.content END) AS name
FROM entries b
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id = 'fld_' || b.dst_id || '_name'
LEFT JOIN entries v ON v.id = r.dst_id
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${oc.sql}
GROUP BY b.src_id`
).bind(tpl.id, ...oc.params).all();
return (res.results ?? []).map((r) => r.name?.trim() ?? "").filter(Boolean);
}
async function readLegacyMaps(db, owner) {
const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME);
if (!tpl) return { byLibrary: /* @__PURE__ */ new Map(), activeIds: [] };
const oc = ownerClause("b.owner_id", owner);
const res = await db.prepare(
// kbdb-sql-ok:同上
`WITH m AS (
SELECT b.src_id AS rid,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_narrative' THEN v.content END) AS narrative,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_top_entities' THEN v.content END) AS top_entities,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_relation_profile' THEN v.content END) AS relation_profile,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_triplet_count' THEN v.content END) AS triplet_count,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_commit_hash' THEN v.content END) AS commit_hash,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status,
MAX(r.created_at) AS ts
FROM entries b
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs'
LEFT JOIN entries v ON v.id = r.dst_id
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${oc.sql}
AND substr(b.src_id, 1, 5) != 'lmap_'
GROUP BY b.src_id)
SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL
ORDER BY m.ts DESC`
).bind(tpl.id, ...oc.params).all();
const byLibrary = /* @__PURE__ */ new Map();
const activeIds = [];
for (const r of res.results ?? []) {
activeIds.push(r.rid);
if (!byLibrary.has(r.library)) byLibrary.set(r.library, r);
}
return { byLibrary, activeIds };
}
async function isOwnerAdopted(db, owner) {
const key = ownerKey(owner);
const adopted = setFor(adoptedOwners, db);
if (adopted.has(key)) return true;
const hit = await db.prepare("SELECT id FROM entries WHERE id = ?").bind(await adoptionMarkerId(owner)).first();
if (hit) adopted.add(key);
return !!hit;
}
async function ensureOwnerAdopted(db, owner, writeSeq) {
if (await isOwnerAdopted(db, owner)) return { baselineIncludesWrite: false };
const key = ownerKey(owner);
let registry = adoptionJobs.get(db);
if (!registry) {
registry = /* @__PURE__ */ new Map();
adoptionJobs.set(db, registry);
}
const jobs = registry;
let job = jobs.get(key);
if (!job) {
const seq = nextWriteSeq();
const done = adoptOwner(db, owner).finally(() => jobs.delete(key));
job = { seq, done };
jobs.set(key, job);
}
await job.done;
setFor(adoptedOwners, db).add(key);
return { baselineIncludesWrite: job.seq > writeSeq };
}
async function adoptOwner(db, owner) {
await ensureTemplateOnce(db);
const tpl = await getTemplate(db, DEFAULT_TRIPLET_TEMPLATE);
const summaries = tpl ? await aggregateTripletSummaries(db, tpl.id, owner) : /* @__PURE__ */ new Map();
const entryCounts = await aggregateEntryCounts(db, owner);
const legacyOwn = await readLegacyMaps(db, owner);
const emptyLegacy = /* @__PURE__ */ new Map();
const legacyShared = owner !== null ? (await readLegacyMaps(db, null)).byLibrary : emptyLegacy;
const portalNames = await portalLibraryNames(db, owner);
const libraries = /* @__PURE__ */ new Set([
...summaries.keys(),
...entryCounts.keys(),
...legacyOwn.byLibrary.keys(),
...portalNames
]);
for (const library of libraries) {
const s = summaries.get(library);
const values = {
triplet_count: String(s?.count ?? 0),
top_entities: JSON.stringify(s?.entities ?? []),
relation_profile: JSON.stringify(s?.relations ?? []),
entry_count: String(entryCounts.get(library) ?? 0)
};
const own = legacyOwn.byLibrary.get(library);
const shared = legacyShared.get(library);
const narrative = own?.narrative?.trim() || shared?.narrative?.trim();
if (narrative) values.narrative = narrative;
const commit = own?.commit_hash || shared?.commit_hash;
if (commit) values.commit_hash = commit;
await putStoredMap(db, owner, library, values);
}
for (const rid of legacyOwn.activeIds) {
await updateRecord(db, rid, { status: "superseded" });
await db.prepare(
// kbdb-sql-ok:同上
`UPDATE entries SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.status', 'deprecated'), updated_at = unixepoch()
WHERE id = ? AND entry_type = 'block'`
).bind(rid).run();
}
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES (?, 'library-map adopted', 'system', ?)`).bind(await adoptionMarkerId(owner), owner).run();
}
function logMapFailure(what, e) {
console.error(`[library-map] ${what}\u6C92\u80FD\u8A18\u9032\u5730\u5716\uFF08\u5730\u5716\u6578\u5B57\u53EF\u80FD\u843D\u5F8C\uFF1BPOST /map/recompute?library= \u53EF\u6821\u6B63\uFF09`, e);
}
function isMapTrackedTemplate(templateName) {
return templateName === DEFAULT_TRIPLET_TEMPLATE || templateName === PORTAL_LIBRARY_TEMPLATE;
}
async function applyTripletContribution(db, owner, c, sign) {
const mapId = await ensureMapRecord(db, owner, c.library);
await bumpCounter(db, cellId(mapId, "triplet_count"), sign);
let sentenceStale = false;
const names = [c.subject, c.object].filter((n) => n !== null);
if (names.length) {
const r = await compareAndSetCell(db, cellId(mapId, "top_entities"), (raw2) => {
const pairs = entitiesToPairs(raw2);
for (const n of names) bump(pairs, n, sign, ENTITY_SKETCH_SIZE);
sortPairs(pairs);
return JSON.stringify(pairs.map(([name, degree]) => ({ name, degree })));
});
if (r && top3Key(r.before) !== top3Key(r.after)) sentenceStale = true;
}
if (c.predicate !== null) {
const predicate = c.predicate;
await compareAndSetCell(db, cellId(mapId, "relation_profile"), (raw2) => {
const pairs = relationsToPairs(raw2);
bump(pairs, predicate, sign, PREDICATE_SKETCH_SIZE);
sortPairs(pairs);
return JSON.stringify(pairs.map(([p, count]) => ({ predicate: p, count })));
});
}
if (sentenceStale) await refreshMapSentence(db, mapId, c.library);
}
async function noteRecordWrite(db, templateName, ownerId, before, after) {
if (!isMapTrackedTemplate(templateName)) return;
const owner = ownerId || null;
const writeSeq = nextWriteSeq();
try {
if (templateName === PORTAL_LIBRARY_TEMPLATE) {
const name = after?.name?.trim();
if (!name) return;
await ensureOwnerAdopted(db, owner, writeSeq);
await ensureMapRecord(db, owner, name);
return;
}
const b = tripletContribution(before);
const a = tripletContribution(after);
if (!b && !a) return;
if (b && a && sameContribution(b, a)) return;
const { baselineIncludesWrite } = await ensureOwnerAdopted(db, owner, writeSeq);
if (baselineIncludesWrite) return;
if (b) await applyTripletContribution(db, owner, b, -1);
if (a) await applyTripletContribution(db, owner, a, 1);
} catch (e) {
logMapFailure("\u4E09\u5143\u7D44\u7570\u52D5", e);
}
}
async function noteEntriesChanged(db, changes) {
const deltas = /* @__PURE__ */ new Map();
const add = (owner, library, delta) => {
const k = `${ownerKey(owner)} ${library}`;
const cur = deltas.get(k) ?? { owner, library, delta: 0 };
cur.delta += delta;
deltas.set(k, cur);
};
for (const { before, after } of changes) {
const bl = countedLibraryOf(before);
const al = countedLibraryOf(after);
const bo = before?.owner_id || null;
const ao = after?.owner_id || null;
if (bl !== null && al !== null && bl === al && bo === ao) continue;
if (bl !== null) add(bo, bl, -1);
if (al !== null) add(ao, al, 1);
}
const pending = [...deltas.values()].filter((d) => d.delta !== 0);
if (!pending.length) return;
const writeSeq = nextWriteSeq();
try {
const byOwner = /* @__PURE__ */ new Map();
for (const d of pending) {
const k = ownerKey(d.owner);
const g = byOwner.get(k) ?? { owner: d.owner, items: [] };
g.items.push(d);
byOwner.set(k, g);
}
for (const { owner, items } of byOwner.values()) {
const { baselineIncludesWrite } = await ensureOwnerAdopted(db, owner, writeSeq);
if (baselineIncludesWrite) continue;
for (const d of items) {
const mapId = await ensureMapRecord(db, owner, d.library);
await bumpCounter(db, cellId(mapId, "entry_count"), d.delta);
}
}
} catch (e) {
logMapFailure("\u5361\u7247\u7570\u52D5", e);
}
}
function toInt(raw2) {
const n = Number(raw2 ?? 0);
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
}
function entitiesOf(raw2) {
return entitiesToPairs(raw2 ?? null).map(([name, degree]) => ({ name, degree }));
}
function relationsOf(raw2) {
return relationsToPairs(raw2 ?? null).map(([predicate, count]) => ({ predicate, count }));
}
async function readStoredRows(db, owner) {
const oh = owner === void 0 ? "" : await ownerHash(owner);
const lo = owner === void 0 ? MAP_PAGE_PREFIX : `${MAP_PAGE_PREFIX}${oh}:`;
const hi = owner === void 0 ? MAP_PAGE_UPPER_ALL : `${MAP_PAGE_PREFIX}${oh};`;
const res = await db.prepare("SELECT id, owner_id FROM entries WHERE page_name >= ? AND page_name < ? AND +entry_type = 'block'").bind(lo, hi).all();
const heads = (res.results ?? []).filter((r) => owner === void 0 || (r.owner_id ?? "") === owner);
if (!heads.length) return [];
const cells = await readCellsById(db, heads.flatMap((h) => LIBRARY_MAP_SLOTS.map((s) => cellId(h.id, s))));
const out = [];
for (const h of heads) {
const get = (slot) => cells.get(cellId(h.id, slot));
const library = get("library")?.content;
if (!library) continue;
let updated = 0;
for (const s of LIBRARY_MAP_SLOTS) updated = Math.max(updated, get(s)?.updated_at ?? 0);
out.push({
record_id: h.id,
owner_id: h.owner_id,
library,
narrative: get("narrative")?.content?.trim() || null,
entities: entitiesOf(get("top_entities")?.content),
relations: relationsOf(get("relation_profile")?.content),
triplet_count: toInt(get("triplet_count")?.content),
entry_count: toInt(get("entry_count")?.content),
commit_hash: get("commit_hash")?.content || null,
updated_at: updated
});
}
return out;
}
function legacyToStored(rows, ownerId) {
return [...rows].map((r) => ({
record_id: r.rid,
owner_id: ownerId,
library: r.library,
narrative: r.narrative?.trim() || null,
entities: entitiesOf(r.top_entities),
relations: relationsOf(r.relation_profile),
triplet_count: toInt(r.triplet_count),
entry_count: null,
commit_hash: r.commit_hash || null,
updated_at: r.ts ?? 0
}));
}
function mergeByLibrary(maps) {
const byLib = /* @__PURE__ */ new Map();
for (const m of maps) {
const cur = byLib.get(m.library);
if (!cur) {
byLib.set(m.library, { ...m, entities: [...m.entities], relations: [...m.relations] });
continue;
}
cur.triplet_count += m.triplet_count;
cur.entry_count = cur.entry_count === null || m.entry_count === null ? null : cur.entry_count + m.entry_count;
cur.narrative = cur.narrative || m.narrative;
cur.commit_hash = cur.commit_hash || m.commit_hash;
cur.updated_at = Math.max(cur.updated_at, m.updated_at);
const ents = cur.entities.map((e) => [e.name, e.degree]);
for (const e of m.entities) {
const i = ents.findIndex((p) => p[0] === e.name);
if (i >= 0) ents[i][1] += e.degree;
else ents.push([e.name, e.degree]);
}
sortPairs(ents);
cur.entities = ents.slice(0, ENTITY_SKETCH_SIZE).map(([name, degree]) => ({ name, degree }));
const rels = cur.relations.map((r) => [r.predicate, r.count]);
for (const r of m.relations) {
const i = rels.findIndex((p) => p[0] === r.predicate);
if (i >= 0) rels[i][1] += r.count;
else rels.push([r.predicate, r.count]);
}
sortPairs(rels);
cur.relations = rels.slice(0, PREDICATE_SKETCH_SIZE).map(([predicate, count]) => ({ predicate, count }));
}
return [...byLib.values()];
}
async function readStoredMaps(db, owner) {
const scope = owner || void 0;
if (scope !== void 0) {
if (await isOwnerAdopted(db, scope)) return readStoredRows(db, scope);
const legacy2 = await readLegacyMaps(db, scope);
return legacyToStored(legacy2.byLibrary.values(), scope);
}
const all = await readStoredRows(db, void 0);
if (all.length) return mergeByLibrary(all);
const legacy = await readLegacyMaps(db, void 0);
return legacyToStored(legacy.byLibrary.values(), null);
}
// kbdb/src/actions/entry-crud.ts
function uid2(prefix) {
return `${prefix}_${crypto.randomUUID()}`;
}
async function createEntry(db, input) {
const id = input.id ?? uid2("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");
await noteEntriesChanged(db, [{ before: null, after: row }]);
return row;
}
async function getEntry(db, id) {
const row = await db.prepare("SELECT * FROM entries WHERE id = ?").bind(id).first();
return row ?? null;
}
var NOT_MACHINERY_PREDICATE = "(src_id IS NULL AND entry_type NOT IN ('record', 'sheet', 'field', 'system'))";
function eqTerm(column, exactKeyPresent) {
return exactKeyPresent ? `+${column} = ?` : `${column} = ?`;
}
async function listEntries(db, f = {}) {
const conds = [];
const params = [];
const exact = Boolean(f.page_name || f.source);
if (f.entry_type) {
conds.push(eqTerm("entry_type", exact));
params.push(f.entry_type);
} else {
conds.push(NOT_MACHINERY_PREDICATE);
}
if (f.owner_id) {
conds.push(eqTerm("owner_id", exact));
params.push(f.owner_id);
}
if (f.parent_id) {
conds.push(eqTerm("parent_id", exact));
params.push(f.parent_id);
}
if (f.page_name) {
conds.push("page_name = ?");
params.push(f.page_name);
}
if (f.source) {
conds.push("json_extract(metadata_json, '$.source') = ?");
params.push(f.source);
}
if (f.library && f.library.length > 0) {
conds.push(libraryPredicate(f.library));
params.push(...f.library);
}
if (f.exclude_kind && f.exclude_kind.length > 0) {
const ph = f.exclude_kind.map(() => "?").join(",");
conds.push(`COALESCE(json_extract(metadata_json, '$.kind'), '') NOT IN (${ph})`);
params.push(...f.exclude_kind);
}
if (f.q) {
const m = buildContentLike(f.q);
conds.push(...m.conds);
params.push(...m.params);
}
const where = conds.length ? `WHERE ${conds.join(" AND ")}` : "";
const limit = Math.min(f.limit ?? 100, 1e3);
const offset = f.offset ?? 0;
const pageSizeKnown = Number.isFinite(limit) && limit > 0;
const rowsRes = await db.prepare(`SELECT * FROM entries ${where} ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?`).bind(...params, limit, offset).all();
const entries = rowsRes.results ?? [];
let total;
if (pageSizeKnown && entries.length > 0 && entries.length < limit) total = offset + entries.length;
else if (pageSizeKnown && entries.length === 0 && offset === 0) total = 0;
else {
const countRow = await db.prepare(`SELECT COUNT(*) as total FROM entries ${where}`).bind(...params).first();
total = countRow?.total ?? 0;
}
return { entries, total };
}
async function blocksOfPages(db, pages, perPageLimit = 8) {
if (pages.length === 0) return [];
const params = [];
const pairs = pages.map((p) => {
params.push(p.page_name);
if (p.owner_id === null) return "(page_name = ? AND owner_id IS NULL)";
params.push(p.owner_id);
return "(page_name = ? AND owner_id = ?)";
});
const rows = await db.prepare(
`SELECT * FROM entries
WHERE (${pairs.join(" OR ")})
AND ${NOT_MACHINERY_PREDICATE}
AND ${NOT_DEPRECATED_PREDICATE}
ORDER BY created_at ASC, rowid ASC
LIMIT ?`
).bind(...params, pages.length * perPageLimit).all();
return rows.results ?? [];
}
async function updateEntry(db, id, patch) {
const cols = [];
const params = [];
const map = patch;
for (const k of ["content", "parent_id", "page_name", "refs_json", "tags_json", "task_status", "confidence", "metadata_json"]) {
if (k in map && map[k] !== void 0) {
cols.push(`${k} = ?`);
params.push(map[k]);
}
}
if (cols.length === 0) return getEntry(db, id);
const before = map.metadata_json !== void 0 ? await getEntry(db, id) : null;
cols.push("updated_at = unixepoch()");
await db.prepare(`UPDATE entries SET ${cols.join(", ")} WHERE id = ?`).bind(...params, id).run();
const row = await getEntry(db, id);
if (before) await noteEntriesChanged(db, [{ before, after: row }]);
return row;
}
async function deleteEntry(db, id) {
const ref = await db.prepare("SELECT id FROM entries WHERE dst_id = ? LIMIT 1").bind(id).first();
if (ref) throw new Error(`entry ${id} is still referenced by record relation ${ref.id} \u2014 delete the record (or its slot) first`);
const before = await getEntry(db, id);
await db.prepare("DELETE FROM entries WHERE id = ?").bind(id).run();
await db.prepare("DELETE FROM entries WHERE src_id = ?").bind(id).run();
if (before) await noteEntriesChanged(db, [{ before, after: null }]);
}
async function upsertEntry(db, id, input) {
const existing = await getEntry(db, id);
if (!existing) return createEntry(db, { ...input, id });
await db.prepare(
`UPDATE entries
SET content = ?, entry_type = ?, owner_id = ?, parent_id = ?, page_name = ?,
refs_json = ?, tags_json = ?, task_status = ?, confidence = ?, metadata_json = ?,
updated_at = unixepoch()
WHERE id = ?`
).bind(
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,
id
).run();
const row = await getEntry(db, id);
if (!row) throw new Error("upsertEntry: update succeeded but row not found");
await noteEntriesChanged(db, [{ before: existing, after: row }]);
return row;
}
async function embeddedIdsByLibrary(db, ownerId, library) {
const rows = await db.prepare(
`SELECT id FROM entries
WHERE owner_id = ?
AND ${ENTRY_LIBRARY} = ?
AND is_embedded = 1`
).bind(ownerId, library).all();
return (rows.results ?? []).map((r) => r.id);
}
async function markUnembedded(db, ids) {
if (ids.length === 0) return;
const holes = ids.map(() => "?").join(",");
await db.prepare(`UPDATE entries SET is_embedded = 0 WHERE id IN (${holes})`).bind(...ids).run();
}
async function deprecateEntriesByLibrary(db, ownerId, library) {
const result = await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在 worktree ⇒ hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到,同 887463c/962d863/5919c6b 已記載的假警報,非繞牆
`UPDATE entries
SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.status', 'deprecated'),
updated_at = unixepoch()
WHERE owner_id = ?
AND ${ENTRY_LIBRARY} = ?
AND ${NOT_LIBRARY_MAP_ROW}
AND (json_extract(metadata_json, '$.status') IS NULL
OR json_extract(metadata_json, '$.status') != 'deprecated')`
).bind(ownerId, library).run();
return result.meta?.changes ?? 0;
}
var MAX_LIKE_Q_BYTES = 48;
var MAX_LIKE_TERMS = 6;
var utf8Len = (s) => new TextEncoder().encode(s).length;
var LIKE_ESCAPE = "\\";
var CONTENT_LIKE = `content LIKE ? ESCAPE '${LIKE_ESCAPE}'`;
function escapeLikeLiteral(s) {
return s.replace(/[\\%_]/g, (ch) => LIKE_ESCAPE + ch);
}
var likeBytes = (s) => utf8Len(escapeLikeLiteral(s));
var likePattern = (s) => `%${escapeLikeLiteral(s)}%`;
function chunkByBytes(s, maxBytes) {
const out = [];
let cur = "";
for (const ch of s) {
if (likeBytes(cur + ch) > maxBytes) {
if (cur) out.push(cur);
cur = ch;
} else {
cur += ch;
}
}
if (cur) out.push(cur);
return out;
}
function buildContentLike(q) {
if (likeBytes(q) <= MAX_LIKE_Q_BYTES) {
return { conds: [CONTENT_LIKE], params: [likePattern(q)], split: false };
}
const terms = [];
for (const word of q.split(/\s+/).filter(Boolean)) {
for (const piece of chunkByBytes(word, MAX_LIKE_Q_BYTES)) {
terms.push(piece);
if (terms.length >= MAX_LIKE_TERMS) break;
}
if (terms.length >= MAX_LIKE_TERMS) break;
}
if (terms.length === 0) terms.push(chunkByBytes(q, MAX_LIKE_Q_BYTES)[0] ?? "");
return {
conds: terms.map(() => CONTENT_LIKE),
params: terms.map(likePattern),
split: true
};
}
var MAX_SEARCH_TERMS = 6;
var MAX_TERM_WEIGHT = 8;
var KEYWORD_RELATIVE_CUT = 0.6;
var CJK_STOP_CHARS = new Set(
"\u7684\u4E86\u662F\u5728\u6211\u4F60\u4ED6\u5979\u5B83\u5011\u9019\u90A3\u54EA\u8AB0\u55CE\u5462\u5427\u554A\u5440\u561B\u5594\u54E6\u4EC0\u9EBC\u600E\u4E4B\u4E4E\u800C\u4F46\u4E26\u537B\u5C31\u90FD\u4E5F\u5F88\u592A\u53EA\u9084\u53C8\u518D\u6BCF\u4E9B\u628A\u88AB\u8DDF\u8B93\u82E5".split("")
);
var ASCII_STOP_WORDS = /* @__PURE__ */ new Set([
"the",
"a",
"an",
"and",
"or",
"of",
"to",
"in",
"on",
"at",
"is",
"are",
"was",
"were",
"be",
"do",
"does",
"did",
"for",
"it",
"its",
"this",
"that",
"these",
"those",
"with",
"what",
"how",
"why",
"when",
"where",
"who",
"which",
"can",
"could",
"should",
"would",
"my",
"our",
"your",
"their",
"me",
"we",
"you",
"they"
]);
var isCjkChar = (ch) => /[-ヿ㐀-䶿一-鿿豈-]/.test(ch);
var isWordChar = (ch) => /[A-Za-z0-9_.-]/.test(ch);
function splitRuns(q) {
const runs = [];
let cur = "";
let curCjk = false;
const flush = () => {
if (cur) runs.push({ text: cur, cjk: curCjk });
cur = "";
};
for (const ch of q) {
const cjk = isCjkChar(ch);
if (!cjk && !isWordChar(ch)) {
flush();
continue;
}
if (cur && cjk !== curCjk) flush();
cur += ch;
curCjk = cjk;
}
flush();
return runs;
}
function contentBigrams(run) {
const chars = [...run];
const out = [];
for (let i = 0; i + 1 < chars.length; i++) {
if (CJK_STOP_CHARS.has(chars[i]) || CJK_STOP_CHARS.has(chars[i + 1])) continue;
out.push(chars[i] + chars[i + 1]);
}
return out;
}
function tokenizeQuery(q) {
const found = /* @__PURE__ */ new Map();
const add = (t, w) => {
for (const piece of chunkByBytes(t, MAX_LIKE_Q_BYTES)) {
if (!piece) continue;
found.set(piece, Math.max(found.get(piece) ?? 0, Math.min(w, MAX_TERM_WEIGHT)));
}
};
const runs = splitRuns(q);
const isQuestion = runs.length > 1;
for (const run of runs) {
if (!run.cjk) {
const w = run.text.toLowerCase();
if (w.length >= 2 && !ASCII_STOP_WORDS.has(w)) add(run.text, run.text.length);
continue;
}
const chars = [...run.text];
if (chars.length >= 2 && chars.length <= 4) add(run.text, chars.length);
if (isQuestion || chars.length > 4) for (const bg of contentBigrams(run.text)) add(bg, 2);
}
return [...found.entries()].map(([term, weight]) => ({ term, weight })).sort((a, b) => b.weight - a.weight || a.term.localeCompare(b.term)).slice(0, MAX_SEARCH_TERMS);
}
function buildSearchScore(q) {
const trimmed = q.trim();
const terms = tokenizeQuery(trimmed);
if (terms.length === 0) {
const m = buildContentLike(trimmed);
return {
scoreExpr: m.conds.map(() => `CASE WHEN ${CONTENT_LIKE} THEN 1 ELSE 0 END`).join(" + "),
scoreParams: m.params,
terms: [],
legacyShape: true
};
}
const parts = [];
const params = [];
for (const { term, weight } of terms) {
parts.push(`CASE WHEN ${CONTENT_LIKE} THEN ${weight} ELSE 0 END`);
params.push(likePattern(term));
}
const single = terms.length === 1 && terms[0].term === trimmed;
if (!single && likeBytes(trimmed) <= MAX_LIKE_Q_BYTES) {
const bonus = terms.reduce((s, t) => s + t.weight, 0);
parts.push(`CASE WHEN ${CONTENT_LIKE} THEN ${bonus} ELSE 0 END`);
params.push(likePattern(trimmed));
}
return { scoreExpr: parts.join(" + "), scoreParams: params, terms, legacyShape: single };
}
function applyRelativeCut(rows) {
if (rows.length <= 1) return rows;
const cut = rows[0].match_score * KEYWORD_RELATIVE_CUT;
return rows.filter((r) => r.match_score >= cut);
}
function libraryPredicate(libraries) {
const placeholders = libraries.map(() => "?").join(",");
return `${ENTRY_LIBRARY} IN (${placeholders})`;
}
var NOT_DEPRECATED_PREDICATE = "(json_extract(metadata_json, '$.status') IS NULL OR json_extract(metadata_json, '$.status') != 'deprecated')";
function isDeprecatedEntry(entry) {
if (!entry.metadata_json) return false;
try {
const meta = JSON.parse(entry.metadata_json);
return !!meta && meta.status === "deprecated";
} catch {
return false;
}
}
async function searchEntries(db, q, owner_id, entry_type, limit = 50, library, source, includeDeprecated = false) {
const plan = buildSearchScore(q);
const conds = [];
const params = [...plan.scoreParams];
if (owner_id) {
conds.push("owner_id = ?");
params.push(owner_id);
}
if (entry_type) {
conds.push("entry_type = ?");
params.push(entry_type);
} else {
conds.push(NOT_MACHINERY_PREDICATE);
}
if (source) {
conds.push("json_extract(metadata_json, '$.source') = ?");
params.push(source);
}
if (library && library.length > 0) {
conds.push(libraryPredicate(library));
params.push(...library);
}
if (!includeDeprecated) {
conds.push(NOT_DEPRECATED_PREDICATE);
}
conds.push(NOT_LIBRARY_MAP_CELL);
const inner = conds.length > 0 ? `WHERE ${conds.join(" AND ")}` : "";
const res = await db.prepare(
`SELECT * FROM (
SELECT *, (${plan.scoreExpr}) AS match_score FROM entries ${inner}
) WHERE match_score > 0
ORDER BY match_score DESC, updated_at DESC
LIMIT ?`
).bind(...params, Math.min(limit, 200)).all();
return applyRelativeCut(res.results ?? []);
}
// kbdb/src/search-rank.ts
var MACHINE_SECTIONS = /* @__PURE__ */ new Set([
"\u51FA\u8655",
"\u5361\u7247\u95DC\u4FC2",
"\u5167\u6587\u77E5\u8B58\u95DC\u4FC2",
"\u95DC\u806F",
"\u4F86\u6E90",
"\u53CD\u5411\u9023\u7D50",
"source",
"sources",
"relations",
"related",
"links",
"references",
"backlinks"
]);
var ATX_HEADING = /^\s{0,3}#{1,6}\s+(.*)$/;
var LIST_MARKER = /^\s*(?:[-*+]|\d{1,3}[.)])\s+/;
var BREADCRUMB_ARROW = /^\s*(?:←|→|⇐|⇒)\s*/;
var TRIPLE_SEP = ">".repeat(2);
var WORD_CHAR = /[A-Za-z0-9-ヿ㐀-䶿一-鿿豈-]/;
function isMachineLine(raw2) {
const line = raw2.trim();
if (!line) return true;
if (ATX_HEADING.test(line)) return true;
if (line.includes(TRIPLE_SEP)) return true;
if (/^-{3,}$|^\*{3,}$|^={3,}$/.test(line)) return true;
let rest = line.replace(LIST_MARKER, "").replace(BREADCRUMB_ARROW, "");
rest = rest.replace(/\[\[[^\]]*\]\]/g, " ").replace(/!?\[[^\]]*\]\([^)]*\)/g, " ").replace(/`[^`]*`/g, " ").replace(/[*_~]/g, " ");
return !WORD_CHAR.test(rest);
}
function classifyBlock(content) {
const text = (content ?? "").trim();
if (!text) return 1 /* Structural */;
const lines = text.split("\n");
const head = lines.find((l) => ATX_HEADING.test(l.trim()));
if (head) {
const name = (head.trim().match(ATX_HEADING)?.[1] ?? "").trim().toLowerCase();
if (MACHINE_SECTIONS.has(name)) return 1 /* Structural */;
}
return lines.every(isMachineLine) ? 1 /* Structural */ : 0 /* Fact */;
}
function rankByTier(entries) {
return entries.map((e, i) => ({ e, i, tier: classifyBlock(e.content) })).sort((a, b) => a.tier - b.tier || b.e.score - a.e.score || a.i - b.i).map((x) => x.e);
}
function topPages(entries, limit) {
const best = /* @__PURE__ */ new Map();
for (const e of entries) {
const page = (e.page_name ?? "").trim();
if (!page) continue;
const key = `${e.owner_id ?? ""}\0${page}`;
const cur = best.get(key);
if (!cur || e.score > cur.score) {
best.set(key, { page_name: page, owner_id: e.owner_id, score: e.score, matched_block_id: e.id });
}
}
return [...best.values()].sort((a, b) => b.score - a.score).slice(0, limit);
}
// kbdb/src/actions/library-index.ts
var DEFAULT_CARD_LIMIT = 200;
var MAX_CARD_LIMIT = 500;
var GLOSS_HEADINGS = ["## \u6458\u8981", "## \u4E00\u53E5\u8A71\u5B9A\u7FA9"];
var INDEX_CARD_NAME = "00-INDEX";
function glossOf(block) {
if (!block) return null;
const lines = String(block).split("\n");
const body = lines.filter((l) => !GLOSS_HEADINGS.some((h) => l.trim().startsWith(h))).map((l) => l.trim()).filter(Boolean);
if (body.length === 0) return null;
const text = body.join(" ").replace(/\s+/g, " ").trim();
return text || null;
}
async function listLibraryCards(db, f) {
const library = f.library;
const owner = f.owner_id ?? "";
const limit = Math.min(Math.max(f.limit ?? DEFAULT_CARD_LIMIT, 1), MAX_CARD_LIMIT);
const glossLike = GLOSS_HEADINGS.map(() => "content LIKE ?").join(" OR ");
const glossParams = GLOSS_HEADINGS.map((h) => `${h}%`);
const where = `WHERE entry_type = 'block'
AND page_name IS NOT NULL AND page_name != ''
AND COALESCE(json_extract(metadata_json, '$.kind'), '') != 'library_map'
AND (? = '' OR owner_id = ?)
AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'
AND ${ENTRY_LIBRARY} = ?`;
const whereParams = [owner, owner, library];
const [rowsRes, countRow, indexRow] = await Promise.all([
db.prepare(
`SELECT
page_name,
COUNT(*) AS block_count,
MAX(CASE WHEN ${glossLike} THEN content END) AS gloss_block,
MAX(json_extract(metadata_json, '$.source_path')) AS source_path,
MAX(updated_at) AS updated_at
FROM entries
${where}
GROUP BY page_name
ORDER BY MAX(updated_at) DESC, page_name ASC
LIMIT ?`
).bind(...glossParams, ...whereParams, limit).all(),
db.prepare(`SELECT COUNT(DISTINCT page_name) AS total FROM entries ${where}`).bind(...whereParams).first(),
// 🔴 目錄卡在不在,**不能靠掃上面那一頁**——它照 updated_at 排序,
// 目錄卡很可能不在前 limit 名內 ⇒ 那樣會回報「沒有目錄」而其實有,
// 而這個欄位正是呼叫端用來決定「要不要退回導出清單」的開關。
// 它只有一張、名字固定,所以用卡名直接問一次(EXISTS,成本可忽略)。
db.prepare(`SELECT 1 AS hit FROM entries ${where} AND page_name = ? LIMIT 1`).bind(...whereParams, INDEX_CARD_NAME).first()
]);
const cards = (rowsRes.results ?? []).map((r) => ({
page_name: r.page_name,
block_count: Number(r.block_count) || 0,
gloss: glossOf(r.gloss_block),
source_path: r.source_path ?? null,
updated_at: Number(r.updated_at) || 0
}));
return {
library,
cards,
total: countRow?.total ?? cards.length,
has_index_card: !!indexRow
};
}
// kbdb/src/actions/maintenance-quota.ts
var DEFAULT_MAINTENANCE_DAILY_WRITE_LIMIT = 2e4;
function maintenanceDailyLimit(env) {
const raw2 = env.KBDB_MAINTENANCE_DAILY_WRITE_LIMIT;
const n = raw2 ? parseInt(raw2, 10) : NaN;
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAINTENANCE_DAILY_WRITE_LIMIT;
}
function utcDay() {
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
}
function maintenanceUsageId() {
return `kbdb-maintenance-usage:${utcDay()}`;
}
async function getMaintenanceUsageToday(db) {
const row = await db.prepare("SELECT metadata_json FROM entries WHERE id = ?").bind(maintenanceUsageId()).first();
if (!row) return 0;
try {
const parsed = row.metadata_json ? JSON.parse(row.metadata_json) : {};
return Number(parsed.writes) || 0;
} catch {
return 0;
}
}
async function addMaintenanceUsage(db, by) {
if (by <= 0) return;
const id = maintenanceUsageId();
const existing = await db.prepare("SELECT metadata_json FROM entries WHERE id = ?").bind(id).first();
let prev = 0;
if (existing) {
try {
const parsed = existing.metadata_json ? JSON.parse(existing.metadata_json) : {};
prev = Number(parsed.writes) || 0;
} catch {
prev = 0;
}
await db.prepare("UPDATE entries SET metadata_json = ?, updated_at = unixepoch() WHERE id = ?").bind(JSON.stringify({ day: utcDay(), writes: prev + by }), id).run();
} else {
await db.prepare(`INSERT INTO entries (id, entry_type, metadata_json) VALUES (?, 'kbdb_maintenance_usage', ?)`).bind(id, JSON.stringify({ day: utcDay(), writes: by })).run();
}
}
async function maintenanceBudgetToday(env, db) {
const limit = maintenanceDailyLimit(env);
let used = 0;
try {
used = await getMaintenanceUsageToday(db);
} catch {
used = 0;
}
return { limit, used, remaining: Math.max(0, limit - used) };
}
// kbdb/src/embed.ts
var DEFAULT_EMBED_MODEL = "@cf/baai/bge-m3";
var MIN_SCORE_ABS_FLOOR = 0.45;
var MIN_SCORE_TOP_RATIO = 0.8;
function relativeMinScore(topScore) {
return Math.max(MIN_SCORE_ABS_FLOOR, topScore * MIN_SCORE_TOP_RATIO);
}
function embedModel(env) {
const m = (env.EMBED_MODEL ?? "").trim();
return m || DEFAULT_EMBED_MODEL;
}
function embedEnabled(env) {
return !!(env.VECTORIZE && env.AI);
}
async function embedText(env, text) {
const t = (text ?? "").trim();
if (!t || !env.AI) return null;
const res = await env.AI.run(embedModel(env), { text: [t] });
return res?.data?.[0] ?? null;
}
async function embedOnWrite(env, entry) {
if (!embedEnabled(env)) return false;
if (!isEmbeddable(entry)) return false;
const vec = await embedText(env, entry.content ?? "");
if (!vec) return false;
await env.VECTORIZE.upsert([
{
id: entry.id,
values: vec,
// metadata 走 indexed 範圍:owner_id(租戶隔離)、entry_type、source(#5.1 過濾與語義共用)、
// library(portal-auth P1「庫」filter)。library 在寫入端正規化:未標記='general'(design §3.2
// 「未蓋章的舊資料視同 general」——D1 側用查詢端 COALESCE fallback,Vectorize filter 做不了
// COALESCE,故在 upsert 時蓋 'general',查詢端單純 $in 即可)。
metadata: {
owner_id: entry.owner_id ?? "",
entry_type: entry.entry_type,
source: readSource(entry) ?? "",
library: readLibrary(entry) ?? "general"
}
}
]);
await env.DB.prepare("UPDATE entries SET is_embedded = 1, content_hash = ? WHERE id = ?").bind(embedModel(env), entry.id).run();
return true;
}
function isEmbeddable(entry) {
const meta = parseMeta2(entry.metadata_json);
return meta?.embed === true;
}
function readSource(entry) {
const meta = parseMeta2(entry.metadata_json);
const s = meta?.source;
return typeof s === "string" ? s : null;
}
function readLibrary(entry) {
const meta = parseMeta2(entry.metadata_json);
const l = meta?.library;
return typeof l === "string" && l.trim() !== "" ? l : null;
}
function parseMeta2(json) {
if (!json) return null;
try {
const p = JSON.parse(json);
return p && typeof p === "object" ? p : null;
} catch {
return null;
}
}
var BACKFILL_PREDICATE = "is_embedded = 0 AND content IS NOT NULL AND content <> '' AND json_extract(metadata_json, '$.embed') = 1";
var DEFAULT_BACKFILL_DAILY_LIMIT = 1800;
function backfillDailyLimit(env) {
const raw2 = env.EMBED_BACKFILL_DAILY_LIMIT;
const n = raw2 ? parseInt(raw2, 10) : NaN;
return Number.isFinite(n) && n > 0 ? n : DEFAULT_BACKFILL_DAILY_LIMIT;
}
function utcDay2() {
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
}
function backfillUsageId() {
return `embed-backfill-usage:${utcDay2()}`;
}
async function getBackfillUsageToday(db) {
const row = await db.prepare("SELECT metadata_json FROM entries WHERE id = ?").bind(backfillUsageId()).first();
if (!row) return 0;
try {
const parsed = row.metadata_json ? JSON.parse(row.metadata_json) : {};
return Number(parsed.embedded) || 0;
} catch {
return 0;
}
}
async function addBackfillUsage(db, by) {
if (by <= 0) return;
const id = backfillUsageId();
const existing = await db.prepare("SELECT metadata_json FROM entries WHERE id = ?").bind(id).first();
let prev = 0;
if (existing) {
try {
const parsed = existing.metadata_json ? JSON.parse(existing.metadata_json) : {};
prev = Number(parsed.embedded) || 0;
} catch {
prev = 0;
}
await db.prepare("UPDATE entries SET metadata_json = ?, updated_at = unixepoch() WHERE id = ?").bind(JSON.stringify({ day: utcDay2(), embedded: prev + by }), id).run();
} else {
await db.prepare(`INSERT INTO entries (id, entry_type, metadata_json) VALUES (?, 'embed_backfill_usage', ?)`).bind(id, JSON.stringify({ day: utcDay2(), embedded: by })).run();
}
}
function selectionCriteriaPredicate(opts) {
const conds = [];
const params = [];
if (opts.owner_id) {
conds.push("owner_id = ?");
params.push(opts.owner_id);
}
if (opts.source) {
conds.push("json_extract(metadata_json, '$.source') = ?");
params.push(opts.source);
}
if (opts.library) {
conds.push(`${ENTRY_LIBRARY} = ?`);
params.push(opts.library);
}
if (typeof opts.since === "number") {
conds.push("created_at >= ?");
params.push(opts.since);
}
if (typeof opts.until === "number") {
conds.push("created_at < ?");
params.push(opts.until);
}
return { conds, params };
}
async function backfillEmbeddings(env, opts = {}) {
if (!embedEnabled(env)) {
return {
enabled: false,
processed: 0,
skipped: 0,
remaining: 0,
scanned: 0,
quota_limit: 0,
quota_used_today: 0,
quota_exceeded: false
};
}
const limit = Math.min(Math.max(opts.limit ?? 25, 1), 100);
const offset = Math.max(opts.offset ?? 0, 0);
const basePredicate = opts.reindex ? "content IS NOT NULL AND content <> '' AND json_extract(metadata_json, '$.embed') = 1" : BACKFILL_PREDICATE;
const sel = selectionCriteriaPredicate(opts);
const conds = [basePredicate, "COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'", ...sel.conds];
const params = [...sel.params];
const where = conds.join(" AND ");
const res = await env.DB.prepare(`SELECT * FROM entries WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).bind(...params, limit, offset).all();
const rows = res.results ?? [];
const scanned = rows.length;
const dailyCap = backfillDailyLimit(env);
let usedToday = 0;
try {
usedToday = await getBackfillUsageToday(env.DB);
} catch {
usedToday = 0;
}
const remainingQuota = Math.max(0, dailyCap - usedToday);
let processed = 0;
const candidates = rows.filter((e) => (e.content ?? "").trim().length > 0);
const embeddable = candidates.slice(0, remainingQuota);
const quotaExceeded = candidates.length > embeddable.length;
if (embeddable.length > 0 && env.AI && env.VECTORIZE) {
const texts = embeddable.map((e) => (e.content ?? "").trim());
const out = await env.AI.run(embedModel(env), { text: texts });
const data = out?.data ?? [];
const vectors = embeddable.map((e, i) => ({ e, vec: data[i] })).filter((x) => Array.isArray(x.vec) && x.vec.length > 0).map((x) => ({
id: x.e.id,
values: x.vec,
metadata: {
owner_id: x.e.owner_id ?? "",
entry_type: x.e.entry_type,
source: readSource(x.e) ?? "",
library: readLibrary(x.e) ?? "general"
// 同 embedOnWrite:寫入端正規化(P1)
}
}));
if (vectors.length > 0) {
await env.VECTORIZE.upsert(vectors);
const ids = vectors.map((v) => v.id);
const placeholders = ids.map(() => "?").join(",");
await env.DB.prepare(`UPDATE entries SET is_embedded = 1, content_hash = ? WHERE id IN (${placeholders})`).bind(embedModel(env), ...ids).run();
processed = vectors.length;
try {
await addBackfillUsage(env.DB, processed);
} catch {
}
}
}
const remRow = await env.DB.prepare(`SELECT COUNT(*) as c FROM entries WHERE ${where}`).bind(...params).first();
const totalMatching = remRow?.c ?? 0;
const remaining = opts.reindex ? Math.max(0, totalMatching - (offset + scanned)) : totalMatching;
return {
enabled: true,
processed,
skipped: scanned - processed,
remaining,
scanned,
quota_limit: dailyCap,
quota_used_today: usedToday + processed,
quota_exceeded: quotaExceeded
};
}
async function backfillStatus(env, opts = {}) {
const conds = [];
const params = [];
if (opts.owner_id) {
conds.push("owner_id = ?");
params.push(opts.owner_id);
}
if (opts.source) {
conds.push("json_extract(metadata_json, '$.source') = ?");
params.push(opts.source);
}
const extra = conds.length ? ` AND ${conds.join(" AND ")}` : "";
const pendingRow = await env.DB.prepare(`SELECT COUNT(*) as c FROM entries WHERE ${BACKFILL_PREDICATE}${extra}`).bind(...params).first();
const embeddedRow = await env.DB.prepare(`SELECT COUNT(*) as c FROM entries WHERE is_embedded = 1 AND json_extract(metadata_json, '$.embed') = 1${extra}`).bind(...params).first();
return { enabled: embedEnabled(env), pending: pendingRow?.c ?? 0, embedded: embeddedRow?.c ?? 0 };
}
async function reconcileEmbedGeneration(env, opts = {}) {
if (!embedEnabled(env)) {
return {
enabled: false,
checked: 0,
confirmed_current: 0,
reset_to_pending: 0,
remaining: 0,
scanned: 0,
quota_limit: 0,
quota_used_today: 0,
quota_exceeded: false
};
}
const limit = Math.min(Math.max(opts.limit ?? 50, 1), 200);
const currentModel = embedModel(env);
const sel = selectionCriteriaPredicate(opts);
const conds = [
"is_embedded = 1",
"(content_hash IS NULL OR content_hash != ?)",
"COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'",
...sel.conds
];
const params = [currentModel, ...sel.params];
const where = conds.join(" AND ");
const res = await env.DB.prepare(`SELECT id FROM entries WHERE ${where} ORDER BY created_at DESC LIMIT ?`).bind(...params, limit).all();
const scannedIds = (res.results ?? []).map((r) => r.id);
const scanned = scannedIds.length;
const budget = await maintenanceBudgetToday(env, env.DB);
const ids = scannedIds.slice(0, budget.remaining);
const quotaExceeded = scanned > ids.length;
const checked = ids.length;
let confirmed_current = 0;
let reset_to_pending = 0;
if (ids.length > 0 && env.VECTORIZE) {
const found = await env.VECTORIZE.getByIds(ids);
const foundIds = new Set(found.map((v) => v.id));
const presentIds = ids.filter((id) => foundIds.has(id));
const missingIds = ids.filter((id) => !foundIds.has(id));
if (presentIds.length > 0) {
const ph = presentIds.map(() => "?").join(",");
await env.DB.prepare(`UPDATE entries SET content_hash = ? WHERE id IN (${ph})`).bind(currentModel, ...presentIds).run();
confirmed_current = presentIds.length;
}
if (missingIds.length > 0) {
const ph = missingIds.map(() => "?").join(",");
await env.DB.prepare(`UPDATE entries SET is_embedded = 0, content_hash = NULL WHERE id IN (${ph})`).bind(...missingIds).run();
reset_to_pending = missingIds.length;
}
}
const remRow = await env.DB.prepare(`SELECT COUNT(*) as c FROM entries WHERE ${where}`).bind(...params).first();
const written = confirmed_current + reset_to_pending;
try {
await addMaintenanceUsage(env.DB, written);
} catch {
}
return {
enabled: true,
checked,
confirmed_current,
reset_to_pending,
remaining: remRow?.c ?? 0,
scanned,
quota_limit: budget.limit,
quota_used_today: budget.used + written,
quota_exceeded: quotaExceeded
};
}
async function embedSelfTest(env, opts = {}) {
if (!embedEnabled(env)) {
return { enabled: false, tested: false, passed: null, note: "embed \u6A21\u7D44\u672A\u958B\uFF08\u7F3A Vectorize/AI binding\uFF09\uFF0C\u8A9E\u7FA9\u641C\u5C0B\u9019\u689D\u8DEF\u76EE\u524D\u4E0D\u5B58\u5728" };
}
const conds = ["is_embedded = 1", "content IS NOT NULL AND content <> ''"];
const params = [];
if (opts.owner_id) {
conds.push("owner_id = ?");
params.push(opts.owner_id);
}
const where = conds.join(" AND ");
const row = await env.DB.prepare(`SELECT * FROM entries WHERE ${where} ORDER BY updated_at DESC LIMIT 1`).bind(...params).first();
if (!row) {
return { enabled: true, tested: false, passed: null, note: "\u5C1A\u7121\u4EFB\u4F55\u5361\u7247\u88AB\u6A19\u8A18\u70BA\u300C\u5DF2\u5D4C\u5165\u300D\uFF0C\u7121\u6CD5\u81EA\u6211\u6AA2\u67E5\uFF08\u53EF\u80FD\u662F\u9084\u6C92\u5361\u7247\uFF0C\u4E5F\u53EF\u80FD\u662F\u5D4C\u5165\u5F9E\u672A\u6210\u529F\u904E\uFF09" };
}
const sample = (row.content ?? "").trim().slice(0, 200);
if (!sample) {
return { enabled: true, tested: false, passed: null, note: "\u53D6\u6A23\u5361\u7247\u5167\u5BB9\u70BA\u7A7A\uFF0C\u8DF3\u904E\u81EA\u6211\u6AA2\u67E5" };
}
let hits;
try {
hits = await semanticSearch(env, sample, { owner_id: opts.owner_id, topK: 10, min_score: 0 });
} catch (e) {
if (e instanceof EmbedQueryFailedError) {
return { enabled: true, tested: false, passed: null, note: `\u81EA\u6211\u6AA2\u67E5\u6C92\u8DD1\u6210\uFF1A${e.message}\uFF08\u8A9E\u7FA9\u641C\u5C0B\u6B64\u523B\u540C\u6A23\u6703\u6545\u969C\uFF0C\u591A\u534A\u662F Workers AI \u984D\u5EA6\u6216\u670D\u52D9\u554F\u984C\uFF09` };
}
throw e;
}
if (hits === null) {
return { enabled: false, tested: false, passed: null, note: "embed \u6A21\u7D44\u56DE\u5831\u672A\u958B\uFF08binding \u6AA2\u67E5\u671F\u9593\u6D88\u5931\uFF0C\u7F55\u898B\uFF09" };
}
const passed = hits.some((h) => h.id === row.id);
return {
enabled: true,
tested: true,
passed,
note: passed ? "\u62FF\u4E00\u5F35\u5DF2\u6A19\u8A18\u300C\u5DF2\u5D4C\u5165\u300D\u7684\u5361\u7247\u81EA\u6211\u67E5\u8A62\uFF0C\u80FD\u641C\u5230\u81EA\u5DF1\u2014\u2014\u8A9E\u7FA9\u641C\u5C0B\u9019\u689D\u8DEF\u662F\u901A\u7684" : "\u62FF\u4E00\u5F35\u5DF2\u6A19\u8A18\u300C\u5DF2\u5D4C\u5165\u300D\u7684\u5361\u7247\u81EA\u6211\u67E5\u8A62\uFF0C\u537B\u641C\u4E0D\u5230\u81EA\u5DF1\u2014\u2014\u50CF\u662F index \u6C92\u6536\u9304\u5230\u9019\u6279\u5411\u91CF\uFF08\u9700\u8981\u91CD\u65B0 reindex\uFF09"
};
}
var EmbedQueryFailedError = class extends Error {
constructor(detail) {
super(`\u67E5\u8A62\u5411\u91CF\u5316\u5931\u6557\uFF1A${detail}`);
this.name = "EmbedQueryFailedError";
}
};
async function semanticSearch(env, q, opts = {}) {
if (!embedEnabled(env)) return null;
if (!(q ?? "").trim()) return [];
let vec;
try {
vec = await embedText(env, q);
} catch (e) {
throw new EmbedQueryFailedError(e instanceof Error ? e.message : String(e));
}
if (!vec) throw new EmbedQueryFailedError("Workers AI \u6C92\u6709\u56DE\u51FA\u5411\u91CF\uFF08\u56DE\u61C9\u5F62\u72C0\u7570\u5E38\u6216\u7A7A\u56DE\u61C9\uFF09");
const filter = {};
if (opts.owner_id) filter.owner_id = opts.owner_id;
if (opts.source) filter.source = opts.source;
if (opts.entry_type) filter.entry_type = opts.entry_type;
if (opts.library && opts.library.length > 0) filter.library = { $in: opts.library };
const res = await env.VECTORIZE.query(vec, {
topK: Math.min(opts.topK ?? 20, 100),
returnMetadata: "indexed",
...Object.keys(filter).length ? { filter } : {}
});
const minScore = opts.min_score ?? MIN_SCORE_ABS_FLOOR;
return (res.matches ?? []).filter((m) => m.score >= minScore).map((m) => ({
id: m.id,
score: m.score,
owner_id: m.metadata?.owner_id,
entry_type: m.metadata?.entry_type,
source: m.metadata?.source,
library: m.metadata?.library
}));
}
// kbdb/src/actions/credential-legacy-migration.ts
async function legacyCredentialsTableExists(db) {
const row = await db.prepare(`SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = 'credentials'`).first();
return row !== null;
}
async function migrateLegacyCredentialsForOwner(db, ownerId) {
if (!ownerId) return 0;
if (!await legacyCredentialsTableExists(db)) return 0;
const before = await db.prepare(`SELECT COUNT(*) AS n FROM entries WHERE entry_type = 'credential' AND owner_id = ?1`).bind(ownerId).first();
await db.prepare(
`INSERT INTO entries (id, entry_type, owner_id, page_name, metadata_json, created_at, updated_at)
SELECT
'e_cred_' || lower(hex(randomblob(8))),
'credential',
c.api_key,
c.name,
json_object('service', c.service, 'sensitivity', c.sensitivity, 'secret_ref', c.secret_ref, 'last_used_at', c.last_used_at),
c.created_at,
unixepoch()
FROM credentials c
WHERE c.api_key = ?1
AND NOT EXISTS (
SELECT 1 FROM entries e
WHERE e.entry_type = 'credential' AND e.owner_id = c.api_key AND e.page_name = c.name
)`
).bind(ownerId).run();
const after = await db.prepare(`SELECT COUNT(*) AS n FROM entries WHERE entry_type = 'credential' AND owner_id = ?1`).bind(ownerId).first();
return (after?.n ?? 0) - (before?.n ?? 0);
}
// kbdb/src/actions/library-backfill.ts
async function rowsForLibraryMap(db, ids) {
const out = /* @__PURE__ */ new Map();
for (let i = 0; i < ids.length; i += 90) {
const chunk = ids.slice(i, i + 90);
const res = await db.prepare(`SELECT id, owner_id, entry_type, src_id, metadata_json FROM entries WHERE id IN (${chunk.map(() => "?").join(",")})`).bind(...chunk).all();
for (const r of res.results ?? []) out.set(r.id, r);
}
return out;
}
var MAX_PAGE_NAMES = 300;
var HARD_LIMIT_CAP = 500;
function criteriaPredicate(c) {
const conds = [ENTRY_UNLABELLED, NOT_LIBRARY_MAP_ROW];
const params = [];
if (c.owner_id) {
conds.push("owner_id = ?");
params.push(c.owner_id);
}
if (c.entry_type) {
conds.push("entry_type = ?");
params.push(c.entry_type);
}
if (c.page_names && c.page_names.length > 0) {
const names = c.page_names.slice(0, MAX_PAGE_NAMES);
conds.push(`page_name IN (${names.map(() => "?").join(",")})`);
params.push(...names);
}
if (c.source_prefix) {
conds.push("json_extract(metadata_json, '$.source') LIKE ? || '%'");
params.push(c.source_prefix);
}
if (c.page_name_prefix) {
conds.push("page_name LIKE ? || '%'");
params.push(c.page_name_prefix);
}
if (typeof c.since === "number") {
conds.push("created_at >= ?");
params.push(c.since);
}
if (typeof c.until === "number") {
conds.push("created_at < ?");
params.push(c.until);
}
return { conds, params };
}
async function backfillEntryLibraryTags(db, env, opts) {
const library = (opts.library ?? "").trim();
if (!library) throw new Error("library required");
const ownerId = (opts.owner_id ?? "").trim();
if (!ownerId) throw new Error("owner_id required\uFF08\u6A19\u5EAB\u662F\u8DE8\u5927\u91CF\u65E2\u6709\u8CC7\u6599\u7684\u6279\u6B21\u5BEB\u5165\uFF0C\u4E0D\u51C6\u7121\u79DF\u6236\u7BC4\u570D\u5730\u6383\u5168\u5EAB\u2014\u20142026-08-11 leo \u76F4\u4EE4\uFF09");
const limit = Math.min(Math.max(opts.limit ?? 100, 1), HARD_LIMIT_CAP);
const sel = criteriaPredicate({ ...opts, owner_id: ownerId });
const where = sel.conds.join(" AND ");
const params = sel.params;
const res = await db.prepare(`SELECT id FROM entries WHERE ${where} ORDER BY created_at ASC LIMIT ?`).bind(...params, limit).all();
const scannedIds = (res.results ?? []).map((r) => r.id);
const scanned = scannedIds.length;
const budget = await maintenanceBudgetToday(env, db);
const ids = scannedIds.slice(0, budget.remaining);
const quotaExceeded = scanned > ids.length;
let tagged = 0;
if (ids.length > 0) {
const ph = ids.map(() => "?").join(",");
const beforeRows = await rowsForLibraryMap(db, ids);
await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/),既有語句未改,worktree 路徑假警報
`UPDATE entries SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.library', ?), updated_at = unixepoch() WHERE id IN (${ph})`
).bind(library, ...ids).run();
tagged = ids.length;
const afterRows = await rowsForLibraryMap(db, ids);
await noteEntriesChanged(db, ids.map((id) => ({ before: beforeRows.get(id) ?? null, after: afterRows.get(id) ?? null })));
}
try {
await addMaintenanceUsage(db, tagged);
} catch {
}
const remRow = await db.prepare(`SELECT COUNT(*) as c FROM entries WHERE ${where}`).bind(...params).first();
return {
library,
scanned,
tagged,
remaining: remRow?.c ?? 0,
quota_limit: budget.limit,
quota_used_today: budget.used + tagged,
quota_exceeded: quotaExceeded
};
}
async function libraryBackfillStatus(db, opts = {}) {
const sel = criteriaPredicate(opts);
const where = sel.conds.join(" AND ");
const row = await db.prepare(`SELECT COUNT(*) as c FROM entries WHERE ${where}`).bind(...sel.params).first();
return { pending: row?.c ?? 0 };
}
// kbdb/src/routes/entries.ts
var entryRoutes = new Hono2();
var PAGE_EXPANSION_MAX_PAGES = 5;
var PAGE_EXPANSION_PER_PAGE = 4;
function fireAndForget(c, p) {
let ctx;
try {
ctx = c.executionCtx;
} catch {
ctx = void 0;
}
if (ctx) ctx.waitUntil(p.catch(() => {
}));
else void p.catch(() => {
});
}
var parseLibraryParam = parseLibraryList;
entryRoutes.post("/", async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.entry_type) return c.json({ success: false, error: "entry_type required" }, 400);
const entry = await createEntry(c.env.DB, body);
if (embedEnabled(c.env)) c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {
}));
return c.json({ success: true, entry });
});
entryRoutes.get("/libraries", async (c) => {
const owner = c.req.query("owner_id") || "";
const rows = await c.env.DB.prepare(
`SELECT DISTINCT ${ENTRY_LIBRARY} AS library
FROM entries
WHERE (?1 = '' OR owner_id = ?1)
AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'
ORDER BY library`
).bind(owner).all();
const libraries = (rows.results ?? []).map((r) => r.library).filter(Boolean);
return c.json({ success: true, libraries, count: libraries.length });
});
entryRoutes.get("/library-cards", async (c) => {
const library = c.req.query("library") || "";
if (!library.trim()) return c.json({ success: false, error: "library required" }, 400);
const limitRaw = Number(c.req.query("limit"));
const result = await listLibraryCards(c.env.DB, {
library,
owner_id: c.req.query("owner_id") || void 0,
// 壞值當沒帶(回預設),不 clamp 成 1——clamp 會回一筆長得像「這個庫只有一張卡」
// 的結果(#100 教訓:把「讀不到」演成「你沒有」)。
limit: Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : void 0
});
return c.json({ success: true, ...result, count: result.cards.length });
});
entryRoutes.get("/library-stats", async (c) => {
const owner = c.req.query("owner_id") || "";
const rows = await c.env.DB.prepare(
`SELECT
${ENTRY_LIBRARY} AS library,
COUNT(DISTINCT page_name) AS card_count
FROM entries
WHERE (?1 = '' OR owner_id = ?1)
AND entry_type = 'block'
AND page_name IS NOT NULL
AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'
GROUP BY library
ORDER BY library`
).bind(owner).all();
const stats = (rows.results ?? []).map((r) => ({ library: r.library, card_count: r.card_count }));
return c.json({ success: true, stats });
});
entryRoutes.get("/", async (c) => {
const entryType = c.req.query("entry_type") || void 0;
const ownerId = c.req.query("owner_id") || void 0;
if (entryType === "credential" && ownerId) {
await migrateLegacyCredentialsForOwner(c.env.DB, ownerId).catch(() => {
});
}
const { entries, total } = await listEntries(c.env.DB, {
entry_type: entryType,
owner_id: ownerId,
parent_id: c.req.query("parent_id") || void 0,
page_name: c.req.query("page_name") || void 0,
source: c.req.query("source") || void 0,
library: parseLibraryParam(c.req.query("library")),
q: c.req.query("q") || c.req.query("search") || void 0,
// e.g. 只要使用者的原文、不要地圖自己產的摘要: ?exclude_kind=library_map(Arcrun#44)
// 與軟刪(metadata.status=deprecated)互補、不重疊:軟刪只標「非現役」的舊地圖,
// 而且 listEntries 這條路**根本沒有 deprecated 謂詞**(只有 searchEntries 有)
// ⇒ 對本端點軟刪救不到。這個 filter 連現役那顆一起排除——對「找原文」的呼叫端來說,
// 地圖摘要不管新舊都不是使用者寫的內容。
exclude_kind: parseLibraryParam(c.req.query("exclude_kind")),
limit: c.req.query("limit") ? Number(c.req.query("limit")) : void 0,
offset: c.req.query("offset") ? Number(c.req.query("offset")) : void 0
});
return c.json({ success: true, entries, count: entries.length, total });
});
entryRoutes.get("/search", async (c) => {
const q = c.req.query("q");
if (!q) return c.json({ success: false, error: "q required" }, 400);
const owner_id = c.req.query("owner_id") || void 0;
const source = c.req.query("source") || void 0;
const entry_type = c.req.query("entry_type") || void 0;
const library = parseLibraryParam(c.req.query("library"));
const mode = c.req.query("mode") === "semantic" ? "semantic" : "keyword";
const include_deprecated = c.req.query("include_deprecated") === "true";
const topKNum = Number(c.req.query("top_k"));
const top_k = Number.isFinite(topKNum) && topKNum > 0 ? Math.floor(topKNum) : void 0;
const minScoreNum = Number(c.req.query("min_score"));
const min_score = Number.isFinite(minScoreNum) && minScoreNum > 0 ? minScoreNum : void 0;
if (mode === "semantic") {
const requestedTopK = top_k ?? 20;
const fetchTopK = include_deprecated ? requestedTopK : Math.min(requestedTopK * 3, 100);
let hits;
try {
hits = await semanticSearch(c.env, q, {
owner_id,
source,
entry_type,
library,
topK: fetchTopK,
min_score
});
} catch (e) {
if (e instanceof EmbedQueryFailedError) {
const entries3 = await searchEntries(c.env.DB, q, owner_id, entry_type, void 0, library, source, include_deprecated);
return c.json({
success: true,
entries: entries3,
count: entries3.length,
mode: "keyword",
requested_mode: "semantic",
degraded_reason: "embed_query_failed",
capability_hint: "\u8A9E\u610F\u641C\u5C0B\u66AB\u6642\u6545\u969C\uFF0C\u5148\u7528\u95DC\u9375\u5B57\u5E6B\u4F60\u627E\u4E86\u4E0B\u9762\u7684\u7D50\u679C\u3002\u9019\u662F\u6211\u5011\u7CFB\u7D71\u7684\u554F\u984C\uFF0C\u4E0D\u662F\u4F60\u7684\u64CD\u4F5C\u554F\u984C\uFF0C\u4F60\u4E0D\u9700\u8981\u505A\u4EFB\u4F55\u4E8B\uFF0C\u7A0D\u5F8C\u5B83\u6703\u81EA\u52D5\u6062\u5FA9\u3002",
admin_hint: `${e.message}\u3002\u5E38\u898B\u539F\u56E0\uFF1AWorkers AI \u7576\u65E5\u984D\u5EA6\u7528\u5B8C\u6216\u670D\u52D9\u66AB\u6642\u7570\u5E38\uFF1B\u672C\u6B21\u5DF2\u964D\u7D1A\u95DC\u9375\u5B57\u641C\u5C0B\uFF0C\u8CC7\u6599\u8207\u7D22\u5F15\u7686\u672A\u53D7\u5F71\u97FF\u3002`
});
}
throw e;
}
if (hits === null) {
const entries3 = await searchEntries(c.env.DB, q, owner_id, entry_type, void 0, library, source, include_deprecated);
return c.json({
success: true,
entries: entries3,
count: entries3.length,
mode: "keyword",
requested_mode: "semantic",
degraded_reason: "module_off",
capability_hint: "\u8A9E\u610F\u641C\u5C0B\u76EE\u524D\u6545\u969C\uFF0C\u5148\u7528\u95DC\u9375\u5B57\u5E6B\u4F60\u627E\u4E86\u4E0B\u9762\u7684\u7D50\u679C\u3002\u9019\u662F\u6211\u5011\u7CFB\u7D71\u7684\u554F\u984C\uFF0C\u4E0D\u662F\u4F60\u7684\u64CD\u4F5C\u554F\u984C\uFF0C\u4F60\u4E0D\u9700\u8981\u505A\u4EFB\u4F55\u4E8B\uFF0C\u6211\u5011\u6703\u4FEE\u597D\u5B83\u3002",
admin_hint: "\u6545\u969C\uFF1Akbdb worker \u7F3A VECTORIZE/AI binding\uFF08embedEnabled=false\uFF09\u3002\u8A9E\u610F\u641C\u5C0B\u662F\u5B89\u88DD\u5373\u63D0\u4F9B\u7684\u529F\u80FD\uFF0C\u7F3A binding\uFF1D\u90E8\u7F72\u5C64\u4E8B\u6545\uFF08\u5E38\u898B\uFF1Aredeploy \u6C92\u5E36 kbdb_embed \u6CE8\u5165\u3001\u6216\u5B89\u88DD\u6642 Vectorize index \u5EFA\u7ACB\u5931\u6557\u88AB\u653E\u884C\uFF09\u3002\u4FEE\u6CD5\uFF1A\u78BA\u8A8D Vectorize index \u5B58\u5728\u5F8C\u4EE5 kbdb_embed:true \u91CD\u90E8 kbdb\u3002\u672C\u6B21\u5DF2\u964D\u7D1A\u95DC\u9375\u5B57\u641C\u5C0B\u3002"
});
}
const orphanIds = [];
const deprecatedIds = [];
let entries2 = (await Promise.all(
hits.map(async (h) => {
const e = await getEntry(c.env.DB, h.id);
if (!e) {
orphanIds.push(h.id);
return null;
}
return { ...e, score: h.score };
})
)).filter((e) => e !== null);
if (!include_deprecated) {
entries2 = entries2.filter((e) => {
const dep = isDeprecatedEntry(e);
if (dep) deprecatedIds.push(e.id);
return !dep;
});
}
const staleIds = [...orphanIds, ...deprecatedIds];
if (staleIds.length > 0 && c.env.VECTORIZE) {
fireAndForget(c, (async () => {
await c.env.VECTORIZE.deleteByIds(staleIds);
await markUnembedded(c.env.DB, deprecatedIds);
})());
}
if (min_score === void 0 && entries2.length > 1) {
const cut = relativeMinScore(entries2[0].score);
entries2 = entries2.filter((e) => e.score >= cut);
}
if (!include_deprecated && entries2.length > 0) {
const pages = topPages(entries2, PAGE_EXPANSION_MAX_PAGES);
if (pages.length > 0) {
const seen = new Set(entries2.map((e) => e.id));
const scoreOf = new Map(pages.map((p) => [`${p.owner_id ?? ""} ${p.page_name}`, p]));
const added = /* @__PURE__ */ new Map();
for (const b of await blocksOfPages(c.env.DB, pages, PAGE_EXPANSION_PER_PAGE)) {
if (seen.has(b.id)) continue;
if (classifyBlock(b.content) !== 0 /* Fact */) continue;
const key = `${b.owner_id ?? ""} ${(b.page_name ?? "").trim()}`;
const hit = scoreOf.get(key);
if (!hit) continue;
const n = added.get(key) ?? 0;
if (n >= PAGE_EXPANSION_PER_PAGE) continue;
added.set(key, n + 1);
entries2.push({
...b,
score: hit.score,
retrieved_via: "page_expansion",
matched_block_id: hit.matched_block_id
});
}
}
}
entries2 = rankByTier(entries2);
entries2 = entries2.slice(0, requestedTopK);
if (entries2.length === 0) {
let empty_reason;
let capability_hint;
let admin_hint;
if (hits.length === 0) {
const status = await backfillStatus(c.env, { owner_id });
if (status.embedded === 0 && status.pending > 0) {
empty_reason = "no_index";
capability_hint = "\u8A9E\u610F\u641C\u5C0B\u7684\u7D22\u5F15\u51FA\u4E86\u72C0\u6CC1\uFF0C\u6240\u4EE5\u66AB\u6642\u641C\u4E0D\u5230\u2014\u2014\u9019\u662F\u6211\u5011\u7CFB\u7D71\u7684\u554F\u984C\uFF0C\u4E0D\u662F\u4F60\u6253\u7684\u5B57\u6709\u554F\u984C\u3002\u7CFB\u7D71\u6B63\u5728\u81EA\u52D5\u91CD\u5EFA\uFF0C\u7A0D\u5F8C\u518D\u641C\u4E00\u6B21\u770B\u770B\u3002";
admin_hint = `owner_id=${owner_id ?? "(all)"} \u7BC4\u570D embedded=0 \u4F46 pending=${status.pending}\uFF1A\u8CC7\u6599\u5728\u3001\u7D22\u5F15\u5F9E\u6C92\u5EFA\u6210\uFF1D\u5BEB\u5165\u7AEF\u5D4C\u5165\u5F9E\u672A\u6210\u529F\uFF08\u6545\u969C\uFF09\u3002\u672C\u6B21\u5DF2\u80CC\u666F\u89F8\u767C backfill \u81EA\u7652\uFF08\u6BCF\u6279 100\uFF0C\u51AA\u7B49\uFF09\u3002`;
fireAndForget(c, backfillEmbeddings(c.env, { owner_id, limit: 100 }));
} else if (status.embedded === 0) {
empty_reason = "no_index";
capability_hint = "\u9019\u500B\u77E5\u8B58\u5EAB\u9084\u6C92\u6709\u6574\u7406\u597D\u7684\u5167\u5BB9\u53EF\u4EE5\u641C\u5C0B\u2014\u2014\u901A\u5E38\u662F\u525B\u88DD\u597D\u3001\u8CC7\u6599\u9084\u6C92\u540C\u6B65\u9032\u4F86\u3002\u7B49\u540C\u6B65\u5C0F\u5E6B\u624B\u8DD1\u5B8C\u518D\u4F86\u641C\u5C31\u6709\u4E86\u3002";
admin_hint = `owner_id=${owner_id ?? "(all)"} \u7BC4\u570D embedded=0 \u4E14 pending=0\uFF1A\u6C92\u6709\u4EFB\u4F55\u6A19\u8A18 embed:true \u7684 entry\u2014\u2014\u591A\u534A\u662F ingest \u9084\u6C92\u8DD1\uFF08\u6B63\u5E38\u7684\u7A7A\uFF09\uFF0C\u5C11\u6578\u60C5\u6CC1\u662F ingest \u7BA1\u7DDA\u6C92\u6A19 embed \u65D7\u6A19\uFF08\u8981\u67E5\u7BA1\u7DDA\uFF09\u3002`;
} else {
empty_reason = "no_match";
capability_hint = "\u6C92\u6709\u627E\u5230\u7B26\u5408\u7684\u5167\u5BB9\uFF0C\u63DB\u500B\u8AAA\u6CD5\u6216\u66F4\u5177\u9AD4\u7684\u95DC\u9375\u5B57\u518D\u8A66\u8A66\u770B\u3002";
admin_hint = `owner_id=${owner_id ?? "(all)"} \u5DF2\u6709 ${status.embedded} \u7B46\u5D4C\u5165\u8CC7\u6599\uFF0C\u4F46\u672C\u6B21\u67E5\u8A62\u5728 Vectorize \u7AEF\u96F6\u547D\u4E2D\uFF08\u542B embed.ts \u7D55\u5C0D\u9580\u6ABB\u904E\u6FFE\uFF09\u3002`;
if (status.pending > 0) fireAndForget(c, backfillEmbeddings(c.env, { owner_id, limit: 100 }));
}
} else {
empty_reason = "stale_index";
capability_hint = "\u9019\u6B21\u6BD4\u5C0D\u5230\u7684\u5167\u5BB9\u6E90\u982D\u5DF2\u7D93\u88AB\u79FB\u9664\u6216\u4E0B\u67B6\u4E86\uFF0C\u6240\u4EE5\u6C92\u6709\u53EF\u986F\u793A\u7684\u7D50\u679C\u3002\u7CFB\u7D71\u5DF2\u81EA\u52D5\u6E05\u7406\u904E\u671F\u7D22\u5F15\uFF08\u6211\u5011\u7684\u554F\u984C\uFF0C\u4F60\u4E0D\u7528\u505A\u4EFB\u4F55\u4E8B\uFF09\uFF0C\u63DB\u500B\u95DC\u9375\u5B57\u5C31\u80FD\u6B63\u5E38\u641C\u3002";
admin_hint = `Vectorize \u547D\u4E2D ${hits.length} \u7B46\uFF0C\u4F46 hydrate \u5F8C\u5168\u90E8\u662F\u5DF2\u4E0B\u67B6\u6216\u627E\u4E0D\u5230\u5C0D\u61C9\u8CC7\u6599\uFF08\u5B64\u5152\u5411\u91CF\uFF09\uFF0C\u975E\u5206\u6578\u9580\u6ABB\u9020\u6210\u2014\u2014\u76F8\u5C0D\u9580\u6ABB\u6578\u5B78\u4E0A\u4E0D\u53EF\u80FD\u780D\u5149\u975E\u7A7A\u7D50\u679C\uFF08cut<=top\uFF09\u3002\u672C\u6B21\u5DF2\u80CC\u666F\u89F8\u767C\u5411\u91CF\u6E05\u7406\uFF08deleteByIds\uFF09\u3002`;
}
return c.json({
success: true,
entries: entries2,
count: entries2.length,
mode: "semantic",
empty_reason,
capability_hint,
admin_hint
});
}
return c.json({ success: true, entries: entries2, count: entries2.length, mode: "semantic" });
}
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type, void 0, library, source, include_deprecated);
return c.json({ success: true, entries, count: entries.length, mode: "keyword" });
});
entryRoutes.get("/:id", async (c) => {
const entry = await getEntry(c.env.DB, c.req.param("id"));
if (!entry) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true, entry });
});
entryRoutes.patch("/deprecate-by-library", async (c) => {
const body = await c.req.json().catch(() => null);
const ownerId = String(body?.owner_id ?? "").trim();
const library = String(body?.library ?? "").trim();
if (!ownerId || !library) return c.json({ success: false, error: "owner_id \u8207 library \u5FC5\u586B" }, 400);
const ids = embedEnabled(c.env) ? await embeddedIdsByLibrary(c.env.DB, ownerId, library) : [];
const count = await deprecateEntriesByLibrary(c.env.DB, ownerId, library);
let vectors_deleted = 0;
if (ids.length > 0) {
try {
await c.env.VECTORIZE.deleteByIds(ids);
await markUnembedded(c.env.DB, ids);
vectors_deleted = ids.length;
} catch {
}
}
return c.json({ success: true, deprecated_count: count, vectors_deleted });
});
entryRoutes.post("/backfill-library", async (c) => {
const body = await c.req.json().catch(() => ({}));
const library = String(body.library ?? "").trim();
const ownerId = String(body.owner_id ?? "").trim();
if (!library || !ownerId) return c.json({ success: false, error: "library \u8207 owner_id \u5FC5\u586B" }, 400);
try {
const result = await backfillEntryLibraryTags(c.env.DB, c.env, {
library,
owner_id: ownerId,
entry_type: body.entry_type || void 0,
page_names: Array.isArray(body.page_names) && body.page_names.length > 0 ? body.page_names : void 0,
source_prefix: body.source_prefix || void 0,
page_name_prefix: body.page_name_prefix || void 0,
since: body.since !== void 0 ? Number(body.since) : void 0,
until: body.until !== void 0 ? Number(body.until) : void 0,
limit: body.limit !== void 0 ? Number(body.limit) : void 0
});
return c.json({ success: true, ...result });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
entryRoutes.get("/backfill-library/status", async (c) => {
const status = await libraryBackfillStatus(c.env.DB, {
owner_id: c.req.query("owner_id") || void 0,
entry_type: c.req.query("entry_type") || void 0,
source_prefix: c.req.query("source_prefix") || void 0,
page_name_prefix: c.req.query("page_name_prefix") || void 0,
since: c.req.query("since") ? Number(c.req.query("since")) : void 0,
until: c.req.query("until") ? Number(c.req.query("until")) : void 0
});
return c.json({ success: true, ...status });
});
entryRoutes.put("/:id", async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.entry_type) return c.json({ success: false, error: "entry_type required" }, 400);
const entry = await upsertEntry(c.env.DB, c.req.param("id"), body);
if (embedEnabled(c.env)) c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {
}));
return c.json({ success: true, entry });
});
entryRoutes.patch("/:id", async (c) => {
const body = await c.req.json().catch(() => ({}));
const entry = await updateEntry(c.env.DB, c.req.param("id"), body);
if (!entry) return c.json({ success: false, error: "not found" }, 404);
if (embedEnabled(c.env) && body.content !== void 0) {
c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {
}));
}
return c.json({ success: true, entry });
});
entryRoutes.delete("/:id", async (c) => {
const id = c.req.param("id");
let vector_deleted = null;
if (embedEnabled(c.env)) {
try {
await c.env.VECTORIZE.deleteByIds([id]);
vector_deleted = true;
} catch {
vector_deleted = false;
}
}
await deleteEntry(c.env.DB, id);
return c.json({ success: true, vector_deleted });
});
// kbdb/src/routes/templates.ts
function readFields(body) {
if (!body) return void 0;
const raw2 = Array.isArray(body.slots) ? body.slots : Array.isArray(body.fields) ? body.fields : void 0;
return raw2;
}
var templateRoutes = new Hono2();
templateRoutes.post("/", async (c) => {
const body = await c.req.json().catch(() => null);
const fields = readFields(body);
if (!body || !body.name || !Array.isArray(fields)) {
return c.json({ success: false, error: "name and slots[] (alias: fields[]) required" }, 400);
}
const tpl = await createTemplate(c.env.DB, { ...body, slots: fields });
return c.json({ success: true, template: tpl });
});
templateRoutes.get("/", async (c) => {
const templates = await listTemplates(c.env.DB);
return c.json({ success: true, templates, count: templates.length });
});
templateRoutes.get("/:idOrName", async (c) => {
const tpl = await getTemplate(c.env.DB, c.req.param("idOrName"));
if (!tpl) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true, template: tpl });
});
templateRoutes.patch("/:id", async (c) => {
const body = await c.req.json().catch(() => ({}));
const fields = readFields(body);
const tpl = await updateTemplate(c.env.DB, c.req.param("id"), {
...body,
...fields === void 0 ? {} : { slots: fields }
});
if (!tpl) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true, template: tpl });
});
// kbdb/src/actions/library-map.ts
var DETAIL_TOP_ENTITIES = 10;
async function deprecateStaleMapBlocks(db, library, keepId, owner_id) {
const params = [library, keepId];
if (owner_id) params.push(owner_id);
const res = await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/),worktree 路徑造成的假警報同本檔既有註解
`UPDATE entries
SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.status', 'deprecated'),
updated_at = unixepoch()
WHERE entry_type = 'block'
AND json_extract(metadata_json, '$.kind') = 'library_map'
AND json_extract(metadata_json, '$.library') = ?
AND id != ?
${owner_id ? "AND owner_id = ?" : ""}
AND (json_extract(metadata_json, '$.status') IS NULL
OR json_extract(metadata_json, '$.status') != 'deprecated')`
).bind(...params).run();
return res.meta?.changes ?? 0;
}
function toRow(m) {
return {
library: m.library,
narrative: m.narrative,
top_entities: m.entities.slice(0, 3).map((e) => e.name),
triplet_count: m.triplet_count,
entry_count: m.entry_count,
updated_at: m.updated_at
};
}
function bridgesFor(target, all) {
const mine = new Set(target.entities.map((e) => e.name));
const pairs = [];
for (const m of all) {
if (m.library === target.library) continue;
for (const e of m.entities) if (mine.has(e.name)) pairs.push({ entity: e.name, library: m.library });
}
pairs.sort((a, b) => a.entity < b.entity ? -1 : a.entity > b.entity ? 1 : a.library < b.library ? -1 : a.library > b.library ? 1 : 0);
const grouped = /* @__PURE__ */ new Map();
for (const p of pairs) {
if (!grouped.has(p.entity) && grouped.size >= 50) continue;
const libs = grouped.get(p.entity) ?? [];
if (!libs.includes(p.library)) libs.push(p.library);
grouped.set(p.entity, libs);
}
return [...grouped.entries()].map(([entity, libraries]) => ({ entity, libraries }));
}
function toDetail(m, all) {
const top = m.entities.slice(0, DETAIL_TOP_ENTITIES);
return {
record_id: m.record_id,
library: m.library,
narrative: m.narrative,
content: mapContent(m.library, m.narrative, top.slice(0, 3).map((e) => e.name)),
top_entities: top,
relation_profile: m.relations,
bridges: bridgesFor(m, all),
triplet_count: m.triplet_count,
entry_count: m.entry_count,
commit_hash: m.commit_hash,
status: "active",
updated_at: m.updated_at
};
}
async function listLibraryMaps(db, owner_id) {
const maps = await readStoredMaps(db, owner_id);
return maps.map(toRow).sort((a, b) => a.library.localeCompare(b.library));
}
async function getLibraryMapDetail(db, library, owner_id) {
const maps = await readStoredMaps(db, owner_id);
const m = maps.find((x) => x.library === library);
return m ? toDetail(m, maps) : null;
}
async function storedTripletCountsByLibrary(db, owner_id) {
const out = /* @__PURE__ */ new Map();
for (const m of await readStoredMaps(db, owner_id)) if (m.triplet_count > 0) out.set(m.library, m.triplet_count);
return out;
}
async function recomputeLibraryMap(db, input) {
const library = input.library.trim();
if (!library) throw new Error("library required");
const tripletTemplateName = input.triplet_template ?? DEFAULT_TRIPLET_TEMPLATE;
await ensureLibraryMapTemplate(db);
const librarySlotAdded = await ensureTripletLibrarySlot(db, tripletTemplateName);
const tripletTpl = await getTemplate(db, tripletTemplateName);
if (!tripletTpl) throw new Error(`triplet template not found: ${tripletTemplateName}`);
const ownerParam = input.owner_id || void 0;
const owner = ownerParam ?? null;
await noteRecordWrite(db, "portal_library", owner, null, { name: library });
const summaries = await aggregateTripletSummaries(db, tripletTpl.id, owner, {
library,
sourcePrefix: input.source_prefix
});
const s = summaries.get(library);
const entryCount = (await aggregateEntryCounts(db, owner, library)).get(library) ?? 0;
const values = {
triplet_count: String(s?.count ?? 0),
top_entities: JSON.stringify(s?.entities ?? []),
relation_profile: JSON.stringify(s?.relations ?? []),
entry_count: String(entryCount)
};
if (input.narrative?.trim()) values.narrative = input.narrative.trim();
if (input.commit_hash) values.commit_hash = input.commit_hash;
const mapId = await putStoredMap(db, owner, library, values);
const deprecatedStaleMaps = await deprecateStaleMapBlocks(db, library, mapId, ownerParam);
const map = await getLibraryMapDetail(db, library, ownerParam);
if (!map) throw new Error("recompute wrote the map but it is not readable back");
return {
map: ownerParam ? map : { ...map, record_id: mapId },
superseded: [],
deprecated_stale_maps: deprecatedStaleMaps,
triplet_template: tripletTemplateName,
triplet_library_slot_added: librarySlotAdded
};
}
async function tripletCountsByLibrary(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) {
const tpl = await getTemplate(db, tripletTemplateName);
if (!tpl) return /* @__PURE__ */ new Map();
const params = owner_id ? [tpl.id, owner_id] : [tpl.id];
const res = await db.prepare(
// kbdb-sql-ok:牆內本體(kbdb/src/actions/),worktree 路徑假警報同上
`SELECT ${libraryOf("tr.library")} AS library, COUNT(*) AS n
FROM (
SELECT b.src_id AS rid,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status,
MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library
FROM entries b
LEFT JOIN entries r ON r.src_id = b.src_id
AND r.rel_id IN ('fld_' || b.dst_id || '_status', 'fld_' || b.dst_id || '_library')
LEFT JOIN entries v ON v.id = r.dst_id
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND +b.owner_id = ?" : ""}
GROUP BY b.src_id
) AS tr
WHERE COALESCE(tr.status, 'active') = 'active'
GROUP BY ${libraryOf("tr.library")}`
).bind(...params).all();
const m = /* @__PURE__ */ new Map();
for (const r of res.results ?? []) m.set(r.library, r.n);
return m;
}
async function setLibraryNarrative(db, library, narrative, owner_id) {
const lib = library.trim();
if (!lib) throw new Error("library required");
const text = narrative.trim();
if (!text) throw new Error("narrative required");
await putNarrative(db, owner_id || null, lib, text);
const after = await getLibraryMapDetail(db, lib, owner_id);
if (!after) throw new Error("narrative written but map not readable back");
if (!owner_id) {
return { ...after, record_id: (await mapIdentity(null, lib)).id };
}
return after;
}
function questionTokens(question) {
const t = question.normalize("NFKC").toLowerCase();
const out = /* @__PURE__ */ new Set();
for (const w of t.match(/[a-z0-9]{2,}/g) ?? []) out.add(w);
for (const seg of t.replace(/[^一-鿿]/g, " ").split(/\s+/)) {
for (let i = 0; i + 1 < seg.length; i++) out.add(seg.slice(i, i + 2));
}
return out;
}
async function selectLibrariesForQuestion(db, question, opts = {}) {
const q = question.trim();
if (!q) throw new Error("question required");
const owner = opts.owner_id || void 0;
const maxLibs = Math.min(Math.max(Math.floor(opts.limit ?? 3), 1), 10);
const qNorm = q.normalize("NFKC").toLowerCase();
const qLen = [...qNorm].length;
const maps = await readStoredMaps(db, owner);
const allMaps = maps.map(toRow).sort((a, b) => a.library.localeCompare(b.library));
const considered = allMaps.length;
const hits = /* @__PURE__ */ new Map();
for (const m of maps) {
for (const e of m.entities) {
if ([...e.name].length < 2) continue;
const n = e.name.normalize("NFKC").toLowerCase();
if (!(qNorm.includes(n) || qLen >= 2 && n.includes(qNorm))) continue;
const cur = hits.get(m.library) ?? { entities: /* @__PURE__ */ new Set(), score: 0 };
cur.entities.add(e.name);
cur.score += 1 + Math.log(1 + e.degree);
hits.set(m.library, cur);
}
}
let libraries = [...hits.entries()].map(([library, v]) => ({
library,
score: Math.round(v.score * 100) / 100,
matched_entities: [...v.entities].slice(0, 8),
via: "graph"
})).sort((a, b) => b.score - a.score || a.library.localeCompare(b.library)).slice(0, maxLibs);
let route = libraries.length ? "graph" : "all";
if (!libraries.length && considered) {
const qToks = questionTokens(q);
const scored = allMaps.map((m) => {
const matched = [];
let score = 0;
for (const name of [m.library, ...m.top_entities]) {
const n = name.normalize("NFKC").toLowerCase();
if (n.length >= 2 && qNorm.includes(n)) {
matched.push(name);
score += 2;
}
}
if (m.narrative) {
const nToks = questionTokens(m.narrative);
let overlap = 0;
for (const t of qToks) if (nToks.has(t)) overlap += 1;
score += Math.min(overlap, 6) * 0.25;
}
return { library: m.library, score: Math.round(score * 100) / 100, matched_entities: matched, via: "index" };
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.library.localeCompare(b.library)).slice(0, maxLibs);
if (scored.length) {
libraries = scored;
route = "index";
}
}
const selected = new Set(libraries.map((l) => l.library));
return {
route,
vector_used: false,
libraries_considered: considered,
libraries,
indexes: allMaps.map((m) => ({ ...m, selected: selected.has(m.library) }))
};
}
// kbdb/src/routes/records.ts
var recordRoutes = new Hono2();
var isStringMap = (v) => !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
recordRoutes.post("/", async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.template || !body.values && !body.entry_ids) {
return c.json({ success: false, error: "template and values (or entry_ids) required" }, 400);
}
if (body.values !== void 0 && !isStringMap(body.values)) {
return c.json({ success: false, error: "values must be an object of {slot: string}" }, 400);
}
if (body.entry_ids !== void 0 && !isStringMap(body.entry_ids)) {
return c.json({ success: false, error: "entry_ids must be an object of {slot: entry_id}" }, 400);
}
try {
const rec = await createRecord(c.env.DB, body);
return c.json({ success: true, record: rec });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
recordRoutes.get("/triplet-stats", async (c) => {
const owner = c.req.query("owner_id") || void 0;
const counts = owner ? await storedTripletCountsByLibrary(c.env.DB, owner) : await tripletCountsByLibrary(c.env.DB, void 0);
const stats = [...counts.entries()].map(([library, triplet_count]) => ({ library, triplet_count })).sort((a, b) => a.library.localeCompare(b.library));
return c.json({ success: true, stats });
});
var intParam = (raw2, fallback, min, max) => {
const n = Number(raw2);
if (raw2 === void 0 || raw2 === "" || !Number.isFinite(n)) return fallback;
const v = Math.floor(n);
return v < min ? fallback : Math.min(v, max);
};
recordRoutes.get("/by-template/:template", async (c) => {
const limit = intParam(c.req.query("limit"), 100, 1, 500);
const offset = intParam(c.req.query("offset"), 0, 0, Number.MAX_SAFE_INTEGER);
const { records, total } = await searchByTemplatePage(
c.env.DB,
c.req.param("template"),
c.req.query("owner_id") || void 0,
limit,
offset
);
return c.json({ success: true, records, count: records.length, limit, offset, total });
});
recordRoutes.get("/:recordId", async (c) => {
const rec = await getRecord(c.env.DB, c.req.param("recordId"));
if (!rec) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true, record: rec });
});
recordRoutes.patch("/:recordId", async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.values || typeof body.values !== "object") {
return c.json({ success: false, error: "values required" }, 400);
}
try {
const rec = await updateRecord(c.env.DB, c.req.param("recordId"), body.values);
if (!rec) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true, record: rec });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
recordRoutes.delete("/:recordId", async (c) => {
const found = await deleteRecord(c.env.DB, c.req.param("recordId"));
if (!found) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true });
});
// kbdb/src/actions/recipe-stat.ts
function statId(canonicalId) {
return `recipestat:${canonicalId}`;
}
async function recordRecipeResult(db, canonicalId, ok, nowMs) {
const id = statId(canonicalId);
const existing = await db.prepare("SELECT metadata_json FROM entries WHERE id = ?").bind(id).first();
let stat;
if (existing) {
const prev = existing.metadata_json ? JSON.parse(existing.metadata_json) : 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;
}
async function getRecipeStat(db, canonicalId) {
const row = await db.prepare("SELECT metadata_json FROM entries WHERE id = ?").bind(statId(canonicalId)).first();
if (!row || !row.metadata_json) return emptyStat(canonicalId);
return JSON.parse(row.metadata_json);
}
function emptyStat(canonicalId) {
return { canonical_id: canonicalId, success_count: 0, failure_count: 0, last_status: null, last_at: null };
}
// kbdb/src/routes/recipe-stats.ts
var recipeStatRoutes = new Hono2();
recipeStatRoutes.post("/record", async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.canonical_id || typeof body.ok !== "boolean") {
return c.json({ success: false, error: "canonical_id and ok(boolean) required" }, 400);
}
const at = typeof body.at === "number" ? body.at : 0;
const stat = await recordRecipeResult(c.env.DB, body.canonical_id, body.ok, at);
return c.json({ success: true, stat });
});
recipeStatRoutes.get("/:canonical_id", async (c) => {
const stat = await getRecipeStat(c.env.DB, c.req.param("canonical_id"));
return c.json({ success: true, stat });
});
// kbdb/src/routes/embed.ts
var embedRoutes = new Hono2();
var OFF_HINT = "\u8A9E\u7FA9\u88DC\u5D4C\u9700\u5148\u958B embed \u6A21\u7D44\uFF08Vectorize+AI binding\uFF09\u3002\u53EB CC\u300C\u5E6B\u6211\u958B\u8A9E\u7FA9\u67E5\u8A62\u300D\uFF08\u8A2D kbdb_embed:true + redeploy \u6CE8\u5165 binding\uFF09\u5F8C\u518D\u547C\u53EB\u672C\u7AEF\u9EDE\u3002";
embedRoutes.post("/backfill", async (c) => {
if (!embedEnabled(c.env)) {
return c.json(
{ success: false, error: "embed module not enabled (need VECTORIZE + AI bindings)", capability_hint: OFF_HINT },
409
);
}
const body = await c.req.json().catch(() => ({}));
const result = await backfillEmbeddings(c.env, {
limit: body.limit !== void 0 ? Number(body.limit) : void 0,
owner_id: body.owner_id || void 0,
source: body.source || void 0,
library: body.library || void 0,
since: body.since !== void 0 ? Number(body.since) : void 0,
until: body.until !== void 0 ? Number(body.until) : void 0,
// reindex(Arcrun#11):重推既有向量讓事後建立的 Vectorize metadata index 收錄(見 embed.ts)。
reindex: body.reindex === true,
offset: body.offset !== void 0 ? Number(body.offset) : void 0
});
return c.json({ success: true, ...result });
});
embedRoutes.get("/backfill/status", async (c) => {
const status = await backfillStatus(c.env, {
owner_id: c.req.query("owner_id") || void 0,
source: c.req.query("source") || void 0
});
return c.json({ success: true, ...status });
});
embedRoutes.post("/reconcile", async (c) => {
if (!embedEnabled(c.env)) {
return c.json(
{ success: false, error: "embed module not enabled (need VECTORIZE + AI bindings)", capability_hint: OFF_HINT },
409
);
}
const body = await c.req.json().catch(() => ({}));
const result = await reconcileEmbedGeneration(c.env, {
limit: body.limit !== void 0 ? Number(body.limit) : void 0,
owner_id: body.owner_id || void 0,
library: body.library || void 0,
since: body.since !== void 0 ? Number(body.since) : void 0,
until: body.until !== void 0 ? Number(body.until) : void 0
});
return c.json({ success: true, ...result });
});
embedRoutes.get("/selftest", async (c) => {
const result = await embedSelfTest(c.env, { owner_id: c.req.query("owner_id") || void 0 });
return c.json({ success: true, ...result });
});
// kbdb/src/routes/map.ts
var mapRoutes = new Hono2();
mapRoutes.get("/select", async (c) => {
const q = c.req.query("q") || c.req.query("question") || "";
if (!q.trim()) return c.json({ success: false, error: "q required" }, 400);
const owner = c.req.query("owner_id") || void 0;
const limitNum = Number(c.req.query("limit"));
const limit = Number.isFinite(limitNum) && limitNum > 0 ? Math.floor(limitNum) : void 0;
try {
const selection = await selectLibrariesForQuestion(c.env.DB, q, {
owner_id: owner,
limit,
triplet_template: c.req.query("triplet_template") || void 0
});
return c.json({ success: true, ...selection });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
mapRoutes.post("/recompute", async (c) => {
const body = await c.req.json().catch(() => ({}));
const library = c.req.query("library") || (typeof body.library === "string" ? body.library : "");
if (!library || !library.trim()) return c.json({ success: false, error: "library required" }, 400);
try {
const result = await recomputeLibraryMap(c.env.DB, {
library,
narrative: typeof body.narrative === "string" ? body.narrative : void 0,
commit_hash: typeof body.commit_hash === "string" ? body.commit_hash : void 0,
owner_id: typeof body.owner_id === "string" ? body.owner_id : c.req.query("owner_id") || void 0,
source_prefix: typeof body.source_prefix === "string" ? body.source_prefix : void 0,
triplet_template: typeof body.triplet_template === "string" ? body.triplet_template : void 0,
top_n: typeof body.top_n === "number" ? body.top_n : void 0
});
return c.json({ success: true, ...result });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
mapRoutes.put("/:library/narrative", async (c) => {
const body = await c.req.json().catch(() => ({}));
const library = c.req.param("library");
const narrative = typeof body.narrative === "string" ? body.narrative : "";
if (!narrative.trim()) return c.json({ success: false, error: "narrative required" }, 400);
const owner = (typeof body.owner_id === "string" ? body.owner_id : c.req.query("owner_id")) || void 0;
try {
const map = await setLibraryNarrative(c.env.DB, library, narrative, owner);
return c.json({ success: true, map });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
mapRoutes.get("/", async (c) => {
const owner = c.req.query("owner_id") || void 0;
const libraries = await listLibraryMaps(c.env.DB, owner);
return c.json({ success: true, libraries, count: libraries.length });
});
mapRoutes.get("/:library", async (c) => {
const owner = c.req.query("owner_id") || void 0;
const library = c.req.param("library");
const map = await getLibraryMapDetail(c.env.DB, library, owner);
if (!map) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true, map });
});
// kbdb/src/actions/graph-query.ts
var D1_MAX_BOUND_PARAMS = 90;
function chunkForD1(items, fixedParams) {
const size = Math.max(1, D1_MAX_BOUND_PARAMS - fixedParams);
if (items.length <= size) return [items];
const out = [];
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
return out;
}
async function findTripletEdgesByNode(db, templateIdOrName, fields, nodeValue, owner_id, libraries) {
if (!nodeValue || fields.length === 0) return [];
const tpl = await getTemplate(db, templateIdOrName);
if (!tpl) return [];
const valueSql = owner_id ? `SELECT id FROM entries WHERE content = ? AND entry_type = 'value' AND (owner_id = ? OR owner_id IS NULL)` : `SELECT id FROM entries WHERE content = ? AND entry_type = 'value'`;
const valueParams = owner_id ? [nodeValue, owner_id] : [nodeValue];
const valueRows = await db.prepare(valueSql).bind(...valueParams).all();
const valueIds = (valueRows.results ?? []).map((r) => r.id);
if (valueIds.length === 0) return [];
const fieldIds = fields.map((f) => fieldEntryId(tpl.id, f));
const relPh = fieldIds.map(() => "?").join(",");
const recordIdSet = /* @__PURE__ */ new Set();
for (const idsChunk of chunkForD1(valueIds, fieldIds.length + (owner_id ? 1 : 0))) {
const dstPh = idsChunk.map(() => "?").join(",");
const relSql = owner_id ? `SELECT DISTINCT src_id AS record_id FROM entries
WHERE dst_id IN (${dstPh}) AND rel_id IN (${relPh}) AND owner_id = ?` : `SELECT DISTINCT src_id AS record_id FROM entries
WHERE dst_id IN (${dstPh}) AND rel_id IN (${relPh})`;
const relParams = owner_id ? [...idsChunk, ...fieldIds, owner_id] : [...idsChunk, ...fieldIds];
const relRows = await db.prepare(relSql).bind(...relParams).all();
for (const r of relRows.results ?? []) recordIdSet.add(r.record_id);
}
const recordIds = [...recordIdSet];
if (recordIds.length === 0) return [];
const libFilter = libraries && libraries.length > 0 ? libraries : null;
const libAgg = "MAX(CASE WHEN r.rel_id = ? THEN v.content END)";
const pivotFixed = 5 + (libFilter ? 1 + libFilter.length : 0);
const rows = [];
for (const recChunk of chunkForD1(recordIds, pivotFixed)) {
const recPh = recChunk.map(() => "?").join(",");
const pivotSql = `
SELECT b.src_id AS record_id,
MAX(CASE WHEN r.rel_id = ? THEN v.content END) AS subject,
MAX(CASE WHEN r.rel_id = ? THEN v.content END) AS predicate,
MAX(CASE WHEN r.rel_id = ? THEN v.content END) AS object,
MAX(CASE WHEN r.rel_id = ? THEN v.content END) AS status,
${libAgg} AS library
FROM entries b
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs'
LEFT JOIN entries v ON v.id = r.dst_id
WHERE b.rel_id = 'sys_belongs' AND b.src_id IN (${recPh})
GROUP BY b.src_id${libFilter ? `
HAVING ${libraryOf(libAgg)} IN (${libFilter.map(() => "?").join(",")})` : ""}`;
const pivotParams = [
fieldEntryId(tpl.id, "subject"),
fieldEntryId(tpl.id, "predicate"),
fieldEntryId(tpl.id, "object"),
fieldEntryId(tpl.id, "status"),
fieldEntryId(tpl.id, "library"),
...recChunk,
...libFilter ? [fieldEntryId(tpl.id, "library"), ...libFilter] : []
];
const pivotRows = await db.prepare(pivotSql).bind(...pivotParams).all();
rows.push(...pivotRows.results ?? []);
}
return rows;
}
function neighborToEdge(n) {
return n.direction === "in" ? { subject: n.node, predicate: n.predicate, object: n.from } : { subject: n.from, predicate: n.predicate, object: n.node };
}
async function graphNeighbors(db, start, opts = {}) {
const depth = Math.max(1, Math.min(Math.floor(opts.depth ?? 1) || 1, 10));
const template = opts.template ?? "triplet";
const directed = !!opts.directed;
const owner_id = opts.owner_id;
const libraries = opts.library && opts.library.length > 0 ? opts.library : void 0;
const startNode = canonicalEntity(start);
const visited = /* @__PURE__ */ new Set([startNode]);
let frontier = [startNode];
const neighbors = [];
for (let d = 1; d <= depth; d++) {
if (frontier.length === 0) break;
const next = [];
for (const cur of frontier) {
const outgoing = await findTripletEdgesByNode(db, template, ["subject"], cur, owner_id, libraries);
for (const e of outgoing) {
if (e.status === "deprecated") continue;
const nb = e.object;
if (!nb || visited.has(nb)) continue;
visited.add(nb);
neighbors.push({ node: nb, predicate: e.predicate ?? "", from: cur, depth: d, direction: "out" });
next.push(nb);
}
if (!directed) {
const incoming = await findTripletEdgesByNode(db, template, ["object"], cur, owner_id, libraries);
for (const e of incoming) {
if (e.status === "deprecated") continue;
const nb = e.subject;
if (!nb || visited.has(nb)) continue;
visited.add(nb);
neighbors.push({ node: nb, predicate: e.predicate ?? "", from: cur, depth: d, direction: "in" });
next.push(nb);
}
}
}
frontier = next;
}
return { success: true, start: startNode, depth, directed, libraries: libraries ?? null, neighbors, count: neighbors.length };
}
// kbdb/src/actions/entity-canon-backfill.ts
var HARD_LIMIT_CAP2 = 2e3;
var DEFAULT_LIMIT = 500;
async function canonicalizeTripletEntities(db, env, opts = {}) {
const templateName = opts.template ?? "triplet";
const dryRun = opts.dry_run !== false;
const limit = Math.min(Math.max(opts.limit ?? DEFAULT_LIMIT, 1), HARD_LIMIT_CAP2);
const offset = Math.max(opts.offset ?? 0, 0);
const ownerId = opts.owner_id?.trim() || void 0;
const tpl = await getTemplate(db, templateName);
if (!tpl) throw new Error(`triplet template not found: ${templateName}`);
const slots = JSON.parse(tpl.slots_json);
if (!isTripletShaped(slots)) {
throw new Error(`template is not triplet-shaped (need subject/predicate/object): ${templateName}`);
}
const sql = `SELECT b.src_id AS rid,
MAX(CASE WHEN r.rel_id = ? THEN r.dst_id END) AS subject_eid,
MAX(CASE WHEN r.rel_id = ? THEN v.content END) AS subject,
MAX(CASE WHEN r.rel_id = ? THEN r.dst_id END) AS object_eid,
MAX(CASE WHEN r.rel_id = ? THEN v.content END) AS object,
MAX(CASE WHEN r.rel_id = ? THEN v.content END) AS library
FROM entries b
LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs'
LEFT JOIN entries v ON v.id = r.dst_id
WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerId ? " AND b.owner_id = ?" : ""}
GROUP BY b.src_id
ORDER BY b.src_id
LIMIT ? OFFSET ?`;
const sField = fieldEntryId(tpl.id, "subject");
const oField = fieldEntryId(tpl.id, "object");
const lField = fieldEntryId(tpl.id, "library");
const params = [sField, sField, oField, oField, lField, tpl.id];
if (ownerId) params.push(ownerId);
params.push(limit, offset);
const res = await db.prepare(sql).bind(...params).all();
const rows = res.results ?? [];
const rewrites = /* @__PURE__ */ new Map();
const groups = /* @__PURE__ */ new Map();
let scannedCells = 0;
for (const row of rows) {
const lib = libraryOfValue(row.library);
for (const slot of ENTITY_SLOTS) {
const raw2 = slot === "subject" ? row.subject : row.object;
const eid = slot === "subject" ? row.subject_eid : row.object_eid;
if (typeof raw2 !== "string" || !raw2 || !eid) continue;
scannedCells++;
const canon = canonicalEntity(raw2);
const key = `${lib}\0${canon}`;
const g = groups.get(key) ?? { library: lib, canonical: canon, from: /* @__PURE__ */ new Set(), cells: 0 };
g.from.add(raw2);
g.cells++;
groups.set(key, g);
if (canon !== raw2) rewrites.set(eid, canon);
}
}
const changedCells = rewrites.size;
const merges = [...groups.values()].filter((g) => g.from.size > 1).map((g) => ({ library: g.library, canonical: g.canonical, from: [...g.from].sort(), cells: g.cells })).sort((a, b) => b.from.length - a.from.length || a.canonical.localeCompare(b.canonical));
const budget = await maintenanceBudgetToday(env, db);
let applied = 0;
let quotaExceeded = false;
if (!dryRun && changedCells > 0) {
const entries = [...rewrites.entries()].slice(0, budget.remaining);
quotaExceeded = entries.length < changedCells;
if (entries.length > 0) {
await db.batch(
entries.map(
([eid, content]) => db.prepare("UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?").bind(content, eid)
)
);
applied = entries.length;
}
try {
await addMaintenanceUsage(db, applied);
} catch {
}
}
return {
dry_run: dryRun,
triplet_template: templateName,
scanned_records: rows.length,
scanned_cells: scannedCells,
changed_cells: changedCells,
applied_cells: applied,
merges,
quota_limit: budget.limit,
quota_used_today: budget.used + applied,
quota_exceeded: quotaExceeded,
// 這一批滿了就還有下一批;沒滿就是掃到底了(誠實回 null,不要讓呼叫端自己猜)
next_offset: rows.length === limit ? offset + limit : null
};
}
// kbdb/src/routes/graph.ts
var graphRoutes = new Hono2();
graphRoutes.post("/canonicalize-entities", async (c) => {
const body = await c.req.json().catch(() => ({}));
try {
const result = await canonicalizeTripletEntities(c.env.DB, c.env, {
template: typeof body.template === "string" ? body.template : void 0,
owner_id: typeof body.owner_id === "string" ? body.owner_id : void 0,
dry_run: body.dry_run === false ? false : true,
limit: typeof body.limit === "number" ? body.limit : void 0,
offset: typeof body.offset === "number" ? body.offset : void 0
});
return c.json({ success: true, ...result });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
graphRoutes.get("/neighbors/:node", async (c) => {
const node = c.req.param("node");
if (!node) return c.json({ success: false, error: "node required" }, 400);
const depthRaw = Number(c.req.query("depth"));
const depth = Number.isFinite(depthRaw) && depthRaw > 0 ? Math.floor(depthRaw) : void 0;
const directed = c.req.query("directed") === "true";
const template = c.req.query("template") || void 0;
const owner_id = c.req.query("owner_id") || void 0;
const library = parseLibraryList(c.req.query("library"));
try {
const result = await graphNeighbors(c.env.DB, node, { depth, template, directed, owner_id, library });
const edges = result.neighbors.map(neighborToEdge);
return c.json({ ...result, edges });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 500);
}
});
// kbdb/src/actions/execution-log.ts
var SUCCESS_MESSAGE_MAX = 200;
var FAILED_MESSAGE_MAX = 2e3;
var TARGET_MAX = 300;
var DEFAULT_DAILY_LIMIT = 2e4;
var DEGRADE_RATIO = 0.8;
function dailyLimit(env) {
const raw2 = env.EXECUTION_LOG_DAILY_WRITE_LIMIT;
const n = raw2 ? parseInt(raw2, 10) : NaN;
return Number.isFinite(n) && n > 0 ? n : DEFAULT_DAILY_LIMIT;
}
function utcDay3() {
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
}
function truncate(s, max) {
if (s.length <= max) return s;
return s.slice(0, Math.max(0, max - 1)) + "\u2026";
}
async function checkUsage(db, limit) {
const id = `exlog-usage:${utcDay3()}`;
const existing = await db.prepare("SELECT metadata_json FROM entries WHERE id = ?").bind(id).first();
let count;
if (existing) {
let prevWrites = 0;
try {
const prev = existing.metadata_json ? JSON.parse(existing.metadata_json) : {};
prevWrites = Number(prev.writes) || 0;
} catch {
prevWrites = 0;
}
count = prevWrites + 1;
await db.prepare("UPDATE entries SET metadata_json = ?, updated_at = unixepoch() WHERE id = ?").bind(JSON.stringify({ day: utcDay3(), writes: count }), id).run();
} else {
count = 1;
await db.prepare(`INSERT INTO entries (id, entry_type, metadata_json) VALUES (?, 'execution_log_usage', ?)`).bind(id, JSON.stringify({ day: utcDay3(), writes: count })).run();
}
if (count > limit) return "skip";
if (count > limit * DEGRADE_RATIO) return "log_failure_only";
return "log";
}
async function recordExecutionLog(db, env, input) {
const limit = dailyLimit(env);
let mode;
try {
mode = await checkUsage(db, limit);
} catch {
mode = "log";
}
if (mode === "skip") return { written: false, mode };
if (mode === "log_failure_only" && input.verdict !== "failed") return { written: false, mode };
const maxLen = input.verdict === "failed" ? FAILED_MESSAGE_MAX : SUCCESS_MESSAGE_MAX;
const target = input.target ? truncate(String(input.target), TARGET_MAX) : null;
await createEntry(db, {
entry_type: "execution_log",
owner_id: input.owner_id ?? null,
page_name: input.workflow_id,
// 索引欄位(idx_entries_page)=查詢鍵,讀取端靠它篩單一 workflow
content: truncate(input.message ?? "", maxLen),
metadata_json: JSON.stringify({
verdict: input.verdict,
duration_ms: Math.max(0, Math.round(input.duration_ms)),
target
})
});
return { written: true, mode };
}
async function listExecutionLog(db, workflowId, ownerId, limit) {
const { entries } = await listEntries(db, {
entry_type: "execution_log",
page_name: workflowId,
owner_id: ownerId,
limit
});
return entries.map((e) => {
let meta = {};
try {
meta = e.metadata_json ? JSON.parse(e.metadata_json) : {};
} catch {
}
return {
workflow_id: workflowId,
verdict: meta.verdict ?? "unknown",
duration_ms: meta.duration_ms ?? 0,
message: e.content ?? "",
...meta.target ? { target: meta.target } : {},
recorded_at: e.created_at
};
});
}
async function latestExecutionLog(db, workflowId, ownerId) {
const rows = await listExecutionLog(db, workflowId, ownerId, 1);
return rows[0] ?? null;
}
var DEFAULT_RETENTION_DAYS = 90;
var CLEANUP_BATCH_LIMIT = 500;
function retentionConfigId(ownerId) {
return `exlog-retention:${ownerId}`;
}
async function getRetentionDays(db, ownerId) {
if (!ownerId) return DEFAULT_RETENTION_DAYS;
const row = await db.prepare(`SELECT metadata_json FROM entries WHERE id = ?`).bind(retentionConfigId(ownerId)).first();
if (!row) return DEFAULT_RETENTION_DAYS;
try {
const parsed = row.metadata_json ? JSON.parse(row.metadata_json) : {};
if (parsed.retention_days === null) return null;
const n = Number(parsed.retention_days);
return Number.isFinite(n) && n > 0 ? n : DEFAULT_RETENTION_DAYS;
} catch {
return DEFAULT_RETENTION_DAYS;
}
}
async function setRetentionDays(db, ownerId, days) {
const id = retentionConfigId(ownerId);
const metadata = JSON.stringify({ retention_days: days, updated_at: Math.floor(Date.now() / 1e3) });
const existing = await db.prepare(`SELECT id FROM entries WHERE id = ?`).bind(id).first();
if (existing) {
await db.prepare(`UPDATE entries SET metadata_json = ?, updated_at = unixepoch() WHERE id = ?`).bind(metadata, id).run();
} else {
await db.prepare(
`INSERT INTO entries (id, entry_type, owner_id, metadata_json) VALUES (?, 'execution_log_retention_config', ?, ?)`
).bind(id, ownerId, metadata).run();
}
}
async function cleanupExpiredLogs(db) {
const nowSec = Math.floor(Date.now() / 1e3);
const overridesRes = await db.prepare(`SELECT owner_id, metadata_json FROM entries WHERE entry_type = 'execution_log_retention_config'`).all();
const overrides = overridesRes.results ?? [];
const neverDeleteOwners = [];
const customOwners = [];
for (const row of overrides) {
if (!row.owner_id) continue;
let parsed = {};
try {
parsed = row.metadata_json ? JSON.parse(row.metadata_json) : {};
} catch {
continue;
}
if (parsed.retention_days === null) {
neverDeleteOwners.push(row.owner_id);
} else {
const n = Number(parsed.retention_days);
if (Number.isFinite(n) && n > 0) customOwners.push({ owner_id: row.owner_id, days: n });
}
}
let deleted = 0;
for (const { owner_id, days } of customOwners) {
const cutoff = nowSec - days * 86400;
const res = await db.prepare(
`DELETE FROM entries WHERE id IN (
SELECT id FROM entries WHERE entry_type = 'execution_log' AND owner_id = ? AND created_at < ?
LIMIT ?
)`
).bind(owner_id, cutoff, CLEANUP_BATCH_LIMIT).run();
deleted += res.meta?.changes ?? 0;
}
const defaultCutoff = nowSec - DEFAULT_RETENTION_DAYS * 86400;
const excluded = [...neverDeleteOwners, ...customOwners.map((o) => o.owner_id)];
const sql = excluded.length > 0 ? `DELETE FROM entries WHERE id IN (
SELECT id FROM entries WHERE entry_type = 'execution_log'
AND created_at < ?
AND (owner_id IS NULL OR owner_id NOT IN (${excluded.map(() => "?").join(",")}))
LIMIT ?
)` : `DELETE FROM entries WHERE id IN (
SELECT id FROM entries WHERE entry_type = 'execution_log' AND created_at < ? LIMIT ?
)`;
const binds = excluded.length > 0 ? [defaultCutoff, ...excluded, CLEANUP_BATCH_LIMIT] : [defaultCutoff, CLEANUP_BATCH_LIMIT];
const res2 = await db.prepare(sql).bind(...binds).run();
deleted += res2.meta?.changes ?? 0;
return { deleted, checked_overrides: overrides.length };
}
// kbdb/src/routes/execution-log.ts
var executionLogRoutes = new Hono2();
executionLogRoutes.post("/record", async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.workflow_id || body.verdict !== "success" && body.verdict !== "failed") {
return c.json({ success: false, error: 'workflow_id \u8207 verdict("success"|"failed") \u5FC5\u586B' }, 400);
}
const result = await recordExecutionLog(c.env.DB, c.env, {
workflow_id: body.workflow_id,
owner_id: body.owner_id ?? null,
verdict: body.verdict,
duration_ms: typeof body.duration_ms === "number" ? body.duration_ms : 0,
message: body.message ?? "",
target: body.target ?? null
});
return c.json({ success: true, ...result });
});
executionLogRoutes.get("/", async (c) => {
const workflowId = c.req.query("workflow_id");
if (!workflowId) return c.json({ success: false, error: "workflow_id \u5FC5\u586B" }, 400);
const ownerId = c.req.query("owner_id") || void 0;
const limitParam = c.req.query("limit");
const limit = Math.min(Math.max(parseInt(limitParam || "10", 10) || 10, 1), 100);
const executions = await listExecutionLog(c.env.DB, workflowId, ownerId, limit);
return c.json({ success: true, executions });
});
executionLogRoutes.get("/latest", async (c) => {
const workflowId = c.req.query("workflow_id");
if (!workflowId) return c.json({ success: false, error: "workflow_id \u5FC5\u586B" }, 400);
const ownerId = c.req.query("owner_id") || void 0;
const execution = await latestExecutionLog(c.env.DB, workflowId, ownerId);
return c.json({ success: true, execution });
});
executionLogRoutes.get("/retention", async (c) => {
const ownerId = c.req.query("owner_id");
if (!ownerId) return c.json({ success: false, error: "owner_id \u5FC5\u586B" }, 400);
const retentionDays = await getRetentionDays(c.env.DB, ownerId);
return c.json({ success: true, owner_id: ownerId, retention_days: retentionDays, default_days: DEFAULT_RETENTION_DAYS });
});
executionLogRoutes.put("/retention", async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.owner_id) return c.json({ success: false, error: "owner_id \u5FC5\u586B" }, 400);
const days = body.retention_days;
if (days !== null && (typeof days !== "number" || !Number.isFinite(days) || days <= 0)) {
return c.json({ success: false, error: "retention_days \u5FC5\u9808\u662F\u6B63\u6574\u6578\uFF0C\u6216 null\uFF08\u4EE3\u8868\u4E0D\u522A\u9664\uFF09" }, 400);
}
await setRetentionDays(c.env.DB, body.owner_id, days === null ? null : Math.round(days));
return c.json({ success: true, owner_id: body.owner_id, retention_days: days === null ? null : Math.round(days) });
});
executionLogRoutes.post("/cleanup", async (c) => {
const result = await cleanupExpiredLogs(c.env.DB);
return c.json({ success: true, ...result });
});
// kbdb/src/actions/schema-generation.ts
var GENERATIONS = [
{
n: 1,
file: "0001_base.sql",
what: "\u6838\u5FC3\u5169\u5F35\u8868\uFF08entries\uFF0Ftemplates\uFF09\u8207\u57FA\u672C\u7D22\u5F15",
checks: [
{ kind: "table", name: "entries" },
{ kind: "table", name: "templates" },
{ kind: "index", name: "idx_entries_owner" },
{ kind: "index", name: "idx_entries_type" },
{ kind: "template", name: "recipe_stat" }
]
},
{
n: 2,
file: "0002_credentials.sql",
what: "\uFF08\u5DF2\u9000\u5F79\uFF09\u7368\u7ACB credentials \u8868",
checks: [],
unprobeable: "0006 \u6703\u628A\u9019\u5F35\u8868\u62C6\u6389 \u21D2\u300C\u5957\u904E\u53C8\u62C6\u4E86\u300D\u8207\u300C\u5F9E\u4F86\u6C92\u5957\u904E\u300D\u4E8B\u5F8C\u9577\u5F97\u4E00\u6A21\u4E00\u6A23\uFF0Cschema \u5206\u4E0D\u51FA\u4F86\u3002\u5B83\u4E5F\u4E0D\u5F71\u97FF\u4EFB\u4F55\u73FE\u884C\u67E5\u8A62\uFF0C\u6545\u4E00\u5F8B\u8996\u70BA\u5DF2\u6EFF\u8DB3\uFF08\u5224\u6E96\u662F\u300C\u73FE\u5728\u80FD\u4E0D\u80FD\u7528\u300D\uFF0C\u4E0D\u662F\u300C\u6B77\u53F2\u8D70\u904E\u54EA\u4E9B\u6B65\u300D\uFF09\u3002"
},
{
n: 3,
file: "0003_library_map.sql",
what: "\u85CF\u66F8\u5730\u5716\uFF08library_map\uFF09\u7684 sheet \u5B9A\u7FA9",
checks: [{ kind: "template", name: "library_map" }]
},
{
n: 4,
file: "0004_execution_log_template.sql",
what: "\u57F7\u884C\u7D00\u9304\uFF08execution_log\uFF09\u7684 sheet \u5B9A\u7FA9",
checks: [{ kind: "template", name: "execution_log" }]
},
{
n: 5,
file: "0005_credential_template.sql",
what: "\u6191\u8B49\u76EE\u9304\uFF08credential\uFF09\u7684 sheet \u5B9A\u7FA9",
checks: [{ kind: "template", name: "credential" }]
},
{
n: 6,
file: "0006_drop_credentials_table.sql",
what: "\u820A credentials \u8868\u9000\u5834\uFF08D38 \u570D\u7246\u4FEE\u5FA9\uFF09",
checks: [{ kind: "no_table", name: "credentials" }]
},
{
n: 7,
file: "0007_tree_record_model.sql",
what: "\u6A39\u72C0 record \u6A21\u578B\uFF1A\u95DC\u4FC2\u4F4F\u5728 entries \u7684 src_id\uFF0Frel_id\uFF0Fdst_id \u4E09\u500B\u6307\u6A19\u6B04",
checks: [
{ kind: "entries_column", name: "src_id" },
{ kind: "entries_column", name: "rel_id" },
{ kind: "entries_column", name: "dst_id" },
{ kind: "index", name: "idx_entries_rel_src" },
{ kind: "index", name: "idx_entries_rel_dst" },
{ kind: "entry", id: "sys_root" },
{ kind: "entry", id: "sys_belongs" },
{ kind: "entry", id: "sys_field_of" },
// 舊表還在=0007 的資料搬遷沒跑完(它的最後一句就是拆掉它)。
// 🪦 entry_values 2026-08-15 廢除;「看到它=那台沒跟上,不是它還合法」。
{ kind: "no_table", name: "entry_values" }
]
},
{
n: 8,
file: "0008_entries_content_index.sql",
what: "entries.content \u7D22\u5F15\u2014\u2014graph \u9130\u5C45\u67E5\u8A62\u5F9E\u7BC0\u9EDE\u540D\u76F4\u63A5\u67E5\uFF08\u4E0D\u6488\u5168\u8868\uFF09\uFF0CArcrun#168 \u6839\u56E0\u4FEE\u5FA9\u7684\u5730\u57FA",
checks: [{ kind: "index", name: "idx_entries_content" }]
},
{
n: 9,
file: "0009_entries_list_indexes.sql",
what: "entries \u5217\u8868\uFF0F\u5B58\u5728\u6027\u7D22\u5F15\u2014\u2014list \u7AEF\u9EDE\u4E0D\u518D\u6392\u5E8F\u6574\u500B owner\u3001\u300C\u9019\u5F35\u5361\u5728\u4E0D\u5728\u300D\u53EA\u8B80\u547D\u4E2D\u5217\uFF08Arcrun#210 D1 \u514D\u8CBB\u5C64\u4E09\u5929\u9023\u71D2\u7684\u6839\u56E0\u4FEE\u5FA9\uFF09",
checks: [
{ kind: "index", name: "idx_entries_owner_type_created" },
{ kind: "index", name: "idx_entries_owner_created" },
{ kind: "index", name: "idx_entries_source" }
]
}
];
var EXPECTED_GENERATION = GENERATIONS[GENERATIONS.length - 1].n;
var LEDGER_FILES = GENERATIONS.map((g) => g.file);
var LEDGER_NUMBERS = GENERATIONS.map((g) => g.n);
var RETIRED_TABLES = ["entry_values", "credentials"];
async function readEntriesColumns(db, ddl) {
const cols = /* @__PURE__ */ new Set();
try {
const r = await db.prepare("PRAGMA table_info(entries)").all();
for (const row of r.results ?? []) if (row?.name) cols.add(String(row.name));
} catch {
}
if (cols.size > 0) return cols;
if (!ddl) return cols;
const open = ddl.indexOf("(");
const body = open >= 0 ? ddl.slice(open + 1) : ddl;
for (const line of body.split(/[\n,]/)) {
const m = /^\s*"?([A-Za-z_][A-Za-z0-9_]*)"?\s+(TEXT|INTEGER|REAL|BLOB|NUMERIC)/i.exec(line);
if (m) cols.add(m[1]);
}
return cols;
}
async function probeDataLayer(db) {
let facts;
try {
const [tableRows, indexRows] = await Promise.all([
db.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'table'").all(),
db.prepare("SELECT name FROM sqlite_master WHERE type = 'index'").all()
]);
const tables = new Set((tableRows.results ?? []).map((r) => r.name));
const indexes = new Set((indexRows.results ?? []).map((r) => r.name));
const entriesDdl = (tableRows.results ?? []).find((r) => r.name === "entries")?.sql ?? null;
const entriesColumns = tables.has("entries") ? await readEntriesColumns(db, entriesDdl) : /* @__PURE__ */ new Set();
const wantedTemplates = uniq(
GENERATIONS.flatMap(
(g) => g.checks.filter((c) => c.kind === "template").map((c) => c.name)
)
);
const wantedEntries = uniq(
GENERATIONS.flatMap(
(g) => g.checks.filter((c) => c.kind === "entry").map((c) => c.id)
)
);
const templates = tables.has("templates") ? await pluck(db, `SELECT name FROM templates WHERE name IN (${qs(wantedTemplates)})`, wantedTemplates, "name") : /* @__PURE__ */ new Set();
const entries = tables.has("entries") ? await pluck(db, `SELECT id FROM entries WHERE id IN (${qs(wantedEntries)})`, wantedEntries, "id") : /* @__PURE__ */ new Set();
facts = { tables, indexes, entriesColumns, templates, entries };
} catch (e) {
const detail = e instanceof Error ? e.message : String(e);
return {
ok: false,
summary: `\u8CC7\u6599\u5C64\u63A2\u6E2C\u5931\u6557\u2014\u2014\u9023 schema \u90FD\u8B80\u4E0D\u5230\uFF08${detail}\uFF09\u3002\u9019\u53F0\u7684\u77E5\u8B58\u5EAB\u73FE\u5728\u4E0D\u53EF\u7528\u3002`,
expected_generation: EXPECTED_GENERATION,
actual_generation: -1,
behind_by: -1,
missing: [],
legacy_tables: [],
remedy: "\u78BA\u8A8D D1 binding\uFF08DB\uFF09\u6307\u5411\u6B63\u78BA\u7684 arcrun-kbdb\uFF0C\u4E26\u91CD\u8DD1\u5B89\u88DD\u5668\u6216 `acr update`\u3002",
probe_error: detail
};
}
const missing = [];
for (const g of GENERATIONS) {
if (g.unprobeable) continue;
const evidence = g.checks.map((c) => explain(c, facts)).filter((e) => e !== null);
if (evidence.length > 0) missing.push({ generation: g.n, migration: g.file, what: g.what, evidence });
}
let actual = 0;
for (const g of GENERATIONS) {
if (missing.some((m) => m.generation === g.n)) break;
actual = g.n;
}
const legacy = RETIRED_TABLES.filter((t) => facts.tables.has(t));
const ok = missing.length === 0;
const behind = EXPECTED_GENERATION - actual;
return {
ok,
summary: ok ? `\u8CC7\u6599\u5C64\u8207\u7A0B\u5F0F\u78BC\u540C\u4EE3\uFF08\u7B2C ${actual} \u4EE3\uFF09\u3002` : `\u{1F534} \u8CC7\u6599\u5C64\u843D\u5F8C ${behind} \u4EE3\uFF1A\u9019\u53F0\u505C\u5728\u7B2C ${actual} \u4EE3\uFF0C\u9019\u4EFD\u7A0B\u5F0F\u78BC\u9700\u8981\u7B2C ${EXPECTED_GENERATION} \u4EE3\u3002\u7F3A ${missing.map((m) => m.migration).join("\u3001")}\u3002` + (legacy.length > 0 ? `\u820A\u8868\u9084\u5728\uFF1A${legacy.join("\u3001")}\u3002` : "") + "\u5728\u88DC\u9F4A\u4E4B\u524D\uFF0C\u51E1\u662F\u78B0\u5230\u9019\u5E7E\u4EE3\u65B0\u589E\u7D50\u69CB\u7684\u67E5\u8A62\u90FD\u6703\u5931\u6557\uFF08\u73FE\u8C61\uFF1D\u4E00\u5806 500\uFF09\u3002",
expected_generation: EXPECTED_GENERATION,
actual_generation: actual,
behind_by: behind,
missing,
legacy_tables: legacy,
remedy: ok ? "\u7121\u9700\u52D5\u4F5C\u3002" : "\u91CD\u8DD1\u5B89\u88DD\u5668\uFF0C\u6216\u5728\u88DD\u4E86 CLI \u7684\u6A5F\u5668\u4E0A\u8DD1 `acr update`\u2014\u2014\u5169\u689D\u8DEF\u90FD\u6703\u628A kbdb/migrations \u5E95\u4E0B\u7684\u6BCF\u4E00\u652F\u4F9D\u5E8F\u88DC\u8DD1\uFF08\u51AA\u7B49\uFF0C\u53EF\u91CD\u8DD1\uFF09\u3002"
};
}
function explain(c, f) {
switch (c.kind) {
case "table":
return f.tables.has(c.name) ? null : `\u7F3A\u8CC7\u6599\u8868 ${c.name}`;
case "no_table":
return f.tables.has(c.name) ? `\u820A\u8CC7\u6599\u8868 ${c.name} \u9084\u5728\uFF08\u8A72\u4EE3\u7684\u6700\u5F8C\u4E00\u6B65\u662F\u62C6\u6389\u5B83\uFF09` : null;
case "entries_column":
if (!f.tables.has("entries")) return "\u8B80\u4E0D\u5230 entries \u7684\u6B04\u4F4D\uFF08\u8868\u4E0D\u5B58\u5728\uFF09";
return f.entriesColumns.has(c.name) ? null : `entries \u7F3A\u6B04\u4F4D ${c.name}`;
case "index":
return f.indexes.has(c.name) ? null : `\u7F3A\u7D22\u5F15 ${c.name}`;
case "template":
return f.templates.has(c.name) ? null : `templates \u7F3A ${c.name}`;
case "entry":
return f.entries.has(c.id) ? null : `\u7F3A\u7CFB\u7D71\u4FDD\u7559\u5217 ${c.id}`;
}
}
function uniq(xs) {
return [...new Set(xs)];
}
function qs(xs) {
return xs.map(() => "?").join(", ");
}
async function pluck(db, sql, binds, field) {
if (binds.length === 0) return /* @__PURE__ */ new Set();
const r = await db.prepare(sql).bind(...binds).all();
return new Set((r.results ?? []).map((row) => row[field]));
}
// kbdb/src/index.ts
var app = new Hono2();
app.use("*", async (c, next) => {
const path = new URL(c.req.url).pathname;
if (path === "/" || path === "/health") return next();
const token = c.env.KBDB_INTERNAL_TOKEN;
if (!token) {
console.warn("[kbdb] KBDB_INTERNAL_TOKEN \u672A\u8A2D\u5B9A\u2014\u2014\u5168\u90E8\u8ACB\u6C42\u62D2\u7D55\uFF0C\u8ACB\u91CD\u8DD1\u5B89\u88DD\u5668\u4EE5\u6CE8\u5165\u91D1\u9470");
return c.json({ error: "Unauthorized", detail: "kbdb \u5C1A\u672A\u8A2D\u5B9A\u5167\u90E8\u91D1\u9470\uFF0C\u8ACB\u91CD\u8DD1\u5B89\u88DD\u5668" }, 401);
}
const auth = c.req.header("Authorization");
if (!auth || auth !== `Bearer ${token}`) {
return c.json({ error: "Unauthorized" }, 401);
}
return next();
});
app.get("/", (c) => c.json({ service: "arcrun-kbdb", tier: "base", status: "ok" }));
var HEALTH_CACHE_MS = 1e4;
var healthCache = null;
app.get("/health", async (c) => {
const now = Date.now();
if (!healthCache || now - healthCache.at > HEALTH_CACHE_MS) {
healthCache = { at: now, report: await probeDataLayer(c.env.DB) };
}
const dataLayer = healthCache.report;
return c.json(
{ ok: dataLayer.ok, status: dataLayer.ok ? "ok" : "degraded", data_layer: dataLayer },
dataLayer.ok ? 200 : 503
);
});
app.get("/maintenance/relation-orphans", async (c) => {
const { scanRelationOrphans: scanRelationOrphans2 } = await Promise.resolve().then(() => (init_relation_orphans(), relation_orphans_exports));
const limit = Number(c.req.query("limit") ?? "200");
const report = await scanRelationOrphans2(c.env.DB, Number.isFinite(limit) ? limit : 200);
return c.json({ success: true, ...report });
});
app.route("/entries", entryRoutes);
app.route("/templates", templateRoutes);
app.route("/sheets", templateRoutes);
app.route("/records", recordRoutes);
app.route("/recipe-stats", recipeStatRoutes);
app.route("/execution-log", executionLogRoutes);
app.route("/embed", embedRoutes);
app.route("/map", mapRoutes);
app.route("/graph", graphRoutes);
var index_default = app;
export {
index_default as default
};