@mawaru/sdk 0.38.0 → 0.40.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.
Files changed (91) hide show
  1. package/_schemas/ai-manifest.test.ts +8 -0
  2. package/_schemas/ai-manifest.ts +2 -0
  3. package/_schemas/graph.test.ts +51 -9
  4. package/_schemas/graph.ts +23 -8
  5. package/_schemas/human-manifest.test.ts +129 -0
  6. package/_schemas/human-manifest.ts +158 -0
  7. package/_schemas/node.ts +4 -0
  8. package/_schemas/port-spec.test.ts +164 -40
  9. package/_schemas/port-spec.ts +77 -26
  10. package/dist/_cli/login.js +1 -15
  11. package/dist/_cli/openBrowser.d.ts +1 -0
  12. package/dist/_cli/openBrowser.js +15 -0
  13. package/dist/_cli/previewApi.d.ts +1 -1
  14. package/dist/_cli/previewCommand.d.ts +8 -0
  15. package/dist/_cli/previewCommand.js +95 -37
  16. package/dist/_cli/previewFiles.d.ts +2 -1
  17. package/dist/_cli/previewFiles.js +42 -19
  18. package/dist/_cli/previewSlack.d.ts +1 -1
  19. package/dist/_cli/previewSlack.js +1 -1
  20. package/dist/_schemas/ai-manifest.d.ts +3 -0
  21. package/dist/_schemas/ai-manifest.js +2 -0
  22. package/dist/_schemas/graph.d.ts +23 -3
  23. package/dist/_schemas/graph.js +20 -8
  24. package/dist/_schemas/human-manifest.d.ts +27 -0
  25. package/dist/_schemas/human-manifest.js +121 -0
  26. package/dist/_schemas/node.d.ts +1 -0
  27. package/dist/_schemas/node.js +4 -0
  28. package/dist/_schemas/port-spec.d.ts +8 -9
  29. package/dist/_schemas/port-spec.js +64 -9
  30. package/dist/cli.js +3 -2
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/init.js +22 -3
  34. package/dist/typegen.d.ts +3 -1
  35. package/dist/typegen.js +96 -14
  36. package/dist/validate.js +55 -0
  37. package/dist/views/_approval/ApprovalBar.d.ts +20 -0
  38. package/dist/views/_approval/ApprovalBar.js +74 -0
  39. package/dist/views/_approvalActions.d.ts +18 -0
  40. package/dist/views/_approvalActions.js +10 -0
  41. package/dist/views/_bridge.d.ts +12 -0
  42. package/dist/views/_bridge.js +10 -0
  43. package/dist/views/_parts/DocumentCard.d.ts +2 -0
  44. package/dist/views/_parts/DocumentCard.js +16 -0
  45. package/dist/views/_parts/FilePreview.d.ts +5 -0
  46. package/dist/views/_parts/FilePreview.js +101 -0
  47. package/dist/views/_parts/Linkify.d.ts +7 -0
  48. package/dist/views/_parts/Linkify.js +28 -0
  49. package/dist/views/_parts/Markdown.d.ts +44 -0
  50. package/dist/views/_parts/Markdown.js +313 -0
  51. package/dist/views/_parts/SchemaForm.d.ts +19 -0
  52. package/dist/views/_parts/SchemaForm.js +310 -0
  53. package/dist/views/_parts/fileRef.d.ts +7 -0
  54. package/dist/views/_parts/fileRef.js +11 -0
  55. package/dist/views/_parts/useNarrow.d.ts +5 -0
  56. package/dist/views/_parts/useNarrow.js +23 -0
  57. package/dist/views/host.d.ts +18 -0
  58. package/dist/views/host.js +20 -0
  59. package/dist/views/human.d.ts +73 -0
  60. package/dist/views/human.js +6 -0
  61. package/dist/views/index.d.ts +8 -0
  62. package/dist/views/index.js +12 -0
  63. package/dist/views/protocol.d.ts +34 -0
  64. package/dist/views/protocol.js +5 -0
  65. package/dist/views/runtime.d.ts +3 -0
  66. package/dist/views/runtime.js +86 -0
  67. package/dist/{slack-view.d.ts → views/slack.d.ts} +1 -1
  68. package/docs/development.md +45 -5
  69. package/index.ts +1 -0
  70. package/package.json +36 -4
  71. package/skills/create-loop/SKILL.md +6 -3
  72. package/views/_approval/ApprovalBar.tsx +239 -0
  73. package/views/_approvalActions.tsx +31 -0
  74. package/views/_bridge.tsx +36 -0
  75. package/views/_parts/DocumentCard.ts +18 -0
  76. package/views/_parts/FilePreview.tsx +180 -0
  77. package/views/_parts/Linkify.test.ts +40 -0
  78. package/views/_parts/Linkify.tsx +40 -0
  79. package/views/_parts/Markdown.test.ts +131 -0
  80. package/views/_parts/Markdown.tsx +472 -0
  81. package/views/_parts/SchemaForm.tsx +742 -0
  82. package/views/_parts/fileRef.ts +21 -0
  83. package/views/_parts/useNarrow.ts +24 -0
  84. package/views/host.ts +23 -0
  85. package/views/human.ts +91 -0
  86. package/views/index.ts +16 -0
  87. package/views/protocol.ts +40 -0
  88. package/views/runtime.tsx +132 -0
  89. package/views/slack.test.ts +40 -0
  90. package/{slack-view.ts → views/slack.ts} +3 -3
  91. /package/dist/{slack-view.js → views/slack.js} +0 -0
