@mawaru/sdk 0.38.0 → 0.40.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 (91) hide show
  1. package/_schemas/ai-manifest.test.ts +8 -0
  2. package/_schemas/ai-manifest.ts +2 -0
  3. package/_schemas/graph.test.ts +51 -9
  4. package/_schemas/graph.ts +23 -8
  5. package/_schemas/human-manifest.test.ts +129 -0
  6. package/_schemas/human-manifest.ts +158 -0
  7. package/_schemas/node.ts +4 -0
  8. package/_schemas/port-spec.test.ts +164 -40
  9. package/_schemas/port-spec.ts +77 -26
  10. package/dist/_cli/login.js +1 -15
  11. package/dist/_cli/openBrowser.d.ts +1 -0
  12. package/dist/_cli/openBrowser.js +15 -0
  13. package/dist/_cli/previewApi.d.ts +1 -1
  14. package/dist/_cli/previewCommand.d.ts +8 -0
  15. package/dist/_cli/previewCommand.js +95 -37
  16. package/dist/_cli/previewFiles.d.ts +2 -1
  17. package/dist/_cli/previewFiles.js +42 -19
  18. package/dist/_cli/previewSlack.d.ts +1 -1
  19. package/dist/_cli/previewSlack.js +1 -1
  20. package/dist/_schemas/ai-manifest.d.ts +3 -0
  21. package/dist/_schemas/ai-manifest.js +2 -0
  22. package/dist/_schemas/graph.d.ts +23 -3
  23. package/dist/_schemas/graph.js +20 -8
  24. package/dist/_schemas/human-manifest.d.ts +27 -0
  25. package/dist/_schemas/human-manifest.js +121 -0
  26. package/dist/_schemas/node.d.ts +1 -0
  27. package/dist/_schemas/node.js +4 -0
  28. package/dist/_schemas/port-spec.d.ts +8 -9
  29. package/dist/_schemas/port-spec.js +64 -9
  30. package/dist/cli.js +3 -2
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/init.js +22 -3
  34. package/dist/typegen.d.ts +3 -1
  35. package/dist/typegen.js +96 -14
  36. package/dist/validate.js +55 -0
  37. package/dist/views/_approval/ApprovalBar.d.ts +20 -0
  38. package/dist/views/_approval/ApprovalBar.js +74 -0
  39. package/dist/views/_approvalActions.d.ts +18 -0
  40. package/dist/views/_approvalActions.js +10 -0
  41. package/dist/views/_bridge.d.ts +12 -0
  42. package/dist/views/_bridge.js +10 -0
  43. package/dist/views/_parts/DocumentCard.d.ts +2 -0
  44. package/dist/views/_parts/DocumentCard.js +16 -0
  45. package/dist/views/_parts/FilePreview.d.ts +5 -0
  46. package/dist/views/_parts/FilePreview.js +101 -0
  47. package/dist/views/_parts/Linkify.d.ts +7 -0
  48. package/dist/views/_parts/Linkify.js +28 -0
  49. package/dist/views/_parts/Markdown.d.ts +44 -0
  50. package/dist/views/_parts/Markdown.js +313 -0
  51. package/dist/views/_parts/SchemaForm.d.ts +19 -0
  52. package/dist/views/_parts/SchemaForm.js +310 -0
  53. package/dist/views/_parts/fileRef.d.ts +7 -0
  54. package/dist/views/_parts/fileRef.js +11 -0
  55. package/dist/views/_parts/useNarrow.d.ts +5 -0
  56. package/dist/views/_parts/useNarrow.js +23 -0
  57. package/dist/views/host.d.ts +18 -0
  58. package/dist/views/host.js +20 -0
  59. package/dist/views/human.d.ts +73 -0
  60. package/dist/views/human.js +6 -0
  61. package/dist/views/index.d.ts +8 -0
  62. package/dist/views/index.js +12 -0
  63. package/dist/views/protocol.d.ts +34 -0
  64. package/dist/views/protocol.js +5 -0
  65. package/dist/views/runtime.d.ts +3 -0
  66. package/dist/views/runtime.js +86 -0
  67. package/dist/{slack-view.d.ts → views/slack.d.ts} +1 -1
  68. package/docs/development.md +45 -5
  69. package/index.ts +1 -0
  70. package/package.json +36 -4
  71. package/skills/create-loop/SKILL.md +6 -3
  72. package/views/_approval/ApprovalBar.tsx +239 -0
  73. package/views/_approvalActions.tsx +31 -0
  74. package/views/_bridge.tsx +36 -0
  75. package/views/_parts/DocumentCard.ts +18 -0
  76. package/views/_parts/FilePreview.tsx +180 -0
  77. package/views/_parts/Linkify.test.ts +40 -0
  78. package/views/_parts/Linkify.tsx +40 -0
  79. package/views/_parts/Markdown.test.ts +131 -0
  80. package/views/_parts/Markdown.tsx +472 -0
  81. package/views/_parts/SchemaForm.tsx +742 -0
  82. package/views/_parts/fileRef.ts +21 -0
  83. package/views/_parts/useNarrow.ts +24 -0
  84. package/views/host.ts +23 -0
  85. package/views/human.ts +91 -0
  86. package/views/index.ts +16 -0
  87. package/views/protocol.ts +40 -0
  88. package/views/runtime.tsx +132 -0
  89. package/views/slack.test.ts +40 -0
  90. package/{slack-view.ts → views/slack.ts} +3 -3
  91. /package/dist/{slack-view.js → views/slack.js} +0 -0
