@mawaru/sdk 0.5.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.
Files changed (43) hide show
  1. package/_schemas/ai-manifest.test.ts +135 -0
  2. package/_schemas/ai-manifest.ts +76 -0
  3. package/_schemas/graph.test.ts +872 -0
  4. package/_schemas/graph.ts +439 -0
  5. package/_schemas/hook-manifest.test.ts +45 -0
  6. package/_schemas/hook-manifest.ts +25 -0
  7. package/_schemas/node.test.ts +76 -0
  8. package/_schemas/node.ts +54 -0
  9. package/_schemas/port-spec.test.ts +657 -0
  10. package/_schemas/port-spec.ts +405 -0
  11. package/_schemas/program-manifest.test.ts +126 -0
  12. package/_schemas/program-manifest.ts +43 -0
  13. package/dist/_schemas/ai-manifest.d.ts +25 -0
  14. package/dist/_schemas/ai-manifest.js +67 -0
  15. package/dist/_schemas/graph.d.ts +226 -0
  16. package/dist/_schemas/graph.js +372 -0
  17. package/dist/_schemas/hook-manifest.d.ts +18 -0
  18. package/dist/_schemas/hook-manifest.js +21 -0
  19. package/dist/_schemas/node.d.ts +47 -0
  20. package/dist/_schemas/node.js +42 -0
  21. package/dist/_schemas/port-spec.d.ts +62 -0
  22. package/dist/_schemas/port-spec.js +336 -0
  23. package/dist/_schemas/program-manifest.d.ts +10 -0
  24. package/dist/_schemas/program-manifest.js +41 -0
  25. package/dist/cli.d.ts +2 -0
  26. package/dist/cli.js +54 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +9 -0
  29. package/dist/init.d.ts +6 -0
  30. package/dist/init.js +81 -0
  31. package/dist/typegen.d.ts +14 -0
  32. package/dist/typegen.js +206 -0
  33. package/dist/validate.d.ts +6 -0
  34. package/dist/validate.js +130 -0
  35. package/docs/development.md +59 -0
  36. package/index.ts +9 -0
  37. package/package.json +47 -0
  38. package/skills/create-loop/SKILL.md +87 -0
  39. package/templates/CLAUDE.md +23 -0
  40. package/templates/README.md +12 -0
  41. package/templates/echo/config.json +23 -0
  42. package/templates/echo/main.ts +4 -0
  43. package/templates/mawaru-runner.yml +71 -0
