@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
@@ -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", "draft")])).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,10 +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
- it("ai human 直結:approve は封筒を剥がした data 型、reject は指示エンベロープ", () => {
223
- const aiOut = port("out", "main", envelope)
141
+ // human の in / approve / reject はビューの宣言をそのまま転写する(上流の kind は見ない)。
142
+ // sdk はカタログを知らないので、呼び出し元が viewPortSchemasOf で解決して渡す
143
+ // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
144
+ it("ai → human 直結(コピー宣言):in も approve も上流の out と同型、reject はビューの宣言型", () => {
145
+ const aiOut = port("out", "main", declaredOut)
224
146
  const ai = {
225
147
  id: uuid(),
226
148
  kind: "ai",
@@ -237,13 +159,144 @@ describe("型伝播(導出ポートの解決)", () => {
237
159
  const resolved = resolvePortSchemas(
238
160
  [ai, human],
239
161
  [{ from_port_id: aiOut.id, to_port_id: humanIn.id }],
162
+ {
163
+ viewPortSchemasOf: () => ({
164
+ in: "T",
165
+ approve: "T",
166
+ reject: instructionSchema(),
167
+ }),
168
+ },
240
169
  )
241
- expect(resolved.get(humanIn.id)).toEqual(envelope)
242
- expect(resolved.get(approve.id)).toEqual({ type: "string" })
170
+ // 上流が AI でも中身を取り出さない:AI の出力形式がそのまま下流へ流れる
171
+ expect(resolved.get(humanIn.id)).toEqual(declaredOut)
172
+ expect(resolved.get(approve.id)).toEqual(declaredOut)
243
173
  expect(resolved.get(reject.id)).toEqual(instructionSchema())
244
174
  })
245
175
 
246
- it("ai → wait → human:素通しで封筒型が伝播し、reject は指示エンベロープのまま", () => {
176
+ describe("承認ビューの宣言を human のポートへ転写する", () => {
177
+ const viewIn = {
178
+ type: "object",
179
+ properties: {
180
+ data: {
181
+ type: "object",
182
+ properties: { prs: { type: "array", items: { type: "object" } } },
183
+ required: ["prs"],
184
+ },
185
+ description: { type: "string" },
186
+ },
187
+ required: ["data", "description"],
188
+ }
189
+ const viewOut = {
190
+ type: "object",
191
+ properties: { prs: { type: "array", items: { type: "object" } } },
192
+ required: ["prs"],
193
+ }
194
+ // reject もビューの宣言(具体型のみ。コピー指示は意味を持たない)
195
+ const viewReject = {
196
+ type: "object",
197
+ properties: { reason: { type: "string" } },
198
+ required: ["reason"],
199
+ }
200
+ const build = (upstreamKind: string, upstreamOut: JsonSchema) => {
201
+ const out = port("out", "main", upstreamOut)
202
+ const upstream = {
203
+ id: uuid(),
204
+ kind: upstreamKind,
205
+ ports: [port("in", "in"), out],
206
+ }
207
+ const humanIn = port("in", "in")
208
+ const approve = port("out", "approve")
209
+ const reject = port("out", "reject")
210
+ const human = {
211
+ id: uuid(),
212
+ kind: "human",
213
+ ports: [humanIn, approve, reject],
214
+ }
215
+ return { upstream, human, out, humanIn, approve, reject }
216
+ }
217
+
218
+ it("宣言のあるビューは上流の kind に依らず同じ型になる", () => {
219
+ for (const kind of ["ai", "program"]) {
220
+ const { upstream, human, out, humanIn, approve, reject } = build(
221
+ kind,
222
+ declaredOut,
223
+ )
224
+ const resolved = resolvePortSchemas(
225
+ [upstream, human],
226
+ [{ from_port_id: out.id, to_port_id: humanIn.id }],
227
+ {
228
+ viewPortSchemasOf: () => ({
229
+ in: viewIn,
230
+ approve: viewOut,
231
+ reject: viewReject,
232
+ }),
233
+ },
234
+ )
235
+ expect(resolved.get(humanIn.id)).toEqual(viewIn)
236
+ expect(resolved.get(approve.id)).toEqual(viewOut)
237
+ expect(resolved.get(reject.id)).toEqual(viewReject)
238
+ }
239
+ })
240
+
241
+ it("上流の out は書き換えない(逆伝播しない)", () => {
242
+ const { upstream, human, out, humanIn } = build("ai", declaredOut)
243
+ const resolved = resolvePortSchemas(
244
+ [upstream, human],
245
+ [{ from_port_id: out.id, to_port_id: humanIn.id }],
246
+ {
247
+ viewPortSchemasOf: () => ({
248
+ in: viewIn,
249
+ approve: viewOut,
250
+ reject: viewReject,
251
+ }),
252
+ },
253
+ )
254
+ expect(resolved.get(out.id)).toEqual(declaredOut)
255
+ })
256
+
257
+ it("コピー宣言(標準ビュー)は上流の型をそのまま in と approve へ流す", () => {
258
+ const { upstream, human, out, humanIn, approve } = build(
259
+ "ai",
260
+ declaredOut,
261
+ )
262
+ const resolved = resolvePortSchemas(
263
+ [upstream, human],
264
+ [{ from_port_id: out.id, to_port_id: humanIn.id }],
265
+ {
266
+ viewPortSchemasOf: () => ({
267
+ in: "T",
268
+ approve: "T",
269
+ reject: viewReject,
270
+ }),
271
+ },
272
+ )
273
+ expect(resolved.get(humanIn.id)).toEqual(declaredOut)
274
+ expect(resolved.get(approve.id)).toEqual(declaredOut)
275
+ })
276
+
277
+ it("未接続でも宣言で固定される", () => {
278
+ const humanIn = port("in", "in")
279
+ const approve = port("out", "approve")
280
+ const reject = port("out", "reject")
281
+ const human = {
282
+ id: uuid(),
283
+ kind: "human",
284
+ ports: [humanIn, approve, reject],
285
+ }
286
+ const resolved = resolvePortSchemas([human], [], {
287
+ viewPortSchemasOf: () => ({
288
+ in: viewIn,
289
+ approve: viewOut,
290
+ reject: viewReject,
291
+ }),
292
+ })
293
+ expect(resolved.get(humanIn.id)).toEqual(viewIn)
294
+ expect(resolved.get(approve.id)).toEqual(viewOut)
295
+ expect(resolved.get(reject.id)).toEqual(viewReject)
296
+ })
297
+ })
298
+
299
+ it("ai → wait → human:素通しで上流の宣言型が伝播し、reject はビュー宣言が無ければ未宣言", () => {
247
300
  // ai の in は宣言型(nodes/ai/<dir>/config.json の inputs.in 由来。実行repoの構造見直し)
248
301
  const aiInSchema = {
249
302
  type: "object",
@@ -251,7 +304,7 @@ describe("型伝播(導出ポートの解決)", () => {
251
304
  required: ["inquiry"],
252
305
  }
253
306
  const aiIn = port("in", "in", aiInSchema)
254
- const aiOut = port("out", "main", envelope)
307
+ const aiOut = port("out", "main", declaredOut)
255
308
  const waitIn = port("in", "in")
256
309
  const waitOut = port("out", "main")
257
310
  const humanIn = port("in", "in")
@@ -273,16 +326,41 @@ describe("型伝播(導出ポートの解決)", () => {
273
326
  { from_port_id: waitOut.id, to_port_id: humanIn.id },
274
327
  ]
275
328
  const resolved = resolvePortSchemas([ai, wait, human], conns)
276
- expect(resolved.get(waitIn.id)).toEqual(envelope)
277
- expect(resolved.get(waitOut.id)).toEqual(envelope)
278
- expect(resolved.get(humanIn.id)).toEqual(envelope)
279
- // 封筒剥がしは「in の直接の接続元が AI ノード」のときだけ。wait 越しは素通し
280
- expect(resolved.get(approve.id)).toEqual(envelope)
281
- 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)
282
336
  // ai の in は宣言型がそのまま残る(const 扱いは廃止)
283
337
  expect(resolved.get(aiIn.id)).toEqual(aiInSchema)
284
338
  })
285
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
+
286
364
  it("未接続の導出ポート・純パラメトリック閉路は any", () => {
287
365
  const in1 = port("in", "in")
288
366
  const out1 = port("out", "main")
@@ -308,7 +386,7 @@ describe("型伝播(導出ポートの解決)", () => {
308
386
  properties: { inquiry: { type: "string" } },
309
387
  required: ["inquiry"],
310
388
  }
311
- const aiOut = port("out", "main", envelope)
389
+ const aiOut = port("out", "main", declaredOut)
312
390
  const ai = {
313
391
  id: uuid(),
314
392
  kind: "ai",
@@ -324,7 +402,7 @@ describe("型伝播(導出ポートの解決)", () => {
324
402
  [ai, program],
325
403
  [{ from_port_id: aiOut.id, to_port_id: progIn.id }],
326
404
  )
327
- expect(resolved.get(progIn.id)).toEqual(envelope)
405
+ expect(resolved.get(progIn.id)).toEqual(declaredOut)
328
406
  })
329
407
 
330
408
  it("ai の in も接続鏡映(sticky):具体型の接続元だけ上書きする", () => {
@@ -332,7 +410,7 @@ describe("型伝播(導出ポートの解決)", () => {
332
410
  type: "object",
333
411
  properties: { topic: { type: "string" } },
334
412
  }
335
- const progOut = port("out", "main", envelope)
413
+ const progOut = port("out", "main", declaredOut)
336
414
  const program = {
337
415
  id: uuid(),
338
416
  kind: "program",
@@ -349,7 +427,7 @@ describe("型伝播(導出ポートの解決)", () => {
349
427
  [program, ai],
350
428
  [{ from_port_id: progOut.id, to_port_id: aiIn.id }],
351
429
  )
352
- expect(resolved.get(aiIn.id)).toEqual(envelope)
430
+ expect(resolved.get(aiIn.id)).toEqual(declaredOut)
353
431
  // 接続元が any(源泉の無い wait)→ 宣言を維持
354
432
  const waitOut = port("out", "main")
355
433
  const wait = {
@@ -401,7 +479,7 @@ describe("型伝播(導出ポートの解決)", () => {
401
479
  })
402
480
 
403
481
  it("program の in:wait 越しでも具体型が鏡映される(固定点反復)", () => {
404
- const aiOut = port("out", "main", envelope)
482
+ const aiOut = port("out", "main", declaredOut)
405
483
  const ai = {
406
484
  id: uuid(),
407
485
  kind: "ai",
@@ -423,7 +501,7 @@ describe("型伝播(導出ポートの解決)", () => {
423
501
  { from_port_id: waitOut.id, to_port_id: progIn.id },
424
502
  ],
425
503
  )
426
- expect(resolved.get(progIn.id)).toEqual(envelope)
504
+ expect(resolved.get(progIn.id)).toEqual(declaredOut)
427
505
  })
428
506
 
429
507
  // start の run 入力契約は接続先から導出する
@@ -534,7 +612,7 @@ describe("型伝播(導出ポートの解決)", () => {
534
612
  })
535
613
 
536
614
  it("propagateDerivedPorts:schema を書き戻し、変化のないノードは同じ参照を返す", () => {
537
- const progOut = port("out", "main", envelope)
615
+ const progOut = port("out", "main", declaredOut)
538
616
  const program = {
539
617
  id: uuid(),
540
618
  kind: "program",
@@ -551,7 +629,10 @@ describe("型伝播(導出ポートの解決)", () => {
551
629
  [{ from_port_id: progOut.id, to_port_id: waitIn.id }],
552
630
  )
553
631
  expect(next[0]).toBe(program) // 宣言のみ=無変更
554
- 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
+ ])
555
636
  // 接続を消すと any に戻る
556
637
  const cleared = propagateDerivedPorts(next, [])
557
638
  expect(cleared[1]?.ports.map((p) => p.schema)).toEqual([
@@ -562,7 +643,7 @@ describe("型伝播(導出ポートの解決)", () => {
562
643
 
563
644
  it("propagateDerivedPorts:side / offset(表示位置)を落とさない", () => {
564
645
  const progOut = {
565
- ...port("out", "main", envelope),
646
+ ...port("out", "main", declaredOut),
566
647
  side: "top" as const,
567
648
  offset: 0.3,
568
649
  }