@carllee1983/dbcli 8.0.0 → 9.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbcli-agent",
3
- "version": "8.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "Database CLI skill and command reference for AI agents.",
5
5
  "author": {
6
6
  "name": "Carl Lee",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbcli-agent",
3
- "version": "8.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "Database CLI skill and command reference for AI agents",
5
5
  "author": {
6
6
  "name": "Carl Lee",
@@ -3137,7 +3137,7 @@ dbcli skill --install codex # install to ~/.codex/skills/dbcli/
3137
3137
  **Options:**
3138
3138
  - `--install <platform>` — `claude` | `gemini` | `antigravity` | `copilot` | `cursor` | `codex` | `windsurf`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
3139
3139
  - `--output <path>` — write `SKILL.md` to a file instead of stdout. Does not install `reference.md`.
3140
- - `--lang <en|zh-TW>` — source language for the emitted SKILL content (default `en`). It selects `assets/SKILL.md` vs `assets/SKILL.zh-TW.md`; the install/output filename stays `SKILL.md` regardless.
3140
+ - Skill content is always the canonical English `assets/SKILL.md`; the install/output filename is `SKILL.md`.
3141
3141
 
3142
3142
  **Notes:**
3143
3143
  - Both files come straight from `assets/SKILL.md` + `assets/reference.md` inside the dbcli package — no runtime rendering. Keep these in sync when shipping a release.
@@ -3872,7 +3872,7 @@ GET /orders/_search
3872
3872
 
3873
3873
  - Enter a request line `<METHOD> /<path>`, then an optional multi-line JSON body; a **blank line** submits the block. Responses render as pretty-printed JSON.
3874
3874
  - **The connection's permission tier applies, as it does to `dbcli query`.** The request is classified through the same classifier the query path uses, on the path the server will actually route — the query string is discarded and percent-encoding and dot segments are resolved first, so `?filter_path=...` cannot change the verdict.
3875
- - **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/0014-elasticsearch-reads-are-an-allowlist.md).
3875
+ - **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/ADR-0014-elasticsearch-reads-are-an-allowlist.md).
3876
3876
  - A path is refused unless it is **byte-identical** to what the URL parser produces — the same parser the request goes through — and the refusal tells you the canonical spelling to write instead. This is what stops a path that reads one way here and routes another way at the server: `#` truncates a path in transit (`POST /_reindex#/_count` reaches Elasticsearch as `POST /_reindex`), and tab, newline and `\` are rewritten too. A document id containing a space or a non-ASCII character must be written percent-encoded; the error message gives you the exact string.
3877
3877
  - A quoted string request body is refused; write the body as JSON. So is a `source=` query parameter — Elasticsearch accepts it in place of a body, where none of the body checks can see it. `_source`, `_source_includes` and `_source_excludes` are unaffected.
3878
3878
  - Blacklisted field names are refused in the query string as well as the body, because the URI-search form names fields directly (`?q=…`, `?sort=…`, `?docvalue_fields=…`). A name matches on any dot component, so `password.keyword` — the multi-field every `text` field gets by default — and `params._source.password` inside a script are both refused.
@@ -2,7 +2,7 @@
2
2
  "name": "dbcli-agent",
3
3
  "displayName": "dbcli Agent",
4
4
  "description": "Database CLI skill and command reference for AI agents.",
5
- "version": "8.0.0",
5
+ "version": "9.1.0",
6
6
  "author": {
7
7
  "name": "Carl Lee",
8
8
  "url": "https://github.com/CarlLee1983"
@@ -3137,7 +3137,7 @@ dbcli skill --install codex # install to ~/.codex/skills/dbcli/
3137
3137
  **Options:**
3138
3138
  - `--install <platform>` — `claude` | `gemini` | `antigravity` | `copilot` | `cursor` | `codex` | `windsurf`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
3139
3139
  - `--output <path>` — write `SKILL.md` to a file instead of stdout. Does not install `reference.md`.
3140
- - `--lang <en|zh-TW>` — source language for the emitted SKILL content (default `en`). It selects `assets/SKILL.md` vs `assets/SKILL.zh-TW.md`; the install/output filename stays `SKILL.md` regardless.
3140
+ - Skill content is always the canonical English `assets/SKILL.md`; the install/output filename is `SKILL.md`.
3141
3141
 
3142
3142
  **Notes:**
3143
3143
  - Both files come straight from `assets/SKILL.md` + `assets/reference.md` inside the dbcli package — no runtime rendering. Keep these in sync when shipping a release.
@@ -3872,7 +3872,7 @@ GET /orders/_search
3872
3872
 
3873
3873
  - Enter a request line `<METHOD> /<path>`, then an optional multi-line JSON body; a **blank line** submits the block. Responses render as pretty-printed JSON.
3874
3874
  - **The connection's permission tier applies, as it does to `dbcli query`.** The request is classified through the same classifier the query path uses, on the path the server will actually route — the query string is discarded and percent-encoding and dot segments are resolved first, so `?filter_path=...` cannot change the verdict.
3875
- - **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/0014-elasticsearch-reads-are-an-allowlist.md).
3875
+ - **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/ADR-0014-elasticsearch-reads-are-an-allowlist.md).
3876
3876
  - A path is refused unless it is **byte-identical** to what the URL parser produces — the same parser the request goes through — and the refusal tells you the canonical spelling to write instead. This is what stops a path that reads one way here and routes another way at the server: `#` truncates a path in transit (`POST /_reindex#/_count` reaches Elasticsearch as `POST /_reindex`), and tab, newline and `\` are rewritten too. A document id containing a space or a non-ASCII character must be written percent-encoded; the error message gives you the exact string.
