@mawaru/sdk 0.13.0 → 0.15.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/ai-manifest.test.ts +54 -59
- package/_schemas/ai-manifest.ts +38 -23
- package/_schemas/duplicate-selection.test.ts +20 -0
- package/_schemas/duplicate-selection.ts +18 -2
- package/_schemas/extract-component.test.ts +201 -24
- package/_schemas/extract-component.ts +137 -54
- package/_schemas/graph.test.ts +95 -72
- package/_schemas/graph.ts +22 -29
- package/_schemas/node.ts +4 -0
- package/_schemas/port-spec.test.ts +120 -129
- package/_schemas/port-spec.ts +76 -111
- package/dist/_schemas/ai-manifest.d.ts +12 -1
- package/dist/_schemas/ai-manifest.js +35 -20
- package/dist/_schemas/duplicate-selection.js +18 -2
- package/dist/_schemas/extract-component.d.ts +4 -3
- package/dist/_schemas/extract-component.js +105 -38
- package/dist/_schemas/graph.d.ts +18 -0
- package/dist/_schemas/graph.js +21 -26
- package/dist/_schemas/node.d.ts +1 -0
- package/dist/_schemas/node.js +4 -0
- package/dist/_schemas/port-spec.d.ts +2 -5
- package/dist/_schemas/port-spec.js +63 -94
- package/dist/typegen.js +6 -6
- package/docs/development.md +5 -1
- package/package.json +2 -3
- package/skills/create-loop/SKILL.md +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/echo/main.ts +1 -1
- package/templates/mawaru-runner.yml +1 -1
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { GraphConnection, GraphNode } from "./graph.js"
|
|
2
|
+
import type { GraphPort } from "./node.js"
|
|
2
3
|
|
|
3
|
-
// 矩形選択からのグループ化・コンポーネント化(ノードのグループ化・コンポーネント化.md
|
|
4
|
+
// 矩形選択からのグループ化・コンポーネント化(ノードのグループ化・コンポーネント化.md/
|
|
5
|
+
// コンポーネントの複数入口・複数出口.md)。
|
|
4
6
|
// 選択境界を分類・検証し(analyze)、「子(部品)loop の graph」と「親の置換後グラフ」を
|
|
5
7
|
// 生成する(build)。純関数(id 採番は newId で注入)。エディタとテストの両方から使う。
|
|
6
8
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
+
// 境界は start / end が吸収し、成立条件は型だけで決める(配線の由来は見ない):
|
|
10
|
+
// - 入口:外→選択内の線の到達先ポート。複数可。条件は全入口ポートの型が同一なこと
|
|
11
|
+
// (子では start.main から全入口へ fan-out する)
|
|
12
|
+
// - 出口:選択内→外の線の出発ポート。複数可・無条件(子では出発ポートごとに end の
|
|
13
|
+
// in を1つ作り、component の out として親へ射影する)
|
|
9
14
|
|
|
10
15
|
export type ExtractAnalysis =
|
|
11
16
|
| { ok: false; reason: string }
|
|
12
17
|
| {
|
|
13
18
|
ok: true
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// 外部への出力の出発ポート(同一ポートからの fan-out は1
|
|
17
|
-
|
|
19
|
+
// 外部からの入力を受けるポート(重複排除・出現順。入出力の無い孤立した島は空)
|
|
20
|
+
entryPortIds: string[]
|
|
21
|
+
// 外部への出力の出発ポート(同一ポートからの fan-out は1出口扱い。出現順)
|
|
22
|
+
exitPortIds: string[]
|
|
18
23
|
internal: GraphConnection[]
|
|
19
24
|
inbound: GraphConnection[]
|
|
20
25
|
outbound: GraphConnection[]
|
|
@@ -26,8 +31,10 @@ export const analyzeExtractSelection = (
|
|
|
26
31
|
selectedIds: readonly string[],
|
|
27
32
|
): ExtractAnalysis => {
|
|
28
33
|
const selected = new Set(selectedIds)
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
// 1ノードからコンポーネント化できる(グループ化の「2つ以上」はエディタ側の
|
|
35
|
+
// 有効条件。コンポーネントの設定値(refs)と1ノードのコンポーネント化.md)
|
|
36
|
+
if (selected.size < 1) {
|
|
37
|
+
return { ok: false, reason: "ノードを選択してください" }
|
|
31
38
|
}
|
|
32
39
|
const selectedNodes = nodes.filter((n) => selected.has(n.id))
|
|
33
40
|
const flow = selectedNodes.find((n) => n.kind === "start" || n.kind === "end")
|
|
@@ -41,6 +48,9 @@ export const analyzeExtractSelection = (
|
|
|
41
48
|
const nodeIdByPortId = new Map(
|
|
42
49
|
nodes.flatMap((n) => n.ports.map((p) => [p.id, n.id] as const)),
|
|
43
50
|
)
|
|
51
|
+
const portById = new Map(
|
|
52
|
+
nodes.flatMap((n) => n.ports.map((p) => [p.id, p] as const)),
|
|
53
|
+
)
|
|
44
54
|
const internal: GraphConnection[] = []
|
|
45
55
|
const inbound: GraphConnection[] = []
|
|
46
56
|
const outbound: GraphConnection[] = []
|
|
@@ -52,29 +62,27 @@ export const analyzeExtractSelection = (
|
|
|
52
62
|
else if (fromIn && !toIn) outbound.push(conn)
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
// 入口ポート(重複排除・出現順)。条件は「全入口の型が同一」だけ:
|
|
66
|
+
// 起点(どの外部ポートから入るか)は見ない。ノードが前のノードを知らないのと
|
|
67
|
+
// 同じ思想を境界にも適用する(グループ化=入口の合流の宣言)
|
|
68
|
+
const entryPortIds = [...new Set(inbound.map((c) => c.to_port_id))]
|
|
69
|
+
const entrySchemas = new Set(
|
|
70
|
+
entryPortIds.map((id) => JSON.stringify(portById.get(id)?.schema ?? {})),
|
|
57
71
|
)
|
|
58
|
-
if (
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
reason:
|
|
62
|
-
"外部からの入力が複数のノードに入っています(入口は1つにしてください)",
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const exitPortIds = new Set(outbound.map((c) => c.from_port_id))
|
|
66
|
-
if (exitPortIds.size > 1) {
|
|
72
|
+
if (entrySchemas.size > 1) {
|
|
67
73
|
return {
|
|
68
74
|
ok: false,
|
|
69
75
|
reason:
|
|
70
|
-
"
|
|
76
|
+
"入口の型が揃っていません(同じ型の入口だけを1つのグループにまとめられます)",
|
|
71
77
|
}
|
|
72
78
|
}
|
|
79
|
+
// 出口は無条件(出発ポートごとに end の in = component の out になる)
|
|
80
|
+
const exitPortIds = [...new Set(outbound.map((c) => c.from_port_id))]
|
|
73
81
|
|
|
74
82
|
return {
|
|
75
83
|
ok: true,
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
entryPortIds,
|
|
85
|
+
exitPortIds,
|
|
78
86
|
internal,
|
|
79
87
|
inbound,
|
|
80
88
|
outbound,
|
|
@@ -84,24 +92,30 @@ export const analyzeExtractSelection = (
|
|
|
84
92
|
export type ExtractPlan = {
|
|
85
93
|
// 子 loop に入るノード(新 id でコピー。seed 済み start/end は含まない=呼び出し側が足す)
|
|
86
94
|
childNodes: GraphNode[]
|
|
87
|
-
// 内部接続(新 id)+ start.main →
|
|
95
|
+
// 内部接続(新 id)+ start.main → 各入口/各出口 → end の対応する in
|
|
88
96
|
childConnections: GraphConnection[]
|
|
89
|
-
//
|
|
97
|
+
// 出口を反映した子の end ノード(出口ごとに in を持つ。出口が無ければ渡された姿のまま)
|
|
98
|
+
childEnd: GraphNode
|
|
99
|
+
// 親に置く GROUP / COMPONENT ノード(in 1つ+出口ごとの out・参照=componentLoopId・選択の重心)
|
|
90
100
|
groupNode: GraphNode
|
|
91
101
|
// 置換後の親グラフ(選択ノードを除き groupNode を足した全量。線は繋ぎ直し済み)
|
|
92
102
|
parentNodes: GraphNode[]
|
|
93
103
|
parentConnections: GraphConnection[]
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
// 出口が1つのときの component out キー(既存コンポーネントとの互換。
|
|
107
|
+
// end の in が1つの loop は out main の1口として振る舞う)
|
|
108
|
+
const SINGLE_OUT_KEY = "main"
|
|
109
|
+
|
|
96
110
|
export const buildExtractPlan = (params: {
|
|
97
111
|
nodes: GraphNode[]
|
|
98
112
|
connections: GraphConnection[]
|
|
99
113
|
selectedIds: string[]
|
|
100
114
|
name: string
|
|
101
115
|
componentLoopId: string
|
|
102
|
-
// 子 loop(作成済み・seed
|
|
116
|
+
// 子 loop(作成済み・seed 済み)の構造ノードのポート/ノード
|
|
103
117
|
childStartOutPortId: string
|
|
104
|
-
|
|
118
|
+
childEnd: GraphNode
|
|
105
119
|
newId: () => string
|
|
106
120
|
}): { ok: false; reason: string } | ({ ok: true } & ExtractPlan) => {
|
|
107
121
|
const {
|
|
@@ -111,7 +125,7 @@ export const buildExtractPlan = (params: {
|
|
|
111
125
|
name,
|
|
112
126
|
componentLoopId,
|
|
113
127
|
childStartOutPortId,
|
|
114
|
-
|
|
128
|
+
childEnd,
|
|
115
129
|
newId,
|
|
116
130
|
} = params
|
|
117
131
|
const analysis = analyzeExtractSelection(nodes, connections, selectedIds)
|
|
@@ -138,28 +152,64 @@ export const buildExtractPlan = (params: {
|
|
|
138
152
|
}
|
|
139
153
|
})
|
|
140
154
|
|
|
155
|
+
const portById = new Map(
|
|
156
|
+
nodes.flatMap((n) => n.ports.map((p) => [p.id, p] as const)),
|
|
157
|
+
)
|
|
141
158
|
const mapConn = (conn: GraphConnection): GraphConnection => ({
|
|
142
159
|
from_port_id: portIdMap.get(conn.from_port_id) ?? conn.from_port_id,
|
|
143
160
|
to_port_id: portIdMap.get(conn.to_port_id) ?? conn.to_port_id,
|
|
144
161
|
})
|
|
145
162
|
const childConnections = analysis.internal.map(mapConn)
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
// 境界の配線(入口):start.main → 各入口ポート(複数なら fan-out)
|
|
164
|
+
for (const entryPortId of analysis.entryPortIds) {
|
|
165
|
+
childConnections.push({
|
|
166
|
+
from_port_id: childStartOutPortId,
|
|
167
|
+
to_port_id: portIdMap.get(entryPortId) ?? entryPortId,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 境界の配線(出口):出発ポートごとに end の in を1つ充てる。
|
|
172
|
+
// 1つ目は seed 済みの in を使い(名前を出口ポート名へ)、2つ目以降は追加する。
|
|
173
|
+
// component の out はこの in たちの射影(出口1つは互換のため main キー)
|
|
174
|
+
const seededEndIns = childEnd.ports.filter((p) => p.io === "in")
|
|
175
|
+
const endInKeys = new Set(seededEndIns.map((p) => p.key))
|
|
176
|
+
const nextEndInKey = (): string => {
|
|
177
|
+
for (let i = 2; ; i++) {
|
|
178
|
+
const key = `in_${i}`
|
|
179
|
+
if (!endInKeys.has(key)) {
|
|
180
|
+
endInKeys.add(key)
|
|
181
|
+
return key
|
|
182
|
+
}
|
|
157
183
|
}
|
|
158
184
|
}
|
|
159
|
-
|
|
185
|
+
const exits = analysis.exitPortIds.map((exitPortId, i) => {
|
|
186
|
+
const exitName = portById.get(exitPortId)?.name ?? "出力"
|
|
187
|
+
const seeded = i === 0 ? seededEndIns[0] : undefined
|
|
188
|
+
const endIn: GraphPort = seeded
|
|
189
|
+
? { ...seeded, name: exitName }
|
|
190
|
+
: {
|
|
191
|
+
id: newId(),
|
|
192
|
+
io: "in",
|
|
193
|
+
key: nextEndInKey(),
|
|
194
|
+
name: exitName,
|
|
195
|
+
schema: {},
|
|
196
|
+
}
|
|
197
|
+
return { exitPortId, endIn }
|
|
198
|
+
})
|
|
199
|
+
const resolvedChildEnd: GraphNode =
|
|
200
|
+
exits.length === 0
|
|
201
|
+
? childEnd
|
|
202
|
+
: {
|
|
203
|
+
...childEnd,
|
|
204
|
+
ports: [
|
|
205
|
+
...exits.map((e) => e.endIn),
|
|
206
|
+
...childEnd.ports.filter((p) => p.io !== "in"),
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
for (const exit of exits) {
|
|
160
210
|
childConnections.push({
|
|
161
|
-
from_port_id: portIdMap.get(
|
|
162
|
-
to_port_id:
|
|
211
|
+
from_port_id: portIdMap.get(exit.exitPortId) ?? exit.exitPortId,
|
|
212
|
+
to_port_id: exit.endIn.id,
|
|
163
213
|
})
|
|
164
214
|
}
|
|
165
215
|
|
|
@@ -173,13 +223,38 @@ export const buildExtractPlan = (params: {
|
|
|
173
223
|
selectedNodes.reduce((sum, n) => sum + n.position_y, 0) /
|
|
174
224
|
selectedNodes.length,
|
|
175
225
|
}
|
|
226
|
+
// out は出口ごと(キーは end の in の射影。出口1つ以下は互換の main 1口)
|
|
227
|
+
const groupOuts: { exitPortId: string | null; port: GraphPort }[] =
|
|
228
|
+
exits.length <= 1
|
|
229
|
+
? [
|
|
230
|
+
{
|
|
231
|
+
exitPortId: exits[0]?.exitPortId ?? null,
|
|
232
|
+
port: {
|
|
233
|
+
id: newId(),
|
|
234
|
+
io: "out",
|
|
235
|
+
key: SINGLE_OUT_KEY,
|
|
236
|
+
name: exits[0] ? exits[0].endIn.name : "出力",
|
|
237
|
+
schema: {},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
]
|
|
241
|
+
: exits.map((e) => ({
|
|
242
|
+
exitPortId: e.exitPortId,
|
|
243
|
+
port: {
|
|
244
|
+
id: newId(),
|
|
245
|
+
io: "out",
|
|
246
|
+
key: e.endIn.key,
|
|
247
|
+
name: e.endIn.name,
|
|
248
|
+
schema: {},
|
|
249
|
+
},
|
|
250
|
+
}))
|
|
176
251
|
const groupNode: GraphNode = {
|
|
177
252
|
id: newId(),
|
|
178
253
|
kind: "component",
|
|
179
254
|
name,
|
|
180
255
|
ports: [
|
|
181
256
|
{ id: newId(), io: "in", key: "in", name: "入力", schema: {} },
|
|
182
|
-
|
|
257
|
+
...groupOuts.map((o) => o.port),
|
|
183
258
|
],
|
|
184
259
|
hooks: [],
|
|
185
260
|
position_x: centroid.x,
|
|
@@ -187,12 +262,16 @@ export const buildExtractPlan = (params: {
|
|
|
187
262
|
component: { component_loop_id: componentLoopId },
|
|
188
263
|
}
|
|
189
264
|
const groupIn = groupNode.ports.find((p) => p.io === "in")
|
|
190
|
-
|
|
191
|
-
if (!groupIn || !groupMain) {
|
|
265
|
+
if (!groupIn) {
|
|
192
266
|
return { ok: false, reason: "groupNode のポート生成に失敗しました" }
|
|
193
267
|
}
|
|
268
|
+
const groupOutByExitPortId = new Map(
|
|
269
|
+
groupOuts
|
|
270
|
+
.filter((o) => o.exitPortId !== null)
|
|
271
|
+
.map((o) => [o.exitPortId as string, o.port] as const),
|
|
272
|
+
)
|
|
194
273
|
|
|
195
|
-
// 親の置換:選択関連の線(内部・境界)を除き、境界を group の in
|
|
274
|
+
// 親の置換:選択関連の線(内部・境界)を除き、境界を group の in/対応する out へ繋ぎ直す
|
|
196
275
|
const removed = new Set(
|
|
197
276
|
[...analysis.internal, ...analysis.inbound, ...analysis.outbound].map(
|
|
198
277
|
(c) => `${c.from_port_id}→${c.to_port_id}`,
|
|
@@ -202,14 +281,17 @@ export const buildExtractPlan = (params: {
|
|
|
202
281
|
...connections.filter(
|
|
203
282
|
(c) => !removed.has(`${c.from_port_id}→${c.to_port_id}`),
|
|
204
283
|
),
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
284
|
+
// 同一起点から複数入口へ入っていた線は group.in への1本に畳む(重複線を作らない)
|
|
285
|
+
...[...new Set(analysis.inbound.map((c) => c.from_port_id))].map(
|
|
286
|
+
(from_port_id) => ({
|
|
287
|
+
from_port_id,
|
|
288
|
+
to_port_id: groupIn.id,
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
291
|
+
...analysis.outbound.flatMap((c) => {
|
|
292
|
+
const out = groupOutByExitPortId.get(c.from_port_id)
|
|
293
|
+
return out ? [{ from_port_id: out.id, to_port_id: c.to_port_id }] : []
|
|
294
|
+
}),
|
|
213
295
|
]
|
|
214
296
|
const parentNodes = [...nodes.filter((n) => !selected.has(n.id)), groupNode]
|
|
215
297
|
|
|
@@ -217,6 +299,7 @@ export const buildExtractPlan = (params: {
|
|
|
217
299
|
ok: true,
|
|
218
300
|
childNodes,
|
|
219
301
|
childConnections,
|
|
302
|
+
childEnd: resolvedChildEnd,
|
|
220
303
|
groupNode,
|
|
221
304
|
parentNodes,
|
|
222
305
|
parentConnections,
|
package/_schemas/graph.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
waitConfigSchema,
|
|
9
9
|
} from "./graph.js"
|
|
10
10
|
import type { GraphPort } from "./node.js"
|
|
11
|
-
import {
|
|
11
|
+
import { resolvePortSchemas } from "./port-spec.js"
|
|
12
12
|
|
|
13
13
|
// テスト用の決定的な uuid(common は node 型定義を持たないため crypto は使わない)
|
|
14
14
|
let seq = 0
|
|
@@ -89,15 +89,14 @@ const port = (io: "in" | "out", key: string): GraphPort => ({
|
|
|
89
89
|
schema: { type: "object" },
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
// ai の out
|
|
93
|
-
|
|
94
|
-
const textEnvelope = envelopeSchemaOf({ type: "string" })
|
|
92
|
+
// ai の out も自由な宣言型。デフォルトノードはテキスト(string)を宣言する
|
|
93
|
+
const textOutput = { type: "string" }
|
|
95
94
|
|
|
96
95
|
// 既定は ai ノード。ai の in は 上流データの in + 差し戻しの instruction の2口
|
|
97
96
|
const aiPorts = (): GraphPort[] => [
|
|
98
97
|
port("in", "in"),
|
|
99
98
|
port("in", "instruction"),
|
|
100
|
-
{ ...port("out", "main"), schema:
|
|
99
|
+
{ ...port("out", "main"), schema: textOutput },
|
|
101
100
|
]
|
|
102
101
|
|
|
103
102
|
// ai 以外に流用するとき用(in は1つ)
|
|
@@ -388,17 +387,27 @@ describe("saveGraphBodySchema", () => {
|
|
|
388
387
|
).toThrow(/key が重複/)
|
|
389
388
|
})
|
|
390
389
|
|
|
391
|
-
it("in
|
|
390
|
+
it("in ポートの口数:ai は1つ以上(自由)、program 等は ちょうど1つ", () => {
|
|
391
|
+
// ai:0個は拒否、複数は許可(in の追加・削除は自由)
|
|
392
392
|
const zero = makeNode([port("out", "main")])
|
|
393
393
|
expect(() =>
|
|
394
394
|
saveGraphBodySchema.parse({ nodes: [zero], connections: [] }),
|
|
395
395
|
).toThrow(/in ポート/)
|
|
396
|
-
|
|
397
|
-
const two = makeNode([
|
|
396
|
+
const many = makeNode([
|
|
398
397
|
port("in", "in"),
|
|
399
|
-
port("in", "
|
|
398
|
+
port("in", "feedback"),
|
|
399
|
+
port("in", "extra"),
|
|
400
400
|
port("out", "main"),
|
|
401
401
|
])
|
|
402
|
+
expect(() =>
|
|
403
|
+
saveGraphBodySchema.parse({ nodes: [many], connections: [] }),
|
|
404
|
+
).not.toThrow()
|
|
405
|
+
|
|
406
|
+
// program:ちょうど1つ
|
|
407
|
+
const two = {
|
|
408
|
+
...makeNode([port("in", "in"), port("in", "in2"), port("out", "main")]),
|
|
409
|
+
kind: "program" as const,
|
|
410
|
+
}
|
|
402
411
|
expect(() =>
|
|
403
412
|
saveGraphBodySchema.parse({ nodes: [two], connections: [] }),
|
|
404
413
|
).toThrow(/in ポート/)
|
|
@@ -497,8 +506,6 @@ describe("saveGraphBodySchema", () => {
|
|
|
497
506
|
|
|
498
507
|
// ---- kind 別ポート構成と型の接続バリデーション(ポートのkind別仕様.md) ----
|
|
499
508
|
|
|
500
|
-
const aiEnvelope = textEnvelope
|
|
501
|
-
|
|
502
509
|
const humanNode = () => ({
|
|
503
510
|
...makeNode([
|
|
504
511
|
port("in", "in"),
|
|
@@ -511,7 +518,7 @@ const aiNode = () => ({
|
|
|
511
518
|
...makeNode([
|
|
512
519
|
port("in", "in"),
|
|
513
520
|
port("in", "instruction"),
|
|
514
|
-
{ ...port("out", "main"), schema:
|
|
521
|
+
{ ...port("out", "main"), schema: textOutput },
|
|
515
522
|
]),
|
|
516
523
|
kind: "ai" as const,
|
|
517
524
|
})
|
|
@@ -549,27 +556,27 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
549
556
|
...makeNode([
|
|
550
557
|
port("in", "in"),
|
|
551
558
|
port("in", "instruction"),
|
|
552
|
-
{ ...port("out", "spam"), schema:
|
|
553
|
-
{ ...port("out", "sales"), schema:
|
|
554
|
-
{ ...port("out", "support"), schema:
|
|
559
|
+
{ ...port("out", "spam"), schema: textOutput },
|
|
560
|
+
{ ...port("out", "sales"), schema: textOutput },
|
|
561
|
+
{ ...port("out", "support"), schema: textOutput },
|
|
555
562
|
]),
|
|
556
563
|
kind: "ai" as const,
|
|
557
564
|
}
|
|
558
565
|
expect(() =>
|
|
559
566
|
saveGraphBodySchema.parse({ nodes: [multi], connections: [] }),
|
|
560
567
|
).not.toThrow()
|
|
561
|
-
// in
|
|
562
|
-
const
|
|
568
|
+
// in の key も自由(in / instruction の固定構成は廃止)
|
|
569
|
+
const freeIn = {
|
|
563
570
|
...makeNode([
|
|
564
571
|
port("in", "input"),
|
|
565
|
-
port("in", "
|
|
566
|
-
{ ...port("out", "main"), schema:
|
|
572
|
+
port("in", "feedback"),
|
|
573
|
+
{ ...port("out", "main"), schema: textOutput },
|
|
567
574
|
]),
|
|
568
575
|
kind: "ai" as const,
|
|
569
576
|
}
|
|
570
577
|
expect(() =>
|
|
571
|
-
saveGraphBodySchema.parse({ nodes: [
|
|
572
|
-
).toThrow(
|
|
578
|
+
saveGraphBodySchema.parse({ nodes: [freeIn], connections: [] }),
|
|
579
|
+
).not.toThrow()
|
|
573
580
|
})
|
|
574
581
|
|
|
575
582
|
it("program は out を複数持てる(key 自由)", () => {
|
|
@@ -586,49 +593,38 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
586
593
|
).not.toThrow()
|
|
587
594
|
})
|
|
588
595
|
|
|
589
|
-
it("ai
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
{
|
|
594
|
-
])
|
|
595
|
-
expect(() =>
|
|
596
|
-
saveGraphBodySchema.parse({ nodes: [bad], connections: [] }),
|
|
597
|
-
).toThrow(/封筒型 \{ data, description \}/)
|
|
598
|
-
// main 以外の出口も封筒型を強制する
|
|
599
|
-
const badExtra = makeNode([
|
|
600
|
-
port("in", "in"),
|
|
601
|
-
port("in", "instruction"),
|
|
602
|
-
{ ...port("out", "main"), schema: aiEnvelope },
|
|
603
|
-
{ ...port("out", "extra"), schema: { type: "object" } },
|
|
604
|
-
])
|
|
605
|
-
expect(() =>
|
|
606
|
-
saveGraphBodySchema.parse({ nodes: [badExtra], connections: [] }),
|
|
607
|
-
).toThrow(/封筒型 \{ data, description \}/)
|
|
608
|
-
// data は任意の型でよい(カタログ外の object も通る)
|
|
609
|
-
const custom = makeNode([
|
|
610
|
-
port("in", "in"),
|
|
611
|
-
port("in", "instruction"),
|
|
596
|
+
it("ai の出力宣言は任意の JSON Schema でよい(形の強制はしない)", () => {
|
|
597
|
+
for (const schema of [
|
|
598
|
+
{ type: "object" },
|
|
599
|
+
{ type: "string" },
|
|
600
|
+
{ type: "array", items: { type: "number" } },
|
|
612
601
|
{
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
602
|
+
// かつて強制していた { data, description } もただの object として valid
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
data: { type: "string" },
|
|
606
|
+
description: { type: "string" },
|
|
607
|
+
},
|
|
608
|
+
required: ["data", "description"],
|
|
619
609
|
},
|
|
620
|
-
])
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
610
|
+
]) {
|
|
611
|
+
const node = makeNode([
|
|
612
|
+
port("in", "in"),
|
|
613
|
+
port("in", "instruction"),
|
|
614
|
+
{ ...port("out", "main"), schema },
|
|
615
|
+
])
|
|
616
|
+
expect(() =>
|
|
617
|
+
saveGraphBodySchema.parse({ nodes: [node], connections: [] }),
|
|
618
|
+
).not.toThrow()
|
|
619
|
+
}
|
|
624
620
|
})
|
|
625
621
|
})
|
|
626
622
|
|
|
627
623
|
describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
628
|
-
// 接続の可否は kind
|
|
629
|
-
//
|
|
630
|
-
// → docs/tasks/wip
|
|
631
|
-
it("human の reject → ai の instruction
|
|
624
|
+
// 接続の可否は kind ではなく型で決まる。reject / instruction の型固定は廃止され、
|
|
625
|
+
// ai の in はどの口も接続鏡映(接続元の型に同期)なので接続は自明に通る
|
|
626
|
+
// → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
|
|
627
|
+
it("human の reject → ai の instruction を許可(kind の縛りも型固定も無い)", () => {
|
|
632
628
|
const human = humanNode()
|
|
633
629
|
const ai = aiNode()
|
|
634
630
|
const rejectPort = outByKey(human, "reject")
|
|
@@ -645,7 +641,7 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
|
645
641
|
).not.toThrow()
|
|
646
642
|
})
|
|
647
643
|
|
|
648
|
-
it("reject は ai 以外へも繋げる(接続鏡映の in
|
|
644
|
+
it("reject は ai 以外へも繋げる(接続鏡映の in が接続元の型を採用する)", () => {
|
|
649
645
|
const human = humanNode()
|
|
650
646
|
const prog = programNode()
|
|
651
647
|
expect(() =>
|
|
@@ -661,10 +657,11 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
|
661
657
|
).not.toThrow()
|
|
662
658
|
})
|
|
663
659
|
|
|
664
|
-
it("instruction
|
|
660
|
+
it("instruction も他の in と同じ接続鏡映:宣言と違う型の out でも同期して繋がる", () => {
|
|
665
661
|
const upstream = aiNode()
|
|
666
662
|
const downstream = aiNode()
|
|
667
|
-
// ai.main
|
|
663
|
+
// ai.main は string、instruction の宣言は { type: "object" } だが、
|
|
664
|
+
// 鏡映で接続元に同期される(instruction の特別扱いは無い)
|
|
668
665
|
expect(() =>
|
|
669
666
|
saveGraphBodySchema.parse({
|
|
670
667
|
nodes: [upstream, downstream],
|
|
@@ -675,10 +672,10 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
|
675
672
|
},
|
|
676
673
|
],
|
|
677
674
|
}),
|
|
678
|
-
).toThrow(
|
|
675
|
+
).not.toThrow()
|
|
679
676
|
})
|
|
680
677
|
|
|
681
|
-
it("human の in
|
|
678
|
+
it("human の in はどの型でも接続できる(形の制約は課さない)", () => {
|
|
682
679
|
const ai = aiNode()
|
|
683
680
|
const human = humanNode()
|
|
684
681
|
expect(() =>
|
|
@@ -688,8 +685,7 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
|
688
685
|
}),
|
|
689
686
|
).not.toThrow()
|
|
690
687
|
|
|
691
|
-
// program out
|
|
692
|
-
// (input 全体を data として表示する)
|
|
688
|
+
// program out が素の object でも接続できる
|
|
693
689
|
const human2 = humanNode()
|
|
694
690
|
const progPlain = {
|
|
695
691
|
...makeNode([
|
|
@@ -707,7 +703,8 @@ describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
|
707
703
|
})
|
|
708
704
|
|
|
709
705
|
it("program の in への接続は 伝播型 ⊆ 宣言スキーマ(human 経由の伝播込み)", () => {
|
|
710
|
-
// ai
|
|
706
|
+
// ai → human(approve は in と同型)→ program(string を要求) は、
|
|
707
|
+
// program の in が接続鏡映(sticky)なので通る
|
|
711
708
|
const ai = aiNode()
|
|
712
709
|
const human = humanNode()
|
|
713
710
|
const prog = programNode({ type: "string" })
|
|
@@ -1027,6 +1024,30 @@ describe("saveGraphBodySchema:コンポーネントノード(component kind
|
|
|
1027
1024
|
).not.toThrow()
|
|
1028
1025
|
})
|
|
1029
1026
|
|
|
1027
|
+
it("component.refs(配置の設定値)を受け付けて保持する(省略も可)", () => {
|
|
1028
|
+
const withRefs = {
|
|
1029
|
+
...componentNode(),
|
|
1030
|
+
component: {
|
|
1031
|
+
component_loop_id: uuid(),
|
|
1032
|
+
refs: { labels: ["bug"], assignee: "kazuwombat" },
|
|
1033
|
+
},
|
|
1034
|
+
}
|
|
1035
|
+
const parsed = saveGraphBodySchema.parse({
|
|
1036
|
+
nodes: [withRefs],
|
|
1037
|
+
connections: [],
|
|
1038
|
+
})
|
|
1039
|
+
expect(parsed.nodes[0]?.component?.refs).toEqual({
|
|
1040
|
+
labels: ["bug"],
|
|
1041
|
+
assignee: "kazuwombat",
|
|
1042
|
+
})
|
|
1043
|
+
// 省略時は undefined のまま(既存グラフの互換。保存側で {} 扱い)
|
|
1044
|
+
const omitted = saveGraphBodySchema.parse({
|
|
1045
|
+
nodes: [componentNode()],
|
|
1046
|
+
connections: [],
|
|
1047
|
+
})
|
|
1048
|
+
expect(omitted.nodes[0]?.component?.refs).toBeUndefined()
|
|
1049
|
+
})
|
|
1050
|
+
|
|
1030
1051
|
it("component_loop_id の無い component ノードを拒否する", () => {
|
|
1031
1052
|
const node = { ...componentNode(), component: undefined }
|
|
1032
1053
|
expect(() =>
|
|
@@ -1034,21 +1055,23 @@ describe("saveGraphBodySchema:コンポーネントノード(component kind
|
|
|
1034
1055
|
).toThrow(/component_loop_id/)
|
|
1035
1056
|
})
|
|
1036
1057
|
|
|
1037
|
-
it("
|
|
1038
|
-
|
|
1058
|
+
it("in は in 1つ固定・out は複数可(子 end の in 射影。コンポーネントの複数入口・複数出口.md)", () => {
|
|
1059
|
+
// in の key 違いは拒否
|
|
1060
|
+
const badIn = {
|
|
1039
1061
|
...componentNode(),
|
|
1040
|
-
ports: [port("in", "
|
|
1062
|
+
ports: [port("in", "input"), port("out", "main")],
|
|
1041
1063
|
}
|
|
1042
1064
|
expect(() =>
|
|
1043
|
-
saveGraphBodySchema.parse({ nodes: [
|
|
1044
|
-
).toThrow(/component
|
|
1065
|
+
saveGraphBodySchema.parse({ nodes: [badIn], connections: [] }),
|
|
1066
|
+
).toThrow(/component の in ポートは in/)
|
|
1067
|
+
// out は key 自由・複数可(キーは保存時にサーバが子契約から焼き込む)
|
|
1045
1068
|
const multiOut = {
|
|
1046
1069
|
...componentNode(),
|
|
1047
1070
|
ports: [port("in", "in"), port("out", "main"), port("out", "sub")],
|
|
1048
1071
|
}
|
|
1049
1072
|
expect(() =>
|
|
1050
1073
|
saveGraphBodySchema.parse({ nodes: [multiOut], connections: [] }),
|
|
1051
|
-
).toThrow(
|
|
1074
|
+
).not.toThrow()
|
|
1052
1075
|
})
|
|
1053
1076
|
|
|
1054
1077
|
it("component の in への接続は契約への適合を検証する(伝播型 ⊆ 契約)", () => {
|