Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbd375f7ac | |||
| 613071f41d | |||
| 27f7448914 | |||
| 98b221b435 | |||
| 2dd80cd703 |
@@ -60,7 +60,7 @@
|
|||||||
},
|
},
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "節點層附帶資訊。【向量化分工(leo 2026-06-26,ingest#1 升格成契約)】ingest 在此【打標】哪些 token 要向量化 + embed 什麼;base/KBDB embed 模組【讀標執行】實際 embedding;ingest 自己不算向量。兩類節點(實體詞條 / wikilink 卡)都進 nodes[],謂詞向量見 triplets[].predicate_vector。",
|
"description": "節點層附帶資訊(選填)。entity_type 與 gloss 是【節點】屬性,不是【邊】屬性 → 放這裡,不放 triplets。graph 用 gloss 去 embed(每節點一句,不是裸詞)、用 entity_type 去 typing。",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
@@ -69,26 +69,11 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"description": "節點名(須對應某 triplet 的 subject/object 原字面)。實體詞條=正規名;wikilink 卡=卡標題。"
|
"description": "節點名(須對應某 triplet 的 subject 或 object 原字面)。"
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "去重鍵。wikilink 卡用【檔名】→ 一卡一 node,被多條邊指到也只 embed 一次,不以出現次數重複。實體詞條用正規名。選填(無則以 name 去重)。"
|
|
||||||
},
|
},
|
||||||
"gloss": {
|
"gloss": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "一句話描述。base embed 對【名 + gloss 一起】embedding(實體同義詞字面差太遠,靠描述拉近)。選填(建議 deep tier 產)。"
|
"description": "一句話描述,供 embedding。例如 'Graph RAG — 用關係遍歷檢索、保住異見的 RAG 變體'。選填(建議 deep tier 產出)。"
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "type": "string" },
|
|
||||||
"description": "同義詞(如『黃仁勳』/『Jensen Huang』)。base 歸一(collapse)成同一 node。選填。"
|
|
||||||
},
|
|
||||||
"embed": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "【向量化打標】此節點要不要進向量庫。true=base 讀標去 embed(名+gloss);false=base 看到就不理(如結構符號/散文不該進 nodes[],真進了標 false)。預設 true(實體詞條與 wikilink 卡都要)。",
|
|
||||||
"$comment": "ingest 打標,base 讀標執行。embed 動作歸 base embed 模組,ingest 不算向量。"
|
|
||||||
},
|
},
|
||||||
"entity_type": {
|
"entity_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -101,7 +86,7 @@
|
|||||||
"triplets": {
|
"triplets": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"description": "邊(關係)。ingest 只產原始 (s,p,o) + confidence + 謂詞向量打標。端點(s/o)以字面 match nodes[].name。",
|
"description": "邊(關係)。ingest 只產原始 (s,p,o) + confidence。",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["subject", "predicate", "object"],
|
"required": ["subject", "predicate", "object"],
|
||||||
@@ -110,16 +95,10 @@
|
|||||||
"subject": { "type": "string", "minLength": 1, "description": "主詞(實體名,須與 nodes[].name 對得上若有提供)" },
|
"subject": { "type": "string", "minLength": 1, "description": "主詞(實體名,須與 nodes[].name 對得上若有提供)" },
|
||||||
"predicate": { "type": "string", "minLength": 1, "description": "謂詞(關係)" },
|
"predicate": { "type": "string", "minLength": 1, "description": "謂詞(關係)" },
|
||||||
"object": { "type": "string", "minLength": 1, "description": "受詞(目標實體或值)" },
|
"object": { "type": "string", "minLength": 1, "description": "受詞(目標實體或值)" },
|
||||||
"predicate_embed": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "【謂詞向量化打標】謂詞要不要 embed。base 讀標 → embed【謂詞裸詞,無描述】(謂詞同義詞字面本就近,如『參考』/『參照』,裸詞 embed 即自動聚類),存 edge 的 predicate_vector。為支援『關係過濾』查詢(查『參考』不漏『參照』)→ 預設 true。embed 動作歸 base,ingest 只打標。",
|
|
||||||
"$comment": "ingest 打標,base 讀標執行 embed。"
|
|
||||||
},
|
|
||||||
"confidence":{ "type": "number", "minimum": 0, "maximum": 1, "default": 1.0, "description": "萃取可信度。淺萃可附自評;graph 不據此過濾,只記錄。" }
|
"confidence":{ "type": "number", "minimum": 0, "maximum": 1, "default": 1.0, "description": "萃取可信度。淺萃可附自評;graph 不據此過濾,只記錄。" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$comment": "禁止欄位(graph 領域,ingest 絕不可送): id(節點去重鍵的 id 例外,那是 ingest 提供的去重鍵非 record id) / clusters / bridge_score / created_at / updated_at / 以及 triplet 上的 subject_entity_type|object_entity_type(類型只走 nodes[])。【向量化分工】ingest 打標(embed/predicate_embed + 帶 gloss/aliases),base/KBDB embed 模組讀標執行 embedding,ingest 不算向量。結構符號(>>/←)與給人讀的散文(## 摘要)不進 envelope。"
|
"$comment": "禁止欄位(graph 領域,ingest 絕不可送): id / clusters / bridge_score / created_at / updated_at / 以及 triplet 上的 subject_entity_type|object_entity_type(類型只走 nodes[])。送了即違反 ingest=純餵食器的邊界,graph 應拒收或忽略。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
// node 層打標落地 — 把 envelope nodes[] 的向量化打標(embed/gloss/aliases)存進 entity slot。
|
|
||||||
// 向量化分工(ingest#1 升格,2026-06-26):ingest 打標、base/KBDB embed 模組讀標執行;graph 不算向量。
|
|
||||||
// 鐵律:走 base API(API-as-Wall)、零 SQL。
|
|
||||||
|
|
||||||
import type { KbdbClient } from '../lib/kbdb-client';
|
|
||||||
import { TPL_ENTITY, ensurePluginTemplates } from '../lib/templates';
|
|
||||||
|
|
||||||
export type IngestNode = {
|
|
||||||
name: string;
|
|
||||||
id?: string;
|
|
||||||
aliases?: string[];
|
|
||||||
gloss?: string;
|
|
||||||
embed?: boolean;
|
|
||||||
entity_type?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 把 node 層打標存進 entity record,供 base embed 模組讀標執行 embedding。
|
|
||||||
* 去重:以 id(無則 name)為鍵,同鍵在這批內只存一筆——wikilink 卡被多條邊指到仍是一個 node。
|
|
||||||
* graph 不做 embedding,只負責透傳/落地打標。
|
|
||||||
*/
|
|
||||||
export async function persistNodes(
|
|
||||||
client: KbdbClient,
|
|
||||||
nodes: IngestNode[],
|
|
||||||
owner_id?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
if (!nodes || nodes.length === 0) return;
|
|
||||||
await ensurePluginTemplates(client);
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const n of nodes) {
|
|
||||||
const key = (n.id ?? n.name).toLowerCase().trim();
|
|
||||||
if (seen.has(key)) continue; // 同卡多邊指到 → 只存一次
|
|
||||||
seen.add(key);
|
|
||||||
await client.createRecord(
|
|
||||||
TPL_ENTITY,
|
|
||||||
{
|
|
||||||
canonical: n.name,
|
|
||||||
node_id: n.id ?? '',
|
|
||||||
aliases_json: JSON.stringify(n.aliases ?? []),
|
|
||||||
entity_type: n.entity_type ?? '',
|
|
||||||
gloss: n.gloss ?? '',
|
|
||||||
// contract 預設 true;只在明確 false 時存標(base 看 'false' 跳過 embed)。
|
|
||||||
embed: n.embed === false ? 'false' : 'true',
|
|
||||||
owner: owner_id ?? '',
|
|
||||||
},
|
|
||||||
owner_id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ export type CreateTripletData = {
|
|||||||
source_uri?: string;
|
source_uri?: string;
|
||||||
content_hash?: string;
|
content_hash?: string;
|
||||||
source_anchor?: string;
|
source_anchor?: string;
|
||||||
predicate_embed?: boolean; // 謂詞向量化打標(ingest 打標、base 讀標執行);graph 不算向量
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 建立三元組 → POST /records(template=triplet)。 */
|
/** 建立三元組 → POST /records(template=triplet)。 */
|
||||||
@@ -49,7 +48,6 @@ export async function createTriplet(
|
|||||||
if (data.source_uri) values.source_uri = data.source_uri;
|
if (data.source_uri) values.source_uri = data.source_uri;
|
||||||
if (data.content_hash) values.content_hash = data.content_hash;
|
if (data.content_hash) values.content_hash = data.content_hash;
|
||||||
if (data.source_anchor) values.source_anchor = data.source_anchor;
|
if (data.source_anchor) values.source_anchor = data.source_anchor;
|
||||||
if (data.predicate_embed === false) values.predicate_embed = 'false'; // 謂詞向量化打標透傳,base 讀標執行
|
|
||||||
|
|
||||||
const id = await client.createRecord(TPL_TRIPLET, values, data.owner_id);
|
const id = await client.createRecord(TPL_TRIPLET, values, data.owner_id);
|
||||||
return { id, subject: data.subject, predicate: data.predicate, object: data.object };
|
return { id, subject: data.subject, predicate: data.predicate, object: data.object };
|
||||||
|
|||||||
@@ -6,17 +6,11 @@ import { z } from '@hono/zod-openapi';
|
|||||||
import type { KbdbClient } from '../lib/kbdb-client';
|
import type { KbdbClient } from '../lib/kbdb-client';
|
||||||
import { TPL_TRIPLET, ensurePluginTemplates, recordToTriplet } from '../lib/templates';
|
import { TPL_TRIPLET, ensurePluginTemplates, recordToTriplet } from '../lib/templates';
|
||||||
import { createTriplet } from './triplet-crud';
|
import { createTriplet } from './triplet-crud';
|
||||||
import { persistNodes } from './node-persist';
|
|
||||||
|
|
||||||
// Zod 鏡射契約:strict() = additionalProperties:false → 禁送欄位 422(route 把 ZodError 轉 422)。
|
// Zod 鏡射契約:strict() = additionalProperties:false → 禁送欄位 422(route 把 ZodError 轉 422)。
|
||||||
// 向量化打標欄位(ingest#1 升格,2026-06-26):ingest 打標、base/KBDB embed 模組讀標執行;graph 自己不算向量。
|
|
||||||
// strict() 仍保留 → 真正的 graph 領域禁送欄位(bridge_score / clusters / 邊上 entity_type)照樣 422。
|
|
||||||
const NodeSchema = z.object({
|
const NodeSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
id: z.string().optional(), // 去重鍵(wikilink 卡用檔名 → 一卡一 node,多邊指到不重建)
|
|
||||||
aliases: z.array(z.string()).optional(), // 同義詞,base collapse 成同一 node
|
|
||||||
gloss: z.string().optional(),
|
gloss: z.string().optional(),
|
||||||
embed: z.boolean().optional(), // 向量化打標,base 讀標執行(預設 true)
|
|
||||||
entity_type: z.enum(['person', 'event', 'product', 'market', 'org']).optional(),
|
entity_type: z.enum(['person', 'event', 'product', 'market', 'org']).optional(),
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
@@ -24,7 +18,6 @@ const EdgeSchema = z.object({
|
|||||||
subject: z.string().min(1),
|
subject: z.string().min(1),
|
||||||
predicate: z.string().min(1),
|
predicate: z.string().min(1),
|
||||||
object: z.string().min(1),
|
object: z.string().min(1),
|
||||||
predicate_embed: z.boolean().optional(), // 謂詞向量化打標,base 讀標執行(預設 true)
|
|
||||||
confidence: z.number().min(0).max(1).optional(),
|
confidence: z.number().min(0).max(1).optional(),
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
@@ -66,14 +59,13 @@ export async function ingestEnvelope(
|
|||||||
return { skipped: true, ingested: 0, deprecated: 0 };
|
return { skipped: true, ingested: 0, deprecated: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) 先 append 新批 active(透傳 predicate_embed 打標,供 base embed 模組讀標執行)。
|
// 1) 先 append 新批 active。
|
||||||
for (const e of env.triplets) {
|
for (const e of env.triplets) {
|
||||||
await createTriplet(client, {
|
await createTriplet(client, {
|
||||||
subject: e.subject,
|
subject: e.subject,
|
||||||
predicate: e.predicate,
|
predicate: e.predicate,
|
||||||
object: e.object,
|
object: e.object,
|
||||||
confidence: e.confidence,
|
confidence: e.confidence,
|
||||||
predicate_embed: e.predicate_embed,
|
|
||||||
source_block_id: env.source.block_id,
|
source_block_id: env.source.block_id,
|
||||||
source_uri: env.source.uri,
|
source_uri: env.source.uri,
|
||||||
content_hash: env.source.content_hash,
|
content_hash: env.source.content_hash,
|
||||||
@@ -82,10 +74,6 @@ export async function ingestEnvelope(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1b) 落地 node 層打標(embed / gloss / aliases),供 base embed 模組讀標執行 embedding。
|
|
||||||
// graph 自己不算向量(鐵律一致)。id 作去重鍵:同一卡(同 id/檔名)只存一筆 entity,不以邊數重複。
|
|
||||||
await persistNodes(client, env.nodes ?? [], owner_id);
|
|
||||||
|
|
||||||
// 2) 後翻舊批 status=deprecated(指向本批 source_uri;append 在前 → 無空窗)。
|
// 2) 後翻舊批 status=deprecated(指向本批 source_uri;append 在前 → 無空窗)。
|
||||||
for (const old of priorActive) {
|
for (const old of priorActive) {
|
||||||
await client.updateRecord(old.id, { status: 'deprecated', superseded_by: env.source.content_hash });
|
await client.updateRecord(old.id, { status: 'deprecated', superseded_by: env.source.content_hash });
|
||||||
|
|||||||
@@ -18,13 +18,9 @@ export const TRIPLET_SLOTS = [
|
|||||||
// source_uri+content_hash 承載 ingest idempotency(按 source_uri 分組 deprecate)。
|
// source_uri+content_hash 承載 ingest idempotency(按 source_uri 分組 deprecate)。
|
||||||
// source_anchor 供 get_source 精準回跳原文(T3.7)。
|
// source_anchor 供 get_source 精準回跳原文(T3.7)。
|
||||||
'status', 'superseded_by', 'source_uri', 'content_hash', 'source_anchor',
|
'status', 'superseded_by', 'source_uri', 'content_hash', 'source_anchor',
|
||||||
// 謂詞向量化打標(ingest#1 升格,2026-06-26):ingest 打標、base embed 模組讀標執行;graph 不算向量。
|
|
||||||
'predicate_embed',
|
|
||||||
];
|
];
|
||||||
// gloss(T3.2b):一句話描述,供「詞+gloss」語義 normalize 的 embedding 對象。
|
// gloss(T3.2b):一句話描述,供「詞+gloss」語義 normalize 的 embedding 對象。
|
||||||
// embed(ingest#1 升格,2026-06-26):向量化打標,base embed 模組讀標執行;graph 不算向量。
|
export const ENTITY_SLOTS = ['canonical', 'aliases_json', 'entity_type', 'owner', 'gloss'];
|
||||||
// node_id(去重鍵):wikilink 卡用檔名,一卡一 node、多邊指到只 embed 一次(base 讀此鍵歸一)。
|
|
||||||
export const ENTITY_SLOTS = ['canonical', 'aliases_json', 'entity_type', 'owner', 'gloss', 'embed', 'node_id'];
|
|
||||||
export const ENTITY_PENDING_SLOTS = [
|
export const ENTITY_PENDING_SLOTS = [
|
||||||
'raw_name', 'candidate_entity_id', 'candidate_canonical', 'similarity',
|
'raw_name', 'candidate_entity_id', 'candidate_canonical', 'similarity',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -39,21 +39,6 @@
|
|||||||
INDEX 是**所有檢索角度的入口**,不只標籤。原文是唯讀 SSoT,wiki 是改寫過的記憶。
|
INDEX 是**所有檢索角度的入口**,不只標籤。原文是唯讀 SSoT,wiki 是改寫過的記憶。
|
||||||
新增角度只要在這裡加一節(如「決策角度」「原則角度」),指向對應 cards 或 push 檔——**不必新增實體特殊檔**。
|
新增角度只要在這裡加一節(如「決策角度」「原則角度」),指向對應 cards 或 push 檔——**不必新增實體特殊檔**。
|
||||||
|
|
||||||
### 快速導航(本專案速覽)
|
|
||||||
|
|
||||||
**這個專案是什麼**:KBDB-graph —— KBDB 的 graph 插件(triplet 採集 + graph 查詢),類比 Apache AGE 之於 Postgres。已抽成獨立 public repo `uncle6me-web/kbdb-graph-plugin`(leo 產權)。基本盤(block CRUD,D1 三表)在 `arcrun/kbdb`,不在這。
|
|
||||||
|
|
||||||
**動工前必讀**:
|
|
||||||
- `docs/HANDOFF-kbdb-plugin.md` —— 本目錄專屬交棒。
|
|
||||||
- 上游約束見 `CLAUDE.md` 最頂 + `github.com/uncle6me-web/InkStoneCo`。
|
|
||||||
|
|
||||||
**文件去哪找**:
|
|
||||||
- SDD(design+tasks)→ `docs/3-specs/`(現有:kbdb-graph-extraction、blocks-edit-api、plugin-install、arcrun-key-auth)
|
|
||||||
- 歷史記錄 / bug 復盤 → `docs/5-records/`(PATCH 403 bug、upsert feature request)
|
|
||||||
- 分類規則全表 → `docs/README.md`
|
|
||||||
|
|
||||||
**絕對限制**:本目錄只做 graph 插件 / **API-as-Wall(插件絕不碰表、零 SQL、零 migration、零建表)** / 部署繞開 GitHub、禁跨 repo Actions / 樂高法(actions < 100 行)。
|
|
||||||
|
|
||||||
### 標籤角度(按 `TAXONOMY.md` 的軸聚類,指向桶子索引)
|
### 標籤角度(按 `TAXONOMY.md` 的軸聚類,指向桶子索引)
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
@@ -64,17 +49,11 @@ INDEX 是**所有檢索角度的入口**,不只標籤。原文是唯讀 SSoT
|
|||||||
- [[ai/00-INDEX]] — AI 協作(M 卡)
|
- [[ai/00-INDEX]] — AI 協作(M 卡)
|
||||||
```
|
```
|
||||||
|
|
||||||
(尚未建 cards,現有決策見下「決策角度」。)
|
### 決策角度(取代舊 decisions-summary.md 的視圖)
|
||||||
|
|
||||||
### 決策角度(取代舊 decisions-summary.md 的視圖;完整脈絡見 `decisions-summary.md` + `docs/2-architecture/decisions/`)
|
```markdown
|
||||||
|
- [[某決策卡]] — 一句話結論(YYYY-MM-DD)
|
||||||
- **KBDB-graph 定位**(2026-06-13)— 本 repo = KBDB 的 graph 插件,獨立成 repo,類比 AGE 之於 Postgres。
|
```
|
||||||
- **🔒 KBDB 鐵律 + API-as-Wall**(2026-06-14,最高原則)— 插件絕不碰表、零 SQL、零 migration,讀寫全走基本盤 HTTP API;新類型=建 template+填 slot,永不建表。
|
|
||||||
- **獨立 repo 名**(2026-06-14)— public `uncle6me-web/kbdb-graph-plugin`,無 Actions。
|
|
||||||
- **掛載介面 = 基本盤 API(非共用 D1)**(2026-06-14,推翻原判斷)— 圖在插件層記憶體從 records 組裝,不直接 SQL、不建 VIEW。
|
|
||||||
- **安裝契約:KBDB_BASE_URL 安裝時 AI 填**(2026-06-14)— AI 查 CF subdomain 拼 URL → `wrangler secret put` + `deploy`;本地測試用 `.dev.vars`。
|
|
||||||
- **~~萬物皆 Block(v3)~~**(2026-02-28 提出,2026-06-14 淘汰)— 帶獨立 blocks 表的「v3」是違規殘留已刪;基本盤真身 = arcrun/kbdb 3 表。
|
|
||||||
- **避免再被 GitHub flag**(上游鐵律)— 禁跨 repo 自動同步 Actions;部署繞開 GitHub。
|
|
||||||
|
|
||||||
> 結構:INDEX(多角度入口)→ `cards/<bucket>/00-INDEX.md`(桶子索引,固定名)→ 概念原子卡。
|
> 結構:INDEX(多角度入口)→ `cards/<bucket>/00-INDEX.md`(桶子索引,固定名)→ 概念原子卡。
|
||||||
> 指 `00-INDEX` **一律帶路徑** `[[bucket/00-INDEX]]`(固定名跨桶撞名);卡片間用裸 `[[卡名]]`。
|
> 指 `00-INDEX` **一律帶路徑** `[[bucket/00-INDEX]]`(固定名跨桶撞名);卡片間用裸 `[[卡名]]`。
|
||||||
|
|||||||
@@ -40,6 +40,26 @@
|
|||||||
**原因**:當初 monorepo→多 worker 的 Actions 自動同步 + 高頻 API 害帳號被 flag。
|
**原因**:當初 monorepo→多 worker 的 Actions 自動同步 + 高頻 API 害帳號被 flag。
|
||||||
**詳細**:InkStoneCo 頂層 CLAUDE.md。
|
**詳細**:InkStoneCo 頂層 CLAUDE.md。
|
||||||
|
|
||||||
|
## ingest 取代用「先 append 後 deprecate」— 2026-06-26(總管採納,優於頂層原寫法)
|
||||||
|
**結論**:`POST /triplets/ingest` 同 source.uri 出新 content_hash 時,**先 append 新批 active,再翻舊批 status=deprecated**。非頂層 SDD 原寫的「deprecate-then-append」。
|
||||||
|
**原因**:先翻舊批會出現「全無 active」的中途空窗(若 append 失敗);先 append 保證任一時刻都有 active 集合可查。總管已回頭把頂層 design §5 對齊成此寫法。
|
||||||
|
**詳細**:`docs/3-specs/ingest-contract/design.md` §5;issue #1 總管確認 comment。
|
||||||
|
|
||||||
|
## idempotency 鍵存進 triplet template slot — 2026-06-26
|
||||||
|
**結論**:triplet template 增 `source_uri`+`content_hash`(+`source_anchor` 供 get_source)slot 承載 ingest idempotency;按 `source_uri` 分組 deprecate、同 hash no-op。`source_block_id`(Logseq)不夠用。
|
||||||
|
**原因**:純 plugin 領域 slot(base 不需知道),合鐵律(slot 非建表);contract `ingest-candidate.json` 本就有 source.uri+content_hash,落地存進 slot 是自然動作,**不改 contract**。
|
||||||
|
**詳細**:`design.md` §3/§5;issue #1。
|
||||||
|
|
||||||
|
## ensureTemplate 改 slot-diff 補丁 — 2026-06-26
|
||||||
|
**結論**:`ensureTemplate` 命中既有 template 時,比對 slot 差集 → 缺的走 base `PATCH /templates/:id` 補上(**取代原 early-return**)。
|
||||||
|
**原因**:原 early-return 不補新 slot → seed 後新增的 slot(status/gloss…)對既有環境永遠進不來。slot-diff 讓既有+全新環境都自動收斂,免另跑一次性遷移腳本。
|
||||||
|
**詳細**:`src/lib/kbdb-client.ts`;issue #1 前置警示1。
|
||||||
|
|
||||||
|
## refresh 純被動代轉(人發起,守 fan-out 紅線)— 2026-06-26
|
||||||
|
**結論**:`POST /graph/refresh` 只代轉 ingest 重抓+萃,**只能人發起調用觸發**,禁排程/webhook/cron 自動 refresh。graph 自己不抓不萃(ingest 純餵食器職責)。
|
||||||
|
**原因**:自動 fan-out 正是當初害 GitHub flag 的模式。`KBDB_INGEST_URL` 未設時誠實回 `forwarded:false`,不假綠。
|
||||||
|
**詳細**:`src/actions/graph-refresh.ts`;issue #1 T3.6b。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
格式:
|
格式:
|
||||||
|
|||||||
@@ -51,6 +51,18 @@
|
|||||||
原因: 基本盤 = D1 only(免費、無信用卡);embed 是可選加購層。插件混進來會破壞分層。
|
原因: 基本盤 = D1 only(免費、無信用卡);embed 是可選加購層。插件混進來會破壞分層。
|
||||||
日期: 2026-06-14
|
日期: 2026-06-14
|
||||||
|
|
||||||
|
⚠️ MISTAKE: 照「移除某端點」字面就整檔刪 action(沒查反向依賴)
|
||||||
|
症狀: issue 寫「移除 search-query.ts 代理 base 關鍵字那條」→ 差點整個刪 search-query.ts,但 search-suggest.ts 內部依賴 keywordSearch helper,刪了會連帶弄壞 suggest。
|
||||||
|
正確做法: 動手前 grep 反向依賴(誰 import 它)。「移除公開端點」≠「刪內部 helper」——收斂 POST /search 為 suggest-only,keywordSearch 留作 suggest 的內部建構塊。
|
||||||
|
原因: 把「移除對外能力」誤當「刪實作檔」。一個 helper 常同時餵公開端點和內部功能,刪檔前必須看清它還餵誰。
|
||||||
|
日期: 2026-06-26
|
||||||
|
|
||||||
|
⚠️ MISTAKE: tsc 報一堆錯就以為自己改壞了
|
||||||
|
症狀: 跑 `tsc --noEmit` 看到 index.ts/entities.ts/graph.ts 一片 Hono 型別錯,疑心是這次改動破壞編譯。
|
||||||
|
正確做法: 先確認專案的 gate 是什麼——本 repo gate = `vitest run`(package.json test script),不是 tsc。那些 Hono 型別錯是既有 noise(改動前就在)。判斷「我是否弄壞」要對齊專案真正的驗收命令,不是隨手挑一個工具。
|
||||||
|
原因: 預設 tsc 乾淨=健康,但此 repo tsc 從來沒乾淨過;拿錯的 gate 當基準會誤判。
|
||||||
|
日期: 2026-06-26
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
格式:
|
格式:
|
||||||
|
|||||||
+27
-19
@@ -1,35 +1,43 @@
|
|||||||
# 當前狀態
|
# 當前狀態
|
||||||
|
|
||||||
> 更新時間:2026-06-14
|
> 更新時間:2026-06-26
|
||||||
> 每次 session 結束必須更新此檔(用 /wiki-update)。
|
> 每次 session 結束必須更新此檔(用 /wiki-update)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 已完成(2026-06-14:按 leo 鐵律全面改寫 + 獨立成 repo)
|
## 最新(2026-06-26:issue #1 T3 — ingest 寫入端 + graph 端 API)
|
||||||
|
|
||||||
HANDOFF 5 項待辦全做完:
|
開 PR [#2](https://github.com/uncle6me-web/kbdb-graph-plugin/pull/2)(分支 `feat/ingest-contract-sdd`,4 commits,未 merge)。對應 [issue #1](https://github.com/uncle6me-web/kbdb-graph-plugin/issues/1)(頂層 mira-dissolve T3)。
|
||||||
|
|
||||||
1. ✅ **改寫成走基本盤 API**(核心):21 個違規直接 SQL action 全改走 `src/lib/kbdb-client.ts`。寫 triplet=`POST /records`(template=triplet);查圖=取 triplet records 在**插件層記憶體**組鄰接表跑演算法(不靠 DB VIEW)。entity 正規化降級 exact match。
|
完成:
|
||||||
2. ✅ **刪所有 migrations**(插件零建表)+ 清基本盤 action/route(block-*/entry-crud/record-crud/tag/profile/admin/partner/convert/tasks/personality)。
|
1. ✅ **wiki 合併**:舊 `.claude/wiki/` → `system-dev/wiki/`(導入 system-dev-template)。
|
||||||
3. ✅ **獨立成 repo**:`git init` + 推 **public `uncle6me-web/kbdb-graph-plugin`**(無 Actions)。
|
2. ✅ **ingest-contract SDD**(`docs/3-specs/ingest-contract/`)+ 搬入 `contracts/ingest-candidate.json`(T3.1/3.8)。
|
||||||
4. ✅ **裁剪 CLAUDE.md**:移除 KBDB v3 基本盤規範,只留 graph 插件 + 🔒 鐵律 + 安裝契約。
|
3. ✅ **寫入端 + 取代**(T3.2–3.5):`POST /triplets/ingest`、ensureTemplate slot-diff 補丁、`updateRecord`、idempotency、**先 append 後 deprecate**、active-only 查詢。
|
||||||
5. ✅ **部署繞開 GitHub**:`scripts/install.sh`(wrangler 直推),無 .github/workflows。
|
4. ✅ **get_source + refresh**(T3.6/3.7):`GET /graph/source/:name`、`POST /graph/refresh`(純被動代轉)、keyword 收斂(3.6d)。
|
||||||
|
|
||||||
驗證:`wrangler deploy --dry-run` 通過(bundle 無 D1/AI/Vectorize);`vitest run` 10 passed(mock client);全違規掃描零命中;所有 action ≤100 行。
|
驗證:`vitest run` **19 passed**(mock);zero SQL / 無 D1·Vectorize·AI;`wrangler deploy --dry-run` bundle 乾淨;所有 action ≤100 行。
|
||||||
|
|
||||||
## 同場修正的 SDD 錯誤判斷
|
## 正在做 / 阻擋
|
||||||
|
|
||||||
design.md 原本「讀現狀(21 個直接 SQL)推翻鐵律、問要不要共用 D1」是**讀違規現狀推翻規則**的錯。已改正為 **API-as-Wall(走 API,非共用 D1,零建表/零 SQL)**,並記進 mistakes.md。
|
- [🔄] **PR #2 待 review/merge** — 阻擋點:等 leo/總管。graph repo 端 T3 實作面已收斂,剩跨 repo 接通。
|
||||||
|
|
||||||
## 下次 session 第一件事
|
## 下次 session 第一件事
|
||||||
|
|
||||||
**實際部署**:等基本盤 `arcrun-kbdb` 上線/有網址後,跑 `bash scripts/install.sh` 一次到位
|
**確認 PR #2 狀態**:若已 merge → 切回 `main` 拉新狀態;若有 review 意見 → 在 `feat/ingest-contract-sdd` 上改。
|
||||||
(自動查 CF subdomain 拼 `KBDB_BASE_URL` → `wrangler secret put` → `wrangler deploy`)。
|
本 repo 端 T3 無剩餘可單獨做的 task(其餘待跨 repo / 部署,見下)。
|
||||||
現在不空跑部署(避免上線一個打不到基本盤的殼)。
|
|
||||||
|
|
||||||
## 已知缺口([→arcrun],待基本盤補)
|
## 待負責人確認
|
||||||
|
|
||||||
- base 無 `PUT /records/:id` → entity addAlias 用「重建 record」覆寫。
|
- **PR #2 merge** — 等:leo/總管 review。
|
||||||
- base 無 `DELETE /records/:id` → triplet/entity update/delete、pending confirm/reject 為 soft(不硬刪)。
|
- **MCP 註冊薄殼**(圖工具 traverse/neighbors/source/refresh 併入 arcrun `u6u-mcp-server`)— 等:總管協調 arcrun,**不另起 graph MCP**。graph 端 HTTP API 已備好。
|
||||||
- 語意搜尋 / embedding 屬基本盤 optional embed 模組,插件只做 keyword/exact。
|
|
||||||
- arcrun 端 MCP/CLI 的 KBDB 薄殼仍待補(見 arcrun HANDOFF §2);插件目前直打基本盤 HTTP API。
|
## 已知問題 / 缺口
|
||||||
|
|
||||||
|
| 問題 | 優先級 | 狀態 |
|
||||||
|
|------|--------|------|
|
||||||
|
| refresh 端到端走通 | 🟡 | 待 ingest repo(T4)部署 + `KBDB_INGEST_URL` 設定;未設時誠實回 `forwarded:false` |
|
||||||
|
| semantic normalize | 🟡 | 仍 exact-only,留接口;待 base embed(Arcrun #7,code done 待部署) |
|
||||||
|
| MCP 工具實際可用 | 🟡 | graph 端 API 備好,待 arcrun 註冊薄殼 |
|
||||||
|
| base `DELETE /records/:id` | ⚪ | 仍無 → triplet/entity delete 為 soft(不硬刪);死資料清除原則見頂層 T8 |
|
||||||
|
|
||||||
|
> 註:base `PATCH /records/:id` **已就緒**(Arcrun #6 closed,本 session ingest deprecate 即用此)。舊 status 列的「base 無 PUT /records/:id」缺口已部分解除。
|
||||||
|
|||||||
@@ -85,78 +85,6 @@ describe('ingestEnvelope — 污染 envelope 422(契約 strict)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ingestEnvelope — 向量化打標欄位(contract 升格,ingest#1)', () => {
|
|
||||||
it('帶 nodes[].embed/id/aliases + triplets[].predicate_embed → 通過(非 422)', async () => {
|
|
||||||
const c = mockClient();
|
|
||||||
const env: IngestEnvelope = {
|
|
||||||
source: { uri: 'github:uncle6me-web/wiki@v.md', content_hash: 'hv' },
|
|
||||||
extractor: { model: 'claude-sonnet-4-6', tier: 'deep' },
|
|
||||||
nodes: [
|
|
||||||
{ name: 'Graph RAG', id: 'graph-rag.md', aliases: ['圖譜 RAG'], gloss: '關係遍歷檢索', embed: true },
|
|
||||||
{ name: '黃仁勳', id: '黃仁勳', aliases: ['Jensen Huang'], embed: false },
|
|
||||||
],
|
|
||||||
triplets: [
|
|
||||||
{ subject: 'Graph RAG', predicate: '參考', object: '黃仁勳', predicate_embed: true },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
// schema 層先驗:合法新欄位不被 strict 擋。
|
|
||||||
expect(IngestEnvelopeSchema.safeParse(env).success).toBe(true);
|
|
||||||
|
|
||||||
// 落地:triplet 寫入、node 打標存進 entity slot。
|
|
||||||
const res = await ingestEnvelope(c, env);
|
|
||||||
expect(res).toEqual({ skipped: false, ingested: 1, deprecated: 0 });
|
|
||||||
|
|
||||||
const { triplets } = await queryTriplets(c, {});
|
|
||||||
expect(triplets.length).toBe(1);
|
|
||||||
|
|
||||||
// node 打標落地成 entity record(gloss/aliases/embed 標示透傳供 base 讀)。
|
|
||||||
const entities = await c.listRecordsByTemplate('entity');
|
|
||||||
const gr = entities.find((e) => e.values.canonical === 'Graph RAG');
|
|
||||||
expect(gr?.values.node_id).toBe('graph-rag.md');
|
|
||||||
expect(gr?.values.embed).toBe('true');
|
|
||||||
expect(JSON.parse(gr!.values.aliases_json!)).toEqual(['圖譜 RAG']);
|
|
||||||
expect(gr?.values.gloss).toBe('關係遍歷檢索');
|
|
||||||
const jensen = entities.find((e) => e.values.canonical === '黃仁勳');
|
|
||||||
expect(jensen?.values.embed).toBe('false'); // 明確 false 透傳
|
|
||||||
});
|
|
||||||
|
|
||||||
it('同 id 的 node 被多次帶入 → 去重,只存一筆 entity(一卡一 node)', async () => {
|
|
||||||
const c = mockClient();
|
|
||||||
const env: IngestEnvelope = {
|
|
||||||
source: { uri: 'github:uncle6me-web/wiki@dup.md', content_hash: 'hd' },
|
|
||||||
extractor: { model: 'm', tier: 'deep' },
|
|
||||||
nodes: [
|
|
||||||
{ name: 'Graph RAG', id: 'graph-rag.md' },
|
|
||||||
{ name: 'Graph RAG(別名)', id: 'graph-rag.md' }, // 同 id → 同卡,不重建
|
|
||||||
],
|
|
||||||
triplets: [{ subject: 'Graph RAG', predicate: 'r', object: 'X' }],
|
|
||||||
};
|
|
||||||
await ingestEnvelope(c, env);
|
|
||||||
const entities = await c.listRecordsByTemplate('entity');
|
|
||||||
expect(entities.filter((e) => e.values.node_id === 'graph-rag.md').length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('帶真正 graph 領域禁送欄位(bridge_score)→ 仍 422', () => {
|
|
||||||
const polluted = {
|
|
||||||
source: { uri: 'u', content_hash: 'h' },
|
|
||||||
extractor: { model: 'm', tier: 'deep' },
|
|
||||||
nodes: [{ name: 'A', embed: true }],
|
|
||||||
triplets: [{ subject: 'A', predicate: 'r', object: 'B', predicate_embed: true, bridge_score: 3 }],
|
|
||||||
};
|
|
||||||
expect(IngestEnvelopeSchema.safeParse(polluted).success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('node 帶 graph 領域禁送欄位(clusters)→ 仍 422', () => {
|
|
||||||
const polluted = {
|
|
||||||
source: { uri: 'u', content_hash: 'h' },
|
|
||||||
extractor: { model: 'm', tier: 'deep' },
|
|
||||||
nodes: [{ name: 'A', embed: true, clusters: ['c1'] }],
|
|
||||||
triplets: [{ subject: 'A', predicate: 'r', object: 'B' }],
|
|
||||||
};
|
|
||||||
expect(IngestEnvelopeSchema.safeParse(polluted).success).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ingestEnvelope — rollback(翻回 status)', () => {
|
describe('ingestEnvelope — rollback(翻回 status)', () => {
|
||||||
it('把 deprecated 翻回 active 後,active 查詢重新見到它', async () => {
|
it('把 deprecated 翻回 active 後,active 查詢重新見到它', async () => {
|
||||||
const c = mockClient();
|
const c = mockClient();
|
||||||
|
|||||||
Reference in New Issue
Block a user