3877
3877
  - A quoted string request body is refused; write the body as JSON. So is a `source=` query parameter — Elasticsearch accepts it in place of a body, where none of the body checks can see it. `_source`, `_source_includes` and `_source_excludes` are unaffected.
3878
3878
  - Blacklisted field names are refused in the query string as well as the body, because the URI-search form names fields directly (`?q=…`, `?sort=…`, `?docvalue_fields=…`). A name matches on any dot component, so `password.keyword` — the multi-field every `text` field gets by default — and `params._source.password` inside a script are both refused.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,109 @@ 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
+ ## [9.1.0] - 2026-09-11
9
+
10
+ ForgeFlow Story DBCLI-014 到 DBCLI-036 在 `v9.0.0` 之後交付。其中只有最後三張
11
+ (DBCLI-034、035、036)改動使用者看得到的行為——SQLite 成為第六種 engine;其餘
12
+ 二十張全部落在 ForgeFlow/ForgePilot 的工作控制面、`make verify` 的證據與效能門檻上,
13
+ 照 8.0.0 處理 DBCLI-013 的方式,收在 Changed 底下。
14
+
15
+ 版本為 **9.1.0**。新增一種 engine 是 MINOR;本區間沒有任何一處拒絕先前被接受的輸入、
16
+ 改變既有 JSON 欄位的意義,或移除旗標與指令。三張 SQLite Story 各自帶一條回歸驗收
17
+ (DBCLI-034 AC-012、DBCLI-035 AC-010、DBCLI-036 AC-012/013),要求既有 fixture 的判決
18
+ 一字不變,而不是「大致相容」。
19
+
20
+ ### Added
21
+
22
+ - **SQLite 成為第六種 engine:一個檔案就是一條連線。** `list`、`schema`、`query`、`q`、
23
+ `export`、`queries` 走的是其他 SQL engine 同一條權限階梯、同一份 blacklist 與同一套
24
+ audit 規則,`--use a,b` 扇出同樣可用。連線設定只有 `file` 一個欄位有意義——沒有 host、
25
+ port、user、password,因為存取控制來自作業系統的檔案權限,而不是網路上的一次認證。
26
+
27
+ `query-only` 由 engine 執行,不是由 dbcli 代為保證。PostgreSQL 與 MySQL 把語句放進
28
+ read-only transaction,SQLite 沒有這種 transaction,所以 dbcli 直接以
29
+ `SQLITE_OPEN_READONLY` 開檔——一個根本沒有寫入通道的 handle。代價講明:帶著未重放
30
+ write-ahead log 的資料庫開不成唯讀,因為重放本身就是寫入;dbcli 會指名 WAL,而不是把
31
+ `SQLITE_CANTOPEN` 原樣丟出來。(DBCLI-034、ADR-0037)
32
+
33
+ dbcli 不會替你建立資料庫檔。路徑不存在、路徑存在但不是 SQLite 資料庫,都在連線時
34
+ 失敗並指名原因;`:memory:`、`file::memory:` 與任何帶 `mode=memory` 的路徑則更早,
35
+ 在設定解析時就被拒絕——連線是 blacklist、audit log 與 schema cache 共同綁定的身分,
36
+ 而一個記憶體資料庫每次呼叫都是不同的資料庫,這種身分撐不住。(ADR-0038)
37
+
38
+ - **SQLite 專屬語句有了判決,寫入路徑打通。** `insert`、`update`、`delete` 在與其他 SQL
39
+ engine 相同的階梯與 `--dry-run` 下對 SQLite 檔案運作,產生的 SQL 使用 SQLite 標準的
40
+ 雙引號識別字與 `?` 佔位符。`REPLACE INTO` 要求 `data-admin` 而不是 `read-write`——它
41
+ 會先移除衝突的那一列再寫入替代值,那正是它與 INSERT 的差別;`PRAGMA`、`VACUUM`、
42
+ `REINDEX` 要求 `admin`。MySQL 的 `REPLACE INTO` 仍維持先前的分類,這條映射只在 SQLite
43
+ 方言上生效。
44
+
45
+ `ATTACH` 與 `DETACH` 在任何權限層級都被拒絕,`admin` 也不例外。它們不是階梯問題:
46
+ 提高權限不會讓「碰到連線沒有指名的第二個資料庫檔」變得可以接受。拒絕走的是新的
47
+ `ConnectionBoundaryError` 而不是 `PermissionError`,因為後者會印出 `required: admin`,
48
+ 等於邀請一次幫不上忙的提權。(DBCLI-035、ADR-0037)
49
+
50
+ - **SQLite 連線不必手寫。** `dbcli init --system sqlite` 只問兩件事——檔案路徑與權限——
51
+ 不問 host、port、user、password 或資料庫名稱;非互動時用 `--file <path>`。路徑在任何
52
+ 東西被寫進 `config.json` 之前就檢查,不存在、不是 SQLite 資料庫、或任何記憶體形式
53
+ 一律拒絕並保持設定檔原封不動。
54
+
55
+ `use` 與 `status` 在其他 engine 顯示 host 的位置顯示檔案路徑,所以 SQLite 連線不會
56
+ render 成看起來像解析失敗的 `:0/`;`status --format json` 與 `use` 的機器輸出各多一個
57
+ 只在 SQLite 連線上出現的 `file` 欄位。`doctor` 檢查的是檔案而非網路端點:存在、可讀,
58
+ 以及只在權限允許寫入時才檢查可寫——`query-only` 下的唯讀檔案是設定正確,會被如此
59
+ 回報而不是判為失敗。v1 設定無法升級成 SQLite(v1 格式早於這個 engine),訊息改為
60
+ 指向 `dbcli init --system sqlite`。(DBCLI-036)
61
+
62
+ - **`docs/feature-matrix.md` 多了 SQLite 一欄與限制摘要。** 表格與
63
+ `src/adapters/capabilities.ts` 的 `sqlite` 登錄同步。
64
+
65
+ ### Changed
66
+
67
+ - **ForgePilot 成為生命週期的唯一權威,`specs/handoff.md` 只留下沒有別的家的東西。**
68
+ handoff 從工作佇列變成交付紀錄,並由 gate 比對倉庫自己的歷史雙向檢查:宣告了交付卻
69
+ 沒有東西背書會紅,歷史已經宣稱交付卻沒登進清單也會紅;「誰被允許 commit 或 push」
70
+ 是交付回答不了的另一個問題,gate 停止回答它。ForgeFlow 升級到 0.6.0,Story 的
71
+ `## Authority`、風險宣告與逐條驗收證據改由上游 checker 執行而非在本倉庫重寫,Story
72
+ 對 ADR 的依賴從散文連結變成解析得開的一行。(DBCLI-014、016、018、021、027、028、
73
+ 029、033;ADR-0025、0027、0029、0030、0031、0032、0036)
74
+
75
+ - **`make verify` 自帶依賴安裝,寫出綁定 revision 的 Verification Attestation,並少
76
+ build 一次。** FAIL 會指名它停在哪一步,log 邊跑邊說自己在第幾步、每步花了多久;
77
+ attestation 不是 evidence receipt,兩個階段都不准決定 verdict。`test:perf` 的判決不再
78
+ 取決於機器忙不忙:十九條讀時鐘的門換成計算實際做了多少工作,blacklist 遮蔽成本改由
79
+ 確定性計數表達。(DBCLI-015、017、019、020、030、031、032;ADR-0026、0028、0033、
80
+ 0034、0035)
81
+
82
+ - **五條被承認的 contract 豁免全部歸零。** DBCLI-PLAT-004 到 007、012 的信任邊界與
83
+ fixture 矩陣改成指名真正的欄位與真的存在的測試;先前沒有任何斷言的三列
84
+ correlation-id fixture 補上測試。純規格與測試修正,程式行為未變。
85
+ (DBCLI-022、023、024、025、026)
86
+
87
+ - **ADR 檔名統一為 `ADR-00NN-*.md`。** `docs/adr/0001-*.md` 這類舊檔名全部改名,讓
88
+ Story 的決策記錄連結可以被 checker 解析;指向舊路徑的外部書籤需要調整。
89
+ (DBCLI-021、ADR-0029)
90
+
91
+ ### Fixed
92
+
93
+ - **Windows 上三個 SQLite 測試依賴 POSIX 權限語意而假紅。** `chmod` 在 Windows 拿不掉
94
+ 讀取權限、也擋不住目錄內建檔,這兩個情境改為 `skipIf(win32)`;「可讀但不是資料庫」
95
+ 的 fixture 改用臨時目錄內寫出的文字檔,不再依賴 `/etc/passwd`。
96
+
97
+ ## [9.0.0] - 2026-09-06
98
+
99
+ ### Changed
100
+
101
+ - **AI Skill now has one canonical English source.** `dbcli skill`, `--output`, and every
102
+ supported platform install emit `assets/SKILL.md`; the user-facing documentation remains
103
+ bilingual.
104
+
105
+ ### Removed
106
+
107
+ - **BREAKING:** Removed `dbcli skill --lang <en|zh-TW>`, `assets/SKILL.zh-TW.md`, and the
108
+ bilingual Skill parity gate. Existing Traditional-Chinese installs are reported as outdated
109
+ and become the canonical English Skill when reinstalled.
110
+
8
111
  ## [8.0.0] - 2026-09-05
