@mawaru/sdk 0.31.0 → 0.33.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.
@@ -548,6 +548,14 @@ const outByKey = (node: TestNode, key: string): GraphPort => {
548
548
  }
549
549
 
550
550
  describe("saveGraphBodySchema(kind 別ポート構成)", () => {
551
+ it("human の追加出口を保存できる", () => {
552
+ const human = humanNode()
553
+ human.ports.push(port("out", "notion_task"))
554
+ expect(() =>
555
+ saveGraphBodySchema.parse({ nodes: [human], connections: [] }),
556
+ ).not.toThrow()
557
+ })
558
+
551
559
  it("human の担当なし(human 設定の省略)を拒否する", () => {
552
560
  const { human: _human, ...noAssign } = humanNode()
553
561
  expect(() =>
@@ -900,6 +908,16 @@ describe("saveGraphBodySchema(refs:ループ変数の定義スキーマ)",
900
908
  ).toThrow(/required/)
901
909
  })
902
910
 
911
+ it("properties に予約キー input(run の起動入力)は置けない", () => {
912
+ expect(() =>
913
+ saveGraphBodySchema.parse({
914
+ nodes: [makeNode()],
915
+ connections: [],
916
+ refs: { type: "object", properties: { input: { type: "string" } } },
917
+ }),
918
+ ).toThrow(/input/)
919
+ })
920
+
903
921
  it("object 以外の type・旧「値の袋」形は拒否する", () => {
904
922
  for (const refs of [
905
923
  { type: "string" },
package/_schemas/graph.ts CHANGED
@@ -20,6 +20,12 @@ import {
20
20
  export const refsBagSchema = z.record(portKeySchema, z.unknown())
21
21
  export type RefsBag = z.infer<typeof refsBagSchema>
22
22
 
23
+ // 予約キー:run の起動入力(runs.start_payload)。Run.start が袋に写し、全 step
24
+ // (Program の refs.json・AI のプロンプト・承認ビューの ctx)から読める。
25
+ // ループ定義には置けず、起動指定・end のパッチでも書けない
26
+ // (起動入力をrefsに入れて全ノードから参照できるようにする.md)
27
+ export const REFS_INPUT_KEY = "input"
28
+
23
29
  // ループ変数の定義=袋全体を表す 1 つの object JSON Schema
24
30
  // (ループ変数(refs)のJSON Schema化.md)。properties のキーが変数名、
25
31
  // default が静的なデフォルト値、required は「run 起動時に値が確定して
@@ -31,6 +37,13 @@ export const loopRefsSchema = z
31
37
  required: z.array(portKeySchema).optional(),
32
38
  })
33
39
  .superRefine((refs, ctx) => {
40
+ if (REFS_INPUT_KEY in refs.properties) {
41
+ ctx.addIssue({
42
+ code: "custom",
43
+ path: ["properties", REFS_INPUT_KEY],
44
+ message: `properties のキー ${REFS_INPUT_KEY} は予約キー(run の起動入力)です`,
45
+ })
46
+ }
34
47
  for (const key of refs.required ?? []) {
35
48
  if (!(key in refs.properties)) {
36
49
  ctx.addIssue({
@@ -90,7 +90,26 @@ describe("kindPortsViolation(kind 別ポート構成)", () => {
90
90
  ).toBeNull()
91
91
  })
92
92
 
93
- it("human は in / approve / reject 固定", () => {
93
+ it("human は追加の出口を持てるが基本ポートは必須", () => {
94
+ const ports = [
95
+ port("in", "in"),
96
+ port("out", "approve"),
97
+ port("out", "reject"),
98
+ port("out", "notion_task"),
99
+ ]
100
+ expect(kindPortsViolation("human", ports)).toBeNull()
101
+ expect(
102
+ kindPortsViolation(
103
+ "human",
104
+ ports.filter((p) => p.key !== "reject"),
105
+ ),
106
+ ).toMatch(/human/)
107
+ expect(
108
+ kindPortsViolation("human", [...ports, port("in", "extra")]),
109
+ ).toMatch(/human/)
110
+ })
111
+
112
+ it("human は in / approve / reject を持つ", () => {
94
113
  expect(
95
114
  kindPortsViolation("human", [
96
115
  port("in", "in"),
@@ -132,6 +151,35 @@ describe("kindPortsViolation(kind 別ポート構成)", () => {
132
151
  })
133
152
 
134
153
  describe("型伝播(導出ポートの解決)", () => {
154
+ it.each([
155
+ "notion_task",
156
+ "main",
157
+ ])("Humanの追加出口 %s の宣言型は承認ビューの型で上書きしない", (key) => {
158
+ const extra = port("out", key, {
159
+ type: "object",
160
+ properties: { title: { type: "string" } },
161
+ required: ["title"],
162
+ })
163
+ const human = {
164
+ id: uuid(),
165
+ kind: "human",
166
+ ports: [
167
+ port("in", "in"),
168
+ port("out", "approve"),
169
+ port("out", "reject"),
170
+ extra,
171
+ ],
172
+ }
173
+ const resolved = resolvePortSchemas([human], [], {
174
+ viewPortSchemasOf: () => ({
175
+ in: { type: "string" },
176
+ approve: { type: "string" },
177
+ reject: { type: "string" },
178
+ }),
179
+ })
180
+ expect(resolved.get(extra.id)).toEqual(extra.schema)
181
+ })
182
+
135
183
  // 伝播テスト用の任意の宣言型(中身の形に意味はない)
136
184
  const declaredOut = {
137
185
  type: "object",
@@ -139,11 +139,10 @@ export const dataFormatOf = (schema: JsonSchema): DataFormat | null => {
139
139
  }
140
140
 
141
141
  // ---- kind 別ポート構成 ----
142
- // human / wait は構成(数・key・io)が固定。program / ai は in 1つ+out 1つ以上
142
+ // wait は構成が固定。human は基本ポートを保ち追加 out を持てる。program / ai は out 自由
143
143
  // (key 自由。複数 out=分岐。AIノードの複数出口(分岐).md)
144
144
 
145
145
  const FIXED_PORT_KEYS: Record<string, { in: string[]; out: string[] }> = {
146
- human: { in: ["in"], out: ["approve", "reject"] },
147
146
  wait: { in: ["in"], out: ["main"] },
148
147
  // start は in 1つ(run 入力契約)+ out main 1つの恒等パススルー
149
148
  start: { in: ["in"], out: ["main"] },
@@ -199,6 +198,19 @@ export const kindPortsViolation = (
199
198
  }
200
199
  return null
201
200
  }
201
+ if (kind === "human") {
202
+ const inKeys = ports.filter((p) => p.io === "in").map((p) => p.key)
203
+ const outKeys = ports.filter((p) => p.io === "out").map((p) => p.key)
204
+ if (
205
+ inKeys.length !== 1 ||
206
+ inKeys[0] !== "in" ||
207
+ !outKeys.includes("approve") ||
208
+ !outKeys.includes("reject")
209
+ ) {
210
+ return "human のポート構成は in(in) と out(approve / reject) が必須です(out は追加できます)"
211
+ }
212
+ return null
213
+ }
202
214
  const fixed = FIXED_PORT_KEYS[kind]
203
215
  if (!fixed) {
204
216
  // program / ai(自由 out):in の構成だけ固定
@@ -334,7 +346,7 @@ const MIRROR_KINDS = new Set(["program", "ai"])
334
346
  const isDerivedPort = (kind: string, port: Pick<GraphPort, "io" | "key">) =>
335
347
  DERIVED_KINDS.has(kind) &&
336
348
  (port.io === "in" ||
337
- port.key === "main" ||
349
+ (kind !== "human" && port.key === "main") ||
338
350
  port.key === "approve" ||
339
351
  port.key === "reject")
340
352
 
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  export declare const refsBagSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
3
3
  export type RefsBag = z.infer<typeof refsBagSchema>;
4
+ export declare const REFS_INPUT_KEY = "input";
4
5
  export declare const loopRefsSchema: z.ZodObject<{
5
6
  type: z.ZodLiteral<"object">;
6
7
  properties: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
@@ -6,6 +6,11 @@ import { hasInboundConflict, isSchemaSubset, kindPortsViolation, resolvePortSche
6
6
  // key はポート key と同じ規則(^[a-z][a-z0-9_]*$)。実行系ボディ
7
7
  // (run 起動の上書き・step end のパッチ)が使う契約
8
8
  export const refsBagSchema = z.record(portKeySchema, z.unknown());
9
+ // 予約キー:run の起動入力(runs.start_payload)。Run.start が袋に写し、全 step
10
+ // (Program の refs.json・AI のプロンプト・承認ビューの ctx)から読める。
11
+ // ループ定義には置けず、起動指定・end のパッチでも書けない
12
+ // (起動入力をrefsに入れて全ノードから参照できるようにする.md)
13
+ export const REFS_INPUT_KEY = "input";
9
14
  // ループ変数の定義=袋全体を表す 1 つの object JSON Schema
10
15
  // (ループ変数(refs)のJSON Schema化.md)。properties のキーが変数名、
11
16
  // default が静的なデフォルト値、required は「run 起動時に値が確定して
@@ -17,6 +22,13 @@ export const loopRefsSchema = z
17
22
  required: z.array(portKeySchema).optional(),
18
23
  })
19
24
  .superRefine((refs, ctx) => {
25
+ if (REFS_INPUT_KEY in refs.properties) {
26
+ ctx.addIssue({
27
+ code: "custom",
28
+ path: ["properties", REFS_INPUT_KEY],
29
+ message: `properties のキー ${REFS_INPUT_KEY} は予約キー(run の起動入力)です`,
30
+ });
31
+ }
20
32
  for (const key of refs.required ?? []) {
21
33
  if (!(key in refs.properties)) {
22
34
  ctx.addIssue({
@@ -118,10 +118,9 @@ export const dataFormatOf = (schema) => {
118
118
  return { format: "list", columns };
119
119
  };
120
120
  // ---- kind 別ポート構成 ----
121
- // human / wait は構成(数・key・io)が固定。program / ai は in 1つ+out 1つ以上
121
+ // wait は構成が固定。human は基本ポートを保ち追加 out を持てる。program / ai は out 自由
122
122
  // (key 自由。複数 out=分岐。AIノードの複数出口(分岐).md)
123
123
  const FIXED_PORT_KEYS = {
124
- human: { in: ["in"], out: ["approve", "reject"] },
125
124
  wait: { in: ["in"], out: ["main"] },
126
125
  // start は in 1つ(run 入力契約)+ out main 1つの恒等パススルー
127
126
  start: { in: ["in"], out: ["main"] },
@@ -166,6 +165,17 @@ export const kindPortsViolation = (kind, ports) => {
166
165
  }
167
166
  return null;
168
167
  }
168
+ if (kind === "human") {
169
+ const inKeys = ports.filter((p) => p.io === "in").map((p) => p.key);
170
+ const outKeys = ports.filter((p) => p.io === "out").map((p) => p.key);
171
+ if (inKeys.length !== 1 ||
172
+ inKeys[0] !== "in" ||
173
+ !outKeys.includes("approve") ||
174
+ !outKeys.includes("reject")) {
175
+ return "human のポート構成は in(in) と out(approve / reject) が必須です(out は追加できます)";
176
+ }
177
+ return null;
178
+ }
169
179
  const fixed = FIXED_PORT_KEYS[kind];
170
180
  if (!fixed) {
171
181
  // program / ai(自由 out):in の構成だけ固定
@@ -258,7 +268,7 @@ const DERIVED_KINDS = new Set(["wait", "human", "end", "start"]);
258
268
  const MIRROR_KINDS = new Set(["program", "ai"]);
259
269
  const isDerivedPort = (kind, port) => DERIVED_KINDS.has(kind) &&
260
270
  (port.io === "in" ||
261
- port.key === "main" ||
271
+ (kind !== "human" && port.key === "main") ||
262
272
  port.key === "approve" ||
263
273
  port.key === "reject");
264
274
  // port_id → 解決済みスキーマ。導出は接続を遡って固定点まで反復し、
package/dist/typegen.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { REFS_INPUT_KEY } from "./_schemas/graph.js";
4
5
  import { programManifestSchema, } from "./_schemas/program-manifest.js";
5
6
  // 生成ファイルに刻む config.json 内容のハッシュ。validate が drift(config を変えて
6
7
  // typegen し忘れ)の検出に使う
@@ -139,8 +140,12 @@ export const renderTypesFile = (manifest, rawConfig) => {
139
140
  const inputSchema = manifest.inputs.in ?? {};
140
141
  const outputEntries = Object.entries(manifest.outputs).map(([key, schema]) => ` ${propertyKey(key)}: ${tsTypeFromJsonSchema(schema, 1)}`);
141
142
  // refs=ループ変数の袋(実行時に refs.json で渡される)。config の refs はループ定義
142
- // からの転写(キー→推定型)なので、宣言が無ければ型も出さない
143
- const refEntries = Object.entries(manifest.refs).map(([key, schema]) => ` ${propertyKey(key)}: ${tsTypeFromJsonSchema(schema, 1)}`);
143
+ // からの転写(キー→推定型)。予約キー input(run の起動入力)は定義に現れないが
144
+ // 必ず袋に載るので、宣言が無くても Refs / Ctx を出す
145
+ const refEntries = [
146
+ ` ${propertyKey(REFS_INPUT_KEY)}: unknown`,
147
+ ...Object.entries(manifest.refs).map(([key, schema]) => ` ${propertyKey(key)}: ${tsTypeFromJsonSchema(schema, 1)}`),
148
+ ];
144
149
  return [
145
150
  "// このファイルは mawaru typegen が config.json から自動生成した(手で編集しない)",
146
151
  `${CONFIG_HASH_PREFIX}${configHash(rawConfig)}`,
@@ -152,16 +157,13 @@ export const renderTypesFile = (manifest, rawConfig) => {
152
157
  "// run の戻り値:ちょうど1つの出口ポートのキーで返す { <出口キー>: 値 }",
153
158
  outputUnionType(Object.keys(manifest.outputs)),
154
159
  "",
155
- ...(refEntries.length > 0
156
- ? [
157
- `export type Refs = {\n${refEntries.join("\n")}\n}`,
158
- "",
159
- "// run の第2引数:refs=届いた袋の読み、setRefs=袋へのパッチの宣言",
160
- "// (書いたキーだけが完了時に袋へマージされ、下流のノードから読める)",
161
- "export type Ctx = { refs: Refs; setRefs: (patch: Partial<Refs>) => void }",
162
- "",
163
- ]
164
- : []),
160
+ `export type Refs = {\n${refEntries.join("\n")}\n}`,
161
+ "",
162
+ "// run の第2引数:refs=届いた袋の読み、setRefs=袋へのパッチの宣言",
163
+ "// (書いたキーだけが完了時に袋へマージされ、下流のノードから読める。",
164
+ "// input は run の起動入力=読み専用)",
165
+ "export type Ctx = { refs: Refs; setRefs: (patch: Partial<Refs>) => void }",
166
+ "",
165
167
  ].join("\n");
166
168
  };
167
169
  // nodes/program/ 直下の各 dir の config.json から types.d.ts を生成する
@@ -30,7 +30,9 @@ skills/<dir>/ # AI ノードが参照する skill(SKILL.md)
30
30
  - `{ <出口ポートのkey>: <データ> }` の**単一キーのオブジェクト**(どの出口から出るかを戻り値自身が運ぶ)
31
31
  - `"pending"` — 完了保留(外部イベントで後から end が届く)
32
32
  - `ctx` — ループ変数の袋の読み書き:
33
- - `ctx.refs` — 届いた袋(loop に定義された変数の、この step 開始時点の値)
33
+ - `ctx.refs` — 届いた袋(loop に定義された変数の、この step 開始時点の値)。
34
+ 予約キー `ctx.refs.input` は **run の起動入力**(開始ノードに渡された値。コンポーネントの
35
+ 中ならコンポーネントの入力)。読み専用で、loop の定義には置けず `setRefs` でも書けない
34
36
  - `ctx.setRefs({ key: value })` — 袋へのパッチの宣言。**完了時にだけ**反映され、以降の
35
37
  ノードが `ctx.refs` で読める(動的な値を下流へ渡す唯一の経路)。何度呼んでもトップレベル
36
38
  キー後勝ちで畳まれる。`"pending"` を返す実行では反映されない。loop に定義の無いキーを
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mawaru/sdk",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "mawaru 実行 repo の開発 SDK。契約スキーマ(config.json 規約・ループ graph API)の正 + typegen / validate CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,7 +87,8 @@ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等
87
87
  - kind 別の固定ポート構成(human / wait / ai)は `port-spec.ts` の `kindPortsViolation` に従う
88
88
  - ループ変数(refs=run 全体で読める key→値 の袋)が要るならトップレベルの `refs` に入れる。
89
89
  ノード単位の宣言やバインドは無い。handler は実行時に `refs.json` で全量を受け取り、
90
- `end` の `refs` パッチで値を更新できる(config.json の `refs` はループ定義からの転写=生成物)
90
+ `end` の `refs` パッチで値を更新できる(config.json の `refs` はループ定義からの転写=生成物)。
91
+ キー `input` は予約(run の起動入力が自動で入る。定義に置くと 422)
91
92
  - `position_x` / `position_y` は左→右に流れる単純配置(例: x を 320 刻み、y は 240)
92
93
  5. **保存**:`PUT loops/{loopId}/graph`。400 / 422 / 409 ならメッセージを読んで直して再送。
93
94
  6. **報告**:エディタ URL `{MAWARU_APP_URL}/loops/{loopId}/edit` をユーザーに渡し、