@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,47 @@
1
+ import { z } from "zod";
2
+ export declare const nodeKindSchema: z.ZodEnum<{
3
+ ai: "ai";
4
+ human: "human";
5
+ program: "program";
6
+ guardrail: "guardrail";
7
+ wait: "wait";
8
+ start: "start";
9
+ end: "end";
10
+ }>;
11
+ export type NodeKind = z.infer<typeof nodeKindSchema>;
12
+ export declare const PORT_KEY_PATTERN: RegExp;
13
+ export declare const portKeySchema: z.ZodString;
14
+ export declare const DEFAULT_OUTPUT_PORT = "main";
15
+ export declare const DEFAULT_INPUT_PORT = "in";
16
+ export declare const jsonSchemaSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
17
+ export type JsonSchema = z.infer<typeof jsonSchemaSchema>;
18
+ export declare const portIoSchema: z.ZodEnum<{
19
+ in: "in";
20
+ out: "out";
21
+ }>;
22
+ export type PortIo = z.infer<typeof portIoSchema>;
23
+ export declare const portSideSchema: z.ZodEnum<{
24
+ top: "top";
25
+ right: "right";
26
+ bottom: "bottom";
27
+ left: "left";
28
+ }>;
29
+ export type PortSide = z.infer<typeof portSideSchema>;
30
+ export declare const graphPortSchema: z.ZodObject<{
31
+ id: z.ZodUUID;
32
+ io: z.ZodEnum<{
33
+ in: "in";
34
+ out: "out";
35
+ }>;
36
+ key: z.ZodString;
37
+ name: z.ZodString;
38
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
39
+ side: z.ZodOptional<z.ZodEnum<{
40
+ top: "top";
41
+ right: "right";
42
+ bottom: "bottom";
43
+ left: "left";
44
+ }>>;
45
+ offset: z.ZodOptional<z.ZodNumber>;
46
+ }, z.core.$strip>;
47
+ export type GraphPort = z.infer<typeof graphPortSchema>;
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ export const nodeKindSchema = z.enum([
3
+ "ai",
4
+ "human",
5
+ "program",
6
+ "guardrail",
7
+ "wait",
8
+ // 開始・終了ノード(恒等パススルー。run は必ず start から始まり end で終わる。
9
+ // docs/tasks/wip/開始・終了ノードの定義.md)
10
+ "start",
11
+ "end",
12
+ ]);
13
+ // ポート key(input/output 用の参照キー)の規則(db.md §2)。
14
+ // name(ユーザーが見る表示名)は自由テキスト
15
+ export const PORT_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
16
+ export const portKeySchema = z.string().regex(PORT_KEY_PATTERN);
17
+ // 単一出口の既定ポート key/入力ポート key(db.md §2)
18
+ export const DEFAULT_OUTPUT_PORT = "main";
19
+ export const DEFAULT_INPUT_PORT = "in";
20
+ // JSON Schema そのもの(中身の妥当性はここでは検証しない)
21
+ export const jsonSchemaSchema = z.record(z.string(), z.unknown());
22
+ export const portIoSchema = z.enum(["in", "out"]);
23
+ // ポートの表示位置=ノード境界上の「辺+辺上の比率」(top/bottom は左→右、
24
+ // left/right は上→下)。エディタの見た目のみで実行・接続の意味には関与しない。
25
+ // 未指定は自動配置(先頭=右辺中央・以降=下辺等間隔)
26
+ export const portSideSchema = z.enum(["top", "right", "bottom", "left"]);
27
+ // グラフ保存契約のポート(ports テーブルの1行。id はクライアント採番。
28
+ // docs/tasks/wip/ポートのテーブル化.md)
29
+ export const graphPortSchema = z
30
+ .object({
31
+ id: z.uuid(),
32
+ io: portIoSchema,
33
+ key: portKeySchema,
34
+ name: z.string().min(1),
35
+ schema: jsonSchemaSchema,
36
+ side: portSideSchema.optional(),
37
+ offset: z.number().min(0).max(1).optional(),
38
+ })
39
+ .refine((p) => (p.side === undefined) === (p.offset === undefined), {
40
+ message: "side と offset は両方指定するか両方省略します",
41
+ path: ["side"],
42
+ });
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import type { GraphPort, JsonSchema } from "./node.js";
3
+ export declare const ANY_SCHEMA: JsonSchema;
4
+ export declare const isAnySchema: (schema: JsonSchema) => boolean;
5
+ export declare const FILE_SCHEMA_MARKER = "x-mawaru-type";
6
+ export declare const FILE_SCHEMA_TYPE = "file";
7
+ export declare const fileSchema: () => JsonSchema;
8
+ export declare const isFileSchema: (schema: JsonSchema) => boolean;
9
+ export declare const LIST_COLUMN_TYPES: readonly ["string", "number", "boolean", "date"];
10
+ export declare const listColumnTypeSchema: z.ZodEnum<{
11
+ string: "string";
12
+ number: "number";
13
+ boolean: "boolean";
14
+ date: "date";
15
+ }>;
16
+ export type ListColumnType = z.infer<typeof listColumnTypeSchema>;
17
+ export declare const listColumnSchema: z.ZodObject<{
18
+ key: z.ZodString;
19
+ name: z.ZodString;
20
+ type: z.ZodEnum<{
21
+ string: "string";
22
+ number: "number";
23
+ boolean: "boolean";
24
+ date: "date";
25
+ }>;
26
+ }, z.core.$strip>;
27
+ export type ListColumn = z.infer<typeof listColumnSchema>;
28
+ export declare const dataFormatSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
29
+ format: z.ZodLiteral<"text">;
30
+ }, z.core.$strip>, z.ZodObject<{
31
+ format: z.ZodLiteral<"list">;
32
+ columns: z.ZodArray<z.ZodObject<{
33
+ key: z.ZodString;
34
+ name: z.ZodString;
35
+ type: z.ZodEnum<{
36
+ string: "string";
37
+ number: "number";
38
+ boolean: "boolean";
39
+ date: "date";
40
+ }>;
41
+ }, z.core.$strip>>;
42
+ }, z.core.$strip>], "format">;
43
+ export type DataFormat = z.infer<typeof dataFormatSchema>;
44
+ export declare const dataSchemaOf: (format: DataFormat) => JsonSchema;
45
+ export declare const dataFormatOf: (schema: JsonSchema) => DataFormat | null;
46
+ export declare const envelopeSchemaOf: (dataSchema: JsonSchema) => JsonSchema;
47
+ export declare const envelopeDataSchemaOf: (schema: JsonSchema) => JsonSchema | null;
48
+ export declare const isAiEnvelopeSchema: (schema: JsonSchema) => boolean;
49
+ export declare const kindPortsViolation: (kind: string, ports: Pick<GraphPort, "io" | "key">[]) => string | null;
50
+ type PropagationNode = {
51
+ id: string;
52
+ kind: string;
53
+ ports: GraphPort[];
54
+ };
55
+ type PortLink = {
56
+ from_port_id: string;
57
+ to_port_id: string;
58
+ };
59
+ export declare const resolvePortSchemas: <N extends PropagationNode>(nodes: N[], connections: PortLink[]) => Map<string, JsonSchema>;
60
+ export declare const propagateDerivedPorts: <N extends PropagationNode>(nodes: N[], connections: PortLink[]) => N[];
61
+ export declare const isSchemaSubset: (sub: JsonSchema, sup: JsonSchema) => boolean;
62
+ export {};
@@ -0,0 +1,336 @@
1
+ import { z } from "zod";
2
+ // ポートの kind 別型システム(docs/tasks/wip/ポートのkind別仕様.md)。
3
+ // 型の決まり方は3分類:宣言(program / AI の in・out)/固定(human reject の
4
+ // content 型)/導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)
5
+ // content 型・未接続の導出ポートは any({})で表す。content の意味(エンジンが
6
+ // content に変換して注入する口)はスキーマではなく kind が決める
7
+ export const ANY_SCHEMA = {};
8
+ export const isAnySchema = (schema) => Object.keys(schema).length === 0 || schema.type === undefined;
9
+ // ---- file 型(AI ノードのファイル入力。docs/tasks/wip/AIノードのファイル入力(file型と添付).md) ----
10
+ // schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
11
+ // ではなく storage path の参照(url は startAiStep の dispatch 時に発行して client_payload に
12
+ // のみ載せる)。ajv は strict:false で未知キーワードを無視するので検証は素の JSON Schema のまま。
13
+ export const FILE_SCHEMA_MARKER = "x-mawaru-type";
14
+ export const FILE_SCHEMA_TYPE = "file";
15
+ export const fileSchema = () => ({
16
+ type: "object",
17
+ [FILE_SCHEMA_MARKER]: FILE_SCHEMA_TYPE,
18
+ properties: {
19
+ path: { type: "string" },
20
+ name: { type: "string" },
21
+ mime_type: { type: "string" },
22
+ size: { type: "number" },
23
+ },
24
+ required: ["path", "name"],
25
+ });
26
+ export const isFileSchema = (schema) => schema[FILE_SCHEMA_MARKER] === FILE_SCHEMA_TYPE;
27
+ // ---- データフォーマットカタログ ----
28
+ // human 承認 UI の表示・編集フォームの出し分けに使う(テキスト=string(markdown)/
29
+ // リスト=Array<Row>(列定義つき))。バリデーションには使わない(data は任意の型)
30
+ export const LIST_COLUMN_TYPES = [
31
+ "string",
32
+ "number",
33
+ "boolean",
34
+ "date",
35
+ ];
36
+ export const listColumnTypeSchema = z.enum(LIST_COLUMN_TYPES);
37
+ export const listColumnSchema = z.object({
38
+ key: z.string().min(1),
39
+ name: z.string().min(1),
40
+ type: listColumnTypeSchema,
41
+ });
42
+ export const dataFormatSchema = z.discriminatedUnion("format", [
43
+ z.object({ format: z.literal("text") }),
44
+ z.object({
45
+ format: z.literal("list"),
46
+ columns: z.array(listColumnSchema).min(1),
47
+ }),
48
+ ]);
49
+ const columnTypeToJsonSchema = (type) => type === "date" ? { type: "string", format: "date" } : { type };
50
+ const columnTypeFromJsonSchema = (schema) => {
51
+ if (schema.type === "string") {
52
+ return schema.format === "date" ? "date" : "string";
53
+ }
54
+ if (schema.type === "number" || schema.type === "boolean")
55
+ return schema.type;
56
+ return null;
57
+ };
58
+ // フォーマット → S の JSON Schema。列の表示名は title に載せる
59
+ export const dataSchemaOf = (format) => {
60
+ if (format.format === "text")
61
+ return { type: "string" };
62
+ return {
63
+ type: "array",
64
+ items: {
65
+ type: "object",
66
+ properties: Object.fromEntries(format.columns.map((col) => [
67
+ col.key,
68
+ { ...columnTypeToJsonSchema(col.type), title: col.name },
69
+ ])),
70
+ required: format.columns.map((col) => col.key),
71
+ },
72
+ };
73
+ };
74
+ // S の JSON Schema → フォーマット(構造マッチの逆引き。カタログ外は null)
75
+ export const dataFormatOf = (schema) => {
76
+ if (schema.type === "string" && schema.format === undefined) {
77
+ return { format: "text" };
78
+ }
79
+ if (schema.type !== "array")
80
+ return null;
81
+ const items = schema.items;
82
+ if (items?.type !== "object")
83
+ return null;
84
+ const properties = (items.properties ?? {});
85
+ const columns = [];
86
+ for (const [key, prop] of Object.entries(properties)) {
87
+ const type = columnTypeFromJsonSchema(prop);
88
+ if (!type)
89
+ return null;
90
+ columns.push({ key, name: prop.title ?? key, type });
91
+ }
92
+ if (columns.length === 0)
93
+ return null;
94
+ return { format: "list", columns };
95
+ };
96
+ // 封筒型 { data: T, description: string }。T は任意の JSON Schema、description は
97
+ // AI によるデータの説明(AI が必ず生成する)。AI の全出口に強制する
98
+ export const envelopeSchemaOf = (dataSchema) => ({
99
+ type: "object",
100
+ properties: {
101
+ data: dataSchema,
102
+ description: { type: "string" },
103
+ },
104
+ required: ["data", "description"],
105
+ });
106
+ // 封筒型なら data のサブスキーマを返す(data キー必須の object。それ以外は null)
107
+ export const envelopeDataSchemaOf = (schema) => {
108
+ if (schema.type !== "object")
109
+ return null;
110
+ const properties = (schema.properties ?? {});
111
+ const required = (schema.required ?? []);
112
+ const data = properties.data;
113
+ if (!data || !required.includes("data"))
114
+ return null;
115
+ return data;
116
+ };
117
+ // AI の出口として妥当な封筒型か:data と description: string の両方が必須
118
+ export const isAiEnvelopeSchema = (schema) => {
119
+ if (envelopeDataSchemaOf(schema) === null)
120
+ return false;
121
+ const properties = (schema.properties ?? {});
122
+ const required = (schema.required ?? []);
123
+ return (properties.description?.type === "string" &&
124
+ required.includes("description"));
125
+ };
126
+ // ---- kind 別ポート構成 ----
127
+ // human / wait は構成(数・key・io)が固定。program / ai は in 1つ+out 1つ以上
128
+ // (key 自由。複数 out=分岐。AIノードの複数出口(分岐).md)
129
+ const FIXED_PORT_KEYS = {
130
+ human: { in: ["in"], out: ["approve", "reject"] },
131
+ wait: { in: ["in"], out: ["main"] },
132
+ // start は in 1つ(run 入力契約)+ out main 1つの恒等パススルー
133
+ start: { in: ["in"], out: ["main"] },
134
+ };
135
+ const FREE_OUT_KINDS = new Set(["program", "ai"]);
136
+ // kind 別のポート構成違反を返す(適合なら null)。in=1/out>=1 の構造ルールは
137
+ // graph.ts 側の既存チェックが担い、ここは key の固定だけ見る
138
+ export const kindPortsViolation = (kind, ports) => {
139
+ // end は out が main 1つ固定・in は1つ以上(複数ルートを別々の in で集約する。
140
+ // 開始・終了ノードの定義.md)。in の key は自由(自動採番)
141
+ if (kind === "end") {
142
+ const outKeys = ports.filter((p) => p.io === "out").map((p) => p.key);
143
+ if (outKeys.length !== 1 || outKeys[0] !== "main") {
144
+ return `end の out ポートは "main" ちょうど1つです`;
145
+ }
146
+ if (!ports.some((p) => p.io === "in")) {
147
+ return `end には in ポートが1つ以上必要です`;
148
+ }
149
+ return null;
150
+ }
151
+ const fixed = FIXED_PORT_KEYS[kind];
152
+ if (!fixed) {
153
+ // program / ai(自由 out):in の key だけ "in" 固定
154
+ if (FREE_OUT_KINDS.has(kind) &&
155
+ ports.some((p) => p.io === "in" && p.key !== "in"))
156
+ return `${kind} の in ポートの key は "in" です`;
157
+ return null;
158
+ }
159
+ const keysOf = (io) => ports
160
+ .filter((p) => p.io === io)
161
+ .map((p) => p.key)
162
+ .sort();
163
+ const expect = (io) => [...fixed[io]].sort();
164
+ if (keysOf("in").join(",") !== expect("in").join(",") ||
165
+ keysOf("out").join(",") !== expect("out").join(",")) {
166
+ return `${kind} のポート構成は in(${fixed.in.join(",")}) / out(${fixed.out.join(",")}) 固定です`;
167
+ }
168
+ return null;
169
+ };
170
+ // end も導出ノード(各 in は上流から、out main は any)。個別処理は
171
+ // resolvePortSchemas 内で行うが、初期スキーマを any にするためここに含める。
172
+ // start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
173
+ // (run 入力契約=接続先の入力宣言。開始ノードの入力契約を接続先から導出.md)
174
+ const DERIVED_KINDS = new Set(["wait", "human", "end", "start"]);
175
+ // in を接続鏡映(sticky)するノード:接続元 out が具体型のときだけ上書き
176
+ const MIRROR_KINDS = new Set(["program", "ai"]);
177
+ const isDerivedPort = (kind, port) => DERIVED_KINDS.has(kind) &&
178
+ (port.io === "in" || port.key === "main" || port.key === "approve");
179
+ // AI の in は nodes/ai/<dir>/config.json の inputs.in 由来の宣言型が初期値で、
180
+ // 接続時は program と同じ鏡映で接続元と同値に同期される(const 扱い廃止。
181
+ // 接続バリデーションの「ai の in は常に許可」は維持)
182
+ const isConstPort = (kind, port) => kind === "human" && port.io === "out" && port.key === "reject";
183
+ // port_id → 解決済みスキーマ。導出は接続を遡って固定点まで反復し、
184
+ // 未接続・複数流入・純パラメトリック閉路は any に落とす(MVP の割り切り)
185
+ export const resolvePortSchemas = (nodes, connections) => {
186
+ const resolved = new Map();
187
+ const kindByPortId = new Map();
188
+ for (const node of nodes) {
189
+ for (const port of node.ports) {
190
+ kindByPortId.set(port.id, node.kind);
191
+ resolved.set(port.id, isDerivedPort(node.kind, port) || isConstPort(node.kind, port)
192
+ ? ANY_SCHEMA
193
+ : port.schema);
194
+ }
195
+ }
196
+ // in ポートへの流入(差し戻しの fan-in があり得る。1本のときだけ型を採用)
197
+ const inbound = new Map();
198
+ // out ポートからの流出(start の逆向き転写が接続先の in を引くのに使う)
199
+ const outbound = new Map();
200
+ for (const conn of connections) {
201
+ const list = inbound.get(conn.to_port_id) ?? [];
202
+ list.push(conn.from_port_id);
203
+ inbound.set(conn.to_port_id, list);
204
+ const outs = outbound.get(conn.from_port_id) ?? [];
205
+ outs.push(conn.to_port_id);
206
+ outbound.set(conn.from_port_id, outs);
207
+ }
208
+ for (let i = 0; i < nodes.length + 1; i++) {
209
+ let changed = false;
210
+ for (const node of nodes) {
211
+ const assign = (portId, schema) => {
212
+ if (JSON.stringify(resolved.get(portId)) !== JSON.stringify(schema)) {
213
+ resolved.set(portId, schema);
214
+ changed = true;
215
+ }
216
+ };
217
+ // start:out main は接続先 in の解決値を転写(逆向き導出)。fan-out は
218
+ // 非 any が1種類(JSON 等値)のときだけ採用し、未接続・型割れは any。
219
+ // in は out main と同型(恒等の向きを out→in に逆転。run 入力契約は
220
+ // 接続先の入力宣言から自動で決まる)
221
+ if (node.kind === "start") {
222
+ const inPort = node.ports.find((p) => p.io === "in");
223
+ const outMain = node.ports.find((p) => p.io === "out" && p.key === "main");
224
+ if (inPort && outMain) {
225
+ const targets = (outbound.get(outMain.id) ?? [])
226
+ .map((id) => resolved.get(id) ?? ANY_SCHEMA)
227
+ .filter((s) => !isAnySchema(s))
228
+ .map((s) => JSON.stringify(s));
229
+ const uniq = [...new Set(targets)];
230
+ const schema = uniq.length === 1 && uniq[0] !== undefined
231
+ ? JSON.parse(uniq[0])
232
+ : ANY_SCHEMA;
233
+ assign(outMain.id, schema);
234
+ assign(inPort.id, schema);
235
+ }
236
+ continue;
237
+ }
238
+ // end:各 in を自分の単一上流から導出(複数ルートを別 in で集約)。
239
+ // out main は any 固定(ルートごとに型が違いうる。Loop-in-Loop で詰める)
240
+ if (node.kind === "end") {
241
+ for (const port of node.ports) {
242
+ if (port.io !== "in")
243
+ continue;
244
+ const srcs = inbound.get(port.id) ?? [];
245
+ assign(port.id, srcs.length === 1 && srcs[0] !== undefined
246
+ ? (resolved.get(srcs[0]) ?? ANY_SCHEMA)
247
+ : ANY_SCHEMA);
248
+ }
249
+ continue;
250
+ }
251
+ const isMirror = MIRROR_KINDS.has(node.kind);
252
+ if (!DERIVED_KINDS.has(node.kind) && !isMirror)
253
+ continue;
254
+ const inPort = node.ports.find((p) => p.io === "in");
255
+ if (!inPort)
256
+ continue;
257
+ const sources = inbound.get(inPort.id) ?? [];
258
+ const inSchema = sources.length === 1 && sources[0] !== undefined
259
+ ? (resolved.get(sources[0]) ?? ANY_SCHEMA)
260
+ : ANY_SCHEMA;
261
+ // 鏡映(program)は具体型が流れてきたときだけ上書き(宣言を any で潰さない)
262
+ if (isMirror) {
263
+ if (!isAnySchema(inSchema))
264
+ assign(inPort.id, inSchema);
265
+ continue;
266
+ }
267
+ assign(inPort.id, inSchema);
268
+ // human の approve:接続元が AI ノードなら封筒 { data, description } を剥がして
269
+ // data 型を流す(承認は data だけを下流へ渡す)。それ以外の上流は従来どおり素通し
270
+ const fromAiNode = sources.length === 1 &&
271
+ sources[0] !== undefined &&
272
+ kindByPortId.get(sources[0]) === "ai";
273
+ for (const port of node.ports) {
274
+ if (port.io === "out" && isDerivedPort(node.kind, port)) {
275
+ assign(port.id, node.kind === "human" && port.key === "approve" && fromAiNode
276
+ ? (envelopeDataSchemaOf(inSchema) ?? ANY_SCHEMA)
277
+ : inSchema);
278
+ }
279
+ }
280
+ }
281
+ if (!changed)
282
+ break;
283
+ }
284
+ return resolved;
285
+ };
286
+ // ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
287
+ // schema が変わらないポート・ノードは同じ参照を返す
288
+ export const propagateDerivedPorts = (nodes, connections) => {
289
+ const resolved = resolvePortSchemas(nodes, connections);
290
+ return nodes.map((node) => {
291
+ let touched = false;
292
+ const ports = node.ports.map((port) => {
293
+ const schema = resolved.get(port.id) ?? port.schema;
294
+ if (JSON.stringify(schema) === JSON.stringify(port.schema))
295
+ return port;
296
+ touched = true;
297
+ return { ...port, schema };
298
+ });
299
+ return touched ? { ...node, ports } : node;
300
+ });
301
+ };
302
+ // ---- 接続バリデーション(伝播型 ⊆ 宣言スキーマ) ----
303
+ // 完全な JSON Schema 包含ではなく実用的な構造チェック:any は素通し、type 一致、
304
+ // object は宣言側 required の存在と型互換、array は items 互換
305
+ export const isSchemaSubset = (sub, sup) => {
306
+ if (isAnySchema(sup) || isAnySchema(sub))
307
+ return true;
308
+ // file は nominal:マーカー一致の file 同士だけ可(file → any は上の any 判定で許可)。
309
+ // 構造が同じだけの普通の object は file に繋がない/file は普通の object に繋がない
310
+ if (isFileSchema(sub) || isFileSchema(sup))
311
+ return isFileSchema(sub) && isFileSchema(sup);
312
+ if (sub.type !== sup.type)
313
+ return false;
314
+ if (sup.type === "object") {
315
+ const subProps = (sub.properties ?? {});
316
+ const subRequired = (sub.required ?? []);
317
+ const supProps = (sup.properties ?? {});
318
+ for (const key of (sup.required ?? [])) {
319
+ const subProp = subProps[key];
320
+ if (!subProp || !subRequired.includes(key))
321
+ return false;
322
+ const supProp = supProps[key];
323
+ if (supProp && !isSchemaSubset(subProp, supProp))
324
+ return false;
325
+ }
326
+ return true;
327
+ }
328
+ if (sup.type === "array") {
329
+ const supItems = sup.items;
330
+ const subItems = sub.items;
331
+ if (!supItems || !subItems)
332
+ return true;
333
+ return isSchemaSubset(subItems, supItems);
334
+ }
335
+ return true;
336
+ };
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const programManifestSchema: z.ZodObject<{
3
+ name: z.ZodOptional<z.ZodString>;
4
+ description: z.ZodOptional<z.ZodString>;
5
+ env: z.ZodDefault<z.ZodArray<z.ZodString>>;
6
+ inputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
7
+ outputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
8
+ refs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
9
+ }, z.core.$strip>;
10
+ export type ProgramManifest = z.infer<typeof programManifestSchema>;
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { jsonSchemaSchema, portKeySchema } from "./node.js";
3
+ // nodes/program/<dir>/config.json(リポジトリ規約 v2)。プログラム自身が入出力スキーマを宣言し、
4
+ // エディタが取り込んで ports を自動生成する(実行時の正は ports テーブルのまま。
5
+ // docs/tasks/wip/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
6
+ // inputs / outputs は「ポート key → JSON Schema」のマップ。将来 join(in 複数)を導入しても
7
+ // 形式が変わらないよう最初から複数形にしてある
8
+ export const programManifestSchema = z
9
+ .object({
10
+ // 表示名(省略時はディレクトリ名)
11
+ name: z.string().min(1).optional(),
12
+ description: z.string().optional(),
13
+ // 実行に必要な secret 名の宣言(runner が宣言分だけを子プロセスへ渡す。
14
+ // 実行repoの構造見直し)
15
+ env: z.array(z.string().min(1)).default([]),
16
+ inputs: z.record(z.string(), jsonSchemaSchema),
17
+ outputs: z.record(z.string(), jsonSchemaSchema),
18
+ // refs=ループ変数の定義の転写(key → 値から推定した JSON Schema)。
19
+ // loop(エディタ)が正で、このセクションはグラフ保存が一方向に上書きする生成物。
20
+ // handler 実装者が「refs.json に何が来るか」を repo 側で見るためにある。
21
+ // 値そのものは repo に書かない(上流出力の参照(refs).md)
22
+ refs: z.record(portKeySchema, jsonSchemaSchema).default({}),
23
+ })
24
+ .superRefine((manifest, ctx) => {
25
+ // グラフ側の既存制約(in ポートちょうど1つ・key "in" 固定)に整合させる
26
+ const inKeys = Object.keys(manifest.inputs);
27
+ if (inKeys.length !== 1 || inKeys[0] !== "in") {
28
+ ctx.addIssue({
29
+ code: "custom",
30
+ path: ["inputs"],
31
+ message: 'inputs は "in" の1件だけ宣言してください(join 導入まで)',
32
+ });
33
+ }
34
+ if (Object.keys(manifest.outputs).length < 1) {
35
+ ctx.addIssue({
36
+ code: "custom",
37
+ path: ["outputs"],
38
+ message: "outputs は1件以上宣言してください",
39
+ });
40
+ }
41
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { relative } from "node:path";
3
+ import { runInit } from "./init.js";
4
+ import { runTypegen } from "./typegen.js";
5
+ import { validateRepo } from "./validate.js";
6
+ // 使い方: mawaru <init|typegen|validate> [repoルート](省略時はカレントディレクトリ)
7
+ const USAGE = `使い方: mawaru <command> [repoルート]
8
+
9
+ コマンド:
10
+ init repo を mawaru 実行用に初期化する(runner yml・nodes/ 骨組み・スキルポインタ等。再実行は冪等)
11
+ typegen nodes/program/<dir>/config.json から types.d.ts(Input / Outputs / Output 型)を生成する
12
+ validate リポジトリ規約を検証する(error があれば exit 1。CI 向け)`;
13
+ const [command, rootArg] = process.argv.slice(2);
14
+ const root = rootArg ?? process.cwd();
15
+ switch (command) {
16
+ case "init": {
17
+ const result = runInit(root);
18
+ for (const path of result.written)
19
+ console.log(`生成: ${path}`);
20
+ for (const path of result.skipped)
21
+ console.log(`保持: ${path}(既存)`);
22
+ for (const note of result.notes)
23
+ console.log(`→ ${note}`);
24
+ break;
25
+ }
26
+ case "typegen": {
27
+ const result = runTypegen(root);
28
+ for (const path of result.written) {
29
+ console.log(`生成: ${relative(root, path)}`);
30
+ }
31
+ for (const error of result.errors) {
32
+ console.error(`✖ ${error.dir}: ${error.message}`);
33
+ }
34
+ process.exit(result.errors.length > 0 ? 1 : 0);
35
+ break;
36
+ }
37
+ case "validate": {
38
+ const issues = validateRepo(root);
39
+ for (const issue of issues) {
40
+ const mark = issue.level === "error" ? "✖" : "⚠";
41
+ console.log(`${mark} ${issue.path}: ${issue.message}`);
42
+ }
43
+ const errorCount = issues.filter((i) => i.level === "error").length;
44
+ console.log(issues.length === 0
45
+ ? "問題は見つかりませんでした"
46
+ : `error ${errorCount} 件 / warning ${issues.length - errorCount} 件`);
47
+ process.exit(errorCount > 0 ? 1 : 0);
48
+ break;
49
+ }
50
+ default: {
51
+ console.error(USAGE);
52
+ process.exit(command === undefined || command === "--help" ? 0 : 1);
53
+ }
54
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./_schemas/ai-manifest.js";
2
+ export * from "./_schemas/graph.js";
3
+ export * from "./_schemas/hook-manifest.js";
4
+ export * from "./_schemas/node.js";
5
+ export * from "./_schemas/port-spec.js";
6
+ export * from "./_schemas/program-manifest.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // mawaru の契約スキーマ(実行 repo の config.json 規約+ループ graph API のボディ)。
2
+ // 正はここ(npm 公開)で、@mawaru/common(private)が re-export して
3
+ // backend / frontend が使う。CLI(typegen / validate)の実装は export しない
4
+ export * from "./_schemas/ai-manifest.js";
5
+ export * from "./_schemas/graph.js";
6
+ export * from "./_schemas/hook-manifest.js";
7
+ export * from "./_schemas/node.js";
8
+ export * from "./_schemas/port-spec.js";
9
+ export * from "./_schemas/program-manifest.js";
package/dist/init.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type InitResult = {
2
+ written: string[];
3
+ skipped: string[];
4
+ notes: string[];
5
+ };
6
+ export declare const runInit: (root: string) => InitResult;