@@ -1,21 +1,23 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, watch } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import { requireCredentials } from "./credentials.js";
5
+ import { openBrowser } from "./openBrowser.js";
5
6
  import { expectOk, mawaruApiFor } from "./previewApi.js";
6
- import { readViewFiles, resolveView } from "./previewFiles.js";
7
+ import { kindRoot, readViewFiles, resolveView, } from "./previewFiles.js";
7
8
  import { previewSlack, SLACK_SURFACES, } from "./previewSlack.js";
8
- // `mawaru preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|ID>]`
9
+ // `mawaru preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|ID>] [--once]`
9
10
  // (実行 repo のルートで実行)
10
11
  //
11
12
  // views/<種類>/<名前>/ のビューを、push せずに手元のファイルのまま確かめる。描画は mawaru API がやるので、
12
13
  // `mawaru login` のセッション(web と同じ membership)と、どのテナントのつもりで描くかの
13
14
  // MAWARU_TENANT_ID(repo の .env。create-loop スキルと同じ)が要る。
14
- // ファイルの集め方・cases.ts の評価・ケースの絞り込みは種類をまたいで共通で、種類ごとに「確かめ方」
15
- // (Slack は送る。承認ビューは repo へ移すときに足す)だけを差し替える
16
- // (docs/tasks/wip/Slackビューの顧客repo配置.md の「手元プレビュー」)
17
- const USAGE = "使い方: npx mawaru preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|チャンネル ID>]\n" +
18
- " 例: npx mawaru preview slack/pr-review(views/slack/pr-review/ の cases.ts の全ケースを Slack に送る)";
15
+ // ファイルの集め方・認証は種類をまたいで共通で、種類ごとに「確かめ方」だけを差し替える
16
+ // (Slack は cases.ts の全ケースを Slack に送る。承認ビューは本番と同じ承認画面で開き、保存するたびに送り直す)
17
+ // (docs/tasks/done/Slackビューの顧客repo配置.md・docs/tasks/wip/承認ビューの顧客repo配置.md の「手元プレビュー」)
18
+ const USAGE = "使い方: npx mawaru preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|チャンネル ID>] [--once]\n" +
19
+ " 例: npx mawaru preview slack/pr-review(views/slack/pr-review/ の cases.ts の全ケースを Slack に送る)\n" +
20
+ " npx mawaru preview human/invoice-review(承認画面で開き、保存するたびに送り直す。--once は1回送って終わる)";
19
21
  export const parsePreviewArgs = (args) => {
20
22
  const { values, positionals } = parseArgs({
21
23
  args,
@@ -24,6 +26,7 @@ export const parsePreviewArgs = (args) => {
24
26
  case: { type: "string" },
25
27
  surface: { type: "string" },
26
28
  channel: { type: "string" },
29
+ once: { type: "boolean" },
27
30
  },
28
31
  });
29
32
  const [target, ...extra] = positionals;
@@ -40,35 +43,12 @@ export const parsePreviewArgs = (args) => {
40
43
  ? { surface: values.surface }
41
44
  : {}),
42
45
  ...(values.channel !== undefined ? { channel: values.channel } : {}),
46
+ ...(values.once ? { once: true } : {}),
43
47
  };
44
48
  };
