name: graph_neighbors
description: >
  同步查詢：給一個節點 → 從 KBDB triplet 記錄建鄰接表 → 記憶體 BFS 找 N 跳鄰居 → 同步回鄰居清單。
  這是「查詢面工作流」示範：用同步查詢 trigger（GET /q/:ns/graph_neighbors 或
  POST /webhooks/named/:name/query），把 workflow 最終節點輸出直接當 HTTP response 拿回，
  取代 graph plugin 內建的 GET /graph/neighbors。對照 kbdb-graph-plugin graph-traverse.ts 的記憶體 BFS。

# ══════════════════════════════════════════════════════════════════════════════
# ⚠️ 先改成你自己的 KBDB URL（self-hosted 必看）
#   fetch_triplets 的 base URL 走 {{input.kbdb_base}} 參數（每次呼叫帶），**沒有寫死官方庫**。
#   照抄本示範時，把觸發參數 kbdb_base 換成你自己部署的 KBDB 對外 URL——
#   抄了別人的值＝你的查詢與資料流向直接打進別人的庫（KBDB_BASE_URL fallback 同家族坑）。
# ══════════════════════════════════════════════════════════════════════════════

# ── 1042 與 kbdb_base 該帶哪種 URL ──
# cypher-executor 執行 http_request 節點時對 kbdb 發 fetch。若打同 zone URL 會踩 CF 1042
# （same-zone self-fetch）。兩條路皆可：
#   a) 帶 KBDB 的 custom domain（跨 zone、走公網前門，天然避開 1042）；
#   b) self-hosted 有 `global_fetch_strictly_public` compatibility flag
#      （credential-primitives-wasm Phase 7，cypher wrangler.toml），開了之後
#      workers.dev URL 亦可直接帶。

# ── 觸發（同步查詢 trigger，非 202）──
#   GET  https://{你的-cypher-domain}/q/{namespace}/graph_neighbors?node=Arcrun&depth=2&template=graph_triplet&namespace={namespace}&kbdb_base=https://{你的-kbdb-domain}
#   POST https://{你的-cypher-domain}/webhooks/named/graph_neighbors/query
#        -H "X-Arcrun-API-Key: {namespace}"
#        -d '{"node":"Arcrun","depth":2,"template":"graph_triplet","namespace":"{namespace}","kbdb_base":"https://{你的-kbdb-domain}"}'
#   → 直接回 { success, start, depth, directed, neighbors:[...], count }（最終節點輸出本身）。
#   （敏感輸入用 POST /query：GET /q/ 的 query string 會進各層 log，見 description.md。）

flow:
  - "input >> ON_SUCCESS >> fetch_triplets"
  - "fetch_triplets >> ON_SUCCESS >> bfs_neighbors"

config:
  # 1) 撈本租戶的 triplet 記錄。triplet = base 萬用表的一個 template（graph plugin 寫入），
  #    slots = subject / predicate / object。base 端點：GET /records/by-template/:template?owner_id=
  #    回 { success, records:[{ record_id, values:{subject,predicate,object} }], count }。
  #    ⚠️ template 名（此處 {{input.template}}，預設由呼叫者帶 graph_triplet）以實際部署的
  #       kbdb-graph-plugin triplet template id 為準——上線前對一次。
  #    ⚠️ base URL 由呼叫者帶（{{input.kbdb_base}}）——不寫死任何一家的庫（見檔頂警示）。
  fetch_triplets:
    component: http_request
    method: GET
    url: "{{input.kbdb_base}}/records/by-template/{{input.template}}?owner_id={{input.namespace}}"
    headers:
      Accept: "application/json"

  # 2) ★ 記憶體 BFS（通用 code 零件，sandbox inline JS，無 LLM、無 fs/網路，stdin→stdout JSON）。
  #    對照 kbdb-graph-plugin graph-traverse.ts:23-51 的記憶體 BFS：把 triplet 當有向邊
  #    subject --predicate--> object 建鄰接表，從 start 逐跳擴張到 depth 上限，收集新訪節點當鄰居。
  #    directed=false（預設）時把邊當雙向（無向圖鄰居）；directed=true 只走 subject→object。
  bfs_neighbors:
    component: code
    code: |
      // graph_neighbors — 記憶體 BFS 找 N 跳鄰居（純函式、決定性、零 token）。
      // input（由下方 input: 映射解析後注入）：
      //   records[]  : triplet 記錄（{ values:{subject,predicate,object} } 或扁平 {subject,predicate,object}）
      //   start      : 起點節點名（字串）
      //   depth      : 最大跳數（字串或數字，來自 query string 時是字串）
      //   directed   : "true" 只走 subject→object；否則當無向

      const records = Array.isArray(input.records) ? input.records : [];
      const start = String(input.start == null ? '' : input.start);
      const maxDepth = Math.max(1, parseInt(String(input.depth == null ? 1 : input.depth), 10) || 1);
      const directed = String(input.directed == null ? '' : input.directed) === 'true';

      if (!start) {
        return { success: false, error: 'graph_neighbors 缺 start（node）參數' };
      }

      // 建鄰接表：subject --predicate--> object。無向時同時加反向邊。
      const adj = new Map();
      function addEdge(from, to, predicate) {
        if (!adj.has(from)) adj.set(from, []);
        adj.get(from).push({ node: to, predicate: predicate });
      }
      for (const r of records) {
        const v = (r && typeof r === 'object' && r.values && typeof r.values === 'object') ? r.values : r;
        if (!v || typeof v !== 'object') continue;
        const s = v.subject, p = v.predicate, o = v.object;
        if (!s || !o) continue;
        addEdge(s, o, p);
        if (!directed) addEdge(o, s, p);
      }

      // BFS：一層一跳，收集首次訪到的節點當鄰居（記 depth / 來源 / 關係）。
      const visited = new Set([start]);
      let frontier = [start];
      const neighbors = [];
      for (let d = 1; d <= maxDepth; d++) {
        const next = [];
        for (const cur of frontier) {
          const outs = adj.get(cur) || [];
          for (const e of outs) {
            if (visited.has(e.node)) continue;
            visited.add(e.node);
            neighbors.push({ node: e.node, predicate: e.predicate, from: cur, depth: d });
            next.push(e.node);
          }
        }
        frontier = next;
        if (frontier.length === 0) break;
      }

      return {
        success: true,
        start: start,
        depth: maxDepth,
        directed: directed,
        neighbors: neighbors,
        count: neighbors.length,
      };
    # input 映射：{{...}} 對 workflow context 展開後注入 code 沙箱的 `input` 變數。
    # {{input.X}} 的 input = 上游 input 節點輸出（＝觸發 context）；{{fetch_triplets.data.records}}
    # = http_request 回應 body 的 records 陣列（單一 ref pass-through 保留陣列型別）。
    input:
      records: "{{fetch_triplets.data.records}}"
      start: "{{input.node}}"
      depth: "{{input.depth}}"
      directed: "{{input.directed}}"
    limits:
      timeout_ms: 3000            # 純 CPU BFS，充裕
      max_output_bytes: 2097152   # 鄰居清單上限 2 MiB（呼叫端同步查詢輸出也有 5 MiB 硬上限）
