@mawaru/sdk 0.39.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 (84) hide show
  1. package/_schemas/graph.test.ts +15 -9
  2. package/_schemas/graph.ts +20 -6
  3. package/_schemas/human-manifest.test.ts +129 -0
  4. package/_schemas/human-manifest.ts +158 -0
  5. package/_schemas/port-spec.test.ts +40 -40
  6. package/_schemas/port-spec.ts +10 -24
  7. package/dist/_cli/login.js +1 -15
  8. package/dist/_cli/openBrowser.d.ts +1 -0
  9. package/dist/_cli/openBrowser.js +15 -0
  10. package/dist/_cli/previewApi.d.ts +1 -1
  11. package/dist/_cli/previewCommand.d.ts +8 -0
  12. package/dist/_cli/previewCommand.js +95 -37
  13. package/dist/_cli/previewFiles.d.ts +2 -1
  14. package/dist/_cli/previewFiles.js +42 -19
  15. package/dist/_cli/previewSlack.d.ts +1 -1
  16. package/dist/_cli/previewSlack.js +1 -1
  17. package/dist/_schemas/graph.d.ts +21 -3
  18. package/dist/_schemas/graph.js +17 -6
  19. package/dist/_schemas/human-manifest.d.ts +27 -0
  20. package/dist/_schemas/human-manifest.js +121 -0
  21. package/dist/_schemas/port-spec.d.ts +6 -9
  22. package/dist/_schemas/port-spec.js +7 -7
  23. package/dist/cli.js +3 -2
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/init.js +22 -3
  27. package/dist/typegen.d.ts +3 -1
  28. package/dist/typegen.js +96 -14
  29. package/dist/validate.js +55 -0
  30. package/dist/views/_approval/ApprovalBar.d.ts +20 -0
  31. package/dist/views/_approval/ApprovalBar.js +74 -0
  32. package/dist/views/_approvalActions.d.ts +18 -0
  33. package/dist/views/_approvalActions.js +10 -0
  34. package/dist/views/_bridge.d.ts +12 -0
  35. package/dist/views/_bridge.js +10 -0
  36. package/dist/views/_parts/DocumentCard.d.ts +2 -0
  37. package/dist/views/_parts/DocumentCard.js +16 -0
  38. package/dist/views/_parts/FilePreview.d.ts +5 -0
  39. package/dist/views/_parts/FilePreview.js +101 -0
  40. package/dist/views/_parts/Linkify.d.ts +7 -0
  41. package/dist/views/_parts/Linkify.js +28 -0
  42. package/dist/views/_parts/Markdown.d.ts +44 -0
  43. package/dist/views/_parts/Markdown.js +313 -0
  44. package/dist/views/_parts/SchemaForm.d.ts +19 -0
  45. package/dist/views/_parts/SchemaForm.js +310 -0
  46. package/dist/views/_parts/fileRef.d.ts +7 -0
  47. package/dist/views/_parts/fileRef.js +11 -0
  48. package/dist/views/_parts/useNarrow.d.ts +5 -0
  49. package/dist/views/_parts/useNarrow.js +23 -0
  50. package/dist/views/host.d.ts +18 -0
  51. package/dist/views/host.js +20 -0
  52. package/dist/views/human.d.ts +73 -0
  53. package/dist/views/human.js +6 -0
  54. package/dist/views/index.d.ts +8 -0
  55. package/dist/views/index.js +12 -0
  56. package/dist/views/protocol.d.ts +34 -0
  57. package/dist/views/protocol.js +5 -0
  58. package/dist/views/runtime.d.ts +3 -0
  59. package/dist/views/runtime.js +86 -0
  60. package/dist/{slack-view.d.ts → views/slack.d.ts} +1 -1
  61. package/docs/development.md +40 -2
  62. package/index.ts +1 -0
  63. package/package.json +36 -4
  64. package/skills/create-loop/SKILL.md +6 -3
  65. package/views/_approval/ApprovalBar.tsx +239 -0
  66. package/views/_approvalActions.tsx +31 -0
  67. package/views/_bridge.tsx +36 -0
  68. package/views/_parts/DocumentCard.ts +18 -0
  69. package/views/_parts/FilePreview.tsx +180 -0
  70. package/views/_parts/Linkify.test.ts +40 -0
  71. package/views/_parts/Linkify.tsx +40 -0
  72. package/views/_parts/Markdown.test.ts +131 -0
  73. package/views/_parts/Markdown.tsx +472 -0
  74. package/views/_parts/SchemaForm.tsx +742 -0
  75. package/views/_parts/fileRef.ts +21 -0
  76. package/views/_parts/useNarrow.ts +24 -0
  77. package/views/host.ts +23 -0
  78. package/views/human.ts +91 -0
  79. package/views/index.ts +16 -0
  80. package/views/protocol.ts +40 -0
  81. package/views/runtime.tsx +132 -0
  82. package/views/slack.test.ts +40 -0
  83. package/{slack-view.ts → views/slack.ts} +3 -3
  84. /package/dist/{slack-view.js → views/slack.js} +0 -0