9
112
 
10
113
  ForgeFlow Story DBCLI-001 到 DBCLI-012 全數在 `v7.0.1`(`224ee59d`)之後交付;該 tag
@@ -309,8 +412,8 @@ ForgeFlow Story DBCLI-001 到 DBCLI-012 全數在 `v7.0.1`(`224ee59d`)之後
309
412
  ADR-0019 在自己的 Consequences 裡寫下這一則:一份設定的大小寫折疊仍然是三套規則。
310
413
  這一版把它收成一套,並在收的過程中發現同一份設定還有一條完全繞過它的路——規則從來
311
414
  沒有抵達 `dbcli q` 與 `dbcli report` 用的那個 adapter。設計決策記在
312
- `docs/adr/0020-one-fold-rule-for-every-blacklist-comparison.md` 與
313
- `docs/adr/0021-connection-only-adapters-say-without-rules.md`。
415
+ `docs/adr/ADR-0020-one-fold-rule-for-every-blacklist-comparison.md` 與
416
+ `docs/adr/ADR-0021-connection-only-adapters-say-without-rules.md`。
314
417
 
315
418
  2026-09-01 直接呼叫四個比對器量到的起點——除了最後一列,每一列都是「寫入被拒、
316
419
  讀取原文回傳」的設定,而操作者確認規則有效的方式,通常就是看寫入被擋下來:
