@mawaru/sdk 0.39.0 → 0.41.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 (79) hide show
  1. package/_schemas/graph.test.ts +15 -9
  2. package/_schemas/graph.ts +15 -6
  3. package/_schemas/human-manifest.test.ts +67 -0
  4. package/_schemas/human-manifest.ts +31 -0
  5. package/_schemas/port-spec.test.ts +33 -92
  6. package/_schemas/port-spec.ts +36 -59
  7. package/dist/_cli/previewCommand.js +3 -3
  8. package/dist/_cli/previewFiles.js +2 -3
  9. package/dist/_cli/previewSlack.d.ts +1 -1
  10. package/dist/_cli/previewSlack.js +1 -1
  11. package/dist/_schemas/graph.d.ts +6 -3
  12. package/dist/_schemas/graph.js +12 -6
  13. package/dist/_schemas/human-manifest.d.ts +9 -0
  14. package/dist/_schemas/human-manifest.js +29 -0
  15. package/dist/_schemas/port-spec.d.ts +5 -11
  16. package/dist/_schemas/port-spec.js +28 -37
  17. package/dist/cli.js +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/init.js +22 -3
  21. package/dist/typegen.d.ts +3 -1
  22. package/dist/typegen.js +96 -14
  23. package/dist/validate.js +57 -0
  24. package/dist/views/_bridge.d.ts +12 -0
  25. package/dist/views/_bridge.js +10 -0
  26. package/dist/views/_humanProps.d.ts +10 -0
  27. package/dist/views/_humanProps.js +11 -0
  28. package/dist/views/_parts/DocumentCard.d.ts +2 -0
  29. package/dist/views/_parts/DocumentCard.js +16 -0
  30. package/dist/views/_parts/FilePreview.d.ts +5 -0
  31. package/dist/views/_parts/FilePreview.js +101 -0
  32. package/dist/views/_parts/Linkify.d.ts +7 -0
  33. package/dist/views/_parts/Linkify.js +28 -0
  34. package/dist/views/_parts/Markdown.d.ts +44 -0
  35. package/dist/views/_parts/Markdown.js +313 -0
  36. package/dist/views/_parts/SchemaForm.d.ts +19 -0
  37. package/dist/views/_parts/SchemaForm.js +310 -0
  38. package/dist/views/_parts/fileRef.d.ts +7 -0
  39. package/dist/views/_parts/fileRef.js +11 -0
  40. package/dist/views/_parts/useNarrow.d.ts +5 -0
  41. package/dist/views/_parts/useNarrow.js +23 -0
  42. package/dist/views/components.d.ts +7 -0
  43. package/dist/views/components.js +12 -0
  44. package/dist/views/host.d.ts +23 -0
  45. package/dist/views/host.js +20 -0
  46. package/dist/views/human.d.ts +54 -0
  47. package/dist/views/human.js +5 -0
  48. package/dist/views/protocol.d.ts +44 -0
  49. package/dist/views/protocol.js +5 -0
  50. package/dist/views/runtime.d.ts +2 -0
  51. package/dist/views/runtime.js +86 -0
  52. package/dist/views/sample.d.ts +22 -0
  53. package/dist/views/sample.js +38 -0
  54. package/dist/{slack-view.d.ts → views/slack.d.ts} +1 -1
  55. package/docs/development.md +39 -2
  56. package/index.ts +1 -0
  57. package/package.json +40 -4
  58. package/skills/create-loop/SKILL.md +7 -3
  59. package/templates/CLAUDE.md +2 -0
  60. package/views/_bridge.tsx +36 -0
  61. package/views/_humanProps.ts +27 -0
  62. package/views/_parts/DocumentCard.ts +18 -0
  63. package/views/_parts/FilePreview.tsx +180 -0
  64. package/views/_parts/Linkify.test.ts +40 -0
  65. package/views/_parts/Linkify.tsx +40 -0
  66. package/views/_parts/Markdown.test.ts +131 -0
  67. package/views/_parts/Markdown.tsx +472 -0
  68. package/views/_parts/SchemaForm.tsx +742 -0
  69. package/views/_parts/fileRef.ts +21 -0
  70. package/views/_parts/useNarrow.ts +24 -0
  71. package/views/components.ts +16 -0
  72. package/views/host.ts +27 -0
  73. package/views/human.ts +83 -0
  74. package/views/protocol.ts +63 -0
  75. package/views/runtime.tsx +123 -0
  76. package/views/sample.tsx +94 -0
  77. package/views/slack.test.ts +40 -0
  78. package/{slack-view.ts → views/slack.ts} +3 -3
  79. /package/dist/{slack-view.js → views/slack.js} +0 -0
@@ -116,10 +116,16 @@ export const assignMemberSchema = z.union([z.uuid(), z.literal(RUN_STARTER)]);
116
116
  // メンバーが tenant の membership であることの検証は保存 API 側で行う
117
117
  // (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
