@mawaru/sdk 0.13.0 → 0.15.0

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.
@@ -2,12 +2,14 @@ import { z } from "zod"
2
2
  import type { GraphPort, JsonSchema, PortIo } from "./node.js"
3
3
 
4
4
  // ポートの kind 別型システム(docs/tasks/wip/ポートのkind別仕様.md)。
5
- // 型の決まり方は3分類:宣言(program / AI の in・out)/固定(human reject の
6
- // content 型)/導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)
5
+ // 型の決まり方は2分類:宣言(program / ai の in・outhuman はビューの宣言を転写)/
6
+ // 導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)。
7
+ // mawaru がコードで型を固定するポートは無い
8
+ // (docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md)
7
9
 
8
10
  // 「まだ型が決まっていない」=未宣言を {} で表す(未接続・上流が未宣言・fan-in の型割れ)。
9
- // 「何でも受ける」型は存在しない:AI の in は接続元の具体型を鏡映し、差し戻しの指示は
10
- // instruction ポートの宣言型を持つ(docs/tasks/wip/ポートスキーマのany撲滅.md)
11
+ // 「何でも受ける」型は存在しない:program / ai の in は接続元の具体型を鏡映する
12
+ // docs/tasks/wip/ポートスキーマのany撲滅.md)
11
13
  export const UNDECLARED_SCHEMA: JsonSchema = {}
12
14
 
13
15
  export const isUndeclaredSchema = (schema: JsonSchema): boolean =>
