6 Commits

Author SHA1 Message Date
uncle6me-web 2aa26a5bdd feat(cli): acr search 跨類統一搜尋 + acr parts 去 hardcode recipe(issue #13 根治)
leo 2026-06-29 重構根因:「telegram 與 google 不一致」其實是查錯表——舊世界要找一個能力
必須先決定它是 component/recipe/workflow 才查對清單,但查到答案前根本不知道屬哪類 →
「先知道分類」成了查詢前置,倒因為果(leo:連我都會查錯表)。這是搜尋設計缺陷,非粗心。

兩部分互補修法:
1. acr parts 清理(W3.1/W3.3):移除 5 個 hardcode 的 recipe(gmail_send/google_sheets_append/
   telegram_send/line_notify_send/notion)——它們是 recipe(動態,存 store)不是零件(component)。
   只留真 WASM 零件。檔頭寫死分流原則 + 移除處留註解防再 hardcode。footer 改指向動態清單 + acr search。
2. acr search <term> 新增(W3.2,正向修法):cli/src/commands/search.ts。fan-out 4 來源——
   component(靜態 BUILTIN_COMPONENTS) + recipe(GET /recipes) + auth-recipe(GET /auth-recipes)
   + workflow(GET /webhooks/named),依類別回 counts+命中。各來源獨立 try(離線降級指路、不連坐)。
   查的人不必先選表 → 結構性消除「查錯表」。

薄殼合規(rule 07 §2):search 只 fan-out 既有清單端點 + 過濾 + 分組 + 印出(介面層暴露/格式化),
不在介面層拼裝能力。component 靜態(PR-only)、recipe/auth/workflow 動態(store) 分流明確、不再 conflate。

實證(對 leo21c 跑,read-only GET):
- acr search telegram → component:0 recipe:1 auth-recipe:0 workflow:1(一眼看出 telegram 是 recipe)
- acr parts → 只剩 http_request 等真零件,5 個 recipe 已不在
- tsc 全綠、build 通過

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:08:11 +08:00
uncle6me-web 764f657201 merge: issue #13 步驟2 清 parts.ts 降級零件殘留
telegram/gmail/sheets/line 改走 recipe canonical_id、移除已刪 ai_transform。
TS tsc 過、gmail 讀取留 TODO 不硬塞。自動派工第 2 件共用框架 PR。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:06:54 +08:00
uncle6me-web 225aa9f9e7 清 parts.ts 降級零件殘留:telegram/gmail/sheets/line 改走 recipe、移除已刪的 ai_transform(issue #13 步驟2)
telegram/gmail/google_sheets/line_notify 已於 2026-05-29 Phase 2 降級為 recipe,
但 acr parts 仍列舊 component: 形態誤導 CC。參照 notion 樣板改成「走 recipe」描述 +
canonical_id 對齊 api-recipe-seeds.ts(telegram_send/gmail_send/google_sheets_append/line_notify_send)。
ai_transform_compile/run 已刪除(mindset §2),整段移除。
canonical_id 均已核實存在於 cypher-executor/src/lib/api-recipe-seeds.ts。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:05:39 +08:00
uncle6me-web caa8e103ff merge: issue #13 步驟1 telegram 示例改 recipe(gmail 留 TODO)
PR review 抓出 gmail_send 假綠已擋下,只併對的 telegram 三處。
自動派工首次共用框架 PR 閉環:subagent 診斷→改→獨立 review→拆併。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:47:01 +08:00
uncle6me-web a234201235 修正示例 yaml:telegram 改用 recipe canonical_id;gmail 讀取留 TODO(issue #13 步驟1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:45:50 +08:00
uncle6me-web f21906ca6a chore(wiki): 本 session capture(薄殼防複發/歷史債卡 + mistakes #19-21 + status)
兩次 /wiki-capture 累積的知識落盤:
- cards/decisions/ 新卡:薄殼防複發-能力對照表加smoke、薄殼規則晚於實作-MCP漂移是歷史債
  (+ 00-INDEX 編入,決策桶現 15 張)
- mistakes #19 死端點假綠(grep route/smoke 驗端點存在)
- mistakes #20 gitignored 檔無 git 史(時間靠檔內註記)
- mistakes #21 wrangler.toml services=[...] inline 在 [vars] 後被吸成 vars.services(issue #12)
- decisions-summary:薄殼防複發機制、workflow description 由操盤 CC 據實生成
- status:本 session #8/#11/#12 進度 + merge 結果

純記憶/文檔,無 code。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 01:43:26 +08:00
12 changed files with 346 additions and 102 deletions
+26 -98
View File
@@ -1,7 +1,17 @@
/** /**
* acr parts — 列出所有可用零件(內建清單,不依賴 registry.arcrun.dev * acr parts — 列出所有可用零件(component)」(內建清單,不依賴 registry.arcrun.dev
* acr parts scaffold <component> — 輸出 config 範本(可直接貼入 workflow.yaml * acr parts scaffold <component> — 輸出 config 範本(可直接貼入 workflow.yaml
* acr parts publish <component> — 提交零件至公眾 registryPhase 5,封測後) * acr parts publish <component> — 提交零件至公眾 registryPhase 5,封測後)
*
* ⚠️ 分類原則(2026-06-29issue #13 / component-gatekeeping W3):
* - **零件(component= 靜態清單**WASM,只能走 GitHub PR + 人 merge 新增(mindset §4 人類閘門),
* 固定慢增 → 用 BUILTIN_COMPONENTS hardcode 反映真實,正確。
* - **recipe / auth-recipe / workflow = 動態,存在 store**:任何人 `acr recipe push` 即新增 →
* **絕不可 hardcode 在這裡**(會「submitted = invisible」+ 誤導查錯表)。它們各有動態清單:
* recipe → `acr recipe list`GET /recipes)|auth-recipe → `acr auth-recipe list`GET /auth-recipes)|workflow → `acr list`GET /webhooks/named
* - **跨類找東西用 `acr search <term>`**fan-out 上述 4 個來源,不必先知道它是哪一類)。
* 歷史教訓:本檔曾把 gmail_send/telegram_send/notion 等 5 個 recipe hardcode 進零件清單 →
* 誤導「telegram 是零件 / 走不同機制」。已移除(W3.1)。
*/ */
import { readFileSync, existsSync } from 'node:fs'; import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
@@ -25,7 +35,7 @@ interface ComponentDef {
credentials_required?: CredentialRequirement[]; credentials_required?: CredentialRequirement[];
} }
const BUILTIN_COMPONENTS: ComponentDef[] = [ export const BUILTIN_COMPONENTS: ComponentDef[] = [
// ── 控制類(Logic) ──────────────────────────────────────────────────────── // ── 控制類(Logic) ────────────────────────────────────────────────────────
{ {
canonical_id: 'if_control', canonical_id: 'if_control',
@@ -174,27 +184,9 @@ const BUILTIN_COMPONENTS: ComponentDef[] = [
type: string type: string
format: email`, format: email`,
}, },
// ── AI 類 ────────────────────────────────────────────────────────────────── // ── AI 類:已移除 ──────────────────────────────────────────────────────────
{ // ai_transform_compile / ai_transform_run 於 2026-05-29 刪除(mindset §2arcrun 是
canonical_id: 'ai_transform_compile', // AI 呼叫的工具,不是工具回頭呼叫 LLM)。需要 AI 判斷/轉換由操盤的 CC 自己做。
display_name: 'AI Transform Compile',
category: 'ai',
description: '將自然語言規則編譯成可執行轉換程式',
config_example:
` compile_node:
component: ai_transform_compile
rule: "把 name 轉成大寫,並在前面加上 Hello "`,
},
{
canonical_id: 'ai_transform_run',
display_name: 'AI Transform Run',
category: 'ai',
description: '執行 ai_transform_compile 產生的轉換程式',
config_example:
` run_node:
component: ai_transform_run
program: "{{compiled_program}}"`,
},
// ── API 整合類(Recipe 型,不需 deploy Worker) ──────────────────────────── // ── API 整合類(Recipe 型,不需 deploy Worker) ────────────────────────────
{ {
canonical_id: 'http_request', canonical_id: 'http_request',
@@ -211,78 +203,9 @@ const BUILTIN_COMPONENTS: ComponentDef[] = [
body: body:
key: "{{value}}"`, key: "{{value}}"`,
}, },
{ // ⚠️ 此處曾 hardcode gmail_send / google_sheets_append / telegram_send / line_notify_send / notion
canonical_id: 'gmail', // 這 5 個是 **recipe(動態,存 store)不是零件(component**,已於 W3.1 移除(issue #13 根治)。
display_name: 'Gmail', // 要找它們:`acr search <term>`(跨類)或 `acr recipe list` / `acr auth-recipe list`(動態清單)。
category: 'api',
description: '寄送 Gmail(需要 gmail_token credential',
config_example:
` mail_node:
component: gmail
to: "recipient@example.com"
subject: "來自 arcrun 的通知"
body: "{{message}}"`,
credentials_required: [
{ key: 'gmail_token', type: 'OAuth2 access token', inject_as: 'access_token' },
],
},
{
canonical_id: 'google_sheets',
display_name: 'Google Sheets',
category: 'api',
description: 'Google Sheets 讀寫(需要 google_oauth credential',
config_example:
` sheet_node:
component: google_sheets
spreadsheet_id: "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
range: "Sheet1!A:B"
operation: append
values:
- ["{{name}}", "{{email}}"]`,
credentials_required: [
{ key: 'google_oauth', type: 'OAuth2 access token', inject_as: 'access_token' },
],
},
{
canonical_id: 'telegram',
display_name: 'Telegram',
category: 'api',
description: '發送 Telegram 訊息(需要 telegram_bot_token credential',
config_example:
` tg_node:
component: telegram
chat_id: "123456789"
text: "{{message}}"`,
credentials_required: [
{ key: 'telegram_bot_token', type: 'Bot token', inject_as: 'bot_token' },
],
},
{
canonical_id: 'line_notify',
display_name: 'LINE Notify',
category: 'api',
description: '發送 LINE Notify 通知(需要 line_token credential',
config_example:
` line_node:
component: line_notify
message: "{{notification}}"`,
credentials_required: [
{ key: 'line_token', type: 'LINE Notify token', inject_as: 'token' },
],
},
{
canonical_id: 'notion',
display_name: 'Notion',
category: 'api',
description: '透過 recipe 操作 Notion API(需先 acr recipe push',
config_example:
` # 先上傳 recipeacr recipe push notion.yaml
notion_node:
component: rec_xxxxxxxx # acr recipe push 後得到的 hash
database_id: "{{db_id}}"
properties:
Name: "{{title}}"`,
},
]; ];
// ── 指令實作 ────────────────────────────────────────────────────────────────── // ── 指令實作 ──────────────────────────────────────────────────────────────────
@@ -301,7 +224,7 @@ export async function cmdParts(): Promise<void> {
grouped[comp.category].push(comp); grouped[comp.category].push(comp);
} }
console.log(chalk.bold(`\n arcrun 零件庫(${BUILTIN_COMPONENTS.length} 個內建零件)\n`)); console.log(chalk.bold(`\n arcrun 零件庫(${BUILTIN_COMPONENTS.length} 個內建零件 / component,靜態 PR-only\n`));
for (const cat of ['logic', 'data', 'ai', 'api']) { for (const cat of ['logic', 'data', 'ai', 'api']) {
const comps = grouped[cat]; const comps = grouped[cat];
@@ -318,8 +241,13 @@ export async function cmdParts(): Promise<void> {
} }
console.log(chalk.gray(' 使用 acr parts scaffold <component> 取得 config 範本')); console.log(chalk.gray(' 使用 acr parts scaffold <component> 取得 config 範本'));
console.log(chalk.gray(' 第三方服務整合(Notion/Slack/GitHub 等):acr auth-recipe list')); console.log('');
console.log(chalk.gray(' API 整合類若需打自訂服務,請用 acr recipe push 建立 recipe\n')); console.log(chalk.bold(' 零件之外(動態,存在 store,不在上面這份靜態清單):'));
console.log(chalk.gray(' • API recipe(打外部服務) acr recipe list'));
console.log(chalk.gray(' • 第三方服務認證(auth-recipe acr auth-recipe list'));
console.log(chalk.gray(' • 已部署的 workflow acr list'));
console.log(chalk.cyan(' • 不確定某能力是哪一類? acr search <關鍵字> ← 跨類一次搜,免先選表'));
console.log('');
} }
export async function cmdPartsScaffold(componentId: string): Promise<void> { export async function cmdPartsScaffold(componentId: string): Promise<void> {
+170
View File
@@ -0,0 +1,170 @@
/**
* acr search <term> — 跨類統一搜尋(component / recipe / auth-recipe / workflow
*
* 為什麼存在(issue #13leo 2026-06-29):
* 舊世界要找一個能力(如 "telegram")必須**先決定**「它是零件?recipe?還是 workflow?」才能查對的清單。
* 但查的人(人或 AI)在查到答案前**根本不知道**它屬哪一類 →「先知道分類」成了查詢的前置條件,倒因為果。
* leo:「連我都會查錯表。」所以這不是粗心,是搜尋設計缺陷。
* 本指令一次掃全部類別、依類別分組回 counts + 命中 → 查的人不必先選表,看一眼就知道 telegram 是 recipe。
*
* 薄殼原則(rule 07 §2):本指令只做 fan-out 呼叫既有清單來源 + 過濾 + 分組 + 印出(介面層的暴露/格式化)。
* - component:靜態(BUILTIN_COMPONENTSPR-only,複用 parts.ts 同一份,不另存)。
* - recipe / auth-recipe / workflow**動態**,從 store 端點即時抓(GET /recipes、/auth-recipes、/webhooks/named)。
* 不在介面層拼裝任何能力;過濾是對 API 回傳值做的純客戶端格式化(§2.3 允許)。
*/
import chalk from 'chalk';
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
import { BUILTIN_COMPONENTS } from './parts.js';
interface Hit {
id: string; // canonical_id / service / workflow name
label: string; // display_name / 描述用名稱
detail?: string; // description(截斷)
}
interface CategoryResult {
key: 'component' | 'recipe' | 'auth-recipe' | 'workflow';
title: string;
dynamic: boolean; // 是否來自 store(動態)
listHint: string; // 列全部的指令
hits: Hit[];
error?: string; // 該來源抓取失敗(離線/服務不可用)→ 降級顯示,不中斷其他類
}
/** term 命中:id / label / detail 任一含 term(不分大小寫) */
function matches(term: string, ...fields: Array<string | undefined>): boolean {
const t = term.toLowerCase();
return fields.some((f) => (f ?? '').toLowerCase().includes(t));
}
function trim(s: string | undefined, n = 70): string | undefined {
if (!s) return undefined;
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}
async function fetchJson(url: string, headers: Record<string, string>): Promise<unknown> {
const res = await fetch(url, { method: 'GET', headers });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function cmdSearch(term: string): Promise<void> {
const q = (term ?? '').trim();
if (!q) {
console.error(chalk.red(' 請提供搜尋關鍵字:acr search <term>'));
process.exit(1);
}
const config = loadConfig();
const baseUrl = getCypherExecutorUrl(config);
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (config.api_key) headers['X-Arcrun-API-Key'] = config.api_key;
// ── component(靜態,本地,永不失敗)──
const componentRes: CategoryResult = {
key: 'component',
title: '零件 component(靜態 · PR-only · WASM',
dynamic: false,
listHint: 'acr parts',
hits: BUILTIN_COMPONENTS
.filter((c) => matches(q, c.canonical_id, c.display_name, c.description))
.map((c) => ({ id: c.canonical_id, label: c.display_name, detail: trim(c.description) })),
};
// ── recipe / auth-recipe / workflow(動態,store;各自獨立 try,互不連坐)──
const dynamicSpecs: Array<{
key: CategoryResult['key'];
title: string;
listHint: string;
url: string;
extract: (data: unknown) => Hit[];
}> = [
{
key: 'recipe',
title: 'API recipe(動態 · store · 打外部服務)',
listHint: 'acr recipe list',
url: `${baseUrl}/recipes`,
extract: (data) => {
const arr = (data as { recipes?: Array<{ canonical_id: string; display_name?: string; description?: string }> }).recipes ?? [];
return arr
.filter((r) => matches(q, r.canonical_id, r.display_name, r.description))
.map((r) => ({ id: r.canonical_id, label: r.display_name ?? r.canonical_id, detail: trim(r.description) }));
},
},
{
key: 'auth-recipe',
title: '第三方服務認證 auth-recipe(動態 · store',
listHint: 'acr auth-recipe list',
url: `${baseUrl}/auth-recipes`,
extract: (data) => {
const arr = (data as { recipes?: Array<{ service: string; display_name?: string; description?: string }> }).recipes ?? [];
return arr
.filter((r) => matches(q, r.service, r.display_name, r.description))
.map((r) => ({ id: r.service, label: r.display_name ?? r.service, detail: trim(r.description) }));
},
},
{
key: 'workflow',
title: '已部署 workflow(動態 · store',
listHint: 'acr list',
url: `${baseUrl}/webhooks/named`,
extract: (data) => {
const arr = (data as { workflows?: Array<{ name: string; description?: string }> }).workflows ?? [];
return arr
.filter((w) => matches(q, w.name, w.description))
.map((w) => ({ id: w.name, label: w.name, detail: trim(w.description) }));
},
},
];
const dynamicResults = await Promise.all(
dynamicSpecs.map(async (spec): Promise<CategoryResult> => {
try {
const data = await fetchJson(spec.url, headers);
return { key: spec.key, title: spec.title, dynamic: true, listHint: spec.listHint, hits: spec.extract(data) };
} catch (e) {
return { key: spec.key, title: spec.title, dynamic: true, listHint: spec.listHint, hits: [], error: e instanceof Error ? e.message : String(e) };
}
}),
);
const all: CategoryResult[] = [componentRes, ...dynamicResults];
// ── 印出:先一行 counts 摘要(看一眼就知道屬哪類),再各類明細 ──
console.log(chalk.bold(`\n 搜尋「${q}\n`));
const countLine = all
.map((r) => {
const n = r.error ? '?' : String(r.hits.length);
const c = r.error ? chalk.yellow(`${r.key}:${n}`) : (r.hits.length > 0 ? chalk.green(`${r.key}:${n}`) : chalk.gray(`${r.key}:0`));
return c;
})
.join(' ');
console.log(' ' + countLine + '\n');
let anyHit = false;
for (const r of all) {
if (r.error) {
console.log(chalk.yellow(` ${r.title} — 無法讀取(${r.error});離線時改用 ${chalk.cyan(r.listHint)}`));
console.log('');
continue;
}
if (r.hits.length === 0) continue;
anyHit = true;
console.log(chalk.bold.underline(` ${r.title}`));
for (const h of r.hits) {
const label = h.label && h.label !== h.id ? ` ${h.label}` : '';
console.log(` ${chalk.cyan(h.id.padEnd(24))}${label}`);
if (h.detail) console.log(chalk.gray(` ${h.detail}`));
}
console.log('');
}
if (!anyHit && !all.some((r) => r.error)) {
console.log(chalk.yellow(` 四類都沒有命中「${q}」。`));
console.log(chalk.gray(' • 想新增打外部服務的能力 → acr recipe push(建 recipe'));
console.log(chalk.gray(' • 服務認證設定 → acr auth-recipe list 找對應服務'));
console.log(chalk.gray(' • 零件(WASM)只能走 GitHub PR 新增(稀有)'));
console.log('');
}
}
+7
View File
@@ -20,6 +20,7 @@ import { cmdValidate } from './commands/validate.js';
import { cmdParts, cmdPartsScaffold, cmdPartsPublish } from './commands/parts.js'; import { cmdParts, cmdPartsScaffold, cmdPartsPublish } from './commands/parts.js';
import { cmdRecipePush, cmdRecipeList, cmdRecipeDelete, cmdRecipeSearch, cmdRecipePull, cmdRecipeSubmitP } from './commands/recipe.js'; import { cmdRecipePush, cmdRecipeList, cmdRecipeDelete, cmdRecipeSearch, cmdRecipePull, cmdRecipeSubmitP } from './commands/recipe.js';
import { cmdList } from './commands/list.js'; import { cmdList } from './commands/list.js';
import { cmdSearch } from './commands/search.js';
import { cmdLogs } from './commands/logs.js'; import { cmdLogs } from './commands/logs.js';
import { cmdUpdate } from './commands/update.js'; import { cmdUpdate } from './commands/update.js';
import { cmdInstallHarness } from './commands/install-harness.js'; import { cmdInstallHarness } from './commands/install-harness.js';
@@ -205,6 +206,12 @@ program
.description('列出 CF KV 中所有已部署的 workflow') .description('列出 CF KV 中所有已部署的 workflow')
.action(() => cmdList()); .action(() => cmdList());
// acr search <term> — 跨類統一搜尋(component / recipe / auth-recipe / workflow),免先選表
program
.command('search <term>')
.description('跨類搜尋能力(零件/recipe/auth-recipe/workflow),一次掃全部、依類別回 counts+命中')
.action((term: string) => cmdSearch(term));
// acr logs <workflow_name> // acr logs <workflow_name>
program program
.command('logs <workflow>') .command('logs <workflow>')
+1 -1
View File
@@ -54,7 +54,7 @@ config:
格式:markdown bullets,每條 < 30 字,標明來源。 格式:markdown bullets,每條 < 30 字,標明來源。
push_digest: push_digest:
component: telegram component: telegram_send
chat_id: "{{secret.LEO_TELEGRAM_CHAT_ID}}" chat_id: "{{secret.LEO_TELEGRAM_CHAT_ID}}"
text: | text: |
☀️ 早安 {{_today}} ☀️ 早安 {{_today}}
@@ -12,6 +12,7 @@ config:
cron_expr: "0 8 * * *" # 每天 08:00 UTC(依需求調時區) cron_expr: "0 8 * * *" # 每天 08:00 UTC(依需求調時區)
fetch_unread: fetch_unread:
# TODO(#13): gmail 讀取尚無對應 recipe,待 seed 補 gmail_list 後再映射;gmail_send 是送信端點,不可用於 action:list
component: gmail component: gmail
action: "list" action: "list"
query: "is:unread newer_than:1d" query: "is:unread newer_than:1d"
@@ -35,7 +36,7 @@ config:
{{fetch_unread.messages}} {{fetch_unread.messages}}
push_to_telegram: push_to_telegram:
component: telegram component: telegram_send
chat_id: "{{secret.LEO_TELEGRAM_CHAT_ID}}" chat_id: "{{secret.LEO_TELEGRAM_CHAT_ID}}"
text: | text: |
📬 今日 email 摘要 📬 今日 email 摘要
+1 -1
View File
@@ -35,7 +35,7 @@ config:
# 純記錄成功,下游若需要可繼續鏈 # 純記錄成功,下游若需要可繼續鏈
final_fail_notify: final_fail_notify:
component: telegram component: telegram_send
chat_id: "{{secret.LEO_TELEGRAM_CHAT_ID}}" chat_id: "{{secret.LEO_TELEGRAM_CHAT_ID}}"
text: | text: |
⚠️ workflow {{input.workflow_name}} 兩次重試都失敗 ⚠️ workflow {{input.workflow_name}} 兩次重試都失敗
@@ -10,6 +10,7 @@
- [[自力救濟階梯-缺能力怎麼補]] — 自家API缺→補API/第三方→workflow補丁/純計算→code-node - [[自力救濟階梯-缺能力怎麼補]] — 自家API缺→補API/第三方→workflow補丁/純計算→code-node
- [[薄殼原則-能力長在API]] — CLI/MCP/lib 只暴露,齊的單位是「能力」不是「端點」 - [[薄殼原則-能力長在API]] — CLI/MCP/lib 只暴露,齊的單位是「能力」不是「端點」
- [[薄殼規則晚於實作-MCP漂移是歷史債]] — 為何 MCP/CLI 不一致:紀律 2026-06-07 才補、補前漂移 - [[薄殼規則晚於實作-MCP漂移是歷史債]] — 為何 MCP/CLI 不一致:紀律 2026-06-07 才補、補前漂移
- [[薄殼防複發-能力對照表加smoke]] — 防死端點假綠:對照清單 + 本機 smoke(非 CI),自驗能攔
## 串接 / 部署 ## 串接 / 部署
@@ -34,4 +34,5 @@ MCP 和 CLI 不一致的根因不是「MCP 更早開發所以舊」,而是**
- 薄殼原則 >> 修正 >> MCP 漂移 - 薄殼原則 >> 修正 >> MCP 漂移
### 卡片關係 ### 卡片關係
- [[薄殼規則晚於實作-MCP漂移是歷史債]] >> 補充歷史成因於 >> [[薄殼原則-能力長在API]] - [[薄殼規則晚於實作-MCP漂移是歷史債]] >> 補充歷史成因於 >> [[薄殼原則-能力長在API]]
- [[薄殼規則晚於實作-MCP漂移是歷史債]] >> 被治理於 >> [[薄殼防複發-能力對照表加smoke]]
- (相關 memory`mcp-self-hosted-bug-fixed``thin-shell-capability-in-api` — MCP 後端接線與薄殼鐵律) - (相關 memory`mcp-self-hosted-bug-fixed``thin-shell-capability-in-api` — MCP 後端接線與薄殼鐵律)
@@ -0,0 +1,37 @@
---
tags: [薄殼, 平台原則, 架構決策, 機制說明]
gloss: 防 CLI/MCP 薄殼漂移(死端點假綠)的雙層機制——靜態能力對照清單 + 本機 smoke test 對真端點斷言非 404。
---
# 薄殼防複發 — 能力對照表 + 本機 smoke
← [[decisions/00-INDEX]]
**來源**`system-dev/docs/3-specs/thin-shell-alignment/design.md §5`issue #11)、`docs/4-guides/cli-mcp-capability-matrix.md``scripts/thin-shell-smoke.sh`
**最後更新**2026-06-27
## 摘要
治「薄殼打了不存在的 server 端點」(死端點假綠)用雙層:靜態能力對照清單(review 防線)+ 本機 smoke test(對真端點斷言非 404,死端點當場現形)。本機手動跑,非 CI。
## 重點
- **根因是假綠**:MCP deploy 在宣稱「一致性落地」的同一 commit 裡打了不存在的 `/workflows/deploy`(必 404),從未端到端跑過。光靠「宣稱對齊」會再犯。
- **層 1 能力對照清單**`docs/4-guides/cli-mcp-capability-matrix.md`):表「能力 × CLI 端點 × MCP 端點 × route 存在? × 同源?」,新薄殼能力必填一行,填前 grep 確認 route 存在。
- **層 2 本機 smoke**`scripts/thin-shell-smoke.sh`):對每能力打真端點斷言非 404。**本機手動跑非 CI/cron/輪詢**(守 flag 紅線,對齊「執行鏈路不依賴 CI」鐵律)。
- **機制自驗**:注入故意死端點 → smoke 當場攔下、exit 1(已驗證能攔)。
- **smoke 區分「code 有 route」vs「prod 真部署」**:首跑就揭 #8 search/backfill 在 prod 仍 404code 已 commit 但未部署)——這正是它要揭的假綠。
- **不用 CI 高頻打真端點**(違 flag 紅線);smoke 是「宣布完成前手動跑一次」的閘。
## 實體
- **能力對照清單**cli-mcp-capability-matrix)— 靜態表列每能力的 CLI/MCP 端點與 route 存在性,review 防線。
- **本機 smoke test**thin-shell-smoke.sh)— 對真端點打、斷言非 404 的腳本,死端點現形,本機手動跑。
- **死端點假綠**dead endpoint)— 薄殼打了 server 不存在的 route(404),卻被宣稱成完成。
- **機制自驗**(注入死端點測試)— 故意加不存在端點驗 smoke 能攔,證明機制有效。
## 關聯
### 內文知識關係
- 死端點假綠 >> 被攔於 >> 本機 smoke test
- 能力對照清單 >> 互補於 >> 本機 smoke test
- 機制自驗 >> 證明 >> 本機 smoke test
- 本機 smoke test >> 區分 >> 死端點假綠
### 卡片關係
- [[薄殼防複發-能力對照表加smoke]] >> 治理 >> [[薄殼規則晚於實作-MCP漂移是歷史債]]
- [[薄殼防複發-能力對照表加smoke]] >> 落實 >> [[薄殼原則-能力長在API]]
+35
View File
@@ -327,6 +327,41 @@ code-node 規則已定、零件實作屬 wishlist C1 另案。**詳見**DECIS
--- ---
## 薄殼防複發機制=對照清單 + 本機 smoke2026-06-27issue #11
**Q:CLI/MCP 薄殼漂移(死端點假綠)怎麼防複發?**
**決策**:雙層,非 CI。
- **層 1 能力對照清單** `docs/4-guides/cli-mcp-capability-matrix.md`:一張表「能力 × CLI 端點 × MCP 端點 × route 存在? × 同源?」,新增薄殼能力**必填一行**,PR review 對照。
- **層 2 本機 smoke test** `scripts/thin-shell-smoke.sh`:對每能力打真端點、**斷言非 404**(死端點當場現形)。**本機手動跑非 CI/cron/輪詢**(守 flag 紅線,對齊「執行鏈路不依賴 CI」)。
- **機制自驗**:注入故意死端點 → smoke 當場攔(已驗證能攔,exit 1)。
**為什麼**:根因是**假綠**——宣稱「一致性落地」但端點不存在,光靠宣稱會再犯。smoke 區分「code 有 route」vs「prod 真部署」(首跑就揭 #8 search/backfill 在 prod 仍 404=未部署)。
**避坑**:不用 CI 高頻打真端點(違 flag 紅線);smoke 是「宣布完成前手動跑一次」的閘。
**詳見**thin-shell-alignment SDD §5、[[薄殼規則晚於實作-MCP漂移是歷史債]] 卡、mistakes #19
---
## workflow description 由操盤 CC 據實生成、用戶可改(2026-06-27issue #8
**Q:強制 workflow 填 description,但 low-code 用戶不知道要填,怎麼辦?**
**決策**:強制非空落 API(不變);**空時由操盤的 CC 據實寫一句「能做什麼」,用戶可改**——非逼用戶手填、非介面層機械塞佔位。
**調和關鍵(分清兩種「生成」)**
-**操盤 CC 據實生成**CC 剛建這 workflow、最懂它做什麼 → 寫真描述(leo 例「呼叫可 Upsert Google Sheets」)。**真描述非假裝**,不違 mindset §7。
-**介面層機械塞佔位**(從 name 複製、`workflow_xxx`)=假描述,仍禁。
**為什麼**low-code 用戶不知道要填 description(逼填違北極星「不增負擔」);但自動機械填=假裝有(違誠實)。北極星「執行力外包給 AI」=AI 據實幫他寫好、他可改。
**避坑**description 是一句「能做什麼」供語意搜尋,**非寫文章**。
**詳見**workflow-discovery SDD §3.2。
---
## 快速決策樹 ## 快速決策樹
``` ```
+61
View File
@@ -387,6 +387,64 @@ listEntries 加 `source` filter 即可,表結構一個欄位都不動。
--- ---
## 19. 薄殼打的端點 server 不一定存在——死端點假綠(2026-06-27issue #8/#11
**錯誤模式**:以為「tsc 綠 + 介面有這個工具」=能力通了。實際 MCP `u6u_deploy_workflow`
`/workflows/deploy`、CLI `run.ts``/webhooks/<name>`——**兩個端點 cypher 根本沒有,必 404**
且當初 commit message 還宣稱「部署一致性落地」。**從未端到端跑過**。
**症狀**:宣稱對齊/完成,但薄殼打的 route 在 server 不存在;tsc 綠掩蓋了端點不存在。
**正確做法**
1. 宣稱對齊/完成前,**grep server route 清單確認端點存在**
`grep -rE "Router\.(post|get)\('/xxx'" cypher-executor/src/routes/`)。
2. 或跑 `scripts/thin-shell-smoke.sh`(對真端點打、斷言非 404,死端點當場現形)。
3. **tsc 綠 ≠ 端點存在 ≠ 端到端通**。三者分開驗(mindset §7 完成=客觀證據)。
**通用教訓**:薄殼漂移(CLI/MCP 各長各的)的根因常是「規則晚於實作 + 假綠」。
防複發見 [[薄殼防複發-能力對照表加smoke]] 卡 + `docs/4-guides/cli-mcp-capability-matrix.md`。日期:2026-06-27。
---
## 20. gitignored 檔查不到 git 史——別用 git log 推斷時間/狀態(2026-06-27
**錯誤模式**:想用 `git log` 查 rule 07 / SDD / wiki 的建立時間 → 全空白,卡住。
**症狀**`git log -- .claude/rules/07-thin-shell.md` 等回空,誤以為檔案有問題或 follow 寫錯。
**正確做法**:先 `git check-ignore <path>` 確認。`.claude/``docs/`、部分 `system-dev/docs/`
**全 gitignored** → git 無這些檔的歷史。查它們的時序只能靠**檔內日期註記**,且要誠實標
「估計非 git 鐵證」。**注**`system-dev/wiki/` **不是** gitignored(可 commit),只有 `docs/` + `.claude/hooks/` 是。
**通用教訓**:查歷史前先確認檔在不在 git 追蹤範圍;gitignored 的東西「精確時間」不可考,別假裝有鐵證。日期:2026-06-27。
---
## 21. wrangler.toml 的 `services = [...]` inline 放在 `[vars]` 後 → binding 消失(2026-06-27issue #12
**錯誤模式**:以為「toml 裡有 `services = [{ binding="CYPHER_EXECUTOR", ... }]`」= binding 就會生效。
實際 self-hosted MCP 報 `CYPHER_EXECUTOR service binding not configured`binding 像憑空消失。
**根因(TOML 語法坑)**`services = [...]`**inline array 形式**,且位置在 `[vars]` table **之後**
TOML 規則:`[vars]` header 後的所有 key(含 `services`)都歸 `vars` table → `services` 被解析成
**`vars.services`(一個普通 env var 陣列)而非頂層 service bindings** → wrangler 看不到任何 binding。
self-hosted 部署 `injectMultiTenant``[vars]` 注入 `MULTI_TENANT` 後此問題暴露(官方不注入故沒早爆,
且 whoami 不需 binding)。
**正確做法**service binding 一律用 **`[[services]]` array-of-tables**(每個 binding 獨立頂層 table
不受 `[vars]` 影響),**不要用 inline `services = [...]`**——尤其檔案有 `[vars]` 時。對齊官方
`cypher-executor/wrangler.toml` 慣例(它用 `[[services]]` 故不中招)。
**驗法(決定性)**`wrangler deploy --dry-run --outdir /tmp/x` 列 bindings
- 正確 → `env.CYPHER_EXECUTOR (arcrun-cypher-executor) Worker`
- 中招 → `env.services ([{"binding":...}]) Environment Variable`binding 變成一個叫 services 的 env var)。
**通用教訓**TOML 頂層 key 必須在任何 `[table]` header 之前;要在 table 後宣告的集合用
array-of-tables`[[x]]`)不用 inline array。改 toml 後用 `wrangler --dry-run` 驗 binding 真的在
(別只看「檔裡有寫」)。連帶:這跟 mistakes #17「注入順序」、#11「死端點假綠」同類——**配置寫了 ≠ 生效**,要驗。日期:2026-06-27。
---
## 快速檢查清單(做新功能前) ## 快速檢查清單(做新功能前)
- [ ] 這是工作流還是零件?問「有必要嗎?」 - [ ] 這是工作流還是零件?問「有必要嗎?」
@@ -400,3 +458,6 @@ listEntries 加 `source` filter 即可,表結構一個欄位都不動。
- [ ] 碰 KBDB?確認打基本盤現存 route/entries /templates /records /entries/search),別假設 v3 /blocks /search 還在 - [ ] 碰 KBDB?確認打基本盤現存 route/entries /templates /records /entries/search),別假設 v3 /blocks /search 還在
- [ ] KBDB 要新可查欄位?用 json_extract 查 metadata_json,別加真欄(表不變鐵律,#18 - [ ] KBDB 要新可查欄位?用 json_extract 查 metadata_json,別加真欄(表不變鐵律,#18
- [ ] 改 deploy.ts toml 注入順序?想「後面還有沒有 pass(如 strip)會動同一段」(#17 - [ ] 改 deploy.ts toml 注入順序?想「後面還有沒有 pass(如 strip)會動同一段」(#17
- [ ] 新增/改薄殼工具?grep 確認它打的 server route 真的存在,別假綠(#19);宣稱對齊前跑 thin-shell-smoke.sh
- [ ] 想用 git log 查某檔歷史?先 git check-ignore——gitignored 檔無 git 史,時間靠檔內註記(#20
- [ ] 改 wrangler.toml service binding?用 [[services]] 不用 inline services=[...][vars] 後會被吸走);改完 wrangler --dry-run 驗 binding 真在(#21
+4 -1
View File
@@ -21,7 +21,10 @@ metadata:
> - ✅ **已實作 tsc 全綠**1.1 `/webhooks/named` 強制 description2.2+Q4 KBDB base 通用 entry_type filter(改4處:searchEntries/semanticSearch/route/proxy)|2.1 部署雙寫 embeddable entry(注意 KBDB 用 metadata_json 字串)|3.1 cypher `/workflows/search`3.2 MCP `u6u_search_workflows`4.1 `/workflows/backfill-search-entries`1.3b `GET /webhooks/named` 補 description/created_at 欄位。 > - ✅ **已實作 tsc 全綠**1.1 `/webhooks/named` 強制 description2.2+Q4 KBDB base 通用 entry_type filter(改4處:searchEntries/semanticSearch/route/proxy)|2.1 部署雙寫 embeddable entry(注意 KBDB 用 metadata_json 字串)|3.1 cypher `/workflows/search`3.2 MCP `u6u_search_workflows`4.1 `/workflows/backfill-search-entries`1.3b `GET /webhooks/named` 補 description/created_at 欄位。
> - ⏸ **卡待總管定**Phase 1.2/1.3MCP deploy 改打 /webhooks/named)卡在 ①-a/b/c——實作期發現 /webhooks/named 吃 graph 非 YAMLYAML→graph 編排寫在 CLI push.ts 介面層,MCP 複製=違 rule 07。①-c(先通債另開 issue)我推薦,待總管定。 > - ⏸ **卡待總管定**Phase 1.2/1.3MCP deploy 改打 /webhooks/named)卡在 ①-a/b/c——實作期發現 /webhooks/named 吃 graph 非 YAMLYAML→graph 編排寫在 CLI push.ts 介面層,MCP 複製=違 rule 07。①-c(先通債另開 issue)我推薦,待總管定。
> - **完成標準**:tsc 綠≠完成,框架級待 leo21c 端到端實證(強制填擋空/搜尋命中/租戶隔離/降級 hint/MCP 不再 404)。issue open。 > - **完成標準**:tsc 綠≠完成,框架級待 leo21c 端到端實證(強制填擋空/搜尋命中/租戶隔離/降級 hint/MCP 不再 404)。issue open。
> - **未 commit**(待 leo 明示;wiki 骨架與 #8 SDD/code 建議分兩 commit)。署名鐵律:跨 repo comment 開頭 `[arcrun CC]`#12)。 > - **已 merge 進 main**fast-forward3 commit934b926 #4/5/7/8 功能 / 558e80b wiki-init / 5d38b59 #11 薄殼對齊)+ push origin main。repo Actions `enabled:false`leo 關),merge **未觸發部署**。**merge≠部署**#5/#7/#8/#11 端到端待 leo21c wrangler 直推(Mira 線)。
> - **issue #11 全做**P0 run 死端點/P1 list 同源/R4 防複發機制:對照清單+smoke+自驗能攔);P2 validate 依賴 #10、tag resource_id 語意債待方向①。
> - **issue #12self-hosted MCP 缺 CYPHER_EXECUTOR binding)✅ 修+merge maincommit 222a382**:根因非總管假設的 strip 誤清,是 **TOML 坑——mcp toml `services=[...]` inline 在 [vars] 後被吸成 vars.services**binding 消失);修法 inline→`[[services]]` array-of-tables(對齊官方 cypher)。wrangler --dry-run 雙向驗(修法後 binding 在/舊版變 env.services)。**端到端待 leo21c acr update 重部 MCP**;連帶解鎖 #11 self-hosted MCP + mira #6 門鈴 + acr push。見 mistakes #21
> - 署名鐵律:跨 repo comment 開頭 `[arcrun CC]`#12 issue 慣例,非本次 bug)。
> >
> **2026-06-26 上個 sessionissue #4/#5/#6/#7 一批)** > **2026-06-26 上個 sessionissue #4/#5/#6/#7 一批)**
> - **#6**base `PATCH /records/:id`):✅ updateRecord + route,三表 append-only 不破。tsc 綠,端到端待 leo21c。issue open。 > - **#6**base `PATCH /records/:id`):✅ updateRecord + route,三表 append-only 不破。tsc 綠,端到端待 leo21c。issue open。