@@ -174,20 +174,26 @@ describe("humanConfigSchema", () => {
174
174
  ).toThrow(/担当/)
175
175
  })
176
176
 
177
- // 承認ビューは不透明なキー(program_dir / ai_dir と同じ)。実在するか・そのテナントが
178
- // 選べるかは mawaru 側のカタログ(非公開)と保存 API が見るので、ここは形だけ
179
- it("承認ビューを受け付ける", () => {
177
+ // 実装は nodes/human/ 直下のディレクトリ名(program_dir / ai_dir と同じ)。repo にあるかは保存 API が見る
178
+ it("実装(human_dir・ref)を受け付ける", () => {
180
179
  const parsed = humanConfigSchema.parse({
181
180
  assignment: { units: [[uuid()]] },
182
- view: "acme/invoice",
181
+ human_dir: "invoice-review",
182
+ ref: "try-view",
183
183
  })
184
- expect(parsed.view).toBe("acme/invoice")
184
+ expect(parsed.human_dir).toBe("invoice-review")
185
+ expect(parsed.ref).toBe("try-view")
185
186
  })
186
187
 
187
- it("空のビューキーは拒否する", () => {
188
- expect(() =>
189
- humanConfigSchema.parse({ assignment: { units: [[uuid()]] }, view: "" }),
190
- ).toThrow()
188
+ it("空・パスの human_dir は拒否する", () => {
189
+ for (const human_dir of ["", "a/b", "..", "a\\b"]) {
190
+ expect(() =>
191
+ humanConfigSchema.parse({
192
+ assignment: { units: [[uuid()]] },
193
+ human_dir,
194
+ }),
195
+ ).toThrow()
196
+ }
191
197
  })
192
198
 
193
199
  it("fixed:1ユニット(複数人の and/or)を受け付ける", () => {
package/_schemas/graph.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod"
2
+ import { humanDeclarationSchema } from "./human-manifest.js"
2
3
  import {
3
4
  graphPortSchema,
4
5
  jsonSchemaSchema,
@@ -156,10 +157,23 @@ export type AssignMember = z.infer<typeof assignMemberSchema>
156
157
  // メンバーが tenant の membership であることの検証は保存 API 側で行う
157
158
  // (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
158
159
  export const humanConfigSchema = z.object({
159
- // 承認 UI(承認ビュー)の識別子。program_dir / ai_dir と同じ「不透明なキー」で、
160
- // どのキーが実在するか・そのテナントが選べるかは保存時に mawaru 側が検証する
161
- // (カタログは非公開。@mawaru/common の approval-view.ts)。省略時は標準ビュー
162
- view: z.string().min(1).optional(),
160
+ // 実装(承認画面と入出力の宣言)の置き場所:実行 repo の nodes/human/ 直下のディレクトリ名。
161
+ // program_dir / ai_dir と同じ。省略時は mawaru 内蔵の標準の承認画面
162
+ // (docs/tasks/wip/承認ビューの顧客repo配置.md の「human ノードの実装として置く」)
163
+ human_dir: z
164
+ .string()
165
+ .min(1)
166
+ .refine(
167
+ (v) => !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..",
168
+ "human_dir は nodes/human/ 直下のディレクトリ名で指定してください",
169
+ )
170
+ .optional(),
171
+ // 読むブランチ(省略時はデフォルトブランチ)。publish でその先頭の commit に固定する
172
+ ref: z.string().min(1).optional(),
173
+ // 宣言の写し(in / approve / reject の型)。伝播はこれを読む。human_dir を選んだときに
174
+ // 一覧 API(GET /tenants/:id/github/humans)の宣言を入れる。省略すると保存時に mawaru が repo の
175
+ // config.json から読む(下書きに同じ human_dir の同じノードがあればその写しを保つ)
176
+ declaration: humanDeclarationSchema.optional(),
163
177
  assignment: z
164
178
  .object({
165
179
  strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
@@ -293,8 +307,8 @@ export const graphNodeSchema = z.object({
293
307
  // 表示アイコン(全 kind 共通の任意設定。human はフロントが担当から導出するので通常設定しない)
294
308
  icon: nodeIconSchema.optional(),
295
309
  // Slack ビュー(全 kind 共通の任意設定。実行中(human は承認待ち)・完了メッセージの見せ方)。
296
- // human の view と同じ「不透明なキー」で、選べる key は GET slack-views が返す。
297
- // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/slack-view の契約で書く)。
310
+ // human の view と同じ「不透明なキー」で、選べる key は GET views/slack が返す。
311
+ // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/views/slack の契約で書く)。
298
312
  // 保存時に mawaru が見るのは common/* の実在と <slug>/* の可視性だけ。
299
313
  // 省略=承認待ちは承認ボタンだけ・完了は ✅ の1行(データは出さない)。
300
314
  // ポートの型には一切影響しない
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import {
3
+ humanDeclarationOfManifest,
4
+ humanDeclarationSchema,
5
+ humanManifestSchema,
6
+ STANDARD_HUMAN_DECLARATION,
7
+ } from "./human-manifest.js"
8
+ import { fileSchema, instructionSchema } from "./port-spec.js"
9
+
10
+ const pr = {
11
+ type: "object",
12
+ properties: {
13
+ title: { type: "string", description: "PR のタイトル" },
14
+ gifs: { type: "array", items: fileSchema() },
15
+ count: { type: "integer" },
16
+ },
17
+ required: ["title"],
18
+ }
19
+
20
+ const issuesOf = (value: unknown) => {
21
+ const parsed = humanDeclarationSchema.safeParse(value)
22
+ return parsed.success
23
+ ? []
24
+ : parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
25
+ }
26
+
27
+ describe("humanDeclarationSchema(宣言の検査)", () => {
28
+ it("標準ビューの宣言と具体型の宣言を通す", () => {
29
+ expect(issuesOf(STANDARD_HUMAN_DECLARATION)).toEqual([])
30
+ expect(
31
+ issuesOf({ in: pr, approve: pr, reject: instructionSchema() }),
32
+ ).toEqual([])
33
+ // 未宣言({})も宣言の1つ(スキーマ未定義)
34
+ expect(issuesOf({ in: {}, approve: "T", reject: {} })).toEqual([])
35
+ })
36
+
37
+ it("type の配列(nullable)は場所つきで弾く", () => {
38
+ expect(
39
+ issuesOf({
40
+ in: {
41
+ type: "object",
42
+ properties: { summary: { type: ["string", "null"] } },
43
+ },
44
+ approve: "T",
45
+ reject: instructionSchema(),
46
+ }),
47
+ ).toEqual([
48
+ "in.properties.summary.type: 配列(nullable)は使えません。任意の値は required から外してください",
49
+ ])
50
+ })
51
+
52
+ it("知らない type・形の崩れた properties / required / items を弾く", () => {
53
+ expect(
54
+ issuesOf({
55
+ in: { type: "strin" },
56
+ approve: { type: "object", properties: [] },
57
+ reject: {
58
+ type: "object",
59
+ required: [1],
60
+ properties: { list: { type: "array", items: "x" } },
61
+ },
62
+ }),
63
+ ).toEqual([
64
+ "in.type: type は object / array / string / number / integer / boolean のどれかにしてください(strin)",
65
+ "approve.properties: properties はオブジェクトで書いてください",
66
+ "reject.properties.list.items: items はスキーマ(オブジェクト)で書いてください",
67
+ "reject.required: required は文字列の配列で書いてください",
68
+ ])
69
+ })
70
+
71
+ it("type の無い構造(properties / items だけ)を弾く", () => {
72
+ expect(
73
+ issuesOf({
74
+ in: { properties: { a: { type: "string" } } },
75
+ approve: "T",
76
+ reject: instructionSchema(),
77
+ }),
78
+ ).toEqual([
79
+ "in.type: properties / items を書くときは type も書いてください",
80
+ ])
81
+ })
82
+
83
+ it('reject は上流のコピー("T")にできない', () => {
84
+ expect(issuesOf({ in: "T", approve: "T", reject: "T" })).not.toEqual([])
85
+ })
86
+ })
87
+
88
+ describe("humanManifestSchema(nodes/human/<dir>/config.json)", () => {
89
+ it("省略した口は上流のコピー(in)・in と同型(approve)・標準の差し戻し(reject)になる", () => {
90
+ const config = humanManifestSchema.parse({})
91
+ expect(humanDeclarationOfManifest(config)).toEqual(
92
+ STANDARD_HUMAN_DECLARATION,
93
+ )
94
+ })
95
+
96
+ it("書いた口の型をそのまま宣言にする", () => {
97
+ const config = humanManifestSchema.parse({
98
+ inputs: { in: { schema: pr } },
99
+ outputs: { approve: { type: "array", items: pr }, reject: pr },
100
+ })
101
+ expect(humanDeclarationOfManifest(config)).toEqual({
102
+ in: pr,
103
+ approve: { type: "array", items: pr },
104
+ reject: pr,
105
+ })
106
+ })
107
+
108
+ it("in / approve / reject 以外の口・知らないキーは弾く", () => {
109
+ expect(
110
+ humanManifestSchema.safeParse({ inputs: { instruction: {} } }).success,
111
+ ).toBe(false)
112
+ expect(
113
+ humanManifestSchema.safeParse({ outputs: { notion: {} } }).success,
114
+ ).toBe(false)
115
+ expect(humanManifestSchema.safeParse({ name: "x" }).success).toBe(false)
116
+ })
117
+
118
+ it("中の型も宣言の検査を通す", () => {
119
+ const parsed = humanManifestSchema.safeParse({
120
+ inputs: { in: { schema: { type: ["string", "null"] } } },
121
+ })
122
+ expect(parsed.success).toBe(false)
123
+ expect(
124
+ parsed.error?.issues.map((i) => `${i.path.join(".")}: ${i.message}`),
125
+ ).toEqual([
126
+ "inputs.in.schema.type: 配列(nullable)は使えません。任意の値は required から外してください",
127
+ ])
128
+ })
129
+ })
@@ -0,0 +1,158 @@
1
+ import { z } from "zod"
2
+ import { type JsonSchema, jsonSchemaSchema } from "./node.js"
3
+ import {
4
+ COPY_UPSTREAM_SCHEMA,
5
+ type DeclaredPortSchema,
6
+ instructionSchema,
7
+ } from "./port-spec.js"
8
+
9
+ // ---- human ノードの宣言(in / approve / reject の型) ----
10
+ // 正は実装を持つ側(顧客 repo の nodes/human/<dir>/config.json。実装を指さないノードは標準の宣言)。
11
+ // human ノードは実装を選んだ時点の写し(node_humans.declaration)を持ち、伝播はそれを読む
12
+ // (docs/tasks/wip/承認ビューの顧客repo配置.md の「宣言(ポート型)」)。
13
+ // "T"(COPY_UPSTREAM_SCHEMA)は「上流の out をそのまま受ける」(in)/「in と同型」(approve)。
14
+ // reject は具体型だけ(上流のコピーは意味を持たない)
15
+
16
+ export type HumanDeclaration = {
17
+ in: DeclaredPortSchema
18
+ approve: DeclaredPortSchema
19
+ reject: JsonSchema
20
+ }
21
+
22
+ // 宣言に書ける type。file は x-mawaru-type マーカー付きの object
23
+ const DECLARED_TYPES = [
24
+ "object",
25
+ "array",
26
+ "string",
27
+ "number",
28
+ "integer",
29
+ "boolean",
30
+ ] as const
31
+
32
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
33
+ typeof value === "object" && value !== null && !Array.isArray(value)
34
+
35
+ // JSON Schema の形の崩れを場所つきで返す。接続の比較(schemaFit)が扱える形だけを通す:
36
+ // 顧客 repo の宣言は mawaru のレビューを通らないので、壊れた型がノードに写る前に止める
37
+ const schemaIssues = (
38
+ schema: Record<string, unknown>,
39
+ path: (string | number)[],
40
+ ): { path: (string | number)[]; message: string }[] => {
41
+ const issues: { path: (string | number)[]; message: string }[] = []
42
+ const { type, properties, required, items } = schema
43
+ if (Array.isArray(type)) {
44
+ issues.push({
45
+ path: [...path, "type"],
46
+ message:
47
+ "配列(nullable)は使えません。任意の値は required から外してください",
48
+ })
49
+ } else if (
50
+ type !== undefined &&
51
+ !DECLARED_TYPES.includes(type as (typeof DECLARED_TYPES)[number])
52
+ ) {
53
+ issues.push({
54
+ path: [...path, "type"],
55
+ message: `type は ${DECLARED_TYPES.join(" / ")} のどれかにしてください(${String(type)})`,
56
+ })
57
+ } else if (
58
+ type === undefined &&
59
+ (properties !== undefined || items !== undefined)
60
+ ) {
61
+ issues.push({
62
+ path: [...path, "type"],
63
+ message: "properties / items を書くときは type も書いてください",
64
+ })
65
+ }
66
+ if (properties !== undefined) {
67
+ if (!isPlainObject(properties)) {
68
+ issues.push({
69
+ path: [...path, "properties"],
70
+ message: "properties はオブジェクトで書いてください",
71
+ })
72
+ } else {
73
+ for (const [key, child] of Object.entries(properties)) {
74
+ if (!isPlainObject(child)) {
75
+ issues.push({
76
+ path: [...path, "properties", key],
77
+ message: "スキーマはオブジェクトで書いてください",
78
+ })
79
+ continue
80
+ }
81
+ issues.push(...schemaIssues(child, [...path, "properties", key]))
82
+ }
83
+ }
84
+ }
85
+ if (
86
+ required !== undefined &&
87
+ !(Array.isArray(required) && required.every((r) => typeof r === "string"))
88
+ ) {
89
+ issues.push({
90
+ path: [...path, "required"],
91
+ message: "required は文字列の配列で書いてください",
92
+ })
93
+ }
94
+ if (items !== undefined) {
95
+ if (!isPlainObject(items)) {
96
+ issues.push({
97
+ path: [...path, "items"],
98
+ message: "items はスキーマ(オブジェクト)で書いてください",
99
+ })
100
+ } else {
101
+ issues.push(...schemaIssues(items, [...path, "items"]))
102
+ }
103
+ }
104
+ return issues
105
+ }
106
+
107
+ // 宣言に書く1つの型(検査つき)
108
+ const checkedSchema = jsonSchemaSchema.superRefine((schema, ctx) => {
109
+ for (const issue of schemaIssues(schema, []))
110
+ ctx.addIssue({ code: "custom", ...issue })
111
+ })
112
+
113
+ const declaredPortSchema = z.union([
114
+ z.literal(COPY_UPSTREAM_SCHEMA),
115
+ checkedSchema,
116
+ ])
117
+
118
+ export const humanDeclarationSchema = z.object({
119
+ in: declaredPortSchema,
120
+ approve: declaredPortSchema,
121
+ reject: checkedSchema,
122
+ }) satisfies z.ZodType<HumanDeclaration>
123
+
124
+ // 標準ビュー(common/standard)の宣言:上流の型をそのまま受けて出し、差し戻しは標準の指示フォーム
125
+ export const STANDARD_HUMAN_DECLARATION: HumanDeclaration = {
126
+ in: COPY_UPSTREAM_SCHEMA,
127
+ approve: COPY_UPSTREAM_SCHEMA,
128
+ reject: instructionSchema(),
129
+ }
130
+
131
+ // ---- 顧客 repo の nodes/human/<dir>/config.json ----
132
+ // 形は AI の config.json に揃える(inputs の値は { schema }、outputs の値は素の JSON Schema)。
133
+ // 口は in / approve / reject 固定。省略した口は STANDARD_HUMAN_DECLARATION と同じ扱い
134
+ export const humanManifestSchema = z
135
+ .object({
136
+ inputs: z
137
+ .object({ in: z.object({ schema: checkedSchema }).strict().optional() })
138
+ .strict()
139
+ .optional(),
140
+ outputs: z
141
+ .object({
142
+ approve: checkedSchema.optional(),
143
+ reject: checkedSchema.optional(),
144
+ })
145
+ .strict()
146
+ .optional(),
147
+ })
148
+ .strict()
149
+
150
+ export type HumanManifest = z.infer<typeof humanManifestSchema>
151
+
152
+ export const humanDeclarationOfManifest = (
153
+ config: HumanManifest,
154
+ ): HumanDeclaration => ({
155
+ in: config.inputs?.in?.schema ?? COPY_UPSTREAM_SCHEMA,
156
+ approve: config.outputs?.approve ?? COPY_UPSTREAM_SCHEMA,
157
+ reject: config.outputs?.reject ?? instructionSchema(),
158
+ })
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
+ import type { HumanDeclaration } from "./human-manifest.js"
2
3
  import type { GraphPort, JsonSchema } from "./node.js"
3
4
  import {
4
5
  arrayOf,
@@ -153,6 +154,15 @@ describe("kindPortsViolation(kind 別ポート構成)", () => {
153
154
  })
154
155
  })
155
156
 
157
+ // human ノードに実装の宣言の写し(node_humans.declaration)を持たせる
158
+ const withViewDeclaration = <N extends { kind: string }>(
159
+ nodes: N[],
160
+ declaration: HumanDeclaration,
161
+ ) =>
162
+ nodes.map((node) =>
163
+ node.kind === "human" ? { ...node, human: { declaration } } : node,
164
+ )
165
+
156
166
  describe("型伝播(導出ポートの解決)", () => {
157
167
  it.each([
158
168
  "notion_task",
@@ -173,13 +183,14 @@ describe("型伝播(導出ポートの解決)", () => {
173
183
  extra,
174
184
  ],
175
185
  }
176
- const resolved = resolvePortSchemas([human], [], {
177
- viewPortSchemasOf: () => ({
186
+ const resolved = resolvePortSchemas(
187
+ withViewDeclaration([human], {
178
188
  in: { type: "string" },
179
189
  approve: { type: "string" },
180
190
  reject: { type: "string" },
181
191
  }),
182
- })
192
+ [],
193
+ )
183
194
  expect(resolved.get(extra.id)).toEqual(extra.schema)
184
195
  })
185
196
 
@@ -191,7 +202,7 @@ describe("型伝播(導出ポートの解決)", () => {
191
202
  }
192
203
 
193
204
  // human の in / approve / reject はビューの宣言をそのまま転写する(上流の kind は見ない)。
194
- // sdk はカタログを知らないので、呼び出し元が viewPortSchemasOf で解決して渡す
205
+ // 宣言はノードの写し(declaration)を読む(sdk はカタログを知らない)
195
206
  // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
196
207
  it("ai → human 直結(コピー宣言):in も approve も上流の out と同型、reject はビューの宣言型", () => {
197
208
  const aiOut = port("out", "main", declaredOut)
@@ -209,15 +220,12 @@ describe("型伝播(導出ポートの解決)", () => {
209
220
  ports: [humanIn, approve, reject],
210
221
  }
211
222
  const resolved = resolvePortSchemas(
212
- [ai, human],
223
+ withViewDeclaration([ai, human], {
224
+ in: "T",
225
+ approve: "T",
226
+ reject: instructionSchema(),
227
+ }),
213
228
  [{ from_port_id: aiOut.id, to_port_id: humanIn.id }],
214
- {
215
- viewPortSchemasOf: () => ({
216
- in: "T",
217
- approve: "T",
218
- reject: instructionSchema(),
219
- }),
220
- },
221
229
  )
222
230
  // 上流が AI でも中身を取り出さない:AI の出力形式がそのまま下流へ流れる
223
231
  expect(resolved.get(humanIn.id)).toEqual(declaredOut)
@@ -225,7 +233,7 @@ describe("型伝播(導出ポートの解決)", () => {
225
233
  expect(resolved.get(reject.id)).toEqual(instructionSchema())
226
234
  })
227
235
 
228
- describe("承認ビューの宣言を human のポートへ転写する", () => {
236
+ describe("実装の宣言を human のポートへ転写する", () => {
229
237
  const viewIn = {
230
238
  type: "object",
231
239
  properties: {
@@ -274,15 +282,12 @@ describe("型伝播(導出ポートの解決)", () => {
274
282
  declaredOut,
275
283
  )
276
284
  const resolved = resolvePortSchemas(
277
- [upstream, human],
285
+ withViewDeclaration([upstream, human], {
286
+ in: viewIn,
287
+ approve: viewOut,
288
+ reject: viewReject,
289
+ }),
278
290
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
279
- {
280
- viewPortSchemasOf: () => ({
281
- in: viewIn,
282
- approve: viewOut,
283
- reject: viewReject,
284
- }),
285
- },
286
291
  )
287
292
  expect(resolved.get(humanIn.id)).toEqual(viewIn)
288
293
  expect(resolved.get(approve.id)).toEqual(viewOut)
@@ -293,15 +298,12 @@ describe("型伝播(導出ポートの解決)", () => {
293
298
  it("上流の out は書き換えない(逆伝播しない)", () => {
294
299
  const { upstream, human, out, humanIn } = build("ai", declaredOut)
295
300
  const resolved = resolvePortSchemas(
296
- [upstream, human],
301
+ withViewDeclaration([upstream, human], {
302
+ in: viewIn,
303
+ approve: viewOut,
304
+ reject: viewReject,
305
+ }),
297
306
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
298
- {
299
- viewPortSchemasOf: () => ({
300
- in: viewIn,
301
- approve: viewOut,
302
- reject: viewReject,
303
- }),
304
- },
305
307
  )
306
308
  expect(resolved.get(out.id)).toEqual(declaredOut)
307
309
  })
@@ -312,15 +314,12 @@ describe("型伝播(導出ポートの解決)", () => {
312
314
  declaredOut,
313
315
  )
314
316
  const resolved = resolvePortSchemas(
315
- [upstream, human],
317
+ withViewDeclaration([upstream, human], {
318
+ in: "T",
319
+ approve: "T",
320
+ reject: viewReject,
321
+ }),
316
322
  [{ from_port_id: out.id, to_port_id: humanIn.id }],
317
- {
318
- viewPortSchemasOf: () => ({
319
- in: "T",
320
- approve: "T",
321
- reject: viewReject,
322
- }),
323
- },
324
323
  )
325
324
  expect(resolved.get(humanIn.id)).toEqual(declaredOut)
326
325
  expect(resolved.get(approve.id)).toEqual(declaredOut)
@@ -335,13 +334,14 @@ describe("型伝播(導出ポートの解決)", () => {
335
334
  kind: "human",
336
335
  ports: [humanIn, approve, reject],
337
336
  }
338
- const resolved = resolvePortSchemas([human], [], {
339
- viewPortSchemasOf: () => ({
337
+ const resolved = resolvePortSchemas(
338
+ withViewDeclaration([human], {
340
339
  in: viewIn,
341
340
  approve: viewOut,
342
341
  reject: viewReject,
343
342
  }),
344
- })
343
+ [],
344
+ )
345
345
  expect(resolved.get(humanIn.id)).toEqual(viewIn)
346
346
  expect(resolved.get(approve.id)).toEqual(viewOut)
347
347
  expect(resolved.get(reject.id)).toEqual(viewReject)
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod"
2
+ import type { HumanDeclaration } from "./human-manifest.js"
2
3
  import type { GraphPort, JsonSchema, PortIo } from "./node.js"
3
4
 
4
5
  // ポートの kind 別型システム(docs/tasks/done/ポートのkind別仕様.md)。
5
- // 型の決まり方は2分類:宣言(program / ai の in・out。human はビューの宣言を転写)/
6
+ // 型の決まり方は2分類:宣言(program / ai の in・out。human は実装(nodes/human/)の宣言を転写)/
6
7
  // 導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)。
7
8
  // mawaru がコードで型を固定するポートは無い
8
9
  // (docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md)
@@ -265,7 +266,7 @@ export const kindPortsViolation = (
265
266
  // ---- 型伝播(導出ポートの解決) ----
266
267
  // 接続の両端は同じスキーマになる(値入力フォームのSchemaForm統一.md)。
267
268
  // wait は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
268
- // human はビューの宣言を転写("T" は素通しと同じ挙動。reject もビューの宣言)。
269
+ // human は実装(nodes/human/)の宣言を転写("T" は素通しと同じ挙動。reject もビューの宣言)。
269
270
  // 宣言 out(program / ai)は触らない。
270
271
  // program / ai の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
271
272
  // 上書きし、未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の
@@ -328,6 +329,9 @@ type PropagationNode = {
328
329
  id: string
329
330
  kind: string
330
331
  ports: GraphPort[]
332
+ // human のときだけ意味を持つ:実装(nodes/human/<dir>/)の宣言の写し(node_humans.declaration)。
333
+ // 無ければ in・approve は上流のコピー、reject は触らない
334
+ human?: { declaration?: HumanDeclaration } | null
331
335
  }
332
336
  type PortLink = { from_port_id: string; to_port_id: string }
333
337
 
@@ -338,23 +342,6 @@ export const COPY_UPSTREAM_SCHEMA = "T"
338
342
  // ポートの型宣言:具体型か、上流からのコピー指示か
339
343
  export type DeclaredPortSchema = JsonSchema | typeof COPY_UPSTREAM_SCHEMA
340
344
 
341
- // 伝播のオプション。sdk は承認ビューのカタログを知らない(非公開。企業向けビューの
342
- // キーが npm 公開物に載るのを避ける)ので、呼び出し元が解決して渡す
343
- // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
344
- export type PropagationOptions<N> = {
345
- // human の in / approve / reject に転写する型(承認ビューの宣言)。null / undefined
346
- // ならその node は対象外(human 以外)。上流ノードの kind は一切見ない。
347
- // reject は具体型のみ(上流からのコピー指示は意味を持たない)
348
- viewPortSchemasOf?: (node: N) =>
349
- | {
350
- in: DeclaredPortSchema
351
- approve: DeclaredPortSchema
352
- reject: JsonSchema
353
- }
354
- | null
355
- | undefined
356
- }
357
-
358
345
  // end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
359
346
  // resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
360
347
  // start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
@@ -380,7 +367,6 @@ const isDerivedPort = (kind: string, port: Pick<GraphPort, "io" | "key">) =>
380
367
  export const resolvePortSchemas = <N extends PropagationNode>(
381
368
  nodes: N[],
382
369
  connections: PortLink[],
383
- options?: PropagationOptions<N>,
384
370
  ): Map<string, JsonSchema> => {
385
371
  const resolved = new Map<string, JsonSchema>()
386
372
  const kindByPortId = new Map<string, string>()
@@ -504,13 +490,14 @@ export const resolvePortSchemas = <N extends PropagationNode>(
504
490
  const inSchema = mergeInboundSchemas(
505
491
  sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
506
492
  )
507
- // 承認ビューの宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
493
+ // 実装(nodes/human/<dir>/)の宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
508
494
  // "T" は「上流の out をコピー」(in)/「in と同型」(approve)。reject は具体型の宣言
509
495
  // のみで、宣言が無ければ未宣言のまま(mawaru が型を焼き込むことはしない)。
510
496
  // 上流の out は書き換えない:逆伝播すると fan-out 先のビューが食い違ったとき
511
497
  // 型割れが config.json へ write-through され、元の出力宣言を黙って壊す。
512
498
  // ズレは接続の型エラーで見せてユーザーの [修正する] で直す
513
- const declared = options?.viewPortSchemasOf?.(node)
499
+ const declared =
500
+ node.kind === "human" ? node.human?.declaration : undefined
514
501
  const effectiveIn =
515
502
  declared && declared.in !== COPY_UPSTREAM_SCHEMA
516
503
  ? declared.in
@@ -588,9 +575,8 @@ export const connectionAccepted = (
588
575
  export const propagateDerivedPorts = <N extends PropagationNode>(
589
576
  nodes: N[],
590
577
  connections: PortLink[],
591
- options?: PropagationOptions<N>,
592
578
  ): N[] => {
593
- const resolved = resolvePortSchemas(nodes, connections, options)
579
+ const resolved = resolvePortSchemas(nodes, connections)
594
580
  const flexiblePorts = resolveFlexiblePorts(nodes, connections)
595
581
  return nodes.map((node) => {
596
582
  let touched = false
@@ -1,7 +1,7 @@
1
- import { spawn } from "node:child_process";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { createServer } from "node:http";
4
3
  import { saveCredentials } from "./credentials.js";
4
+ import { openBrowser } from "./openBrowser.js";
5
5
  // `gh auth login` と同じループバック方式(RFC 8252)。
6
6
  // CLI が 127.0.0.1 の一時ポートで待ち受け、ブラウザで mawaru の /cli-auth を開く。
7
7
  // 承認するとブラウザがループバック URL へ**遷移**してトークンを渡す
@@ -11,20 +11,6 @@ const page = (title, message) => `<!doctype html><meta charset="utf-8"><title>${
11
11
  `<body style="font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">` +
12
12
  `<div style="text-align:center"><h1 style="font-size:1.25rem">${title}</h1>` +
13
13
  `<p style="color:#666">${message}</p></div></body>`;
14
- // ブラウザを開く(開けなければ URL を出して手で開いてもらう。依存は足さない)
15
- const openBrowser = (url) => {
16
- const [command, args] = process.platform === "darwin"
17
- ? ["open", [url]]
18
- : process.platform === "win32"
19
- ? ["cmd", ["/c", "start", "", url]]
20
- : ["xdg-open", [url]];
21
- try {
22
- spawn(command, args, { stdio: "ignore", detached: true }).unref();
23
- }
24
- catch {
25
- // 何もしない(URL は既に印字済み)
26
- }
27
- };
28
14
  export const runLogin = async (timeoutMs = 5 * 60 * 1000) => {
29
15
  // 別のローカルプロセスが投げ込んだ応答を受け付けないための照合値
30
16
  const state = randomUUID();
@@ -0,0 +1 @@
1
+ export declare const openBrowser: (url: string) => void;
@@ -0,0 +1,15 @@
1
+ import { spawn } from "node:child_process";
2
+ // ブラウザを開く(開けなければ URL を出して手で開いてもらう。依存は足さない)
3
+ export const openBrowser = (url) => {
4
+ const [command, args] = process.platform === "darwin"
5
+ ? ["open", [url]]
6
+ : process.platform === "win32"
7
+ ? ["cmd", ["/c", "start", "", url]]
8
+ : ["xdg-open", [url]];
9
+ try {
10
+ spawn(command, args, { stdio: "ignore", detached: true }).unref();
11
+ }
12
+ catch {
13
+ // 何もしない(URL は既に印字済み)
14
+ }
15
+ };
@@ -1,5 +1,5 @@
1
1
  import { type Credentials } from "./credentials.js";
2
- export type MawaruApi = (method: "GET" | "POST", path: string, body?: unknown) => Promise<{
2
+ export type MawaruApi = (method: "GET" | "POST" | "PUT", path: string, body?: unknown) => Promise<{
3
3
  status: number;
4
4
  body: unknown;
5
5
  }>;