@@ -1,7 +1,9 @@
1
1
  import { describe, expect, it } from "vitest"
2
+ import type { HumanDeclaration } from "./human-manifest.js"
2
3
  import type { GraphPort, JsonSchema } from "./node.js"
3
4
  import {
4
5
  arrayOf,
6
+ connectionAccepted,
5
7
  dataFormatOf,
6
8
  dataSchemaOf,
7
9
  fileSchema,
@@ -12,6 +14,7 @@ import {
12
14
  kindPortsViolation,
13
15
  mergeInboundSchemas,
14
16
  propagateDerivedPorts,
17
+ resolveFlexiblePorts,
15
18
  resolvePortSchemas,
16
19
  schemaFit,
17
20
  schemaTypeKey,
@@ -151,6 +154,15 @@ describe("kindPortsViolation(kind 別ポート構成)", () => {
151
154
  })
152
155
  })
153
156
 
157
+ // human ノードに実装の宣言の写し(node_humans.declaration)を持たせる
158
+ const withViewDeclaration = <N extends { kind: string }>(
159
+ nodes: N[],
160
+ declaration: HumanDeclaration,
161
+ ) =>
162
+ nodes.map((node) =>
163
+ node.kind === "human" ? { ...node, human: { declaration } } : node,
164
+ )
165
+
154
166
  describe("型伝播(導出ポートの解決)", () => {
155
167
  it.each([
156
168
  "notion_task",
@@ -171,13 +183,14 @@ describe("型伝播(導出ポートの解決)", () => {
171
183
  extra,
172
184
  ],
173
185
  }
174
- const resolved = resolvePortSchemas([human], [], {
175
- viewPortSchemasOf: () => ({
186
+ const resolved = resolvePortSchemas(
187
+ withViewDeclaration([human], {
176
188
  in: { type: "string" },
177
189
  approve: { type: "string" },
178
190
  reject: { type: "string" },
179
191
  }),
180
- })
192
+ [],
193
+ )
181
194
  expect(resolved.get(extra.id)).toEqual(extra.schema)
182
195
  })
183
196
 
@@ -189,7 +202,7 @@ describe("型伝播(導出ポートの解決)", () => {
189
202
  }
190
203
 
191
204
  // human の in / approve / reject はビューの宣言をそのまま転写する(上流の kind は見ない)。
192
- // sdk はカタログを知らないので、呼び出し元が viewPortSchemasOf で解決して渡す
205
+ // 宣言はノードの写し(declaration)を読む(sdk はカタログを知らない)
193
206
  // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
194
207
  it("ai → human 直結(コピー宣言):in も approve も上流の out と同型、reject はビューの宣言型", () => {
195
208
  const aiOut = port("out", "main", declaredOut)
@@ -207,15 +220,12 @@ describe("型伝播(導出ポートの解決)", () => {
207
220
  ports: [humanIn, approve, reject],
208
221
  }
209
222
  const resolved = resolvePortSchemas(
210
- [ai, human],
223
+ withViewDeclaration([ai, human], {
224
+ in: "T",
225
+ approve: "T",
226
+ reject: instructionSchema(),
227
+ }),
211
228
  [{ from_port_id: aiOut.id, to_port_id: humanIn.id }],
212
- {
213
- viewPortSchemasOf: () => ({
214
- in: "T",
215
- approve: "T",
216
- reject: instructionSchema(),
217
- }),
218
- },
219
229
  )
220
230
  // 上流が AI でも中身を取り出さない:AI の出力形式がそのまま下流へ流れる
221
231
  expect(resolved.get(humanIn.id)).toEqual(declaredOut)
@@ -223,7 +233,7 @@ describe("型伝播(導出ポートの解決)", () => {
223
233
  expect(resolved.get(reject.id)).toEqual(instructionSchema())
224
234
  })
225
235
 
226
- describe("承認ビューの宣言を human のポートへ転写する", () => {
236
+ describe("実装の宣言を human のポートへ転写する", () => {
227
237
  const viewIn = {
228
238
  type: "object",
229
239
  properties: {
@@ -272,15 +282,12 @@ describe("型伝播(導出ポートの解決)", () => {
272
282
  declaredOut,
273
283
  )
274
284
  const resolved = resolvePortSchemas(
275
- [upstream, human],
285
+ withViewDeclaration([upstream, human], {
286
+ in: viewIn,
287
+ approve: viewOut,
288
+ reject: viewReject,
289
+ }),
276
290
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
277
- {
278
- viewPortSchemasOf: () => ({
279
- in: viewIn,
280
- approve: viewOut,
281
- reject: viewReject,
282
- }),
283
- },
284
291
  )
285
292
  expect(resolved.get(humanIn.id)).toEqual(viewIn)
286
293
  expect(resolved.get(approve.id)).toEqual(viewOut)
@@ -291,15 +298,12 @@ describe("型伝播(導出ポートの解決)", () => {
291
298
  it("上流の out は書き換えない(逆伝播しない)", () => {
292
299
  const { upstream, human, out, humanIn } = build("ai", declaredOut)
293
300
  const resolved = resolvePortSchemas(
294
- [upstream, human],
301
+ withViewDeclaration([upstream, human], {
302
+ in: viewIn,
303
+ approve: viewOut,
304
+ reject: viewReject,
305
+ }),
295
306
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
296
- {
297
- viewPortSchemasOf: () => ({
298
- in: viewIn,
299
- approve: viewOut,
300
- reject: viewReject,
301
- }),
302
- },
303
307
  )
304
308
  expect(resolved.get(out.id)).toEqual(declaredOut)
305
309
  })
@@ -310,15 +314,12 @@ describe("型伝播(導出ポートの解決)", () => {
310
314
  declaredOut,
311
315
  )
312
316
  const resolved = resolvePortSchemas(
313
- [upstream, human],
317
+ withViewDeclaration([upstream, human], {
318
+ in: "T",
319
+ approve: "T",
320
+ reject: viewReject,
321
+ }),
314
322
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
315
- {
316
- viewPortSchemasOf: () => ({
317
- in: "T",
318
- approve: "T",
319
- reject: viewReject,
320
- }),
321
- },
322
323
  )
323
324
  expect(resolved.get(humanIn.id)).toEqual(declaredOut)
324
325
  expect(resolved.get(approve.id)).toEqual(declaredOut)
@@ -333,13 +334,14 @@ describe("型伝播(導出ポートの解決)", () => {
333
334
  kind: "human",
334
335
  ports: [humanIn, approve, reject],
335
336
  }
336
- const resolved = resolvePortSchemas([human], [], {
337
- viewPortSchemasOf: () => ({
337
+ const resolved = resolvePortSchemas(
338
+ withViewDeclaration([human], {
338
339
  in: viewIn,
339
340
  approve: viewOut,
340
341
  reject: viewReject,
341
342
  }),
342
- })
343
+ [],
344
+ )
343
345
  expect(resolved.get(humanIn.id)).toEqual(viewIn)
344
346
  expect(resolved.get(approve.id)).toEqual(viewOut)
345
347
  expect(resolved.get(reject.id)).toEqual(viewReject)
@@ -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 = {
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod"
2
+ import type { HumanDeclaration } from "./human-manifest.js"
2
3
  import type { GraphPort, JsonSchema, PortIo } from "./node.js"
3
4
 
4
5
  // ポートの kind 別型システム(docs/tasks/done/ポートのkind別仕様.md)。
5
- // 型の決まり方は2分類:宣言(program / ai の in・out。human はビューの宣言を転写)/
6
+ // 型の決まり方は2分類:宣言(program / ai の in・out。human は実装(nodes/human/)の宣言を転写)/
6
7
  // 導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)。
7
8
  // mawaru がコードで型を固定するポートは無い
8
9
  // (docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md)
@@ -265,7 +266,7 @@ export const kindPortsViolation = (
265
266
  // ---- 型伝播(導出ポートの解決) ----
266
267
  // 接続の両端は同じスキーマになる(値入力フォームのSchemaForm統一.md)。
267
268
  // wait は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
268
- // human はビューの宣言を転写("T" は素通しと同じ挙動。reject もビューの宣言)。
269
+ // human は実装(nodes/human/)の宣言を転写("T" は素通しと同じ挙動。reject もビューの宣言)。
269
270
  // 宣言 out(program / ai)は触らない。
270
271
  // program / ai の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
271
272
  // 上書きし、未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の
@@ -328,6 +329,9 @@ type PropagationNode = {
328
329
  id: string
329
330
  kind: string
330
331
  ports: GraphPort[]
332
+ // human のときだけ意味を持つ:実装(nodes/human/<dir>/)の宣言の写し(node_humans.declaration)。
333
+ // 無ければ in・approve は上流のコピー、reject は触らない
334
+ human?: { declaration?: HumanDeclaration } | null
331
335
  }
332
336
  type PortLink = { from_port_id: string; to_port_id: string }
333
337
 
@@ -338,23 +342,6 @@ export const COPY_UPSTREAM_SCHEMA = "T"
338
342
  // ポートの型宣言:具体型か、上流からのコピー指示か
339
343
  export type DeclaredPortSchema = JsonSchema | typeof COPY_UPSTREAM_SCHEMA
340
344
 
341
- // 伝播のオプション。sdk は承認ビューのカタログを知らない(非公開。企業向けビューの
342
- // キーが npm 公開物に載るのを避ける)ので、呼び出し元が解決して渡す
343
- // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
344
- export type PropagationOptions<N> = {
345
- // human の in / approve / reject に転写する型(承認ビューの宣言)。null / undefined
346
- // ならその node は対象外(human 以外)。上流ノードの kind は一切見ない。
347
- // reject は具体型のみ(上流からのコピー指示は意味を持たない)
348
- viewPortSchemasOf?: (node: N) =>
349
- | {
350
- in: DeclaredPortSchema
351
- approve: DeclaredPortSchema
352
- reject: JsonSchema
353
- }
354
- | null
355
- | undefined
356
- }
357
-
358
345
  // end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
359
346
  // resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
360
347
  // start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
@@ -380,7 +367,6 @@ const isDerivedPort = (kind: string, port: Pick<GraphPort, "io" | "key">) =>
380
367
  export const resolvePortSchemas = <N extends PropagationNode>(
381
368
  nodes: N[],
382
369
  connections: PortLink[],
383
- options?: PropagationOptions<N>,
384
370
  ): Map<string, JsonSchema> => {
385
371
  const resolved = new Map<string, JsonSchema>()
386
372
  const kindByPortId = new Map<string, string>()
@@ -487,6 +473,8 @@ export const resolvePortSchemas = <N extends PropagationNode>(
487
473
  if (isMirror) {
488
474
  for (const port of node.ports) {
489
475
  if (port.io !== "in") continue
476
+ // flexible な in は宣言(推奨型)を維持する(接続の型を問わない口)
477
+ if (port.flexible === true) continue
490
478
  const sources = inbound.get(port.id) ?? []
491
479
  const inSchema = mergeInboundSchemas(
492
480
  sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
@@ -502,13 +490,14 @@ export const resolvePortSchemas = <N extends PropagationNode>(
502
490
  const inSchema = mergeInboundSchemas(
503
491
  sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
504
492
  )
505
- // 承認ビューの宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
493
+ // 実装(nodes/human/<dir>/)の宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
506
494
  // "T" は「上流の out をコピー」(in)/「in と同型」(approve)。reject は具体型の宣言
507
495
  // のみで、宣言が無ければ未宣言のまま(mawaru が型を焼き込むことはしない)。
508
496
  // 上流の out は書き換えない:逆伝播すると fan-out 先のビューが食い違ったとき
509
497
  // 型割れが config.json へ write-through され、元の出力宣言を黙って壊す。
510
498
  // ズレは接続の型エラーで見せてユーザーの [修正する] で直す
511
- const declared = options?.viewPortSchemasOf?.(node)
499
+ const declared =
500
+ node.kind === "human" ? node.human?.declaration : undefined
512
501
  const effectiveIn =
513
502
  declared && declared.in !== COPY_UPSTREAM_SCHEMA
514
503
  ? declared.in
@@ -535,19 +524,81 @@ export const resolvePortSchemas = <N extends PropagationNode>(
535
524
 
536
525
  // ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
537
526
  // schema が変わらないポート・ノードは同じ参照を返す
527
+ // flexible の導出:start の in / out main は、接続先の in が1つ以上あり**全て** flexible
528
+ // (AI の宣言 in・契約で焼き込まれた component の in)のときだけ flexible。
529
+ // 非 flexible の接続先(program 等)が1つでも混ざれば型どおりの入力が要るので false。
530
+ // 直結のみ(wait 等の素通し経由は型と同じく流さない)。戻りは flexible になる port_id の集合
531
+ export const resolveFlexiblePorts = (
532
+ nodes: PropagationNode[],
533
+ connections: PortLink[],
534
+ ): Set<string> => {
535
+ const flexibleByPortId = new Map<string, boolean>()
536
+ for (const node of nodes) {
537
+ if (node.kind === "start") continue
538
+ for (const port of node.ports) {
539
+ if (port.io === "in")
540
+ flexibleByPortId.set(port.id, port.flexible === true)
541
+ }
542
+ }
543
+ const outbound = new Map<string, string[]>()
544
+ for (const conn of connections) {
545
+ const outs = outbound.get(conn.from_port_id) ?? []
546
+ outs.push(conn.to_port_id)
547
+ outbound.set(conn.from_port_id, outs)
548
+ }
549
+ const flexible = new Set<string>()
550
+ for (const node of nodes) {
551
+ if (node.kind !== "start") continue
552
+ const inPort = node.ports.find((p) => p.io === "in")
553
+ const outMain = node.ports.find((p) => p.io === "out" && p.key === "main")
554
+ if (!inPort || !outMain) continue
555
+ const targets = outbound.get(outMain.id) ?? []
556
+ if (
557
+ targets.length > 0 &&
558
+ targets.every((id) => flexibleByPortId.get(id) === true)
559
+ ) {
560
+ flexible.add(inPort.id)
561
+ flexible.add(outMain.id)
562
+ }
563
+ }
564
+ return flexible
565
+ }
566
+
567
+ // 接続の型が受け入れられるか:受け側が flexible なら常に可、それ以外は 伝播型 ⊆ 宣言。
568
+ // sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
569
+ // (判定が散ると表示と保存がズレる)
570
+ export const connectionAccepted = (
571
+ from: JsonSchema,
572
+ to: Pick<GraphPort, "schema" | "flexible">,
573
+ ): boolean => to.flexible === true || isSchemaSubset(from, to.schema)
574
+
538
575
  export const propagateDerivedPorts = <N extends PropagationNode>(
539
576
  nodes: N[],
540
577
  connections: PortLink[],
541
- options?: PropagationOptions<N>,
542
578
  ): N[] => {
543
- const resolved = resolvePortSchemas(nodes, connections, options)
579
+ const resolved = resolvePortSchemas(nodes, connections)
580
+ const flexiblePorts = resolveFlexiblePorts(nodes, connections)
544
581
  return nodes.map((node) => {
545
582
  let touched = false
546
583
  const ports = node.ports.map((port) => {
547
584
  const schema = resolved.get(port.id) ?? port.schema
548
- if (JSON.stringify(schema) === JSON.stringify(port.schema)) return port
585
+ // start の flexible は導出結果で置き換える(false は省略=キー無し)
586
+ const flexible =
587
+ node.kind === "start"
588
+ ? flexiblePorts.has(port.id) || undefined
589
+ : port.flexible
590
+ if (
591
+ JSON.stringify(schema) === JSON.stringify(port.schema) &&
592
+ flexible === port.flexible
593
+ )
594
+ return port
549
595
  touched = true
550
- return { ...port, schema }
596
+ const { flexible: _dropped, ...rest } = port
597
+ return {
598
+ ...rest,
599
+ schema,
600
+ ...(flexible ? { flexible } : {}),
601
+ } as typeof port
551
602
  })
552
603
  return touched ? { ...node, ports } : node
553
604
  })
@@ -1,7 +1,7 @@
1
- import { spawn } from "node:child_process";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { createServer } from "node:http";
4
3
  import { saveCredentials } from "./credentials.js";
4
+ import { openBrowser } from "./openBrowser.js";
5
5
  // `gh auth login` と同じループバック方式(RFC 8252)。
6
6
  // CLI が 127.0.0.1 の一時ポートで待ち受け、ブラウザで mawaru の /cli-auth を開く。
7
7
  // 承認するとブラウザがループバック URL へ**遷移**してトークンを渡す
@@ -11,20 +11,6 @@ const page = (title, message) => `<!doctype html><meta charset="utf-8"><title>${
11
11
  `<body style="font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">` +
12
12
  `<div style="text-align:center"><h1 style="font-size:1.25rem">${title}</h1>` +
13
13
  `<p style="color:#666">${message}</p></div></body>`;
14
- // ブラウザを開く(開けなければ URL を出して手で開いてもらう。依存は足さない)
15
- const openBrowser = (url) => {
16
- const [command, args] = process.platform === "darwin"
17
- ? ["open", [url]]
18
- : process.platform === "win32"
19
- ? ["cmd", ["/c", "start", "", url]]
20
- : ["xdg-open", [url]];
21
- try {
22
- spawn(command, args, { stdio: "ignore", detached: true }).unref();
23
- }
24
- catch {
25
- // 何もしない(URL は既に印字済み)
26
- }
27
- };
28
14
  export const runLogin = async (timeoutMs = 5 * 60 * 1000) => {
29
15
  // 別のローカルプロセスが投げ込んだ応答を受け付けないための照合値
30
16
  const state = randomUUID();
@@ -0,0 +1 @@
1
+ export declare const openBrowser: (url: string) => void;
@@ -0,0 +1,15 @@
1
+ import { spawn } from "node:child_process";
2
+ // ブラウザを開く(開けなければ URL を出して手で開いてもらう。依存は足さない)
3
+ export const openBrowser = (url) => {
4
+ const [command, args] = process.platform === "darwin"
5
+ ? ["open", [url]]
6
+ : process.platform === "win32"
7
+ ? ["cmd", ["/c", "start", "", url]]
8
+ : ["xdg-open", [url]];
9
+ try {
10
+ spawn(command, args, { stdio: "ignore", detached: true }).unref();
11
+ }
12
+ catch {
13
+ // 何もしない(URL は既に印字済み)
14
+ }
15
+ };
@@ -1,5 +1,5 @@
1
1
  import { type Credentials } from "./credentials.js";
2
- export type MawaruApi = (method: "GET" | "POST", path: string, body?: unknown) => Promise<{
2
+ export type MawaruApi = (method: "GET" | "POST" | "PUT", path: string, body?: unknown) => Promise<{
3
3
  status: number;
4
4
  body: unknown;
5
5
  }>;
@@ -5,14 +5,22 @@ export type PreviewOptions = {
5
5
  case?: string;
6
6
  surface?: SlackSurface;
7
7
  channel?: string;
8
+ once?: boolean;
8
9
  };
9
10
  export declare const parsePreviewArgs: (args: string[]) => PreviewOptions;
10
11
  type Log = (line: string) => void;
12
+ export type PreviewEnv = {
13
+ tenantId: string;
14
+ appUrl: string;
15
+ openUrl: (url: string) => void;
16
+ watch: (dir: string, onChange: () => Promise<void>) => Promise<void>;
17
+ };
11
18
  export declare const runPreviewWith: (params: {
12
19
  root: string;
13
20
  options: PreviewOptions;
14
21
  api: MawaruApi;
15
22
  log: Log;
23
+ env?: Partial<PreviewEnv>;
16
24
  }) => Promise<number>;
17
25
  export declare const runPreview: (args: string[]) => Promise<number>;
18
26
  export {};