@@ -534,8 +637,8 @@ ADR-0019 在自己的 Consequences 裡寫下這一則:一份設定的大小寫
534
637
  ## [6.0.0] - 2026-09-01 - 一份黑名單設定,四個互不相同的比對器
535
638
 
536
639
  規格 SQL 第 7、8、9 則與 MongoDB 第 3–6 則。設計決策記在
537
- `docs/adr/0018-a-blacklist-rule-that-does-not-match-fails-loudly.md` 與
538
- `docs/adr/0019-one-blacklist-rule-one-matcher.md`。
640
+ `docs/adr/ADR-0018-a-blacklist-rule-that-does-not-match-fails-loudly.md` 與
641
+ `docs/adr/ADR-0019-one-blacklist-rule-one-matcher.md`。
539
642
 
540
643
  SQL 那半:本機 MariaDB、表 `probe_users (id, Password, note)`、值 `s3cret`,八種設定裡七種洩漏,而全部都被設定載入器無聲接受——操作者「規則有效」的唯一證據是 dbcli 沒有抱怨。
541
644
 
@@ -575,7 +678,7 @@ MongoDB 那半(本機容器 MongoDB 7.0.31 實測):同一組規則被四
575
678
 
576
679
  ## [5.1.0] - 2026-08-31 - 一次被黑名單擋下的查詢,紀錄裡指向的是沒被擋的那張表
577
680
 
578
- `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`。
681
+ `docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 10 則。設計決策記在 `docs/adr/ADR-0017-the-audit-target-stays-wrong-and-the-record-stops-depending-on-it.md`。
579
682
 
580
683
  ### Added
581
684
 
@@ -589,7 +692,7 @@ MongoDB 那半(本機容器 MongoDB 7.0.31 實測):同一組規則被四
589
692
 
590
693
  ## [5.0.0] - 2026-08-31 - shell 裡一句真的改了資料的 UPDATE,稽核裡沒有任何一列
591
694
 
592
- `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`。
695
+ `docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 11 則自己註明是純讀碼結論、值得先端到端確認。確認了,結論成立,而且比讀碼看到的更嚴重。本機 MariaDB、`read-write` 連線、每次先清空 audit:`dbcli query "SELECT …"` 寫一列,而在 `dbcli>` 提示符打的同一句寫零列;一句帶 WHERE 的 `UPDATE` 走完全程、改掉了資料,同樣零列。設計決策記在 `docs/adr/ADR-0016-the-sql-shell-audits-every-statement-in-two-rows.md`。
593
696
 
594
697
  ### Changed
595
698
 
@@ -769,7 +872,7 @@ Three numbers in this repository disagreed with reality and nothing checked any
769
872
 
770
873
  - **`_update_by_query` 從 `read-write` 收緊為 `admin`**:它是獨立的區段,精確比對之下落到破壞性預設,而它確實會改寫索引裡的每一份文件。
771
874
 
772
- - **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.
875
+ - **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/ADR-0013-evidence-artifact-format-versions-are-independent-of-the-package-version.md`, amending ADR-0012.
773
876
 
774
877
  - **`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.
775
878
 
@@ -779,7 +882,7 @@ Three numbers in this repository disagreed with reality and nothing checked any
779
882
 
780
883
  ## [3.0.0] - 2026-08-16 - Evidence that could not reproduce itself, and a hash that hid nothing
781
884
 
