@mawaru/sdk 0.5.0 → 0.8.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 +17 -2
- package/_schemas/ai-manifest.ts +9 -9
- package/_schemas/extract-component.test.ts +220 -0
- package/_schemas/extract-component.ts +224 -0
- package/_schemas/graph.test.ts +201 -12
- package/_schemas/graph.ts +99 -10
- package/_schemas/node.ts +3 -0
- package/_schemas/port-spec.test.ts +190 -37
- package/_schemas/port-spec.ts +165 -58
- package/_schemas/program-manifest.ts +9 -8
- package/dist/_schemas/ai-manifest.js +6 -9
- package/dist/_schemas/extract-component.d.ts +35 -0
- package/dist/_schemas/extract-component.js +146 -0
- package/dist/_schemas/graph.d.ts +19 -0
- package/dist/_schemas/graph.js +82 -11
- package/dist/_schemas/node.d.ts +1 -0
- package/dist/_schemas/node.js +3 -0
- package/dist/_schemas/port-spec.d.ts +10 -2
- package/dist/_schemas/port-spec.js +136 -54
- package/dist/_schemas/program-manifest.js +6 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +3 -0
- package/index.ts +1 -0
- package/package.json +1 -1
- package/templates/gitignore +4 -0
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 { envelopeSchemaOf } from "./port-spec.js"
|
|
11
|
+
import { envelopeSchemaOf, resolvePortSchemas } from "./port-spec.js"
|
|
12
12
|
|
|
13
13
|
// テスト用の決定的な uuid(common は node 型定義を持たないため crypto は使わない)
|
|
14
14
|
let seq = 0
|
|
@@ -93,14 +93,21 @@ const port = (io: "in" | "out", key: string): GraphPort => ({
|
|
|
93
93
|
// data=テキストの封筒を持つ
|
|
94
94
|
const textEnvelope = envelopeSchemaOf({ type: "string" })
|
|
95
95
|
|
|
96
|
+
// 既定は ai ノード。ai の in は 上流データの in + 差し戻しの instruction の2口
|
|
97
|
+
const aiPorts = (): GraphPort[] => [
|
|
98
|
+
port("in", "in"),
|
|
99
|
+
port("in", "instruction"),
|
|
100
|
+
{ ...port("out", "main"), schema: textEnvelope },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
// ai 以外に流用するとき用(in は1つ)
|
|
104
|
+
const simplePorts = (): GraphPort[] => [port("in", "in"), port("out", "main")]
|
|
105
|
+
|
|
96
106
|
const makeNode = (ports?: GraphPort[]) => ({
|
|
97
107
|
id: uuid(),
|
|
98
108
|
kind: "ai" as const,
|
|
99
109
|
name: "ノード",
|
|
100
|
-
ports: ports ??
|
|
101
|
-
port("in", "in"),
|
|
102
|
-
{ ...port("out", "main"), schema: textEnvelope },
|
|
103
|
-
],
|
|
110
|
+
ports: ports ?? aiPorts(),
|
|
104
111
|
position_x: 0,
|
|
105
112
|
position_y: 0,
|
|
106
113
|
})
|
|
@@ -172,6 +179,24 @@ describe("humanConfigSchema", () => {
|
|
|
172
179
|
})
|
|
173
180
|
})
|
|
174
181
|
|
|
182
|
+
// 承認ビューは不透明なキー(program_dir / ai_dir と同じ)。実在するか・そのテナントが
|
|
183
|
+
// 選べるかは mawaru 側のカタログ(非公開)と保存 API が見るので、ここは形だけ
|
|
184
|
+
it("承認ビューとそのパラメータを受け付ける", () => {
|
|
185
|
+
const parsed = humanConfigSchema.parse({
|
|
186
|
+
assignment: {},
|
|
187
|
+
view: "acme/invoice",
|
|
188
|
+
view_config: { threshold: 1000 },
|
|
189
|
+
})
|
|
190
|
+
expect(parsed.view).toBe("acme/invoice")
|
|
191
|
+
expect(parsed.view_config).toEqual({ threshold: 1000 })
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("空のビューキーは拒否する", () => {
|
|
195
|
+
expect(() =>
|
|
196
|
+
humanConfigSchema.parse({ assignment: {}, view: "" }),
|
|
197
|
+
).toThrow()
|
|
198
|
+
})
|
|
199
|
+
|
|
175
200
|
it("fixed:1ユニット(複数人の and/or)を受け付ける", () => {
|
|
176
201
|
const u1 = uuid()
|
|
177
202
|
const u2 = uuid()
|
|
@@ -258,6 +283,52 @@ describe("saveGraphBodySchema", () => {
|
|
|
258
283
|
})
|
|
259
284
|
})
|
|
260
285
|
|
|
286
|
+
// fan-in(同じ in ポートへの流入2本以上)は同じ型でなければならない
|
|
287
|
+
// (docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
288
|
+
const programNode = (outSchema: GraphPort["schema"]) => ({
|
|
289
|
+
...makeNode([
|
|
290
|
+
port("in", "in"),
|
|
291
|
+
{ ...port("out", "main"), schema: outSchema },
|
|
292
|
+
]),
|
|
293
|
+
kind: "program" as const,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it("fan-in:流入の型が同じなら受け付ける", () => {
|
|
297
|
+
const a = programNode({ type: "string" })
|
|
298
|
+
const b = programNode({ type: "string" })
|
|
299
|
+
const sink = { ...makeNode(simplePorts()), kind: "wait" as const }
|
|
300
|
+
expect(() =>
|
|
301
|
+
saveGraphBodySchema.parse({
|
|
302
|
+
nodes: [a, b, sink],
|
|
303
|
+
connections: [connect(a, sink), connect(b, sink)],
|
|
304
|
+
}),
|
|
305
|
+
).not.toThrow()
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it("fan-in:異なる具体型の流入を拒否する", () => {
|
|
309
|
+
const a = programNode({ type: "string" })
|
|
310
|
+
const b = programNode({ type: "number" })
|
|
311
|
+
const sink = { ...makeNode(simplePorts()), kind: "wait" as const }
|
|
312
|
+
expect(() =>
|
|
313
|
+
saveGraphBodySchema.parse({
|
|
314
|
+
nodes: [a, b, sink],
|
|
315
|
+
connections: [connect(a, sink), connect(b, sink)],
|
|
316
|
+
}),
|
|
317
|
+
).toThrow(/異なる型を接続できません/)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it("fan-in:具体型1種類+未宣言 の混在は受け付ける(未宣言は判断材料にしない)", () => {
|
|
321
|
+
const a = programNode({ type: "string" })
|
|
322
|
+
const b = { ...makeNode(simplePorts()), kind: "wait" as const } // out main は導出=未宣言
|
|
323
|
+
const sink = { ...makeNode(simplePorts()), kind: "wait" as const }
|
|
324
|
+
expect(() =>
|
|
325
|
+
saveGraphBodySchema.parse({
|
|
326
|
+
nodes: [a, b, sink],
|
|
327
|
+
connections: [connect(a, sink), connect(b, sink)],
|
|
328
|
+
}),
|
|
329
|
+
).not.toThrow()
|
|
330
|
+
})
|
|
331
|
+
|
|
261
332
|
it("node id の重複を拒否する", () => {
|
|
262
333
|
const a = makeNode()
|
|
263
334
|
expect(() =>
|
|
@@ -410,6 +481,7 @@ const humanNode = () => ({
|
|
|
410
481
|
const aiNode = () => ({
|
|
411
482
|
...makeNode([
|
|
412
483
|
port("in", "in"),
|
|
484
|
+
port("in", "instruction"),
|
|
413
485
|
{ ...port("out", "main"), schema: aiEnvelope },
|
|
414
486
|
]),
|
|
415
487
|
kind: "ai" as const,
|
|
@@ -418,6 +490,11 @@ const programNode = (inSchema: GraphPort["schema"] = {}) => ({
|
|
|
418
490
|
...makeNode([{ ...port("in", "in"), schema: inSchema }, port("out", "main")]),
|
|
419
491
|
kind: "program" as const,
|
|
420
492
|
})
|
|
493
|
+
const inByKey = (node: TestNode, key: string): GraphPort => {
|
|
494
|
+
const p = node.ports.find((p) => p.io === "in" && p.key === key)
|
|
495
|
+
if (!p) throw new Error(`in port not found: ${key}`)
|
|
496
|
+
return p
|
|
497
|
+
}
|
|
421
498
|
const outByKey = (node: TestNode, key: string): GraphPort => {
|
|
422
499
|
const p = node.ports.find((p) => p.io === "out" && p.key === key)
|
|
423
500
|
if (!p) throw new Error(`out port not found: ${key}`)
|
|
@@ -442,6 +519,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
442
519
|
const multi = {
|
|
443
520
|
...makeNode([
|
|
444
521
|
port("in", "in"),
|
|
522
|
+
port("in", "instruction"),
|
|
445
523
|
{ ...port("out", "spam"), schema: aiEnvelope },
|
|
446
524
|
{ ...port("out", "sales"), schema: aiEnvelope },
|
|
447
525
|
{ ...port("out", "support"), schema: aiEnvelope },
|
|
@@ -451,10 +529,11 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
451
529
|
expect(() =>
|
|
452
530
|
saveGraphBodySchema.parse({ nodes: [multi], connections: [] }),
|
|
453
531
|
).not.toThrow()
|
|
454
|
-
// in
|
|
532
|
+
// in の構成は in / instruction 固定のまま
|
|
455
533
|
const badIn = {
|
|
456
534
|
...makeNode([
|
|
457
535
|
port("in", "input"),
|
|
536
|
+
port("in", "instruction"),
|
|
458
537
|
{ ...port("out", "main"), schema: aiEnvelope },
|
|
459
538
|
]),
|
|
460
539
|
kind: "ai" as const,
|
|
@@ -481,6 +560,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
481
560
|
it("ai の出力宣言が封筒型 { data, description } でないと拒否する(全出口に適用)", () => {
|
|
482
561
|
const bad = makeNode([
|
|
483
562
|
port("in", "in"),
|
|
563
|
+
port("in", "instruction"),
|
|
484
564
|
{ ...port("out", "main"), schema: { type: "object" } },
|
|
485
565
|
])
|
|
486
566
|
expect(() =>
|
|
@@ -489,6 +569,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
489
569
|
// main 以外の出口も封筒型を強制する
|
|
490
570
|
const badExtra = makeNode([
|
|
491
571
|
port("in", "in"),
|
|
572
|
+
port("in", "instruction"),
|
|
492
573
|
{ ...port("out", "main"), schema: aiEnvelope },
|
|
493
574
|
{ ...port("out", "extra"), schema: { type: "object" } },
|
|
494
575
|
])
|
|
@@ -498,6 +579,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
498
579
|
// data は任意の型でよい(カタログ外の object も通る)
|
|
499
580
|
const custom = makeNode([
|
|
500
581
|
port("in", "in"),
|
|
582
|
+
port("in", "instruction"),
|
|
501
583
|
{
|
|
502
584
|
...port("out", "main"),
|
|
503
585
|
schema: envelopeSchemaOf({
|
|
@@ -514,32 +596,57 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
514
596
|
})
|
|
515
597
|
|
|
516
598
|
describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
517
|
-
it("human の reject → ai
|
|
599
|
+
it("human の reject → ai の instruction は許可。ai の in や ai 以外は拒否", () => {
|
|
518
600
|
const human = humanNode()
|
|
519
601
|
const ai = aiNode()
|
|
520
602
|
const prog = programNode()
|
|
603
|
+
const rejectPort = outByKey(human, "reject")
|
|
604
|
+
// 差し戻しは指示の口(instruction)へ
|
|
521
605
|
expect(() =>
|
|
522
606
|
saveGraphBodySchema.parse({
|
|
523
607
|
nodes: [human, ai],
|
|
524
608
|
connections: [
|
|
525
609
|
{
|
|
526
|
-
from_port_id:
|
|
527
|
-
to_port_id:
|
|
610
|
+
from_port_id: rejectPort.id,
|
|
611
|
+
to_port_id: inByKey(ai, "instruction").id,
|
|
528
612
|
},
|
|
529
613
|
],
|
|
530
614
|
}),
|
|
531
615
|
).not.toThrow()
|
|
616
|
+
// 上流データの口(in)には繋げない(意味の違う入力を fan-in させない)
|
|
617
|
+
expect(() =>
|
|
618
|
+
saveGraphBodySchema.parse({
|
|
619
|
+
nodes: [human, ai],
|
|
620
|
+
connections: [
|
|
621
|
+
{ from_port_id: rejectPort.id, to_port_id: inByKey(ai, "in").id },
|
|
622
|
+
],
|
|
623
|
+
}),
|
|
624
|
+
).toThrow(/instruction/)
|
|
625
|
+
// ai 以外にも繋げない
|
|
532
626
|
expect(() =>
|
|
533
627
|
saveGraphBodySchema.parse({
|
|
534
628
|
nodes: [human, prog],
|
|
629
|
+
connections: [
|
|
630
|
+
{ from_port_id: rejectPort.id, to_port_id: inOf(prog).id },
|
|
631
|
+
],
|
|
632
|
+
}),
|
|
633
|
+
).toThrow(/instruction/)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it("instruction ポートに繋げるのは human の reject だけ", () => {
|
|
637
|
+
const ai = aiNode()
|
|
638
|
+
const prog = programNode()
|
|
639
|
+
expect(() =>
|
|
640
|
+
saveGraphBodySchema.parse({
|
|
641
|
+
nodes: [prog, ai],
|
|
535
642
|
connections: [
|
|
536
643
|
{
|
|
537
|
-
from_port_id:
|
|
538
|
-
to_port_id:
|
|
644
|
+
from_port_id: outOf(prog).id,
|
|
645
|
+
to_port_id: inByKey(ai, "instruction").id,
|
|
539
646
|
},
|
|
540
647
|
],
|
|
541
648
|
}),
|
|
542
|
-
).toThrow(/
|
|
649
|
+
).toThrow(/instruction ポートに接続できるのは/)
|
|
543
650
|
})
|
|
544
651
|
|
|
545
652
|
it("human の in はどの型でも接続できる(封筒型の制約は課さない)", () => {
|
|
@@ -870,3 +977,85 @@ describe("refsDefaultsOf(定義スキーマ→デフォルト値の袋)", ()
|
|
|
870
977
|
expect(refsDefaultsOf({ type: "object", properties: {} })).toEqual({})
|
|
871
978
|
})
|
|
872
979
|
})
|
|
980
|
+
|
|
981
|
+
describe("saveGraphBodySchema:コンポーネントノード(component kind)", () => {
|
|
982
|
+
// コンポーネント=kind='component' の loop を配置して呼ぶノード。ポートは
|
|
983
|
+
// in/main 固定で、スキーマは子 loop の契約から保存時にサーバが焼き込む(宣言扱い)
|
|
984
|
+
const componentNode = (
|
|
985
|
+
inSchema: GraphPort["schema"] = { type: "object" },
|
|
986
|
+
) => ({
|
|
987
|
+
...makeNode([
|
|
988
|
+
{ ...port("in", "in"), schema: inSchema },
|
|
989
|
+
port("out", "main"),
|
|
990
|
+
]),
|
|
991
|
+
kind: "component" as const,
|
|
992
|
+
component: { component_loop_id: uuid() },
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
it("component ノード(in/main+component_loop_id)を受け付ける", () => {
|
|
996
|
+
expect(() =>
|
|
997
|
+
saveGraphBodySchema.parse({ nodes: [componentNode()], connections: [] }),
|
|
998
|
+
).not.toThrow()
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
it("component_loop_id の無い component ノードを拒否する", () => {
|
|
1002
|
+
const node = { ...componentNode(), component: undefined }
|
|
1003
|
+
expect(() =>
|
|
1004
|
+
saveGraphBodySchema.parse({ nodes: [node], connections: [] }),
|
|
1005
|
+
).toThrow(/component_loop_id/)
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
it("ポート構成は in/main 固定(out の key 違い・複数 out を拒否)", () => {
|
|
1009
|
+
const badKey = {
|
|
1010
|
+
...componentNode(),
|
|
1011
|
+
ports: [port("in", "in"), port("out", "result")],
|
|
1012
|
+
}
|
|
1013
|
+
expect(() =>
|
|
1014
|
+
saveGraphBodySchema.parse({ nodes: [badKey], connections: [] }),
|
|
1015
|
+
).toThrow(/component のポート構成/)
|
|
1016
|
+
const multiOut = {
|
|
1017
|
+
...componentNode(),
|
|
1018
|
+
ports: [port("in", "in"), port("out", "main"), port("out", "sub")],
|
|
1019
|
+
}
|
|
1020
|
+
expect(() =>
|
|
1021
|
+
saveGraphBodySchema.parse({ nodes: [multiOut], connections: [] }),
|
|
1022
|
+
).toThrow(/component のポート構成/)
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
it("component の in への接続は契約への適合を検証する(伝播型 ⊆ 契約)", () => {
|
|
1026
|
+
// 上流 program の out {type:"string"} → component の in {type:"number"} は不適合
|
|
1027
|
+
const prog = {
|
|
1028
|
+
...makeNode([
|
|
1029
|
+
port("in", "in"),
|
|
1030
|
+
{ ...port("out", "main"), schema: { type: "string" } },
|
|
1031
|
+
]),
|
|
1032
|
+
kind: "program" as const,
|
|
1033
|
+
}
|
|
1034
|
+
const comp = componentNode({ type: "number" })
|
|
1035
|
+
expect(() =>
|
|
1036
|
+
saveGraphBodySchema.parse({
|
|
1037
|
+
nodes: [prog, comp],
|
|
1038
|
+
connections: [connect(prog, comp)],
|
|
1039
|
+
}),
|
|
1040
|
+
).toThrow(/入力契約に適合しません/)
|
|
1041
|
+
// 適合(string → string)なら通る
|
|
1042
|
+
const okComp = componentNode({ type: "string" })
|
|
1043
|
+
expect(() =>
|
|
1044
|
+
saveGraphBodySchema.parse({
|
|
1045
|
+
nodes: [prog, okComp],
|
|
1046
|
+
connections: [connect(prog, okComp)],
|
|
1047
|
+
}),
|
|
1048
|
+
).not.toThrow()
|
|
1049
|
+
})
|
|
1050
|
+
|
|
1051
|
+
it("component の in の宣言スキーマは接続で潰されない(導出/鏡映扱いにしない)", () => {
|
|
1052
|
+
const contract = {
|
|
1053
|
+
type: "object",
|
|
1054
|
+
properties: { pr_urls: { type: "array" } },
|
|
1055
|
+
required: ["pr_urls"],
|
|
1056
|
+
}
|
|
1057
|
+
const comp = componentNode(contract)
|
|
1058
|
+
const resolved = resolvePortSchemas([comp], [])
|
|
1059
|
+
expect(resolved.get(inOf(comp).id)).toEqual(contract)
|
|
1060
|
+
})
|
|
1061
|
+
})
|
package/_schemas/graph.ts
CHANGED
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
portKeySchema,
|
|
7
7
|
} from "./node.js"
|
|
8
8
|
import {
|
|
9
|
+
AI_INSTRUCTION_PORT_KEY,
|
|
10
|
+
hasInboundConflict,
|
|
9
11
|
isAiEnvelopeSchema,
|
|
10
12
|
isSchemaSubset,
|
|
11
13
|
kindPortsViolation,
|
|
12
14
|
resolvePortSchemas,
|
|
15
|
+
UNDECLARED_SCHEMA,
|
|
13
16
|
} from "./port-spec.js"
|
|
14
17
|
|
|
15
18
|
// refs=ループ変数の袋(key→任意の JSON 値。上流出力の参照(refs).md)。
|
|
@@ -72,6 +75,7 @@ export const graphNodeKindSchema = nodeKindSchema.extract([
|
|
|
72
75
|
"wait",
|
|
73
76
|
"start",
|
|
74
77
|
"end",
|
|
78
|
+
"component",
|
|
75
79
|
])
|
|
76
80
|
export type GraphNodeKind = z.infer<typeof graphNodeKindSchema>
|
|
77
81
|
|
|
@@ -133,6 +137,12 @@ export type AiConfig = z.infer<typeof aiConfigSchema>
|
|
|
133
137
|
// メンバーが tenant の membership であることの検証は保存 API 側で行う
|
|
134
138
|
// (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
|
|
135
139
|
export const humanConfigSchema = z.object({
|
|
140
|
+
// 承認 UI(承認ビュー)の識別子。program_dir / ai_dir と同じ「不透明なキー」で、
|
|
141
|
+
// どのキーが実在するか・そのテナントが選べるかは保存時に mawaru 側が検証する
|
|
142
|
+
// (カタログは非公開。@mawaru/common の approval-view.ts)。省略時は標準ビュー
|
|
143
|
+
view: z.string().min(1).optional(),
|
|
144
|
+
// ビューに渡すパラメータ(列・ラベル・閾値など)。形はビューごとに違うので任意の JSON
|
|
145
|
+
view_config: z.unknown().nullish(),
|
|
136
146
|
assignment: z
|
|
137
147
|
.object({
|
|
138
148
|
strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
|
|
@@ -171,6 +181,14 @@ export const humanConfigSchema = z.object({
|
|
|
171
181
|
})
|
|
172
182
|
export type HumanConfig = z.infer<typeof humanConfigSchema>
|
|
173
183
|
|
|
184
|
+
// コンポーネントノードの kind 別設定(node_components に対応)。呼ぶ先は
|
|
185
|
+
// kind='component' の loop。同 tenant・kind の検証と自 loop 参照の禁止は保存 API 側で行う
|
|
186
|
+
// (docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
|
|
187
|
+
export const componentConfigSchema = z.object({
|
|
188
|
+
component_loop_id: z.uuid(),
|
|
189
|
+
})
|
|
190
|
+
export type ComponentConfig = z.infer<typeof componentConfigSchema>
|
|
191
|
+
|
|
174
192
|
// ノードにアタッチされた IOHook(node_hooks に対応。アタッチできるのは program ノードのみ)。
|
|
175
193
|
// on_input / on_output / on_signal はアタッチ時に repo の config.json(hookManifestSchema)から
|
|
176
194
|
// エディタが焼き込むスナップショット。ref 省略時はデフォルトブランチ
|
|
@@ -206,6 +224,7 @@ export const graphNodeSchema = z.object({
|
|
|
206
224
|
program: programConfigSchema.optional(),
|
|
207
225
|
ai: aiConfigSchema.optional(),
|
|
208
226
|
human: humanConfigSchema.optional(),
|
|
227
|
+
component: componentConfigSchema.optional(),
|
|
209
228
|
})
|
|
210
229
|
export type GraphNode = z.infer<typeof graphNodeSchema>
|
|
211
230
|
|
|
@@ -279,16 +298,20 @@ export const saveGraphBodySchema = z
|
|
|
279
298
|
if (port.io === "in") inCount++
|
|
280
299
|
else outCount++
|
|
281
300
|
})
|
|
282
|
-
// end
|
|
283
|
-
//
|
|
284
|
-
|
|
301
|
+
// end は「in は1つ以上」(複数ルートを別々の in で集約する)。
|
|
302
|
+
// ai は2つ(上流データの in + 差し戻しの instruction)。他 kind は1つ固定。
|
|
303
|
+
// key の妥当性は kindPortsViolation が別途見る
|
|
304
|
+
const expectIn = node.kind === "ai" ? 2 : 1
|
|
305
|
+
const inCountOk =
|
|
306
|
+
node.kind === "end" ? inCount >= 1 : inCount === expectIn
|
|
307
|
+
if (!inCountOk) {
|
|
285
308
|
ctx.addIssue({
|
|
286
309
|
code: "custom",
|
|
287
310
|
path: ["nodes", i, "ports"],
|
|
288
311
|
message:
|
|
289
312
|
node.kind === "end"
|
|
290
313
|
? "in ポートが1つ以上必要です"
|
|
291
|
-
:
|
|
314
|
+
: `in ポートはちょうど${expectIn}つ必要です`,
|
|
292
315
|
})
|
|
293
316
|
}
|
|
294
317
|
if (outCount < 1) {
|
|
@@ -321,6 +344,16 @@ export const saveGraphBodySchema = z
|
|
|
321
344
|
hookDirs.add(hook.hook_dir)
|
|
322
345
|
})
|
|
323
346
|
|
|
347
|
+
// component は参照先が無いと成立しない(他 kind の設定はデフォルトで補えるが
|
|
348
|
+
// component_loop_id は補えない)
|
|
349
|
+
if (node.kind === "component" && node.component === undefined) {
|
|
350
|
+
ctx.addIssue({
|
|
351
|
+
code: "custom",
|
|
352
|
+
path: ["nodes", i, "component"],
|
|
353
|
+
message: "component ノードには component_loop_id が必要です",
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
324
357
|
// kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md)
|
|
325
358
|
const violation = kindPortsViolation(node.kind, node.ports)
|
|
326
359
|
if (violation) {
|
|
@@ -353,7 +386,7 @@ export const saveGraphBodySchema = z
|
|
|
353
386
|
const usedPairs = new Set<string>()
|
|
354
387
|
graph.connections.forEach((conn, i) => {
|
|
355
388
|
const from = portById.get(conn.from_port_id)
|
|
356
|
-
if (
|
|
389
|
+
if (from?.io !== "out") {
|
|
357
390
|
ctx.addIssue({
|
|
358
391
|
code: "custom",
|
|
359
392
|
path: ["connections", i, "from_port_id"],
|
|
@@ -361,7 +394,7 @@ export const saveGraphBodySchema = z
|
|
|
361
394
|
})
|
|
362
395
|
}
|
|
363
396
|
const to = portById.get(conn.to_port_id)
|
|
364
|
-
if (
|
|
397
|
+
if (to?.io !== "in") {
|
|
365
398
|
ctx.addIssue({
|
|
366
399
|
code: "custom",
|
|
367
400
|
path: ["connections", i, "to_port_id"],
|
|
@@ -382,6 +415,31 @@ export const saveGraphBodySchema = z
|
|
|
382
415
|
// 型の接続バリデーション(ポートのkind別仕様.md §接続バリデーション)。
|
|
383
416
|
// 導出ポートは接続から型を解決した上で判定する。ai の in・wait の in は常に許可
|
|
384
417
|
const resolved = resolvePortSchemas(graph.nodes, graph.connections)
|
|
418
|
+
|
|
419
|
+
// fan-in(同じ in ポートへの流入が2本以上)は同じ型でなければならない。
|
|
420
|
+
// 型が割れていると伝播が未宣言に落ちて下流の型チェックが効かなくなる
|
|
421
|
+
// (docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
422
|
+
const inboundByPort = new Map<string, string[]>()
|
|
423
|
+
for (const conn of graph.connections) {
|
|
424
|
+
const list = inboundByPort.get(conn.to_port_id) ?? []
|
|
425
|
+
list.push(conn.from_port_id)
|
|
426
|
+
inboundByPort.set(conn.to_port_id, list)
|
|
427
|
+
}
|
|
428
|
+
for (const [toPortId, fromPortIds] of inboundByPort) {
|
|
429
|
+
if (fromPortIds.length < 2) continue
|
|
430
|
+
const schemas = fromPortIds.map(
|
|
431
|
+
(id) => resolved.get(id) ?? UNDECLARED_SCHEMA,
|
|
432
|
+
)
|
|
433
|
+
if (!hasInboundConflict(schemas)) continue
|
|
434
|
+
const to = portById.get(toPortId)
|
|
435
|
+
const toNode = nodeByPortId.get(toPortId)
|
|
436
|
+
ctx.addIssue({
|
|
437
|
+
code: "custom",
|
|
438
|
+
path: ["connections"],
|
|
439
|
+
message: `同じ入力ポート(${toNode?.name ?? "?"}.${to?.key ?? "?"})に異なる型を接続できません`,
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
385
443
|
graph.connections.forEach((conn, i) => {
|
|
386
444
|
const from = portById.get(conn.from_port_id)
|
|
387
445
|
const to = portById.get(conn.to_port_id)
|
|
@@ -405,16 +463,33 @@ export const saveGraphBodySchema = z
|
|
|
405
463
|
})
|
|
406
464
|
}
|
|
407
465
|
|
|
408
|
-
// human の reject の接続先は
|
|
466
|
+
// human の reject の接続先は AI の instruction ポートのみ。
|
|
467
|
+
// 差し戻しは「元のセッションに指示を与える」操作で、上流データの口(in)とは
|
|
468
|
+
// 意味が違う(docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
409
469
|
if (
|
|
410
470
|
fromNode.kind === "human" &&
|
|
411
471
|
from.key === "reject" &&
|
|
412
|
-
toNode.kind
|
|
472
|
+
!(toNode.kind === "ai" && to.key === AI_INSTRUCTION_PORT_KEY)
|
|
413
473
|
) {
|
|
414
474
|
ctx.addIssue({
|
|
415
475
|
code: "custom",
|
|
416
476
|
path: ["connections", i, "to_port_id"],
|
|
417
|
-
message:
|
|
477
|
+
message:
|
|
478
|
+
"差し戻し(reject)の接続先は AI ノードの instruction ポートのみです",
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// instruction ポートに繋げるのは human の reject だけ(データを流し込ませない)
|
|
483
|
+
if (
|
|
484
|
+
toNode.kind === "ai" &&
|
|
485
|
+
to.key === AI_INSTRUCTION_PORT_KEY &&
|
|
486
|
+
!(fromNode.kind === "human" && from.key === "reject")
|
|
487
|
+
) {
|
|
488
|
+
ctx.addIssue({
|
|
489
|
+
code: "custom",
|
|
490
|
+
path: ["connections", i, "to_port_id"],
|
|
491
|
+
message:
|
|
492
|
+
"instruction ポートに接続できるのは human の差し戻し(reject)だけです",
|
|
418
493
|
})
|
|
419
494
|
}
|
|
420
495
|
|
|
@@ -424,7 +499,8 @@ export const saveGraphBodySchema = z
|
|
|
424
499
|
// それ以外は input 全体を data として表示する。型制約は課さない)
|
|
425
500
|
|
|
426
501
|
// program の in への接続は 伝播型 ⊆ 宣言スキーマ。in は接続鏡映(sticky)なので
|
|
427
|
-
//
|
|
502
|
+
// 具体型の接続では自明に通り、接続元が未宣言 × 宣言維持のときだけ従来の意味を持つ
|
|
503
|
+
// (未宣言は unknown=判断できないので通す。作りかけのグラフを保存できなくしない)
|
|
428
504
|
const toSchema = resolved.get(conn.to_port_id) ?? to.schema
|
|
429
505
|
if (toNode.kind === "program" && !isSchemaSubset(fromSchema, toSchema)) {
|
|
430
506
|
ctx.addIssue({
|
|
@@ -434,6 +510,19 @@ export const saveGraphBodySchema = z
|
|
|
434
510
|
"接続元の型が program の入力スキーマに適合しません(伝播型 ⊆ 宣言スキーマ)",
|
|
435
511
|
})
|
|
436
512
|
}
|
|
513
|
+
|
|
514
|
+
// component の in は子 loop の契約(焼き込み済み宣言)。program と同じ包含チェック
|
|
515
|
+
if (
|
|
516
|
+
toNode.kind === "component" &&
|
|
517
|
+
!isSchemaSubset(fromSchema, toSchema)
|
|
518
|
+
) {
|
|
519
|
+
ctx.addIssue({
|
|
520
|
+
code: "custom",
|
|
521
|
+
path: ["connections", i, "to_port_id"],
|
|
522
|
+
message:
|
|
523
|
+
"接続元の型がコンポーネントの入力契約に適合しません(伝播型 ⊆ 契約)",
|
|
524
|
+
})
|
|
525
|
+
}
|
|
437
526
|
})
|
|
438
527
|
})
|
|
439
528
|
export type SaveGraphBody = z.infer<typeof saveGraphBodySchema>
|
package/_schemas/node.ts
CHANGED
|
@@ -10,6 +10,9 @@ export const nodeKindSchema = z.enum([
|
|
|
10
10
|
// docs/tasks/wip/開始・終了ノードの定義.md)
|
|
11
11
|
"start",
|
|
12
12
|
"end",
|
|
13
|
+
// コンポーネント(kind='component' の loop を配置して呼ぶ。子 run・戻り値あり。
|
|
14
|
+
// docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
|
|
15
|
+
"component",
|
|
13
16
|
])
|
|
14
17
|
export type NodeKind = z.infer<typeof nodeKindSchema>
|
|
15
18
|
|