Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5d696556e | |||
| 5919c6b90f |
@@ -330,6 +330,15 @@ type LibraryNameSet = Set<string>;
|
|||||||
|
|
||||||
// 這個 owner 底下、依 triplet 自身 'library' slot 分組的即時三元組數(缺 library slot 值的舊
|
// 這個 owner 底下、依 triplet 自身 'library' slot 分組的即時三元組數(缺 library slot 值的舊
|
||||||
// triplet 歸 'general')——與 GET /records/triplet-stats(t142)同一套分組語意,兩處數字對得上。
|
// triplet 歸 'general')——與 GET /records/triplet-stats(t142)同一套分組語意,兩處數字對得上。
|
||||||
|
//
|
||||||
|
// 2026-08-11 修根因(Arcrun#87,動工前量測 comment 第四節):這裡原本完全不過濾 status,
|
||||||
|
// 而 recomputeLibraryMap(上方 withLib)只算 COALESCE(status,'active')='active'。兩邊判準不
|
||||||
|
// 一致,只要有一筆 superseded triplet,這裡的即時計數就會跟重算後的快取對不上,
|
||||||
|
// ensureFreshLibraryMaps 判定 stale,每次讀地圖都觸發重算,每次都新建一筆 library_map
|
||||||
|
// record(superseded 舊的),無止盡寫 D1,且加劇 recomputeLibraryMap 本身非原子 supersede
|
||||||
|
// 的競態(另一個已知病,wiki 08-10 條目)。實測:間隔數秒連讀兩次地圖、中間無任何寫入動作,
|
||||||
|
// updated_at 仍前進。修法:這裡的 status 判準改成與 recomputeLibraryMap 逐字一致,兩邊算出
|
||||||
|
// 的計數才會在資料未變動時相等,stale 判定回歸「真的有資料變動才 stale」。
|
||||||
async function liveTripletCountsByLibrary(
|
async function liveTripletCountsByLibrary(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
tripletTemplateId: string,
|
tripletTemplateId: string,
|
||||||
@@ -337,15 +346,18 @@ async function liveTripletCountsByLibrary(
|
|||||||
): Promise<LibraryCountMap> {
|
): Promise<LibraryCountMap> {
|
||||||
const params: unknown[] = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId];
|
const params: unknown[] = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId];
|
||||||
const res = await db
|
const res = await db
|
||||||
.prepare(
|
.prepare( // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree matrix/arcrun/.worktree-fix-87/(避免打斷另一 session 佔用中的 matrix/arcrun 主 checkout),hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆
|
||||||
`SELECT COALESCE(NULLIF(lib_e.content, ''), 'general') AS library, COUNT(*) AS n
|
`SELECT COALESCE(NULLIF(lib_e.content, ''), 'general') AS library, COUNT(*) AS n
|
||||||
FROM (
|
FROM (
|
||||||
SELECT DISTINCT ev.record_id
|
SELECT ev.record_id AS rid,
|
||||||
|
MAX(CASE WHEN ev.slot_name = 'status' THEN e.content END) AS status
|
||||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||||
WHERE ev.template_id = ?${owner_id ? ' AND e.owner_id = ?' : ''}
|
WHERE ev.template_id = ?${owner_id ? ' AND e.owner_id = ?' : ''}
|
||||||
|
GROUP BY ev.record_id
|
||||||
) AS tr
|
) AS tr
|
||||||
LEFT JOIN entry_values lev ON lev.record_id = tr.record_id AND lev.slot_name = 'library'
|
LEFT JOIN entry_values lev ON lev.record_id = tr.rid AND lev.slot_name = 'library'
|
||||||
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
||||||
|
WHERE COALESCE(tr.status, 'active') = 'active'
|
||||||
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')`,
|
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')`,
|
||||||
)
|
)
|
||||||
.bind(...params)
|
.bind(...params)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
ensureFreshLibraryMaps,
|
ensureFreshLibraryMaps,
|
||||||
LIBRARY_MAP_SLOTS,
|
LIBRARY_MAP_SLOTS,
|
||||||
} from '../src/actions/library-map';
|
} from '../src/actions/library-map';
|
||||||
import { createTemplate, createRecord, getRecord, getTemplate } from '../src/actions/record-crud';
|
import { createTemplate, createRecord, getRecord, getTemplate, searchByTemplate } from '../src/actions/record-crud';
|
||||||
import { createEntry } from '../src/actions/entry-crud';
|
import { createEntry } from '../src/actions/entry-crud';
|
||||||
import type { Bindings } from '../src/types';
|
import type { Bindings } from '../src/types';
|
||||||
|
|
||||||
@@ -292,6 +292,39 @@ describe('M3 收尾 — 即時新鮮度(ensureFreshLibraryMaps,讀端自動
|
|||||||
expect(secondBody.libraries.find((l) => l.library === 'kb')!.triplet_count).toBe(2);
|
expect(secondBody.libraries.find((l) => l.library === 'kb')!.triplet_count).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Arcrun#87 迴歸:superseded triplet 存在時,連讀兩次地圖不會再次觸發重算(不再無止盡寫入)', async () => {
|
||||||
|
// 重現票上的根因:liveTripletCountsByLibrary 原本不濾 status,recomputeLibraryMap 只算
|
||||||
|
// active——只要庫裡混了 superseded triplet,兩邊算出來的數字永遠對不上,
|
||||||
|
// ensureFreshLibraryMaps 就永遠判定 stale,每次讀地圖都重算、每次都新建一筆 record。
|
||||||
|
const db = makeSqliteD1();
|
||||||
|
await seedTripletTemplate(db);
|
||||||
|
await ensureTripletLibrarySlot(db, 'triplet');
|
||||||
|
await seedTriplet(db, { s: 'A', p: '連結至', o: 'B', library: 'kb' }); // active
|
||||||
|
await seedTriplet(db, { s: 'A', p: '連結至', o: 'C', library: 'kb', status: 'superseded' }); // 已淘汰
|
||||||
|
|
||||||
|
const { app, env } = makeApp(db);
|
||||||
|
|
||||||
|
// 第一次讀:資料是新的(從沒 recompute 過),觸發一次重算是正常的。
|
||||||
|
const first = await app.request('/map', {}, env);
|
||||||
|
const firstBody = (await first.json()) as { libraries: { library: string; triplet_count: number }[] };
|
||||||
|
expect(firstBody.libraries.find((l) => l.library === 'kb')!.triplet_count).toBe(1); // 只算 active 那筆
|
||||||
|
|
||||||
|
const countAfterFirst = (await searchByTemplate(db, 'library_map')).length;
|
||||||
|
|
||||||
|
// 第二次讀:中間沒有任何寫入動作。修好之前,這裡會再次判定 stale 並多新建一筆 record。
|
||||||
|
const second = await app.request('/map', {}, env);
|
||||||
|
const secondBody = (await second.json()) as { libraries: { library: string; triplet_count: number }[] };
|
||||||
|
expect(secondBody.libraries.find((l) => l.library === 'kb')!.triplet_count).toBe(1);
|
||||||
|
|
||||||
|
const countAfterSecond = (await searchByTemplate(db, 'library_map')).length;
|
||||||
|
expect(countAfterSecond).toBe(countAfterFirst); // 沒有新增任何 library_map record
|
||||||
|
|
||||||
|
// 第三次也一樣,多讀幾次確認不是巧合。
|
||||||
|
await app.request('/map', {}, env);
|
||||||
|
const countAfterThird = (await searchByTemplate(db, 'library_map')).length;
|
||||||
|
expect(countAfterThird).toBe(countAfterFirst);
|
||||||
|
});
|
||||||
|
|
||||||
it('narrative 不會被自動重算靜默洗掉:先人工帶 narrative,之後的自動重算要保留它', async () => {
|
it('narrative 不會被自動重算靜默洗掉:先人工帶 narrative,之後的自動重算要保留它', async () => {
|
||||||
const db = makeSqliteD1();
|
const db = makeSqliteD1();
|
||||||
await seedTripletTemplate(db);
|
await seedTripletTemplate(db);
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 卡在人類閘前的產物(`Arcrun#89` / `#90` / `#91`)
|
||||||
|
|
||||||
|
> **為什麼這個資料夾存在**:這三樣東西都做完並實測過了,但落地的最後一步是
|
||||||
|
> **終端機裡等人親手打字的互動閘**,AI 打不進去。
|
||||||
|
> 2026-08-11 它們原本只存在於某個 session 的暫存目錄——**那種目錄一關就沒了**。
|
||||||
|
> 先搶進版控,等人有空時再落地。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、兩份 recipe(`#89`/`#90`)
|
||||||
|
|
||||||
|
`recipes/gitea_put_file.yaml` — 把檔案寫回 Gitea repo。**出貨線有 7 站等它。**
|
||||||
|
`recipes/cf_worker_deploy_simple.yaml` — 部署單檔 Worker(classic 格式)。
|
||||||
|
|
||||||
|
**落地指令**(一份跑一次):
|
||||||
|
|
||||||
|
```
|
||||||
|
acr recipe push pending-human-gate/recipes/gitea_put_file.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
跑的時候會停下來要你**親手輸入資源名確認**——那是「把資源變成可被外部呼叫」的暴露同意閘,
|
||||||
|
不是卡住,是設計如此。
|
||||||
|
|
||||||
|
⚠️ **`cf_worker_deploy_simple.yaml` 先別急著推**:`#90` 查出一件結構性的事——
|
||||||
|
recipe 引擎的 body 一律 JSON,而 Cloudflare 上傳 Worker 的 API 要的是原始 JS 或 multipart。
|
||||||
|
⇒ **classic 版只適用於沒有 bindings 的簡單情形**。而實查安裝器那站有 9 把 KV + 一顆 D1,
|
||||||
|
**classic 版幫不上它**。詳見 `Leo/Arcrun#90`。
|
||||||
|
|
||||||
|
### 金鑰(D36)
|
||||||
|
|
||||||
|
兩份 recipe 都只寫名字(`gitea_token`/`cf_api_token`),真身由 credential 中心在執行前回填。
|
||||||
|
對應的 auth-recipe **已經註冊在 leo21c 上**,可以直接查證:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -s https://arcrun-cypher-executor.leo21c.workers.dev/auth-recipes/gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、`hash` 零件(`#91`)
|
||||||
|
|
||||||
|
`hash-component/` — sha256/sha1/md5,hex/base64。出貨線的版本號機制與成品指紋核對都要它。
|
||||||
|
|
||||||
|
**已實測**(tinygo 編出來、wasmtime 真跑,三種演算法都跟系統原生指令**逐位元一致**)。
|
||||||
|
`.wasm` 是 1.3 MB 編譯產物,**沒有進版控**——要驗自己重編:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd pending-human-gate/hash-component && tinygo build -target=wasi -o /tmp/hash.wasm main.go
|
||||||
|
echo '{"algorithm":"sha256","input":"hello"}' | wasmtime /tmp/hash.wasm
|
||||||
|
printf 'hello' | shasum -a 256 # 兩者應該一致
|
||||||
|
```
|
||||||
|
|
||||||
|
**落地要走零件投稿流程**(D27/D28):`docs/component-pr-review-standard.md` 的 checklist
|
||||||
|
+ 人在終端機互動跑 `scripts/component-arm.sh`。
|
||||||
|
🔴 `registry/components/` 底下有機械閘(`component-guard.sh`)擋著 AI 直接寫入——**那是刻意的**,
|
||||||
|
所以這份放在 `pending-human-gate/`,不是放在它最終該去的位置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 落地之後
|
||||||
|
|
||||||
|
三樣都上去之後,`Arcrun#89`/`#91` 才能從 **◐ 半通** 變 **✅**——
|
||||||
|
而判準是**貼一次真實的執行輸出**(recipe 對某個測試檔案回 2xx、零件在真端點上跑出正確雜湊),
|
||||||
|
不是「推上去了」。
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
canonical_id: "hash"
|
||||||
|
display_name: "計算雜湊"
|
||||||
|
category: "logic"
|
||||||
|
version: "v1"
|
||||||
|
wasi_target: "preview1"
|
||||||
|
stability: "floating"
|
||||||
|
runtime_compat:
|
||||||
|
- "cf-workers"
|
||||||
|
- "workerd"
|
||||||
|
- "wazero"
|
||||||
|
constraints:
|
||||||
|
max_size_kb: 2048
|
||||||
|
max_cold_start_ms: 50
|
||||||
|
no_network_syscall: true
|
||||||
|
no_filesystem_syscall: true
|
||||||
|
io_model: "stdin_stdout_json"
|
||||||
|
input_schema:
|
||||||
|
type: object
|
||||||
|
required: [input]
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
type: string
|
||||||
|
enum: [sha256, sha1, md5]
|
||||||
|
description: 雜湊演算法,預設 sha256
|
||||||
|
input:
|
||||||
|
type: string
|
||||||
|
description: 要算雜湊的內容
|
||||||
|
encoding:
|
||||||
|
type: string
|
||||||
|
enum: [hex, base64]
|
||||||
|
description: 輸出編碼,預設 hex
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
result:
|
||||||
|
type: string
|
||||||
|
algorithm:
|
||||||
|
type: string
|
||||||
|
encoding:
|
||||||
|
type: string
|
||||||
|
gherkin_tests:
|
||||||
|
- scenario: "sha256 hex(預設)"
|
||||||
|
given: '{"algorithm":"sha256","input":"hello"}'
|
||||||
|
then_contains: '"result":"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"'
|
||||||
|
- scenario: "sha1"
|
||||||
|
given: '{"algorithm":"sha1","input":"hello"}'
|
||||||
|
then_contains: '"result":"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"'
|
||||||
|
- scenario: "md5"
|
||||||
|
given: '{"algorithm":"md5","input":"hello"}'
|
||||||
|
then_contains: '"result":"5d41402abc4b2a76b9719d911017c592"'
|
||||||
|
- scenario: "base64 編碼"
|
||||||
|
given: '{"algorithm":"sha256","input":"hello","encoding":"base64"}'
|
||||||
|
then_contains: '"result":"LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="'
|
||||||
|
- scenario: "預設 algorithm=sha256"
|
||||||
|
given: '{"input":"hello"}'
|
||||||
|
then_contains: '"algorithm":"sha256"'
|
||||||
|
- scenario: "不支援的 algorithm"
|
||||||
|
given: '{"algorithm":"crc32","input":"hello"}'
|
||||||
|
then_contains: '{"success":false'
|
||||||
|
tags: [builtin, logic, hash, checksum, versioning]
|
||||||
|
description: >-
|
||||||
|
計算內容雜湊(sha256/sha1/md5,輸出 hex 或 base64)。純計算,無網路/檔案 syscall。
|
||||||
|
用途:出貨線版本號機制(Leo/Arcrun#91)——內容一變雜湊必變,是「改了東西版本沒動」在結構上
|
||||||
|
不可能發生的機制來源;build 站核對官方成品指紋也用它。
|
||||||
|
config_example: |
|
||||||
|
compute_hash: # 節點名稱(可自訂)
|
||||||
|
algorithm: "sha256" # 演算法(選填,預設 sha256),可選值:sha256/sha1/md5
|
||||||
|
input: "{{ctx.bundle_content}}" # 要算雜湊的內容(必填)
|
||||||
|
encoding: "hex" # 輸出編碼(選填,預設 hex),可選值:hex/base64
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// hash — 計算內容雜湊(純計算,無網路/檔案 syscall)
|
||||||
|
// 支援: sha256, sha1, md5;輸出編碼: hex(預設), base64
|
||||||
|
// 用途:出貨線版本號機制(Leo/Arcrun#91)——內容一變雜湊必變,
|
||||||
|
// 是「改了東西版本沒動」在結構上不可能發生的機制來源。
|
||||||
|
//
|
||||||
|
//go:build tinygo
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Input struct {
|
||||||
|
Algorithm string `json:"algorithm"` // sha256(預設)| sha1 | md5
|
||||||
|
Input string `json:"input"`
|
||||||
|
Encoding string `json:"encoding"` // hex(預設)| base64
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
raw, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
writeError("failed to read stdin: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var in Input
|
||||||
|
if err := json.Unmarshal(raw, &in); err != nil {
|
||||||
|
writeError("invalid input JSON: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm := in.Algorithm
|
||||||
|
if algorithm == "" {
|
||||||
|
algorithm = "sha256"
|
||||||
|
}
|
||||||
|
encoding := in.Encoding
|
||||||
|
if encoding == "" {
|
||||||
|
encoding = "hex"
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum []byte
|
||||||
|
switch algorithm {
|
||||||
|
case "sha256":
|
||||||
|
h := sha256.Sum256([]byte(in.Input))
|
||||||
|
sum = h[:]
|
||||||
|
case "sha1":
|
||||||
|
h := sha1.Sum([]byte(in.Input))
|
||||||
|
sum = h[:]
|
||||||
|
case "md5":
|
||||||
|
h := md5.Sum([]byte(in.Input))
|
||||||
|
sum = h[:]
|
||||||
|
default:
|
||||||
|
writeError("不支援的 algorithm: " + algorithm + "(支援 sha256/sha1/md5)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result string
|
||||||
|
switch encoding {
|
||||||
|
case "hex":
|
||||||
|
result = hex.EncodeToString(sum)
|
||||||
|
case "base64":
|
||||||
|
result = base64.StdEncoding.EncodeToString(sum)
|
||||||
|
default:
|
||||||
|
writeError("不支援的 encoding: " + encoding + "(支援 hex/base64)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"result": result,
|
||||||
|
"algorithm": algorithm,
|
||||||
|
"encoding": encoding,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
os.Stdout.Write(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(msg string) {
|
||||||
|
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
|
||||||
|
os.Stdout.Write(out)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name = "arcrun-hash"
|
||||||
|
main = "src/index.ts"
|
||||||
|
compatibility_date = "2025-02-19"
|
||||||
|
workers_dev = true
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
COMPONENT_ID = "hash"
|
||||||
|
|
||||||
|
[[routes]]
|
||||||
|
pattern = "hash.arcrun.dev/*"
|
||||||
|
zone_name = "arcrun.dev"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
canonical_id: cf_worker_deploy_simple
|
||||||
|
display_name: Cloudflare Worker Deploy (single-file, classic format)
|
||||||
|
description: >-
|
||||||
|
PUT /accounts/{account_id}/workers/scripts/{script_name} 部署單檔 Worker(CF 「classic Service
|
||||||
|
Worker」格式,非 ES module)。_path 帶 /{account_id}/workers/scripts/{script_name}。
|
||||||
|
auth: cloudflare_workers static_key(Bearer token)。
|
||||||
|
⚠️ 已知限制(誠實記錄,非隱藏債):這個 recipe 走 arcrun 的「recipe body 一律 JSON.stringify」
|
||||||
|
引擎行為(cypher-executor/src/lib/component-loader.ts makeRecipeRunner),CF 這支 API 卻要求
|
||||||
|
body 是「原始 JS 原始碼」或(現代 ES module + bindings 情境)multipart/form-data——兩者都不是
|
||||||
|
JSON。純 recipe 模型在這支 API 上天生對不上,這不是可以在 recipe schema 裡修的事。
|
||||||
|
正解=07-thin-shell §3.5 自力救濟階梯「第三方 API 缺能力→ workflow/code-node 補丁」:
|
||||||
|
用 http_request 零件直接打(body 走它的原生 string 模式,不透過本 recipe wrapper),
|
||||||
|
header 用 {{credential.cf_api_token}} 直接內插(D36 credential 模板,不必經過 recipe/auth_service
|
||||||
|
間接層);若目標 Worker 需要 bindings/compatibility_flags(現代 ES module 格式常態),
|
||||||
|
上游加一個 code 節點組出 multipart/form-data body(純資料編碼,非業務邏輯,合法局部整形)。
|
||||||
|
本 recipe 保留給「目標帳號仍接受 classic 格式」的簡單場景;不保證覆蓋所有部署情境。
|
||||||
|
endpoint: https://api.cloudflare.com/client/v4/accounts{{_path}}
|
||||||
|
method: PUT
|
||||||
|
auth_service: cloudflare_workers
|
||||||
|
headers:
|
||||||
|
Content-Type: application/javascript
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
canonical_id: gitea_put_file
|
||||||
|
display_name: Gitea Put File (Create/Update)
|
||||||
|
description: >-
|
||||||
|
Gitea PUT /repos/{owner}/{repo}/contents/{filepath} 建立或更新檔案並產生 commit。
|
||||||
|
_path 帶完整路徑(例 /Leo/arcrun-rag-bundles/contents/manifest.json,filepath 各段需 URL-encode)。
|
||||||
|
body 帶 {message, content(base64), branch, sha(更新既有檔案時必填,取自前一次 GET 的 content.sha;
|
||||||
|
新建檔案時不帶)}。auth: gitea static_key,header Authorization: token <TOKEN>(D36:定義只留
|
||||||
|
{{credential.*}} 名字,真身由 credential 中心於執行前回填,非本 recipe 職責)。
|
||||||
|
endpoint: https://git.uncle6.me/api/v1/repos{{_path}}
|
||||||
|
method: PUT
|
||||||
|
auth_service: gitea
|
||||||
@@ -44,3 +44,34 @@ leo 否決②——「**藏書地圖就是 arcrun 的最重要功能,讓 AI
|
|||||||
不報錯。`mcp/tests/unit/tools/kbdb-map.test.ts` 新增 1 案釘住舊謊言不再出現(18/18 全綠)。
|
不報錯。`mcp/tests/unit/tools/kbdb-map.test.ts` 新增 1 案釘住舊謊言不再出現(18/18 全綠)。
|
||||||
tsc 兩包乾淨。實測:`yuga3bse` 租戶(從未 backfill 過、真實 triplet 資料橫跨 5 個庫)改前
|
tsc 兩包乾淨。實測:`yuga3bse` 租戶(從未 backfill 過、真實 triplet 資料橫跨 5 個庫)改前
|
||||||
`kbdb_get_map` 回 `{libraries:[],count:0}`——改動待部署後需重新實測驗證非空。
|
`kbdb_get_map` 回 `{libraries:[],count:0}`——改動待部署後需重新實測驗證非空。
|
||||||
|
|
||||||
|
### M3 止血(2026-08-11,Arcrun#87,總管交辦「動工前的量測」comment 第四節)
|
||||||
|
|
||||||
|
**08-08 那次改法本身留了一個判準缺口,這次補上**:`ensureFreshLibraryMaps` 比對
|
||||||
|
「即時三元組數」(`liveTripletCountsByLibrary`)與「快取的地圖數」(`recomputeLibraryMap`
|
||||||
|
算出來寫進去的),但兩邊的 status 過濾不一致——`recomputeLibraryMap` 只算
|
||||||
|
`COALESCE(status,'active')='active'`,`liveTripletCountsByLibrary` 完全不濾 status。
|
||||||
|
只要一個庫裡混了任何一筆 superseded/deprecated triplet,兩邊數字就永遠對不上,
|
||||||
|
`ensureFreshLibraryMaps` 就永遠判定 stale ⇒ **每次讀地圖都觸發重算,每次都新建一筆
|
||||||
|
library_map record(superseded 舊的),無止盡寫 D1**——且加劇 `recomputeLibraryMap`
|
||||||
|
本身非原子 supersede 的既有競態(更高重算頻率 = 更高並發重算機率),是 `kb` 庫
|
||||||
|
全部 44 筆被標 superseded、`notes` 庫兩筆同時 active(`arcrun-rag#50`)這兩個症狀的
|
||||||
|
共同根因之一。
|
||||||
|
|
||||||
|
**修法**:`liveTripletCountsByLibrary`(`kbdb/src/actions/library-map.ts`)的 SQL 改成
|
||||||
|
先 pivot 出每筆 triplet record 的 status,再套用與 `recomputeLibraryMap` 逐字一致的
|
||||||
|
`COALESCE(status,'active')='active'` 過濾,兩邊判準對齊後,資料未變動時兩個計數必然相等,
|
||||||
|
stale 判定回歸「真的有資料變動才 stale」。
|
||||||
|
|
||||||
|
**驗證**:新增迴歸案「Arcrun#87 迴歸:superseded triplet 存在時,連讀兩次地圖不會再次
|
||||||
|
觸發重算」(`kbdb/tests/library-map.test.ts`,19/19 全綠);反向驗證過——把同一顆測試跑在
|
||||||
|
修前的舊 SQL 上會失敗(`library_map` record 數 2 vs 期望 1),證明測試真的釘住這個 bug、
|
||||||
|
不是空氣測試。另外用 leo21c MCP 連線(`bfezv28v`)連讀兩次 `kbdb_get_map()`(無中間寫入)
|
||||||
|
獨立重現修前症狀:`general` 庫 `updated_at` 從 `1786457080` 前進到 `1786457114`。
|
||||||
|
|
||||||
|
**尚待**:改動只在分支 `fix/library-map-recompute-loop-87-v3`(未 push、未部署 leo21c);
|
||||||
|
既有 100 筆 library_map 殘骸(`kb` 44 筆 superseded/`general` 41/`notes` 2)未清——
|
||||||
|
清除需要一個目前不存在的 DELETE 通道(cypher-executor 的 `/kbdb/records/:id` proxy 只有
|
||||||
|
GET/POST/PATCH,無 DELETE;kbdb base 自己雖有 `DELETE /records/:recordId` 但走 leo21c
|
||||||
|
需要 `KBDB_INTERNAL_TOKEN`,非 CC 可持有的機密)——待總管部署本修法+視情況補一支
|
||||||
|
DELETE proxy 後再清。
|
||||||
|
|||||||
Reference in New Issue
Block a user