782
- 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.
885
+ 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/ADR-0012-known-defects-get-fixed-whether-or-not-anyone-is-using-the-code.md`, superseding ADR 0011.
783
886
 
784
887
  ### Changed
785
888
 
@@ -863,7 +966,7 @@ Three shapes that ran unattended before now exit `1`, all of them full-table wri
863
966
 
864
967
  ### Changed
865
968
 
866
- - **BREAKING: a statement that is not limited to particular rows is refused when nobody is watching.** `dbcli query "UPDATE users SET banned = 1"` used to execute against any read-write connection without asking anything, and the caller most likely to produce an unqualified `UPDATE` is the agent this product exists to serve. Raw SQL now passes through a two-tier gate before the connection is opened. Tier one is any write — an `INSERT`, an `UPDATE` or `DELETE` that has a `WHERE` or `LIMIT`, a `CREATE`, an `ALTER` — and behaves as it always has for a non-interactive caller; at a terminal it shows what dbcli understood the statement to do and asks, which `--yes` skips. Tier two is `UPDATE` / `DELETE` with no `WHERE`, `DROP`, `TRUNCATE`, several statements in one string — one statement to a classifier reading the leading keyword, two to a driver — and any statement the SQL parser cannot read: at a terminal the operator types the target table name, and **no flag skips it** — not `--yes`, not `--force`. Away from a terminal, or under `--format json`, tier two is refused: exit `1`, a `reason=` a caller can branch on (`no_where`, `ddl_destruction`, `unparseable`, `multiple_statements`), and the statement never sent, because the gate runs before the adapter is built rather than after. A parse failure resolves to tier two rather than tier one; the cost of being wrong is a needlessly typed table name against a needlessly emptied table. The reasoning, the alternatives, and the condition that would falsify it are in `docs/adr/0010-unattended-callers-are-refused-full-table-writes.md` (#70).
969
+ - **BREAKING: a statement that is not limited to particular rows is refused when nobody is watching.** `dbcli query "UPDATE users SET banned = 1"` used to execute against any read-write connection without asking anything, and the caller most likely to produce an unqualified `UPDATE` is the agent this product exists to serve. Raw SQL now passes through a two-tier gate before the connection is opened. Tier one is any write — an `INSERT`, an `UPDATE` or `DELETE` that has a `WHERE` or `LIMIT`, a `CREATE`, an `ALTER` — and behaves as it always has for a non-interactive caller; at a terminal it shows what dbcli understood the statement to do and asks, which `--yes` skips. Tier two is `UPDATE` / `DELETE` with no `WHERE`, `DROP`, `TRUNCATE`, several statements in one string — one statement to a classifier reading the leading keyword, two to a driver — and any statement the SQL parser cannot read: at a terminal the operator types the target table name, and **no flag skips it** — not `--yes`, not `--force`. Away from a terminal, or under `--format json`, tier two is refused: exit `1`, a `reason=` a caller can branch on (`no_where`, `ddl_destruction`, `unparseable`, `multiple_statements`), and the statement never sent, because the gate runs before the adapter is built rather than after. A parse failure resolves to tier two rather than tier one; the cost of being wrong is a needlessly typed table name against a needlessly emptied table. The reasoning, the alternatives, and the condition that would falsify it are in `docs/adr/ADR-0010-unattended-callers-are-refused-full-table-writes.md` (#70).
867
970
 
868
971
  - **BREAKING: `dbcli update` / `dbcli delete` refuse a `--where` that matches on nothing unique.** Their `WHERE` is mandatory, so "no `WHERE`" cannot happen — but `--where "status=active"` reads like a filter and writes like a full-table statement. When the conditions cover neither the primary key nor any unique index, the same tier-two treatment applies: type the table name, or be refused with `reason=non_unique_where`. The schema needed to tell the two apart is already in hand at that point, so this costs no extra round trip. `--force` is unaffected in what it always did — skip the ordinary confirmation — and does not open this gate (#70).
869
972
 
@@ -910,7 +1013,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
910
1013
 
911
1014
  - **A write that fails in a terminal now says so in prose too, on stderr.** Prose covered the endings a person cares least about — a successful write, a cancellation, a dry run — while the two that actually stop you got a raw JSON envelope on stdout: a blacklist refusal and a validation failure (a malformed `--set`, a missing `--where`). Both now go through `printMutationFailure`, which prints the reason as a sentence when somebody is watching and the same envelope, byte for byte, when nobody is. Human-mode failures — including the executor's own `status: "error"` — write to **stderr**, joining the `PermissionError` and `ConnectionError` branches that always did, so every human-facing failure is on the error stream while routine progress stays on stdout. A blacklist refusal gets a hint naming `dbcli blacklist list`; nothing else gets the `--recovery` line, because `--recovery` writes a plan for a statement that failed against the database and a refusal at this stage never reached one (#70).
912
1015
 
913
- - **Core modules can no longer write to stdout, and CI fails if one starts.** `DataExecutor` printed the generated SQL and blocked on `promptUser.confirm` from inside `src/core`, on the same stdout that carries the JSON envelope agents parse — and `dist/core.mjs` shipped a real `import("@inquirer/prompts")` so that a library consumer could be handed an interactive prompt it never asked for. Core now describes the pending mutation and asks the caller through a `confirm` callback; `src/commands/mutation-confirm.ts` holds the CLI's implementation. `scripts/check-core-no-stdout.ts` enforces the boundary in CI, with the 16 modules that predate the rule in a ratchet list that can only shrink. The reasoning, its alternatives, and the condition that would falsify it are recorded in `docs/adr/0009-core-does-not-write-to-stdout.md` (#70).
1016
+ - **Core modules can no longer write to stdout, and CI fails if one starts.** `DataExecutor` printed the generated SQL and blocked on `promptUser.confirm` from inside `src/core`, on the same stdout that carries the JSON envelope agents parse — and `dist/core.mjs` shipped a real `import("@inquirer/prompts")` so that a library consumer could be handed an interactive prompt it never asked for. Core now describes the pending mutation and asks the caller through a `confirm` callback; `src/commands/mutation-confirm.ts` holds the CLI's implementation. `scripts/check-core-no-stdout.ts` enforces the boundary in CI, with the 16 modules that predate the rule in a ratchet list that can only shrink. The reasoning, its alternatives, and the condition that would falsify it are recorded in `docs/adr/ADR-0009-core-does-not-write-to-stdout.md` (#70).
914
1017
 
915
1018
  ### Fixed
916
1019
 
@@ -962,7 +1065,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
962
1065
 
963
1066
  ### Removed
964
1067
 
965
- - **`engines.node` — the runtime declaration now matches the artifact.** `package.json` claimed `node: ">=18.0.0"` since the first release, but measured on Node v22.17.1 against the `v1.54.1` `dist/`, only one published entry point actually loaded. `node dist/cli.mjs` threw `ERR_MODULE_NOT_FOUND` as soon as the launcher reached its dynamic import: `src/cli.ts` keeps the runtime path as a non-literal `'./cli-runtime'` so the `--version` launcher does not inline the heavy runtime, and Bun's resolver appends `.mjs` where Node's ESM resolver requires the extension. `import('dist/core.mjs')` — the `./core` subpath export — threw `Bun is not defined`, the bundle holding 30 Bun global references against `dist/cli-runtime.mjs`'s 143. The `bin` shebang is `#!/usr/bin/env bun` regardless, and the `--version` fast path branches on `import.meta.main`, which is `undefined` before Node 24. `engines` now declares `bun >= 1.3.3` alone. `dist/agent-core.mjs` was and remains Node-importable, and is documented as the one entry point that is. npm and npx stay supported as distribution channels, with the docs stating plainly that the installed executable still runs under Bun. `tests/integration/runtime-contract.test.ts` holds both ends: re-declaring `engines.node` fails unless `dist/cli.mjs` and `dist/core.mjs` really import in a bare Node process, and `dist/agent-core.mjs` must keep importing there whatever else changes. The alternative — `--target node` plus ~169 Bun API replacements in `src/` and a Node runtime CI suite — is rejected and its cost recorded in `docs/adr/0008-dbcli-is-a-bun-program-and-engines-says-so.md` (#65).
1068
+ - **`engines.node` — the runtime declaration now matches the artifact.** `package.json` claimed `node: ">=18.0.0"` since the first release, but measured on Node v22.17.1 against the `v1.54.1` `dist/`, only one published entry point actually loaded. `node dist/cli.mjs` threw `ERR_MODULE_NOT_FOUND` as soon as the launcher reached its dynamic import: `src/cli.ts` keeps the runtime path as a non-literal `'./cli-runtime'` so the `--version` launcher does not inline the heavy runtime, and Bun's resolver appends `.mjs` where Node's ESM resolver requires the extension. `import('dist/core.mjs')` — the `./core` subpath export — threw `Bun is not defined`, the bundle holding 30 Bun global references against `dist/cli-runtime.mjs`'s 143. The `bin` shebang is `#!/usr/bin/env bun` regardless, and the `--version` fast path branches on `import.meta.main`, which is `undefined` before Node 24. `engines` now declares `bun >= 1.3.3` alone. `dist/agent-core.mjs` was and remains Node-importable, and is documented as the one entry point that is. npm and npx stay supported as distribution channels, with the docs stating plainly that the installed executable still runs under Bun. `tests/integration/runtime-contract.test.ts` holds both ends: re-declaring `engines.node` fails unless `dist/cli.mjs` and `dist/core.mjs` really import in a bare Node process, and `dist/agent-core.mjs` must keep importing there whatever else changes. The alternative — `--target node` plus ~169 Bun API replacements in `src/` and a Node runtime CI suite — is rejected and its cost recorded in `docs/adr/ADR-0008-dbcli-is-a-bun-program-and-engines-says-so.md` (#65).
966
1069
 
