@mawaru/sdk 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_schemas/extract-component.test.ts +75 -0
- package/_schemas/extract-component.ts +24 -1
- package/_schemas/graph.test.ts +54 -7
- package/_schemas/graph.ts +7 -3
- package/_schemas/port-spec.test.ts +28 -0
- package/_schemas/port-spec.ts +8 -0
- package/dist/_schemas/extract-component.d.ts +1 -0
- package/dist/_schemas/extract-component.js +17 -2
- package/dist/_schemas/graph.js +5 -3
- package/dist/_schemas/port-spec.d.ts +1 -0
- package/dist/_schemas/port-spec.js +6 -0
- package/package.json +1 -1
- package/skills/create-loop/SKILL.md +6 -0
|
@@ -417,3 +417,78 @@ describe("buildExtractPlan(子グラフと親置換の生成)", () => {
|
|
|
417
417
|
})
|
|
418
418
|
})
|
|
419
419
|
})
|
|
420
|
+
|
|
421
|
+
// forで配列を要素ごとに処理する(繰り返しの枠).md:繰り返しは結果を配列で返すので出口はちょうど1つ
|
|
422
|
+
describe("buildExtractPlan(singleExit:繰り返しの出口は1つ)", () => {
|
|
423
|
+
const childEndNode = (): GraphNode => ({
|
|
424
|
+
id: uuid(),
|
|
425
|
+
kind: "end",
|
|
426
|
+
name: "終了",
|
|
427
|
+
ports: [port("in", "in", {}), port("out", "main", {})],
|
|
428
|
+
hooks: [],
|
|
429
|
+
position_x: 560,
|
|
430
|
+
position_y: 200,
|
|
431
|
+
})
|
|
432
|
+
const plan = (
|
|
433
|
+
nodes: GraphNode[],
|
|
434
|
+
connections: { from_port_id: string; to_port_id: string }[],
|
|
435
|
+
selectedIds: string[],
|
|
436
|
+
childEnd = childEndNode(),
|
|
437
|
+
) =>
|
|
438
|
+
buildExtractPlan({
|
|
439
|
+
nodes,
|
|
440
|
+
connections,
|
|
441
|
+
selectedIds,
|
|
442
|
+
name: "繰り返し",
|
|
443
|
+
componentLoopId: uuid(),
|
|
444
|
+
childStartOutPortId: uuid(),
|
|
445
|
+
childEnd,
|
|
446
|
+
newId: uuid,
|
|
447
|
+
singleExit: true,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it("外へ出る線が無ければ、線の出ていない唯一の出力を出口にする(結果を返す)", () => {
|
|
451
|
+
const { d, nodes, connections } = chain()
|
|
452
|
+
const childEnd = childEndNode()
|
|
453
|
+
|
|
454
|
+
const result = plan(nodes, connections, [d.id], childEnd)
|
|
455
|
+
|
|
456
|
+
if (!result.ok) throw new Error(result.reason)
|
|
457
|
+
const [nd] = result.childNodes
|
|
458
|
+
if (!nd) throw new Error("コピーがない")
|
|
459
|
+
expect(result.childConnections).toContainEqual({
|
|
460
|
+
from_port_id: outOf(nd).id,
|
|
461
|
+
to_port_id: inOf(childEnd).id,
|
|
462
|
+
})
|
|
463
|
+
expect(result.groupNode.ports.filter((p) => p.io === "out")).toHaveLength(1)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it("外へ出る線があればそれが出口(ふだんの抽出と同じ)", () => {
|
|
467
|
+
const { b, c, nodes, connections } = chain()
|
|
468
|
+
|
|
469
|
+
const result = plan(nodes, connections, [b.id, c.id])
|
|
470
|
+
|
|
471
|
+
if (!result.ok) throw new Error(result.reason)
|
|
472
|
+
expect(result.childEnd.ports.filter((p) => p.io === "in")).toHaveLength(1)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it("出口が2つ以上・出口にできる出力が決まらない選択は拒否する", () => {
|
|
476
|
+
const a = makeNode("program", "A")
|
|
477
|
+
const b = makeNode("program", "B")
|
|
478
|
+
const c = makeNode("program", "C")
|
|
479
|
+
const d = makeNode("program", "D")
|
|
480
|
+
// B と C を選ぶ:B → D、C → D の2つの出口
|
|
481
|
+
const twoExits = plan(
|
|
482
|
+
[a, b, c, d],
|
|
483
|
+
[connect(a, b), connect(a, c), connect(b, d), connect(c, d)],
|
|
484
|
+
[b.id, c.id],
|
|
485
|
+
)
|
|
486
|
+
expect(twoExits.ok).toBe(false)
|
|
487
|
+
if (twoExits.ok) return
|
|
488
|
+
expect(twoExits.reason).toContain("出口を1つ")
|
|
489
|
+
|
|
490
|
+
// 外へ出る線が無く、線の出ていない出力が2つ(B と C)
|
|
491
|
+
const ambiguous = plan([b, c], [], [b.id, c.id])
|
|
492
|
+
expect(ambiguous.ok).toBe(false)
|
|
493
|
+
})
|
|
494
|
+
})
|
|
@@ -118,6 +118,10 @@ export const buildExtractPlan = (params: {
|
|
|
118
118
|
childStartOutPortId: string
|
|
119
119
|
childEnd: GraphNode
|
|
120
120
|
newId: () => string
|
|
121
|
+
// 繰り返し(for):結果を配列で返すので出口はちょうど1つ。外へ出る線が無ければ、
|
|
122
|
+
// 選択の中で線の出ていない唯一の出力を出口にする
|
|
123
|
+
// (forで配列を要素ごとに処理する(繰り返しの枠).md)
|
|
124
|
+
singleExit?: boolean
|
|
121
125
|
}): { ok: false; reason: string } | ({ ok: true } & ExtractPlan) => {
|
|
122
126
|
const {
|
|
123
127
|
nodes,
|
|
@@ -128,11 +132,30 @@ export const buildExtractPlan = (params: {
|
|
|
128
132
|
childStartOutPortId,
|
|
129
133
|
childEnd,
|
|
130
134
|
newId,
|
|
135
|
+
singleExit = false,
|
|
131
136
|
} = params
|
|
132
137
|
const analysis = analyzeExtractSelection(nodes, connections, selectedIds)
|
|
133
138
|
if (!analysis.ok) return analysis
|
|
134
139
|
|
|
135
140
|
const selected = new Set(selectedIds)
|
|
141
|
+
let exitPortIds = analysis.exitPortIds
|
|
142
|
+
if (singleExit) {
|
|
143
|
+
if (exitPortIds.length === 0) {
|
|
144
|
+
const wiredOut = new Set(analysis.internal.map((c) => c.from_port_id))
|
|
145
|
+
const free = nodes
|
|
146
|
+
.filter((n) => selected.has(n.id))
|
|
147
|
+
.flatMap((n) =>
|
|
148
|
+
n.ports
|
|
149
|
+
.filter((p) => p.io === "out" && !wiredOut.has(p.id))
|
|
150
|
+
.map((p) => p.id),
|
|
151
|
+
)
|
|
152
|
+
exitPortIds = free.length === 1 ? free : []
|
|
153
|
+
}
|
|
154
|
+
if (exitPortIds.length !== 1) {
|
|
155
|
+
return { ok: false, reason: "繰り返しの出口を1つにしてください" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
137
160
|
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
138
161
|
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
@@ -185,7 +208,7 @@ export const buildExtractPlan = (params: {
|
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
}
|
|
188
|
-
const exits =
|
|
211
|
+
const exits = exitPortIds.map((exitPortId, i) => {
|
|
189
212
|
const exitName = portById.get(exitPortId)?.name ?? "出力"
|
|
190
213
|
const seeded = i === 0 ? seededEndIns[0] : undefined
|
|
191
214
|
const endIn: GraphPort = seeded
|
package/_schemas/graph.test.ts
CHANGED
|
@@ -997,22 +997,69 @@ describe("saveGraphBodySchema:開始/終了ノード(start/end kind)", ()
|
|
|
997
997
|
).toThrow(/in ポートはちょうど1つ/)
|
|
998
998
|
})
|
|
999
999
|
|
|
1000
|
-
it("
|
|
1001
|
-
const s = startNode()
|
|
1000
|
+
it("end からの接続を拒否する", () => {
|
|
1002
1001
|
const e = endNode()
|
|
1003
1002
|
const other = makeNode()
|
|
1003
|
+
expect(() =>
|
|
1004
|
+
saveGraphBodySchema.parse({
|
|
1005
|
+
nodes: [e, other],
|
|
1006
|
+
connections: [connect(e, other)],
|
|
1007
|
+
}),
|
|
1008
|
+
).toThrow(/終了ノード/)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
// start の入口へ戻すと次の周回になる
|
|
1012
|
+
// (docs/tasks/wip/startノードに入力をつないで周回する(runの周回).md)
|
|
1013
|
+
it("start の入口へ1本つなげる(周回の最後から戻す)", () => {
|
|
1014
|
+
const s = startNode()
|
|
1015
|
+
const other = { ...makeNode(simplePorts()), kind: "wait" }
|
|
1004
1016
|
expect(() =>
|
|
1005
1017
|
saveGraphBodySchema.parse({
|
|
1006
1018
|
nodes: [s, other],
|
|
1007
|
-
connections: [connect(other, s)],
|
|
1019
|
+
connections: [connect(s, other), connect(other, s)],
|
|
1008
1020
|
}),
|
|
1009
|
-
).toThrow(
|
|
1021
|
+
).not.toThrow()
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
it("start の入口につなげる線は1本だけ", () => {
|
|
1025
|
+
const s = startNode()
|
|
1026
|
+
const a = makeNode()
|
|
1027
|
+
const b = makeNode()
|
|
1010
1028
|
expect(() =>
|
|
1011
1029
|
saveGraphBodySchema.parse({
|
|
1012
|
-
nodes: [
|
|
1013
|
-
connections: [connect(
|
|
1030
|
+
nodes: [s, a, b],
|
|
1031
|
+
connections: [connect(a, s), connect(b, s)],
|
|
1014
1032
|
}),
|
|
1015
|
-
).toThrow(
|
|
1033
|
+
).toThrow(/開始ノードの入口につなげる線は1本だけです/)
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
it("start の入口の型(直後のノードが受ける型)に合わない線を拒否する", () => {
|
|
1037
|
+
const s = startNode()
|
|
1038
|
+
const objectIn = {
|
|
1039
|
+
type: "object",
|
|
1040
|
+
properties: { title: { type: "string" } },
|
|
1041
|
+
required: ["title"],
|
|
1042
|
+
}
|
|
1043
|
+
const next = {
|
|
1044
|
+
...makeNode([
|
|
1045
|
+
{ ...port("in", "in"), schema: objectIn },
|
|
1046
|
+
port("out", "main"),
|
|
1047
|
+
]),
|
|
1048
|
+
kind: "program",
|
|
1049
|
+
}
|
|
1050
|
+
const last = {
|
|
1051
|
+
...makeNode([
|
|
1052
|
+
port("in", "in"),
|
|
1053
|
+
{ ...port("out", "main"), schema: { type: "string" } },
|
|
1054
|
+
]),
|
|
1055
|
+
kind: "program",
|
|
1056
|
+
}
|
|
1057
|
+
expect(() =>
|
|
1058
|
+
saveGraphBodySchema.parse({
|
|
1059
|
+
nodes: [s, next, last],
|
|
1060
|
+
connections: [connect(s, next), connect(next, last), connect(last, s)],
|
|
1061
|
+
}),
|
|
1062
|
+
).toThrow(/入力スキーマに適合しません/)
|
|
1016
1063
|
})
|
|
1017
1064
|
})
|
|
1018
1065
|
|
package/_schemas/graph.ts
CHANGED
|
@@ -521,12 +521,16 @@ export const saveGraphBodySchema = z
|
|
|
521
521
|
const toNode = nodeByPortId.get(conn.to_port_id)
|
|
522
522
|
if (!from || !to || !fromNode || !toNode) return // 構造違反は検出済み
|
|
523
523
|
|
|
524
|
-
// start
|
|
525
|
-
|
|
524
|
+
// end からは引き出せない(終端)。start の入口へは戻せる(次の周回になる)が、
|
|
525
|
+
// 線は1本だけ(2本あると両方が着いたときに周回が2本並んで走る)
|
|
526
|
+
if (
|
|
527
|
+
toNode.kind === "start" &&
|
|
528
|
+
(inboundByPort.get(conn.to_port_id)?.length ?? 0) > 1
|
|
529
|
+
) {
|
|
526
530
|
ctx.addIssue({
|
|
527
531
|
code: "custom",
|
|
528
532
|
path: ["connections", i, "to_port_id"],
|
|
529
|
-
message: "
|
|
533
|
+
message: "開始ノードの入口につなげる線は1本だけです",
|
|
530
534
|
})
|
|
531
535
|
}
|
|
532
536
|
if (fromNode.kind === "end") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js"
|
|
3
3
|
import {
|
|
4
|
+
arrayOf,
|
|
4
5
|
dataFormatOf,
|
|
5
6
|
dataSchemaOf,
|
|
6
7
|
fileSchema,
|
|
@@ -1116,3 +1117,30 @@ describe("schemaTypeKey(型としての同一性)", () => {
|
|
|
1116
1117
|
).not.toBe(schemaTypeKey({ type: "array", items: [{ type: "number" }] }))
|
|
1117
1118
|
})
|
|
1118
1119
|
})
|
|
1120
|
+
|
|
1121
|
+
describe("arrayOf(for の配置ポート:1件分の型を配列に包む)", () => {
|
|
1122
|
+
it("宣言された型は items に入れた配列になる", () => {
|
|
1123
|
+
const error = { type: "object", properties: { id: { type: "string" } } }
|
|
1124
|
+
expect(arrayOf(error)).toEqual({ type: "array", items: error })
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it("未宣言の型は items を持たない配列になる(要素の型は判断できないまま)", () => {
|
|
1128
|
+
expect(arrayOf(UNDECLARED_SCHEMA)).toEqual({ type: "array" })
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
it("配列にした型同士は要素の型で適合を判定する", () => {
|
|
1132
|
+
const narrow = {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
properties: { id: { type: "string" }, title: { type: "string" } },
|
|
1135
|
+
required: ["id", "title"],
|
|
1136
|
+
}
|
|
1137
|
+
const wide = {
|
|
1138
|
+
type: "object",
|
|
1139
|
+
properties: { id: { type: "string" } },
|
|
1140
|
+
required: ["id"],
|
|
1141
|
+
}
|
|
1142
|
+
expect(schemaFit(arrayOf(narrow), arrayOf(wide))).toBe("ok")
|
|
1143
|
+
expect(schemaFit(arrayOf(wide), arrayOf(narrow))).toBe("mismatch")
|
|
1144
|
+
expect(schemaFit(wide, arrayOf(wide))).toBe("mismatch")
|
|
1145
|
+
})
|
|
1146
|
+
})
|
package/_schemas/port-spec.ts
CHANGED
|
@@ -15,6 +15,14 @@ export const UNDECLARED_SCHEMA: JsonSchema = {}
|
|
|
15
15
|
export const isUndeclaredSchema = (schema: JsonSchema): boolean =>
|
|
16
16
|
Object.keys(schema).length === 0 || schema.type === undefined
|
|
17
17
|
|
|
18
|
+
// 1件分の型を配列に包む(for の配置ポート:in = 中身の start.in の配列、out = 中身の
|
|
19
|
+
// end の in の配列。docs/tasks/done/forで配列を要素ごとに処理する(繰り返しの枠).md)。
|
|
20
|
+
// 未宣言は items を持たせない(要素の型は判断できないまま)
|
|
21
|
+
export const arrayOf = (schema: JsonSchema): JsonSchema =>
|
|
22
|
+
isUndeclaredSchema(schema)
|
|
23
|
+
? { type: "array" }
|
|
24
|
+
: { type: "array", items: schema }
|
|
25
|
+
|
|
18
26
|
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
19
27
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
20
28
|
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
|
@@ -55,11 +55,26 @@ export const analyzeExtractSelection = (nodes, connections, selectedIds) => {
|
|
|
55
55
|
// end の in が1つの loop は out main の1口として振る舞う)
|
|
56
56
|
const SINGLE_OUT_KEY = "main";
|
|
57
57
|
export const buildExtractPlan = (params) => {
|
|
58
|
-
const { nodes, connections, selectedIds, name, componentLoopId, childStartOutPortId, childEnd, newId, } = params;
|
|
58
|
+
const { nodes, connections, selectedIds, name, componentLoopId, childStartOutPortId, childEnd, newId, singleExit = false, } = params;
|
|
59
59
|
const analysis = analyzeExtractSelection(nodes, connections, selectedIds);
|
|
60
60
|
if (!analysis.ok)
|
|
61
61
|
return analysis;
|
|
62
62
|
const selected = new Set(selectedIds);
|
|
63
|
+
let exitPortIds = analysis.exitPortIds;
|
|
64
|
+
if (singleExit) {
|
|
65
|
+
if (exitPortIds.length === 0) {
|
|
66
|
+
const wiredOut = new Set(analysis.internal.map((c) => c.from_port_id));
|
|
67
|
+
const free = nodes
|
|
68
|
+
.filter((n) => selected.has(n.id))
|
|
69
|
+
.flatMap((n) => n.ports
|
|
70
|
+
.filter((p) => p.io === "out" && !wiredOut.has(p.id))
|
|
71
|
+
.map((p) => p.id));
|
|
72
|
+
exitPortIds = free.length === 1 ? free : [];
|
|
73
|
+
}
|
|
74
|
+
if (exitPortIds.length !== 1) {
|
|
75
|
+
return { ok: false, reason: "繰り返しの出口を1つにしてください" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
63
78
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
64
79
|
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
65
80
|
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
@@ -108,7 +123,7 @@ export const buildExtractPlan = (params) => {
|
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
};
|
|
111
|
-
const exits =
|
|
126
|
+
const exits = exitPortIds.map((exitPortId, i) => {
|
|
112
127
|
const exitName = portById.get(exitPortId)?.name ?? "出力";
|
|
113
128
|
const seeded = i === 0 ? seededEndIns[0] : undefined;
|
|
114
129
|
const endIn = seeded
|
package/dist/_schemas/graph.js
CHANGED
|
@@ -447,12 +447,14 @@ export const saveGraphBodySchema = z
|
|
|
447
447
|
const toNode = nodeByPortId.get(conn.to_port_id);
|
|
448
448
|
if (!from || !to || !fromNode || !toNode)
|
|
449
449
|
return; // 構造違反は検出済み
|
|
450
|
-
// start
|
|
451
|
-
|
|
450
|
+
// end からは引き出せない(終端)。start の入口へは戻せる(次の周回になる)が、
|
|
451
|
+
// 線は1本だけ(2本あると両方が着いたときに周回が2本並んで走る)
|
|
452
|
+
if (toNode.kind === "start" &&
|
|
453
|
+
(inboundByPort.get(conn.to_port_id)?.length ?? 0) > 1) {
|
|
452
454
|
ctx.addIssue({
|
|
453
455
|
code: "custom",
|
|
454
456
|
path: ["connections", i, "to_port_id"],
|
|
455
|
-
message: "
|
|
457
|
+
message: "開始ノードの入口につなげる線は1本だけです",
|
|
456
458
|
});
|
|
457
459
|
}
|
|
458
460
|
if (fromNode.kind === "end") {
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js";
|
|
3
3
|
export declare const UNDECLARED_SCHEMA: JsonSchema;
|
|
4
4
|
export declare const isUndeclaredSchema: (schema: JsonSchema) => boolean;
|
|
5
|
+
export declare const arrayOf: (schema: JsonSchema) => JsonSchema;
|
|
5
6
|
export declare const FILE_SCHEMA_MARKER = "x-mawaru-type";
|
|
6
7
|
export declare const FILE_SCHEMA_TYPE = "file";
|
|
7
8
|
export declare const fileSchema: () => JsonSchema;
|
|
@@ -9,6 +9,12 @@ import { z } from "zod";
|
|
|
9
9
|
// (docs/tasks/done/ポートスキーマのany撲滅.md)
|
|
10
10
|
export const UNDECLARED_SCHEMA = {};
|
|
11
11
|
export const isUndeclaredSchema = (schema) => Object.keys(schema).length === 0 || schema.type === undefined;
|
|
12
|
+
// 1件分の型を配列に包む(for の配置ポート:in = 中身の start.in の配列、out = 中身の
|
|
13
|
+
// end の in の配列。docs/tasks/done/forで配列を要素ごとに処理する(繰り返しの枠).md)。
|
|
14
|
+
// 未宣言は items を持たせない(要素の型は判断できないまま)
|
|
15
|
+
export const arrayOf = (schema) => isUndeclaredSchema(schema)
|
|
16
|
+
? { type: "array" }
|
|
17
|
+
: { type: "array", items: schema };
|
|
12
18
|
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
13
19
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
14
20
|
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
package/package.json
CHANGED
|
@@ -77,6 +77,12 @@ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等
|
|
|
77
77
|
1つの出口から複数の線を引くと枝が並行に走る(fan-out)。枝の結果を待ち合わせるときは
|
|
78
78
|
合流(`join`)を置く:線のつながった入口が全部揃うまで待ち、`{ [入口の key]: 値 }` にまとめて
|
|
79
79
|
`main` から流す。並行した枝を合流させずに同じノード(待機など)へ戻すと、枝の数だけ走り続ける。
|
|
80
|
+
配列の要素ごとに同じ手順や別ループを回すときは繰り返し(for)にする:`POST loops` に
|
|
81
|
+
`kind: "for"` と `owner_loop_id`(このループ)を渡して中身の loop を作り、その graph
|
|
82
|
+
(start → 1件分の処理 → end)を保存してから、このループに component ノード
|
|
83
|
+
(`component.component_loop_id`=作った for)を置く。配置ノードの in / out は中身の型の配列に
|
|
84
|
+
自動で焼き込まれ、全件そろうと入力と同じ順の配列で `main` から流れる(1件ずつは同時に走る)。
|
|
85
|
+
中身の出口(end につないだ in)は1つにする。
|
|
80
86
|
2. **repo 側の準備**:足りない program / ai は `nodes/program/<dir>/` / `nodes/ai/<dir>/` に作る
|
|
81
87
|
(config.json → `npx mawaru typegen` → main.ts 実装 → `npx mawaru validate`)。
|
|
82
88
|
**AI ノードの決定論的な処理は prompt ではなく `setup.sh` に書く**(下記)。
|