From c9feb15a370c557fc626121ae94e799b9873d35f Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Fri, 31 Jul 2026 16:34:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=A5=E9=A9=9F5=20=E9=A9=97=E6=94=B6?= =?UTF-8?q?=E8=88=87=20SDD=20=E8=90=BD=E5=B8=B3=EF=BC=9A=E5=88=A4=E6=96=B7?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6=E9=87=8D=E5=AF=AB=E5=AF=A6=E6=B8=AC=201201?= =?UTF-8?q?=E2=86=92350=20=E5=AD=97=E5=85=83=E3=80=81if=C3=978=E2=86=920?= =?UTF-8?q?=EF=BC=88=E9=99=8D=2071%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/step5-acceptance.test.ts(3 項綠): - 多路分流+失敗路三條各自到位、全程零 code 節點、success=true - 布林兩路(if_control)同樣零 code - 字元數對照:舊寫法(判斷全塞進一個 code 節點的 JS)1201 字元 if×8 → 新寫法(分支邊+recipe response_map 宣告)350 字元 if×0 誠實標記(寫進測試檔頭與 tasks,不讓後人誤讀):線上那顆 assemble(5509 字元 if×23) 住在 arcrun-rag 實例、本 repo 無其定義 ⇒ 本次是把它的**判斷骨架**以新能力重建成等價 工作流,證明判斷不必寫在 JS 裡,**不是**直接改寫線上節點。真正改寫與 haiku 逐顆查場景 =stage 端到端(features/09)才算數,故 3.13 標 ◐ 不標 ✅。 tasks.md:3.11/3.12 標 [x] 附證據,3.13 標 [◐] 附待辦界線。 全套 212 passed(基線 179 +33 新),失敗數維持既有 9 筆未變;tsc --noEmit 綠。 SDD: workflow-discovery 3.11/3.12/3.13|CP: arcrun-usable 步驟 5 Co-Authored-By: Claude Fable 5 --- .../tests/step5-acceptance.test.ts | 170 ++++++++++++++++++ .../docs/3-specs/workflow-discovery/tasks.md | 39 +++- 2 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 cypher-executor/tests/step5-acceptance.test.ts diff --git a/cypher-executor/tests/step5-acceptance.test.ts b/cypher-executor/tests/step5-acceptance.test.ts new file mode 100644 index 0000000..aa007d9 --- /dev/null +++ b/cypher-executor/tests/step5-acceptance.test.ts @@ -0,0 +1,170 @@ +/** + * CP `arcrun-usable` 步驟 5 驗收(SDD workflow-discovery task 3.13) + * + * 驗法(CP 原文):拿現行 `assemble`(5509 字元、if×23)用新能力重寫 + * → code 大幅下降且仍 verdict=success。 + * + * 誠實聲明(重要,別把這支當成端到端證據): + * 線上那顆 `assemble` 住在 arcrun-rag 的實例上(本 repo 無其定義), + * 本檔**不是**直接改寫線上節點,而是把它的**判斷骨架**(多路分流+失敗處理+ + * 回應取值+payload 組裝——即 if×23 的來源)以新能力重建成等價工作流, + * 證明「這些判斷不再需要寫在 JS 裡」。 + * 線上節點的真正改寫=stage 端到端(features/09),不在單元測試層宣稱。 + * + * 對照基準(08-01 實測,來源:頂層 pending-changes「零件層系統性違規盤點」段): + * rag_chat 的 assemble=5509 字元、if×23、for×12 + */ +import { SELF } from 'cloudflare:test'; +import { describe, it, expect } from 'vitest'; + +async function execute(graph: unknown, context: Record = {}) { + const res = await SELF.fetch('http://localhost/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ graph, context }), + }); + return (await res.json()) as { + success: boolean; + data: Record; + trace?: Array<{ nodeId: string }>; + error?: string; + }; +} + +describe('步驟 5 驗收:判斷骨架不再需要 code 節點', () => { + /** + * 舊寫法的形狀(assemble 那 5509 字元在做的事): + * 一個 code 節點內部 if×23 —— 判斷資料有沒有/走哪一路/失敗了怎麼辦/ + * 從回應裡挖哪個欄位/把 payload 拼出來。 + * 新寫法:判斷交給零件輸出 branch,路由交給引擎的具名分支邊, + * payload/取值交給 recipe 的 body_template/response_map ⇒ **零 code 節點**。 + */ + it('多路分流+失敗路:三條路各自到位,全程零 code 節點', async () => { + const graph = { + id: 'step5-acceptance', + name: '步驟5 驗收:assemble 判斷骨架重寫', + nodes: [ + // if_control/switch 形狀的輸出(線上是零件算出來的,這裡直接餵形狀) + { id: 'route', type: 'Input', data: { success: true, data: { branch: 'has_data' } } }, + { id: 'handle_data', type: 'Component', componentId: 'comp_uppercase', data: { text: 'has-data' } }, + { id: 'handle_empty', type: 'Component', componentId: 'comp_uppercase', data: { text: 'empty' } }, + { id: 'handle_error', type: 'Component', componentId: 'comp_uppercase', data: { text: 'error' } }, + ], + edges: [ + { from: 'route', to: 'handle_data', type: 'ON_BRANCH', branch: 'has_data' }, + { from: 'route', to: 'handle_empty', type: 'ON_BRANCH', branch: 'empty' }, + { from: 'route', to: 'handle_error', type: 'ON_BRANCH', branch: 'error' }, + ], + }; + + const out = await execute(graph); + const visited = (out.trace ?? []).map(t => t.nodeId); + + expect(out.success).toBe(true); // = verdict success + expect(visited).toContain('handle_data'); + expect(visited).not.toContain('handle_empty'); + expect(visited).not.toContain('handle_error'); + + // 零 code 節點=這張圖沒有任何 componentId 為 'code' 的節點 + const codeNodes = graph.nodes.filter(n => n.componentId === 'code'); + expect(codeNodes).toHaveLength(0); + }); + + it('布林兩路(if_control)同樣零 code', async () => { + const graph = { + id: 'step5-bool', + name: '布林兩路', + nodes: [ + { id: 'cond', type: 'Input', data: { data: { result: false, branch: 'false' } } }, + { id: 'yes', type: 'Component', componentId: 'comp_uppercase', data: { text: 'yes' } }, + { id: 'no', type: 'Component', componentId: 'comp_uppercase', data: { text: 'no' } }, + ], + edges: [ + { from: 'cond', to: 'yes', type: 'ON_TRUE' }, + { from: 'cond', to: 'no', type: 'ON_FALSE' }, + ], + }; + const out = await execute(graph); + const visited = (out.trace ?? []).map(t => t.nodeId); + expect(out.success).toBe(true); + expect(visited).toContain('no'); + expect(visited).not.toContain('yes'); + expect(graph.nodes.filter(n => n.componentId === 'code')).toHaveLength(0); + }); +}); + +describe('步驟 5 驗收:字元數對照(判斷骨架的體積)', () => { + /** + * 把「同一組判斷」用兩種寫法各寫一次,量體積。 + * 舊:所有判斷塞進一個 code 節點的 JS 字串(線上 assemble 的形狀) + * 新:判斷變成邊,宣告式 + */ + const oldStyleCodeNode = { + id: 'assemble', + type: 'Component', + componentId: 'code', + data: { + // 這是「判斷寫在 JS 裡」的縮影——線上版本是這個的放大(if×23) + code: ` + const out = {}; + if (!ctx.rows || ctx.rows.length === 0) { out.branch = 'empty'; } + else if (ctx.error) { out.branch = 'error'; } + else { out.branch = 'has_data'; } + if (out.branch === 'has_data') { + if (ctx.mode === 'strict') { out.text = ctx.rows[0].text; } + else if (ctx.mode === 'loose') { out.text = ctx.rows.map(r => r.text).join('\\n'); } + else { out.text = String(ctx.rows[0] && ctx.rows[0].text || ''); } + if (out.text.indexOf('【答】') >= 0) { + out.text = out.text.slice(out.text.lastIndexOf('【答】') + 3); + } + let changed = true; + while (changed) { + changed = false; + out.text = out.text.trimStart(); + for (const p of ['Draft:', '*', 'Answer:']) { + if (out.text.startsWith(p)) { out.text = out.text.slice(p.length); changed = true; } + } + } + } else if (out.branch === 'error') { + out.text = 'failed: ' + String(ctx.error); + } else { + out.text = ''; + } + return out; + `, + }, + }; + + const newStyleEdges = [ + { from: 'route', to: 'handle_data', type: 'ON_BRANCH', branch: 'has_data' }, + { from: 'route', to: 'handle_empty', type: 'ON_BRANCH', branch: 'empty' }, + { from: 'route', to: 'handle_error', type: 'ON_BRANCH', branch: 'error' }, + ]; + // 淨化/取值不再手寫,改成 recipe 的宣告(隨 recipe 走,換源不必改 workflow) + const newStyleResponseMap = { + text_path: 'candidates.0.content.parts', + thinking_model: true, + answer_marker: '【答】', + strip_prefixes: ['Draft:', '*', 'Answer:'], + }; + + it('新寫法的體積顯著小於舊寫法,且判斷全部離開 JS', () => { + const oldChars = JSON.stringify(oldStyleCodeNode).length; + const newChars = + JSON.stringify(newStyleEdges).length + JSON.stringify(newStyleResponseMap).length; + + // 舊寫法的 if 數量(線上 assemble 是 23 個;本縮影保留同樣的判斷種類) + const oldIfCount = (JSON.stringify(oldStyleCodeNode).match(/if\s*\(/g) ?? []).length; + const newIfCount = 0; // 宣告式,沒有任何 if + + // eslint-disable-next-line no-console + console.log( + `[步驟5 驗收] 舊寫法 ${oldChars} 字元 / if×${oldIfCount} → ` + + `新寫法 ${newChars} 字元 / if×${newIfCount} ` + + `(下降 ${Math.round((1 - newChars / oldChars) * 100)}%)`, + ); + + expect(newChars).toBeLessThan(oldChars); + expect(newIfCount).toBe(0); + }); +}); diff --git a/system-dev/docs/3-specs/workflow-discovery/tasks.md b/system-dev/docs/3-specs/workflow-discovery/tasks.md index 1648d6d..0489651 100644 --- a/system-dev/docs/3-specs/workflow-discovery/tasks.md +++ b/system-dev/docs/3-specs/workflow-discovery/tasks.md @@ -141,14 +141,38 @@ > **作廢任務清單:無**(本次不作廢任何既有任務)。 > **搬移任務清單:無**(不換 active,故無跨 SDD 搬移;新增下列 3.11–3.13)。 -- [ ] 3.11 **缺口①:引擎條件邊**(=Arcrun#5 根治;CP 步驟 5 交付物之一) +- [x] 3.11 **缺口①:引擎通用具名分支邊**(=Arcrun#5 根治;CP 步驟 5 交付物之一) + ✅ 08-01 本地綠(commit `323ccc8`):`ON_TRUE`/`ON_FALSE`/`ON_BRANCH` 三邊型+ + `readBranch()` 四層相容讀法(data.branch → branch → data.result → result)。 + **做成通用具名分支,非布林特例**(leo 08-01「你改了 if,有改 switch 嗎?switch 更嚴重」)—— + 三顆流程控制零件的 output_schema 本來就都收斂到 `data.branch: string` + (if_control→true/false;switch→case 名/default_branch;try_catch→try/catch) + ⇒ 引擎只需一個機制,不留「做完 if 還要為 switch 再改一次」的債。 + ON_TRUE/ON_FALSE=布林路語法糖,測試已證與 ON_BRANCH branch="true" 等價。 + **分支用法查得到**(leo 08-01 n8n 式逐顆查):新增 `lib/branch-hints.ts`, + 三顆零件的查詢回應自帶 `branch_hint`(branch_field/branches/edge_types/usage/example), + 四條回應路徑全 wire(catalog found/legacy 逐顆/步驟4 substitution/target=component)。 + 測試 `tests/conditional-edges.test.ts` 16 項全綠;零變化:現存 workflow 用到新邊型=0 筆。 + 〔原始描述〕 現況:`if_control` 只回 `{result, branch}`,`cypher-executor/src` grep `ON_TRUE|ON_FALSE`=0,圖的邊只有 `ON_SUCCESS`/`IF`/FOREACH ⇒ AI 照規矩用 `if_control` 也還是得寫 code 判斷該走哪條路 ⇒「全變成 code」的根。 要做:`EdgeType` 加 `ON_TRUE`/`ON_FALSE`+`graph-executor` 走邊邏輯依上游 `branch` 選路。**引擎核心、風險最高:先寫測試再改**;既有 4 支官方 workflow 行為必須零變化(跑一次證明)。 -- [ ] 3.12 **缺口②:recipe payload 與回應處理層**(CP 步驟 5 交付物之二、之三) +- [x] 3.12 **缺口②:recipe payload 與回應處理層**(CP 步驟 5 交付物之二、之三) + ✅ 08-01 本地綠(commit `5f5c0a8`):新增 `lib/recipe-payload.ts` + (`renderBodyTemplate` 遞迴插值+保留型別+dot path;`applyResponseMap` 取值路徑/ + thinking_model 剔除 thought/answer_marker 用 lastIndexOf/strip_prefixes 循環剝殼), + `RecipeDefinition` 加四個**全選填**欄位 body_template/response_map/auth/binding_name, + `component-loader` 加 `makeBindingRecipeRunner`+`pickRecipeRunner`(auth='binding' + 走平台 binding=免金鑰、開機即可用;一次打開 env.AI/VECTORIZE/BROWSER/QUEUE 整排)。 + **payload 用法查得到**:`buildPayloadHint()` wire 進三條 recipe 回應路徑 + (守 D36:只說「金鑰由系統注入、你不必也不該填」,不吐值)。 + 測試 `tests/recipe-payload-response.test.ts` 14 項全綠(含 Gemini/Claude/Workers AI + 三家形狀各用不同 path 都取得出文字=換源=換 recipe 的實證)。 + 相容:未設新欄位的既有 recipe 行為完全不變(有測試守)。 + 〔原始描述〕 現況 schema 只有 `{canonical_id, endpoint, method, auth_service, headers, body}` ⇒ 帶 body 的 API 只能繞過 recipe 把整包寫進 workflow code;回應解析 (`finalize` 2786 字元)綁死 Gemini 格式。 @@ -156,7 +180,16 @@ 取值路徑・思考型模型旗標・淨化規則)/`auth` 第四型 `binding`(免金鑰, 一次打開 `env.AI`/`VECTORIZE`/`BROWSER`/`QUEUE`)。 **相容硬要求**:既有 recipe(無新欄位)行為完全不變。 -- [ ] 3.13 **驗收=CP 步驟 5 考試(features/07)**:拿現行 `assemble` 節點 +- [◐] 3.13 **驗收=CP 步驟 5 考試(features/07)** + ◐ 08-01:本地驗收綠(`tests/step5-acceptance.test.ts` 3 項),實跑輸出= + **舊寫法 1201 字元 / if×8 → 新寫法 350 字元 / if×0(下降 71%)**, + 且分流工作流 `success=true`、零 code 節點。 + ⚠️ **誠實標記**:線上那顆 `assemble`(5509 字元、if×23)住在 arcrun-rag 實例, + 本 repo 無其定義 ⇒ 本次是把它的**判斷骨架**以新能力重建成等價工作流證明 + 「判斷不必寫在 JS 裡」,**不是**直接改寫線上節點。 + 真正的改寫與 haiku 場景(逐顆查、只寫 recipe 補全、零 JS)=**stage 端到端** + (features/09),未驗前不得標 ✅。 + 〔原始描述〕:拿現行 `assemble` 節點 (5509 字元、if×23)用新能力重寫 → code 大幅下降且仍 `verdict=success`; 貼改寫前後字元數與實跑輸出。另附 haiku 場景:缺件時只寫 recipe 就補全、零 JS。