967
1070
  ### Added
968
1071
 
@@ -1244,7 +1347,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1244
1347
 
1245
1348
  ## [1.47.1] - 2026-08-05 - 唯讀保證涵蓋所有執行路徑(安全性修復)
1246
1349
 
1247
- 決策記錄:`docs/adr/0004-database-access-stays-a-cli-surface.md`。
1350
+ 決策記錄:`docs/adr/ADR-0004-database-access-stays-a-cli-surface.md`。
1248
1351
 
1249
1352
  ### Security
1250
1353
 
@@ -1284,7 +1387,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1284
1387
 
1285
1388
  ## [1.47.0] - 2026-08-05 - 連線逾時可設定
1286
1389
 
1287
- 決策記錄:`docs/adr/0003-connection-timeout-override-resolved-at-adapter-construction.md`。
1390
+ 決策記錄:`docs/adr/ADR-0003-connection-timeout-override-resolved-at-adapter-construction.md`。
1288
1391
 
1289
1392
  ### Added
1290
1393
 
@@ -1300,7 +1403,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
1300
1403
 
1301
1404
  ## [1.46.0] - 2026-08-04 - MongoDB 逐欄連線設定
1302
1405
 
1303
- 決策記錄:`docs/adr/0002-mongodb-connection-field-first-config.md`;規格:`docs/specs/2026-08-04-mongodb-field-first-connection.md`。
1406
+ 決策記錄:`docs/adr/ADR-0002-mongodb-connection-field-first-config.md`;規格:`docs/specs/2026-08-04-mongodb-field-first-connection.md`。
1304
1407
 
1305
1408
  ### Changed
1306
1409
 
package/README.dev.md CHANGED
@@ -106,7 +106,7 @@ Then ship a patch version with the fix. Prefer **deprecate** over **unpublish**
106
106
 
107
107
  - **`files` (in `package.json`):** Publishes `dist/`, `assets/`, `README.md`, `CHANGELOG.md`, `LICENSE`. The `assets/` tree is required for `dbcli skill` to copy bundled `SKILL.md` / `reference.md` from the installed package.
108
108
  - **`prepublishOnly`:** `bun run build` so `dist/cli.mjs` matches current source.
