@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
@@ -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
@@ -156,10 +156,19 @@ export type AssignMember = z.infer<typeof assignMemberSchema>
156
156
  // メンバーが tenant の membership であることの検証は保存 API 側で行う
157
157
  // (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
158
158
  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(),
159
+ // 実装(承認画面と入出力の宣言)の置き場所:実行 repo の nodes/human/ 直下のディレクトリ名。
160
+ // program_dir / ai_dir と同じ。省略時は mawaru 内蔵の標準の承認画面
161
+ // (docs/tasks/wip/承認ビューの顧客repo配置.md の「human ノードの実装として置く」)
162
+ human_dir: z
163
+ .string()
164
+ .min(1)
165
+ .refine(
166
+ (v) => !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..",
167
+ "human_dir は nodes/human/ 直下のディレクトリ名で指定してください",
168
+ )
169
+ .optional(),
170
+ // 読むブランチ(省略時はデフォルトブランチ)。publish でその先頭の commit に固定する
171
+ ref: z.string().min(1).optional(),
163
172
  assignment: z
164
173
  .object({
165
174
  strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
@@ -293,8 +302,8 @@ export const graphNodeSchema = z.object({
293
302
  // 表示アイコン(全 kind 共通の任意設定。human はフロントが担当から導出するので通常設定しない)
294
303
  icon: nodeIconSchema.optional(),
295
304
  // Slack ビュー(全 kind 共通の任意設定。実行中(human は承認待ち)・完了メッセージの見せ方)。
296
- // human の view と同じ「不透明なキー」で、選べる key は GET slack-views が返す。
297
- // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/slack-view の契約で書く)。
305
+ // human の view と同じ「不透明なキー」で、選べる key は GET views/slack が返す。
306
+ // <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/views/slack の契約で書く)。
298
307
  // 保存時に mawaru が見るのは common/* の実在と <slug>/* の可視性だけ。
299
308
  // 省略=承認待ちは承認ボタンだけ・完了は ✅ の1行(データは出さない)。
300
309
  // ポートの型には一切影響しない
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { humanManifestSchema } from "./human-manifest.js"
3
+ import { fileSchema, instructionSchema } from "./port-spec.js"
4
+
5
+ const pr = {
6
+ type: "object",
7
+ properties: {
8
+ title: { type: "string", description: "PR のタイトル" },
9
+ gifs: { type: "array", items: fileSchema() },
10
+ },
11
+ required: ["title"],
12
+ }
13
+
14
+ const messagesOf = (value: unknown) => {
15
+ const parsed = humanManifestSchema.safeParse(value)
16
+ return parsed.success ? [] : parsed.error.issues.map((i) => i.message)
17
+ }
18
+
19
+ describe("humanManifestSchema(nodes/human/<dir>/config.json)", () => {
20
+ it("in・approve・reject を書いた宣言を通す(追加の出口も書ける)", () => {
21
+ const config = {
22
+ inputs: { in: { schema: pr } },
23
+ outputs: {
24
+ approve: { type: "array", items: pr },
25
+ reject: instructionSchema(),
26
+ notion: pr,
27
+ },
28
+ }
29
+ // refs はエディタが転写する生成物なので、無ければ空になる
30
+ expect(humanManifestSchema.parse(config)).toEqual({ ...config, refs: {} })
31
+ })
32
+
33
+ it("in・approve・reject のどれかが無ければ弾く(省略したときの決まりは無い)", () => {
34
+ const full = {
35
+ inputs: { in: { schema: pr } },
36
+ outputs: { approve: pr, reject: instructionSchema() },
37
+ }
38
+ const violation =
39
+ "human のポート構成は in(in) と out(approve / reject) が必須です(out は追加できます)"
40
+ expect(messagesOf({ ...full, inputs: {} })).toEqual([violation])
41
+ expect(messagesOf({ ...full, outputs: { approve: pr } })).toEqual([
42
+ violation,
43
+ ])
44
+ expect(
45
+ messagesOf({ ...full, outputs: { reject: instructionSchema() } }),
46
+ ).toEqual([violation])
47
+ expect(humanManifestSchema.safeParse({}).success).toBe(false)
48
+ })
49
+
50
+ it("in 以外の入口は弾く", () => {
51
+ expect(
52
+ humanManifestSchema.safeParse({
53
+ inputs: { in: { schema: pr }, instruction: { schema: pr } },
54
+ outputs: { approve: pr, reject: instructionSchema() },
55
+ }).success,
56
+ ).toBe(false)
57
+ })
58
+
59
+ it('上流のコピー("T")は書けない(型は具体的に書く)', () => {
60
+ expect(
61
+ humanManifestSchema.safeParse({
62
+ inputs: { in: { schema: "T" } },
63
+ outputs: { approve: "T", reject: instructionSchema() },
64
+ }).success,
65
+ ).toBe(false)
66
+ })
67
+ })
@@ -0,0 +1,31 @@
1
+ import { z } from "zod"
2
+ import { jsonSchemaSchema, portKeySchema } from "./node.js"
3
+ import { kindPortsViolation } from "./port-spec.js"
4
+
5
+ // 顧客 repo の nodes/human/<dir>/config.json(human ノードの実装の入出力の宣言)。
6
+ // program / AI の config.json と同じく、エディタがディレクトリを選んだときに inputs / outputs から
7
+ // ports を起こし、ポートの編集は保存時にここへ書き戻される(実行時の正は ports テーブルのまま)。
8
+ // 形は AI に揃える(inputs の値は { schema }、outputs の値は素の JSON Schema)。
9
+ // 口の構成(in 1つ・approve / reject は必須・追加の出口は自由)は kind のポート構成の規則をそのまま使う
10
+ export const humanManifestSchema = z
11
+ .object({
12
+ inputs: z.record(z.string(), z.object({ schema: jsonSchemaSchema })),
13
+ outputs: z.record(z.string(), jsonSchemaSchema),
14
+ // refs=ループ変数の定義の転写(program と同じ。エディタのグラフ保存が一方向に書く生成物)
15
+ refs: z.record(portKeySchema, jsonSchemaSchema).default({}),
16
+ })
17
+ .superRefine((manifest, ctx) => {
18
+ const violation = kindPortsViolation("human", [
19
+ ...Object.keys(manifest.inputs).map((key) => ({
20
+ io: "in" as const,
21
+ key,
22
+ })),
23
+ ...Object.keys(manifest.outputs).map((key) => ({
24
+ io: "out" as const,
25
+ key,
26
+ })),
27
+ ])
28
+ if (violation) ctx.addIssue({ code: "custom", message: violation })
29
+ })
30
+
31
+ export type HumanManifest = z.infer<typeof humanManifestSchema>
@@ -153,11 +153,17 @@ describe("kindPortsViolation(kind 別ポート構成)", () => {
153
153
  })
154
154
  })
155
155
 
156
+ // 実装(nodes/human/<dir>/)を指す human にする
157
+ const withHumanDir = <N extends { kind: string }>(nodes: N[]) =>
158
+ nodes.map((node) =>
159
+ node.kind === "human" ? { ...node, human: { human_dir: "review" } } : node,
160
+ )
161
+
156
162
  describe("型伝播(導出ポートの解決)", () => {
157
163
  it.each([
158
164
  "notion_task",
159
165
  "main",
160
- ])("Humanの追加出口 %s の宣言型は承認ビューの型で上書きしない", (key) => {
166
+ ])("Humanの追加出口 %s の宣言型は伝播で上書きしない", (key) => {
161
167
  const extra = port("out", key, {
162
168
  type: "object",
163
169
  properties: { title: { type: "string" } },
@@ -173,14 +179,10 @@ describe("型伝播(導出ポートの解決)", () => {
173
179
  extra,
174
180
  ],
175
181
  }
176
- const resolved = resolvePortSchemas([human], [], {
177
- viewPortSchemasOf: () => ({
178
- in: { type: "string" },
179
- approve: { type: "string" },
180
- reject: { type: "string" },
181
- }),
182
- })
183
- expect(resolved.get(extra.id)).toEqual(extra.schema)
182
+ for (const nodes of [[human], withHumanDir([human])]) {
183
+ const resolved = resolvePortSchemas(nodes, [])
184
+ expect(resolved.get(extra.id)).toEqual(extra.schema)
185
+ }
184
186
  })
185
187
 
186
188
  // 伝播テスト用の任意の宣言型(中身の形に意味はない)
@@ -190,10 +192,9 @@ describe("型伝播(導出ポートの解決)", () => {
190
192
  required: ["text"],
191
193
  }
192
194
 
193
- // human の in / approve / reject はビューの宣言をそのまま転写する(上流の kind は見ない)。
194
- // sdk はカタログを知らないので、呼び出し元が viewPortSchemasOf で解決して渡す
195
- // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
196
- it("ai → human 直結(コピー宣言):in も approve も上流の out と同型、reject はビューの宣言型", () => {
195
+ // 実装を指さない human(標準の承認画面)は wait と同じ素通し。上流の kind は見ない
196
+ // → docs/tasks/wip/承認ビューの顧客repo配置.md の「ポート型」
197
+ it("実装を指さない human:in も approve も上流の out と同型、reject は標準の差し戻し", () => {
197
198
  const aiOut = port("out", "main", declaredOut)
198
199
  const ai = {
199
200
  id: uuid(),
@@ -211,13 +212,6 @@ describe("型伝播(導出ポートの解決)", () => {
211
212
  const resolved = resolvePortSchemas(
212
213
  [ai, human],
213
214
  [{ 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
215
  )
222
216
  // 上流が AI でも中身を取り出さない:AI の出力形式がそのまま下流へ流れる
223
217
  expect(resolved.get(humanIn.id)).toEqual(declaredOut)
@@ -225,25 +219,17 @@ describe("型伝播(導出ポートの解決)", () => {
225
219
  expect(resolved.get(reject.id)).toEqual(instructionSchema())
226
220
  })
227
221
 
228
- describe("承認ビューの宣言を human のポートへ転写する", () => {
222
+ describe("実装(nodes/human/<dir>/)を指す human は program / ai と同じ", () => {
229
223
  const viewIn = {
230
224
  type: "object",
231
- properties: {
232
- data: {
233
- type: "object",
234
- properties: { prs: { type: "array", items: { type: "object" } } },
235
- required: ["prs"],
236
- },
237
- description: { type: "string" },
238
- },
239
- required: ["data", "description"],
225
+ properties: { prs: { type: "array", items: { type: "object" } } },
226
+ required: ["prs"],
240
227
  }
241
228
  const viewOut = {
242
229
  type: "object",
243
- properties: { prs: { type: "array", items: { type: "object" } } },
244
- required: ["prs"],
230
+ properties: { merged: { type: "boolean" } },
231
+ required: ["merged"],
245
232
  }
246
- // reject もビューの宣言(具体型のみ。コピー指示は意味を持たない)
247
233
  const viewReject = {
248
234
  type: "object",
249
235
  properties: { reason: { type: "string" } },
@@ -256,18 +242,19 @@ describe("型伝播(導出ポートの解決)", () => {
256
242
  kind: upstreamKind,
257
243
  ports: [port("in", "in"), out],
258
244
  }
259
- const humanIn = port("in", "in")
260
- const approve = port("out", "approve")
261
- const reject = port("out", "reject")
245
+ const humanIn = port("in", "in", viewIn)
246
+ const approve = port("out", "approve", viewOut)
247
+ const reject = port("out", "reject", viewReject)
262
248
  const human = {
263
249
  id: uuid(),
264
250
  kind: "human",
251
+ human: { human_dir: "review" },
265
252
  ports: [humanIn, approve, reject],
266
253
  }
267
254
  return { upstream, human, out, humanIn, approve, reject }
268
255
  }
269
256
 
270
- it("宣言のあるビューは上流の kind に依らず同じ型になる", () => {
257
+ it("out(approve・reject)は宣言のまま(上流の kind に依らない)", () => {
271
258
  for (const kind of ["ai", "program"]) {
272
259
  const { upstream, human, out, humanIn, approve, reject } = build(
273
260
  kind,
@@ -276,79 +263,33 @@ describe("型伝播(導出ポートの解決)", () => {
276
263
  const resolved = resolvePortSchemas(
277
264
  [upstream, human],
278
265
  [{ 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
266
  )
287
- expect(resolved.get(humanIn.id)).toEqual(viewIn)
288
267
  expect(resolved.get(approve.id)).toEqual(viewOut)
289
268
  expect(resolved.get(reject.id)).toEqual(viewReject)
290
269
  }
291
270
  })
292
271
 
293
- it("上流の out は書き換えない(逆伝播しない)", () => {
272
+ it("in は上流の具体型を写す(program / ai の in と同じ鏡映)", () => {
294
273
  const { upstream, human, out, humanIn } = build("ai", declaredOut)
295
274
  const resolved = resolvePortSchemas(
296
275
  [upstream, human],
297
276
  [{ 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
- )
306
- expect(resolved.get(out.id)).toEqual(declaredOut)
307
- })
308
-
309
- it("コピー宣言(標準ビュー)は上流の型をそのまま in と approve へ流す", () => {
310
- const { upstream, human, out, humanIn, approve } = build(
311
- "ai",
312
- declaredOut,
313
- )
314
- const resolved = resolvePortSchemas(
315
- [upstream, human],
316
- [{ 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
277
  )
325
278
  expect(resolved.get(humanIn.id)).toEqual(declaredOut)
326
- expect(resolved.get(approve.id)).toEqual(declaredOut)
279
+ // 上流の out は書き換えない(逆伝播しない)
280
+ expect(resolved.get(out.id)).toEqual(declaredOut)
327
281
  })
328
282
 
329
- it("未接続でも宣言で固定される", () => {
330
- const humanIn = port("in", "in")
331
- const approve = port("out", "approve")
332
- const reject = port("out", "reject")
333
- const human = {
334
- id: uuid(),
335
- kind: "human",
336
- ports: [humanIn, approve, reject],
337
- }
338
- const resolved = resolvePortSchemas([human], [], {
339
- viewPortSchemasOf: () => ({
340
- in: viewIn,
341
- approve: viewOut,
342
- reject: viewReject,
343
- }),
344
- })
283
+ it("未接続の in は宣言のまま", () => {
284
+ const { human, humanIn, approve, reject } = build("ai", declaredOut)
285
+ const resolved = resolvePortSchemas([human], [])
345
286
  expect(resolved.get(humanIn.id)).toEqual(viewIn)
346
287
  expect(resolved.get(approve.id)).toEqual(viewOut)
347
288
  expect(resolved.get(reject.id)).toEqual(viewReject)
348
289
  })
349
290
  })
350
291
 
351
- it("ai → wait → human:素通しで上流の宣言型が伝播し、reject はビュー宣言が無ければ未宣言", () => {
292
+ it("ai → wait → human:素通しで上流の宣言型が伝播し、reject は標準の差し戻し", () => {
352
293
  // ai の in は宣言型(nodes/ai/<dir>/config.json の inputs.in 由来。実行repoの構造見直し)
353
294
  const aiInSchema = {
354
295
  type: "object",
@@ -383,8 +324,8 @@ describe("型伝播(導出ポートの解決)", () => {
383
324
  expect(resolved.get(humanIn.id)).toEqual(declaredOut)
384
325
  // approve は in と同型(上流の kind による中身の取り出しはしない)
385
326
  expect(resolved.get(approve.id)).toEqual(declaredOut)
386
- // reject はビューの宣言でのみ決まる(sdk 単体=カタログ無しでは未宣言。型固定は廃止)
387
- expect(resolved.get(reject.id)).toEqual(UNDECLARED_SCHEMA)
327
+ // reject は標準の承認画面の差し戻し(指示+参考ファイル)
328
+ expect(resolved.get(reject.id)).toEqual(instructionSchema())
388
329
  // ai の in は宣言型がそのまま残る(const 扱いは廃止)
389
330
  expect(resolved.get(aiIn.id)).toEqual(aiInSchema)
390
331
  })
@@ -2,7 +2,7 @@ import { z } from "zod"
2
2
  import type { GraphPort, JsonSchema, PortIo } from "./node.js"
3
3
 
4
4
  // ポートの kind 別型システム(docs/tasks/done/ポートのkind別仕様.md)。
5
- // 型の決まり方は2分類:宣言(program / ai の in・out。human はビューの宣言を転写)/
5
+ // 型の決まり方は2分類:宣言(program / ai と、実装(nodes/human/<dir>/)を指す human の in・out)/
6
6
  // 導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)。
7
7
  // mawaru がコードで型を固定するポートは無い
8
8
  // (docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md)
@@ -265,9 +265,10 @@ export const kindPortsViolation = (
265
265
  // ---- 型伝播(導出ポートの解決) ----
266
266
  // 接続の両端は同じスキーマになる(値入力フォームのSchemaForm統一.md)。
267
267
  // wait は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
268
- // human はビューの宣言を転写("T" は素通しと同じ挙動。reject もビューの宣言)。
269
- // 宣言 out(program / ai)は触らない。
270
- // program / ai の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
268
+ // human は実装(nodes/human/<dir>/)を指していれば program / ai と同じ宣言ノード、指していなければ
269
+ // (標準の承認画面)wait と同じ素通し(approve は in と同型、reject は標準の差し戻し instructionSchema)。
270
+ // 宣言 out(program / ai / 実装を指す human)は触らない。
271
+ // program / ai(と実装を指す human)の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
271
272
  // 上書きし、未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の
272
273
  // config.json へも書き戻される。docs/tasks/done/ポート編集のrepo書き戻し同期.md)
273
274
 
@@ -328,33 +329,11 @@ type PropagationNode = {
328
329
  id: string
329
330
  kind: string
330
331
  ports: GraphPort[]
332
+ // human のときだけ意味を持つ:実装(nodes/human/<dir>/)を指しているか
333
+ human?: { human_dir?: string | null } | null
331
334
  }
332
335
  type PortLink = { from_port_id: string; to_port_id: string }
333
336
 
334
- // 「上流の out をそのままコピーする」(in)/「in と同型」(approve)を表す指示。
335
- // 承認ビューのカタログ(@mawaru/common)の COPY_UPSTREAM と同じ値
336
- export const COPY_UPSTREAM_SCHEMA = "T"
337
-
338
- // ポートの型宣言:具体型か、上流からのコピー指示か
339
- export type DeclaredPortSchema = JsonSchema | typeof COPY_UPSTREAM_SCHEMA
340
-
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
337
  // end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
359
338
  // resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
360
339
  // start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
@@ -365,10 +344,22 @@ const DERIVED_KINDS = new Set(["wait", "human", "end", "start", "join"])
365
344
  // in を接続鏡映(sticky)するノード:接続元 out が具体型のときだけ上書き
366
345
  const MIRROR_KINDS = new Set(["program", "ai"])
367
346
 
368
- const isDerivedPort = (kind: string, port: Pick<GraphPort, "io" | "key">) =>
369
- DERIVED_KINDS.has(kind) &&
347
+ // 実装(nodes/human/<dir>/)を指す human は program / ai と同じ(in は鏡映・out は宣言。
348
+ // 型は config.json へ書き戻される)。指していない human は導出(標準の承認画面の素通し)
349
+ const isMirrorNode = (node: PropagationNode): boolean =>
350
+ MIRROR_KINDS.has(node.kind) ||
351
+ (node.kind === "human" && Boolean(node.human?.human_dir))
352
+
353
+ const isDerivedNode = (node: PropagationNode): boolean =>
354
+ DERIVED_KINDS.has(node.kind) && !isMirrorNode(node)
355
+
356
+ const isDerivedPort = (
357
+ node: PropagationNode,
358
+ port: Pick<GraphPort, "io" | "key">,
359
+ ) =>
360
+ isDerivedNode(node) &&
370
361
  (port.io === "in" ||
371
- (kind !== "human" && port.key === "main") ||
362
+ (node.kind !== "human" && port.key === "main") ||
372
363
  port.key === "approve" ||
373
364
  port.key === "reject")
374
365
 
@@ -380,7 +371,6 @@ const isDerivedPort = (kind: string, port: Pick<GraphPort, "io" | "key">) =>
380
371
  export const resolvePortSchemas = <N extends PropagationNode>(
381
372
  nodes: N[],
382
373
  connections: PortLink[],
383
- options?: PropagationOptions<N>,
384
374
  ): Map<string, JsonSchema> => {
385
375
  const resolved = new Map<string, JsonSchema>()
386
376
  const kindByPortId = new Map<string, string>()
@@ -389,7 +379,7 @@ export const resolvePortSchemas = <N extends PropagationNode>(
389
379
  kindByPortId.set(port.id, node.kind)
390
380
  resolved.set(
391
381
  port.id,
392
- isDerivedPort(node.kind, port) ? UNDECLARED_SCHEMA : port.schema,
382
+ isDerivedPort(node, port) ? UNDECLARED_SCHEMA : port.schema,
393
383
  )
394
384
  }
395
385
  }
@@ -480,9 +470,9 @@ export const resolvePortSchemas = <N extends PropagationNode>(
480
470
  }
481
471
  continue
482
472
  }
483
- const isMirror = MIRROR_KINDS.has(node.kind)
484
- if (!DERIVED_KINDS.has(node.kind) && !isMirror) continue
485
- // 鏡映(program / ai):全ての in が対象。具体型が流れてきたときだけ上書きする
473
+ const isMirror = isMirrorNode(node)
474
+ if (!isDerivedNode(node) && !isMirror) continue
475
+ // 鏡映(program / ai / 実装を指す human):全ての in が対象。具体型が流れてきたときだけ上書きする
486
476
  // (宣言を未宣言で潰さない)。ai の in はどの口も対等(instruction の特別扱いは無い)
487
477
  if (isMirror) {
488
478
  for (const port of node.ports) {
@@ -497,36 +487,24 @@ export const resolvePortSchemas = <N extends PropagationNode>(
497
487
  }
498
488
  continue
499
489
  }
500
- // 導出(wait / human):in は上流から
490
+ // 導出(wait / 実装を指さない human):in は上流から、out(main・approve)は in と同型。
491
+ // human の reject は標準の承認画面の差し戻し(指示+参考ファイル)。
492
+ // 上流の out は書き換えない(逆伝播すると fan-out 先の型割れが config.json へ書き戻され、
493
+ // 元の出力宣言を黙って壊す。ズレは接続の型エラーで見せる)
501
494
  const inPort = node.ports.find((p) => p.io === "in")
502
495
  if (!inPort) continue
503
496
  const sources = inbound.get(inPort.id) ?? []
504
497
  const inSchema = mergeInboundSchemas(
505
498
  sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
506
499
  )
507
- // 承認ビューの宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
508
- // "T" は「上流の out をコピー」(in)/「in と同型」(approve)。reject は具体型の宣言
509
- // のみで、宣言が無ければ未宣言のまま(mawaru が型を焼き込むことはしない)。
510
- // 上流の out は書き換えない:逆伝播すると fan-out 先のビューが食い違ったとき
511
- // 型割れが config.json へ write-through され、元の出力宣言を黙って壊す。
512
- // ズレは接続の型エラーで見せてユーザーの [修正する] で直す
513
- const declared = options?.viewPortSchemasOf?.(node)
514
- const effectiveIn =
515
- declared && declared.in !== COPY_UPSTREAM_SCHEMA
516
- ? declared.in
517
- : inSchema
518
- assign(inPort.id, effectiveIn)
500
+ assign(inPort.id, inSchema)
519
501
  for (const port of node.ports) {
520
- if (port.io !== "out" || !isDerivedPort(node.kind, port)) continue
521
- if (node.kind === "human" && port.key === "reject") {
522
- if (declared) assign(port.id, declared.reject)
523
- continue
524
- }
502
+ if (port.io !== "out" || !isDerivedPort(node, port)) continue
525
503
  assign(
526
504
  port.id,
527
- declared && declared.approve !== COPY_UPSTREAM_SCHEMA
528
- ? declared.approve
529
- : effectiveIn,
505
+ node.kind === "human" && port.key === "reject"
506
+ ? instructionSchema()
507
+ : inSchema,
530
508
  )
531
509
  }
532
510
  }
@@ -588,9 +566,8 @@ export const connectionAccepted = (
588
566
  export const propagateDerivedPorts = <N extends PropagationNode>(
589
567
  nodes: N[],
590
568
  connections: PortLink[],
591
- options?: PropagationOptions<N>,
592
569
  ): N[] => {
593
- const resolved = resolvePortSchemas(nodes, connections, options)
570
+ const resolved = resolvePortSchemas(nodes, connections)
594
571
  const flexiblePorts = resolveFlexiblePorts(nodes, connections)
595
572
  return nodes.map((node) => {
596
573
  let touched = false
@@ -12,8 +12,8 @@ import { previewSlack, SLACK_SURFACES, } from "./previewSlack.js";
12
12
  // `mawaru login` のセッション(web と同じ membership)と、どのテナントのつもりで描くかの
13
13
  // MAWARU_TENANT_ID(repo の .env。create-loop スキルと同じ)が要る。
14
14
  // ファイルの集め方・cases.ts の評価・ケースの絞り込みは種類をまたいで共通で、種類ごとに「確かめ方」
15
- // (Slack は送る。承認ビューは repo へ移すときに足す)だけを差し替える
16
- // (docs/tasks/wip/Slackビューの顧客repo配置.md の「手元プレビュー」)
15
+ // (Slack は送る)だけを差し替える
16
+ // (docs/tasks/done/Slackビューの顧客repo配置.md の「手元プレビュー」)
17
17
  const USAGE = "使い方: npx mawaru preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|チャンネル ID>]\n" +
18
18
  " 例: npx mawaru preview slack/pr-review(views/slack/pr-review/ の cases.ts の全ケースを Slack に送る)";
19
19
  export const parsePreviewArgs = (args) => {
@@ -44,7 +44,7 @@ export const parsePreviewArgs = (args) => {
44
44
  };
45
45
  const PREVIEWERS = {
46
46
  slack: {
47
- casesPath: "/slack-views/cases",
47
+ casesPath: "/views/slack/cases",
48
48
  run: ({ name, files, cases, options, api, log }) => {
49
49
  const channel = options.channel ?? process.env.MAWARU_PREVIEW_CHANNEL;
50
50
  if (!channel) {
@@ -2,9 +2,8 @@ import { readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  // 手元プレビュー(`mawaru preview`)が読む、実行 repo の views/<種類>/<名前>/ のファイル。
4
4
  // 描画は mawaru API がやるので、ここは「対象を決めてファイルを集める」だけで、種類をまたいで共通
5
- // (docs/tasks/wip/Slackビューの顧客repo配置.md の「手元プレビュー」)
6
- // 手元プレビューできるビューの種類(views/ 直下のディレクトリ名)。承認ビューを repo へ移すときに
7
- // "approval" を足す(previewCommand.ts の振り分けにも足す)
5
+ // (docs/tasks/done/Slackビューの顧客repo配置.md の「手元プレビュー」)
6
+ // 手元プレビューできるビューの種類(views/ 直下のディレクトリ名。previewCommand.ts の振り分けと揃える)
8
7
  export const VIEW_KINDS = ["slack"];
9
8
  const dirsIn = (dir) => {
10
9
  try {
@@ -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
@@ -51,7 +51,8 @@ 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>;
55
56
  assignment: z.ZodObject<{
56
57
  strategy: z.ZodDefault<z.ZodEnum<{
57
58
  fixed: "fixed";
@@ -194,7 +195,8 @@ export declare const graphNodeSchema: z.ZodObject<{
194
195
  ref: z.ZodOptional<z.ZodString>;
195
196
  }, z.core.$strip>>;
196
197
  human: z.ZodOptional<z.ZodObject<{
197
- view: z.ZodOptional<z.ZodString>;
198
+ human_dir: z.ZodOptional<z.ZodString>;
199
+ ref: z.ZodOptional<z.ZodString>;
198
200
  assignment: z.ZodObject<{
199
201
  strategy: z.ZodDefault<z.ZodEnum<{
200
202
  fixed: "fixed";
@@ -295,7 +297,8 @@ export declare const saveGraphBodySchema: z.ZodObject<{
295
297
  ref: z.ZodOptional<z.ZodString>;
296
298
  }, z.core.$strip>>;
297
299
  human: z.ZodOptional<z.ZodObject<{
298
- view: z.ZodOptional<z.ZodString>;
300
+ human_dir: z.ZodOptional<z.ZodString>;
301
+ ref: z.ZodOptional<z.ZodString>;
299
302
  assignment: z.ZodObject<{
300
303
  strategy: z.ZodDefault<z.ZodEnum<{
301
304
  fixed: "fixed";