@qijenchen/design-system 0.1.0-beta.140 → 0.1.0-beta.142
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/ds-canonical/fork/AGENTS.md +3 -3
- package/ds-canonical/fork/common/instruction.md +3 -3
- package/ds-canonical/fork/consumer/lock.json +7 -7
- package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +24 -2
- package/ds-canonical/fork/governance.lock +4 -4
- package/ds-canonical/fork/manifest.json +10 -10
- package/ds-canonical/hooks/record_release_consent.sh +8 -1
- package/ds-canonical/hooks/tests/test_record_release_consent.sh +13 -0
- package/ds-canonical/references/ssot-index.md +1 -1
- package/ds-canonical/rules/meta-patterns.md +3 -3
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +75 -0
- package/package.json +1 -1
- package/src/components/AgentPanel/agent-panel.anatomy.stories.tsx +2 -2
- package/src/components/AgentPanel/agent-panel.spec.md +1 -1
- package/src/components/AgentPanel/agent-panel.stories.tsx +3 -3
- package/src/components/Dialog/dialog.stories.tsx +6 -6
- package/src/components/FileViewer/file-viewer.stories.tsx +2 -2
- package/src/components/PeoplePicker/people-picker.stories.tsx +1 -1
- package/src/tokens/color/color.spec.md +30 -0
- package/src/tokens/uiSize/uiSize.css +4 -2
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
- Never install a mutable tag or semver range for internal packages. Upgrades are reviewed pull
|
|
21
21
|
requests pinning an exact version with the regenerated lock/adapters.
|
|
22
22
|
<!-- canonical-decision-authority:start -->
|
|
23
|
-
**Decision/Engineering Authority**:user 只拍板產品/UI/UX SSOT
|
|
23
|
+
**Decision/Engineering Authority**:user 只拍板產品/UI/UX SSOT 真取捨及可感知/產品語意變更(behavior/interaction/IA/visual/token/layout/content/a11y/canonical rules);核准 = user 在對話中對 **exact target + choice** 說可(**「我說可以就是可以,就是授權」— user 2026-08-04 verbatim**;最新一則 user 訊息的明確 blanket 授權即核准當下 pending 的 exact 提案)。引用/條件/舊 scope/跨 target 無效。其餘工程/external writes 皆 Standing Authorization AUTO,含已核准 UI/UX 實作/機械 generation/sync,與 `infra/governance/release-workflow.json` 的五步;依 frozen scope、SSOT、required checks、security、least privilege、rollback/readback 收斂,不逐 milestone 重問。**Deep Audit 必須在單一 branch/PR 完成 remediation 與 local/CI candidate validation**,禁把 immutable publish 當 iteration/test loop,**每次 audit 最多一次 final release**;額外 release 需 incident evidence ref。Certification/rollout/preview-canary/independent review 都是**明確要求時**的附加 assurance,不得進入標準五步的 blocking graph。(digest 佐證等細則見 `governance/memory/feedback_ship_then_revert_anti_pattern.md`。)
|
|
24
24
|
|
|
25
|
-
**Visual baseline**:user 對 exact image set/UI/UX
|
|
25
|
+
**Visual baseline**:user 對 exact image set/UI/UX 語意說「可以改」即拍板;Agent 自動 apply/generate/test/commit/PR/CI/merge,禁再核准或簽章。只有 user 明確要求 independent cryptographic review 才啟用該 policy。
|
|
26
26
|
|
|
27
|
-
**Human-only boundaries**:僅 login/MFA/OAuth/owner/billing、缺 credential reference
|
|
27
|
+
**Human-only boundaries**:僅 login/MFA/OAuth/owner/billing、缺 credential reference(只問 vault/Secret Manager 的 reference,禁 secret)、plan 外付費、法律/帳號/組織權限/商業承諾,以及上述產品決策。Agent 先完成唯一方案與 preflight,只問**一個 exact action**,readback 後續跑;technical failure 一律 fail-closed,不是 human decision。
|
|
28
28
|
<!-- canonical-decision-authority:end -->
|
|
29
29
|
|
|
30
30
|
## Six working principles
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
- Never install a mutable tag or semver range for internal packages. Upgrades are reviewed pull
|
|
21
21
|
requests pinning an exact version with the regenerated lock/adapters.
|
|
22
22
|
<!-- canonical-decision-authority:start -->
|
|
23
|
-
**Decision/Engineering Authority**:user 只拍板產品/UI/UX SSOT
|
|
23
|
+
**Decision/Engineering Authority**:user 只拍板產品/UI/UX SSOT 真取捨及可感知/產品語意變更(behavior/interaction/IA/visual/token/layout/content/a11y/canonical rules);核准 = user 在對話中對 **exact target + choice** 說可(**「我說可以就是可以,就是授權」— user 2026-08-04 verbatim**;最新一則 user 訊息的明確 blanket 授權即核准當下 pending 的 exact 提案)。引用/條件/舊 scope/跨 target 無效。其餘工程/external writes 皆 Standing Authorization AUTO,含已核准 UI/UX 實作/機械 generation/sync,與 `infra/governance/release-workflow.json` 的五步;依 frozen scope、SSOT、required checks、security、least privilege、rollback/readback 收斂,不逐 milestone 重問。**Deep Audit 必須在單一 branch/PR 完成 remediation 與 local/CI candidate validation**,禁把 immutable publish 當 iteration/test loop,**每次 audit 最多一次 final release**;額外 release 需 incident evidence ref。Certification/rollout/preview-canary/independent review 都是**明確要求時**的附加 assurance,不得進入標準五步的 blocking graph。(digest 佐證等細則見 `governance/memory/feedback_ship_then_revert_anti_pattern.md`。)
|
|
24
24
|
|
|
25
|
-
**Visual baseline**:user 對 exact image set/UI/UX
|
|
25
|
+
**Visual baseline**:user 對 exact image set/UI/UX 語意說「可以改」即拍板;Agent 自動 apply/generate/test/commit/PR/CI/merge,禁再核准或簽章。只有 user 明確要求 independent cryptographic review 才啟用該 policy。
|
|
26
26
|
|
|
27
|
-
**Human-only boundaries**:僅 login/MFA/OAuth/owner/billing、缺 credential reference
|
|
27
|
+
**Human-only boundaries**:僅 login/MFA/OAuth/owner/billing、缺 credential reference(只問 vault/Secret Manager 的 reference,禁 secret)、plan 外付費、法律/帳號/組織權限/商業承諾,以及上述產品決策。Agent 先完成唯一方案與 preflight,只問**一個 exact action**,readback 後續跑;technical failure 一律 fail-closed,不是 human decision。
|
|
28
28
|
<!-- canonical-decision-authority:end -->
|
|
29
29
|
|
|
30
30
|
## Six working principles
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"schemaVersion": 1,
|
|
4
4
|
"role": "template-consumer",
|
|
5
5
|
"release": {
|
|
6
|
-
"designSystem": "0.1.0-beta.
|
|
7
|
-
"storybookConfig": "0.1.0-beta.
|
|
6
|
+
"designSystem": "0.1.0-beta.142",
|
|
7
|
+
"storybookConfig": "0.1.0-beta.142"
|
|
8
8
|
},
|
|
9
9
|
"upgradeTrust": {
|
|
10
10
|
"schemaVersion": 1,
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"implementationSha256": "98b74118fb5b410b77ad2994aa9105fa7609c7353822f8c5213cce54d94f0af4"
|
|
43
43
|
},
|
|
44
44
|
"payload": {
|
|
45
|
-
"forkCorpusLockSha256": "
|
|
45
|
+
"forkCorpusLockSha256": "c36b98e7a275779a7bfc3f35019cf2044b29649158276c5af47b94a2f10ba3bc",
|
|
46
46
|
"discoveryPolicySha256": "fba289aeeb5a3f11139b9589f7ab08b5e8c8799c7bdf8f0ea57ebfeb8e7c9da6",
|
|
47
47
|
"providerMaterializersSha256": "732e85b01a12e7dd19469e26e4427f0e0cfe5117a78fc9f69c2f0f89702f463a",
|
|
48
|
-
"providerLifecycleSha256": "
|
|
48
|
+
"providerLifecycleSha256": "ac7706d54514a21e1f4b68f353b5ca9f509cb4cb078bf303aa7c468dd434e394",
|
|
49
49
|
"providerInventorySha256": "00adf1c4f6b3367055a9cc3fdfe658660771b4c9931f696e12761a95076ad92c",
|
|
50
50
|
"executionRuntimeSha256": "d9377cc45308731f287d7b1711b7c38fc4e02ff8f84729bcbbf01bb52d136904",
|
|
51
|
-
"commonInstructionSha256": "
|
|
51
|
+
"commonInstructionSha256": "e730127bff1820a035b11c3b3119e7354975d7e9c8c69d7e8b1adfe307c53ecc",
|
|
52
52
|
"providerArtifactDigests": {
|
|
53
53
|
"claude": {
|
|
54
54
|
"instructionSha256": "f56a44c730506258cc7e2581dbdd31bfd1e6d13d60dc8a8ee8bcc398228453fb",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"treeDigests": {}
|
|
58
58
|
},
|
|
59
59
|
"codex": {
|
|
60
|
-
"instructionSha256": "
|
|
60
|
+
"instructionSha256": "e730127bff1820a035b11c3b3119e7354975d7e9c8c69d7e8b1adfe307c53ecc",
|
|
61
61
|
"hookSha256": "7c1a887511e6d4e4d06c374084544c8ef77ac684a196d4b7bad2965cb4200226",
|
|
62
62
|
"skillsSha256": "611ee0bf8e283c622458009dfd0b9f3bfba64de10d76d1d7817e1c8cdb524a1f",
|
|
63
63
|
"treeDigests": {}
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"governance/trust/issuers.json": "b4da935a9d39239f99fdb15f9ee0a05b538ac0c427b32b77fce8010560fdef2b",
|
|
102
102
|
"governance/visual-baseline-review-policy.json": "339179f3bb7805f067c40cb22509734fa3f1cf2834129e64f6e0bf08aa70b334",
|
|
103
103
|
"schemas/provider-cli-toolchain.schema.json": "7e5c6c14c8a9295373f56df968afdb8cb8888764ce1b060993a655ccec8ac61d",
|
|
104
|
-
"scripts/audit-consumer-a11y.mjs": "
|
|
104
|
+
"scripts/audit-consumer-a11y.mjs": "b4305068d9a403726c2370b7678566484f21a84fd6478e58491ab0ffe79bdaef",
|
|
105
105
|
"scripts/check-plugin-installed.mjs": "f2a673cc011743fe51f87746b9aebbd3332378cd2732088eda621e16a3c74386",
|
|
106
106
|
"scripts/consumer-source-harness.mjs": "725072155d0b15b373bf6befb333cd2085dc8a38fff8538b4f4c8ea56f9c5308",
|
|
107
107
|
"scripts/lib/a11y-static-server.mjs": "29072309bd7aba9a64c54a77370a9633a6b95bf135bfde39dd7098a97d23b373",
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
import { existsSync, lstatSync, readdirSync, realpathSync } from 'node:fs'
|
|
13
13
|
import { join, resolve } from 'node:path'
|
|
14
14
|
import { chromium } from 'playwright'
|
|
15
|
+
|
|
16
|
+
// 受限沙箱起 Chromium 需要的兩個參數。**刻意就地寫,不 import 共用 helper**:
|
|
17
|
+
// 這支腳本會被交付到 consumer repo,而 import `lib/launch-browser.mjs` 會讓那支也必須進
|
|
18
|
+
// 四個交付/白名單清單(2026-09-21 實測:fork delivery / 檔案所有權 / published-template 鏡像 /
|
|
19
|
+
// 鏡像路徑白名單),為兩個旗標擴張消費端交付面不值得 —— M17 的門檻是同值 **3+** 個消費者才抽共用,
|
|
20
|
+
// 這裡是第 2 個。DS 內部閘的單一住所仍是 `scripts/lib/launch-browser.mjs` 的 `SANDBOX_ARGS`,
|
|
21
|
+
// 兩邊若要改必須一起改(值相同:單行程 + 關沙箱)。
|
|
22
|
+
const SANDBOX_ARGS = ['--single-process', '--no-sandbox']
|
|
15
23
|
import { createRequire } from 'node:module'
|
|
16
24
|
import { fileURLToPath } from 'node:url'
|
|
17
25
|
import { resolveA11yStaticFile, startA11yStaticServer } from './lib/a11y-static-server.mjs'
|
|
@@ -89,7 +97,10 @@ export async function auditUrlWithAxe({
|
|
|
89
97
|
} = {}) {
|
|
90
98
|
let browser
|
|
91
99
|
try {
|
|
92
|
-
|
|
100
|
+
// 用 repo 共用的沙箱參數(`lib/launch-browser.mjs` 的單一住所),不要自己起一套 ——
|
|
101
|
+
// 2026-09-21 夜間 harness 實測:裸 launch 在受限沙箱起不來,而全 repo 其他瀏覽器閘都能跑,
|
|
102
|
+
// 差別就只在這組參數。同一份程式在不同環境給不同答案,本身就是量具問題。
|
|
103
|
+
browser = await browserType.launch({ headless: true, args: [...SANDBOX_ARGS] })
|
|
93
104
|
const page = await (await browser.newContext()).newPage()
|
|
94
105
|
const response = await page.goto(url, { waitUntil: 'networkidle' })
|
|
95
106
|
if (!response || !response.ok()) throw new Error(`owned app server returned ${response?.status() ?? 'no response'}`)
|
|
@@ -163,7 +174,18 @@ export async function runConsumerA11yAudit({
|
|
|
163
174
|
|
|
164
175
|
logger.log('')
|
|
165
176
|
if (errors.length > 0) {
|
|
166
|
-
|
|
177
|
+
// **「儀器失效」與「產品有問題」要分開報**(2026-09-21)。
|
|
178
|
+
// 原本兩者共用 errors 陣列、共用同一句「N app(s) have a11y issues」——
|
|
179
|
+
// 於是瀏覽器起不來會被報成「這個 app 有無障礙問題」,指控一個不存在的問題。
|
|
180
|
+
// 兩邊都照樣紅(缺證據不得放行),但**訊息必須指向正確的方向**,
|
|
181
|
+
// 否則下一個人會拿著錯的線索去查錯的地方。
|
|
182
|
+
const infrastructure = errors.filter((item) => item.auditError)
|
|
183
|
+
const product = errors.filter((item) => !item.auditError)
|
|
184
|
+
if (product.length > 0) logger.error(`❌ ${product.length} app(s) have a11y issues`)
|
|
185
|
+
if (infrastructure.length > 0) {
|
|
186
|
+
logger.error(`❌ ${infrastructure.length} app(s) 量不到(儀器失效,不是產品有問題):`)
|
|
187
|
+
for (const item of infrastructure) logger.error(` - ${item.app}:${String(item.auditError).split('\n')[0]}`)
|
|
188
|
+
}
|
|
167
189
|
return 1
|
|
168
190
|
}
|
|
169
191
|
logger.log('✅ All apps pass WCAG 2 A + AA')
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
{
|
|
8
8
|
"schemaVersion": 1,
|
|
9
9
|
"file": "AGENTS.md",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "e730127bff1820a035b11c3b3119e7354975d7e9c8c69d7e8b1adfe307c53ecc",
|
|
11
11
|
"source": "common instruction compatibility alias",
|
|
12
12
|
"classification": null,
|
|
13
13
|
"destination": null
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
{
|
|
272
272
|
"schemaVersion": 1,
|
|
273
273
|
"file": "common/instruction.md",
|
|
274
|
-
"sha256": "
|
|
274
|
+
"sha256": "e730127bff1820a035b11c3b3119e7354975d7e9c8c69d7e8b1adfe307c53ecc",
|
|
275
275
|
"source": "scripts/fork-role-sources/AGENTS.product.md + AGENTS.md#canonical-decision-authority",
|
|
276
276
|
"classification": null,
|
|
277
277
|
"destination": null
|
|
@@ -591,7 +591,7 @@
|
|
|
591
591
|
{
|
|
592
592
|
"schemaVersion": 1,
|
|
593
593
|
"file": "consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob",
|
|
594
|
-
"sha256": "
|
|
594
|
+
"sha256": "b4305068d9a403726c2370b7678566484f21a84fd6478e58491ab0ffe79bdaef",
|
|
595
595
|
"source": "scripts/audit-consumer-a11y.mjs",
|
|
596
596
|
"classification": null,
|
|
597
597
|
"destination": "scripts/audit-consumer-a11y.mjs"
|
|
@@ -919,7 +919,7 @@
|
|
|
919
919
|
{
|
|
920
920
|
"schemaVersion": 1,
|
|
921
921
|
"file": "manifest.json",
|
|
922
|
-
"sha256": "
|
|
922
|
+
"sha256": "3038331b69df4216d22da5c5db400e1a3b8a3617e9d7bcf29092e0e626ffdf16",
|
|
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.
|
|
107
|
+
"releaseVersion": "0.1.0-beta.142",
|
|
108
108
|
"immutableHead": {
|
|
109
109
|
"providerInventorySha256": "86e9d0668cb54ad1bf05c36e78b4aa80417d5f468c929dd3ddcf69c1afbf32e4",
|
|
110
|
-
"releaseVersion": "0.1.0-beta.
|
|
111
|
-
"snapshotSha256": "
|
|
110
|
+
"releaseVersion": "0.1.0-beta.141",
|
|
111
|
+
"snapshotSha256": "d8d011add214941a53850b5e73deee2a651aba0cd6403578e278c0cefca8f4f6"
|
|
112
112
|
},
|
|
113
|
-
"ledgerSha256": "
|
|
114
|
-
"previousSnapshotSha256": "
|
|
115
|
-
"currentSnapshotSha256": "
|
|
113
|
+
"ledgerSha256": "cfb9144a5670bd3266bc91f4035f4ed6e26b82c9b2064979090affa14eddb896",
|
|
114
|
+
"previousSnapshotSha256": "d8d011add214941a53850b5e73deee2a651aba0cd6403578e278c0cefca8f4f6",
|
|
115
|
+
"currentSnapshotSha256": "b3d88cb3d3099d879a2ad4460d9ecf12436821c60d1d0b0e82bc9986f5244b14",
|
|
116
116
|
"currentProviderInventorySha256": "86e9d0668cb54ad1bf05c36e78b4aa80417d5f468c929dd3ddcf69c1afbf32e4",
|
|
117
117
|
"retiredProviders": [],
|
|
118
118
|
"currentSnapshot": {
|
|
119
|
-
"releaseVersion": "0.1.0-beta.
|
|
120
|
-
"previousSnapshotSha256": "
|
|
119
|
+
"releaseVersion": "0.1.0-beta.142",
|
|
120
|
+
"previousSnapshotSha256": "d8d011add214941a53850b5e73deee2a651aba0cd6403578e278c0cefca8f4f6",
|
|
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.
|
|
197
|
-
"previousSnapshotSha256": "
|
|
196
|
+
"releaseVersion": "0.1.0-beta.141",
|
|
197
|
+
"previousSnapshotSha256": "63e26cddf08fa7fa1b8cca9047288d8a348605518c22b55e1734f7dd3962fff9",
|
|
198
198
|
"providers": [
|
|
199
199
|
{
|
|
200
200
|
"id": "claude",
|
|
@@ -51,6 +51,11 @@ case "$KIND" in
|
|
|
51
51
|
echo "🛑 偵測到「不要發版」但本來就沒有有效同意,無需撤回。"
|
|
52
52
|
fi
|
|
53
53
|
;;
|
|
54
|
+
deferred)
|
|
55
|
+
# 條件/延後的說法(等我看完再發版 / 確認完才發)。不記錄也不撤回,但一定要出聲 ——
|
|
56
|
+
# 這道閘存在的理由就是「user 看過預覽、確認過,才發」,把條件句當成現在就發等於自廢。
|
|
57
|
+
echo "⏸ 偵測到發版字眼,但判為**條件或延後**的說法(${REASON}),**未記錄為同意**。等你確認完再說一次「發版」即可。"
|
|
58
|
+
;;
|
|
54
59
|
question)
|
|
55
60
|
# 保守側:不記錄也不撤回。但一定要說出來,否則 user 會以為自己已經授權了。
|
|
56
61
|
echo "❓ 偵測到發版相關字眼,但判為問句(${REASON}),**未記錄為同意**。若這是要我發版,直接說「發版」即可。"
|
|
@@ -60,7 +65,9 @@ case "$KIND" in
|
|
|
60
65
|
echo "⚠️ 目前在 main,發版同意只在工作分支上記錄;請先切到工作分支。"
|
|
61
66
|
exit 0
|
|
62
67
|
fi
|
|
63
|
-
|
|
68
|
+
# --source hook-user-prompt:這一句是 user 當場打進對話的,不是 agent 事後宣稱的。
|
|
69
|
+
# 兩條路的判準相同,但可信度不同,收據要記得下這個差別(2026-09-21)。
|
|
70
|
+
OUT=$(cd "$REPO_ROOT" && node "$ORCH" consent --quote "$PROMPT" --branch "$BRANCH" --source hook-user-prompt 2>&1 || echo "")
|
|
64
71
|
if printf '%s' "$OUT" | grep -q RELEASE_CONSENT_RECORDED; then
|
|
65
72
|
echo "✅ 已記錄發版同意(綁「你看過的預覽內容」,不綁 commit 也不綁分支)。版號 bump、CI 修正、換分支都不需要再講一次;只有預覽看得見的內容變了才需要重新確認。"
|
|
66
73
|
else
|
|
@@ -69,10 +69,23 @@ git switch -q --detach HEAD
|
|
|
69
69
|
run "發版" >/dev/null; check "9b. detached HEAD 仍能落地(branch 只是出處紀錄)" yes "$FILE"
|
|
70
70
|
git switch -q claude/another-branch
|
|
71
71
|
|
|
72
|
+
# 9c. **條件 / 延後**的說法不得落地,而且必須出聲(2026-09-21 對抗稽核:
|
|
73
|
+
# 先前這類會被判成「現在就發版」並寫出有效收據 —— 連 SSOT 自己存的 userVerbatim 都會)
|
|
74
|
+
rm -f "$FILE"
|
|
75
|
+
run "等我看完預覽再發版" >/dev/null; check "9c. 「等我看完預覽再發版」→ 不落地" no "$FILE"
|
|
76
|
+
run "等我看完預覽再發版" 2>/dev/null | grep -q "條件或延後"; assert "9d. 條件句必須印出「條件或延後」" $?
|
|
77
|
+
# 9e. 純問句那條路(先前沒覆蓋,把 SSOT 的問句判準停用它照樣全綠)
|
|
78
|
+
run "現在可以發版嗎?" >/dev/null; check "9e. 「現在可以發版嗎?」→ 不落地" no "$FILE"
|
|
79
|
+
run "發版" >/dev/null; check "9f. 「發版」→ 落地" yes "$FILE"
|
|
80
|
+
|
|
72
81
|
# 10. 判準來自 SSOT,不是 hook 自己寫的 regex
|
|
73
82
|
grep -q "release-consent-language.mjs" "$HOOK"; assert "10. hook 消費共用判準(不自己寫 regex)" $?
|
|
74
83
|
grep -qE "grep -qE '\(發版\|" "$HOOK"; [ $? -ne 0 ]; assert "11. hook 內不得殘留自己的同意詞 regex" $?
|
|
75
84
|
|
|
85
|
+
# 11b. hook 落地的收據必須自報出處 —— 不得跟 agent 手動落地混為一談(2026-09-21)。
|
|
86
|
+
# 兩條路的判準相同,但可信度不同:前者是 user 當場打進對話的字,後者是 agent **宣稱** user 說過。
|
|
87
|
+
grep -q -- "--source hook-user-prompt" "$HOOK"; assert "11b. hook 落地自報出處(--source hook-user-prompt)" $?
|
|
88
|
+
|
|
76
89
|
# 12. **隔離對照**:整支測試跑完,真 repo 的同意收據必須完全沒被動過
|
|
77
90
|
REAL_AFTER=$(real_fingerprint)
|
|
78
91
|
[ "$REAL_BEFORE" = "$REAL_AFTER" ]; assert "12. 測試全程沒有動到真 repo 的同意收據" $?
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
| **DataTable scroll**(3-region synced + cross-OS scrollbar)| `data-table.spec.md:245-251` | 「center body 用 native overflow-x-auto(非 ScrollArea),pinned column 三 region 同步」 | ✅ aligned(v15.13 interim CSS parity for Windows Bug H,deferred ScrollArea full migration)| 2026-04-30 / 2026-05-07 |
|
|
23
23
|
| **Cell view vs edit content position**(picker offset)| `field.spec.md`「Layout Family 4」段 + `data-table.spec.md`「Body cell internal」row | 「Field control view + edit 共用同 wrapper geometry(Model A:view×default = edit 幾何減 chrome,保留 px/py)」 | ✅ aligned:DataTable cell 場景走 Field naked(`showDisplayEndIcon` opt-in);standalone `mode="view"` 走 view×default = edit 幾何減 chrome(2026-07-16 Model A,原 bare-span deferral 已兌現;值內容層 4-mode 共享 renderer,見 select.tsx 檔頭 `@renderer-symmetry-allow`)| 2026-05-07~08 round 1-5 → 2026-07-16 Model A resolved |
|
|
24
24
|
| **Field state machine focus dominates**(v13.3)| `field-wrapper.tsx` cva compoundVariants + `field.spec.md`「focus dominates」段 + `check_field_family_invariants.sh` A.3 | 「focus dominates everything;naked 不寫平行 outline ring」 | ✅ aligned;hook A.3 enforce | 2026-05-06 v9-v13.3 5 round refinement |
|
|
25
|
-
| **Protected solo workflow**(1 task = 1 branch + 1 PR / required CI / standing engineering delegation)| `infra/governance/release-workflow.json` + `feedback_solo_dev_workflow.md` + AGENTS.md「# Git / release canonical」 | protected main 禁 direct push;PR 建立正常;required CI/conversations resolved **且 user
|
|
25
|
+
| **Protected solo workflow**(1 task = 1 branch + 1 PR / required CI / standing engineering delegation)| `infra/governance/release-workflow.json` + `feedback_solo_dev_workflow.md` + AGENTS.md「# Git / release canonical」 | protected main 禁 direct push;PR 建立正常;required CI/conversations resolved **且 user 對他看過的預覽內容說過「發版」**(綁預覽內容指紋,不綁 commit 也不綁分支;一份同意 = 一次 final release)後,由 `release:auto` 走 `pr-checks → merge → publish → readback → consumer`(缺同意收據會停在 `AWAITING_USER_RELEASE_CONSENT`;規則 SSOT = AGENTS.md「# Git / release canonical」1.5);preview/canary/attestation 非 blocking | ✅ aligned(M28 + hooks + GitHub ruleset)| 2026-07-20 migration / 2026-08-02 five-step reconciliation |
|
|
26
26
|
| **Wrapper-vs-primitive schema unify**(SelectOption / MenuItemProps 等)| `SelectMenu/select-menu.tsx` SelectMenuOption + `Combobox/combobox.tsx` extends + `PeoplePicker/people-picker.tsx` extends | 「wrapper API schema 必 `extends primitive`,wrapper 內部 mapping 必 forward 全 primitive surface field(M30)」 | ✅ aligned(2026-05-10 Round 1 commit `561945b` Combobox extends + forward 全 field;PeoplePicker 對齊 + hook `check_wrapper_primitive_schema_drift.sh` BLOCKER)| 2026-05-10 M30 codify |
|
|
27
27
|
| **空值顯示符號 + 分流**(empty value display)| `field-context.ts` `useFieldEmptyDisplay()`(分流 hook)+ `field-wrapper.tsx` `EMPTY_DISPLAY = '-'`(半形常數)+ `field-controls.spec.md`「null / undefined 值」段 | 「符號 = 半形 hyphen `-`(U+002D),**非**全形 em dash;`surface==='table-cell' && isEditable ? '' : '-'`;可編輯 form edit 走 native placeholder。全 Field family 空值渲染必經 hook,consumer 禁直引 `EMPTY_DISPLAY`」 | ✅ aligned(2026-07-08 user 拍板半形;hook `check_field_family_invariants.sh` A.6 BLOCKER — 禁非 owner 直引常數 + 禁 hardcode 全形 em dash;DescriptionList / ProfileCard / FileViewer consumer 同步半形)| 2026-07-08 user directive |
|
|
28
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Meta-Pattern 預警(33 active M-rules)
|
|
2
2
|
|
|
3
|
-
**mindset #6 的具體化**。每條吸收數十個具體 bug,是失敗記憶索引上游。任務前先過全部 M-rules(
|
|
3
|
+
**mindset #6 的具體化**。每條吸收數十個具體 bug,是失敗記憶索引上游。任務前先過全部 M-rules(**權威清單見下方 cluster 表,本行不重述** —— 同一份列舉出現三處必然漂移;2026-09-21 新增 M37 時這裡就沒跟上,而 counter 只比數字不比清單所以零訊號)。
|
|
4
4
|
|
|
5
5
|
## 2026-05-10 cluster cross-link(per codex Q-13 deep prune audit + 2026-05-22 fold update)
|
|
6
6
|
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
| **M24** | **State 顯著性 precedence:disabled > muted > emphasis**。元件在 disabled state 時,內部所有文字載體(label / value / placeholder / icon)統一切 disabled token(`text-fg-disabled` neutral-6),**不**繼續 muted token(`text-fg-muted` neutral-7)。muted 是裝飾,disabled 是語意 state — state 勝 emphasis。同理:error / warning state 內 placeholder 應對應 state token。Hook `check_field_family_invariants.sh` A.4(原 check_disabled_placeholder_color folded,live P1 stderr)。 | 2026-05-04 5+ violation:`bareInputStyles` 永遠 muted / `select.tsx` 3 處 plain&tag empty 不分 mode / `textarea.tsx` 同;Input.tsx 唯一做對。User 5+ 次糾正才 codify |
|
|
65
65
|
| **M25** | **Layered chain invariant 必整鏈 forward(viewport-aware overlay scroll 範例)**。Overlay surface(Popover / HoverCard / Dialog / Sheet)的 viewport-aware scroll 機制要求 root → SurfaceBody 之間**所有中間 wrapper 都 forward `flex flex-col h-full`**;任何中間 div 沒 forward → SurfaceBody flex-1 失效 → body 不 scroll。同類 chain pattern:density 透傳 / fieldCtx 鏈 / theme subtree(M3 portal 逃逸對偶)。Hook `check_pattern_invariants.sh` C.1(原 check_overlay_panel_scroll_chain folded,live P1 WARN)。 | 2026-05-04:Filter / Sort panel root `<div w-[640px]>` 無 flex-col → user 縮 viewport 時 body 不 scroll。NameCard 因自設 max-h flex-col 才繞過(無中間 wrapper) |
|
|
66
66
|
| **M26** | **Behavior / visual canonical decision 前必跑 WebFetch + WebSearch 取 ≥ 3 source,不可憑印象 propose**。M22 升級版 — M22 管「寫 spec / tsx 含 claim 必附 cite」(實作後),M26 管「propose / 決策前必先 fetch」(實作前)。Pipeline:(1) WebFetch 3 家世界級 source(Atlassian / Material / Polaris / Ant / Carbon / Apple HIG / shadcn / Radix);(2) 全 403 → WebSearch fallback 用 snippet,**明示「search-only confidence」**;(3) WebFetch + WebSearch 都失敗 → STOP propose,告知 user「無法 verify,要看 screenshot/實機」。Hook `check_propose_without_benchmark.sh`(2026-05-26 backfill — 前期 doc claim 但 file missing;UserPromptSubmit 偵測 user prompt 含 propose / visual / behavior decision keyword + 近 20 turn WebFetch/WebSearch < 2 → soft inject 提醒)。 | 2026-05-05:user 反覆糾「為什麼每次都沒 webfetch 只憑印象」— Jira drag handle / cell display 兩題我憑印象 propose 多輪;升級成硬規範 |
|
|
67
|
-
| **M28** | **Git ops 必先 grep canonical**(mindset #2 AI-ops 子規則)。AI 執行 branch/PR/merge/release ops 前必讀 `governance/memory/feedback_solo_dev_workflow.md` + `AGENTS.md # Git / release canonical`。鐵律:1 task = 1 working branch + 1 PR;protected main 禁 direct push;PR 建立是正常路徑;standard release 只走 `infra/governance/release-workflow.json` 的五步,由 `release:auto` 續跑 —— **但合併前有一道發版同意閘**(user
|
|
67
|
+
| **M28** | **Git ops 必先 grep canonical**(mindset #2 AI-ops 子規則)。AI 執行 branch/PR/merge/release ops 前必讀 `governance/memory/feedback_solo_dev_workflow.md` + `AGENTS.md # Git / release canonical`。鐵律:1 task = 1 working branch + 1 PR;protected main 禁 direct push;PR 建立是正常路徑;standard release 只走 `infra/governance/release-workflow.json` 的五步,由 `release:auto` 續跑 —— **但合併前有一道發版同意閘**(user 說「發版」才落 receipt,綁 **user 看過的預覽內容**、不綁 commit 也不綁分支,且一份同意只授權一次 final release;缺則停在 `AWAITING_USER_RELEASE_CONSENT`)。五步與同意閘的散文 SSOT = `AGENTS.md`「# Git / release canonical」,本條不重述。Hook `check_solo_workflow.sh` 只機械攔 R1 第二 branch / R2 direct-main push;retired tag-preflight、preview/canary、attestation、soak、fleet ceremony 不得回到 blocking graph。Break-glass override audit-logged,仍不能繞 GitHub ruleset。 | 2026-05-08 的 branch/PR sprawl促成機械化;2026-07-20 保留一任務一分支與 protected PR;2026-07-26 淘汰 chat-keyword gate,2026-08-02 再移除 retired local tag-preflight branch。 |
|
|
68
68
|
| **M29** | **DS Anchor Preflight invariant — 視覺 / 結構 propose 前必 grep `*.spec.md` 找 owner SSOT**。動 component / pattern / token 視覺結構前,**必先**:(a) `rg <touched component> + <behavior keyword>` in `packages/design-system/src/**/*.spec.md` (b) 出 **3-column owner table**(`candidate owner spec` / `canonical sentence/table row` / `conflicting code/comment if any`)(c) 若 spec ≠ code → STOP 寫 RFC 不直接 propose implementation。沒 3-column = anti-pattern,提案不被接受。SSOT index `packages/design-system/ds-canonical/references/ssot-index.md`(high-risk interface owner mapping)Step 0.1 先查再 grep verify。Hook `check_ds_anchor_preflight.sh` P0 BLOCKER(exit 2,2026-07-07 升級;bypass GOVERNANCE_BYPASS_DS_ANCHOR=1 audit-logged)。codex-collab/SKILL.md Step 0.1 mandatory pre-grep。對齊 Carbon「design spec is source of truth」+ Atlassian「system-wide coordination first」。 | 2026-05-08:Claude+Codex 連 5 round 跳過 spec anchor pre-grep,直覺加 primitive(D path / Path A);user 抓「為何沒研讀設計原則」 |
|
|
69
69
|
| **M30** | **Wrapper-vs-primitive schema unify invariant — wrapper API option / config schema 必 `extends` primitive SSOT,且 wrapper 內部轉換 mapping 必 forward 全 primitive surface field**。Primitive(`SelectMenuOption` / `MenuItemProps` / `ItemAnatomyProps` 等)宣告 SSOT 後,所有 wrapper(`Select.SelectOption` / `Combobox.SelectOption` / `PeoplePicker` 等)若有同類 schema,(a) 用 TypeScript `extends primitive` 機械繼承,**不可平行 declare** weak schema(同名 / 部分欄位)(b) wrapper 內部 `xxxToYyy()` mapping 必 forward 全 primitive surface field(不可只 forward `value` + `label`,silently drop avatar / description / disabled 等)(c) wrapper-only field 加在 `extends body`(不污染 primitive)。Hook `check_wrapper_primitive_schema_drift.sh` 機械強制(P0 BLOCKER):grep `export interface .*Option \{` cross components/,同名 ≥ 2 處且未 `extends` primitive → BLOCK。對齊 Polaris ChoiceList / Material Autocomplete / Carbon Dropdown wrapper-vs-primitive schema-extension idiom。 | 2026-05-10 PeoplePicker weak schema 漏 avatar,mapping 只 forward 2 fields(詳 historical-bugs.md) |
|
|
70
70
|
| **M32** | **Audit script 必 pixel-quantified verify ≠ attribute existence**(2026-05-12)。Audit 必驗 `rect.top / .left / .height` numeric pixel,**不**可只驗 `getAttribute('data-state')` / `class.includes(...)` = false-positive trap(DOM-pass ≠ visual-pass)。對齊 Material X-DataGrid / AG Grid playwright pixel snapshot / Polaris visual diff canonical。Hook `check_pixel_quantified_audit.sh` 機械強制(scans `getAttribute(` without paired `getBoundingClientRect(`)。**+ Sub-invariant(2026-05-30,doc-claim layer)**:story/anatomy/a11y/spec 的**文字宣稱 ≠ code 真實**——audit 必 adversarial **讀元件 .tsx +(其 wrap 的 Radix/cmdk/react-day-picker 等 lib)source**,逐句驗 keyboard map/ARIA role/focus 行為/prop 存在性/native-vs-custom;prose-skim「looks fine」= false-pass(prose-pass ≠ source-verified)。**「無 code 改動」≠ 可跳過**。`/deep-audit-cross-codex` A.1b + CP-A1b + report-validator F 機械強制。Anchor:2026-05-30 獨立 adversarial 抓 403 findings/64 單元/202 FALSE_CLAIM(前期 prose-skim story dim 全漏);prop-existence deterministic detector 因 passthrough over-flag 證實此類本質需 LLM 讀 source → 用 mandatory-dim + report-validator 保證真跑。**+ Sub-invariant「儀器要先有對照組」(2026-09-08 folded,absorbs 4 bugs)**:任何量測/閘在拿它的**綠燈**當證據前,必須先證明它**在該紅的時候會紅** —— 把被驗的東西弄壞一次,看它是否指名該處,再還原。缺這一步的綠燈是零證據。四個錨(同一天):(a) 22 支瀏覽器腳本因缺沙箱參數永遠回 `SKIPPED-ENV`,332 條不變式從沒真的跑過;(b) 自己寫的 C5/G2 測試假綠(抓到鎖定欄、用滿捲位置),補對照組才發現;(c) `transition-colors` 的 transition-property 含 `outline-color`,聚焦後立刻量抓到過渡中間值,差點寫成「全 DS 焦點框失效」;(d) 本輪新建的 7 組閘 `grep -rl <name> .github` 回 **0** —— 從沒被 CI 呼叫,跟 ci.yml 註解裡記過兩次的 distribute-column-widths / pagination 同一種病;(e) Playwright headless 預設帶 `--hide-scrollbars`,任何量捲軸厚度的探針永遠得 0 —— Bug H(Windows 捲軸溢出)五個月沒人量到就是這樣,要 `ignoreDefaultArgs` 拿掉再用根規則 `::-webkit-scrollbar{width:17px}` 造 Windows 幾何(2026-09-08);(f) 同頁多個目標逐一 `scrollIntoView` 後,先前算好的視窗座標拿去截圖是**全白** —— 截圖前一刻才對該元素重算(同日)。(g) **headless Chromium 的 rAF 被釘在 16.7ms**,連故意把每列加上 box-shadow+blur 都不動,CPU 降速 6 倍也不動 —— 任何「每幀時間」類量具在 headless 一律無效,綠燈是零證據;同一天建了兩版都被自我檢查判死(2026-09-15)。要量幀時間只能在真瀏覽器(而且分頁必須是可見的,背景分頁 rAF 不跑);headless 改用 CDP `Tracing`(看光柵/主執行緒任務)或 `LayerTree.profileSnapshot`(光柵成本),那兩個會紅。(h) **跨網域/跨主機的效能比較無效;先歸因再消融**(2026-09-15,兩天教訓):user 機器「main 17ms / 分支 33ms」追了兩天、DS 內部消融七項全 33、本機永遠重現不出 —— 最後用 Long Animation Frames API 在他機器上看長幀 `scripts[].sourceURL`,5301ms 全在 `thin-client-min.js`(那台機器的網路/安全產品對 `*.netlify.app` 注入的遠端隔離 thin client),而 main 在 github.io 是白名單本地渲染、main 放到 Netlify 一樣慢。**唯一變數是網域**。機械問法:(1) A/B 兩頁同網域嗎?(2) 長幀裡的 script 來源是我們建置裡的檔嗎?兩題沒答完前,任何消融都是白做(`governance/memory/reference_perf_validation_same_host.md`)。**機械化問法**:寫完任一閘立刻問**四**題 —— 「弄壞它會紅嗎(跑一次)」「誰呼叫它(grep CI)」「它的量測值會不會受動畫/時序影響(等穩態再量)」「**它在機器慢的時候還會綠嗎**」。**第四題是 2026-09-20 補的,而且是本條前三題全過仍被咬**:`action-bar-toolbar` 閘用 `waitUntil:'load'` + 固定睡 900ms 當「已渲染」的代理,本機 20 格全綠、對照組會紅、CI 真的有叫它 —— 前三題滿分,卻在一個**沒動任何 `.tsx`** 的 commit 上假紅,掉的正是每支示範的**第一個寬度**(冷啟動最慢),而它印的是「示範沒有消費 DataToolbar?」—— **指控一個不存在的問題,比沉默更貴**。判準:任何 `page.goto` 之後要抓元素的閘,一律等**那個元素本身**(`waitForSelector`),固定睡眠只留給「元素出現後的版面穩定」;證明方式必須**與機器速度無關** —— 造一個故意晚到的元素,證明「固定睡眠抓不到、等元素抓得到」兩面都成立(`scripts/lib/launch-browser.mjs` 的 `gotoStory`,對照組在 `action-bar-toolbar-invariant.mjs --selftest`)。同族當場全修(M10):CI 必跑且會因此假紅的另外三支 `agent-fab-drag-click` / `overlay-detached-anchor` / `data-table-fast-scroll` 一併改。**+ Sub-invariant「臨時查詢也是量具:回 0 筆先證明它拿得到資料」(2026-09-19,一天內三犯)**:同一天三次因為查詢寫錯而讀到空結果、又差點把空結果當事實 —— (1) GitHub `head_sha=` 用短 SHA(要完整 40 碼)→ 回 0 筆,差點宣稱「WM 沒有 CI」;(2) 同一個寫法再犯一次;(3) **直接捏造一串 40 碼 SHA** 貼進 API → 回 0 筆,差點宣稱「改名後 workflow 不跑了」。這跟同期修的「零筆 check ≠ 全綠」是**同一條規則**,差別只在那條管別人的資料、這條管我自己的查詢。**判準**:任何回 0 筆/空集合的查詢,在解讀它之前先用一個**已知該有資料**的輸入跑一次同一條查詢(例:換成上一個已知成功的 commit);拿不到資料就是查詢壞了,不是事實。識別字串一律從來源複製或 `rev-parse` 取得,**禁止手打或憑記憶補齊**。**Split note**:原 mega 8 sub-invariants 拆 4 home — (b) fix-time root-invariant → M12 extension / (c)(d)(h) batch + parallel + claim-verify → `/bug-fix-rhythm` skill / (e) tool preflight(CLI binary 必跑 4-test discovery:which / npx / package.json / auth.json,禁短路「not installed」假警報)— 原 memory `feedback_tool_binary_preflight_sweep.md` 已 retire,invariant 一行收容於此 / (f) ship gate → AGENTS.md `# 稽核 canonical` / (g) Layer marker → M31 + hook。git blame 2026-05-12 commit 留 audit trail。 | 2026-05-12 4 audit-vs-visual gap absorbed:cell-align / divider 2px / breadcrumb tooltip / row-alignment 全 audit「Δ=0 ALL PASS」但 user 抓視覺壞(詳 historical-bugs.md)。|
|
|
@@ -77,7 +77,7 @@
|
|
|
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)
|
|
80
|
+
| **M37** | **禁以「當下剛好成立的觀察量」代替要保證的性質(identity substitution)**(2026-09-20 立,一天內同根因發作 7 次)。任何旗標/欄位/名稱/時間,若它**不是**那個性質本身、只是當時剛好跟它一起成立,就是代理;代理會在**寫的當下看起來完全正確**,然後在條件變了以後靜默失效 —— 而且通常**零報錯**。**三問**(寫任何判斷式、閘、契約前):(1) 我要保證的性質**逐字**是什麼?(2) 我實際拿來判斷的是**哪個值**?(3) 兩者何時會分開?想得出任何一種情境,就是代理,換成直接量那個性質。**第八個形狀(2026-09-20,main 因此紅一次):「沒觀察到」被當成「沒發生」**——量測拿到 `NaN`/空集合/零筆,代表的是「這次沒看到」,不是「那件事沒發生」。錨例:列 hover 閘把 `lost = 樣本數 − 量到的樣本數`,於是截圖串流停送幀 1461ms(門檻 1500ms)那三次被當成「產品沒變色」而指控產品 —— 同一份內容在分支上剛跑綠、前 11 次 CI 全綠。程式其實**已經算出** hover 之後到達的幀數,只拿去印 debug、沒參與判定;而閘的檔頭還寫著「lost 與串流間隔無關」——**那是註解裡的斷言,程式沒有任何地方在執行它**。判準:任何「沒量到」的樣本,判定前先問「這段時間儀器**有沒有能力看到**」;看得到卻沒發生 → 真訊號;看不到 → 不得指控被測物,但也**不准默默放行**,可用樣本不足時要以**儀器失效**的名義紅(`scripts/lib/hover-latency-policy.mjs`,判定表用真實 CI 數字 + 「拿掉全盲資訊就重現誤紅」的對照組)。**七個同日錨例**:①「user 同意發這份工作」寫成綁 commit SHA → 發版必然產生新 commit,user 被迫為同一份工作講六次「發版」;②「必過 check」綁 check 名字;③「出處可信」綁 job 名;④「這道閘存在」= 檔案存在(沒有任何執行面呼叫它);⑤ 自家 helper 回 `{ok,…}` 卻讀 `.status`(`undefined !== 0` 恆真);⑥「畫面已渲染」= 過了 900 毫秒(慢的 runner 上假紅,還指控一個不存在的問題);⑦「已派工」被當成「已處理完」—— 而派工的目的正是讓分支**稍後**出現,於是正確路徑被自己鎖死,空轉到逾時。**⑦ 的特徵值得單獨記**:兩件事的**時間順序相反**時,共用旗標必然鎖死後發生的那件。**驗法**:判斷式抽成純函式 + 判定表,但**必須連「那個值是誰算的」一起測**(M32 的參數邊界盲點),而且測**序列**不只測靜態幾格 —— ⑦ 只在「派工之後分支才出現」這個順序上發作。呼叫端必須真的消費那支純函式,否則是兩份平行實作。**第九個形狀(2026-09-21):把「今天幾號」當成被驗的性質 —— 測試的顏色取決於日曆**。`test-verify-deep-audit-coverage.mjs` 的 fixture 寫死一張 2026-07-20 簽發、**2026-08-20 到期**的能力憑證,而 16 個呼叫裡有 15 個沒傳 `now`,用的是真實時間。8/20 一過 → 憑證過期 → 選不出 peer → 「peer runtime surface must be explicit」那道閘**結構上不可能觸發** → 測試紅。它紅在 8/20,而治理 harness runner 從 **8/11** 起就被另一件事擋住(一支測試自己用 `new Function`+`--eval` 違反來源政策,整個 runner 因此 blocked),夜間 lane 連續 **41 晚**全紅 —— 沒有人看見。**這條鏈是本 M-rule 的完整標本**:代理(憑證有效 ↔ 今天幾號)造出定時紅燈,另一個代理(閘存在 ↔ 有人跑)讓紅燈沒有觀眾。判準:**任何 fixture 的時間窗必須相對於「現在」推導**(`certifiedAt = now − 1d` / `expiresAt = now + 30d`),寫死日期即是定時炸彈;而且窗與時鐘的關係要有一行 assert 守著,免得反向腐爛。**第十種形狀(2026-09-21,一次 `release:auto` 裡六個位置)**:**發布鏈上「這個名字/這筆記錄存在」被當成「那件事真的發生了」** —— 版號字串有 release ↔ 這份內容已發布 / 分支有 MERGED 的 PR ↔ 這個 head 在 main 上 / tag 的名字 ↔ 那個 commit 裡的版號 / commit 上所有 check-run ↔ 守護 main 的 CI / 帳本有一行 ↔ 授權被消耗掉 / 最新的 tag ↔ 線上目前那一份。**第 2–6 個是在修第 1 個的那次跑裡才現形的**(M10:修同族要當場列完同族),代價是一個永久指向錯 commit 的受保護 tag(不能刪、不該繞)。六格對照表 + 三個遞移教訓(fixture 缺欄位 = 判定表測不到要測的事;同一個 unknown 在不同用途要走相反方向)→ `skills/design-system-audit/references/historical-bugs.md`「M37 第十種形狀」。同日一併抓到的同族:87 支 checker gate 有 **41 支沒有 meta-test**(沒人證明過它們會紅),而唯一會講出這件事的 `audit-gate-meta-test-coverage.mjs` **自己也沒有任何執行面呼叫它**;`check:bundle-size`(2026-07-14 起)與 `check:naming-structure`(2026-08-05 起)同病 —— 前者讓兩個月的體積成長全沒被擋,後者因一個空的編輯器暫存目錄假紅一個多月。 | 2026-09-20 一日七犯:consent↔commit / required check↔名字 / provenance↔job 名 / 閘存在↔檔案存在 / 回傳形狀假設 / 已渲染↔900ms / 已派工↔已處理(全部有 commit + 兩面對照組 receipt) |
|
|
81
81
|
|
|
82
82
|
## 判斷 meta-principle 是否漏寫的 test
|
|
83
83
|
|
|
@@ -262,3 +262,78 @@ DatePicker `today + selected`:藍 bar 疊在藍底隱形;`hover + disabled`:ring
|
|
|
262
262
|
- 2026-08-11 一日連環五鎖:build graph journal 死鎖(復原全刪全蓋碰禁寫目標)/ authority fingerprint 把自家 index-publish 誤判竄改 / blanket 授權辨識漏「開頭裸可以」/ `gh auth status` 帳號級體檢擋 repo-scoped token / gh Go TLS 拒 sandbox 代理——五層全是自家機制,user 原話「不要再說有人擋你了,完全沒有」;逐層拆除後 release 全自動走通,零 user 指令。
|
|
263
263
|
- 2026-08-28 hooks/scripts symlink 本地殘影(8/1 舊版 generator 合法生成、8/2 佈局改版後無人能寫回):我先誤稱「我們自己沙箱設的保護」(實查自家 settings `denyWrite=[]`,鎖是 Claude Code 對 hook 設定目錄的**平台內建**防注入保護),再指 `! git restore` 給 user(對話內 `!` 與 Bash **同一個沙箱**,必再被擋 = 指死路),user 原話「你不要再作繭自縛了,我說要做就做」;正解一行 `git update-index --skip-worktree`(只寫 `.git` 可寫區,不碰被鎖路徑)→ codify M36(b') 第 4 問(鎖的主人是誰 + 交 user 的指令必先驗證不在同牆內)。
|
|
264
264
|
- 四路同 provider 對抗審查查不到 (a) 類(reviewer 無對話紀錄),故必須以 M36 rule + 來源總帳格式機械化。
|
|
265
|
+
|
|
266
|
+
## 失敗記憶索引搬家(2026-09-21)
|
|
267
|
+
|
|
268
|
+
repo 根的治理 bootstrap 檔,其 root→cwd 合併鏈超過預設 32KiB 上限(41252 bytes),超過就**靜默截斷**——
|
|
269
|
+
整份治理內容會被砍掉一段而沒有任何訊號。這條規則 bootstrap 自己就寫著,
|
|
270
|
+
而抓它的 `check-agents-bootstrap.mjs` 從來沒有任何執行面呼叫它。
|
|
271
|
+
|
|
272
|
+
依 canonical(「新 bug → 歸 Meta-Pattern OR 索引表 1 行」+ meta-patterns「具體 bug 歷史詳解移到本檔」),
|
|
273
|
+
把下列幾列的 narrative 逐字搬來,bootstrap 只留**判準一行 + 指標**。一個 invariant 都沒有刪。
|
|
274
|
+
|
|
275
|
+
### 工具靜默陷阱(rsync 等長同秒 / rg 黏寫 flag / mktemp 失敗回空)
|
|
276
|
+
|
|
277
|
+
| 工具靜默陷阱:`rsync -a` 等長同秒跳過 / `rg` 黏寫 `-rn` 的 `-r`=replace / `mktemp -d` 失敗回空 → `cd ""` 原地 → trap 刪掉 cwd | 必 `--checksum`、flag 分開寫;mktemp 後必 `[ -n "$V" ]` + `[ -d "$V" ]` 才可正規化/註冊 cleanup(2026-07-28)。**2026-09-18 beta.134 再犯**:consumer mirror 的 `package-lock.json` 換版號後與舊版**完全等長**(版本字串/resolved URL/integrity base64 三者都是固定長度,實測 292873 bytes 不變),clone 又與生成落在同一秒 → rsync 靜默跳過、lock 沒進 commit,直到 consumer 的 `npm ci` 才炸。**上游那句 `✓ lock pins the exact released version` 還是綠的,因為它驗的是來源那份、不是真的被複製過去的那份 —— 綠燈驗錯對象比沒有綠燈更騙人**。規則早就寫了、三處也遵守了,第四處漏掉 → 機械化 `scripts/rsync-checksum-invariant.mjs`(CI required,對照組用合成檔且必須只紅在合成檔上)。**2026-09-20 同一條的第二種形狀:驗了「怎麼判」,沒驗「判的那個值怎麼來」**——發版同意改綁分支後,判定表 8 格全綠、`npm run test:release-consent` 全綠,但餵給它的 `productVisibleFilesChanged` **恆回 true**(自家 helper `run()` 回 `{ok,stdout,stderr}` 沒有 `status`,寫成 `diff.status !== 0` 就是 `undefined !== 0`,恆真且零報錯),整個修正從第一天起是死的,是我刻意去驗它才抓到。**吃參數的純函式,參數邊界就是測試的天然盲點**:判定表把值當輸入,於是永遠測不到算那個值的程式。判準:純函式測試通過後,必再問「這個參數在正式流程裡是誰算的,那支有沒有被測」,並用**真實資料**(真 git commit、真 API 回應)跑兩面對照 —— 該 true 的一筆、該 false 的一筆,兩邊都找得到才算數,找不到對照組要 fail 而不是空跑當綠|
|
|
278
|
+
|
|
279
|
+
### 量 focus 顏色不等 transition
|
|
280
|
+
|
|
281
|
+
| 量 focus 顏色不等 transition = 量到過渡中間值 | `transition-colors` 的 transition-property **含 `outline-color`**;聚焦後立刻 `getComputedStyle().outlineColor` 會抓到中間值(量到 currentColor,看起來像「焦點框顏色壞了」)。2026-09-08 差點據此寫成「全 DS 焦點框失效」,等 600ms 後三個元件都回主色。**凡量 focus 顏色先等過渡**;另 `document.body.focus()` 不重設 Tab 起點(body 不可聚焦),Tab 會從上一個聚焦元素繼續往後走,要重設只能 reload |
|
|
282
|
+
|
|
283
|
+
### 拖曳 vs 點擊的判準(輸入代理會丟 move、延後 click)
|
|
284
|
+
|
|
285
|
+
| 拖曳 vs 點擊只靠「收到幾個 pointermove」或「setTimeout(0) 內吞 click」= 輸入代理 / 遠端隔離環境靜默失效 | 代理會丟掉 / 合併 pointermove、讓 click 晚一個 task 送達;本機 Chromium 兩者都成立所以永遠重現不了(2026-09-16 FAB「拖一下就開面板」,程式碼自 9/8 起零改動)。判準必是**放開點離按下點的距離**,吞 click 的旗標由**下一次 pointerdown** 才清,鍵盤合成 click(detail 0)放行;閘 `scripts/agent-fab-drag-click-invariant.mjs` 用合成事件造「晚到的 click」與「0 個 move 放開在 80px 外」 |
|
|
286
|
+
|
|
287
|
+
### git commit 指定路徑的部分提交會鎖住 index
|
|
288
|
+
|
|
289
|
+
| `git commit -- <路徑>`(指定路徑的部分提交)會讓治理 pre-commit 掛掉 | 錯誤訊息是 `authority generation index publish could not stage:hooks/scripts`,看起來像沙箱權限問題,其實是**部分提交模式下 git 自己鎖著 index**,hook 裡的 `git update-index --cacheinfo` 拿不到鎖。同一個 `update-index` 手動跑 rc=0。正解:把要的東西 `git add` 進暫存區,然後跑**不帶路徑**的 `git commit`;要排除某個檔就用 `git update-index --cacheinfo <HEAD 的 blob>` 把它還原成 HEAD 版(2026-09-18) (以下逐字引用的歷史紀錄提到 provider view,**那是 generated delivery view,不是 canonical authority、不得當 SSOT**) |
|
|
290
|
+
|
|
291
|
+
### 寫了閘卻沒有任何執行面呼叫它
|
|
292
|
+
|
|
293
|
+
| **寫了閘卻沒有任何執行面呼叫它** | spec 寫「由 `xxx.mjs` 機械強制」、腳本存在、甚至有包裝測試 —— 但 CI / npm script / hook 都沒提到它,那條保護是假的而且**零訊號**。2026-09-18 機械盤點:`scripts/*.mjs` 有**大量**名字像閘/測試卻不可達(把 CI + package.json + hooks + skills + infra/test 全算進來之後)。**確切數字不在本檔硬寫** —— 它會隨棘輪往下走,SSOT 是 `scripts/gate-reachability-baseline.json`(2026-09-20 prune 抓到本檔與 ci.yml 都還停在舊值 94,而 baseline 已是 93;2026-09-21 再降到 4)。**2026-09-21 補的第三種形狀**:量具把「被字面提到」當成「會被跑」的替身 —— 動態探索出來的執行路徑(`run-gate-meta-tests.mjs` 掃 pair、`run-harnesses.mjs` 讀 harness inventory)沒有任何地方會寫出被跑者的檔名,於是 63 支每晚真的在跑的測試被誤判成孤兒。閘要模出真正的執行路徑,不是只 grep 字串。同族錨例:推播閘(hook 在、6 個情境測試全綠,但真實入口被 `requires: peer-cli` 擋掉,從沒跑過)、native/custom parity 閘(只掛非 required 的 nightly)。**判準**:寫完任一支閘立刻問「誰呼叫它」,grep 執行面拿不到答案就等於沒寫。棘輪閘 `scripts/gate-reachability-invariant.mjs`(CI required)鎖 baseline,新增孤兒立刻紅。**同族第二種形狀(2026-09-19)**:文件講的那道防線**檔案根本不在**——hook home 自己的 README 在 Stop 區列 5 支 hook,其中 3 支(`stop_harvest_corrections` / `stop_capture_metrics` / `stop_meta_self_audit`)早在 2026-05-13 就折進 `stop_passive_logging.sh`、檔名已不存在,而「折進去」這件事就寫在同一張表的**上一列**。閘 `scripts/hook-citation-liveness-invariant.mjs`(CI required):文件提到的 hook 名必須存在於 canonical hook 樹,否則該行前後 3 行要明講它是舊名/已折/未實作 |
|
|
294
|
+
### `tsc -b` **在本 repo 的 composite 設定下根本不檢查 DS 原始碼**
|
|
295
|
+
|
|
296
|
+
| `tsc -b` **在本 repo 的 composite 設定下根本不檢查 DS 原始碼** | 不只是「不 emit declaration」——2026-09-06 實證:`data-table.tsx` 少傳一個必填 prop(TS2741),`npx tsc -b --force` 回 **0**,`npm run build:lib` 才報錯。**「tsc -b 通過」不構成型別正確的證據**,任何 .tsx 改動的型別驗證一律以 `npm run build:lib` 為準 |
|
|
297
|
+
|
|
298
|
+
### hook 測試直跑留 fixture `.git/` 進 corpus
|
|
299
|
+
|
|
300
|
+
| hook 測試直跑留 fixture `.git/` 進 corpus | `git status` 不顯但 snapshot tree fingerprint 全算 → trio 漂移;測試必經 run-all.sh(自帶隔離),清 debris 用 `find -type f` 對照 `git ls-files`(2026-08-05) |
|
|
301
|
+
|
|
302
|
+
### SMIL `begin="indefinite"` 動畫掛上後沒人 `beginElement()` = 永不起跑(靜默)
|
|
303
|
+
|
|
304
|
+
| SMIL `begin="indefinite"` 動畫掛上後沒人 `beginElement()` = 永不起跑(靜默、base 值定格) | begin-once 守衛的觸發 key 必含每個會新掛動畫的狀態段(think→exit 同 key 漏掉 7 個 animate);驗證看 `getStartTime()` 是否丟例外,CI C6(2026-09-03) |
|
|
305
|
+
|
|
306
|
+
### `file://` 開 storybook = story 整個不渲染而且不報錯
|
|
307
|
+
|
|
308
|
+
| `file://` 開 storybook = story 整個不渲染而且不報錯 | CORS 擋掉模組載入,`#storybook-root` 子節點 0、畫面空白,探針卻拿得到 Storybook 自己的 UI(「Set string」按鈕)而誤以為有渲染。瀏覽器閘一律起本機靜態站。同場:CSSOM 對含 `var()` 的簡寫回**空字串**,用 `r.style.outline` 掃規則會全空,要用 `r.cssText`(2026-09-08)|
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
## M37 第十種形狀:發布鏈上「這個名字/這筆記錄存在」被當成「那件事真的發生了」(2026-09-21,一次跑裡六個位置)
|
|
312
|
+
|
|
313
|
+
同一天、同一次 `release:auto` 裡連續發作六次,而且**第 2 到第 6 個是在修第 1 個的那次跑裡才現形的**。
|
|
314
|
+
六個位置的共同形狀:拿一個**名字或一筆記錄**當成它所指的那件事。
|
|
315
|
+
|
|
316
|
+
| # | 要保證的性質 | 實際量到的值 | 何時分開 | 後果 |
|
|
317
|
+
|---|---|---|---|---|
|
|
318
|
+
| 1 | protected main 上這份內容已發布 | 這個版號字串有對應的 GitHub Release | 版號沒 bump | 五步全報 complete、exit 0,**一個位元都沒發出去** |
|
|
319
|
+
| 2 | 這個 head 通過 CI / 已在 main 上 | 這條分支有一個 PR / 那個 PR 是 MERGED | PR 合併後在同分支續推 commit | pr-checks + merge 讀**別份內容**的綠燈全綠 |
|
|
320
|
+
| 3 | tag 指的那個 commit 帶著這個版號 | tag 的名字 | bump 還沒併進 main | 在版號為 beta.140 的 commit 上建了 `v0.1.0-beta.141` |
|
|
321
|
+
| 4 | 守護 main 的 CI 通過了 | 那個 commit 上**所有** check-run | 發布流程自己失敗過一次 | main 看起來永久紅 → 自鎖;且指控「main CI 紅了」而 main CI 其實是 success |
|
|
322
|
+
| 5 | 這份發版授權被消耗掉了 | 帳本裡有一行 | 發布被中斷/失敗 | 一次失敗的嘗試燒掉授權,使用者被迫再說一次「發版」 |
|
|
323
|
+
| 6 | 要比的基準是線上目前那一份 | 最新的 tag | 建了 tag 但沒發成 | 拿從來沒出貨的東西當基準,印出與事實相反的「會不會改變畫面」 |
|
|
324
|
+
|
|
325
|
+
**不可逆的代價**:#3 建出來的 `v0.1.0-beta.141` tag 受 GitHub ruleset 保護
|
|
326
|
+
(`DELETE` 回 HTTP 422「Cannot delete this tag」),不能刪也**不該繞**(繞 ruleset 是 canonical 明文禁止),
|
|
327
|
+
於是它永久指向錯的 commit,該版號報廢,改發 beta.142。**沒有任何東西被發布出去**
|
|
328
|
+
(無 release、npm 無該版),但一個版號永久損失 —— 這就是把「名字」當「內容」的真實帳單。
|
|
329
|
+
|
|
330
|
+
**三個遞移教訓**:
|
|
331
|
+
1. **修一條同族缺陷時,同族全部要當場列出來逐一判定**(M10 sub-rule)。第 1 個修完就發布,
|
|
332
|
+
第 2–6 個當場現形 —— 它們本來就在同一條鏈上,只是沒去看。
|
|
333
|
+
2. **fixture 缺欄位 = 判定表測不到要測的事**。這輪四支測試失敗全是因為 fixture 沒給
|
|
334
|
+
`headSha` / `tagCommitSha` / `protectedMainSha` / `versionAtReleaseCommit` —— 缺欄位讓
|
|
335
|
+
fixture 同時也代表了假綠狀態,於是那一格永遠是綠的。
|
|
336
|
+
3. **「讀不到」的方向要看用途,不是統一選一個**。同一支 `classifyReleaseLookup` 三值判定,
|
|
337
|
+
在「這份授權消耗掉了嗎」要把 `null` 算成**已消耗**(不讓一份授權發兩次),
|
|
338
|
+
在「基準是哪一版」只有 `true` 能用(讀不到不得充當已發布)。同一個 unknown、兩個相反方向,
|
|
339
|
+
兩邊都得寫下來並各有對照組。
|
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.
|
|
7
|
+
"version": "0.1.0-beta.142",
|
|
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",
|
|
@@ -144,8 +144,8 @@ const InspectorView = () => {
|
|
|
144
144
|
<p className="font-mono text-fg-muted mb-2">WIDTH</p>
|
|
145
145
|
<ul className="flex flex-col gap-1">
|
|
146
146
|
<li><span className="font-mono">--agent-panel-width</span> · 400(預設)</li>
|
|
147
|
-
<li><span className="font-mono"
|
|
148
|
-
<li><span className="font-mono"
|
|
147
|
+
<li><span className="font-mono">PANEL_WIDTH_MIN</span> · 360(TS 常數,非 token)</li>
|
|
148
|
+
<li><span className="font-mono">PANEL_WIDTH_MAX</span> · 640(TS 常數,非 token)</li>
|
|
149
149
|
<li><span className="font-mono">視窗上限</span> · 50vw(與 max 取較小者)</li>
|
|
150
150
|
<li><span className="font-mono">鍵盤步進</span> · ←/→ 16 · Home 最窄 · End 最寬</li>
|
|
151
151
|
</ul>
|
|
@@ -72,7 +72,7 @@ SMIL keySplines 無法消費 CSS var,`agent-panel-logo.tsx` 內常數為 swell/s
|
|
|
72
72
|
- 開合=淡入+自右滑入 `--motion-duration-surface`(模態面板級);減動作停。
|
|
73
73
|
- **可調寬**(`resizable`,預設開):左緣 `<ResizeHandle direction="horizontal" position="start">`
|
|
74
74
|
(`patterns/resize-handle` 視覺 primitive:熱區 7、線 1、idle divider / hover border-hover / 拖曳中 primary,
|
|
75
|
-
與 DataTable 欄寬把手同一顆 primitive,2026-09-02 起兩邊同元件;resize-handle.spec.md Roadmap Phase 2);寬度夾在
|
|
75
|
+
與 DataTable 欄寬把手同一顆 primitive,2026-09-02 起兩邊同元件;resize-handle.spec.md Roadmap Phase 2);寬度夾在 360 ~ 640(`agent-panel.tsx` 的 `PANEL_WIDTH_MIN` / `PANEL_WIDTH_MAX`,單一住所;同 AppShell 的 `ASIDE_WIDTH_MIN` / `MAX` 前例,CSS 不另設 token —— 這兩個界限只有 JS 的 clamp 會用)
|
|
76
76
|
且 ≤ 視窗寬 50%(較小者勝);預設 `--agent-panel-width` 400。受控 `width` +`onWidthChange`(拖曳中每格都發,
|
|
77
77
|
受控端才有即時回饋)/ `onWidthCommit`(放開或鍵盤一步發一次,要落地儲存接這個);或
|
|
78
78
|
非受控 `defaultWidth`。**無雙擊重設**(2026-09-02 拍板);以 Sheet 承載時同樣可拖(不衝突)。
|
|
@@ -801,9 +801,9 @@ function TaskDialog({ task, portalContainer, persistentElements, onSave, onCance
|
|
|
801
801
|
</DialogHeader>
|
|
802
802
|
<DialogBody>
|
|
803
803
|
<div className="flex flex-col gap-[var(--layout-space-loose)]">
|
|
804
|
-
<Field required>
|
|
804
|
+
<Field required id="demo-task-title">
|
|
805
805
|
<FieldLabel>標題</FieldLabel>
|
|
806
|
-
<Input
|
|
806
|
+
<Input value={draft.title} onChange={(e) => set('title', e.target.value)} placeholder="例:修正登入逾時" />
|
|
807
807
|
</Field>
|
|
808
808
|
<Field>
|
|
809
809
|
<FieldLabel>指派人</FieldLabel>
|
|
@@ -1156,7 +1156,7 @@ function UrlRegistryScene() {
|
|
|
1156
1156
|
* DS 沒有也不該有;`persistentElements` 不讀 URL、不建立資格。
|
|
1157
1157
|
*/
|
|
1158
1158
|
export const UrlRegistryDemo: Story = {
|
|
1159
|
-
name: '示意(假資料)—
|
|
1159
|
+
name: '示意(假資料)— 網址註冊表:誰能與代理並存',
|
|
1160
1160
|
parameters: {
|
|
1161
1161
|
docs: {
|
|
1162
1162
|
description: {
|
|
@@ -568,7 +568,7 @@ export const OpenSnapshot = {
|
|
|
568
568
|
若焦點跑到 Dialog 外(例如背景那顆按鈕),就證實焦點鎖被廢掉了。
|
|
569
569
|
量測由 `scripts/dialog-focus-trap-poc.mjs` 執行。 */
|
|
570
570
|
export const FocusTrapWithConcurrentOverlay: Story = {
|
|
571
|
-
name: '焦點鎖 × 並存浮層
|
|
571
|
+
name: '焦點鎖 × 並存浮層',
|
|
572
572
|
tags: ['test-only'],
|
|
573
573
|
render: () => (
|
|
574
574
|
<div className="flex flex-col gap-3 p-6">
|
|
@@ -605,7 +605,7 @@ export const FocusTrapWithConcurrentOverlay: Story = {
|
|
|
605
605
|
/* 上面那個 POC 的**對照組**:同樣的浮層,但沒有 Dialog。
|
|
606
606
|
沒有這一組的話,「Tab 沒跑出 Dialog」也可能只是因為那顆鈕本來就走不到 —— 那樣就什麼都沒證明。 */
|
|
607
607
|
export const FocusTrapControlNoDialog: Story = {
|
|
608
|
-
name: '
|
|
608
|
+
name: '同一版面沒有對話框時的焦點順序',
|
|
609
609
|
tags: ['test-only'],
|
|
610
610
|
render: () => (
|
|
611
611
|
<div className="flex flex-col gap-3 p-6">
|
|
@@ -659,9 +659,9 @@ export const CoexistencePoc: Story = {
|
|
|
659
659
|
<DialogDescription>並存契約夾具</DialogDescription>
|
|
660
660
|
</DialogHeader>
|
|
661
661
|
<DialogBody>
|
|
662
|
-
<Field>
|
|
662
|
+
<Field id="coexist-inside-input">
|
|
663
663
|
<FieldLabel>留言</FieldLabel>
|
|
664
|
-
<Input
|
|
664
|
+
<Input value={inside} onChange={(e) => setInside(e.target.value)} />
|
|
665
665
|
</Field>
|
|
666
666
|
</DialogBody>
|
|
667
667
|
<DialogFooter>
|
|
@@ -672,9 +672,9 @@ export const CoexistencePoc: Story = {
|
|
|
672
672
|
)}
|
|
673
673
|
</div>
|
|
674
674
|
<aside ref={asideRef} id="coexist-aside" aria-label="評論" className="flex w-[300px] shrink-0 flex-col gap-2 border-l border-divider bg-surface p-4">
|
|
675
|
-
<Field>
|
|
675
|
+
<Field id="coexist-aside-input">
|
|
676
676
|
<FieldLabel>新增評論</FieldLabel>
|
|
677
|
-
<Input
|
|
677
|
+
<Input value={aside} onChange={(e) => setAside(e.target.value)} />
|
|
678
678
|
</Field>
|
|
679
679
|
<div><Button id="coexist-aside-btn" variant="primary" disabled={!aside.trim()} onClick={() => setAside('')}>送出</Button></div>
|
|
680
680
|
</aside>
|
|
@@ -458,9 +458,9 @@ export const CoexistenceContract: Story = {
|
|
|
458
458
|
<ul className="flex flex-col gap-1">
|
|
459
459
|
{comments.map((c, i) => <li key={i} className="text-body">{c}</li>)}
|
|
460
460
|
</ul>
|
|
461
|
-
<Field>
|
|
461
|
+
<Field id="fv-aside-input">
|
|
462
462
|
<FieldLabel>新增評論</FieldLabel>
|
|
463
|
-
<Input
|
|
463
|
+
<Input placeholder="可以打字" value={aside} onChange={(e) => setAside(e.target.value)} />
|
|
464
464
|
</Field>
|
|
465
465
|
<div>
|
|
466
466
|
<Button id="fv-aside-btn" variant="primary" disabled={!aside.trim()} onClick={() => { const t = aside.trim(); if (t) { setComments((c) => [...c, `你:${t}`]); setAside('') } }}>送出</Button>
|
|
@@ -144,7 +144,7 @@ export const Multi: Story = {
|
|
|
144
144
|
|
|
145
145
|
讀法:容器 720px 寬、六個人。hug 欄位應該長到放得下的長度,而不是縮成「1 顆 + +5」。 */
|
|
146
146
|
export const HugWidthMultiStack: Story = {
|
|
147
|
-
name: '
|
|
147
|
+
name: '寬度貼合內容 × 多人',
|
|
148
148
|
render: () => (
|
|
149
149
|
<div className="flex flex-col gap-4" style={{ width: 720 }}>
|
|
150
150
|
<p className="text-caption text-fg-muted">
|
|
@@ -96,6 +96,36 @@ Tailwind utility 透過 `@theme inline` 橋接 semantic token,元件寫 `bg-pr
|
|
|
96
96
|
文字色一律使用 neutral alpha token,疊加在任何背景都能維持對比。
|
|
97
97
|
弱化 icon hover 後變 `text-fg-secondary`。
|
|
98
98
|
|
|
99
|
+
### 已定案(禁再問):`--fg-muted` 在淺色主題未達 WCAG AA —— 維持現狀
|
|
100
|
+
|
|
101
|
+
| 主題 | 實際值 | 白/深底上的對比 | AA(4.5:1)|
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| 淺色 | `#8C8C8C`(黑 `--_na7: 45%` 疊白)| **3.36:1** | ✗ 未達 |
|
|
104
|
+
| 深色 | 白 `--_na7: 65%` 疊 `#0a0a0a` | 8.42:1 | ✓ |
|
|
105
|
+
|
|
106
|
+
**這不是疏漏,是明示的已知取捨。** 2026-09-21 全庫 axe 掃描 1040 個 story:critical = 0、
|
|
107
|
+
serious = 5051,其中 **color-contrast 佔 5046**,壓倒性來自這一個 token(placeholder、caption、
|
|
108
|
+
弱化說明文字)。當時量過的選項:`--_na7` 調到 55% → `#737373` = 4.74:1(剛過 AA)、
|
|
109
|
+
60% → `#666666` = 5.74:1。
|
|
110
|
+
|
|
111
|
+
> **外部對照的信心等級**:Atlassian `color.text.subtlest` = `#6B6E76`(算出來 5.10:1)
|
|
112
|
+
> 與「白底最淺可過 AA 的灰 `#767676` = 4.54:1」兩筆,**只有搜尋摘要來源,沒有一手官方檔案佐證**
|
|
113
|
+
> —— Atlassian / Polaris / Material 3 的 token 頁都是 JS 渲染,WebFetch 取不到內容(2026-09-21 實測)。
|
|
114
|
+
> 依 M26 標記為 **search-only confidence**;要當硬依據前須實地確認。
|
|
115
|
+
> 本表 `3.36 / 4.74 / 5.74 / 8.42` 四個數字則是用 WCAG 公式對**本 repo 實際 token 值**算出來的,可重算驗證。
|
|
116
|
+
|
|
117
|
+
**user 已經拍板兩次「維持現狀」,這是 standing decision,不得再主動提案或再問一次。**
|
|
118
|
+
user 2026-09-21 verbatim:「這個問題你很早之前就問過我了,而且上次我就說維持現狀。
|
|
119
|
+
這次也是,等我要改再等我主動」。**只有 user 主動提起才重啟這題**;
|
|
120
|
+
agent 不得因為 a11y 閘、稽核報告或新一輪掃描又把它列成待決事項。
|
|
121
|
+
|
|
122
|
+
代價已講明並被接受:螢幕閱讀器使用者不受影響,但**低視力使用者讀 placeholder / caption 會吃力**。
|
|
123
|
+
(第一次提案的理由與數據保留在上表,供 user 未來主動翻案時直接取用,不需重新研究。)
|
|
124
|
+
|
|
125
|
+
落地方式:a11y 閘(`scripts/audit-a11y.mjs`,夜間 `a11y-and-size.yml`)改為**只擋新增的回歸**,
|
|
126
|
+
這 5046 筆寫進 `infra/governance/baseline/a11y-baseline.json` 當已知帳。
|
|
127
|
+
要翻案只要把 `primitives.css` 的淺色 `--_na7` 從 45% 調到 55%,再跑一次 `--baseline-write`。
|
|
128
|
+
|
|
99
129
|
## Disabled 狀態
|
|
100
130
|
|
|
101
131
|
disabled 元件內的所有子元素必須呈現 disabled 狀態:
|
|
@@ -108,8 +108,10 @@
|
|
|
108
108
|
/* AgentPanel — 智慧代理右側面板寬(2026-09-02;agent-panel.spec.md token table SSOT)
|
|
109
109
|
稿「Task-Desktop」全六畫面 rightSider 實測 400;命名同 --sidebar-width L4 layout 前例 */
|
|
110
110
|
--agent-panel-width: 25rem; /* 400px 預設 */
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
/* 拖拉上下限(360 / 640)**不設 token**:值只有 JS 的 clamp 邏輯會用,CSS 這邊沒有任何消費者。
|
|
112
|
+
2026-09-21 orphan-token 稽核抓到 —— 原本宣告了 --agent-panel-width-min / -max 卻沒人讀,
|
|
113
|
+
而真正生效的是 agent-panel.tsx 的 PANEL_WIDTH_MIN / PANEL_WIDTH_MAX,同值兩個住所必然漂移(M17)。
|
|
114
|
+
單一住所對齊既有前例:AppShell 的 ASIDE_WIDTH_MIN / MAX 也只住在 app-shell.tsx。 */
|
|
113
115
|
|
|
114
116
|
/* Tree Indent — TreeView + DataTable nested rows(SSOT,2026-05-04)
|
|
115
117
|
公式:chevronSize + gap-2 = 16+8(sm/md)/ 20+8(lg)
|