109
- - **`engines`:** Declares `bun >= 1.3.3` only. `node` was removed because the published bundles cannot run on Node: `dist/cli.mjs` dynamic-imports the extensionless `./cli-runtime` (Bun's resolver appends `.mjs`, Node's does not) and `dist/core.mjs` calls Bun globals. `tests/integration/runtime-contract.test.ts` fails if `engines.node` comes back without the bundles being fixed to match, and if `dist/agent-core.mjs` — the one entry point that is Node-importable — regresses. See `docs/adr/0008-dbcli-is-a-bun-program-and-engines-says-so.md`.
109
+ - **`engines`:** Declares `bun >= 1.3.3` only. `node` was removed because the published bundles cannot run on Node: `dist/cli.mjs` dynamic-imports the extensionless `./cli-runtime` (Bun's resolver appends `.mjs`, Node's does not) and `dist/core.mjs` calls Bun globals. `tests/integration/runtime-contract.test.ts` fails if `engines.node` comes back without the bundles being fixed to match, and if `dist/agent-core.mjs` — the one entry point that is Node-importable — regresses. See `docs/adr/ADR-0008-dbcli-is-a-bun-program-and-engines-says-so.md`.
110
110
  - **Shebang:** `scripts/build.ts` prepends `#!/usr/bin/env bun` to `dist/cli.mjs`; the `bin` field in `package.json` points at that file.
111
111
  - **`exports`:** `./core` and `./agent-core` only — there is deliberately no `.` entry. It used to point at `dist/cli.mjs`, and `src/cli-runtime.ts` calls `outputHelp()` and `parseAsync(process.argv)` at module top level, so importing the package ran the CLI against the host's argv and never returned. `bin` does not resolve through `exports`, so the executable is unaffected. `tests/integration/runtime-contract.test.ts` pins both the absence of `.` and the side effect that justifies it.
112
112
  - **`postinstall`:** `scripts/postinstall-check-bun.mjs` reports a missing Bun — npm ignores `engines.bun`, so this is the only signal an npm-only machine gets before `dbcli` refuses to start. The command is `bun … || node …` so it runs under whichever runtime is present: Bun-only machines have no `node` to invoke it with, and a hard `node` dependency there would break the primary install path. It must **not** end in `|| exit 0`; v1.55.0 shipped with that mask and npm, which hides lifecycle output unless a script fails, therefore showed nothing at all. On a global install (`npm_config_global === 'true'`) a missing Bun exits 1, so the reason is printed and npm rolls the `bin` back; a dependency install only warns, because that is the `./agent-core` consumer. Both outcomes are pinned in `tests/integration/runtime-contract.test.ts`.
package/README.md CHANGED
@@ -1715,6 +1715,83 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for full setup, testing, and release pr
1715
1715
 
1716
1716
  ---
1717
1717
 
1718
+ ## Governance
1719
+
1720
+ For people changing this repository. dbcli itself depends on none of the tools
1721
+ below: `package.json` and `src/` never mention them, `.forgepilot/` is
1722
+ gitignored local state, and a clone without any of them runs `make verify`
1723
+ normally.
1724
+
1725
+ ### How work is specified and reviewed: ForgeFlow
1726
+
1727
+ Each unit of work is a Story under `specs/stories/`, made of a `story.md` (the
1728
+ intent boundary) and an `acceptance.md` (the criteria). That shape is not a
1729
+ local convention — it is upstream ForgeFlowV2's contract. The adopted version
1730
+ and revision are pinned in `specs/.forgeflow-adoption`, and the CI
1731
+ `forgeflow-contract` job clones **that exact revision** and runs upstream's own
1732
+ checkers against this repository (`bun run forgeflow:contract`, which needs
1733
+ `FORGEFLOW_ROOT`). The contract is executed by upstream's code rather than
1734
+ reimplemented here, because two implementations of one contract diverge on a
1735
+ schedule nobody controls (ADR-0027).
1736
+
1737
+ A Story declaring itself security sensitive must enumerate its trust-boundary
1738
+ fields and a security fixture matrix, and **every cell must be an exact value**
1739
+ rather than prose. That rule catches more than wording: clearing the last of it
1740
+ turned up three fixture rows that asserted nothing at all, and one row
1741
+ describing a mechanism that does not exist.
1742
+
1743
+ The exemption list in `scripts/lib/forgeflow-contract.ts` is a ratchet that may
1744
+ shrink and never grow. It is currently empty, so any finding at all fails the
1745
+ gate; adding an entry back requires lowering — never raising — the two counts
1746
+ beside it, which a test enforces.
1747
+
1748
+ ### When work is done: ForgePilot and `make verify`
1749
+
1750
+ `make verify` is the automated completion authority. Passing it makes work
1751
+ *eligible* for human review; it does not approve or merge anything. Do not
1752
+ bypass verification or delete failing tests to obtain a pass.
1753
+
1754
+ ForgePilot is an operator tool that holds the work queue and the verification
1755
+ evidence. It runs this repository's own `make verify` inside a detached worktree
1756
+ at an exact commit, so a recorded pass is bound to that revision rather than
1757
+ meaning "it passes on a machine that already has everything installed". It has
1758
+ no completion command — only a human can approve.
1759
+
1760
+ ### Where decisions live: `docs/adr/`
1761
+
1762
+ A decision that is hard to reverse, surprising without context, or the result of
1763
+ a real trade-off gets a record in `docs/adr/`, named `ADR-<digits>-<slug>.md`
1764
+ with its status as a `* Status:` bullet rather than front matter. That shape
1765
+ exists so a Story's `Decision:` field resolves to it mechanically (ADR-0029).
1766
+ Open questions live there as `status: proposed`; undecided is a state that has
1767
+ to be storable.
1768
+
1769
+ Prefer closing a record with a falsification condition — one sentence saying
1770
+ when the decision would no longer hold — naming the files it depends on in
1771
+ backticks. Tooling reads the boundary list from those backticks, so the marker
1772
+ is load-bearing rather than decorative.
1773
+
1774
+ ### The principle behind all of it
1775
+
1776
+ Every gate here is a variation on one lesson: **a declaration nothing compares
1777
+ to anything is not a control.** Delivery claims in the handoff are reconciled
1778
+ against commit trailers; the adopted ForgeFlow version is reconciled against
1779
+ every place that restates it; the ForgeFlow contract itself is run against a
1780
+ pinned upstream revision.
1781
+
1782
+ The lesson has a limit, and finding it cost a decision. A Story's `## Authority`
1783
+ was briefly reconciled against the fact of its delivery, on the reasoning that
1784
+ reaching `main` requires commits on a pushed branch (ADR-0030). It does — but
1785
+ `completed_stories` records that a Story was delivered, not **who** performed
1786
+ each operation, and the normal flow here is an agent that is granted `modify` and
1787
+ a local `commit` handing its branch to a human who pushes and merges. The gate
1788
+ refused that flow by name, so the rule was removed (ADR-0031). Whether an
1789
+ Authority declaration is true is a question this repository cannot observe and
1790
+ Human Review can; a gate that guesses at it is worse than an unchecked
1791
+ declaration, because its verdict looks like evidence.
1792
+
1793
+ ---
1794
+
1718
1795
  ## License
