@mawaru/sdk 0.5.0 → 0.9.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 +232 -12
- package/_schemas/graph.ts +121 -13
- 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 +25 -3
- package/dist/_schemas/graph.js +100 -14
- 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/cli.js +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +3 -0
- package/dist/typegen.js +8 -1
- package/docs/development.md +9 -2
- package/index.ts +1 -0
- package/package.json +11 -17
- 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()
|
|
@@ -236,6 +261,37 @@ describe("humanConfigSchema", () => {
|
|
|
236
261
|
humanConfigSchema.parse({ assignment: { units: [["not-a-uuid"]] } }),
|
|
237
262
|
).toThrow()
|
|
238
263
|
})
|
|
264
|
+
|
|
265
|
+
it("run_starter:固定メンバーと混ぜて受け付ける(実行時に start_by で解決)", () => {
|
|
266
|
+
const u1 = uuid()
|
|
267
|
+
const parsed = humanConfigSchema.parse({
|
|
268
|
+
assignment: {
|
|
269
|
+
strategy: "fixed",
|
|
270
|
+
approval_mode: "all",
|
|
271
|
+
units: [["run_starter", u1]],
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
expect(parsed.assignment.units).toEqual([["run_starter", u1]])
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it("run_starter:交代制(round_robin)との併用を拒否する", () => {
|
|
278
|
+
expect(() =>
|
|
279
|
+
humanConfigSchema.parse({
|
|
280
|
+
assignment: {
|
|
281
|
+
strategy: "round_robin",
|
|
282
|
+
units: [["run_starter"], [uuid()]],
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
).toThrow()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it("run_starter:ユニット内の重複を拒否する", () => {
|
|
289
|
+
expect(() =>
|
|
290
|
+
humanConfigSchema.parse({
|
|
291
|
+
assignment: { units: [["run_starter", "run_starter"]] },
|
|
292
|
+
}),
|
|
293
|
+
).toThrow()
|
|
294
|
+
})
|
|
239
295
|
})
|
|
240
296
|
|
|
241
297
|
describe("saveGraphBodySchema", () => {
|
|
@@ -258,6 +314,52 @@ describe("saveGraphBodySchema", () => {
|
|
|
258
314
|
})
|
|
259
315
|
})
|
|
260
316
|
|
|
317
|
+
// fan-in(同じ in ポートへの流入2本以上)は同じ型でなければならない
|
|
318
|
+
// (docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
319
|
+
const programNode = (outSchema: GraphPort["schema"]) => ({
|
|
320
|
+
...makeNode([
|
|
321
|
+
port("in", "in"),
|
|
322
|
+
{ ...port("out", "main"), schema: outSchema },
|
|
323
|
+
]),
|
|
324
|
+
kind: "program" as const,
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it("fan-in:流入の型が同じなら受け付ける", () => {
|
|
328
|
+
const a = programNode({ type: "string" })
|
|
329
|
+
const b = programNode({ type: "string" })
|
|
330
|
+
const sink = { ...makeNode(simplePorts()), kind: "wait" as const }
|
|
331
|
+
expect(() =>
|
|
332
|
+
saveGraphBodySchema.parse({
|
|
333
|
+
nodes: [a, b, sink],
|
|
334
|
+
connections: [connect(a, sink), connect(b, sink)],
|
|
335
|
+
}),
|
|
336
|
+
).not.toThrow()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("fan-in:異なる具体型の流入を拒否する", () => {
|
|
340
|
+
const a = programNode({ type: "string" })
|
|
341
|
+
const b = programNode({ type: "number" })
|
|
342
|
+
const sink = { ...makeNode(simplePorts()), kind: "wait" as const }
|
|
343
|
+
expect(() =>
|
|
344
|
+
saveGraphBodySchema.parse({
|
|
345
|
+
nodes: [a, b, sink],
|
|
346
|
+
connections: [connect(a, sink), connect(b, sink)],
|
|
347
|
+
}),
|
|
348
|
+
).toThrow(/異なる型を接続できません/)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it("fan-in:具体型1種類+未宣言 の混在は受け付ける(未宣言は判断材料にしない)", () => {
|
|
352
|
+
const a = programNode({ type: "string" })
|
|
353
|
+
const b = { ...makeNode(simplePorts()), kind: "wait" as const } // out main は導出=未宣言
|
|
354
|
+
const sink = { ...makeNode(simplePorts()), kind: "wait" as const }
|
|
355
|
+
expect(() =>
|
|
356
|
+
saveGraphBodySchema.parse({
|
|
357
|
+
nodes: [a, b, sink],
|
|
358
|
+
connections: [connect(a, sink), connect(b, sink)],
|
|
359
|
+
}),
|
|
360
|
+
).not.toThrow()
|
|
361
|
+
})
|
|
362
|
+
|
|
261
363
|
it("node id の重複を拒否する", () => {
|
|
262
364
|
const a = makeNode()
|
|
263
365
|
expect(() =>
|
|
@@ -410,6 +512,7 @@ const humanNode = () => ({
|
|
|
410
512
|
const aiNode = () => ({
|
|
411
513
|
...makeNode([
|
|
412
514
|
port("in", "in"),
|
|
515
|
+
port("in", "instruction"),
|
|
413
516
|
{ ...port("out", "main"), schema: aiEnvelope },
|
|
414
517
|
]),
|
|
415
518
|
kind: "ai" as const,
|
|
@@ -418,6 +521,11 @@ const programNode = (inSchema: GraphPort["schema"] = {}) => ({
|
|
|
418
521
|
...makeNode([{ ...port("in", "in"), schema: inSchema }, port("out", "main")]),
|
|
419
522
|
kind: "program" as const,
|
|
420
523
|
})
|
|
524
|
+
const inByKey = (node: TestNode, key: string): GraphPort => {
|
|
525
|
+
const p = node.ports.find((p) => p.io === "in" && p.key === key)
|
|
526
|
+
if (!p) throw new Error(`in port not found: ${key}`)
|
|
527
|
+
return p
|
|
528
|
+
}
|
|
421
529
|
const outByKey = (node: TestNode, key: string): GraphPort => {
|
|
422
530
|
const p = node.ports.find((p) => p.io === "out" && p.key === key)
|
|
423
531
|
if (!p) throw new Error(`out port not found: ${key}`)
|
|
@@ -442,6 +550,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
442
550
|
const multi = {
|
|
443
551
|
...makeNode([
|
|
444
552
|
port("in", "in"),
|
|
553
|
+
port("in", "instruction"),
|
|
445
554
|
{ ...port("out", "spam"), schema: aiEnvelope },
|
|
446
555
|
{ ...port("out", "sales"), schema: aiEnvelope },
|
|
447
556
|
{ ...port("out", "support"), schema: aiEnvelope },
|
|
@@ -451,10 +560,11 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
451
560
|
expect(() =>
|
|
452
561
|
saveGraphBodySchema.parse({ nodes: [multi], connections: [] }),
|
|
453
562
|
).not.toThrow()
|
|
454
|
-
// in
|
|
563
|
+
// in の構成は in / instruction 固定のまま
|
|
455
564
|
const badIn = {
|
|
456
565
|
...makeNode([
|
|
457
566
|
port("in", "input"),
|
|
567
|
+
port("in", "instruction"),
|
|
458
568
|
{ ...port("out", "main"), schema: aiEnvelope },
|
|
459
569
|
]),
|
|
460
570
|
kind: "ai" as const,
|
|
@@ -481,6 +591,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
481
591
|
it("ai の出力宣言が封筒型 { data, description } でないと拒否する(全出口に適用)", () => {
|
|
482
592
|
const bad = makeNode([
|
|
483
593
|
port("in", "in"),
|
|
594
|
+
port("in", "instruction"),
|
|
484
595
|
{ ...port("out", "main"), schema: { type: "object" } },
|
|
485
596
|
])
|
|
486
597
|
expect(() =>
|
|
@@ -489,6 +600,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
489
600
|
// main 以外の出口も封筒型を強制する
|
|
490
601
|
const badExtra = makeNode([
|
|
491
602
|
port("in", "in"),
|
|
603
|
+
port("in", "instruction"),
|
|
492
604
|
{ ...port("out", "main"), schema: aiEnvelope },
|
|
493
605
|
{ ...port("out", "extra"), schema: { type: "object" } },
|
|
494
606
|
])
|
|
@@ -498,6 +610,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
498
610
|
// data は任意の型でよい(カタログ外の object も通る)
|
|
499
611
|
const custom = makeNode([
|
|
500
612
|
port("in", "in"),
|
|
613
|
+
port("in", "instruction"),
|
|
501
614
|
{
|
|
502
615
|
...port("out", "main"),
|
|
503
616
|
schema: envelopeSchemaOf({
|
|
@@ -514,32 +627,57 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
|
514
627
|
})
|
|
515
628
|
|
|
516
629
|
describe("saveGraphBodySchema(型の接続バリデーション)", () => {
|
|
517
|
-
it("human の reject → ai
|
|
630
|
+
it("human の reject → ai の instruction は許可。ai の in や ai 以外は拒否", () => {
|
|
518
631
|
const human = humanNode()
|
|
519
632
|
const ai = aiNode()
|
|
520
633
|
const prog = programNode()
|
|
634
|
+
const rejectPort = outByKey(human, "reject")
|
|
635
|
+
// 差し戻しは指示の口(instruction)へ
|
|
521
636
|
expect(() =>
|
|
522
637
|
saveGraphBodySchema.parse({
|
|
523
638
|
nodes: [human, ai],
|
|
524
639
|
connections: [
|
|
525
640
|
{
|
|
526
|
-
from_port_id:
|
|
527
|
-
to_port_id:
|
|
641
|
+
from_port_id: rejectPort.id,
|
|
642
|
+
to_port_id: inByKey(ai, "instruction").id,
|
|
528
643
|
},
|
|
529
644
|
],
|
|
530
645
|
}),
|
|
531
646
|
).not.toThrow()
|
|
647
|
+
// 上流データの口(in)には繋げない(意味の違う入力を fan-in させない)
|
|
648
|
+
expect(() =>
|
|
649
|
+
saveGraphBodySchema.parse({
|
|
650
|
+
nodes: [human, ai],
|
|
651
|
+
connections: [
|
|
652
|
+
{ from_port_id: rejectPort.id, to_port_id: inByKey(ai, "in").id },
|
|
653
|
+
],
|
|
654
|
+
}),
|
|
655
|
+
).toThrow(/instruction/)
|
|
656
|
+
// ai 以外にも繋げない
|
|
532
657
|
expect(() =>
|
|
533
658
|
saveGraphBodySchema.parse({
|
|
534
659
|
nodes: [human, prog],
|
|
660
|
+
connections: [
|
|
661
|
+
{ from_port_id: rejectPort.id, to_port_id: inOf(prog).id },
|
|
662
|
+
],
|
|
663
|
+
}),
|
|
664
|
+
).toThrow(/instruction/)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it("instruction ポートに繋げるのは human の reject だけ", () => {
|
|
668
|
+
const ai = aiNode()
|
|
669
|
+
const prog = programNode()
|
|
670
|
+
expect(() =>
|
|
671
|
+
saveGraphBodySchema.parse({
|
|
672
|
+
nodes: [prog, ai],
|
|
535
673
|
connections: [
|
|
536
674
|
{
|
|
537
|
-
from_port_id:
|
|
538
|
-
to_port_id:
|
|
675
|
+
from_port_id: outOf(prog).id,
|
|
676
|
+
to_port_id: inByKey(ai, "instruction").id,
|
|
539
677
|
},
|
|
540
678
|
],
|
|
541
679
|
}),
|
|
542
|
-
).toThrow(/
|
|
680
|
+
).toThrow(/instruction ポートに接続できるのは/)
|
|
543
681
|
})
|
|
544
682
|
|
|
545
683
|
it("human の in はどの型でも接続できる(封筒型の制約は課さない)", () => {
|
|
@@ -870,3 +1008,85 @@ describe("refsDefaultsOf(定義スキーマ→デフォルト値の袋)", ()
|
|
|
870
1008
|
expect(refsDefaultsOf({ type: "object", properties: {} })).toEqual({})
|
|
871
1009
|
})
|
|
872
1010
|
})
|
|
1011
|
+
|
|
1012
|
+
describe("saveGraphBodySchema:コンポーネントノード(component kind)", () => {
|
|
1013
|
+
// コンポーネント=kind='component' の loop を配置して呼ぶノード。ポートは
|
|
1014
|
+
// in/main 固定で、スキーマは子 loop の契約から保存時にサーバが焼き込む(宣言扱い)
|
|
1015
|
+
const componentNode = (
|
|
1016
|
+
inSchema: GraphPort["schema"] = { type: "object" },
|
|
1017
|
+
) => ({
|
|
1018
|
+
...makeNode([
|
|
1019
|
+
{ ...port("in", "in"), schema: inSchema },
|
|
1020
|
+
port("out", "main"),
|
|
1021
|
+
]),
|
|
1022
|
+
kind: "component" as const,
|
|
1023
|
+
component: { component_loop_id: uuid() },
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
it("component ノード(in/main+component_loop_id)を受け付ける", () => {
|
|
1027
|
+
expect(() =>
|
|
1028
|
+
saveGraphBodySchema.parse({ nodes: [componentNode()], connections: [] }),
|
|
1029
|
+
).not.toThrow()
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
it("component_loop_id の無い component ノードを拒否する", () => {
|
|
1033
|
+
const node = { ...componentNode(), component: undefined }
|
|
1034
|
+
expect(() =>
|
|
1035
|
+
saveGraphBodySchema.parse({ nodes: [node], connections: [] }),
|
|
1036
|
+
).toThrow(/component_loop_id/)
|
|
1037
|
+
})
|
|
1038
|
+
|
|
1039
|
+
it("ポート構成は in/main 固定(out の key 違い・複数 out を拒否)", () => {
|
|
1040
|
+
const badKey = {
|
|
1041
|
+
...componentNode(),
|
|
1042
|
+
ports: [port("in", "in"), port("out", "result")],
|
|
1043
|
+
}
|
|
1044
|
+
expect(() =>
|
|
1045
|
+
saveGraphBodySchema.parse({ nodes: [badKey], connections: [] }),
|
|
1046
|
+
).toThrow(/component のポート構成/)
|
|
1047
|
+
const multiOut = {
|
|
1048
|
+
...componentNode(),
|
|
1049
|
+
ports: [port("in", "in"), port("out", "main"), port("out", "sub")],
|
|
1050
|
+
}
|
|
1051
|
+
expect(() =>
|
|
1052
|
+
saveGraphBodySchema.parse({ nodes: [multiOut], connections: [] }),
|
|
1053
|
+
).toThrow(/component のポート構成/)
|
|
1054
|
+
})
|
|
1055
|
+
|
|
1056
|
+
it("component の in への接続は契約への適合を検証する(伝播型 ⊆ 契約)", () => {
|
|
1057
|
+
// 上流 program の out {type:"string"} → component の in {type:"number"} は不適合
|
|
1058
|
+
const prog = {
|
|
1059
|
+
...makeNode([
|
|
1060
|
+
port("in", "in"),
|
|
1061
|
+
{ ...port("out", "main"), schema: { type: "string" } },
|
|
1062
|
+
]),
|
|
1063
|
+
kind: "program" as const,
|
|
1064
|
+
}
|
|
1065
|
+
const comp = componentNode({ type: "number" })
|
|
1066
|
+
expect(() =>
|
|
1067
|
+
saveGraphBodySchema.parse({
|
|
1068
|
+
nodes: [prog, comp],
|
|
1069
|
+
connections: [connect(prog, comp)],
|
|
1070
|
+
}),
|
|
1071
|
+
).toThrow(/入力契約に適合しません/)
|
|
1072
|
+
// 適合(string → string)なら通る
|
|
1073
|
+
const okComp = componentNode({ type: "string" })
|
|
1074
|
+
expect(() =>
|
|
1075
|
+
saveGraphBodySchema.parse({
|
|
1076
|
+
nodes: [prog, okComp],
|
|
1077
|
+
connections: [connect(prog, okComp)],
|
|
1078
|
+
}),
|
|
1079
|
+
).not.toThrow()
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
it("component の in の宣言スキーマは接続で潰されない(導出/鏡映扱いにしない)", () => {
|
|
1083
|
+
const contract = {
|
|
1084
|
+
type: "object",
|
|
1085
|
+
properties: { pr_urls: { type: "array" } },
|
|
1086
|
+
required: ["pr_urls"],
|
|
1087
|
+
}
|
|
1088
|
+
const comp = componentNode(contract)
|
|
1089
|
+
const resolved = resolvePortSchemas([comp], [])
|
|
1090
|
+
expect(resolved.get(inOf(comp).id)).toEqual(contract)
|
|
1091
|
+
})
|
|
1092
|
+
})
|
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
|
|
|
@@ -126,18 +130,31 @@ export const aiConfigSchema = z.object({
|
|
|
126
130
|
})
|
|
127
131
|
export type AiConfig = z.infer<typeof aiConfigSchema>
|
|
128
132
|
|
|
133
|
+
// 担当ユニットのメンバー:user_id の名指し、または動的な担当「ループを実行した人」
|
|
134
|
+
// (run の起動者。実行時に runs.start_by で解決し、解決できなければその1人だけ落ちる。
|
|
135
|
+
// docs/tasks/wip/Human担当に「ループを実行した人」を追加する.md)
|
|
136
|
+
export const RUN_STARTER = "run_starter"
|
|
137
|
+
export const assignMemberSchema = z.union([z.uuid(), z.literal(RUN_STARTER)])
|
|
138
|
+
export type AssignMember = z.infer<typeof assignMemberSchema>
|
|
139
|
+
|
|
129
140
|
// Human ノードの kind 別設定(node_humans + node_human_assignees に対応)。
|
|
130
141
|
// 担当 = ユニット(1人以上のメンバー)の順序付きリスト + 承認モード + 選出戦略。
|
|
131
|
-
// units[i]
|
|
142
|
+
// units[i] はメンバーの配列(1人=個人・複数=グループ)。units 空 = tenant の誰でも。
|
|
132
143
|
// approval_mode はノード単位(any=誰か1人 / all=全員。差し戻しは1人で成立=veto)。
|
|
133
144
|
// メンバーが tenant の membership であることの検証は保存 API 側で行う
|
|
134
145
|
// (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
|
|
135
146
|
export const humanConfigSchema = z.object({
|
|
147
|
+
// 承認 UI(承認ビュー)の識別子。program_dir / ai_dir と同じ「不透明なキー」で、
|
|
148
|
+
// どのキーが実在するか・そのテナントが選べるかは保存時に mawaru 側が検証する
|
|
149
|
+
// (カタログは非公開。@mawaru/common の approval-view.ts)。省略時は標準ビュー
|
|
150
|
+
view: z.string().min(1).optional(),
|
|
151
|
+
// ビューに渡すパラメータ(列・ラベル・閾値など)。形はビューごとに違うので任意の JSON
|
|
152
|
+
view_config: z.unknown().nullish(),
|
|
136
153
|
assignment: z
|
|
137
154
|
.object({
|
|
138
155
|
strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
|
|
139
156
|
approval_mode: z.enum(["any", "all"]).default("any"),
|
|
140
|
-
units: z.array(z.array(
|
|
157
|
+
units: z.array(z.array(assignMemberSchema).min(1)).default([]),
|
|
141
158
|
})
|
|
142
159
|
.superRefine((assignment, ctx) => {
|
|
143
160
|
if (assignment.strategy === "fixed" && assignment.units.length > 1) {
|
|
@@ -157,13 +174,25 @@ export const humanConfigSchema = z.object({
|
|
|
157
174
|
message: "round_robin にはユニットが2つ以上必要です",
|
|
158
175
|
})
|
|
159
176
|
}
|
|
177
|
+
// 交代制と「ループを実行した人」は排他:どの回に誰が担当かが起動者と交代位置の
|
|
178
|
+
// 掛け算になって読めなくなるため、設定できる形の方を減らす
|
|
179
|
+
if (
|
|
180
|
+
assignment.strategy === "round_robin" &&
|
|
181
|
+
assignment.units.some((unit) => unit.includes(RUN_STARTER))
|
|
182
|
+
) {
|
|
183
|
+
ctx.addIssue({
|
|
184
|
+
code: "custom",
|
|
185
|
+
path: ["units"],
|
|
186
|
+
message: "round_robin の担当に「ループを実行した人」は入れられません",
|
|
187
|
+
})
|
|
188
|
+
}
|
|
160
189
|
// ユニット内の重複は拒否(またぐ重複は「Aは毎回・相手が交代」の形があるので許す)
|
|
161
190
|
assignment.units.forEach((unit, i) => {
|
|
162
191
|
if (new Set(unit).size !== unit.length) {
|
|
163
192
|
ctx.addIssue({
|
|
164
193
|
code: "custom",
|
|
165
194
|
path: ["units", i],
|
|
166
|
-
message: "
|
|
195
|
+
message: "ユニット内で担当が重複しています",
|
|
167
196
|
})
|
|
168
197
|
}
|
|
169
198
|
})
|
|
@@ -171,6 +200,14 @@ export const humanConfigSchema = z.object({
|
|
|
171
200
|
})
|
|
172
201
|
export type HumanConfig = z.infer<typeof humanConfigSchema>
|
|
173
202
|
|
|
203
|
+
// コンポーネントノードの kind 別設定(node_components に対応)。呼ぶ先は
|
|
204
|
+
// kind='component' の loop。同 tenant・kind の検証と自 loop 参照の禁止は保存 API 側で行う
|
|
205
|
+
// (docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
|
|
206
|
+
export const componentConfigSchema = z.object({
|
|
207
|
+
component_loop_id: z.uuid(),
|
|
208
|
+
})
|
|
209
|
+
export type ComponentConfig = z.infer<typeof componentConfigSchema>
|
|
210
|
+
|
|
174
211
|
// ノードにアタッチされた IOHook(node_hooks に対応。アタッチできるのは program ノードのみ)。
|
|
175
212
|
// on_input / on_output / on_signal はアタッチ時に repo の config.json(hookManifestSchema)から
|
|
176
213
|
// エディタが焼き込むスナップショット。ref 省略時はデフォルトブランチ
|
|
@@ -206,6 +243,7 @@ export const graphNodeSchema = z.object({
|
|
|
206
243
|
program: programConfigSchema.optional(),
|
|
207
244
|
ai: aiConfigSchema.optional(),
|
|
208
245
|
human: humanConfigSchema.optional(),
|
|
246
|
+
component: componentConfigSchema.optional(),
|
|
209
247
|
})
|
|
210
248
|
export type GraphNode = z.infer<typeof graphNodeSchema>
|
|
211
249
|
|
|
@@ -279,16 +317,20 @@ export const saveGraphBodySchema = z
|
|
|
279
317
|
if (port.io === "in") inCount++
|
|
280
318
|
else outCount++
|
|
281
319
|
})
|
|
282
|
-
// end
|
|
283
|
-
//
|
|
284
|
-
|
|
320
|
+
// end は「in は1つ以上」(複数ルートを別々の in で集約する)。
|
|
321
|
+
// ai は2つ(上流データの in + 差し戻しの instruction)。他 kind は1つ固定。
|
|
322
|
+
// key の妥当性は kindPortsViolation が別途見る
|
|
323
|
+
const expectIn = node.kind === "ai" ? 2 : 1
|
|
324
|
+
const inCountOk =
|
|
325
|
+
node.kind === "end" ? inCount >= 1 : inCount === expectIn
|
|
326
|
+
if (!inCountOk) {
|
|
285
327
|
ctx.addIssue({
|
|
286
328
|
code: "custom",
|
|
287
329
|
path: ["nodes", i, "ports"],
|
|
288
330
|
message:
|
|
289
331
|
node.kind === "end"
|
|
290
332
|
? "in ポートが1つ以上必要です"
|
|
291
|
-
:
|
|
333
|
+
: `in ポートはちょうど${expectIn}つ必要です`,
|
|
292
334
|
})
|
|
293
335
|
}
|
|
294
336
|
if (outCount < 1) {
|
|
@@ -321,6 +363,16 @@ export const saveGraphBodySchema = z
|
|
|
321
363
|
hookDirs.add(hook.hook_dir)
|
|
322
364
|
})
|
|
323
365
|
|
|
366
|
+
// component は参照先が無いと成立しない(他 kind の設定はデフォルトで補えるが
|
|
367
|
+
// component_loop_id は補えない)
|
|
368
|
+
if (node.kind === "component" && node.component === undefined) {
|
|
369
|
+
ctx.addIssue({
|
|
370
|
+
code: "custom",
|
|
371
|
+
path: ["nodes", i, "component"],
|
|
372
|
+
message: "component ノードには component_loop_id が必要です",
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
324
376
|
// kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md)
|
|
325
377
|
const violation = kindPortsViolation(node.kind, node.ports)
|
|
326
378
|
if (violation) {
|
|
@@ -353,7 +405,7 @@ export const saveGraphBodySchema = z
|
|
|
353
405
|
const usedPairs = new Set<string>()
|
|
354
406
|
graph.connections.forEach((conn, i) => {
|
|
355
407
|
const from = portById.get(conn.from_port_id)
|
|
356
|
-
if (
|
|
408
|
+
if (from?.io !== "out") {
|
|
357
409
|
ctx.addIssue({
|
|
358
410
|
code: "custom",
|
|
359
411
|
path: ["connections", i, "from_port_id"],
|
|
@@ -361,7 +413,7 @@ export const saveGraphBodySchema = z
|
|
|
361
413
|
})
|
|
362
414
|
}
|
|
363
415
|
const to = portById.get(conn.to_port_id)
|
|
364
|
-
if (
|
|
416
|
+
if (to?.io !== "in") {
|
|
365
417
|
ctx.addIssue({
|
|
366
418
|
code: "custom",
|
|
367
419
|
path: ["connections", i, "to_port_id"],
|
|
@@ -382,6 +434,31 @@ export const saveGraphBodySchema = z
|
|
|
382
434
|
// 型の接続バリデーション(ポートのkind別仕様.md §接続バリデーション)。
|
|
383
435
|
// 導出ポートは接続から型を解決した上で判定する。ai の in・wait の in は常に許可
|
|
384
436
|
const resolved = resolvePortSchemas(graph.nodes, graph.connections)
|
|
437
|
+
|
|
438
|
+
// fan-in(同じ in ポートへの流入が2本以上)は同じ型でなければならない。
|
|
439
|
+
// 型が割れていると伝播が未宣言に落ちて下流の型チェックが効かなくなる
|
|
440
|
+
// (docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
441
|
+
const inboundByPort = new Map<string, string[]>()
|
|
442
|
+
for (const conn of graph.connections) {
|
|
443
|
+
const list = inboundByPort.get(conn.to_port_id) ?? []
|
|
444
|
+
list.push(conn.from_port_id)
|
|
445
|
+
inboundByPort.set(conn.to_port_id, list)
|
|
446
|
+
}
|
|
447
|
+
for (const [toPortId, fromPortIds] of inboundByPort) {
|
|
448
|
+
if (fromPortIds.length < 2) continue
|
|
449
|
+
const schemas = fromPortIds.map(
|
|
450
|
+
(id) => resolved.get(id) ?? UNDECLARED_SCHEMA,
|
|
451
|
+
)
|
|
452
|
+
if (!hasInboundConflict(schemas)) continue
|
|
453
|
+
const to = portById.get(toPortId)
|
|
454
|
+
const toNode = nodeByPortId.get(toPortId)
|
|
455
|
+
ctx.addIssue({
|
|
456
|
+
code: "custom",
|
|
457
|
+
path: ["connections"],
|
|
458
|
+
message: `同じ入力ポート(${toNode?.name ?? "?"}.${to?.key ?? "?"})に異なる型を接続できません`,
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
385
462
|
graph.connections.forEach((conn, i) => {
|
|
386
463
|
const from = portById.get(conn.from_port_id)
|
|
387
464
|
const to = portById.get(conn.to_port_id)
|
|
@@ -405,16 +482,33 @@ export const saveGraphBodySchema = z
|
|
|
405
482
|
})
|
|
406
483
|
}
|
|
407
484
|
|
|
408
|
-
// human の reject の接続先は
|
|
485
|
+
// human の reject の接続先は AI の instruction ポートのみ。
|
|
486
|
+
// 差し戻しは「元のセッションに指示を与える」操作で、上流データの口(in)とは
|
|
487
|
+
// 意味が違う(docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
409
488
|
if (
|
|
410
489
|
fromNode.kind === "human" &&
|
|
411
490
|
from.key === "reject" &&
|
|
412
|
-
toNode.kind
|
|
491
|
+
!(toNode.kind === "ai" && to.key === AI_INSTRUCTION_PORT_KEY)
|
|
413
492
|
) {
|
|
414
493
|
ctx.addIssue({
|
|
415
494
|
code: "custom",
|
|
416
495
|
path: ["connections", i, "to_port_id"],
|
|
417
|
-
message:
|
|
496
|
+
message:
|
|
497
|
+
"差し戻し(reject)の接続先は AI ノードの instruction ポートのみです",
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// instruction ポートに繋げるのは human の reject だけ(データを流し込ませない)
|
|
502
|
+
if (
|
|
503
|
+
toNode.kind === "ai" &&
|
|
504
|
+
to.key === AI_INSTRUCTION_PORT_KEY &&
|
|
505
|
+
!(fromNode.kind === "human" && from.key === "reject")
|
|
506
|
+
) {
|
|
507
|
+
ctx.addIssue({
|
|
508
|
+
code: "custom",
|
|
509
|
+
path: ["connections", i, "to_port_id"],
|
|
510
|
+
message:
|
|
511
|
+
"instruction ポートに接続できるのは human の差し戻し(reject)だけです",
|
|
418
512
|
})
|
|
419
513
|
}
|
|
420
514
|
|
|
@@ -424,7 +518,8 @@ export const saveGraphBodySchema = z
|
|
|
424
518
|
// それ以外は input 全体を data として表示する。型制約は課さない)
|
|
425
519
|
|
|
426
520
|
// program の in への接続は 伝播型 ⊆ 宣言スキーマ。in は接続鏡映(sticky)なので
|
|
427
|
-
//
|
|
521
|
+
// 具体型の接続では自明に通り、接続元が未宣言 × 宣言維持のときだけ従来の意味を持つ
|
|
522
|
+
// (未宣言は unknown=判断できないので通す。作りかけのグラフを保存できなくしない)
|
|
428
523
|
const toSchema = resolved.get(conn.to_port_id) ?? to.schema
|
|
429
524
|
if (toNode.kind === "program" && !isSchemaSubset(fromSchema, toSchema)) {
|
|
430
525
|
ctx.addIssue({
|
|
@@ -434,6 +529,19 @@ export const saveGraphBodySchema = z
|
|
|
434
529
|
"接続元の型が program の入力スキーマに適合しません(伝播型 ⊆ 宣言スキーマ)",
|
|
435
530
|
})
|
|
436
531
|
}
|
|
532
|
+
|
|
533
|
+
// component の in は子 loop の契約(焼き込み済み宣言)。program と同じ包含チェック
|
|
534
|
+
if (
|
|
535
|
+
toNode.kind === "component" &&
|
|
536
|
+
!isSchemaSubset(fromSchema, toSchema)
|
|
537
|
+
) {
|
|
538
|
+
ctx.addIssue({
|
|
539
|
+
code: "custom",
|
|
540
|
+
path: ["connections", i, "to_port_id"],
|
|
541
|
+
message:
|
|
542
|
+
"接続元の型がコンポーネントの入力契約に適合しません(伝播型 ⊆ 契約)",
|
|
543
|
+
})
|
|
544
|
+
}
|
|
437
545
|
})
|
|
438
546
|
})
|
|
439
547
|
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
|
|