@qijenchen/design-system 0.1.0-beta.139 → 0.1.0-beta.140

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,7 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
- import { lineClampClass, cn } from "../../lib/utils.js";
4
+ import { cn, lineClampClass } from "../../lib/utils.js";
5
5
  import { Avatar } from "../../components/Avatar/avatar.js";
6
6
  import { Tooltip, TooltipTrigger, TooltipContent } from "../../components/Tooltip/tooltip.js";
7
7
  import { useTruncated } from "../../hooks/use-truncated.js";
@@ -3,8 +3,8 @@
3
3
  "schemaVersion": 1,
4
4
  "role": "template-consumer",
5
5
  "release": {
6
- "designSystem": "0.1.0-beta.139",
7
- "storybookConfig": "0.1.0-beta.139"
6
+ "designSystem": "0.1.0-beta.140",
7
+ "storybookConfig": "0.1.0-beta.140"
8
8
  },
9
9
  "upgradeTrust": {
10
10
  "schemaVersion": 1,
@@ -42,10 +42,10 @@
42
42
  "implementationSha256": "98b74118fb5b410b77ad2994aa9105fa7609c7353822f8c5213cce54d94f0af4"
43
43
  },
44
44
  "payload": {
45
- "forkCorpusLockSha256": "8994366cc9b76e78ef838b784fb73da5e06d82975ef6bd90fc813e6dc7c85a62",
45
+ "forkCorpusLockSha256": "d247561ebd02cbdfb8fce6e9d8135efe01723c6f69393ffe837e5fe9666db284",
46
46
  "discoveryPolicySha256": "fba289aeeb5a3f11139b9589f7ab08b5e8c8799c7bdf8f0ea57ebfeb8e7c9da6",
47
47
  "providerMaterializersSha256": "732e85b01a12e7dd19469e26e4427f0e0cfe5117a78fc9f69c2f0f89702f463a",
48
- "providerLifecycleSha256": "1bcd2de06a6886edf3b3cfad45b5f8ce74f8a20dcff064e1edb850249e8389ef",
48
+ "providerLifecycleSha256": "d02bd09c2415b9e4a17a7a921afc250f6b300a7cea77b89ef2a0a6c939043ed9",
49
49
  "providerInventorySha256": "00adf1c4f6b3367055a9cc3fdfe658660771b4c9931f696e12761a95076ad92c",
50
50
  "executionRuntimeSha256": "d9377cc45308731f287d7b1711b7c38fc4e02ff8f84729bcbbf01bb52d136904",
51
51
  "commonInstructionSha256": "a72c51a412d6cdb3961d691744013c8362e826626a330cecf4a296a6f4d93a5c",
@@ -919,7 +919,7 @@
919
919
  {
920
920
  "schemaVersion": 1,
921
921
  "file": "manifest.json",
922
- "sha256": "38930bc05f2b774dde3863ec5ee54a82d86fab5ad4c4d4befd1345231a03ba18",
922
+ "sha256": "f3faf19161db089c6953e472e70a16af8f38100af922db1415e00e029bb19212",
923
923
  "source": null,
924
924
  "classification": null,
925
925
  "destination": null
@@ -104,20 +104,20 @@
104
104
  },
105
105
  "providerLifecycle": {
106
106
  "schemaVersion": 4,
107
- "releaseVersion": "0.1.0-beta.139",
107
+ "releaseVersion": "0.1.0-beta.140",
108
108
  "immutableHead": {
109
109
  "providerInventorySha256": "86e9d0668cb54ad1bf05c36e78b4aa80417d5f468c929dd3ddcf69c1afbf32e4",
110
- "releaseVersion": "0.1.0-beta.138",
111
- "snapshotSha256": "4d86c0026722b3e90720e7140b620a8bdbab4c7ae555f6fefe9388cd3b8c9364"
110
+ "releaseVersion": "0.1.0-beta.139",
111
+ "snapshotSha256": "e772f888a887d7ee76db9f9e51bac616344a97469b03aacaa2fb086cae3a4f37"
112
112
  },
113
- "ledgerSha256": "d9ae523dd798b9d9a2f1f8d5e0743948b2ef5312400d334251ce6d15da6f33b7",
114
- "previousSnapshotSha256": "4d86c0026722b3e90720e7140b620a8bdbab4c7ae555f6fefe9388cd3b8c9364",
115
- "currentSnapshotSha256": "e772f888a887d7ee76db9f9e51bac616344a97469b03aacaa2fb086cae3a4f37",
113
+ "ledgerSha256": "d3f2dd0aadcf42b2e9f8e64e67b7f317aac946a392f68220dce78f0917f0d0d0",
114
+ "previousSnapshotSha256": "e772f888a887d7ee76db9f9e51bac616344a97469b03aacaa2fb086cae3a4f37",
115
+ "currentSnapshotSha256": "63e26cddf08fa7fa1b8cca9047288d8a348605518c22b55e1734f7dd3962fff9",
116
116
  "currentProviderInventorySha256": "86e9d0668cb54ad1bf05c36e78b4aa80417d5f468c929dd3ddcf69c1afbf32e4",
117
117
  "retiredProviders": [],
118
118
  "currentSnapshot": {
119
- "releaseVersion": "0.1.0-beta.139",
120
- "previousSnapshotSha256": "4d86c0026722b3e90720e7140b620a8bdbab4c7ae555f6fefe9388cd3b8c9364",
119
+ "releaseVersion": "0.1.0-beta.140",
120
+ "previousSnapshotSha256": "e772f888a887d7ee76db9f9e51bac616344a97469b03aacaa2fb086cae3a4f37",
121
121
  "providers": [
122
122
  {
123
123
  "id": "claude",
@@ -193,8 +193,8 @@
193
193
  "retiredProviders": []
194
194
  },
195
195
  "immutableHeadSnapshot": {
196
- "releaseVersion": "0.1.0-beta.138",
197
- "previousSnapshotSha256": "c799b3844b4b2de737c8c953e27c804e70c828a46eda4e60f638f607e65aaa7c",
196
+ "releaseVersion": "0.1.0-beta.139",
197
+ "previousSnapshotSha256": "4d86c0026722b3e90720e7140b620a8bdbab4c7ae555f6fefe9388cd3b8c9364",
198
198
  "providers": [
199
199
  {
200
200
  "id": "claude",
@@ -1,44 +1,71 @@
1
1
  #!/bin/bash
2
- # record_release_consent.sh — UserPromptSubmit:user 在對話說「發版」→ 落地發版同意 receipt(綁**當前分支**)
2
+ # record_release_consent.sh — UserPromptSubmit:user 在對話說「發版」→ 落地發版同意 receipt
3
3
  #
4
4
  # Why(2026-09-02 user verbatim):「按照我們規定的工作流程應該是你要先把我們討論的東西部署到 netlify,
5
5
  # 等我確認後認為都沒問題,主動說要發版,你才會 push 到 GitHub main 同時更新 GitHub 上的 storybook 並發版到 NPM」
6
- # 機制:release-orchestrator 在 merge 前讀 .git/governance-runtime/release-consent/branch__<分支>.json;
7
- # 2026-09-20:receipt 由綁 commit 改為綁**分支**(= 該 PR)。原本以 <headSha>.json 命名,而發版必然
8
- # 產生新 commit(版號 bump 是必要步驟、CI 紅了修一次又一版),於是 user 為同一份工作被迫一再重講。
9
- # user 原話:「我他媽已經說發版一百次了,你他媽到底是要我說幾次?」——那不是沒授權,是機制設計錯了。
10
- # 綁分支對得上 user 實際看的東西:預覽連結 deploy-preview-<PR> 本來就是每個 PR 一條(canonical:
11
- # 1 chat = 1 branch = 1 PR)。防線不減反增:換分支照樣要重新同意;同意後若**預覽看得見的內容**
12
- # (packages/<pkg>/src 的 ts/tsx/js/jsx/css)又變了,orchestrator 判定失效、要求重新確認。
13
- # hook 是 Claude 的寫入端(provider adapter);Codex 等由同一 canonical 生成同款 hook。
14
- # 判定:含同意詞(發版/發佈/發布/可以合併/推到 main/release it/ship it/publish it)且不是問句、不是否定。
15
- # 否定(不要發版/先不要/暫停發版)→ 刪除當前 HEAD receipt(撤回)。
6
+ #
7
+ # ── 綁定對象的兩次修正(別再改錯方向)──────────────────────────────────────
8
+ # v1 commit:發版必然產生新 commit(版號 bumpCI 修正),user 為同一份工作被迫一再重講。
9
+ # v2 綁分支:改完之後立刻開了新分支,又要一次同意 —— **問題只是換地方發作**。
10
+ # v3 綁 **user 看過並認可的預覽內容**(2026-09-20):commit 與分支都是 agent 切工作的單位,
11
+ # 不是 user 授權的單位。內容沒變 commit、跨分支都算數;內容變了 必須重新確認。
12
+ #
13
+ # ── 判準只有一份 ───────────────────────────────────────────────────────────
14
+ # 同意詞/問句/否定/「早就說過了」的判定全部住在 infra/governance/release-workflow.json,
15
+ # scripts/lib/release-consent-language.mjs 唯一實作,**hook agent 手動落地共用同一支**。
16
+ # 本檔不再自己寫 regex —— 2026-09-20 稽核實證:三方各寫一套的結果是
17
+ # 「問句/否定不算同意」只在 hook 這條路存在,agent 走手動路徑完全繞過(14 筆收據 10 筆如此)。
18
+ #
19
+ # ── 不得靜默 ───────────────────────────────────────────────────────────────
20
+ # 偵測到發版意圖卻不受理時**必須印出來**。靜默丟棄比擋下來更糟:user 以為講過了,
21
+ # agent 以為沒講,於是 user 再講一次、再一次 —— 那正是他連續兩天暴怒的原因。
16
22
  set -u
17
23
  INPUT=$(cat 2>/dev/null || echo '{}')
18
24
  PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
19
25
  [ -z "$PROMPT" ] && exit 0
26
+ # 腳本位置由**本檔自己的位置**推出(hooks 在 packages/design-system/ds-canonical/hooks/),
27
+ # 不從 cwd 的 git toplevel 推 —— 測試會在 fixture repo 裡跑,那裡沒有這些腳本。
28
+ # 收據要寫去哪則由 GOVERNANCE_RELEASE_CONSENT_DIR 決定(測試導向自己的沙箱)。
29
+ SELF_DIR=$(cd "$(dirname "$0")" && pwd)
30
+ REPO_ROOT=$(cd "$SELF_DIR/../../../.." && pwd)
31
+ LANG_CLI="$REPO_ROOT/scripts/lib/release-consent-language.mjs"
32
+ ORCH="$REPO_ROOT/scripts/release-orchestrator.mjs"
33
+ [ -f "$LANG_CLI" ] || exit 0
34
+ [ -f "$ORCH" ] || exit 0
20
35
  ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
36
+
37
+ VERDICT=$(printf '%s' "$PROMPT" | node "$LANG_CLI" 2>/dev/null | head -1)
38
+ KIND=${VERDICT%% *}
39
+ REASON=${VERDICT#* }
40
+ [ -z "$KIND" ] && exit 0
41
+ [ "$KIND" = "none" ] && exit 0
42
+
21
43
  BRANCH=$(git -C "$ROOT" branch --show-current 2>/dev/null || echo "")
22
- HEAD_SHA=$(git -C "$ROOT" rev-parse 'HEAD^{commit}' 2>/dev/null || echo "")
23
- [ -z "$HEAD_SHA" ] && exit 0
24
- DIR="$ROOT/.git/governance-runtime/release-consent"
25
- # 檔名取自分支(非法字元換成底線),與 release-orchestrator 的 consentFileName() 同一套規則。
26
- BRANCH_KEY=$(printf '%s' "$BRANCH" | sed 's/[^A-Za-z0-9._-]/_/g')
27
- FILE="$DIR/branch__$BRANCH_KEY.json"
28
44
 
29
- # 否定優先:撤回
30
- if printf '%s' "$PROMPT" | grep -qE '(不要發版|先不要發|不要發|還不要|暫停發版|先別發|不用發版|don.?t release|do not release|hold the release)'; then
31
- if [ -f "$FILE" ]; then rm -f "$FILE"; echo "🛑 已撤回發版同意 receipt(分支 ${BRANCH});release:auto 會停在預覽階段。"; fi
32
- exit 0
33
- fi
34
- # 同意詞
35
- printf '%s' "$PROMPT" | grep -qE '(發版|發佈|發布|可以合併|推到 ?main|push to main|release it|ship it|publish it)' || exit 0
36
- # 問句 ≠ 同意(M36)
37
- if printf '%s' "$PROMPT" | grep -qE '(\?|?|是否|要不要|可以嗎|該不該|嗎[。!!]?$|嗎[,,])'; then exit 0; fi
38
- [ "$BRANCH" = "main" ] && exit 0
39
- mkdir -p "$DIR" 2>/dev/null || exit 0
40
- QUOTE_SHA=$(printf '%s' "$PROMPT" | shasum -a 256 | cut -d' ' -f1)
41
- jq -n --arg head "$HEAD_SHA" --arg branch "$BRANCH" --arg quote "$PROMPT" --arg qsha "$QUOTE_SHA" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
42
- '{schemaVersion:2, branch:$branch, consentedHeadSha:$head, quote:$quote, quoteSha256:$qsha, source:"user-prompt-hook", recordedAt:$at}' > "$FILE" 2>/dev/null || exit 0
43
- echo "✅ 已記錄發版同意 receipt(分支 ${BRANCH});release:auto 現在可合併並發布。版號 bump CI 修正不需要再講一次;只有預覽看得見的內容又變了才需要。"
45
+ case "$KIND" in
46
+ withdraw)
47
+ OUT=$(cd "$REPO_ROOT" && node "$ORCH" withdraw 2>/dev/null || echo "")
48
+ if printf '%s' "$OUT" | grep -q RELEASE_CONSENT_WITHDRAWN; then
49
+ echo "🛑 已撤回發版同意(${REASON});release:auto 會停在預覽階段。"
50
+ else
51
+ echo "🛑 偵測到「不要發版」但本來就沒有有效同意,無需撤回。"
52
+ fi
53
+ ;;
54
+ question)
55
+ # 保守側:不記錄也不撤回。但一定要說出來,否則 user 會以為自己已經授權了。
56
+ echo "❓ 偵測到發版相關字眼,但判為問句(${REASON}),**未記錄為同意**。若這是要我發版,直接說「發版」即可。"
57
+ ;;
58
+ consent)
59
+ if [ "$BRANCH" = "main" ]; then
60
+ echo "⚠️ 目前在 main,發版同意只在工作分支上記錄;請先切到工作分支。"
61
+ exit 0
62
+ fi
63
+ OUT=$(cd "$REPO_ROOT" && node "$ORCH" consent --quote "$PROMPT" --branch "$BRANCH" 2>&1 || echo "")
64
+ if printf '%s' "$OUT" | grep -q RELEASE_CONSENT_RECORDED; then
65
+ echo "✅ 已記錄發版同意(綁「你看過的預覽內容」,不綁 commit 也不綁分支)。版號 bump、CI 修正、換分支都不需要再講一次;只有預覽看得見的內容變了才需要重新確認。"
66
+ else
67
+ echo "⚠️ 判為同意但落地失敗,未記錄 —— 請看:$(printf '%s' "$OUT" | tail -1)"
68
+ fi
69
+ ;;
70
+ esac
44
71
  exit 0
@@ -1,45 +1,82 @@
1
1
  #!/bin/bash
2
2
  # test_record_release_consent.sh — 發版同意 receipt hook
3
3
  #
4
- # 2026-09-20:receipt 由綁 commit 改為綁**分支**(= 該 PR)。原本檔名是 <headSha>.json,而發版
5
- # 必然產生新 commit(版號 bump 是必要步驟、CI 紅了修一次又一版),於是 user 為同一份工作被迫
6
- # 一再重講「發版」。本測試除了原本六格,另加兩格釘死改動的重點:
7
- # 7. 同一分支上再 commit → receipt 仍在(這格就是 user 不必重講的保證)
8
- # 8. 換到另一條分支 → 那條分支沒有 receipt(換一份工作仍要重新同意;2026-09-02 事故就是這格)
4
+ # 2026-09-20 第二次改寫。綁定對象的演進與理由見 hook 檔頭;本測試只釘住**行為**:
5
+ # · 判準與 agent 手動落地共用同一份(infra/governance/release-workflow.json)
6
+ # · 同意綁「預覽內容」,不綁 commit 也不綁分支 → 換分支、加 commit 都不必重講
7
+ # · 問句不記錄**也不撤回**,而且必須出聲(靜默丟棄是 user 連續暴怒的直接原因)
8
+ # · 否定詞要帶發版受詞:「不要發生問題」不得被當成撤回
9
+ # · A-不-A 問句「要不要發版」字面內嵌「不要發版」,不得被當成撤回
10
+ #
11
+ # **隔離**:先前版本 `TMP=$(mktemp -d)` 沒有守衛,沙箱裡 mktemp 失敗回空 → `cd ""` 留在真 repo
12
+ # → 測試把 user 真正的同意收據覆寫/刪除(2026-09-20 實際發生三次)。現在兩道:
13
+ # 1. mktemp 結果必須非空且是目錄,否則整支測試 fail(不是靜默繼續)
14
+ # 2. GOVERNANCE_RELEASE_CONSENT_DIR 導向沙箱,並在最後**驗證真 repo 的收據沒被動過**
9
15
  set -u
10
16
  HOOK="$(cd "$(dirname "$0")/.." && pwd)/record_release_consent.sh"
11
- TMP=$(mktemp -d)
17
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
18
+ REAL_CONSENT="$REPO_ROOT/.git/governance-runtime/release-consent"
12
19
  PASS=0; FAIL=0
13
- cd "$TMP" && git init -q . && git config user.email t@t && git config user.name t && git commit -q --allow-empty -m init && git switch -q -c claude/feature
14
- DIR=".git/governance-runtime/release-consent"
15
- FILE="$DIR/branch__claude_feature.json"
16
- run() { printf '{"prompt":%s}' "$(printf '%s' "$1" | jq -Rs .)" | bash "$HOOK"; }
17
- check() { if [ "$2" = "yes" ] && [ -f "$3" ]; then echo " PASS $1"; PASS=$((PASS+1)); elif [ "$2" = "no" ] && [ ! -f "$3" ]; then echo " PASS $1"; PASS=$((PASS+1)); else echo " FAIL $1"; FAIL=$((FAIL+1)); fi; }
18
- assert() { if [ "$2" = "0" ]; then echo " PASS $1"; PASS=$((PASS+1)); else echo " FAIL $1"; FAIL=$((FAIL+1)); fi; }
19
20
 
20
- run "要不要發版?" >/dev/null; check "1. 問句「要不要發版?」→ 不落地" no "$FILE"
21
- run "可以發版了" >/dev/null; check "2. 「可以發版了」→ 落地" yes "$FILE"
22
- grep -q '"source": "user-prompt-hook"' "$FILE" 2>/dev/null; assert "3. receipt 含 source" $?
23
- run "先不要發版,我再看看" >/dev/null; check "4. 否定「先不要發版」→ 撤回" no "$FILE"
24
- run "發版" >/dev/null; check "5. 單字「發版」→ 落地" yes "$FILE"
21
+ # 帶明確樣板:macOS mktemp 在無樣板時走 confstr 暫存目錄(不吃 TMPDIR),沙箱下會 EPERM
22
+ # 而回空字串 —— 那正是先前 `cd ""` 留在真 repo 的原因(失敗索引有記)。
23
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}/consent-hook-test.XXXXXX" 2>/dev/null || echo "")
24
+ if [ -z "$TMP" ] || [ ! -d "$TMP" ]; then
25
+ echo " FAIL mktemp -d 沒有給出可用目錄 —— 沒有隔離就不准往下跑(會動到真 repo 的同意收據)"
26
+ exit 1
27
+ fi
28
+ export GOVERNANCE_RELEASE_CONSENT_DIR="$TMP/consent"
29
+ FILE="$GOVERNANCE_RELEASE_CONSENT_DIR/current.json"
30
+
31
+ # 真 repo 收據的事前指紋(最後要驗它沒被動過)
32
+ real_fingerprint() { ls -1 "$REAL_CONSENT" 2>/dev/null | sort | shasum -a 256 | cut -d' ' -f1; }
33
+ REAL_BEFORE=$(real_fingerprint)
25
34
 
26
- # 6. receipt 必須綁分支且帶同意當下的 head(供 orchestrator 判斷「同意之後預覽有沒有變」)
27
- grep -q '"schemaVersion": 2' "$FILE" 2>/dev/null && grep -q '"branch": "claude/feature"' "$FILE" 2>/dev/null && grep -q '"consentedHeadSha"' "$FILE" 2>/dev/null
28
- assert "6. receipt 綁分支(schemaVersion 2 + branch + consentedHeadSha)" $?
35
+ cd "$TMP" && git init -q . && git config user.email t@t && git config user.name t \
36
+ && git commit -q --allow-empty -m init && git switch -q -c claude/feature
37
+
38
+ run() { printf '{"prompt":%s}' "$(printf '%s' "$1" | jq -Rs .)" | bash "$HOOK"; }
39
+ check() { if [ "$2" = "yes" ] && [ -f "$3" ]; then echo " PASS $1"; PASS=$((PASS+1));
40
+ elif [ "$2" = "no" ] && [ ! -f "$3" ]; then echo " PASS $1"; PASS=$((PASS+1));
41
+ else echo " FAIL $1"; FAIL=$((FAIL+1)); fi; }
42
+ assert() { if [ "$2" = "0" ]; then echo " PASS $1"; PASS=$((PASS+1)); else echo " FAIL $1"; FAIL=$((FAIL+1)); fi; }
29
43
 
30
- # 7. **改動重點**:同一分支上又出現新 commit(版號 bump / CI 修正)→ receipt 仍在,user 不必重講
31
- git commit -q --allow-empty -m "release: bump version"
32
- check "7. 同分支新 commit 後 receipt 仍在(user 不必重講)" yes "$FILE"
44
+ # 1. A-不-A 問句:不記錄
45
+ run "要不要發版?" >/dev/null; check "1. 「要不要發版?」→ 不落地" no "$FILE"
46
+ # 2. 而且必須**出聲**(靜默丟棄 = user 以為自己授權了)
47
+ run "要不要發版?" 2>/dev/null | grep -q "未記錄為同意"; assert "2. 問句必須印出「未記錄為同意」" $?
48
+ # 3. 明確同意 → 落地
49
+ run "可以發版了" >/dev/null; check "3. 「可以發版了」→ 落地" yes "$FILE"
50
+ # 4. 綁的是預覽內容,不是 commit / 分支
51
+ grep -q '"schemaVersion": 3' "$FILE" 2>/dev/null && grep -q '"productDigest"' "$FILE" 2>/dev/null
52
+ assert "4. receipt 綁預覽內容指紋(schemaVersion 3 + productDigest)" $?
53
+ # 5. 否定 → 撤回
54
+ run "先不要發版,我再看看" >/dev/null; check "5. 「先不要發版」→ 撤回" no "$FILE"
55
+ # 6. 否定詞必須帶發版受詞:「不要發生問題」不是撤回
56
+ run "發版吧,但不要發生問題" >/dev/null; check "6. 「不要發生問題」不得被當成撤回" yes "$FILE"
57
+ # 7. A-不-A 問句不得撤回既有同意(字面內嵌「不要發版」)
58
+ run "要不要發版?" >/dev/null; check "7. 「要不要發版?」不得撤回既有同意" yes "$FILE"
59
+ # 8. 換一條分支:同一份預覽內容 → 同意仍然成立(**user 不必重講**)
60
+ git switch -q -c claude/another-branch
61
+ run "隨便講點別的" >/dev/null; check "8. 換分支後同意仍在(綁內容不綁分支)" yes "$FILE"
62
+ # 9. 主張早就同意過(帶問號)→ 仍算同意
63
+ rm -f "$FILE"
64
+ run "我他媽到底要講幾次發版?" >/dev/null; check "9. 「到底要講幾次發版?」→ 算同意" yes "$FILE"
65
+ # 9b. **detached HEAD**(GitHub Actions 的常態)—— 本機永遠在有名字的分支上,所以這一面
66
+ # 在本機永遠測不到。2026-09-20 就是這樣:本機 12/12 綠,hooks-linux 六格紅。
67
+ rm -f "$FILE"
68
+ git switch -q --detach HEAD
69
+ run "發版" >/dev/null; check "9b. detached HEAD 仍能落地(branch 只是出處紀錄)" yes "$FILE"
70
+ git switch -q claude/another-branch
33
71
 
34
- # 8. **防線仍在**:換一條分支 = 換一份工作 那條分支沒有 receipt
35
- git switch -q -c claude/other
36
- check "8. 換分支後該分支無 receipt(仍需重新同意)" no "$DIR/branch__claude_other.json"
37
- git switch -q claude/feature
72
+ # 10. 判準來自 SSOT,不是 hook 自己寫的 regex
73
+ grep -q "release-consent-language.mjs" "$HOOK"; assert "10. hook 消費共用判準(不自己寫 regex)" $?
74
+ grep -qE "grep -qE '\(發版\|" "$HOOK"; [ $? -ne 0 ]; assert "11. hook 內不得殘留自己的同意詞 regex" $?
38
75
 
39
- # 9. main 上永不落地
40
- git switch -q main 2>/dev/null || git switch -q -c main
41
- rm -f "$FILE"; run "發版" >/dev/null; check "9. main 上說「發版」→ 不落地" no "$DIR/branch__main.json"
76
+ # 12. **隔離對照**:整支測試跑完,真 repo 的同意收據必須完全沒被動過
77
+ REAL_AFTER=$(real_fingerprint)
78
+ [ "$REAL_BEFORE" = "$REAL_AFTER" ]; assert "12. 測試全程沒有動到真 repo 的同意收據" $?
42
79
 
43
- echo "Passed: $PASS / $((PASS+FAIL))"
44
- rm -rf "$TMP"
80
+ cd / && rm -rf "$TMP"
81
+ echo " ── $PASS passed / $FAIL failed"
45
82
  [ "$FAIL" -eq 0 ]
@@ -17,7 +17,7 @@ paths:
17
17
 
18
18
  | Rule | Paths | Scope |
19
19
  |------|-------|-------|
20
- | [meta-patterns.md](meta-patterns.md) | always(fundamental)| 32 active M-rules(M1-M32 + M36,M27/M33/M34/M35 retired)|
20
+ | [meta-patterns.md](meta-patterns.md) | always(fundamental)| 33 active M-rules(M1-M32 + M36 + M37,M27/M33/M34/M35 retired)|
21
21
  | [spec-rules.md](spec-rules.md) | `**/*.spec.md` + `packages/design-system/src/**` | Spec 撰寫紀律 + SSOT / 邊界案例 / 職責分離 |
22
22
  | [ui-development.md](ui-development.md) | `**/*.tsx` + `**/*.ts` | 建 UI 前必讀 / Tailwind 5 條 / Token 4 條 / Props 命名 / shadcn |
23
23
  | [story-rules.md](story-rules.md) | `**/*.stories.tsx` | 三層 stories canonical + Title / 範例 / 撰寫紀律 |
@@ -1,10 +1,10 @@
1
- # Meta-Pattern 預警(32 active M-rules)
1
+ # Meta-Pattern 預警(33 active M-rules)
2
2
 
3
3
  **mindset #6 的具體化**。每條吸收數十個具體 bug,是失敗記憶索引上游。任務前先過全部 M-rules(M1-M32 + M36,M27 retired 2026-05-15 → M23(c) child / M33-M35 retired 2026-05-22 → folded into M20/M7/M23(d) per `/knowledge-prune` deep audit Lens 1+2)。
4
4
 
5
5
  ## 2026-05-10 cluster cross-link(per codex Q-13 deep prune audit + 2026-05-22 fold update)
6
6
 
7
- 32 M-rules(M1-M32 + M36,M27/M33/M34/M35 retired)分 6 cluster(舉一反三 grouping):
7
+ 33 M-rules(M1-M32 + M36 + M37,M27/M33/M34/M35 retired)分 6 cluster(舉一反三 grouping):
8
8
 
9
9
  | Cluster | M-rules | 共同主軸 |
10
10
  |---|---|---|
@@ -13,7 +13,7 @@
13
13
  | **C. Auto-integrate pipeline** | M14 / M19 / M31 | 對話結論 5-layer / trigger phrase auto-pipeline / claude-codex 5-step |
14
14
  | **D. Verify / similar-bug / visual coverage** | M10 / M13 / M15 | proactive scan / user 第 2 次截圖 / visual-audit-coverable |
15
15
  | **E. Propose-time discipline** | M18 / M21@watch | 4-Q 自檢 / 新元件抽象前 prop variant test |
16
- | **F. Sharp invariants(各自獨立)** | M2 / M3 / M4 / M5 / M6 / M7(含 M34 子規則)/ M9 / M11 / M12 / M16@watch / M17 / M20(含 M33 子規則)/ M24 / M25@watch / M28 / M32 / M36 | sharp invariant(M30 歸 cluster A,曾誤雙列) |
16
+ | **F. Sharp invariants(各自獨立)** | M2 / M3 / M4 / M5 / M6 / M7(含 M34 子規則)/ M9 / M11 / M12 / M16@watch / M17 / M20(含 M33 子規則)/ M24 / M25@watch / M28 / M32 / M36 / M37 | sharp invariant(M30 歸 cluster A,曾誤雙列) |
17
17
 
18
18
  `@watch` 標記(2026-05-15)= 該 M-rule 目前 absorbs 1-2 bugs(charter 要 ≥3)。1 quarter 觀察:無新 bug → demote 為既有 M-rule 子規則;有新 bug → 升 sharp keep。**2026-05-22 fold prune**:M33/M34/M35 各 absorb 1 bug → folded into parent cluster home(M33→M20 / M34→M7 / M35→M23(d)),減 3 條 M-rule 同時保留全部 invariants。Velocity 修正:本季 net add 12 rules(15 new − 3 fold)= 18 → 12 趨近 charter ≤3/quarter target,持續觀察。
19
19
 
@@ -77,6 +77,8 @@
77
77
 
78
78
  <!-- M35 retired 2026-05-22(per /knowledge-prune deep audit Lens 1+2):folded into M23(d) as sub-rule「Nearest same-purpose canonical wins」。Original verdict(codex 2026-05-20「Cite 存在 ≠ consume 落實」)+ Triple test + registry SSOT + hook R8 retained at M23(d)。Single-bug rule + same-class「優先消費既有」與 M23 合併。 -->
79
79
 
80
+ | **M37** | **禁以「當下剛好成立的觀察量」代替要保證的性質(identity substitution)**(2026-09-20 立,一天內同根因發作 7 次)。任何旗標/欄位/名稱/時間,若它**不是**那個性質本身、只是當時剛好跟它一起成立,就是代理;代理會在**寫的當下看起來完全正確**,然後在條件變了以後靜默失效 —— 而且通常**零報錯**。**三問**(寫任何判斷式、閘、契約前):(1) 我要保證的性質**逐字**是什麼?(2) 我實際拿來判斷的是**哪個值**?(3) 兩者何時會分開?想得出任何一種情境,就是代理,換成直接量那個性質。**七個同日錨例**:①「user 同意發這份工作」寫成綁 commit SHA → 發版必然產生新 commit,user 被迫為同一份工作講六次「發版」;②「必過 check」綁 check 名字;③「出處可信」綁 job 名;④「這道閘存在」= 檔案存在(沒有任何執行面呼叫它);⑤ 自家 helper 回 `{ok,…}` 卻讀 `.status`(`undefined !== 0` 恆真);⑥「畫面已渲染」= 過了 900 毫秒(慢的 runner 上假紅,還指控一個不存在的問題);⑦「已派工」被當成「已處理完」—— 而派工的目的正是讓分支**稍後**出現,於是正確路徑被自己鎖死,空轉到逾時。**⑦ 的特徵值得單獨記**:兩件事的**時間順序相反**時,共用旗標必然鎖死後發生的那件。**驗法**:判斷式抽成純函式 + 判定表,但**必須連「那個值是誰算的」一起測**(M32 的參數邊界盲點),而且測**序列**不只測靜態幾格 —— ⑦ 只在「派工之後分支才出現」這個順序上發作。呼叫端必須真的消費那支純函式,否則是兩份平行實作。 | 2026-09-20 一日七犯:consent↔commit / required check↔名字 / provenance↔job 名 / 閘存在↔檔案存在 / 回傳形狀假設 / 已渲染↔900ms / 已派工↔已處理(全部有 commit + 兩面對照組 receipt) |
81
+
80
82
  ## 判斷 meta-principle 是否漏寫的 test
81
83
 
82
84
  - 同類 bug 一年內被糾正 3 次 → meta-principle 漏寫或沒執行,檢討本清單
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "git+https://github.com/ajenchen/design-system.git"
6
6
  },
7
- "version": "0.1.0-beta.139",
7
+ "version": "0.1.0-beta.140",
8
8
  "private": false,
9
9
  "description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
10
10
  "type": "module",