1719
1796
 
1720
1797
  See LICENSE file for details.
package/README.zh-TW.md CHANGED
@@ -1357,8 +1357,7 @@ DML/DDL 指令上的部分覆蓋缺口已於 v1.20.1 結清,完整對照表見
1357
1357
  [`.planning/phases/25-recovery-envelope-bi-directional-linkage/25-J1-COVERAGE-MATRIX.md`](./.planning/phases/25-recovery-envelope-bi-directional-linkage/25-J1-COVERAGE-MATRIX.md)。
1358
1358
 
1359
1359
  進階 agent 工作流程(session handoff、forensics walk-through)詳見
1360
- [`assets/SKILL.md`](./assets/SKILL.md) §Audit Log usage(英文)或
1361
- [`assets/SKILL.zh-TW.md`](./assets/SKILL.zh-TW.md) §Audit Log 使用。
1360
+ [`assets/SKILL.md`](./assets/SKILL.md) §Audit Log usage。
1362
1361
 
1363
1362
  ---
1364
1363
 
@@ -3137,7 +3137,7 @@ dbcli skill --install codex # install to ~/.codex/skills/dbcli/
3137
3137
  **Options:**
3138
3138
  - `--install <platform>` — `claude` | `gemini` | `antigravity` | `copilot` | `cursor` | `codex` | `windsurf`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
3139
3139
  - `--output <path>` — write `SKILL.md` to a file instead of stdout. Does not install `reference.md`.
3140
- - `--lang <en|zh-TW>` — source language for the emitted SKILL content (default `en`). It selects `assets/SKILL.md` vs `assets/SKILL.zh-TW.md`; the install/output filename stays `SKILL.md` regardless.
3140
+ - Skill content is always the canonical English `assets/SKILL.md`; the install/output filename is `SKILL.md`.
3141
3141
 
3142
3142
  **Notes:**
3143
3143
  - Both files come straight from `assets/SKILL.md` + `assets/reference.md` inside the dbcli package — no runtime rendering. Keep these in sync when shipping a release.
@@ -3872,7 +3872,7 @@ GET /orders/_search
3872
3872
 
3873
3873
  - Enter a request line `<METHOD> /<path>`, then an optional multi-line JSON body; a **blank line** submits the block. Responses render as pretty-printed JSON.
3874
3874
  - **The connection's permission tier applies, as it does to `dbcli query`.** The request is classified through the same classifier the query path uses, on the path the server will actually route — the query string is discarded and percent-encoding and dot segments are resolved first, so `?filter_path=...` cannot change the verdict.
3875
- - **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/0014-elasticsearch-reads-are-an-allowlist.md).
3875
+ - **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/ADR-0014-elasticsearch-reads-are-an-allowlist.md).
3876
3876
  - A path is refused unless it is **byte-identical** to what the URL parser produces — the same parser the request goes through — and the refusal tells you the canonical spelling to write instead. This is what stops a path that reads one way here and routes another way at the server: `#` truncates a path in transit (`POST /_reindex#/_count` reaches Elasticsearch as `POST /_reindex`), and tab, newline and `\` are rewritten too. A document id containing a space or a non-ASCII character must be written percent-encoded; the error message gives you the exact string.
3877
3877
  - A quoted string request body is refused; write the body as JSON. So is a `source=` query parameter — Elasticsearch accepts it in place of a body, where none of the body checks can see it. `_source`, `_source_includes` and `_source_excludes` are unaffected.
3878
3878
  - Blacklisted field names are refused in the query string as well as the body, because the URI-search form names fields directly (`?q=…`, `?sort=…`, `?docvalue_fields=…`). A name matches on any dot component, so `password.keyword` — the multi-field every `text` field gets by default — and `params._source.password` inside a script are both refused.