Compare commits

..

6 Commits

Author SHA1 Message Date
uncle6me-web b6ef0f07dc fix(kbdb): 開通 PATCH /kbdb/records/:id proxy + 補三元組 library 補標測試
leo 2026-08-11:貼 wiki 卡「這裏就有 3 個三元組,如果三元組是 0 一定是有錯」——
三元組沒有不見(新式 1,633 筆/舊式 636 筆都還在),問題是地圖看不到:新式三元組
只有 171 筆填了 library slot,其餘因寫入時 template 還沒有該 slot 而被靜默丟棄
(record-crud.ts createRecord 只替「已宣告」的 slot 建值,不是替 caller 傳的
values 建值——順序錯了值就消失,不報錯)。

本次改動:
- cypher-executor/src/routes/kbdb-proxy.ts:補 PATCH /kbdb/records/:recordId。
  基本盤(kbdb/src/routes/records.ts)早有這個端點(mira-dissolve T2.1 的
  updateRecord),但這條 proxy 之前只轉發 GET/POST /kbdb/records,外部(工作流/
  CLI/補標腳本)打不到——能力在,通道沒開,補標三元組只能繞去改表(違 D38)。
  純轉發、無業務邏輯,比照既有 PATCH /kbdb/entries/:id 慣例。
- kbdb/tests/triplet-library-backfill.test.ts:真 node:sqlite 驗三件事——
  ①源頭順序(ensure-slot 必須在 write 之前,事後補救不了已寫的那筆,重現+
  驗證正解)②存量補標(地圖輸出前後對照)③不會重複做(同批跑兩次,第二次
  touch 0 筆)。順手記錄一個相關但不在本次範圍的小落差:liveTripletCountsByLibrary
  的 'general' fallback 桶與 recomputeLibraryMap 的精確比對語意沒對齊。
- cypher-executor/tests/kbdb-records-patch-proxy.test.ts:新端點的租戶閘/
  參數驗證/轉發/404 透傳。

全數綠燈:kbdb 181/181、cypher-executor 新增 8 案全過(既有 14 案失敗為
pre-existing,已用 git stash 比對確認與本次改動無關)。

