@mawaru/sdk 0.17.0 → 0.19.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.
@@ -1,5 +1,6 @@
1
1
  import type { GraphConnection, GraphNode } from "./graph.js"
2
2
  import type { GraphPort } from "./node.js"
3
+ import { schemaTypeKey } from "./port-spec.js"
3
4
 
4
5
  // 矩形選択からのグループ化・コンポーネント化(ノードのグループ化・コンポーネント化.md/
5
6
  // コンポーネントの複数入口・複数出口.md)。
@@ -67,7 +68,7 @@ export const analyzeExtractSelection = (
67
68
  // 同じ思想を境界にも適用する(グループ化=入口の合流の宣言)
68
69
  const entryPortIds = [...new Set(inbound.map((c) => c.to_port_id))]
69
70
  const entrySchemas = new Set(
70
- entryPortIds.map((id) => JSON.stringify(portById.get(id)?.schema ?? {})),
71
+ entryPortIds.map((id) => schemaTypeKey(portById.get(id)?.schema ?? {})),
71
72
  )
72
73
  if (entrySchemas.size > 1) {
73
74
  return {
@@ -312,7 +312,7 @@ describe("saveGraphBodySchema", () => {
312
312
  })
313
313
 
314
314
  // fan-in(同じ in ポートへの流入2本以上)は同じ型でなければならない
315
- // (docs/tasks/wip/ポートスキーマのany撲滅.md)
315
+ // (docs/tasks/done/ポートスキーマのany撲滅.md)
316
316
  const programNode = (outSchema: GraphPort["schema"]) => ({
317
317
  ...makeNode([
318
318
  port("in", "in"),
@@ -333,6 +333,18 @@ describe("saveGraphBodySchema", () => {
333
333
  ).not.toThrow()
334
334
  })
335
335
 
336
+ it("fan-in:注釈(title)だけが違う流入は同じ型として受け付ける", () => {
337
+ const a = programNode({ type: "string", title: "回答" })
338
+ const b = programNode({ type: "string", title: "要開発者確認" })
339
+ const sink = { ...makeNode(simplePorts()), kind: "wait" as const }
340
+ expect(() =>
341
+ saveGraphBodySchema.parse({
342
+ nodes: [a, b, sink],
343
+ connections: [connect(a, sink), connect(b, sink)],
344
+ }),
345
+ ).not.toThrow()
346
+ })
347
+
336
348
  it("fan-in:異なる具体型の流入を拒否する", () => {
337
349
  const a = programNode({ type: "string" })
338
350
  const b = programNode({ type: "number" })
@@ -623,7 +635,7 @@ describe("saveGraphBodySchema(kind 別ポート構成)", () => {
623
635
  describe("saveGraphBodySchema(型の接続バリデーション)", () => {
624
636
  // 接続の可否は kind ではなく型で決まる。reject / instruction の型固定は廃止され、
625
637
  // ai の in はどの口も接続鏡映(接続元の型に同期)なので接続は自明に通る
626
- // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
638
+ // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
627
639
  it("human の reject → ai の instruction を許可(kind の縛りも型固定も無い)", () => {
628
640
  const human = humanNode()
629
641
  const ai = aiNode()
package/_schemas/graph.ts CHANGED
@@ -99,7 +99,7 @@ export type WaitConfig = z.infer<typeof waitConfigSchema>
99
99
  // Program ノードの kind 別設定(node_programs に対応)。実行先の repo はノード設定でなく
100
100
  // installation から `${account_login}/${GITHUB_REPO_NAME}` に固定(backend)。program_dir は nodes/program/ 直下のディレクトリ名
101
101
  // (例: sendReply。日本語可。エントリは main.ts 固定。リポジトリ規約 v2 は
102
- // docs/tasks/wip/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
102
+ // docs/tasks/done/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
103
103
  // ref 省略時はデフォルトブランチ
104
104
  export const programConfigSchema = z.object({
105
105
  program_dir: z
@@ -130,7 +130,7 @@ export type AiConfig = z.infer<typeof aiConfigSchema>
130
130
 
131
131
  // 担当ユニットのメンバー:user_id の名指し、または動的な担当「ループを実行した人」
132
132
  // (run の起動者。実行時に runs.start_by で解決し、解決できなければその1人だけ落ちる。
133
- // docs/tasks/wip/Human担当に「ループを実行した人」を追加する.md)
133
+ // docs/tasks/done/Human担当に「ループを実行した人」を追加する.md)
134
134
  export const RUN_STARTER = "run_starter"
135
135
  export const assignMemberSchema = z.union([z.uuid(), z.literal(RUN_STARTER)])
136
136
  export type AssignMember = z.infer<typeof assignMemberSchema>
@@ -198,7 +198,7 @@ export type HumanConfig = z.infer<typeof humanConfigSchema>
198
198
 
199
199
  // コンポーネントノードの kind 別設定(node_components に対応)。呼ぶ先は
200
200
  // kind='component' の loop。同 tenant・kind の検証と自 loop 参照の禁止は保存 API 側で行う
201
- // (docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
201
+ // (docs/tasks/done/コンポーネント(ループの部品化と再利用).md)
202
202
  export const componentConfigSchema = z.object({
203
203
  component_loop_id: z.uuid(),
204
204
  // 配置の設定値:子 loop の refs 定義(default/required)への上書き。
@@ -211,7 +211,7 @@ export type ComponentConfig = z.infer<typeof componentConfigSchema>
211
211
  // ノードにアタッチされた IOHook(node_hooks に対応。アタッチできるのは program ノードのみ)。
212
212
  // on_input / on_output / on_signal はアタッチ時に repo の config.json(hookManifestSchema)から
213
213
  // エディタが焼き込むスナップショット。ref 省略時はデフォルトブランチ
214
- // (docs/tasks/wip/NodeIOHooks(ノードへのIOフックのアタッチ).md・
214
+ // (docs/tasks/done/NodeIOHooks(ノードへのIOフックのアタッチ).md・
215
215
  // NodeIOHooksのonSignal(外部シグナルでのフック発火).md)
216
216
  export const graphNodeHookSchema = z.object({
217
217
  hook_dir: z
@@ -231,7 +231,7 @@ export type GraphNodeHook = z.infer<typeof graphNodeHookSchema>
231
231
 
232
232
  // 新規ノードの id・ポートの id もクライアントが採番する(一時IDの解決を不要にする)
233
233
  // ノードの表示アイコン:プリセット・絵文字・画像 URL のいずれか一つ。
234
- // 未設定(省略)=既定の kind アイコン(docs/tasks/wip/ノードのアイコン(プログラム任意設定・HUMAN担当者の自動表示).md)。
234
+ // 未設定(省略)=既定の kind アイコン(docs/tasks/done/ノードのアイコン(プログラム任意設定・HUMAN担当者の自動表示).md)。
235
235
  // preset は契約上不透明な文字列(承認ビューと同じ方式):カタログはフロントが
236
236
  // 表示時に参照し、無い key は既定グリフへフォールバックする
237
237
  export const nodeIconSchema = z.union([
@@ -467,7 +467,7 @@ export const saveGraphBodySchema = z
467
467
 
468
468
  // fan-in(同じ in ポートへの流入が2本以上)は同じ型でなければならない。
469
469
  // 型が割れていると伝播が未宣言に落ちて下流の型チェックが効かなくなる
470
- // (docs/tasks/wip/ポートスキーマのany撲滅.md)
470
+ // (docs/tasks/done/ポートスキーマのany撲滅.md)
471
471
  const inboundByPort = new Map<string, string[]>()
472
472
  for (const conn of graph.connections) {
473
473
  const list = inboundByPort.get(conn.to_port_id) ?? []
@@ -514,7 +514,7 @@ export const saveGraphBodySchema = z
514
514
 
515
515
  // 差し戻し(reject)と instruction の組み合わせに kind の縛りも型固定も無い:
516
516
  // reject の型はビューの宣言、instruction は ai の in の1つ(接続鏡映)でしかない
517
- // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
517
+ // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
518
518
 
519
519
  // 型検証は kind によらず常に 伝播型(out) ⊆ 伝播型(in)。
520
520
  // 未宣言は unknown=判断できないので通す(作りかけのグラフを保存できなくしない)。
@@ -3,7 +3,7 @@ import { z } from "zod"
3
3
  // nodes/hook/<dir>/config.json(NodeIOHooks)。フック自身が発火イベント(on)と
4
4
  // 必要 env を宣言し、エディタがアタッチ時に読んで node_hooks へ焼き込む
5
5
  // (programManifestSchema と同型の「repo が正・編集時に取り込む」方式。
6
- // docs/tasks/wip/NodeIOHooks(ノードへのIOフックのアタッチ).md)。
6
+ // docs/tasks/done/NodeIOHooks(ノードへのIOフックのアタッチ).md)。
7
7
  // フックは任意のノードに付くため inputs / outputs のスキーマ宣言は持たない(unknown を受ける)。
8
8
  // signal は外部シグナル(signals API)での発火(NodeIOHooksのonSignal(外部シグナルでのフック発火).md)
9
9
  export const hookEventSchema = z.enum(["input", "output", "signal"])
package/_schemas/node.ts CHANGED
@@ -7,11 +7,11 @@ export const nodeKindSchema = z.enum([
7
7
  "guardrail",
8
8
  "wait",
9
9
  // 開始・終了ノード(恒等パススルー。run は必ず start から始まり end で終わる。
10
- // docs/tasks/wip/開始・終了ノードの定義.md)
10
+ // docs/tasks/done/開始・終了ノードの定義.md)
11
11
  "start",
12
12
  "end",
13
13
  // コンポーネント(kind='component' の loop を配置して呼ぶ。子 run・戻り値あり。
14
- // docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
14
+ // docs/tasks/done/コンポーネント(ループの部品化と再利用).md)
15
15
  "component",
16
16
  ])
17
17
  export type NodeKind = z.infer<typeof nodeKindSchema>
@@ -39,7 +39,7 @@ export const portSideSchema = z.enum(["top", "right", "bottom", "left"])
39
39
  export type PortSide = z.infer<typeof portSideSchema>
40
40
 
41
41
  // グラフ保存契約のポート(ports テーブルの1行。id はクライアント採番。
42
- // docs/tasks/wip/ポートのテーブル化.md)
42
+ // docs/tasks/done/ポートのテーブル化.md)
43
43
  export const graphPortSchema = z
44
44
  .object({
45
45
  id: z.uuid(),
@@ -13,6 +13,7 @@ import {
13
13
  propagateDerivedPorts,
14
14
  resolvePortSchemas,
15
15
  schemaFit,
16
+ schemaTypeKey,
16
17
  UNDECLARED_SCHEMA,
17
18
  } from "./port-spec.js"
18
19
 
@@ -140,7 +141,7 @@ describe("型伝播(導出ポートの解決)", () => {
140
141
 
141
142
  // human の in / approve / reject はビューの宣言をそのまま転写する(上流の kind は見ない)。
142
143
  // sdk はカタログを知らないので、呼び出し元が viewPortSchemasOf で解決して渡す
143
- // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
144
+ // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
144
145
  it("ai → human 直結(コピー宣言):in も approve も上流の out と同型、reject はビューの宣言型", () => {
145
146
  const aiOut = port("out", "main", declaredOut)
146
147
  const ai = {
@@ -888,4 +889,77 @@ describe("mergeInboundSchemas / hasInboundConflict(fan-in)", () => {
888
889
  )
889
890
  expect(hasInboundConflict([])).toBe(false)
890
891
  })
892
+
893
+ it("注釈(title / description)だけが違う流入は同じ型として合流できる", () => {
894
+ const answer = {
895
+ type: "object",
896
+ required: ["answer"],
897
+ properties: { answer: { type: "string", description: "顧客への回答" } },
898
+ } as const
899
+ const titled = { ...answer, title: "回答" }
900
+ const otherTitle = {
901
+ type: "object",
902
+ title: "要開発者確認",
903
+ required: ["answer"],
904
+ properties: { answer: { type: "string", description: "回答ドラフト" } },
905
+ } as const
906
+
907
+ expect(hasInboundConflict([titled, otherTitle])).toBe(false)
908
+ expect(mergeInboundSchemas([titled, otherTitle])).not.toEqual(
909
+ UNDECLARED_SCHEMA,
910
+ )
911
+ })
912
+
913
+ it("キー順・required の並びが違うだけの流入は同じ型として合流できる", () => {
914
+ const a = {
915
+ type: "object",
916
+ required: ["answer", "reasoning"],
917
+ properties: { answer: { type: "string" }, reasoning: { type: "string" } },
918
+ } as const
919
+ const b = {
920
+ properties: { reasoning: { type: "string" }, answer: { type: "string" } },
921
+ required: ["reasoning", "answer"],
922
+ type: "object",
923
+ } as const
924
+
925
+ expect(hasInboundConflict([a, b])).toBe(false)
926
+ expect(mergeInboundSchemas([a, b])).not.toEqual(UNDECLARED_SCHEMA)
927
+ })
928
+
929
+ it("合流の採用値は流入の順番に依らず決まる(保存ごとに注釈が揺れない)", () => {
930
+ const a = { type: "object", title: "あ", properties: {} } as const
931
+ const b = { type: "object", title: "い", properties: {} } as const
932
+ expect(mergeInboundSchemas([a, b])).toEqual(mergeInboundSchemas([b, a]))
933
+ })
934
+
935
+ it("構造が違えば注釈を無視しても衝突のまま", () => {
936
+ const a = { type: "object", properties: { x: { type: "string" } } } as const
937
+ const b = { type: "object", properties: { x: { type: "number" } } } as const
938
+ expect(hasInboundConflict([a, b])).toBe(true)
939
+ expect(mergeInboundSchemas([a, b])).toEqual(UNDECLARED_SCHEMA)
940
+ })
941
+
942
+ it("file マーカーは型の一部(注釈として落とさない)", () => {
943
+ expect(hasInboundConflict([fileSchema(), { type: "object" }])).toBe(true)
944
+ })
945
+ })
946
+
947
+ describe("schemaTypeKey(型としての同一性)", () => {
948
+ it("注釈だけの違いは同じキーになる", () => {
949
+ expect(schemaTypeKey({ type: "string", title: "回答" })).toBe(
950
+ schemaTypeKey({ type: "string", description: "根拠", title: "報告" }),
951
+ )
952
+ })
953
+
954
+ it("default は挙動(フォームの初期値)なので型の一部として残す", () => {
955
+ expect(schemaTypeKey({ type: "string", default: "a" })).not.toBe(
956
+ schemaTypeKey({ type: "string" }),
957
+ )
958
+ })
959
+
960
+ it("配列の要素順は保つ(items のタプルは意味を持つ)", () => {
961
+ expect(
962
+ schemaTypeKey({ type: "array", items: [{ type: "string" }] }),
963
+ ).not.toBe(schemaTypeKey({ type: "array", items: [{ type: "number" }] }))
964
+ })
891
965
  })
@@ -1,15 +1,15 @@
1
1
  import { z } from "zod"
2
2
  import type { GraphPort, JsonSchema, PortIo } from "./node.js"
3
3
 
4
- // ポートの kind 別型システム(docs/tasks/wip/ポートのkind別仕様.md)。
4
+ // ポートの kind 別型システム(docs/tasks/done/ポートのkind別仕様.md)。
5
5
  // 型の決まり方は2分類:宣言(program / ai の in・out。human はビューの宣言を転写)/
6
6
  // 導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)。
7
7
  // mawaru がコードで型を固定するポートは無い
8
- // (docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md)
8
+ // (docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md)
9
9
 
10
10
  // 「まだ型が決まっていない」=未宣言を {} で表す(未接続・上流が未宣言・fan-in の型割れ)。
11
11
  // 「何でも受ける」型は存在しない:program / ai の in は接続元の具体型を鏡映する
12
- // (docs/tasks/wip/ポートスキーマのany撲滅.md)
12
+ // (docs/tasks/done/ポートスキーマのany撲滅.md)
13
13
  export const UNDECLARED_SCHEMA: JsonSchema = {}
14
14
 
15
15
  export const isUndeclaredSchema = (schema: JsonSchema): boolean =>
@@ -20,7 +20,7 @@ export const isUndeclaredSchema = (schema: JsonSchema): boolean =>
20
20
  // ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
21
21
  // **file の位置は名前ではなくこのマーカーで決まる**:runner が宣言スキーマと値を並行走査して
22
22
  // 入力はワークスペースへ materialize(パスに置換)、出力はアップロード(参照に置換)する
23
- // (docs/tasks/wip/ポートスキーマ編集でファイル型を選べるようにする.md)。
23
+ // (docs/tasks/done/ポートスキーマ編集でファイル型を選べるようにする.md)。
24
24
  // ajv は strict:false で未知キーワードを無視するので検証は素の JSON Schema のまま。
25
25
  export const FILE_SCHEMA_MARKER = "x-mawaru-type"
26
26
  export const FILE_SCHEMA_TYPE = "file"
@@ -236,29 +236,60 @@ export const kindPortsViolation = (
236
236
  // 宣言 out(program / ai)は触らない。
237
237
  // program / ai の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
238
238
  // 上書きし、未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の
239
- // config.json へも書き戻される。docs/tasks/wip/ポート編集のrepo書き戻し同期.md)
239
+ // config.json へも書き戻される。docs/tasks/done/ポート編集のrepo書き戻し同期.md)
240
+
241
+ // ---- 型としての同一性(schemaTypeKey) ----
242
+ // 「同じ型か」を判定する唯一の物差し。合流(fan-in)・end の in の畳み込み・
243
+ // グループ化の入口・エディタの着地判定が全部これを使う。
244
+ // 素の JSON 等値だと、意味の同じ型が **注釈の差** や **キーの並び順** だけで別型に
245
+ // 見えて合流できなくなる(古い取り込みで ports.schema に残った title が典型)。
246
+ // 型=構造なので、人間向けの注釈は落とし、順序に意味の無い集合は並べ直して比べる。
247
+ // default は「フォームの初期値」という挙動なので型の一部として残す。
248
+ // x-mawaru-type(file の nominal マーカー)も当然残す
249
+ const ANNOTATION_KEYWORDS = new Set([
250
+ "title",
251
+ "description",
252
+ "$comment",
253
+ "examples",
254
+ "deprecated",
255
+ ])
256
+
257
+ // required は集合(並びに意味が無い)。properties のキー順は表示順として意味を
258
+ // 持ちうるが、型の同一性には効かせない
259
+ const canonicalize = (value: unknown, key?: string): unknown => {
260
+ if (Array.isArray(value)) {
261
+ const items = value.map((v) => canonicalize(v))
262
+ return key === "required" ? [...items].sort() : items
263
+ }
264
+ if (value === null || typeof value !== "object") return value
265
+ return Object.fromEntries(
266
+ Object.entries(value as Record<string, unknown>)
267
+ .filter(([k]) => !ANNOTATION_KEYWORDS.has(k))
268
+ .map(([k, v]) => [k, canonicalize(v, k)] as const)
269
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
270
+ )
271
+ }
272
+
273
+ export const schemaTypeKey = (schema: JsonSchema): string =>
274
+ JSON.stringify(canonicalize(schema))
240
275
 
241
276
  // 複数の流入(fan-in)から型を1つに決める:未宣言を除いて具体型が1種類ならその型、
242
277
  // 0種類なら未宣言。2種類以上は型が割れているので未宣言に落とす
243
- // (保存時に graph.ts が 422 で弾く。伝播は止めずに固定点へ収束させる)
278
+ // (保存時に graph.ts が 422 で弾く。伝播は止めずに固定点へ収束させる)。
279
+ // 注釈だけが違う流入は同じ型なので、採用する1つを JSON 等値で安定に選ぶ
280
+ // (接続の並び順で title が揺れて repo 書き戻しが空コミットを打たないように)
244
281
  export const mergeInboundSchemas = (schemas: JsonSchema[]): JsonSchema => {
245
- const uniq = [
246
- ...new Set(
247
- schemas
248
- .filter((s) => !isUndeclaredSchema(s))
249
- .map((s) => JSON.stringify(s)),
250
- ),
251
- ]
252
- return uniq.length === 1 && uniq[0] !== undefined
253
- ? (JSON.parse(uniq[0]) as JsonSchema)
254
- : UNDECLARED_SCHEMA
282
+ const declared = schemas.filter((s) => !isUndeclaredSchema(s))
283
+ if (new Set(declared.map(schemaTypeKey)).size !== 1) return UNDECLARED_SCHEMA
284
+ return declared
285
+ .map((s) => [JSON.stringify(s), s] as const)
286
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))[0]?.[1] as JsonSchema
255
287
  }
256
288
 
257
289
  // 流入の具体型が2種類以上あるか(=同じポートに違う型を流し込んでいる)
258
290
  export const hasInboundConflict = (schemas: JsonSchema[]): boolean =>
259
- new Set(
260
- schemas.filter((s) => !isUndeclaredSchema(s)).map((s) => JSON.stringify(s)),
261
- ).size > 1
291
+ new Set(schemas.filter((s) => !isUndeclaredSchema(s)).map(schemaTypeKey))
292
+ .size > 1
262
293
 
263
294
  type PropagationNode = {
264
295
  id: string
@@ -276,7 +307,7 @@ export type DeclaredPortSchema = JsonSchema | typeof COPY_UPSTREAM_SCHEMA
276
307
 
277
308
  // 伝播のオプション。sdk は承認ビューのカタログを知らない(非公開。企業向けビューの
278
309
  // キーが npm 公開物に載るのを避ける)ので、呼び出し元が解決して渡す
279
- // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
310
+ // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
280
311
  export type PropagationOptions<N> = {
281
312
  // human の in / approve / reject に転写する型(承認ビューの宣言)。null / undefined
282
313
  // ならその node は対象外(human 以外)。上流ノードの kind は一切見ない。
@@ -4,7 +4,7 @@ import { manifestInKeysViolation } from "./port-spec.js"
4
4
 
5
5
  // nodes/program/<dir>/config.json(リポジトリ規約 v2)。プログラム自身が入出力スキーマを宣言し、
6
6
  // エディタが取り込んで ports を自動生成する(実行時の正は ports テーブルのまま。
7
- // docs/tasks/wip/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
7
+ // docs/tasks/done/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
8
8
  // inputs / outputs は「ポート key → JSON Schema」のマップ。将来 join(in 複数)を導入しても
9
9
  // 形式が変わらないよう最初から複数形にしてある
10
10
  export const programManifestSchema = z
@@ -1,3 +1,4 @@
1
+ import { schemaTypeKey } from "./port-spec.js";
1
2
  export const analyzeExtractSelection = (nodes, connections, selectedIds) => {
2
3
  const selected = new Set(selectedIds);
3
4
  // 1ノードからコンポーネント化できる(グループ化の「2つ以上」はエディタ側の
@@ -32,7 +33,7 @@ export const analyzeExtractSelection = (nodes, connections, selectedIds) => {
32
33
  // 起点(どの外部ポートから入るか)は見ない。ノードが前のノードを知らないのと
33
34
  // 同じ思想を境界にも適用する(グループ化=入口の合流の宣言)
34
35
  const entryPortIds = [...new Set(inbound.map((c) => c.to_port_id))];
35
- const entrySchemas = new Set(entryPortIds.map((id) => JSON.stringify(portById.get(id)?.schema ?? {})));
36
+ const entrySchemas = new Set(entryPortIds.map((id) => schemaTypeKey(portById.get(id)?.schema ?? {})));
36
37
  if (entrySchemas.size > 1) {
37
38
  return {
38
39
  ok: false,
@@ -72,7 +72,7 @@ export const waitConfigSchema = z.discriminatedUnion("wait_type", [
72
72
  // Program ノードの kind 別設定(node_programs に対応)。実行先の repo はノード設定でなく
73
73
  // installation から `${account_login}/${GITHUB_REPO_NAME}` に固定(backend)。program_dir は nodes/program/ 直下のディレクトリ名
74
74
  // (例: sendReply。日本語可。エントリは main.ts 固定。リポジトリ規約 v2 は
75
- // docs/tasks/wip/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
75
+ // docs/tasks/done/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
76
76
  // ref 省略時はデフォルトブランチ
77
77
  export const programConfigSchema = z.object({
78
78
  program_dir: z
@@ -93,7 +93,7 @@ export const aiConfigSchema = z.object({
93
93
  });
94
94
  // 担当ユニットのメンバー:user_id の名指し、または動的な担当「ループを実行した人」
95
95
  // (run の起動者。実行時に runs.start_by で解決し、解決できなければその1人だけ落ちる。
96
- // docs/tasks/wip/Human担当に「ループを実行した人」を追加する.md)
96
+ // docs/tasks/done/Human担当に「ループを実行した人」を追加する.md)
97
97
  export const RUN_STARTER = "run_starter";
98
98
  export const assignMemberSchema = z.union([z.uuid(), z.literal(RUN_STARTER)]);
99
99
  // Human ノードの kind 別設定(node_humans + node_human_assignees に対応)。
@@ -153,7 +153,7 @@ export const humanConfigSchema = z.object({
153
153
  });
154
154
  // コンポーネントノードの kind 別設定(node_components に対応)。呼ぶ先は
155
155
  // kind='component' の loop。同 tenant・kind の検証と自 loop 参照の禁止は保存 API 側で行う
156
- // (docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
156
+ // (docs/tasks/done/コンポーネント(ループの部品化と再利用).md)
157
157
  export const componentConfigSchema = z.object({
158
158
  component_loop_id: z.uuid(),
159
159
  // 配置の設定値:子 loop の refs 定義(default/required)への上書き。
@@ -164,7 +164,7 @@ export const componentConfigSchema = z.object({
164
164
  // ノードにアタッチされた IOHook(node_hooks に対応。アタッチできるのは program ノードのみ)。
165
165
  // on_input / on_output / on_signal はアタッチ時に repo の config.json(hookManifestSchema)から
166
166
  // エディタが焼き込むスナップショット。ref 省略時はデフォルトブランチ
167
- // (docs/tasks/wip/NodeIOHooks(ノードへのIOフックのアタッチ).md・
167
+ // (docs/tasks/done/NodeIOHooks(ノードへのIOフックのアタッチ).md・
168
168
  // NodeIOHooksのonSignal(外部シグナルでのフック発火).md)
169
169
  export const graphNodeHookSchema = z.object({
170
170
  hook_dir: z
@@ -179,7 +179,7 @@ export const graphNodeHookSchema = z.object({
179
179
  });
180
180
  // 新規ノードの id・ポートの id もクライアントが採番する(一時IDの解決を不要にする)
181
181
  // ノードの表示アイコン:プリセット・絵文字・画像 URL のいずれか一つ。
182
- // 未設定(省略)=既定の kind アイコン(docs/tasks/wip/ノードのアイコン(プログラム任意設定・HUMAN担当者の自動表示).md)。
182
+ // 未設定(省略)=既定の kind アイコン(docs/tasks/done/ノードのアイコン(プログラム任意設定・HUMAN担当者の自動表示).md)。
183
183
  // preset は契約上不透明な文字列(承認ビューと同じ方式):カタログはフロントが
184
184
  // 表示時に参照し、無い key は既定グリフへフォールバックする
185
185
  export const nodeIconSchema = z.union([
@@ -394,7 +394,7 @@ export const saveGraphBodySchema = z
394
394
  const resolved = resolvePortSchemas(graph.nodes, graph.connections);
395
395
  // fan-in(同じ in ポートへの流入が2本以上)は同じ型でなければならない。
396
396
  // 型が割れていると伝播が未宣言に落ちて下流の型チェックが効かなくなる
397
- // (docs/tasks/wip/ポートスキーマのany撲滅.md)
397
+ // (docs/tasks/done/ポートスキーマのany撲滅.md)
398
398
  const inboundByPort = new Map();
399
399
  for (const conn of graph.connections) {
400
400
  const list = inboundByPort.get(conn.to_port_id) ?? [];
@@ -439,7 +439,7 @@ export const saveGraphBodySchema = z
439
439
  }
440
440
  // 差し戻し(reject)と instruction の組み合わせに kind の縛りも型固定も無い:
441
441
  // reject の型はビューの宣言、instruction は ai の in の1つ(接続鏡映)でしかない
442
- // → docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md
442
+ // → docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md
443
443
  // 型検証は kind によらず常に 伝播型(out) ⊆ 伝播型(in)。
444
444
  // 未宣言は unknown=判断できないので通す(作りかけのグラフを保存できなくしない)。
445
445
  // 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  // nodes/hook/<dir>/config.json(NodeIOHooks)。フック自身が発火イベント(on)と
3
3
  // 必要 env を宣言し、エディタがアタッチ時に読んで node_hooks へ焼き込む
4
4
  // (programManifestSchema と同型の「repo が正・編集時に取り込む」方式。
5
- // docs/tasks/wip/NodeIOHooks(ノードへのIOフックのアタッチ).md)。
5
+ // docs/tasks/done/NodeIOHooks(ノードへのIOフックのアタッチ).md)。
6
6
  // フックは任意のノードに付くため inputs / outputs のスキーマ宣言は持たない(unknown を受ける)。
7
7
  // signal は外部シグナル(signals API)での発火(NodeIOHooksのonSignal(外部シグナルでのフック発火).md)
8
8
  export const hookEventSchema = z.enum(["input", "output", "signal"]);
@@ -6,11 +6,11 @@ export const nodeKindSchema = z.enum([
6
6
  "guardrail",
7
7
  "wait",
8
8
  // 開始・終了ノード(恒等パススルー。run は必ず start から始まり end で終わる。
9
- // docs/tasks/wip/開始・終了ノードの定義.md)
9
+ // docs/tasks/done/開始・終了ノードの定義.md)
10
10
  "start",
11
11
  "end",
12
12
  // コンポーネント(kind='component' の loop を配置して呼ぶ。子 run・戻り値あり。
13
- // docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
13
+ // docs/tasks/done/コンポーネント(ループの部品化と再利用).md)
14
14
  "component",
15
15
  ]);
16
16
  // ポート key(input/output 用の参照キー)の規則(db.md §2)。
@@ -28,7 +28,7 @@ export const portIoSchema = z.enum(["in", "out"]);
28
28
  // 未指定は自動配置(先頭=右辺中央・以降=下辺等間隔)
29
29
  export const portSideSchema = z.enum(["top", "right", "bottom", "left"]);
30
30
  // グラフ保存契約のポート(ports テーブルの1行。id はクライアント採番。
31
- // docs/tasks/wip/ポートのテーブル化.md)
31
+ // docs/tasks/done/ポートのテーブル化.md)
32
32
  export const graphPortSchema = z
33
33
  .object({
34
34
  id: z.uuid(),
@@ -47,6 +47,7 @@ export declare const dataFormatOf: (schema: JsonSchema) => DataFormat | null;
47
47
  export declare const fixedInKeysOf: (kind: string) => string[] | null;
48
48
  export declare const manifestInKeysViolation: (kind: string, keys: string[]) => string | null;
49
49
  export declare const kindPortsViolation: (kind: string, ports: Pick<GraphPort, "io" | "key">[]) => string | null;
50
+ export declare const schemaTypeKey: (schema: JsonSchema) => string;
50
51
  export declare const mergeInboundSchemas: (schemas: JsonSchema[]) => JsonSchema;
51
52
  export declare const hasInboundConflict: (schemas: JsonSchema[]) => boolean;
52
53
  type PropagationNode = {
@@ -1,12 +1,12 @@
1
1
  import { z } from "zod";
2
- // ポートの kind 別型システム(docs/tasks/wip/ポートのkind別仕様.md)。
2
+ // ポートの kind 別型システム(docs/tasks/done/ポートのkind別仕様.md)。
3
3
  // 型の決まり方は2分類:宣言(program / ai の in・out。human はビューの宣言を転写)/
4
4
  // 導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)。
5
5
  // mawaru がコードで型を固定するポートは無い
6
- // (docs/tasks/wip/ポート型固定の撤去とセッション再開のポート属性化.md)
6
+ // (docs/tasks/done/ポート型固定の撤去とセッション再開のポート属性化.md)
7
7
  // 「まだ型が決まっていない」=未宣言を {} で表す(未接続・上流が未宣言・fan-in の型割れ)。
8
8
  // 「何でも受ける」型は存在しない:program / ai の in は接続元の具体型を鏡映する
9
- // (docs/tasks/wip/ポートスキーマのany撲滅.md)
9
+ // (docs/tasks/done/ポートスキーマのany撲滅.md)
10
10
  export const UNDECLARED_SCHEMA = {};
11
11
  export const isUndeclaredSchema = (schema) => Object.keys(schema).length === 0 || schema.type === undefined;
12
12
  // ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
@@ -14,7 +14,7 @@ export const isUndeclaredSchema = (schema) => Object.keys(schema).length === 0 |
14
14
  // ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
15
15
  // **file の位置は名前ではなくこのマーカーで決まる**:runner が宣言スキーマと値を並行走査して
16
16
  // 入力はワークスペースへ materialize(パスに置換)、出力はアップロード(参照に置換)する
17
- // (docs/tasks/wip/ポートスキーマ編集でファイル型を選べるようにする.md)。
17
+ // (docs/tasks/done/ポートスキーマ編集でファイル型を選べるようにする.md)。
18
18
  // ajv は strict:false で未知キーワードを無視するので検証は素の JSON Schema のまま。
19
19
  export const FILE_SCHEMA_MARKER = "x-mawaru-type";
20
20
  export const FILE_SCHEMA_TYPE = "file";
@@ -199,22 +199,53 @@ export const kindPortsViolation = (kind, ports) => {
199
199
  // 宣言 out(program / ai)は触らない。
200
200
  // program / ai の in は**全ての口が**接続鏡映(sticky):接続元 out が具体型のときだけ
201
201
  // 上書きし、未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の
202
- // config.json へも書き戻される。docs/tasks/wip/ポート編集のrepo書き戻し同期.md)
202
+ // config.json へも書き戻される。docs/tasks/done/ポート編集のrepo書き戻し同期.md)
203
+ // ---- 型としての同一性(schemaTypeKey) ----
204
+ // 「同じ型か」を判定する唯一の物差し。合流(fan-in)・end の in の畳み込み・
205
+ // グループ化の入口・エディタの着地判定が全部これを使う。
206
+ // 素の JSON 等値だと、意味の同じ型が **注釈の差** や **キーの並び順** だけで別型に
207
+ // 見えて合流できなくなる(古い取り込みで ports.schema に残った title が典型)。
208
+ // 型=構造なので、人間向けの注釈は落とし、順序に意味の無い集合は並べ直して比べる。
209
+ // default は「フォームの初期値」という挙動なので型の一部として残す。
210
+ // x-mawaru-type(file の nominal マーカー)も当然残す
211
+ const ANNOTATION_KEYWORDS = new Set([
212
+ "title",
213
+ "description",
214
+ "$comment",
215
+ "examples",
216
+ "deprecated",
217
+ ]);
218
+ // required は集合(並びに意味が無い)。properties のキー順は表示順として意味を
219
+ // 持ちうるが、型の同一性には効かせない
220
+ const canonicalize = (value, key) => {
221
+ if (Array.isArray(value)) {
222
+ const items = value.map((v) => canonicalize(v));
223
+ return key === "required" ? [...items].sort() : items;
224
+ }
225
+ if (value === null || typeof value !== "object")
226
+ return value;
227
+ return Object.fromEntries(Object.entries(value)
228
+ .filter(([k]) => !ANNOTATION_KEYWORDS.has(k))
229
+ .map(([k, v]) => [k, canonicalize(v, k)])
230
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
231
+ };
232
+ export const schemaTypeKey = (schema) => JSON.stringify(canonicalize(schema));
203
233
  // 複数の流入(fan-in)から型を1つに決める:未宣言を除いて具体型が1種類ならその型、
204
234
  // 0種類なら未宣言。2種類以上は型が割れているので未宣言に落とす
205
- // (保存時に graph.ts が 422 で弾く。伝播は止めずに固定点へ収束させる)
235
+ // (保存時に graph.ts が 422 で弾く。伝播は止めずに固定点へ収束させる)。
236
+ // 注釈だけが違う流入は同じ型なので、採用する1つを JSON 等値で安定に選ぶ
237
+ // (接続の並び順で title が揺れて repo 書き戻しが空コミットを打たないように)
206
238
  export const mergeInboundSchemas = (schemas) => {
207
- const uniq = [
208
- ...new Set(schemas
209
- .filter((s) => !isUndeclaredSchema(s))
210
- .map((s) => JSON.stringify(s))),
211
- ];
212
- return uniq.length === 1 && uniq[0] !== undefined
213
- ? JSON.parse(uniq[0])
214
- : UNDECLARED_SCHEMA;
239
+ const declared = schemas.filter((s) => !isUndeclaredSchema(s));
240
+ if (new Set(declared.map(schemaTypeKey)).size !== 1)
241
+ return UNDECLARED_SCHEMA;
242
+ return declared
243
+ .map((s) => [JSON.stringify(s), s])
244
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))[0]?.[1];
215
245
  };
216
246
  // 流入の具体型が2種類以上あるか(=同じポートに違う型を流し込んでいる)
217
- export const hasInboundConflict = (schemas) => new Set(schemas.filter((s) => !isUndeclaredSchema(s)).map((s) => JSON.stringify(s))).size > 1;
247
+ export const hasInboundConflict = (schemas) => new Set(schemas.filter((s) => !isUndeclaredSchema(s)).map(schemaTypeKey))
248
+ .size > 1;
218
249
  // 「上流の out をそのままコピーする」(in)/「in と同型」(approve)を表す指示。
219
250
  // 承認ビューのカタログ(@mawaru/common)の COPY_UPSTREAM と同じ値
220
251
  export const COPY_UPSTREAM_SCHEMA = "T";
@@ -3,7 +3,7 @@ import { jsonSchemaSchema, portKeySchema } from "./node.js";
3
3
  import { manifestInKeysViolation } from "./port-spec.js";
4
4
  // nodes/program/<dir>/config.json(リポジトリ規約 v2)。プログラム自身が入出力スキーマを宣言し、
5
5
  // エディタが取り込んで ports を自動生成する(実行時の正は ports テーブルのまま。
6
- // docs/tasks/wip/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
6
+ // docs/tasks/done/プログラムのリポジトリ規約v2(ディレクトリ+config.json).md)。
7
7
  // inputs / outputs は「ポート key → JSON Schema」のマップ。将来 join(in 複数)を導入しても
8
8
  // 形式が変わらないよう最初から複数形にしてある
9
9
  export const programManifestSchema = z
package/dist/typegen.js CHANGED
@@ -78,6 +78,13 @@ export const tsTypeFromJsonSchema = (schema, indent = 0) => {
78
78
  if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
79
79
  return "unknown";
80
80
  }
81
+ // file 型(x-mawaru-type マーカー):runner が実行前に参照を実体化し、その位置を
82
+ // ワークスペースのローカルパス(文字列)に置き換えて handler へ渡す/handler は
83
+ // 出力にローカルパスを書く契約なので、参照 object ではなく string を生成する
84
+ // (AI 側の schemaWithFilesAsPath と同じ規約)
85
+ if (schema["x-mawaru-type"] === "file") {
86
+ return "string";
87
+ }
81
88
  if ("const" in schema) {
82
89
  return literalType(schema.const);
83
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mawaru/sdk",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "mawaru 実行 repo の開発 SDK。契約スキーマ(config.json 規約・ループ graph API)の正 + typegen / validate CLI",
5
5
  "type": "module",
6
6
  "bin": {