@carllee1983/dbcli 3.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,252 @@ All notable changes to dbcli are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [6.0.0] - 2026-09-01 - 一份黑名單設定,四個互不相同的比對器
9
+
10
+ 規格 SQL 第 7、8、9 則與 MongoDB 第 3–6 則。設計決策記在
11
+ `docs/adr/0018-a-blacklist-rule-that-does-not-match-fails-loudly.md` 與
12
+ `docs/adr/0019-one-blacklist-rule-one-matcher.md`。
13
+
14
+ SQL 那半:本機 MariaDB、表 `probe_users (id, Password, note)`、值 `s3cret`,八種設定裡七種洩漏,而全部都被設定載入器無聲接受——操作者「規則有效」的唯一證據是 dbcli 沒有抱怨。
15
+
16
+ MongoDB 那半(本機容器 MongoDB 7.0.31 實測):同一組規則被四個比對器讀,沒有兩個一致——請求側是字面的點號成分比對、讀取遮罩懂 `foo.*`、寫入側是祖先走訪、集合名是 `Set.has`。於是 `user.*` 擋得住讀擋不住寫,`pass*` 到處都不生效卻仍印出「可能已遮罩」的提示,`secrets*` 沒有任何一個比對器認得。規格第 3 則實測後不成立:十四個 update operator 全被 ADR-0015 的請求側檢查擋下,寫入側那個缺口是真的但被外層蓋住——仍然照深度防禦補齊。
17
+
18
+ ### Changed
19
+
20
+ - **BREAKING:欄位規則與回傳欄位名比對時,第一段摺成小寫。** 先前規則 `password` 對欄位 `Password` 完全不命中,而最嚴重的一則不是設定寫錯:規則大小寫**寫對了**,`SELECT Password AS PASSWORD` 照樣把值送回來,`query-only` 就做得到——遮罩比對的是回傳時的鍵名,而別名選了那個鍵名,與 ADR-0015 為 MongoDB `$project` 關掉的是同一個形狀,只是這裡經由大小寫抵達。只摺第一段:後面的段落是巢狀物件的鍵(JSON 欄位裡的 `profile.SSN`),不是 SQL 識別字。代價是 PostgreSQL 允許 `"Password"` 與 `"password"` 並存於同一張表,規則寫其中一個現在兩個都遮——過度拒絕,與 ADR-0014、0015 同一個方向。
21
+
22
+ - **BREAKING:以自己的表限定的欄位項改為載入失敗。** `{"users": ["users.password"]}` 從來沒有命中過任何東西。它不能被靜默改寫,因為欄位項裡的點號已經有第二個合法意思——`profile.ssn` 是巢狀路徑,正是第 8 則那個祖先走訪存在的理由。比對第一段是否等於它所在的表名鍵,是唯一不需要猜測就能分辨兩者的判準。**含有這種條目的設定會停止載入,直到改掉為止。**
23
+
24
+ - **寫入側改為與讀取側相同的祖先走訪。** `checkColumnBlacklistOnWrite` 是字面 `includes`,而 `filterColumnsForTables` 會沿點號向上走,於是規則 `profile` 之下 `profile.ssn` 能寫不能讀。沒有任何一種「被列入黑名單」的讀法能容許這件事。
25
+
26
+ - **BREAKING:`blacklist.tables` 的每個條目都是 glob,對所有引擎皆然。** 先前 `isTableBlacklisted` 是 `Set.has`,`tables: ["secrets*"]` 在 MongoDB 與 SQL 完全不擋,而使用者依 Redis 那側的文件正是那樣寫的(`blacklist-validator.ts` 裡 ES 的註解已經記下同一件事)。同一個鍵不該在 Redis 是 glob、在 ES 是 glob、在 SQL 是字面。代價是含 `*` 的設定在 SQL 連線上開始擋東西;方向只會多拒絕不會少拒絕,字面名稱仍匹配它自己。真的叫 `report*` 的表寫成 `report\*` 可回到字面比對。
27
+
28
+ - **BREAKING:`blacklist.columns` 的每一段都是 glob。** `pass*` 現在真的遮 `password`,先前它被 `compilePatterns` 拒絕,而被拒絕的結果是整份文件原樣回傳。萬用字元不跨點號:`pass*` 不匹配 `user.password`。唯一保留的特例是結尾整段的 `*`——`user.*` 仍然涵蓋 `user` 自己與它底下的一切,照純段落 glob 讀會變成只匹配 `user.<一段>`,那會讓已經部署的規則無聲縮小。
29
+
30
+ - **BREAKING:無法編譯的欄位規則改為中止操作。** 先前 `field-masker.ts` 看到 `patterns.length === 0` 就原樣回傳整份文件,而 CLI 照樣印「Some fields may have been redacted」——那句提示是操作者手上唯一的證據,而它是錯的。ADR-0019 Decision 3 與 ADR-0018 Decision 2 同一個理由。`a.*.b` 這種先前被拒的寫法現在合法,留下來會被拒的是真的壞掉的條目(空段落、空字串)。
31
+
32
+ - **請求側、讀取側、寫入側改用同一個比對器。** `reachesProtectedField` 與 `checkColumnBlacklistOnWrite` 現在走 `path-matcher`,所以帶萬用字元的規則在三個端點是同一個意思。仍然逐點號成分比對,不是子字串比對——`passwordless` 不會被 `password` 誤傷。
33
+
34
+ - **`insert` 傳扁平化的欄位路徑,不再是頂層鍵。** `insert --data '{"user":{"password":"x"}}'` 在規則 `user.password` 之下寫得進去(實測確認),因為 `Object.keys(data)` 只看得到 `user`。SQL 的扁平 data 扁平化後結果相同,行為不變。
35
+
36
+ - **`update` 的寫入欄位收集涵蓋所有 operator。** 先前只看 `$set` / `$unset`,`$rename`、`$inc`、`$push`、`$bit` 等十二個 operator 寫的欄位不進黑名單檢查。實測那些 operator 全被請求側擋下,所以這是補一個構不到的洞——一個只因為另一個控制夠嚴才擋得住的控制不算控制,與 ADR-0015 同一個理由。
37
+
38
+ ### Fixed
39
+
40
+ - **glob 的萬用字元涵蓋換行,與 Redis 一致。** `globToRegex` 把 `*` 譯成 `.*`,而 JavaScript 的 `.` 在沒有 dotAll 時不匹配 `\n`;Redis 的 `stringmatchlen` 逐位元組比對,`*` 吃任何位元組。於是 `secrets:*` 保護不到 `secrets:\nx`,而 `parseRedisCommand` 在引號內保留換行,這條路是可達的;同一組 regex 也驅動 `sampleKeyNames`,所以 `dbcli list` 也照樣顯示那個 key。`$` 不需要配套改動:JavaScript 把它錨在輸入結尾(不像 Perl 允許結尾換行),所以字面 pattern 仍然不匹配尾端多一個換行的 key——那也正是 Redis 的答案。同一個函式也用於 Elasticsearch 的 index 運算式,那裡 index 名不含換行,因此不受影響。
41
+
42
+ - **設定裡的前後空白與外層引號會被去掉。** `[" password "]`、`["\"password\""]`、``["`password`"]``、鍵 `" users "` 全部靜默無效。ES 那側的 `es-index-target.ts` 早就 trim 兼解引號,SQL 側沒有。
43
+
44
+ - **表規則同時以完整名稱與最後一段查找。** `{"public.users": …}` 對 `SELECT * FROM users` 不生效,反向也一樣——`extractTableReferences` 會保留限定名稱的完整字串,所以每個鍵只對它被寫成的那一種拼法生效。這是查找的改動而非解析時的猜測:沒有任何地方決定操作者指的是哪一種拼法,同一張表的兩種拼法解析到同一份規則。
45
+
46
+ - **`$rename` 的風險說明不再宣稱它不外洩。** `dml-plan.ts` 的 RENAME tier 訊息寫著 `field rename does not exfiltrate data`,那是錯的:改名之後受保護欄位的值躺在讀取遮罩不認得的名字底下。程式碼裡的斷言本身要當成待驗證的宣稱。
47
+
48
+ - **glob 比對改為線性時間,不再走 regex。** `globToRegex` 把每個 `*` 譯成 `.*`,而多個 `.*` 對**不匹配**的名稱會災難性回溯:`'a' + '*'.repeat(50) + 'b'` 比對一個 300 字元的字串,跑三分鐘沒有回來(2026-08-31 實測),改用 `globMatches` 之後同一則 0.23ms。每一次黑名單判定都走這條路——Redis key 規則、Elasticsearch index 運算式,而在本輪之後還加上所有引擎的欄位規則與表名。設定不必有惡意就踩得到(`*_*_*_*` 是很自然的寫法),而可達的輸入包含來自資料庫而非操作者的 Redis key 名稱。這個缺陷比本輪更早,本輪把它的影響面從兩條路徑擴大到全部,所以在這裡一起修掉。`globToRegex` 保留給真的需要 `RegExp` 物件的呼叫端,兩者以小字母表的**窮舉**比對釘住答案一致——那個窮舉當場就抓到第一版的一個真 bug:`*` 之後的跳脫字元沒有清掉尾綴萬用字元旗標,於是 `*\a` 會匹配 `ab`。
49
+
50
+ ## [5.1.0] - 2026-08-31 - 一次被黑名單擋下的查詢,紀錄裡指向的是沒被擋的那張表
51
+
52
+ `docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 10 則。設計決策記在 `docs/adr/0017-the-audit-target-stays-wrong-and-the-record-stops-depending-on-it.md`。
53
+
54
+ ### Added
55
+
56
+ - **SQL 的稽核紀錄帶 `metadata.blacklist_checked`:黑名單拿這句語句比對過的每一個識別子。** 黑名單走 tokenizer(`extractTableReferences`),稽核的 `target` 走另一套單名推導(`extractTableName`),兩份解析給出兩個答案。實測:`SELECT * FROM a JOIN salaries s …` 的 `target` 只有 `a`;`CREATE TABLE dump AS SELECT * FROM salaries` 的 `target` 是被讀的 `salaries`,被建立的 `dump` 不在紀錄裡;`INSERT INTO staging SELECT * FROM salaries` 的 `target` 是 `staging`,被讀的 `salaries` 不見。最尖銳的一則規格沒寫:把 `salaries` 設進黑名單後跑那句 JOIN,拒絕是對的,而**那次拒絕的稽核列 `target` 是 `a`**——用 `target` 去查「有沒有人試圖碰受保護的表」查不到,真正的表名只活在 `error` 的自由文字裡。
57
+
58
+ - **這份清單原樣保存,不做過濾。** `extractTableReferences` 是刻意過度收集的:那句 JOIN 回傳 `["a","salaries","s","id","s.id","a.id"]`,那句 CTAS 回傳 `["dump","salaries","CREATE","TABLE"]`——別名、含點的欄位參照、SQL 關鍵字都在裡面。對黑名單而言這是對的,多一個識別子只會讓它多拒絕。規格原本提的 `metadata.tables` 因此會把 `CREATE` 當成表名寫進稽核;改為以欄位名說明實情,而不是再造一套與黑名單對不起來的解析。
59
+
60
+ ### Changed
61
+
62
+ - **`target` 維持原樣,刻意不改。** 它是下游拿來 filter 的欄位,而兩種候選修法(改成「被作用的那張表」、或改成 tokenizer 的首張)都會在沒有人被告知的情況下改變既有查詢的結果。這次修的是「有一張表完全不在紀錄裡」,把還在的那個欄位一起搬動並不會讓缺席修得更好。`target` 與 `blacklist_checked` 對某些語句會不一致,那是刻意的,不是待清理的 bug。
63
+
64
+ ## [5.0.0] - 2026-08-31 - shell 裡一句真的改了資料的 UPDATE,稽核裡沒有任何一列
65
+
66
+ `docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 11 則自己註明是純讀碼結論、值得先端到端確認。確認了,結論成立,而且比讀碼看到的更嚴重。本機 MariaDB、`read-write` 連線、每次先清空 audit:`dbcli query "SELECT …"` 寫一列,而在 `dbcli>` 提示符打的同一句寫零列;一句帶 WHERE 的 `UPDATE` 走完全程、改掉了資料,同樣零列。設計決策記在 `docs/adr/0016-the-sql-shell-audits-every-statement-in-two-rows.md`。
67
+
68
+ ### Changed
69
+
70
+ - **BREAKING(對解析 audit 紀錄的下游而言):`metadata.es_shell_phase` 改名為 `metadata.shell_phase`。** 兩個 shell 現在共用同一個鍵。ADR-0016 Decision 2 的理由是讀 audit 的人不該需要先知道一列是哪個引擎產生的,而兩個鍵名正是那件事。任何在 4.0.0 上解析 `es_shell_phase` 的東西都會斷。
71
+
72
+ - **SQL shell 為每一句語句寫稽核列,讀取也不例外。** 先前只有 tier-two 的寫入閘門決策會進 audit(`createShellWriteGate` 對非 tier-two 直接 return,而 `repl-engine.ts` 自己沒有任何稽核呼叫),所以提示符打的 `SELECT` 與一句真的改了資料的 `UPDATE` 同樣不留痕跡。較窄的方案(只記寫入與拒絕)被否決:一旦「沒有紀錄」有第二種解釋——走了哪個入口——它對任何一個入口都不再有意義。
73
+
74
+ - **形狀與 Elasticsearch shell 相同:送出前 `attempt`、回來或拋出後 `outcome`。** 理由沿用 `EsShellAuditSink` 寫在型別註解裡的那句:只在回程寫的一列描述不了一個沒有回程的語句——被中斷的長 `UPDATE`、`SIGTERM`、client 端逾時而伺服器仍在跑。被拒絕的語句只寫 `outcome`,因為它從未被嘗試。三個拒絕出口(權限、黑名單、寫入閘門)各自補上,其中權限那個出口先前連 tier-two 的決策列都沒有——無 WHERE 的 `DELETE` 在 `read-write` 下由權限檢查擋下,發生在寫入閘門之前。
75
+
76
+ - **稽核輪替的筆數上限從 1000 提高到 10000。** 在上面兩項之下,一次認真的互動 session 就能自己跑到 1000 列,然後輪替會丟掉寫入、留下讀取——剛好把這份檔案的價值反轉。位元組上限不變,它管的是磁碟。這個值先前有五份寫死的副本(schema、schema 自己的 `.default`、v1→v2 遷移、`config.ts`、`init-shared.ts`),已收成單一常數 `DEFAULT_AUDIT_ROTATION`。
77
+
78
+ ### Fixed
79
+
80
+ - **shell 的權限拒絕訊息不再宣稱要求的等級是使用者已經持有的那個。** `repl-engine.ts` 把 `required` 算成 `classification.type === 'UNKNOWN' ? 'admin' : 'read-write'`——一個猜測,不是 `checkPermission` 實際判定的等級。於是 `read-write` 使用者刪整張表讀到 `Permission denied. Required: read-write (current: read-write)`,實際需要的是 `data-admin`。`minimumPermissionFor` 的註解記載這一類錯誤已在其他呼叫端修過,shell 的 SQL 這處是漏掉的一站。
81
+
82
+ ### Note
83
+
84
+ `audit.strict: true` 現在也涵蓋 shell 裡的讀取:送出前那一列寫不出去就拒絕執行。這與 `dbcli query` 和 Elasticsearch shell 既有的行為一致,但 shell 的讀取路徑先前沒有這個失敗模式。不需要這個的人維持 `strict` 關閉即可——那是預設,寫失敗只留一行警告。
85
+
86
+ ## [4.0.0] - 2026-08-30 - Elasticsearch 的 shell 從來沒有問過 permission,同樣的形狀在 Redis 與 MongoDB 也成立,以及三個沒人比對的版本契約
87
+
88
+ **建議所有把 Elasticsearch 連線交給 AI agent 操作的使用者升級。** `dbcli shell` 連到 Elasticsearch 時,完全沒有檢查連線設定的 permission 等級就把請求送到叢集:`shell.ts` 在到達 SQL 與 Redis 共用的那道閘門之前就分支到 `es-shell.ts`。因此 `permission: query-only` 的連線可以送出 `POST /<index>/_delete_by_query` 清空索引、`DELETE /<index>` 刪掉索引、`PUT /<index>/_mapping` 改寫 schema —— 同樣這些請求走 `dbcli query` 一律會被拒絕。這條路徑也不寫任何 audit 紀錄,所以受影響的人事後無從查證發生過什麼。
89
+
90
+ 它可以腳本化:shell 用管線餵入的 stdin 驅動與互動輸入相同的迴圈,所以一個 agent 用單一非互動指令就能做到上述任何一項。
91
+
92
+ 影響範圍是所有 Elasticsearch 連線,`1.22`(ES shell 首次出現)起至 `3.0.0` 止。SQL、Redis、MongoDB 的 shell 不受影響 —— 它們走的是有閘門的那一條分支。沒有任何生產事故的紀錄,但這是從缺席推論出來的,而這條路徑本來就不寫 audit,受影響的操作者本來就無從發現。
93
+
94
+ ES shell 那條分支第九輪把「一個比對函式,有沒有把它要比的兩樣東西都正規化?」問到其他引擎上,找到 Redis 四個、MongoDB 兩個 CRITICAL。設計決策記在 ADR-0015。
95
+
96
+ Three numbers in this repository disagreed with reality and nothing checked any of them. The evidence pack and receipt formats still said `version: 1` after v3.0.0 broke both of them, so two mutually incompatible layouts shipped under one version number and an old artifact was reported as a tampered one. Five plugin manifests said `1.51.2` while the package said `3.0.0`. `SECURITY.md` promised fixes for `1.x`. Each is now stated once, derived from one source, and guarded by a check that fails on drift.
97
+
98
+ ### Fixed
99
+
100
+ - **BREAKING(對 `query-only` 與 `read-write` 的 Elasticsearch shell 使用者而言):ES shell 現在套用連線的 permission 等級。** 請求由 `dbcli query` 使用的同一個分類器判定 —— shell 交給它的是真正的 method 與 path,而 `query` 只生得出一個合成的 `_search`。無法證明是文件層級的操作一律落到需要 `admin` 的那一級:`DELETE /<index>`、`DELETE /_all`、`_delete_by_query`、`PUT /_mapping`、`PUT /_settings`、`POST /_aliases`、`POST /_reindex`。拒絕訊息會指出可行的等級,而且請求不會送出。先前能在 `query-only` 下跑這些請求的人,現在會被擋。
101
+
102
+ - **ES shell 的每個請求都寫入 audit,執行或被拒都寫。** side-effect tier 取自該請求的分類結果而非發起它的命令 —— 用命令的能力表來標記是一個已知缺陷,同一個破壞性操作曾因為經由不同命令而被記成三種不同的 tier。
103
+
104
+ - **分類器改讀伺服器實際路由的路徑。** 先前它拿到的是原始文字(含 query string),而黑名單拿到的是去掉 query string 的路徑。分類器裡每個子字串比對因此都會命中攻擊者控制的參數值,而 `filter_path` 是每個端點都接受、且吃任意字串的通用參數:`POST /<index>/_delete_by_query?filter_path=_count` 判成搜尋、`DELETE /<index>?filter_path=_bulk` 判成搜尋、`PUT /<index>/_mapping?filter_path=_bulk` 判成搜尋,三者都在 `query-only` 下實測執行成功。現在 query string 被丟棄、百分比編碼與 dot segment 在分類前解析,欄位也從 `apiPath` 改名為 `rawPath` 並在模組內正規化,呼叫端不可能再傳錯一個。
105
+
106
+ - **比對改為位置感知的路徑區段,不再是子字串。** `_search`、`_count`、`_bulk` 都是合法的文件 id,所以精確區段比對本身也不夠:`POST /<index>/_doc/_search` 是「索引+id」的寫入請求,最後一段卻是 `_search`。`_search` 與 `_count` 只在一段或兩段路徑的端點位置才算數,文件 id 一律不透明、永不參與比對。
107
+
108
+ - **Elasticsearch 的讀取判定維持白名單,並補上讓它不堪用的那幾個形狀。** 新增 `_cat/*`(不含 `_cat/aliases` 與 `_cat/tasks`)、`_cluster/health`、`GET`/`HEAD` 裸索引名稱。其餘一律落到需要 `admin` 的預設 —— 包含任何沒被列上的端點。這個方向是刻意的:白名單漏一項,使用者多付一個不必要的 admin 要求;拒絕集漏一項,使用者拿到一個繞過。記在 ADR-0014。
109
+
110
+ - **`_bulk` 的 body 無法辨識或無法解析時判為 `DROP`。** 先前判 `SELECT`,而 bulk 分支由路徑單獨選中,所以那是一個通用的降級管道。
111
+
112
+ - **路徑必須與 URL parser 產出的字串逐位元組相同,否則一律拒絕**,而拒絕訊息會給出正確的寫法。dbcli 原本有一套自己的「路由後路徑」概念,它在*近似* `fetch` 的行為——而近似的價值等於它最糟的那個缺口。`#` 就是一個:`fetch` 會丟棄第一個 `#` 之後的一切,所以 `POST /_reindex#/_count` 在 dbcli 眼中是兩段的 count、伺服器收到的卻是 `POST /_reindex`,那是任意索引對拷,因此同時也是黑名單繞過——把受保護的索引拷進可讀的索引再正常讀。tab、換行與 `\` 是同一形狀的另外三個缺口。現在改問傳輸層用的同一個 parser,adapter 也改用它組 URL,所以驗證過的字串就是送出的字串。
113
+
114
+ - **`source` query 參數一律拒絕。** Elasticsearch 接受 `source=<json>&source_content_type=...` 取代 request body,而這條路徑上每個 body 側檢查都讀 `req.body` —— 被保護的欄位名稱寫在偷渡的 body 裡時,那個為此存在的檢查完全看不到。參數以精確鍵名比對,`_source`、`_source_includes`、`_source_excludes` 不受影響。
115
+
116
+ - **黑名單欄位名稱在 query string 裡也會被拒絕**,因為 URI search 形式直接在參數裡指名欄位(`?q=password:*`、`?sort=password:asc`、`?docvalue_fields=`),而值會以請求自選的 key 回傳。比對改為看**點分元件**而非整串相等:`password.keyword` 是標準動態 mapping 對每個 `text` 欄位預設產生的 multi-field,而 `params._source.password` 是 Painless script 讀欄位的寫法——受保護的名稱可能落在點分路徑的任一端。回應遮罩套用同一條規則。
117
+
118
+ - **path 與 query 都從同一個 `URL` 解析,不再用 `String.split('?')`。** `split` 會在每一個 `?` 切開,而解構只取第二個元素,所以第二個 `?` 之後的一切都從這些檢查讀到的 query 中消失,adapter 卻拿到完整路徑:`?filter_path=x?&source=<body>` 因此對每一個為了找出偷渡 body 而存在的檢查隱形。逐位元組相等也隨之擴及整個 request target,不再只有路徑。
119
+
120
+ - **引號字串形式的 request body 一律拒絕。** JSON 字串字面值是合法的 body,卻能挾帶 NDJSON 通過每一個只走物件與陣列的檢查 —— 一個 bulk delete 曾因此從無害的路徑名稱抵達黑名單索引。
121
+
122
+ - **`_ingest` 與 `_tasks` 移出 shell 的 unscoped metadata 白名單**:pipeline 定義常內嵌憑證,詳細 task 列表會帶出執行中查詢的 request source。
123
+
124
+ - **server-side script 的攔截點移到傳輸層,shell 不再繞過它。** `assertNoElasticsearchScript` 原本只掛在 `ElasticsearchAdapter.execute()`,而 shell 走的是另一個執行入口 `request()`。因此 `query-only` 可以用 `POST /<index>/_search` 帶 `script_fields` 在叢集上執行任意 Painless,並把黑名單欄位以請求自選的 key 讀回來(欄位遮罩看的是 key 名稱,所以遮不到);`read-write` 則可用 `POST /<index>/_update/<id>` 的 `ctx._source` 直接改文件——同樣的操作走 `dbcli update` 一律被擋。檢查現在放在 `request()`,兩個入口共用。這也推翻了 ADR-0014 原本記下的一個「上限」:`doc['pass' + 'word']` 確實不是任何字面掃描擋得住的,但一律拒絕 `script` 鍵的控制本來就存在於這個倉庫裡。
125
+
126
+ - **shell 在退出前把在飛的請求與 audit 排乾。** `readline` 不會 await `'line'` handler,所以 EOF 的 `'close'` 會在請求還沒回來、audit 還沒寫出時就 `process.exit(0)`:`printf 'DELETE /orders\n\n' | dbcli shell` 會把請求送到叢集而一列紀錄都不留。權限與黑名單檢查都在送出前同步完成,所以檢查會通過、封包會出去,唯一沒發生的就是稽核——也就是原始漏洞報告裡「不留 audit」的那一半。**SQL shell 有同一個缺陷**(它有序列化那一半,缺排乾那一半),一併修正。
127
+
128
+ - **audit 在送出之前先記一筆 attempt,回應之後再記 outcome,而且每一列都說得出操作。** 只在回應後寫的紀錄描述不了一個沒有回來的請求:`_delete_by_query` 在 client 端逾時會 abort socket 而叢集把刪除做完,執行中被 SIGTERM 則連一列都沒有。同時每列補上 `<METHOD> <routed path>`——先前 `DELETE /orders`、`POST /orders/_update_by_query`、`PUT /orders/_mapping`、`POST /orders/_close` 產生的是四列一模一樣的紀錄。
129
+
130
+ - **只設定 `blacklist.columns`、沒設定 `blacklist.tables` 時,指不出索引的路徑不再被放行。** 整段黑名單檢查原本以 `tables` 是否為空決定要不要跳過,連帶跳過了「路徑指不出索引就拒絕」那一道守門員——而那道守門員才是擋住 `_sql`、`_mget`、`_search/scroll` 的東西。`POST /_sql` 配 `SELECT * FROM users` 會把受保護欄位的值原文放在 `rows` 陣列裡回傳,而欄位名只出現在 `columns[].name` 的值裡、不是 key,所以回應遮罩結構上救不回來。
131
+
132
+ - **query string 的欄位比對補上 Lucene 語法字元。** 切詞器只切 `[\s,:()"'[\]{}]`,於是 `?q=+password:hunter2` 通過而 `?q=password:hunter2` 被拒。`+`、`-`、`*`、`!`、`^`、`~`、`|`、`/`、`\` 一併視為分隔字元。
133
+
134
+ - **搜尋的 size 上限改讀路由後的路徑。** 原本用 `path.includes('_search')` 判斷,所以 `PUT /<index>/_doc/_search`(id 剛好叫 `_search` 的寫入)與 `?routing=_search` 都被當成搜尋,把一個使用者從未輸入的 `size` 欄位寫進文件。這是檔案裡最後一處對原始路徑做子字串比對的地方。
135
+
136
+ - **`dbcli audit tail` 與 `audit show` 的輸出逃脫控制字元。** ES shell 的 audit target 來自路徑,`%0A` 解碼後是真正的換行,因此一列紀錄能在表格輸出裡長成兩列、其中一列是偽造的。JSONL 檔本身不受影響。
137
+
138
+ - **連線失敗訊息裡的 URL 帳密會被遮蔽。** `nodes` 常寫成 `https://elastic:hunter2@host:9243`,而該字串會整串進入 audit 的 error 欄;`redactSensitive` 原本只認 `keyword=value` 形式,一個字元都吃不到 URL 的 userinfo。
139
+
140
+ - **server-side script 的比對改看形狀,不是兩個字面名稱。** 上一項把檢查點搬到傳輸層,但它認得的名字只有 `script` 與 `script_fields`,而 `scripted_metric` 聚合把四個 script 槽拼成 `init_script`、`map_script`、`combine_script`、`reduce_script`。`query-only` 因此仍可跑任意 Painless,並把黑名單欄位以 `aggregations.<name>.value` 這個請求自選的 key 讀回來。現在任何等於 `script`/`script_fields` 或以 `_script` 結尾的鍵都算——**一份名字清單擋不住一個會自己組名字的 API**。掃描另外加上深度上限,`'['.repeat(100000)` 這種合法 JSON 先前會讓它以 `RangeError` 收場。
141
+
142
+ - **黑名單欄位名含 `-`、`*`、`|`、`/` 時重新擋得住。** 上一項把 Lucene 運算子加進 query string 的分隔字元集,卻讓 `user-password` 被切成 `user` 與 `password`——兩者都不在黑名單裡。ES 的欄位名本來就允許這些字元,而 `?sort=user-password:asc` 會把值原樣放在 `hits.hits[].sort`,遮罩摸不到。現在保守與加寬兩套切法都跑、取聯集:多切一次只多幾個不命中的 term,少切一次會漏掉一個受保護欄位。
143
+
144
+ - **shell 的 block 在排入佇列的當下取快照。** 上一項把 `rl.on('line', async ...)` 改成 `queue.enqueue(submit)`,而 `submit` 是在任務跑起來時才讀共用的 `blockLines`;readline 會把管線送來的行在同一個 tick 全部同步發完,於是兩個命令被合併成一個 block,`parseEsRequest` 解析失敗,**兩個命令一個都沒送出**,audit 零列,exit code 仍是 0。互動模式下則是還沒打空行提交的內容被當成前一筆的 body 送進叢集。
145
+
146
+ - **`attempt` 那一列不再宣稱成功。** 它原本硬寫 `success: true`,於是「還沒送出」與「送出並成功」在紀錄裡長得一樣——包含被傳輸層擋下、從未離開行程的請求——而且讓每個操作的成功計數翻倍。現在一律 `false`,真相由 `outcome` 那一列說。
147
+
148
+ - **`audit tail --for-agent` 與 `--brief` 保留 statement 與 phase。** brief 原本只留 `ts`/`command`/`target`/`success`,所以「一列 audit 要說得出對誰做了什麼」只在 `audit show --no-brief` 修好了,agent 讀到的仍是兩筆一模一樣的紀錄。`audit tail` 的表格另外新增 `statement` 欄位。
149
+
150
+ - **Elasticsearch 的 audit statement 不再套用 SQL 字面值遮罩。** `redactSql` 會把數字換成 `0`,於是 `DELETE /orders/_doc/12345` 記成 `DELETE /orders/_doc/0`、`POST /logs-2026.08.30/_delete_by_query` 記成 `POST /logs-0.0/...`——操作對象被遮罩吃掉。ES 的 statement 是路徑不是語句,改用一般的敏感字串遮罩。
151
+
152
+ - **audit 表格的 cell 逃脫涵蓋非 C0 控制字元。** U+202E(RTL override)會讓該 cell 之後整段以右到左顯示,tier 與 success 欄可被視覺調換;U+2028/U+0085 在許多終端機裡同樣算換行。這是零權限的日誌注入——被 blacklist 拒絕的請求照樣寫紀錄,而 target 是攻擊者選的字串。
153
+
154
+ - **URL 帳密的遮蔽貪婪到最後一個 `@`。** 密碼裡含字面 `@` 時(`https://elastic:p@ssw0rd@host`)先前只遮到第一個,尾巴留在紀錄裡。
155
+
156
+ - **搜尋的 size 上限與分類器讀同一個路徑函式。** 先前 cap 不看 method 且讀解碼後的路徑,分類器看 method 且讀原始路徑,於是 `PUT /orders/_search` 與 `POST /orders/%5Fsearch` 上兩者給出不同答案。都不可利用,但「同一個請求、兩個函式、兩種答案」是前幾輪 CRITICAL 的形狀。
157
+
158
+ - **`wrapper` query 一律拒絕。** 它帶的是 base64 編碼的 query,伺服器解碼後執行,而所有 body 側檢查都只走物件的鍵、碰不到字串內部。裡面可以放 `function_score.script_score`,於是黑名單欄位的數值原文會以每筆 hit 的 `_score` 回來——那不是受保護的鍵名,回應遮罩不會動它;黑名單詞比對也看不到 base64 裡的欄位名。這與已經拒絕的字串形式 body 和 `?source=` 是同一個原則:**dbcli 檢查不了的編碼 body 不放行**,解碼一種編碼只會邀請下一種。
159
+
160
+ - **含 `.` 的黑名單欄位名重新生效。** `namesProtectedField` 先比整串相等,再把 term 拆成單一元件比對——而拆出來的元件永遠不含 `.`,所以 `blacklist.columns` 寫成 `user.password` 對整個檢查毫無作用。同一個函式也是回應遮罩的判斷,於是請求端放行 `?docvalue_fields=user.password.keyword`、回應端原樣返回 `_source.user.password`。ES 的 object field 一律以點分名稱呈現,那是最自然的設定寫法。比對改為**連續的點分元件區段**,遮罩則帶著走過的鍵路徑,才比對得到巢狀呈現的回應。扁平欄位名的行為完全不變——這正是這個缺陷七輪沒被發現的原因,每個測試用的都是扁平名稱。
161
+
162
+ - **`_script` 後綴的比對縮回 `scripted_metric` 底下。** 無條件的後綴規則會讓 `deploy_script`、`build_script` 這種一般欄位名在 `query-only` 的唯讀查詢上被拒絕,訊息還說它「executes script code on the cluster」——`term`/`match`/`range`/`sort`/`exists` 都把欄位名放在鍵的位置。`scripted_metric` 是唯一內層沒有字面 `script` 鍵的聚合,其餘 script 載體都已被第一條規則接住。
163
+
164
+ - **編碼後才出現的 `..` 一律拒絕。** `%2F` 原封不動通過位元組同一性檢查,但 `normalizeEsPath` 會先解碼再讓 `..` 刪掉前一段,跨過一個伺服器根本不存在的段界。`GET /secrets%2F..%2Fpublic/_search` 因此讓 `secrets` 從路徑區段檢查、index 抽取與 audit 三處同時消失。ES 是否解析得出那個 index expression 未經驗證——那正是拒絕而非正規化的理由。
165
+
166
+ - **`exit` 之後排在佇列裡的命令不再執行。** `'line'` handler 在同一個 tick 把管線的所有行 enqueue 完,所以 `rl.close()` 執行時後面的 block 早已在鏈上,而 `'close'` 的排乾語意是「全部跑完」——`printf 'exit\n\nDELETE /orders\n\n'` 會把索引刪掉。兩個 shell 都補上關閉旗標。
167
+
168
+ - **只有空白的行屬於 block 的內容,不是它的結尾。** 提交的判斷原本是 `trim()` 後為空,於是編輯器留下的空白會把 block 截斷、前半段以一個**沒有 body** 的請求送出——而 `POST /_update_by_query` 沒有 body 是合法的、作用範圍是整個索引,且 audit 寫下的字串與使用者本來要送的那筆逐字相同。改成只在真正的空行提交:反方向的代價是分隔行帶空白時命令會黏成一塊而解析失敗,但那個失敗可見且什麼都不會送出。
169
+
170
+ - **shell 的錯誤訊息逃脫控制字元。** 訊息內嵌使用者寫的路徑,而 `ESC[2K ESC[1G` 會清掉整行並把游標移回行首,用後續字元蓋掉「Refused」,讓操作者看到一則自己寫的假成功訊息。audit 檔與 `audit tail` 早已處理這件事,唯獨 shell 自己的 stderr 沒有;逃脫邏輯抽成共用的 `escapeControlCharacters`。
171
+
172
+ - **BREAKING(對用管線或腳本驅動 ES shell 的呼叫端而言):ES shell 的退出碼反映失敗。** 先前一律 `exit(0)`,所以 `dbcli shell < script.txt` 的呼叫端分不出「全部成功」與「一條都沒跑」——權限拒絕、blacklist 拒絕、strict-audit 拒絕全部只印一行紅字。**這會改變既有腳本的行為**:一個 session 內只要有任何一個請求失敗,`dbcli shell` 就以 `1` 結束,先前依賴它一律成功的 CI 步驟會開始紅。記在 ADR-0014 Decision 10。
173
+
174
+ - **不含欄位名的 query 參數不再進入黑名單切詞器。** `?routing=abc-name-1` 在黑名單欄位叫 `name` 時被誤擋,而 `routing`/`scroll`/`preference`/`filter_path` 的值沒有任何欄位名語意。
175
+
176
+ - **i18n 插值不再讓值改寫訊息。** `MessageLoader.interpolate` 用的是 `String.replace(regex, value)`,而替換字串會展開 `$&`、`$'`、`` $` `` 與 `$1`。這條分支第一次把**操作者可控**的字串餵進去(ES shell 的路徑、命中的 index expression、被拒的欄位名),於是拒絕訊息以及由它組成的 audit `error` 欄位可以被被拒的那個人部分改寫:`GET /sec$&rets/_search` 記下來的索引名是 `sec{index}rets`,`$'` 則會把整句複製一份接在後面。改成單次掃描 `\{name\}` 加替換函式,順帶修掉「依序替換讓值裡的 `{other}` 被二次替換」。所有 `t_vars` 呼叫端一併受惠。
177
+
178
+ - **ES shell 的訊息改走 i18n。** `src/commands/es-shell.ts` 的 i18n 呼叫數是 0,而同一層的 `src/commands/shell.ts` 是 14——CONTRIBUTING.md 明文寫「All user-facing messages must be translatable」,所以那是違規不是偏好。19 則訊息移進 `shell.es.*`,`blacklist table add` 這次新增的萬用字元拒絕訊息一併移進 `blacklist.refuse_wildcard`。`BlacklistRejection: ` 這個前綴刻意留在程式碼裡不翻譯:recovery 路徑與數個測試比對的是它,翻譯過的前綴是壞掉的比對器,不是翻譯過的訊息。
179
+
180
+ - **`es-shell.ts` 拆成兩個檔案。** 894 行、`runEsRequest` 363 行,對照 CONTRIBUTING.md 的 800 與 50。成因寫在 ADR-0014 裡:九輪,每一輪都往同一個函式再塞一個檢查。切線沿著檢查本來就有的分界——`es-shell-guards.ts` 放「關於這個請求」的純函式(伺服器會路由到哪、指名了哪些索引與欄位、回應要遮掉什麼),不讀設定、不開連線、不寫 audit;session 檔留下讀取迴圈、tier gate 的呼叫、audit 接線與退出碼。行為沒有改變,5873 個測試全過。
181
+
182
+ - **`audit.strict` 的強制點改在「效果發生前」的稽核寫入。** 先前只有 ES shell 讀這個鍵,但它放在全域 `audit` 區塊、文件也寫得像全域開關:設了 `strict: true` 再把 audit 目錄設成不可寫,`dbcli delete` 照樣執行。現在 ES shell 送出請求前那一列與 SQL 寫入閘門的決定紀錄都走 `writeAuditEntryBeforeEffect`。事後才寫的紀錄不在範圍內——那時拒絕擋不回任何東西,只會把一次已完成的操作回報成失敗。同時:`enabled: false` 配 `strict: true` 在讀設定時就失敗(那組合的語意是「不記錄、也不擋」);`dbcli audit health` 印得出 strict 的狀態;沒有 sink 或 sink 回 `null`(舊版 `writeAuditEntry` 的失敗形狀)在 strict 下都算失敗。
183
+
184
+ - **`recover` 與 `inspect` 的 audit 摘要跟上 statement 與 phase。** 第六輪修好了 `audit tail` 的 brief,但沒動 `briefifyForRecent`,於是這兩條路徑上一次成功的請求仍呈現為兩列只差 `success` 的紀錄。`topQueriedTable` 另外不再把 attempt 列重複計數,也不再把 `/_cat/indices` 這種路由路徑當成「最常查詢的資料表」。
185
+
186
+ - **黑名單條目本身可以是萬用字元、逗號清單、`_all`、帶前後空白。** `indexExpressionReaches` 原本只展開**請求端**(逗號、萬用字元、date math、CCS、百分號編碼),把黑名單條目當純字面字串比對——於是 `blacklist.tables: ["secrets*"]` 對 Elasticsearch 完全無效,而 `["*"]` 這個讀起來像「全面封鎖」的寫法是零保護。加重因素是:**同一個 `blacklist.tables` 陣列在 Redis 連線上就是以 glob 執行的**,使用者文件也明文教 `dbcli blacklist table add 'secrets:*'`。依文件寫下的設定,在 Redis 擋、在 ES 靜默放行。`dbcli query --index` 走的 `checkIndexBlacklist` 與欄位遮罩的 `filterColumnsForIndexExpression` 有同樣的不對稱,一併修正。
187
+
188
+ - **`dbcli blacklist table add` 接受真實的 Elasticsearch index 名。** 驗證規則原本是 SQL 識別字的形狀(`^[a-zA-Z_][a-zA-Z0-9_]*$`),拒絕 `my-index`、`logs-2026.08.30`、`.kibana`,等於 ES 使用者只能手編設定檔——而手編正是最容易把條目寫成 glob 的路徑,直接餵養上面那個缺陷。
189
+
190
+ - **拼錯或用 ES 詞彙寫的黑名單不再被靜默忽略。** zod 預設剝掉未知鍵,所以 `blacklist.indices`、`blacklist.fields`,以及寫在**連線層級**的 `blacklist`(它只有頂層一份),解析後都是空黑名單且沒有任何警告——使用者看著設定檔以為有保護。這幾種形狀現在是解析錯誤。沒有改成全域 `.strict()`:那會拒絕無害的額外鍵。
191
+
192
+ - **`blacklist add/remove` 不再把 v2 多連線設定壓成 v1。** 它讀設定走的是 v1 路徑(對 v2 檔案回傳「選中那條連線」的扁平化結果),寫回時以 v1 schema 整包覆寫。加一條黑名單因此會讓 `connections`、`default`、`envFile`、`environment` 全部消失,**而預設 permission 變成當時選中那條連線的值**——ES shell 的 tier gate 讀的正是它。整個過程走 `writeConfigWithIntegrity`,完整性紀錄同步更新,事後沒有 tamper 訊號。
193
+
194
+ - **根層 `--config` 對 blacklist 指令生效。** 每個子指令自己宣告了一個**帶預設值**的 `--config`,所以 commander 永遠不會回落到根層那個:`dbcli --config /path blacklist table add x` 會改到 `.dbcli` 而不是 `/path`,並且回報成功。
195
+
196
+ - **百分號編碼的路徑段不再改變分級。** `new URL().pathname` 不解百分號編碼,而 Elasticsearch 對路徑參數會解,於是 `GET /%2A` 是 `query-only` 的讀取而 `GET /*` 需要 `admin`——同一個請求兩個 tier,正是 `isBareIndexSegment` 的註解明文禁止的情形。路徑段現在在 `routedSegments` 解碼一次,且**不重新切分**:解碼後的 `/` 留在原段內,重新切分會複製第七輪修掉的 `%2F..%2F` 缺陷。
197
+
198
+ - **無 index 的 metadata 白名單細到子資源。** 它原本只比對第一段,於是整個 `_cluster` 與 `_cat` 前綴放行——而 `_cluster/state` 回傳 `metadata.ingest.pipeline[]`(`_ingest` 被移出白名單的理由就是它常內嵌憑證)、`metadata.stored_scripts`、以及黑名單索引的完整 mapping;`_cat/tasks` 回傳執行中查詢的 source(`_tasks` 被移出的理由)。`_nodes/stats`、`_nodes/settings`、`_nodes/hot_threads` 同樣扣住。
199
+
200
+ - **search template 端點一律拒絕。** template 的 `source` 是一段在叢集上渲染成完整 search body 的字串,stored template 的內容根本不在請求裡——所以指向黑名單索引的 terms lookup 對每一個 body 側檢查都是隱形的。與 `wrapper` 同一個原則。
201
+
202
+ - **黑名單條目的前後空白不再讓它變成死設定。** ES 的 index 名與欄位名都不能帶空白,所以 `[" secrets "]` 保證無效,而先前沒有任何提示。
203
+
204
+ - **audit 的 `target` 不再能由 SQL 註解或字串字面值指定。** 它原本走一條不剝註解、不剝字串的 regex,只取第一個 `FROM|INTO|UPDATE` 之後的識別字,所以 `/* FROM audit_decoy */ DELETE FROM users` 會以 `audit_decoy` 入帳。`target` 正是稽核者事後篩選用的主要欄位——找不到的紀錄與不存在的紀錄,對追查是同一件事。改成掃描:剝掉註解(含 PostgreSQL 的巢狀區塊註解)與字串字面值,保留引號識別字,並只在括號深度 0 比對關鍵字。順帶修好兩個日常誤記:`DELETE FROM public.users` 記成 `public`(schema 名)、`SELECT EXTRACT(MONTH FROM created_at) FROM salaries` 記成 `created_at`(欄位名)。
205
+
206
+ - **經 `query` 執行的 DML 記成 `db-write` 而非 `readonly`。** `writeAuditEntry` 在沒拿到 `sideEffectTier` 時取命令的能力等級,而 `query` 的能力是 `readonly`。於是 `DELETE`/`UPDATE`/`INSERT`/`CREATE TABLE AS` 全部以 `readonly` 入帳,而被寫入閘門**拒絕**的 `DROP`/`TRUNCATE` 由 `recordGateDecision` 帶著 `db-write`——以 tier 篩選破壞性操作會找到被擋下的那些、漏掉真正發生的那些。「讀」的判定沿用既有的 tier 語意(在 `query-only` 下被允許的就是讀),而不是另外維護一份唯讀型別清單。
207
+
208
+ - **`blacklist table add` 對 SQL 與 MongoDB 連線拒絕萬用字元條目。** 上一版為了 Elasticsearch 與 Redis 的名稱放寬了字元集,但沒有問「這個寫法對這個引擎有沒有意義」:那兩個引擎的黑名單比對是字面相等,`secret*` 這種條目永遠不會命中,而 CLI 回報成功。錯誤訊息會說出原因,不只是「名稱非法」。
209
+
210
+ - **BREAKING(對 `blacklist.tables` 設了 glob 的 Redis 使用者而言):`insert`/`update`/`delete` 現在也套 glob 黑名單。** 這三條只經過 `BlacklistValidator.checkTableBlacklist`(字面相等,不懂 glob),而 `RedisAdapter` 的三個方法一次 `checkKeyArgs` 都沒有。於是 `dbcli blacklist table add 'secrets:*'` 之後 `dbcli delete secrets:api_key` 照刪——而 `'secrets:*'` 正是使用者文件教的寫法,只有寫成完整字面 key 才擋得住。檢查改掛在 adapter 邊界。先前能刪改這些 key 的腳本現在會被擋。
211
+
212
+ - **`redis.mask` 對 `query`/`list`/`schema`/`insert`/`update`/`delete` 完全無效。** mask rules 是 `createRedisAdapter` 的第三個選擇性參數,八個呼叫端裡六個沒傳。文件明寫 `dbcli query "GET secret:api_key"` 回 `{"value":"[REDACTED]"}`,實際回明文。`export` 與 `shell` 有傳,所以任何人想驗證這個功能時它都是好的。factory 改為接整個 config。
213
+
214
+ - **指令表與權限白名單的落差不再是黑名單繞過。** `checkKeyArgs` 拿不到 command spec 時 fail-open,而 32 個被權限放行的指令沒有 spec:`LPOP secrets:list` 在 `read-write` 下把黑名單 key 的值取出來兼銷毀,`XRANGE secrets:stream - +` 在 `query-only` 下讀得到。改為 fail-closed(只在有設黑名單時生效),補齊 32 個 spec,並加契約測試釘住兩張表的關係。`RedisCommandSpec.permissionTier` 直接移除:它是一份沒人強制、已在五個指令上分歧的副本。
215
+
216
+ - **`SCAN` 不再列舉得出黑名單 key 名。** `SCAN 0 MATCH secrets:*` 只要 `query-only` 且完全不被檢查,而 `KEYS secrets:*` 要 `admin` 且被擋——低權限那條路才是通的。MATCH 現在會被找出來(不限位置、大小寫不敏感)並比對重疊;而只修 MATCH 擋不住裸 `SCAN 0`,所以 `SCAN` 與 `KEYS` 的回應也會把受保護的 key 名濾掉。cursor 不動。
217
+
218
+ - **MongoDB 的整份文件轉移也擋得住。** 底下那則「`blacklist.columns` 擋得住換名了」只擋「請求裡指名了受保護欄位」,而 `[{"$project":{"all":"$$ROOT"}}]` 一個欄位名都沒提就把整份文件放到自選的鍵底下回傳,遮罩比對的是完整路徑 `all.password`、對不上錨定在 `password` 的規則。`{"$objectToArray":"$$ROOT"}` 更徹底:文件變成 `[{k:"password",v:"p1"}]`,受保護的名字成了**值**,任何看鍵名的遮罩結構上都追不到。`$replaceRoot`/`$replaceWith` 把子樹提到頂層,同樣讓 `user.password` 這種規則失去指涉。這些現在一律視為「一次指名了所有受保護欄位」;`$getField` 的 `field` 若是運算式而非字串常數也一併拒絕——dbcli 判斷不出它會解析成什麼名字,就不轉發。`$$ROOT` 原本**有**被處理,但是當成「要剝掉的前綴」(為了讓 `$$ROOT.password` 命中),單獨出現時被讀成一個叫 `ROOT` 的路徑。
219
+
220
+ - **`SCAN` 的 `MATCH` 改為檢查每一個出現位置。** Redis 解析選項是後者覆寫前者,所以 `SCAN 0 MATCH benign:* MATCH secrets:*` 真正送出去的是 `secrets:*`,而只讀第一個等於檢查了一個從未送出的 pattern。回應過濾本來就會把 key 濾掉所以沒有洩漏,但「指名就拒絕」這條規則在這裡沒有成立。
221
+
222
+ - **`filterReturnedKeyNames` 對認不得的回應形狀改為 fail-closed。** 原本原樣轉發,與同一批修補在 `checkKeyArgs` 選的預設相反。非字串的 key 同樣丟棄——無法與 glob 比對的東西,回答不了「這個受不受保護」。
223
+
224
+ - **`MongoDBAdapter.insert` 補上兩個攔截點。** 它既沒有 `assertNoMongoServerSideScript` 也沒有欄位檢查,而 #47 的註解宣稱所有路徑一致受檢。實際蓋住它的是 `insert.ts` 的 `checkColumnBlacklistOnWrite`——掛在呼叫端的控制,正是 ADR-0015 Decision 1 要移除的安排。
225
+
226
+ - **`$lookup` 帶進來的 collection 名改為大小寫不敏感比對。** 請求側是精確比對、遮罩側 (`findCaseInsensitive`) 不敏感,於是同一份設定在遮罩生效、在請求側拒絕不生效。
227
+
228
+ - **MongoDB 的 `blacklist.columns` 擋得住換名了。** 遮罩只看回傳文件的鍵名,而 aggregation 自己決定那些鍵名:`$project:{"leak":"$password"}`、`$addFields`、`$set` 都把值搬到別的鍵下原文回傳,`query-only` 即可。`$group:{"_id":"$password"}` 更是保證出口——`_id` 為了保住文件參照而被無條件豁免。回應側追不完,所以改成**請求側拒絕**:請求裡指名受保護欄位就拒絕,與 Elasticsearch 的 `namesProtectedField` 同一個形狀,包含同樣的過度拒絕(值剛好等於受保護欄位名也會被拒)。檢查掛在 `assertNoMongoServerSideScript` 旁邊,那是所有 MongoDB 路徑本來就共用的攔截點。
229
+
230
+ ### Added
231
+
232
+ - **`audit.strict` 設定(預設 `false`)。** 開啟時,送出前那一列 audit 寫不出去就拒絕執行請求。audit 一直是 best-effort——磁碟滿、目錄不可寫、lock budget 耗盡(可被刻意耗盡)時操作照樣執行、零紀錄,只有一行 stderr 警告,而管線模式通常看不到。對多數指令這是對的取捨;但 ES shell 這條路徑上 audit 就是控制本身,而先前連相反的取捨都無法表達。只管送出前那一列:`outcome` 寫不出去時請求已經在叢集上了。
233
+
234
+ - **`bun run manifest:check` (and `manifest:sync`) makes manifest drift fail the build.** It checks every manifest's version against `package.json`, the plugin name, that declared skill directories and entry files exist, that the root and portable copies agree, that the marketplace listing names the plugin, and that `SECURITY.md`'s supported major matches the package. Blocking in CI's drift job and in `release:check`. `tests/unit/scripts/plugin-manifest-contract.test.ts` breaks one thing at a time in a scratch tree and asserts the script goes red, because a guard only ever run against a correct tree is how five manifests drifted through a green build.
235
+
236
+ - **Frozen legacy artifact fixtures under `tests/fixtures/evidence-legacy/`.** Six files produced by the code that actually shipped — v2.1.0's builder and v3.0.0's — never regenerated. A test that builds its own "legacy" input proves the builder agrees with itself, which is precisely the check that would have caught none of this.
237
+
238
+ - **CI runs a dependency audit and installs from a frozen lockfile.** `bun audit` ran only inside `release:check`, by hand, shortly before a release; it is now its own blocking job on every push. Every job installs with `--frozen-lockfile`, and the workflow declares `permissions: contents: read`.
239
+
240
+ ### Changed
241
+
242
+ - **BREAKING:`@carllee1983/dbcli/core` 不再匯出 `AdapterFactory`。** 它回傳的 adapter 的 `request()` 是 public,所以任何函式庫使用者都能拿到一條不經 permission、不經 blacklist、不寫 audit 的路徑。`QueryExecutor` 與 `DataExecutor` 保留——它們自己帶著閘門。CLI 使用者不受影響。
243
+
244
+ - **`_update_by_query` 從 `read-write` 收緊為 `admin`**:它是獨立的區段,精確比對之下落到破壞性預設,而它確實會改寫索引裡的每一份文件。
245
+
246
+ - **BREAKING (artifact format): evidence packs and receipts are `version: 2`, and the format version is no longer tied to the package version.** v3.0.0 changed the pack digest input, the id derivation, the `coverage` field and the receipt `observation` while leaving both constants at `1` — ADR-0012 decided to amend the schema in place, which was right about the repairs and wrong about the number. A reader now classifies an artifact by version *and* structure before computing anything: `v1-coverage` (2.1.0 and earlier), `v1-untagged-v3` (3.0.0), current, or unsupported. Both legacy formats stay readable and integrity-checkable through frozen reimplementations of their own digests, and neither is ever `trust: "current-valid"`. There is no migration and cannot be one — a pack's id derives from its digest, so rewriting an old pack would mint a new artifact wearing an old one's provenance, and a v2.1.0 receipt's hashed `observation` can only be "recovered" by inverting it, which is a guess and not a record. Recorded in `docs/adr/0013-evidence-artifact-format-versions-are-independent-of-the-package-version.md`, amending ADR-0012.
247
+
248
+ - **`evidence validate --format json` distinguishes three answers where it used to give one.** `status` is `current-valid`, `current-references-expired`, `recognized-legacy`, or `unsupported`, with `trust` stating plainly whether the pack may be relied on; `recognized-legacy` also carries `legacyFormat`, `producedBy`, and that format's own `integrity`. An unknown version fails closed. Previously every one of these arrived as `evidence pack digest mismatch`, which tells the holder of a two-week-old file that someone tampered with it.
249
+
250
+ - **`SECURITY.md` documents the major line that actually exists.** It listed `1.x` as supported against a `3.0.0` package — a security document making a false promise about which releases get fixes. `3.x` is supported; `2.x` and `1.x` are explicitly not, with no backport commitment invented to soften it.
251
+
252
+ - **Plugin and extension manifests carry the package version.** `.claude-plugin`, `.codex-plugin`, `.cursor-plugin`, `gemini-extension.json` and the portable `plugins/dbcli-agent` copy all read `1.51.2` — two majors behind what a user installing from a marketplace would receive.
253
+
8
254
  ## [3.0.0] - 2026-08-16 - Evidence that could not reproduce itself, and a hash that hid nothing
9
255
 
10
256
  The evidence subsystem shipped in v1.53.0 and, until this week, nobody had composed a pack outside its own tests. The first real use — a `verify safe-backfill --after-write` against a live PostgreSQL — came back `not_verified` on data that was correct, and the audit that followed found three more defects of the same kind: an evidence pack whose digest covered a random UUID, so the same claims never produced the same pack twice; a receipt "fingerprint" that was an unsalted SHA-256 over eight possible values; and a blacklist comparison with no identifier boundaries, so a protected column named `id` refused any claim containing the word "identifier". Fixing them changes both published formats, which is what makes this a major release: **packs written by 2.x will fail validation under 3.0.0, and `observation.fingerprint` no longer exists.** The reversal that authorized the repairs — known defects get fixed whether or not anyone is using the code — is recorded in `docs/adr/0012-known-defects-get-fixed-whether-or-not-anyone-is-using-the-code.md`, superseding ADR 0011.
@@ -215,16 +461,23 @@ Automation that performs unqualified full-table writes stops working. Three shap
215
461
  ### Changed
216
462
 
217
463
  - **The CLI loads what a command actually needs.** Subcommands register lazily, SQL drivers load at connection time rather than at import, and `node-sql-parser` is both deferred and externalized from the bundle — measured at roughly 8ms off startup for the lazy registration alone.
464
+
218
465
  - **Full-schema scans cost less.** Per-table queries are merged, row estimates replace `COUNT(*)`, and remaining work runs with bounded parallelism. The query path no longer loads the layered schema in full — it fetches the single table it needs.
466
+
219
467
  - **Repeated lookups are cached within a process.** Config binding files are read and validated once per process, and the skill update check keeps a TTL cache instead of re-checking on every invocation. Redis and Elasticsearch list operations were narrowed to what the caller asked for.
468
+
220
469
  - **Identifier quoting and error classification each have one implementation.** Quote/encode helpers were consolidated into a shared utility, and driver errors are now classified by error code first rather than by matching message text.
470
+
221
471
  - **Server-side script protection lives in the adapter layer**, so every caller is covered by the same guard rather than each command re-implementing it.
222
472
 
223
473
  ### Fixed
224
474
 
225
475
  - **`bun run build` was non-deterministic.** Consecutive builds of identical sources alternated between two `dist/cli-runtime.mjs` outputs about 690KB apart, depending on whether the bundler pulled in 48 `@inquirer/*` modules. `@inquirer/prompts` is now external, which also removes a silent degradation path where the prompt implementation quietly changed between builds. `bun run build:determinism` checks this in CI.
476
+
226
477
  - **One CLI query writes exactly one audit entry.** Some paths recorded the same query more than once.
478
+
227
479
  - **Windows CI is green again.** Path separator assumptions, CRLF handling in test fixtures, and CRLF frontmatter stripping in skill sources were all Unix-only.
480
+
228
481
  - **The startup benchmark measures the noise floor rather than the median**, which is what actually distinguishes a regression from scheduler jitter, and a guide test no longer flakes on a random UUID colliding with `'5432'`.
229
482
 
230
483
  ## [1.53.0] - 2026-08-09 - Offline evidence, semantic contracts, and impact assessment
@@ -232,13 +485,17 @@ Automation that performs unqualified full-table writes stops working. Three shap
232
485
  ### Added
233
486
 
234
487
  - **Offline evidence packs.** `dbcli evidence compose`, `validate`, and `render` create, verify, and render workspace-contained evidence packs from safe claim text plus existing verification artifacts, audit entries, and optional assert receipts. Packs omit SQL, rows, targets, credentials, audit metadata, and verification summaries; source retention loss remains visible without preventing historical rendering.
488
+
235
489
  - **Evidence receipts for post-write assertions.** `assert --evidence-receipt <path>` atomically records safe provenance only after the verdict, audit attempt, and optional verification artifact are authoritative. Receipts are workspace-contained, contain no SQL or returned data, cannot be used as execution approval, and may be composed into an evidence pack.
490
+
236
491
  - **Reviewable semantic contracts.** `dbcli contract validate|context|search|drift` governs optional `dbcli.contracts.json` evidence expectations for canonical semantic terms. The commands are offline and read-only; only valid approved contracts enter ordinary agent context.
492
+
237
493
  - **Offline impact assessment.** `dbcli impact assess` creates a declared-coverage report for a design change against exactly one local schema-cache or ORM baseline, optionally incorporating reviewed data-access metadata and redaction-first proxy workload evidence. It never connects, executes SQL, or claims complete coverage.
238
494
 
239
495
  ### Changed
240
496
 
241
497
  - **Shared execution and adapter boundaries.** Query execution now uses an injectable command runtime, SQL adapters share readiness and driver-error handling, and doctor accepts a non-SQL collector runtime. These internal changes keep the CLI behavior stable while making offline evidence and impact workflows testable.
498
+
242
499
  - **Documentation and agent skills cover the new surfaces.** English and Traditional Chinese Markdown and HTML guides, installed skill copies, and reference material document evidence packs, assert receipts, semantic contracts, and impact assessment.
243
500
 
244
501
  ### Fixed
@@ -250,10 +507,15 @@ Automation that performs unqualified full-table writes stops working. Three shap
250
507
  ### Fixed
251
508
 
252
509
  - **The skill documented three flags that do not exist.** `q --use` (the global `dbcli --use <name> q` form is the real one), `q --collection` (a MongoDB snippet's `target:` is the only collection source), and `audit tail --recovery-ref` (it lives on `audit show`). The same wrong `audit tail --recovery-ref` is corrected in `docs/user` (en/zh, Markdown and HTML).
510
+
253
511
  - **The `design` artifact example failed its own validator.** It is replaced with one that validates clean, and the naming rules it has to satisfy are now documented: lowercase kebab-case for design names versus SQL identifiers for tables and columns, endpoints referencing a model rather than a table, strict objects, descriptions that must not contain SQL keywords, and the size limits. Structural violations report `INVALID_ARTIFACT`, a code the severity table did not list.
512
+
254
513
  - **Stale lists corrected against the CLI.** `--recovery` covers `lint` and `diff`; verification subject kinds include `table` (what `verify constraint` writes); the Redis permission table includes `XLEN` / `XREAD` / `XRANGE` / `XREVRANGE` / `XADD` / `XDEL` / `LREM`; `audit show` lists `--brief` and `--for-agent`; the snippet guard emits `LIMIT 1001`, fetching one extra row to detect truncation. Elasticsearch supports `q`, and the 10 000 bound belongs to `query` — `export --no-limit` streams via the scroll API. `schema --help` claimed `--sample-size` defaults to 50; it is 100.
514
+
255
515
  - **`design` was unreachable from the skill entry point.** `SKILL.md` did not mention it at all, so an agent asked to design or review a schema would hand-write DDL and bypass the review-only `propose` contract. It now has a command row, both workflows, and a guardrail that a proposed plan is never executed.
516
+
256
517
  - **Six drifts between the English and Traditional Chinese skills.** The most serious dropped the permission-tier semantics (multi-statement SQL rejected below `admin`, snippets free of write and DDL keywords, `$out` / `$merge` requiring `data-admin`). The MongoDB connection guidance also disagreed between languages — field-by-field is the recommendation, full URI the escape hatch — and the zh-TW side was missing the env-refs MongoDB exception, the `uri`-wins-silently gotcha, `--slow-ms` on `query` and `q`, and the `proxy analyze` action guidance.
518
+
257
519
  - **Cursor and Windsurf installs were the Claude skill verbatim.** Windsurf does not parse frontmatter, so roughly 900 characters of `description:` were read as rule text; Cursor reads `description` / `globs` / `alwaysApply` and received none of them. Both platforms keep `reference.md` outside the primary file's directory, so every mention of it resolved to nothing. `dbcli skill --install` now shapes the file per platform — Cursor as an Agent Requested rule, Windsurf with the frontmatter stripped and the description kept as prose — and repoints the reference path. Recognizing an existing dbcli install no longer depends on the frontmatter, so a Windsurf reinstall stops backing up dbcli's own file. See ADR 0006.
258
520
 
259
521
  ### Added
@@ -263,6 +525,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
263
525
  ### Changed
264
526
 
265
527
  - **The bilingual parity gate compares content, not just shape.** It checked heading levels, fence counts, table rows, and a curated token list for mere presence — every drift above kept that structure intact. It now compares per-section counts of every code token and list item; run against 1.52.0 it reports 48 problems it used to pass. Its success message no longer reads as "the docs are aligned" when it only checked the skeleton.
528
+
266
529
  - **The skill entry point carries less that an agent cannot act on.** The always-loaded `description` drops from 990 to 626 characters with every trigger branch intact, release markers such as `(v1.23)` and an internal ticket id are gone (an agent has exactly one installed version), and the `proxy` row's flag wall becomes an anchor.
267
530
 
268
531
  ## [1.52.0] - 2026-08-07 - Offline database design assistant and slow-query hints
@@ -270,8 +533,11 @@ Automation that performs unqualified full-table writes stops working. Three shap
270
533
  ### Added
271
534
 
272
535
  - **Offline database design assistant.** `dbcli design init|validate|render|diff|propose` authors and reviews a version-controlled `dbcli.design.json` beside the code. Every subcommand is offline: none opens a connection, executes DDL, or calls a provider, and `design init` is the only writer — to the explicit `--output` path, refusing to overwrite. `validate` is fail-closed, so `render`, `diff`, and `propose` refuse to work while `error` findings remain; `render` emits `json`, `markdown`, or `mermaid`.
536
+
273
537
  - **Design drift comparison and review-only proposals.** `design diff` and `design propose` compare the artifact against the local schema cache (`--against-cache`) or local ORM definitions (`--against-orm`, supporting Prisma, DDL, Drizzle, TypeORM, Sequelize, and JSON), with `--orm-format` and `--ignore` for control. `propose` turns drift into a plan a human reviews and never applies a write: each entry carries a `dry-run` or `migration-review` safety level plus `preflight`, `rollback`, and `verification` steps.
538
+
274
539
  - **Two further design review rules.** `REVERSE_RELATIONSHIP` (error) fires when the same endpoints are declared again in the opposite direction, and `PREFIX_REDUNDANT_INDEX` (warn) fires when a non-unique index is a leading-column prefix of a longer index.
540
+
275
541
  - **Passive slow-query hint on `query` and `q`.** At or above `--slow-ms` (default 1000, `0` disables), a finished query gains a Performance hint footer and `metadata.performanceAdvisory`. It reuses the execution time already measured — no `EXPLAIN`, no schema read, no second request. The recommendation is engine-aware: PostgreSQL, MySQL, MariaDB, and Redis are pointed at `guide slow-query`; MongoDB and Elasticsearch state the timing instead. `--recovery` suppresses the hint so that envelope keeps its contract.
276
542
 
277
543
  ### Changed
@@ -303,8 +569,11 @@ Automation that performs unqualified full-table writes stops working. Three shap
303
569
  ### Added
304
570
 
305
571
  - **Business semantic context commands.** Add the offline, read-only `dbcli semantic validate`, `context`, `search`, `drift`, and `migrate` commands for a reviewable `dbcli.semantic.json`. Semantic models, metrics, aliases, and v2 relationships are checked against the cached visible schema and saved-query names; v1 files remain supported and `migrate --to 2` writes only to stdout.
572
+
306
573
  - **Deterministic governed semantic search and relationship drift checks.** `semantic search` returns only reviewed metadata, removes blacklist names from free-text results, and supports bounded result counts. `semantic drift` identifies stale, invalid, or unavailable local semantic evidence, including relationship references that no longer match declared visible fields.
574
+
307
575
  - **Offline validation boundary for agent query drafts.** `dbcli semantic draft validate --input <file|->` accepts an explicit untrusted JSON draft and validates its references and read-only SQL without executing it, persisting it, or calling a provider. Reports contain hashes, canonical references, and safe violation codes rather than candidate SQL; a successful validation is explicitly not permission to execute.
576
+
308
577
  - **Semantic context in agent-facing skill context.** `dbcli skill context` includes the validated semantic context when present, after blacklist filtering, so agents receive only governed schema and semantic metadata.
309
578
 
310
579
  ## [1.50.0] - 2026-08-06 - QueryLens proxy query analysis
@@ -352,32 +621,59 @@ Automation that performs unqualified full-table writes stops working. Three shap
352
621
  ### Security
353
622
 
354
623
  - **blacklist 只檢查 SQL 的第一張表。** 擋下與遮罩都以單次 regex match 取得的單一表名為準,因此只要敏感表是經由 `JOIN`、逗號、`UNION` 或子查詢進入查詢,它既不會被擋、欄位也不會被遮罩。在 `users` 已列入 blacklist 的設定下,`SELECT * FROM users` 會被擋,但 `SELECT o.id, u.password_hash FROM orders o JOIN users u ON u.id = o.user_id` 會照常回傳 `users` 的敏感欄位。繞過不需要任何特殊語法,一個 `JOIN` 就夠,且在 `query-only` 權限下即可利用。影響 `query` / `export` / `q` / REPL 等所有 SQL 路徑。
624
+
355
625
  - **`export` 的 SQL 路徑完全沒有套用 blacklist。** 該路徑建立 `QueryExecutor` 時把 validator 傳成 `undefined`,因此連單表的情況都不擋、不遮罩:`dbcli export "SELECT * FROM users" --format json` 會把已宣告為敏感的欄位原樣寫進檔案。
626
+
356
627
  - **`export` 的 Elasticsearch 路徑檢查 index 但不遮罩欄位。** 同一個 index 上 `dbcli query` 會遮蔽的欄位,`dbcli export` 會寫進檔案。
628
+
357
629
  - **`dbcli report` 完全沒有套用 blacklist。** 它直接呼叫 adapter 執行 snippet,而 collector 會載入使用者可寫的 snippet 目錄(不只內建),回傳的 rows 會被嵌進報告。既不擋黑名單資料表,也不遮罩欄位。
630
+
358
631
  - **`dbcli q --verify` 的第二段查詢未經檢查。** blacklist 只套用在 snippet 本體,frontmatter 的 `verify.query` 是另一段直接送到 adapter 的 SQL。
632
+
359
633
  - **互動式 shell 從未套用 `blacklist.columns`。** REPL 不走 `QueryExecutor`,它把 adapter 回傳的 rows 直接格式化輸出,因此 `dbcli shell` 裡的 `SELECT * FROM users` 會完整回傳 `dbcli query` 會遮蔽的欄位。
634
+
360
635
  - **MongoDB 的 `$lookup` / `$unionWith` 從未被檢查。** 這是 #23 的 MongoDB 寫法:指令指定一個 collection,pipeline 卻讀另一個。`query` / `export` / `q` 三條路徑都只檢查被指名的那個 collection,因此 `$lookup: { from: 'secrets' }` 既不被擋,嵌入的欄位也不被遮罩。現在會遞迴讀取 `$lookup.from`、`$unionWith.coll`、`$graphLookup.from`、`$out`、`$merge.into`(含 sub-pipeline),並把來源 collection 的欄位規則重新錨定到 `as` 指定的巢狀路徑。
636
+
361
637
  - **字串常值的反斜線解讀會讓掃描器失步。** 表名列舉先前假定反斜線不轉義引號,理由是「提早結束字串只會讓更多文字可見」—— 這個推理是錯的:提早結束會翻轉引號奇偶性,於是下一個引號開啟一段直到輸入結尾的偽字串,把整個 `FROM` 子句藏起來。`SELECT E'\'' AS x, * FROM secrets` 在 `query-only` 下即可取回整張黑名單資料表。現在兩種解讀都掃描並取聯集。
638
+
362
639
  - **PostgreSQL 的 `U&"\0073ecrets"` 未解碼。** 回報的是原始文字,而伺服器解析出的是 `secrets`,因此擋下與遮罩都被繞過。`UESCAPE` 允許以幾乎任何字元代替反斜線(只要不是十六進位數字、`+`、引號或空白),包含一般字母,因此 `U&"x0073ecrets" UESCAPE 'x'` 是純英數字串;現在會對每個合法的 escape 字元各解碼一次。
640
+
363
641
  - **⚠️ 非 ASCII 的 dollar-quote 標籤造成權限繞過(不只 blacklist)。** dollar-quote 的標籤依「未加引號的識別字」規則,而 PostgreSQL 識別字接受高位元組,因此 `$é$ … $é$` 是真正的字串;但語句剖析器的標籤樣式只接受 ASCII,於是該區段被當成一般文字,裡面的 `'` 開啟一段直到輸入結尾的字串,把後面整批語句藏起來。**在 `permission: query-only` 下,`SELECT $é$ ' $é$ ; DROP TABLE users; -- ` 會通過權限判定並送到資料庫。** 這條在 1.47.0 以前就存在,與 1.47.1 修掉的 `a$q$` 是同一族 —— 當時修了識別字延續字元的判定,沒有修標籤本身。詞法邊界規則已抽到 `src/utils/sql-lexical.ts` 由兩個掃描器共用,因為同一條規則已經三次在一個檔案修、另一個沒修。
642
+
364
643
  - **Elasticsearch 的 `--index` 是運算式而非名稱。** 它接受逗號清單、萬用字元、`_all`、百分比編碼(`%2A`)、date math(`<logs-{now/d}>`)與跨叢集限定(`cluster:index`),因此 `--index "secrets,orders"`、`"*"`、`"sec*"`、`"_all"`、`"<secrets>"`、`"*:secrets"`、`"%2A"` 全都能讀取黑名單 index 而不與任何黑名單項目相等。`query` / `export` / `q` / ES shell **四條**路徑皆受影響。現在會正規化運算式後逐一檢查具名 index,萬用字元則在「可能匹配到黑名單 index」時拒絕。
644
+
365
645
  - **同一個問題也讓欄位遮罩整個失效。** 遮罩仍以原始運算式做等值查表,因此 `--index "us*"` 或 `"users,orders"` 匹配不到任何欄位規則 —— 在只設定欄位黑名單(資料表本身未列入)時,`checkIndexBlacklist` 會放行,然後所有受保護欄位原樣回傳,`export` 更會寫進檔案。現在改以「該運算式可能觸及的所有 index 的規則聯集」遮罩。
646
+
366
647
  - **ES 目標的比對讀的是原始文字,不是伺服器實際路由的路徑。** `%5F` 是 `_`、`%2F` 是 `/`、`..` 會退一層,因此 `GET /%5Fsearch`(實為 `/_search`)、`/secrets%2F_search`(實為 `/secrets/_search`)、`/_cat/../secrets/_search` 全都通過檢查。另有 `_ALL` 大小寫、`%252A` 雙重編碼、`<<secrets>>`、`c:d:secrets`、`secrets:` 等拼法在 `--index` 上同樣繞過。現在路徑會先解碼並解析(重複至穩定)再檢查,且**任何一段**命中黑名單 index 就拒絕 —— `/_cat/indices/secrets` 不讀文件,但黑名單保護的是物件本身。正規化規則抽到 `src/utils/es-index-target.ts` 由 validator 與 shell 共用。
648
+
367
649
  - **ES shell 只看路徑,request body 指名的 index 完全未檢查。** `_mget` 的 `docs[]._index`、`_bulk` action 的 `_index`、`terms` lookup 的 `index` 都能指向黑名單 index —— 把路徑指向無害的 index,正好讓這些端點重新打開。
650
+
368
651
  - **ES shell 的「未指名 index 即拒絕」讀的是原始路徑,其餘檢查讀的是解析後路徑。** 因此 `GET /_cat/../_search`、`/_ingest/../_sql`、`/_license/../_msearch` 只要前綴在允許清單內就放行,而 HTTP 客戶端會把 `..` 解掉,實際送出的是未界定的 `_search`。現在檢查與送出的是同一個字串,且路徑的文字與路由結果不一致時直接拒絕。
652
+
369
653
  - **ES shell 的欄位遮罩保護的是鍵名,不是值。** Elasticsearch 會把欄位值放在**請求指定**的鍵底下回傳:`{"sort":["password"]}` 一個請求就能依序取回整欄,`aggs.*.field`、`script_fields`、`docvalue_fields`、runtime field 同理,都不需要 scripting 權限。現在請求本體中只要出現受保護欄位名(含字串內以非識別字切出的片段)即拒絕,遮罩回應則作為第二道。
654
+
370
655
  - **Elasticsearch data stream 與 rollover 的支撐 index 名稱不同,等值比對蓋不到。** `.ds-secrets-2026.08.05-000001`、`secrets-000001` 都能讀到 `secrets` 的資料。現在依命名慣例一併涵蓋。**alias 仍是天花板** —— alias 指向哪個 index 是伺服器端知識,且 `GET /_cat/aliases` 會揭露對應關係;已記入威脅模型。
656
+
371
657
  - **request body 中陣列型的 `index` / `_index` 未被檢查**(`_msearch` 標頭、`_reindex` 的 `source.index` 都接受陣列)。
658
+
372
659
  - **`globToRegex` 的字元類別掃描不理會轉義**,`[a\]b]` 被讀成字面字串而非「a、]、b 三選一」的類別。
660
+
373
661
  - **ES shell 完全沒有欄位遮罩。** `dbcli query --index users` 會遮蔽的欄位,ES shell 原樣回傳。現在回應中任何名稱命中欄位黑名單的鍵一律移除(不論深度)—— ES 回應是任意文件結構,與其為 `hits.hits` 等各種外層建模,不如從嚴。
662
+
374
663
  - **ES shell 對任何未指名 index 的路徑完全跳過檢查。** 路徑第一段以 `_` 開頭時取不到 index,於是 `GET /_all/_search`、`/_search`、`/_msearch`、`/_mget`、`/_sql` 全都放行 —— 它們都會讀到黑名單 index 的文件。現在:有設定黑名單時,無法界定 index 的請求一律拒絕,僅以**允許清單**放行純叢集中繼資料端點(`_cat`、`_cluster`、`_nodes`、`_tasks`、`_ingest`、`_license`),因為改用拒絕清單就得窮舉現在與未來所有會回傳文件的端點。
664
+
375
665
  - **`export` 的 ES 路徑先取資料才檢查 blacklist。** 雖然不會寫出檔案,但黑名單 index 已被查詢、scroll context 已被開啟。檢查已移到抓取之前。
666
+
376
667
  - **MongoDB 巢狀 `$lookup` 的遮罩前綴不含巢狀層級。** `$facet` 分支或 `$lookup.pipeline` 內的 `$lookup`,文件實際落在 `fb.sec.*` / `outer.sec.*`,規則卻被錨定在 `sec.*`,因此不會遮罩。
668
+
377
669
  - **同一個 collection 被 join 兩次時只有第一次被遮罩。** 前綴是以「尚未見過的 collection」為單位記錄的,因此 `$lookup ... as: 'first'` 與 `$lookup ... as: 'second'` 只產生一組前綴,`second.token` 外洩。
670
+
378
671
  - **⚠️ 修復本身引入的回歸(已修):dollar-quote 判定改為「必須以可起始識別字的字元開頭」之後,數字後接識別字的情況被誤判。** `1a$q$` 在 PostgreSQL 是數值常值 `1` 加上識別字 `a$q$`(`$` 被吸收、不開引號),但新規則只看第一個字元、把整串當成數字,於是**憑空造出一個 dollar-quote**,`SELECT 1a$q$ ; DELETE FROM secrets ; SELECT 1 AS z$q$` 在 `query-only` 下通過。判定改為:以識別字字元開頭則吸收;`$` 開頭是位置參數;數字開頭則先吃掉數值前綴,若其後仍有識別字字元就吸收。
672
+
379
673
  - **PostgreSQL 中 dollar-quote 接在位置參數之後未被識別。** `$1$q$` 裡的 `$1` 是參數而非識別字,但判定只排除「以數字開頭」的字元串,`$` 開頭的被當成識別字,於是 `$q$` 未被識別、裡面的 `'` 再次讓掃描失步。判定改為「該字元串必須以可*起始*識別字的字元開頭」。目前不可利用(`query.ts` 一律傳空參數,`$1` 在伺服器端是語法錯誤),支援參數綁定後即會成為實洞。
674
+
380
675
  - **`globToRegex` 對轉義的字面量比對過少。** `sec\*` 應保護鍵 `sec*`,卻編譯成可比對 `sec\x` 而比不到 `sec*` —— Redis key 黑名單的樣式在這個方向上是會洩漏的。
676
+
381
677
  - **PostgreSQL 中 dollar-quote 接在數字之後未被識別。** 只有*識別字*會吸收後面的 `$`:`1$q$` 是數值常值加上真正的 dollar-quote,`a1$q$` 則是單一識別字。原本只看前一個字元,兩者分不開,於是該引號未被識別、裡面的 `'` 再次讓掃描失步。
382
678
 
383
679
  除前兩條外,其餘皆是在修 #23 的過程中、經由列舉「哪些路徑直接呼叫 adapter」與七輪對抗性審查找出來的 —— 與 1.47.1 的教訓相同:這類缺陷表現為**未設防的路徑**,不是缺少機制。
@@ -385,19 +681,29 @@ Automation that performs unqualified full-table writes stops working. Three shap
385
681
  ### Changed
386
682
 
387
683
  - **⚠️ 行為收緊:語句只要參照到任何一張黑名單資料表就會被擋下。** 過去只有排在最前面的那張表算數。升級後,先前能執行的跨表查詢(JOIN / 逗號 / UNION / 子查詢帶進黑名單表)會開始被拒絕 —— 那正是原本應該被擋的行為。
684
+
388
685
  - **⚠️ 遮罩改以「所有被參照的表」的欄位規則聯集計算。** JOIN 結果的欄位名不帶表限定(`u.password_hash` 回傳成 `password_hash`),無法從結果反推欄位屬於哪張表,因此只要**任何一張**被參照的表把該欄位列入黑名單就遮蔽。
686
+
389
687
  - **⚠️ 表名列舉刻意過度回報,可能誤擋。** 新的 `src/utils/sql-tables.ts` 除了走訪 `FROM` / `JOIN` / `INTO` / `UPDATE` / `USING` / `TRUNCATE` / `COPY` / `STRAIGHT_JOIN` 等位置,**還會把語句中每一個非保留字的識別字都列為候選表名**。這表示:若某個欄位名、別名或函式名剛好等於一張黑名單資料表的名稱,該語句會被擋下。這是刻意的取捨 —— 歷次對抗性審查各自都在「精確走訪」版本裡找到新的文法角落(`{ oj … }`、`STRAIGHT_JOIN`、`FROM a USE INDEX (i), secrets`、`ANALYZE t`、`U&"t"`),與 1.47.1 記錄的天花板是同一個模式,因此保證不建立在走訪完整之上,而建立在「表名是識別字,而每個識別字都會被回報」。被擋時錯誤訊息會指出是哪個名稱命中 —— 實務上會遇到的情形是:黑名單有一張叫 `token` 的資料表時,`SELECT t.token FROM api_keys t` 會被拒絕。
688
+
390
689
  - **保留字清單只留三種方言都保留的字**(38 個)。 這是列舉唯一會 fail-open 的地方:清單裡的字若在某個方言其實可當未加引號的表名,那張表就隱形。`FILTER` / `PARTITION` / `SET` / `CURRENT` / `UPDATE` / `DELETE` / `INSERT` / `NULLS` / `OVER` / `EXISTS`、`RLIKE` / `XOR` / `ILIKE` / `STRAIGHT_JOIN`,以及 `BETWEEN`(PostgreSQL 的 `col_name_keyword`,`ColId` 接受)與 `EXCEPT` / `INTERSECT`(MySQL 8.0.31、MariaDB 10.3 才成為保留字)皆已移除 —— 每一個都在某個支援的方言裡是合法表名。
690
+
391
691
  - **語句在所有歧義解讀下各掃描一次並取聯集。** 反斜線是否轉義引號取決於伺服器模式,未宣告方言時註解規則也不同。挑一種解讀正是失步繞過的成因。
692
+
392
693
  - **無法辨識出資料表時,遮罩套用全部欄位規則而非不套用。** 過去(以及本次修復的第一版)在表名解析不出來時直接跳過遮罩,等於把任何解析缺口變成洩漏。
694
+
393
695
  - **`query` 的大小防護不再對 schema 限定名靜默失效。** 舊的單次比對對 `FROM public.users` 回傳 `public`,那不是 schema 快取的鍵,於是防護整段被跳過。
394
696
 
395
697
  ### Fixed
396
698
 
397
699
  - **`snapshot` 記錄的 redacted 欄位清單過去只取第一張表**,與實際遮罩的範圍不一致。
700
+
398
701
  - **表名列舉在長 dotted chain 上是二次成長**(16 KB 的 `a.a.a…` 要 320ms,125 KB 要 22 秒)。改為單趟走訪後同一輸入為 2ms。
702
+
399
703
  - **`decodedVariants` 對「相異字元數」是二次成長**(40 KB 的識別字要 3.4 秒)。改為單趟同時解碼所有合法 escape 字元後,同一輸入為 4ms。
704
+
400
705
  - **`globToRegex` 遇到無法解析的字元類別(`[\]*`)會拋 `SyntaxError`**,從安全檢查裡竄出去而不是回答它。
706
+
401
707
  - **識別字中超出 Unicode 上限的 escape 會讓掃描整個拋例外。** `\+FFFFFF` 是 16777215,`String.fromCodePoint` 會丟 `RangeError`;而 `"` 在所有方言都被當識別字引號,因此任何含該樣式的 MySQL 字串(例如 Windows 路徑)都會讓指令中斷。
402
708
 
403
709
  ### Known limits
@@ -419,10 +725,15 @@ Automation that performs unqualified full-table writes stops working. Three shap
419
725
  修復六個「看起來是讀、實際會寫」的繞過,它們都能在設定為 `permission: query-only` 的連線上寫入資料。**建議所有把資料庫交給 AI agent 操作的使用者升級。**
420
726
 
421
727
  - **MongoDB `$out` / `$merge` 未被擋下(`query`、`q`、`export`)。** 這兩個 aggregation stage 只在多連線 fan-out 路徑被檢查,單連線 `dbcli query`、saved snippet、以及 `dbcli export` 全都會執行它們,不論 permission 等級。`$out` 可覆寫任意 collection。`--dry-run` 會把這種 pipeline 預覽成安全操作。影響 MongoDB 連線。
728
+
422
729
  - **PostgreSQL 多語句堆疊。** 權限分類只讀第一個關鍵字,而 PostgreSQL 的 simple query protocol 會執行字串裡每一個以分號分隔的語句,因此 `SELECT 1 LIMIT 1; DELETE FROM users` 會以 SELECT 的身分通過 `query-only`。影響 PostgreSQL;MySQL / MariaDB 走 prepared statement,不受影響。
730
+
423
731
  - **snippet 的偽唯讀語句。** snippet 只要求開頭是 `SELECT` 或 `WITH`,因此 `WITH x AS (DELETE FROM users RETURNING *) SELECT * FROM x` 與 `SELECT … INTO` 都能通過。一個 commit 進 repo、看起來是唯讀報表的 `.sql` 檔可以寫入資料庫。影響 PostgreSQL / MariaDB。
732
+
424
733
  - **snippet frontmatter 的 `verify.query` 未經驗證。** 過去只檢查它是非空字串,然後由 `dbcli q <name> --verify` 原封執行。
734
+
425
735
  - **唯讀證明只接在多連線 fan-out 上,單連線 `query` / `export` / REPL 沒有。** 權限判定只看第一個關鍵字,因此 `query-only` 連線接受並執行下列語句:`WITH gone AS (DELETE FROM users RETURNING *) SELECT * FROM gone`(data-modifying CTE)、`SELECT * INTO evil_copy FROM users`(建表)、`EXPLAIN ANALYZE DELETE FROM users`(`EXPLAIN ANALYZE` 會真的執行該語句)。同一句 SQL 加上 `--use a,b` 會被擋,不加就執行。auto-limit 補的 `LIMIT 1000` 對 CTE 無效,整張表仍會被刪。**這條在 1.47.0 以前就存在**,且不需要任何特殊語法。影響 PostgreSQL 與 MariaDB。
736
+
426
737
  - **PostgreSQL 識別字中的 `$` 被誤判為 dollar-quote 起點。** PostgreSQL 的識別字從第二個字元起允許 `$`,因此 `a$q$` 是**一個識別字**;但語句剖析器把它讀成字串起點,於是 `SELECT 1 AS a$q$ LIMIT 1; DELETE FROM users; SELECT 1 AS b$q$` 中間整段對所有安全檢查隱形,資料庫卻照常執行三段。**這條在 1.47.0 以前就存在**:多連線 fan-out 的唯讀斷言(`dbcli query --use a,b`)用的正是同一個剖析器,因此可被此手法繞過。影響 PostgreSQL。
427
738
 
428
739
  利用這些繞過需要能下達指令的一方送出 payload,也就是 agent 本身 —— 而 dbcli 的威脅模型前提正是 agent 不完全可信,因此這些屬於權限繞過,不以「使用者自己下的指令」論。
@@ -430,10 +741,15 @@ Automation that performs unqualified full-table writes stops working. Three shap
430
741
  ### Changed
431
742
 
432
743
  - **⚠️ 行為收緊:開頭讀取但夾帶寫入的語句一律需要 `admin`。** 例如 `WITH x AS (INSERT … RETURNING *) SELECT …`、`SELECT … INTO`、`EXPLAIN ANALYZE <寫入>`、`DESCRIBE ANALYZE <寫入>`(`DESCRIBE` 在 MySQL/MariaDB 是 `EXPLAIN` 的同義字)。過去這些一律不檢查;中間曾嘗試「比照該寫入的等級」,但那需要為 `INSERT INTO` 的 `INTO` 加文字例外,而例外之間會互相作用出新的繞過,因此改為單一規則。**升級後需要改用 `admin` 的用法有三類**:`data-admin` 連線上的可寫 CTE;對寫入語句做 `EXPLAIN ANALYZE` / `DESCRIBE ANALYZE` 效能分析(它會真的執行該語句);以及 MySQL/MariaDB 的 `SELECT … INTO @variable`——後者其實是純讀取,只是與 PostgreSQL 會建表的 `SELECT … INTO <table>` 共用關鍵字,為它加例外正是本次反覆出問題的來源,因此選擇留下這個誤擋。被擋時的錯誤訊息會指出是哪一個關鍵字觸發的。 不含 `ANALYZE` 的 `EXPLAIN` 只做計畫、不執行,維持唯讀;`SHOW` / `DESCRIBE` 不接受子查詢,因此 `SHOW CREATE TABLE users` 仍是讀取;`replace()` / `TRUNCATE()` / `INSERT()` 是函式不是語句,維持唯讀。
744
+
433
745
  - **admin 以下的權限等級拒絕多語句 SQL。** 因為只有第一個語句會決定權限判定。`admin` 不受影響(它本來就允許所有語句類型)。分隔符依**該連線實際的方言**判定:`$$…$$` 只在 PostgreSQL 是字串、反引號只在 MySQL/MariaDB 引號化識別字、`#` 只在 MySQL/MariaDB 起始註解(在 PostgreSQL 是運算子)。方言未知時從嚴。
746
+
434
747
  - **snippet 的唯讀證明依 `engine` 宣告的方言判定。** 因此 `SELECT \`update\` FROM t`(MySQL 反引號識別字)、`# drop …` 註解、`a.create` 這類欄位名不再被誤判為寫入;`FOR UPDATE` / `FOR SHARE` 是取鎖的讀取,同樣不算寫入。
748
+
435
749
  - **無法解析的 snippet 只跳過該檔並發出警告,不再讓整個 snippet 目錄失效。** `queries check` 仍會回報它們並以 exit 1 結束。
750
+
436
751
  - **snippet 一律拒絕寫入關鍵字。** snippet 依合約唯讀,這條規則不看 permission 等級,`admin` 連線亦然。
752
+
437
753
  - **MongoDB 寫入 stage 在單連線 `query` 需要 `data-admin` 以上;在 snippet 與 `export` 一律拒絕。**
438
754
 
439
755
  ### Added
@@ -447,11 +763,13 @@ Automation that performs unqualified full-table writes stops working. Three shap
447
763
  ### Added
448
764
 
449
765
  - **新的 root-level 全域旗標 `--timeout <ms>`。** 覆寫連線設定中的 `timeout`;兩者都沒有時沿用各 adapter 內建的 5000ms。合法值為 100~600000 的整數,須放在子指令之前(和 `--global` / `--use` 一樣是 root-level flag)。對所有引擎有效,典型用途是 MongoDB 跨 VPN 或連 Atlas 時,預設 5 秒的 server selection timeout 太緊:`dbcli --timeout 20000 --use <conn> list`。這個覆寫只在建立連線時套用,不會寫回設定檔;要永久生效請在連線設定裡寫 `timeout` 欄位。
766
+
450
767
  - **連線設定檔新增 `timeout` 欄位。** 四種連線 schema 皆支援,毫秒、100~600000 整數、可省略。
451
768
 
452
769
  ### Changed
453
770
 
454
771
  - **設定檔驗證失敗的錯誤訊息改為可讀格式。** 過去會吐出整包 Zod `unionErrors` 巢狀 JSON;現在只列出與該連線 `system` 相符的分支問題,逐欄列出欄位路徑。
772
+
455
773
  - **文件明確禁止 `2>&1`。** 診斷訊息走 stderr、結果走 stdout,合併兩者會讓 `--format json` 的輸出無法解析;SKILL 與 reference 都補上導管寫法。
456
774
 
457
775
  ## [1.46.0] - 2026-08-04 - MongoDB 逐欄連線設定
@@ -461,20 +779,27 @@ Automation that performs unqualified full-table writes stops working. Three shap
461
779
  ### Changed
462
780
 
463
781
  - **⚠️ BREAKING(互動流程):`dbcli init` 對 MongoDB 改為先問「連線設定方式」。** 過去第一個提問是 MongoDB URI,留空才退回逐欄詢問 —— 於是逐欄路徑事實上沒人走,所有文件也只教「整條 URI 貼進去」。現在預設是「逐欄填寫」,貼 URI 降為明示的進階選項。**設定檔格式向下相容**,既有含 `uri` 的設定不需修改;`--uri`、`--no-interactive` 等非互動用法行為完全不變,只有互動提問的順序改變。
782
+
464
783
  - **逐欄模式在有帳號時會明確寫出 `authSource`。** 過去只有帶 `--auth-source` 才會(而且寫了也會被 schema 丟掉),現在未指定時會寫入 `admin`。連線結果與過去等價(adapter 本來就以 `admin` 為預設),但設定檔內容會多這一行 —— 包含 `--no-interactive` 的既有腳本。
784
+
465
785
  - **`uri` 與逐欄欄位仍是 `uri` 優先,但不再靜默。** 兩者同時存在時 `dbcli doctor` 會發出 warning 指出逐欄值被忽略;`srv: true` 又指定非預設 `port` 也會 warning。這兩種設定過去都是「改了欄位卻沒生效」而無從診斷。
466
786
 
467
787
  ### Added
468
788
 
469
789
  - **MongoDB 連線設定新增 `authSource` / `replicaSet` / `tls` / `srv` 四個欄位。** 過去這些選項只能塞進 `uri` 的 query string —— 這正是逐欄路徑不堪用的根因。其中 `authSource` 更微妙:runtime 型別與 `init --auth-source` flag 都存在,但 zod schema 沒有此鍵,`z.object` 會 strip 掉未知欄位,於是它落盤即遺失,只有 init 當下那次連線測試吃得到,等同一個死 flag。`srv: true` 會組出 `mongodb+srv://` 並沿用既有的 DNS SRV 展開(含 DoH fallback),讓 Atlas 這類最常見的雲端場景也能逐欄設定。`authSource` 與 `replicaSet` 支援 `{"$env": "..."}` 參照。
790
+
470
791
  - **MongoDB 逐欄分支支援 `--use-env-refs`。** 過去 mongo 在 init 的 early-return 發生在 env-ref 分支之前,想用環境變數參照只能手改 `config.json`。現在五個 `--env-*` 旗標對 mongo 全部生效,密碼不必明文落盤。與 SQL 路徑的差異:mongo 只要求 `--env-host`,其餘留空即寫入字面值而不產生 `$env` —— 因為未定義的 `$env` 會讓之後每一個指令 fail closed,對無認證連線而言那是壞掉的設定。env-ref 模式同樣跳過連線測試(參照此時還沒有值可連),與 SQL 路徑一致。
792
+
471
793
  - **連線失敗訊息按成因分類。** 認證失敗提示檢查 `authSource`(並說明 Atlas 與多數自架環境為 `admin`)、DNS/SRV 解析失敗提示 `srv` 設定與網路 DNS、TLS 握手失敗提示 `tls` 欄位與自簽憑證情境。原本三種情況共用同兩條泛用訊息。
472
794
 
473
795
  ### Fixed
474
796
 
475
797
  - **逐欄模式的連線字串跳脫不完整。** `buildUri()` 原本只對 `password` 做 `encodeURIComponent`,`user` 與 `database` 直接字串拼接 —— 帳號含 `@`、資料庫名含 `/` 都會讓 driver 把 authority 切在錯的位置。現在三者一致跳脫,`host` 則改為驗證不含 `/@?#` 並在違反時明確報錯。
798
+
476
799
  - **`host` 為空字串或含埠號、空白時會產出壞掉的連線字串。** `mongodb://:27017/db` 與 `mongodb://h:1234:27017/db` 過去都會被送進 driver,換來一個難懂的錯誤。現在在組字串前就擋下並說明埠號該填在 `port` 欄位。IPv6 位址需加方括號(`[::1]`),與 driver 的要求一致 —— 未加方括號的 `::1` 過去會組出 `mongodb://::1:27017/db`。同理 `authSource` 為空字串時會退回 `admin`,不再送出 `authSource=`。
800
+
477
801
  - **連線失敗分類會被連線字串本身誤導。** driver 的錯誤訊息經常回吐原始 URI,而 `mongodb+srv://` 與這次新增的 `?tls=true` 正好含有 `SRV` 與 `TLS` 字樣 —— 用裸字串比對會讓一個單純的連線被拒歸類成 DNS 或 TLS 問題。改為優先讀 driver 的結構化 error code,訊息比對則收斂成 driver 實際會產生的片語。
802
+
478
803
  - **只填 `user` 沒填 `password` 會靜默降級成無認證連線。** 原本的 `if (user && password)` 在密碼缺漏時直接落到無認證分支,錯誤會延後到伺服器端才浮現、且看起來像是權限問題。現在直接拋 `ConnectionError`,訊息說明補上密碼或一併清空 `user`。
479
804
 
480
805
  ## [1.45.1] - 2026-08-04 - Windows 上的 agent mode 修復
@@ -486,6 +811,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
486
811
  ### Changed
487
812
 
488
813
  - **移除 schema loader 的牆鐘時間斷言。** `initialize` 的 `loadTime < 200ms` 跑在阻擋性的 `bun test` 裡,但共用 CI runner 不是量測儀器(Windows 冷啟動 270ms 就紅,程式本身無異常)。改為斷言合約(有量到並回報 loadTime),時間預算歸 `tests/perf/*.bench.ts` —— CI 對該套件本來就設 `continue-on-error`,正因為 timing 依環境而定。
814
+
489
815
  - **`docs/security-threat-model.md` 補上平台差異。** POSIX 用 `0o700`/`0o600` 保護設定,Windows 沒有等價 mode bits,機密性靠 profile ACL;竄改偵測兩邊一致。
490
816
  - 這兩項修復讓 `windows-latest` CI job 自 v1.40.0 以來首次通過(6 個 matrix job + docs-parity 全綠)。
491
817
 
@@ -494,11 +820,13 @@ Automation that performs unqualified full-table writes stops working. Three shap
494
820
  ### Added
495
821
 
496
822
  - **root-level `--global` 旗標。** 原本每條連線都綁在專案上:`init` 會在 `./.dbcli/config.json` 寫 binding stub,真正的設定落在 `~/.config/dbcli/projects/<project-id>/`。要在多個專案共用同一條連線,只能在每個 repo 重跑一次 `init`,或手動複製設定。`--global` 讓 `~/.config/dbcli/config.json` 成為一個獨立的 v2 registry:`dbcli --global init --conn-name shared ...` 直接寫進去、不建立也不修改專案 binding,`dbcli --global use --list` / `--global query` 則在不依賴當前目錄的情況下操作它。scope 必須明確選取 —— 未帶 `--global` 時一切照舊走專案 binding,避免在不相關的專案裡誤用全域連線。全域檔案沿用與 home storage 專案設定相同的私有檔案權限與 integrity record。
823
+
497
824
  - **`getDbcliConfigHome()` / `getGlobalConfigPath()` / `isGlobalConfigPath()` 加入 `public.ts`。** 前者把 per-user root 改為延遲解析並支援 `DBCLI_CONFIG_HOME` 覆寫,測試與 embedder 不必 reload module 就能隔離 config home。
498
825
 
499
826
  ### Changed
500
827
 
501
828
  - **`migrate` 與 `queries` 子指令補上 Commander `command` 傳遞。** 這兩處原本以 `resolveConfigPath(undefined, opts)` 解析設定路徑,看不到 ancestor 的 root-level 旗標 —— 沒有 `--global` 時症狀被 `.dbcli` 預設值蓋掉,加上 `--global` 後就會靜默讀錯 registry。現在 36 個 `resolveConfigPath` 呼叫點全部傳入 command。
829
+
502
830
  - **`resolveConfigPath` 的優先序明確化。** 顯式 `--config` 仍最優先(`--global --config <path>` 因此是確定的),其次是顯式 `--global`,最後才是 `.dbcli` 預設值。
503
831
 
504
832
  ## [1.44.1] - 2026-08-02 - `agent-core` 的 `loadEnvFile` 改用 node:fs,可在 Node 執行
@@ -526,31 +854,49 @@ Automation that performs unqualified full-table writes stops working. Three shap
526
854
  ### Added
527
855
 
528
856
  - **穩定的 `./agent-core` 子路徑匯出。** 以五個 runtime functions(env 載入、env reference、連線選取、名稱解析、lookahead 截斷)與三個型別形成 agent CLI 共用的 semver interface;`./core` 仍是 dbcli 專用介面。建置同時產出 ESM 與型別宣告,CI purity gate 禁止資料庫、adapter 或 CLI framework 相依滲入。
857
+
529
858
  - **欄位投影 `--fields`。** SQL 與 MongoDB 通用;`--fields a,b` 取用、`--fields=-raw_response` 排除,兩種形式不可混用。MongoDB 會把 `projection`(find)或 `$project`(aggregate)下推給 driver,未明確指定時不回傳 `_id`。黑名單欄位不會因為被 `--fields` 點名而洩漏。
859
+
530
860
  - **欄位值截斷 `--truncate`。** table 輸出預設在 120 個 Unicode code point 截斷並標記 `…(+N chars)`,以 code point 計數所以不會切壞中文與 emoji;`--no-truncate` 可關閉。`--format json` / `csv` 會拒絕此旗標而非靜默忽略。
861
+
531
862
  - **從檔案或 stdin 讀查詢 `-f, --query-file`。** `-f -` 讀 stdin,可用 heredoc 傳含 `$regex`、巢狀日期物件的 MongoDB pipeline,完全避開 shell 引號問題。同時給檔案與位置參數會明確報錯。
863
+
532
864
  - **單次連線指定。** 新增 `DBCLI_CONNECTION` 環境變數,`query` / `list` / `schema` / `export` / `check` 也接受子指令層級的 `--use`。優先序為 `--use` > `DBCLI_CONNECTION` > 儲存的預設值,兩者都不會改寫 `.dbcli/config.json`,因此平行執行不會互相污染。
865
+
533
866
  - **唯讀多連線扇出 `--use a,b`。** 同一查詢對多個連線執行,JSON 回傳 `results` 陣列並逐一標示 `ok` / `error`,table 則分段標註連線名。單一連線失敗不會取消其他連線。彙總 exit code:全成功 `0`、部分失敗 `2`、全失敗或執行前拒絕 `1`。寫入語句、`--recovery`、`--ui` 與 CSV/HTML 輸出在扇出下一律拒絕。
534
867
 
535
868
  ### Changed
536
869
 
537
870
  - **HTML dashboard 明示不完整與遮蔽結果。** `query`、`q` 與 HTML export 會把既有的截斷與 security metadata 傳入 dashboard;在 KPI、圖表與 raw table 之前顯示醒目提示,避免使用不完整資料得出結論。
871
+
538
872
  - **截斷改為出現在結果本身。** dbcli 擁有的 row cap 會多取一列前瞻,因此能區分「剛好 N 筆」與「被砍到 N 筆」:table footer 顯示 `Rows: N (truncated; limit N)`、`--format json` 帶 `metadata.truncated` 與 `metadata.limit_applied`、CSV 附加 `# truncated; limit N` 註解行。`dbcli q` 的 snippet size guard 同樣依此回報,不再讓整數列數被誤讀為全集。
873
+
539
874
  - **`dbcli export` 撞到 auto-limit 改為 fail closed。** 匯出檔沒有地方記錄資料被丟掉(jsonl 是一行一筆、MongoDB `--format json` 是裸陣列),stderr 警告又會在重導向後消失,因此改為 exit `1` 且不寫檔,要求以 `--no-limit` 或 `--limit N` 明確表態。Elasticsearch 匯出的 1000 筆上限同此處理。
875
+
540
876
  - **CLI 錯誤輸出收斂。** 連線類錯誤在所有指令路徑都會被頂層 handler 攔截並格式化,stderr 首行即為人類可讀訊息,不再由 Bun 印出打包後的 code frame 與未解碼的中文跳脫序列。stack 改掛在 `-v` / `-vv` 之下,預設不輸出。
541
877
 
542
878
  ### Fixed
543
879
 
544
880
  - **MySQL 8 schema introspection 相容預設 `ONLY_FULL_GROUP_BY`。** 外鍵查詢現在完整分組 referenced table,不再讓 `dbcli schema <table>` 在原廠預設設定下失敗。
881
+
545
882
  - **已分類的連線錯誤不再被巢狀 adapter catch 重包。** `mapError` 直接保留既有 `ConnectionError` 的 identity、code、message 與 hints,消除 `Connection failed: Connection failed:` 重複前綴與分類退化。
883
+
546
884
  - **stdout 管線與 Windows CI 修復。** redirected stdout 以完整同步寫入避免 64KB 截斷;測試 filesystem 與換行處理改為跨平台實作,Windows matrix 恢復全綠。
885
+
547
886
  - **發布依賴安全更新。** 將 PostCSS 鎖定至 `8.5.25`、`brace-expansion` 鎖定至 `5.0.9`,清除 release gate 回報的 3 個 high-severity advisories;並統一 Prettier 格式,讓完整 9 階段發布檢查恢復全綠。
887
+
548
888
  - **`--no-limit` 過去被靜默忽略。** Commander 會把 `--no-limit` 折進 `limit` 屬性(設為 `false`)而不會產生 `noLimit`,但 `query` / `q` / `export` 都讀 `options.noLimit`,導致這個旗標自始無效——`query` 仍套用 1000 筆上限,`q` 仍包 size guard。CLI 邊界現在會把 Commander 的否定形式轉回指令實際讀取的形狀。
889
+
549
890
  - **`dbcli export` 的 SQL 路徑忽略 `--limit` 與 `--no-limit`。** 該分支未把選項傳給 QueryExecutor,任何 `--limit N` 都不生效。
891
+
550
892
  - **`-v` / `-vv` 的 stack 開關過去對 `q` / `insert` / `update` / `delete` 無效。** 這四個指令自行輸出在地化訊息、繞過共用的錯誤呈現層,因此 verbose 對它們不會多印任何東西。改為共用同一個呈現函式:措辭維持不變,但 verbose 下會補上 stack。
893
+
551
894
  - **Redis 的 size-guard warning 在 `query` 被丟棄。** adapter 早已算出 `REDIS_SIZE_TRUNCATE` / `REDIS_SIZE_REWRITE` / `REDIS_BLACKLIST_FILTERED`,但 `query` 分支完全沒讀 `result.warnings`——文件卻聲稱結果會帶 `warnings[]`。現在每則 warning 都會印到 stderr,且被裁切的回覆會回報 `truncated` / `limit_applied`,與其他引擎一致。
895
+
552
896
  - **`--query-file -` 在互動式終端會無提示空等。** 改為立即拒絕並說明需要 piped input,與 repo 中其他 stdin 消費端(`insert`、`shell`、`audit`)既有的 TTY 檢查一致。
897
+
553
898
  - **單一連線 (v1) 設定會靜默忽略 `--use` / `DBCLI_CONNECTION`。** v1 沒有具名連線可選,過去卻照樣執行那唯一的連線,讓使用者以為切換成功——正是 issue #7 要避免的情境。現在會明確報錯並指出升級為 v2 的方式。
899
+
554
900
  - **skill assets 與 reference 補齊。** `assets/SKILL.md`、`SKILL.zh-TW.md` 與 `reference.md` 新增查詢工作流程旗標章節;`reference.md` 原本記載「MongoDB 不套用 auto-limit」與實際行為不符,已更正為套用於 filter 與未自帶 `$limit` 的 pipeline。
555
901
 
556
902
  ## [1.42.0] - 2026-07-20 - Drizzle Snapshot 與 ORM DDL 工作流擴充
@@ -558,16 +904,19 @@ Automation that performs unqualified full-table writes stops working. Three shap
558
904
  ### Added
559
905
 
560
906
  - **Drizzle Kit snapshot 可直接用於 ORM drift 比對。** `dbcli diff --against-orm` 新增 Drizzle snapshot 格式偵測與 `NormalizedSchema` adapter,支援 PostgreSQL v7 snapshot 的 table、column、primary key、unique constraint、index 與 foreign key metadata。
907
+
561
908
  - **TypeORM/Sequelize DDL alias。** `--orm-format typeorm`、`typeorm-ddl`、`sequelize` 與 `sequelize-ddl` 可直接走既有 DDL adapter;自動忽略 `typeorm_metadata` 與 `SequelizeMeta` bookkeeping table,並補上 source-file 使用者的可執行匯出/比對指引。
562
909
 
563
910
  ### Changed
564
911
 
565
912
  - **ORM drift 文件完整同步。** 英文/繁體中文的 Markdown 與 HTML 使用者文件、skill assets、各平台 plugin 副本及 reference 已補上 Drizzle snapshot、TypeORM/Sequelize DDL 的格式、限制與操作範例。
913
+
566
914
  - **跨平台發版 metadata 對齊。** npm package、Codex/Claude/Cursor plugin、packaged Codex plugin 與 Gemini extension 統一為 `1.42.0`。
567
915
 
568
916
  ### Fixed
569
917
 
570
918
  - **不支援的 ORM 輸入改為 fail closed。** Drizzle snapshot 會拒絕不支援的版本/dialect、generated/identity/enum/composite primary key 等結構,以及無法無損轉換的 column default;TypeORM/Sequelize source file 則回報完整的匯出 DDL recipe,不再被 JSON/DDL fallback 誤解析。
919
+
571
920
  - **Qualified ignore identity 保留完整。** ORM drift 的 ignore 比對不再把 schema-qualified identity 降成 bare table name,避免同名 table 跨 schema 時被錯誤忽略;ORM DDL alias 也會正確沿用 DDL 輸入處理與 bookkeeping ignore。
572
921
 
573
922
  ## [1.41.0] - 2026-07-19 - ORM Drift 比對與無損 Schema Identity
@@ -575,18 +924,23 @@ Automation that performs unqualified full-table writes stops working. Three shap
575
924
  ### Added
576
925
 
577
926
  - **`dbcli diff --against-orm` ORM drift 比對。** 可將 Prisma schema、DDL/migration SQL 或 normalized JSON 與既有 SQL schema cache 比對;支援多檔 DDL、filesystem glob、格式自動偵測、大小寫敏感的 `--ignore` pattern,以及 JSON、table、Markdown 輸出。比對只讀本地 cache,不連線、不更新 cache,也不執行提案。
927
+
578
928
  - **結構化 drift 分類與安全提案。** 報告區分 `missing_in_db`、`missing_in_orm`、`mismatch`、`unmanaged` 與 `unparsed`;只有計分後的 error 會使 drift exit code 為 `1`。可無損表達的缺漏欄位/index 會產生 shell-safe、預設 dry-run 的 `migrate` 提案,其餘情況升級至 `migration-review`。
929
+
579
930
  - **`orm-drift-review` agent task pack。** 工作流依序執行 blacklist 檢查、schema cache 更新與 ORM drift JSON 比對,並要求將 dry-run DDL 與精確目標交給獨立 migration review。
580
931
 
581
932
  ### Changed
582
933
 
583
934
  - **Schema identity 改為精確保存。** PostgreSQL schema/table 名稱不再正規化為小寫;quoted 與 unquoted identifier 依 SQL 規則解析,qualified name、ignore pattern、foreign key 與 drift output 都保留大小寫與 schema identity。
935
+
584
936
  - **ORM drift 文件完整同步。** 英文/繁體中文的 Markdown 與 HTML 使用者文件、skill assets、各平台 plugin 副本及 reference 已補上格式、exit code、安全邊界與操作流程。
937
+
585
938
  - **跨平台發版 metadata 對齊。** npm package、Codex/Claude/Cursor plugin、packaged Codex plugin 與 Gemini extension 統一為 `1.41.0`。
586
939
 
587
940
  ### Fixed
588
941
 
589
942
  - **Lossy ORM drift proposal 改為 fail closed。** Schema-qualified target、dash-leading positional、無法無損表達的 index column、identity collision 與不支援語法不再輸出可能損壞的指令,而是阻擋或升級人工審查。
943
+
590
944
  - **DDL/Prisma adapter identity 與語意硬化。** 多檔 DDL 共用 deterministic context,foreign key pairing、default schema resolution、table option/partition 阻擋、重複 index 去重與 Unicode code-point 穩定排序皆保留來源語意。
591
945
 
592
946
  ## [1.40.0] - 2026-07-19 - SQL Lint、安全強化與 Agent 工作流擴充
@@ -594,19 +948,25 @@ Automation that performs unqualified full-table writes stops working. Three shap
594
948
  ### Added
595
949
 
596
950
  - **新增唯讀 `dbcli lint` 靜態 SQL 顧問。** 支援 inline SQL、saved query、SQL 檔案與 glob/混合批次輸入,提供 text、JSON、Markdown 輸出、最低嚴重度篩選、`--no-schema` 與 `--recovery`;指令不連線、不執行 SQL,也不會自動套用 rewrite。
951
+
597
952
  - **九條結構與 schema-aware lint 規則。** 涵蓋 `SELECT *`、未錨定 `LIKE`、深度 `OFFSET`、non-sargable predicate、`OR`/subquery 改寫機會、重複 `DISTINCT` + `GROUP BY`、implicit cast,以及 `NOT IN` 右側 NULL 風險;finding 可附 confidence 標籤的草稿與 shell-safe 驗證指令。
953
+
598
954
  - **MongoDB agent task packs。** 新增 `mongo-safe-backfill` 與 `mongo-schema-drift-review`,補上 MongoDB 安全回填與 schema drift 檢視工作流。
599
955
 
600
956
  ### Changed
601
957
 
602
958
  - **Slow-query guide 納入 lint。** `guide slow-query` 現在會先安排本機靜態分析,再銜接 explain 與診斷 snippets,brief plan 也保留執行 metadata。
959
+
603
960
  - **Agent 與使用者文件完整同步。** `lint` 已寫入 skill assets、platform plugin 副本及英文/繁體中文 Markdown 與 HTML 文件;GitHub Pages 產品介紹頁同步完成雙語、可及性與行動裝置導覽重構。
961
+
604
962
  - **跨平台發版 metadata 對齊。** npm package、Codex/Claude/Cursor plugin、packaged Codex plugin 與 Gemini extension 統一為 `1.40.0`。
605
963
 
606
964
  ### Fixed
607
965
 
608
966
  - **Lint 採 fail-closed 安全邊界。** 解析失敗、schema binding 不明、identifier 大小寫碰撞、CTE/derived/qualified relation 與不安全 rewrite proof 會阻擋對應建議,不再借用不可靠的 cache facts。
967
+
609
968
  - **`NOT IN` NULL 分析補齊 scope 與 provenance。** 遞迴處理巢狀 SELECT、CTE、derived statement、JOIN `ON`、`WHERE`、`HAVING`、outer-join null extension、nullable 投影與 CASE/cast/aggregate,並保留正確 traversal order。
969
+
610
970
  - **Lint audit/recovery 遮蔽與驗證指令硬化。** positional、global、bulk 與 `--` 後的 SQL 都會遮蔽;只有結構上已證明唯讀的 SQL 才建議 `explain --analyze`,session assignment 與 function-bearing statement 會保守退回 plain explain。
611
971
 
612
972
  ## [1.39.2] - 2026-07-03 - Windows 跨平台、skill 安裝安全與 plugin 版本對齊
@@ -616,13 +976,17 @@ Automation that performs unqualified full-table writes stops working. Three shap
616
976
  ### Fixed
617
977
 
618
978
  - **Windows 跨平台修復(Windows CI 首次全綠)。** filesystem 操作與 path 檢查改為跨平台實作、修正 `emit` 子行程 import 與殘留的 path assertion,並以 portable `node:fs` 取代僅限 unix 的 coreutils spawns。此前 Windows job 從未通過(fail-fast 總是先取消它)。
979
+
619
980
  - **Skill 安裝安全強化。** 修正 output / install 旗標衝突、強化安裝安全檢查與 task 過濾條件。
981
+
620
982
  - **zh-TW skill 安裝不再被誤判為永遠過期。**
983
+
621
984
  - **Skill 參考修正。** 移除文件中不存在的 `blacklist add`、補回缺漏的 reference flags。
622
985
 
623
986
  ### Changed
624
987
 
625
988
  - **文件補齊。** 明示 `--where` 僅支援等值比較、補上 Redis / Elasticsearch 寫入模型說明、記錄 home-storage 綁定並重新同步 md/html parity、對齊 config-location-policy 與實作綁定模型。
989
+
626
990
  - **Plugin manifest 版本對齊。** `.claude-plugin` / `.cursor-plugin` / `.codex-plugin` 及 `plugins/dbcli-agent` 的 `plugin.json` 版本更新為 1.39.2(先前漂移在 1.37.1 / 1.31.0,未跟上主版本;`plugin:sync` / `plugin:check` 只同步 skill 內容不同步版本)。
627
991
 
628
992
  ### Internal
@@ -676,6 +1040,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
676
1040
  ### Added
677
1041
 
678
1042
  - **`dbcli verify rollback` 情境執行器(第三個內建 verify 情境)。** 透過已穩定的 scenario registry 註冊,以 preflight / after-write 兩種模式驗證「還原變更後資料庫是否回到預期的先前狀態」,且**永遠不執行**還原寫入 / DDL——只分析 `--statement` 並執行回讀斷言。以必填的 `--kind <ddl|dml>` 選擇還原語句文法:`ddl` 複用 `migration` 的單語句 `ALTER TABLE` 契約,`dml` 複用 `safe-backfill` 的 `UPDATE` plan 契約。安全邏輯完全複用兩個 sibling 情境的 classifier,無重複實作。artifact 沿用既有 subject kind(`ddl→migration`、`dml→backfill`)並以 `subject.command = 'verify rollback'` 記錄出處,因此 artifact schema 與版本不變。
1043
+
679
1044
  - **巢狀 bash / zsh / fish shell 補全。** 以遞迴 command-tree metadata model 從指令樹生成巢狀子指令與旗標補全,並由共用 registry 驅動 REPL 的補全與分派;補全會排除 denylisted 指令。
680
1045
 
681
1046
  ### Changed
@@ -687,8 +1052,11 @@ Automation that performs unqualified full-table writes stops working. Three shap
687
1052
  ### Added
688
1053
 
689
1054
  - **`dbcli verify safe-backfill` 情境執行器。** 以 preflight / after-write 兩種模式驗證安全回填工作流,並**永遠不執行回填寫入**:preflight 依序跑黑名單、schema、目標表與唯讀 verify-query 防護後回傳 `ready` / `blocked` 並印出精確的 after-write 指令;after-write 重跑防護、執行回讀斷言,並寫入 v1 `VerificationArtifact`(狀態對應 `verified` / `not_verified` / `indeterminate`,防護失敗為 `blocked`)。
1055
+
690
1056
  - **`dbcli verify migration` 情境執行器。** 對 schema migration 做 preflight / after-write 驗證,且**永遠不執行 DDL**:分析提案的 `ALTER TABLE`、跑唯讀防護、要求 DDL 目標與 `--table` 相符(schema-aware),after-write 後記錄 `migration` 主體的證據。MVP 僅接受單語句 `ALTER TABLE`,並阻擋 `CREATE TABLE` / `DROP TABLE` / `CREATE INDEX` 及多語句 DDL。
1057
+
691
1058
  - **`ALTER TABLE` 目標識別字契約。** `verify migration` 的目標擷取改用 quote-aware tokenizer:支援 `table` / `schema.table` / `catalog.schema.table`,每區段可為未加引號名稱或雙引號 / 反引號 / 方括號識別字(含 `""`、`]]` 跳脫),因此 `"user accounts"`、`"tenant-1"."orders"` 等含空白或連字號的名稱皆可接受。無法完整解析的目標(未封閉引號、不支援的跳脫、超過三段)會 fail closed 並以「目標無法解析」為由阻擋,與 `must match --table` 的不符原因明確區分。
1059
+
692
1060
  - **`verification summary --latest-only` 交接選項。** 於既有 summary 輸出之上額外回傳最新一筆有效 artifact,方便 agent 在交接時直接引用最新證據;無 artifact 時回傳 `latest: null` 並維持 exit 0,無效檔案不會被升入 `latest`。
693
1061
 
694
1062
  ### Changed
@@ -700,8 +1068,11 @@ Automation that performs unqualified full-table writes stops working. Three shap
700
1068
  ### Added
701
1069
 
702
1070
  - **`dbcli assert --write-verification-artifact` 橋接(opt-in)。** `assert` 的判定結果(verdict)現在可選擇性地寫成一份結果型 `VerificationArtifact`:透過 subject 解析器將斷言主體對應到 artifact 的 `subject`、依 pass/fail 對應驗證狀態,並以既有的原子寫入器落地於 `.dbcli/verification/`。省略旗標時行為完全不變、不寫入任何檔案;`safe-backfill-verify` 仍維持 plan-only。artifact 路徑一律相對於 cwd,與 `--config` 無關。
1071
+
703
1072
  - **唯讀 `verification` 指令介面(inspect + 生命週期)。** 新增核心 artifact 讀取器(含 schema 驗證、filter / summarize / find 輔助函式),並以此建構出 `verification list`(表格輸出,支援 subject-kind 篩選)、`verification show`、`verification summary` 等唯讀檢視指令,讓 agent 能直接讀取與彙整既有驗證證據,而非自行解析檔案。
1073
+
704
1074
  - **`verification prune` 保留期清理。** 依保留期(duration 解析)與全域 `--keep-latest` 規則挑選清理候選,全域 keep-latest 優先於各項篩選;具刪除安全防護(缺少 mtime 的檔案排除在外、預設 dry-run 預覽、`--execute` 才實際刪除),並在 execute 模式輸出 deleted / skipped 明細表。
1075
+
705
1076
  - **完整 v1 證據驗證。** 對 `subject` / `evidence` / 選用欄位進行完整驗證,並加入執行期 evidence-kind 防護,確保讀取與寫入兩端對 schema v1 的解讀一致。
706
1077
 
707
1078
  ## [1.34.0] - 2026-06-18 - Verification Artifact Writer
@@ -709,8 +1080,11 @@ Automation that performs unqualified full-table writes stops working. Three shap
709
1080
  ### Added
710
1081
 
711
1082
  - **驗證證據建構器(`buildVerificationArtifact`)。** 純函式,產生 schema v1 的 `VerificationArtifact`:可注入 `now` / `idFactory` 以利測試確定性、證據文字欄位上限 2000 字元(超過截斷並標註)、證據筆數上限 20(超過保留前 19 筆並補一筆 `manual` 截斷標記);拒絕非法狀態、空白 summary、空證據。集中化證據裁切,讓後續寫入器與指令介面不必各自重複截斷決策。
1083
+
712
1084
  - **`safe-backfill-verify` 計畫的「已規劃」驗證中繼資料。** `dbcli skill tasks plan safe-backfill-verify --format json` 現在輸出一個 `verification` 區塊(`status: "planned"`,取計畫中最後一個 `assert` 步驟作為證據)。此為**已規劃**證據,**不代表**驗證已執行或通過,與結果型 `VerificationArtifact` 明確區隔。其他 task pack 不受影響。
1085
+
713
1086
  - **驗證證據寫入器(`writeVerificationArtifact`)。** 將建構出的 artifact 以原子方式寫入 `.dbcli/verification/verification-<YYYYMMDD-HHMMSS>-<short-id>.json`:檔名完全由 artifact 內部產生(UTC 時間戳 + `[a-z0-9]` 淨化短 id,杜絕路徑穿越)、缺少目錄時自動建立、以 `link()` 獨佔建立確保不會靜默覆寫既有檔案、回傳寫入路徑。
1087
+
714
1088
  - **`recover --apply --write-verification-artifact`(opt-in)。** 僅在 verify 步驟實際執行時,將 recovery 驗證結果寫成一份 `recovery-verify` artifact(狀態取合約 `verificationStatus`,附 `recoveryRef`)。省略旗標時行為完全不變、不寫入任何檔案;寫入失敗只記到 stderr,不影響結束碼。保留既有 `verifyStatus`、不嵌入任何指令輸出或機密。
715
1089
 
716
1090
  ## [1.33.0] - 2026-06-18 - Workflow Pack Expansion
@@ -718,6 +1092,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
718
1092
  ### Added
719
1093
 
720
1094
  - **4 個新的 plan-only Agent Task Pack(皆唯讀)。** `pr-database-review`(PR 變更持久化路徑、查詢、migration 的資料庫風險審查)、`migration-review`(在套用 DDL 前擷取變更前 schema 證據並預覽 migration)、`safe-backfill-verify`(規劃安全 backfill 並產生 read-back `assert` 驗證指令)、`slow-endpoint-investigation`(串接 proxy / explain / missing-index 證據調查慢端點)。每個 pack 都以 `safety.mode: plan-only`、`risk: readonly` 步驟組成,只產生計畫、永不寫入;SQL 類 pack 先支援 `postgres` 與 `mysql`。
1095
+
721
1096
  - **Skill 路由更新(en / zh-TW)。** 在 `SKILL.md` 與 `SKILL.zh-TW.md` 的 Agent Task Packs 段落各加入一段精簡導引,讓 agent 在自行組合手動的審查、migration、backfill、效能流程前,先選擇對應的 workflow pack;已重新同步所有 plugin / platform skill 副本。
722
1097
 
723
1098
  ## [1.32.0] - 2026-06-18 - Agent Task Packs Expansion & Skill Parity Guards
@@ -725,8 +1100,11 @@ Automation that performs unqualified full-table writes stops working. Three shap
725
1100
  ### Added
726
1101
 
727
1102
  - **4 個新的內建 Agent Task Pack(皆 `plan-only` 唯讀)。** `audit-permissions`(權限等級與 blacklist 覆蓋稽核)、`safe-backfill`(在寫入前做 blacklist + schema + 風險檢查的回填計畫)、`schema-drift-review`(快取/committed schema 與線上 schema 的漂移比對)、`connection-health`(連線可達性 / 設定 / 容量分級三步診斷)。皆走確定存在的唯讀指令;用 `dbcli skill tasks list` 瀏覽完整清單。
1103
+
728
1104
  - **平台清單 parity 檢查(`scripts/check-platform-parity.ts`,`bun run platform:check`)。** 以 `SUPPORTED_PLATFORMS` 為單一真實來源,驗證 README、SKILL.md、SKILL.zh-TW.md、reference.md 與 CLI `--install` 選項描述的平台列舉完全一致(缺項或多項皆報錯),並掛進 `release-check.sh`。
1105
+
729
1106
  - **語意 parity 守門。** `scripts/check-skill-parity.ts` 在結構比對外,新增 14 個語言不變的安全/命令 token(`query`/`insert`/`update`/`delete`/`export`/`schema`、`blacklist`、`--dry-run`/`--no-limit`/`--recovery`、`LIMIT 1000`、三個權限等級)在 EN 與 zh-TW 皆須對稱出現的檢查。
1107
+
730
1108
  - **安裝與 context CLI 測試覆蓋。** 新增 `skill --install` 對 7 個平台寫入 temp HOME/cwd 的 smoke 測試(含 cursor/windsurf 的 root-rule + reference 雙檔結構),以及 `skill context` 的 xml/json/markdown、預設格式、無效格式與 blacklist 不外洩的 CLI 入口測試。
731
1109
 
732
1110
  ### Fixed
@@ -738,7 +1116,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
738
1116
  ### Added
739
1117
 
740
1118
  - **`@carllee1983/dbcli/core` 公開匯出 `DataExecutor` 與資料執行型別。** 在 `./core` barrel 開出資料編輯介面(insert/update/delete 執行面),讓外部消費者(如 `dbcli-gui` sidecar)能重用與 CLI 同源的資料寫入能力,不必重寫 adapter 邏輯。CLI 行為不變。
1119
+
741
1120
  - **Agent plugin 打包與 marketplace 安裝。** 將 dbcli 打包為 agent plugin(Ponytail 風格 marketplace install),新增 GitHub Copilot CLI plugin 支援與 Cursor plugin 安裝(add-plugin metadata、marketplace 提交路徑),並依各 agent 拆分安裝指令與文件。
1121
+
742
1122
  - **開發者工作流 skill 指引(en/zh-TW)。** 在 dbcli skill 新增「Developer workflows」段落,把資料庫影響隱含於開發任務時的最小安全路徑(DB-backed 功能、資料錯誤排查、ORM/migration、PR 審查、慢查詢、回填、環境驗證)寫入 SKILL en/zh-TW 與各平台副本,並以可執行的指令錨點取代不可執行的 migrate 範例。
743
1123
 
744
1124
  ## [1.30.0] - 2026-06-09 - Connection Writer API
@@ -750,6 +1130,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
750
1130
  ### Fixed
751
1131
 
752
1132
  - **`writeV2Config` 改為 atomic temp+rename 寫入**,避免寫入中斷時破壞設定庫。
1133
+
753
1134
  - **`migrateV1ToV2` 對非 SQL 的 v1 連線 fail-loud 拒絕**,防止把不相容連線寫進 v2 設定庫。
754
1135
 
755
1136
  ## [1.29.0] - 2026-06-08 - Core Config-Read Entrypoint
@@ -789,6 +1170,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
789
1170
  ### Added
790
1171
 
791
1172
  - **`dbcli snapshot <query>` — 結果指紋。** 將任一查詢結果轉成確定性、黑名單安全的 `ResultSnapshot`(`rowCount` + 每欄聚合:null/distinct 計數、min/max/sum、順序無關的 checksum)。預設落檔至 `.dbcli/snapshots/snap-<timestamp>.json`,亦支援 `--out`、`--stdout`、`--rows`(連同遮罩後的列一併存檔)、`--format`、`--no-limit`。
1173
+
792
1174
  - **`dbcli assert <query>` — 行內不變量檢查。** 三種模式:`--expect`(`rows > 0`、`value == 5000`、`col:email not null`、`col:id unique`、`col:amount between 0 and 100`、`col:age >= 18`)、`--vs <query> --compare rows|value`(跨查詢對帳)、`--against <snapshot> --tolerance <pct>`(對既有快照基準比對)。預設失敗時 `exit 1`,可用 `--no-fail` 僅報告不改變 exit code。
793
1175
  - 兩個指令均沿用既有 adapter / QueryExecutor / blacklist / audit 堆疊,黑名單欄位由 QueryExecutor 在源頭遮罩,指紋天生安全。目前支援 SQL 引擎(PostgreSQL / MySQL / MariaDB)。
794
1176
 
@@ -813,7 +1195,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
813
1195
  ### Added
814
1196
 
815
1197
  - **`dbcli explain` 一級指令。** 把 `EXPLAIN` / `ANALYZE SELECT` / `EXPLAIN (ANALYZE, BUFFERS) SELECT` 包成統一介面,單條 query、`@saved-query`、`@file.sql`、`@glob/*` 通吃。輸出統一的 `ExplainRow` schema,附 5 條 actionable annotations(`full-scan` / `temp-table` / `filesort` / `cost-estimate-skew` / `nested-loop-large`)。輸出格式 markdown(預設)/ json / table。支援 `--bulk` 多筆批次。MariaDB + MySQL + PostgreSQL。(v1.23 P2)
1198
+
816
1199
  - **`dbcli guide missing-index-for` 單條 query 複合索引顧問。** 解析一條 `SELECT`,結合真實 `EXPLAIN` 計畫與既有索引,輸出帶 `confidence`(high/medium/low)與 `reason` 的索引候選;偵測既有索引碰撞(single-col 可擴成 composite),並把函式/運算式欄位與無法解析的 SQL 列為 `warnings`。輸出格式 yaml(預設)/ json / markdown,支援 `--min-confidence` 過濾。唯讀(僅 EXPLAIN + 索引內省)。(v1.23 P3)
1200
+
817
1201
  - **`dbcli inspect` 情境感知 `suggestedCommands` 與新的 `hints` 欄位。** `suggestedCommands` 改為三層加權(bootstrap / context-aware / discovery):collector 讀近 10 條 audit 找出最熱門資料表,有 task pack 時自動建議 `skill tasks plan analyze-table-perf --param table=<table>` 與 `skill tasks list`。新增與 `suggestedCommands` 平行的 `hints` 欄位(JSON 機器可讀 + markdown `## Hints`),提示最熱門資料表、可用 task pack 數量與 schema 快取概況。新增內建 task pack `analyze-table-perf`(唯讀 `plan-only`,吃必填 `table` 參數)。audit 讀取唯讀且永不 throw。(v1.23 P4)
818
1202
 
819
1203
  ### Fixed
@@ -833,7 +1217,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
833
1217
  ### Added
834
1218
 
835
1219
  - **Elasticsearch interactive shell.** `dbcli shell` 對 ES 連線開啟 Kibana Dev Tools 風格 REPL:輸入請求行 `<METHOD> /<path>` 加上可選的多行 JSON body,以空白行送出整個區塊,回應以美化 JSON 呈現。以讀取為主 — index 層級黑名單於前端直接拒絕受保護 index;`_search` 若 body 未指定 `size` 自動上限 1000 筆。(P1)
1220
+
836
1221
  - **Elasticsearch export.** `dbcli export` 對 ES 連線支援兩種形式:傳入 search DSL 並以 `--index` 指定索引以匯出命中結果,或直接以 index 名稱當作查詢、透過 `match_all` + scroll 匯出整個索引。輸出 JSON / JSONL / CSV,預設上限 1000 筆(`--no-limit` 匯出全索引,以 scroll 分批串流)。匯出前套用索引層級黑名單檢查,並寫入稽核紀錄。(P2)
1222
+
837
1223
  - **Redis value / hash-field 遮罩。** 新增 `.dbcli` `redis.mask` 設定區塊:key 命中 `keyPattern` glob 者,其值(或指定的 hash `fields`)於讀取時(`GET`、`GETRANGE`、`HGETALL`、`HGET`、`HMGET`、`HVALS`)回傳 `[REDACTED]`。遮罩與既有 key-glob 拒絕黑名單並存,且**拒絕一律優先於遮罩**。(P3)
838
1224
 
839
1225
  ### Fixed
@@ -853,7 +1239,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
853
1239
  ### Added
854
1240
 
855
1241
  - **Redis shell.** `dbcli shell` 現對 Redis 連線開啟互動式 REPL,具備歷史、readline、tab 補全(指令 + key 前綴)與 `.no-limit on/off` meta 指令。單行語意。
1242
+
856
1243
  - **Redis size guard.** `SCAN` / `HSCAN` / `SSCAN` / `ZSCAN` 在缺少時補上 `COUNT 1000`;`LRANGE` / `ZRANGE` / `ZREVRANGE` 夾限 `stop`;`ZRANGEBYSCORE` 補上 `LIMIT 0 1000`。`HGETALL` / `HKEYS` / `HVALS` / `SMEMBERS` / `KEYS` 的無上限回覆在 client 端截斷至 1000 並帶 `REDIS_SIZE_TRUNCATE` 警告。`--no-limit` 略過所有防護。
1244
+
857
1245
  - **Redis blacklist 強制。** `dbcli blacklist add 'pattern'` 現會封鎖 key 命中的 Redis 讀寫。採 Redis 原生 glob(`*`、`?`、`[abc]`、`[a-z]`)。與黑名單重疊的 `KEYS` / `SCAN MATCH` 會被拒絕;未重疊的掃描則濾掉黑名單 keys 並帶 `REDIS_BLACKLIST_FILTERED` 警告。稽核記錄含 `metadata.rejection_reason: 'blacklist'` 與 `matched_pattern`。
858
1246
 
859
1247
  ### Changed
@@ -870,14 +1258,19 @@ Automation that performs unqualified full-table writes stops working. Three shap
870
1258
  ### Added
871
1259
 
872
1260
  - **MongoDB MVP 全套支援。** `q` 指令現以 limited-supported 等級納入 MongoDB(`find` / `aggregate` 兩種 snippet body),路由經過專屬分支與 field-masker;`schema` 採 `$sample` + 遞迴 path 偵測(含 BSON 型別),新增 `--sample-method` 旗標;`query` / `export` 套用 `maskMongoRows` 對巢狀結構遞迴遮罩。
1261
+
873
1262
  - **MongoDB blacklist 強化。** 新增 path-matcher(exact / dotted / suffix-wildcard)、field-masker 遞迴遮罩、insert / update 在寫入前強制套用 nested-path blacklist;`blacklist list` 對 collection 上的 middle-`*` pattern 發出警告。
1263
+
874
1264
  - **MongoDB 安全模型升級。** update operator 從硬性 allowlist 改為分級安全(tiered operator safety);schema 對 blacklist 欄位直接 redact;`cache` / `doctor` 暴露 `sampleMethod`。
1265
+
875
1266
  - **MongoDB snippets 一級公民化。** 內建 reference snippets(find + aggregate)、`queries list/search/suggest` 將 MongoDB snippets 與 SQL 引擎並列;`mongoStrategy` 驗證 body 與 params 並支援 map 形式插值。
1267
+
876
1268
  - **Recovery — per-code branching for connection codes (MVP)。** `recover --next` 對 connection 類錯誤碼支援多 branch 派發:新增 `buildConnectionBranches` factory(4 個 connection branch)、`matchConnectionBranch` resolver、`classify` emit `branches` / `branchFork`,並提供 `--branch <id>` 旗標讓 agent 顯式選擇 branch。輸出 `NextResult.branchId` 與 markdown 中的 branchId/description 一併呈現。
877
1269
 
878
1270
  ### Changed
879
1271
 
880
1272
  - **MongoDB `q` 文件升級。** `docs/feature-matrix.md` / 雙語 user docs 將 MongoDB `q` 從 unsupported 改為 limited supported(記載目前支援的 body 形式與限制)。
1273
+
881
1274
  - **Recovery schema 新增 `branches` / `branchFork`。** 行為向下相容(無 branch 時與舊版一致);`GuideStep` / `NextResult` / `NextStepOutput` 全鏈打通 `branchId`;`shellQuote` 抽離為共用模組。
882
1275
 
883
1276
  ### Security
@@ -908,7 +1301,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
908
1301
  ### Changed
909
1302
 
910
1303
  - **Phase 23-04 follow-up closure — full DML/DDL audit coverage.** `insert / update / delete / export / q / schema` now invoke `writeAuditEntry` on every happy / failure / rejection branch (BlacklistError / PermissionError / ConnectionError / validation all flow through the wired catch block). This closes the v1.20.0 INTEGRATE-01 / INTEGRATE-04 partial gap noted in v1.20.0's Known limitation paragraph.
1304
+
911
1305
  - **Bi-directional `audit_ref` ⇄ `recovery_ref` linkage on every `--recovery`-capable command.** When any of the 6 newly-wired commands fails with `--recovery`, the audit entry's `recovery_ref` and the recovery envelope's `audit_ref` carry matching UUIDs — identical in shape to the Phase 25 `query` / `inspect` round-trip wiring. Agents can pivot from `.dbcli/last-recovery.json` to the audit entry via `dbcli audit tail --recovery-ref <id>`.
1306
+
912
1307
  - **AI-agent skill docs (`assets/SKILL.md`, `assets/SKILL.zh-TW.md`, `assets/reference.md`)** updated to advertise full 8-command bi-directional coverage; bilingual user docs (`docs/user/en/index.{md,html}`, `docs/user/zh-TW/index.{md,html}`) gained a `--recovery` row noting the cross-command linkage.
913
1308
 
914
1309
  ### Tests
@@ -975,7 +1370,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
975
1370
  ### Added
976
1371
 
977
1372
  - **Expanded Antigravity Protocol**: Added Phase 0 (Scout) for research and Phase 3 (Auditor) for validation to the core agentic workflow.
1373
+
978
1374
  - **Enhanced Agent Support**: `dbcli skill --install` now supports **Codex (OMX)** and **Windsurf**.
1375
+
979
1376
  - **Cursor Rules Update**: `dbcli skill --install cursor` now uses the modern `.cursor/rules/*.mdc` project-local format.
980
1377
  - New `GEMINI.md` project-level instruction file with full Antigravity lifecycle guidance.
981
1378
 
@@ -1120,11 +1517,13 @@ Automation that performs unqualified full-table writes stops working. Three shap
1120
1517
  ### Fixed
1121
1518
 
1122
1519
  - **Packaged `dist/cli.mjs` 找不到 assets**:1.10.0 bundle 在 `task-paths.ts` / `snippet-paths.ts` 用 `import.meta.dir + ../../../` 解析 builtin 目錄,bundle 後三層往上會跳出 package root,npm 全域安裝的使用者執行 `dbcli queries list` / `dbcli skill tasks list` 讀不到資源。抽出 `src/utils/package-root.ts` 以 `package.json` 走訪定位 root,dev 與 bundle 都正確;`skill.ts` 內既有的 `findPackageRoot` 也收斂到同一處。
1520
+
1123
1521
  - **`dbcli q` 略過 blacklist 檢查(安全)**:`q.ts` 把空字串當作 `tableName` 傳給 `BlacklistValidator.filterColumns`,column-level redaction 永遠不命中;同時也沒呼叫 `checkTableBlacklist`,使用者可以透過 saved snippet 直接 SELECT 黑名單表/欄位繞開保護。改為從 `prepared.rewrittenSql` 抽出主表(SQL)或 `prepared.execHints.index`(ES),執行前先 `checkTableBlacklist('SELECT', target)`,並把真正的 `tableName` 餵給 `filterColumns`;Redis 維持原樣。
1124
1522
 
1125
1523
  ### Added
1126
1524
 
1127
1525
  - **dist/ 整合 smoke 測試**:`tests/integration/dist-smoke.test.ts` 從 OS tmpdir 執行 `dist/cli.mjs`,覆蓋 `--version`、`skill --output`、`queries list`、`skill tasks list`,守住 packaged assets path 不再回退。
1526
+
1128
1527
  - **`q` blacklist 迴歸測試**:`tests/unit/commands/q-blacklist.test.ts` 覆蓋黑名單表阻擋、欄位 redact、未受影響 snippet 三種情境。
1129
1528
 
1130
1529
  ### Changed
@@ -1154,7 +1553,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
1154
1553
  ### Changed
1155
1554
 
1156
1555
  - **Redis 驅動**:改用 Bun 內建 `RedisClient`,移除外部 `ioredis` 依賴。
1556
+
1157
1557
  - **Elasticsearch adapter**:refactor 並收斂錯誤訊息與 ExecutionResult 形狀,與 SQL / Mongo / Redis 對齊。
1558
+
1158
1559
  - **文件**:`assets/SKILL.md` 與 `assets/reference.md` 補上 ES / Redis snippet 工作流;`docs/feature-matrix.md` 更新 saved-queries 欄位。
1159
1560
 
1160
1561
  ### Fixed
@@ -1201,6 +1602,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1201
1602
  ### Fixed
1202
1603
 
1203
1604
  - **`insert` / `update` / `delete` / `export` / `diff` 對 Redis / Elasticsearch 的早期錯誤訊息**:先前會落入 SQL DataExecutor 出現「Column ... not found in table」之類誤導訊息,現在直接回傳明確的「不支援」JSON,並指引正確替代路徑(Redis 改用 `query`、Elasticsearch 改用外部工具或 `query --index`)。
1605
+
1204
1606
  - **TypeScript 嚴格度**:`bun run typecheck` 從 43 個錯誤降為 0。
1205
1607
  - `ConnectionConfig` union 加入 `ElasticsearchConnectionConfig`。
1206
1608
  - `ResolvedConnection.connection.system`、`ReplContext.system` 涵蓋 `'elasticsearch'`。
@@ -1229,6 +1631,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1229
1631
  - Discovery: Implemented schema inspection for MongoDB collections.
1230
1632
  - Diagnostics: Added comprehensive MongoDB environment and connection diagnostics to `dbcli doctor`.
1231
1633
  - **Improved AI Skill Installation**: `dbcli skill --install` now deploys both `SKILL.md` (high-level workflow) and `reference.md` (full command syntax and examples) to target platforms (Claude Code, Gemini CLI, Copilot, Cursor).
1634
+
1232
1635
  - **Security model enhancement**: `dbcli init` now defaults to a more secure storage model, placing sensitive connection details in `~/.config/dbcli/` rather than the local project workspace.
1233
1636
 
1234
1637
  ### Changed
@@ -1240,6 +1643,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1240
1643
  ### Fixed
1241
1644
 
1242
1645
  - **Doctor diagnostics for MongoDB SRV**: `dbcli doctor` now reports whether the current execution environment can resolve `mongodb+srv://` connections directly or only through the DNS-over-HTTPS fallback used by the MongoDB adapter.
1646
+
1243
1647
  - **Documentation**: Clarified the new MongoDB SRV environment diagnostic in README, README.zh-TW, and `assets/SKILL.md`.
1244
1648
 
1245
1649
  ## [1.5.1] - 2026-04-22
@@ -1247,6 +1651,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1247
1651
  ### Fixed
1248
1652
 
1249
1653
  - **MongoDB SRV Connections**: `mongodb+srv://` URIs are now expanded and connected through the MongoDB adapter, and MongoDB operations consistently use the configured database.
1654
+
1250
1655
  - **MongoDB Documentation**: Clarified SRV URI support and configured-database behavior in README, README.zh-TW, and `assets/SKILL.md`.
1251
1656
 
1252
1657
  ## [1.5.0] - 2026-04-21
@@ -1258,6 +1663,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1258
1663
  - Layered schema loading (Hot/Cold) integrated into `configModule`.
1259
1664
  - Per-connection isolation: Each connection now has its own schema directory (`.dbcli/schemas/<connection>/`).
1260
1665
  - **Improved Migration UX**: Added proactive hints during schema migration to ensure data consistency.
1666
+
1261
1667
  - **Documentation Update**: Added per-connection schema isolation details to `SKILL.md` for AI agents.
1262
1668
  - Clarified schema storage layout in `.dbcli/schemas/`.
1263
1669
  - Added usage examples for `--use <connection>` with schema commands.
@@ -1292,6 +1698,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1292
1698
  - Intelligent SQL generation per database dialect.
1293
1699
  - Default dry-run mode for safety.
1294
1700
  - **Enhanced Data Health Checks**: Added `rowCount` and `size` checks to the `dbcli check` command.
1701
+
1295
1702
  - **Comprehensive Documentation**: Updated README (en/zh-TW) with Internals & Strategy sections and new command references.
1296
1703
 
1297
1704
  ### Changed
@@ -1307,7 +1714,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
1307
1714
  ### Changed
1308
1715
 
1309
1716
  - **Adapter `execute()` 回傳型別重構**: 從 `T[]` 改為 `ExecutionResult<T>`,包含 `rows`、`affectedRows`、`lastInsertId` 欄位,DML 操作(INSERT/UPDATE/DELETE)現在回傳正確的 affected rows 計數
1717
+
1310
1718
  - **Export 覆寫確認**: `export --output` 寫入已存在檔案時會提示確認,可用 `--force` 跳過
1719
+
1311
1720
  - **`ExecutionResult<T>` 介面**: 新增統一的查詢結果型別定義於 `src/adapters/types.ts`
1312
1721
 
1313
1722
  ---
@@ -1319,7 +1728,9 @@ Automation that performs unqualified full-table writes stops working. Three shap
1319
1728
  dbcli v1.0.0 is the first stable release. All three milestones are complete:
1320
1729
 
1321
1730
  - **M1 (v0.6.0):** Smart REPL — interactive shell with SQL + dbcli commands
1731
+
1322
1732
  - **M2 (v0.8.0):** Schema DDL — CREATE/DROP/ALTER TABLE, INDEX, CONSTRAINT, ENUM
1733
+
1323
1734
  - **M3 (v1.0.0):** Stabilization — documentation, permission matrix, known limitations update
1324
1735
 
1325
1736
  ### Added
@@ -1335,22 +1746,31 @@ dbcli v1.0.0 is the first stable release. All three milestones are complete:
1335
1746
  - PostgreSQL: SERIAL, native ENUM types, ALTER COLUMN TYPE, double-quote identifiers
1336
1747
  - MySQL: AUTO_INCREMENT, inline ENUM, MODIFY COLUMN, backtick identifiers
1337
1748
  - **DDLExecutor**: Unified execution pipeline — admin permission check → blacklist protection → SQL generation → dry-run/execute → schema cache auto-refresh
1749
+
1338
1750
  - **Default dry-run for DDL**: All `migrate` commands preview SQL without `--execute`. Destructive operations also require `--force`
1751
+
1339
1752
  - **142 new tests**: column-parser (17), PG DDL (35), MySQL DDL (25), factory (5), DDL executor (22), schema cache DDL (6), CLI migrate (26), live-db migrate lifecycle (6)
1340
1753
 
1341
1754
  ### Fixed
1342
1755
 
1343
1756
  - **Schema comment encoding**: Fixed double-encoded UTF-8 comments from MySQL/MariaDB `information_schema` (e.g., `帳號` → `帳號`)
1757
+
1344
1758
  - **MySQL connection charset**: Added `charset: utf8mb4` and `SET NAMES utf8mb4`
1759
+
1345
1760
  - **DDL multi-line SQL execution**: Fixed statement splitting to use `;\n` instead of `\n`
1761
+
1346
1762
  - **MySQL DROP INDEX**: Added `--table` option (MariaDB requires `ON <table>`)
1347
1763
 
1348
1764
  ### Changed
1349
1765
 
1350
1766
  - **Permission model**: 4 levels — query-only, read-write, data-admin, admin (DDL requires admin)
1767
+
1351
1768
  - **Known Limitations**: Removed "Read-only schema" and "CLI-only" (both resolved). Added "No migration version tracking" as post-v1.0 item
1769
+
1352
1770
  - **Test infrastructure**: `docker-compose.test.yml` for MySQL 8 + PostgreSQL 16 integration testing
1771
+
1353
1772
  - **Package scripts**: Added `test:unit`, `test:integration`, `test:docker`
1773
+
1354
1774
  - **SKILL.md**: Updated with full `migrate` command reference and AI agent guidelines
1355
1775
 
1356
1776
  ### Test Results (v1.0.0)
@@ -1368,14 +1788,19 @@ dbcli v1.0.0 is the first stable release. All three milestones are complete:
1368
1788
  ### Fixed
1369
1789
 
1370
1790
  - **Schema comment encoding**: Fixed double-encoded UTF-8 comments from MySQL/MariaDB `information_schema`. Comments stored through latin1 (cp1252) connections now correctly display CJK characters (e.g., `帳號` → `帳號`)
1791
+
1371
1792
  - **MySQL connection charset**: Added `charset: utf8mb4` and `SET NAMES utf8mb4` to MySQL adapter connections
1372
1793
 
1373
1794
  ### Added
1374
1795
 
1375
1796
  - **`fixDoubleEncodedUtf8()` utility** (`src/utils/encoding.ts`): Detects and reverses cp1252-to-UTF-8 double encoding with full cp1252 reverse mapping table. Applied to schema comments in both MySQL and PostgreSQL adapters
1797
+
1376
1798
  - **`docker-compose.test.yml`**: MySQL 8.4 (port 3307) + PostgreSQL 16 (port 5433) for integration testing, with health checks and tmpfs for fast ephemeral storage
1799
+
1377
1800
  - **Environment-driven adapter tests**: `mysql.test.ts` and `postgresql.test.ts` now read connection from `MYSQL_*` / `PG_*` env vars, falling back to docker-compose defaults. Auto-skip when DB is unreachable
1801
+
1378
1802
  - **`live-db.test.ts`**: 55 comprehensive CLI-level integration tests covering all commands against live MariaDB — list, schema, query, blacklist CRUD, insert/update/delete lifecycle, export, check, diff, status, doctor, shell, format validation, SQL injection protection
1803
+
1379
1804
  - **New test scripts**: `test:unit`, `test:integration`, `test:docker` in package.json
1380
1805
 
1381
1806
  ### Test Results
@@ -1393,16 +1818,27 @@ dbcli v1.0.0 is the first stable release. All three milestones are complete:
1393
1818
  ### Added
1394
1819
 
1395
1820
  - **`dbcli shell` command:** Interactive database shell with SQL execution and dbcli command dispatch
1821
+
1396
1822
  - **SQL-only mode:** `--sql` flag restricts to SQL statements only
1823
+
1397
1824
  - **Auto-completion (Tab):** Context-aware completion for SQL keywords, table names, column names, and dbcli commands
1825
+
1398
1826
  - **Multi-line SQL:** Accumulates input until `;` is found, with `...>` continuation prompt
1827
+
1399
1828
  - **SQL syntax highlighting:** Real-time colorization of keywords, strings, and numbers in verbose mode
1829
+
1400
1830
  - **Meta commands:** `.help`, `.quit`/`.exit`, `.clear`, `.format`, `.history`, `.timing`
1831
+
1401
1832
  - **Persistent history:** Stored in `~/.dbcli_history` (max 1000 entries), with up/down navigation and Ctrl+R search
1833
+
1402
1834
  - **Permission & blacklist integration:** Full enforcement within REPL session — SQL goes through PermissionGuard, query results go through blacklist filtering
1835
+
1403
1836
  - **Auto-reconnect:** Attempts to reconnect once on connection errors, then displays error without crashing the session
1837
+
1404
1838
  - **Error resilience:** SQL/permission/connection errors never crash the session
1839
+
1405
1840
  - **i18n support:** All shell messages available in English and Traditional Chinese
1841
+
1406
1842
  - **102 new tests:** input-classifier (25), multiline-buffer (10), meta-commands (15), completer (17), history-manager (8), command-dispatcher (12), repl-engine (12), shell-command (3)
1407
1843
 
1408
1844
  ---
@@ -1412,9 +1848,13 @@ dbcli v1.0.0 is the first stable release. All three milestones are complete:
1412
1848
  ### Fixed
1413
1849
 
1414
1850
  - **`init --use-env-refs` permission bug**: Interactive env-ref mode now correctly offers all 4 permission levels (was missing `data-admin`)
1851
+
1415
1852
  - **`init` i18n completeness**: All 10 hardcoded English messages replaced with i18n keys (supports en/zh-TW)
1853
+
1416
1854
  - **`init` duplicate code**: Extracted shared `.dbcli exists` overwrite check into `checkOverwrite()` helper
1855
+
1417
1856
  - **`--use-env-refs` help text**: Improved option description to clarify CI/CD and multi-env use case
1857
+
1418
1858
  - **Documentation**: Added `--use-env-refs` to README (en/zh-TW), CHANGELOG, and SKILL.md with AI agent guidance
1419
1859
 
1420
1860
  ---
@@ -1424,13 +1864,17 @@ dbcli v1.0.0 is the first stable release. All three milestones are complete:
1424
1864
  ### Added
1425
1865
 
1426
1866
  - **Database version check**: Warns on stderr when connected database version is below minimum supported (PostgreSQL 12+, MySQL 8.0+, MariaDB 10.5+). Non-blocking — connection proceeds normally.
1867
+
1427
1868
  - **`dbcli doctor` DB version check**: New "Database version" item in Connection & Data group.
1869
+
1428
1870
  - **`dbcli init --use-env-refs`**: Store environment variable references (`{"$env": "DB_HOST"}`) in config instead of actual values. Supports interactive and non-interactive modes with `--env-host`, `--env-port`, `--env-user`, `--env-password`, `--env-database` options. Suitable for CI/CD and multi-environment deployments.
1429
1871
 
1430
1872
  ### Fixed
1431
1873
 
1432
1874
  - **`init` permission bug**: Interactive env-ref mode now correctly offers all 4 permission levels (was missing `data-admin`)
1875
+
1433
1876
  - **`init` i18n**: All hardcoded English messages in init command replaced with i18n keys (10 messages)
1877
+
1434
1878
  - **`init` duplicate code**: Extracted shared `.dbcli exists` overwrite check into `checkOverwrite()` helper
1435
1879
 
1436
1880
  ---
@@ -1442,16 +1886,27 @@ dbcli v1.0.0 is the first stable release. All three milestones are complete:
1442
1886
  ### Added
1443
1887
 
1444
1888
  - **Color system** (`picocolors`): Semantic color helpers (`success`/`error`/`warn`/`info`/`dim`/`bold`) with automatic `NO_COLOR` support
1889
+
1445
1890
  - **SQL syntax highlighting**: Keywords (blue bold), strings (green), numbers (yellow) — applied in verbose mode and dry-run preview
1891
+
1446
1892
  - **Leveled logger**: Four levels — quiet (`-q`), normal (default), verbose (`-v`), debug (`-vv`) — all output to stderr to keep stdout clean for structured data
1893
+
1447
1894
  - **`--no-color` global flag**: Disable colored output; also respects `NO_COLOR` environment variable (<https://no-color.org/>)
1895
+
1448
1896
  - **`-v, --verbose` global flag**: Increase verbosity (`-v` = verbose, `-vv` = debug)
1897
+
1449
1898
  - **`-q, --quiet` global flag**: Suppress non-essential output
1899
+
1450
1900
  - **`dbcli doctor` command**: Full self-diagnostic — checks Bun version, dbcli version (npm registry), config validity, permission level, blacklist completeness (detects unprotected sensitive columns like `password`/`token`/`secret`), database connectivity, schema cache freshness, and large table warnings (> 1M rows). Supports `--format json` for AI agents. Exits with code 1 on errors.
1901
+
1451
1902
  - **`dbcli completion` command**: Shell auto-completion script generation for bash, zsh, and fish. `--install` flag auto-writes to the shell rc file using idempotent marker blocks.
1903
+
1452
1904
  - **`dbcli upgrade` command**: Self-update from npm registry. `--check` flag for check-only mode.
1905
+
1453
1906
  - **Background version check**: Every command silently checks the npm registry (at most once per 24 hours, cached in `.dbcli/version-check.json`). Shows a one-line hint to stderr after the command completes if a newer version is available. Suppressed by `--quiet`.
1907
+
1454
1908
  - **Table formatter colorization**: Table headers now display in bold
1909
+
1455
1910
  - **62 new tests**: colors (7), sql-highlight (6), logger (10), doctor (12), completion (8), upgrade/version-check (19)
1456
1911
 
1457
1912
  ### Dependencies
@@ -1473,12 +1928,19 @@ Added table and column-level blacklisting to protect sensitive data from AI agen
1473
1928
  - `blacklist table add/remove <table>` — manage table-level blacklist
1474
1929
  - `blacklist column add/remove <table>.<column>` — manage column-level blacklist
1475
1930
  - **Table-level blacklisting:** Reject all operations (query, insert, update, delete) on blacklisted tables
1931
+
1476
1932
  - **Column-level blacklisting:** Automatically omit blacklisted columns from SELECT results
1933
+
1477
1934
  - **Security notifications:** Footer in table/CSV/JSON output when columns are filtered (e.g., "Security: 2 column(s) were omitted based on your blacklist")
1935
+
1478
1936
  - **Context-aware override:** `DBCLI_OVERRIDE_BLACKLIST=true` environment variable for temporary bypass with warning
1937
+
1479
1938
  - **i18n support:** Blacklist messages in English and Traditional Chinese
1939
+
1480
1940
  - **Performance:** < 1ms overhead per query (O(1) Set/Map lookups)
1941
+
1481
1942
  - **103 new tests:** 83 core + 12 CLI wiring + 8 formatter security tests
1943
+
1482
1944
  - **`dbcli schema --reset`:** Clear all existing schema data and re-fetch from database — solves stale schema after switching DB connections
1483
1945
 
1484
1946
  ### Configuration
@@ -1513,9 +1975,13 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1513
1975
  ### Phase 1: Project Scaffold
1514
1976
 
1515
1977
  - **Foundation established:** CLI framework with Commander.js v13.0+
1978
+
1516
1979
  - **Build process:** Bun bundler with native TypeScript support (1.1MB binary, <100ms startup)
1980
+
1517
1981
  - **Test infrastructure:** Vitest with 80%+ coverage target
1982
+
1518
1983
  - **Cross-platform CI:** GitHub Actions matrix testing (ubuntu, macos, windows)
1984
+
1519
1985
  - **Code quality:** ESLint + Prettier configured
1520
1986
 
1521
1987
  **Status:** ✅ Complete
@@ -1525,10 +1991,15 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1525
1991
  ### Phase 2: Init & Config
1526
1992
 
1527
1993
  - **`dbcli init` command:** Interactive configuration with `.env` parsing
1994
+
1528
1995
  - **Hybrid initialization:** Auto-fills from .env, prompts only for missing values
1996
+
1529
1997
  - **Config management:** `.dbcli` JSON file with immutable copy-on-write semantics
1998
+
1530
1999
  - **Database support preparation:** Multi-database adapter layer foundation
2000
+
1531
2001
  - **RFC 3986 percent-decoding:** Handles special characters in DATABASE_URL passwords
2002
+
1532
2003
  - **Validation:** Zod schemas for type-safe configuration
1533
2004
 
1534
2005
  **Status:** ✅ Complete
@@ -1540,9 +2011,13 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1540
2011
  ### Phase 3: DB Connection
1541
2012
 
1542
2013
  - **Multi-database support:** PostgreSQL, MySQL, MariaDB via unified adapter interface
2014
+
1543
2015
  - **Bun.sql integration:** Native SQL API (zero npm dependencies for drivers)
2016
+
1544
2017
  - **Connection testing:** Validates credentials before saving config
2018
+
1545
2019
  - **Error mapping:** Categorized error messages with troubleshooting hints (5 categories: ECONNREFUSED, ETIMEDOUT, AUTH_FAILED, ENOTFOUND, UNKNOWN)
2020
+
1546
2021
  - **Adapter pattern:** Clean abstraction enabling driver swaps without CLI changes
1547
2022
 
1548
2023
  **Status:** ✅ Complete
@@ -1554,9 +2029,13 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1554
2029
  ### Phase 4: Permission Model
1555
2030
 
1556
2031
  - **Three-tier permission system:** Query-only, Read-Write, Admin
2032
+
1557
2033
  - **SQL classification:** Character state machine for robust SQL analysis (handles comments, strings, CTEs, subqueries)
2034
+
1558
2035
  - **Permission enforcement:** Coarse-grained checks (no per-table/column fine-grained control in V1)
2036
+
1559
2037
  - **Default-deny approach:** Uncertain operations require Admin mode
2038
+
1560
2039
  - **Zero external dependencies:** Pure TypeScript string processing
1561
2040
 
1562
2041
  **Status:** ✅ Complete
@@ -1568,10 +2047,15 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1568
2047
  ### Phase 5: Schema Discovery
1569
2048
 
1570
2049
  - **`dbcli list` command:** Display all tables with metadata
2050
+
1571
2051
  - **`dbcli schema [table]` command:** Show single table structure or scan entire database
2052
+
1572
2053
  - **Foreign key extraction:** PostgreSQL FK metadata from pg_stat_user_tables; MySQL from REFERENTIAL_CONSTRAINTS
2054
+
1573
2055
  - **Output formatters:** Table (ASCII) and JSON (AI-parseable)
2056
+
1574
2057
  - **Schema storage:** Complete metadata in `.dbcli` for offline AI reference
2058
+
1575
2059
  - **Column details:** Type, constraints, nullable, defaults, primary keys, foreign keys
1576
2060
 
1577
2061
  **Status:** ✅ Complete
@@ -1585,10 +2069,15 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1585
2069
  ### Phase 6: Query Operations
1586
2070
 
1587
2071
  - **`dbcli query "SQL"` command:** Direct SQL execution with permission enforcement
2072
+
1588
2073
  - **Output formatters:** Table (human-readable), JSON (AI-parseable), CSV (RFC 4180 compliant)
2074
+
1589
2075
  - **Auto-limiting:** Query-only mode limits to 1000 rows (with user notification)
2076
+
1590
2077
  - **Helpful errors:** Levenshtein distance table suggestions for typos
2078
+
1591
2079
  - **Structured results:** Metadata including row count, execution time, columns
2080
+
1592
2081
  - **Permission guarding:** Blocks write operations in Query-only/Read-Write modes
1593
2082
 
1594
2083
  **Status:** ✅ Complete
@@ -1604,11 +2093,17 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1604
2093
  ### Phase 7: Data Modification
1605
2094
 
1606
2095
  - **`dbcli insert [table]` command:** Insert rows with parameterized queries
2096
+
1607
2097
  - **`dbcli update [table]` command:** Update existing rows with WHERE clause and SET columns
2098
+
1608
2099
  - **`dbcli delete [table]` command:** Delete rows (Admin-only for safety)
2100
+
1609
2101
  - **Parameterized SQL:** Prevents SQL injection across all modification commands
2102
+
1610
2103
  - **Confirmation flows:** --force flag for bypass; default prompts user
2104
+
1611
2105
  - **Dry-run mode:** `--dry-run` shows SQL without executing
2106
+
1612
2107
  - **Permission enforcement:** Insert/Update require Read-Write+; Delete requires Admin
1613
2108
 
1614
2109
  **Status:** ✅ Complete
@@ -1622,11 +2117,17 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1622
2117
  ### Phase 8: Schema Refresh & Export
1623
2118
 
1624
2119
  - **`dbcli schema --refresh` command:** Detect and apply schema changes incrementally
2120
+
1625
2121
  - **`dbcli export "SQL"` command:** Export query results as JSON or CSV
2122
+
1626
2123
  - **SchemaDiffEngine:** Two-phase diff algorithm (table-level, column-level)
2124
+
1627
2125
  - **Type normalization:** Case-insensitive comparison for column types
2126
+
1628
2127
  - **Immutable merge:** Preserves metadata.createdAt, updates schemaLastUpdated
2128
+
1629
2129
  - **Streaming output:** CSV generated line-by-line; JSON buffered for validity
2130
+
1630
2131
  - **File output:** `--output file` support for both export and schema refresh
1631
2132
 
1632
2133
  **Status:** ✅ Complete
@@ -1640,11 +2141,17 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1640
2141
  ### Phase 9: AI Integration
1641
2142
 
1642
2143
  - **`dbcli skill` command:** Generate AI-consumable skill documentation
2144
+
1643
2145
  - **SkillGenerator class:** Runtime CLI introspection (collects commands dynamically)
2146
+
1644
2147
  - **Permission-based filtering:** Query-only hides insert/update/delete; Read-Write hides delete
2148
+
1645
2149
  - **SKILL.md format:** YAML frontmatter + markdown (compatible with Claude Code, Gemini, Copilot, Cursor)
2150
+
1646
2151
  - **Platform installation:** `dbcli skill --install {claude|gemini|copilot|cursor}`
2152
+
1647
2153
  - **Cross-platform paths:** Installs to correct location per platform (.claude/, .local/share/gemini/, etc.)
2154
+
1648
2155
  - **Dynamic updates:** Skill regenerates as CLI evolves; no manual documentation maintenance
1649
2156
 
1650
2157
  **Status:** ✅ Complete
@@ -1658,9 +2165,13 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1658
2165
  ### Phase 10: Polish & Distribution
1659
2166
 
1660
2167
  - **npm publication:** `files` whitelist, `engines` constraints, `prepublishOnly` hook
2168
+
1661
2169
  - **Cross-platform validation:** Windows CI matrix with .cmd wrapper verification
2170
+
1662
2171
  - **Comprehensive documentation:** API reference, permission model, AI guide, troubleshooting
2172
+
1663
2173
  - **Performance benchmarking:** CLI startup < 200ms, query overhead < 50ms
2174
+
1664
2175
  - **Release readiness:** v1.0.0 quality gates met, all requirements satisfied
1665
2176
 
1666
2177
  **Status:** ✅ Complete
@@ -1670,7 +2181,9 @@ dbcli v0.1.0-beta is a complete, production-ready CLI tool enabling AI agents an
1670
2181
  ## Known Limitations
1671
2182
 
1672
2183
  - **Single database per project:** Each directory uses one `.dbcli` config. For multi-database setups, use separate directories or `--config` flag. This is by design, not a technical limitation.
2184
+
1673
2185
  - **No audit logging:** WHO/WHAT/WHEN tracking deferred to post-v1.0
2186
+
1674
2187
  - **No migration version tracking:** `migrate` commands execute DDL directly without version history or rollback. The `migrate` namespace is reserved for future migration tracking support.
1675
2188
 
1676
2189
  ---