45
- const PREVIEWERS = {
46
- slack: {
47
- casesPath: "/slack-views/cases",
48
- run: ({ name, files, cases, options, api, log }) => {
49
- const channel = options.channel ?? process.env.MAWARU_PREVIEW_CHANNEL;
50
- if (!channel) {
51
- throw new Error("送り先のチャンネルを --channel か repo の .env の MAWARU_PREVIEW_CHANNEL で指定してください(#名前 かチャンネル ID)");
52
- }
53
- return previewSlack({
54
- name,
55
- files,
56
- cases: cases,
57
- surfaces: options.surface ? [options.surface] : SLACK_SURFACES,
58
- channel,
59
- api,
60
- log,
61
- });
62
- },
63
- },
64
- };
65
- // 終了コードを返す(描画に失敗したものがあれば 1)。テストはここを呼ぶ
66
- export const runPreviewWith = async (params) => {
67
- const { root, options, api, log } = params;
68
- const view = resolveView(root, options.target);
69
- const files = readViewFiles(root, view);
70
- const previewer = PREVIEWERS[view.kind];
71
- const { cases } = expectOk("ケースの読み込み", await api("POST", previewer.casesPath, { files }));
49
+ // Slack ビュー:cases.ts を mawaru API で評価し、絞り込んだケース×面を Slack に送る
50
+ const previewSlackView = async ({ view, files, options, api, log, }) => {
51
+ const { cases } = expectOk("ケースの読み込み", await api("POST", "/views/slack/cases", { files }));
72
52
  const picked = options.case === undefined
73
53
  ? cases
74
54
  : cases.filter((c) => c.name === options.case);
@@ -77,15 +57,92 @@ export const runPreviewWith = async (params) => {
77
57
  ? "cases.ts にケースがありません"
78
58
  : `ケース「${options.case}」がありません(ある: ${cases.map((c) => c.name).join(", ")})`);
79
59
  }
80
- const failed = await previewer.run({
60
+ const channel = options.channel ?? process.env.MAWARU_PREVIEW_CHANNEL;
61
+ if (!channel) {
62
+ throw new Error("送り先のチャンネルを --channel か repo の .env の MAWARU_PREVIEW_CHANNEL で指定してください(#名前 かチャンネル ID)");
63
+ }
64
+ return previewSlack({
81
65
  name: view.name,
82
66
  files,
83
67
  cases: picked,
68
+ surfaces: options.surface ? [options.surface] : SLACK_SURFACES,
69
+ channel,
70
+ api,
71
+ log,
72
+ });
73
+ };
74
+ // human ノードの実装(承認画面):ファイルを mawaru に置いて、本番と同じ承認画面(sandbox iframe)で開く。
75
+ // cases.ts の評価は画面を開いたときに mawaru がやる。保存するたびに置き直し、開いている画面が読み直す
76
+ const previewHuman = async ({ root, view, files, options, api, log, env, }) => {
77
+ const created = expectOk("プレビューの用意", await api("POST", "/previews", {
78
+ kind: "human",
79
+ name: view.name,
80
+ files,
81
+ }));
82
+ if (created.error) {
83
+ log(`✘ human/${view.name} をまとめられません\n${created.error}`);
84
+ return 1;
85
+ }
86
+ const url = `${env.appUrl.replace(/\/$/, "")}/previews/${created.id}?tenant=${env.tenantId}`;
87
+ log(`✔ human/${view.name} を開きます: ${url}`);
88
+ env.openUrl(url);
89
+ if (options.once)
90
+ return 0;
91
+ log("保存するたびに送り直します(Ctrl-C で終わる)");
92
+ await env.watch(kindRoot(root, "human"), async () => {
93
+ const res = await api("PUT", `/previews/${created.id}`, {
94
+ files: readViewFiles(root, view),
95
+ });
96
+ if (res.status >= 300) {
97
+ log(`✘ 送り直せませんでした(${res.status})`);
98
+ return;
99
+ }
100
+ const { error } = res.body;
101
+ log(error ? `✘ まとめられません\n${error}` : "✔ 送り直しました");
102
+ });
103
+ return 0;
104
+ };
105
+ const PREVIEWERS = {
106
+ slack: previewSlackView,
107
+ human: previewHuman,
108
+ };
109
+ // 保存の監視:まとめて保存されることがあるので少し待ってから1回送る
110
+ const watchFiles = (dir, onChange) => new Promise((resolve) => {
111
+ let timer;
112
+ let running = Promise.resolve();
113
+ const watcher = watch(dir, { recursive: true }, () => {
114
+ clearTimeout(timer);
115
+ timer = setTimeout(() => {
116
+ running = running.then(onChange);
117
+ }, 300);
118
+ });
119
+ process.once("SIGINT", () => {
120
+ watcher.close();
121
+ resolve();
122
+ });
123
+ });
124
+ // 終了コードを返す(描画・まとめに失敗したものがあれば 1)。テストはここを呼ぶ
125
+ export const runPreviewWith = async (params) => {
126
+ const { root, options, api, log } = params;
127
+ const view = resolveView(root, options.target);
128
+ const files = readViewFiles(root, view);
129
+ const failed = await PREVIEWERS[view.kind]({
130
+ root,
131
+ view,
132
+ files,
84
133
  options,
85
134
  api,
86
135
  log,
136
+ env: {
137
+ tenantId: params.env?.tenantId ?? "",
138
+ appUrl: params.env?.appUrl ??
139
+ process.env.MAWARU_APP_URL ??
140
+ "https://app.mawaru.ai",
141
+ openUrl: params.env?.openUrl ?? openBrowser,
142
+ watch: params.env?.watch ?? watchFiles,
143
+ },
87
144
  });
88
- if (failed > 0)
145
+ if (view.kind === "slack" && failed > 0)
89
146
  log(`失敗 ${failed} 件`);
90
147
  return failed > 0 ? 1 : 0;
91
148
  };
@@ -105,5 +162,6 @@ export const runPreview = async (args) => {
105
162
  options,
106
163
  api: mawaruApiFor(credentials, tenantId),
107
164
  log: (line) => console.log(line),
165
+ env: { tenantId },
108
166
  });
109
167
  };
