@mawaru/sdk 0.11.0 → 0.14.1

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.
Files changed (43) hide show
  1. package/_schemas/ai-manifest.test.ts +54 -59
  2. package/_schemas/ai-manifest.ts +38 -23
  3. package/_schemas/duplicate-selection.test.ts +235 -0
  4. package/_schemas/duplicate-selection.ts +48 -0
  5. package/_schemas/extract-component.test.ts +51 -2
  6. package/_schemas/extract-component.ts +4 -2
  7. package/_schemas/graph.test.ts +105 -86
  8. package/_schemas/graph.ts +20 -77
  9. package/_schemas/node.ts +4 -0
  10. package/_schemas/port-spec.test.ts +194 -113
  11. package/_schemas/port-spec.ts +95 -107
  12. package/dist/_cli/credentials.d.ts +14 -0
  13. package/dist/_cli/credentials.js +81 -0
  14. package/dist/_cli/login.d.ts +5 -0
  15. package/dist/_cli/login.js +82 -0
  16. package/dist/_cli/resumeSession.d.ts +21 -0
  17. package/dist/_cli/resumeSession.js +125 -0
  18. package/dist/_cli/sessionCommand.d.ts +3 -0
  19. package/dist/_cli/sessionCommand.js +55 -0
  20. package/dist/_schemas/ai-manifest.d.ts +12 -1
  21. package/dist/_schemas/ai-manifest.js +35 -20
  22. package/dist/_schemas/duplicate-selection.d.ts +12 -0
  23. package/dist/_schemas/duplicate-selection.js +34 -0
  24. package/dist/_schemas/extract-component.js +4 -2
  25. package/dist/_schemas/graph.d.ts +14 -12
  26. package/dist/_schemas/graph.js +21 -60
  27. package/dist/_schemas/hook-manifest.d.ts +2 -2
  28. package/dist/_schemas/node.d.ts +3 -2
  29. package/dist/_schemas/node.js +4 -0
  30. package/dist/_schemas/port-spec.d.ts +11 -6
  31. package/dist/_schemas/port-spec.js +72 -97
  32. package/dist/cli.js +50 -5
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/dist/init.js +1 -1
  36. package/dist/typegen.js +6 -6
  37. package/docs/development.md +5 -1
  38. package/index.ts +1 -0
  39. package/package.json +11 -18
  40. package/skills/create-loop/SKILL.md +1 -1
  41. package/templates/CLAUDE.md +1 -1
  42. package/templates/echo/main.ts +1 -1
  43. package/templates/mawaru-runner.yml +1 -1
@@ -8,7 +8,7 @@ import {
8
8
  waitConfigSchema,
9
9
  } from "./graph.js"
10
10
  import type { GraphPort } from "./node.js"
11
- import { envelopeSchemaOf, resolvePortSchemas } from "./port-spec.js"
11
+ import { resolvePortSchemas } from "./port-spec.js"
12
12
 
13
13
  // テスト用の決定的な uuid(common は node 型定義を持たないため crypto は使わない)
14
14
  let seq = 0
@@ -89,15 +89,14 @@ const port = (io: "in" | "out", key: string): GraphPort => ({
89
89
  schema: { type: "object" },
90
90
  })
91
91
 
92
- // ai の out 宣言は封筒型 { data, description } 固定なので、デフォルトノードは
93
- // data=テキストの封筒を持つ
94
- const textEnvelope = envelopeSchemaOf({ type: "string" })
92
+ // ai の out も自由な宣言型。デフォルトノードはテキスト(string)を宣言する
93
+ const textOutput = { type: "string" }
95
94
 
96
95
  // 既定は ai ノード。ai の in は 上流データの in + 差し戻しの instruction の2口
97
96
  const aiPorts = (): GraphPort[] => [
98
97
  port("in", "in"),
99
98
  port("in", "instruction"),
100
- { ...port("out", "main"), schema: textEnvelope },
99
+ { ...port("out", "main"), schema: textOutput },
101
100
  ]
