feat(workflow-discovery): /cypher/search 改為真查 registry——修「查詢回假信號」
📋 SDD:workflow-discovery(本 commit 同時執行 D35 交接:portal-auth 26/26 完成 → closed superseded_by workflow-discovery;workflow-discovery paused → active。單一活性已驗=1 份) 🎯 對應 task:3.x 搜尋端誠實化(CP2-B) 病灶(leo 2026-07-30 定性「腹語術」): search-nodes.ts 無條件回 status:'found'、missingNodes 永遠 []—— 型別宣告了 'missing' 但程式碼從不使用。實測「完全不存在的東西xyz」也回 found。 ⇒ AI 拿到假信號 → 以為零件存在 → 部署才發現沒有 → 改寫 code ⇒ 正式 workflow 只用 2 個零件、8 個 code 節點含 if×61。 修法: - 查 registry 判真實存在(走 HTTP,守 D28 禁新增 service binding; URL 用既有 wasmWorkerUrl() 慣例組,不自創) - found 時附 input_schema/success_rate/stability ⇒ AI 才填得出 payload、才看得到「測過幾次」(leo:AI 只要填 payload) - 查不通回 'unknown' 而非 'missing'——**誠實限制**: 不能因查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code) - missing 真的回傳出去(原本寫死 []) ⚠️ 實測發現 registry **沒有列表端點**(GET /components → 404,只有 /components/<id>) ⇒ 改為逐個查(節點數通常 <10、5s timeout)。補列表端點後可改抓一次=CP2-B 待辦。 驗:tsc 零錯誤;vitest 9 failed/179 passed=**與改動前 stash 對帳完全相同**(既有債非本次造成)。 ⏳ 待部署到實例後跑 arcrun-usable/verify.sh 驗 01 那組轉綠。
This commit is contained in:
@@ -17,10 +17,12 @@ export async function handleCypherSearch(
|
||||
throw new Error('無法解析任何節點');
|
||||
}
|
||||
|
||||
const { nodeResults } = searchNodes(parsed);
|
||||
// 2026-07-30:查 registry 判真實存在(workflow-discovery)。
|
||||
// `missing` 以前寫死 [],等於告訴 AI「什麼都有」——那是「腹語術」的入口。
|
||||
const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env);
|
||||
|
||||
const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result');
|
||||
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: [] };
|
||||
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes };
|
||||
}
|
||||
|
||||
export async function handleCypherExecute(
|
||||
@@ -50,7 +52,7 @@ export async function handleCypherExecute(
|
||||
throw new Error('無法解析任何節點');
|
||||
}
|
||||
|
||||
const { nodeResults } = searchNodes(parsed, config);
|
||||
const { nodeResults } = await searchNodes(parsed, config, env);
|
||||
|
||||
const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config);
|
||||
const parseResult = graphSchema.safeParse(graph);
|
||||
|
||||
@@ -1,27 +1,53 @@
|
||||
import type { ParsedTriplets, NodeRole } from './triplet-parser';
|
||||
import { resolveNodeRole } from './triplet-parser';
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
|
||||
export type NodeStatus = 'found' | 'missing' | 'unknown';
|
||||
|
||||
export type NodeInfo = {
|
||||
status: NodeStatus;
|
||||
componentId?: string;
|
||||
type: NodeRole;
|
||||
/** 零件契約(found 時附上,讓 AI 知道怎麼填 payload)。 */
|
||||
input_schema?: unknown;
|
||||
/** 成功率(found 時附上,讓「被測過幾次」看得見)。 */
|
||||
success_rate?: number;
|
||||
stability?: string;
|
||||
/** missing 時給的相近零件建議(避免 AI 只知道「沒有」卻不知道該用什麼)。 */
|
||||
suggestions?: string[];
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
nodeResults: Record<string, { status: 'found' | 'missing'; componentId?: string; type: NodeRole }>;
|
||||
nodeResults: Record<string, NodeInfo>;
|
||||
missingNodes: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 對所有節點進行解析,確認每個節點對應的零件 ID。
|
||||
* 對所有節點進行解析,確認每個節點對應的零件 ID 與**是否真的存在**。
|
||||
*
|
||||
* 注意:此步驟只做靜態解析,不做遠端查找。
|
||||
* 零件是否真的存在由 component-loader 在執行時決定(Service Binding / KV / URL)。
|
||||
* ⚠️ 2026-07-30 改為會查 registry(workflow-discovery task 3.x)。
|
||||
*
|
||||
* 優先序:
|
||||
* 1. Input/Output 角色:自動標記,componentId = 小寫節點名稱
|
||||
* 2. config[nodeName].component 已指定:使用 config 提供的 componentId
|
||||
* 3. 其他:componentId = 節點名稱(交給 component-loader 在執行時解析)
|
||||
* 改之前的行為(病灶):無條件回 `status: 'found'`、`missingNodes` 永遠是空陣列——
|
||||
* 型別雖宣告了 `'missing'` 但程式碼從不使用。實測「完全不存在的東西xyz」也回 found。
|
||||
*
|
||||
* 為什麼這是嚴重問題(leo 2026-07-30 定性「腹語術」):
|
||||
* AI 寫意圖 → 查詢回「都 found」(假信號)→ 實際零件不存在
|
||||
* → 部署/執行才發現 → 最快的修法是改寫成 `code` 節點自己寫 JS
|
||||
* → 於是正式 workflow 只用 2 個零件、8 個 code 節點含 if×61
|
||||
* ⇒ 「零件被測過 1000 次所以 AI 只要填 payload」的價值完全落空。
|
||||
*
|
||||
* 誠實限制:查不到 registry(未部署/網路失敗)時回 `'unknown'` 而不是 `'missing'`——
|
||||
* 不能因為查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code,正是要避免的事)。
|
||||
*/
|
||||
export function searchNodes(
|
||||
export async function searchNodes(
|
||||
parsed: ParsedTriplets,
|
||||
config?: Record<string, Record<string, unknown>>,
|
||||
): SearchResult {
|
||||
const nodeResults: Record<string, { status: 'found' | 'missing'; componentId?: string; type: NodeRole }> = {};
|
||||
env?: { WORKER_SUBDOMAIN?: string },
|
||||
): Promise<SearchResult> {
|
||||
const nodeResults: Record<string, NodeInfo> = {};
|
||||
const missingNodes: string[] = [];
|
||||
|
||||
const sub = env?.WORKER_SUBDOMAIN;
|
||||
|
||||
for (const nodeName of parsed.nodeNames) {
|
||||
const role = resolveNodeRole(nodeName, parsed);
|
||||
@@ -33,8 +59,85 @@ export function searchNodes(
|
||||
|
||||
const configComponent = config?.[nodeName]?.component as string | undefined;
|
||||
const componentId = configComponent ?? nodeName;
|
||||
nodeResults[nodeName] = { status: 'found', componentId, type: role };
|
||||
|
||||
// config 明確給了 component(多半是安裝器代入的 worker URL 或既有 workflow)
|
||||
// → 不判 missing。這條路徑的存在性由 component-loader 在執行時決定(原行為)。
|
||||
if (configComponent) {
|
||||
nodeResults[nodeName] = { status: 'found', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sub) {
|
||||
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
|
||||
const q = await fetchComponent(sub, componentId);
|
||||
if (!q.ok) {
|
||||
// registry 查不通(未部署/網路失敗)⇒ 誠實回 unknown。
|
||||
// **不能誤判 missing**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。
|
||||
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
if (q.entry) {
|
||||
nodeResults[nodeName] = {
|
||||
status: 'found',
|
||||
componentId,
|
||||
type: role,
|
||||
input_schema: q.entry.input_schema,
|
||||
success_rate: q.entry.success_rate,
|
||||
stability: q.entry.stability,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
nodeResults[nodeName] = { status: 'missing', componentId, type: role };
|
||||
missingNodes.push(nodeName);
|
||||
}
|
||||
|
||||
return { nodeResults, missingNodes: [] };
|
||||
return { nodeResults, missingNodes };
|
||||
}
|
||||
|
||||
type CatalogEntry = {
|
||||
input_schema?: unknown;
|
||||
success_rate?: number;
|
||||
stability?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 查單一零件是否存在於 registry。
|
||||
*
|
||||
* ⚠️ 為什麼逐個查而非抓整份目錄:registry **沒有列表端點**
|
||||
* (實測 `GET /components` → 404,只有 `GET /components/<id>`)。
|
||||
* 這是 CP2-B 記載的缺口(「修 /components 404」)——補了列表端點後可改為抓一次。
|
||||
* 現階段逐個查:節點數通常 <10,且有 5s timeout,可接受。
|
||||
*
|
||||
* 回傳 `null` 代表「查不到 registry 或該零件不存在」,由呼叫端區分:
|
||||
* 整體查不通 → `unknown`;查得通但這顆沒有 → `missing`。
|
||||
*/
|
||||
async function fetchComponent(
|
||||
subdomain: string,
|
||||
id: string,
|
||||
): Promise<{ ok: boolean; entry?: CatalogEntry }> {
|
||||
try {
|
||||
const base = wasmWorkerUrl('registry', subdomain);
|
||||
const res = await fetch(`${base}/components/${encodeURIComponent(id)}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (res.status === 404) return { ok: true }; // registry 活著,但沒這顆
|
||||
if (!res.ok) return { ok: false };
|
||||
const body = (await res.json()) as { success?: boolean; data?: Record<string, unknown> };
|
||||
if (body.success === false) return { ok: true }; // 同上:回「零件不存在」
|
||||
const d = body.data ?? (body as unknown as Record<string, unknown>);
|
||||
return {
|
||||
ok: true,
|
||||
entry: {
|
||||
input_schema: d.input_schema,
|
||||
success_rate: typeof d.success_rate === 'number' ? d.success_rate : undefined,
|
||||
stability: typeof d.stability === 'string' ? d.stability : undefined,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function resolveWebhookGraph(
|
||||
const parsed = parseTriplets(body.triplets as unknown[]);
|
||||
if (!parsed) return { resolvedGraph: {}, error: '無法解析 triplets' };
|
||||
|
||||
const { nodeResults } = searchNodes(parsed);
|
||||
const { nodeResults } = await searchNodes(parsed);
|
||||
|
||||
const graphId = `webhook-${Date.now()}`;
|
||||
const graphName = description || `Webhook ${new Date().toISOString()}`;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
status: active
|
||||
superseded_by: ""
|
||||
status: closed
|
||||
superseded_by: "workflow-discovery"
|
||||
closed_at: "2026-07-30"
|
||||
closed_reason: "26/26 任務全完成、0 未完成待搬;封測 portal 多人授權已上線"
|
||||
---
|
||||
|
||||
# portal-auth — Design(RAG Portal 多人授權)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
status: paused
|
||||
status: active
|
||||
superseded_by: ""
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user