Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbd375f7ac | |||
| 613071f41d | |||
| 27f7448914 | |||
| 98b221b435 | |||
| 2dd80cd703 |
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
description: 處理本 repo 的 GitHub issue(讀/回/結案),跨 repo 發要先問人
|
||||||
|
---
|
||||||
|
|
||||||
|
# /issue-handle — GitHub issue 處理指引
|
||||||
|
|
||||||
|
你(CC)可以、也該主動用內建的 `gh` CLI 讀寫**自己 repo** 的 GitHub issue。
|
||||||
|
很多人不知道這件事——`gh` 已內建認證,零開發、零外部依賴。issue 同源於 repo,
|
||||||
|
比 Notion / Sheets 更適合做交辦與待辦,不必引入外部 SaaS。
|
||||||
|
|
||||||
|
這份指引分三層,界線要守住。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 讀 / 回 / 結案(普世基本功 — 直接做,不用問)
|
||||||
|
|
||||||
|
對**自己這個 repo**,主動處理 open issue:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh issue list --state open # 看有哪些待辦
|
||||||
|
gh issue view <n> # 讀完整內容
|
||||||
|
# …實作…
|
||||||
|
gh issue comment <n> --body "做了什麼、怎麼決定的、改了哪些檔"
|
||||||
|
gh issue close <n> # 確認解決後結案
|
||||||
|
```
|
||||||
|
|
||||||
|
回覆要有料:說清楚**做了什麼、為什麼這樣決定、動了哪些檔**,而不是只回「done」。
|
||||||
|
issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆判斷對不對。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 發 issue 給「別的 repo」(要先問人 — 不可擅自)
|
||||||
|
|
||||||
|
當你發現**別的 repo** 有值得修正的地方時:
|
||||||
|
|
||||||
|
> ❌ 不要擅自 `gh issue create -R other/repo …`
|
||||||
|
> ✅ 先問人類:「我發現 X repo 有 Y 問題,要我幫你去那邊發 issue 嗎?」得到同意才發。
|
||||||
|
|
||||||
|
理由:通用 template 不知道使用者對那個 repo 有沒有權限、想不想發。留一道人類確認最安全。
|
||||||
|
(對**自己 repo** 開 issue 記待辦則可直接做——那是自己的 repo。)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. flag 安全界線(最重要 — 絕不可越)
|
||||||
|
|
||||||
|
**「有事才讀」,禁止自動輪詢。**
|
||||||
|
|
||||||
|
- ✅ 想看 issue → 當下主動 `gh issue list`。
|
||||||
|
- 🚫 **禁止**掛 GitHub Actions / cron / webhook 去**自動輪詢** issue。
|
||||||
|
|
||||||
|
為什麼這條是硬底線:自動輪詢 + 事件 fan-out 正是會觸發 GitHub 異常偵測、
|
||||||
|
害你的 token 被 rate limit 砍掉的流量模式。template 給很多人用,這條界線必須守住,
|
||||||
|
保護使用者不踩雷。需要「定期檢查」就由人類主動跑這個指令,不要自動化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## (可選)用 label 分來源
|
||||||
|
|
||||||
|
若想用同一套流程同時管「內部交辦」與「外部回報」,可建兩個 label:
|
||||||
|
`internal`(協作/交辦)、`user`(外部使用者回報),靠 label 分流。
|
||||||
|
這對通用 template 不預設——你的 repo 需要再建。
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
### 第二步:尋找對應 SDD
|
### 第二步:尋找對應 SDD
|
||||||
|
|
||||||
在 `docs/3-specs/` 下尋找對應的子系統目錄,確認有沒有:
|
在 `system-dev/docs/3-specs/` 下尋找對應的子系統目錄,確認有沒有:
|
||||||
- `design.md`(設計文件)
|
- `design.md`(設計文件)
|
||||||
- `tasks.md`(任務清單)
|
- `tasks.md`(任務清單)
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
**情況 A:找到對應 SDD**
|
**情況 A:找到對應 SDD**
|
||||||
```
|
```
|
||||||
✅ 找到 SDD:docs/3-specs/[子系統]/
|
✅ 找到 SDD:system-dev/docs/3-specs/[子系統]/
|
||||||
📋 design.md:[確認]
|
📋 design.md:[確認]
|
||||||
📋 tasks.md:[確認,列出相關 task]
|
📋 tasks.md:[確認,列出相關 task]
|
||||||
🎯 對應 task:[編號和描述]
|
🎯 對應 task:[編號和描述]
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
```
|
```
|
||||||
⚠️ 找不到對應 SDD
|
⚠️ 找不到對應 SDD
|
||||||
任務:[描述]
|
任務:[描述]
|
||||||
建議在 docs/3-specs/[建議子系統名]/ 建立 SDD
|
建議在 system-dev/docs/3-specs/[建議子系統名]/ 建立 SDD
|
||||||
|
|
||||||
要我幫你起草 design.md 嗎?(需要你確認後才動手)
|
要我幫你起草 design.md 嗎?(需要你確認後才動手)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,13 +7,21 @@
|
|||||||
|
|
||||||
## 執行流程
|
## 執行流程
|
||||||
|
|
||||||
|
### 第零步:機敏檢查(寫入前一律先過)
|
||||||
|
|
||||||
|
把任何內容寫進 wiki 前,先確認**不含**密碼 / API 金鑰 / 私鑰 / 連線字串帳密 / 個資(身分證、信用卡)。
|
||||||
|
- 命中 → 不要記「值」,改記「位置」(例:「DB 密碼放 `.env`,不入 wiki」)
|
||||||
|
- 來源整檔機敏 → 提醒使用者加進 `system-dev/wiki/.wikiignore`
|
||||||
|
- 真要保留示範格式 → 該行尾加 `wiki-secret-ok` 標記
|
||||||
|
> 這是協議層自律。最後一道 `wiki-secret-scan.sh` hook 會在寫入 `system-dev/wiki/` 時機械攔截,但別依賴它兜底——當場就不要把機敏值帶進來。
|
||||||
|
|
||||||
### 第一步:辨識對話中的可記錄內容
|
### 第一步:辨識對話中的可記錄內容
|
||||||
|
|
||||||
掃描當前對話,找出:
|
掃描當前對話,找出:
|
||||||
|
|
||||||
| 類型 | 判斷標準 | 存到哪 |
|
| 類型 | 判斷標準 | 存到哪 |
|
||||||
|------|---------|-------|
|
|------|---------|-------|
|
||||||
| 架構決策 | 「為什麼選A不選B」「我們決定用X」 | `decisions-summary.md` + `docs/2-architecture/decisions/` |
|
| 架構決策 | 「為什麼選A不選B」「我們決定用X」 | `decisions-summary.md` + `system-dev/docs/2-architecture/decisions/` |
|
||||||
| CC 的誤解被糾正 | CC 說了某件事,使用者說「不是,是...」 | `mistakes.md` |
|
| CC 的誤解被糾正 | CC 說了某件事,使用者說「不是,是...」 | `mistakes.md` |
|
||||||
| 重要狀態更新 | 完成了某件事、阻擋了某件事 | `status.md` |
|
| 重要狀態更新 | 完成了某件事、阻擋了某件事 | `status.md` |
|
||||||
| 技術發現 | 踩到坑、找到解法、重要行為確認 | `mistakes.md` 或對應 SDD |
|
| 技術發現 | 踩到坑、找到解法、重要行為確認 | `mistakes.md` 或對應 SDD |
|
||||||
@@ -51,10 +59,10 @@
|
|||||||
## [主題] — [YYYY-MM-DD]
|
## [主題] — [YYYY-MM-DD]
|
||||||
**結論**:[一句話]
|
**結論**:[一句話]
|
||||||
**原因**:[簡短說明]
|
**原因**:[簡短說明]
|
||||||
**詳細**:docs/2-architecture/decisions/[檔名]
|
**詳細**:system-dev/docs/2-architecture/decisions/[檔名]
|
||||||
```
|
```
|
||||||
|
|
||||||
重大決策同時在 `docs/2-architecture/decisions/` 建立 ADR 檔案。
|
重大決策同時在 `system-dev/docs/2-architecture/decisions/` 建立 ADR 檔案。
|
||||||
|
|
||||||
### 第四步:確認
|
### 第四步:確認
|
||||||
|
|
||||||
|
|||||||
+168
-27
@@ -1,7 +1,25 @@
|
|||||||
# /wiki-init — 初始化或接入 LLM Wiki 系統
|
# /wiki-init — 初始化或接入 LLM Wiki 系統
|
||||||
|
|
||||||
初始化這個專案的 LLM Wiki 記憶系統。
|
初始化這個專案的 LLM Wiki 記憶系統。
|
||||||
新專案建立空白結構,已有專案掃描現有文件並建立 wiki。
|
新專案建立空白結構,已有專案掃描現有文件並**改寫**成 wiki。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心概念:wiki 是 AI 改寫過的記憶,不是原文索引
|
||||||
|
|
||||||
|
記憶系統的目的,是讓 AI **之後讀得快**。但人類寫的原始文件——不管是 vault 的隨手記、開發專案的會議記錄、規格草稿、散落的 `.md`——天生是亂的:重複、流水帳、半成品、口語。
|
||||||
|
|
||||||
|
如果 wiki 只是一份 `[[原文檔名]]` 指回原文的**索引**,那每次未來要用都得重新解析那團亂,等於沒省到。**wiki 的價值在於「改寫一次,之後每次讀都便宜」**。
|
||||||
|
|
||||||
|
所以原則對**所有專案**一致(不分 vault 或一般開發):
|
||||||
|
|
||||||
|
> 人類寫的原文是 **SSoT**(真理來源,永遠唯讀)。
|
||||||
|
> 但實際要長期保存、被 AI 反覆讀的是 **AI 改寫整理過的 wiki**。
|
||||||
|
> **AI 是總編輯**——把原文改寫成自包含、概念原子化、互相連結、適於 AI 讀的知識條目。
|
||||||
|
|
||||||
|
唯一例外:原文是**不可改動的正式文件**(簽署過的規格、法規、合約),必須逐字讀原文——這種才在 wiki 裡用指針指回去,並註明「逐字依原文」。除此之外,一律改寫。
|
||||||
|
|
||||||
|
**raw source 永遠唯讀**:所有產出只往 `system-dev/wiki/` 寫,絕不改動、搬移、重新命名原文。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,45 +28,93 @@
|
|||||||
### 第一步:偵測專案狀態
|
### 第一步:偵測專案狀態
|
||||||
|
|
||||||
檢查以下項目,判斷是新專案還是已有專案:
|
檢查以下項目,判斷是新專案還是已有專案:
|
||||||
- 根目錄有沒有 `.claude/wiki/`
|
- 根目錄有沒有 `system-dev/wiki/`
|
||||||
- 根目錄有沒有 `docs/`
|
- 根目錄有沒有 `docs/`(或 vault 的 `pages/`、`journals/`、根目錄 `.md`)
|
||||||
- 有沒有散落的 `.md` 檔案
|
- 有沒有散落的 `.md` 檔案
|
||||||
|
|
||||||
|
同時**偵測 raw source 路徑**(同 install.sh 邏輯):
|
||||||
|
- 根目錄有 `logseq/` → Logseq vault,raw source = `pages/` + `journals/`
|
||||||
|
- 根目錄有 `.obsidian/` → Obsidian vault,raw source = 根目錄所有 `.md`
|
||||||
|
- 都沒有 → 一般專案,raw source = `docs/` 下所有 `.md`(及散落的 `.md`)
|
||||||
|
|
||||||
**新專案**(幾乎空的)→ 直接建立結構,跳到第三步
|
**新專案**(幾乎空的)→ 直接建立結構,跳到第三步
|
||||||
**已有專案**(有文件)→ 執行第二步
|
**已有專案**(有文件)→ 執行第二步
|
||||||
|
|
||||||
### 第二步:已有專案的掃描(已有專案才執行)
|
### 第二步:已有專案的掃描(已有專案才執行)
|
||||||
|
|
||||||
1. 遞迴找出所有 `.md` 檔案
|
1. 遞迴找出 raw source 裡所有 `.md` 檔案
|
||||||
2. 對每個檔案標注建議位置和信心度
|
2. **先套用 `system-dev/wiki/.wikiignore`**:命中 pattern 的檔案整個排除,不讀不編入。
|
||||||
3. 列出清單給使用者確認,**停下來等確認**
|
- 若 `.wikiignore` 不存在,從範本建立一份(預設排除 `.env`/`*.pem`/`*secret*` 等)
|
||||||
|
- 被排除的檔案在清單裡標「🚫 .wikiignore 排除」,**不可被覆蓋**
|
||||||
|
3. 對其餘檔案標注**改寫計畫**:會萃取成哪些 wiki 條目。一份原文可能拆成多個概念原子條目,多份相關原文也可能合併成一條。
|
||||||
|
4. 列出清單給使用者確認,**停下來等確認**
|
||||||
|
|
||||||
分類規則:
|
> **量大時建議用 Haiku 改寫**:逐份原文「改寫成 wiki 格式」是重複、機械、判斷成本低的工作——正適合 Haiku。原文數量多(如數十、上百份)時,主動建議:
|
||||||
```
|
> 「共 N 份原文要改寫,這類逐份萃取很適合用 Haiku 並行處理(便宜、夠快)。要我派 Haiku subagent 改寫嗎?」
|
||||||
有明確子系統 + 設計內容 → docs/3-specs/[子系統]/
|
> 得同意後,用 Task / subagent 把每份原文(或每批)丟給 Haiku 改寫,主模型只負責切分概念、定條目邊界、最後審稿與互連。
|
||||||
解釋為什麼做某個決定 → docs/2-architecture/decisions/
|
|
||||||
說明怎麼操作 → docs/4-guides/
|
> 機敏防護(三層):
|
||||||
記錄發生過的事 → docs/5-records/
|
> - **L1 .wikiignore**:整檔排除(這一步)
|
||||||
給外部使用者看的 → docs/6-user/
|
> - **L2 行內標記**:檔案要編入但某段不要 → 遇到 `<!-- wiki:ignore -->` … `<!-- wiki:end -->` 之間的內容**略過**,只留「(此處機敏,已略過)」
|
||||||
不確定 → 列為「待確認」,問使用者
|
> - **L3 hook**:萬一機敏值仍被寫進 wiki,`wiki-secret-scan.sh` 會 exit 2 擋下
|
||||||
```
|
> 編入任何檔案前,先檢查是否含密碼/金鑰/個資——有就改記「位置」而非「值」。
|
||||||
|
|
||||||
### 第三步:建立缺少的結構
|
### 第三步:建立缺少的結構
|
||||||
|
|
||||||
只建立不存在的目錄和檔案,**已有的一律不動**:
|
只建立不存在的目錄和檔案,**已有的一律不動**。
|
||||||
|
|
||||||
|
wiki 採**三層 + 標籤橫切**架構(183 卡實證,issue #8):
|
||||||
|
|
||||||
目錄:
|
|
||||||
```
|
```
|
||||||
docs/{1-vision,2-architecture/decisions,3-specs,4-guides,5-records/{incidents,test-reports},6-user}
|
system-dev/wiki/
|
||||||
.claude/wiki/
|
├── INDEX.md ← 索引:多角度視圖的家(標籤角度、決策角度、…)
|
||||||
|
├── TAXONOMY.md ← 標籤字典(cards 的分類元資料,受控擴充)
|
||||||
|
├── status.md ← [push] 時態狀態:當前進度、下一步
|
||||||
|
├── mistakes.md ← [push] 踩過的坑、被糾正的誤解(防不自覺盲區)
|
||||||
|
├── principles.md ← [push] 跨全局的設計原則(行動前必服從)
|
||||||
|
└── cards/ ← [pull] 一切知識內容:原文摘要、AI 筆記、決策、概念…
|
||||||
|
└── <bucket>/ ← 儲存桶(分類由 frontmatter 標籤承載)
|
||||||
|
├── 00-INDEX.md ← 桶子索引(固定名,容器:只連不重寫,H2/H3 分節)
|
||||||
|
└── <概念全名>.md ← 概念原子卡(一概念一檔,自包含)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **[push] / [pull] 是這套 wiki 的核心判準——因為 wiki 主要是給 AI(CC)看的。**
|
||||||
|
> 見下方「核心判準:push vs pull」。`decisions-summary.md` 已**降級為 cards + INDEX 決策視圖**(決策是知識內容=card);既有的 decisions-summary 若存在,保留為相容,不刪。
|
||||||
|
|
||||||
|
關鍵原則:**資料夾只是儲存桶,分類由 frontmatter 標籤承載**。資料夾名不該硬繼承原稿目錄——原稿目錄是「人為了整理草稿」分的,wiki 連分類都該由 AI 重新組織。
|
||||||
|
|
||||||
|
> **桶子索引固定叫 `00-INDEX.md`**(issue #6):`00-` 前綴讓它排序最前、一眼可辨(像 README 之於資料夾),AI 載入任何 `cards/<bucket>/` 一律先讀它,不必猜。檔內 H1 仍寫主題名(如 `# PKM 知識管理`),語意不丟。
|
||||||
|
|
||||||
|
一般專案仍可同時建 `system-dev/docs/` 分類樹(SDD 等):
|
||||||
|
```
|
||||||
|
system-dev/docs/{1-vision,2-architecture/decisions,3-specs,4-guides,5-records/{incidents,test-reports},6-user}
|
||||||
|
```
|
||||||
|
(純 PKM vault 不需要 `system-dev/docs/` 分類樹時,只建 `system-dev/wiki/`。)
|
||||||
|
|
||||||
檔案(不存在才建):
|
檔案(不存在才建):
|
||||||
- `.claude/wiki/INDEX.md`
|
- `system-dev/wiki/INDEX.md`、`TAXONOMY.md`
|
||||||
- `.claude/wiki/status.md`
|
- `system-dev/wiki/status.md`、`mistakes.md`、`principles.md`(三個 push 檔)
|
||||||
- `.claude/wiki/mistakes.md`
|
- `system-dev/docs/README.md`(一般專案才需要)
|
||||||
- `.claude/wiki/decisions-summary.md`
|
|
||||||
- `docs/README.md`
|
---
|
||||||
|
|
||||||
|
## 核心判準:push vs pull(wiki 是給 AI 看的)
|
||||||
|
|
||||||
|
整理任何內容前,先判斷它該 **push** 還 **pull**——判準是「**CC 做事時會不會被動看見**」:
|
||||||
|
|
||||||
|
- **push**:CC 行動前必須主動出現在 context(session 開始就由 hook 注入)。給「CC 不會主動去查、但不看就出事」的東西。
|
||||||
|
- **pull**:CC 想到要查、或載入相關卡時才看見。給「CC 面對它時自然會查」的知識。
|
||||||
|
|
||||||
|
**為什麼這是核心**:mistakes 防的是 CC「不自覺的盲區」——一個你不知道存在的錯,你不會主動去檢索它。靠 CC 自覺去查自己沒自覺的盲區是自相矛盾的,所以 pull 對盲區失效,**必須 push**。原則同理:沒被推到眼前的準繩,CC 設計時很可能沒想到要服從就做了。
|
||||||
|
|
||||||
|
| 內容 | push/pull | 注入形態(hook)|
|
||||||
|
|------|-----------|----------------|
|
||||||
|
| **status** | push | **全文**——CC 必須知道精確的下一步,摘要會漏 task 編號 |
|
||||||
|
| **principles** | push | **全文(一行一條)**——短而硬的約束,漏一條就違反;≤15 條,超過代表該下放成 card |
|
||||||
|
| **mistakes** | push | **標題清單 + 一行症狀**,全文按需 pull——量可能大,摘要足以觸發「我正撞到某條」的認出 |
|
||||||
|
| **decisions、原文摘要、概念知識、一切其餘** | pull | 寫成 cards;CC 面對時自然會查,INDEX 提供角度入口 |
|
||||||
|
|
||||||
|
**principles 維護規則**:一行一條精煉準繩(如「不污染用戶根目錄」「目標用戶 low-code」「wiki 主要給 AI 看」)。發現新的跨全局原則 → append 一行;超過 ~15 條代表某些該合併或下放成 card。**累積原則只改 principles.md,不必問用戶開新檔。**
|
||||||
|
|
||||||
### 第四步:訪談(每次一個問題)
|
### 第四步:訪談(每次一個問題)
|
||||||
|
|
||||||
@@ -60,18 +126,93 @@ docs/{1-vision,2-architecture/decisions,3-specs,4-guides,5-records/{incidents,te
|
|||||||
|
|
||||||
把答案填進 `CLAUDE.md`(如果存在)或建立新的。
|
把答案填進 `CLAUDE.md`(如果存在)或建立新的。
|
||||||
|
|
||||||
### 第五步:已有專案的文件歸檔
|
### 第五步:改寫成 wiki(AI 當總編輯)
|
||||||
|
|
||||||
(第二步確認後執行)
|
(第二步確認後執行)
|
||||||
|
|
||||||
按照確認好的分類移動檔案,完成後更新 `CLAUDE.md` 的路徑引用。
|
**不搬動原文**。逐份讀 raw source,改寫萃取成 `cards/<bucket>/` 裡的自包含原子卡:
|
||||||
|
|
||||||
### 第六步:完成報告
|
- **概念原子化**:一張卡講一個概念,不是一篇原文對一張卡。原文太雜就拆,多份相關原文就合。
|
||||||
|
- **自包含**:讀卡就懂,不必回去翻原文。把口語、重複、流水帳改寫成結構化知識,**不寫「詳見原文」**。
|
||||||
|
- **保留來源指針**:每卡標 `**來源**:原文相對路徑`,為可追溯,不是要使用者回去讀。
|
||||||
|
- **frontmatter 標籤分類**(見下方):分類走 frontmatter `tags:`,不靠資料夾、不靠行內 `#tag`。
|
||||||
|
- **互相連結(typed-edge 三元組)**:`## 關聯` 不只列裸 `[[頁面]]`,改寫成帶語義的三元組(見下方)。
|
||||||
|
- **萃 gloss(node 一句說明)**:frontmatter 放 `gloss:` —— 這張卡(= 一個 entity / graph node)的一句話定義,供下游語義 normalize(見下方)。
|
||||||
|
|
||||||
|
卡片格式(每張卡):
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
tags: [知識管理, AI協作, 方法論]
|
||||||
|
gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,選填、deep tier 才產)
|
||||||
|
---
|
||||||
|
# 概念全名
|
||||||
|
|
||||||
|
← [[<bucket>/00-INDEX]]
|
||||||
|
|
||||||
|
**來源**:`[raw source 相對路徑]`
|
||||||
|
**最後更新**:YYYY-MM-DD
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
[一句話核心]
|
||||||
|
|
||||||
|
## 重點
|
||||||
|
- [自包含改寫的要點,不依賴原文]
|
||||||
|
|
||||||
|
## 關聯
|
||||||
|
- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]]
|
||||||
|
- [[原子筆記]] >> 是其最小單元 >> [[卡片盒筆記法]]
|
||||||
|
```
|
||||||
|
|
||||||
|
**麵包屑用帶路徑 wikilink**(issue #7):H1 次行放 `← [[<bucket>/00-INDEX]]` 指回桶子索引。
|
||||||
|
桶子索引固定名 `00-INDEX` 跨桶會撞名,故**指 00-INDEX 一律帶路徑**(`[[pkm/00-INDEX]]`,Logseq 原生支援、下游 ingest 也能對應到具體檔)。普通卡片間連結仍用裸 `[[卡名]]`(卡名唯一,不需路徑)。
|
||||||
|
|
||||||
|
**frontmatter 標籤分類**(issue #8):
|
||||||
|
- **用 frontmatter `tags:` 而非行內 `#tag`**:卡片內文常大量用 `#`(講筆記法時的 `#猜想`、`#book100`),分類標籤若也行內 `#`,下游 ingest 無法區分「分類」與「內文範例」會污染 graph。frontmatter 與內文完全分開,零歧義。
|
||||||
|
- **用標籤而非資料夾分類**:資料夾=強制單一歸屬;標籤=多重歸屬。一張卡可同時屬知識管理+AI協作+架構設計,硬塞一個資料夾會在其他檢索角度漏掉。
|
||||||
|
- **雙軸 taxonomy**(寫進 `TAXONOMY.md` 當字典;**受控擴充**,非凍結):
|
||||||
|
- 領域(主軸,1-3 個):如 知識管理/學習認知/AI協作/生產力/系統設計/工具教學
|
||||||
|
- 形態(副軸,0-2 個):方法論/工具實作/觀點主張/架構設計/案例經驗
|
||||||
|
- 一般開發專案的軸可不同(如 子系統/層級/決策類型),由 AI 依專案性質提出、寫進 TAXONOMY.md。
|
||||||
|
- **遇到現有軸裝不下的內容**:先查是否只是現有標籤的同義詞;確實是新軸才加進 TAXONOMY.md(附定義)再用——**禁止繞過字典在卡片直接冒新標籤**。字典是 per-repo,跨 repo 不必共用。
|
||||||
|
|
||||||
|
**typed-edge 規則**(issue #5,把「關係」也預編譯,下游 ingest 直接 parse 出帶類型的有向邊):
|
||||||
|
1. **方向性**:`A >> 謂詞 >> B` 必須讀成「A(謂詞)B」一句通順的話;A、B 順序就是主→賓真實方向。
|
||||||
|
2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、是…的實作),動詞天然帶方向。
|
||||||
|
3. **謂詞自由書寫,不受控詞彙**:下游對謂詞 embedding 時同義謂詞會自動聚類;但方向仍靠書寫順序保證。
|
||||||
|
4. **向後相容**:純 `[[A]]` 仍合法(視為無類型邊),盡量補謂詞。
|
||||||
|
|
||||||
|
> `>>` 是分隔語法,repo 可自選符號,但全程一致。
|
||||||
|
|
||||||
|
**萃 gloss 規則**(issue #9,把「node 的一句說明」也預編譯,供下游 KBDB 語義 normalize):
|
||||||
|
- **gloss = 這個 entity / graph node 是什麼的一句話**。下游對「entity 名 + gloss」一起做 embedding 求相似度,自動歸一同義詞(比只對名字準、比手維護 alias 表自動)。
|
||||||
|
- **在知識生產的當下、由 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]]`。
|
||||||
|
|
||||||
|
> 與 claude.ai Cowork 的 `system-dev/docs/SKILL.md` 改寫邏輯一致,兩條路徑(CC / Cowork)產出同一種 wiki。
|
||||||
|
|
||||||
|
### 第六步:完成報告 + 驗證
|
||||||
|
|
||||||
|
完成後**驗證原文 0 動**(踩過的坑,issue #8):
|
||||||
|
```
|
||||||
|
git status --short pages/ journals/ # 或一般專案的 docs/ ——須 0 新增 0 修改
|
||||||
|
```
|
||||||
|
|
||||||
|
> **改寫時必守**(subagent 尤其):
|
||||||
|
> 1. **絕不寫入 raw source**:subagent 目標一律給絕對路徑到 `cards/<bucket>/`,明寫「絕不寫入 pages/journals/docs 原稿」;事後用上面的 `git status` 驗。
|
||||||
|
> 2. **檔名 = 卡片全名**,否則 `[[全名]]` 對不到檔。冒號用全形「:」、斜線用全形「/」,**全程一種字元**,避免 `/`、`∕`、`:` 混用斷鏈。
|
||||||
|
> 3. **量大用 Haiku 並行改寫**,主模型只切概念邊界+審稿+修跨資料夾斷鏈。
|
||||||
|
|
||||||
告知:
|
告知:
|
||||||
```
|
```
|
||||||
✅ wiki-init 完成
|
✅ wiki-init 完成
|
||||||
建立了:[列出新建的目錄和檔案]
|
建立了:[列出新建的目錄和檔案]
|
||||||
跳過了:[列出已有因此不動的]
|
跳過了:[列出已有因此不動的]
|
||||||
|
改寫了:[N 份原文 → M 張原子卡、K 條 typed-edge、M 條 gloss(deep tier)]
|
||||||
|
原文驗證:pages/ journals/ git status 0 異動 ✅
|
||||||
下一步:用 /wiki-capture 把重要決策存進 wiki
|
下一步:用 /wiki-capture 把重要決策存進 wiki
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# /wiki-recall — Session 開始,手動接關
|
||||||
|
|
||||||
|
開新對話時接上次進度。**Fallback 命令**:SessionStart hook 沒啟動時手動接關;要完整脈絡時也用。
|
||||||
|
|
||||||
|
> 主路徑是 SessionStart hook 自動注入 status 重點,不靠你打命令。
|
||||||
|
> 這支命令應對 hook 失效,以及需要比「status 重點」更完整脈絡的時候。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 命名閉環
|
||||||
|
|
||||||
|
init(建) → update(存,session 末) ↔ **recall(接,session 初)** → capture(隨時存結論)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 執行流程
|
||||||
|
|
||||||
|
### 第一步:讀 status.md(當前進度)
|
||||||
|
|
||||||
|
讀 `system-dev/wiki/status.md`,掌握:
|
||||||
|
- 正在做什麼、阻擋點
|
||||||
|
- 下次 session 第一件事
|
||||||
|
- 待負責人確認、已知問題
|
||||||
|
|
||||||
|
### 第二步:讀 decisions-summary.md(為什麼這樣做)
|
||||||
|
|
||||||
|
讀 `system-dev/wiki/decisions-summary.md`,掌握相關的架構決策——避免重新討論已定案的事。
|
||||||
|
|
||||||
|
### 第三步:讀 mistakes.md(別重犯)
|
||||||
|
|
||||||
|
讀 `system-dev/wiki/mistakes.md`,掌握已知誤解 + 快速檢查清單。
|
||||||
|
|
||||||
|
### 第四步:掃 wishlist / HANDOFF(如果有)
|
||||||
|
|
||||||
|
- `docs/wishlist.md`:待補功能
|
||||||
|
- 任何 `HANDOFF.md` / 交接note:上一棒留下的脈絡
|
||||||
|
|
||||||
|
### 第五步:回報接關結果
|
||||||
|
|
||||||
|
```
|
||||||
|
📍 接關完成
|
||||||
|
🔄 上次正在做:[status 的「正在做」]
|
||||||
|
🎯 下次第一件事:[status 的「下次 session 第一件事」]
|
||||||
|
⚠️ 待確認:[如有]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 鐵律:快照非即時狀態
|
||||||
|
|
||||||
|
status / wiki 是 **point-in-time 快照,不是即時狀態**。
|
||||||
|
|
||||||
|
接關 = 讀快照 **+ 核實快照**,**不盲信**。
|
||||||
|
|
||||||
|
> 實例:某專案 status 曾寫「待 A 收尾 X」,實際 X 早已完成。
|
||||||
|
> 照舊資訊行動會去催一件已完成的事。
|
||||||
|
|
||||||
|
動手前,先用當前 code / git / 檔案核實快照寫的事項是否仍成立。發現落差 → 先更新 status,再動手。
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PreToolUse hook 範本骨架 —— 專案自訂禁令(預設空殼,不攔任何東西)
|
||||||
|
#
|
||||||
|
# ⚠️ 定位(讀清楚再用):
|
||||||
|
# 這支跟其他三支 hook 不同——它不是「裝上就生效的警察」,而是一個「按需手填的
|
||||||
|
# 空插槽」。預設狀態下 FORBIDDEN_PATTERNS 是空的,它【不攔任何東西】。
|
||||||
|
# 別誤以為裝了它就有保護——空殼 = 沒保護。
|
||||||
|
#
|
||||||
|
# 🤖 有 CC 在場的話,通常不需要這個範本:
|
||||||
|
# 直接叫你的 CC「幫我寫一支 guard hook,禁止改 X」。CC 現寫的條件邏輯,
|
||||||
|
# 表達力遠勝這裡的 glob FORBIDDEN_PATTERNS(例如「禁子 repo 的 code 但放行 .md」
|
||||||
|
# 這種細緻規則,glob 寫不出來,CC 的條件判斷寫得出來)。
|
||||||
|
# 這個範本只對「不靠 CC、想自己手動 DIY bash」的用戶有價值。
|
||||||
|
#
|
||||||
|
# 要啟用(手動 DIY 路線):
|
||||||
|
# 1. 在下面 FORBIDDEN_PATTERNS 填禁改的路徑/檔名 pattern
|
||||||
|
# 2. 到 .claude/settings.json 的 PreToolUse 加掛這支
|
||||||
|
#
|
||||||
|
# 掛在 PreToolUse(matcher: Write|Edit)。stdin 收 JSON:{ tool_name, tool_input:{ file_path } }
|
||||||
|
# 命中禁令 → exit 2 擋。
|
||||||
|
#
|
||||||
|
# 誠實限制:只擋直接寫檔。bash 繞道、helper 間接改動擋不到。留痕可審 ≠ 技術防偽。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── 專案自訂:禁改的 pattern(一行一個,case glob 語法)──────
|
||||||
|
# 範例(已註解,啟用前請改成自己的):
|
||||||
|
# "*/db/schema.sql" # 禁手改 schema
|
||||||
|
# "*/migrations/*" # migration 一旦建立不可改
|
||||||
|
FORBIDDEN_PATTERNS=(
|
||||||
|
# "*/your/protected/path/*"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 沒設任何禁令 → 空殼狀態,安靜放行。
|
||||||
|
# (不在這裡 print——PreToolUse 每次 Write/Edit 都會跑,每次喊話會洗版。
|
||||||
|
# 「這是空殼」的提醒改由 install.sh / update.sh 安裝時告知,那裡用戶一定看得到。)
|
||||||
|
[ ${#FORBIDDEN_PATTERNS[@]} -eq 0 ] && exit 0
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||||
|
else
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$FILE_PATH" ] && exit 0
|
||||||
|
|
||||||
|
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
|
||||||
|
# shellcheck disable=SC2254
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
$pattern)
|
||||||
|
cat >&2 <<EOF
|
||||||
|
🚫 專案禁令攔截:$FILE_PATH 命中禁改規則($pattern)。
|
||||||
|
|
||||||
|
這是本專案 .claude/hooks/pre-write-guard.sh 設定的硬底線。
|
||||||
|
要動 → 先和負責人確認,並更新禁令設定。
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PreToolUse hook 範本骨架 —— 專案自訂禁令(預設空殼,不攔任何東西)
|
||||||
|
#
|
||||||
|
# ⚠️ 定位(讀清楚再用):
|
||||||
|
# 這支跟其他三支 hook 不同——它不是「裝上就生效的警察」,而是一個「按需手填的
|
||||||
|
# 空插槽」。預設狀態下 FORBIDDEN_PATTERNS 是空的,它【不攔任何東西】。
|
||||||
|
# 別誤以為裝了它就有保護——空殼 = 沒保護。
|
||||||
|
#
|
||||||
|
# 🤖 有 CC 在場的話,通常不需要這個範本:
|
||||||
|
# 直接叫你的 CC「幫我寫一支 guard hook,禁止改 X」。CC 現寫的條件邏輯,
|
||||||
|
# 表達力遠勝這裡的 glob FORBIDDEN_PATTERNS(例如「禁子 repo 的 code 但放行 .md」
|
||||||
|
# 這種細緻規則,glob 寫不出來,CC 的條件判斷寫得出來)。
|
||||||
|
# 這個範本只對「不靠 CC、想自己手動 DIY bash」的用戶有價值。
|
||||||
|
#
|
||||||
|
# 要啟用(手動 DIY 路線):
|
||||||
|
# 1. 在下面 FORBIDDEN_PATTERNS 填禁改的路徑/檔名 pattern
|
||||||
|
# 2. 到 .claude/settings.json 的 PreToolUse 加掛這支
|
||||||
|
#
|
||||||
|
# 掛在 PreToolUse(matcher: Write|Edit)。stdin 收 JSON:{ tool_name, tool_input:{ file_path } }
|
||||||
|
# 命中禁令 → exit 2 擋。
|
||||||
|
#
|
||||||
|
# 誠實限制:只擋直接寫檔。bash 繞道、helper 間接改動擋不到。留痕可審 ≠ 技術防偽。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── 專案自訂:禁改的 pattern(一行一個,case glob 語法)──────
|
||||||
|
# 範例(已註解,啟用前請改成自己的):
|
||||||
|
# "*/db/schema.sql" # 禁手改 schema
|
||||||
|
# "*/migrations/*" # migration 一旦建立不可改
|
||||||
|
FORBIDDEN_PATTERNS=(
|
||||||
|
# "*/your/protected/path/*"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 沒設任何禁令 → 空殼狀態,安靜放行。
|
||||||
|
# (不在這裡 print——PreToolUse 每次 Write/Edit 都會跑,每次喊話會洗版。
|
||||||
|
# 「這是空殼」的提醒改由 install.sh / update.sh 安裝時告知,那裡用戶一定看得到。)
|
||||||
|
[ ${#FORBIDDEN_PATTERNS[@]} -eq 0 ] && exit 0
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||||
|
else
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$FILE_PATH" ] && exit 0
|
||||||
|
|
||||||
|
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
|
||||||
|
# shellcheck disable=SC2254
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
$pattern)
|
||||||
|
cat >&2 <<EOF
|
||||||
|
🚫 專案禁令攔截:$FILE_PATH 命中禁改規則($pattern)。
|
||||||
|
|
||||||
|
這是本專案 .claude/hooks/pre-write-guard.sh 設定的硬底線。
|
||||||
|
要動 → 先和負責人確認,並更新禁令設定。
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PreToolUse hook — 動 code 前檢查有沒有對應 SDD
|
||||||
|
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
|
||||||
|
#
|
||||||
|
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
||||||
|
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, ... } }
|
||||||
|
# 行為:動到 code 檔(.ts/.go/...)但 system-dev/docs/3-specs/ 下沒有任何 SDD → 警告(exit 2 擋)。
|
||||||
|
#
|
||||||
|
# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。
|
||||||
|
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
|
||||||
|
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
|
||||||
|
# 解析 file_path。優先用 jq,沒有 jq 退回 grep(容錯)。
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||||
|
else
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
||||||
|
[ -z "$FILE_PATH" ] && exit 0
|
||||||
|
|
||||||
|
# 只管 code 檔。docs/markdown/設定檔等放行。
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 改 SDD 自己 / 測試檔 → 放行
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*system-dev/docs/3-specs/*) exit 0 ;;
|
||||||
|
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 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),但 system-dev/docs/3-specs/ 下找不到任何 SDD。
|
||||||
|
|
||||||
|
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md)。
|
||||||
|
|
||||||
|
請先:
|
||||||
|
1. 確認這個改動屬於哪個子系統
|
||||||
|
2. 在 system-dev/docs/3-specs/[子系統]/ 建立 design.md(可用 /sdd-check 協助)
|
||||||
|
3. 在回覆開頭宣告已讀 SDD + 對應 task
|
||||||
|
|
||||||
|
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 有 SDD:放行,但留痕提醒要宣告(stderr 警告,不擋)
|
||||||
|
echo "📋 提醒:system-dev/docs/3-specs/ 下有 SDD。動手前請確認已讀對應 design.md 並在回覆宣告。" >&2
|
||||||
|
exit 0
|
||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# SessionStart hook — 開 session 自動注入 status.md 重點
|
||||||
|
# wishlist §1 主路徑:不靠 CC 自覺、不用人說,開 session 就把進度推到眼前。
|
||||||
|
#
|
||||||
|
# 掛在 settings.json 的 SessionStart(matcher: startup|resume|clear)。
|
||||||
|
# stdout 會被當成 context 注入給 CC。
|
||||||
|
#
|
||||||
|
# 鐵律:status 是 point-in-time 快照,非即時狀態。
|
||||||
|
# 這個 hook 只負責「把快照推到眼前」,核實快照是 CC 的責任——下面的提醒就是要它別盲信。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STATUS_FILE="system-dev/wiki/status.md"
|
||||||
|
PRINCIPLES_FILE="system-dev/wiki/principles.md"
|
||||||
|
MISTAKES_FILE="system-dev/wiki/mistakes.md"
|
||||||
|
|
||||||
|
# ── 舊結構防呆(1.9.0 遷移第 2 層保險)──
|
||||||
|
# 新版 wiki 收進 system-dev/。若偵測到舊位置 .claude/wiki/ 還在、但新位置沒 status,
|
||||||
|
# 代表使用者升級了規則卻沒跑遷移(low-code 使用者常見)→ 出聲提示,讓 CC 當場可代為遷移,
|
||||||
|
# 不要默默用不到新結構而出錯。
|
||||||
|
if [ -d ".claude/wiki" ] && [ ! -f "$STATUS_FILE" ]; then
|
||||||
|
echo "⚠️ 偵測到舊版 wiki 結構(.claude/wiki/),尚未遷移到 system-dev/wiki/。"
|
||||||
|
echo " 請跑:bash system-dev/scripts/update.sh"
|
||||||
|
echo " 或直接叫我(CC):「幫我把 wiki 遷移到 system-dev/」——我可以代為搬移。"
|
||||||
|
echo " (未遷移時接關與 /wiki-init 會找錯位置。)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 三個 push 檔都沒有 → 安靜退出,不干擾還沒 /wiki-init 的專案
|
||||||
|
if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── push 1/3:principles(全文,行動前必服從)──
|
||||||
|
# 放最前:原則是「會被遺忘的盲區」,要第一眼看見。全文成本低(一行一條、≤15 條)。
|
||||||
|
if [ -f "$PRINCIPLES_FILE" ] && grep -q '^- ' "$PRINCIPLES_FILE" 2>/dev/null; then
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "📐 設計原則(行動前必服從,來自 principles.md)"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
grep '^- ' "$PRINCIPLES_FILE" # 只注入原則條目本身,不含說明區
|
||||||
|
# context 保護:原則應 ≤15 條(push 全文)。超過 → 提示該合併或下放成 card,不截斷(截斷會漏原則)。
|
||||||
|
P_COUNT=$(grep -c '^- ' "$PRINCIPLES_FILE")
|
||||||
|
if [ "$P_COUNT" -gt 15 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "(⚠️ principles 已 ${P_COUNT} 條 > 15:請考慮合併相近原則、或把較細的下放成 card)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── push 2/3:status(全文,當前進度)──
|
||||||
|
if [ -f "$STATUS_FILE" ]; then
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "📍 接關:上次進度(來自 $STATUS_FILE 快照)"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
cat "$STATUS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── push 3/3:mistakes(標題清單 + 一行症狀,全文按需展開)──
|
||||||
|
# 不全文注入(可能累積很多、含長 context)。只給「標題行」讓 CC 認出「我正撞到某條」→ 再去讀全文。
|
||||||
|
if [ -f "$MISTAKES_FILE" ] && grep -q 'MISTAKE' "$MISTAKES_FILE" 2>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "────────────────────────────────────────────────"
|
||||||
|
echo "⚠️ 已知踩坑(標題;撞到時讀 $MISTAKES_FILE 全文)"
|
||||||
|
grep -E 'MISTAKE' "$MISTAKES_FILE" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "────────────────────────────────────────────────"
|
||||||
|
echo "⚠️ 以上是 point-in-time 快照,非即時狀態。"
|
||||||
|
echo " 動手前先核實:快照寫的事項是否真的還沒做完?"
|
||||||
|
echo " 需要完整脈絡(decisions / mistakes / SDD)→ 執行 /wiki-recall"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
# ── 遷移殘留偵測(1.9.x):wiki 已搬到 system-dev/,但 CLAUDE.md 內容裡可能還寫舊路徑 ──
|
||||||
|
# update.sh 鐵則「絕不碰 CLAUDE.md」(用戶資料),所以遷移不會自動改它內容裡的死引用。
|
||||||
|
# 這裡只「偵測 + 提示 CC 代修」——由 CC(看得懂語義、不誤傷用戶自寫內容)來改,不讓腳本盲改。
|
||||||
|
if [ -f "CLAUDE.md" ] && grep -q '\.claude/wiki' CLAUDE.md 2>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "🔧 偵測到 CLAUDE.md 內仍有舊路徑 \`.claude/wiki/\`(wiki 已遷移到 system-dev/wiki/)。"
|
||||||
|
echo " 這些是遷移後的死引用,會讓你(CC)找錯位置。"
|
||||||
|
echo " 請主動幫使用者把 CLAUDE.md 裡的 \`.claude/wiki/\` 改成 \`system-dev/wiki/\`、"
|
||||||
|
echo " 工具 docs 路徑改成 \`system-dev/docs/\`(但 raw source 宣告的 \`docs/\` 保留不動)。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+113
@@ -0,0 +1,113 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PreToolUse hook — 寫入 wiki 前掃機敏資訊(L3 硬攔截)
|
||||||
|
#
|
||||||
|
# 為什麼存在:wiki 的 ignore 規則(.wikiignore + 行內標記)是「協議層」,靠 CC 遵守。
|
||||||
|
# 但密碼/金鑰/個資外洩是「不可逆」後果——只靠口頭約束太危險。
|
||||||
|
# 這支 hook 是機械式底線:CC 真的把機敏資訊寫進 system-dev/wiki/ 的那一刻 → exit 2 擋下。
|
||||||
|
#
|
||||||
|
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
||||||
|
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, content?, new_string? } }
|
||||||
|
# 行為:只在目標路徑是 system-dev/wiki/** 時啟動,掃要寫入的內容,命中機敏特徵 → exit 2。
|
||||||
|
#
|
||||||
|
# 誠實限制(抄 sdd-guard):regex 偵測有偽陰/偽陽。
|
||||||
|
# 擋的是「明顯特徵的機敏字串被自動抄進 wiki」,擋不了刻意混淆/編碼的繞道。
|
||||||
|
# 價值是「意外外洩的機械底線 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
|
||||||
|
# ── 解析 file_path 與要寫入的內容。優先 jq,無 jq 退回 grep(容錯)──────
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||||
|
# Write 用 content;Edit 用 new_string。兩個都抓,合起來掃。
|
||||||
|
CONTENT=$(printf '%s' "$INPUT" | jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")')
|
||||||
|
else
|
||||||
|
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||||
|
# 無 jq 時內容解析不可靠(JSON 跳脫),退回掃整包 INPUT,寧可多掃不漏掃
|
||||||
|
CONTENT="$INPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
||||||
|
[ -z "$FILE_PATH" ] && exit 0
|
||||||
|
|
||||||
|
# 只管寫進 wiki 的動作。其他路徑放行(這支專責 wiki 洩漏,不是全域 secret scanner)
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*system-dev/wiki/*) ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$CONTENT" ] && exit 0
|
||||||
|
|
||||||
|
# 行內豁免:若該段內容已被標記為刻意保留(例:範例文件要示範格式),略過該行
|
||||||
|
# 標記:行尾加 # wiki-secret-ok (或 <!-- wiki-secret-ok -->)
|
||||||
|
# 先把標記過的行抽掉再掃。
|
||||||
|
SCAN=$(printf '%s' "$CONTENT" | grep -v -E 'wiki-secret-ok' || true)
|
||||||
|
[ -z "$SCAN" ] && exit 0
|
||||||
|
|
||||||
|
# ── 機敏特徵 pattern。一行一類,命中即攔。──────────────────────────
|
||||||
|
# 設計取捨:偏向高訊號 pattern(有明確結構的金鑰/標記),降低偽陽。
|
||||||
|
# 純「password=xxx」這類也納入,因為那正是使用者最擔心的場景。
|
||||||
|
HITS=""
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local label="$1" regex="$2"
|
||||||
|
# -e 讓以 - 開頭的 pattern(如 PEM 的 -----BEGIN)不被當成選項。
|
||||||
|
# grep 無命中回傳 1,在 set -e 下會中止 → 用 if 包住吸收掉。
|
||||||
|
if printf '%s' "$SCAN" | grep -qiE -e "$regex"; then
|
||||||
|
HITS="${HITS}
|
||||||
|
• ${label}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 密碼/密鑰賦值(password = ..., secret: ..., api_key=...)
|
||||||
|
check "密碼/密鑰賦值 (password/secret/api_key/token = ...)" \
|
||||||
|
'(pass(word)?|secret|api[_-]?key|access[_-]?key|auth[_-]?token|priv(ate)?[_-]?key)[[:space:]]*[:=][[:space:]]*[^[:space:]<>"'"'"']{6,}'
|
||||||
|
|
||||||
|
# 私鑰 PEM 區塊
|
||||||
|
check "私鑰檔內容 (BEGIN ... PRIVATE KEY)" \
|
||||||
|
'-----BEGIN[[:space:]].*PRIVATE KEY-----'
|
||||||
|
|
||||||
|
# 常見雲端/服務金鑰前綴
|
||||||
|
check "服務金鑰特徵 (AWS/GitHub/Slack/Google/Stripe 等)" \
|
||||||
|
'(AKIA[0-9A-Z]{16}|gh[pousr]_[0-9A-Za-z]{20,}|xox[baprs]-[0-9A-Za-z-]{10,}|AIza[0-9A-Za-z_-]{20,}|sk_(live|test)_[0-9A-Za-z]{16,})'
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
check "JWT token" \
|
||||||
|
'eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}'
|
||||||
|
|
||||||
|
# 連線字串內嵌帳密 (proto://user:pass@host)
|
||||||
|
check "連線字串內嵌帳密 (proto://user:pass@host)" \
|
||||||
|
'[a-z][a-z0-9+.-]*://[^[:space:]:/@]+:[^[:space:]:/@]+@'
|
||||||
|
|
||||||
|
# 台灣身分證字號(個資)。BSD/GNU grep 都支援 ERE,避免 \b(BSD 不認),改用字元類邊界。
|
||||||
|
check "台灣身分證字號 (個資)" \
|
||||||
|
'(^|[^A-Za-z0-9])[A-Z][12][0-9]{8}([^0-9]|$)'
|
||||||
|
|
||||||
|
# 信用卡號(個資,粗略 13-16 連續數字,可含空格/連字號分隔)。避免 PCRE,用 ERE 近似。
|
||||||
|
check "疑似信用卡號 (個資)" \
|
||||||
|
'(^|[^0-9])[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{0,4}([^0-9]|$)'
|
||||||
|
|
||||||
|
# Email 不擋(wiki 常需記聯絡人),手機號也不擋(偽陽太高)——刻意留白。
|
||||||
|
|
||||||
|
if [ -n "$HITS" ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
🚫 Wiki 機敏攔截:偵測到可能的機敏資訊要寫進 ${FILE_PATH}。
|
||||||
|
|
||||||
|
命中特徵:${HITS}
|
||||||
|
|
||||||
|
wiki 是會被 CC 反覆讀取、可能進版控的記憶空間。
|
||||||
|
密碼 / 金鑰 / 個資寫進去 = 不可逆外洩風險。
|
||||||
|
|
||||||
|
請改成下列任一做法:
|
||||||
|
1. 不要把機敏值寫進 wiki,改記「位置」(例:「DB 密碼放 1Password / .env,不入 wiki」)
|
||||||
|
2. 確定是誤判(例:在示範格式)→ 該行尾加註記 wiki-secret-ok 後重寫
|
||||||
|
3. 整個來源檔本就機敏 → 加進 system-dev/wiki/.wikiignore,別讓它被編入
|
||||||
|
|
||||||
|
誠實限制:本掃描靠特徵比對,有偽陽/偽陰,是「意外外洩的機械底線」而非保險箱。
|
||||||
|
真正的密鑰本就不該進版控。
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# .claude/wiki/ — LLM 記憶系統
|
|
||||||
|
|
||||||
> 新 session 開始時從這裡導航。
|
|
||||||
> 目的:讓 CC 不需要重新學習已知的事。
|
|
||||||
> 維護者:CC(人不手動編輯這裡)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 核心檔案
|
|
||||||
|
|
||||||
| 檔案 | 何時讀 | 內容 |
|
|
||||||
|------|-------|------|
|
|
||||||
| `status.md` | session 開始第一件事 | 當前進度、下一步 |
|
|
||||||
| `mistakes.md` | 做新功能前 | 已知誤解、快速檢查清單 |
|
|
||||||
| `decisions-summary.md` | 遇到設計判斷時 | 架構決策摘要 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 維護規則
|
|
||||||
|
|
||||||
1. 只增不刪——記錄 append,決策改了加新條目說明「舊決策已更新」
|
|
||||||
2. status.md 每次 session 結束更新
|
|
||||||
3. mistakes.md 每次被糾正後 append
|
|
||||||
4. 發現新的重要決策 → 同時更新 decisions-summary.md 和 docs/2-architecture/decisions/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 快速導航
|
|
||||||
|
|
||||||
**這個專案是什麼**:KBDB-graph —— KBDB 的 graph 插件(triplet 採集 + graph 查詢),類比 Apache AGE 之於 Postgres。要抽成獨立 repo(leo 產權)。基本盤(block CRUD)在 `arcrun/kbdb`,不在這。
|
|
||||||
|
|
||||||
**動工前必讀**:
|
|
||||||
- `docs/HANDOFF-kbdb-plugin.md` —— 本目錄專屬交棒(待辦:確認邊界 / 改名+git init+推 GitHub / 定義掛載介面)。
|
|
||||||
- 上游約束見 `CLAUDE.md` 最頂 + `github.com/uncle6me-web/InkStoneCo`。
|
|
||||||
|
|
||||||
**文件去哪找**:
|
|
||||||
- SDD(design+tasks)→ `docs/3-specs/`(現有:arcrun-key-auth、blocks-edit-api)
|
|
||||||
- 歷史記錄 / bug 復盤 → `docs/5-records/`(現有:PATCH 403 bug、upsert feature request)
|
|
||||||
- 分類規則全表 → `docs/README.md`
|
|
||||||
|
|
||||||
**絕對限制**:本目錄只做 graph 插件 / 萬物皆 Block(禁 CREATE/ALTER TABLE)/ API-as-Wall / 部署繞開 GitHub、禁跨 repo Actions / 樂高法(actions < 100 行)。
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# 當前狀態
|
|
||||||
|
|
||||||
> 更新時間:2026-06-14
|
|
||||||
> 每次 session 結束必須更新此檔(用 /wiki-update)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 已完成(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。
|
|
||||||
|
|
||||||
## 下次 session 第一件事
|
|
||||||
|
|
||||||
**實際部署**:等基本盤 `arcrun-kbdb` 上線/有網址後,跑 `bash scripts/install.sh` 一次到位
|
|
||||||
(自動查 CF subdomain 拼 `KBDB_BASE_URL` → `wrangler secret put` → `wrangler deploy`)。
|
|
||||||
現在不空跑部署(避免上線一個打不到基本盤的殼)。
|
|
||||||
|
|
||||||
## 已知缺口([→arcrun],待基本盤補)
|
|
||||||
|
|
||||||
- base 無 `PUT /records/:id` → entity addAlias 用「重建 record」覆寫。
|
|
||||||
- base 無 `DELETE /records/:id` → triplet/entity update/delete、pending confirm/reject 為 soft(不硬刪)。
|
|
||||||
- 語意搜尋 / embedding 屬基本盤 optional embed 模組,插件只做 keyword/exact。
|
|
||||||
- arcrun 端 MCP/CLI 的 KBDB 薄殼仍待補(見 arcrun HANDOFF §2);插件目前直打基本盤 HTTP API。
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# contracts/ — graph 插件邊界契約
|
||||||
|
|
||||||
|
> 本目錄放 graph 插件對外的 JSON Schema 契約。兩份契約是**不同方向的東西,別混**。
|
||||||
|
|
||||||
|
## 兩份契約的分工
|
||||||
|
|
||||||
|
| 檔案 | 是什麼 | 方向 | 誰產 / 誰收 |
|
||||||
|
|------|--------|------|-------------|
|
||||||
|
| `ingest-candidate.json` | **輸入候選**(envelope)| ingest 插件 **→** graph 插件 | ingest 萃取後送進來,graph 收下處理 |
|
||||||
|
| `triplet.json` | **已存三元組**(graph 內部存的單筆 S-P-O)| graph 內部 / 對查詢面 | graph 寫進基本盤 records 後的形態 |
|
||||||
|
|
||||||
|
**核心區別**:`ingest-candidate` 是「還沒進庫的一批原始萃取」,`triplet` 是「已經正規化、存好、可查的單筆」。
|
||||||
|
|
||||||
|
- ingest 只送原始 `(subject, predicate, object) + confidence`,**禁送** graph 領域欄位(`id` / `clusters` / `bridge_score` / `created_at` / triplet 上的 `*_entity_type`)。送了 → graph 以 422 拒收(`additionalProperties: false`)。
|
||||||
|
- 正規化、clusters、bridge_score、embed、`status`/`superseded_by` 取代邏輯——全是 **graph 領域**,ingest 一無所知(純餵食器)。
|
||||||
|
|
||||||
|
## ingest-candidate envelope 重點
|
||||||
|
|
||||||
|
- **一個 envelope = 一個來源檔(canonical MD)一次萃取的產物**。
|
||||||
|
- `source.uri`(穩定識別)+ `source.content_hash`(快照鍵)共同決定 idempotency:
|
||||||
|
- 同 uri + 同 hash → no-op(`{skipped:true}`)。
|
||||||
|
- 同 uri + 新 hash → **deprecate-then-append**(舊 active 翻 `status=deprecated` + `superseded_by`,append 新批 active)。
|
||||||
|
- `nodes[]`(選填)帶 `gloss` / `entity_type` —— **節點屬性**,不是邊屬性,故獨立於 `triplets[]`。graph 用 gloss 去 embed(每節點一句,非裸詞)。
|
||||||
|
|
||||||
|
完整 schema 與欄位說明見 `ingest-candidate.json` 的 `description` / `$comment`。
|
||||||
|
本 repo 對此契約的實作(端點、取代邏輯、測試)見 `docs/3-specs/ingest-contract/`。
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "IngestCandidateEnvelope",
|
||||||
|
"description": "ingest 插件 → graph 插件 的唯一邊界契約。一個 envelope = 一個來源檔(canonical MD)一次萃取的產物。graph 收下後負責正規化/clusters/bridge_score/embed,並以『翻 triplet template 實例的 status slot』做取代:同 source.uri 出新 content_hash 時,graph 把舊 active 實例 PATCH 成 deprecated、append 新批 active(可查、可 rollback、可清),ingest 對這些一無所知。注意:這是【輸入候選】,不是【已存三元組】(後者見 triplet.json)。",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["source", "extractor", "triplets"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"source": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "這批候選來自哪個 canonical MD。同時是『指回原文的指標』與『append-only 快照鍵』。",
|
||||||
|
"required": ["uri", "content_hash"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"uri": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "來源的穩定識別 = 快照鍵 + get_source 指標。格式: 'github:<owner>/<repo>@<path>' 例如 'github:uncle6me-web/LLM-Wiki-for-n8n@.claude/wiki/graph-rag.md'。同一 uri 的後續 envelope 會【取代】(latest-wins)前一批,而非疊加。"
|
||||||
|
},
|
||||||
|
"content_hash": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "來源檔內容的 hash(快照鍵)。graph 比對: 與該 uri 現存快照同 hash → no-op 跳過;不同 → 寫新快照。"
|
||||||
|
},
|
||||||
|
"anchor": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "檔內定位(heading slug / block id),供 get_source 精準回跳。選填。"
|
||||||
|
},
|
||||||
|
"commit": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "git commit sha(可追溯)。選填。"
|
||||||
|
},
|
||||||
|
"block_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "向後相容: Logseq Block ID(= 既有 triplet.json 的 source_block_id)。非 git 來源時用。選填。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extractor": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "萃取出處。供『升級率』觀測與『要不要重萃』判斷;不影響圖結構。",
|
||||||
|
"required": ["model", "tier"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "產生這批的模型,例如 'workers-ai/@cf/...' 或 'claude-sonnet-4-6'。"
|
||||||
|
},
|
||||||
|
"tier": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["shallow", "deep"],
|
||||||
|
"description": "shallow = Workers AI 淺萃;deep = Claude API 深萃(淺萃 JSON-fail/過稀時升級)。"
|
||||||
|
},
|
||||||
|
"extracted_at": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "萃取的 unix 時間(秒)。快照排序用。選填(graph 收件時可補)。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodes": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "節點層附帶資訊(選填)。entity_type 與 gloss 是【節點】屬性,不是【邊】屬性 → 放這裡,不放 triplets。graph 用 gloss 去 embed(每節點一句,不是裸詞)、用 entity_type 去 typing。",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "節點名(須對應某 triplet 的 subject 或 object 原字面)。"
|
||||||
|
},
|
||||||
|
"gloss": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "一句話描述,供 embedding。例如 'Graph RAG — 用關係遍歷檢索、保住異見的 RAG 變體'。選填(建議 deep tier 產出)。"
|
||||||
|
},
|
||||||
|
"entity_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["person", "event", "product", "market", "org"],
|
||||||
|
"description": "節點類型提示。graph 最終決定;ingest 只提示。選填。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"triplets": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"description": "邊(關係)。ingest 只產原始 (s,p,o) + confidence。",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["subject", "predicate", "object"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"subject": { "type": "string", "minLength": 1, "description": "主詞(實體名,須與 nodes[].name 對得上若有提供)" },
|
||||||
|
"predicate": { "type": "string", "minLength": 1, "description": "謂詞(關係)" },
|
||||||
|
"object": { "type": "string", "minLength": 1, "description": "受詞(目標實體或值)" },
|
||||||
|
"confidence":{ "type": "number", "minimum": 0, "maximum": 1, "default": 1.0, "description": "萃取可信度。淺萃可附自評;graph 不據此過濾,只記錄。" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$comment": "禁止欄位(graph 領域,ingest 絕不可送): id / clusters / bridge_score / created_at / updated_at / 以及 triplet 上的 subject_entity_type|object_entity_type(類型只走 nodes[])。送了即違反 ingest=純餵食器的邊界,graph 應拒收或忽略。"
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# ingest-contract — 設計
|
||||||
|
|
||||||
|
> **藍圖在頂層**:本 SDD 只放 **kbdb-graph-plugin 內部實作細節**。跨專案脈絡(為什麼拆 ingest/graph、mira 蒸發、整體資料流)見 InkStoneCo `docs/3-specs/mira-dissolve/`(design + requirements)。
|
||||||
|
> **對應交辦**:[kbdb-graph-plugin #1](https://github.com/uncle6me-web/kbdb-graph-plugin/issues/1)(T3)。
|
||||||
|
> **鐵律**:API-as-Wall / 零建表 / 零 SQL / 零 migration。embedding/語義 normalize 屬 base 模組,graph 只調不自算。圖在插件層記憶體組裝。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 範圍
|
||||||
|
|
||||||
|
graph 插件補上**寫入端**:收 ingest 送來的 `ingest-candidate` envelope,正規化後以「翻 triplet 的 `status` slot」做 **deprecate-then-append** 取代,並讓查詢面 active-only。查詢面(search/traverse/neighbors)已成熟,本批只補 active 過濾。
|
||||||
|
|
||||||
|
不在本批:圖工具併入 KBDB MCP 的**註冊薄殼**(需 arcrun 配合,見 §6);semantic normalize 端到端(依賴 base embed 部署)。
|
||||||
|
|
||||||
|
## 2. 邊界契約
|
||||||
|
|
||||||
|
唯一入口契約 = `contracts/ingest-candidate.json`(已搬入,T3.1)。要點見 `contracts/README.md`。
|
||||||
|
graph 收件後負責的領域欄位(ingest 禁送):`id` / `clusters` / `bridge_score` / `created_at` / triplet 上的 `*_entity_type`。`additionalProperties:false` → 違規欄位 422。
|
||||||
|
|
||||||
|
## 3. template slot 變更(T3.2 / 3.2b)
|
||||||
|
|
||||||
|
現況(`src/lib/templates.ts`,已核實):
|
||||||
|
- `TRIPLET_SLOTS` 無 `status` / `superseded_by`。
|
||||||
|
- `ENTITY_SLOTS` 無 `gloss`。
|
||||||
|
|
||||||
|
要加:
|
||||||
|
- triplet:`status`(`active` | `deprecated`,預設 `active`)、`superseded_by`(指向取代它的新 record id,空=未被取代)。
|
||||||
|
- entity:`gloss`(一句話描述,供「詞+gloss」語義 normalize 的 embedding 對象)。
|
||||||
|
|
||||||
|
### ⚠️ ensureTemplate early-return 陷阱(前置警示 1,已核實)
|
||||||
|
|
||||||
|
`KbdbClient.ensureTemplate`(`src/lib/kbdb-client.ts:120`)命中既有 template 即 `return`,**不會把新 slot 補進已 seed 的 template**。只改 `TRIPLET_SLOTS` 陣列對既有環境無效。
|
||||||
|
|
||||||
|
**對策**:`ensureTemplate` 改為「命中既有 → 比對 slot 差集 → 缺的走 base `PATCH /templates/:id` 補上」,而非 early-return。新環境(template 不存在)走原 `POST /templates` 路徑不變。如此既有 + 全新環境都正確收斂,不需另跑一次性遷移腳本。
|
||||||
|
|
||||||
|
## 4. KbdbClient.updateRecord(前置警示 2,已核實)
|
||||||
|
|
||||||
|
client 現有 `createRecord` / `getRecord` / `listRecordsByTemplate`,**無 record 層 PATCH**(`updateEntry` 是 entry 層,不是 record)。
|
||||||
|
|
||||||
|
新增 `updateRecord(recordId, values)` → 呼叫 base `PATCH /records/:id`(base #6 已就緒)。deprecate(翻 status)與 rollback(翻回 status)都靠它。
|
||||||
|
|
||||||
|
## 5. POST /triplets/ingest(核心,T3.3)
|
||||||
|
|
||||||
|
純函式 action(`src/actions/triplet-ingest.ts`,<100 行,第一參數收 `KbdbClient`),route 只驗證 + 呼叫。流程:
|
||||||
|
|
||||||
|
1. **驗證 envelope**:Zod schema 鏡射 `ingest-candidate.json`,`additionalProperties:false` → 禁送欄位(如 `bridge_score`)回 422。
|
||||||
|
2. **idempotency**:以 `source.uri` 為鍵查該來源現存 active triplet 的 `content_hash`(存進 triplet 的某 slot,見下)。同 hash → no-op 回 `{skipped:true}`。
|
||||||
|
3. **deprecate-then-append**:新 hash → 查該 uri 所有 active triplet,逐筆 `updateRecord(id, {status:'deprecated', superseded_by:<新批暫不知 id,二階段或留空>})`,再 append 新批 active。
|
||||||
|
4. **回應**:`{ ingested: N, deprecated: M, skipped: false }`。
|
||||||
|
|
||||||
|
### content_hash / source.uri 存哪
|
||||||
|
|
||||||
|
triplet record 需記住它來自哪個 snapshot 才能做 idempotency 與 active-only。沿用既有 `source_block_id` 不夠(那是 Logseq block)。設計:triplet template 增記 `source_uri` + `content_hash` slot(屬 §3 同批 slot 變更,非建表)。active-only 與 deprecate 都按 `source_uri` 分組。
|
||||||
|
|
||||||
|
> 註:`superseded_by` 指向「取代它的新 record id」。新批 record 是 append 後才有 id → 若要精確回填,deprecate 分兩步(先 append 拿 id,再翻舊批 status + superseded_by)。先 append 後 deprecate 比較安全(中途失敗不會留下「全無 active」的空窗)。
|
||||||
|
|
||||||
|
## 6. 查詢 active-only(T3.5)
|
||||||
|
|
||||||
|
traverse / search / neighbors 從 records 組鄰接表前,先 `filter(status === 'active')`(預設值缺省視為 active,相容舊資料)。active 集合永遠乾淨,deprecated 仍可查(rollback / 考古)但不進圖遍歷。
|
||||||
|
|
||||||
|
## 7. 跨 repo 協調點:圖工具併入 KBDB MCP(T3.6,需 arcrun 配合)
|
||||||
|
|
||||||
|
圖工具(traverse / neighbors / get_source)+ `refresh` 要**併進 arcrun 的 KBDB MCP**(`u6u-mcp-server`),**不另起 graph 獨立 MCP**。
|
||||||
|
|
||||||
|
- **graph repo 端能備好的**:圖查詢的 HTTP API + 邏輯、`get_source` 端點(T3.7)、`refresh` 代轉 ingest 的端點。
|
||||||
|
- **需 arcrun 端動的**:MCP 工具註冊薄殼那層 → 標清在 issue #1,由總管協調。本批不擅自開獨立 graph MCP。
|
||||||
|
- **`refresh` 紅線**(T3.6b):只能人發起的 MCP 調用觸發,**禁掛排程/webhook 自動 refresh**(否則變回 fan-out,踩 flag 紅線)。
|
||||||
|
- **T3.6d**:整合時移除 `search-query.ts` 代理 base 關鍵字那條(重複,關鍵字歸 KBDB MCP)。
|
||||||
|
|
||||||
|
## 7.5 get_source / refresh 落地(C 段,已實作)
|
||||||
|
|
||||||
|
- **get_source**(`graph-source.ts` + `GET /graph/source/:name`):給節點名 → 回觸及它的 active triplet 的來源指標(`uri` / `anchor` / `block_id` / `content_hash`),按 uri+anchor 去重。為此 triplet template 增 `source_anchor` slot(ingest 從 `source.anchor` 帶入)。
|
||||||
|
- **refresh**(`graph-refresh.ts` + `POST /graph/refresh`):純被動代轉 ingest 重抓+萃。graph 自己不抓不萃(ingest 純餵食器職責)。
|
||||||
|
- 🚫 紅線:只人發起 MCP 調用觸發,無排程/webhook。
|
||||||
|
- ingest 對象 = `KBDB_INGEST_URL`(env,T4 就緒前留空)。未設 → 誠實回 `{forwarded:false}`,不假綠。
|
||||||
|
- **search keyword 收斂**(T3.6d):`POST /search` 移除公開 keyword 模式(重複 KBDB MCP `kbdb_search`),收斂為 suggest-only。`keywordSearch` helper 保留為 suggest 內部建構塊。
|
||||||
|
|
||||||
|
## 8. 不做 / 延後
|
||||||
|
|
||||||
|
- **graph CLI**(T3.7b):延後。人少在命令行 traverse、AI 用不到 → 不做(非省工,是不誤導 AI 以為有這條路)。
|
||||||
|
- **semantic normalize**(T3.2c):本批先 exact-only,semantic 留接口。base embed(Arcrun #7)部署後才端到端,屆時標「待 base embed 部署驗」。
|
||||||
|
|
||||||
|
## 9. 樂高法遵循
|
||||||
|
|
||||||
|
- 每個 action <100 行、一檔一事、無狀態(狀態走 base API 持久化)。
|
||||||
|
- route 不含業務邏輯,只 `makeKbdbClient(c.env)` + 驗證 + 呼叫 action。
|
||||||
|
- 測試走 `tests/mock-client.ts`,不打真網路。
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# ingest-contract — tasks
|
||||||
|
|
||||||
|
> **唯一進度來源**,不靠對話記憶。對應 [issue #1](https://github.com/uncle6me-web/kbdb-graph-plugin/issues/1)(頂層 mira-dissolve T3)。
|
||||||
|
> 完成一項即時打勾 + 註記證據。端到端需 leo21c 部署的,標「待部署驗」,不假綠。
|
||||||
|
|
||||||
|
## A. 契約 + template slot
|
||||||
|
|
||||||
|
- [x] **3.1** 搬 `contracts/ingest-candidate.json` 進本 repo + `contracts/README.md` 標明候選≠已存(2026-06-26)
|
||||||
|
- [x] **3.2** `ensureTemplate` 改 slot-diff 補丁(命中既有 → base `PATCH /templates/:id` 補缺 slot,不再 early-return);`TRIPLET_SLOTS` 加 `status`+`superseded_by`+`source_uri`+`content_hash`(2026-06-26,`kbdb-client.ts`+`templates.ts`)
|
||||||
|
- [x] **3.2b** `ENTITY_SLOTS` 加 `gloss`(已核實現無)(2026-06-26)
|
||||||
|
- [ ] **3.2c** normalize 分層 fallback 接口:exact-only 先做;semantic 留接口(待 base embed,Arcrun #7)
|
||||||
|
|
||||||
|
## B. 寫入端 + 取代(核心)
|
||||||
|
|
||||||
|
- [x] **3.3a** `KbdbClient.updateRecord(id, values)` → base `PATCH /records/:id`(2026-06-26,mock 同步)
|
||||||
|
- [x] **3.3b** `src/actions/triplet-ingest.ts`:Zod strict 驗證 → idempotency(uri+hash)→ **先 append 後 deprecate**。88 行純函式(2026-06-26)
|
||||||
|
- [x] **3.3c** `POST /triplets/ingest` route(驗證失敗 → 422 hook,只驗證+呼叫 action)(2026-06-26)
|
||||||
|
- [x] **3.4** 測試 6 案全綠:正常 / 同 hash no-op / 新 hash deprecate / 污染(bridge_score+頂層 id) 422 / rollback(`vitest run` 16 passed)(2026-06-26)
|
||||||
|
- [x] **3.5** 查詢 active-only:`queryTriplets` 缺省 filter `status==='active'`(traverse/search/neighbors 皆經此;`includeDeprecated` opt-out 供 rollback/考古)(2026-06-26)
|
||||||
|
|
||||||
|
## C. MCP(⚠️ 跨 repo,需 arcrun 配合 → issue 標清)
|
||||||
|
|
||||||
|
- [x] **3.6** 圖查詢 + `refresh` **HTTP API/邏輯備好(graph 端)**:`GET /graph/source/:name`、`POST /graph/refresh`、既有 traverse/neighbors/path/relation。**MCP 註冊薄殼仍待 arcrun 配合**(不另起 graph MCP)(2026-06-26)
|
||||||
|
- [x] **3.6b** `refresh` 紅線:`graph-refresh.ts` 純被動代轉,只人發起調用觸發;無排程/webhook(2026-06-26)
|
||||||
|
- [x] **3.6d** 移除 graph **公開** keyword 端點(`POST /search` 收斂為 suggest-only;keywordSearch helper 留作 suggest 內部建構塊)(2026-06-26)
|
||||||
|
- [x] **3.7** `get_source`:`graph-source.ts` + `GET /graph/source/:name`(回 uri+anchor+block_id+content_hash,active-only,去重)。連帶加 `source_anchor` slot(2026-06-26)
|
||||||
|
- [x] **3.7b** ~~graph CLI~~ 延後不做(人少用、AI 用不到 → 不誤導)
|
||||||
|
|
||||||
|
> **跨 repo 待接(總管協調)**:圖工具(traverse/neighbors/source)+ refresh 的 **MCP 註冊薄殼**併入 arcrun `u6u-mcp-server`(KBDB MCP),待 arcrun #7 落地後兩邊接。graph 端 HTTP API 已就緒。
|
||||||
|
> **refresh 待部署**:`KBDB_INGEST_URL` 未設時 `refresh` 誠實回 `forwarded:false`(ingest repo T4 未就緒)。端到端待 ingest 部署驗。
|
||||||
|
|
||||||
|
## 完成準則
|
||||||
|
|
||||||
|
- 全程 zero SQL / zero migration / 無 D1·Vectorize·AI 綁定(`wrangler deploy --dry-run` bundle 乾淨)
|
||||||
|
- 所有 action ≤100 行;`vitest run` 全綠(mock client)
|
||||||
|
- 端到端 ingest→graph 走通需 base 上線 + ingest repo(T4)就緒 → 標「待部署驗」
|
||||||
|
- issue #1 留 open,待實證綠燈才結案
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// refresh(T3.6 / T3.6b)— 代轉 ingest 重抓+萃某來源。
|
||||||
|
//
|
||||||
|
// 🚫 紅線:只能【人發起的 MCP 調用】觸發。禁掛排程/webhook 自動 refresh
|
||||||
|
// (否則變回 fan-out,踩 GitHub flag 紅線)。本端點純被動:收到一次調用 → 代轉一次。
|
||||||
|
// graph 自己不抓檔、不萃取(那是 ingest 純餵食器的職責);graph 只把 refresh 意圖
|
||||||
|
// 轉給 ingest 的端點,ingest 抓+萃完後再走 POST /triplets/ingest 回灌。
|
||||||
|
|
||||||
|
export type RefreshRequest = { uri: string; owner_id?: string };
|
||||||
|
export type RefreshResult = { forwarded: boolean; ingest_url?: string; note?: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代轉 refresh 給 ingest 服務。ingestUrl 由 env 注入(KBDB_INGEST_URL)。
|
||||||
|
* 未設 → 誠實回 {forwarded:false}(ingest repo T4 尚未就緒/未部署),不假裝成功。
|
||||||
|
*/
|
||||||
|
export async function refreshSource(
|
||||||
|
req: RefreshRequest,
|
||||||
|
ingestUrl: string | undefined,
|
||||||
|
): Promise<RefreshResult> {
|
||||||
|
if (!ingestUrl) {
|
||||||
|
return {
|
||||||
|
forwarded: false,
|
||||||
|
note: 'KBDB_INGEST_URL 未設:ingest 服務尚未就緒(T4 待部署),refresh 無對象可轉。',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const res = await fetch(ingestUrl.replace(/\/$/, '') + '/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ uri: req.uri, owner_id: req.owner_id }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`[ingest] refresh ${req.uri}: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return { forwarded: true, ingest_url: ingestUrl };
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// get_source(T3.7)— 指回原文:給一個節點名,回它所有 triplet 的來源指標。
|
||||||
|
// 鐵律:走 base API、零 SQL。圖在插件層組裝。
|
||||||
|
// 用途:圖遍歷找到一筆知識後,回跳產生它的 canonical MD(source.uri + anchor)。
|
||||||
|
|
||||||
|
import type { KbdbClient } from '../lib/kbdb-client';
|
||||||
|
import { getNodeEdges } from './graph-nodes';
|
||||||
|
|
||||||
|
export type SourceRef = {
|
||||||
|
uri: string | null; // 來源穩定識別(github:owner/repo@path)
|
||||||
|
anchor: string | null; // 檔內定位(heading slug / block id)
|
||||||
|
block_id: string | null; // 向後相容:Logseq block id
|
||||||
|
content_hash: string | null; // 該批快照 hash
|
||||||
|
edge: { subject: string; predicate: string; object: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 給節點名,回觸及它的(active)triplet 的來源指標清單,去重同 uri+anchor。 */
|
||||||
|
export async function getSource(client: KbdbClient, node: string): Promise<SourceRef[]> {
|
||||||
|
const edges = await getNodeEdges(client, node); // 已 active-only(經 queryTriplets)
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const refs: SourceRef[] = [];
|
||||||
|
|
||||||
|
for (const t of edges) {
|
||||||
|
const key = `${t.source_uri ?? ''}#${t.source_anchor ?? ''}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
refs.push({
|
||||||
|
uri: t.source_uri,
|
||||||
|
anchor: t.source_anchor,
|
||||||
|
block_id: t.source_block_id,
|
||||||
|
content_hash: t.content_hash,
|
||||||
|
edge: { subject: t.subject, predicate: t.predicate, object: t.object },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ export type CreateTripletData = {
|
|||||||
bridge_score?: number;
|
bridge_score?: number;
|
||||||
subject_entity_type?: string;
|
subject_entity_type?: string;
|
||||||
object_entity_type?: string;
|
object_entity_type?: string;
|
||||||
|
source_uri?: string;
|
||||||
|
content_hash?: string;
|
||||||
|
source_anchor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 建立三元組 → POST /records(template=triplet)。 */
|
/** 建立三元組 → POST /records(template=triplet)。 */
|
||||||
@@ -37,10 +40,14 @@ export async function createTriplet(
|
|||||||
confidence: String(data.confidence ?? 1.0),
|
confidence: String(data.confidence ?? 1.0),
|
||||||
clusters_json: JSON.stringify(clusters),
|
clusters_json: JSON.stringify(clusters),
|
||||||
bridge_score: String(bridgeScore),
|
bridge_score: String(bridgeScore),
|
||||||
|
status: 'active',
|
||||||
};
|
};
|
||||||
if (data.source_block_id) values.source_block_id = data.source_block_id;
|
if (data.source_block_id) values.source_block_id = data.source_block_id;
|
||||||
if (data.subject_entity_type) values.subject_entity_type = data.subject_entity_type;
|
if (data.subject_entity_type) values.subject_entity_type = data.subject_entity_type;
|
||||||
if (data.object_entity_type) values.object_entity_type = data.object_entity_type;
|
if (data.object_entity_type) values.object_entity_type = data.object_entity_type;
|
||||||
|
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;
|
||||||
|
|
||||||
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 };
|
||||||
@@ -54,6 +61,7 @@ export type TripletFilters = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
owner_id?: string;
|
owner_id?: string;
|
||||||
entity_type?: string;
|
entity_type?: string;
|
||||||
|
includeDeprecated?: boolean; // 預設只回 active;rollback/考古才開(T3.5)
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 查三元組 → 取 template 全部 record,插件層 filter(base 無複合 slot 查詢)。 */
|
/** 查三元組 → 取 template 全部 record,插件層 filter(base 無複合 slot 查詢)。 */
|
||||||
@@ -64,6 +72,9 @@ export async function queryTriplets(
|
|||||||
const records = await client.listRecordsByTemplate(TPL_TRIPLET, filters.owner_id);
|
const records = await client.listRecordsByTemplate(TPL_TRIPLET, filters.owner_id);
|
||||||
let triplets = records.map(recordToTriplet);
|
let triplets = records.map(recordToTriplet);
|
||||||
|
|
||||||
|
// active-only:deprecated 不進圖遍歷/查詢(缺省 status 視為 active,相容舊資料)。
|
||||||
|
if (!filters.includeDeprecated) triplets = triplets.filter((t) => t.status === 'active');
|
||||||
|
|
||||||
if (filters.subject) triplets = triplets.filter((t) => t.subject === filters.subject);
|
if (filters.subject) triplets = triplets.filter((t) => t.subject === filters.subject);
|
||||||
if (filters.predicate) triplets = triplets.filter((t) => t.predicate === filters.predicate);
|
if (filters.predicate) triplets = triplets.filter((t) => t.predicate === filters.predicate);
|
||||||
if (filters.object) triplets = triplets.filter((t) => t.object === filters.object);
|
if (filters.object) triplets = triplets.filter((t) => t.object === filters.object);
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// ingest 寫入端 — 收 ingest-candidate envelope,做 idempotency + deprecate-then-append。
|
||||||
|
// 契約:contracts/ingest-candidate.json。鐵律:走 base API、零 SQL。
|
||||||
|
// 取代策略:先 append 新批 active,後翻舊批 status=deprecated(中途失敗不留「全無 active」空窗)。
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Zod 鏡射契約:strict() = additionalProperties:false → 禁送欄位 422(route 把 ZodError 轉 422)。
|
||||||
|
const NodeSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
gloss: z.string().optional(),
|
||||||
|
entity_type: z.enum(['person', 'event', 'product', 'market', 'org']).optional(),
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
const EdgeSchema = z.object({
|
||||||
|
subject: z.string().min(1),
|
||||||
|
predicate: z.string().min(1),
|
||||||
|
object: z.string().min(1),
|
||||||
|
confidence: z.number().min(0).max(1).optional(),
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
export const IngestEnvelopeSchema = z.object({
|
||||||
|
source: z.object({
|
||||||
|
uri: z.string().min(1),
|
||||||
|
content_hash: z.string().min(1),
|
||||||
|
anchor: z.string().optional(),
|
||||||
|
commit: z.string().optional(),
|
||||||
|
block_id: z.string().optional(),
|
||||||
|
}).strict(),
|
||||||
|
extractor: z.object({
|
||||||
|
model: z.string().min(1),
|
||||||
|
tier: z.enum(['shallow', 'deep']),
|
||||||
|
extracted_at: z.number().int().optional(),
|
||||||
|
}).strict(),
|
||||||
|
nodes: z.array(NodeSchema).optional(),
|
||||||
|
triplets: z.array(EdgeSchema).min(1),
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
export type IngestEnvelope = z.infer<typeof IngestEnvelopeSchema>;
|
||||||
|
|
||||||
|
export type IngestResult = { skipped: boolean; ingested: number; deprecated: number };
|
||||||
|
|
||||||
|
/** 收 envelope → idempotency → 先 append 後 deprecate。回 {skipped,ingested,deprecated}。 */
|
||||||
|
export async function ingestEnvelope(
|
||||||
|
client: KbdbClient,
|
||||||
|
env: IngestEnvelope,
|
||||||
|
owner_id?: string,
|
||||||
|
): Promise<IngestResult> {
|
||||||
|
await ensurePluginTemplates(client);
|
||||||
|
|
||||||
|
// 同 source_uri 的現存 active triplet(idempotency 分組 + 待 deprecate 對象)。
|
||||||
|
const all = (await client.listRecordsByTemplate(TPL_TRIPLET, owner_id)).map(recordToTriplet);
|
||||||
|
const priorActive = all.filter((t) => t.source_uri === env.source.uri && t.status === 'active');
|
||||||
|
|
||||||
|
// 同 hash → no-op(envelope 已落地過)。
|
||||||
|
if (priorActive.some((t) => t.content_hash === env.source.content_hash)) {
|
||||||
|
return { skipped: true, ingested: 0, deprecated: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 先 append 新批 active。
|
||||||
|
for (const e of env.triplets) {
|
||||||
|
await createTriplet(client, {
|
||||||
|
subject: e.subject,
|
||||||
|
predicate: e.predicate,
|
||||||
|
object: e.object,
|
||||||
|
confidence: e.confidence,
|
||||||
|
source_block_id: env.source.block_id,
|
||||||
|
source_uri: env.source.uri,
|
||||||
|
content_hash: env.source.content_hash,
|
||||||
|
source_anchor: env.source.anchor,
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { skipped: false, ingested: env.triplets.length, deprecated: priorActive.length };
|
||||||
|
}
|
||||||
+21
-3
@@ -113,12 +113,25 @@ export class KbdbClient {
|
|||||||
// --- templates(= 替代建表;插件要新類型只能建 template) ---
|
// --- templates(= 替代建表;插件要新類型只能建 template) ---
|
||||||
|
|
||||||
async ensureTemplate(name: string, slots: string[], description?: string): Promise<void> {
|
async ensureTemplate(name: string, slots: string[], description?: string): Promise<void> {
|
||||||
const existing = await this.req<{ id?: string } | { error: string }>(
|
const existing = await this.req<{ id?: string; slots?: string[] } | { error: string }>(
|
||||||
'GET',
|
'GET',
|
||||||
`/templates/${encodeURIComponent(name)}`,
|
`/templates/${encodeURIComponent(name)}`,
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
if (existing && (existing as any).id) return;
|
|
||||||
await this.req('POST', '/templates', { name, slots, description, created_by: 'kbdb-graph' });
|
// 全新 template → 建。
|
||||||
|
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;既有環境免另跑遷移腳本即收斂。
|
||||||
|
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((existing as any).id)}`, {
|
||||||
|
slots: [...have, ...missing],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- records(= template 實例,填 slot) ---
|
// --- records(= template 實例,填 slot) ---
|
||||||
@@ -141,6 +154,11 @@ export class KbdbClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 翻 record 的 slot 值(base PATCH /records/:id)。deprecate(翻 status)與 rollback 都靠它。 */
|
||||||
|
async updateRecord(recordId: string, values: Record<string, string>): Promise<void> {
|
||||||
|
await this.req('PATCH', `/records/${encodeURIComponent(recordId)}`, { values });
|
||||||
|
}
|
||||||
|
|
||||||
async listRecordsByTemplate(template: string, owner_id?: string): Promise<BaseRecord[]> {
|
async listRecordsByTemplate(template: string, owner_id?: string): Promise<BaseRecord[]> {
|
||||||
const { records } = await this.req<{ records: BaseRecord[] }>(
|
const { records } = await this.req<{ records: BaseRecord[] }>(
|
||||||
'GET',
|
'GET',
|
||||||
|
|||||||
+12
-1
@@ -14,8 +14,13 @@ export const TRIPLET_SLOTS = [
|
|||||||
'subject', 'predicate', 'object', 'source_block_id',
|
'subject', 'predicate', 'object', 'source_block_id',
|
||||||
'confidence', 'clusters_json', 'bridge_score',
|
'confidence', 'clusters_json', 'bridge_score',
|
||||||
'subject_entity_type', 'object_entity_type',
|
'subject_entity_type', 'object_entity_type',
|
||||||
|
// 取代/快照(T3.2):status=active|deprecated;superseded_by=取代它的新 record id;
|
||||||
|
// source_uri+content_hash 承載 ingest idempotency(按 source_uri 分組 deprecate)。
|
||||||
|
// source_anchor 供 get_source 精準回跳原文(T3.7)。
|
||||||
|
'status', 'superseded_by', 'source_uri', 'content_hash', 'source_anchor',
|
||||||
];
|
];
|
||||||
export const ENTITY_SLOTS = ['canonical', 'aliases_json', 'entity_type', 'owner'];
|
// gloss(T3.2b):一句話描述,供「詞+gloss」語義 normalize 的 embedding 對象。
|
||||||
|
export const ENTITY_SLOTS = ['canonical', 'aliases_json', 'entity_type', 'owner', 'gloss'];
|
||||||
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',
|
||||||
];
|
];
|
||||||
@@ -41,6 +46,12 @@ export function recordToTriplet(rec: BaseRecord): Triplet {
|
|||||||
bridge_score: parseInt(v.bridge_score ?? '0', 10),
|
bridge_score: parseInt(v.bridge_score ?? '0', 10),
|
||||||
subject_entity_type: (v.subject_entity_type as Triplet['subject_entity_type']) || null,
|
subject_entity_type: (v.subject_entity_type as Triplet['subject_entity_type']) || null,
|
||||||
object_entity_type: (v.object_entity_type as Triplet['object_entity_type']) || null,
|
object_entity_type: (v.object_entity_type as Triplet['object_entity_type']) || null,
|
||||||
|
// 缺省視為 active(相容尚無 status slot 的舊資料)。
|
||||||
|
status: v.status === 'deprecated' ? 'deprecated' : 'active',
|
||||||
|
superseded_by: v.superseded_by || null,
|
||||||
|
source_uri: v.source_uri || null,
|
||||||
|
content_hash: v.content_hash || null,
|
||||||
|
source_anchor: v.source_anchor || null,
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
updated_at: 0,
|
updated_at: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import type { Bindings } from '../types';
|
|||||||
import { traverseGraph, queryRelation } from '../actions/graph-traverse';
|
import { traverseGraph, queryRelation } from '../actions/graph-traverse';
|
||||||
import { getNodeEdges, getNeighbors } from '../actions/graph-nodes';
|
import { getNodeEdges, getNeighbors } from '../actions/graph-nodes';
|
||||||
import { findShortestPath } from '../actions/graph-path';
|
import { findShortestPath } from '../actions/graph-path';
|
||||||
|
import { getSource } from '../actions/graph-source';
|
||||||
|
import { refreshSource } from '../actions/graph-refresh';
|
||||||
import { makeKbdbClient } from '../lib/kbdb-client';
|
import { makeKbdbClient } from '../lib/kbdb-client';
|
||||||
|
|
||||||
const graphRoutes = new Hono<{ Bindings: Bindings }>();
|
const graphRoutes = new Hono<{ Bindings: Bindings }>();
|
||||||
@@ -46,4 +48,21 @@ graphRoutes.get('/path', async (c) => {
|
|||||||
return c.json({ from, to, ...result });
|
return c.json({ from, to, ...result });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /graph/source/:name — get_source(T3.7):回節點的原文來源指標(uri+anchor)
|
||||||
|
graphRoutes.get('/source/:name', async (c) => {
|
||||||
|
const name = decodeURIComponent(c.req.param('name'));
|
||||||
|
const sources = await getSource(makeKbdbClient(c.env), name);
|
||||||
|
return c.json({ node: name, sources, count: sources.length });
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /graph/refresh — 代轉 ingest 重抓+萃(T3.6/3.6b)
|
||||||
|
// 🚫 只人發起 MCP 調用觸發,禁排程/webhook 自動(fan-out 紅線)。
|
||||||
|
graphRoutes.post('/refresh', async (c) => {
|
||||||
|
const body = await c.req.json().catch(() => ({}));
|
||||||
|
const uri = body?.uri;
|
||||||
|
if (!uri) return c.json({ error: 'uri required' }, 400);
|
||||||
|
const result = await refreshSource({ uri, owner_id: body?.owner_id }, c.env.KBDB_INGEST_URL);
|
||||||
|
return c.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
export { graphRoutes };
|
export { graphRoutes };
|
||||||
|
|||||||
+9
-16
@@ -1,43 +1,36 @@
|
|||||||
// 搜尋路由入口 — 僅驗證參數,呼叫 actions
|
// 搜尋路由入口 — 僅驗證參數,呼叫 actions
|
||||||
//
|
//
|
||||||
// 插件層只做基本盤 keyword 搜尋(D1 LIKE,走 GET /entries/search)。
|
// T3.6d:移除 graph 的 keyword 搜尋【公開端點】——純關鍵字搜尋重複於 KBDB MCP 的 kbdb_search,
|
||||||
|
// 歸 KBDB MCP,不由 graph 代理。graph 只保留圖專屬的 `suggest`(跨知識建議)。
|
||||||
|
// 注意:keywordSearch helper 仍保留,作為 suggest 的內部建構塊(非對外端點)。
|
||||||
// 語意搜尋 / embedding 屬基本盤 optional embed 模組,不在插件 → 已移除 /search/embed。
|
// 語意搜尋 / embedding 屬基本盤 optional embed 模組,不在插件 → 已移除 /search/embed。
|
||||||
|
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { Bindings, Variables } from '../types';
|
import type { Bindings, Variables } from '../types';
|
||||||
import { keywordSearch } from '../actions/search-query';
|
|
||||||
import { suggestKnowledge } from '../actions/search-suggest';
|
import { suggestKnowledge } from '../actions/search-suggest';
|
||||||
import { makeKbdbClient } from '../lib/kbdb-client';
|
import { makeKbdbClient } from '../lib/kbdb-client';
|
||||||
|
|
||||||
const searchRoutes = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
const searchRoutes = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
||||||
|
|
||||||
const UnifiedSearchSchema = z.object({
|
const SuggestSchema = z.object({
|
||||||
query: z.string().min(1),
|
query: z.string().min(1),
|
||||||
type: z.enum(['keyword', 'suggest']).optional().default('keyword'),
|
|
||||||
topK: z.number().min(1).max(20).optional(),
|
topK: z.number().min(1).max(20).optional(),
|
||||||
owner_id: z.string().optional(),
|
owner_id: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 統一搜尋入口:POST /search
|
// 統一搜尋入口:POST /search — 僅 suggest(keyword 歸 KBDB MCP,T3.6d)
|
||||||
searchRoutes.post('/', async (c) => {
|
searchRoutes.post('/', async (c) => {
|
||||||
const parsed = UnifiedSearchSchema.safeParse(await c.req.json());
|
const parsed = SuggestSchema.safeParse(await c.req.json());
|
||||||
if (!parsed.success) return c.json({ error: parsed.error.flatten() }, 400);
|
if (!parsed.success) return c.json({ error: parsed.error.flatten() }, 400);
|
||||||
const { query, type, topK, owner_id } = parsed.data;
|
const { query, topK, owner_id } = parsed.data;
|
||||||
|
|
||||||
// Namespace 讀取過濾:partner token 只能搜到自己 namespace 的資料
|
// Namespace 讀取過濾:partner token 只能搜到自己 namespace 的資料
|
||||||
const namespace = c.get('namespace');
|
const namespace = c.get('namespace');
|
||||||
const effectiveOwner = namespace ?? owner_id;
|
const effectiveOwner = namespace ?? owner_id;
|
||||||
|
|
||||||
const client = makeKbdbClient(c.env);
|
const result = await suggestKnowledge(makeKbdbClient(c.env), query, topK, effectiveOwner);
|
||||||
|
return c.json(result);
|
||||||
if (type === 'suggest') {
|
|
||||||
const result = await suggestKnowledge(client, query, topK, effectiveOwner);
|
|
||||||
return c.json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = await keywordSearch(client, query, { limit: topK, owner_id: effectiveOwner });
|
|
||||||
return c.json({ matches, count: matches.length, mode: 'keyword' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export { searchRoutes };
|
export { searchRoutes };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
|
|||||||
import type { Bindings, Variables } from '../types';
|
import type { Bindings, Variables } from '../types';
|
||||||
import { createTriplet, queryTriplets } from '../actions/triplet-crud';
|
import { createTriplet, queryTriplets } from '../actions/triplet-crud';
|
||||||
import { getTripletStats } from '../actions/triplet-stats';
|
import { getTripletStats } from '../actions/triplet-stats';
|
||||||
|
import { ingestEnvelope, IngestEnvelopeSchema } from '../actions/triplet-ingest';
|
||||||
import { makeKbdbClient } from '../lib/kbdb-client';
|
import { makeKbdbClient } from '../lib/kbdb-client';
|
||||||
|
|
||||||
const tripletRoutes = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>();
|
const tripletRoutes = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>();
|
||||||
@@ -97,4 +98,31 @@ tripletRoutes.openapi(createRouteDefinition, async (c) => {
|
|||||||
return c.json({ ok: true }, 201);
|
return c.json({ ok: true }, 201);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /triplets/ingest — 收 ingest-candidate envelope(idempotency + deprecate-then-append)
|
||||||
|
const ingestRoute = createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: '/ingest',
|
||||||
|
request: {
|
||||||
|
body: { content: { 'application/json': { schema: IngestEnvelopeSchema } } },
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: { description: 'Envelope ingested (or skipped if same content_hash)' },
|
||||||
|
422: { description: 'Invalid envelope (forbidden field or shape mismatch)' },
|
||||||
|
},
|
||||||
|
tags: ['Triplets'],
|
||||||
|
});
|
||||||
|
|
||||||
|
tripletRoutes.openapi(
|
||||||
|
ingestRoute,
|
||||||
|
async (c) => {
|
||||||
|
const env = c.req.valid('json');
|
||||||
|
const result = await ingestEnvelope(makeKbdbClient(c.env), env);
|
||||||
|
return c.json(result, 200);
|
||||||
|
},
|
||||||
|
// strict() 驗證失敗(如送禁止欄位 bridge_score)→ 422,不是預設 400。
|
||||||
|
(zres, c) => {
|
||||||
|
if (!zres.success) return c.json({ error: 'invalid envelope', issues: zres.error.issues }, 422);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export { tripletRoutes };
|
export { tripletRoutes };
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type Bindings = {
|
|||||||
// 插件不碰 DB/Vectorize/AI — 全走基本盤 API(API-as-Wall)。
|
// 插件不碰 DB/Vectorize/AI — 全走基本盤 API(API-as-Wall)。
|
||||||
// 語意搜尋/embedding 屬基本盤 optional embed 模組,不在插件。
|
// 語意搜尋/embedding 屬基本盤 optional embed 模組,不在插件。
|
||||||
KBDB_BASE_URL?: string; // 基本盤 arcrun/kbdb API 網址(leo: 可設定,先留空)
|
KBDB_BASE_URL?: string; // 基本盤 arcrun/kbdb API 網址(leo: 可設定,先留空)
|
||||||
|
KBDB_INGEST_URL?: string; // ingest 服務網址(refresh 代轉對象;T4 就緒前留空)
|
||||||
KBDB_INTERNAL_TOKEN?: string;
|
KBDB_INTERNAL_TOKEN?: string;
|
||||||
ENVIRONMENT: string;
|
ENVIRONMENT: string;
|
||||||
API_KEY?: string;
|
API_KEY?: string;
|
||||||
@@ -17,6 +18,8 @@ export type Variables = {
|
|||||||
|
|
||||||
export type EntityType = 'person' | 'event' | 'product' | 'market' | 'org';
|
export type EntityType = 'person' | 'event' | 'product' | 'market' | 'org';
|
||||||
|
|
||||||
|
export type TripletStatus = 'active' | 'deprecated';
|
||||||
|
|
||||||
export type Triplet = {
|
export type Triplet = {
|
||||||
id: string;
|
id: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -28,6 +31,11 @@ export type Triplet = {
|
|||||||
bridge_score: number; // 跨越的 cluster 數量,Scout 發現指標
|
bridge_score: number; // 跨越的 cluster 數量,Scout 發現指標
|
||||||
subject_entity_type: EntityType | null; // 主體 entity 類型(人格疊加局勢分析用)
|
subject_entity_type: EntityType | null; // 主體 entity 類型(人格疊加局勢分析用)
|
||||||
object_entity_type: EntityType | null; // 客體 entity 類型
|
object_entity_type: EntityType | null; // 客體 entity 類型
|
||||||
|
status: TripletStatus; // active(進圖遍歷)| deprecated(被取代,可查/可 rollback)
|
||||||
|
superseded_by: string | null; // 取代它的新 record id(active 時為 null)
|
||||||
|
source_uri: string | null; // ingest 來源穩定識別(idempotency 分組鍵 + get_source 指標)
|
||||||
|
content_hash: string | null; // 來源快照 hash(idempotency 比對鍵)
|
||||||
|
source_anchor: string | null; // 檔內定位(heading slug / block id),get_source 精準回跳
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1.10.0
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# [主題] — Architecture Decision Record
|
||||||
|
|
||||||
|
> 日期:[YYYY-MM-DD]
|
||||||
|
> 狀態:[提議中 / 已採納 / 已廢棄]
|
||||||
|
> 影響範圍:[哪些子系統 / 模組]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
[遇到了什麼問題,需要做這個決定?]
|
||||||
|
|
||||||
|
## 決定
|
||||||
|
|
||||||
|
**[結論,一句話。]**
|
||||||
|
|
||||||
|
## 原因
|
||||||
|
|
||||||
|
[詳細說明為什麼這樣決定。]
|
||||||
|
|
||||||
|
## 放棄的選項
|
||||||
|
|
||||||
|
| 選項 | 放棄原因 |
|
||||||
|
|------|---------|
|
||||||
|
| [選項 A] | [原因] |
|
||||||
|
| [選項 B] | [原因] |
|
||||||
|
|
||||||
|
## 影響與後續
|
||||||
|
|
||||||
|
[這個決定影響哪些地方?有什麼技術債或需要注意的事?]
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# [子系統名稱] — Design
|
||||||
|
|
||||||
|
> 狀態:[草稿 / 審核中 / 已採納 / 已廢棄]
|
||||||
|
> 建立:[YYYY-MM-DD] | 最後更新:[YYYY-MM-DD]
|
||||||
|
> 負責人:[名稱]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一句話說明
|
||||||
|
|
||||||
|
[這個子系統做什麼,一句話。]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景與問題
|
||||||
|
|
||||||
|
[為什麼需要這個子系統?解決了什麼問題?]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 範圍
|
||||||
|
|
||||||
|
### 包含(In Scope)
|
||||||
|
- [這個 SDD 涵蓋的功能]
|
||||||
|
|
||||||
|
### 不包含(Out of Scope)
|
||||||
|
- [明確排除的功能,避免 CC 自行延伸]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 設計
|
||||||
|
|
||||||
|
### 架構概覽
|
||||||
|
|
||||||
|
[用文字或 ASCII 描述系統結構]
|
||||||
|
|
||||||
|
```
|
||||||
|
[元件 A] → [元件 B] → [元件 C]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 關鍵決策
|
||||||
|
|
||||||
|
| 決策 | 選擇 | 原因 | 放棄的選項 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| [問題] | [選擇] | [原因] | [其他選項] |
|
||||||
|
|
||||||
|
### API / 介面定義
|
||||||
|
|
||||||
|
[端點、資料格式、輸入輸出規格]
|
||||||
|
|
||||||
|
### 資料模型
|
||||||
|
|
||||||
|
[資料結構、欄位說明]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技術限制
|
||||||
|
|
||||||
|
- [不能用什麼]
|
||||||
|
- [必須相容什麼]
|
||||||
|
- [效能要求]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 驗收標準
|
||||||
|
|
||||||
|
完成的定義(CC 完成任何 task 前必須確認):
|
||||||
|
- [ ] [可客觀驗證的條件,例如:POST /api/xxx 回傳 200]
|
||||||
|
- [ ] [...]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相關文件
|
||||||
|
|
||||||
|
- [連結到相關 ADR]
|
||||||
|
- [連結到相關 SDD]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# [子系統名稱] — Tasks
|
||||||
|
|
||||||
|
> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。
|
||||||
|
> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1:[Phase 名稱]
|
||||||
|
|
||||||
|
### 前置條件
|
||||||
|
- [ ] [這個 Phase 開始前必須完成的事]
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [ ] 1.1 [task 描述]
|
||||||
|
- 驗收:[客觀可驗證的完成標準]
|
||||||
|
- 注意:[CC 容易犯的錯,可選]
|
||||||
|
|
||||||
|
- [ ] 1.2 [task 描述]
|
||||||
|
- 驗收:[...]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2:[Phase 名稱]
|
||||||
|
|
||||||
|
> 前置條件:Phase 1 全部完成
|
||||||
|
|
||||||
|
- [ ] 2.1 [task 描述]
|
||||||
|
- 驗收:[...]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成定義
|
||||||
|
|
||||||
|
整個 SDD 完成 = 以下全部達成:
|
||||||
|
- [ ] 所有 tasks 標 [x]
|
||||||
|
- [ ] 驗收標準通過(有客觀證據)
|
||||||
|
- [ ] design.md 與實作一致(如有出入需更新)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 狀態說明
|
||||||
|
|
||||||
|
| 標記 | 意義 |
|
||||||
|
|------|------|
|
||||||
|
| `[ ]` | 未開始 |
|
||||||
|
| `[🔄]` | 進行中(當前 session)|
|
||||||
|
| `[x]` | 完成(有驗收證據)|
|
||||||
|
| `[~]` | 暫緩(說明原因)|
|
||||||
|
| `[!]` | 阻擋中(說明阻擋原因)|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 文件分類索引
|
||||||
|
|
||||||
|
> CC 整理文件時的分類依據。找不到分類就問,不要猜。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分類規則
|
||||||
|
|
||||||
|
| 目錄 | 放什麼 | 判斷標準 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| **1-vision/** | 為什麼做這個 | 產品願景、北極星、設計哲學 |
|
||||||
|
| **2-architecture/** | 系統怎麼設計的 | 架構圖、技術棧、元件關係 |
|
||||||
|
| **2-architecture/decisions/** | 為什麼這樣設計 | ADR,選A不選B的原因 |
|
||||||
|
| **3-specs/** | 要做什麼 | SDD,每個子系統一個目錄 |
|
||||||
|
| **4-guides/** | 怎麼做 | 部署、開發流程、CLI 用法 |
|
||||||
|
| **5-records/** | 發生過什麼 | 歷史記錄,不修改只增加 |
|
||||||
|
| **5-records/incidents/** | 生產問題復盤 | 故障原因、時間線、改進方案 |
|
||||||
|
| **5-records/test-reports/** | 測試結果 | 壓測報告、驗收記錄 |
|
||||||
|
| **6-user/** | 給使用者看的 | README、安裝教學、FAQ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CC 整理文件時的判斷流程
|
||||||
|
|
||||||
|
```
|
||||||
|
這個文件是...
|
||||||
|
├── 有明確子系統 + 設計內容? → docs/3-specs/[子系統]/
|
||||||
|
├── 解釋為什麼做某個決定? → docs/2-architecture/decisions/
|
||||||
|
├── 說明怎麼操作? → docs/4-guides/
|
||||||
|
├── 記錄發生過的事? → docs/5-records/
|
||||||
|
├── 給外部使用者看的? → docs/6-user/
|
||||||
|
└── 以上都不確定? → 列為「待確認」,問負責人
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SDD 結構(docs/3-specs/ 下每個子系統)
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/3-specs/[子系統名]/
|
||||||
|
├── design.md ← 設計文件(要做什麼、怎麼做、邊界在哪)
|
||||||
|
└── tasks.md ← 任務清單([ ] 未開始 [🔄] 進行中 [x] 完成)
|
||||||
|
```
|
||||||
|
|
||||||
|
CC 動手前必須有這兩個檔案。找不到就停手。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## .claude/wiki/ — CC 的記憶空間(CC 維護,人不手動編輯)
|
||||||
|
|
||||||
|
| 檔案 | 用途 | 更新時機 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `INDEX.md` | wiki 導引 | 新增 wiki 檔案時 |
|
||||||
|
| `mistakes.md` | CC 已知誤解 + 避坑 | 每次被糾正後 |
|
||||||
|
| `status.md` | 當前進度 + 下一步 | 每次 session 結束 |
|
||||||
|
| `decisions-summary.md` | 架構決策摘要 | 重大決策後 |
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
name: wiki-cowork-scan
|
||||||
|
description: "掃描本機 Documents 下所有裝了 system-dev-template 的資料夾,自動整理 LLM Wiki。支援一般專案、Logseq vault、Obsidian vault 三種結構,偵測方式與 install.sh 一致。觸發時機:使用者說「整理 wiki」「幫我掃 wiki」「更新我的 wiki」「wiki 掃描」,或 Cowork cron 定期觸發。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Wiki Cowork Scan
|
||||||
|
|
||||||
|
## 核心原則
|
||||||
|
|
||||||
|
這個 skill 與 Claude Code 的 `/wiki-init` `/wiki-capture` 共用同一套規則:
|
||||||
|
|
||||||
|
| 層 | 規則 |
|
||||||
|
|------------|-------------------------------------------|
|
||||||
|
| raw source | 只讀,不動 |
|
||||||
|
| `system-dev/wiki/` | 唯一輸出地點,只增不覆 |
|
||||||
|
| `CLAUDE.md` | 不動 |
|
||||||
|
| `logseq/`、`.obsidian/`、`assets/` | 絕對不動 |
|
||||||
|
|
||||||
|
**CC 和 Cowork 輸出格式相同,任何一方整理過的內容,另一方看到就跳過或補充,不覆蓋。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一步:發現所有目標資料夾
|
||||||
|
|
||||||
|
掃描 `~/Documents`(遞迴深度 3 層),找出所有含 `system-dev/wiki/` 的資料夾。
|
||||||
|
|
||||||
|
```
|
||||||
|
~/Documents/
|
||||||
|
project-a/system-dev/wiki/ ← ✅ 目標
|
||||||
|
Logseq/system-dev/wiki/ ← ✅ 目標
|
||||||
|
其他資料夾/ ← ❌ 跳過
|
||||||
|
```
|
||||||
|
|
||||||
|
找到後列出清單,告訴使用者:「找到 N 個 wiki 資料夾,開始整理。」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第二步:對每個資料夾偵測 vault 類型
|
||||||
|
|
||||||
|
進入每個目標資料夾的**根目錄**(`system-dev/wiki/` 的上兩層),依序判斷:
|
||||||
|
|
||||||
|
### 判斷順序(與 install.sh 一致)
|
||||||
|
|
||||||
|
```
|
||||||
|
if 根目錄有 logseq/ 資料夾
|
||||||
|
→ vault 類型:Logseq
|
||||||
|
→ raw source:pages/、journals/
|
||||||
|
→ 忽略:logseq/、assets/
|
||||||
|
|
||||||
|
else if 根目錄有 .obsidian/ 資料夾
|
||||||
|
→ vault 類型:Obsidian
|
||||||
|
→ raw source:根目錄下所有 .md(排除 .obsidian/ 內的檔案)
|
||||||
|
|
||||||
|
else
|
||||||
|
→ vault 類型:一般專案
|
||||||
|
→ raw source:docs/ 下所有 .md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第三步:讀取現有 wiki 狀態
|
||||||
|
|
||||||
|
進入 `system-dev/wiki/`,讀取:
|
||||||
|
|
||||||
|
- `INDEX.md`:目前已有哪些 wiki 頁面(多角度視圖入口)
|
||||||
|
- `status.md`:上次整理時間、進度
|
||||||
|
- `principles.md`(如果有):本專案跨全局的設計原則——整理時必須服從
|
||||||
|
|
||||||
|
目的:**知道哪些已整理過,只處理新增或有變動的 raw source**,不重複整理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第四步:整理規則
|
||||||
|
|
||||||
|
### 核心判準:push vs pull(wiki 是給 AI 看的)
|
||||||
|
|
||||||
|
整理任何內容前,先判斷它該進 **push 檔** 還 **cards(pull)**——判準是「**CC 做事時會不會被動看見**」:
|
||||||
|
|
||||||
|
- **push 檔**(`status.md` / `mistakes.md` / `principles.md`):CC session 開始就被 hook 注入。給「CC 不會主動查、但不看就出事」的東西。
|
||||||
|
- **pull**(`cards/`):CC 想到要查才看見。一切知識內容(原文摘要、AI 筆記、決策、概念…)都寫成 cards。
|
||||||
|
|
||||||
|
| 內容 | 去哪 | 理由 |
|
||||||
|
|------|------|------|
|
||||||
|
| 當前進度、下一步 | `status.md`(push 全文) | 時態狀態,不看會重做 |
|
||||||
|
| 跨全局設計原則(一行一條,≤15) | `principles.md`(push 全文) | 會被遺忘的盲區,CC 設計時必服從 |
|
||||||
|
| 踩坑、被糾正的誤解 | `mistakes.md`(push 摘要+按需展開) | 防 CC 不自覺的盲區 |
|
||||||
|
| 決策、原文摘要、概念知識、其餘一切 | `cards/<bucket>/`(pull) | 知識內容;CC 面對時自然會查 |
|
||||||
|
|
||||||
|
> `decisions-summary.md` 已**降級為 cards + INDEX 決策視圖**(決策=知識內容)。既有的保留為相容,不刪。
|
||||||
|
> CC 與 Cowork **共用此判準**,產出一致:任一方寫進 push 檔或 cards,另一方看到就跳過或補充,不覆蓋。
|
||||||
|
|
||||||
|
### 讀 raw source
|
||||||
|
|
||||||
|
逐一讀取 raw source 的 `.md` 檔。跳過:
|
||||||
|
- 檔名以 `.` 開頭的隱藏檔
|
||||||
|
- `.wikiignore` 裡列出的 glob pattern(如果存在)
|
||||||
|
- 含有 `<!-- wiki:ignore -->` 標記的區段
|
||||||
|
|
||||||
|
### 整理邏輯
|
||||||
|
|
||||||
|
每個 raw source 檔案,判斷:
|
||||||
|
|
||||||
|
1. **INDEX.md 裡已有對應條目,且 raw source 未修改** → 跳過
|
||||||
|
2. **INDEX.md 裡已有條目,但 raw source 有新內容** → 更新對應 wiki 頁面,補充新資訊,不刪舊內容
|
||||||
|
3. **INDEX.md 裡沒有對應條目** → 新建 wiki 頁面
|
||||||
|
|
||||||
|
### Wiki 卡片格式(概念原子卡,存到 `cards/<bucket>/`)
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
tags: [知識管理, AI協作, 方法論]
|
||||||
|
gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,選填、deep tier 才產)
|
||||||
|
---
|
||||||
|
# 概念全名
|
||||||
|
|
||||||
|
← [[<bucket>/00-INDEX]]
|
||||||
|
|
||||||
|
**來源**:`[raw source 相對路徑]`
|
||||||
|
**最後更新**:YYYY-MM-DD
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
[一句話核心]
|
||||||
|
|
||||||
|
## 重點
|
||||||
|
|
||||||
|
- [自包含改寫的要點,不寫「詳見原文」]
|
||||||
|
|
||||||
|
## 關聯
|
||||||
|
|
||||||
|
- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]]
|
||||||
|
- [[原子筆記]] >> 是其最小單元 >> [[卡片盒筆記法]]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 架構:三層 + 標籤橫切(183 卡實證)
|
||||||
|
|
||||||
|
```
|
||||||
|
INDEX.md ← 頂層:標籤視圖(非資料夾列表)
|
||||||
|
TAXONOMY.md ← 標籤字典(受控擴充:先查重再登記)
|
||||||
|
cards/<bucket>/
|
||||||
|
├── 00-INDEX.md ← 桶子索引(固定名,容器:只連不重寫)
|
||||||
|
└── <概念全名>.md ← 概念原子卡
|
||||||
|
```
|
||||||
|
|
||||||
|
- **資料夾只是儲存桶,分類由 frontmatter `tags:` 承載**——不繼承原稿目錄,由 AI 重新組織。
|
||||||
|
- **桶子索引固定名 `00-INDEX.md`**:`00-` 排序最前、一眼可辨,載入任何桶先讀它。
|
||||||
|
- **frontmatter `tags:` 而非行內 `#tag`**:內文常用 `#`(如 `#猜想`),行內標籤會讓 ingest 分不清「分類」與「內文範例」污染 graph;frontmatter 零歧義。標籤只能用 `TAXONOMY.md` 列出的;**禁止繞過字典在卡片直接冒新標籤**,但字典可受控擴充(遇新軸先查重、確認非同義詞,再登記進本 repo 的 TAXONOMY.md)。
|
||||||
|
- **麵包屑帶路徑**:H1 次行 `← [[<bucket>/00-INDEX]]`。指 `00-INDEX` 因固定名跨桶撞名,**一律帶路徑**;卡片間連結用裸 `[[卡名]]`。
|
||||||
|
|
||||||
|
### 使用 typed-edge 三元組(不只裸 `[[wikilink]]`)
|
||||||
|
|
||||||
|
整理時,發現內容與其他頁面有關聯,用**帶語義的三元組**寫進 `## 關聯`,而非只列裸 `[[頁面]]`。裸 `[[A]]` 只說「有關」、沒說關係,下游要建 knowledge graph 還得回讀兩張卡;三元組把關係也預編譯,ingest 直接 parse 出帶類型的有向邊。
|
||||||
|
|
||||||
|
格式 `A >> 謂詞 >> B`,規則:
|
||||||
|
1. **方向性**:必須讀成「A(謂詞)B」一句通順的話;A、B 順序=主→賓真實方向。
|
||||||
|
2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、是…的實作),天然帶方向。
|
||||||
|
3. **謂詞自由書寫**,不受控詞彙;下游對謂詞 embedding 時同義謂詞會自動聚類,但方向仍靠書寫順序保證。
|
||||||
|
4. **向後相容**:純 `[[A]]` 仍合法(無類型邊),盡量補謂詞。
|
||||||
|
|
||||||
|
`>>` 為分隔語法,全程一致即可。這是 Karpathy LLM Wiki「知識互連」的強化版——連結不只存在,還帶類型與方向。
|
||||||
|
|
||||||
|
### 萃 gloss(node 一句說明,供下游語義 normalize)
|
||||||
|
|
||||||
|
每張卡=一個 entity / graph node。deep tier 改寫時,frontmatter 補一句 `gloss:`——這個 node 是什麼的一句定義。下游 KBDB 對「entity 名 + gloss」一起做 embedding 求相似度,自動歸一同義詞(比只對名字準、比手維護 alias 表自動)。
|
||||||
|
|
||||||
|
- **在知識生產的當下、由整理者(CC / Cowork)建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔/跨庫視角,編不出貼合的 gloss。
|
||||||
|
- **選填、deep tier 才產**:淺萃不浪費。
|
||||||
|
- **gloss ≠ 摘要**:`gloss` 是 frontmatter 給機器 normalize 的定義句(「X 是…」);`## 摘要` 是給人讀的核心句。
|
||||||
|
- **對齊下游 envelope**:frontmatter `gloss:` 對應 ingest envelope 的 `nodes[].gloss`。
|
||||||
|
|
||||||
|
> **改寫時必守**:① 絕不寫入 raw source(只往 `cards/<bucket>/` 寫,事後驗 raw source 0 異動);② 檔名=卡片全名,冒號用全形「:」、斜線用全形「/」,全程一種字元避免斷鏈。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第五步:更新 INDEX.md 和 status.md
|
||||||
|
|
||||||
|
### INDEX.md 格式(頂層 = 標籤視圖)
|
||||||
|
|
||||||
|
頂層 INDEX 按 `TAXONOMY.md` 的軸聚類,指向各桶子索引(帶路徑),不是平鋪頁面列表:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Wiki Index
|
||||||
|
|
||||||
|
> 最後更新:YYYY-MM-DD HH:MM | 來源:cowork-scan | 總卡數:N
|
||||||
|
|
||||||
|
### 知識管理
|
||||||
|
- [[pkm/00-INDEX]] — PKM 知識管理(N 卡)
|
||||||
|
|
||||||
|
### AI 協作
|
||||||
|
- [[ai/00-INDEX]] — AI 協作(M 卡)
|
||||||
|
```
|
||||||
|
|
||||||
|
桶子索引 `cards/<bucket>/00-INDEX.md` 是容器(只連不重寫,H2/H3 分節列出該桶卡片)。
|
||||||
|
|
||||||
|
### status.md 更新
|
||||||
|
|
||||||
|
在現有內容**末尾追加**(不覆蓋):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## YYYY-MM-DD HH:MM|cowork-scan
|
||||||
|
|
||||||
|
- vault 類型:[Logseq / Obsidian / 一般專案]
|
||||||
|
- 掃描檔案:N 個
|
||||||
|
- 新增頁面:N 個
|
||||||
|
- 更新頁面:N 個
|
||||||
|
- 跳過:N 個(未變動)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第六步:回報結果
|
||||||
|
|
||||||
|
整理完所有資料夾後,輸出摘要:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Wiki 整理完成
|
||||||
|
|
||||||
|
資料夾 1:~/Documents/project-a
|
||||||
|
類型:一般專案
|
||||||
|
新增:3 頁,更新:1 頁,跳過:12 頁
|
||||||
|
|
||||||
|
資料夾 2:~/Documents/Logseq
|
||||||
|
類型:Logseq vault
|
||||||
|
新增:5 頁,更新:2 頁,跳過:47 頁
|
||||||
|
|
||||||
|
總計:8 頁新增,3 頁更新
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 絕對禁止
|
||||||
|
|
||||||
|
- ❌ 修改任何 raw source 檔案
|
||||||
|
- ❌ 修改 `CLAUDE.md`
|
||||||
|
- ❌ 動 `logseq/`、`.obsidian/`、`assets/` 資料夾
|
||||||
|
- ❌ 刪除 `system-dev/wiki/` 裡已有的頁面(只增補,不刪除)
|
||||||
|
- ❌ 把機敏資訊(密碼、金鑰、個資)寫進 wiki(遇到跳過並記錄)
|
||||||
|
- ❌ 整理沒有 `system-dev/wiki/` 的資料夾(那不是這個 skill 的目標)
|
||||||
Executable
+453
@@ -0,0 +1,453 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# system-dev-template installer
|
||||||
|
# 已有專案接入腳本——只建立缺少的東西,已有的一律不動。
|
||||||
|
#
|
||||||
|
# 模組化安裝:
|
||||||
|
# --wiki 只裝 LLM Wiki(記憶系統 + 機敏防護)
|
||||||
|
# --sdd 只裝 SDD 系統(動 code 前必須有 design.md)
|
||||||
|
# --all 兩個都裝(預設)
|
||||||
|
# 無參數 互動式詢問
|
||||||
|
#
|
||||||
|
# 為什麼留在同一個 repo 用參數選,而不是 fork:
|
||||||
|
# 使用者多半非專業,最怕「我要去哪個 repo」。一個入口 + 選單最友善。
|
||||||
|
# 等未來功能多到 3+ 個再演進成「模板組合器」。模組邊界先在這裡劃好。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── i18n:依 locale 選語言,預設英文 ──────────────────
|
||||||
|
# 為什麼預設英文:curl | bash 常是 LANG=C,外國人預設就該看得懂;
|
||||||
|
# 台灣使用者 locale 多為 zh_TW,會自動切回繁中。
|
||||||
|
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
|
||||||
|
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
|
||||||
|
*) IS_ZH="no" ;;
|
||||||
|
esac
|
||||||
|
# t "中文" "English" → 依語系印出對應字串
|
||||||
|
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
|
||||||
|
# tn = 不換行版(給 prompt 用)
|
||||||
|
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
||||||
|
|
||||||
|
REPO_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/template"
|
||||||
|
# install.sh / update.sh 住在 main/scripts/(不在 template/)。
|
||||||
|
SCRIPTS_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts"
|
||||||
|
CREATED=()
|
||||||
|
SKIPPED=()
|
||||||
|
|
||||||
|
# ── 解析模組參數 ──────────────────────────────────
|
||||||
|
MODULE=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--wiki|--wiki-only) MODULE="wiki" ;;
|
||||||
|
--sdd|--sdd-only) MODULE="sdd" ;;
|
||||||
|
--all) MODULE="all" ;;
|
||||||
|
-h|--help)
|
||||||
|
if [ "$IS_ZH" = "yes" ]; then
|
||||||
|
cat <<'HELP'
|
||||||
|
用法:install.sh [--wiki | --sdd | --all]
|
||||||
|
--wiki 只裝 LLM Wiki(CC 記憶系統 + 機敏防護)
|
||||||
|
--sdd 只裝 SDD 系統(動 code 前強制要有設計文件)
|
||||||
|
--all 兩個都裝(預設)
|
||||||
|
無參數 互動式詢問要裝哪個
|
||||||
|
HELP
|
||||||
|
else
|
||||||
|
cat <<'HELP'
|
||||||
|
Usage: install.sh [--wiki | --sdd | --all]
|
||||||
|
--wiki Install LLM Wiki only (CC memory system + secret protection)
|
||||||
|
--sdd Install SDD system only (require a design doc before touching code)
|
||||||
|
--all Install both (default)
|
||||||
|
no flag Interactively ask which to install
|
||||||
|
HELP
|
||||||
|
fi
|
||||||
|
exit 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔧 system-dev-template installer"
|
||||||
|
echo "================================="
|
||||||
|
t "只建立缺少的目錄和檔案,已有的不動。" \
|
||||||
|
"Only creates missing dirs and files; never touches what already exists."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── 無參數 → 互動式詢問(給非專業使用者)──────────
|
||||||
|
if [ -z "$MODULE" ]; then
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
t "要安裝哪一塊?" "Which part do you want to install?"
|
||||||
|
t " 1) LLM Wiki —— 讓 CC 記住決策、不重複犯錯(含機敏防護)" \
|
||||||
|
" 1) LLM Wiki — let CC remember decisions and avoid repeating mistakes (with secret protection)"
|
||||||
|
t " 2) SDD —— 動 code 前強制先有設計文件" \
|
||||||
|
" 2) SDD — require a design doc before touching code"
|
||||||
|
t " 3) 兩個都裝(推薦)" " 3) Install both (recommended)"
|
||||||
|
echo ""
|
||||||
|
tn "請輸入 1 / 2 / 3 [預設 3]:" "Enter 1 / 2 / 3 [default 3]: "
|
||||||
|
read -r choice || choice=3
|
||||||
|
case "$choice" in
|
||||||
|
1) MODULE="wiki" ;;
|
||||||
|
2) MODULE="sdd" ;;
|
||||||
|
*) MODULE="all" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
# 非互動環境(如 curl | bash 無 tty)→ 預設全裝
|
||||||
|
MODULE="all"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
WANT_WIKI=false
|
||||||
|
WANT_SDD=false
|
||||||
|
case "$MODULE" in
|
||||||
|
wiki) WANT_WIKI=true ;;
|
||||||
|
sdd) WANT_SDD=true ;;
|
||||||
|
all) WANT_WIKI=true; WANT_SDD=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── 偵測 vault 類型 → 決定 raw source(原始文件)路徑 ──────────
|
||||||
|
# 為什麼:這個模板原本假設「原始文件在 docs/」,但 Logseq / Obsidian
|
||||||
|
# 這種 PKM vault 有自己的目錄慣例,整理時不能照 docs/ 那套搬動,
|
||||||
|
# 否則會破壞 vault 結構、讓筆記變不可讀。
|
||||||
|
# 偵測結果寫進 CLAUDE.md,讓 CC 和未來的 Cowork skill 都知道
|
||||||
|
# 「該讀/該整理哪裡」而不是亂動。
|
||||||
|
# 必須在建立 CLAUDE.md 之前跑完。
|
||||||
|
VAULT_TYPE=""
|
||||||
|
RAW_SOURCE=""
|
||||||
|
IS_VAULT="no" # 只有 logseq/obsidian 這種「筆記軟體 vault」才算 yes
|
||||||
|
if [ -d "logseq" ]; then
|
||||||
|
VAULT_TYPE="logseq"
|
||||||
|
RAW_SOURCE="pages/, journals/"
|
||||||
|
IS_VAULT="yes"
|
||||||
|
elif [ -d ".obsidian" ]; then
|
||||||
|
VAULT_TYPE="obsidian"
|
||||||
|
RAW_SOURCE="$(tn './ (整個 vault 根目錄的 .md)' './ (all .md under the vault root)')"
|
||||||
|
IS_VAULT="yes"
|
||||||
|
else
|
||||||
|
VAULT_TYPE="docs"
|
||||||
|
RAW_SOURCE="docs/"
|
||||||
|
fi
|
||||||
|
# 偵測到是筆記 vault → 出聲告訴使用者「我看到了,會小心、不破壞你的筆記結構」。
|
||||||
|
# 不是筆記(一般開發案等)→ 不囉嗦,默默把 docs/ 當原始文件夾安裝完成。
|
||||||
|
if [ "$IS_VAULT" = "yes" ]; then
|
||||||
|
t "🗂️ 偵測到 ${VAULT_TYPE} 筆記庫 → 原始文件:${RAW_SOURCE}" \
|
||||||
|
"🗂️ Detected a ${VAULT_TYPE} note vault → raw source: ${RAW_SOURCE}"
|
||||||
|
t " (會保留你筆記軟體的目錄/檔名結構,不搬動、不改名)" \
|
||||||
|
" (your note app's directory/file structure is preserved — nothing is moved or renamed)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 把「raw source 宣告區塊」吐出來,給新建的 CLAUDE.md append 或
|
||||||
|
# 給已存在的 CLAUDE.md 當手動補貼的提示。內容對 CC / Cowork 都是
|
||||||
|
# 機器可讀的指令(明確路徑 + 不可破壞 vault 結構的約束)。
|
||||||
|
# 寫進 CLAUDE.md 的 raw source 宣告區塊。給人也給 AI 看:
|
||||||
|
# 依 locale 只寫「一種語言」進 CLAUDE.md(雙語會讓每個 session 的 context 更滿)。
|
||||||
|
emit_raw_source_block() {
|
||||||
|
local source_kind
|
||||||
|
if [ "$IS_ZH" = "yes" ]; then
|
||||||
|
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} 筆記庫"
|
||||||
|
else source_kind="一般專案(原始文件放 raw source 路徑)"; fi
|
||||||
|
cat <<BLOCK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 原始文件空間(raw source)
|
||||||
|
|
||||||
|
> 安裝時偵測到的來源型態:**${source_kind}**
|
||||||
|
> CC 與 Cowork 整理/讀取「人寫的原始文件」時,**只在這裡找、只在這裡動**。
|
||||||
|
|
||||||
|
| 項目 | 值 |
|
||||||
|
|------|----|
|
||||||
|
| 來源型態 | \`${source_kind}\` |
|
||||||
|
| raw source | \`${RAW_SOURCE}\` |
|
||||||
|
|
||||||
|
**約束(CC 與 Cowork 都必須遵守)**
|
||||||
|
|
||||||
|
- 整理 wiki/知識時,原始文件**一律從上方 raw source 路徑讀取**,不要假設是 \`docs/\`。
|
||||||
|
BLOCK
|
||||||
|
if [ "$IS_VAULT" = "yes" ]; then
|
||||||
|
cat <<BLOCK
|
||||||
|
- 這是 **${VAULT_TYPE} 筆記庫**:保留它原本的目錄與檔名慣例,**不得搬動、改名、重新分類** \`.md\` 檔,
|
||||||
|
以免破壞筆記軟體結構造成筆記不可讀。整理只在 \`system-dev/wiki/\` 產出,**不動 raw source 本身**。
|
||||||
|
BLOCK
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} note vault"
|
||||||
|
else source_kind="regular project (raw source lives at the path below)"; fi
|
||||||
|
cat <<BLOCK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Raw source space
|
||||||
|
|
||||||
|
> Source type detected at install time: **${source_kind}**
|
||||||
|
> When CC and Cowork curate/read human-written raw source, **look only here and act only here**.
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Source type | \`${source_kind}\` |
|
||||||
|
| raw source | \`${RAW_SOURCE}\` |
|
||||||
|
|
||||||
|
**Constraints (both CC and Cowork must obey)**
|
||||||
|
|
||||||
|
- When curating the wiki/knowledge, **always read raw source from the path above** — don't assume \`docs/\`.
|
||||||
|
BLOCK
|
||||||
|
if [ "$IS_VAULT" = "yes" ]; then
|
||||||
|
cat <<BLOCK
|
||||||
|
- This is a **${VAULT_TYPE} note vault**: keep its original directory and file-naming conventions. **Do not move, rename, or re-classify** \`.md\` files,
|
||||||
|
or you'll break the note-app structure and make notes unreadable. Curation output goes only into \`system-dev/wiki/\`; **never touch the raw source itself**.
|
||||||
|
BLOCK
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 工具函式 ──────────────────────────────────────
|
||||||
|
create_dir() {
|
||||||
|
if [ ! -d "$1" ]; then
|
||||||
|
mkdir -p "$1"
|
||||||
|
CREATED+=("$1/")
|
||||||
|
else
|
||||||
|
SKIPPED+=("$1/ $(tn '(已存在)' '(already exists)')")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download_if_missing() {
|
||||||
|
local dest="$1" src="$2"
|
||||||
|
if [ ! -f "$dest" ]; then
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
curl -sSL "$src" -o "$dest"
|
||||||
|
CREATED+=("$dest")
|
||||||
|
else
|
||||||
|
SKIPPED+=("$dest $(tn '(已存在,跳過)' '(already exists, skipped)')")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 共用結構 ──────────────────────────────────────
|
||||||
|
# 工具自己的文件骨架收進 system-dev/docs/(不污染用戶根目錄、不跟用戶自己的 docs/ 混)。
|
||||||
|
# 注意語義分離:這裡的 system-dev/docs/ 是「工具文件」;用戶的 raw source(原始文件)
|
||||||
|
# 另有其處(見上方 vault 偵測),工具只讀、不搬。
|
||||||
|
# .claude/ 只留 CC 死綁的 commands/ + hooks/,工具資料一律不放這。
|
||||||
|
create_dir "system-dev/docs/1-vision"
|
||||||
|
create_dir "system-dev/docs/2-architecture/decisions"
|
||||||
|
create_dir "system-dev/docs/4-guides"
|
||||||
|
create_dir "system-dev/docs/5-records/incidents"
|
||||||
|
create_dir "system-dev/docs/5-records/test-reports"
|
||||||
|
create_dir "system-dev/docs/6-user"
|
||||||
|
create_dir ".claude/commands"
|
||||||
|
create_dir ".claude/hooks"
|
||||||
|
download_if_missing "system-dev/docs/README.md" "$REPO_URL/system-dev/docs/README.md"
|
||||||
|
|
||||||
|
# 工具版號:放 system-dev/,不寄生 .claude/。
|
||||||
|
download_if_missing "system-dev/VERSION" "$REPO_URL/system-dev/VERSION"
|
||||||
|
|
||||||
|
# ── WIKI 模組 ─────────────────────────────────────
|
||||||
|
# wiki 是工具資料 → 放 system-dev/wiki/(不放 .claude/)。
|
||||||
|
# commands/ 與 hooks/ 是 CC 機制檔 → 維持 .claude/。
|
||||||
|
if $WANT_WIKI; then
|
||||||
|
create_dir "system-dev/wiki"
|
||||||
|
download_if_missing "system-dev/wiki/INDEX.md" "$REPO_URL/system-dev/wiki/INDEX.md"
|
||||||
|
download_if_missing "system-dev/wiki/TAXONOMY.md" "$REPO_URL/system-dev/wiki/TAXONOMY.md"
|
||||||
|
download_if_missing "system-dev/wiki/status.md" "$REPO_URL/system-dev/wiki/status.md"
|
||||||
|
download_if_missing "system-dev/wiki/mistakes.md" "$REPO_URL/system-dev/wiki/mistakes.md"
|
||||||
|
download_if_missing "system-dev/wiki/principles.md" "$REPO_URL/system-dev/wiki/principles.md"
|
||||||
|
download_if_missing "system-dev/wiki/.wikiignore" "$REPO_URL/system-dev/wiki/.wikiignore"
|
||||||
|
|
||||||
|
# wiki 改寫產物(AI 自讀定稿卡片)的正式落點:由工具建好,不靠用戶自救。
|
||||||
|
create_dir "system-dev/wiki/cards"
|
||||||
|
[ -f "system-dev/wiki/cards/.gitkeep" ] || { : > "system-dev/wiki/cards/.gitkeep"; CREATED+=("system-dev/wiki/cards/.gitkeep"); }
|
||||||
|
|
||||||
|
download_if_missing ".claude/commands/wiki-init.md" "$REPO_URL/.claude/commands/wiki-init.md"
|
||||||
|
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"
|
||||||
|
|
||||||
|
# wiki 相關 hooks:接關 + 機敏掃描
|
||||||
|
download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh"
|
||||||
|
download_if_missing ".claude/hooks/wiki-secret-scan.sh" "$REPO_URL/.claude/hooks/wiki-secret-scan.sh"
|
||||||
|
|
||||||
|
# Cowork(claude.ai)整理 wiki 用的 skill:與 CC 的 /wiki-init 共用同一套規則
|
||||||
|
# (含 typed-edge、frontmatter 標籤、gloss)。沒這支 → claude.ai 來掃時身上沒規則。
|
||||||
|
download_if_missing "system-dev/docs/SKILL.md" "$REPO_URL/system-dev/docs/SKILL.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── SDD 模組 ──────────────────────────────────────
|
||||||
|
if $WANT_SDD; then
|
||||||
|
create_dir "system-dev/docs/3-specs"
|
||||||
|
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
|
||||||
|
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
|
||||||
|
download_if_missing "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$REPO_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
|
||||||
|
|
||||||
|
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"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 安裝/更新腳本:一開始就放進 system-dev/scripts/ ──
|
||||||
|
# 為什麼一開始就裝:之後要更新,用戶(或 CC)直接 `bash system-dev/scripts/update.sh`,
|
||||||
|
# 不必每次都記那串 curl。腳本來源在 main/scripts/(不在 template/)。
|
||||||
|
create_dir "system-dev/scripts"
|
||||||
|
download_if_missing "system-dev/scripts/install.sh" "$SCRIPTS_URL/install.sh"
|
||||||
|
download_if_missing "system-dev/scripts/update.sh" "$SCRIPTS_URL/update.sh"
|
||||||
|
|
||||||
|
# ── 共用 hook:專案自訂禁令骨架(預設停用)────────
|
||||||
|
download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh"
|
||||||
|
|
||||||
|
# ── 共用指引:GitHub issue 處理(讀/回普世,跨 repo 發要先問,禁自動輪詢)──
|
||||||
|
download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md"
|
||||||
|
|
||||||
|
chmod +x .claude/hooks/*.sh 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
|
||||||
|
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
|
||||||
|
build_hooks_json() {
|
||||||
|
local session_hooks="" pretool_hooks=""
|
||||||
|
|
||||||
|
if $WANT_WIKI; then
|
||||||
|
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PreToolUse 依模組疊加
|
||||||
|
local pt=()
|
||||||
|
$WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }')
|
||||||
|
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
|
||||||
|
$WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }')
|
||||||
|
local IFS=,
|
||||||
|
pretool_hooks="${pt[*]}"
|
||||||
|
|
||||||
|
printf '{\n "hooks": {\n'
|
||||||
|
if [ -n "$session_hooks" ]; then
|
||||||
|
printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks"
|
||||||
|
fi
|
||||||
|
printf ' "PreToolUse": [\n { "matcher": "Write|Edit",\n "hooks": [ %s ] }\n ]\n' "$pretool_hooks"
|
||||||
|
printf ' }\n}\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -f ".claude/settings.json" ]; then
|
||||||
|
build_hooks_json > .claude/settings.json
|
||||||
|
CREATED+=(".claude/settings.json $(tn "(依 $MODULE 模組產生)" "(generated for module: $MODULE)")")
|
||||||
|
else
|
||||||
|
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── CLAUDE.md:只在完全不存在時建立 ────────────────
|
||||||
|
# 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入,
|
||||||
|
# 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。
|
||||||
|
if [ ! -f "CLAUDE.md" ]; then
|
||||||
|
download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md"
|
||||||
|
if [ -f "CLAUDE.md" ]; then
|
||||||
|
emit_raw_source_block >> CLAUDE.md
|
||||||
|
CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE})" "← raw source declaration written (${VAULT_TYPE})")")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SKIPPED+=("CLAUDE.md $(tn '(已存在,請手動加入對應區塊)' '(already exists — add the block manually)')")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 輸出結果 ──────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
t "✅ 建立了:" "✅ Created:"
|
||||||
|
# 注意:macOS bash 3.2 在 set -u 下展開「空陣列」會炸 unbound variable,
|
||||||
|
# 所以這裡先確認有元素才展開(SKIPPED 區塊在下方本來就有守,CREATED 補上)。
|
||||||
|
if [ ${#CREATED[@]} -gt 0 ]; then
|
||||||
|
for item in "${CREATED[@]}"; do echo " + $item"; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#SKIPPED[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "⚠️ 跳過(已存在):" "⚠️ Skipped (already exists):"
|
||||||
|
for item in "${SKIPPED[@]}"; do echo " - $item"; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "─────────────────────────────────"
|
||||||
|
|
||||||
|
# CLAUDE.md 已存在 → 依模組提醒手動加區塊
|
||||||
|
if [ -f "CLAUDE.md" ]; then
|
||||||
|
if ! grep -q "raw source" CLAUDE.md; then
|
||||||
|
echo ""
|
||||||
|
t "📌 CLAUDE.md 已存在但缺少 raw source 宣告。" \
|
||||||
|
"📌 CLAUDE.md exists but lacks a raw source declaration."
|
||||||
|
t " 請手動把以下區塊貼進去,讓 CC 與 Cowork 知道原始文件在哪、不要亂動既有結構:" \
|
||||||
|
" Paste the block below in so CC and Cowork know where the raw source is and won't disturb your structure:"
|
||||||
|
emit_raw_source_block | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
if $WANT_WIKI && ! grep -q "wiki/status.md" CLAUDE.md; then
|
||||||
|
echo ""
|
||||||
|
t "📌 CLAUDE.md 已存在但缺少 wiki 讀取順序,請手動加入:" \
|
||||||
|
"📌 CLAUDE.md exists but lacks the wiki reading order — please add it manually:"
|
||||||
|
echo ""
|
||||||
|
if [ "$IS_ZH" = "yes" ]; then
|
||||||
|
cat <<'SNIP'
|
||||||
|
## Wiki 讀取順序(push:hook 開 session 自動注入)
|
||||||
|
| 檔案 | 時機 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `system-dev/wiki/status.md` | session 開始第一件事 | 當前進度 |
|
||||||
|
| `system-dev/wiki/principles.md` | 設計任何東西前 | 跨全局原則,必服從 |
|
||||||
|
| `system-dev/wiki/mistakes.md` | 做新功能前 | 已知踩坑 |
|
||||||
|
SNIP
|
||||||
|
else
|
||||||
|
cat <<'SNIP'
|
||||||
|
## Wiki reading order (push: auto-injected at session start)
|
||||||
|
| File | When | Purpose |
|
||||||
|
|------|------|---------|
|
||||||
|
| `system-dev/wiki/status.md` | first thing at session start | current progress |
|
||||||
|
| `system-dev/wiki/principles.md` | before designing anything | global principles, must obey |
|
||||||
|
| `system-dev/wiki/mistakes.md` | before building a new feature | known pitfalls |
|
||||||
|
SNIP
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if $WANT_SDD && ! grep -q "system-dev/docs/3-specs" CLAUDE.md; then
|
||||||
|
echo ""
|
||||||
|
t "📌 CLAUDE.md 已存在但缺少 SDD 鐵律,請手動加入:" \
|
||||||
|
"📌 CLAUDE.md exists but lacks the SDD iron rule — please add it manually:"
|
||||||
|
echo ""
|
||||||
|
if [ "$IS_ZH" = "yes" ]; then
|
||||||
|
cat <<'SNIP'
|
||||||
|
## 絕對鐵律
|
||||||
|
1. 任何 code 變動前必須有對應 SDD(system-dev/docs/3-specs/[子系統]/design.md)
|
||||||
|
找不到 → 停手問負責人,不要自行建立。
|
||||||
|
SNIP
|
||||||
|
else
|
||||||
|
cat <<'SNIP'
|
||||||
|
## Iron rule
|
||||||
|
1. Every code change must have a matching SDD (system-dev/docs/3-specs/[subsystem]/design.md).
|
||||||
|
Not found → stop and ask the owner; do not create one on your own.
|
||||||
|
SNIP
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# settings.json 已存在 → 依模組提醒要合併哪些 hook
|
||||||
|
if [ -f ".claude/settings.json" ]; then
|
||||||
|
MISSING_HOOKS=()
|
||||||
|
$WANT_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING_HOOKS+=("SessionStart: session-start-recall.sh")
|
||||||
|
$WANT_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
|
||||||
|
$WANT_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): sdd-guard.sh")
|
||||||
|
if [ ${#MISSING_HOOKS[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "📌 .claude/settings.json 已存在,請手動把以下 hooks 合併進去(保留既有設定):" \
|
||||||
|
"📌 .claude/settings.json exists — merge the hooks below in manually (keep your existing settings):"
|
||||||
|
for h in "${MISSING_HOOKS[@]}"; do echo " • $h"; done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pre-write-guard 是空殼,提醒它預設不攔(避免「以為有保護其實沒有」的安全錯覺)
|
||||||
|
echo ""
|
||||||
|
t "ℹ️ .claude/hooks/pre-write-guard.sh 是「按需手填的空插槽」,預設不攔任何東西。" \
|
||||||
|
"ℹ️ .claude/hooks/pre-write-guard.sh is an empty slot to fill on demand — by default it blocks nothing."
|
||||||
|
t " 需要專案禁令?最簡單是叫你的 CC 寫一支貼合的 guard hook(比範本表達力強);" \
|
||||||
|
" Need project-specific bans? Easiest is to ask your CC to write a tailored guard hook (more expressive than the template);"
|
||||||
|
t " 或自己填 FORBIDDEN_PATTERNS 並到 settings.json 掛上才會生效。" \
|
||||||
|
" or fill in FORBIDDEN_PATTERNS yourself and wire it into settings.json to take effect."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
t "🚀 下一步:" "🚀 Next steps:"
|
||||||
|
if $WANT_WIKI; then
|
||||||
|
t " 在 Claude Code 對話裡執行 /wiki-init" \
|
||||||
|
" In a Claude Code conversation, run /wiki-init"
|
||||||
|
t " CC 會掃描現有文件、套用 .wikiignore、建立 wiki。" \
|
||||||
|
" CC will scan your existing docs, apply .wikiignore, and build the wiki."
|
||||||
|
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)"
|
||||||
|
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)"
|
||||||
|
echo ""
|
||||||
Executable
+314
@@ -0,0 +1,314 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# system-dev-template updater
|
||||||
|
# 已安裝舊版的人,一鍵更新到新版。
|
||||||
|
#
|
||||||
|
# 核心安全原則:只覆蓋「模板/邏輯檔」,絕不碰「使用者資料檔」。
|
||||||
|
# ✅ 可覆蓋:hooks/*.sh、commands/*.md、TEMPLATE-*、wiki/INDEX.md
|
||||||
|
# ——這些由模板維護,使用者不會手改,新版直接換掉。
|
||||||
|
# 🔒 絕不碰:wiki/status.md、mistakes.md、decisions-summary.md、TAXONOMY.md、.wikiignore、
|
||||||
|
# settings.json、CLAUDE.md
|
||||||
|
# ——這些是使用者自己填的內容,覆蓋=清空他的記憶與設定。
|
||||||
|
#
|
||||||
|
# 「第一次更新」的雞生蛋問題:
|
||||||
|
# 舊版本機沒有 update.sh。所以第一次靠 README 那行 curl 從遠端抓這支腳本來跑。
|
||||||
|
# 跑完它會把自己也更新進 scripts/update.sh,之後就能直接跑本機的 `bash scripts/update.sh`。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── i18n:依 locale 選語言,預設英文(curl | bash 常為 LANG=C)──
|
||||||
|
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
|
||||||
|
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
|
||||||
|
*) IS_ZH="no" ;;
|
||||||
|
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://raw.githubusercontent.com/uncle6me-web/system-dev-template/main"
|
||||||
|
TEMPLATE_URL="$REPO_RAW/template"
|
||||||
|
|
||||||
|
UPDATED=()
|
||||||
|
KEPT=()
|
||||||
|
NEW=()
|
||||||
|
TEMPLATED=()
|
||||||
|
MIGRATED=()
|
||||||
|
|
||||||
|
# ── 版本比對:先看本機 vs 遠端,給使用者「值不值得更新」的判斷 ──
|
||||||
|
# VERSION 新位置在 system-dev/,舊位置在 .claude/(1.8.x 以前)。優先讀新、回退舊。
|
||||||
|
LOCAL_VER="$(tn '(未知)' '(unknown)')"
|
||||||
|
if [ -f "system-dev/VERSION" ]; then
|
||||||
|
LOCAL_VER="$(tr -d '[:space:]' < system-dev/VERSION)"
|
||||||
|
elif [ -f ".claude/VERSION" ]; then
|
||||||
|
LOCAL_VER="$(tr -d '[:space:]' < .claude/VERSION)"
|
||||||
|
fi
|
||||||
|
REMOTE_VER="$(curl -sSL "$TEMPLATE_URL/system-dev/VERSION" 2>/dev/null | tr -d '[:space:]' || echo '')"
|
||||||
|
# 容錯:curl 對 404 會把「404:NotFound」當內容輸出(非空),舊版誤把它寫進 VERSION。
|
||||||
|
# 這裡驗證必須像版號(X.Y.Z),否則一律視為取不到,避免污染 VERSION 檔。
|
||||||
|
case "$REMOTE_VER" in
|
||||||
|
[0-9]*.[0-9]*.[0-9]*) : ;; # 形如 1.9.0 → 合法
|
||||||
|
*) REMOTE_VER="" ;; # 404 / HTML 錯誤頁 / 其他 → 當作沒抓到
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔄 system-dev-template updater"
|
||||||
|
echo "================================="
|
||||||
|
t " 本機版本:${LOCAL_VER}" " Local version: ${LOCAL_VER}"
|
||||||
|
t " 最新版本:${REMOTE_VER:-取不到(檢查網路)}" \
|
||||||
|
" Latest version: ${REMOTE_VER:-unavailable (check network)}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -z "$REMOTE_VER" ]; then
|
||||||
|
t "❌ 取不到遠端版本,可能是網路問題。請稍後再試。" \
|
||||||
|
"❌ Could not fetch the remote version (likely a network issue). Please try again later."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LOCAL_VER" = "$REMOTE_VER" ]; then
|
||||||
|
t "✅ 已是最新版(${LOCAL_VER}),不需更新。" \
|
||||||
|
"✅ Already up to date (${LOCAL_VER}), nothing to update."
|
||||||
|
t " (仍會同步模板邏輯檔,確保 hooks/commands 與最新一致。)" \
|
||||||
|
" (Template logic files will still be synced to keep hooks/commands in line with the latest.)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 結構遷移(1.9.0):舊版把 wiki/VERSION 放 .claude/、工具 docs 放根 docs/ ──
|
||||||
|
# 新版一律收進 system-dev/。這裡冪等遷移:偵測舊位置 → 搬到 system-dev/,已搬過則略過。
|
||||||
|
# 必須在「模組偵測」之前跑(偵測靠目錄存在與否判斷,搬完才看得到新位置)。
|
||||||
|
#
|
||||||
|
# 安全原則:
|
||||||
|
# - wiki 整包搬(含 cards/ 與可能的 wiki/.git),用 mv 保留內含 .git。
|
||||||
|
# - docs 只搬「工具自己鋪的白名單」子目錄;用戶自填在 docs/ 的其他內容一律不動。
|
||||||
|
# - 目的地已存在同名 → 不覆蓋(保留用戶在新位置的東西),略過該項。
|
||||||
|
migrate_dir() { # $1=舊路徑 $2=新路徑
|
||||||
|
local from="$1" to="$2"
|
||||||
|
[ -e "$from" ] || return 0 # 舊的不存在 → 無需遷移
|
||||||
|
if [ -e "$to" ]; then
|
||||||
|
return 0 # 新的已存在 → 冪等略過,不覆蓋
|
||||||
|
fi
|
||||||
|
mkdir -p "$(dirname "$to")"
|
||||||
|
if mv "$from" "$to" 2>/dev/null; then
|
||||||
|
MIGRATED+=("$from → $to")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 任一舊位置還在 → 需要遷移(遷移本身冪等:已搬的項目會被 migrate_dir 略過)。
|
||||||
|
NEEDS_MIGRATE="no"
|
||||||
|
if [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ] \
|
||||||
|
|| [ -d "docs/3-specs" ] || [ -f "docs/SKILL.md" ] || [ -f "docs/README.md" ]; then
|
||||||
|
NEEDS_MIGRATE="yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NEEDS_MIGRATE" = "yes" ]; then
|
||||||
|
t "🔧 偵測到舊版結構,遷移到 system-dev/ …" "🔧 Old layout detected — migrating into system-dev/ …"
|
||||||
|
mkdir -p system-dev
|
||||||
|
|
||||||
|
# wiki(含 cards/ 與內含的 .git)整包搬
|
||||||
|
migrate_dir ".claude/wiki" "system-dev/wiki"
|
||||||
|
# 工具版號
|
||||||
|
migrate_dir ".claude/VERSION" "system-dev/VERSION"
|
||||||
|
# 工具文件白名單(只搬工具鋪的,用戶自填的 docs 內容不動)
|
||||||
|
migrate_dir "docs/SKILL.md" "system-dev/docs/SKILL.md"
|
||||||
|
migrate_dir "docs/README.md" "system-dev/docs/README.md"
|
||||||
|
migrate_dir "docs/1-vision" "system-dev/docs/1-vision"
|
||||||
|
migrate_dir "docs/2-architecture" "system-dev/docs/2-architecture"
|
||||||
|
migrate_dir "docs/3-specs" "system-dev/docs/3-specs"
|
||||||
|
migrate_dir "docs/4-guides" "system-dev/docs/4-guides"
|
||||||
|
migrate_dir "docs/5-records" "system-dev/docs/5-records"
|
||||||
|
migrate_dir "docs/6-user" "system-dev/docs/6-user"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 工具函式 ───────────────────────────────────────
|
||||||
|
# 覆蓋更新:模板/邏輯檔,無條件抓最新版蓋掉。
|
||||||
|
update_file() {
|
||||||
|
local dest="$1" src="$2"
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
if [ -f "$dest" ]; then
|
||||||
|
if curl -sSL "$src" -o "$dest.tmp" 2>/dev/null && [ -s "$dest.tmp" ]; then
|
||||||
|
if cmp -s "$dest" "$dest.tmp"; then
|
||||||
|
rm -f "$dest.tmp" # 內容相同,不算更新
|
||||||
|
else
|
||||||
|
mv "$dest.tmp" "$dest"
|
||||||
|
UPDATED+=("$dest")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$dest.tmp"
|
||||||
|
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
|
||||||
|
NEW+=("$dest") # 新功能:舊版沒有的檔
|
||||||
|
else
|
||||||
|
rm -f "$dest"
|
||||||
|
t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留:使用者資料檔,只記錄「有保留」,永遠不動。
|
||||||
|
keep_file() {
|
||||||
|
[ -f "$1" ] && KEPT+=("$1") || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# 補新檔:舊版沒有、新版才有的「使用者資料檔」(如 principles.md)。
|
||||||
|
# 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。
|
||||||
|
add_if_missing() {
|
||||||
|
local dest="$1" src="$2"
|
||||||
|
if [ -f "$dest" ]; then
|
||||||
|
KEPT+=("$dest")
|
||||||
|
elif curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
|
||||||
|
NEW+=("$dest")
|
||||||
|
else
|
||||||
|
rm -f "$dest"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 客製檔:使用者一定會手填內容(如 pre-write-guard.sh)。
|
||||||
|
# - 已存在 → 絕不覆蓋,但把最新模板版抓到 <檔名>.template.sh 旁邊,供使用者自行 diff 採納。
|
||||||
|
# - 不存在 → 視同新檔,直接抓本體(第一次安裝才會走這條)。
|
||||||
|
keep_with_template() {
|
||||||
|
local dest="$1" src="$2"
|
||||||
|
if [ -f "$dest" ]; then
|
||||||
|
KEPT+=("$dest")
|
||||||
|
local tmpl="${dest%.sh}.template.sh"
|
||||||
|
if curl -sSL "$src" -o "$tmpl.tmp" 2>/dev/null && [ -s "$tmpl.tmp" ]; then
|
||||||
|
if [ -f "$tmpl" ] && cmp -s "$tmpl" "$tmpl.tmp"; then
|
||||||
|
rm -f "$tmpl.tmp" # 模板版沒變,不重複提示
|
||||||
|
else
|
||||||
|
mv "$tmpl.tmp" "$tmpl"
|
||||||
|
TEMPLATED+=("$tmpl")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$tmpl.tmp"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
update_file "$dest" "$src" # 還沒裝過 → 當新檔處理
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 偵測已安裝哪些模組(依現有檔案判斷,更新只動已裝的)──
|
||||||
|
# 遷移已在上面跑完,這裡看新位置 system-dev/。
|
||||||
|
HAS_WIKI=false
|
||||||
|
HAS_SDD=false
|
||||||
|
[ -d "system-dev/wiki" ] && HAS_WIKI=true
|
||||||
|
if [ -f ".claude/hooks/sdd-guard.sh" ] || [ -d "system-dev/docs/3-specs/TEMPLATE-sdd" ]; then HAS_SDD=true; fi
|
||||||
|
|
||||||
|
t "📦 偵測到已安裝模組:" "📦 Detected installed modules:"
|
||||||
|
$HAS_WIKI && echo " • LLM Wiki"
|
||||||
|
$HAS_SDD && echo " • SDD"
|
||||||
|
{ $HAS_WIKI || $HAS_SDD; } || \
|
||||||
|
t " (未偵測到任何模組——這裡可能還沒安裝,請改跑 install.sh)" \
|
||||||
|
" (No modules detected — nothing installed here yet; run install.sh instead.)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── 客製檔:使用者手填的 guardrail,永不覆蓋(issue #3)──
|
||||||
|
# pre-write-guard.sh 的定位是「空白客製模板,使用者沒配置前不提供保護」(CHANGELOG 1.2.0)。
|
||||||
|
# 下游通常已塞滿自己的 enforcement,直接覆蓋=無聲關掉整套 guardrail。
|
||||||
|
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
|
||||||
|
keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh"
|
||||||
|
|
||||||
|
# ── 模板/邏輯檔:覆蓋更新 ──────────────────────────
|
||||||
|
# 共用 hook 與指引
|
||||||
|
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
|
||||||
|
update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
|
||||||
|
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
# Cowork(claude.ai)的 wiki 整理 skill:規則檔,可覆蓋
|
||||||
|
update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md"
|
||||||
|
|
||||||
|
# wiki 的「使用者資料」:絕不碰
|
||||||
|
keep_file "system-dev/wiki/status.md"
|
||||||
|
keep_file "system-dev/wiki/mistakes.md"
|
||||||
|
# principles.md(1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留
|
||||||
|
add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md"
|
||||||
|
keep_file "system-dev/wiki/decisions-summary.md"
|
||||||
|
keep_file "system-dev/wiki/TAXONOMY.md"
|
||||||
|
keep_file "system-dev/wiki/.wikiignore"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $HAS_SDD; then
|
||||||
|
# SDD 範本與 hook:可覆蓋
|
||||||
|
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
|
||||||
|
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
|
||||||
|
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"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 自我更新:把最新的 update.sh / install.sh 抓到 system-dev/scripts/ ──
|
||||||
|
# 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。
|
||||||
|
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 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」──
|
||||||
|
keep_file ".claude/settings.json"
|
||||||
|
keep_file "CLAUDE.md"
|
||||||
|
|
||||||
|
# ── 結果輸出 ───────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "─────────────────────────────────"
|
||||||
|
if [ ${#MIGRATED[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "📦 結構遷移(已收進 system-dev/):" "📦 Layout migrated (moved into system-dev/):"
|
||||||
|
for f in "${MIGRATED[@]}"; do echo " ⇒ $f"; done
|
||||||
|
fi
|
||||||
|
if [ ${#NEW[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "🆕 新功能(舊版沒有,已加入):" "🆕 New features (absent in the old version, now added):"
|
||||||
|
for f in "${NEW[@]}"; do echo " + $f"; done
|
||||||
|
fi
|
||||||
|
if [ ${#UPDATED[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
|
||||||
|
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
|
||||||
|
fi
|
||||||
|
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "✨ 模板邏輯檔已全部最新,無需變動。" \
|
||||||
|
"✨ All template logic files are already up to date — no changes needed."
|
||||||
|
fi
|
||||||
|
if [ ${#KEPT[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "🔒 完整保留(你的內容/設定,從未碰過):" \
|
||||||
|
"🔒 Fully preserved (your content/settings, never touched):"
|
||||||
|
for f in "${KEPT[@]}"; do echo " = $f"; done
|
||||||
|
fi
|
||||||
|
if [ ${#TEMPLATED[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "📋 客製檔有新版範本(你的原檔沒動,新版另存旁邊,請自行 diff 採納):" \
|
||||||
|
"📋 Custom files have a new template version (your original is untouched; the new one is saved alongside — diff and adopt as you like):"
|
||||||
|
for f in "${TEMPLATED[@]}"; do
|
||||||
|
echo " → $f"
|
||||||
|
t " 比對:diff \"${f%.template.sh}.sh\" \"$f\"" \
|
||||||
|
" compare: diff \"${f%.template.sh}.sh\" \"$f\""
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── settings.json 提醒:新模組 hook 可能要手動補 ──
|
||||||
|
if [ -f ".claude/settings.json" ]; then
|
||||||
|
MISSING=()
|
||||||
|
$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")
|
||||||
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
|
||||||
|
"📌 settings.json is yours (untouched), but these hooks are missing — please add them manually:"
|
||||||
|
for h in "${MISSING[@]}"; do echo " • $h"; done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
t "🚀 更新完成:${LOCAL_VER} → ${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER} → ${REMOTE_VER}"
|
||||||
|
t " 下次更新直接跑:bash system-dev/scripts/update.sh" " Next time, just run: bash system-dev/scripts/update.sh"
|
||||||
|
t " 改了什麼看:CHANGELOG.md" " See what changed: CHANGELOG.md"
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# .wikiignore — 不想被編入 wiki 的內容(像 .gitignore)
|
||||||
|
#
|
||||||
|
# 三層防護的 L1(檔案層)。CC 在 /wiki-init、/wiki-capture 掃描文件時,
|
||||||
|
# 命中這裡 pattern 的「整個檔案」不讀、不編入 wiki。
|
||||||
|
#
|
||||||
|
# 語法:一行一個 glob pattern,相對專案根目錄。# 開頭是註解。
|
||||||
|
#
|
||||||
|
# ── 同場另兩層 ──────────────────────────────────────
|
||||||
|
# L2 行內標記:檔案要編入,但某段不要 → 在該段前後包:
|
||||||
|
# <!-- wiki:ignore -->
|
||||||
|
# 這幾行不會被編入 wiki
|
||||||
|
# <!-- wiki:end -->
|
||||||
|
# L3 機械底線:萬一機敏值仍被寫進 .claude/wiki/,wiki-secret-scan.sh 會 exit 2 擋下。
|
||||||
|
#
|
||||||
|
# 三層的分工:L1 整檔排除(你主動列)|L2 局部遮蔽(你標記)|L3 兜底攔截(自動掃)。
|
||||||
|
# ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ── 機敏檔案(預設就該排除)──────────────────────
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*credentials*
|
||||||
|
*secret*
|
||||||
|
**/secrets/**
|
||||||
|
|
||||||
|
# ── 個資 / 客戶資料(依專案調整)────────────────
|
||||||
|
# customers/**
|
||||||
|
# *personal-data*
|
||||||
|
|
||||||
|
# ── 草稿 / 暫存(不值得進記憶)──────────────────
|
||||||
|
# **/draft/**
|
||||||
|
# *.tmp.md
|
||||||
|
# SCRATCH.md
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# system-dev/wiki/ — LLM 記憶系統
|
||||||
|
|
||||||
|
> 新 session 開始時從這裡導航。
|
||||||
|
> 目的:讓 CC 不需要重新學習已知的事。
|
||||||
|
> 維護者:CC(人不手動編輯這裡)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## push 檔(session 開始由 hook 主動注入,CC 行動前必看見)
|
||||||
|
|
||||||
|
| 檔案 | 注入形態 | 內容 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `status.md` | 全文 | 當前進度、下一步(時態狀態)|
|
||||||
|
| `principles.md` | 全文(一行一條)| 跨全局設計原則,行動前必服從 |
|
||||||
|
| `mistakes.md` | 標題+一行症狀,全文按需展開 | 踩過的坑、被糾正的誤解(防不自覺盲區)|
|
||||||
|
|
||||||
|
> 為什麼這三個 push 而非 pull:它們是「CC 不會主動查、但不看就出事」的盲區。詳見 `/wiki-init` 的「push vs pull」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## pull:cards/(CC 按需檢索)
|
||||||
|
|
||||||
|
一切知識內容——原文摘要、AI 筆記、決策、概念知識——都寫成 `cards/<bucket>/` 的概念原子卡。
|
||||||
|
`decisions-summary.md` 已降級為 cards(決策=知識內容);既有的保留為相容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 維護規則
|
||||||
|
|
||||||
|
1. 只增不刪——記錄 append,內容改了加新條目說明「舊的已更新」
|
||||||
|
2. status.md 每次 session 結束更新;mistakes/principles 一發現就 append
|
||||||
|
3. principles 一行一條、≤15 條(超過代表該合併或下放成 card)
|
||||||
|
4. **新增一個檢索角度 = 在下方「多角度視圖」加一節,不開新實體檔、不問用戶**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 多角度視圖(由 /wiki-init、/wiki-capture 填入)
|
||||||
|
|
||||||
|
INDEX 是**所有檢索角度的入口**,不只標籤。原文是唯讀 SSoT,wiki 是改寫過的記憶。
|
||||||
|
新增角度只要在這裡加一節(如「決策角度」「原則角度」),指向對應 cards 或 push 檔——**不必新增實體特殊檔**。
|
||||||
|
|
||||||
|
### 標籤角度(按 `TAXONOMY.md` 的軸聚類,指向桶子索引)
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
#### 知識管理
|
||||||
|
- [[pkm/00-INDEX]] — PKM 知識管理(N 卡)
|
||||||
|
|
||||||
|
#### AI 協作
|
||||||
|
- [[ai/00-INDEX]] — AI 協作(M 卡)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 決策角度(取代舊 decisions-summary.md 的視圖)
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- [[某決策卡]] — 一句話結論(YYYY-MM-DD)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 結構:INDEX(多角度入口)→ `cards/<bucket>/00-INDEX.md`(桶子索引,固定名)→ 概念原子卡。
|
||||||
|
> 指 `00-INDEX` **一律帶路徑** `[[bucket/00-INDEX]]`(固定名跨桶撞名);卡片間用裸 `[[卡名]]`。
|
||||||
|
> 分類由卡片 frontmatter `tags:` 承載,標籤字典見 `TAXONOMY.md`。詳見 `/wiki-init` 規範。
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# TAXONOMY.md — 標籤字典(本 repo 專屬)
|
||||||
|
|
||||||
|
> wiki 卡片的 frontmatter `tags:` **只能用這裡列出的標籤**。
|
||||||
|
> 這不是凍結——字典**可以擴充**,只是**禁止繞過字典在卡片裡直接冒新標籤**。
|
||||||
|
> 維護者:CC(由 /wiki-init 初始化、隨 wiki 演進受控擴充)。
|
||||||
|
>
|
||||||
|
> **遇到現有軸都裝不下的內容時,照這個流程(先查、後擴、登記)**:
|
||||||
|
> 1. **先查既有**:現有標籤真的都不合,還是只是同義詞?(`知識管理` vs `KM` vs `筆記管理` 是同一軸,別重造)
|
||||||
|
> 2. **確實是新軸** → 把新標籤加進本檔(附一句定義),再用。不必停下來問人,但「先登記再使用」這道查核不能省。
|
||||||
|
> 3. 自由增生才是要防的——同義標籤散開會讓同類卡片分散、下游聚類失準。受控擴充(先查重、再登記)不會。
|
||||||
|
>
|
||||||
|
> **字典是每個 repo 各自的**:跨 repo 引擎靠各 repo 自己一致的 taxonomy 接合,不是逼所有 repo 共用一份。
|
||||||
|
> 知識型 vault 的領域軸(知識管理/學習認知)和開發 repo(子系統/基礎設施)本來就該不同。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分類採雙軸(一張卡可多重歸屬)
|
||||||
|
|
||||||
|
分類由 **frontmatter `tags:`** 承載,不靠資料夾、不靠行內 `#tag`。
|
||||||
|
一張卡同時掛「領域 1-3 個 + 形態 0-2 個」,可從任一軸過濾。
|
||||||
|
|
||||||
|
### 領域(主軸,每卡 1-3 個)
|
||||||
|
|
||||||
|
> 由 /wiki-init 依專案性質提出。以下為 PKM / 知識型 vault 的實證一組,請按你的專案調整:
|
||||||
|
|
||||||
|
- `知識管理`
|
||||||
|
- `學習認知`
|
||||||
|
- `AI協作`
|
||||||
|
- `生產力`
|
||||||
|
- `系統設計`
|
||||||
|
- `工具教學`
|
||||||
|
|
||||||
|
(一般開發專案的領域軸可能是:`子系統A` / `子系統B` / `基礎設施` / `前端` / `後端` …)
|
||||||
|
|
||||||
|
### 形態(副軸,每卡 0-2 個)
|
||||||
|
|
||||||
|
- `方法論`
|
||||||
|
- `工具實作`
|
||||||
|
- `觀點主張`
|
||||||
|
- `架構設計`
|
||||||
|
- `案例經驗`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 規則
|
||||||
|
|
||||||
|
1. **先查重、再登記、才使用**:禁止繞過字典在卡片直接冒新標籤;新標籤先確認非現有同義詞,加進本檔(附定義)再用。
|
||||||
|
2. **領域 vs 形態分開**:領域是「講什麼主題」,形態是「以什麼形式呈現」,不要混。
|
||||||
|
3. **頂層 INDEX.md 的標籤視圖依本字典的軸聚類**——字典改了,INDEX 視圖跟著更新。
|
||||||
|
4. **新增領域軸要慎**:領域是檢索骨架,動它影響全庫聚類;形態軸(呈現形式)擴充較安全。不確定就先用現有最接近的,並在卡片或本檔註記「待人類複核此分類」。
|
||||||
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
格式:
|
格式:
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# principles — 跨全局設計原則(push:CC 行動前必服從)
|
||||||
|
|
||||||
|
> 這個檔由 hook 在 session 開始**全文注入**,讓 CC 設計任何東西前都先看見這些準繩。
|
||||||
|
> 為什麼 push 而非寫成 card:原則是「會被遺忘的盲區」——沒推到眼前,CC 設計時很可能沒想到要服從就做了。
|
||||||
|
>
|
||||||
|
> 規則:**一行一條**,精煉成準繩(不是長篇論述)。≤15 條;超過代表某些該合併、或下放成 card。
|
||||||
|
> 發現新的跨全局原則 → append 一行。累積原則只改這個檔,**不必問用戶開新檔**。
|
||||||
|
> 區分:原則 = 反覆適用的準繩(這裡);單次選擇 = 決策(寫成 card);踩過的坑 = mistakes.md。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 原則
|
||||||
|
|
||||||
|
<!-- 一行一條。範例格式:
|
||||||
|
- **不污染用戶根目錄**:工具產物收進專屬資料夾,不在用戶根目錄撒檔、不跟用戶自己的檔混。
|
||||||
|
-->
|
||||||
|
|
||||||
|
(尚未填入。由 /wiki-init 或 /wiki-capture 依本專案累積。)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 當前狀態
|
||||||
|
|
||||||
|
> 更新時間:2026-06-26
|
||||||
|
> 每次 session 結束必須更新此檔(用 /wiki-update)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最新(2026-06-26:issue #1 T3 — ingest 寫入端 + graph 端 API)
|
||||||
|
|
||||||
|
開 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`(純被動代轉)、keyword 收斂(3.6d)。
|
||||||
|
|
||||||
|
驗證:`vitest run` **19 passed**(mock);zero SQL / 無 D1·Vectorize·AI;`wrangler deploy --dry-run` bundle 乾淨;所有 action ≤100 行。
|
||||||
|
|
||||||
|
## 正在做 / 阻擋
|
||||||
|
|
||||||
|
- [🔄] **PR #2 待 review/merge** — 阻擋點:等 leo/總管。graph repo 端 T3 實作面已收斂,剩跨 repo 接通。
|
||||||
|
|
||||||
|
## 下次 session 第一件事
|
||||||
|
|
||||||
|
**確認 PR #2 狀態**:若已 merge → 切回 `main` 拉新狀態;若有 review 意見 → 在 `feat/ingest-contract-sdd` 上改。
|
||||||
|
本 repo 端 T3 無剩餘可單獨做的 task(其餘待跨 repo / 部署,見下)。
|
||||||
|
|
||||||
|
## 待負責人確認
|
||||||
|
|
||||||
|
- **PR #2 merge** — 等:leo/總管 review。
|
||||||
|
- **MCP 註冊薄殼**(圖工具 traverse/neighbors/source/refresh 併入 arcrun `u6u-mcp-server`)— 等:總管協調 arcrun,**不另起 graph MCP**。graph 端 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」缺口已部分解除。
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// get_source + refresh(C 段)— 走 mock,零 SQL、不打真網路。
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { ingestEnvelope } from '../src/actions/triplet-ingest';
|
||||||
|
import { getSource } from '../src/actions/graph-source';
|
||||||
|
import { refreshSource } from '../src/actions/graph-refresh';
|
||||||
|
import { mockClient } from './mock-client';
|
||||||
|
|
||||||
|
describe('getSource — 回節點的原文來源指標', () => {
|
||||||
|
it('回觸及節點的 triplet 的 uri + anchor', async () => {
|
||||||
|
const c = mockClient();
|
||||||
|
await ingestEnvelope(c, {
|
||||||
|
source: { uri: 'github:u/w@a.md', content_hash: 'h1', anchor: '#graph-rag' },
|
||||||
|
extractor: { model: 'm', tier: 'deep' },
|
||||||
|
triplets: [{ subject: 'GraphRAG', predicate: '是', object: 'RAG 變體' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const refs = await getSource(c, 'GraphRAG');
|
||||||
|
expect(refs.length).toBe(1);
|
||||||
|
expect(refs[0].uri).toBe('github:u/w@a.md');
|
||||||
|
expect(refs[0].anchor).toBe('#graph-rag');
|
||||||
|
expect(refs[0].edge).toEqual({ subject: 'GraphRAG', predicate: '是', object: 'RAG 變體' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deprecated triplet 不出現在 get_source(active-only)', async () => {
|
||||||
|
const c = mockClient();
|
||||||
|
await ingestEnvelope(c, {
|
||||||
|
source: { uri: 'github:u/w@a.md', content_hash: 'h1', anchor: '#old' },
|
||||||
|
extractor: { model: 'm', tier: 'deep' },
|
||||||
|
triplets: [{ subject: 'X', predicate: 'r', object: 'old' }],
|
||||||
|
});
|
||||||
|
await ingestEnvelope(c, {
|
||||||
|
source: { uri: 'github:u/w@a.md', content_hash: 'h2', anchor: '#new' },
|
||||||
|
extractor: { model: 'm', tier: 'deep' },
|
||||||
|
triplets: [{ subject: 'X', predicate: 'r', object: 'new' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const refs = await getSource(c, 'X');
|
||||||
|
expect(refs.length).toBe(1);
|
||||||
|
expect(refs[0].anchor).toBe('#new'); // 只見 active 批
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refreshSource — 代轉 ingest(人發起)', () => {
|
||||||
|
it('KBDB_INGEST_URL 未設 → 誠實回 forwarded:false,不假裝成功', async () => {
|
||||||
|
const res = await refreshSource({ uri: 'github:u/w@a.md' }, undefined);
|
||||||
|
expect(res.forwarded).toBe(false);
|
||||||
|
expect(res.note).toMatch(/未就緒|未設/);
|
||||||
|
});
|
||||||
|
});
|
||||||
+14
-2
@@ -50,12 +50,19 @@ export class MockKbdbClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ensureTemplate(name: string, slots: string[]): Promise<void> {
|
async ensureTemplate(name: string, slots: string[]): Promise<void> {
|
||||||
if (!this.templates.has(name)) this.templates.set(name, slots);
|
// 對齊真 client 的 slot-diff 行為:既有 template 補缺 slot(不 early-return)。
|
||||||
|
const have = this.templates.get(name);
|
||||||
|
if (!have) {
|
||||||
|
this.templates.set(name, [...slots]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const set = new Set(have);
|
||||||
|
for (const s of slots) if (!set.has(s)) have.push(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createRecord(template: string, values: Record<string, string>, owner_id?: string): Promise<string> {
|
async createRecord(template: string, values: Record<string, string>, owner_id?: string): Promise<string> {
|
||||||
const id = this.id('rec');
|
const id = this.id('rec');
|
||||||
this.records.set(id, { template, values, owner_id });
|
this.records.set(id, { template, values: { ...values }, owner_id });
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +72,11 @@ export class MockKbdbClient {
|
|||||||
return { record_id: recordId, template: r.template, values: r.values };
|
return { record_id: recordId, template: r.template, values: r.values };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateRecord(recordId: string, values: Record<string, string>): Promise<void> {
|
||||||
|
const r = this.records.get(recordId);
|
||||||
|
if (r) Object.assign(r.values, values);
|
||||||
|
}
|
||||||
|
|
||||||
async listRecordsByTemplate(template: string, owner_id?: string): Promise<BaseRecord[]> {
|
async listRecordsByTemplate(template: string, owner_id?: string): Promise<BaseRecord[]> {
|
||||||
return [...this.records.entries()]
|
return [...this.records.entries()]
|
||||||
.filter(([, r]) => r.template === template && (!owner_id || r.owner_id === owner_id))
|
.filter(([, r]) => r.template === template && (!owner_id || r.owner_id === owner_id))
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// ingest 寫入端 — 走 mock KbdbClient(API-as-Wall),零 SQL、不打網路。
|
||||||
|
// 覆蓋 T3.4 五案:正常 envelope / 同 hash no-op / 新 hash deprecate / 污染 envelope 422 / rollback。
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { ingestEnvelope, IngestEnvelopeSchema, type IngestEnvelope } from '../src/actions/triplet-ingest';
|
||||||
|
import { queryTriplets } from '../src/actions/triplet-crud';
|
||||||
|
import { mockClient } from './mock-client';
|
||||||
|
|
||||||
|
function envelope(hash: string, triplets: IngestEnvelope['triplets']): IngestEnvelope {
|
||||||
|
return {
|
||||||
|
source: { uri: 'github:uncle6me-web/wiki@a.md', content_hash: hash },
|
||||||
|
extractor: { model: 'claude-sonnet-4-6', tier: 'deep' },
|
||||||
|
triplets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ingestEnvelope — 正常 envelope', () => {
|
||||||
|
it('append 全部 triplet 為 active,記 source_uri/content_hash', async () => {
|
||||||
|
const c = mockClient();
|
||||||
|
const res = await ingestEnvelope(c, envelope('h1', [
|
||||||
|
{ subject: 'A', predicate: 'rel', object: 'B' },
|
||||||
|
{ subject: 'B', predicate: 'rel', object: 'C' },
|
||||||
|
]));
|
||||||
|
expect(res).toEqual({ skipped: false, ingested: 2, deprecated: 0 });
|
||||||
|
|
||||||
|
const { triplets } = await queryTriplets(c, {});
|
||||||
|
expect(triplets.length).toBe(2);
|
||||||
|
expect(triplets.every((t) => t.status === 'active')).toBe(true);
|
||||||
|
expect(triplets[0].source_uri).toBe('github:uncle6me-web/wiki@a.md');
|
||||||
|
expect(triplets[0].content_hash).toBe('h1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ingestEnvelope — 同 hash no-op', () => {
|
||||||
|
it('同 uri+hash 再送 → skipped,不新增', async () => {
|
||||||
|
const c = mockClient();
|
||||||
|
await ingestEnvelope(c, envelope('h1', [{ subject: 'A', predicate: 'r', object: 'B' }]));
|
||||||
|
const res = await ingestEnvelope(c, envelope('h1', [{ subject: 'A', predicate: 'r', object: 'B' }]));
|
||||||
|
expect(res.skipped).toBe(true);
|
||||||
|
|
||||||
|
const { triplets } = await queryTriplets(c, {});
|
||||||
|
expect(triplets.length).toBe(1); // 沒有重複 append
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ingestEnvelope — 新 hash deprecate-then-append', () => {
|
||||||
|
it('同 uri 新 hash → 舊批轉 deprecated、新批 active;查詢 active-only', async () => {
|
||||||
|
const c = mockClient();
|
||||||
|
await ingestEnvelope(c, envelope('h1', [{ subject: 'A', predicate: 'r', object: 'old' }]));
|
||||||
|
const res = await ingestEnvelope(c, envelope('h2', [{ subject: 'A', predicate: 'r', object: 'new' }]));
|
||||||
|
expect(res).toEqual({ skipped: false, ingested: 1, deprecated: 1 });
|
||||||
|
|
||||||
|
// active-only 查詢只見新批。
|
||||||
|
const active = await queryTriplets(c, {});
|
||||||
|
expect(active.triplets.length).toBe(1);
|
||||||
|
expect(active.triplets[0].object).toBe('new');
|
||||||
|
|
||||||
|
// 舊批仍在(deprecated),可考古/rollback。
|
||||||
|
const all = await queryTriplets(c, { includeDeprecated: true });
|
||||||
|
expect(all.triplets.length).toBe(2);
|
||||||
|
const deprecated = all.triplets.find((t) => t.status === 'deprecated');
|
||||||
|
expect(deprecated?.object).toBe('old');
|
||||||
|
expect(deprecated?.superseded_by).toBe('h2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ingestEnvelope — 污染 envelope 422(契約 strict)', () => {
|
||||||
|
it('triplet 帶 graph 領域欄位 bridge_score → schema 拒收', () => {
|
||||||
|
const polluted = {
|
||||||
|
source: { uri: 'u', content_hash: 'h' },
|
||||||
|
extractor: { model: 'm', tier: 'deep' },
|
||||||
|
triplets: [{ subject: 'A', predicate: 'r', object: 'B', bridge_score: 3 }],
|
||||||
|
};
|
||||||
|
const parsed = IngestEnvelopeSchema.safeParse(polluted);
|
||||||
|
expect(parsed.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('envelope 頂層帶禁止欄位 id → 拒收', () => {
|
||||||
|
const polluted = {
|
||||||
|
id: 'should-not-send',
|
||||||
|
source: { uri: 'u', content_hash: 'h' },
|
||||||
|
extractor: { model: 'm', tier: 'deep' },
|
||||||
|
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();
|
||||||
|
await ingestEnvelope(c, envelope('h1', [{ subject: 'A', predicate: 'r', object: 'old' }]));
|
||||||
|
await ingestEnvelope(c, envelope('h2', [{ subject: 'A', predicate: 'r', object: 'new' }]));
|
||||||
|
|
||||||
|
// 取出被 deprecate 的舊批 id,手動 rollback(翻回 active、清 superseded_by)。
|
||||||
|
const all = await queryTriplets(c, { includeDeprecated: true });
|
||||||
|
const old = all.triplets.find((t) => t.status === 'deprecated')!;
|
||||||
|
await c.updateRecord(old.id, { status: 'active', superseded_by: '' });
|
||||||
|
|
||||||
|
const active = await queryTriplets(c, {});
|
||||||
|
expect(active.triplets.map((t) => t.object).sort()).toEqual(['new', 'old']);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user