@mawaru/sdk 0.38.0 → 0.39.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.
@@ -145,6 +145,14 @@ describe("aiManifestSchema", () => {
145
145
  expect(parsed.inputs.instruction?.resume).toBe(true)
146
146
  })
147
147
 
148
+ it("inputs の flexible(推奨型を維持して任意の型を受ける)が読める", () => {
149
+ const parsed = aiManifestSchema.parse({
150
+ ...valid,
151
+ inputs: { in: { schema: { type: "object" }, flexible: true } },
152
+ })
153
+ expect(parsed.inputs.in?.flexible).toBe(true)
154
+ })
155
+
148
156
  it("旧形式(値が素の JSON Schema)は { schema: 値 } として読める", () => {
149
157
  const legacy = aiManifestSchema.parse({
150
158
  ...valid,
@@ -72,6 +72,8 @@ export const agentOfModel = (model: AiModel): AiAgentId =>
72
72
  export const aiInputDeclSchema = z.object({
73
73
  schema: jsonSchemaSchema,
74
74
  resume: z.boolean().optional(),
75
+ // 推奨型を維持して任意の型を受ける(接続で上書きされず、適合チェックもしない)
76
+ flexible: z.boolean().optional(),
75
77
  })
76
78
  export type AiInputDecl = z.infer<typeof aiInputDeclSchema>
77
79
 
@@ -1203,6 +1203,42 @@ describe("saveGraphBodySchema:コンポーネントノード(component kind
1203
1203
  ).not.toThrow()
1204
1204
  })
1205
1205
 
1206
+ it("flexible な in(契約で焼き込まれた component.in・AI の宣言 in)への接続は型が違っても保存できる", () => {
1207
+ const prog = {
1208
+ ...makeNode([
1209
+ port("in", "in"),
1210
+ { ...port("out", "main"), schema: { type: "string" } },
1211
+ ]),
1212
+ kind: "program" as const,
1213
+ }
1214
+ const comp = {
1215
+ ...componentNode({ type: "number" }),
1216
+ }
1217
+ comp.ports = comp.ports.map((p) =>
1218
+ p.io === "in" ? { ...p, flexible: true } : p,
1219
+ )
1220
+ expect(() =>
1221
+ saveGraphBodySchema.parse({
1222
+ nodes: [prog, comp],
1223
+ connections: [connect(prog, comp)],
1224
+ }),
1225
+ ).not.toThrow()
1226
+ // AI の flexible な in:鏡映されず推奨型 {type:"number"} が残るが、型が違う接続を通す
1227
+ const ai = {
1228
+ ...makeNode([
1229
+ { ...port("in", "in"), schema: { type: "number" }, flexible: true },
1230
+ port("out", "main"),
1231
+ ]),
1232
+ kind: "ai" as const,
1233
+ }
1234
+ expect(() =>
1235
+ saveGraphBodySchema.parse({
1236
+ nodes: [prog, ai],
1237
+ connections: [connect(prog, ai)],
1238
+ }),
1239
+ ).not.toThrow()
1240
+ })
1241
+
1206
1242
  it("component の in への接続は契約への適合を検証する(伝播型 ⊆ 契約)", () => {
1207
1243
  // 上流 program の out {type:"string"} → component の in {type:"number"} は不適合
1208
1244
  const prog = {
package/_schemas/graph.ts CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  portKeySchema,
7
7
  } from "./node.js"
8
8
  import {
9
+ connectionAccepted,
9
10
  hasInboundConflict,
10
- isSchemaSubset,
11
11
  kindPortsViolation,
12
12
  resolvePortSchemas,
13
13
  UNDECLARED_SCHEMA,
@@ -551,9 +551,10 @@ export const saveGraphBodySchema = z
551
551
  // 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
552
552
  // ので自明に通り、実質の検証対象は宣言で固定された口
553
553
  // (component の契約・宣言のあるビューの human.in)になる
554
+ // flexible な in(AI の宣言 in・契約で焼き込まれた component の in)は型を問わない
554
555
  const fromSchema = resolved.get(conn.from_port_id) ?? from.schema
555
556
  const toSchema = resolved.get(conn.to_port_id) ?? to.schema
556
- if (!isSchemaSubset(fromSchema, toSchema)) {
557
+ if (!connectionAccepted(fromSchema, { ...to, schema: toSchema })) {
557
558
  ctx.addIssue({
558
559
  code: "custom",
559
560
  path: ["connections", i, "to_port_id"],
package/_schemas/node.ts CHANGED
@@ -54,6 +54,10 @@ export const graphPortSchema = z
54
54
  // 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
55
55
  // ポート型固定の撤去とセッション再開のポート属性化.md)
56
56
  resume: z.boolean().optional(),
57
+ // in ポートの型属性:true なら宣言(推奨型)を維持し、接続元の型による上書き・
58
+ // 適合チェックをしない(AI の in で設定。start / component へは導出で流れる。
59
+ // AIノード入力のflexible属性(任意の型で呼べるループ).md)
60
+ flexible: z.boolean().optional(),
57
61
  side: portSideSchema.optional(),
58
62
  offset: z.number().min(0).max(1).optional(),
59
63
  })
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"
2
2
  import type { GraphPort, JsonSchema } from "./node.js"
3
3
  import {
4
4
  arrayOf,
5
+ connectionAccepted,
5
6
  dataFormatOf,
6
7
  dataSchemaOf,
7
8
  fileSchema,
@@ -12,6 +13,7 @@ import {
12
13
  kindPortsViolation,
13
14
  mergeInboundSchemas,
14
15
  propagateDerivedPorts,
16
+ resolveFlexiblePorts,
15
17
  resolvePortSchemas,
16
18
  schemaFit,
17
19
  schemaTypeKey,
@@ -661,6 +663,128 @@ describe("型伝播(導出ポートの解決)", () => {
661
663
  expect(resolved.get(start.outMain.id)).toEqual(UNDECLARED_SCHEMA)
662
664
  })
663
665
 
666
+ // flexible:AI の in の型属性。宣言(推奨型)を維持し、接続の型を問わない。
667
+ // start へは型と同じ経路で導出される(AIノード入力のflexible属性(任意の型で呼べるループ).md)
668
+ describe("flexible(推奨型を維持して任意の型を受ける in)", () => {
669
+ const flexibleAi = (declared: JsonSchema) => {
670
+ const inPort: GraphPort = {
671
+ ...port("in", "in", declared),
672
+ flexible: true,
673
+ }
674
+ return {
675
+ node: { id: uuid(), kind: "ai", ports: [inPort, port("out", "main")] },
676
+ inPort,
677
+ }
678
+ }
679
+ const recommended = {
680
+ type: "object",
681
+ properties: { prompt: { type: "string" } },
682
+ required: ["prompt"],
683
+ }
684
+
685
+ it("flexible な in は接続元の具体型で上書きしない(宣言=推奨型を維持)", () => {
686
+ const program = programNode({ type: "string" })
687
+ const progOut = program.node.ports.find(
688
+ (p) => p.io === "out",
689
+ ) as GraphPort
690
+ progOut.schema = declaredOut
691
+ const ai = flexibleAi(recommended)
692
+ const resolved = resolvePortSchemas(
693
+ [program.node, ai.node],
694
+ [{ from_port_id: progOut.id, to_port_id: ai.inPort.id }],
695
+ )
696
+ expect(resolved.get(ai.inPort.id)).toEqual(recommended)
697
+ })
698
+
699
+ it("flexible な in は型の割れた fan-in でも推奨型を維持する", () => {
700
+ const a = programNode({ type: "string" })
701
+ const aOut = a.node.ports.find((p) => p.io === "out") as GraphPort
702
+ aOut.schema = { type: "string" }
703
+ const b = programNode({ type: "string" })
704
+ const bOut = b.node.ports.find((p) => p.io === "out") as GraphPort
705
+ bOut.schema = { type: "number" }
706
+ const ai = flexibleAi(recommended)
707
+ const resolved = resolvePortSchemas(
708
+ [a.node, b.node, ai.node],
709
+ [
710
+ { from_port_id: aOut.id, to_port_id: ai.inPort.id },
711
+ { from_port_id: bOut.id, to_port_id: ai.inPort.id },
712
+ ],
713
+ )
714
+ expect(resolved.get(ai.inPort.id)).toEqual(recommended)
715
+ })
716
+
717
+ it("start → AI(flexible) 直結:start の in / out main が推奨型で flexible になる", () => {
718
+ const start = startNode()
719
+ const ai = flexibleAi(recommended)
720
+ const connections = [
721
+ { from_port_id: start.outMain.id, to_port_id: ai.inPort.id },
722
+ ]
723
+ expect(resolveFlexiblePorts([start.node, ai.node], connections)).toEqual(
724
+ new Set([start.inPort.id, start.outMain.id]),
725
+ )
726
+ const propagated = propagateDerivedPorts(
727
+ [start.node, ai.node],
728
+ connections,
729
+ )[0]
730
+ if (!propagated) throw new Error("start not propagated")
731
+ for (const p of propagated.ports) {
732
+ expect(p.schema).toEqual(recommended)
733
+ expect(p.flexible).toBe(true)
734
+ }
735
+ })
736
+
737
+ it("start → [AI(flexible), program]:非 flexible の接続先が混ざれば start は flexible にならない", () => {
738
+ const start = startNode()
739
+ const ai = flexibleAi(recommended)
740
+ const program = programNode(recommended)
741
+ const connections = [
742
+ { from_port_id: start.outMain.id, to_port_id: ai.inPort.id },
743
+ { from_port_id: start.outMain.id, to_port_id: program.inPort.id },
744
+ ]
745
+ expect(
746
+ resolveFlexiblePorts([start.node, ai.node, program.node], connections),
747
+ ).toEqual(new Set())
748
+ })
749
+
750
+ it("start:未接続なら flexible にならず、ports に残った古い flexible は落とす", () => {
751
+ const inPort: GraphPort = { ...port("in", "in"), flexible: true }
752
+ const outMain: GraphPort = { ...port("out", "main"), flexible: true }
753
+ const start = { id: uuid(), kind: "start", ports: [inPort, outMain] }
754
+ const propagated = propagateDerivedPorts([start], [])[0]
755
+ if (!propagated) throw new Error("start not propagated")
756
+ for (const p of propagated.ports) expect(p.flexible).toBeUndefined()
757
+ })
758
+
759
+ it("component の in(契約の焼き込みで flexible)に直結した start も flexible(推移)", () => {
760
+ const start = startNode()
761
+ const compIn: GraphPort = {
762
+ ...port("in", "in", recommended),
763
+ flexible: true,
764
+ }
765
+ const component = {
766
+ id: uuid(),
767
+ kind: "component",
768
+ ports: [compIn, port("out", "main")],
769
+ }
770
+ expect(
771
+ resolveFlexiblePorts(
772
+ [start.node, component],
773
+ [{ from_port_id: start.outMain.id, to_port_id: compIn.id }],
774
+ ),
775
+ ).toEqual(new Set([start.inPort.id, start.outMain.id]))
776
+ })
777
+
778
+ it("connectionAccepted:受け側が flexible なら型を問わず可、そうでなければ isSchemaSubset", () => {
779
+ const to = port("in", "in", recommended)
780
+ expect(connectionAccepted({ type: "string" }, to)).toBe(false)
781
+ expect(
782
+ connectionAccepted({ type: "string" }, { ...to, flexible: true }),
783
+ ).toBe(true)
784
+ expect(connectionAccepted(recommended, to)).toBe(true)
785
+ })
786
+ })
787
+
664
788
  it("propagateDerivedPorts:schema を書き戻し、変化のないノードは同じ参照を返す", () => {
665
789
  const progOut = port("out", "main", declaredOut)
666
790
  const program = {
@@ -487,6 +487,8 @@ export const resolvePortSchemas = <N extends PropagationNode>(
487
487
  if (isMirror) {
488
488
  for (const port of node.ports) {
489
489
  if (port.io !== "in") continue
490
+ // flexible な in は宣言(推奨型)を維持する(接続の型を問わない口)
491
+ if (port.flexible === true) continue
490
492
  const sources = inbound.get(port.id) ?? []
491
493
  const inSchema = mergeInboundSchemas(
492
494
  sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
@@ -535,19 +537,82 @@ export const resolvePortSchemas = <N extends PropagationNode>(
535
537
 
536
538
  // ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
537
539
  // schema が変わらないポート・ノードは同じ参照を返す
540
+ // flexible の導出:start の in / out main は、接続先の in が1つ以上あり**全て** flexible
541
+ // (AI の宣言 in・契約で焼き込まれた component の in)のときだけ flexible。
542
+ // 非 flexible の接続先(program 等)が1つでも混ざれば型どおりの入力が要るので false。
543
+ // 直結のみ(wait 等の素通し経由は型と同じく流さない)。戻りは flexible になる port_id の集合
544
+ export const resolveFlexiblePorts = (
545
+ nodes: PropagationNode[],
546
+ connections: PortLink[],
547
+ ): Set<string> => {
548
+ const flexibleByPortId = new Map<string, boolean>()
549
+ for (const node of nodes) {
550
+ if (node.kind === "start") continue
551
+ for (const port of node.ports) {
552
+ if (port.io === "in")
553
+ flexibleByPortId.set(port.id, port.flexible === true)
554
+ }
555
+ }
556
+ const outbound = new Map<string, string[]>()
557
+ for (const conn of connections) {
558
+ const outs = outbound.get(conn.from_port_id) ?? []
559
+ outs.push(conn.to_port_id)
560
+ outbound.set(conn.from_port_id, outs)
561
+ }
562
+ const flexible = new Set<string>()
563
+ for (const node of nodes) {
564
+ if (node.kind !== "start") continue
565
+ const inPort = node.ports.find((p) => p.io === "in")
566
+ const outMain = node.ports.find((p) => p.io === "out" && p.key === "main")
567
+ if (!inPort || !outMain) continue
568
+ const targets = outbound.get(outMain.id) ?? []
569
+ if (
570
+ targets.length > 0 &&
571
+ targets.every((id) => flexibleByPortId.get(id) === true)
572
+ ) {
573
+ flexible.add(inPort.id)
574
+ flexible.add(outMain.id)
575
+ }
576
+ }
577
+ return flexible
578
+ }
579
+
580
+ // 接続の型が受け入れられるか:受け側が flexible なら常に可、それ以外は 伝播型 ⊆ 宣言。
581
+ // sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
582
+ // (判定が散ると表示と保存がズレる)
583
+ export const connectionAccepted = (
584
+ from: JsonSchema,
585
+ to: Pick<GraphPort, "schema" | "flexible">,
586
+ ): boolean => to.flexible === true || isSchemaSubset(from, to.schema)
587
+
538
588
  export const propagateDerivedPorts = <N extends PropagationNode>(
539
589
  nodes: N[],
540
590
  connections: PortLink[],
541
591
  options?: PropagationOptions<N>,
542
592
  ): N[] => {
543
593
  const resolved = resolvePortSchemas(nodes, connections, options)
594
+ const flexiblePorts = resolveFlexiblePorts(nodes, connections)
544
595
  return nodes.map((node) => {
545
596
  let touched = false
546
597
  const ports = node.ports.map((port) => {
547
598
  const schema = resolved.get(port.id) ?? port.schema
548
- if (JSON.stringify(schema) === JSON.stringify(port.schema)) return port
599
+ // start の flexible は導出結果で置き換える(false は省略=キー無し)
600
+ const flexible =
601
+ node.kind === "start"
602
+ ? flexiblePorts.has(port.id) || undefined
603
+ : port.flexible
604
+ if (
605
+ JSON.stringify(schema) === JSON.stringify(port.schema) &&
606
+ flexible === port.flexible
607
+ )
608
+ return port
549
609
  touched = true
550
- return { ...port, schema }
610
+ const { flexible: _dropped, ...rest } = port
611
+ return {
612
+ ...rest,
613
+ schema,
614
+ ...(flexible ? { flexible } : {}),
615
+ } as typeof port
551
616
  })
552
617
  return touched ? { ...node, ports } : node
553
618
  })
@@ -49,6 +49,7 @@ export declare const agentOfModel: (model: AiModel) => AiAgentId;
49
49
  export declare const aiInputDeclSchema: z.ZodObject<{
50
50
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
51
51
  resume: z.ZodOptional<z.ZodBoolean>;
52
+ flexible: z.ZodOptional<z.ZodBoolean>;
52
53
  }, z.core.$strip>;
53
54
  export type AiInputDecl = z.infer<typeof aiInputDeclSchema>;
54
55
  export declare const aiManifestSchema: z.ZodObject<{
@@ -87,9 +88,11 @@ export declare const aiManifestSchema: z.ZodObject<{
87
88
  inputs: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
88
89
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
89
90
  resume: z.ZodOptional<z.ZodBoolean>;
91
+ flexible: z.ZodOptional<z.ZodBoolean>;
90
92
  }, z.core.$strip>, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<{
91
93
  schema: Record<string, unknown>;
92
94
  resume?: boolean | undefined;
95
+ flexible?: boolean | undefined;
93
96
  }, Record<string, unknown>>>]>>;
94
97
  outputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
95
98
  }, z.core.$strip>;
@@ -65,6 +65,8 @@ export const agentOfModel = (model) => model.startsWith("claude-") ? "claude" :
65
65
  export const aiInputDeclSchema = z.object({
66
66
  schema: jsonSchemaSchema,
67
67
  resume: z.boolean().optional(),
68
+ // 推奨型を維持して任意の型を受ける(接続で上書きされず、適合チェックもしない)
69
+ flexible: z.boolean().optional(),
68
70
  });
69
71
  // 旧形式(値が素の JSON Schema)は { schema: 値 } として読む。判別は「schema キーを
70
72
  // 持つか」:JSON Schema のトップレベルに "schema" キーは現れない(メタスキーマ参照は
@@ -152,6 +152,7 @@ export declare const graphNodeSchema: z.ZodObject<{
152
152
  name: z.ZodString;
153
153
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
154
154
  resume: z.ZodOptional<z.ZodBoolean>;
155
+ flexible: z.ZodOptional<z.ZodBoolean>;
155
156
  side: z.ZodOptional<z.ZodEnum<{
156
157
  top: "top";
157
158
  right: "right";
@@ -252,6 +253,7 @@ export declare const saveGraphBodySchema: z.ZodObject<{
252
253
  name: z.ZodString;
253
254
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
254
255
  resume: z.ZodOptional<z.ZodBoolean>;
256
+ flexible: z.ZodOptional<z.ZodBoolean>;
255
257
  side: z.ZodOptional<z.ZodEnum<{
256
258
  top: "top";
257
259
  right: "right";
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { graphPortSchema, jsonSchemaSchema, nodeKindSchema, portKeySchema, } from "./node.js";
3
- import { hasInboundConflict, isSchemaSubset, kindPortsViolation, resolvePortSchemas, UNDECLARED_SCHEMA, } from "./port-spec.js";
3
+ import { connectionAccepted, hasInboundConflict, kindPortsViolation, resolvePortSchemas, UNDECLARED_SCHEMA, } from "./port-spec.js";
4
4
  // refs=ループ変数の袋(key→任意の JSON 値。上流出力の参照(refs).md)。
5
5
  // run 開始時に定義から複製され、全 step に全量差し込まれる。
6
6
  // key はポート key と同じ規則(^[a-z][a-z0-9_]*$)。実行系ボディ
@@ -473,9 +473,10 @@ export const saveGraphBodySchema = z
473
473
  // 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
474
474
  // ので自明に通り、実質の検証対象は宣言で固定された口
475
475
  // (component の契約・宣言のあるビューの human.in)になる
476
+ // flexible な in(AI の宣言 in・契約で焼き込まれた component の in)は型を問わない
476
477
  const fromSchema = resolved.get(conn.from_port_id) ?? from.schema;
477
478
  const toSchema = resolved.get(conn.to_port_id) ?? to.schema;
478
- if (!isSchemaSubset(fromSchema, toSchema)) {
479
+ if (!connectionAccepted(fromSchema, { ...to, schema: toSchema })) {
479
480
  ctx.addIssue({
480
481
  code: "custom",
481
482
  path: ["connections", i, "to_port_id"],
@@ -39,6 +39,7 @@ export declare const graphPortSchema: z.ZodObject<{
39
39
  name: z.ZodString;
40
40
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
41
41
  resume: z.ZodOptional<z.ZodBoolean>;
42
+ flexible: z.ZodOptional<z.ZodBoolean>;
42
43
  side: z.ZodOptional<z.ZodEnum<{
43
44
  top: "top";
44
45
  right: "right";
@@ -43,6 +43,10 @@ export const graphPortSchema = z
43
43
  // 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
44
44
  // ポート型固定の撤去とセッション再開のポート属性化.md)
45
45
  resume: z.boolean().optional(),
46
+ // in ポートの型属性:true なら宣言(推奨型)を維持し、接続元の型による上書き・
47
+ // 適合チェックをしない(AI の in で設定。start / component へは導出で流れる。
48
+ // AIノード入力のflexible属性(任意の型で呼べるループ).md)
49
+ flexible: z.boolean().optional(),
46
50
  side: portSideSchema.optional(),
47
51
  offset: z.number().min(0).max(1).optional(),
48
52
  })
@@ -70,6 +70,8 @@ export type PropagationOptions<N> = {
70
70
  } | null | undefined;
71
71
  };
72
72
  export declare const resolvePortSchemas: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => Map<string, JsonSchema>;
73
+ export declare const resolveFlexiblePorts: (nodes: PropagationNode[], connections: PortLink[]) => Set<string>;
74
+ export declare const connectionAccepted: (from: JsonSchema, to: Pick<GraphPort, "schema" | "flexible">) => boolean;
73
75
  export declare const propagateDerivedPorts: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => N[];
74
76
  export type SchemaFit = "ok" | "mismatch" | "unknown";
75
77
  export declare const schemaFit: (sub: JsonSchema, sup: JsonSchema) => SchemaFit;
@@ -382,6 +382,9 @@ export const resolvePortSchemas = (nodes, connections, options) => {
382
382
  for (const port of node.ports) {
383
383
  if (port.io !== "in")
384
384
  continue;
385
+ // flexible な in は宣言(推奨型)を維持する(接続の型を問わない口)
386
+ if (port.flexible === true)
387
+ continue;
385
388
  const sources = inbound.get(port.id) ?? [];
386
389
  const inSchema = mergeInboundSchemas(sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA));
387
390
  if (!isUndeclaredSchema(inSchema))
@@ -426,16 +429,68 @@ export const resolvePortSchemas = (nodes, connections, options) => {
426
429
  };
427
430
  // ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
428
431
  // schema が変わらないポート・ノードは同じ参照を返す
432
+ // flexible の導出:start の in / out main は、接続先の in が1つ以上あり**全て** flexible
433
+ // (AI の宣言 in・契約で焼き込まれた component の in)のときだけ flexible。
434
+ // 非 flexible の接続先(program 等)が1つでも混ざれば型どおりの入力が要るので false。
435
+ // 直結のみ(wait 等の素通し経由は型と同じく流さない)。戻りは flexible になる port_id の集合
436
+ export const resolveFlexiblePorts = (nodes, connections) => {
437
+ const flexibleByPortId = new Map();
438
+ for (const node of nodes) {
439
+ if (node.kind === "start")
440
+ continue;
441
+ for (const port of node.ports) {
442
+ if (port.io === "in")
443
+ flexibleByPortId.set(port.id, port.flexible === true);
444
+ }
445
+ }
446
+ const outbound = new Map();
447
+ for (const conn of connections) {
448
+ const outs = outbound.get(conn.from_port_id) ?? [];
449
+ outs.push(conn.to_port_id);
450
+ outbound.set(conn.from_port_id, outs);
451
+ }
452
+ const flexible = new Set();
453
+ for (const node of nodes) {
454
+ if (node.kind !== "start")
455
+ continue;
456
+ const inPort = node.ports.find((p) => p.io === "in");
457
+ const outMain = node.ports.find((p) => p.io === "out" && p.key === "main");
458
+ if (!inPort || !outMain)
459
+ continue;
460
+ const targets = outbound.get(outMain.id) ?? [];
461
+ if (targets.length > 0 &&
462
+ targets.every((id) => flexibleByPortId.get(id) === true)) {
463
+ flexible.add(inPort.id);
464
+ flexible.add(outMain.id);
465
+ }
466
+ }
467
+ return flexible;
468
+ };
469
+ // 接続の型が受け入れられるか:受け側が flexible なら常に可、それ以外は 伝播型 ⊆ 宣言。
470
+ // sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
471
+ // (判定が散ると表示と保存がズレる)
472
+ export const connectionAccepted = (from, to) => to.flexible === true || isSchemaSubset(from, to.schema);
429
473
  export const propagateDerivedPorts = (nodes, connections, options) => {
430
474
  const resolved = resolvePortSchemas(nodes, connections, options);
475
+ const flexiblePorts = resolveFlexiblePorts(nodes, connections);
431
476
  return nodes.map((node) => {
432
477
  let touched = false;
433
478
  const ports = node.ports.map((port) => {
434
479
  const schema = resolved.get(port.id) ?? port.schema;
435
- if (JSON.stringify(schema) === JSON.stringify(port.schema))
480
+ // start の flexible は導出結果で置き換える(false は省略=キー無し)
481
+ const flexible = node.kind === "start"
482
+ ? flexiblePorts.has(port.id) || undefined
483
+ : port.flexible;
484
+ if (JSON.stringify(schema) === JSON.stringify(port.schema) &&
485
+ flexible === port.flexible)
436
486
  return port;
437
487
  touched = true;
438
- return { ...port, schema };
488
+ const { flexible: _dropped, ...rest } = port;
489
+ return {
490
+ ...rest,
491
+ schema,
492
+ ...(flexible ? { flexible } : {}),
493
+ };
439
494
  });
440
495
  return touched ? { ...node, ports } : node;
441
496
  });
@@ -56,10 +56,12 @@ views/
56
56
  走り続ける)。省略時は `claude-opus`。認証は repo の Actions secrets に置く:Claude Code は
57
57
  `ANTHROPIC_API_KEY`(または `CLAUDE_CODE_OAUTH_TOKEN`)、Codex は明示的な `auth_mode` と対応する認証情報(下記「Codex の認証方式」参照)
58
58
  (`env` に宣言しなくても渡される)
59
- - `inputs` は1件以上・key 自由。値は `{ "schema": <JSON Schema>, "resume": true }` の形
59
+ - `inputs` は1件以上・key 自由。値は `{ "schema": <JSON Schema>, "resume": true, "flexible": true }` の形
60
60
  (`resume` は省略可。true の口から入力が届いた実行は、同じノードの直近セッションを
61
- 復元して再開する——差し戻し→修正の続き実行に使う)。旧形式(値が素の JSON Schema)も
62
- 読める
61
+ 復元して再開する——差し戻し→修正の続き実行に使う。`flexible` も省略可。true の口は
62
+ `schema` を推奨型として維持し、接続で型が上書きされず型の違う接続も通る——この AI が
63
+ 先頭のループを他のループや API から型を揃えずに呼べるようにする)。旧形式(値が素の
64
+ JSON Schema)も読める
63
65
  - 実行時はラッパーがエージェント CLI(Claude Code / Codex、非対話)を起動し、`outputs` の
64
66
  スキーマに適合する output を検証して報告する。出口が複数あれば AI が1つ選ぶ(複数出口=分岐)
65
67
  - **文体の既定は「短く」**:出力の文章は承認画面・Slack・顧客への回答としてそのまま人が読むため、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mawaru/sdk",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "mawaru 実行 repo の開発 SDK。契約スキーマ(config.json 規約・ループ graph API)の正 + typegen / validate CLI",
5
5
  "type": "module",
6
6
  "bin": {