118
118
  export const humanConfigSchema = z.object({
119
- // 承認 UI(承認ビュー)の識別子。program_dir / ai_dir と同じ「不透明なキー」で、
120
- // どのキーが実在するか・そのテナントが選べるかは保存時に mawaru 側が検証する
121
- // (カタログは非公開。@mawaru/common の approval-view.ts)。省略時は標準ビュー
122
- view: z.string().min(1).optional(),
119
+ // 実装(承認画面と入出力の宣言)の置き場所:実行 repo の nodes/human/ 直下のディレクトリ名。
120
+ // program_dir / ai_dir と同じ。省略時は mawaru 内蔵の標準の承認画面
121
+ // (docs/tasks/wip/承認ビューの顧客repo配置.md の「human ノードの実装として置く」)
122
+ human_dir: z
123
+ .string()
124
+ .min(1)
125
+ .refine((v) => !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..", "human_dir は nodes/human/ 直下のディレクトリ名で指定してください")
126
+ .optional(),
127
+ // 読むブランチ(省略時はデフォルトブランチ)。publish でその先頭の commit に固定する
128
+ ref: z.string().min(1).optional(),
123
129
  assignment: z
124
130
  .object({
125
131
  strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
@@ -236,8 +242,8 @@ export const graphNodeSchema = z.object({
236
242
  // 表示アイコン(全 kind 共通の任意設定。human はフロントが担当から導出するので通常設定しない)
237
243
  icon: nodeIconSchema.optional(),
238
244
  // Slack ビュー(全 kind 共通の任意設定。実行中(human は承認待ち)・完了メッセージの見せ方)。
239
- // human の view と同じ「不透明なキー」で、選べる key は GET slack-views が返す。
240
- // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/slack-view の契約で書く)。
245
+ // human の view と同じ「不透明なキー」で、選べる key は GET views/slack が返す。
246
+ // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/views/slack の契約で書く)。
241
247
  // 保存時に mawaru が見るのは common/* の実在と <slug>/* の可視性だけ。
242
248
  // 省略=承認待ちは承認ボタンだけ・完了は ✅ の1行(データは出さない)。
243
249
  // ポートの型には一切影響しない
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ export declare const humanManifestSchema: z.ZodObject<{
3
+ inputs: z.ZodRecord<z.ZodString, z.ZodObject<{
4
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
5
+ }, z.core.$strip>>;
6
+ outputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
7
+ refs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
8
+ }, z.core.$strip>;
9
+ export type HumanManifest = z.infer<typeof humanManifestSchema>;
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import { jsonSchemaSchema, portKeySchema } from "./node.js";
3
+ import { kindPortsViolation } from "./port-spec.js";
4
+ // 顧客 repo の nodes/human/<dir>/config.json(human ノードの実装の入出力の宣言)。
5
+ // program / AI の config.json と同じく、エディタがディレクトリを選んだときに inputs / outputs から
6
+ // ports を起こし、ポートの編集は保存時にここへ書き戻される(実行時の正は ports テーブルのまま)。
7
+ // 形は AI に揃える(inputs の値は { schema }、outputs の値は素の JSON Schema)。
8
+ // 口の構成(in 1つ・approve / reject は必須・追加の出口は自由)は kind のポート構成の規則をそのまま使う
9
+ export const humanManifestSchema = z
10
+ .object({
11
+ inputs: z.record(z.string(), z.object({ schema: jsonSchemaSchema })),
12
+ outputs: z.record(z.string(), jsonSchemaSchema),
13
+ // refs=ループ変数の定義の転写(program と同じ。エディタのグラフ保存が一方向に書く生成物)
14
+ refs: z.record(portKeySchema, jsonSchemaSchema).default({}),
15
+ })
16
+ .superRefine((manifest, ctx) => {
17
+ const violation = kindPortsViolation("human", [
18
+ ...Object.keys(manifest.inputs).map((key) => ({
19
+ io: "in",
20
+ key,
21
+ })),
22
+ ...Object.keys(manifest.outputs).map((key) => ({
23
+ io: "out",
24
+ key,
25
+ })),
26
+ ]);
27
+ if (violation)
28
+ ctx.addIssue({ code: "custom", message: violation });
29
+ });
@@ -55,24 +55,18 @@ type PropagationNode = {
55
55
  id: string;
56
56
  kind: string;
57
57
  ports: GraphPort[];
58
+ human?: {
59
+ human_dir?: string | null;
60
+ } | null;
58
61
  };
59
62
  type PortLink = {
60
63
  from_port_id: string;
61
64
  to_port_id: string;
62
65
  };
63
- export declare const COPY_UPSTREAM_SCHEMA = "T";
64
- export type DeclaredPortSchema = JsonSchema | typeof COPY_UPSTREAM_SCHEMA;
65
- export type PropagationOptions<N> = {
66
- viewPortSchemasOf?: (node: N) => {
67
- in: DeclaredPortSchema;
68
- approve: DeclaredPortSchema;
69
- reject: JsonSchema;
70
- } | null | undefined;
71
- };
72
- export declare const resolvePortSchemas: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => Map<string, JsonSchema>;
66
+ export declare const resolvePortSchemas: <N extends PropagationNode>(nodes: N[], connections: PortLink[]) => Map<string, JsonSchema>;
73
67
  export declare const resolveFlexiblePorts: (nodes: PropagationNode[], connections: PortLink[]) => Set<string>;
74
68
  export declare const connectionAccepted: (from: JsonSchema, to: Pick<GraphPort, "schema" | "flexible">) => boolean;
75
- export declare const propagateDerivedPorts: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => N[];
69
+ export declare const propagateDerivedPorts: <N extends PropagationNode>(nodes: N[], connections: PortLink[]) => N[];
76
70
  export type SchemaFit = "ok" | "mismatch" | "unknown";
77
71
  export declare const schemaFit: (sub: JsonSchema, sup: JsonSchema) => SchemaFit;
78
72
  export declare const isSchemaSubset: (sub: JsonSchema, sup: JsonSchema) => boolean;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  // ポートの kind 別型システム(docs/tasks/done/ポートのkind別仕様.md)。
3
- // 型の決まり方は2分類:宣言(program / ai の in・out。human はビューの宣言を転写)/
3
+ // 型の決まり方は2分類:宣言(program / ai と、実装(nodes/human/<dir>/)を指す human の in・out)/
4
4
  // 導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)。
5
5
  // mawaru がコードで型を固定するポートは無い
6
6
  // (docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md)
@@ -222,9 +222,10 @@ export const kindPortsViolation = (kind, ports) => {
222
222
  // ---- 型伝播(導出ポートの解決) ----
223
223
  // 接続の両端は同じスキーマになる(値入力フォームのSchemaForm統一.md)。
224
224
  // wait は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
225
- // human はビューの宣言を転写("T" は素通しと同じ挙動。reject もビューの宣言)。
226
- // 宣言 out(program / ai)は触らない。
227
- // program / ai の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
225
+ // human は実装(nodes/human/<dir>/)を指していれば program / ai と同じ宣言ノード、指していなければ
226
+ // (標準の承認画面)wait と同じ素通し(approve は in と同型、reject は標準の差し戻し instructionSchema)。
227
+ // 宣言 out(program / ai / 実装を指す human)は触らない。
228
+ // program / ai(と実装を指す human)の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
228
229
  // 上書きし、未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の
229
230
  // config.json へも書き戻される。docs/tasks/done/ポート編集のrepo書き戻し同期.md)
230
231
  // ---- 型としての同一性(schemaTypeKey) ----
@@ -273,9 +274,6 @@ export const mergeInboundSchemas = (schemas) => {
273
274
  // 流入の具体型が2種類以上あるか(=同じポートに違う型を流し込んでいる)
274
275
  export const hasInboundConflict = (schemas) => new Set(schemas.filter((s) => !isUndeclaredSchema(s)).map(schemaTypeKey))
275
276
  .size > 1;
276
- // 「上流の out をそのままコピーする」(in)/「in と同型」(approve)を表す指示。
277
- // 承認ビューのカタログ(@mawaru/common)の COPY_UPSTREAM と同じ値
278
- export const COPY_UPSTREAM_SCHEMA = "T";
279
277
  // end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
280
278
  // resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
281
279
  // start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
@@ -284,9 +282,14 @@ export const COPY_UPSTREAM_SCHEMA = "T";
284
282
  const DERIVED_KINDS = new Set(["wait", "human", "end", "start", "join"]);
285
283
  // in を接続鏡映(sticky)するノード:接続元 out が具体型のときだけ上書き
286
284
  const MIRROR_KINDS = new Set(["program", "ai"]);
287
- const isDerivedPort = (kind, port) => DERIVED_KINDS.has(kind) &&
285
+ // 実装(nodes/human/<dir>/)を指す human は program / ai と同じ(in は鏡映・out は宣言。
286
+ // 型は config.json へ書き戻される)。指していない human は導出(標準の承認画面の素通し)
287
+ const isMirrorNode = (node) => MIRROR_KINDS.has(node.kind) ||
288
+ (node.kind === "human" && Boolean(node.human?.human_dir));
289
+ const isDerivedNode = (node) => DERIVED_KINDS.has(node.kind) && !isMirrorNode(node);
290
+ const isDerivedPort = (node, port) => isDerivedNode(node) &&
288
291
  (port.io === "in" ||
289
- (kind !== "human" && port.key === "main") ||
292
+ (node.kind !== "human" && port.key === "main") ||
290
293
  port.key === "approve" ||
291
294
  port.key === "reject");
292
295
  // port_id → 解決済みスキーマ。導出は接続を遡って固定点まで反復し、
@@ -294,13 +297,13 @@ const isDerivedPort = (kind, port) => DERIVED_KINDS.has(kind) &&
294
297
  // ai の in は nodes/ai/<dir>/config.json の inputs[key].schema 由来の宣言型が初期値で、
295
298
  // 接続時は program と同じ鏡映で接続元と同値に同期される(どの in も対等。
296
299
  // instruction への型の焼き込みは廃止)
297
- export const resolvePortSchemas = (nodes, connections, options) => {
300
+ export const resolvePortSchemas = (nodes, connections) => {
298
301
  const resolved = new Map();
299
302
  const kindByPortId = new Map();
300
303
  for (const node of nodes) {
301
304
  for (const port of node.ports) {
302
305
  kindByPortId.set(port.id, node.kind);
303
- resolved.set(port.id, isDerivedPort(node.kind, port) ? UNDECLARED_SCHEMA : port.schema);
306
+ resolved.set(port.id, isDerivedPort(node, port) ? UNDECLARED_SCHEMA : port.schema);
304
307
  }
305
308
  }
306
309
  // in ポートへの流入(差し戻しの fan-in があり得る。1本のときだけ型を採用)
@@ -373,10 +376,10 @@ export const resolvePortSchemas = (nodes, connections, options) => {
373
376
  }
374
377
  continue;
375
378
  }
376
- const isMirror = MIRROR_KINDS.has(node.kind);
377
- if (!DERIVED_KINDS.has(node.kind) && !isMirror)
379
+ const isMirror = isMirrorNode(node);
380
+ if (!isDerivedNode(node) && !isMirror)
378
381
  continue;
379
- // 鏡映(program / ai):全ての in が対象。具体型が流れてきたときだけ上書きする
382
+ // 鏡映(program / ai / 実装を指す human):全ての in が対象。具体型が流れてきたときだけ上書きする
380
383
  // (宣言を未宣言で潰さない)。ai の in はどの口も対等(instruction の特別扱いは無い)
381
384
  if (isMirror) {
382
385
  for (const port of node.ports) {
@@ -392,34 +395,22 @@ export const resolvePortSchemas = (nodes, connections, options) => {
392
395
  }
393
396
  continue;
394
397
  }
395
- // 導出(wait / human):in は上流から
398
+ // 導出(wait / 実装を指さない human):in は上流から、out(main・approve)は in と同型。
399
+ // human の reject は標準の承認画面の差し戻し(指示+参考ファイル)。
400
+ // 上流の out は書き換えない(逆伝播すると fan-out 先の型割れが config.json へ書き戻され、
401
+ // 元の出力宣言を黙って壊す。ズレは接続の型エラーで見せる)
396
402
  const inPort = node.ports.find((p) => p.io === "in");
397
403
  if (!inPort)
398
404
  continue;
399
405
  const sources = inbound.get(inPort.id) ?? [];
400
406
  const inSchema = mergeInboundSchemas(sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA));
401
- // 承認ビューの宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
402
- // "T" は「上流の out をコピー」(in)/「in と同型」(approve)。reject は具体型の宣言
403
- // のみで、宣言が無ければ未宣言のまま(mawaru が型を焼き込むことはしない)。
404
- // 上流の out は書き換えない:逆伝播すると fan-out 先のビューが食い違ったとき
405
- // 型割れが config.json へ write-through され、元の出力宣言を黙って壊す。
406
- // ズレは接続の型エラーで見せてユーザーの [修正する] で直す
407
- const declared = options?.viewPortSchemasOf?.(node);
408
- const effectiveIn = declared && declared.in !== COPY_UPSTREAM_SCHEMA
409
- ? declared.in
410
- : inSchema;
411
- assign(inPort.id, effectiveIn);
407
+ assign(inPort.id, inSchema);
412
408
  for (const port of node.ports) {
413
- if (port.io !== "out" || !isDerivedPort(node.kind, port))
409
+ if (port.io !== "out" || !isDerivedPort(node, port))
414
410
  continue;
415
- if (node.kind === "human" && port.key === "reject") {
416
- if (declared)
417
- assign(port.id, declared.reject);
418
- continue;
419
- }
420
- assign(port.id, declared && declared.approve !== COPY_UPSTREAM_SCHEMA
421
- ? declared.approve
422
- : effectiveIn);
411
+ assign(port.id, node.kind === "human" && port.key === "reject"
412
+ ? instructionSchema()
413
+ : inSchema);
423
414
  }
424
415
  }
425
416
  if (!changed)
@@ -470,8 +461,8 @@ export const resolveFlexiblePorts = (nodes, connections) => {
470
461
  // sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
471
462
  // (判定が散ると表示と保存がズレる)
472
463
  export const connectionAccepted = (from, to) => to.flexible === true || isSchemaSubset(from, to.schema);
473
- export const propagateDerivedPorts = (nodes, connections, options) => {
474
- const resolved = resolvePortSchemas(nodes, connections, options);
464
+ export const propagateDerivedPorts = (nodes, connections) => {
465
+ const resolved = resolvePortSchemas(nodes, connections);
475
466
  const flexiblePorts = resolveFlexiblePorts(nodes, connections);
476
467
  return nodes.map((node) => {
477
468
  let touched = false;
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ const USAGE = `使い方: mawaru <command> [引数]
12
12
 
13
13
  コマンド:
14
14
  init repo を mawaru 実行用に初期化する(runner yml・nodes/ 骨組み・スキルポインタ等。再実行は冪等)
15
- typegen nodes/program/<dir>/config.json から types.d.ts(Input / Outputs / Output 型)を生成する
15
+ typegen nodes/program/<dir>/・nodes/human/<dir>/ の config.json から types.d.ts を生成する
16
16
  validate リポジトリ規約を検証する(error があれば exit 1。CI 向け)
17
17
  login ブラウザで承認して mawaru にログインする(~/.mawaru/credentials.json)
18
18
  logout 保存した資格情報を消す
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./_schemas/duplicate-selection.js";
3
3
  export * from "./_schemas/extract-component.js";
4
4
  export * from "./_schemas/graph.js";
5
5
  export * from "./_schemas/hook-manifest.js";
6
+ export * from "./_schemas/human-manifest.js";
6
7
  export * from "./_schemas/node.js";
7
8
  export * from "./_schemas/port-spec.js";
8
9
  export * from "./_schemas/program-manifest.js";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export * from "./_schemas/duplicate-selection.js";
6
6
  export * from "./_schemas/extract-component.js";
7
7
  export * from "./_schemas/graph.js";
8
8
  export * from "./_schemas/hook-manifest.js";
9
+ export * from "./_schemas/human-manifest.js";
9
10
  export * from "./_schemas/node.js";
10
11
  export * from "./_schemas/port-spec.js";
11
12
  export * from "./_schemas/program-manifest.js";
package/dist/init.js CHANGED
@@ -68,13 +68,32 @@ export const runInit = (root) => {
68
68
  // 作用してしまうため、ドット無しの "gitignore" で持つ
69
69
  writeIfAbsent(".gitignore", template("gitignore"));
70
70
  // package.json:@mawaru/sdk を devDependencies に(ポインタ・docs の参照解決と
71
- // typegen / validate CLI のため)。バージョンはこのパッケージ自身から取る
72
- const sdkVersion = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf-8")).version;
71
+ // typegen / validate CLI のため)。承認ビュー(TSX)の型のために @types/react も。
72
+ // バージョンはこのパッケージ自身から取る
73
+ const sdkPackage = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf-8"));
73
74
  writeIfAbsent("package.json", `${JSON.stringify({
74
75
  name: "mawaru",
75
76
  private: true,
76
77
  type: "module",
77
- devDependencies: { "@mawaru/sdk": `^${sdkVersion}` },
78
+ devDependencies: {
79
+ "@mawaru/sdk": `^${sdkPackage.version}`,
80
+ "@types/react": sdkPackage.devDependencies["@types/react"],
81
+ },
82
+ }, null, 2)}\n`);
83
+ // tsconfig.json:エディタで human ノードの実装(nodes/human/)と Slack ビュー(views/)を型検査するための設定。
84
+ // ビルドはしない(mawaru がまとめる)。import の解決は mawaru のまとめ方(esbuild)に揃える
85
+ writeIfAbsent("tsconfig.json", `${JSON.stringify({
86
+ compilerOptions: {
87
+ target: "ES2022",
88
+ module: "ESNext",
89
+ moduleResolution: "Bundler",
90
+ jsx: "react-jsx",
91
+ lib: ["ES2023", "DOM", "DOM.Iterable"],
92
+ strict: true,
93
+ noEmit: true,
94
+ skipLibCheck: true,
95
+ },
96
+ include: ["nodes/human", "views"],
78
97
  }, null, 2)}\n`);
79
98
  // runner yml の `npm ci` とポインタの参照解決は node_modules / lockfile が前提
80
99
  if (!existsSync(join(root, "package-lock.json"))) {
package/dist/typegen.d.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { type HumanManifest } from "./_schemas/human-manifest.js";
1
2
  import type { JsonSchema } from "./_schemas/node.js";
2
3
  import { type ProgramManifest } from "./_schemas/program-manifest.js";
3
4
  export declare const CONFIG_HASH_PREFIX = "// mawaru:config-hash=";
4
5
  export declare const configHash: (rawConfig: string) => string;
5
- export declare const tsTypeFromJsonSchema: (schema: JsonSchema, indent?: number) => string;
6
+ export declare const tsTypeFromJsonSchema: (schema: JsonSchema, indent?: number, fileAs?: string) => string;
6
7
  export declare const renderTypesFile: (manifest: ProgramManifest, rawConfig: string) => string;
8
+ export declare const renderHumanTypesFile: (manifest: HumanManifest, rawConfig: string) => string;
7
9
  export type TypegenResult = {
8
10
  written: string[];
9
11
  errors: {
package/dist/typegen.js CHANGED
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { REFS_INPUT_KEY } from "./_schemas/graph.js";
5
+ import { humanManifestSchema, } from "./_schemas/human-manifest.js";
5
6
  import { programManifestSchema, } from "./_schemas/program-manifest.js";
6
7
  // 生成ファイルに刻む config.json 内容のハッシュ。validate が drift(config を変えて
7
8
  // typegen し忘れ)の検出に使う
@@ -12,14 +13,14 @@ const propertyKey = (key) => IDENTIFIER_RE.test(key) ? key : JSON.stringify(key)
12
13
  const literalType = (value) => value === null ? "null" : (JSON.stringify(value) ?? "unknown");
13
14
  // union / tuple の要素は結合子と混ざらないよう括弧で包む
14
15
  const wrapIfUnion = (type) => type.includes(" | ") || type.includes(" & ") ? `(${type})` : type;
15
- const objectType = (schema, indent) => {
16
+ const objectType = (schema, indent, fileAs) => {
16
17
  const properties = schema.properties;
17
18
  if (typeof properties !== "object" ||
18
19
  properties === null ||
19
20
  Array.isArray(properties)) {
20
21
  const additional = schema.additionalProperties;
21
22
  if (typeof additional === "object" && additional !== null) {
22
- return `Record<string, ${tsTypeFromJsonSchema(additional, indent)}>`;
23
+ return `Record<string, ${tsTypeFromJsonSchema(additional, indent, fileAs)}>`;
23
24
  }
24
25
  if (additional === false) {
25
26
  return "Record<string, never>";
@@ -39,14 +40,14 @@ const objectType = (schema, indent) => {
39
40
  lines.push(`${inner}/** ${description} */`);
40
41
  }
41
42
  const optional = required.has(key) ? "" : "?";
42
- lines.push(`${inner}${propertyKey(key)}${optional}: ${tsTypeFromJsonSchema(propSchema, indent + 1)}`);
43
+ lines.push(`${inner}${propertyKey(key)}${optional}: ${tsTypeFromJsonSchema(propSchema, indent + 1, fileAs)}`);
43
44
  }
44
45
  if (lines.length === 0) {
45
46
  return "Record<string, unknown>";
46
47
  }
47
48
  return `{\n${lines.join("\n")}\n${" ".repeat(indent)}}`;
48
49
  };
49
- const namedType = (typeName, schema, indent) => {
50
+ const namedType = (typeName, schema, indent, fileAs) => {
50
51
  switch (typeName) {
51
52
  case "string":
52
53
  return "string";
@@ -60,22 +61,24 @@ const namedType = (typeName, schema, indent) => {
60
61
  case "array": {
61
62
  const items = schema.items;
62
63
  if (Array.isArray(items)) {
63
- return `[${items.map((i) => tsTypeFromJsonSchema(i, indent)).join(", ")}]`;
64
+ return `[${items.map((i) => tsTypeFromJsonSchema(i, indent, fileAs)).join(", ")}]`;
64
65
  }
65
66
  if (typeof items === "object" && items !== null) {
66
- return `${wrapIfUnion(tsTypeFromJsonSchema(items, indent))}[]`;
67
+ return `${wrapIfUnion(tsTypeFromJsonSchema(items, indent, fileAs))}[]`;
67
68
  }
68
69
  return "unknown[]";
69
70
  }
70
71
  case "object":
71
- return objectType(schema, indent);
72
+ return objectType(schema, indent, fileAs);
72
73
  default:
73
74
  return "unknown";
74
75
  }
75
76
  };
76
77
  // JSON Schema(zodFromJsonSchema が扱うサブセット)を TS 型の文字列に写す。
77
78
  // 解釈できない形は unknown に落とす(型が付かないだけで、実行時検証は ports 側が担う)
78
- export const tsTypeFromJsonSchema = (schema, indent = 0) => {
79
+ // fileAs=file 型の TS 型。program の handler は手元のパス(string)を受ける。承認ビューは参照
80
+ // (FileRef)のまま受ける
81
+ export const tsTypeFromJsonSchema = (schema, indent = 0, fileAs = "string") => {
79
82
  if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
80
83
  return "unknown";
81
84
  }
@@ -84,7 +87,7 @@ export const tsTypeFromJsonSchema = (schema, indent = 0) => {
84
87
  // 出力にローカルパスを書く契約なので、参照 object ではなく string を生成する
85
88
  // (AI 側の schemaWithFilesAsPath と同じ規約)
86
89
  if (schema["x-mawaru-type"] === "file") {
87
- return "string";
90
+ return fileAs;
88
91
  }
89
92
  if ("const" in schema) {
90
93
  return literalType(schema.const);
@@ -97,22 +100,24 @@ export const tsTypeFromJsonSchema = (schema, indent = 0) => {
97
100
  const variants = schema.oneOf ?? schema.anyOf;
98
101
  if (Array.isArray(variants) && variants.length > 0) {
99
102
  return variants
100
- .map((v) => tsTypeFromJsonSchema(v, indent))
103
+ .map((v) => tsTypeFromJsonSchema(v, indent, fileAs))
101
104
  .join(" | ");
102
105
  }
103
106
  if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
104
107
  return schema.allOf
105
- .map((v) => wrapIfUnion(tsTypeFromJsonSchema(v, indent)))
108
+ .map((v) => wrapIfUnion(tsTypeFromJsonSchema(v, indent, fileAs)))
106
109
  .join(" & ");
107
110
  }
108
111
  const type = schema.type;
109
112
  if (Array.isArray(type)) {
110
113
  return type.length > 0
111
- ? type.map((t) => namedType(String(t), schema, indent)).join(" | ")
114
+ ? type
115
+ .map((t) => namedType(String(t), schema, indent, fileAs))
116
+ .join(" | ")
112
117
  : "unknown";
113
118
  }
114
119
  if (typeof type === "string") {
115
- return namedType(type, schema, indent);
120
+ return namedType(type, schema, indent, fileAs);
116
121
  }
117
122
  return "unknown";
118
123
  };
@@ -166,8 +171,85 @@ export const renderTypesFile = (manifest, rawConfig) => {
166
171
  "",
167
172
  ].join("\n");
168
173
  };
169
- // nodes/program/ 直下の各 dir の config.json から types.d.ts を生成する
174
+ // human ノードの実装(nodes/human/<dir>/config.json)の型。program と同じ Input / Outputs / Output / Refs を作り、
175
+ // defineHuman<Input, Output, Refs>(({ input, out, ctx }) => …) に使う。
176
+ // file は参照(FileRef)のまま届く(program の handler と違って実体化しない)
177
+ export const renderHumanTypesFile = (manifest, rawConfig) => {
178
+ const inputType = tsTypeFromJsonSchema(manifest.inputs.in?.schema ?? {}, 0, "FileRef");
179
+ const outputEntries = Object.entries(manifest.outputs).map(([key, schema]) => ` ${propertyKey(key)}: ${tsTypeFromJsonSchema(schema, 1, "FileRef")}`);
180
+ // refs=ループ変数の袋(program と同じ。予約キー input は run の起動入力)
181
+ const refEntries = [
182
+ ` ${propertyKey(REFS_INPUT_KEY)}: unknown`,
183
+ ...Object.entries(manifest.refs).map(([key, schema]) => ` ${propertyKey(key)}: ${tsTypeFromJsonSchema(schema, 1, "FileRef")}`),
184
+ ];
185
+ const usesFileRef = [inputType, ...outputEntries, ...refEntries].some((t) => t.includes("FileRef"));
186
+ return [
187
+ "// このファイルは mawaru typegen が config.json から自動生成した(手で編集しない)",
188
+ `${CONFIG_HASH_PREFIX}${configHash(rawConfig)}`,
189
+ "",
190
+ ...(usesFileRef
191
+ ? ['import type { FileRef } from "@mawaru/sdk/human"', ""]
192
+ : []),
193
+ `export type Input = ${inputType}`,
194
+ "",
195
+ `export type Outputs = {\n${outputEntries.join("\n")}\n}`,
196
+ "",
197
+ "// out に渡す値:ちょうど1つの出口ポートのキーで渡す { <出口キー>: 値 }",
198
+ outputUnionType(Object.keys(manifest.outputs)),
199
+ "",
200
+ `export type Refs = {\n${refEntries.join("\n")}\n}`,
201
+ "",
202
+ ].join("\n");
203
+ };
204
+ // nodes/human/ 直下の各 human ノードの実装の config.json から types.d.ts を生成する(program と同じ走査)
205
+ const runHumanTypegen = (root) => {
206
+ const viewsDir = join(root, "nodes", "human");
207
+ const written = [];
208
+ const errors = [];
209
+ if (!existsSync(viewsDir))
210
+ return { written, errors };
211
+ for (const entry of readdirSync(viewsDir, { withFileTypes: true })) {
212
+ if (!entry.isDirectory())
213
+ continue;
214
+ const dir = `nodes/human/${entry.name}`;
215
+ const configPath = join(viewsDir, entry.name, "config.json");
216
+ if (!existsSync(configPath)) {
217
+ errors.push({ dir, message: "config.json がありません" });
218
+ continue;
219
+ }
220
+ const rawConfig = readFileSync(configPath, "utf8");
221
+ let parsed;
222
+ try {
223
+ parsed = JSON.parse(rawConfig);
224
+ }
225
+ catch {
226
+ errors.push({ dir, message: "config.json が JSON として不正です" });
227
+ continue;
228
+ }
229
+ const config = humanManifestSchema.safeParse(parsed);
230
+ if (!config.success) {
231
+ errors.push({
232
+ dir,
233
+ message: `config.json が規約に合いません(${config.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(" / ")})`,
234
+ });
235
+ continue;
236
+ }
237
+ const typesPath = join(viewsDir, entry.name, "types.d.ts");
238
+ writeFileSync(typesPath, renderHumanTypesFile(config.data, rawConfig));
239
+ written.push(typesPath);
240
+ }
241
+ return { written, errors };
242
+ };
243
+ // nodes/program/ 直下の各 dir と、nodes/human/ 直下の各実装の config.json から types.d.ts を生成する
170
244
  export const runTypegen = (root) => {
245
+ const programs = runProgramTypegen(root);
246
+ const views = runHumanTypegen(root);
247
+ return {
248
+ written: [...programs.written, ...views.written],
249
+ errors: [...programs.errors, ...views.errors],
250
+ };
251
+ };
252
+ const runProgramTypegen = (root) => {
171
253
  const programsDir = join(root, "nodes", "program");
172
254
  if (!existsSync(programsDir)) {
173
255
  return {
package/dist/validate.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { convertJsonSchemaToZod } from "zod-from-json-schema";
5
+ import { humanManifestSchema } from "./_schemas/human-manifest.js";
5
6
  import { programManifestSchema } from "./_schemas/program-manifest.js";
6
7
  import { CONFIG_HASH_PREFIX, configHash } from "./typegen.js";
7
8
  // エディタ・オーケストレータは zod-from-json-schema(common の zodFromJsonSchema)で
@@ -101,6 +102,61 @@ const validateProgram = (programsDir, dir, root) => {
101
102
  checkTypegenDrift(programDir, rawConfig, path, issues);
102
103
  return issues;
103
104
  };
105
+ // human ノードの実装(nodes/human/<dir>/):config.json・index.tsx・types.d.ts の drift(program と同じ検査)
106
+ const validateHumans = (root) => {
107
+ const issues = [];
108
+ const viewsDir = join(root, "nodes", "human");
109
+ if (!existsSync(viewsDir))
110
+ return issues;
111
+ for (const entry of readdirSync(viewsDir, { withFileTypes: true })) {
112
+ if (!entry.isDirectory())
113
+ continue;
114
+ const viewDir = join(viewsDir, entry.name);
115
+ const path = relative(root, viewDir);
116
+ if (!existsSync(join(viewDir, "index.tsx"))) {
117
+ issues.push({ level: "error", path, message: "index.tsx がありません" });
118
+ }
119
+ const configPath = join(viewDir, "config.json");
120
+ if (!existsSync(configPath)) {
121
+ issues.push({ level: "error", path, message: "config.json がありません" });
122
+ continue;
123
+ }
124
+ const rawConfig = readFileSync(configPath, "utf8");
125
+ let parsed;
126
+ try {
127
+ parsed = JSON.parse(rawConfig);
128
+ }
129
+ catch {
130
+ issues.push({
131
+ level: "error",
132
+ path: `${path}/config.json`,
133
+ message: "JSON として不正です",
134
+ });
135
+ continue;
136
+ }
137
+ const config = humanManifestSchema.safeParse(parsed);
138
+ if (!config.success) {
139
+ for (const issue of config.error.issues) {
140
+ const at = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
141
+ issues.push({
142
+ level: "error",
143
+ path: `${path}/config.json`,
144
+ message: `${at}${issue.message}`,
145
+ });
146
+ }
147
+ continue;
148
+ }
149
+ checkPortSchemas(Object.fromEntries(Object.entries(config.data.inputs).map(([key, value]) => [
150
+ key,
151
+ value.schema,
152
+ ])), "inputs", `${path}/config.json`, issues);
153
+ checkPortSchemas(config.data.outputs, "outputs", `${path}/config.json`, issues);
154
+ if (existsSync(join(viewDir, "types.d.ts"))) {
155
+ checkTypegenDrift(viewDir, rawConfig, path, issues);
156
+ }
157
+ }
158
+ return issues;
159
+ };
104
160
  // リポジトリ規約の構造検証。error が1つでもあれば CI を落とす想定(cli.ts が exit 1)
105
161
  export const validateRepo = (root) => {
106
162
  const issues = [];
@@ -119,6 +175,7 @@ export const validateRepo = (root) => {
119
175
  }
120
176
  }
121
177
  }
178
+ issues.push(...validateHumans(root));
122
179
  if (!existsSync(join(root, ".github", "workflows", "mawaru-runner.yml"))) {
123
180
  issues.push({
124
181
  level: "warning",
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FileRef } from "./_parts/fileRef.js";
3
+ export type ViewBridge = {
4
+ fileUrl: (file: FileRef) => Promise<string>;
5
+ uploadFile: (file: File) => Promise<FileRef>;
6
+ copyText: (text: string) => Promise<void>;
7
+ };
8
+ export declare const ViewBridgeProvider: ({ bridge, children, }: {
9
+ bridge: ViewBridge;
10
+ children: ReactNode;
11
+ }) => import("react").JSX.Element;
12
+ export declare const useViewBridge: () => ViewBridge;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const ViewBridgeContext = createContext(null);
4
+ export const ViewBridgeProvider = ({ bridge, children, }) => (_jsx(ViewBridgeContext.Provider, { value: bridge, children: children }));
5
+ export const useViewBridge = () => {
6
+ const bridge = useContext(ViewBridgeContext);
7
+ if (!bridge)
8
+ throw new Error("ViewBridgeProvider の中で使ってください");
9
+ return bridge;
10
+ };
@@ -0,0 +1,10 @@
1
+ import type { FileRef } from "./_parts/fileRef.js";
2
+ import type { HumanProps } from "./human.js";
3
+ import type { HumanViewData } from "./protocol.js";
4
+ export type HumanOps = {
5
+ out: (output: Record<string, unknown>) => Promise<void>;
6
+ fetchFileUrl: (file: FileRef) => Promise<string>;
7
+ uploadFile: (file: File) => Promise<FileRef>;
8
+ writeText: (text: string) => Promise<void>;
9
+ };
10
+ export declare const humanPropsOf: (data: HumanViewData, ops: HumanOps) => HumanProps;
@@ -0,0 +1,11 @@
1
+ export const humanPropsOf = (data, ops) => ({
2
+ input: data.input,
3
+ out: ops.out,
4
+ ctx: {
5
+ input: { schema: data.inputSchema, fetchFileUrl: ops.fetchFileUrl },
6
+ output: { schema: data.outputSchema, uploadFile: ops.uploadFile },
7
+ clipboard: { writeText: ops.writeText },
8
+ refs: (data.refs ?? {}),
9
+ step: { isAssignee: data.isAssignee, editable: data.editable },
10
+ },
11
+ });