@@ -38,14 +40,12 @@ export const fileSchema = (): JsonSchema => ({
38
40
  export const isFileSchema = (schema: JsonSchema): boolean =>
39
41
  schema[FILE_SCHEMA_MARKER] === FILE_SCHEMA_TYPE
40
42
 
41
- // ---- 差し戻しの指示(AI の instruction ポート / human の reject ポート) ----
42
- // AI 入力の既存エンベロープ { prompt, files } と同じ形にする。配管が効く根拠は
43
- // files が file 型として宣言されていることであって、キー名ではない
44
- // (runner がマーカーで位置を見つけて materialize する)。
45
- // スクリーンショットを添えて差し戻すケースがあるので文章だけにしない
46
- // (docs/tasks/wip/ポートスキーマのany撲滅.md)
47
- export const AI_INSTRUCTION_PORT_KEY = "instruction"
48
-
43
+ // ---- 差し戻しの指示のデフォルト型 ----
44
+ // 雛形(エディタの既定ポート・ais/create)と承認ビューの既定宣言が参照する
45
+ // **デフォルトであって強制ではない**:ユーザーはポートの宣言を自由に変えられ、
46
+ // 接続の成立は通常の型検証だけが決める。配管が効く根拠は files file 型として
47
+ // 宣言されていることであって、キー名ではない(runner がマーカーで位置を見つけて
48
+ // materialize する)。スクリーンショットを添えて差し戻すケースがあるので文章だけにしない
49
49
  export const instructionSchema = (): JsonSchema => ({
50
50
  type: "object",
51
51
  properties: {
@@ -138,38 +138,6 @@ export const dataFormatOf = (schema: JsonSchema): DataFormat | null => {
138
138
  return { format: "list", columns }
139
139
  }
140
140
 
141
- // 封筒型 { data: T, description: string }。T は任意の JSON Schema、description は
142
- // AI によるデータの説明(AI が必ず生成する)。AI の全出口に強制する
143
- export const envelopeSchemaOf = (dataSchema: JsonSchema): JsonSchema => ({
144
- type: "object",
145
- properties: {
146
- data: dataSchema,
147
- description: { type: "string" },
148
- },
149
- required: ["data", "description"],
150
- })
151
-
152
- // 封筒型なら data のサブスキーマを返す(data キー必須の object。それ以外は null)
153
- export const envelopeDataSchemaOf = (schema: JsonSchema): JsonSchema | null => {
154
- if (schema.type !== "object") return null
155
- const properties = (schema.properties ?? {}) as Record<string, JsonSchema>
156
- const required = (schema.required ?? []) as string[]
157
- const data = properties.data
158
- if (!data || !required.includes("data")) return null
159
- return data
160
- }
161
-
162
- // AI の出口として妥当な封筒型か:data と description: string の両方が必須
163
- export const isAiEnvelopeSchema = (schema: JsonSchema): boolean => {
164
- if (envelopeDataSchemaOf(schema) === null) return false
165
- const properties = (schema.properties ?? {}) as Record<string, JsonSchema>
166
- const required = (schema.required ?? []) as string[]
167
- return (
168
- properties.description?.type === "string" &&
169
- required.includes("description")
170
- )
171
- }
172
-
173
141
  // ---- kind 別ポート構成 ----
174
142
  // human / wait は構成(数・key・io)が固定。program / ai は in 1つ+out 1つ以上
175
143
  // (key 自由。複数 out=分岐。AIノードの複数出口(分岐).md)
@@ -179,30 +147,28 @@ const FIXED_PORT_KEYS: Record<string, { in: string[]; out: string[] }> = {
179
147
  wait: { in: ["in"], out: ["main"] },
180
148
  // start は in 1つ(run 入力契約)+ out main 1つの恒等パススルー
181
149
  start: { in: ["in"], out: ["main"] },
182
- // component は in(子 loop の start.in 契約)+ out main(end.out の戻り値)。
183
- // スキーマは保存時にサーバが子 loop の契約から焼き込む宣言型
184
- // (DERIVED/MIRROR には入れない=接続で潰さない)
185
- component: { in: ["in"], out: ["main"] },
186
150
  }
187
151
 
188
- const FREE_OUT_KINDS = new Set(["program", "ai"])
152
+ // component in(子 loop の start.in 契約)1つ+ out は子 end の in ポートの射影
153
+ // (複数可。end の in が1つなら互換のため main 1口)。スキーマ・構成とも保存時に
154
+ // サーバが子 loop の契約から焼き込む宣言型(DERIVED/MIRROR には入れない=接続で潰さない。
155
+ // コンポーネントの複数入口・複数出口.md)
156
+ const FREE_OUT_KINDS = new Set(["program", "ai", "component"])
189
157
 
190
- // out は自由(複数出口=分岐)だが in の構成は固定。ai は上流データを受ける in と、
191
- // 差し戻しの指示を受ける instruction の2口(意味の違う入力を1つのポートに
192
- // fan-in させない。docs/tasks/wip/ポートスキーマのany撲滅.md)
158
+ // out は自由(複数出口=分岐)。program in の構成も固定(1口)だが、
159
+ // ai in も自由(追加・削除・key 自由。セッション再開は key ではなくポートの
160
+ // resume 属性で決まる。ポート型固定の撤去とセッション再開のポート属性化.md)
193
161
  const FIXED_IN_KEYS: Record<string, string[]> = {
194
162
  program: ["in"],
195
- ai: ["in", AI_INSTRUCTION_PORT_KEY],
163
+ component: ["in"],
196
164
  }
197
165
 
198
166
  export const fixedInKeysOf = (kind: string): string[] | null =>
199
167
  FIXED_IN_KEYS[kind] ?? null
200
168
 
201
169
  // config.json(manifest)の inputs 検査(適合なら null)。同じルールを二重に持たないため
202
- // manifest 側はここを参照する(docs/tasks/wip/manifestのin複数対応(instructionポート).md)。
203
- // **必須は "in" だけ**で、kind が固定する他の in(ai instruction)は任意:スキーマは
204
- // constSchemaOf が焼き込む固定型なのでエディタ側で補え、in を2口に分ける前に同期された
205
- // config.json もそのまま読める
170
+ // manifest 側はここを参照する。in の構成が固定の kind(program)だけが使う
171
+ // (ai inputs key 自由なので ai-manifest 側で件数と key の規則だけ見る)
206
172
  export const manifestInKeysViolation = (
207
173
  kind: string,
208
174
  keys: string[],
@@ -265,13 +231,12 @@ export const kindPortsViolation = (
265
231
 
266
232
  // ---- 型伝播(導出ポートの解決) ----
267
233
  // 接続の両端は同じスキーマになる(値入力フォームのSchemaForm統一.md)。
268
- // wait / human は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
269
- // 例外は human approve:接続元が AI ノードのときだけ封筒を剥がして data 型になる。
270
- // human の reject は未宣言のまま(段階2 で instruction エンベロープを宣言する)。
234
+ // wait は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
235
+ // human はビューの宣言を転写("T" は素通しと同じ挙動。reject もビューの宣言)。
271
236
  // 宣言 out(program / ai)は触らない。
272
- // program / ai の in は接続鏡映(sticky):接続元 out が具体型のときだけ上書きし、
273
- // 未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の config.json
274
- // へも書き戻される。docs/tasks/wip/ポート編集のrepo書き戻し同期.md)
237
+ // program / ai の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
238
+ // 上書きし、未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の
239
+ // config.json へも書き戻される。docs/tasks/wip/ポート編集のrepo書き戻し同期.md)
275
240
 
276
241
  // 複数の流入(fan-in)から型を1つに決める:未宣言を除いて具体型が1種類ならその型、
277
242
  // 0種類なら未宣言。2種類以上は型が割れているので未宣言に落とす
@@ -311,13 +276,19 @@ export type DeclaredPortSchema = JsonSchema | typeof COPY_UPSTREAM_SCHEMA
311
276
 
312
277
  // 伝播のオプション。sdk は承認ビューのカタログを知らない(非公開。企業向けビューの
313
278
  // キーが npm 公開物に載るのを避ける)ので、呼び出し元が解決して渡す
314
- // → docs/tasks/wip/Humanの入出力を承認ビューの宣言に一本化する.md
279
+ // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
315
280
  export type PropagationOptions<N> = {
316
- // human の in / approve に転写する型(承認ビューの宣言)。null / undefined なら
317
- // その node は対象外(human 以外)。上流ノードの kind は一切見ない
318
- viewPortSchemasOf?: (
319
- node: N,
320
- ) => { in: DeclaredPortSchema; out: DeclaredPortSchema } | null | undefined
281
+ // human の in / approve / reject に転写する型(承認ビューの宣言)。null / undefined
282
+ // ならその node は対象外(human 以外)。上流ノードの kind は一切見ない。
283
+ // reject は具体型のみ(上流からのコピー指示は意味を持たない)
284
+ viewPortSchemasOf?: (node: N) =>
285
+ | {
286
+ in: DeclaredPortSchema
287
+ approve: DeclaredPortSchema
288
+ reject: JsonSchema
289
+ }
290
+ | null
291
+ | undefined
321
292
  }
322
293
 
323
294
  // end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
@@ -331,32 +302,16 @@ const MIRROR_KINDS = new Set(["program", "ai"])
331
302
 
332
303
  const isDerivedPort = (kind: string, port: Pick<GraphPort, "io" | "key">) =>
333
304
  DERIVED_KINDS.has(kind) &&
334
- (port.io === "in" || port.key === "main" || port.key === "approve")
335
-
336
- // AI の in は nodes/ai/<dir>/config.json inputs.in 由来の宣言型が初期値で、
337
- // 接続時は program と同じ鏡映で接続元と同値に同期される(const 扱い廃止。
338
- // 接続バリデーションの「ai の in は常に許可」は維持)
339
- // 宣言型で固定されるポート(導出も鏡映もしない)。差し戻しの指示エンベロープを
340
- // human の出口と AI の入口の両方に焼き込むことで、両端が同じ具体型で一致する
341
- const constSchemaOf = (
342
- kind: string,
343
- port: Pick<GraphPort, "io" | "key">,
344
- ): JsonSchema | null => {
345
- if (kind === "human" && port.io === "out" && port.key === "reject") {
346
- return instructionSchema()
347
- }
348
- if (
349
- kind === "ai" &&
350
- port.io === "in" &&
351
- port.key === AI_INSTRUCTION_PORT_KEY
352
- ) {
353
- return instructionSchema()
354
- }
355
- return null
356
- }
305
+ (port.io === "in" ||
306
+ port.key === "main" ||
307
+ port.key === "approve" ||
308
+ port.key === "reject")
357
309
 
358
310
  // port_id → 解決済みスキーマ。導出は接続を遡って固定点まで反復し、
359
- // 未接続・純パラメトリック閉路・fan-in の型割れは未宣言に落とす
311
+ // 未接続・純パラメトリック閉路・fan-in の型割れは未宣言に落とす。
312
+ // ai の in は nodes/ai/<dir>/config.json の inputs[key].schema 由来の宣言型が初期値で、
313
+ // 接続時は program と同じ鏡映で接続元と同値に同期される(どの in も対等。
314
+ // instruction への型の焼き込みは廃止)
360
315
  export const resolvePortSchemas = <N extends PropagationNode>(
361
316
  nodes: N[],
362
317
  connections: PortLink[],
@@ -367,11 +322,9 @@ export const resolvePortSchemas = <N extends PropagationNode>(
367
322
  for (const node of nodes) {
368
323
  for (const port of node.ports) {
369
324
  kindByPortId.set(port.id, node.kind)
370
- const constSchema = constSchemaOf(node.kind, port)
371
325
  resolved.set(
372
326
  port.id,
373
- constSchema ??
374
- (isDerivedPort(node.kind, port) ? UNDECLARED_SCHEMA : port.schema),
327
+ isDerivedPort(node.kind, port) ? UNDECLARED_SCHEMA : port.schema,
375
328
  )
376
329
  }
377
330
  }
@@ -434,20 +387,29 @@ export const resolvePortSchemas = <N extends PropagationNode>(
434
387
  }
435
388
  const isMirror = MIRROR_KINDS.has(node.kind)
436
389
  if (!DERIVED_KINDS.has(node.kind) && !isMirror) continue
437
- // ai in が2口あるので、上流データの口(key="in")だけを鏡映・導出の対象にする
438
- const inPort = node.ports.find((p) => p.io === "in" && p.key === "in")
390
+ // 鏡映(program / ai):全ての in が対象。具体型が流れてきたときだけ上書きする
391
+ // (宣言を未宣言で潰さない)。ai in はどの口も対等(instruction の特別扱いは無い)
392
+ if (isMirror) {
393
+ for (const port of node.ports) {
394
+ if (port.io !== "in") continue
395
+ const sources = inbound.get(port.id) ?? []
396
+ const inSchema = mergeInboundSchemas(
397
+ sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
398
+ )
399
+ if (!isUndeclaredSchema(inSchema)) assign(port.id, inSchema)
400
+ }
401
+ continue
402
+ }
403
+ // 導出(wait / human):in は上流から
404
+ const inPort = node.ports.find((p) => p.io === "in")
439
405
  if (!inPort) continue
440
406
  const sources = inbound.get(inPort.id) ?? []
441
407
  const inSchema = mergeInboundSchemas(
442
408
  sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
443
409
  )
444
- // 鏡映(program / ai)は具体型が流れてきたときだけ上書き(宣言を未宣言で潰さない)
445
- if (isMirror) {
446
- if (!isUndeclaredSchema(inSchema)) assign(inPort.id, inSchema)
447
- continue
448
- }
449
410
  // 承認ビューの宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
450
- // "T" は「上流の out をコピー」(in)/「in と同型」(approve)。
411
+ // "T" は「上流の out をコピー」(in)/「in と同型」(approve)。reject は具体型の宣言
412
+ // のみで、宣言が無ければ未宣言のまま(mawaru が型を焼き込むことはしない)。
451
413
  // 上流の out は書き換えない:逆伝播すると fan-out 先のビューが食い違ったとき
452
414
  // 型割れが config.json へ write-through され、元の出力宣言を黙って壊す。
453
415
  // ズレは接続の型エラーで見せてユーザーの [修正する] で直す
@@ -458,14 +420,17 @@ export const resolvePortSchemas = <N extends PropagationNode>(
458
420
  : inSchema
459
421
  assign(inPort.id, effectiveIn)
460
422
  for (const port of node.ports) {
461
- if (port.io === "out" && isDerivedPort(node.kind, port)) {
462
- assign(
463
- port.id,
464
- declared && declared.out !== COPY_UPSTREAM_SCHEMA
465
- ? declared.out
466
- : effectiveIn,
467
- )
423
+ if (port.io !== "out" || !isDerivedPort(node.kind, port)) continue
424
+ if (node.kind === "human" && port.key === "reject") {
425
+ if (declared) assign(port.id, declared.reject)
426
+ continue
468
427
  }
428
+ assign(
429
+ port.id,
430
+ declared && declared.approve !== COPY_UPSTREAM_SCHEMA
431
+ ? declared.approve
432
+ : effectiveIn,
433
+ )
469
434
  }
470
435
  }
471
436
  if (!changed) break
@@ -8,6 +8,11 @@ export declare const aiModelSchema: z.ZodEnum<{
8
8
  }>;
9
9
  export type AiModel = z.infer<typeof aiModelSchema>;
10
10
  export declare const DEFAULT_AI_MODEL = "claude-opus-5";
11
+ export declare const aiInputDeclSchema: z.ZodObject<{
12
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
13
+ resume: z.ZodOptional<z.ZodBoolean>;
14
+ }, z.core.$strip>;
15
+ export type AiInputDecl = z.infer<typeof aiInputDeclSchema>;
11
16
  export declare const aiManifestSchema: z.ZodObject<{
12
17
  name: z.ZodOptional<z.ZodString>;
13
18
  description: z.ZodOptional<z.ZodString>;
@@ -21,7 +26,13 @@ export declare const aiManifestSchema: z.ZodObject<{
21
26
  skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
22
27
  env: z.ZodDefault<z.ZodArray<z.ZodString>>;
23
28
  verbose: z.ZodOptional<z.ZodBoolean>;
24
- inputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
29
+ inputs: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
30
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
31
+ resume: z.ZodOptional<z.ZodBoolean>;
32
+ }, z.core.$strip>, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<{
33
+ schema: Record<string, unknown>;
34
+ resume?: boolean | undefined;
35
+ }, Record<string, unknown>>>]>>;
25
36
  outputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
26
37
  }, z.core.$strip>;
27
38
  export type AiManifest = z.infer<typeof aiManifestSchema>;
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
- import { jsonSchemaSchema } from "./node.js";
3
- import { isAiEnvelopeSchema, manifestInKeysViolation } from "./port-spec.js";
2
+ import { jsonSchemaSchema, PORT_KEY_PATTERN } from "./node.js";
4
3
  // AI ノードのモデル選択肢(既定は Opus:下書き品質が価値の中心。費用はユーザー持ち)。
5
4
  // エイリアス("opus" 等)は採らずバージョンを固定する:実行のたびに黙って別モデルへ
6
5
  // 乗り換わると品質も費用も再現しないため。新モデルが出たらこの配列を足して SDK を publish する
@@ -12,6 +11,20 @@ export const AI_MODELS = [
12
11
  ];
13
12
  export const aiModelSchema = z.enum(AI_MODELS);
14
13
  export const DEFAULT_AI_MODEL = "claude-opus-5";
14
+ // 入力口の宣言:スキーマ+実行属性。resume: true の口から入力が届いた実行は
15
+ // 同じノードの直近セッションを復元して再開する(backend が ports.resume へ同期して
16
+ // payload 発行時に参照する。ポート型固定の撤去とセッション再開のポート属性化.md)
17
+ export const aiInputDeclSchema = z.object({
18
+ schema: jsonSchemaSchema,
19
+ resume: z.boolean().optional(),
20
+ });
21
+ // 旧形式(値が素の JSON Schema)は { schema: 値 } として読む。判別は「schema キーを
22
+ // 持つか」:JSON Schema のトップレベルに "schema" キーは現れない(メタスキーマ参照は
23
+ // "$schema")ので実用上一意。書き戻し(backend の syncNodeConfigs)は常に新形式で書く
24
+ const aiInputValueSchema = z.union([
25
+ aiInputDeclSchema,
26
+ jsonSchemaSchema.transform((schema) => ({ schema })),
27
+ ]);
15
28
  // nodes/ai/<dir>/config.json(実行repoの構造見直し)。AI ノードの定義(prompt/model/skills/
16
29
  // 入出力スキーマ/env)は repo 側が正で、エディタが取り込んで ports を自動生成する
17
30
  // (実行時の正は ports テーブルのまま。プログラム規約 v2 と同じ関係)。
@@ -34,34 +47,36 @@ export const aiManifestSchema = z
34
47
  // 実行中の経過(発話・ツール呼び出し)を Actions ログへ逐次出すデバッグフラグ。
35
48
  // 通常時は config.json にキーを書かない(インスペクタも false ならキーを消す)
36
49
  verbose: z.boolean().optional(),
37
- inputs: z.record(z.string(), jsonSchemaSchema),
50
+ inputs: z.record(z.string(), aiInputValueSchema),
38
51
  outputs: z.record(z.string(), jsonSchemaSchema),
39
52
  })
40
53
  .superRefine((manifest, ctx) => {
41
- // in の構成は kind が固定する(ai は上流データの in 差し戻しの instruction の2口)。
42
- // ルールの正は port-spec 側で、ここは参照するだけ
43
- const inViolation = manifestInKeysViolation("ai", Object.keys(manifest.inputs));
44
- if (inViolation) {
45
- ctx.addIssue({ code: "custom", path: ["inputs"], message: inViolation });
46
- }
47
- // outputs は1件以上・key 自由(複数出口=分岐。AIノードの複数出口(分岐).md)
48
- if (Object.keys(manifest.outputs).length < 1) {
54
+ // inputs は1件以上・key 自由(in / instruction の固定構成は廃止。
55
+ // セッション再開は key ではなく resume 属性で決まる)
56
+ const inKeys = Object.keys(manifest.inputs);
57
+ if (inKeys.length < 1) {
49
58
  ctx.addIssue({
50
59
  code: "custom",
51
- path: ["outputs"],
52
- message: "outputs は1件以上宣言してください",
60
+ path: ["inputs"],
61
+ message: "inputs は1件以上宣言してください",
53
62
  });
54
63
  }
55
- // Human 承認 UI が依存する封筒型 { data: T, description: string } を全出口に強制
56
- // (どの出口も human に接続できる、という AI ノードの不変条件。saveGraph と同じ判定。
57
- // data=T は任意の JSON Schema)
58
- for (const [key, schema] of Object.entries(manifest.outputs)) {
59
- if (!isAiEnvelopeSchema(schema)) {
64
+ for (const key of inKeys) {
65
+ if (!PORT_KEY_PATTERN.test(key)) {
60
66
  ctx.addIssue({
61
67
  code: "custom",
62
- path: ["outputs", key],
63
- message: `outputs.${key} は封筒型 { data, description } で宣言してください(両方必須・description は string)`,
68
+ path: ["inputs", key],
69
+ message: `inputs の key が不正です: ${key}(英小文字始まり・英数字と _ のみ)`,
64
70
  });
65
71
  }
66
72
  }
73
+ // outputs は1件以上・key 自由(複数出口=分岐。AIノードの複数出口(分岐).md)。
74
+ // スキーマは任意の JSON Schema(形の強制はしない)
75
+ if (Object.keys(manifest.outputs).length < 1) {
76
+ ctx.addIssue({
77
+ code: "custom",
78
+ path: ["outputs"],
79
+ message: "outputs は1件以上宣言してください",
80
+ });
81
+ }
67
82
  });
@@ -24,11 +24,27 @@ export const duplicateSelection = (params) => {
24
24
  position_y: n.position_y + dy,
25
25
  }));
26
26
  // 両端が複製対象のポートである線(=選択内で完結する線)だけを張り替えて写す。
27
- // 外部との線(入口・出口)はコピーしない
27
+ // 外部との線(入口・出口)はコピーしない。手動ルート(waypoints)は
28
+ // ノードと同じ移動量で写し、調整済みの形を保つ
28
29
  const copiedConnections = connections.flatMap((c) => {
29
30
  const from = portIdMap.get(c.from_port_id);
30
31
  const to = portIdMap.get(c.to_port_id);
31
- return from && to ? [{ from_port_id: from, to_port_id: to }] : [];
32
+ return from && to
33
+ ? [
34
+ {
35
+ from_port_id: from,
36
+ to_port_id: to,
37
+ ...(c.waypoints
38
+ ? {
39
+ waypoints: c.waypoints.map((p) => ({
40
+ x: p.x + dx,
41
+ y: p.y + dy,
42
+ })),
43
+ }
44
+ : {}),
45
+ },
46
+ ]
47
+ : [];
32
48
  });
33
49
  return { nodes: copied, connections: copiedConnections };
34
50
  };
@@ -4,8 +4,8 @@ export type ExtractAnalysis = {
4
4
  reason: string;
5
5
  } | {
6
6
  ok: true;
7
- entryNodeId: string | null;
8
- exitPortId: string | null;
7
+ entryPortIds: string[];
8
+ exitPortIds: string[];
9
9
  internal: GraphConnection[];
10
10
  inbound: GraphConnection[];
11
11
  outbound: GraphConnection[];
@@ -14,6 +14,7 @@ export declare const analyzeExtractSelection: (nodes: GraphNode[], connections:
14
14
  export type ExtractPlan = {
15
15
  childNodes: GraphNode[];
16
16
  childConnections: GraphConnection[];
17
+ childEnd: GraphNode;
17
18
  groupNode: GraphNode;
18
19
  parentNodes: GraphNode[];
19
20
  parentConnections: GraphConnection[];
@@ -25,7 +26,7 @@ export declare const buildExtractPlan: (params: {
25
26
  name: string;
26
27
  componentLoopId: string;
27
28
  childStartOutPortId: string;
28
- childEndInPortId: string;
29
+ childEnd: GraphNode;
29
30
  newId: () => string;
30
31
  }) => {
31
32
  ok: false;