102
101
 
103
102
  // ai 以外に流用するとき用(in は1つ)
@@ -153,8 +152,8 @@ describe("programConfigSchema", () => {
153
152
 
154
153
  describe("aiConfigSchema", () => {
155
154
  it("ai_dir を受け付ける(ref は省略可・日本語可)", () => {
156
- expect(aiConfigSchema.parse({ ai_dir: "draftReply" })).toEqual({
157
- ai_dir: "draftReply",
155
+ expect(aiConfigSchema.parse({ ai_dir: "replyComposer" })).toEqual({
156
+ ai_dir: "replyComposer",
158
157
  })
159
158
  expect(
160
159
  aiConfigSchema.parse({ ai_dir: "返信下書き", ref: "develop" }).ref,
@@ -181,14 +180,12 @@ describe("humanConfigSchema", () => {
181
180
 
182
181
  // 承認ビューは不透明なキー(program_dir / ai_dir と同じ)。実在するか・そのテナントが
183
182
  // 選べるかは mawaru 側のカタログ(非公開)と保存 API が見るので、ここは形だけ
184
- it("承認ビューとそのパラメータを受け付ける", () => {
183
+ it("承認ビューを受け付ける", () => {
185
184
  const parsed = humanConfigSchema.parse({
186
185
  assignment: {},
187
186
  view: "acme/invoice",
188
- view_config: { threshold: 1000 },
189
187
  })
190
188
  expect(parsed.view).toBe("acme/invoice")
191
- expect(parsed.view_config).toEqual({ threshold: 1000 })
192
189
  })
193
190
 
194
191
  it("空のビューキーは拒否する", () => {
@@ -390,17 +387,27 @@ describe("saveGraphBodySchema", () => {
390
387
  ).toThrow(/key が重複/)
391
388
  })
392
389
 
393
- it("in ポートがちょうど1つでない node を拒否する(0個・2個)", () => {
390
+ it("in ポートの口数:ai は1つ以上(自由)、program 等は ちょうど1つ", () => {
391
+ // ai:0個は拒否、複数は許可(in の追加・削除は自由)
394
392
  const zero = makeNode([port("out", "main")])
395
393
  expect(() =>
396
394
  saveGraphBodySchema.parse({ nodes: [zero], connections: [] }),
397
395
  ).toThrow(/in ポート/)
398
-
399
- const two = makeNode([
396
+ const many = makeNode([
400
397
  port("in", "in"),
401
- port("in", "in2"),
398
+ port("in", "feedback"),
399
+ port("in", "extra"),
402
400
  port("out", "main"),
403
401
  ])
402
+ expect(() =>
403
+ saveGraphBodySchema.parse({ nodes: [many], connections: [] }),
404
+ ).not.toThrow()
405
+
406
+ // program:ちょうど1つ
407
+ const two = {
408
+ ...makeNode([port("in", "in"), port("in", "in2"), port("out", "main")]),
409
+ kind: "program" as const,
410
+ }
404
411
  expect(() =>
405
412
  saveGraphBodySchema.parse({ nodes: [two], connections: [] }),
406
413
  ).toThrow(/in ポート/)
@@ -499,8 +506,6 @@ describe("saveGraphBodySchema", () => {
499
506
 
500
507
  // ---- kind 別ポート構成と型の接続バリデーション(ポートのkind別仕様.md) ----
501
508
 
502
- const aiEnvelope = textEnvelope
503
-
504
509
  const humanNode = () => ({
505
510
  ...makeNode([
506
511
  port("in", "in"),
@@ -513,7 +518,7 @@ const aiNode = () => ({
513
518
  ...makeNode([
514
519
  port("in", "in"),
515
520
  port("in", "instruction"),
516
- { ...port("out", "main"), schema: aiEnvelope },
521
+ { ...port("out", "main"), schema: textOutput },
517
522
  ]),
518
523
  kind: "ai" as const,
519
524
  })
@@ -551,27 +556,27 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
551
556
  ...makeNode([
552
557
  port("in", "in"),
553
558
  port("in", "instruction"),
554
- { ...port("out", "spam"), schema: aiEnvelope },
555
- { ...port("out", "sales"), schema: aiEnvelope },
556
- { ...port("out", "support"), schema: aiEnvelope },
559
+ { ...port("out", "spam"), schema: textOutput },
560
+ { ...port("out", "sales"), schema: textOutput },
561
+ { ...port("out", "support"), schema: textOutput },
557
562
  ]),
558
563
  kind: "ai" as const,
559
564
  }
560
565
  expect(() =>
561
566
  saveGraphBodySchema.parse({ nodes: [multi], connections: [] }),
562
567
  ).not.toThrow()
563
- // in の構成は in / instruction 固定のまま
564
- const badIn = {
568
+ // in key も自由(in / instruction の固定構成は廃止)
569
+ const freeIn = {
565
570
  ...makeNode([
566
571
  port("in", "input"),
567
- port("in", "instruction"),
568
- { ...port("out", "main"), schema: aiEnvelope },
572
+ port("in", "feedback"),
573
+ { ...port("out", "main"), schema: textOutput },
569
574
  ]),
570
575
  kind: "ai" as const,
571
576
  }
572
577
  expect(() =>
573
- saveGraphBodySchema.parse({ nodes: [badIn], connections: [] }),
574
- ).toThrow(/ai の in ポート/)
578
+ saveGraphBodySchema.parse({ nodes: [freeIn], connections: [] }),
579
+ ).not.toThrow()
575
580
  })
576
581
 
577
582
  it("program は out を複数持てる(key 自由)", () => {
@@ -588,51 +593,41 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
588
593
  ).not.toThrow()
589
594
  })
590
595
 
591
- it("ai の出力宣言が封筒型 { data, description } でないと拒否する(全出口に適用)", () => {
592
- const bad = makeNode([
593
- port("in", "in"),
594
- port("in", "instruction"),
595
- { ...port("out", "main"), schema: { type: "object" } },
596
- ])
597
- expect(() =>
598
- saveGraphBodySchema.parse({ nodes: [bad], connections: [] }),
599
- ).toThrow(/封筒型 \{ data, description \}/)
600
- // main 以外の出口も封筒型を強制する
601
- const badExtra = makeNode([
602
- port("in", "in"),
603
- port("in", "instruction"),
604
- { ...port("out", "main"), schema: aiEnvelope },
605
- { ...port("out", "extra"), schema: { type: "object" } },
606
- ])
607
- expect(() =>
608
- saveGraphBodySchema.parse({ nodes: [badExtra], connections: [] }),
609
- ).toThrow(/封筒型 \{ data, description \}/)
610
- // data は任意の型でよい(カタログ外の object も通る)
611
- const custom = makeNode([
612
- port("in", "in"),
613
- port("in", "instruction"),
596
+ it("ai の出力宣言は任意の JSON Schema でよい(形の強制はしない)", () => {
597
+ for (const schema of [
598
+ { type: "object" },
599
+ { type: "string" },
600
+ { type: "array", items: { type: "number" } },
614
601
  {
615
- ...port("out", "main"),
616
- schema: envelopeSchemaOf({
617
- type: "object",
618
- properties: { title: { type: "string" } },
619
- required: ["title"],
620
- }),
602
+ // かつて強制していた { data, description } もただの object として valid
603
+ type: "object",
604
+ properties: {
605
+ data: { type: "string" },
606
+ description: { type: "string" },
607
+ },
608
+ required: ["data", "description"],
621
609
  },
622
- ])
623
- expect(() =>
624
- saveGraphBodySchema.parse({ nodes: [custom], connections: [] }),
625
- ).not.toThrow()
610
+ ]) {
611
+ const node = makeNode([
612
+ port("in", "in"),
613
+ port("in", "instruction"),
614
+ { ...port("out", "main"), schema },
615
+ ])
616
+ expect(() =>
617
+ saveGraphBodySchema.parse({ nodes: [node], connections: [] }),
618
+ ).not.toThrow()
619
+ }
626
620
  })
627
621
  })
628
622
 
629
623
  describe("saveGraphBodySchema(型の接続バリデーション)", () => {
630
- it("human reject ai の instruction は許可。ai の in や ai 以外は拒否", () => {
624
+ // 接続の可否は kind ではなく型で決まる。reject / instruction の型固定は廃止され、
625
+ // ai の in はどの口も接続鏡映(接続元の型に同期)なので接続は自明に通る
626
+ // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
627
+ it("human の reject → ai の instruction を許可(kind の縛りも型固定も無い)", () => {
631
628
  const human = humanNode()
632
629
  const ai = aiNode()
633
- const prog = programNode()
634
630
  const rejectPort = outByKey(human, "reject")
635
- // 差し戻しは指示の口(instruction)へ
636
631
  expect(() =>
637
632
  saveGraphBodySchema.parse({
638
633
  nodes: [human, ai],
@@ -644,43 +639,43 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
644
639
  ],
645
640
  }),
646
641
  ).not.toThrow()
647
- // 上流データの口(in)には繋げない(意味の違う入力を fan-in させない)
648
- expect(() =>
649
- saveGraphBodySchema.parse({
650
- nodes: [human, ai],
651
- connections: [
652
- { from_port_id: rejectPort.id, to_port_id: inByKey(ai, "in").id },
653
- ],
654
- }),
655
- ).toThrow(/instruction/)
656
- // ai 以外にも繋げない
642
+ })
643
+
644
+ it("reject は ai 以外へも繋げる(接続鏡映の in が接続元の型を採用する)", () => {
645
+ const human = humanNode()
646
+ const prog = programNode()
657
647
  expect(() =>
658
648
  saveGraphBodySchema.parse({
659
649
  nodes: [human, prog],
660
650
  connections: [
661
- { from_port_id: rejectPort.id, to_port_id: inOf(prog).id },
651
+ {
652
+ from_port_id: outByKey(human, "reject").id,
653
+ to_port_id: inOf(prog).id,
654
+ },
662
655
  ],
663
656
  }),
664
- ).toThrow(/instruction/)
657
+ ).not.toThrow()
665
658
  })
666
659
 
667
- it("instruction ポートに繋げるのは human reject だけ", () => {
668
- const ai = aiNode()
669
- const prog = programNode()
660
+ it("instruction も他の in と同じ接続鏡映:宣言と違う型の out でも同期して繋がる", () => {
661
+ const upstream = aiNode()
662
+ const downstream = aiNode()
663
+ // ai.main は string、instruction の宣言は { type: "object" } だが、
664
+ // 鏡映で接続元に同期される(instruction の特別扱いは無い)
670
665
  expect(() =>
671
666
  saveGraphBodySchema.parse({
672
- nodes: [prog, ai],
667
+ nodes: [upstream, downstream],
673
668
  connections: [
674
669
  {
675
- from_port_id: outOf(prog).id,
676
- to_port_id: inByKey(ai, "instruction").id,
670
+ from_port_id: outOf(upstream).id,
671
+ to_port_id: inByKey(downstream, "instruction").id,
677
672
  },
678
673
  ],
679
674
  }),
680
- ).toThrow(/instruction ポートに接続できるのは/)
675
+ ).not.toThrow()
681
676
  })
682
677
 
683
- it("human の in はどの型でも接続できる(封筒型の制約は課さない)", () => {
678
+ it("human の in はどの型でも接続できる(形の制約は課さない)", () => {
684
679
  const ai = aiNode()
685
680
  const human = humanNode()
686
681
  expect(() =>
@@ -690,8 +685,7 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
690
685
  }),
691
686
  ).not.toThrow()
692
687
 
693
- // program out が封筒型でない({ type: "object" } のまま)でも接続できる
694
- // (input 全体を data として表示する)
688
+ // program out が素の object でも接続できる
695
689
  const human2 = humanNode()
696
690
  const progPlain = {
697
691
  ...makeNode([
@@ -709,7 +703,8 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
709
703
  })
710
704
 
711
705
  it("program の in への接続は 伝播型 ⊆ 宣言スキーマ(human 経由の伝播込み)", () => {
712
- // ai(封筒型) → human(approve は data 型に剥がれる) program(string を要求) は通る
706
+ // ai → humanapprove は in と同型)→ program(string を要求) は、
707
+ // program の in が接続鏡映(sticky)なので通る
713
708
  const ai = aiNode()
714
709
  const human = humanNode()
715
710
  const prog = programNode({ type: "string" })
@@ -1029,6 +1024,30 @@ describe("saveGraphBodySchema:コンポーネントノード(component kind
1029
1024
  ).not.toThrow()
1030
1025
  })
1031
1026
 
1027
+ it("component.refs(配置の設定値)を受け付けて保持する(省略も可)", () => {
1028
+ const withRefs = {
1029
+ ...componentNode(),
1030
+ component: {
1031
+ component_loop_id: uuid(),
1032
+ refs: { labels: ["bug"], assignee: "kazuwombat" },
1033
+ },
1034
+ }
1035
+ const parsed = saveGraphBodySchema.parse({
1036
+ nodes: [withRefs],
1037
+ connections: [],
1038
+ })
1039
+ expect(parsed.nodes[0]?.component?.refs).toEqual({
1040
+ labels: ["bug"],
1041
+ assignee: "kazuwombat",
1042
+ })
1043
+ // 省略時は undefined のまま(既存グラフの互換。保存側で {} 扱い)
1044
+ const omitted = saveGraphBodySchema.parse({
1045
+ nodes: [componentNode()],
1046
+ connections: [],
1047
+ })
1048
+ expect(omitted.nodes[0]?.component?.refs).toBeUndefined()
1049
+ })
1050
+
1032
1051
  it("component_loop_id の無い component ノードを拒否する", () => {
1033
1052
  const node = { ...componentNode(), component: undefined }
1034
1053
  expect(() =>
@@ -1068,7 +1087,7 @@ describe("saveGraphBodySchema:コンポーネントノード(component kind
1068
1087
  nodes: [prog, comp],
1069
1088
  connections: [connect(prog, comp)],
1070
1089
  }),
1071
- ).toThrow(/入力契約に適合しません/)
1090
+ ).toThrow(/適合しません/)
1072
1091
  // 適合(string → string)なら通る
1073
1092
  const okComp = componentNode({ type: "string" })
1074
1093
  expect(() =>
package/_schemas/graph.ts CHANGED
@@ -6,9 +6,7 @@ import {
6
6
  portKeySchema,
7
7
  } from "./node.js"
8
8
  import {
9
- AI_INSTRUCTION_PORT_KEY,
10
9
  hasInboundConflict,
11
- isAiEnvelopeSchema,
12
10
  isSchemaSubset,
13
11
  kindPortsViolation,
14
12
  resolvePortSchemas,
@@ -148,8 +146,6 @@ export const humanConfigSchema = z.object({
148
146
  // どのキーが実在するか・そのテナントが選べるかは保存時に mawaru 側が検証する
149
147
  // (カタログは非公開。@mawaru/common の approval-view.ts)。省略時は標準ビュー
150
148
  view: z.string().min(1).optional(),
151
- // ビューに渡すパラメータ(列・ラベル・閾値など)。形はビューごとに違うので任意の JSON
152
- view_config: z.unknown().nullish(),
153
149
  assignment: z
154
150
  .object({
155
151
  strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
@@ -205,6 +201,10 @@ export type HumanConfig = z.infer<typeof humanConfigSchema>
205
201
  // (docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
206
202
  export const componentConfigSchema = z.object({
207
203
  component_loop_id: z.uuid(),
204
+ // 配置の設定値:子 loop の refs 定義(default/required)への上書き。
205
+ // 子 run 起動時の起動時指定として渡る(省略=上書きなし。検証は保存 API と
206
+ // 起動時の mergeRefs が行う。コンポーネントの設定値(refs)と1ノードのコンポーネント化.md)
207
+ refs: z.record(z.string(), z.unknown()).optional(),
208
208
  })
209
209
  export type ComponentConfig = z.infer<typeof componentConfigSchema>
210
210
 
@@ -318,19 +318,18 @@ export const saveGraphBodySchema = z
318
318
  else outCount++
319
319
  })
320
320
  // end は「in は1つ以上」(複数ルートを別々の in で集約する)。
321
- // ai は2つ(上流データの in 差し戻しの instruction)。他 kind は1つ固定。
321
+ // ai も1つ以上(in は自由ポート:追加・削除・key 自由)。他 kind は1つ固定。
322
322
  // key の妥当性は kindPortsViolation が別途見る
323
- const expectIn = node.kind === "ai" ? 2 : 1
324
323
  const inCountOk =
325
- node.kind === "end" ? inCount >= 1 : inCount === expectIn
324
+ node.kind === "end" || node.kind === "ai" ? inCount >= 1 : inCount === 1
326
325
  if (!inCountOk) {
327
326
  ctx.addIssue({
328
327
  code: "custom",
329
328
  path: ["nodes", i, "ports"],
330
329
  message:
331
- node.kind === "end"
330
+ node.kind === "end" || node.kind === "ai"
332
331
  ? "in ポートが1つ以上必要です"
333
- : `in ポートはちょうど${expectIn}つ必要です`,
332
+ : "in ポートはちょうど1つ必要です",
334
333
  })
335
334
  }
336
335
  if (outCount < 1) {
@@ -373,7 +372,8 @@ export const saveGraphBodySchema = z
373
372
  })
374
373
  }
375
374
 
376
- // kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md
375
+ // kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md)。
376
+ // ポートのスキーマ自体は kind によらず任意の JSON Schema(形の強制はしない)
377
377
  const violation = kindPortsViolation(node.kind, node.ports)
378
378
  if (violation) {
379
379
  ctx.addIssue({
@@ -382,21 +382,6 @@ export const saveGraphBodySchema = z
382
382
  message: violation,
383
383
  })
384
384
  }
385
-
386
- // AI の出力宣言は封筒型 { data: T, description: string }(T は任意の JSON Schema)。
387
- // 複数出口でも全出口に強制する(どの出口も human に接続できる不変条件)
388
- if (node.kind === "ai") {
389
- node.ports.forEach((port, j) => {
390
- if (port.io === "out" && !isAiEnvelopeSchema(port.schema)) {
391
- ctx.addIssue({
392
- code: "custom",
393
- path: ["nodes", i, "ports", j],
394
- message:
395
- "AI の出力は封筒型 { data, description } で宣言してください",
396
- })
397
- }
398
- })
399
- }
400
385
  })
401
386
 
402
387
  // 接続:両端がグラフ内の port を指し、from=out / to=in。
@@ -482,64 +467,22 @@ export const saveGraphBodySchema = z
482
467
  })
483
468
  }
484
469
 
485
- // human の reject の接続先は AI instruction ポートのみ。
486
- // 差し戻しは「元のセッションに指示を与える」操作で、上流データの口(in)とは
487
- // 意味が違う(docs/tasks/wip/ポートスキーマのany撲滅.md
488
- if (
489
- fromNode.kind === "human" &&
490
- from.key === "reject" &&
491
- !(toNode.kind === "ai" && to.key === AI_INSTRUCTION_PORT_KEY)
492
- ) {
493
- ctx.addIssue({
494
- code: "custom",
495
- path: ["connections", i, "to_port_id"],
496
- message:
497
- "差し戻し(reject)の接続先は AI ノードの instruction ポートのみです",
498
- })
499
- }
500
-
501
- // instruction ポートに繋げるのは human の reject だけ(データを流し込ませない)
502
- if (
503
- toNode.kind === "ai" &&
504
- to.key === AI_INSTRUCTION_PORT_KEY &&
505
- !(fromNode.kind === "human" && from.key === "reject")
506
- ) {
507
- ctx.addIssue({
508
- code: "custom",
509
- path: ["connections", i, "to_port_id"],
510
- message:
511
- "instruction ポートに接続できるのは human の差し戻し(reject)だけです",
512
- })
513
- }
470
+ // 差し戻し(reject)と instruction の組み合わせに kind の縛りも型固定も無い:
471
+ // reject の型はビューの宣言、instruction は ai の in の1つ(接続鏡映)でしかない
472
+ // docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
514
473
 
474
+ // 型検証は kind によらず常に 伝播型(out) ⊆ 伝播型(in)。
475
+ // 未宣言は unknown=判断できないので通す(作りかけのグラフを保存できなくしない)。
476
+ // 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
477
+ // ので自明に通り、実質の検証対象は宣言で固定された口
478
+ // (component の契約・宣言のあるビューの human.in)になる
515
479
  const fromSchema = resolved.get(conn.from_port_id) ?? from.schema
516
-
517
- // human の in はどの型でも接続できる(上流が AI なら封筒 { data, description }、
518
- // それ以外は input 全体を data として表示する。型制約は課さない)
519
-
520
- // program の in への接続は 伝播型 ⊆ 宣言スキーマ。in は接続鏡映(sticky)なので
521
- // 具体型の接続では自明に通り、接続元が未宣言 × 宣言維持のときだけ従来の意味を持つ
522
- // (未宣言は unknown=判断できないので通す。作りかけのグラフを保存できなくしない)
523
480
  const toSchema = resolved.get(conn.to_port_id) ?? to.schema
524
- if (toNode.kind === "program" && !isSchemaSubset(fromSchema, toSchema)) {
525
- ctx.addIssue({
526
- code: "custom",
527
- path: ["connections", i, "to_port_id"],
528
- message:
529
- "接続元の型が program の入力スキーマに適合しません(伝播型 ⊆ 宣言スキーマ)",
530
- })
531
- }
532
-
533
- // component の in は子 loop の契約(焼き込み済み宣言)。program と同じ包含チェック
534
- if (
535
- toNode.kind === "component" &&
536
- !isSchemaSubset(fromSchema, toSchema)
537
- ) {
481
+ if (!isSchemaSubset(fromSchema, toSchema)) {
538
482
  ctx.addIssue({
539
483
  code: "custom",
540
484
  path: ["connections", i, "to_port_id"],
541
- message:
542
- "接続元の型がコンポーネントの入力契約に適合しません(伝播型 ⊆ 契約)",
485
+ message: `「${fromNode.name}」の出力が「${toNode.name}」の入力スキーマに適合しません`,
543
486
  })
544
487
  }
545
488
  })
package/_schemas/node.ts CHANGED
@@ -47,6 +47,10 @@ export const graphPortSchema = z
47
47
  key: portKeySchema,
48
48
  name: z.string().min(1),
49
49
  schema: jsonSchemaSchema,
50
+ // in ポートの実行属性:true ならこの口から届いた実行は前回セッションを復元して
51
+ // 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
52
+ // ポート型固定の撤去とセッション再開のポート属性化.md)
53
+ resume: z.boolean().optional(),
50
54
  side: portSideSchema.optional(),
51
55
  offset: z.number().min(0).max(1).optional(),
52
56
  })