Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbd375f7ac | |||
| 613071f41d | |||
| 27f7448914 | |||
| 98b221b435 | |||
| 2dd80cd703 |
@@ -8,7 +8,7 @@ description: 處理本 repo 的 GitHub issue(讀/回/結案),跨 repo 發
|
||||
很多人不知道這件事——`gh` 已內建認證,零開發、零外部依賴。issue 同源於 repo,
|
||||
比 Notion / Sheets 更適合做交辦與待辦,不必引入外部 SaaS。
|
||||
|
||||
這份指引分四層,界線要守住。
|
||||
這份指引分三層,界線要守住。
|
||||
|
||||
---
|
||||
|
||||
@@ -20,13 +20,12 @@ description: 處理本 repo 的 GitHub issue(讀/回/結案),跨 repo 發
|
||||
gh issue list --state open # 看有哪些待辦
|
||||
gh issue view <n> # 讀完整內容
|
||||
# …實作…
|
||||
gh issue comment <n> --body "[<本 repo> CC] 做了什麼、怎麼決定的、改了哪些檔"
|
||||
gh issue comment <n> --body "做了什麼、怎麼決定的、改了哪些檔"
|
||||
gh issue close <n> # 確認解決後結案
|
||||
```
|
||||
|
||||
回覆要有料:說清楚**做了什麼、為什麼這樣決定、動了哪些檔**,而不是只回「done」。
|
||||
issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆判斷對不對。
|
||||
跨 repo 的 issue/comment 開頭一律署名 `[<本 repo> CC]`(見第 3 節鐵律)。
|
||||
|
||||
---
|
||||
|
||||
@@ -42,25 +41,7 @@ issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆
|
||||
|
||||
---
|
||||
|
||||
## 3. 跨 repo 署名(鐵律 — 絕不可漏)
|
||||
|
||||
所有 repo(mira / graph-plugin / ingest-plugin / Arcrun / template…)共用**同一個 GitHub 帳號**發 issue/comment,
|
||||
所以 issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**。
|
||||
|
||||
> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。
|
||||
|
||||
- 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`…
|
||||
- 總管下令/追問:`[InkStoneCo 總管]`
|
||||
- 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。
|
||||
|
||||
為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**;
|
||||
`git config user.name` 只影響 commit 作者、不影響 issue/comment author;
|
||||
給每個 repo 開獨立帳號 = 多帳號自動化 = 踩下方第 4 節 flag 鐵律,**不可**。
|
||||
身份只能在**內容層自報**。本 repo 名稱 → 看 `git remote -v` 或 repo 根目錄名。
|
||||
|
||||
---
|
||||
|
||||
## 4. flag 安全界線(最重要 — 絕不可越)
|
||||
## 3. flag 安全界線(最重要 — 絕不可越)
|
||||
|
||||
**「有事才讀」,禁止自動輪詢。**
|
||||
|
||||
|
||||
@@ -4,26 +4,6 @@
|
||||
|
||||
---
|
||||
|
||||
## 生命週期(單一活性鐵律,全文見 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`)
|
||||
|
||||
五條鐵律摘要:
|
||||
|
||||
1. **單一活性**:任何時刻整個 repo 只允許一份 `status: active` 的 SDD;所有開發任務對應它的 tasks,找不到對應任務 → 停下來問,不准直接做。
|
||||
2. **禁止自行建立 SDD**:澄清問題→回答不動文件;任務層變更→更新現行 SDD 的 tasks(標日期與原因);規格層變更→走第 3 條。
|
||||
3. **規格變更只有一條路**:change proposal 寫進 `system-dev/docs/3-specs/pending-changes.md`(摘要+觸發原因+影響分析),然後**停止**等使用者「confirm」。
|
||||
4. **開新 SDD 的唯一時機**:使用者 confirm 後——先把舊 SDD 未完成任務逐條搬入新 SDD(做完前不准寫 code)→ 舊的標 `closed` + `superseded_by` 移入 `archive/` → 新 SDD changelog 記繼承 → 列搬移/作廢清單請最終確認。
|
||||
5. **每次 session 開始**先讀 active SDD 與 pending-changes.md,回報三個數字:
|
||||
|
||||
```
|
||||
📐 現行規格:〈SDD 名稱〉
|
||||
📋 未完成任務:N
|
||||
⚖️ 待裁決 proposal:M
|
||||
```
|
||||
|
||||
若出現**兩份 active=規則已被違反,當場糾正**(收斂到一份,其餘 paused/closed)。
|
||||
|
||||
---
|
||||
|
||||
## 執行流程
|
||||
|
||||
### 第一步:理解任務
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
# /wiki-extract — vault 增量萃取(Logseq / Obsidian → system-dev/wiki)
|
||||
|
||||
把**筆記 vault**(Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地
|
||||
萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**:
|
||||
AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 KBDB。
|
||||
|
||||
> **跟 `/wiki-init` 的分工**:
|
||||
> - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。
|
||||
> - `/wiki-extract` 是**之後每次**的增量重萃——vault 會被 Syncthing/cron 持續灌新筆記,
|
||||
> 這支負責「只萃變動的、沒變的不碰、不浪費 AI run」。給 Routine / cloud-worker 反覆跑。
|
||||
> - **跑它的是你(CC / Routine)=LLM 本人,不需任何 token**。
|
||||
|
||||
> **邊界(硬規矩,別越界)**
|
||||
> - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游
|
||||
> Arcrun 從你產的 `[[wikilink]]` + `## 關聯` 機械映射(另一張 issue),不是這支的事。
|
||||
> - **原始筆記唯讀**:`journals/`、`pages/`、Obsidian 根 `.md` 是 leo 的手寫真身,
|
||||
> 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。
|
||||
> - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。
|
||||
|
||||
---
|
||||
|
||||
## 執行流程
|
||||
|
||||
### 第一步:確認這是 vault repo,定位 raw source
|
||||
|
||||
偵測邏輯**同 install.sh / wiki-init**:
|
||||
|
||||
| 偵測到 | 型態 | raw source(要掃的原文) |
|
||||
|--------|------|--------------------------|
|
||||
| 根目錄有 `logseq/` | Logseq vault | `journals/*.md` + `pages/*.md` |
|
||||
| 根目錄有 `.obsidian/` | Obsidian vault | vault 根下所有 `.md` |
|
||||
| 都沒有 | **不是 vault** | → 停手。這支只處理 vault;一般 dev repo 開發時就手寫 `.claude`/`system-dev/wiki`,不需萃取 |
|
||||
|
||||
沒有 `system-dev/wiki/`?→ 先跑 `/wiki-init`(首次建結構+首萃),再回來用這支做增量。
|
||||
|
||||
### 第二步:content_hash 冪等 —— 決定哪些檔要萃(省 run 的核心)
|
||||
|
||||
讀萃取 manifest:`system-dev/wiki/.extract-manifest.json`(不存在=首次,視同全部要萃)。
|
||||
格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"algo": "sha256",
|
||||
"sources": {
|
||||
"journals/2026_07_01.md": {
|
||||
"content_hash": "<sha256 of file bytes>",
|
||||
"extracted_at": "2026-07-06",
|
||||
"cards": ["Prompt能力即拆解自己邏輯的能力", "程式化邏輯可圖解任何主題不限AI"],
|
||||
"skipped_reason": null
|
||||
},
|
||||
"journals/2026_06_25.md": {
|
||||
"content_hash": "<sha256>",
|
||||
"extracted_at": "2026-07-06",
|
||||
"cards": [],
|
||||
"skipped_reason": "空檔/訊息量不足,無可萃知識點"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
對每個 raw source 檔:
|
||||
|
||||
1. 算目前 `content_hash`(`sha256sum <file>`,取檔案 bytes 的 hash)。
|
||||
2. 跟 manifest 裡該檔的 `content_hash` 比:
|
||||
- **相同 → skip,不讀不萃、不呼叫任何 AI 推理**(就算它上次 `cards: []` 也 skip——空檔沒變還是空)。
|
||||
- **不同或不在 manifest → 這檔要(重)萃**。
|
||||
3. manifest 有、但檔已不存在 → 該檔被刪,把它的 entry 從 manifest 移除(卡片是否連帶處理見第五步)。
|
||||
|
||||
> **這一步是「省 run」的重點**:vault 每天可能只動 1~2 個 journal,其餘幾十個檔 hash 沒變
|
||||
> 就整批跳過,AI 只對真正變動的檔動腦。**重跑一個沒變動的 vault = 零 AI 呼叫、零 diff。**
|
||||
|
||||
### 第三步:對「要萃」的檔,抓知識點 + 任務
|
||||
|
||||
逐個變動檔讀原文,分兩類抽取:
|
||||
|
||||
**(a) 知識點 → 概念原子卡**
|
||||
判準與卡片格式**完全依 `/wiki-init` 第五步**(frontmatter `tags:`/`gloss:`、H1、麵包屑
|
||||
`← [[<bucket>/00-INDEX]]`、`**來源**`、`## 摘要`、`## 重點`、`## 實體`、`## 關聯` 的
|
||||
typed-edge 三元組、TAXONOMY 受控標籤、硬自檢等)——**不在這裡重寫格式,一律回去讀那份**。
|
||||
廢話/訊息量薄的段落略過(在 manifest 記 `skipped_reason`,誠實留痕、不留卡)。
|
||||
|
||||
**(b) Logseq 任務 marker → 任務卡(task_status)**
|
||||
解析**完全依** `system-dev/docs/4-guides/logseq-markers.md`(單一真相源,與 template#4
|
||||
tasks 投影共用同一套;**別自己另寫 mapping**)。摘要:
|
||||
|
||||
- 任務行 regex:`^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+`
|
||||
- 狀態正規化:TODO/LATER→`todo`、DOING/NOW→`in-progress`、WAITING→`blocked`、
|
||||
DONE→`done`、CANCELED/CANCELLED→`closed`。
|
||||
- 跳過 `:LOGBOOK:…:END:` 區塊與 `key:: value` 屬性行(`collapsed::`、`id::`、
|
||||
`SCHEDULED::`、`DEADLINE::`…),**別把 marker 或屬性當任務內文**。
|
||||
|
||||
有實質內容的任務 → 產一張任務卡進 `cards/tasks/` bucket,frontmatter 帶 `task_status`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: [<領域標籤,依 TAXONOMY>]
|
||||
task_status: todo # ← 依上表正規名;這是任務卡才有的欄位
|
||||
gloss: 一句話定義這個任務要達成什麼(供下游 normalize)
|
||||
---
|
||||
# <任務一句話標題(marker 後的內文,去掉 marker)>
|
||||
|
||||
← [[tasks/00-INDEX]]
|
||||
|
||||
**來源**:`journals/2026_07_01.md`(TODO block)
|
||||
**最後更新**:YYYY-MM-DD
|
||||
|
||||
## 摘要
|
||||
[任務要做什麼、脈絡]
|
||||
|
||||
## 實體
|
||||
- **<關鍵實體正規名>**(<同義詞>)— <一句描述>
|
||||
|
||||
## 關聯
|
||||
### 內文知識關係(端點=上方 `## 實體` 正規名,一字不差)
|
||||
- <實體A> >> <謂詞> >> <實體B>
|
||||
### 卡片關係(卡對卡)
|
||||
- [[本任務卡]] >> 涉及 >> [[相關概念卡]]
|
||||
```
|
||||
|
||||
> 純瑣事任務(「買菜」這種無知識量)不必成獨立卡——可在 `cards/tasks/00-INDEX.md`
|
||||
> 列一行帶狀態即可,避免灌垃圾卡。判準同 D16:有沒有知識/專案價值。
|
||||
|
||||
### 第四步:更新桶索引與 INDEX
|
||||
|
||||
- 每個動到的 bucket(如 `cards/notes/`、`cards/tasks/`)更新其 `00-INDEX.md`
|
||||
(容器:只連不重寫,H2/H3 分節)。
|
||||
- 更新 `system-dev/wiki/INDEX.md` 的標籤視圖與卡片清單。
|
||||
- 任務卡可在 INDEX 開一個「任務視圖」按 `task_status` 聚類。
|
||||
|
||||
### 第五步:寫回 manifest + 驗證原文 0 動
|
||||
|
||||
1. 把這次萃過的每個檔的**新 `content_hash`**、`extracted_at`、產出的 `cards`、
|
||||
(或 `skipped_reason`)寫回 `system-dev/wiki/.extract-manifest.json`。
|
||||
**沒動到的檔的 entry 原樣保留**(別整檔重寫掉別人的 hash)。
|
||||
2. 驗證原文零異動(踩過的坑):
|
||||
```
|
||||
git status --short journals/ pages/ # Obsidian 則看根目錄 .md ——須 0 新增 0 修改
|
||||
```
|
||||
有任何原文變動 → 你誤寫了 raw source,回滾。
|
||||
|
||||
### 第六步:完成報告
|
||||
|
||||
```
|
||||
✅ wiki-extract 完成(增量)
|
||||
掃描:N 個 raw source 檔
|
||||
萃取:M 個(content_hash 變動)→ 產出 X 張概念卡 + Y 張任務卡
|
||||
跳過:K 個(hash 未變,零 AI 呼叫)
|
||||
任務狀態分布:todo A / in-progress B / done C / …
|
||||
原文驗證:journals/ pages/ git status 0 異動 ✅
|
||||
manifest:system-dev/wiki/.extract-manifest.json 已更新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 冪等自檢(Routine 反覆跑必守)
|
||||
|
||||
- [ ] 跑之前先讀 manifest,hash 相同的檔**完全不進 AI**(不是「讀了才發現一樣」,是靠 hash 先擋)。
|
||||
- [ ] 對「同一個沒變動的 vault」連跑兩次:第二次應是**零萃取、零卡片 diff、零 manifest 變化**。
|
||||
- [ ] 只有 `system-dev/wiki/` 有寫入;`journals/`、`pages/` git status 全乾淨。
|
||||
- [ ] 任務狀態用正規名,marker/屬性沒混進內文(照 `logseq-markers.md` 自檢)。
|
||||
@@ -158,16 +158,9 @@ gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,
|
||||
## 重點
|
||||
- [自包含改寫的要點,不依賴原文]
|
||||
|
||||
## 實體
|
||||
> 本卡內文的關鍵實體(也是 graph node)。名+描述供下游 embedding normalize。集中放、一行一個、不縮排、不重複。
|
||||
- **原子筆記**(atomic note/卡片原子化)— 每張卡只承載一個不可再分論點的知識記錄單元。
|
||||
- **傳統筆記**(大鍋炒筆記)— 把多主題混雜在同一篇、難精確引用的記錄方式。
|
||||
|
||||
## 關聯
|
||||
### 內文知識關係(內文實體間;端點=上方 `## 實體` 正規名,一字不差)
|
||||
- 原子筆記 >> 對立於 >> 傳統筆記
|
||||
### 卡片關係(卡對卡)
|
||||
- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]]
|
||||
- [[原子筆記]] >> 是其最小單元 >> [[卡片盒筆記法]]
|
||||
```
|
||||
|
||||
**麵包屑用帶路徑 wikilink**(issue #7):H1 次行放 `← [[<bucket>/00-INDEX]]` 指回桶子索引。
|
||||
@@ -182,25 +175,20 @@ gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,
|
||||
- 一般開發專案的軸可不同(如 子系統/層級/決策類型),由 AI 依專案性質提出、寫進 TAXONOMY.md。
|
||||
- **遇到現有軸裝不下的內容**:先查是否只是現有標籤的同義詞;確實是新軸才加進 TAXONOMY.md(附定義)再用——**禁止繞過字典在卡片直接冒新標籤**。字典是 per-repo,跨 repo 不必共用。
|
||||
|
||||
**typed-edge 規則**(issue #5/#11,把「關係」也預編譯,下游 ingest 直接 parse 出帶類型的有向邊):
|
||||
- **重點抓內文實體關係,不只卡對卡**:卡對卡(`[[卡A]] >> 謂詞 >> [[卡B]]`)只是既有雙鏈加動詞、資訊量幾乎沒增加;價值在內文概念關係(`原子筆記 >> 對立於 >> 傳統筆記`,A/B 是內文概念非卡標題)。
|
||||
**typed-edge 規則**(issue #5,把「關係」也預編譯,下游 ingest 直接 parse 出帶類型的有向邊):
|
||||
1. **方向性**:`A >> 謂詞 >> B` 必須讀成「A(謂詞)B」一句通順的話;A、B 順序就是主→賓真實方向。
|
||||
2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、犧牲)。**禁名詞當謂詞**——`>> 存儲格式 >>`、`>> 操作體驗 >>` 讀不通,是錯的。
|
||||
3. **謂詞自由但別太天馬行空**:「參考/參照」皆可(下游 embed 自動聚類),別寫「瞄了一眼」這種抓不到同義的。
|
||||
4. **內文三元組端點用裸文字**(非 `[[wikilink]]`),避免 Logseq 紅色斷鏈;卡對卡那層才用 `[[]]`。
|
||||
5. **向後相容**:純 `[[A]]` 仍合法(視為無類型邊),盡量補謂詞。
|
||||
2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、是…的實作),動詞天然帶方向。
|
||||
3. **謂詞自由書寫,不受控詞彙**:下游對謂詞 embedding 時同義謂詞會自動聚類;但方向仍靠書寫順序保證。
|
||||
4. **向後相容**:純 `[[A]]` 仍合法(視為無類型邊),盡量補謂詞。
|
||||
|
||||
> **★ 硬自檢(Haiku 量產必備)★** 內文三元組端點必須與 `## 實體` 某粗體正規名【一字不差】。**寫完逐條把 A、B 拿去 `## 實體` 比對**,沒有完全相同的 → 這條錯了,改用實體表已有的詞、或把端點補進 `## 實體` 再指它。禁止端點帶括號註解/整句補語/形容詞短語。(實證:光寫規則 Haiku 會略過,端點對不齊 14 條;寫成自檢動作後 14→0。跑 12 張才暴露。)
|
||||
> `>>` 是分隔語法,repo 可自選符號,但全程一致。
|
||||
|
||||
**萃 gloss 規則**(issue #9/#11,把「node 的一句說明」也預編譯,供下游 KBDB 語義 normalize):
|
||||
**萃 gloss 規則**(issue #9,把「node 的一句說明」也預編譯,供下游 KBDB 語義 normalize):
|
||||
- **gloss = 這個 entity / graph node 是什麼的一句話**。下游對「entity 名 + gloss」一起做 embedding 求相似度,自動歸一同義詞(比只對名字準、比手維護 alias 表自動)。
|
||||
- **兩層 gloss**:① frontmatter `gloss:` 描述卡標題這個 node;② `## 實體` 每行描述句描述內文實體 node。**內文實體也是 graph node、也需描述句**才能 normalize(`黃仁勳` vs `Jensen Huang` 靠描述拉近向量)。
|
||||
- **實體要描述、謂詞不用**:實體同義詞字面差遠需描述拉近;謂詞同義詞字面本就近,裸詞 embed 自動聚類。
|
||||
- **在知識生產的當下、由 local CC 建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔 / 跨庫視角,編不出貼合的 gloss(=胡扯)。
|
||||
- **選填、deep tier 才產**:淺萃(只要結構)時不浪費;deep 改寫時每張卡補。
|
||||
- **gloss ≠ 摘要**:`gloss` 是給機器 normalize 的定義句(「X 是…」);`## 摘要` 是給人讀的核心一句。
|
||||
- **格式對齊下游 envelope**:frontmatter `gloss:` 與 `## 實體` 詞條對應下游 ingest envelope 的 `nodes[].gloss`,ingest 直接取用。
|
||||
- **在知識生產的當下、由 local CC 建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔 / 跨庫視角,編不出貼合的 gloss(=胡扯)。local scope 才有完整脈絡寫對。
|
||||
- **選填、deep tier 才產**:淺萃(只要結構)時不浪費;deep 改寫時每張卡補一句 `gloss:`。
|
||||
- **gloss ≠ 摘要**:`gloss` 是 frontmatter 裡給機器 normalize 用的定義句(「X 是…」),求精準可 embedding;`## 摘要` 是給人讀的核心一句。可相近但分屬兩處、兩用途。
|
||||
- **格式對齊下游 envelope**:frontmatter `gloss:` 對應下游 ingest envelope 的 `nodes[].gloss` 欄位,ingest 直接取用、不再回頭補。
|
||||
|
||||
**INDEX.md 是標籤視圖**(非資料夾列表),`00-INDEX.md` 是桶內容器(只連不重寫,H2/H3 分節)。
|
||||
頂層索引指桶子索引帶路徑:`[[pkm/00-INDEX]]`。
|
||||
@@ -228,8 +216,3 @@ git status --short pages/ journals/ # 或一般專案的 docs/ ——須 0
|
||||
原文驗證:pages/ journals/ git status 0 異動 ✅
|
||||
下一步:用 /wiki-capture 把重要決策存進 wiki
|
||||
```
|
||||
|
||||
> **vault repo 首萃後的增量重萃**:Logseq / Obsidian vault 會被持續灌新筆記。首萃(本命令)
|
||||
> 之後,改用 **`/wiki-extract`** 做增量——它靠 content_hash 只萃變動的檔(沒變=零 AI 呼叫),
|
||||
> 並解析 Logseq 大寫任務 marker(TODO/DOING/DONE…→ `task_status`,見
|
||||
> `system-dev/docs/4-guides/logseq-markers.md`)。適合掛給 Routine / cloud-worker 反覆跑。
|
||||
|
||||
+14
-82
@@ -1,16 +1,10 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse hook — 動 code 前檢查 SDD + 單一活性 SDD 鐵律(issue #6)
|
||||
# PreToolUse hook — 動 code 前檢查有沒有對應 SDD
|
||||
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
|
||||
# 生命週期規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md
|
||||
#
|
||||
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
||||
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, ... } }
|
||||
# 行為:
|
||||
# 1. status: active 的 SDD > 1 份 → 單一活性鐵律已被違反,**不論寫什麼檔**一律擋(exit 2),
|
||||
# 先收斂到一份再說。
|
||||
# 2. 動 code 檔(.ts/.go/...)→ 需要「恰好 1 份」active SDD;0 份 → 擋。
|
||||
# 3. 向下相容:3-specs 下完全沒有任何 design.md 帶 frontmatter(老 repo 尚未遷移生命週期制度)
|
||||
# → 退回舊行為:有 design.md 就放行+提醒,沒有才擋。避免 template update 後老 repo 立刻全紅。
|
||||
# 行為:動到 code 檔(.ts/.go/...)但 system-dev/docs/3-specs/ 下沒有任何 SDD → 警告(exit 2 擋)。
|
||||
#
|
||||
# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。
|
||||
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
|
||||
@@ -30,42 +24,6 @@ fi
|
||||
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
||||
[ -z "$FILE_PATH" ] && exit 0
|
||||
|
||||
SPECS_DIR="system-dev/docs/3-specs"
|
||||
|
||||
# ── 統計 active / frontmatter ──────────────────────
|
||||
# 排除 archive/(已封存)與 TEMPLATE(範本自帶 status: draft frontmatter,不算數——
|
||||
# 否則 update 一鋪新版 TEMPLATE-sdd,老 repo 就被誤判「已遷移」而全紅,向下相容破功)。
|
||||
# frontmatter 判定=design.md 前 10 行有 ^status: 行(機器可查,見 SDD-LIFECYCLE.md)。
|
||||
ACTIVE_COUNT=0
|
||||
FM_COUNT=0
|
||||
ACTIVE_LIST=""
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
HEAD10=$(head -10 "$f" 2>/dev/null || true)
|
||||
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*'; then
|
||||
FM_COUNT=$((FM_COUNT + 1))
|
||||
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*active'; then
|
||||
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
||||
ACTIVE_LIST="${ACTIVE_LIST} • ${f}
|
||||
"
|
||||
fi
|
||||
fi
|
||||
done < <(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── 鐵律 1:單一活性被違反(active > 1)→ 不論寫什麼檔一律擋 ──
|
||||
if [ "$ACTIVE_COUNT" -gt 1 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 單一活性鐵律違反:偵測到 ${ACTIVE_COUNT} 份 status: active 的 SDD(任何時刻整個 repo 最多一份):
|
||||
${ACTIVE_LIST}
|
||||
請先收斂到一份:其餘改 status: paused / closed(closed 且被取代者填 superseded_by 並移入 3-specs/archive/)。
|
||||
規則全文見 system-dev/docs/3-specs/SDD-LIFECYCLE.md。收斂前擋下所有寫檔。
|
||||
(本 hook 攔 Write/Edit;修 frontmatter 可用 bash 直改,或由人裁決哪份是現行。)
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 只管 code 檔。docs/markdown/設定檔等放行。
|
||||
case "$FILE_PATH" in
|
||||
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;;
|
||||
@@ -78,54 +36,28 @@ case "$FILE_PATH" in
|
||||
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# ── 向下相容:整個 3-specs 沒有任何帶 frontmatter 的 design.md ──
|
||||
# =老 repo 還沒遷移生命週期制度 → 退回舊行為(有 design.md 就放行+提醒),
|
||||
# 避免 template update 一裝新 hook,老 repo 所有 code 寫入立刻全紅。
|
||||
if [ "$FM_COUNT" -eq 0 ]; then
|
||||
SDD_COUNT=0
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
SDD_COUNT=$(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
# system-dev/docs/3-specs/ 下完全沒有 design.md → 攔
|
||||
SDD_COUNT=0
|
||||
if [ -d "system-dev/docs/3-specs" ]; then
|
||||
SDD_COUNT=$(find system-dev/docs/3-specs -name 'design.md' -not -path '*TEMPLATE*' 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ "$SDD_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下找不到任何 SDD。
|
||||
if [ "$SDD_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 system-dev/docs/3-specs/ 下找不到任何 SDD。
|
||||
|
||||
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md),且遵守單一活性生命週期
|
||||
(system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md)。
|
||||
|
||||
請先:
|
||||
1. 確認這個改動屬於哪個子系統
|
||||
2. 在 ${SPECS_DIR}/[子系統]/ 建立 design.md(可用 /sdd-check 協助),frontmatter 標 status: active
|
||||
2. 在 system-dev/docs/3-specs/[子系統]/ 建立 design.md(可用 /sdd-check 協助)
|
||||
3. 在回覆開頭宣告已讀 SDD + 對應 task
|
||||
|
||||
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 舊行為放行 + 提醒遷移(stderr 警告,不擋)
|
||||
echo "📋 提醒:${SPECS_DIR}/ 有 SDD 但尚未掛生命週期 frontmatter(老結構)。動手前確認已讀對應 design.md;建議依 SDD-LIFECYCLE.md 補 status 標記(現行那份標 active)。" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 新行為:寫 code 檔需「恰好 1 份」active SDD ──
|
||||
if [ "$ACTIVE_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下沒有任何 status: active 的 SDD。
|
||||
|
||||
單一活性鐵律:所有開發任務唯一對應源=那份 active SDD(規則見 system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||
|
||||
請先(擇一,都是人的決定,CC 不得自行建 SDD):
|
||||
1. 把現行規格的 design.md frontmatter 標成 status: active(一份、只能一份)
|
||||
2. 或依 SDD-LIFECYCLE.md 第 3、4 條:proposal 進 pending-changes.md → 使用者 confirm → 開新 SDD 標 active
|
||||
然後在回覆開頭宣告已讀 active SDD + 對應 task。
|
||||
|
||||
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 恰好 1 份 active:放行,留痕提醒要宣告(stderr 警告,不擋)
|
||||
printf '📋 提醒:現行 active SDD=\n%s動手前請確認已讀它的 design.md、對應到 tasks,並在回覆宣告。\n' "$ACTIVE_LIST" >&2
|
||||
# 有 SDD:放行,但留痕提醒要宣告(stderr 警告,不擋)
|
||||
echo "📋 提醒:system-dev/docs/3-specs/ 下有 SDD。動手前請確認已讀對應 design.md 並在回覆宣告。" >&2
|
||||
exit 0
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/bin/bash
|
||||
# subagent-wiki-guard.sh — PreToolUse(Task) hook:subagent 聽到「查」就自己先查 wiki
|
||||
#
|
||||
# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEY,prompt 都只叫它「去查 repo 程式碼」。
|
||||
# agent 於是從**稿子**推論出「這東西還活著、不能動」,總管照單全收去擋 leo 三輪。
|
||||
#
|
||||
# 🔑 設計轉向(leo 2026-07-21):
|
||||
# 第一版是「上游沒交代讀 wiki 就擋下」——但那**還是依賴上游記得寫**,
|
||||
# 跟「我記得讀 wiki」是同一個病。leo 點破:
|
||||
# 「subagent 的問題跟你一樣。你叫它去查,就算你沒說要先查 wiki,
|
||||
# 但它**只要聽到查,就應該主動查 wiki**,因為每個 repo 都有維護自己的 wiki。」
|
||||
# → 改成 **注入式**:不擋、不要求上游改 prompt,直接把「先查 wiki」這條
|
||||
# 以 additionalContext 注入給 subagent,讓它自己做。零依賴任何人記得。
|
||||
#
|
||||
# 行為:偵測到查證/實作類任務 → exit 0 並用 hookSpecificOutput 注入指示。
|
||||
# 已含 wiki 指示、或非查證類任務 → 靜默放行(不重複注入)。
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
PROMPT=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin)
|
||||
print(d.get('tool_input',{}).get('prompt',''))
|
||||
except Exception: print('')
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
[ -z "$PROMPT" ] && exit 0
|
||||
|
||||
# 上游已經交代了 → 不必重複注入
|
||||
if printf '%s' "$PROMPT" | grep -qiE "wiki|agent-memory|mistakes\.md|decisions-summary"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 只對「查證/實作」類任務注入(純寫作、計算、潤稿等不需要)
|
||||
if ! printf '%s' "$PROMPT" | grep -qiE "查|盤點|核實|確認|調查|研究|找出|repo|程式碼|原始碼|source|實作|移除|刪除|重構|修|grep|codebase|\.ts|\.go|src/"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
|
||||
guidance = """【自動注入:查任何東西之前,先查 wiki】
|
||||
|
||||
你所在的 repo 有維護自己的 wiki(通常在 `system-dev/wiki/`,舊結構在 `.claude/wiki/`)。
|
||||
**接到「查/盤點/核實/實作」類任務時,第一個動作是搜尋 wiki,不是翻程式碼。**
|
||||
|
||||
做法(30 秒,省下大量白工):
|
||||
grep -rin "<本題關鍵字>" system-dev/wiki/ 2>/dev/null || grep -rin "<關鍵字>" .claude/wiki/
|
||||
|
||||
為什麼這是划算的:
|
||||
• wiki 是前人已經查過、驗證過、被負責人糾正過的結論——**判準**。
|
||||
• 程式碼與歷史文件是**稿子**:它反映「還沒清乾淨」,不等於「還在用」。
|
||||
從稿子推論會系統性得出過時結論。
|
||||
• wiki 沒記載,才值得花力氣翻原文。
|
||||
|
||||
三條硬規則:
|
||||
1. **wiki 與程式碼衝突 → 以 wiki 為準**,並在回報中明確指出衝突,
|
||||
不要自行用 code 推翻 wiki。
|
||||
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。
|
||||
那是當時狀態,不是永久禁令;條件已滿足就是可動。
|
||||
(2026-07-20 實際事故:agent 只看到「不可動」就回報不能動,
|
||||
實際上解除條件早已滿足,害負責人被擋三輪。)
|
||||
3. 翻原文後若得到**新結論**,回報時明講「wiki 該更新」——wiki 過時是債,要還。
|
||||
"""
|
||||
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"additionalContext": guidance
|
||||
}
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
|
||||
exit 0
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/bin/bash
|
||||
# wiki-first-search.sh — PreToolUse hook:要去翻原文/程式碼前,先把 wiki 命中結果推到眼前
|
||||
#
|
||||
# 病根(2026-07-20 leo 點破,mistakes 第一鐵律):
|
||||
# 總管 session 開頭讀了 agent-memory 前 50 行就開工,關鍵那條在第 56 行 → 拿過期記憶擋了 leo 三輪。
|
||||
# leo:「如果你不是讀而是**搜尋** wiki 就不會只讀 50 行就下定論,
|
||||
# 而是就像我直接在頁面 cmd+F,那些都會高亮。」
|
||||
#
|
||||
# 設計要點(為什麼是這個形狀):
|
||||
# 1. **搜尋 ≠ 通讀**:開場 push 全文(session-start-recall.sh)解決不了這題——量大必然只讀開頭。
|
||||
# 這支反過來:在「你正要去查 code/原文」的當下,用你自己的關鍵字 grep wiki,只推命中行。
|
||||
# 2. **時機是關鍵**:不是開場推、不是寫入時擋,而是**查詢動作發生的那一刻**介入。
|
||||
# 3. **提醒不阻擋**(exit 0):wiki 沒記載時本來就該去翻原文,擋下來反而礙事。
|
||||
# 唯一目的是消滅「不知道 wiki 有寫」這件事。
|
||||
#
|
||||
# 觸發:Grep / Glob / Read 打向 code 或 docs 時(見下方 should_check)。
|
||||
# 輸出:stdout 注入 context(命中的 wiki 行 + 檔名:行號)。
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL=$(printf '%s' "$INPUT" | python3 -c "import json,sys;print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "")
|
||||
|
||||
# 取出這次查詢的關鍵字:Grep 用 pattern,Glob/Read 用路徑的檔名部分
|
||||
QUERY=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys,os,re
|
||||
try:
|
||||
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||
q = ti.get('pattern') or ''
|
||||
if not q:
|
||||
p = ti.get('file_path') or ti.get('path') or ''
|
||||
q = os.path.splitext(os.path.basename(p))[0] if p else ''
|
||||
if not q:
|
||||
# Bash:2026-07-21 補的破口——原版只掛 Grep|Glob|Read,
|
||||
# 但「用 curl/wrangler 亂試部署方法」走的是 Bash,整支 hook 不觸發。
|
||||
# leo 當場點破:wiki 早記著「寄信已驗證可用」,我卻沒查又自創方法。
|
||||
# 只認「會動到外部系統/部署」的高風險指令,避免每個 ls 都洗版。
|
||||
cmd = ti.get('command') or ''
|
||||
if re.search(r'\b(wrangler|curl|npx|acr|gh|deploy|push)\b', cmd):
|
||||
# 取指令中最具識別度的詞(worker 名/資源名/子命令)當搜尋詞
|
||||
cand = re.findall(r'[A-Za-z_][A-Za-z0-9_-]{4,}', cmd)
|
||||
skip = {'https','http','client','accounts','workers','scripts',
|
||||
'application','content','Authorization','Bearer','python3',
|
||||
'curl','npx','bash','echo','grep','local','branch','origin'}
|
||||
cand = [c for c in cand if c not in skip and not c.startswith('-')]
|
||||
q = max(cand, key=len) if cand else ''
|
||||
# grep pattern 常含 regex 元字元;取最長的英數/底線詞當搜尋詞
|
||||
words = re.findall(r'[A-Za-z_][A-Za-z0-9_]{3,}', q)
|
||||
print(max(words, key=len) if words else '')
|
||||
except Exception:
|
||||
print('')
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
[ -z "$QUERY" ] && exit 0
|
||||
|
||||
WIKI_DIR="system-dev/wiki"
|
||||
[ -d "$WIKI_DIR" ] || exit 0
|
||||
|
||||
# 只在「查程式碼/文件」時提醒;查 wiki 本身就不用了(已經在讀了)
|
||||
TARGET=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||
print(ti.get('file_path') or ti.get('path') or '')
|
||||
except Exception: print('')
|
||||
" 2>/dev/null || echo "")
|
||||
case "$TARGET" in
|
||||
*system-dev/wiki*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# grep wiki(不分大小寫、含行號),最多 12 行避免洗版
|
||||
HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -12 || true)
|
||||
[ -z "$HITS" ] && exit 0
|
||||
|
||||
COUNT=$(printf '%s\n' "$HITS" | wc -l | tr -d ' ')
|
||||
|
||||
echo "════════════════════════════════════════════════"
|
||||
printf '📚 wiki 已有「%s」的記載(%s 處,先看這裡再翻原文)\n' "$QUERY" "$COUNT"
|
||||
echo "════════════════════════════════════════════════"
|
||||
printf '%s\n' "$HITS" | sed 's|^system-dev/wiki/| |'
|
||||
echo ""
|
||||
echo "⚠️ wiki 是判準,程式碼與歷史文件只是稿子(mistakes 第一鐵律)。"
|
||||
echo " • 上面若與你將要查的原文衝突 → **以 wiki 為準**,別用 code 推翻 wiki。"
|
||||
echo " • 看到「不可動/待廢除/進行中」→ 先讀它的**解除條件**並逐條核對,"
|
||||
echo " 那是當時狀態不是永久禁令;條件已滿足就是可動。"
|
||||
echo " • wiki 沒答案才值得翻原文——翻完若得到新結論,**回頭更新 wiki**。"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
+1
-24
@@ -8,11 +8,6 @@
|
||||
"type": "command",
|
||||
"command": "bash .claude/hooks/pre-write-guard-no-table.sh",
|
||||
"timeout": 5
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash .claude/hooks/sdd-guard.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -25,25 +20,7 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Grep|Glob|Read|Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,12 @@
|
||||
> **上游約束(InkStoneCo 總管)**:此目錄(原 matrix/kbdb,已改名 kbdb-graph-plugin)是 InkStoneCo 子專案,受頂層知識庫約束。
|
||||
> 動工前讀 `github.com/uncle6me-web/InkStoneCo` 的 CLAUDE.md + `docs/3-specs/matrix-rearrange/`。
|
||||
> **定位(leo 2026-06-13)**:此 repo = **KBDB-graph 插件**(triplet 採集 + graph 查詢),類比 **Apache AGE 之於 Postgres**。基本盤 = `arcrun/kbdb`(D1 三表 + CRUD API),**不在這裡、不動它**。
|
||||
> **本目錄專屬交棒見 `docs/HANDOFF-kbdb-plugin.md`**,SDD 見 `system-dev/docs/3-specs/`(2026-07-17 已從舊 `docs/3-specs/` 遷移統一)。
|
||||
> **本目錄專屬交棒見 `docs/HANDOFF-kbdb-plugin.md`**,SDD 見 `docs/3-specs/kbdb-graph-extraction/`。
|
||||
|
||||
> 本檔案由 Claude Code 自動讀取。所有在此目錄下的開發必須遵守以下規則。
|
||||
|
||||
---
|
||||
|
||||
## 📐 SDD 生命週期鐵律(leo 2026-07-17 拍板,全文見 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`)
|
||||
|
||||
1. **單一活性**:任何時刻整個 repo 最多一份 `status: active` 的 SDD,所有開發任務唯一對應它的 tasks;找不到對應任務 → 停下來問,不准直接做。
|
||||
2. **禁止自行建立 SDD**:澄清問題→回答不動文件;任務層變更→更新現行 SDD 的 tasks(標日期原因);規格層變更→走第 3 條。
|
||||
3. **規格變更只有一條路**:proposal 寫進 `system-dev/docs/3-specs/pending-changes.md`(摘要+原因+影響分析)→ **停止**等使用者「confirm」。
|
||||
4. **開新 SDD 的唯一時機**:confirm 後——先把舊 SDD 未完成任務逐條搬入新 SDD(做完前不准寫 code)→ 舊的標 closed + superseded_by 移入 `archive/` → 列搬移/作廢清單請最終確認。
|
||||
5. **每次 session 開始**:讀 active SDD 與 pending-changes.md,回報三個數字(現行規格〈名稱〉/未完成任務 N/待裁決 proposal M)。
|
||||
|
||||
> **本 repo 現況(2026-07-17)**:**0 份 active**(無現行開發,實作面已收斂、剩跨 repo 接通)——0 份 active 合法。三份 `paused`(ingest-contract / kbdb-graph-extraction / plugin-install,等基本盤部署/跨 repo 接通),兩份 `closed` 入 `archive/`。**重啟開發時先挑一份升 active**(一份、只能一份),再動 code——sdd-guard hook 會擋 0-active 下的 code 寫入。
|
||||
|
||||
---
|
||||
|
||||
## 🔒 KBDB 鐵律(leo 2026-06-14 拍板,最高原則,違反會被 hook exit 2 擋下)
|
||||
|
||||
決策全文:`InkStoneCo/docs/3-specs/matrix-rearrange/DECISION-kbdb-v3-baseplane.md`。
|
||||
@@ -45,43 +33,20 @@
|
||||
GET /records/by-template/:tpl ─ entity 正規化 → template='entity'(exact match)
|
||||
─ 唯一對外通道 = src/lib/kbdb-client.ts
|
||||
```
|
||||
基本盤 API 契約詳見 `system-dev/docs/3-specs/kbdb-graph-extraction/design.md`。
|
||||
基本盤 API 契約詳見 `docs/3-specs/kbdb-graph-extraction/design.md`。
|
||||
**基本盤缺口**(base 無 `PUT/DELETE /records/:id`、無 vectorize 語意搜尋)標 `[→arcrun]`,不得為此自建表。embedding/語意搜尋屬基本盤 optional embed 模組,**不是插件職責**。
|
||||
|
||||
---
|
||||
|
||||
## 🔴 第一鐵律:wiki 是判準,不准跳過(2026-07-20/21 leo 兩度點破)
|
||||
|
||||
**要查任何東西之前,先搜尋 wiki——用 grep,不是只讀開頭幾行。**
|
||||
|
||||
> leo:「花很多力氣去產生 wiki,最重要的就是要可以查詢,**結果要查的時候就跳過,那就白寫了**。」
|
||||
> 「重點是你自己的記憶對嗎?而你有按照規定去切實讀 wiki 嗎?」
|
||||
|
||||
```bash
|
||||
grep -rin "<本題關鍵字>" system-dev/wiki/
|
||||
```
|
||||
|
||||
**三條硬規則**:
|
||||
1. **wiki 與程式碼/歷史文件衝突 → 以 wiki 為準**。程式碼反映「還沒清乾淨」,不等於「還在用」。
|
||||
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。那是當時狀態,不是永久禁令。
|
||||
3. 翻原文後得到新結論 → **回頭更新 wiki**(wiki 過時是債,要還)。
|
||||
|
||||
**動外部系統(部署/curl/wrangler/acr/gh)前**:先找 repo 有沒有**現成腳本或 README 部署段**,
|
||||
別自創方法。(實例:2026-07-21 明明有 `npx wrangler deploy` 這條驗過的路,卻自己 curl 硬幹踩坑。)
|
||||
|
||||
> hook `wiki-first-search.sh` 會在你查 code/下高風險指令時自動推 wiki 命中行;
|
||||
> **但機制只是提醒,判斷是你的責任**。
|
||||
|
||||
|
||||
## Wiki 讀取順序(LLM 記憶系統,CC 維護)
|
||||
|
||||
| 檔案 | 時機 | 用途 |
|
||||
|------|------|------|
|
||||
| `system-dev/wiki/status.md` | session 開始第一件事 | 當前進度、下一步 |
|
||||
| `system-dev/wiki/mistakes.md` | 做新功能前 | 已知誤解、避坑清單 |
|
||||
| `system-dev/wiki/decisions-summary.md` | 設計判斷時 | 架構決策摘要 |
|
||||
| `.claude/wiki/status.md` | session 開始第一件事 | 當前進度、下一步 |
|
||||
| `.claude/wiki/mistakes.md` | 做新功能前 | 已知誤解、避坑清單 |
|
||||
| `.claude/wiki/decisions-summary.md` | 設計判斷時 | 架構決策摘要 |
|
||||
|
||||
文件分類規則見 `docs/README.md`;SDD 在 `system-dev/docs/3-specs/[子系統]/`(design.md + tasks.md),動手前必須有這兩個檔案,且遵守上方生命週期鐵律。
|
||||
文件分類規則見 `docs/README.md`;SDD 在 `docs/3-specs/[子系統]/`(design.md + tasks.md),動手前必須有這兩個檔案。
|
||||
session 結束用 `/wiki-update` 更新 status.md,被糾正後 `/wiki-capture` 寫進 mistakes.md。
|
||||
|
||||
---
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
},
|
||||
"nodes": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
@@ -69,26 +69,11 @@
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "節點名(須對應某 triplet 的 subject/object 原字面)。實體詞條=正規名;wikilink 卡=卡標題。"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "去重鍵。wikilink 卡用【檔名】→ 一卡一 node,被多條邊指到也只 embed 一次,不以出現次數重複。實體詞條用正規名。選填(無則以 name 去重)。"
|
||||
"description": "節點名(須對應某 triplet 的 subject 或 object 原字面)。"
|
||||
},
|
||||
"gloss": {
|
||||
"type": "string",
|
||||
"description": "一句話描述。base embed 對【名 + gloss 一起】embedding(實體同義詞字面差太遠,靠描述拉近)。選填(建議 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 不算向量。"
|
||||
"description": "一句話描述,供 embedding。例如 'Graph RAG — 用關係遍歷檢索、保住異見的 RAG 變體'。選填(建議 deep tier 產出)。"
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string",
|
||||
@@ -101,7 +86,7 @@
|
||||
"triplets": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "邊(關係)。ingest 只產原始 (s,p,o) + confidence + 謂詞向量打標。端點(s/o)以字面 match nodes[].name。",
|
||||
"description": "邊(關係)。ingest 只產原始 (s,p,o) + confidence。",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["subject", "predicate", "object"],
|
||||
@@ -110,16 +95,10 @@
|
||||
"subject": { "type": "string", "minLength": 1, "description": "主詞(實體名,須與 nodes[].name 對得上若有提供)" },
|
||||
"predicate": { "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 不據此過濾,只記錄。" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$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,9 +0,0 @@
|
||||
# 已遷移
|
||||
|
||||
本目錄的 SDD 已於 2026-07-17 全數遷移至 **`system-dev/docs/3-specs/`**(依 2026-06-14 位置統一規則)。
|
||||
|
||||
- 現行/暫停 SDD:`system-dev/docs/3-specs/`(ingest-contract / kbdb-graph-extraction / plugin-install)
|
||||
- 已封存死件:`system-dev/docs/3-specs/archive/`(arcrun-key-auth / blocks-edit-api)
|
||||
- 生命週期鐵律:`system-dev/docs/3-specs/SDD-LIFECYCLE.md`
|
||||
|
||||
請勿在此新增 SDD。
|
||||
-7
@@ -1,10 +1,3 @@
|
||||
---
|
||||
status: closed # active | draft | paused | closed(生命週期鐵律見 ../../SDD-LIFECYCLE.md)
|
||||
superseded_by: ""
|
||||
---
|
||||
|
||||
> **封存(2026-07-17)**:舊 KBDB 時代草稿(等 richblack review,帳號已 suspend)。key auth 屬基本盤 arcrun/kbdb 職責,非本插件範圍(2026-06-14 API-as-Wall 改寫後失效)。
|
||||
|
||||
# KBDB — Arcrun Key Auth
|
||||
|
||||
> 建立:2026-05-05
|
||||
-7
@@ -1,10 +1,3 @@
|
||||
---
|
||||
status: closed # active | draft | paused | closed(生命週期鐵律見 ../../SDD-LIFECYCLE.md)
|
||||
superseded_by: ""
|
||||
---
|
||||
|
||||
> **封存(2026-07-17)**:基於舊「萬物皆 Block/blocks 表」架構,該架構已判定為違規殘留並刪除(2026-06-14 改寫);base `PATCH /records/:id` 已由 Arcrun #6 實作取代本需求。
|
||||
|
||||
# KBDB — Blocks Edit API
|
||||
|
||||
> **建立**:2026-05-06
|
||||
-5
@@ -1,8 +1,3 @@
|
||||
---
|
||||
status: paused # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md)
|
||||
superseded_by: ""
|
||||
---
|
||||
|
||||
# ingest-contract — 設計
|
||||
|
||||
> **藍圖在頂層**:本 SDD 只放 **kbdb-graph-plugin 內部實作細節**。跨專案脈絡(為什麼拆 ingest/graph、mira 蒸發、整體資料流)見 InkStoneCo `docs/3-specs/mira-dissolve/`(design + requirements)。
|
||||
-5
@@ -1,8 +1,3 @@
|
||||
---
|
||||
status: paused # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md)
|
||||
superseded_by: ""
|
||||
---
|
||||
|
||||
# KBDB-graph 抽出 — Design
|
||||
|
||||
> 建立:2026-06-14
|
||||
-5
@@ -1,8 +1,3 @@
|
||||
---
|
||||
status: paused # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md)
|
||||
superseded_by: ""
|
||||
---
|
||||
|
||||
# KBDB-graph 插件安裝 — Design
|
||||
|
||||
## 目標
|
||||
Generated
+1686
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
content_hash?: string;
|
||||
source_anchor?: string;
|
||||
predicate_embed?: boolean; // 謂詞向量化打標(ingest 打標、base 讀標執行);graph 不算向量
|
||||
};
|
||||
|
||||
/** 建立三元組 → POST /records(template=triplet)。 */
|
||||
@@ -49,7 +48,6 @@ export async function createTriplet(
|
||||
if (data.source_uri) values.source_uri = data.source_uri;
|
||||
if (data.content_hash) values.content_hash = data.content_hash;
|
||||
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);
|
||||
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 { TPL_TRIPLET, ensurePluginTemplates, recordToTriplet } from '../lib/templates';
|
||||
import { createTriplet } from './triplet-crud';
|
||||
import { persistNodes } from './node-persist';
|
||||
|
||||
// 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({
|
||||
name: z.string().min(1),
|
||||
id: z.string().optional(), // 去重鍵(wikilink 卡用檔名 → 一卡一 node,多邊指到不重建)
|
||||
aliases: z.array(z.string()).optional(), // 同義詞,base collapse 成同一 node
|
||||
gloss: z.string().optional(),
|
||||
embed: z.boolean().optional(), // 向量化打標,base 讀標執行(預設 true)
|
||||
entity_type: z.enum(['person', 'event', 'product', 'market', 'org']).optional(),
|
||||
}).strict();
|
||||
|
||||
@@ -24,7 +18,6 @@ const EdgeSchema = z.object({
|
||||
subject: z.string().min(1),
|
||||
predicate: z.string().min(1),
|
||||
object: z.string().min(1),
|
||||
predicate_embed: z.boolean().optional(), // 謂詞向量化打標,base 讀標執行(預設 true)
|
||||
confidence: z.number().min(0).max(1).optional(),
|
||||
}).strict();
|
||||
|
||||
@@ -66,14 +59,13 @@ export async function ingestEnvelope(
|
||||
return { skipped: true, ingested: 0, deprecated: 0 };
|
||||
}
|
||||
|
||||
// 1) 先 append 新批 active(透傳 predicate_embed 打標,供 base embed 模組讀標執行)。
|
||||
// 1) 先 append 新批 active。
|
||||
for (const e of env.triplets) {
|
||||
await createTriplet(client, {
|
||||
subject: e.subject,
|
||||
predicate: e.predicate,
|
||||
object: e.object,
|
||||
confidence: e.confidence,
|
||||
predicate_embed: e.predicate_embed,
|
||||
source_block_id: env.source.block_id,
|
||||
source_uri: env.source.uri,
|
||||
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 在前 → 無空窗)。
|
||||
for (const old of priorActive) {
|
||||
await client.updateRecord(old.id, { status: 'deprecated', superseded_by: env.source.content_hash });
|
||||
|
||||
+8
-32
@@ -36,12 +36,6 @@ export class KbdbClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly token?: string,
|
||||
// 2026-07-03 補跑實測發現的坑:Cloudflare 會擋 Worker → 另一個 *.workers.dev
|
||||
// Worker 的直連 fetch(error code 1042,loop-prevention on shared workers.dev zone)。
|
||||
// self-hosted 帳號通常沒有自訂域名可用,正規解法 = Service Binding
|
||||
// (wrangler.toml `[[services]]`),由 CF 內部直接路由、不經公開網路。
|
||||
// 有綁定時優先走它;沒有(例如本地 dev/mock)則 fallback 回全域 fetch。
|
||||
private readonly fetcher?: { fetch: typeof fetch },
|
||||
) {
|
||||
if (!baseUrl) {
|
||||
throw new Error('KBDB_BASE_URL 未設定:插件需指向基本盤 API(不可直連 D1)');
|
||||
@@ -52,8 +46,7 @@ export class KbdbClient {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
||||
|
||||
const doFetch = this.fetcher ? this.fetcher.fetch.bind(this.fetcher) : fetch;
|
||||
const res = await doFetch(this.baseUrl.replace(/\/$/, '') + path, {
|
||||
const res = await fetch(this.baseUrl.replace(/\/$/, '') + path, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
@@ -120,34 +113,23 @@ export class KbdbClient {
|
||||
// --- templates(= 替代建表;插件要新類型只能建 template) ---
|
||||
|
||||
async ensureTemplate(name: string, slots: string[], description?: string): Promise<void> {
|
||||
// 2026-07-03 補跑實測發現的坑:base 的 GET /templates/:name 回傳是
|
||||
// { success, template: { id, slots_json: "[...]" } }(包一層 + slots 是 JSON 字串),
|
||||
// 不是原本假設的 { id, slots } 平鋪陣列。誤判「不存在」會導致對已存在的 name 重複
|
||||
// POST /templates,而 base 對重名衝突沒有回優雅的 409,是直接 500(見 kbdb-graph 實測)。
|
||||
const existing = await this.req<{ template?: { id: string; slots_json?: string } }>(
|
||||
const existing = await this.req<{ id?: string; slots?: string[] } | { error: string }>(
|
||||
'GET',
|
||||
`/templates/${encodeURIComponent(name)}`,
|
||||
).catch(() => null);
|
||||
const tpl = existing?.template;
|
||||
|
||||
// 全新 template → 建。
|
||||
if (!tpl || !tpl.id) {
|
||||
if (!existing || !(existing as any).id) {
|
||||
await this.req('POST', '/templates', { name, slots, description, created_by: 'kbdb-graph' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 既有 template → 補缺 slot(不 early-return;否則 seed 後新增的 slot 永遠進不來)。
|
||||
// 走 base PATCH /templates/:id 增 slot;既有環境免另跑遷移腳本即收斂。
|
||||
let haveList: string[] = [];
|
||||
try {
|
||||
haveList = JSON.parse(tpl.slots_json ?? '[]');
|
||||
} catch {
|
||||
haveList = [];
|
||||
}
|
||||
const have = new Set(haveList);
|
||||
const have = new Set((existing as any).slots ?? []);
|
||||
const missing = slots.filter((s) => !have.has(s));
|
||||
if (missing.length === 0) return;
|
||||
await this.req('PATCH', `/templates/${encodeURIComponent(tpl.id)}`, {
|
||||
await this.req('PATCH', `/templates/${encodeURIComponent((existing as any).id)}`, {
|
||||
slots: [...have, ...missing],
|
||||
});
|
||||
}
|
||||
@@ -193,13 +175,7 @@ function qs(params: Record<string, string | number | undefined>): string {
|
||||
return parts.length ? `?${parts.join('&')}` : '';
|
||||
}
|
||||
|
||||
/** 從 Bindings 建 client。KBDB_BASE_URL 未設時拋錯(不准 fallback 直連 D1)。
|
||||
* 有 KBDB_BASE_SVC(service binding)時優先走它,繞開 workers.dev→workers.dev 的
|
||||
* CF error 1042 封鎖;沒有就退回全域 fetch(本地 dev / 已有自訂域名時仍可用)。 */
|
||||
export function makeKbdbClient(env: {
|
||||
KBDB_BASE_URL?: string;
|
||||
KBDB_INTERNAL_TOKEN?: string;
|
||||
KBDB_BASE_SVC?: { fetch: typeof fetch };
|
||||
}): KbdbClient {
|
||||
return new KbdbClient(env.KBDB_BASE_URL ?? '', env.KBDB_INTERNAL_TOKEN, env.KBDB_BASE_SVC);
|
||||
/** 從 Bindings 建 client。KBDB_BASE_URL 未設時拋錯(不准 fallback 直連 D1)。 */
|
||||
export function makeKbdbClient(env: { KBDB_BASE_URL?: string; KBDB_INTERNAL_TOKEN?: string }): KbdbClient {
|
||||
return new KbdbClient(env.KBDB_BASE_URL ?? '', env.KBDB_INTERNAL_TOKEN);
|
||||
}
|
||||
|
||||
@@ -18,13 +18,9 @@ export const TRIPLET_SLOTS = [
|
||||
// source_uri+content_hash 承載 ingest idempotency(按 source_uri 分組 deprecate)。
|
||||
// source_anchor 供 get_source 精準回跳原文(T3.7)。
|
||||
'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 對象。
|
||||
// embed(ingest#1 升格,2026-06-26):向量化打標,base embed 模組讀標執行;graph 不算向量。
|
||||
// node_id(去重鍵):wikilink 卡用檔名,一卡一 node、多邊指到只 embed 一次(base 讀此鍵歸一)。
|
||||
export const ENTITY_SLOTS = ['canonical', 'aliases_json', 'entity_type', 'owner', 'gloss', 'embed', 'node_id'];
|
||||
export const ENTITY_SLOTS = ['canonical', 'aliases_json', 'entity_type', 'owner', 'gloss'];
|
||||
export const ENTITY_PENDING_SLOTS = [
|
||||
'raw_name', 'candidate_entity_id', 'candidate_canonical', 'similarity',
|
||||
];
|
||||
|
||||
@@ -6,11 +6,6 @@ export type Bindings = {
|
||||
KBDB_BASE_URL?: string; // 基本盤 arcrun/kbdb API 網址(leo: 可設定,先留空)
|
||||
KBDB_INGEST_URL?: string; // ingest 服務網址(refresh 代轉對象;T4 就緒前留空)
|
||||
KBDB_INTERNAL_TOKEN?: string;
|
||||
// Service Binding(wrangler.toml [[services]])→ 直連基本盤 worker,繞開
|
||||
// CF error 1042(Worker 不能公開 fetch 另一個 *.workers.dev Worker)。
|
||||
// 2026-07-03 補跑實測發現:self-hosted 帳號常無自訂域名,KBDB_BASE_URL 單靠公開
|
||||
// fetch 在 workers.dev 對 workers.dev 場景會被 CF 擋,故新增此綁定作為正規解法。
|
||||
KBDB_BASE_SVC?: Fetcher;
|
||||
ENVIRONMENT: string;
|
||||
API_KEY?: string;
|
||||
};
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.16.1
|
||||
1.10.0
|
||||
@@ -1,39 +0,0 @@
|
||||
# SDD 生命週期鐵律(不可違反)
|
||||
|
||||
> 來源:leo 2026-07-17 拍板。
|
||||
> 適用:`system-dev/docs/3-specs/` 下的「規格 SDD」(requirements/design/tasks 三件式資料夾)。
|
||||
> **不適用**:派工表/sprint 檔、journeys/ 卷宗、TEMPLATE-sdd、README、pending-changes.md——它們不是 SDD,不掛 status。
|
||||
|
||||
## 狀態標記(機器可查)
|
||||
|
||||
每個 SDD 資料夾的 `design.md` 最上方掛 YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
status: active # active | draft | paused | closed
|
||||
superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名
|
||||
---
|
||||
```
|
||||
|
||||
- `active`:現行規格,全 repo 開發任務唯一對應源。**任何時刻整個 repo 最多一份。**
|
||||
- `draft`:起草中,尚未採納。
|
||||
- `paused`:動過工、暫停中;恢復=升回 active(先收掉現任 active)或被新 SDD 繼承。
|
||||
- `closed`:已完成或被取代;被取代者填 `superseded_by` 並移入 `3-specs/archive/`。
|
||||
|
||||
## 五條鐵律
|
||||
|
||||
1. **單一活性**:任何時刻只允許一份 `status: active`。所有開發任務必須對應這份 SDD 的 tasks。找不到對應任務 → 停下來問,不准直接做。
|
||||
2. **禁止自行建立 SDD**:CC 在任何情況下不得主動建新 SDD。收到使用者意見先分類:澄清問題→回答即可不動文件;任務層變更(不影響核心設計)→更新現行 SDD 的 tasks 區段並標日期與原因;規格層變更(核心設計/方向改變)→走第 3 條,不准直接改 spec。
|
||||
3. **規格變更只有一條路**:產出 change proposal 寫入 `system-dev/docs/3-specs/pending-changes.md`(變更摘要與觸發原因+影響分析:現行 SDD 哪些任務作廢/修改/不受影響/尚未完成),然後**停止**,等使用者明說「confirm」。沒 confirm 就繼續依現行 SDD 工作。多個 proposal 可並存緩衝區、由人一次裁決——CC 的速度導向影響分析,不是規格增生。
|
||||
4. **開新 SDD 的唯一時機**:使用者 confirm 一份規格層 proposal 時,依序:
|
||||
a. 舊 SDD 未完成且仍有效的任務**逐條搬入**新 SDD 的 tasks——**這步做完前不准寫任何程式碼**(強迫顯式盤點,遺漏會在 d 的清單被看到,而不是三天後才發現)。
|
||||
b. 舊 SDD frontmatter 改 `status: closed, superseded_by: <新SDD>`,資料夾移入 `3-specs/archive/`。
|
||||
c. 新 SDD 的 changelog 首行記錄:繼承自哪份、為何取代。
|
||||
d. 向使用者列出「已搬移任務清單」與「已作廢任務清單」請求最終確認。
|
||||
5. **每次 session 開始**:先讀現行 active SDD 與 pending-changes.md,回報三個數字——「現行規格〈名稱〉+未完成任務 N+待裁決 proposal M」——再開始工作。若回報出現兩份 active=規則已被違反,當場糾正。
|
||||
|
||||
## 硬約束(不信任單點自律,用結構保證不變量)
|
||||
|
||||
- `.claude/hooks/sdd-guard.sh`(PreToolUse Write|Edit):active 數 >1 → 任何寫檔一律擋;寫 code 檔需恰好 1 份 active。
|
||||
- `scripts/sdd-active-check.sh`:獨立檢查,pre-commit / CI 可掛,違反 exit 1。
|
||||
- 誠實限制:hook 只擋語法層明顯違規,繞道可行但留痕可審;不聲稱不可繞過。
|
||||
@@ -1,10 +1,6 @@
|
||||
---
|
||||
status: draft # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md)
|
||||
superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名
|
||||
---
|
||||
|
||||
# [子系統名稱] — Design
|
||||
|
||||
> 狀態:[草稿 / 審核中 / 已採納 / 已廢棄]
|
||||
> 建立:[YYYY-MM-DD] | 最後更新:[YYYY-MM-DD]
|
||||
> 負責人:[名稱]
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Pending Changes(規格變更緩衝區)
|
||||
|
||||
> 規則來源:`SDD-LIFECYCLE.md` 第 3、4 條。
|
||||
> 規格層變更(核心設計/方向改變)**只有這一條路**:CC 把 change proposal 寫進「待裁決」——
|
||||
> 變更摘要與觸發原因+影響分析(現行 SDD 哪些任務作廢/修改/不受影響/尚未完成)——然後**停止**,
|
||||
> 等使用者明說「confirm」才依第 4 條開新 SDD;沒 confirm 就繼續依現行 SDD 工作。
|
||||
> 多個 proposal 可並存,由人一次裁決。本檔不是 SDD,不掛 status。
|
||||
|
||||
## 待裁決
|
||||
|
||||
(無)
|
||||
|
||||
## 已裁決
|
||||
|
||||
(無——裁決後從「待裁決」移到這裡留底,標 confirmed / rejected + 日期。)
|
||||
@@ -1,81 +0,0 @@
|
||||
# Logseq 任務 marker 解析(單一真相源)
|
||||
|
||||
> **這是「Logseq 原生任務語法」解析的唯一權威規格。** 任何要從 Logseq graph
|
||||
> 抓任務狀態的功能,一律 import 這份、不得各寫一份自己的 mapping。
|
||||
>
|
||||
> **已知兩個消費者**(共用同一套解析,見各自 issue):
|
||||
> 1. **vault 萃取**(`/wiki-extract`,template#5):marker → 卡片 frontmatter `task_status`。
|
||||
> 2. **tasks→Project 投影**(`system-dev/workflows/tasks-project-sync.*`,template#4):
|
||||
> 當投影來源是 Logseq graph(notes/kb)時,用這份判斷任務與狀態。
|
||||
>
|
||||
> 兩者**只共用「怎麼 parse」**(哪幾行是任務、marker 是什麼、正規狀態是什麼、跳過什麼);
|
||||
> parse 完各自要「拿狀態做什麼」(寫卡 vs 投影 issue)不同,那部分各管各的。
|
||||
|
||||
---
|
||||
|
||||
## 為什麼不是 GFM checkbox(規格更正,leo 2026-07-04 發現)
|
||||
|
||||
Logseq 的原生任務**不是** GFM 的 `- [ ]` / `- [x]`,而是**大寫 marker 開頭的 block**:
|
||||
|
||||
```
|
||||
- TODO AI 查看 leo21c 內所有 Repo,找到本地 Repo 搬到 Gitea
|
||||
- DOING 建立知識總庫,可查所有子庫
|
||||
- DONE 手機和電腦 Logseq 可以被放進知識總庫
|
||||
```
|
||||
|
||||
若照舊規格只抓 `- [ ]` checkbox,**notes / kb 兩個 Logseq graph 的任務會全數漏抓**。
|
||||
|
||||
> 兩種源、兩套語法、同一條下游管線:
|
||||
> - **SDD `tasks.md`**(各 repo `system-dev/docs/3-specs/**`)→ GFM checkbox(現行不變)。
|
||||
> - **Logseq graph(notes / kb)** → 本檔的大寫 marker。
|
||||
|
||||
---
|
||||
|
||||
## 解析規格
|
||||
|
||||
### 1. 任務行辨識(regex)
|
||||
|
||||
```
|
||||
^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+
|
||||
```
|
||||
|
||||
- marker 必須是 block(`-` bullet)的**開頭第一個 token**、全大寫、後接空白。
|
||||
- `CANCELED` 與英式 `CANCELLED` 皆收(Logseq 兩種都產)。
|
||||
- marker 後面到行尾(或到子 bullet 之前)是**任務內文**。
|
||||
|
||||
### 2. marker → 正規狀態(task_status)
|
||||
|
||||
| Logseq marker | 正規 task_status |
|
||||
|---------------|------------------|
|
||||
| `TODO`、`LATER` | `todo` |
|
||||
| `DOING`、`NOW` | `in-progress` |
|
||||
| `WAITING` | `blocked` |
|
||||
| `DONE` | `done` |
|
||||
| `CANCELED`、`CANCELLED` | `closed` |
|
||||
|
||||
> `LATER`/`NOW` 是 Logseq「排程視圖」用的同義 marker(LATER≈TODO、NOW≈DOING),
|
||||
> 正規化後與 TODO/DOING 併軌,下游不必區分。
|
||||
|
||||
### 3. 必須跳過的東西(別當任務內文)
|
||||
|
||||
Logseq 的任務 block 底下常掛時間戳與屬性行,這些**不是內文**,解析時整段略過:
|
||||
|
||||
- **`:LOGBOOK:` … `:END:` 區塊**:marker 被點擊計時產生的時間戳紀錄。
|
||||
遇到 `:LOGBOOK:` 那行起、到 `:END:` 那行止(含兩端),整塊丟掉。
|
||||
- **屬性行 `key:: value`**:如 `collapsed:: true`、`id:: 65a...`、`SCHEDULED:: <...>`、
|
||||
`DEADLINE:: <...>`。凡符合 `^\s*[\w-]+:: ` 的行都是屬性,不是內文。
|
||||
(`SCHEDULED`/`DEADLINE` 的日期若下游要用可另抓,但**不得當任務描述文字**。)
|
||||
|
||||
### 4. 巢狀子 bullet
|
||||
|
||||
任務 block 底下縮排的子 bullet 是該任務的補充說明(非獨立任務,除非子 bullet 自己也帶 marker)。
|
||||
萃取時可併入該任務的描述脈絡;投影時只取母 block 那行當任務標題。
|
||||
|
||||
---
|
||||
|
||||
## 自檢(實作或 LLM 執行前跑一遍)
|
||||
|
||||
- [ ] 用的是大寫 marker regex,**不是** `- [ ]` checkbox。
|
||||
- [ ] 八個 marker 全部覆蓋(含 `LATER`/`NOW`/`CANCELLED` 別漏)。
|
||||
- [ ] `:LOGBOOK:...:END:` 與 `key:: value` 屬性行有跳過,沒混進任務文字。
|
||||
- [ ] 狀態用上表**正規名**(`todo`/`in-progress`/`blocked`/`done`/`closed`),不是原始 marker 字面。
|
||||
@@ -126,23 +126,10 @@ gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,
|
||||
|
||||
- [自包含改寫的要點,不寫「詳見原文」]
|
||||
|
||||
## 實體
|
||||
|
||||
> 本卡內文的關鍵實體(也是 graph node)。名+描述一起供下游 embedding normalize。
|
||||
> AI 生產、人不必讀;集中放、一實體一行、不縮排、不重複。
|
||||
- **原子筆記**(atomic note/卡片原子化)— 每張卡只承載一個不可再分論點的知識記錄單元。
|
||||
- **傳統筆記**(大鍋炒筆記)— 把多主題混雜在同一篇、難精確引用的記錄方式。
|
||||
|
||||
## 關聯
|
||||
|
||||
### 內文知識關係(內文實體間;端點=上方 `## 實體` 的正規名,一字不差)
|
||||
|
||||
- 原子筆記 >> 對立於 >> 傳統筆記
|
||||
- 傳統筆記 >> 犧牲 >> 精確引用
|
||||
|
||||
### 卡片關係(卡對卡)
|
||||
|
||||
- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]]
|
||||
- [[原子筆記]] >> 是其最小單元 >> [[卡片盒筆記法]]
|
||||
```
|
||||
|
||||
### 架構:三層 + 標籤橫切(183 卡實證)
|
||||
@@ -160,22 +147,15 @@ cards/<bucket>/
|
||||
- **frontmatter `tags:` 而非行內 `#tag`**:內文常用 `#`(如 `#猜想`),行內標籤會讓 ingest 分不清「分類」與「內文範例」污染 graph;frontmatter 零歧義。標籤只能用 `TAXONOMY.md` 列出的;**禁止繞過字典在卡片直接冒新標籤**,但字典可受控擴充(遇新軸先查重、確認非同義詞,再登記進本 repo 的 TAXONOMY.md)。
|
||||
- **麵包屑帶路徑**:H1 次行 `← [[<bucket>/00-INDEX]]`。指 `00-INDEX` 因固定名跨桶撞名,**一律帶路徑**;卡片間連結用裸 `[[卡名]]`。
|
||||
|
||||
### 使用 typed-edge 三元組(抓內文實體關係,不只卡對卡)
|
||||
### 使用 typed-edge 三元組(不只裸 `[[wikilink]]`)
|
||||
|
||||
用**帶語義的三元組** `A >> 謂詞 >> B` 寫進 `## 關聯`。**重點是抓內文裡的實體關係**——卡對卡(`[[卡A]] >> 謂詞 >> [[卡B]]`)只是把既有雙鏈加個動詞、資訊量幾乎沒增加;知識圖譜的價值在內文概念間的關係(`原子筆記 >> 對立於 >> 傳統筆記`,這些 A/B 是內文概念、不是卡標題)。
|
||||
整理時,發現內容與其他頁面有關聯,用**帶語義的三元組**寫進 `## 關聯`,而非只列裸 `[[頁面]]`。裸 `[[A]]` 只說「有關」、沒說關係,下游要建 knowledge graph 還得回讀兩張卡;三元組把關係也預編譯,ingest 直接 parse 出帶類型的有向邊。
|
||||
|
||||
格式 `A >> 謂詞 >> B`,規則:
|
||||
1. **方向性**:必須讀成「A(謂詞)B」一句通順的話;A、B 順序=主→賓真實方向。
|
||||
2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、犧牲),天然帶方向。**禁名詞當謂詞**——`>> 存儲格式 >>`、`>> 操作體驗 >>` 讀不通,是錯的。
|
||||
3. **謂詞自由書寫但別太天馬行空**:寫「參考/參照」皆可(下游 embed 自動聚類同義謂詞),別寫「瞄了一眼」這種抓不到同義的。
|
||||
4. **內文三元組端點用裸文字**(非 `[[wikilink]]`),避免在 Logseq 產生大量紅色斷鏈;卡對卡那層才用 `[[]]`。
|
||||
5. **向後相容**:純 `[[A]]` 仍合法(無類型邊),盡量補謂詞。
|
||||
|
||||
> **★ 硬自檢(Haiku 量產必備護欄)★** —— 內文三元組的「端點 = `## 實體` 詞條」
|
||||
> `A >> 謂詞 >> B` 的 A、B 必須與 `## 實體` 某個粗體正規名【一字不差】。**寫完後逐條自檢**:把 A、B 拿去 `## 實體` 找有沒有完全相同的正規名,沒有 → 這條錯了。
|
||||
> 修法擇一:(a) 改用實體表已有的詞;(b) 端點確是重要實體 → 補進 `## 實體` 再指它。
|
||||
> 禁止:端點帶括號註解、端點是整句補語、端點是形容詞短語。
|
||||
> (實證:光寫規則 Haiku 會略過,端點對不齊 14 條;寫成自檢動作後 14→0。跑 1-2 張看不出,跑 12 張才暴露。)
|
||||
2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、是…的實作),天然帶方向。
|
||||
3. **謂詞自由書寫**,不受控詞彙;下游對謂詞 embedding 時同義謂詞會自動聚類,但方向仍靠書寫順序保證。
|
||||
4. **向後相容**:純 `[[A]]` 仍合法(無類型邊),盡量補謂詞。
|
||||
|
||||
`>>` 為分隔語法,全程一致即可。這是 Karpathy LLM Wiki「知識互連」的強化版——連結不只存在,還帶類型與方向。
|
||||
|
||||
@@ -186,9 +166,7 @@ cards/<bucket>/
|
||||
- **在知識生產的當下、由整理者(CC / Cowork)建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔/跨庫視角,編不出貼合的 gloss。
|
||||
- **選填、deep tier 才產**:淺萃不浪費。
|
||||
- **gloss ≠ 摘要**:`gloss` 是 frontmatter 給機器 normalize 的定義句(「X 是…」);`## 摘要` 是給人讀的核心句。
|
||||
- **兩層 gloss**:① frontmatter `gloss:` 描述「卡標題」這個 node;② `## 實體` 區塊的每行描述句,描述「內文實體」這些 node。**內文實體也是 graph node、也需描述句**才能被下游 embedding normalize(`黃仁勳` vs `Jensen Huang` 靠描述拉近向量)。
|
||||
- **實體要描述、謂詞不用**:實體同義詞字面差遠需描述拉近;謂詞同義詞字面本就近,裸詞 embed 自動聚類。
|
||||
- **對齊下游 envelope**:frontmatter `gloss:` 與 `## 實體` 詞條對應 ingest envelope 的 `nodes[].gloss`。
|
||||
- **對齊下游 envelope**:frontmatter `gloss:` 對應 ingest envelope 的 `nodes[].gloss`。
|
||||
|
||||
> **改寫時必守**:① 絕不寫入 raw source(只往 `cards/<bucket>/` 寫,事後驗 raw source 0 異動);② 檔名=卡片全名,冒號用全形「:」、斜線用全形「/」,全程一種字元避免斷鏈。
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2";
|
||||
# tn = 不換行版(給 prompt 用)
|
||||
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
||||
|
||||
REPO_URL="https://git.uncle6.me/Leo/system-dev-template/raw/branch/main/template"
|
||||
REPO_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/template"
|
||||
# install.sh / update.sh 住在 main/scripts/(不在 template/)。
|
||||
SCRIPTS_URL="https://git.uncle6.me/Leo/system-dev-template/raw/branch/main/scripts"
|
||||
SCRIPTS_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts"
|
||||
CREATED=()
|
||||
SKIPPED=()
|
||||
|
||||
@@ -103,25 +103,6 @@ echo ""
|
||||
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
|
||||
echo ""
|
||||
|
||||
# ── 重複安裝防呆(1.10.1):install 只管「全新安裝」,一切後續歸 update ──
|
||||
# 判準是「裝過沒」,不分新版舊版:
|
||||
# - 新結構 system-dev/ 已存在,或
|
||||
# - 舊結構 .claude/wiki/ 或 .claude/VERSION 存在(裝過舊版、待遷移)
|
||||
# 裝過了還跑 install → 會重複建範本、甚至跟真資料並存(先 install 建空殼,遷移就被擋)。
|
||||
# 正解:偵測到裝過 → 不動任何東西,導去 update(更新/遷移/補新檔都由它處理)。
|
||||
if [ -d "system-dev" ] || [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ]; then
|
||||
t "🛑 偵測到這個專案已經安裝過 system-dev-template。" \
|
||||
"🛑 system-dev-template is already installed in this project."
|
||||
t " 後續的更新、遷移、補新檔,一律由「更新腳本」處理(不要重跑 install):" \
|
||||
" All updates, migrations, and new-file additions are handled by the UPDATER (don't re-run install):"
|
||||
echo ""
|
||||
echo " curl -sSL https://git.uncle6.me/Leo/system-dev-template/raw/branch/main/scripts/update.sh | bash"
|
||||
echo ""
|
||||
t " (重跑 install 可能建出空白範本、跟你的真資料並存,故在此停止。)" \
|
||||
" (Re-running install could create empty templates alongside your real data, so it stops here.)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 偵測 vault 類型 → 決定 raw source(原始文件)路徑 ──────────
|
||||
# 為什麼:這個模板原本假設「原始文件在 docs/」,但 Logseq / Obsidian
|
||||
# 這種 PKM vault 有自己的目錄慣例,整理時不能照 docs/ 那套搬動,
|
||||
@@ -254,10 +235,6 @@ create_dir ".claude/commands"
|
||||
create_dir ".claude/hooks"
|
||||
download_if_missing "system-dev/docs/README.md" "$REPO_URL/system-dev/docs/README.md"
|
||||
|
||||
# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project
|
||||
# 投影(tasks-project-sync)共用同一套解析,別各寫一份。放共用區,兩模組都拿得到。
|
||||
download_if_missing "system-dev/docs/4-guides/logseq-markers.md" "$REPO_URL/system-dev/docs/4-guides/logseq-markers.md"
|
||||
|
||||
# 工具版號:放 system-dev/,不寄生 .claude/。
|
||||
download_if_missing "system-dev/VERSION" "$REPO_URL/system-dev/VERSION"
|
||||
|
||||
@@ -281,8 +258,6 @@ if $WANT_WIKI; then
|
||||
download_if_missing ".claude/commands/wiki-capture.md" "$REPO_URL/.claude/commands/wiki-capture.md"
|
||||
download_if_missing ".claude/commands/wiki-update.md" "$REPO_URL/.claude/commands/wiki-update.md"
|
||||
download_if_missing ".claude/commands/wiki-recall.md" "$REPO_URL/.claude/commands/wiki-recall.md"
|
||||
# vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):給 Routine 反覆跑。
|
||||
download_if_missing ".claude/commands/wiki-extract.md" "$REPO_URL/.claude/commands/wiki-extract.md"
|
||||
|
||||
# wiki 相關 hooks:接關 + 機敏掃描
|
||||
download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh"
|
||||
@@ -302,18 +277,6 @@ if $WANT_SDD; then
|
||||
|
||||
download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md"
|
||||
download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh"
|
||||
|
||||
# SDD 生命週期鐵律(1.14,issue #6):規則真相源 + 規格變更緩衝區 + 獨立單一活性檢查
|
||||
download_if_missing "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$REPO_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md"
|
||||
download_if_missing "system-dev/docs/3-specs/pending-changes.md" "$REPO_URL/system-dev/docs/3-specs/pending-changes.md"
|
||||
download_if_missing "system-dev/scripts/sdd-active-check.sh" "$REPO_URL/scripts/sdd-active-check.sh"
|
||||
|
||||
# ── tasks⇄Project 投影(optional,issue #16)──────────────────
|
||||
# 帶檔 ≠ 啟用:workflow yaml 只是「留記錄+手動啟用素材」,啟用=對話答好且 acr push。
|
||||
# 投影邏輯依附 tasks.md(住 3-specs),故隨 SDD 模組帶下來;裝了不代表開。
|
||||
create_dir "system-dev/workflows"
|
||||
download_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$REPO_URL/system-dev/workflows/tasks-project-sync.yaml"
|
||||
download_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$REPO_URL/system-dev/workflows/tasks-project-sync.local.sh"
|
||||
fi
|
||||
|
||||
# ── 安裝/更新腳本:一開始就放進 system-dev/scripts/ ──
|
||||
@@ -330,8 +293,6 @@ download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/
|
||||
download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md"
|
||||
|
||||
chmod +x .claude/hooks/*.sh 2>/dev/null || true
|
||||
chmod +x system-dev/workflows/*.sh 2>/dev/null || true
|
||||
chmod +x system-dev/scripts/*.sh 2>/dev/null || true
|
||||
|
||||
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
|
||||
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
|
||||
@@ -486,19 +447,6 @@ fi
|
||||
if $WANT_SDD; then
|
||||
t " 動 code 前先在 system-dev/docs/3-specs/[子系統]/ 建 design.md(可用 /sdd-check 協助)" \
|
||||
" Before touching code, create design.md under system-dev/docs/3-specs/[subsystem]/ (use /sdd-check to help)"
|
||||
# ── tasks⇄Project 投影:裝/init 對話 + 一次性廣告(issue #16)──
|
||||
# 判準=能力(arcrun 裝了沒)+意願,不掃檔。落地成「CC 問一句」,install 只是交代 CC 去問。
|
||||
echo ""
|
||||
t " ❓ 待辦同步(optional,需 Arcrun):請你的 CC 問你一句——" \
|
||||
" ❓ Task sync (optional, needs Arcrun): have your CC ask you once —"
|
||||
t " 「您需要把本專案的待辦事項(tasks.md)同步到 GitHub 嗎?」" \
|
||||
" \"Do you want this project's tasks (tasks.md) mirrored to GitHub?\""
|
||||
t " 答「好」→ CC 查環境有沒有 Arcrun(mcp / acr 在 PATH):有就設定同步、沒有就一次性告知" \
|
||||
" Yes → CC checks for Arcrun (mcp / acr in PATH): set it up if present, otherwise inform you once"
|
||||
t " 「Arcrun 是免費的 AI-friendly 工作流套件,想裝跟 Claude 說就行;之後也可手動啟用」。" \
|
||||
" \"Arcrun is a free AI-friendly workflow toolkit — ask Claude to install it; you can also enable sync later.\""
|
||||
t " 答「不好」→ 不做、不再追問。投影 workflow 在 system-dev/workflows/(帶檔≠啟用)。" \
|
||||
" No → nothing happens, no nagging. The projection workflow sits in system-dev/workflows/ (shipped ≠ enabled)."
|
||||
fi
|
||||
t " GitHub issue:CC 可直接 /issue-handle 讀回自己 repo 的 issue(禁自動輪詢)" \
|
||||
" GitHub issues: CC can use /issue-handle to read issues from its own repo (no auto-polling)"
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
# sdd-active-check.sh — 單一活性 SDD 獨立硬約束(SDD 生命週期鐵律,issue #6)
|
||||
# 規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md
|
||||
#
|
||||
# 用法:bash sdd-active-check.sh [specs目錄]
|
||||
# 參數 1(可選)=specs 目錄,預設 system-dev/docs/3-specs
|
||||
#
|
||||
# 行為:統計 status: active 的 design.md(design.md 前 10 行有 ^status: active,
|
||||
# 排除 archive/ 與 TEMPLATE)——
|
||||
# >1 份 → stderr 列出清單,exit 1(違反單一活性)
|
||||
# ≤1 份 → exit 0
|
||||
#
|
||||
# pre-commit 掛法(.git/hooks/pre-commit,記得 chmod +x):
|
||||
# #!/bin/sh
|
||||
# bash system-dev/scripts/sdd-active-check.sh || exit 1
|
||||
# CI 也是同一行,違反即紅。
|
||||
#
|
||||
# 誠實限制:與 sdd-guard.sh 同精神——只做語法層機械檢查,繞道可行但留痕可審,
|
||||
# 不聲稱不可繞過。價值是「不變量被違反時一定有機器出聲」。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SPECS_DIR="${1:-system-dev/docs/3-specs}"
|
||||
|
||||
# 沒有 specs 目錄(沒裝 SDD 模組)→ 無事可查,放行
|
||||
[ -d "$SPECS_DIR" ] || exit 0
|
||||
|
||||
ACTIVE_COUNT=0
|
||||
ACTIVE_LIST=""
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
if head -10 "$f" 2>/dev/null | grep -q '^status:[[:space:]]*active'; then
|
||||
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
||||
ACTIVE_LIST="${ACTIVE_LIST} • ${f}
|
||||
"
|
||||
fi
|
||||
done < <(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null)
|
||||
|
||||
if [ "$ACTIVE_COUNT" -gt 1 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 單一活性鐵律違反:${SPECS_DIR}/ 下有 ${ACTIVE_COUNT} 份 status: active 的 SDD(任何時刻最多一份):
|
||||
${ACTIVE_LIST}
|
||||
請收斂到一份:其餘改 status: paused / closed(closed 且被取代者填 superseded_by 並移入 archive/)。
|
||||
規則見 system-dev/docs/3-specs/SDD-LIFECYCLE.md。
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -23,7 +23,7 @@ esac
|
||||
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
|
||||
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
||||
|
||||
REPO_RAW="https://git.uncle6.me/Leo/system-dev-template/raw/branch/main"
|
||||
REPO_RAW="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main"
|
||||
TEMPLATE_URL="$REPO_RAW/template"
|
||||
|
||||
UPDATED=()
|
||||
@@ -31,7 +31,6 @@ KEPT=()
|
||||
NEW=()
|
||||
TEMPLATED=()
|
||||
MIGRATED=()
|
||||
COEXIST=()
|
||||
|
||||
# ── 版本比對:先看本機 vs 遠端,給使用者「值不值得更新」的判斷 ──
|
||||
# VERSION 新位置在 system-dev/,舊位置在 .claude/(1.8.x 以前)。優先讀新、回退舊。
|
||||
@@ -83,12 +82,7 @@ migrate_dir() { # $1=舊路徑 $2=新路徑
|
||||
local from="$1" to="$2"
|
||||
[ -e "$from" ] || return 0 # 舊的不存在 → 無需遷移
|
||||
if [ -e "$to" ]; then
|
||||
# 目的地已存在。兩種可能:
|
||||
# (a) 已遷移過 → 舊位置不該還在;冪等略過即可。
|
||||
# (b) 用戶先 install 建了空殼 → 舊位置仍有真資料,現在「並存」。
|
||||
# 不能靜默跳過 (b),也絕不自動合併(覆蓋風險)。→ 記為「並存待合併」,警告。
|
||||
COEXIST+=("$from ↔ $to")
|
||||
return 0
|
||||
return 0 # 新的已存在 → 冪等略過,不覆蓋
|
||||
fi
|
||||
mkdir -p "$(dirname "$to")"
|
||||
if mv "$from" "$to" 2>/dev/null; then
|
||||
@@ -217,26 +211,15 @@ keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hoo
|
||||
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
|
||||
update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
|
||||
|
||||
# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project
|
||||
# 投影共用同一套;任一模組在用就補/更新(邏輯檔,可覆蓋)。
|
||||
if $HAS_WIKI || $HAS_SDD; then
|
||||
update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md"
|
||||
fi
|
||||
|
||||
if $HAS_WIKI; then
|
||||
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/,hooks/commands 留 .claude/。
|
||||
update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md"
|
||||
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
|
||||
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
|
||||
# 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG)
|
||||
update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh"
|
||||
update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh"
|
||||
update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md"
|
||||
update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md"
|
||||
update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md"
|
||||
update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md"
|
||||
# vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):邏輯檔,可覆蓋。舊版沒有 → 當新檔補。
|
||||
update_file ".claude/commands/wiki-extract.md" "$TEMPLATE_URL/.claude/commands/wiki-extract.md"
|
||||
# Cowork(claude.ai)的 wiki 整理 skill:規則檔,可覆蓋
|
||||
update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md"
|
||||
|
||||
@@ -257,18 +240,6 @@ if $HAS_SDD; then
|
||||
update_file "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
|
||||
update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md"
|
||||
update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh"
|
||||
|
||||
# SDD 生命週期鐵律(1.14,issue #6):規則檔+獨立檢查腳本=邏輯檔可覆蓋;
|
||||
# pending-changes.md 裝著用戶的 proposal=用戶資料,只補不覆蓋。
|
||||
# (issue #13 教訓:update 不補新檔會造成結構斷層,新檔必須在這裡鋪。)
|
||||
update_file "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$TEMPLATE_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md"
|
||||
add_if_missing "system-dev/docs/3-specs/pending-changes.md" "$TEMPLATE_URL/system-dev/docs/3-specs/pending-changes.md"
|
||||
update_file "system-dev/scripts/sdd-active-check.sh" "$TEMPLATE_URL/scripts/sdd-active-check.sh"
|
||||
|
||||
# tasks⇄Project 投影(issue #16):邏輯檔,可覆蓋。舊版沒有 → add_if_missing 補。
|
||||
# 啟用狀態存遠端(acr push),不在這些檔裡,覆蓋不會關掉誰的同步。
|
||||
add_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.yaml"
|
||||
add_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.local.sh"
|
||||
fi
|
||||
|
||||
# ── 自我更新:把最新的 update.sh / install.sh 抓到 system-dev/scripts/ ──
|
||||
@@ -276,7 +247,7 @@ fi
|
||||
update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh"
|
||||
update_file "system-dev/scripts/install.sh" "$REPO_RAW/scripts/install.sh"
|
||||
|
||||
chmod +x .claude/hooks/*.sh system-dev/scripts/*.sh system-dev/workflows/*.sh 2>/dev/null || true
|
||||
chmod +x .claude/hooks/*.sh system-dev/scripts/*.sh 2>/dev/null || true
|
||||
|
||||
# ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」──
|
||||
keep_file ".claude/settings.json"
|
||||
@@ -290,18 +261,6 @@ if [ ${#MIGRATED[@]} -gt 0 ]; then
|
||||
t "📦 結構遷移(已收進 system-dev/):" "📦 Layout migrated (moved into system-dev/):"
|
||||
for f in "${MIGRATED[@]}"; do echo " ⇒ $f"; done
|
||||
fi
|
||||
if [ ${#COEXIST[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
t "🛑 偵測到 wiki 並存(新舊位置都有資料,需要合併):" \
|
||||
"🛑 Coexisting wiki detected (both old and new locations have data — needs merging):"
|
||||
for f in "${COEXIST[@]}"; do echo " ↔ $f"; done
|
||||
t " 成因:先跑過 install(建了空殼)才遷移,舊位置真資料沒被搬。" \
|
||||
" Cause: install ran first (created an empty shell), so migration skipped your real data in the old location."
|
||||
t " 不自動合併(避免覆蓋你的資料)。請叫你的 CC:" \
|
||||
" Not auto-merged (to avoid overwriting your data). Ask your CC:"
|
||||
t " 「.claude/wiki/ 和 system-dev/wiki/ 並存,請逐檔比對、把真資料合進 system-dev/,再刪舊的」" \
|
||||
" \"There are two wikis (.claude/wiki/ and system-dev/wiki/) — diff each file, merge the real data into system-dev/, then delete the old one.\""
|
||||
fi
|
||||
if [ ${#NEW[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
t "🆕 新功能(舊版沒有,已加入):" "🆕 New features (absent in the old version, now added):"
|
||||
@@ -340,39 +299,6 @@ if [ -f ".claude/settings.json" ]; then
|
||||
$HAS_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING+=("SessionStart: session-start-recall.sh")
|
||||
$HAS_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
|
||||
$HAS_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): sdd-guard.sh")
|
||||
|
||||
# ── 1.16.0:兩支 wiki 讀取 hook 自動註冊(不只提醒)──
|
||||
# 理由:這兩支的整個存在意義就是「不依賴任何人記得」。
|
||||
# 若靠人看提醒去手動補 settings.json,等於把同一個病搬到安裝環節。
|
||||
if $HAS_WIKI && command -v python3 >/dev/null 2>&1; then
|
||||
python3 - <<'PYEOF' 2>/dev/null || true
|
||||
import json, os
|
||||
p = ".claude/settings.json"
|
||||
try:
|
||||
with open(p) as f: d = json.load(f)
|
||||
except Exception:
|
||||
raise SystemExit(0) # 壞掉的 settings 不碰,交給下方提醒
|
||||
pre = d.setdefault("hooks", {}).setdefault("PreToolUse", [])
|
||||
blob = json.dumps(pre)
|
||||
added = []
|
||||
# 1.16.1:既有註冊若漏 Bash(原版只掛 Grep|Glob|Read)就地補上——
|
||||
# 破口實例:用 curl/wrangler 亂試部署方法走 Bash,整支 hook 不觸發。
|
||||
for _e in pre:
|
||||
if "wiki-first-search" in json.dumps(_e) and "Bash" not in _e.get("matcher", ""):
|
||||
_e["matcher"] = "Grep|Glob|Read|Bash"; added.append("wiki-first-search(補Bash)")
|
||||
if "wiki-first-search" not in blob:
|
||||
pre.append({"matcher": "Grep|Glob|Read|Bash", "hooks": [
|
||||
{"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh"}]})
|
||||
added.append("wiki-first-search")
|
||||
if "subagent-wiki-guard" not in blob:
|
||||
pre.append({"matcher": "Task", "hooks": [
|
||||
{"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh"}]})
|
||||
added.append("subagent-wiki-guard")
|
||||
if added:
|
||||
with open(p, "w") as f: json.dump(d, f, ensure_ascii=False, indent=2)
|
||||
print(" ✅ 已自動註冊 wiki 讀取 hook:" + "、".join(added))
|
||||
PYEOF
|
||||
fi
|
||||
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
|
||||
|
||||
@@ -40,6 +40,26 @@
|
||||
**原因**:當初 monorepo→多 worker 的 Actions 自動同步 + 高頻 API 害帳號被 flag。
|
||||
**詳細**: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,10 +51,16 @@
|
||||
原因: 基本盤 = D1 only(免費、無信用卡);embed 是可選加購層。插件混進來會破壞分層。
|
||||
日期: 2026-06-14
|
||||
|
||||
⚠️ MISTAKE: 補對齊/功能 PR 混進 template 基建遷移 → 撞已 merge 的遷移、害衝突
|
||||
症狀: PR#3(receiver Zod 補對齊)從 PR#2 merge【前】的分支切 → 帶了一整批 template 1.9.x 遷移檔(.claude/→system-dev/,40 個)。PR#2 已把那批搬進 main → PR#3 重複撞 = CONFLICTING/DIRTY,且真正的補對齊改動被淹沒。
|
||||
正確做法: 功能/補對齊 PR 只放該功能的改動;template/基建遷移單獨一筆 PR。撞衝突時別在舊分支硬解一堆遷移衝突 → 從最新 origin/main 重切乾淨分支、只 cherry-pick 該功能的 code commit、force-with-lease 覆蓋 PR 分支(PR 自動更新、不用關掉重開)。切分支前先確認 base 是不是落後於已 merge 的東西。
|
||||
原因: 分支從「即將被 merge 的另一支」之前切,會把對方的改動也一起帶上;對方 merge 後兩份就撞。核心改動本身不衝突,衝突全來自混進來的重複遷移。
|
||||
⚠️ 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
|
||||
|
||||
---
|
||||
|
||||
+20
-68
@@ -1,91 +1,43 @@
|
||||
# 當前狀態
|
||||
|
||||
> 更新時間:2026-07-17
|
||||
> 更新時間:2026-06-26
|
||||
> 每次 session 結束必須更新此檔(用 /wiki-update)。
|
||||
|
||||
---
|
||||
|
||||
## 最新(2026-07-17:SDD 生命週期鐵律遷移 + SDD 位置統一)
|
||||
## 最新(2026-06-26:issue #1 T3 — ingest 寫入端 + graph 端 API)
|
||||
|
||||
leo 2026-07-17 拍板全體系「單一活性 SDD」制度(canonical:system-dev-template v1.15.0),本 repo 已遷移:
|
||||
|
||||
1. **位置統一**:舊 `docs/3-specs/` 的 5 份 SDD 全數 `git mv` 到 `system-dev/docs/3-specs/`,舊位置留 README 指針。
|
||||
2. **狀態判定**:**0 份 active**(無現行開發——實作面 2026-06-26 已收斂,剩跨 repo 接通,合法)。三份 `paused`:ingest-contract/kbdb-graph-extraction/plugin-install(等基本盤部署/跨 repo)。兩份 `closed` 入 `archive/`:arcrun-key-auth(舊 KBDB 時代死件)、blocks-edit-api(舊 blocks 表架構死件,已被 Arcrun #6 取代)。
|
||||
3. **鋪檔**:SDD-LIFECYCLE.md/pending-changes.md/sdd-guard.sh(新版,掛進 settings.json PreToolUse Write|Edit)/sdd-check.md/sdd-active-check.sh。
|
||||
4. **驗證**:sdd-active-check exit 0;guard pipe-test——0 active 下寫 code 檔擋(exit 2)、寫 md 放行。
|
||||
5. **CLAUDE.md**:加 SDD 鐵律段(濃縮五條+0-active 註明「重啟開發先挑一份升 active」),並修正遷移後的舊路徑指針(wiki 路徑、SDD 路徑)。
|
||||
|
||||
---
|
||||
|
||||
## 最新(2026-06-26:issue #1 補對齊 — receiver Zod 追上 contract,PR #3 已 merge)
|
||||
|
||||
[PR #3](https://github.com/uncle6me-web/kbdb-graph-plugin/pull/3) 已 merge 進 main(commit `13db97b`)。對應 [issue #1](https://github.com/uncle6me-web/kbdb-graph-plugin/issues/1) 總管補對齊 comment。
|
||||
|
||||
**起因(契約漂移)**:T3 的 strict Zod 鏡射【當時】contract;contract 之後升格(ingest#1 向量化規範)加打標欄位 → ingest 照新 contract 送會被 `.strict()` 擋 422。總管裁定方向 A:graph 追上 contract(contract 是凍結單一真相源,實作追它)。
|
||||
|
||||
完成:
|
||||
1. ✅ `contracts/ingest-candidate.json` 副本同步到頂層單一真相源(`InkStoneCo/system-dev/docs/3-specs/mira-dissolve/`)。
|
||||
2. ✅ Zod 加契約合法新欄位(**保留 `.strict()`**):`NodeSchema`+`id?`/`aliases?`/`embed?`;`EdgeSchema`+`predicate_embed?`。
|
||||
3. ✅ 落地(向量化分工:ingest 打標、base 讀標執行、**graph 不算向量**):`predicate_embed` 透傳進 triplet slot;node 打標(`embed`/`gloss`/`aliases`)存進 entity slot;`id` 作 node 去重鍵(同卡多邊只一筆)。`persistNodes` 拆獨立 action(`src/actions/node-persist.ts`)。
|
||||
4. ✅ 測試 +4:帶向量化欄位【通過】、`bridge_score`/`clusters` 仍【422】、同 id 去重。
|
||||
|
||||
**新增 plugin slot**(非改表、非改 contract):triplet `predicate_embed`;entity `embed`/`node_id`。
|
||||
驗證:`vitest run` **23 passed**;零 SQL / 無 D1·Vectorize·AI;dry-run 乾淨;action ≤100 行。
|
||||
|
||||
**過程教訓(已記 mistakes)**:PR#3 初版從 PR#2 merge 前切 → 混進 PR#2 已做的 template 1.9.x 遷移 40 檔 → 撞 main 衝突。解法=從最新 main 重切、只 cherry-pick 補對齊那筆 code commit、瘦身後 force-push。**補對齊/功能 PR 別混 template 基建遷移。**
|
||||
|
||||
---
|
||||
|
||||
## 前一筆(2026-06-26:issue #1 T3 — ingest 寫入端 + graph 端 API,PR #2 已 merge)
|
||||
|
||||
[PR #2](https://github.com/uncle6me-web/kbdb-graph-plugin/pull/2) 已 merge(commit `7a29dee`,squash)。
|
||||
開 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. ✅ **wiki 合併**:舊 `.claude/wiki/` → `system-dev/wiki/`(導入 system-dev-template)。
|
||||
2. ✅ **ingest-contract SDD**(`docs/3-specs/ingest-contract/`)+ 搬入 `contracts/ingest-candidate.json`(T3.1/3.8)。
|
||||
3. ✅ **寫入端 + 取代**(T3.2–3.5):`POST /triplets/ingest`、ensureTemplate slot-diff 補丁、`updateRecord`、idempotency、**先 append 後 deprecate**、active-only 查詢。
|
||||
4. ✅ **get_source + refresh**(T3.6/3.7):`GET /graph/source/:name`、`POST /graph/refresh`(純被動代轉,未設 `KBDB_INGEST_URL` 時誠實回 `forwarded:false`)、keyword 收斂(3.6d)。
|
||||
4. ✅ **get_source + refresh**(T3.6/3.7):`GET /graph/source/:name`、`POST /graph/refresh`(純被動代轉)、keyword 收斂(3.6d)。
|
||||
|
||||
驗證:`vitest run` 19 passed(mock);zero SQL / 無 D1·Vectorize·AI;dry-run 乾淨;action ≤100 行。
|
||||
|
||||
> 註:base `PATCH /records/:id` 已就緒(Arcrun #6 closed),ingest deprecate 即用此。
|
||||
|
||||
---
|
||||
|
||||
## 已完成(2026-06-14:按 leo 鐵律全面改寫 + 獨立成 repo)
|
||||
|
||||
HANDOFF 5 項待辦全做完:
|
||||
|
||||
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)。
|
||||
3. ✅ **獨立成 repo**:`git init` + 推 **public `uncle6me-web/kbdb-graph-plugin`**(無 Actions)。
|
||||
4. ✅ **裁剪 CLAUDE.md**:移除 KBDB v3 基本盤規範,只留 graph 插件 + 🔒 鐵律 + 安裝契約。
|
||||
5. ✅ **部署繞開 GitHub**:`scripts/install.sh`(wrangler 直推),無 .github/workflows。
|
||||
|
||||
驗證:`wrangler deploy --dry-run` 通過(bundle 無 D1/AI/Vectorize);`vitest run` 10 passed(mock client);全違規掃描零命中;所有 action ≤100 行。
|
||||
|
||||
## 同場修正的 SDD 錯誤判斷
|
||||
|
||||
design.md 原本「讀現狀(21 個直接 SQL)推翻鐵律、問要不要共用 D1」是**讀違規現狀推翻規則**的錯。已改正為 **API-as-Wall(走 API,非共用 D1,零建表/零 SQL)**,並記進 mistakes.md。
|
||||
驗證:`vitest run` **19 passed**(mock);zero SQL / 無 D1·Vectorize·AI;`wrangler deploy --dry-run` bundle 乾淨;所有 action ≤100 行。
|
||||
|
||||
## 正在做 / 阻擋
|
||||
|
||||
- ✅ PR #2(T3)、PR #3(補對齊)皆已 merge 進 main。graph repo 端實作面收斂,**本 repo 無剩餘可單獨做的 task**。
|
||||
- [🔄] 剩三項皆「不在 graph 手上」,等跨 repo 接通 / 部署(見下)。
|
||||
- [🔄] **PR #2 待 review/merge** — 阻擋點:等 leo/總管。graph repo 端 T3 實作面已收斂,剩跨 repo 接通。
|
||||
|
||||
## 下次 session 第一件事
|
||||
|
||||
main 已含 T3 + 補對齊。本 repo 端無待辦——下次動工多半是**新交辦**或**跨 repo 接通就緒後接 MCP 薄殼**。
|
||||
若要實際部署:等基本盤 `arcrun-kbdb` 上線有網址後跑 `bash scripts/install.sh`(自動查 CF subdomain 拼 `KBDB_BASE_URL` → secret → deploy)。現不空跑(避免上線打不到基本盤的殼)。
|
||||
**確認 PR #2 狀態**:若已 merge → 切回 `main` 拉新狀態;若有 review 意見 → 在 `feat/ingest-contract-sdd` 上改。
|
||||
本 repo 端 T3 無剩餘可單獨做的 task(其餘待跨 repo / 部署,見下)。
|
||||
|
||||
## 待負責人確認 / 跨 repo 接通(全通才結 issue #1)
|
||||
## 待負責人確認
|
||||
|
||||
- **MCP 註冊薄殼** — 圖工具(traverse/neighbors/source/refresh)併入 arcrun `u6u-mcp-server`。等:總管協調 arcrun,**不另起 graph MCP**;待 Arcrun #7 部署驗。graph 端 HTTP API 已備好。
|
||||
- **refresh 端到端** — 等:ingest repo(T4)部署 + 設 `KBDB_INGEST_URL`;未設時誠實回 `forwarded:false`。
|
||||
- **semantic normalize** — 仍 exact-only,留接口;等:base embed(Arcrun #7,code done 待部署)。**補對齊已把向量化打標(embed/predicate_embed/gloss/aliases)落地進 slot 供 base 讀**,base 模組就緒即可接。
|
||||
- **PR #2 merge** — 等:leo/總管 review。
|
||||
- **MCP 註冊薄殼**(圖工具 traverse/neighbors/source/refresh 併入 arcrun `u6u-mcp-server`)— 等:總管協調 arcrun,**不另起 graph MCP**。graph 端 HTTP API 已備好。
|
||||
|
||||
## 已知缺口([→arcrun],待基本盤補)
|
||||
## 已知問題 / 缺口
|
||||
|
||||
- base `PATCH /records/:id` ✅ 已就緒(Arcrun #6 closed);但 base 仍無 `DELETE /records/:id` → triplet/entity delete、pending confirm/reject 為 soft(不硬刪)。
|
||||
- 語意搜尋 / embedding 屬基本盤 optional embed 模組,插件只做 keyword/exact(graph 不算向量,鐵律)。
|
||||
- arcrun 端 MCP/CLI 的 KBDB 薄殼仍待補;插件目前直打基本盤 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)', () => {
|
||||
it('把 deprecated 翻回 active 後,active 查詢重新見到它', async () => {
|
||||
const c = mockClient();
|
||||
|
||||
+6
-20
@@ -10,29 +10,15 @@ workers_dev = true
|
||||
|
||||
[vars]
|
||||
ENVIRONMENT = "development"
|
||||
# 基本盤 arcrun/kbdb API 網址:務必只透過 `wrangler secret put KBDB_BASE_URL` 設定。
|
||||
# 2026-07-03 補跑實測發現的坑:若這裡也宣告 KBDB_BASE_URL(哪怕留空字串),
|
||||
# `wrangler deploy` 會用這個 [vars] 值覆蓋掉已存在的 remote secret(vars 優先權 > secret),
|
||||
# 導致部署後 KBDB_BASE_URL 被打回空字串、插件連不到基本盤。故此鍵不可出現在 [vars]。
|
||||
# 基本盤 arcrun/kbdb API 網址(leo 2026-06-14:做成可設定,先留空)。
|
||||
# 部署前用 `wrangler secret put` 或在此填入,例如 https://arcrun-kbdb.<acct>.workers.dev
|
||||
KBDB_BASE_URL = ""
|
||||
|
||||
[alias]
|
||||
"zod/v3" = "zod"
|
||||
"zod/v4" = "zod"
|
||||
"zod/v4-mini" = "zod"
|
||||
|
||||
# 2026-07-03 補跑實測發現的坑:workers.dev 是共享 zone,Worker 直接 fetch()
|
||||
# 另一個 *.workers.dev Worker 會被 CF 擋(error code 1042,loop-prevention)。
|
||||
# self-hosted 帳號通常沒自訂域名可繞,正規解法 = Service Binding:CF 內部直連,
|
||||
# 不經公開網路、不受 1042 影響。KBDB_BASE_URL secret 仍保留(可讀性/未來自訂域名時 fallback),
|
||||
# 但實際讀寫走這個 binding(見 src/lib/kbdb-client.ts / makeKbdbClient)。
|
||||
[[services]]
|
||||
binding = "KBDB_BASE_SVC"
|
||||
service = "arcrun-kbdb"
|
||||
|
||||
# 2026-07-03 補跑實測:custom_domain "kbdb-graph.finally.click" 這個 zone
|
||||
# 不在 leo21c 帳號下(CF API GET /zones?name=finally.click 回空),
|
||||
# self-hosted 到 leo21c 時此路由會部署失敗(但不影響 workers.dev route 的部署本身)。
|
||||
# 註解掉以让 self-hosted 用戶乾淨部署到預設 workers.dev;有自訂域名需求者自行改回。
|
||||
# [[routes]]
|
||||
# pattern = "kbdb-graph.finally.click"
|
||||
# custom_domain = true
|
||||
[[routes]]
|
||||
pattern = "kbdb-graph.finally.click"
|
||||
custom_domain = true
|
||||
|
||||
Reference in New Issue
Block a user