kbdb-graph-plugin(實際的三元組寫入端 createTriplet/ingestEnvelope)需要同款
修正(library 補進 TRIPLET_SLOTS + 從 source_uri 推導),但該 repo 現行
0 份 active SDD,其 sdd-guard 明確要求人決定要不要開啟——本次不越界代為決定,
留在報告中交回。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:48:31 +08:00
uncle6me-web 507620e313 merge: Arcrun 統一編譯零件,成品放固定位置 .worker-builds/(Arcrun#80)
總管 2026-08-11 審過後併入:
- 照本 repo 既有慣例(.component-builds 底下的 wasm 本來就 commit 進來),不新造第二套
- 每顆成品自帶 source_commit + content_sha256 ⇒ 答得出「我是哪個版本編出來的」到單顆層級
- 重現性已驗:兩個不同路徑深度的獨立 clone 分別裝依賴分別編譯,
  5 顆的 content_sha256 與 wasm sha256 逐位元相同(manifest 只差 generated_at)
  ⇒ 這正是 arcrun-rag#39 的驗收條件,也是 #72 那個死結的解

待觀察(不擋本次):每次編譯會 commit 數萬行成品進原始碼 repo,長期會脹。
2026-08-11 13:54:19 +08:00
Leo 1e94f8451e Arcrun#80: commit tier2 worker 官方編譯成品到固定位置 .worker-builds/
從本 repo 的 cypher-executor / kbdb / .component-builds/http_request /
registry/components/code / mcp 五個原始碼目錄,用 scripts/build-worker-artifacts.mjs
編出 5 顆成品,commit 進 repo(與既有 .component-builds/*/component.wasm 同一套
「固定位置、任何人直接拿」慣例)。

每顆 manifest 條目自帶 source_commit(該原始碼目錄最後改動的 commit)與
content_sha256,答得出「我是哪個版本的原始碼編出來的」到單顆層級。

重現性已驗證(見 commit 說明外的驗證記錄):同一個 commit 在兩個獨立 clone
(不同磁碟路徑深度,刻意模擬雲端 vs 地端的目錄結構差異)分別裝依賴、分別編譯,
5 顆的 content_sha256 與 wasm module sha256 逐位元相同,manifest.json 唯一差異
是 generated_at 時間戳。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:45:33 +08:00
Leo dcb6ad693b fix(build-worker-artifacts): dirty 判斷排除自己的輸出目錄
.worker-builds/ 是本腳本的輸出,在成品寫出、commit 之前永遠是 untracked——
拿它判斷「原始碼乾不乾淨」是自己把自己判成髒的假陽性。git status pathspec
排除 .worker-builds 後才是「原始碼有沒有未 commit 變更」的真實訊號。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:45:33 +08:00
Leo 3b0238bb28 Arcrun#80: 新增 tier2 worker 官方編譯腳本(成品固定位置的地基)
背景:安裝器(arcrun-rag)過去自己對 Arcrun 原始碼跑 esbuild,導致同一份原始碼在
不同機器編出不同位元組(見 arcrun-rag changelog 1.4.33 段、arcrun-rag#72):
① esbuild 把入口路徑寫進產物內部註解,該路徑預設相依於執行時 cwd
② 各 worker 目錄用不同套件管理器裝 node_modules,夾帶不同版本間接依賴

本腳本解法:
- absWorkingDir 固定為「本腳本自己算出的 repo 根目錄」,entry 一律用相對路徑餵給
  esbuild——不管 clone 放在磁碟哪個絕對路徑,esbuild 內部產生的相對路徑字串相同
- 不自己跑 install,強制要求呼叫者先用該目錄既有 lockfile 裝好依賴(pnpm frozen /
  npm ci),避免「install 方式不同 → 依賴版本不同 → 位元組不同」
- 每顆 worker 的 manifest entry 自帶 source_commit(該目錄最後改動的 commit),
  答到單顆層級,不是整包一個 source 欄位

涵蓋 5 顆 tier2 worker(與 arcrun-rag bundle-components.mjs CORE_COMPONENTS 對齊):
cypher-executor / kbdb / http_request / code / mcp。

scripts/ 自帶 package.json + pnpm-lock.yaml 鎖 esbuild 版本(0.24.0)——
工具版本本身也是重現性的輸入之一。

驗證:本地跑通 5/5 build;byte-identical 重現性驗證見後續 commit(兩個獨立 clone
比對 sha256)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:45:33 +08:00
uncle6me-web 788295ed71 merge: 忘記密碼的錯誤訊息改白話+健康檢查回報寄信有沒有接上(arcrun-rag#38/#69/#25)
總管 2026-08-11 審過後併入:
- 拿掉行話「實例」;拿掉「請管理員直接幫你改密碼」(單人使用者就是管理員=死路,#25 同病)
- 改成給一條他自己走得完的路:重新跑一次安裝/更新
- /health 增回報 mail_relay_configured,讓安裝器判斷得出這台要不要重推
2026-08-11 13:24:34 +08:00
16 changed files with 60072 additions and 0 deletions
+4
View File
@@ -6,6 +6,10 @@ dist/
# 例外:放行 .component-builds 的部署物 wasm — self-host 用戶 / acr init 從 repo 直接拿這份部署
# (推翻 rule 05 原「wasm 不 commit」慣例,見 .agents/specs/arcrun/sdk-and-website/self-hosted-init.md §6
!.component-builds/**/component.wasm
# 例外:Arcrun#80 tier2 worker 官方編譯成品(cypher-executor/kbdb/http_request/code/mcp 的
# esbuild bundle + 隨附 wasm part)——commit 進 repo 同一套理由:固定位置、any clone 都拿得到,
# 不必自己再編一次(見 scripts/build-worker-artifacts.mjs)。
!.worker-builds/**/*.wasm
# 例外:code 零件(自足 Worker)的 vendored quickjs.wasm 同屬部署物 —— acr init/update 從
# repo archive 直接部署(同上 .component-builds 放行邏輯)。來源=npm 套件
# @jitl/quickjs-wasmfile-release-sync 的 emscripten-module.wasm,由 postinstall vendor-wasm.mjs
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+174
View File
@@ -0,0 +1,174 @@
{
"schema": 1,
"built_for": "arcrun-tier2-worker-artifacts",
"generated_at": "2026-08-11T05:33:37.988Z",
"repo_head": "d8bbf2241bd6b117d76fb27d9e386ecfb0ffe8f7",
"repo_dirty": false,
"workers": [
{
"name": "arcrun-cypher-executor",
"source_dir": "cypher-executor",
"source_commit": "797e7f751cc42cb1f5d9e2e187f18cf51eb981a1",
"main_module": "worker.mjs",
"main_file": "arcrun-cypher-executor/worker.mjs",
"js_bytes": 568855,
"content_sha256": "66e2a6341854e8b2de0567a46282b94669e73b95d152b05b17b0f8b58e257fec",
"modules": [],
"compat_date": "2025-02-19",
"compat_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
"requires": {
"kv": [
"EXEC_CONTEXT",
"WEBHOOKS",
"CREDENTIALS_KV",
"ANALYTICS_KV",
"RECIPES",
"USERS_KV",
"SESSIONS_KV"
],
"d1": [
{
"binding": "CREDENTIALS_DB",
"database_name": "arcrun-kbdb"
}
],
"vectorize": 0,
"ai": true,
"vars": {
"ENVIRONMENT": "production",
"CF_ACCOUNT_ID": "",
"WORKER_SUBDOMAIN": "uncle6-me",
"KBDB_BASE_URL": "https://arcrun-kbdb.uncle6-me.workers.dev",
"CONSOLE_TENANT": "leo",
"PORTAL_SESSION_TTL": "604800",
"PORTAL_SHOW_WORKFLOWS": "admin",
"GITEA_BASE_URL": "https://git.uncle6.me",
"GITEA_SPRINT_REPO": "Leo/InkStoneCo",
"GITEA_SPRINT_DIR": "system-dev/docs/3-specs/autonomy-dispatch"
}
},
"stripped": {
"services": 13
},
"warnings": []
},
{
"name": "arcrun-kbdb",
"source_dir": "kbdb",
"source_commit": "a7e23badf2a771be779a861e69e7efa6e8141dfe",
"main_module": "worker.mjs",
"main_file": "arcrun-kbdb/worker.mjs",
"js_bytes": 135910,
"content_sha256": "5e5a7a030f4fd1f5549ace6791c3827b6497b0bfdd9add46af041af47c472905",
"modules": [],
"compat_date": "2025-02-19",
"compat_flags": [
"nodejs_compat"
],
"requires": {
"kv": [],
"d1": [
{
"binding": "DB",
"database_name": "arcrun-kbdb"
}
],
"vectorize": 0,
"ai": false,
"vars": {
"ENVIRONMENT": "production"
}
},
"warnings": []
},
{
"name": "arcrun-http-request",
"source_dir": ".component-builds/http_request",
"source_commit": "1e85dfb49b0e8d81c0854781d93ee4e6a300c7b3",
"main_module": "worker.mjs",
"main_file": "arcrun-http-request/worker.mjs",
"js_bytes": 80073,
"content_sha256": "9a9dcb71879a7bdfd9fec1bd94eb9742e12cb63733d822ce63eeb1be30008d15",
"modules": [
{
"name": "component.wasm",
"type": "application/wasm",
"file": "arcrun-http-request/component.wasm",
"sha256": "cc15cc785703e7bbb8dbff2d38dc84a4ac24e2f44316182730abae0f170ef133"
}
],
"compat_date": "2025-02-19",
"compat_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
"requires": {
"kv": [],
"d1": [],
"vectorize": 0,
"ai": false,
"vars": {
"COMPONENT_ID": "http_request"
}
},
"warnings": []
},
{
"name": "arcrun-code",
"source_dir": "registry/components/code",
"source_commit": "621cb8d948d61be6202063fd02effb3f538437fe",
"main_module": "worker.mjs",
"main_file": "arcrun-code/worker.mjs",
"js_bytes": 153671,
"content_sha256": "285a7406ec694ae47dccfaf48517f712c74d207a1689dffa15c39f1555b45be5",
"modules": [
{
"name": "quickjs.wasm",
"type": "application/wasm",
"file": "arcrun-code/quickjs.wasm",
"sha256": "105c3bed22d457e43e3d1c3c1c6959fda62a8fe06f0fc8a985303c3a2be72232"
}
],
"compat_date": "2025-02-19",
"compat_flags": [],
"requires": {
"kv": [],
"d1": [],
"vectorize": 0,
"ai": false,
"vars": {
"COMPONENT_ID": "code"
}
},
"warnings": []
},
{
"name": "arcrun-mcp",
"source_dir": "mcp",
"source_commit": "035e8b255b0dcbd4238707f7d2ac8ccf9ee1ba72",
"main_module": "worker.mjs",
"main_file": "arcrun-mcp/worker.mjs",
"js_bytes": 1165130,
"content_sha256": "be15033f32e605f03f69bd10cd87782dafa34dbafeee2ce367bd7361a062a291",
"modules": [],
"compat_date": "2024-11-27",
"compat_flags": [
"nodejs_compat"
],
"requires": {
"kv": [
"OAUTH_KV"
],
"d1": [],
"vectorize": 0,
"ai": false,
"vars": {}
},
"warnings": []
}
],
"notes": []
}
+21
View File
@@ -119,6 +119,27 @@ kbdbProxyRouter.get('/kbdb/records/:recordId', async (c) => {
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// PATCH /kbdb/records/:recordId — 翻某筆 record 的 slot 值({ values:{slot:content} })。
// 補上基本盤既有能力(kbdb/src/routes/records.ts 的 PATCH /records/:recordIdmira-dissolve T2.1
// 缺的對外通道——2026-08-11 leo 三元組 library 補標核實:base 早有這個端點,但這條 proxy
// 之前只轉發 GET/POST,插件/工作流打不到,補標三元組只能繞去改表(違 D38)。單純轉發,無業務邏輯。
// by-id 沿用既有慣例(require-key,不額外做 owner 比對——與本檔 GET .../:recordId、
// PATCH /kbdb/entries/:id 同款)。
kbdbProxyRouter.patch('/kbdb/records/:recordId', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const body = await c.req.json().catch(() => null);
if (!body || typeof body.values !== 'object' || body.values === null) {
return c.json({ error: 'values 必填({slot名: 內容}' }, 400);
}
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/records/${encodeURIComponent(c.req.param('recordId'))}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ values: body.values }),
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// ── search(限本租戶範圍內)────────────────────────────────────────────────────
// GET /kbdb/search?q=&entry_type=&source=&library=&mode= — entries 搜尋,限本租戶 owner_id。
@@ -0,0 +1,89 @@
/**
* PATCH /kbdb/records/:recordId proxy 測試(2026-08-11,三元組 library 補標需求核實)
*
* 背景:基本盤 kbdb/src/routes/records.ts 早有 PATCH /records/:recordIdmira-dissolve T2.1
* updateRecord 已支援「補一個 record 原本沒有的 slot 值」的 idempotent grow)。但這條 cypher
* proxykbdb-proxy.ts)之前只轉發 GET/POST /kbdb/records,沒開 PATCH——外部(工作流/CLI/
* 任何走 X-Arcrun-API-Key 的呼叫者)打不到,等於基本盤能力在,通道沒開。
*
* 驗證 IO 接線(聚合真身在 KBDB 基本盤,這裡只測轉發,比照 kbdb-map-proxy.test.ts 慣例):
* 1. 租戶閘:無 X-Arcrun-API-Key → 401 不碰 KBDB
* 2. body 沒有 values → 400,不轉發
* 3. 轉發:PATCH /kbdb/records/:id → base PATCH /records/:idbody 只帶 { values }
* 4. base 404record 不存在)→ 原樣透傳,不假裝成功
*
* KBDB 打 fetchMock 假 hostwrangler.test.toml KBDB_BASE_URL=https://kbdb.test)+
* disableNetConnect——測試絕不外連。
*/
import { SELF, fetchMock } from 'cloudflare:test';
import { beforeAll, afterEach, describe, it, expect } from 'vitest';
const KEY = { 'X-Arcrun-API-Key': 'leo', 'Content-Type': 'application/json' };
beforeAll(() => {
fetchMock.activate();
fetchMock.disableNetConnect();
});
afterEach(() => fetchMock.assertNoPendingInterceptors());
describe('PATCH /kbdb/records/:recordId — 租戶閘', () => {
it('無 X-Arcrun-API-Key → 401,不碰 KBDB', async () => {
const res = await SELF.fetch('http://localhost/kbdb/records/rec_1', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values: { library: 'kb' } }),
});
expect(res.status).toBe(401);
});
});
describe('PATCH /kbdb/records/:recordId — 參數驗證', () => {
it('body 沒有 values → 400,不轉發', async () => {
const res = await SELF.fetch('http://localhost/kbdb/records/rec_1', {
method: 'PATCH',
headers: KEY,
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
});
describe('PATCH /kbdb/records/:recordId — 轉發', () => {
it('轉發 base PATCH /records/:idbody 只帶 values(不夾帶其他欄位)', async () => {
fetchMock
.get('https://kbdb.test')
.intercept({
path: '/records/rec_1',
method: 'PATCH',
body: JSON.stringify({ values: { library: 'gitea:Leo/kb' } }),
})
.reply(200, {
success: true,
record: { record_id: 'rec_1', template_id: 'tpl-triplet', values: { library: 'gitea:Leo/kb' } },
});
const res = await SELF.fetch('http://localhost/kbdb/records/rec_1', {
method: 'PATCH',
headers: KEY,
body: JSON.stringify({ values: { library: 'gitea:Leo/kb' } }),
});
expect(res.status).toBe(200);
const data = (await res.json()) as { success: boolean; record: { values: Record<string, string> } };
expect(data.success).toBe(true);
expect(data.record.values.library).toBe('gitea:Leo/kb');
});
it('base 404record 不存在)→ 原樣透傳,不假裝成功', async () => {
fetchMock
.get('https://kbdb.test')
.intercept({ path: '/records/nope', method: 'PATCH' })
.reply(404, { success: false, error: 'not found' });
const res = await SELF.fetch('http://localhost/kbdb/records/nope', {
method: 'PATCH',
headers: KEY,
body: JSON.stringify({ values: { library: 'kb' } }),
});
expect(res.status).toBe(404);
const data = (await res.json()) as { success: boolean };
expect(data.success).toBe(false);
});
});
+198
View File
@@ -0,0 +1,198 @@
// 三元組 library 補標 — 源頭順序 + 存量補標 + 冪等(2026-08-11leo 貼 wiki 卡「三元組要恢復」)
//
// 背景(system-dev/wiki/ops-facts.md「三元組在 KBDB 有兩代儲存形式」段,2026-08-11 實測):
// 1,633 筆新式三元組只有 171 筆填了 library slot,地圖(GET /map)因此幾乎看不到資料。
// 根因是**寫入順序**createRecord 只會替 template.slots_json 裡「已宣告」的 slot 建 entry_value
// (見 src/actions/record-crud.ts createRecord`for (const slot of slots) { if (!(slot in
// input.values)) continue }`——注意是遍歷 template 既有 slots,不是遍歷 caller 傳的 values)。
// 若呼叫端在 template 還沒有 'library' slot 時就送出 library 值,那個值會被**靜默丟棄**、
// 不報錯——這正是「查得到 171 筆」的來源:只有「已經跑過一次 ensureTripletLibrarySlot/recompute
// 之後」的批次,library 才真的落地。
//
// 本檔驗三件事(對應 leo 交辦的三個「要驗的」):
// 1. 源頭:ensure-slot 必須在 write 之前,不能事後補(重現+證明順序才是正解)
// 2. 存量:對一批缺 library 的舊 triplet 補標,前後地圖輸出對照
// 3. 不會重複做:同一批跑兩次,第二次 touch 0 筆
//
// 測試手法沿 library-map.test.ts 慣例:真 node:sqliteNode ≥22.5 內建,零新依賴)跑
// migrations 原檔,本檔只是這顆記憶體內測試替身的操作者——不是碰 KBDB 的正式 D1,
// 與正式資料庫零關聯(D38 的牆管的是「牆外程式碼碰 KBDB 的真 D1」,這裡是牆內邏輯的
// 白盒測試替身,kbdb-api-wall-guard 對 *.test.ts 路徑做字面 grep 會誤判,行尾標
// kbdb-sql-ok 是這個誤判的既定逃生艙,見 hook 說明「Genuine exception」)。
import { describe, it, expect } from 'vitest';
import { DatabaseSync } from 'node:sqlite';
import { readFileSync } from 'node:fs';
import {
recomputeLibraryMap,
ensureTripletLibrarySlot,
ensureFreshLibraryMaps,
listLibraryMaps,
} from '../src/actions/library-map';
import { createTemplate, createRecord, updateRecord, getRecord, searchByTemplate } from '../src/actions/record-crud';
function makeSqliteD1(): D1Database {
const raw = new DatabaseSync(':memory:'); // kbdb-sql-ok: 記憶體測試替身,非真 KBDB D1
raw.exec(readFileSync(new URL('../migrations/0001_base.sql', import.meta.url), 'utf8')); // kbdb-sql-ok: 灌測試替身 schema,非真 D1
raw.exec(readFileSync(new URL('../migrations/0003_library_map.sql', import.meta.url), 'utf8')); // kbdb-sql-ok: 同上
function stmt(sql: string, params: unknown[]) {
const s = {
bind(...args: unknown[]) { return stmt(sql, args); },
async all<T>() { return { results: raw.prepare(sql).all(...params) as T[] }; }, // kbdb-sql-ok: 記憶體測試替身
async first<T>() { return (raw.prepare(sql).get(...params) ?? null) as T | null; }, // kbdb-sql-ok: 記憶體測試替身
async run() { raw.prepare(sql).run(...params); return { success: true }; }, // kbdb-sql-ok: 記憶體測試替身
};
return s;
}
return { prepare: (sql: string) => stmt(sql, []) } as unknown as D1Database;
}
// prod 實際 triplet template 的 slotslibrary-map.test.ts 同款常數,2026-07-19 kbdb_list_templates
// 核實)——注意:沒有 library。用它模擬「template 還沒被任何 recompute 摸過」的乾淨起點。
const PROD_TRIPLET_SLOTS = [
'subject', 'predicate', 'object', 'source_block_id', 'confidence', 'clusters_json',
'bridge_score', 'subject_entity_type', 'object_entity_type', 'status', 'superseded_by',
'source_uri', 'content_hash', 'source_anchor', 'predicate_embed',
];
async function seedTripletTemplate(db: D1Database): Promise<void> {
await createTemplate(db, { id: 'tpl-triplet-test', name: 'triplet', slots: PROD_TRIPLET_SLOTS, created_by: 'kbdb-graph' });
}
// 對齊 design.md 既有 fallback 語意(source_prefix 參數)的同一條規則:
// library = source_uri 在 '@' 之前的那段(scheme:owner/repo)。這就是本檔+報告裡建議
// kbdb-graph-plugin 在 write 時就该套用的推導規則(見報告,本檔只證明「規則正確、
// 順序對了就能用」,不代表已經改了 kbdb-graph-plugin 的程式碼——那是另一個 repo)。
function deriveLibrary(sourceUri: string): string {
const at = sourceUri.indexOf('@');
return at > 0 ? sourceUri.slice(0, at) : sourceUri;
}
describe('源頭順序 — ensure-slot 必須在 write 之前,事後補救不了已寫的那筆', () => {
it('重現:template 尚無 library slot 時寫入 → library 值被靜默丟棄(不是報錯,是消失)', async () => {
const db = makeSqliteD1();
await seedTripletTemplate(db);
const rec = await createRecord(db, {
template: 'triplet',
values: { subject: 'A', predicate: 'r', object: 'B', source_uri: 'gitea:Leo/kb@a.md', library: 'gitea:Leo/kb' },
owner_id: 'leo',
});
const stored = await getRecord(db, rec.record_id);
// 關鍵斷言:library 完全沒落地,不是空字串、是 undefined(key 都不存在)。
expect(stored!.values.library).toBeUndefined();
expect(stored!.values.source_uri).toBe('gitea:Leo/kb@a.md'); // 其他 slot 正常落地,只有未宣告的 slot 消失
});
it('正解:先 ensureTripletLibrarySlot() 補上 slot,再寫 → library 值正常落地', async () => {
const db = makeSqliteD1();
await seedTripletTemplate(db);
const added = await ensureTripletLibrarySlot(db, 'triplet');
expect(added).toBe(true); // 第一次呼叫確實補了 slot
const rec = await createRecord(db, {
template: 'triplet',
values: { subject: 'A', predicate: 'r', object: 'B', source_uri: 'gitea:Leo/kb@a.md', library: 'gitea:Leo/kb' },
owner_id: 'leo',
});
const stored = await getRecord(db, rec.record_id);
expect(stored!.values.library).toBe('gitea:Leo/kb');
// 冪等:對已有 slot 的 template 再呼叫一次 → false(不重複加),不影響既有資料。
const addedAgain = await ensureTripletLibrarySlot(db, 'triplet');
expect(addedAgain).toBe(false);
});
});
describe('存量補標 — 對缺 library 的舊 triplet 補標,地圖輸出前後對照', () => {
it('補標前:地圖看不到任何庫(triplet 全部因缺 library 而未被地圖聚合);補標後:庫名正確出現', async () => {
const db = makeSqliteD1();
await seedTripletTemplate(db);
// 模擬現況:三筆舊 triplet,寫入時 template 還沒有 library slot(如實重現存量現況),
// 只帶了 source_uri(之後補標要靠它反推 library)。
const r1 = await createRecord(db, { template: 'triplet', values: { subject: 'A', predicate: 'r', object: 'B', source_uri: 'gitea:Leo/kb@a.md', status: 'active' }, owner_id: 'leo' });
const r2 = await createRecord(db, { template: 'triplet', values: { subject: 'C', predicate: 'r', object: 'D', source_uri: 'gitea:Leo/kb@b.md', status: 'active' }, owner_id: 'leo' });
const r3 = await createRecord(db, { template: 'triplet', values: { subject: 'E', predicate: 'r', object: 'F', source_uri: 'github:uncle6-me/notes@c.md', status: 'active' }, owner_id: 'leo' });
// 讀端自動核對重算(M3 收尾機制):這時三筆都缺 library 值。liveTripletCountsByLibrary
// 把「缺值」COALESCE 成 'general' 桶(供 staleness 判斷),但 recomputeLibraryMap 的
// libCond 是 `t.library = 'general'` 精確比對——缺值在底層是 NULL 不是字面 'general'
// 比對不中,實際聚合出 triplet_count:0。**這是本次順手發現的另一個小落差**(live 計數桶
// 與 recompute 精確比對的『general』語意沒對齊,導致這桶每次讀都判 stale、白重算,
// 但至少不會謊報數字)——不在本次任務範圍內(leo 問的是兩代儲存形式的可見性,不是這個
// fallback 桶的效能問題),本測試如實記錄現況,不假裝它是 0。
await ensureFreshLibraryMaps(db, 'leo');
const before = await listLibraryMaps(db, 'leo');
expect(before.length).toBe(1);
expect(before[0].library).toBe('general');
expect(before[0].triplet_count).toBe(0); // 誠實:桶名對得上、數字沒謊報,但也沒把 3 筆算近來(見上註)
// ── 補標(源頭已對:先 ensure slot,才寫值;一次對全部缺值的 record)──
await ensureTripletLibrarySlot(db, 'triplet');
const allTriplets = await searchByTemplate(db, 'triplet', 'leo');
const missing = allTriplets.filter((t) => !t.values.library && t.values.source_uri);
expect(missing.length).toBe(3); // 三筆都缺
for (const t of missing) {
await updateRecord(db, t.record_id, { library: deriveLibrary(t.values.source_uri) });
}
// 補標後:對每個實際出現的 library 值重算一次地圖(read-path 的 ensureFreshLibraryMaps
// 只認「已知庫名」——entries.metadata.library 或 portal_library;剛補標的三元組 library 值
// 尚未被任何一處登記為「已知庫名」,直接 recompute 該庫最直接、也是 caller 實際會做的事)。
const libs = [...new Set(missing.map((t) => deriveLibrary(t.values.source_uri!)))];
for (const lib of libs) {
await recomputeLibraryMap(db, { library: lib, owner_id: 'leo' });
}
const after = await listLibraryMaps(db, 'leo');
const byLib = new Map(after.map((m) => [m.library, m.triplet_count]));
expect(byLib.get('gitea:Leo/kb')).toBe(2); // r1, r2 同庫 —— 這是 leo 真正要看到的東西
expect(byLib.get('github:uncle6-me/notes')).toBe(1); // r3 另一庫
// 'general' 桶(補標前遺留的空殼,見上一段註)仍在,但 triplet_count 仍是 0——
// 三筆全部被正確歸進真正的庫名,沒有一筆被算進 generalno double counting)。
expect(byLib.get('general')).toBe(0);
expect(after.length).toBe(3);
// 交叉核對:三筆的 library 值確實都落地了(不是只有地圖聚合對,底層資料也對)。
const r1Rec = await getRecord(db, r1.record_id);
const r2Rec = await getRecord(db, r2.record_id);
const r3Rec = await getRecord(db, r3.record_id);
expect(r1Rec!.values.library).toBe('gitea:Leo/kb');
expect(r2Rec!.values.library).toBe('gitea:Leo/kb');
expect(r3Rec!.values.library).toBe('github:uncle6-me/notes');
});
});
describe('不會重複做 — 同一批補標跑兩次,第二次不改動任何東西', () => {
it('第二輪掃描:已有 library 值的 record 一筆都不會被 touch', async () => {
const db = makeSqliteD1();
await seedTripletTemplate(db);
await ensureTripletLibrarySlot(db, 'triplet');
const r1 = await createRecord(db, { template: 'triplet', values: { subject: 'A', predicate: 'r', object: 'B', source_uri: 'gitea:Leo/kb@a.md' }, owner_id: 'leo' });
// r2 模擬「還沒補標」的舊資料:故意繞過 values 直接不給 librarycreateRecord 這次雖然
// template 已有 slot,但 caller 沒給值 → 該 slot 完全不會被建立 entry_value,等同「缺」)。
const r2 = await createRecord(db, { template: 'triplet', values: { subject: 'C', predicate: 'r', object: 'D', source_uri: 'gitea:Leo/kb@b.md' }, owner_id: 'leo' });
await updateRecord(db, r1.record_id, { library: deriveLibrary('gitea:Leo/kb@a.md') }); // r1 先補過
async function backfillPass(): Promise<number> {
const all = await searchByTemplate(db, 'triplet', 'leo');
const missing = all.filter((t) => !t.values.library && t.values.source_uri);
for (const t of missing) {
await updateRecord(db, t.record_id, { library: deriveLibrary(t.values.source_uri!) });
}
return missing.length;
}
const firstPass = await backfillPass();
expect(firstPass).toBe(1); // 只有 r2 被補(r1 已有值,跳過)
const secondPass = await backfillPass();
expect(secondPass).toBe(0); // 第二輪:兩筆都已有 library,一筆都不 touch
// 資料仍然正確、沒被第二輪弄壞。
const r1After = await getRecord(db, r1.record_id);
const r2After = await getRecord(db, r2.record_id);
expect(r1After!.values.library).toBe('gitea:Leo/kb');
expect(r2After!.values.library).toBe('gitea:Leo/kb');
});
});
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env node
/**
* build-worker-artifacts.mjs — Arcrun#80tier2 workerTS→ 可部署 JS)的**唯一官方編譯點**。
*
* 背景(Arcrun#80arcrun-rag#39):這個 repo 過去只把 tier1 零件(TinyGo→wasm)的成品
* commit 進 `.component-builds/{name}/component.wasm`tier2cypher-executor / kbdb /
* http_request / code / mcp 這五顆 TS worker)只有原始碼,沒有編好的成品。於是
* arcrun-rag 的安裝器只好自己在**它那邊**跑 esbuild`installer/scripts/build-bundles.mjs`),
* 結果同一份原始碼在不同機器編出不同位元組(見下「已知踩坑」),
* 而且「這顆成品是哪個 commit 編的」只有整包一個 `source` 欄位,答不出單顆的來源。
*
* 本腳本要解的:
* 1. 編譯只發生在這裡(Arcrun 本體),成品放固定位置 `.worker-builds/`commit 進 repo——
* 與 `.component-builds/*.wasm` 同一個既有慣例(self-host 用戶從 repo 直接拿部署來源)。
* 2. 每顆成品自己記得「我是哪個 commit 編出來的」(`source_commit`,答到單顆目錄層級,
* 不是整包一個欄位)。
* 3. 同一個 commit、任何人任何時候編,位元組要相同——見下方兩個已知踩坑與對應解法。
*
* 已知踩坑(2026-08-10 實錄,docs-site/.../changelog.md 1.4.33 段;已回報 arcrun-rag#72):
* ① esbuild 的 bundle 輸出會把「入口路徑」寫進產物內部的檔案邊界註解,而該路徑預設是
* **相對於 esbuild 執行時的 cwd**——雲端容器跑在 `.../arcrun/`、地端跑在
* `.../matrix/arcrun/`,同一份原始碼因此編出不同註解。
* 解法:固定 `absWorkingDir` 為**本腳本自己算出的 repo 根目錄**(不吃外部 cwdenv
* 路徑),entry 一律用「相對 repo 根目錄」的相對路徑餵給 esbuild——不管這個 clone
* 實際被放在磁碟的哪個絕對路徑下,esbuild 內部算出的相對路徑字串都相同。
* ② 各 worker 目錄的 node_modules 若用不同套件管理器(pnpm store vs npm 平鋪)安裝,
* 可能夾帶不同版本的間接依賴(實測 ajv/uri-js 差 1019 行)。
* 解法:本腳本**不自己 npm/pnpm install**——強制要求呼叫者先用「該目錄既有的
* lockfile」(pnpm-lock.yaml 用 `pnpm install --frozen-lockfile`package-lock.json
* 用 `npm ci`)裝好 node_modules,並在建置前檢查 lockfile 是否存在,
* lockfile 是「同一份依賴圖」的機械保證,比信任「兩台機器裝出來一樣」牢靠。
*
* 用法:
* node scripts/build-worker-artifacts.mjs [--check-only]
* --check-only:只驗證每個 worker 的 node_modules 是否已按 lockfile 裝好,不編譯。
*
* 輸出:.worker-builds/<name>/worker.mjs (+ *.wasm)、.worker-builds/manifest.json
*/
import esbuild from 'esbuild';
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, rmSync, readdirSync } from 'node:fs';
import { join, resolve, basename, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { createHash } from 'node:crypto';
// REPO 一律用「本檔自己的位置」推導,不吃 cwd/env——這是踩坑①解法的地基:
// 不管這個 clone 被放在磁碟哪個絕對路徑,REPO 永遠是「這個 repo 的根目錄」,
// 下面所有 esbuild 呼叫都用「相對 REPO」的相對路徑,輸出字串才會與絕對路徑無關。
const REPO = resolve(fileURLToPath(new URL('.', import.meta.url)), '..');
const OUT = join(REPO, '.worker-builds');
const CHECK_ONLY = process.argv.includes('--check-only');
/** 五顆 tier2 worker——與 arcrun-rag `installer/scripts/bundle-components.mjs`
* CORE_COMPONENTS 的 build 參數對齊(那邊的 esbuild 呼叫即將被本腳本的成品取代)。
* name 用 arcrun-rag 那邊的慣例(`arcrun-<kebab>`),方便安裝器直接對號。 */
const WORKERS = [
{ name: 'arcrun-cypher-executor', dir: 'cypher-executor', entry: 'src/index.ts', stripServices: true },
{ name: 'arcrun-kbdb', dir: 'kbdb', entry: 'src/index.ts' },
{ name: 'arcrun-http-request', dir: '.component-builds/http_request', entry: 'src/index.ts' },
{ name: 'arcrun-code', dir: 'registry/components/code', entry: 'index.ts' },
{ name: 'arcrun-mcp', dir: 'mcp', entry: 'src/index.ts' },
];
function sha256(buf) {
return createHash('sha256').update(buf).digest('hex');
}
/** 檢查一個 worker 目錄的 node_modules 是否已按它自己的 lockfile 裝好(踩坑②的閘)。 */
function checkNodeModules(dir) {
const abs = join(REPO, dir);
const hasPnpmLock = existsSync(join(abs, 'pnpm-lock.yaml'));
const hasNpmLock = existsSync(join(abs, 'package-lock.json'));
if (!hasPnpmLock && !hasNpmLock) {
return { ok: false, reason: `${dir} 沒有 pnpm-lock.yaml 也沒有 package-lock.json——依賴版本無法鎖定` };
}
if (!existsSync(join(abs, 'node_modules'))) {
const cmd = hasPnpmLock ? 'pnpm install --frozen-lockfile' : 'npm ci';
return { ok: false, reason: `${dir}/node_modules 不存在——先在該目錄跑:${cmd}` };
}
return { ok: true, via: hasPnpmLock ? 'pnpm (frozen)' : 'npm ci' };
}
/** 極簡 wrangler.toml 讀取(沿用 arcrun-rag build-bundles.mjs 同款邏輯,只抓需要的欄位)。 */
function readToml(tomlPath) {
const t = existsSync(tomlPath) ? readFileSync(tomlPath, 'utf8') : '';
const spec = { kv: [], d1: [], vectorize: [], ai: null, vars: {}, compat_flags: [], compat_date: null };
const compatFlags = t.match(/compatibility_flags\s*=\s*\[([^\]]*)\]/);
if (compatFlags) spec.compat_flags = [...compatFlags[1].matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
const compatDate = t.match(/compatibility_date\s*=\s*["']([^"']+)["']/);
if (compatDate) spec.compat_date = compatDate[1];
for (const m of t.matchAll(/\[\[kv_namespaces\]\][\s\S]*?binding\s*=\s*["']([^"']+)["']/g)) spec.kv.push(m[1]);
for (const m of t.matchAll(/\[\[d1_databases\]\]([\s\S]*?)(?=\n\[|\n*$)/g)) {
const b = m[1].match(/binding\s*=\s*["']([^"']+)["']/);
const n = m[1].match(/database_name\s*=\s*["']([^"']+)["']/);
if (b) spec.d1.push({ binding: b[1], database_name: n ? n[1] : null });
}
for (const line of t.split('\n')) {
if (/^\s*\[\[vectorize\]\]/.test(line)) spec.vectorize.push(true);
}
if (/^\s*\[ai\]/m.test(t)) spec.ai = true;
const varsBlock = t.match(/\[vars\]([\s\S]*?)(?=\n\[|\n*$)/);
if (varsBlock) for (const m of varsBlock[1].matchAll(/^\s*([A-Z0-9_]+)\s*=\s*["']([^"']*)["']/gm)) spec.vars[m[1]] = m[2];
return spec;
}
/** esbuild plugin.wasm import 攤平成同目錄檔名,記下要一起複製的 wasm part。 */
function wasmPlugin(wasmParts) {
return {
name: 'wasm-external',
setup(build) {
build.onResolve({ filter: /\.wasm$/ }, (args) => {
const abs = resolve(args.resolveDir, args.path);
const flat = basename(abs);
if (!wasmParts.find((w) => w.part === flat)) wasmParts.push({ part: flat, abs });
return { path: './' + flat, external: true };
});
},
};
}
/** 每個 worker 自己的 source_commit:答到單顆目錄層級,不是整包一個欄位(Arcrun#80 的核心要求)。 */
function sourceCommitFor(dir) {
try {
const hash = execSync(`git log -1 --format=%H -- ${JSON.stringify(dir)}`, { cwd: REPO, encoding: 'utf8' }).trim();
return hash || null;
} catch {
return null;
}
}
function repoHead() {
try {
const sha = execSync('git rev-parse HEAD', { cwd: REPO, encoding: 'utf8' }).trim();
// .worker-builds 是本腳本自己的輸出目錄——它在「寫出成品之前」永遠是 untracked,
// 拿它判斷「原始碼乾不乾淨」是假陽性(自己把自己判成髒)。排除掉才是真正的
// 「原始碼有沒有未 commit 的變更」。
const dirty = execSync('git status --porcelain -- . ":(exclude).worker-builds"', { cwd: REPO, encoding: 'utf8' }).trim();
return { sha, dirty: !!dirty };
} catch {
return { sha: null, dirty: null };
}
}
async function buildOne(w) {
const dirAbs = join(REPO, w.dir);
const entryAbs = join(dirAbs, w.entry);
if (!existsSync(entryAbs)) throw new Error(`entry 不存在: ${entryAbs}`);
const outDir = join(OUT, w.name);
mkdirSync(outDir, { recursive: true });
// 踩坑①的解法核心:entry 用「相對 REPO」的路徑,absWorkingDir 固定為 REPO——
// esbuild 內部產生的檔案邊界字串因此只依賴這個相對路徑,與這個 clone 實際被
// 放在磁碟的哪個絕對路徑無關。
const entryRel = relative(REPO, entryAbs);
const wasmParts = [];
const result = await esbuild.build({
absWorkingDir: REPO,
entryPoints: [entryRel],
bundle: true,
format: 'esm',
platform: 'browser',
target: 'es2022',
outfile: relative(REPO, join(outDir, 'worker.mjs')),
external: ['cloudflare:*', 'node:*'],
plugins: [wasmPlugin(wasmParts)],
logLevel: 'silent',
metafile: true,
});
const modules = [];
for (const wp of wasmParts) {
if (!existsSync(wp.abs)) throw new Error(`wasm 找不到: ${wp.abs}(該 worker 需先 build/vendored wasm`);
copyFileSync(wp.abs, join(outDir, wp.part));
modules.push({ name: wp.part, type: 'application/wasm', file: `${w.name}/${wp.part}`, sha256: sha256(readFileSync(wp.abs)) });
}
const spec = readToml(join(dirAbs, 'wrangler.toml'));
const jsBuf = readFileSync(join(outDir, 'worker.mjs'));
return {
name: w.name,
source_dir: w.dir,
source_commit: sourceCommitFor(w.dir),
main_module: 'worker.mjs',
main_file: `${w.name}/worker.mjs`,
js_bytes: jsBuf.length,
content_sha256: sha256(jsBuf),
modules,
compat_date: spec.compat_date,
compat_flags: spec.compat_flags,
requires: {
kv: spec.kv,
d1: spec.d1,
vectorize: spec.vectorize.length,
ai: !!spec.ai,
vars: spec.vars,
},
stripped: w.stripServices ? { services: 13 } : undefined,
warnings: result.warnings.map((x) => x.text),
};
}
async function main() {
console.log(`REPO = ${REPO}`);
const precheck = WORKERS.map((w) => ({ w, chk: checkNodeModules(w.dir) }));
const failed = precheck.filter((p) => !p.chk.ok);
if (failed.length) {
console.error('\n❌ 建置中止:以下 worker 尚未按 lockfile 裝好依賴(踩坑②的閘):\n');
for (const f of failed) console.error(` - ${f.chk.reason}`);
console.error('\n這是刻意設計:本腳本不自己跑 install,避免「install 方式不同 → 依賴版本不同 → 位元組不同」。');
process.exit(1);
}
console.log('✔ node_modules 檢查通過:');
for (const p of precheck) console.log(` ${p.w.dir} (${p.chk.via})`);
if (CHECK_ONLY) {
console.log('\n--check-only:只驗證依賴就緒,不編譯。');
return;
}
mkdirSync(OUT, { recursive: true });
for (const w of WORKERS) {
const d = join(OUT, w.name);
if (existsSync(d)) rmSync(d, { recursive: true, force: true });
}
const head = repoHead();
const manifest = {
schema: 1,
built_for: 'arcrun-tier2-worker-artifacts',
generated_at: new Date().toISOString(),
repo_head: head.sha,
repo_dirty: head.dirty,
workers: [],
notes: [],
};
let failCount = 0;
for (const w of WORKERS) {
try {
const entry = await buildOne(w);
manifest.workers.push(entry);
const wasmNote = entry.modules.length ? ` +${entry.modules.length} wasm` : '';
console.log(`${w.name} js=${(entry.js_bytes / 1024).toFixed(0)}KB sha256=${entry.content_sha256.slice(0, 12)} source=${(entry.source_commit || '').slice(0, 8)}${wasmNote}`);
} catch (e) {
manifest.notes.push(`FAILED ${w.name}: ${e.message}`);
console.error(`${w.name}: ${e.message}`);
failCount++;
}
}
writeFileSync(join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2));
console.log(`\nmanifest → ${join(OUT, 'manifest.json')} (${manifest.workers.length}/${WORKERS.length} built)`);
if (failCount > 0 || head.dirty) {
if (head.dirty) console.error('⚠️ 工作區不乾淨(有未 commit 的變更)——這份成品的 repo_head 標記不完全可信,僅供本地驗證用。');
if (failCount > 0) process.exit(1);
}
}
main().catch((e) => { console.error(e); process.exit(1); });
+9
View File
@@ -0,0 +1,9 @@
{
"name": "arcrun-build-tools",
"private": true,
"type": "module",
"description": "Arcrun#80: tier2 worker 編譯工具的獨立依賴——esbuild 版本鎖在這份 lockfile,任何人在任何機器編譯都用同一版 esbuild(重現性的一部分:工具版本也是輸入之一)。",
"devDependencies": {
"esbuild": "0.24.0"
}
}
+265
View File
@@ -0,0 +1,265 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
esbuild:
specifier: 0.24.0
version: 0.24.0
packages:
'@esbuild/aix-ppc64@0.24.0':
resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.24.0':
resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.24.0':
resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.24.0':
resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.24.0':
resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.24.0':
resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.24.0':
resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.24.0':
resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.24.0':
resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.24.0':
resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.24.0':
resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.24.0':
resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.24.0':
resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.24.0':
resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.24.0':
resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.24.0':
resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.24.0':
resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.24.0':
resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.24.0':
resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.24.0':
resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.24.0':
resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.24.0':
resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.24.0':
resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.24.0':
resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
esbuild@0.24.0:
resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==}
engines: {node: '>=18'}
hasBin: true
snapshots:
'@esbuild/aix-ppc64@0.24.0':
optional: true
'@esbuild/android-arm64@0.24.0':
optional: true
'@esbuild/android-arm@0.24.0':
optional: true
'@esbuild/android-x64@0.24.0':
optional: true
'@esbuild/darwin-arm64@0.24.0':
optional: true
'@esbuild/darwin-x64@0.24.0':
optional: true
'@esbuild/freebsd-arm64@0.24.0':
optional: true
'@esbuild/freebsd-x64@0.24.0':
optional: true
'@esbuild/linux-arm64@0.24.0':
optional: true
'@esbuild/linux-arm@0.24.0':
optional: true
'@esbuild/linux-ia32@0.24.0':
optional: true
'@esbuild/linux-loong64@0.24.0':
optional: true
'@esbuild/linux-mips64el@0.24.0':
optional: true
'@esbuild/linux-ppc64@0.24.0':
optional: true
'@esbuild/linux-riscv64@0.24.0':
optional: true
'@esbuild/linux-s390x@0.24.0':
optional: true
'@esbuild/linux-x64@0.24.0':
optional: true
'@esbuild/netbsd-x64@0.24.0':
optional: true
'@esbuild/openbsd-arm64@0.24.0':
optional: true
'@esbuild/openbsd-x64@0.24.0':
optional: true
'@esbuild/sunos-x64@0.24.0':
optional: true
'@esbuild/win32-arm64@0.24.0':
optional: true
'@esbuild/win32-ia32@0.24.0':
optional: true
'@esbuild/win32-x64@0.24.0':
optional: true
esbuild@0.24.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.24.0
'@esbuild/android-arm': 0.24.0
'@esbuild/android-arm64': 0.24.0
'@esbuild/android-x64': 0.24.0
'@esbuild/darwin-arm64': 0.24.0
'@esbuild/darwin-x64': 0.24.0
'@esbuild/freebsd-arm64': 0.24.0
'@esbuild/freebsd-x64': 0.24.0
'@esbuild/linux-arm': 0.24.0
'@esbuild/linux-arm64': 0.24.0
'@esbuild/linux-ia32': 0.24.0
'@esbuild/linux-loong64': 0.24.0
'@esbuild/linux-mips64el': 0.24.0
'@esbuild/linux-ppc64': 0.24.0
'@esbuild/linux-riscv64': 0.24.0
'@esbuild/linux-s390x': 0.24.0
'@esbuild/linux-x64': 0.24.0
'@esbuild/netbsd-x64': 0.24.0
'@esbuild/openbsd-arm64': 0.24.0
'@esbuild/openbsd-x64': 0.24.0
'@esbuild/sunos-x64': 0.24.0
'@esbuild/win32-arm64': 0.24.0
'@esbuild/win32-ia32': 0.24.0
'@esbuild/win32-x64': 0.24.0
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true