@@ -0,0 +1,226 @@
1
+ import { z } from "zod";
2
+ export declare const refsBagSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
3
+ export type RefsBag = z.infer<typeof refsBagSchema>;
4
+ export declare const loopRefsSchema: z.ZodObject<{
5
+ type: z.ZodLiteral<"object">;
6
+ properties: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
7
+ required: z.ZodOptional<z.ZodArray<z.ZodString>>;
8
+ }, z.core.$strip>;
9
+ export type LoopRefs = z.infer<typeof loopRefsSchema>;
10
+ export declare const EMPTY_LOOP_REFS: LoopRefs;
11
+ export declare const refsDefaultsOf: (refs: LoopRefs) => RefsBag;
12
+ export declare const editableNodeKindSchema: z.ZodEnum<{
13
+ ai: "ai";
14
+ human: "human";
15
+ program: "program";
16
+ wait: "wait";
17
+ }>;
18
+ export type EditableNodeKind = z.infer<typeof editableNodeKindSchema>;
19
+ export declare const graphNodeKindSchema: z.ZodEnum<{
20
+ ai: "ai";
21
+ human: "human";
22
+ program: "program";
23
+ wait: "wait";
24
+ start: "start";
25
+ end: "end";
26
+ }>;
27
+ export type GraphNodeKind = z.infer<typeof graphNodeKindSchema>;
28
+ export declare const waitConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
29
+ wait_type: z.ZodLiteral<"duration">;
30
+ duration_seconds: z.ZodNumber;
31
+ }, z.core.$strip>, z.ZodObject<{
32
+ wait_type: z.ZodLiteral<"until">;
33
+ until_at: z.ZodISODateTime;
34
+ }, z.core.$strip>, z.ZodObject<{
35
+ wait_type: z.ZodLiteral<"cron">;
36
+ cron_expr: z.ZodString;
37
+ }, z.core.$strip>], "wait_type">;
38
+ export type WaitConfig = z.infer<typeof waitConfigSchema>;
39
+ export declare const programConfigSchema: z.ZodObject<{
40
+ program_dir: z.ZodString;
41
+ ref: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>;
43
+ export type ProgramConfig = z.infer<typeof programConfigSchema>;
44
+ export declare const aiConfigSchema: z.ZodObject<{
45
+ ai_dir: z.ZodString;
46
+ ref: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ export type AiConfig = z.infer<typeof aiConfigSchema>;
49
+ export declare const humanConfigSchema: z.ZodObject<{
50
+ assignment: z.ZodObject<{
51
+ strategy: z.ZodDefault<z.ZodEnum<{
52
+ fixed: "fixed";
53
+ round_robin: "round_robin";
54
+ }>>;
55
+ approval_mode: z.ZodDefault<z.ZodEnum<{
56
+ any: "any";
57
+ all: "all";
58
+ }>>;
59
+ units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUUID>>>;
60
+ }, z.core.$strip>;
61
+ }, z.core.$strip>;
62
+ export type HumanConfig = z.infer<typeof humanConfigSchema>;
63
+ export declare const graphNodeHookSchema: z.ZodObject<{
64
+ hook_dir: z.ZodString;
65
+ ref: z.ZodOptional<z.ZodString>;
66
+ on_input: z.ZodBoolean;
67
+ on_output: z.ZodBoolean;
68
+ on_signal: z.ZodDefault<z.ZodBoolean>;
69
+ }, z.core.$strip>;
70
+ export type GraphNodeHook = z.infer<typeof graphNodeHookSchema>;
71
+ export declare const graphNodeSchema: z.ZodObject<{
72
+ id: z.ZodUUID;
73
+ kind: z.ZodEnum<{
74
+ ai: "ai";
75
+ human: "human";
76
+ program: "program";
77
+ wait: "wait";
78
+ start: "start";
79
+ end: "end";
80
+ }>;
81
+ name: z.ZodString;
82
+ ports: z.ZodArray<z.ZodObject<{
83
+ id: z.ZodUUID;
84
+ io: z.ZodEnum<{
85
+ in: "in";
86
+ out: "out";
87
+ }>;
88
+ key: z.ZodString;
89
+ name: z.ZodString;
90
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
91
+ side: z.ZodOptional<z.ZodEnum<{
92
+ top: "top";
93
+ right: "right";
94
+ bottom: "bottom";
95
+ left: "left";
96
+ }>>;
97
+ offset: z.ZodOptional<z.ZodNumber>;
98
+ }, z.core.$strip>>;
99
+ hooks: z.ZodDefault<z.ZodArray<z.ZodObject<{
100
+ hook_dir: z.ZodString;
101
+ ref: z.ZodOptional<z.ZodString>;
102
+ on_input: z.ZodBoolean;
103
+ on_output: z.ZodBoolean;
104
+ on_signal: z.ZodDefault<z.ZodBoolean>;
105
+ }, z.core.$strip>>>;
106
+ position_x: z.ZodNumber;
107
+ position_y: z.ZodNumber;
108
+ wait: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
109
+ wait_type: z.ZodLiteral<"duration">;
110
+ duration_seconds: z.ZodNumber;
111
+ }, z.core.$strip>, z.ZodObject<{
112
+ wait_type: z.ZodLiteral<"until">;
113
+ until_at: z.ZodISODateTime;
114
+ }, z.core.$strip>, z.ZodObject<{
115
+ wait_type: z.ZodLiteral<"cron">;
116
+ cron_expr: z.ZodString;
117
+ }, z.core.$strip>], "wait_type">>;
118
+ program: z.ZodOptional<z.ZodObject<{
119
+ program_dir: z.ZodString;
120
+ ref: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strip>>;
122
+ ai: z.ZodOptional<z.ZodObject<{
123
+ ai_dir: z.ZodString;
124
+ ref: z.ZodOptional<z.ZodString>;
125
+ }, z.core.$strip>>;
126
+ human: z.ZodOptional<z.ZodObject<{
127
+ assignment: z.ZodObject<{
128
+ strategy: z.ZodDefault<z.ZodEnum<{
129
+ fixed: "fixed";
130
+ round_robin: "round_robin";
131
+ }>>;
132
+ approval_mode: z.ZodDefault<z.ZodEnum<{
133
+ any: "any";
134
+ all: "all";
135
+ }>>;
136
+ units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUUID>>>;
137
+ }, z.core.$strip>;
138
+ }, z.core.$strip>>;
139
+ }, z.core.$strip>;
140
+ export type GraphNode = z.infer<typeof graphNodeSchema>;
141
+ export declare const graphConnectionSchema: z.ZodObject<{
142
+ from_port_id: z.ZodUUID;
143
+ to_port_id: z.ZodUUID;
144
+ }, z.core.$strip>;
145
+ export type GraphConnection = z.infer<typeof graphConnectionSchema>;
146
+ export declare const saveGraphBodySchema: z.ZodObject<{
147
+ nodes: z.ZodArray<z.ZodObject<{
148
+ id: z.ZodUUID;
149
+ kind: z.ZodEnum<{
150
+ ai: "ai";
151
+ human: "human";
152
+ program: "program";
153
+ wait: "wait";
154
+ start: "start";
155
+ end: "end";
156
+ }>;
157
+ name: z.ZodString;
158
+ ports: z.ZodArray<z.ZodObject<{
159
+ id: z.ZodUUID;
160
+ io: z.ZodEnum<{
161
+ in: "in";
162
+ out: "out";
163
+ }>;
164
+ key: z.ZodString;
165
+ name: z.ZodString;
166
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
167
+ side: z.ZodOptional<z.ZodEnum<{
168
+ top: "top";
169
+ right: "right";
170
+ bottom: "bottom";
171
+ left: "left";
172
+ }>>;
173
+ offset: z.ZodOptional<z.ZodNumber>;
174
+ }, z.core.$strip>>;
175
+ hooks: z.ZodDefault<z.ZodArray<z.ZodObject<{
176
+ hook_dir: z.ZodString;
177
+ ref: z.ZodOptional<z.ZodString>;
178
+ on_input: z.ZodBoolean;
179
+ on_output: z.ZodBoolean;
180
+ on_signal: z.ZodDefault<z.ZodBoolean>;
181
+ }, z.core.$strip>>>;
182
+ position_x: z.ZodNumber;
183
+ position_y: z.ZodNumber;
184
+ wait: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
185
+ wait_type: z.ZodLiteral<"duration">;
186
+ duration_seconds: z.ZodNumber;
187
+ }, z.core.$strip>, z.ZodObject<{
188
+ wait_type: z.ZodLiteral<"until">;
189
+ until_at: z.ZodISODateTime;
190
+ }, z.core.$strip>, z.ZodObject<{
191
+ wait_type: z.ZodLiteral<"cron">;
192
+ cron_expr: z.ZodString;
193
+ }, z.core.$strip>], "wait_type">>;
194
+ program: z.ZodOptional<z.ZodObject<{
195
+ program_dir: z.ZodString;
196
+ ref: z.ZodOptional<z.ZodString>;
197
+ }, z.core.$strip>>;
198
+ ai: z.ZodOptional<z.ZodObject<{
199
+ ai_dir: z.ZodString;
200
+ ref: z.ZodOptional<z.ZodString>;
201
+ }, z.core.$strip>>;
202
+ human: z.ZodOptional<z.ZodObject<{
203
+ assignment: z.ZodObject<{
204
+ strategy: z.ZodDefault<z.ZodEnum<{
205
+ fixed: "fixed";
206
+ round_robin: "round_robin";
207
+ }>>;
208
+ approval_mode: z.ZodDefault<z.ZodEnum<{
209
+ any: "any";
210
+ all: "all";
211
+ }>>;
212
+ units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUUID>>>;
213
+ }, z.core.$strip>;
214
+ }, z.core.$strip>>;
215
+ }, z.core.$strip>>;
216
+ connections: z.ZodArray<z.ZodObject<{
217
+ from_port_id: z.ZodUUID;
218
+ to_port_id: z.ZodUUID;
219
+ }, z.core.$strip>>;
220
+ refs: z.ZodDefault<z.ZodObject<{
221
+ type: z.ZodLiteral<"object">;
222
+ properties: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
223
+ required: z.ZodOptional<z.ZodArray<z.ZodString>>;
224
+ }, z.core.$strip>>;
225
+ }, z.core.$strip>;
226
+ export type SaveGraphBody = z.infer<typeof saveGraphBodySchema>;
@@ -0,0 +1,372 @@
1
+ import { z } from "zod";
2
+ import { graphPortSchema, jsonSchemaSchema, nodeKindSchema, portKeySchema, } from "./node.js";
3
+ import { isAiEnvelopeSchema, isSchemaSubset, kindPortsViolation, resolvePortSchemas, } from "./port-spec.js";
4
+ // refs=ループ変数の袋(key→任意の JSON 値。上流出力の参照(refs).md)。
5
+ // run 開始時に定義から複製され、全 step に全量差し込まれる。
6
+ // key はポート key と同じ規則(^[a-z][a-z0-9_]*$)。実行系ボディ
7
+ // (run 起動の上書き・step end のパッチ)が使う契約
8
+ export const refsBagSchema = z.record(portKeySchema, z.unknown());
9
+ // ループ変数の定義=袋全体を表す 1 つの object JSON Schema
10
+ // (ループ変数(refs)のJSON Schema化.md)。properties のキーが変数名、
11
+ // default が静的なデフォルト値、required は「run 起動時に値が確定して
12
+ // いなければならないキー」(実行中に handler が書くキーは required にしない)
13
+ export const loopRefsSchema = z
14
+ .object({
15
+ type: z.literal("object"),
16
+ properties: z.record(portKeySchema, jsonSchemaSchema).default({}),
17
+ required: z.array(portKeySchema).optional(),
18
+ })
19
+ .superRefine((refs, ctx) => {
20
+ for (const key of refs.required ?? []) {
21
+ if (!(key in refs.properties)) {
22
+ ctx.addIssue({
23
+ code: "custom",
24
+ path: ["required"],
25
+ message: `required のキー ${key} が properties にありません`,
26
+ });
27
+ }
28
+ }
29
+ });
30
+ export const EMPTY_LOOP_REFS = { type: "object", properties: {} };
31
+ // 定義スキーマ→デフォルト値の袋(default を持つプロパティだけ集める)。
32
+ // run 起動時のマージの土台と、実行フォームの前埋めの両方が使う
33
+ export const refsDefaultsOf = (refs) => Object.fromEntries(Object.entries(refs.properties)
34
+ .filter(([, schema]) => "default" in schema)
35
+ .map(([key, schema]) => [key, schema.default]));
36
+ // パレットから作成できる kind(guardrail は後段の強化バックログ。
37
+ // start/end は loop 作成時に seed される構造ノードなのでパレットには出さない)
38
+ export const editableNodeKindSchema = nodeKindSchema.extract([
39
+ "ai",
40
+ "human",
41
+ "program",
42
+ "wait",
43
+ ]);
44
+ // グラフ一括保存で受け付ける kind(パレット作成の4種+構造ノード start/end)。
45
+ // start/end はパレットに出さないが、seed 済みなので全置換ボディには常に含まれる
46
+ export const graphNodeKindSchema = nodeKindSchema.extract([
47
+ "ai",
48
+ "human",
49
+ "program",
50
+ "wait",
51
+ "start",
52
+ "end",
53
+ ]);
54
+ // Wait ノードの kind 別設定(node_waits の wait_type に対応する判別ユニオン)。
55
+ // duration=経過秒、until=絶対時刻(ISO)、cron=cron 式(次回発火まで1回待つ)。
56
+ // cron 式の妥当性は保存時に backend の cron パーサで検証する(ここは非空のみ)
57
+ export const waitConfigSchema = z.discriminatedUnion("wait_type", [
58
+ z.object({
59
+ wait_type: z.literal("duration"),
60
+ duration_seconds: z.number().int().positive(),
61
+ }),
62
+ z.object({
63
+ wait_type: z.literal("until"),
64
+ until_at: z.iso.datetime({ offset: true }),
65
+ }),
66
+ z.object({
67
+ wait_type: z.literal("cron"),
68
+ cron_expr: z.string().min(1),
69
+ }),
70
+ ]);
71
+ // Program ノードの kind 別設定(node_programs に対応)。実行先の repo はノード設定でなく
72
+ // installation から `${account_login}/${GITHUB_REPO_NAME}` に固定(backend)。program_dir は nodes/program/ 直下のディレクトリ名
73
+ // (例: sendReply。日本語可。エントリは main.ts 固定。リポジトリ規約 v2 は
74
+ // docs/tasks/wip/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
75
+ // ref 省略時はデフォルトブランチ
76
+ export const programConfigSchema = z.object({
77
+ program_dir: z
78
+ .string()
79
+ .min(1)
80
+ .refine((v) => !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..", "program_dir は nodes/program/ 直下のディレクトリ名で指定してください"),
81
+ ref: z.string().min(1).optional(),
82
+ });
83
+ // AI ノードの kind 別設定(node_ais に対応)。定義の本体(prompt/model/skills/スキーマ/env)
84
+ // は実行 repo の nodes/ai/<dir>/config.json が正(aiManifestSchema)で、ノードは参照だけ持つ
85
+ // (programConfigSchema と同じ関係。実行repoの構造見直し)。ref 省略時はデフォルトブランチ
86
+ export const aiConfigSchema = z.object({
87
+ ai_dir: z
88
+ .string()
89
+ .min(1)
90
+ .refine((v) => !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..", "ai_dir は nodes/ai/ 直下のディレクトリ名で指定してください"),
91
+ ref: z.string().min(1).optional(),
92
+ });
93
+ // Human ノードの kind 別設定(node_humans + node_human_assignees に対応)。
94
+ // 担当 = ユニット(1人以上のメンバー)の順序付きリスト + 承認モード + 選出戦略。
95
+ // units[i] は user_id の配列(1人=個人・複数=グループ)。units 空 = tenant の誰でも。
96
+ // approval_mode はノード単位(any=誰か1人 / all=全員。差し戻しは1人で成立=veto)。
97
+ // メンバーが tenant の membership であることの検証は保存 API 側で行う
98
+ // (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
99
+ export const humanConfigSchema = z.object({
100
+ assignment: z
101
+ .object({
102
+ strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
103
+ approval_mode: z.enum(["any", "all"]).default("any"),
104
+ units: z.array(z.array(z.uuid()).min(1)).default([]),
105
+ })
106
+ .superRefine((assignment, ctx) => {
107
+ if (assignment.strategy === "fixed" && assignment.units.length > 1) {
108
+ ctx.addIssue({
109
+ code: "custom",
110
+ path: ["units"],
111
+ message: "fixed のユニットは1つまでです",
112
+ });
113
+ }
114
+ if (assignment.strategy === "round_robin" &&
115
+ assignment.units.length < 2) {
116
+ ctx.addIssue({
117
+ code: "custom",
118
+ path: ["units"],
119
+ message: "round_robin にはユニットが2つ以上必要です",
120
+ });
121
+ }
122
+ // ユニット内の重複は拒否(またぐ重複は「Aは毎回・相手が交代」の形があるので許す)
123
+ assignment.units.forEach((unit, i) => {
124
+ if (new Set(unit).size !== unit.length) {
125
+ ctx.addIssue({
126
+ code: "custom",
127
+ path: ["units", i],
128
+ message: "ユニット内で user_id が重複しています",
129
+ });
130
+ }
131
+ });
132
+ }),
133
+ });
134
+ // ノードにアタッチされた IOHook(node_hooks に対応。アタッチできるのは program ノードのみ)。
135
+ // on_input / on_output / on_signal はアタッチ時に repo の config.json(hookManifestSchema)から
136
+ // エディタが焼き込むスナップショット。ref 省略時はデフォルトブランチ
137
+ // (docs/tasks/wip/NodeIOHooks(ノードへのIOフックのアタッチ).md・
138
+ // NodeIOHooksのonSignal(外部シグナルでのフック発火).md)
139
+ export const graphNodeHookSchema = z.object({
140
+ hook_dir: z
141
+ .string()
142
+ .min(1)
143
+ .refine((v) => !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..", "hook_dir は nodes/hook/ 直下のディレクトリ名で指定してください"),
144
+ ref: z.string().min(1).optional(),
145
+ on_input: z.boolean(),
146
+ on_output: z.boolean(),
147
+ // 既存グラフ(on_signal 導入前の保存物)の互換のため省略時 false
148
+ on_signal: z.boolean().default(false),
149
+ });
150
+ // 新規ノードの id・ポートの id もクライアントが採番する(一時IDの解決を不要にする)
151
+ export const graphNodeSchema = z.object({
152
+ id: z.uuid(),
153
+ kind: graphNodeKindSchema,
154
+ name: z.string().min(1),
155
+ ports: z.array(graphPortSchema),
156
+ hooks: z.array(graphNodeHookSchema).default([]),
157
+ position_x: z.number(),
158
+ position_y: z.number(),
159
+ // kind 別設定(自 kind のときだけ意味を持つ。省略時は保存側がデフォルトを補う)
160
+ wait: waitConfigSchema.optional(),
161
+ program: programConfigSchema.optional(),
162
+ ai: aiConfigSchema.optional(),
163
+ human: humanConfigSchema.optional(),
164
+ });
165
+ export const graphConnectionSchema = z.object({
166
+ from_port_id: z.uuid(),
167
+ to_port_id: z.uuid(),
168
+ });
169
+ // PUT /tenants/:tenantId/loops/:loopId/graph のボディ(グラフ一括保存)。
170
+ // refs=ループ変数の定義スキーマ(loops.refs)。回路と同じ「エディタの保存」で全置換する。
171
+ // 開始/終了は専用 kind のノード(start/end)で表す(loops のカラムは廃止。
172
+ // 開始・終了ノードの定義.md)。exactly-one と削除/追加禁止は状態依存なので put.ts が担い、
173
+ // ここでは stateless な「各 kind 最大1つ」だけ見る
174
+ export const saveGraphBodySchema = z
175
+ .object({
176
+ nodes: z.array(graphNodeSchema),
177
+ connections: z.array(graphConnectionSchema),
178
+ refs: loopRefsSchema.default(EMPTY_LOOP_REFS),
179
+ })
180
+ .superRefine((graph, ctx) => {
181
+ const nodeIds = new Set(graph.nodes.map((n) => n.id));
182
+ if (nodeIds.size !== graph.nodes.length) {
183
+ ctx.addIssue({
184
+ code: "custom",
185
+ path: ["nodes"],
186
+ message: "node の id が重複しています",
187
+ });
188
+ }
189
+ // start / end は各 loop に最大1つ(seed により常にちょうど1つだが、契約としては最大1)
190
+ for (const kind of ["start", "end"]) {
191
+ if (graph.nodes.filter((n) => n.kind === kind).length > 1) {
192
+ ctx.addIssue({
193
+ code: "custom",
194
+ path: ["nodes"],
195
+ message: `${kind} ノードは1つまでです`,
196
+ });
197
+ }
198
+ }
199
+ // ポートの形:id は全体で一意、(io, key) は node 内で一意、
200
+ // in ポートちょうど1つ(join 導入まで)+ out ポート1つ以上
201
+ const portById = new Map();
202
+ const nodeByPortId = new Map();
203
+ graph.nodes.forEach((node, i) => {
204
+ const ioKeys = new Set();
205
+ let inCount = 0;
206
+ let outCount = 0;
207
+ node.ports.forEach((port, j) => {
208
+ if (portById.has(port.id)) {
209
+ ctx.addIssue({
210
+ code: "custom",
211
+ path: ["nodes", i, "ports", j, "id"],
212
+ message: "port の id が重複しています",
213
+ });
214
+ }
215
+ portById.set(port.id, port);
216
+ nodeByPortId.set(port.id, node);
217
+ const ioKey = `${port.io}:${port.key}`;
218
+ if (ioKeys.has(ioKey)) {
219
+ ctx.addIssue({
220
+ code: "custom",
221
+ path: ["nodes", i, "ports", j, "key"],
222
+ message: "同じ io のポートで key が重複しています",
223
+ });
224
+ }
225
+ ioKeys.add(ioKey);
226
+ if (port.io === "in")
227
+ inCount++;
228
+ else
229
+ outCount++;
230
+ });
231
+ // end だけ「in は1つ以上」(複数ルートを別々の in で集約する)。
232
+ // start・他 kind は従来どおり1つ固定
233
+ if (node.kind === "end" ? inCount < 1 : inCount !== 1) {
234
+ ctx.addIssue({
235
+ code: "custom",
236
+ path: ["nodes", i, "ports"],
237
+ message: node.kind === "end"
238
+ ? "in ポートが1つ以上必要です"
239
+ : "in ポートはちょうど1つ必要です",
240
+ });
241
+ }
242
+ if (outCount < 1) {
243
+ ctx.addIssue({
244
+ code: "custom",
245
+ path: ["nodes", i, "ports"],
246
+ message: "out ポートが最低1つ必要です",
247
+ });
248
+ }
249
+ // フックをアタッチできるのは program ノードのみ
250
+ if (node.kind !== "program" && node.hooks.length > 0) {
251
+ ctx.addIssue({
252
+ code: "custom",
253
+ path: ["nodes", i, "hooks"],
254
+ message: "フックをアタッチできるのは program ノードだけです",
255
+ });
256
+ }
257
+ // フックの重複アタッチ(unique(node_id, hook_dir) の事前検証)
258
+ const hookDirs = new Set();
259
+ node.hooks.forEach((hook, j) => {
260
+ if (hookDirs.has(hook.hook_dir)) {
261
+ ctx.addIssue({
262
+ code: "custom",
263
+ path: ["nodes", i, "hooks", j, "hook_dir"],
264
+ message: "同じノードに hook_dir が重複しています",
265
+ });
266
+ }
267
+ hookDirs.add(hook.hook_dir);
268
+ });
269
+ // kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md)
270
+ const violation = kindPortsViolation(node.kind, node.ports);
271
+ if (violation) {
272
+ ctx.addIssue({
273
+ code: "custom",
274
+ path: ["nodes", i, "ports"],
275
+ message: violation,
276
+ });
277
+ }
278
+ // AI の出力宣言は封筒型 { data: T, description: string }(T は任意の JSON Schema)。
279
+ // 複数出口でも全出口に強制する(どの出口も human に接続できる不変条件)
280
+ if (node.kind === "ai") {
281
+ node.ports.forEach((port, j) => {
282
+ if (port.io === "out" && !isAiEnvelopeSchema(port.schema)) {
283
+ ctx.addIssue({
284
+ code: "custom",
285
+ path: ["nodes", i, "ports", j],
286
+ message: "AI の出力は封筒型 { data, description } で宣言してください",
287
+ });
288
+ }
289
+ });
290
+ }
291
+ });
292
+ // 接続:両端がグラフ内の port を指し、from=out / to=in。
293
+ // 同じ out ポートから複数の in への fan-out は許す(並行分岐)。
294
+ // 禁止するのは完全重複(同じ from→to の2本)だけ
295
+ const usedPairs = new Set();
296
+ graph.connections.forEach((conn, i) => {
297
+ const from = portById.get(conn.from_port_id);
298
+ if (!from || from.io !== "out") {
299
+ ctx.addIssue({
300
+ code: "custom",
301
+ path: ["connections", i, "from_port_id"],
302
+ message: "接続元がグラフ内の out ポートではありません",
303
+ });
304
+ }
305
+ const to = portById.get(conn.to_port_id);
306
+ if (!to || to.io !== "in") {
307
+ ctx.addIssue({
308
+ code: "custom",
309
+ path: ["connections", i, "to_port_id"],
310
+ message: "接続先がグラフ内の in ポートではありません",
311
+ });
312
+ }
313
+ const pair = `${conn.from_port_id}→${conn.to_port_id}`;
314
+ if (usedPairs.has(pair)) {
315
+ ctx.addIssue({
316
+ code: "custom",
317
+ path: ["connections", i, "to_port_id"],
318
+ message: "同じ接続が重複しています",
319
+ });
320
+ }
321
+ usedPairs.add(pair);
322
+ });
323
+ // 型の接続バリデーション(ポートのkind別仕様.md §接続バリデーション)。
324
+ // 導出ポートは接続から型を解決した上で判定する。ai の in・wait の in は常に許可
325
+ const resolved = resolvePortSchemas(graph.nodes, graph.connections);
326
+ graph.connections.forEach((conn, i) => {
327
+ const from = portById.get(conn.from_port_id);
328
+ const to = portById.get(conn.to_port_id);
329
+ const fromNode = nodeByPortId.get(conn.from_port_id);
330
+ const toNode = nodeByPortId.get(conn.to_port_id);
331
+ if (!from || !to || !fromNode || !toNode)
332
+ return; // 構造違反は検出済み
333
+ // start には引き込めない(run payload だけが入口)、end からは引き出せない(終端)
334
+ if (toNode.kind === "start") {
335
+ ctx.addIssue({
336
+ code: "custom",
337
+ path: ["connections", i, "to_port_id"],
338
+ message: "開始ノードには接続できません",
339
+ });
340
+ }
341
+ if (fromNode.kind === "end") {
342
+ ctx.addIssue({
343
+ code: "custom",
344
+ path: ["connections", i, "from_port_id"],
345
+ message: "終了ノードからは接続できません",
346
+ });
347
+ }
348
+ // human の reject の接続先は ai ノードのみ(kind 制約)
349
+ if (fromNode.kind === "human" &&
350
+ from.key === "reject" &&
351
+ toNode.kind !== "ai") {
352
+ ctx.addIssue({
353
+ code: "custom",
354
+ path: ["connections", i, "to_port_id"],
355
+ message: "差し戻し(reject)の接続先は AI ノードのみです",
356
+ });
357
+ }
358
+ const fromSchema = resolved.get(conn.from_port_id) ?? from.schema;
359
+ // human の in はどの型でも接続できる(上流が AI なら封筒 { data, description }、
360
+ // それ以外は input 全体を data として表示する。型制約は課さない)
361
+ // program の in への接続は 伝播型 ⊆ 宣言スキーマ。in は接続鏡映(sticky)なので
362
+ // 具体型の接続では自明に通り、接続元 any × 宣言維持のときだけ従来の意味を持つ
363
+ const toSchema = resolved.get(conn.to_port_id) ?? to.schema;
364
+ if (toNode.kind === "program" && !isSchemaSubset(fromSchema, toSchema)) {
365
+ ctx.addIssue({
366
+ code: "custom",
367
+ path: ["connections", i, "to_port_id"],
368
+ message: "接続元の型が program の入力スキーマに適合しません(伝播型 ⊆ 宣言スキーマ)",
369
+ });
370
+ }
371
+ });
372
+ });
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ export declare const hookEventSchema: z.ZodEnum<{
3
+ output: "output";
4
+ input: "input";
5
+ signal: "signal";
6
+ }>;
7
+ export type HookEvent = z.infer<typeof hookEventSchema>;
8
+ export declare const hookManifestSchema: z.ZodObject<{
9
+ name: z.ZodOptional<z.ZodString>;
10
+ description: z.ZodOptional<z.ZodString>;
11
+ on: z.ZodArray<z.ZodEnum<{
12
+ output: "output";
13
+ input: "input";
14
+ signal: "signal";
15
+ }>>;
16
+ env: z.ZodDefault<z.ZodArray<z.ZodString>>;
17
+ }, z.core.$strip>;
18
+ export type HookManifest = z.infer<typeof hookManifestSchema>;
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ // nodes/hook/<dir>/config.json(NodeIOHooks)。フック自身が発火イベント(on)と
3
+ // 必要 env を宣言し、エディタがアタッチ時に読んで node_hooks へ焼き込む
4
+ // (programManifestSchema と同型の「repo が正・編集時に取り込む」方式。
5
+ // docs/tasks/wip/NodeIOHooks(ノードへのIOフックのアタッチ).md)。
6
+ // フックは任意のノードに付くため inputs / outputs のスキーマ宣言は持たない(unknown を受ける)。
7
+ // signal は外部シグナル(signals API)での発火(NodeIOHooksのonSignal(外部シグナルでのフック発火).md)
8
+ export const hookEventSchema = z.enum(["input", "output", "signal"]);
9
+ export const hookManifestSchema = z.object({
10
+ // 表示名(省略時はディレクトリ名)
11
+ name: z.string().min(1).optional(),
12
+ description: z.string().optional(),
13
+ // 発火イベントの宣言。エンジンはこれを見て dispatch を絞る
14
+ // (onInput の無いフックのために Actions run を無駄撃ちしない)
15
+ on: z
16
+ .array(hookEventSchema)
17
+ .min(1)
18
+ .refine((on) => new Set(on).size === on.length, "on が重複しています"),
19
+ // 実行に必要な secret 名の宣言(runner が宣言分だけを子プロセスへ渡す。program と同じ)
20
+ env: z.array(z.string().min(1)).default([]),
21
+ });