@mawaru/sdk 0.13.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.
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,
@@ -203,6 +201,10 @@ export type HumanConfig = z.infer<typeof humanConfigSchema>
203
201
  // (docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
204
202
  export const componentConfigSchema = z.object({
205
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(),
206
208
  })
207
209
  export type ComponentConfig = z.infer<typeof componentConfigSchema>
208
210
 
@@ -316,19 +318,18 @@ export const saveGraphBodySchema = z
316
318
  else outCount++
317
319
  })
318
320
  // end は「in は1つ以上」(複数ルートを別々の in で集約する)。
319
- // ai は2つ(上流データの in 差し戻しの instruction)。他 kind は1つ固定。
321
+ // ai も1つ以上(in は自由ポート:追加・削除・key 自由)。他 kind は1つ固定。
320
322
  // key の妥当性は kindPortsViolation が別途見る
321
- const expectIn = node.kind === "ai" ? 2 : 1
322
323
  const inCountOk =
323
- node.kind === "end" ? inCount >= 1 : inCount === expectIn
324
+ node.kind === "end" || node.kind === "ai" ? inCount >= 1 : inCount === 1
324
325
  if (!inCountOk) {
325
326
  ctx.addIssue({
326
327
  code: "custom",
327
328
  path: ["nodes", i, "ports"],
328
329
  message:
329
- node.kind === "end"
330
+ node.kind === "end" || node.kind === "ai"
330
331
  ? "in ポートが1つ以上必要です"
331
- : `in ポートはちょうど${expectIn}つ必要です`,
332
+ : "in ポートはちょうど1つ必要です",
332
333
  })
333
334
  }
334
335
  if (outCount < 1) {
@@ -371,7 +372,8 @@ export const saveGraphBodySchema = z
371
372
  })
372
373
  }
373
374
 
374
- // kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md
375
+ // kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md)。
376
+ // ポートのスキーマ自体は kind によらず任意の JSON Schema(形の強制はしない)
375
377
  const violation = kindPortsViolation(node.kind, node.ports)
376
378
  if (violation) {
377
379
  ctx.addIssue({
@@ -380,21 +382,6 @@ export const saveGraphBodySchema = z
380
382
  message: violation,
381
383
  })
382
384
  }
383
-
384
- // AI の出力宣言は封筒型 { data: T, description: string }(T は任意の JSON Schema)。
385
- // 複数出口でも全出口に強制する(どの出口も human に接続できる不変条件)
386
- if (node.kind === "ai") {
387
- node.ports.forEach((port, j) => {
388
- if (port.io === "out" && !isAiEnvelopeSchema(port.schema)) {
389
- ctx.addIssue({
390
- code: "custom",
391
- path: ["nodes", i, "ports", j],
392
- message:
393
- "AI の出力は封筒型 { data, description } で宣言してください",
394
- })
395
- }
396
- })
397
- }
398
385
  })
399
386
 
400
387
  // 接続:両端がグラフ内の port を指し、from=out / to=in。
@@ -480,17 +467,15 @@ export const saveGraphBodySchema = z
480
467
  })
481
468
  }
482
469
 
483
- // 差し戻し(reject)と instruction の組み合わせは kind で縛らない:両端とも
484
- // instructionSchema()({ prompt, files })で型が固定されているので、下の型検証だけで
485
- // 「型が合う口にしか繋がらない」が成立する。差し戻し先が AI である必要はない
486
- // ({ prompt, files } を受ける program へも繋げる)
470
+ // 差し戻し(reject)と instruction の組み合わせに kind の縛りも型固定も無い:
471
+ // reject の型はビューの宣言、instruction ai の in の1つ(接続鏡映)でしかない
472
+ // docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
487
473
 
488
474
  // 型検証は kind によらず常に 伝播型(out) ⊆ 伝播型(in)。
489
475
  // 未宣言は unknown=判断できないので通す(作りかけのグラフを保存できなくしない)。
490
476
  // 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
491
477
  // ので自明に通り、実質の検証対象は宣言で固定された口
492
- // (component の契約・ai の instruction・宣言のあるビューの human.in)になる
493
- // → docs/tasks/wip/Humanの入出力を承認ビューの宣言に一本化する.md
478
+ // (component の契約・宣言のあるビューの human.in)になる
494
479
  const fromSchema = resolved.get(conn.from_port_id) ?? from.schema
