@mawaru/sdk 0.34.0 → 0.36.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.
- package/_schemas/extract-component.test.ts +75 -0
- package/_schemas/extract-component.ts +24 -1
- package/_schemas/graph.test.ts +102 -7
- package/_schemas/graph.ts +17 -10
- package/_schemas/node.ts +3 -0
- package/_schemas/port-spec.test.ts +133 -0
- package/_schemas/port-spec.ts +53 -1
- package/dist/_schemas/extract-component.d.ts +1 -0
- package/dist/_schemas/extract-component.js +17 -2
- package/dist/_schemas/graph.d.ts +4 -0
- package/dist/_schemas/graph.js +12 -6
- package/dist/_schemas/node.d.ts +1 -0
- package/dist/_schemas/node.js +3 -0
- package/dist/_schemas/port-spec.d.ts +1 -0
- package/dist/_schemas/port-spec.js +43 -1
- package/package.json +1 -1
- package/skills/create-loop/SKILL.md +12 -2
|
@@ -417,3 +417,78 @@ describe("buildExtractPlan(子グラフと親置換の生成)", () => {
|
|
|
417
417
|
})
|
|
418
418
|
})
|
|
419
419
|
})
|
|
420
|
+
|
|
421
|
+
// forで配列を要素ごとに処理する(繰り返しの枠).md:繰り返しは結果を配列で返すので出口はちょうど1つ
|
|
422
|
+
describe("buildExtractPlan(singleExit:繰り返しの出口は1つ)", () => {
|
|
423
|
+
const childEndNode = (): GraphNode => ({
|
|
424
|
+
id: uuid(),
|
|
425
|
+
kind: "end",
|
|
426
|
+
name: "終了",
|
|
427
|
+
ports: [port("in", "in", {}), port("out", "main", {})],
|
|
428
|
+
hooks: [],
|
|
429
|
+
position_x: 560,
|
|
430
|
+
position_y: 200,
|
|
431
|
+
})
|
|
432
|
+
const plan = (
|
|
433
|
+
nodes: GraphNode[],
|
|
434
|
+
connections: { from_port_id: string; to_port_id: string }[],
|
|
435
|
+
selectedIds: string[],
|
|
436
|
+
childEnd = childEndNode(),
|
|
437
|
+
) =>
|
|
438
|
+
buildExtractPlan({
|
|
439
|
+
nodes,
|
|
440
|
+
connections,
|
|
441
|
+
selectedIds,
|
|
442
|
+
name: "繰り返し",
|
|
443
|
+
componentLoopId: uuid(),
|
|
444
|
+
childStartOutPortId: uuid(),
|
|
445
|
+
childEnd,
|
|
446
|
+
newId: uuid,
|
|
447
|
+
singleExit: true,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it("外へ出る線が無ければ、線の出ていない唯一の出力を出口にする(結果を返す)", () => {
|
|
451
|
+
const { d, nodes, connections } = chain()
|
|
452
|
+
const childEnd = childEndNode()
|
|
453
|
+
|
|
454
|
+
const result = plan(nodes, connections, [d.id], childEnd)
|
|
455
|
+
|
|
456
|
+
if (!result.ok) throw new Error(result.reason)
|
|
457
|
+
const [nd] = result.childNodes
|
|
458
|
+
if (!nd) throw new Error("コピーがない")
|
|
459
|
+
expect(result.childConnections).toContainEqual({
|
|
460
|
+
from_port_id: outOf(nd).id,
|
|
461
|
+
to_port_id: inOf(childEnd).id,
|
|
462
|
+
})
|
|
463
|
+
expect(result.groupNode.ports.filter((p) => p.io === "out")).toHaveLength(1)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it("外へ出る線があればそれが出口(ふだんの抽出と同じ)", () => {
|
|
467
|
+
const { b, c, nodes, connections } = chain()
|
|
468
|
+
|
|
469
|
+
const result = plan(nodes, connections, [b.id, c.id])
|
|
470
|
+
|
|
471
|
+
if (!result.ok) throw new Error(result.reason)
|
|
472
|
+
expect(result.childEnd.ports.filter((p) => p.io === "in")).toHaveLength(1)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it("出口が2つ以上・出口にできる出力が決まらない選択は拒否する", () => {
|
|
476
|
+
const a = makeNode("program", "A")
|
|
477
|
+
const b = makeNode("program", "B")
|
|
478
|
+
const c = makeNode("program", "C")
|
|
479
|
+
const d = makeNode("program", "D")
|
|
480
|
+
// B と C を選ぶ:B → D、C → D の2つの出口
|
|
481
|
+
const twoExits = plan(
|
|
482
|
+
[a, b, c, d],
|
|
483
|
+
[connect(a, b), connect(a, c), connect(b, d), connect(c, d)],
|
|
484
|
+
[b.id, c.id],
|
|
485
|
+
)
|
|
486
|
+
expect(twoExits.ok).toBe(false)
|
|
487
|
+
if (twoExits.ok) return
|
|
488
|
+
expect(twoExits.reason).toContain("出口を1つ")
|
|
489
|
+
|
|
490
|
+
// 外へ出る線が無く、線の出ていない出力が2つ(B と C)
|
|
491
|
+
const ambiguous = plan([b, c], [], [b.id, c.id])
|
|
492
|
+
expect(ambiguous.ok).toBe(false)
|
|
493
|
+
})
|
|
494
|
+
})
|
|
@@ -118,6 +118,10 @@ export const buildExtractPlan = (params: {
|
|
|
118
118
|
childStartOutPortId: string
|
|
119
119
|
childEnd: GraphNode
|
|
120
120
|
newId: () => string
|
|
121
|
+
// 繰り返し(for):結果を配列で返すので出口はちょうど1つ。外へ出る線が無ければ、
|
|
122
|
+
// 選択の中で線の出ていない唯一の出力を出口にする
|
|
123
|
+
// (forで配列を要素ごとに処理する(繰り返しの枠).md)
|
|
124
|
+
singleExit?: boolean
|
|
121
125
|
}): { ok: false; reason: string } | ({ ok: true } & ExtractPlan) => {
|
|
122
126
|
const {
|
|
123
127
|
nodes,
|
|
@@ -128,11 +132,30 @@ export const buildExtractPlan = (params: {
|
|
|
128
132
|
childStartOutPortId,
|
|
129
133
|
childEnd,
|
|
130
134
|
newId,
|
|
135
|
+
singleExit = false,
|
|
131
136
|
} = params
|
|
132
137
|
const analysis = analyzeExtractSelection(nodes, connections, selectedIds)
|
|
133
138
|
if (!analysis.ok) return analysis
|
|
134
139
|
|
|
135
140
|
const selected = new Set(selectedIds)
|
|
141
|
+
let exitPortIds = analysis.exitPortIds
|
|
142
|
+
if (singleExit) {
|
|
143
|
+
if (exitPortIds.length === 0) {
|
|
144
|
+
const wiredOut = new Set(analysis.internal.map((c) => c.from_port_id))
|
|
145
|
+
const free = nodes
|
|
146
|
+
.filter((n) => selected.has(n.id))
|
|
147
|
+
.flatMap((n) =>
|
|
148
|
+
n.ports
|
|
149
|
+
.filter((p) => p.io === "out" && !wiredOut.has(p.id))
|
|
150
|
+
.map((p) => p.id),
|
|
151
|
+
)
|
|
152
|
+
exitPortIds = free.length === 1 ? free : []
|
|
153
|
+
}
|
|
154
|
+
if (exitPortIds.length !== 1) {
|
|
155
|
+
return { ok: false, reason: "繰り返しの出口を1つにしてください" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
137
160
|
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
138
161
|
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
@@ -185,7 +208,7 @@ export const buildExtractPlan = (params: {
|
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
}
|
|
188
|
-
const exits =
|
|
211
|
+
const exits = exitPortIds.map((exitPortId, i) => {
|
|
189
212
|
const exitName = portById.get(exitPortId)?.name ?? "出力"
|
|
190
213
|
const seeded = i === 0 ? seededEndIns[0] : undefined
|
|
191
214
|
const endIn: GraphPort = seeded
|
package/_schemas/graph.test.ts
CHANGED
|
@@ -997,22 +997,117 @@ describe("saveGraphBodySchema:開始/終了ノード(start/end kind)", ()
|
|
|
997
997
|
).toThrow(/in ポートはちょうど1つ/)
|
|
998
998
|
})
|
|
999
999
|
|
|
1000
|
-
it("
|
|
1001
|
-
const s = startNode()
|
|
1000
|
+
it("end からの接続を拒否する", () => {
|
|
1002
1001
|
const e = endNode()
|
|
1003
1002
|
const other = makeNode()
|
|
1003
|
+
expect(() =>
|
|
1004
|
+
saveGraphBodySchema.parse({
|
|
1005
|
+
nodes: [e, other],
|
|
1006
|
+
connections: [connect(e, other)],
|
|
1007
|
+
}),
|
|
1008
|
+
).toThrow(/終了ノード/)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
// start の入口へ戻すと次の周回になる
|
|
1012
|
+
// (docs/tasks/wip/startノードに入力をつないで周回する(runの周回).md)
|
|
1013
|
+
it("start の入口へ1本つなげる(周回の最後から戻す)", () => {
|
|
1014
|
+
const s = startNode()
|
|
1015
|
+
const other = { ...makeNode(simplePorts()), kind: "wait" }
|
|
1004
1016
|
expect(() =>
|
|
1005
1017
|
saveGraphBodySchema.parse({
|
|
1006
1018
|
nodes: [s, other],
|
|
1007
|
-
connections: [connect(other, s)],
|
|
1019
|
+
connections: [connect(s, other), connect(other, s)],
|
|
1008
1020
|
}),
|
|
1009
|
-
).toThrow(
|
|
1021
|
+
).not.toThrow()
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
it("start の入口につなげる線は1本だけ", () => {
|
|
1025
|
+
const s = startNode()
|
|
1026
|
+
const a = makeNode()
|
|
1027
|
+
const b = makeNode()
|
|
1010
1028
|
expect(() =>
|
|
1011
1029
|
saveGraphBodySchema.parse({
|
|
1012
|
-
nodes: [
|
|
1013
|
-
connections: [connect(
|
|
1030
|
+
nodes: [s, a, b],
|
|
1031
|
+
connections: [connect(a, s), connect(b, s)],
|
|
1014
1032
|
}),
|
|
1015
|
-
).toThrow(
|
|
1033
|
+
).toThrow(/開始ノードの入口につなげる線は1本だけです/)
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
it("start の入口の型(直後のノードが受ける型)に合わない線を拒否する", () => {
|
|
1037
|
+
const s = startNode()
|
|
1038
|
+
const objectIn = {
|
|
1039
|
+
type: "object",
|
|
1040
|
+
properties: { title: { type: "string" } },
|
|
1041
|
+
required: ["title"],
|
|
1042
|
+
}
|
|
1043
|
+
const next = {
|
|
1044
|
+
...makeNode([
|
|
1045
|
+
{ ...port("in", "in"), schema: objectIn },
|
|
1046
|
+
port("out", "main"),
|
|
1047
|
+
]),
|
|
1048
|
+
kind: "program",
|
|
1049
|
+
}
|
|
1050
|
+
const last = {
|
|
1051
|
+
...makeNode([
|
|
1052
|
+
port("in", "in"),
|
|
1053
|
+
{ ...port("out", "main"), schema: { type: "string" } },
|
|
1054
|
+
]),
|
|
1055
|
+
kind: "program",
|
|
1056
|
+
}
|
|
1057
|
+
expect(() =>
|
|
1058
|
+
saveGraphBodySchema.parse({
|
|
1059
|
+
nodes: [s, next, last],
|
|
1060
|
+
connections: [connect(s, next), connect(next, last), connect(last, s)],
|
|
1061
|
+
}),
|
|
1062
|
+
).toThrow(/入力スキーマに適合しません/)
|
|
1063
|
+
})
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
// 合流は in を複数持てる(key 自由)+ out(main)
|
|
1067
|
+
const joinNode = (inKeys: string[] = ["in_1", "in_2"]): TestNode => ({
|
|
1068
|
+
...makeNode([...inKeys.map((k) => port("in", k)), port("out", "main")]),
|
|
1069
|
+
kind: "join",
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
describe("saveGraphBodySchema:合流ノード(join kind)", () => {
|
|
1073
|
+
it("2本の枝を合流させるグラフを受け付ける", () => {
|
|
1074
|
+
const a = makeNode(simplePorts())
|
|
1075
|
+
const b = makeNode(simplePorts())
|
|
1076
|
+
const j = joinNode()
|
|
1077
|
+
const [in1, in2] = j.ports.filter((p) => p.io === "in")
|
|
1078
|
+
const parsed = saveGraphBodySchema.parse({
|
|
1079
|
+
nodes: [a, b, j],
|
|
1080
|
+
connections: [
|
|
1081
|
+
{ from_port_id: outOf(a).id, to_port_id: in1?.id },
|
|
1082
|
+
{ from_port_id: outOf(b).id, to_port_id: in2?.id },
|
|
1083
|
+
],
|
|
1084
|
+
})
|
|
1085
|
+
expect(parsed.nodes.map((n) => n.kind)).toContain("join")
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
it("合流は複数置ける", () => {
|
|
1089
|
+
expect(() =>
|
|
1090
|
+
saveGraphBodySchema.parse({
|
|
1091
|
+
nodes: [joinNode(), joinNode()],
|
|
1092
|
+
connections: [],
|
|
1093
|
+
}),
|
|
1094
|
+
).not.toThrow()
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
it("in が無い合流を拒否する", () => {
|
|
1098
|
+
expect(() =>
|
|
1099
|
+
saveGraphBodySchema.parse({ nodes: [joinNode([])], connections: [] }),
|
|
1100
|
+
).toThrow(/in ポートが1つ以上/)
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
it("out は main ちょうど1つ", () => {
|
|
1104
|
+
const bad: TestNode = {
|
|
1105
|
+
...joinNode(),
|
|
1106
|
+
ports: [port("in", "in_1"), port("out", "main"), port("out", "extra")],
|
|
1107
|
+
}
|
|
1108
|
+
expect(() =>
|
|
1109
|
+
saveGraphBodySchema.parse({ nodes: [bad], connections: [] }),
|
|
1110
|
+
).toThrow(/join/)
|
|
1016
1111
|
})
|
|
1017
1112
|
})
|
|
1018
1113
|
|
package/_schemas/graph.ts
CHANGED
|
@@ -74,6 +74,7 @@ export const editableNodeKindSchema = nodeKindSchema.extract([
|
|
|
74
74
|
"human",
|
|
75
75
|
"program",
|
|
76
76
|
"wait",
|
|
77
|
+
"join",
|
|
77
78
|
])
|
|
78
79
|
export type EditableNodeKind = z.infer<typeof editableNodeKindSchema>
|
|
79
80
|
|
|
@@ -87,6 +88,7 @@ export const graphNodeKindSchema = nodeKindSchema.extract([
|
|
|
87
88
|
"start",
|
|
88
89
|
"end",
|
|
89
90
|
"component",
|
|
91
|
+
"join",
|
|
90
92
|
])
|
|
91
93
|
export type GraphNodeKind = z.infer<typeof graphNodeKindSchema>
|
|
92
94
|
|
|
@@ -384,18 +386,19 @@ export const saveGraphBodySchema = z
|
|
|
384
386
|
else outCount++
|
|
385
387
|
})
|
|
386
388
|
// end は「in は1つ以上」(複数ルートを別々の in で集約する)。
|
|
387
|
-
// ai も1つ以上(in は自由ポート:追加・削除・key
|
|
389
|
+
// ai も1つ以上(in は自由ポート:追加・削除・key 自由)。join も1つ以上
|
|
390
|
+
// (待ち合わせる枝ごとに in)。他 kind は1つ固定。
|
|
388
391
|
// key の妥当性は kindPortsViolation が別途見る
|
|
389
|
-
const
|
|
390
|
-
node.kind === "end" || node.kind === "ai"
|
|
392
|
+
const multiIn =
|
|
393
|
+
node.kind === "end" || node.kind === "ai" || node.kind === "join"
|
|
394
|
+
const inCountOk = multiIn ? inCount >= 1 : inCount === 1
|
|
391
395
|
if (!inCountOk) {
|
|
392
396
|
ctx.addIssue({
|
|
393
397
|
code: "custom",
|
|
394
398
|
path: ["nodes", i, "ports"],
|
|
395
|
-
message:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
: "in ポートはちょうど1つ必要です",
|
|
399
|
+
message: multiIn
|
|
400
|
+
? "in ポートが1つ以上必要です"
|
|
401
|
+
: "in ポートはちょうど1つ必要です",
|
|
399
402
|
})
|
|
400
403
|
}
|
|
401
404
|
if (outCount < 1) {
|
|
@@ -518,12 +521,16 @@ export const saveGraphBodySchema = z
|
|
|
518
521
|
const toNode = nodeByPortId.get(conn.to_port_id)
|
|
519
522
|
if (!from || !to || !fromNode || !toNode) return // 構造違反は検出済み
|
|
520
523
|
|
|
521
|
-
// start
|
|
522
|
-
|
|
524
|
+
// end からは引き出せない(終端)。start の入口へは戻せる(次の周回になる)が、
|
|
525
|
+
// 線は1本だけ(2本あると両方が着いたときに周回が2本並んで走る)
|
|
526
|
+
if (
|
|
527
|
+
toNode.kind === "start" &&
|
|
528
|
+
(inboundByPort.get(conn.to_port_id)?.length ?? 0) > 1
|
|
529
|
+
) {
|
|
523
530
|
ctx.addIssue({
|
|
524
531
|
code: "custom",
|
|
525
532
|
path: ["connections", i, "to_port_id"],
|
|
526
|
-
message: "
|
|
533
|
+
message: "開始ノードの入口につなげる線は1本だけです",
|
|
527
534
|
})
|
|
528
535
|
}
|
|
529
536
|
if (fromNode.kind === "end") {
|
package/_schemas/node.ts
CHANGED
|
@@ -13,6 +13,9 @@ export const nodeKindSchema = z.enum([
|
|
|
13
13
|
// コンポーネント(kind='component' の loop を配置して呼ぶ。子 run・戻り値あり。
|
|
14
14
|
// docs/tasks/done/コンポーネント(ループの部品化と再利用).md)
|
|
15
15
|
"component",
|
|
16
|
+
// 合流(線のつながった入口が全部揃うまで待ち、key ごとにまとめて流す。
|
|
17
|
+
// docs/tasks/wip/合流ノード(並行分岐の待ち合わせ).md)
|
|
18
|
+
"join",
|
|
16
19
|
])
|
|
17
20
|
export type NodeKind = z.infer<typeof nodeKindSchema>
|
|
18
21
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js"
|
|
3
3
|
import {
|
|
4
|
+
arrayOf,
|
|
4
5
|
dataFormatOf,
|
|
5
6
|
dataSchemaOf,
|
|
6
7
|
fileSchema,
|
|
@@ -782,6 +783,111 @@ describe("型伝播:fan-in(同じ in ポートへの流入2本以上)", ()
|
|
|
782
783
|
})
|
|
783
784
|
})
|
|
784
785
|
|
|
786
|
+
describe("合流(join)", () => {
|
|
787
|
+
const findings = {
|
|
788
|
+
type: "object",
|
|
789
|
+
properties: { findings: { type: "array" } },
|
|
790
|
+
required: ["findings"],
|
|
791
|
+
}
|
|
792
|
+
const source = (schema: JsonSchema) => {
|
|
793
|
+
const out = port("out", "main", schema)
|
|
794
|
+
return {
|
|
795
|
+
node: { id: uuid(), kind: "program", ports: [port("in", "in"), out] },
|
|
796
|
+
out,
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const named = (key: string, name: string): GraphPort => ({
|
|
800
|
+
...port("in", key),
|
|
801
|
+
name,
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
it("join は in の key 自由・1つ以上、out は main ちょうど1つ", () => {
|
|
805
|
+
expect(
|
|
806
|
+
kindPortsViolation("join", [
|
|
807
|
+
port("in", "in_1"),
|
|
808
|
+
port("in", "in_2"),
|
|
809
|
+
port("out", "main"),
|
|
810
|
+
]),
|
|
811
|
+
).toBeNull()
|
|
812
|
+
expect(
|
|
813
|
+
kindPortsViolation("join", [port("in", "in_1"), port("out", "done")]),
|
|
814
|
+
).toMatch(/join/)
|
|
815
|
+
expect(
|
|
816
|
+
kindPortsViolation("join", [
|
|
817
|
+
port("in", "in_1"),
|
|
818
|
+
port("out", "main"),
|
|
819
|
+
port("out", "extra"),
|
|
820
|
+
]),
|
|
821
|
+
).toMatch(/join/)
|
|
822
|
+
expect(kindPortsViolation("join", [port("out", "main")])).toMatch(/join/)
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
it("各 in は自分の上流から、out main は線のつながった in を key ごとに持つ object", () => {
|
|
826
|
+
const a = source(findings)
|
|
827
|
+
const b = source({ type: "string" })
|
|
828
|
+
const in1 = named("in_1", "スキャン")
|
|
829
|
+
const in2 = named("in_2", "依存チェック")
|
|
830
|
+
const loose = named("in_3", "未接続")
|
|
831
|
+
const out = port("out", "main")
|
|
832
|
+
const join = { id: uuid(), kind: "join", ports: [in1, in2, loose, out] }
|
|
833
|
+
const resolved = resolvePortSchemas(
|
|
834
|
+
[a.node, b.node, join],
|
|
835
|
+
[
|
|
836
|
+
{ from_port_id: a.out.id, to_port_id: in1.id },
|
|
837
|
+
{ from_port_id: b.out.id, to_port_id: in2.id },
|
|
838
|
+
],
|
|
839
|
+
)
|
|
840
|
+
expect(resolved.get(in1.id)).toEqual(findings)
|
|
841
|
+
expect(resolved.get(in2.id)).toEqual({ type: "string" })
|
|
842
|
+
// 未接続の in は待たない=出口にも出ない
|
|
843
|
+
expect(resolved.get(out.id)).toEqual({
|
|
844
|
+
type: "object",
|
|
845
|
+
properties: {
|
|
846
|
+
in_1: { ...findings, title: "スキャン" },
|
|
847
|
+
in_2: { type: "string", title: "依存チェック" },
|
|
848
|
+
},
|
|
849
|
+
required: ["in_1", "in_2"],
|
|
850
|
+
})
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it("out main の型は下流の program の in へ鏡映される", () => {
|
|
854
|
+
const a = source(findings)
|
|
855
|
+
const in1 = port("in", "in_1")
|
|
856
|
+
const out = port("out", "main")
|
|
857
|
+
const join = { id: uuid(), kind: "join", ports: [in1, out] }
|
|
858
|
+
const nextIn = port("in", "in")
|
|
859
|
+
const next = {
|
|
860
|
+
id: uuid(),
|
|
861
|
+
kind: "program",
|
|
862
|
+
ports: [nextIn, port("out", "main")],
|
|
863
|
+
}
|
|
864
|
+
const resolved = resolvePortSchemas(
|
|
865
|
+
[a.node, join, next],
|
|
866
|
+
[
|
|
867
|
+
{ from_port_id: a.out.id, to_port_id: in1.id },
|
|
868
|
+
{ from_port_id: out.id, to_port_id: nextIn.id },
|
|
869
|
+
],
|
|
870
|
+
)
|
|
871
|
+
expect(resolved.get(nextIn.id)).toEqual({
|
|
872
|
+
type: "object",
|
|
873
|
+
properties: { in_1: { ...findings, title: "in_1" } },
|
|
874
|
+
required: ["in_1"],
|
|
875
|
+
})
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it("線が1本も無ければ out main は未宣言", () => {
|
|
879
|
+
const out = port("out", "main")
|
|
880
|
+
const join = {
|
|
881
|
+
id: uuid(),
|
|
882
|
+
kind: "join",
|
|
883
|
+
ports: [port("in", "in_1"), out],
|
|
884
|
+
}
|
|
885
|
+
expect(resolvePortSchemas([join], []).get(out.id)).toEqual(
|
|
886
|
+
UNDECLARED_SCHEMA,
|
|
887
|
+
)
|
|
888
|
+
})
|
|
889
|
+
})
|
|
890
|
+
|
|
785
891
|
describe("file 型(nominal)", () => {
|
|
786
892
|
it("fileSchema はマーカー付き object、isFileSchema で識別できる", () => {
|
|
787
893
|
const schema = fileSchema()
|
|
@@ -1011,3 +1117,30 @@ describe("schemaTypeKey(型としての同一性)", () => {
|
|
|
1011
1117
|
).not.toBe(schemaTypeKey({ type: "array", items: [{ type: "number" }] }))
|
|
1012
1118
|
})
|
|
1013
1119
|
})
|
|
1120
|
+
|
|
1121
|
+
describe("arrayOf(for の配置ポート:1件分の型を配列に包む)", () => {
|
|
1122
|
+
it("宣言された型は items に入れた配列になる", () => {
|
|
1123
|
+
const error = { type: "object", properties: { id: { type: "string" } } }
|
|
1124
|
+
expect(arrayOf(error)).toEqual({ type: "array", items: error })
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it("未宣言の型は items を持たない配列になる(要素の型は判断できないまま)", () => {
|
|
1128
|
+
expect(arrayOf(UNDECLARED_SCHEMA)).toEqual({ type: "array" })
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
it("配列にした型同士は要素の型で適合を判定する", () => {
|
|
1132
|
+
const narrow = {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
properties: { id: { type: "string" }, title: { type: "string" } },
|
|
1135
|
+
required: ["id", "title"],
|
|
1136
|
+
}
|
|
1137
|
+
const wide = {
|
|
1138
|
+
type: "object",
|
|
1139
|
+
properties: { id: { type: "string" } },
|
|
1140
|
+
required: ["id"],
|
|
1141
|
+
}
|
|
1142
|
+
expect(schemaFit(arrayOf(narrow), arrayOf(wide))).toBe("ok")
|
|
1143
|
+
expect(schemaFit(arrayOf(wide), arrayOf(narrow))).toBe("mismatch")
|
|
1144
|
+
expect(schemaFit(wide, arrayOf(wide))).toBe("mismatch")
|
|
1145
|
+
})
|
|
1146
|
+
})
|
package/_schemas/port-spec.ts
CHANGED
|
@@ -15,6 +15,14 @@ export const UNDECLARED_SCHEMA: JsonSchema = {}
|
|
|
15
15
|
export const isUndeclaredSchema = (schema: JsonSchema): boolean =>
|
|
16
16
|
Object.keys(schema).length === 0 || schema.type === undefined
|
|
17
17
|
|
|
18
|
+
// 1件分の型を配列に包む(for の配置ポート:in = 中身の start.in の配列、out = 中身の
|
|
19
|
+
// end の in の配列。docs/tasks/done/forで配列を要素ごとに処理する(繰り返しの枠).md)。
|
|
20
|
+
// 未宣言は items を持たせない(要素の型は判断できないまま)
|
|
21
|
+
export const arrayOf = (schema: JsonSchema): JsonSchema =>
|
|
22
|
+
isUndeclaredSchema(schema)
|
|
23
|
+
? { type: "array" }
|
|
24
|
+
: { type: "array", items: schema }
|
|
25
|
+
|
|
18
26
|
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
19
27
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
20
28
|
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
|
@@ -198,6 +206,19 @@ export const kindPortsViolation = (
|
|
|
198
206
|
}
|
|
199
207
|
return null
|
|
200
208
|
}
|
|
209
|
+
// join は out が main 1つ固定・in は1つ以上(待ち合わせる枝ごとに in。key 自由。
|
|
210
|
+
// 合流ノード(並行分岐の待ち合わせ).md)
|
|
211
|
+
if (kind === "join") {
|
|
212
|
+
const outKeys = ports.filter((p) => p.io === "out").map((p) => p.key)
|
|
213
|
+
if (
|
|
214
|
+
outKeys.length !== 1 ||
|
|
215
|
+
outKeys[0] !== "main" ||
|
|
216
|
+
!ports.some((p) => p.io === "in")
|
|
217
|
+
) {
|
|
218
|
+
return `join のポート構成は in 1つ以上と out "main" ちょうど1つです`
|
|
219
|
+
}
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
201
222
|
if (kind === "human") {
|
|
202
223
|
const inKeys = ports.filter((p) => p.io === "in").map((p) => p.key)
|
|
203
224
|
const outKeys = ports.filter((p) => p.io === "out").map((p) => p.key)
|
|
@@ -338,7 +359,8 @@ export type PropagationOptions<N> = {
|
|
|
338
359
|
// resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
|
|
339
360
|
// start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
|
|
340
361
|
// (run 入力契約=接続先の入力宣言。開始ノードの入力契約を接続先から導出.md)
|
|
341
|
-
|
|
362
|
+
// join も導出ノード(各 in は上流から、out main は線のつながった in を key ごとに持つ object)
|
|
363
|
+
const DERIVED_KINDS = new Set(["wait", "human", "end", "start", "join"])
|
|
342
364
|
|
|
343
365
|
// in を接続鏡映(sticky)するノード:接続元 out が具体型のときだけ上書き
|
|
344
366
|
const MIRROR_KINDS = new Set(["program", "ai"])
|
|
@@ -428,6 +450,36 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
428
450
|
}
|
|
429
451
|
continue
|
|
430
452
|
}
|
|
453
|
+
// join:各 in を自分の上流から導出(end と同じ)。out main は線のつながった in を
|
|
454
|
+
// key ごとに持つ object(「両方」を1段入れて持つ。中身を平らに混ぜると key が
|
|
455
|
+
// ぶつかる)。待つのは線のつながった in だけなので、未接続の in は出口に出さない
|
|
456
|
+
if (node.kind === "join") {
|
|
457
|
+
const properties: Record<string, JsonSchema> = {}
|
|
458
|
+
for (const port of node.ports) {
|
|
459
|
+
if (port.io !== "in") continue
|
|
460
|
+
const srcs = inbound.get(port.id) ?? []
|
|
461
|
+
const schema = mergeInboundSchemas(
|
|
462
|
+
srcs.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
|
|
463
|
+
)
|
|
464
|
+
assign(port.id, schema)
|
|
465
|
+
if (srcs.length > 0) {
|
|
466
|
+
properties[port.key] = { ...schema, title: port.name }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const outMain = node.ports.find(
|
|
470
|
+
(p) => p.io === "out" && p.key === "main",
|
|
471
|
+
)
|
|
472
|
+
const keys = Object.keys(properties)
|
|
473
|
+
if (outMain) {
|
|
474
|
+
assign(
|
|
475
|
+
outMain.id,
|
|
476
|
+
keys.length > 0
|
|
477
|
+
? { type: "object", properties, required: keys }
|
|
478
|
+
: UNDECLARED_SCHEMA,
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
continue
|
|
482
|
+
}
|
|
431
483
|
const isMirror = MIRROR_KINDS.has(node.kind)
|
|
432
484
|
if (!DERIVED_KINDS.has(node.kind) && !isMirror) continue
|
|
433
485
|
// 鏡映(program / ai):全ての in が対象。具体型が流れてきたときだけ上書きする
|
|
@@ -55,11 +55,26 @@ export const analyzeExtractSelection = (nodes, connections, selectedIds) => {
|
|
|
55
55
|
// end の in が1つの loop は out main の1口として振る舞う)
|
|
56
56
|
const SINGLE_OUT_KEY = "main";
|
|
57
57
|
export const buildExtractPlan = (params) => {
|
|
58
|
-
const { nodes, connections, selectedIds, name, componentLoopId, childStartOutPortId, childEnd, newId, } = params;
|
|
58
|
+
const { nodes, connections, selectedIds, name, componentLoopId, childStartOutPortId, childEnd, newId, singleExit = false, } = params;
|
|
59
59
|
const analysis = analyzeExtractSelection(nodes, connections, selectedIds);
|
|
60
60
|
if (!analysis.ok)
|
|
61
61
|
return analysis;
|
|
62
62
|
const selected = new Set(selectedIds);
|
|
63
|
+
let exitPortIds = analysis.exitPortIds;
|
|
64
|
+
if (singleExit) {
|
|
65
|
+
if (exitPortIds.length === 0) {
|
|
66
|
+
const wiredOut = new Set(analysis.internal.map((c) => c.from_port_id));
|
|
67
|
+
const free = nodes
|
|
68
|
+
.filter((n) => selected.has(n.id))
|
|
69
|
+
.flatMap((n) => n.ports
|
|
70
|
+
.filter((p) => p.io === "out" && !wiredOut.has(p.id))
|
|
71
|
+
.map((p) => p.id));
|
|
72
|
+
exitPortIds = free.length === 1 ? free : [];
|
|
73
|
+
}
|
|
74
|
+
if (exitPortIds.length !== 1) {
|
|
75
|
+
return { ok: false, reason: "繰り返しの出口を1つにしてください" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
63
78
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
64
79
|
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
65
80
|
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
@@ -108,7 +123,7 @@ export const buildExtractPlan = (params) => {
|
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
};
|
|
111
|
-
const exits =
|
|
126
|
+
const exits = exitPortIds.map((exitPortId, i) => {
|
|
112
127
|
const exitName = portById.get(exitPortId)?.name ?? "出力";
|
|
113
128
|
const seeded = i === 0 ? seededEndIns[0] : undefined;
|
|
114
129
|
const endIn = seeded
|
package/dist/_schemas/graph.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type LoopRefs = z.infer<typeof loopRefsSchema>;
|
|
|
11
11
|
export declare const EMPTY_LOOP_REFS: LoopRefs;
|
|
12
12
|
export declare const refsDefaultsOf: (refs: LoopRefs) => RefsBag;
|
|
13
13
|
export declare const editableNodeKindSchema: z.ZodEnum<{
|
|
14
|
+
join: "join";
|
|
14
15
|
ai: "ai";
|
|
15
16
|
human: "human";
|
|
16
17
|
program: "program";
|
|
@@ -19,6 +20,7 @@ export declare const editableNodeKindSchema: z.ZodEnum<{
|
|
|
19
20
|
export type EditableNodeKind = z.infer<typeof editableNodeKindSchema>;
|
|
20
21
|
export declare const graphNodeKindSchema: z.ZodEnum<{
|
|
21
22
|
start: "start";
|
|
23
|
+
join: "join";
|
|
22
24
|
ai: "ai";
|
|
23
25
|
human: "human";
|
|
24
26
|
program: "program";
|
|
@@ -131,6 +133,7 @@ export declare const graphNodeSchema: z.ZodObject<{
|
|
|
131
133
|
node_key: z.ZodOptional<z.ZodUUID>;
|
|
132
134
|
kind: z.ZodEnum<{
|
|
133
135
|
start: "start";
|
|
136
|
+
join: "join";
|
|
134
137
|
ai: "ai";
|
|
135
138
|
human: "human";
|
|
136
139
|
program: "program";
|
|
@@ -230,6 +233,7 @@ export declare const saveGraphBodySchema: z.ZodObject<{
|
|
|
230
233
|
node_key: z.ZodOptional<z.ZodUUID>;
|
|
231
234
|
kind: z.ZodEnum<{
|
|
232
235
|
start: "start";
|
|
236
|
+
join: "join";
|
|
233
237
|
ai: "ai";
|
|
234
238
|
human: "human";
|
|
235
239
|
program: "program";
|
package/dist/_schemas/graph.js
CHANGED
|
@@ -52,6 +52,7 @@ export const editableNodeKindSchema = nodeKindSchema.extract([
|
|
|
52
52
|
"human",
|
|
53
53
|
"program",
|
|
54
54
|
"wait",
|
|
55
|
+
"join",
|
|
55
56
|
]);
|
|
56
57
|
// グラフ一括保存で受け付ける kind(パレット作成の4種+構造ノード start/end)。
|
|
57
58
|
// start/end はパレットに出さないが、seed 済みなので全置換ボディには常に含まれる
|
|
@@ -63,6 +64,7 @@ export const graphNodeKindSchema = nodeKindSchema.extract([
|
|
|
63
64
|
"start",
|
|
64
65
|
"end",
|
|
65
66
|
"component",
|
|
67
|
+
"join",
|
|
66
68
|
]);
|
|
67
69
|
// Wait ノードの kind 別設定(node_waits の wait_type に対応する判別ユニオン)。
|
|
68
70
|
// duration=経過秒、cron=cron 式(次に来る該当時刻まで1回待つ)。
|
|
@@ -319,14 +321,16 @@ export const saveGraphBodySchema = z
|
|
|
319
321
|
outCount++;
|
|
320
322
|
});
|
|
321
323
|
// end は「in は1つ以上」(複数ルートを別々の in で集約する)。
|
|
322
|
-
// ai も1つ以上(in は自由ポート:追加・削除・key
|
|
324
|
+
// ai も1つ以上(in は自由ポート:追加・削除・key 自由)。join も1つ以上
|
|
325
|
+
// (待ち合わせる枝ごとに in)。他 kind は1つ固定。
|
|
323
326
|
// key の妥当性は kindPortsViolation が別途見る
|
|
324
|
-
const
|
|
327
|
+
const multiIn = node.kind === "end" || node.kind === "ai" || node.kind === "join";
|
|
328
|
+
const inCountOk = multiIn ? inCount >= 1 : inCount === 1;
|
|
325
329
|
if (!inCountOk) {
|
|
326
330
|
ctx.addIssue({
|
|
327
331
|
code: "custom",
|
|
328
332
|
path: ["nodes", i, "ports"],
|
|
329
|
-
message:
|
|
333
|
+
message: multiIn
|
|
330
334
|
? "in ポートが1つ以上必要です"
|
|
331
335
|
: "in ポートはちょうど1つ必要です",
|
|
332
336
|
});
|
|
@@ -443,12 +447,14 @@ export const saveGraphBodySchema = z
|
|
|
443
447
|
const toNode = nodeByPortId.get(conn.to_port_id);
|
|
444
448
|
if (!from || !to || !fromNode || !toNode)
|
|
445
449
|
return; // 構造違反は検出済み
|
|
446
|
-
// start
|
|
447
|
-
|
|
450
|
+
// end からは引き出せない(終端)。start の入口へは戻せる(次の周回になる)が、
|
|
451
|
+
// 線は1本だけ(2本あると両方が着いたときに周回が2本並んで走る)
|
|
452
|
+
if (toNode.kind === "start" &&
|
|
453
|
+
(inboundByPort.get(conn.to_port_id)?.length ?? 0) > 1) {
|
|
448
454
|
ctx.addIssue({
|
|
449
455
|
code: "custom",
|
|
450
456
|
path: ["connections", i, "to_port_id"],
|
|
451
|
-
message: "
|
|
457
|
+
message: "開始ノードの入口につなげる線は1本だけです",
|
|
452
458
|
});
|
|
453
459
|
}
|
|
454
460
|
if (fromNode.kind === "end") {
|
package/dist/_schemas/node.d.ts
CHANGED
package/dist/_schemas/node.js
CHANGED
|
@@ -12,6 +12,9 @@ export const nodeKindSchema = z.enum([
|
|
|
12
12
|
// コンポーネント(kind='component' の loop を配置して呼ぶ。子 run・戻り値あり。
|
|
13
13
|
// docs/tasks/done/コンポーネント(ループの部品化と再利用).md)
|
|
14
14
|
"component",
|
|
15
|
+
// 合流(線のつながった入口が全部揃うまで待ち、key ごとにまとめて流す。
|
|
16
|
+
// docs/tasks/wip/合流ノード(並行分岐の待ち合わせ).md)
|
|
17
|
+
"join",
|
|
15
18
|
]);
|
|
16
19
|
// ポート key(input/output 用の参照キー)の規則(db.md §2)。
|
|
17
20
|
// name(ユーザーが見る表示名)は自由テキスト
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js";
|
|
3
3
|
export declare const UNDECLARED_SCHEMA: JsonSchema;
|
|
4
4
|
export declare const isUndeclaredSchema: (schema: JsonSchema) => boolean;
|
|
5
|
+
export declare const arrayOf: (schema: JsonSchema) => JsonSchema;
|
|
5
6
|
export declare const FILE_SCHEMA_MARKER = "x-mawaru-type";
|
|
6
7
|
export declare const FILE_SCHEMA_TYPE = "file";
|
|
7
8
|
export declare const fileSchema: () => JsonSchema;
|
|
@@ -9,6 +9,12 @@ import { z } from "zod";
|
|
|
9
9
|
// (docs/tasks/done/ポートスキーマのany撲滅.md)
|
|
10
10
|
export const UNDECLARED_SCHEMA = {};
|
|
11
11
|
export const isUndeclaredSchema = (schema) => Object.keys(schema).length === 0 || schema.type === undefined;
|
|
12
|
+
// 1件分の型を配列に包む(for の配置ポート:in = 中身の start.in の配列、out = 中身の
|
|
13
|
+
// end の in の配列。docs/tasks/done/forで配列を要素ごとに処理する(繰り返しの枠).md)。
|
|
14
|
+
// 未宣言は items を持たせない(要素の型は判断できないまま)
|
|
15
|
+
export const arrayOf = (schema) => isUndeclaredSchema(schema)
|
|
16
|
+
? { type: "array" }
|
|
17
|
+
: { type: "array", items: schema };
|
|
12
18
|
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
13
19
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
14
20
|
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
|
@@ -165,6 +171,17 @@ export const kindPortsViolation = (kind, ports) => {
|
|
|
165
171
|
}
|
|
166
172
|
return null;
|
|
167
173
|
}
|
|
174
|
+
// join は out が main 1つ固定・in は1つ以上(待ち合わせる枝ごとに in。key 自由。
|
|
175
|
+
// 合流ノード(並行分岐の待ち合わせ).md)
|
|
176
|
+
if (kind === "join") {
|
|
177
|
+
const outKeys = ports.filter((p) => p.io === "out").map((p) => p.key);
|
|
178
|
+
if (outKeys.length !== 1 ||
|
|
179
|
+
outKeys[0] !== "main" ||
|
|
180
|
+
!ports.some((p) => p.io === "in")) {
|
|
181
|
+
return `join のポート構成は in 1つ以上と out "main" ちょうど1つです`;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
168
185
|
if (kind === "human") {
|
|
169
186
|
const inKeys = ports.filter((p) => p.io === "in").map((p) => p.key);
|
|
170
187
|
const outKeys = ports.filter((p) => p.io === "out").map((p) => p.key);
|
|
@@ -263,7 +280,8 @@ export const COPY_UPSTREAM_SCHEMA = "T";
|
|
|
263
280
|
// resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
|
|
264
281
|
// start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
|
|
265
282
|
// (run 入力契約=接続先の入力宣言。開始ノードの入力契約を接続先から導出.md)
|
|
266
|
-
|
|
283
|
+
// join も導出ノード(各 in は上流から、out main は線のつながった in を key ごとに持つ object)
|
|
284
|
+
const DERIVED_KINDS = new Set(["wait", "human", "end", "start", "join"]);
|
|
267
285
|
// in を接続鏡映(sticky)するノード:接続元 out が具体型のときだけ上書き
|
|
268
286
|
const MIRROR_KINDS = new Set(["program", "ai"]);
|
|
269
287
|
const isDerivedPort = (kind, port) => DERIVED_KINDS.has(kind) &&
|
|
@@ -331,6 +349,30 @@ export const resolvePortSchemas = (nodes, connections, options) => {
|
|
|
331
349
|
}
|
|
332
350
|
continue;
|
|
333
351
|
}
|
|
352
|
+
// join:各 in を自分の上流から導出(end と同じ)。out main は線のつながった in を
|
|
353
|
+
// key ごとに持つ object(「両方」を1段入れて持つ。中身を平らに混ぜると key が
|
|
354
|
+
// ぶつかる)。待つのは線のつながった in だけなので、未接続の in は出口に出さない
|
|
355
|
+
if (node.kind === "join") {
|
|
356
|
+
const properties = {};
|
|
357
|
+
for (const port of node.ports) {
|
|
358
|
+
if (port.io !== "in")
|
|
359
|
+
continue;
|
|
360
|
+
const srcs = inbound.get(port.id) ?? [];
|
|
361
|
+
const schema = mergeInboundSchemas(srcs.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA));
|
|
362
|
+
assign(port.id, schema);
|
|
363
|
+
if (srcs.length > 0) {
|
|
364
|
+
properties[port.key] = { ...schema, title: port.name };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const outMain = node.ports.find((p) => p.io === "out" && p.key === "main");
|
|
368
|
+
const keys = Object.keys(properties);
|
|
369
|
+
if (outMain) {
|
|
370
|
+
assign(outMain.id, keys.length > 0
|
|
371
|
+
? { type: "object", properties, required: keys }
|
|
372
|
+
: UNDECLARED_SCHEMA);
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
334
376
|
const isMirror = MIRROR_KINDS.has(node.kind);
|
|
335
377
|
if (!DERIVED_KINDS.has(node.kind) && !isMirror)
|
|
336
378
|
continue;
|
package/package.json
CHANGED
|
@@ -73,7 +73,16 @@ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等
|
|
|
73
73
|
|
|
74
74
|
## 手順
|
|
75
75
|
|
|
76
|
-
1. **設計**:要件から必要なノード(program / ai / human / wait)と流れを決め、ユーザーに一言で確認する。
|
|
76
|
+
1. **設計**:要件から必要なノード(program / ai / human / wait / join)と流れを決め、ユーザーに一言で確認する。
|
|
77
|
+
1つの出口から複数の線を引くと枝が並行に走る(fan-out)。枝の結果を待ち合わせるときは
|
|
78
|
+
合流(`join`)を置く:線のつながった入口が全部揃うまで待ち、`{ [入口の key]: 値 }` にまとめて
|
|
79
|
+
`main` から流す。並行した枝を合流させずに同じノード(待機など)へ戻すと、枝の数だけ走り続ける。
|
|
80
|
+
配列の要素ごとに同じ手順や別ループを回すときは繰り返し(for)にする:`POST loops` に
|
|
81
|
+
`kind: "for"` と `owner_loop_id`(このループ)を渡して中身の loop を作り、その graph
|
|
82
|
+
(start → 1件分の処理 → end)を保存してから、このループに component ノード
|
|
83
|
+
(`component.component_loop_id`=作った for)を置く。配置ノードの in / out は中身の型の配列に
|
|
84
|
+
自動で焼き込まれ、全件そろうと入力と同じ順の配列で `main` から流れる(1件ずつは同時に走る)。
|
|
85
|
+
中身の出口(end につないだ in)は1つにする。
|
|
77
86
|
2. **repo 側の準備**:足りない program / ai は `nodes/program/<dir>/` / `nodes/ai/<dir>/` に作る
|
|
78
87
|
(config.json → `npx mawaru typegen` → main.ts 実装 → `npx mawaru validate`)。
|
|
79
88
|
**AI ノードの決定論的な処理は prompt ではなく `setup.sh` に書く**(下記)。
|
|
@@ -84,7 +93,8 @@ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等
|
|
|
84
93
|
- node / port の `id` はクライアント採番(`uuidgen` などで UUID を作る)
|
|
85
94
|
- program / ai ノードの ports は repo の config.json の inputs / outputs をそのまま写す
|
|
86
95
|
(in ポート key は `in`、out ポートは outputs の各 key)
|
|
87
|
-
- kind 別の固定ポート構成(human / wait / ai)は `port-spec.ts` の `kindPortsViolation`
|
|
96
|
+
- kind 別の固定ポート構成(human / wait / ai / join)は `port-spec.ts` の `kindPortsViolation` に従う。
|
|
97
|
+
join は in を枝の数だけ(key 自由。例 `in_1`, `in_2`)+ out `main`。出口の型は自動で決まる
|
|
88
98
|
- ループ変数(refs=run 全体で読める key→値 の袋)が要るならトップレベルの `refs` に入れる。
|
|
89
99
|
ノード単位の宣言やバインドは無い。handler は実行時に `refs.json` で全量を受け取り、
|
|
90
100
|
`end` の `refs` パッチで値を更新できる(config.json の `refs` はループ定義からの転写=生成物)。
|