@mawaru/sdk 0.13.0 → 0.14.1
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/extract-component.test.ts +51 -2
- package/_schemas/extract-component.ts +4 -2
- package/_schemas/graph.test.ts +87 -66
- package/_schemas/graph.ts +14 -29
- package/_schemas/node.ts +4 -0
- package/_schemas/port-spec.test.ts +120 -129
- package/_schemas/port-spec.ts +70 -106
- package/dist/_schemas/ai-manifest.d.ts +12 -1
- package/dist/_schemas/ai-manifest.js +35 -20
- package/dist/_schemas/extract-component.js +4 -2
- package/dist/_schemas/graph.d.ts +5 -0
- package/dist/_schemas/graph.js +15 -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 +57 -89
- 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,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { AI_MODELS, aiManifestSchema } from "./ai-manifest.js"
|
|
3
|
-
import {
|
|
3
|
+
import { instructionSchema } from "./port-spec.js"
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const textOutput = { type: "string" }
|
|
6
6
|
|
|
7
7
|
const valid = {
|
|
8
8
|
name: "返信下書き",
|
|
@@ -11,8 +11,8 @@ const valid = {
|
|
|
11
11
|
model: "claude-opus-4-8",
|
|
12
12
|
skills: ["answer-faq"],
|
|
13
13
|
env: ["ANTHROPIC_API_KEY"],
|
|
14
|
-
inputs: { in: {} },
|
|
15
|
-
outputs: { main:
|
|
14
|
+
inputs: { in: { schema: {} } },
|
|
15
|
+
outputs: { main: textOutput },
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
describe("aiManifestSchema", () => {
|
|
@@ -28,8 +28,8 @@ describe("aiManifestSchema", () => {
|
|
|
28
28
|
it("name / description / model / skills / env は省略できる(既定値が補われる)", () => {
|
|
29
29
|
const manifest = aiManifestSchema.parse({
|
|
30
30
|
prompt: valid.prompt,
|
|
31
|
-
inputs: { in: {} },
|
|
32
|
-
outputs: { main:
|
|
31
|
+
inputs: { in: { schema: {} } },
|
|
32
|
+
outputs: { main: textOutput },
|
|
33
33
|
})
|
|
34
34
|
expect(manifest.name).toBeUndefined()
|
|
35
35
|
expect(manifest.model).toBe("claude-opus-5")
|
|
@@ -56,90 +56,85 @@ describe("aiManifestSchema", () => {
|
|
|
56
56
|
).toThrow()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
it(
|
|
60
|
-
//
|
|
59
|
+
it("inputs は key 自由・1件以上(in / instruction の固定構成は廃止)", () => {
|
|
60
|
+
// 0件は拒否
|
|
61
61
|
expect(() => aiManifestSchema.parse({ ...valid, inputs: {} })).toThrow()
|
|
62
|
+
// key は自由(旧固定の in / instruction 以外も宣言できる)
|
|
62
63
|
expect(() =>
|
|
63
|
-
aiManifestSchema.parse({ ...valid, inputs: { data: {} } }),
|
|
64
|
-
).toThrow()
|
|
65
|
-
// 固定ポートの instruction も宣言に載る(ノードの現在の姿を書き出すため)
|
|
66
|
-
expect(
|
|
67
64
|
aiManifestSchema.parse({
|
|
68
65
|
...valid,
|
|
69
|
-
inputs: {
|
|
70
|
-
})
|
|
71
|
-
).
|
|
72
|
-
//
|
|
66
|
+
inputs: { task: { schema: {} }, feedback: { schema: {} } },
|
|
67
|
+
}),
|
|
68
|
+
).not.toThrow()
|
|
69
|
+
// key はポート key の規則(英小文字始まり・英数字と _)
|
|
70
|
+
for (const key of ["Bad", "1st", "a-b", ""]) {
|
|
71
|
+
expect(() =>
|
|
72
|
+
aiManifestSchema.parse({ ...valid, inputs: { [key]: { schema: {} } } }),
|
|
73
|
+
).toThrow()
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("inputs は { schema, resume } の新形式で宣言でき、resume が読める", () => {
|
|
78
|
+
const parsed = aiManifestSchema.parse({
|
|
79
|
+
...valid,
|
|
80
|
+
inputs: {
|
|
81
|
+
in: { schema: { type: "object" } },
|
|
82
|
+
instruction: { schema: instructionSchema(), resume: true },
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
expect(parsed.inputs.in).toEqual({ schema: { type: "object" } })
|
|
86
|
+
expect(parsed.inputs.instruction?.schema).toEqual(instructionSchema())
|
|
87
|
+
expect(parsed.inputs.instruction?.resume).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("旧形式(値が素の JSON Schema)は { schema: 値 } として読める", () => {
|
|
91
|
+
const legacy = aiManifestSchema.parse({
|
|
92
|
+
...valid,
|
|
93
|
+
inputs: { in: { type: "object" }, instruction: instructionSchema() },
|
|
94
|
+
})
|
|
95
|
+
expect(legacy.inputs.in).toEqual({ schema: { type: "object" } })
|
|
96
|
+
expect(legacy.inputs.instruction).toEqual({
|
|
97
|
+
schema: instructionSchema(),
|
|
98
|
+
})
|
|
99
|
+
// 未宣言 {} も旧形式として { schema: {} } になる
|
|
73
100
|
expect(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
),
|
|
77
|
-
).toEqual(["in"])
|
|
78
|
-
// kind が固定しない key は宣言できない
|
|
79
|
-
expect(() =>
|
|
80
|
-
aiManifestSchema.parse({ ...valid, inputs: { in: {}, extra: {} } }),
|
|
81
|
-
).toThrow()
|
|
101
|
+
aiManifestSchema.parse({ ...valid, inputs: { in: {} } }).inputs.in,
|
|
102
|
+
).toEqual({ schema: {} })
|
|
82
103
|
})
|
|
83
104
|
|
|
84
105
|
it("outputs は1件以上・key 自由(AI の複数出口=分岐)", () => {
|
|
85
106
|
expect(() => aiManifestSchema.parse({ ...valid, outputs: {} })).toThrow()
|
|
86
107
|
expect(() =>
|
|
87
|
-
aiManifestSchema.parse({ ...valid, outputs: { reply:
|
|
108
|
+
aiManifestSchema.parse({ ...valid, outputs: { reply: textOutput } }),
|
|
88
109
|
).not.toThrow()
|
|
89
110
|
expect(() =>
|
|
90
111
|
aiManifestSchema.parse({
|
|
91
112
|
...valid,
|
|
92
|
-
outputs: { spam:
|
|
113
|
+
outputs: { spam: textOutput, sales: textOutput, support: textOutput },
|
|
93
114
|
}),
|
|
94
115
|
).not.toThrow()
|
|
95
116
|
})
|
|
96
117
|
|
|
97
|
-
it("outputs
|
|
118
|
+
it("outputs は任意の JSON Schema を受け付ける(形の強制はしない)", () => {
|
|
98
119
|
for (const schema of [
|
|
99
120
|
{}, // any
|
|
100
121
|
{ type: "string" },
|
|
101
|
-
{ type: "object", properties: { text: { type: "string" } } },
|
|
102
|
-
{
|
|
103
|
-
type: "object",
|
|
104
|
-
properties: { data: { type: "string" } }, // description なし
|
|
105
|
-
required: ["data"],
|
|
106
|
-
},
|
|
122
|
+
{ type: "object", properties: { text: { type: "string" } } },
|
|
123
|
+
{ type: "array", items: { type: "number" } },
|
|
107
124
|
{
|
|
125
|
+
// かつて強制していた { data, description } もただの object として valid
|
|
108
126
|
type: "object",
|
|
109
127
|
properties: {
|
|
110
128
|
data: { type: "string" },
|
|
111
|
-
description: { type: "string" },
|
|
129
|
+
description: { type: "string" },
|
|
112
130
|
},
|
|
113
|
-
required: ["data"],
|
|
131
|
+
required: ["data", "description"],
|
|
114
132
|
},
|
|
115
133
|
]) {
|
|
116
134
|
expect(() =>
|
|
117
135
|
aiManifestSchema.parse({ ...valid, outputs: { main: schema } }),
|
|
118
|
-
).toThrow()
|
|
119
|
-
// 妥当な出口に混ざっていても、封筒型でない出口があれば弾く
|
|
120
|
-
expect(() =>
|
|
121
|
-
aiManifestSchema.parse({
|
|
122
|
-
...valid,
|
|
123
|
-
outputs: { main: envelope, other: schema },
|
|
124
|
-
}),
|
|
125
|
-
).toThrow()
|
|
136
|
+
).not.toThrow()
|
|
126
137
|
}
|
|
127
|
-
// data が任意の構造(カタログ外の object)でも通る
|
|
128
|
-
expect(() =>
|
|
129
|
-
aiManifestSchema.parse({
|
|
130
|
-
...valid,
|
|
131
|
-
outputs: {
|
|
132
|
-
main: envelopeSchemaOf({
|
|
133
|
-
type: "object",
|
|
134
|
-
properties: {
|
|
135
|
-
title: { type: "string" },
|
|
136
|
-
count: { type: "number" },
|
|
137
|
-
},
|
|
138
|
-
required: ["title"],
|
|
139
|
-
}),
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
).not.toThrow()
|
|
143
138
|
})
|
|
144
139
|
|
|
145
140
|
it("ディレクトリ名として不正な skill 名を拒否する", () => {
|
package/_schemas/ai-manifest.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
|
-
import { jsonSchemaSchema } from "./node.js"
|
|
3
|
-
import { isAiEnvelopeSchema, manifestInKeysViolation } from "./port-spec.js"
|
|
2
|
+
import { jsonSchemaSchema, PORT_KEY_PATTERN } from "./node.js"
|
|
4
3
|
|
|
5
4
|
// AI ノードのモデル選択肢(既定は Opus:下書き品質が価値の中心。費用はユーザー持ち)。
|
|
6
5
|
// エイリアス("opus" 等)は採らずバージョンを固定する:実行のたびに黙って別モデルへ
|
|
@@ -15,6 +14,23 @@ export const aiModelSchema = z.enum(AI_MODELS)
|
|
|
15
14
|
export type AiModel = z.infer<typeof aiModelSchema>
|
|
16
15
|
export const DEFAULT_AI_MODEL = "claude-opus-5" satisfies AiModel
|
|
17
16
|
|
|
17
|
+
// 入力口の宣言:スキーマ+実行属性。resume: true の口から入力が届いた実行は
|
|
18
|
+
// 同じノードの直近セッションを復元して再開する(backend が ports.resume へ同期して
|
|
19
|
+
// payload 発行時に参照する。ポート型固定の撤去とセッション再開のポート属性化.md)
|
|
20
|
+
export const aiInputDeclSchema = z.object({
|
|
21
|
+
schema: jsonSchemaSchema,
|
|
22
|
+
resume: z.boolean().optional(),
|
|
23
|
+
})
|
|
24
|
+
export type AiInputDecl = z.infer<typeof aiInputDeclSchema>
|
|
25
|
+
|
|
26
|
+
// 旧形式(値が素の JSON Schema)は { schema: 値 } として読む。判別は「schema キーを
|
|
27
|
+
// 持つか」:JSON Schema のトップレベルに "schema" キーは現れない(メタスキーマ参照は
|
|
28
|
+
// "$schema")ので実用上一意。書き戻し(backend の syncNodeConfigs)は常に新形式で書く
|
|
29
|
+
const aiInputValueSchema = z.union([
|
|
30
|
+
aiInputDeclSchema,
|
|
31
|
+
jsonSchemaSchema.transform((schema): AiInputDecl => ({ schema })),
|
|
32
|
+
])
|
|
33
|
+
|
|
18
34
|
// nodes/ai/<dir>/config.json(実行repoの構造見直し)。AI ノードの定義(prompt/model/skills/
|
|
19
35
|
// 入出力スキーマ/env)は repo 側が正で、エディタが取り込んで ports を自動生成する
|
|
20
36
|
// (実行時の正は ports テーブルのまま。プログラム規約 v2 と同じ関係)。
|
|
@@ -42,38 +58,37 @@ export const aiManifestSchema = z
|
|
|
42
58
|
// 実行中の経過(発話・ツール呼び出し)を Actions ログへ逐次出すデバッグフラグ。
|
|
43
59
|
// 通常時は config.json にキーを書かない(インスペクタも false ならキーを消す)
|
|
44
60
|
verbose: z.boolean().optional(),
|
|
45
|
-
inputs: z.record(z.string(),
|
|
61
|
+
inputs: z.record(z.string(), aiInputValueSchema),
|
|
46
62
|
outputs: z.record(z.string(), jsonSchemaSchema),
|
|
47
63
|
})
|
|
48
64
|
.superRefine((manifest, ctx) => {
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
Object.keys(manifest.inputs),
|
|
54
|
-
)
|
|
55
|
-
if (inViolation) {
|
|
56
|
-
ctx.addIssue({ code: "custom", path: ["inputs"], message: inViolation })
|
|
57
|
-
}
|
|
58
|
-
// outputs は1件以上・key 自由(複数出口=分岐。AIノードの複数出口(分岐).md)
|
|
59
|
-
if (Object.keys(manifest.outputs).length < 1) {
|
|
65
|
+
// inputs は1件以上・key 自由(in / instruction の固定構成は廃止。
|
|
66
|
+
// セッション再開は key ではなく resume 属性で決まる)
|
|
67
|
+
const inKeys = Object.keys(manifest.inputs)
|
|
68
|
+
if (inKeys.length < 1) {
|
|
60
69
|
ctx.addIssue({
|
|
61
70
|
code: "custom",
|
|
62
|
-
path: ["
|
|
63
|
-
message: "
|
|
71
|
+
path: ["inputs"],
|
|
72
|
+
message: "inputs は1件以上宣言してください",
|
|
64
73
|
})
|
|
65
74
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// data=T は任意の JSON Schema)
|
|
69
|
-
for (const [key, schema] of Object.entries(manifest.outputs)) {
|
|
70
|
-
if (!isAiEnvelopeSchema(schema)) {
|
|
75
|
+
for (const key of inKeys) {
|
|
76
|
+
if (!PORT_KEY_PATTERN.test(key)) {
|
|
71
77
|
ctx.addIssue({
|
|
72
78
|
code: "custom",
|
|
73
|
-
path: ["
|
|
74
|
-
message: `
|
|
79
|
+
path: ["inputs", key],
|
|
80
|
+
message: `inputs の key が不正です: ${key}(英小文字始まり・英数字と _ のみ)`,
|
|
75
81
|
})
|
|
76
82
|
}
|
|
77
83
|
}
|
|
84
|
+
// outputs は1件以上・key 自由(複数出口=分岐。AIノードの複数出口(分岐).md)。
|
|
85
|
+
// スキーマは任意の JSON Schema(形の強制はしない)
|
|
86
|
+
if (Object.keys(manifest.outputs).length < 1) {
|
|
87
|
+
ctx.addIssue({
|
|
88
|
+
code: "custom",
|
|
89
|
+
path: ["outputs"],
|
|
90
|
+
message: "outputs は1件以上宣言してください",
|
|
91
|
+
})
|
|
92
|
+
}
|
|
78
93
|
})
|
|
79
94
|
export type AiManifest = z.infer<typeof aiManifestSchema>
|
|
@@ -110,10 +110,20 @@ describe("analyzeExtractSelection(境界の分類と V1 制約)", () => {
|
|
|
110
110
|
expect(ok.ok).toBe(true)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
it("
|
|
113
|
+
it("1ノード選択:そのノードが入口、その out が出口になる", () => {
|
|
114
|
+
const { a, b, c, nodes, connections } = chain()
|
|
115
|
+
const result = analyzeExtractSelection(nodes, connections, [b.id])
|
|
116
|
+
if (!result.ok) throw new Error(result.reason)
|
|
117
|
+
expect(result.entryNodeId).toBe(b.id)
|
|
118
|
+
expect(result.exitPortId).toBe(outOf(b).id)
|
|
119
|
+
expect(result.internal).toEqual([])
|
|
120
|
+
expect(result.inbound).toEqual([connect(a, b)])
|
|
121
|
+
expect(result.outbound).toEqual([connect(b, c)])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("空・start/end 含みは拒否する", () => {
|
|
114
125
|
const { b, nodes, connections } = chain()
|
|
115
126
|
expect(analyzeExtractSelection(nodes, connections, []).ok).toBe(false)
|
|
116
|
-
expect(analyzeExtractSelection(nodes, connections, [b.id]).ok).toBe(false)
|
|
117
127
|
|
|
118
128
|
const start = makeNode("start", "開始")
|
|
119
129
|
const withStart = [...nodes, start]
|
|
@@ -217,4 +227,43 @@ describe("buildExtractPlan(子グラフと親置換の生成)", () => {
|
|
|
217
227
|
void b
|
|
218
228
|
void c
|
|
219
229
|
})
|
|
230
|
+
|
|
231
|
+
it("1ノード選択:start.main → ノード → end.in と配線され、親では component ノードに置き換わる", () => {
|
|
232
|
+
const { a, b, c, nodes, connections } = chain()
|
|
233
|
+
const childStartOutPortId = uuid()
|
|
234
|
+
const childEndInPortId = uuid()
|
|
235
|
+
const plan = buildExtractPlan({
|
|
236
|
+
nodes,
|
|
237
|
+
connections,
|
|
238
|
+
selectedIds: [b.id],
|
|
239
|
+
name: "待つだけ",
|
|
240
|
+
componentLoopId: uuid(),
|
|
241
|
+
childStartOutPortId,
|
|
242
|
+
childEndInPortId,
|
|
243
|
+
newId: uuid,
|
|
244
|
+
})
|
|
245
|
+
if (!plan.ok) throw new Error(plan.reason)
|
|
246
|
+
expect(plan.childNodes).toHaveLength(1)
|
|
247
|
+
const [nb] = plan.childNodes
|
|
248
|
+
if (!nb) throw new Error("コピーがない")
|
|
249
|
+
expect(nb.wait).toEqual({ wait_type: "duration", duration_seconds: 90 })
|
|
250
|
+
expect(plan.childConnections).toContainEqual({
|
|
251
|
+
from_port_id: childStartOutPortId,
|
|
252
|
+
to_port_id: inOf(nb).id,
|
|
253
|
+
})
|
|
254
|
+
expect(plan.childConnections).toContainEqual({
|
|
255
|
+
from_port_id: outOf(nb).id,
|
|
256
|
+
to_port_id: childEndInPortId,
|
|
257
|
+
})
|
|
258
|
+
// 親:B が component ノードに置き換わり、A.main → group.in/group.main → C.in
|
|
259
|
+
expect(plan.parentNodes.some((n) => n.id === b.id)).toBe(false)
|
|
260
|
+
expect(plan.parentConnections).toContainEqual({
|
|
261
|
+
from_port_id: outOf(a).id,
|
|
262
|
+
to_port_id: inOf(plan.groupNode).id,
|
|
263
|
+
})
|
|
264
|
+
expect(plan.parentConnections).toContainEqual({
|
|
265
|
+
from_port_id: outOf(plan.groupNode).id,
|
|
266
|
+
to_port_id: inOf(c).id,
|
|
267
|
+
})
|
|
268
|
+
})
|
|
220
269
|
})
|
|
@@ -26,8 +26,10 @@ export const analyzeExtractSelection = (
|
|
|
26
26
|
selectedIds: readonly string[],
|
|
27
27
|
): ExtractAnalysis => {
|
|
28
28
|
const selected = new Set(selectedIds)
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// 1ノードからコンポーネント化できる(グループ化の「2つ以上」はエディタ側の
|
|
30
|
+
// 有効条件。コンポーネントの設定値(refs)と1ノードのコンポーネント化.md)
|
|
31
|
+
if (selected.size < 1) {
|
|
32
|
+
return { ok: false, reason: "ノードを選択してください" }
|
|
31
33
|
}
|
|
32
34
|
const selectedNodes = nodes.filter((n) => selected.has(n.id))
|
|
33
35
|
const flow = selectedNodes.find((n) => n.kind === "start" || n.kind === "end")
|
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(() =>
|