495
480
  const toSchema = resolved.get(conn.to_port_id) ?? to.schema
496
481
  if (!isSchemaSubset(fromSchema, toSchema)) {
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
  })
@@ -3,12 +3,9 @@ import type { GraphPort, JsonSchema } from "./node.js"
3
3
  import {
4
4
  dataFormatOf,
5
5
  dataSchemaOf,
6
- envelopeDataSchemaOf,
7
- envelopeSchemaOf,
8
6
  fileSchema,
9
7
  hasInboundConflict,
10
8
  instructionSchema,
11
- isAiEnvelopeSchema,
12
9
  isFileSchema,
13
10
  isSchemaSubset,
14
11
  kindPortsViolation,
@@ -77,102 +74,19 @@ describe("データフォーマットカタログ(S)", () => {
77
74
  })
78
75
  })
79
76
 
80
- describe("封筒型 { data: T, description: string }", () => {
81
- it("envelopeSchemaOf:任意の data スキーマを包み、data / description とも必須", () => {
82
- const text = envelopeSchemaOf({ type: "string" })
83
- expect(text).toEqual({
84
- type: "object",
85
- properties: {
86
- data: { type: "string" },
87
- description: { type: "string" },
88
- },
89
- required: ["data", "description"],
90
- })
91
- // カタログ外の任意型も包める
92
- const custom = {
93
- type: "object",
94
- properties: { title: { type: "string" }, count: { type: "number" } },
95
- required: ["title"],
96
- }
97
- expect(envelopeSchemaOf(custom).properties).toMatchObject({ data: custom })
98
- })
99
-
100
- it("envelopeDataSchemaOf:封筒型なら data のサブスキーマ、そうでなければ null", () => {
101
- const dataSchema = { type: "array", items: { type: "string" } }
102
- expect(envelopeDataSchemaOf(envelopeSchemaOf(dataSchema))).toEqual(
103
- dataSchema,
104
- )
105
- // data キーが無い / optional / object でない
106
- expect(envelopeDataSchemaOf({ type: "object", properties: {} })).toBeNull()
77
+ describe("kindPortsViolation(kind 別ポート構成)", () => {
78
+ it("ai in / out とも key 自由(構成の固定なし。口数は graph 側が見る)", () => {
107
79
  expect(
108
- envelopeDataSchemaOf({
109
- type: "object",
110
- properties: { data: { type: "string" } },
111
- }),
80
+ kindPortsViolation("ai", [port("in", "in"), port("out", "main")]),
112
81
  ).toBeNull()
113
- expect(envelopeDataSchemaOf({ type: "string" })).toBeNull()
114
- expect(envelopeDataSchemaOf(UNDECLARED_SCHEMA)).toBeNull()
115
- })
116
-
117
- it("isAiEnvelopeSchema:data と description: string の両方が必須のときだけ true", () => {
118
- expect(isAiEnvelopeSchema(envelopeSchemaOf({ type: "string" }))).toBe(true)
119
- expect(isAiEnvelopeSchema(envelopeSchemaOf({ type: "object" }))).toBe(true)
120
- // description が無い / optional / string でない
121
- expect(
122
- isAiEnvelopeSchema({
123
- type: "object",
124
- properties: { data: { type: "string" } },
125
- required: ["data"],
126
- }),
127
- ).toBe(false)
128
- expect(
129
- isAiEnvelopeSchema({
130
- type: "object",
131
- properties: {
132
- data: { type: "string" },
133
- description: { type: "string" },
134
- },
135
- required: ["data"],
136
- }),
137
- ).toBe(false)
138
- expect(
139
- isAiEnvelopeSchema({
140
- type: "object",
141
- properties: {
142
- data: { type: "string" },
143
- description: { type: "number" },
144
- },
145
- required: ["data", "description"],
146
- }),
147
- ).toBe(false)
148
- expect(isAiEnvelopeSchema(UNDECLARED_SCHEMA)).toBe(false)
149
- })
150
- })
151
-
152
- describe("kindPortsViolation(kind 別ポート構成)", () => {
153
- it("ai は out の key 自由・複数可。in は in / instruction の2口固定(AI の複数出口)", () => {
154
- const ins = [port("in", "in"), port("in", "instruction")]
155
- expect(kindPortsViolation("ai", [...ins, port("out", "main")])).toBeNull()
156
- expect(kindPortsViolation("ai", [...ins, port("out", "reply")])).toBeNull()
157
82
  expect(
158
83
  kindPortsViolation("ai", [
159
- ...ins,
84
+ port("in", "task"),
85
+ port("in", "feedback"),
160
86
  port("out", "spam"),
161
87
  port("out", "sales"),
162
- port("out", "support"),
163
88
  ]),
164
89
  ).toBeNull()
165
- // instruction が無い・key が違うのは違反
166
- expect(
167
- kindPortsViolation("ai", [port("in", "in"), port("out", "main")]),
168
- ).toMatch(/instruction/)
169
- expect(
170
- kindPortsViolation("ai", [
171
- port("in", "input"),
172
- port("in", "instruction"),
173
- port("out", "main"),
174
- ]),
175
- ).toMatch(/ai/)
176
90
  })
177
91
 
178
92
  it("human は in / approve / reject 固定", () => {
@@ -217,13 +131,18 @@ describe("kindPortsViolation(kind 別ポート構成)", () => {
217
131
  })
218
132
 
219
133
  describe("型伝播(導出ポートの解決)", () => {
220
- const envelope = envelopeSchemaOf({ type: "string" })
134
+ // 伝播テスト用の任意の宣言型(中身の形に意味はない)
135
+ const declaredOut = {
136
+ type: "object",
137
+ properties: { text: { type: "string" } },
138
+ required: ["text"],
139
+ }
221
140
 
222
- // human の in / approve はビューの宣言をそのまま転写する(上流の kind は見ない)。
141
+ // human の in / approve / reject はビューの宣言をそのまま転写する(上流の kind は見ない)。
223
142
  // sdk はカタログを知らないので、呼び出し元が viewPortSchemasOf で解決して渡す
224
- // → docs/tasks/wip/Humanの入出力を承認ビューの宣言に一本化する.md
225
- it("ai → human 直結(コピー宣言):in も approve も上流の out と同型、reject は指示エンベロープ", () => {
226
- const aiOut = port("out", "main", envelope)
143
+ // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
144
+ it("ai → human 直結(コピー宣言):in も approve も上流の out と同型、reject はビューの宣言型", () => {
145
+ const aiOut = port("out", "main", declaredOut)
227
146
  const ai = {
228
147
  id: uuid(),
229
148
  kind: "ai",
@@ -240,11 +159,17 @@ describe("型伝播(導出ポートの解決)", () => {
240
159
  const resolved = resolvePortSchemas(
241
160
  [ai, human],
242
161
  [{ from_port_id: aiOut.id, to_port_id: humanIn.id }],
243
- { viewPortSchemasOf: () => ({ in: "T", out: "T" }) },
162
+ {
163
+ viewPortSchemasOf: () => ({
164
+ in: "T",
165
+ approve: "T",
166
+ reject: instructionSchema(),
167
+ }),
168
+ },
244
169
  )
245
170
  // 上流が AI でも中身を取り出さない:AI の出力形式がそのまま下流へ流れる
246
- expect(resolved.get(humanIn.id)).toEqual(envelope)
247
- expect(resolved.get(approve.id)).toEqual(envelope)
171
+ expect(resolved.get(humanIn.id)).toEqual(declaredOut)
172
+ expect(resolved.get(approve.id)).toEqual(declaredOut)
248
173
  expect(resolved.get(reject.id)).toEqual(instructionSchema())
249
174
  })
250
175
 
@@ -266,6 +191,12 @@ describe("型伝播(導出ポートの解決)", () => {
266
191
  properties: { prs: { type: "array", items: { type: "object" } } },
267
192
  required: ["prs"],
268
193
  }
194
+ // reject もビューの宣言(具体型のみ。コピー指示は意味を持たない)
195
+ const viewReject = {
196
+ type: "object",
197
+ properties: { reason: { type: "string" } },
198
+ required: ["reason"],
199
+ }
269
200
  const build = (upstreamKind: string, upstreamOut: JsonSchema) => {
270
201
  const out = port("out", "main", upstreamOut)
271
202
  const upstream = {
@@ -275,65 +206,97 @@ describe("型伝播(導出ポートの解決)", () => {
275
206
  }
276
207
  const humanIn = port("in", "in")
277
208
  const approve = port("out", "approve")
209
+ const reject = port("out", "reject")
278
210
  const human = {
279
211
  id: uuid(),
280
212
  kind: "human",
281
- ports: [humanIn, approve, port("out", "reject")],
213
+ ports: [humanIn, approve, reject],
282
214
  }
283
- return { upstream, human, out, humanIn, approve }
215
+ return { upstream, human, out, humanIn, approve, reject }
284
216
  }
285
217
 
286
218
  it("宣言のあるビューは上流の kind に依らず同じ型になる", () => {
287
219
  for (const kind of ["ai", "program"]) {
288
- const { upstream, human, out, humanIn, approve } = build(kind, envelope)
220
+ const { upstream, human, out, humanIn, approve, reject } = build(
221
+ kind,
222
+ declaredOut,
223
+ )
289
224
  const resolved = resolvePortSchemas(
290
225
  [upstream, human],
291
226
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
292
- { viewPortSchemasOf: () => ({ in: viewIn, out: viewOut }) },
227
+ {
228
+ viewPortSchemasOf: () => ({
229
+ in: viewIn,
230
+ approve: viewOut,
231
+ reject: viewReject,
232
+ }),
233
+ },
293
234
  )
294
235
  expect(resolved.get(humanIn.id)).toEqual(viewIn)
295
236
  expect(resolved.get(approve.id)).toEqual(viewOut)
237
+ expect(resolved.get(reject.id)).toEqual(viewReject)
296
238
  }
297
239
  })
298
240
 
299
241
  it("上流の out は書き換えない(逆伝播しない)", () => {
300
- const { upstream, human, out, humanIn } = build("ai", envelope)
242
+ const { upstream, human, out, humanIn } = build("ai", declaredOut)
301
243
  const resolved = resolvePortSchemas(
302
244
  [upstream, human],
303
245
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
304
- { viewPortSchemasOf: () => ({ in: viewIn, out: viewOut }) },
246
+ {
247
+ viewPortSchemasOf: () => ({
248
+ in: viewIn,
249
+ approve: viewOut,
250
+ reject: viewReject,
251
+ }),
252
+ },
305
253
  )
306
- expect(resolved.get(out.id)).toEqual(envelope)
254
+ expect(resolved.get(out.id)).toEqual(declaredOut)
307
255
  })
308
256
 
309
257
  it("コピー宣言(標準ビュー)は上流の型をそのまま in と approve へ流す", () => {
310
- const { upstream, human, out, humanIn, approve } = build("ai", envelope)
258
+ const { upstream, human, out, humanIn, approve } = build(
259
+ "ai",
260
+ declaredOut,
261
+ )
311
262
  const resolved = resolvePortSchemas(
312
263
  [upstream, human],
313
264
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
314
- { viewPortSchemasOf: () => ({ in: "T", out: "T" }) },
265
+ {
266
+ viewPortSchemasOf: () => ({
267
+ in: "T",
268
+ approve: "T",
269
+ reject: viewReject,
270
+ }),
271
+ },
315
272
  )
316
- expect(resolved.get(humanIn.id)).toEqual(envelope)
317
- expect(resolved.get(approve.id)).toEqual(envelope)
273
+ expect(resolved.get(humanIn.id)).toEqual(declaredOut)
274
+ expect(resolved.get(approve.id)).toEqual(declaredOut)
318
275
  })
319
276
 
320
277
  it("未接続でも宣言で固定される", () => {
321
278
  const humanIn = port("in", "in")
322
279
  const approve = port("out", "approve")
280
+ const reject = port("out", "reject")
323
281
  const human = {
324
282
  id: uuid(),
325
283
  kind: "human",
326
- ports: [humanIn, approve, port("out", "reject")],
284
+ ports: [humanIn, approve, reject],
327
285
  }
328
286
  const resolved = resolvePortSchemas([human], [], {
329
- viewPortSchemasOf: () => ({ in: viewIn, out: viewOut }),
287
+ viewPortSchemasOf: () => ({
288
+ in: viewIn,
289
+ approve: viewOut,
290
+ reject: viewReject,
291
+ }),
330
292
  })
331
293
  expect(resolved.get(humanIn.id)).toEqual(viewIn)
332
294
  expect(resolved.get(approve.id)).toEqual(viewOut)
295
+ expect(resolved.get(reject.id)).toEqual(viewReject)
333
296
  })
334
297
  })
335
298
 
336
- it("ai → wait → human:素通しで封筒型が伝播し、reject は指示エンベロープのまま", () => {
299
+ it("ai → wait → human:素通しで上流の宣言型が伝播し、reject はビュー宣言が無ければ未宣言", () => {
337
300
  // ai の in は宣言型(nodes/ai/<dir>/config.json の inputs.in 由来。実行repoの構造見直し)
338
301
  const aiInSchema = {
339
302
  type: "object",
@@ -341,7 +304,7 @@ describe("型伝播(導出ポートの解決)", () => {
341
304
  required: ["inquiry"],
342
305
  }
343
306
  const aiIn = port("in", "in", aiInSchema)
344
- const aiOut = port("out", "main", envelope)
307
+ const aiOut = port("out", "main", declaredOut)
345
308
  const waitIn = port("in", "in")
346
309
  const waitOut = port("out", "main")
347
310
  const humanIn = port("in", "in")
@@ -363,16 +326,41 @@ describe("型伝播(導出ポートの解決)", () => {
363
326
  { from_port_id: waitOut.id, to_port_id: humanIn.id },
364
327
  ]
365
328
  const resolved = resolvePortSchemas([ai, wait, human], conns)
366
- expect(resolved.get(waitIn.id)).toEqual(envelope)
367
- expect(resolved.get(waitOut.id)).toEqual(envelope)
368
- expect(resolved.get(humanIn.id)).toEqual(envelope)
369
- // 封筒剥がしは「in の直接の接続元が AI ノード」のときだけ。wait 越しは素通し
370
- expect(resolved.get(approve.id)).toEqual(envelope)
371
- expect(resolved.get(reject.id)).toEqual(instructionSchema())
329
+ expect(resolved.get(waitIn.id)).toEqual(declaredOut)
330
+ expect(resolved.get(waitOut.id)).toEqual(declaredOut)
331
+ expect(resolved.get(humanIn.id)).toEqual(declaredOut)
332
+ // approve は in と同型(上流の kind による中身の取り出しはしない)
333
+ expect(resolved.get(approve.id)).toEqual(declaredOut)
334
+ // reject はビューの宣言でのみ決まる(sdk 単体=カタログ無しでは未宣言。型固定は廃止)
335
+ expect(resolved.get(reject.id)).toEqual(UNDECLARED_SCHEMA)
372
336
  // ai の in は宣言型がそのまま残る(const 扱いは廃止)
373
337
  expect(resolved.get(aiIn.id)).toEqual(aiInSchema)
374
338
  })
375
339
 
340
+ it("ai の in はどの口も接続鏡映される(instruction の特別扱い=型固定は廃止)", () => {
341
+ const progOut = port("out", "main", declaredOut)
342
+ const program = {
343
+ id: uuid(),
344
+ kind: "program",
345
+ ports: [port("in", "in"), progOut],
346
+ }
347
+ // 宣言はデフォルトの指示型。接続すると接続元の型に同期される(他の in と同じ)
348
+ const instruction = port("in", "instruction", instructionSchema())
349
+ const ai = {
350
+ id: uuid(),
351
+ kind: "ai",
352
+ ports: [port("in", "in"), instruction, port("out", "main")],
353
+ }
354
+ const resolved = resolvePortSchemas(
355
+ [program, ai],
356
+ [{ from_port_id: progOut.id, to_port_id: instruction.id }],
357
+ )
358
+ expect(resolved.get(instruction.id)).toEqual(declaredOut)
359
+ // 未接続なら宣言(デフォルト型)を維持する
360
+ const kept = resolvePortSchemas([ai], [])
361
+ expect(kept.get(instruction.id)).toEqual(instructionSchema())
362
+ })
363
+
376
364
  it("未接続の導出ポート・純パラメトリック閉路は any", () => {
377
365
  const in1 = port("in", "in")
378
366
  const out1 = port("out", "main")
@@ -398,7 +386,7 @@ describe("型伝播(導出ポートの解決)", () => {
398
386
  properties: { inquiry: { type: "string" } },
399
387
  required: ["inquiry"],
400
388
  }
401
- const aiOut = port("out", "main", envelope)
389
+ const aiOut = port("out", "main", declaredOut)
402
390
  const ai = {
403
391
  id: uuid(),
404
392
  kind: "ai",
@@ -414,7 +402,7 @@ describe("型伝播(導出ポートの解決)", () => {
414
402
  [ai, program],
415
403
  [{ from_port_id: aiOut.id, to_port_id: progIn.id }],
416
404
  )
417
- expect(resolved.get(progIn.id)).toEqual(envelope)
405
+ expect(resolved.get(progIn.id)).toEqual(declaredOut)
418
406
  })
419
407
 
420
408
  it("ai の in も接続鏡映(sticky):具体型の接続元だけ上書きする", () => {
@@ -422,7 +410,7 @@ describe("型伝播(導出ポートの解決)", () => {
422
410
  type: "object",
423
411
  properties: { topic: { type: "string" } },
424
412
  }
425
- const progOut = port("out", "main", envelope)
413
+ const progOut = port("out", "main", declaredOut)
426
414
  const program = {
427
415
  id: uuid(),
428
416
  kind: "program",
@@ -439,7 +427,7 @@ describe("型伝播(導出ポートの解決)", () => {
439
427
  [program, ai],
440
428
  [{ from_port_id: progOut.id, to_port_id: aiIn.id }],
441
429
  )
442
- expect(resolved.get(aiIn.id)).toEqual(envelope)
430
+ expect(resolved.get(aiIn.id)).toEqual(declaredOut)
443
431
  // 接続元が any(源泉の無い wait)→ 宣言を維持
444
432
  const waitOut = port("out", "main")
445
433
  const wait = {
@@ -491,7 +479,7 @@ describe("型伝播(導出ポートの解決)", () => {
491
479
  })
492
480
 
493
481
  it("program の in:wait 越しでも具体型が鏡映される(固定点反復)", () => {
494
- const aiOut = port("out", "main", envelope)
482
+ const aiOut = port("out", "main", declaredOut)
495
483
  const ai = {
496
484
  id: uuid(),
497
485
  kind: "ai",
@@ -513,7 +501,7 @@ describe("型伝播(導出ポートの解決)", () => {
513
501
  { from_port_id: waitOut.id, to_port_id: progIn.id },
514
502
  ],
515
503
  )
516
- expect(resolved.get(progIn.id)).toEqual(envelope)
504
+ expect(resolved.get(progIn.id)).toEqual(declaredOut)
517
505
  })
518
506
 
519
507
  // start の run 入力契約は接続先から導出する
@@ -624,7 +612,7 @@ describe("型伝播(導出ポートの解決)", () => {
624
612
  })
625
613
 
626
614
  it("propagateDerivedPorts:schema を書き戻し、変化のないノードは同じ参照を返す", () => {
627
- const progOut = port("out", "main", envelope)
615
+ const progOut = port("out", "main", declaredOut)
628
616
  const program = {
629
617
  id: uuid(),
630
618
  kind: "program",
@@ -641,7 +629,10 @@ describe("型伝播(導出ポートの解決)", () => {
641
629
  [{ from_port_id: progOut.id, to_port_id: waitIn.id }],
642
630
  )
643
631
  expect(next[0]).toBe(program) // 宣言のみ=無変更
644
- expect(next[1]?.ports.map((p) => p.schema)).toEqual([envelope, envelope])
632
+ expect(next[1]?.ports.map((p) => p.schema)).toEqual([
633
+ declaredOut,
634
+ declaredOut,
635
+ ])
645
636
  // 接続を消すと any に戻る
646
637
  const cleared = propagateDerivedPorts(next, [])
647
638
  expect(cleared[1]?.ports.map((p) => p.schema)).toEqual([
@@ -652,7 +643,7 @@ describe("型伝播(導出ポートの解決)", () => {
652
643
 
653
644
  it("propagateDerivedPorts:side / offset(表示位置)を落とさない", () => {
654
645
  const progOut = {
655
- ...port("out", "main", envelope),
646
+ ...port("out", "main", declaredOut),
656
647
  side: "top" as const,
657
648
  offset: 0.3,
658
649
  }