@@ -1,5 +1,6 @@
1
- export declare const VIEW_KINDS: readonly ["slack"];
1
+ export declare const VIEW_KINDS: readonly ["slack", "human"];
2
2
  export type ViewKind = (typeof VIEW_KINDS)[number];
3
+ export declare const kindRoot: (root: string, kind: ViewKind) => string;
3
4
  export type ViewTarget = {
4
5
  kind: ViewKind;
5
6
  name: string;
@@ -1,11 +1,16 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- // 手元プレビュー(`mawaru preview`)が読む、実行 repo の views/<種類>/<名前>/ のファイル。
4
- // 描画は mawaru API がやるので、ここは「対象を決めてファイルを集める」だけで、種類をまたいで共通
5
- // (docs/tasks/wip/Slackビューの顧客repo配置.md の「手元プレビュー」)
6
- // 手元プレビューできるビューの種類(views/ 直下のディレクトリ名)。承認ビューを repo へ移すときに
7
- // "approval" を足す(previewCommand.ts の振り分けにも足す)
8
- export const VIEW_KINDS = ["slack"];
3
+ // 手元プレビュー(`mawaru preview`)が読む、実行 repo のファイル:Slack ビューは views/slack/<名前>/、
4
+ // human ノードの実装(承認画面)は nodes/human/<名前>/。描画は mawaru API がやるので、ここは
5
+ // 「対象を決めてファイルを集める」だけで、種類をまたいで共通
6
+ // (docs/tasks/wip/Slackビューの顧客repo配置.md・承認ビューの顧客repo配置.md の「手元プレビュー」)
7
+ // 手元プレビューできる種類(previewCommand.ts の振り分けと揃える)と、その置き場
8
+ export const VIEW_KINDS = ["slack", "human"];
9
+ const KIND_ROOTS = {
10
+ slack: ["views", "slack"],
11
+ human: ["nodes", "human"],
12
+ };
13
+ export const kindRoot = (root, kind) => join(root, ...KIND_ROOTS[kind]);
9
14
  const dirsIn = (dir) => {
10
15
  try {
11
16
  return readdirSync(dir, { withFileTypes: true })
@@ -17,15 +22,17 @@ const dirsIn = (dir) => {
17
22
  return [];
18
23
  }
19
24
  };
20
- // views/<種類>/ 直下のディレクトリ(=ビュー)。無ければ空
21
- export const listViews = (root) => VIEW_KINDS.flatMap((kind) => dirsIn(join(root, "views", kind)).map((name) => ({ kind, name })));
25
+ // 種類ごとの置き場の直下のディレクトリ(`_` 始まりは部品の置き場で対象ではない)。無ければ空
26
+ export const listViews = (root) => VIEW_KINDS.flatMap((kind) => dirsIn(kindRoot(root, kind))
27
+ .filter((name) => !name.startsWith("_"))
28
+ .map((name) => ({ kind, name })));
22
29
  const labelOf = (view) => `${view.kind}/${view.name}`;
23
- // 指定(views/slack/pr・slack/pr・pr)をビューに決める。一覧にあるものだけを受ける
24
- // (../ でディレクトリの外を読ませない)。決まらなければ、あるビューを添えて失敗する
30
+ // 指定(views/slack/pr・slack/pr・nodes/human/review・human/review・pr)を対象に決める。一覧にあるものだけを受ける
31
+ // (../ でディレクトリの外を読ませない)。決まらなければ、ある対象を添えて失敗する
25
32
  export const resolveView = (root, spec) => {
26
33
  const normalized = spec
27
34
  .replace(/^\.\//, "")
28
- .replace(/^views\//, "")
35
+ .replace(/^(views|nodes)\//, "")
29
36
  .replace(/\/+$/, "");
30
37
  const views = listViews(root);
31
38
  const found = normalized.includes("/")
@@ -34,17 +41,14 @@ export const resolveView = (root, spec) => {
34
41
  if (found.length === 1 && found[0])
35
42
  return found[0];
36
43
  const known = views.length === 0
37
- ? "views/ にビューがありません"
44
+ ? "views/slack/・nodes/human/ に対象がありません"
38
45
  : `ある対象: ${views.map(labelOf).join(", ")}`;
39
46
  throw new Error(found.length > 1
40
47
  ? `${spec} は複数の種類にあります。<種類>/<名前> で指定してください(${found.map(labelOf).join(", ")})`
41
48
  : `${spec} が見つかりません(${known})`);
42
49
  };
43
- // mawaru API へ送る files({ ビューのディレクトリからの相対パス(/ 区切り): 中身 })。
44
- // .ts だけをサブディレクトリまで集める(上限の検証は API 側)
45
- export const readViewFiles = (root, view) => {
46
- const dir = join(root, "views", view.kind, view.name);
47
- const files = {};
50
+ // dir の下の、拡張子が extensions のファイル(サブディレクトリも)。キーは prefix からの相対パス
51
+ const collect = (dir, prefix, extensions, files) => {
48
52
  const walk = (relative) => {
49
53
  for (const entry of readdirSync(join(dir, relative), {
50
54
  withFileTypes: true,
@@ -54,11 +58,30 @@ export const readViewFiles = (root, view) => {
54
58
  const path = relative ? `${relative}/${entry.name}` : entry.name;
55
59
  if (entry.isDirectory())
56
60
  walk(path);
57
- else if (entry.isFile() && entry.name.endsWith(".ts")) {
58
- files[path] = readFileSync(join(dir, path), "utf8");
61
+ else if (entry.isFile() &&
62
+ extensions.some((ext) => entry.name.endsWith(ext))) {
63
+ files[`${prefix}${path}`] = readFileSync(join(dir, path), "utf8");
59
64
  }
60
65
  }
61
66
  };
62
67
  walk("");
68
+ };
69
+ // mawaru API へ送る files(上限の検証は API 側)。
70
+ // - Slack ビュー:ビューのディレクトリの .ts(キーはビューのディレクトリからの相対パス)
71
+ // - human ノードの実装:nodes/human/<名前>/ と nodes/human/_<部品>/ の .ts / .tsx(キーは nodes/human/ からの相対パス。
72
+ // 本番と同じく、部品は複数の human ノードで共有できる)
73
+ export const readViewFiles = (root, view) => {
74
+ const kindDir = kindRoot(root, view.kind);
75
+ const files = {};
76
+ if (view.kind === "slack") {
77
+ collect(join(kindDir, view.name), "", [".ts"], files);
78
+ return files;
79
+ }
80
+ for (const dir of [
81
+ view.name,
82
+ ...dirsIn(kindDir).filter((name) => name.startsWith("_")),
83
+ ]) {
84
+ collect(join(kindDir, dir), `${dir}/`, [".ts", ".tsx"], files);
85
+ }
63
86
  return files;
64
87
  };
@@ -1,4 +1,4 @@
1
- import type { SlackViewCase } from "../slack-view.js";
1
+ import type { SlackViewCase } from "../views/slack.js";
2
2
  import { type MawaruApi } from "./previewApi.js";
3
3
  export type SlackSurface = "running" | "completed";
4
4
  export declare const SLACK_SURFACES: SlackSurface[];
@@ -42,7 +42,7 @@ export const previewSlack = async (params) => {
42
42
  let failed = 0;
43
43
  for (const testCase of cases) {
44
44
  for (const surface of surfaces) {
45
- const result = expectOk("Slack への送信", await api("POST", "/slack-views/test-post", {
45
+ const result = expectOk("Slack への送信", await api("POST", "/views/slack/test-post", {
46
46
  ...previewBodyOf(files, testCase, surface),
47
47
  channel_id: channelId,
48
48
  ...(thread
@@ -49,6 +49,7 @@ export declare const agentOfModel: (model: AiModel) => AiAgentId;
49
49
  export declare const aiInputDeclSchema: z.ZodObject<{
50
50
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
51
51
  resume: z.ZodOptional<z.ZodBoolean>;
52
+ flexible: z.ZodOptional<z.ZodBoolean>;
52
53
  }, z.core.$strip>;
53
54
  export type AiInputDecl = z.infer<typeof aiInputDeclSchema>;
54
55
  export declare const aiManifestSchema: z.ZodObject<{
@@ -87,9 +88,11 @@ export declare const aiManifestSchema: z.ZodObject<{
87
88
  inputs: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
88
89
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
89
90
  resume: z.ZodOptional<z.ZodBoolean>;
91
+ flexible: z.ZodOptional<z.ZodBoolean>;
90
92
  }, z.core.$strip>, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<{
91
93
  schema: Record<string, unknown>;
92
94
  resume?: boolean | undefined;
95
+ flexible?: boolean | undefined;
93
96
  }, Record<string, unknown>>>]>>;
94
97
  outputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
95
98
  }, z.core.$strip>;
@@ -65,6 +65,8 @@ export const agentOfModel = (model) => model.startsWith("claude-") ? "claude" :
65
65
  export const aiInputDeclSchema = z.object({
66
66
  schema: jsonSchemaSchema,
67
67
  resume: z.boolean().optional(),
68
+ // 推奨型を維持して任意の型を受ける(接続で上書きされず、適合チェックもしない)
69
+ flexible: z.boolean().optional(),
68
70
  });
69
71
  // 旧形式(値が素の JSON Schema)は { schema: 値 } として読む。判別は「schema キーを
70
72
  // 持つか」:JSON Schema のトップレベルに "schema" キーは現れない(メタスキーマ参照は
@@ -51,7 +51,13 @@ export declare const RUN_STARTER = "run_starter";
51
51
  export declare const assignMemberSchema: z.ZodUnion<readonly [z.ZodUUID, z.ZodLiteral<"run_starter">]>;
52
52
  export type AssignMember = z.infer<typeof assignMemberSchema>;
53
53
  export declare const humanConfigSchema: z.ZodObject<{
54
- view: z.ZodOptional<z.ZodString>;
54
+ human_dir: z.ZodOptional<z.ZodString>;
55
+ ref: z.ZodOptional<z.ZodString>;
56
+ declaration: z.ZodOptional<z.ZodObject<{
57
+ in: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
58
+ approve: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
59
+ reject: z.ZodRecord<z.ZodString, z.ZodUnknown>;
60
+ }, z.core.$strip>>;
55
61
  assignment: z.ZodObject<{
56
62
  strategy: z.ZodDefault<z.ZodEnum<{
57
63
  fixed: "fixed";
@@ -152,6 +158,7 @@ export declare const graphNodeSchema: z.ZodObject<{
152
158
  name: z.ZodString;
153
159
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
154
160
  resume: z.ZodOptional<z.ZodBoolean>;
161
+ flexible: z.ZodOptional<z.ZodBoolean>;
155
162
  side: z.ZodOptional<z.ZodEnum<{
156
163
  top: "top";
157
164
  right: "right";
@@ -193,7 +200,13 @@ export declare const graphNodeSchema: z.ZodObject<{
193
200
  ref: z.ZodOptional<z.ZodString>;
194
201
  }, z.core.$strip>>;
195
202
  human: z.ZodOptional<z.ZodObject<{
196
- view: z.ZodOptional<z.ZodString>;
203
+ human_dir: z.ZodOptional<z.ZodString>;
204
+ ref: z.ZodOptional<z.ZodString>;
205
+ declaration: z.ZodOptional<z.ZodObject<{
206
+ in: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
207
+ approve: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
208
+ reject: z.ZodRecord<z.ZodString, z.ZodUnknown>;
209
+ }, z.core.$strip>>;
197
210
  assignment: z.ZodObject<{
198
211
  strategy: z.ZodDefault<z.ZodEnum<{
199
212
  fixed: "fixed";
@@ -252,6 +265,7 @@ export declare const saveGraphBodySchema: z.ZodObject<{
252
265
  name: z.ZodString;
253
266
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
254
267
  resume: z.ZodOptional<z.ZodBoolean>;
268
+ flexible: z.ZodOptional<z.ZodBoolean>;
255
269
  side: z.ZodOptional<z.ZodEnum<{
256
270
  top: "top";
257
271
  right: "right";
@@ -293,7 +307,13 @@ export declare const saveGraphBodySchema: z.ZodObject<{
293
307
  ref: z.ZodOptional<z.ZodString>;
294
308
  }, z.core.$strip>>;
295
309
  human: z.ZodOptional<z.ZodObject<{
296
- view: z.ZodOptional<z.ZodString>;
310
+ human_dir: z.ZodOptional<z.ZodString>;
311
+ ref: z.ZodOptional<z.ZodString>;
312
+ declaration: z.ZodOptional<z.ZodObject<{
313
+ in: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
314
+ approve: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
315
+ reject: z.ZodRecord<z.ZodString, z.ZodUnknown>;
316
+ }, z.core.$strip>>;
297
317
  assignment: z.ZodObject<{
298
318
  strategy: z.ZodDefault<z.ZodEnum<{
299
319
  fixed: "fixed";
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
+ import { humanDeclarationSchema } from "./human-manifest.js";
2
3
  import { graphPortSchema, jsonSchemaSchema, nodeKindSchema, portKeySchema, } from "./node.js";
3
- import { hasInboundConflict, isSchemaSubset, kindPortsViolation, resolvePortSchemas, UNDECLARED_SCHEMA, } from "./port-spec.js";
4
+ import { connectionAccepted, hasInboundConflict, kindPortsViolation, resolvePortSchemas, UNDECLARED_SCHEMA, } from "./port-spec.js";
4
5
  // refs=ループ変数の袋(key→任意の JSON 値。上流出力の参照(refs).md)。
5
6
  // run 開始時に定義から複製され、全 step に全量差し込まれる。
6
7
  // key はポート key と同じ規則(^[a-z][a-z0-9_]*$)。実行系ボディ
@@ -116,10 +117,20 @@ export const assignMemberSchema = z.union([z.uuid(), z.literal(RUN_STARTER)]);
116
117
  // メンバーが tenant の membership であることの検証は保存 API 側で行う
117
118
  // (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
118
119
  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(),
120
+ // 実装(承認画面と入出力の宣言)の置き場所:実行 repo の nodes/human/ 直下のディレクトリ名。
121
+ // program_dir / ai_dir と同じ。省略時は mawaru 内蔵の標準の承認画面
122
+ // (docs/tasks/wip/承認ビューの顧客repo配置.md の「human ノードの実装として置く」)
123
+ human_dir: z
124
+ .string()
125
+ .min(1)
126
+ .refine((v) => !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..", "human_dir は nodes/human/ 直下のディレクトリ名で指定してください")
127
+ .optional(),
128
+ // 読むブランチ(省略時はデフォルトブランチ)。publish でその先頭の commit に固定する
129
+ ref: z.string().min(1).optional(),
130
+ // 宣言の写し(in / approve / reject の型)。伝播はこれを読む。human_dir を選んだときに
131
+ // 一覧 API(GET /tenants/:id/github/humans)の宣言を入れる。省略すると保存時に mawaru が repo の
132
+ // config.json から読む(下書きに同じ human_dir の同じノードがあればその写しを保つ)
133
+ declaration: humanDeclarationSchema.optional(),
123
134
  assignment: z
124
135
  .object({
125
136
  strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
@@ -236,8 +247,8 @@ export const graphNodeSchema = z.object({
236
247
  // 表示アイコン(全 kind 共通の任意設定。human はフロントが担当から導出するので通常設定しない)
237
248
  icon: nodeIconSchema.optional(),
238
249
  // Slack ビュー(全 kind 共通の任意設定。実行中(human は承認待ち)・完了メッセージの見せ方)。
239
- // human の view と同じ「不透明なキー」で、選べる key は GET slack-views が返す。
240
- // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/slack-view の契約で書く)。
250
+ // human の view と同じ「不透明なキー」で、選べる key は GET views/slack が返す。
251
+ // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/views/slack の契約で書く)。
241
252
  // 保存時に mawaru が見るのは common/* の実在と <slug>/* の可視性だけ。
242
253
  // 省略=承認待ちは承認ボタンだけ・完了は ✅ の1行(データは出さない)。
243
254
  // ポートの型には一切影響しない
@@ -473,9 +484,10 @@ export const saveGraphBodySchema = z
473
484
  // 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
474
485
  // ので自明に通り、実質の検証対象は宣言で固定された口
475
486
  // (component の契約・宣言のあるビューの human.in)になる
487
+ // flexible な in(AI の宣言 in・契約で焼き込まれた component の in)は型を問わない
476
488
  const fromSchema = resolved.get(conn.from_port_id) ?? from.schema;
477
489
  const toSchema = resolved.get(conn.to_port_id) ?? to.schema;
478
- if (!isSchemaSubset(fromSchema, toSchema)) {
490
+ if (!connectionAccepted(fromSchema, { ...to, schema: toSchema })) {
479
491
  ctx.addIssue({
480
492
  code: "custom",
481
493
  path: ["connections", i, "to_port_id"],
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { type JsonSchema } from "./node.js";
3
+ import { type DeclaredPortSchema } from "./port-spec.js";
4
+ export type HumanDeclaration = {
5
+ in: DeclaredPortSchema;
6
+ approve: DeclaredPortSchema;
7
+ reject: JsonSchema;
8
+ };
9
+ export declare const humanDeclarationSchema: z.ZodObject<{
10
+ in: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
11
+ approve: z.ZodUnion<readonly [z.ZodLiteral<"T">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
12
+ reject: z.ZodRecord<z.ZodString, z.ZodUnknown>;
13
+ }, z.core.$strip>;
14
+ export declare const STANDARD_HUMAN_DECLARATION: HumanDeclaration;
15
+ export declare const humanManifestSchema: z.ZodObject<{
16
+ inputs: z.ZodOptional<z.ZodObject<{
17
+ in: z.ZodOptional<z.ZodObject<{
18
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
19
+ }, z.core.$strict>>;
20
+ }, z.core.$strict>>;
21
+ outputs: z.ZodOptional<z.ZodObject<{
22
+ approve: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
23
+ reject: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
24
+ }, z.core.$strict>>;
25
+ }, z.core.$strict>;
26
+ export type HumanManifest = z.infer<typeof humanManifestSchema>;
27
+ export declare const humanDeclarationOfManifest: (config: HumanManifest) => HumanDeclaration;
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ import { jsonSchemaSchema } from "./node.js";
3
+ import { COPY_UPSTREAM_SCHEMA, instructionSchema, } from "./port-spec.js";
4
+ // 宣言に書ける type。file は x-mawaru-type マーカー付きの object
5
+ const DECLARED_TYPES = [
6
+ "object",
7
+ "array",
8
+ "string",
9
+ "number",
10
+ "integer",
11
+ "boolean",
12
+ ];
13
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
+ // JSON Schema の形の崩れを場所つきで返す。接続の比較(schemaFit)が扱える形だけを通す:
15
+ // 顧客 repo の宣言は mawaru のレビューを通らないので、壊れた型がノードに写る前に止める
16
+ const schemaIssues = (schema, path) => {
17
+ const issues = [];
18
+ const { type, properties, required, items } = schema;
19
+ if (Array.isArray(type)) {
20
+ issues.push({
21
+ path: [...path, "type"],
22
+ message: "配列(nullable)は使えません。任意の値は required から外してください",
23
+ });
24
+ }
25
+ else if (type !== undefined &&
26
+ !DECLARED_TYPES.includes(type)) {
27
+ issues.push({
28
+ path: [...path, "type"],
29
+ message: `type は ${DECLARED_TYPES.join(" / ")} のどれかにしてください(${String(type)})`,
30
+ });
31
+ }
32
+ else if (type === undefined &&
33
+ (properties !== undefined || items !== undefined)) {
34
+ issues.push({
35
+ path: [...path, "type"],
36
+ message: "properties / items を書くときは type も書いてください",
37
+ });
38
+ }
39
+ if (properties !== undefined) {
40
+ if (!isPlainObject(properties)) {
41
+ issues.push({
42
+ path: [...path, "properties"],
43
+ message: "properties はオブジェクトで書いてください",
44
+ });
45
+ }
46
+ else {
47
+ for (const [key, child] of Object.entries(properties)) {
48
+ if (!isPlainObject(child)) {
49
+ issues.push({
50
+ path: [...path, "properties", key],
51
+ message: "スキーマはオブジェクトで書いてください",
52
+ });
53
+ continue;
54
+ }
55
+ issues.push(...schemaIssues(child, [...path, "properties", key]));
56
+ }
57
+ }
58
+ }
59
+ if (required !== undefined &&
60
+ !(Array.isArray(required) && required.every((r) => typeof r === "string"))) {
61
+ issues.push({
62
+ path: [...path, "required"],
63
+ message: "required は文字列の配列で書いてください",
64
+ });
65
+ }
66
+ if (items !== undefined) {
67
+ if (!isPlainObject(items)) {
68
+ issues.push({
69
+ path: [...path, "items"],
70
+ message: "items はスキーマ(オブジェクト)で書いてください",
71
+ });
72
+ }
73
+ else {
74
+ issues.push(...schemaIssues(items, [...path, "items"]));
75
+ }
76
+ }
77
+ return issues;
78
+ };
79
+ // 宣言に書く1つの型(検査つき)
80
+ const checkedSchema = jsonSchemaSchema.superRefine((schema, ctx) => {
81
+ for (const issue of schemaIssues(schema, []))
82
+ ctx.addIssue({ code: "custom", ...issue });
83
+ });
84
+ const declaredPortSchema = z.union([
85
+ z.literal(COPY_UPSTREAM_SCHEMA),
86
+ checkedSchema,
87
+ ]);
88
+ export const humanDeclarationSchema = z.object({
89
+ in: declaredPortSchema,
90
+ approve: declaredPortSchema,
91
+ reject: checkedSchema,
92
+ });
93
+ // 標準ビュー(common/standard)の宣言:上流の型をそのまま受けて出し、差し戻しは標準の指示フォーム
94
+ export const STANDARD_HUMAN_DECLARATION = {
95
+ in: COPY_UPSTREAM_SCHEMA,
96
+ approve: COPY_UPSTREAM_SCHEMA,
97
+ reject: instructionSchema(),
98
+ };
99
+ // ---- 顧客 repo の nodes/human/<dir>/config.json ----
100
+ // 形は AI の config.json に揃える(inputs の値は { schema }、outputs の値は素の JSON Schema)。
101
+ // 口は in / approve / reject 固定。省略した口は STANDARD_HUMAN_DECLARATION と同じ扱い
102
+ export const humanManifestSchema = z
103
+ .object({
104
+ inputs: z
105
+ .object({ in: z.object({ schema: checkedSchema }).strict().optional() })
106
+ .strict()
107
+ .optional(),
108
+ outputs: z
109
+ .object({
110
+ approve: checkedSchema.optional(),
111
+ reject: checkedSchema.optional(),
112
+ })
113
+ .strict()
114
+ .optional(),
115
+ })
116
+ .strict();
117
+ export const humanDeclarationOfManifest = (config) => ({
118
+ in: config.inputs?.in?.schema ?? COPY_UPSTREAM_SCHEMA,
119
+ approve: config.outputs?.approve ?? COPY_UPSTREAM_SCHEMA,
120
+ reject: config.outputs?.reject ?? instructionSchema(),
121
+ });
@@ -39,6 +39,7 @@ export declare const graphPortSchema: z.ZodObject<{
39
39
  name: z.ZodString;
40
40
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
41
41
  resume: z.ZodOptional<z.ZodBoolean>;
42
+ flexible: z.ZodOptional<z.ZodBoolean>;
42
43
  side: z.ZodOptional<z.ZodEnum<{
43
44
  top: "top";
44
45
  right: "right";
@@ -43,6 +43,10 @@ export const graphPortSchema = z
43
43
  // 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
44
44
  // ポート型固定の撤去とセッション再開のポート属性化.md)
45
45
  resume: z.boolean().optional(),
46
+ // in ポートの型属性:true なら宣言(推奨型)を維持し、接続元の型による上書き・
47
+ // 適合チェックをしない(AI の in で設定。start / component へは導出で流れる。
48
+ // AIノード入力のflexible属性(任意の型で呼べるループ).md)
49
+ flexible: z.boolean().optional(),
46
50
  side: portSideSchema.optional(),
47
51
  offset: z.number().min(0).max(1).optional(),
48
52
  })