@mawaru/sdk 0.5.0 → 0.9.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.
- package/_schemas/ai-manifest.test.ts +17 -2
- package/_schemas/ai-manifest.ts +9 -9
- package/_schemas/extract-component.test.ts +220 -0
- package/_schemas/extract-component.ts +224 -0
- package/_schemas/graph.test.ts +232 -12
- package/_schemas/graph.ts +121 -13
- package/_schemas/node.ts +3 -0
- package/_schemas/port-spec.test.ts +190 -37
- package/_schemas/port-spec.ts +165 -58
- package/_schemas/program-manifest.ts +9 -8
- package/dist/_schemas/ai-manifest.js +6 -9
- package/dist/_schemas/extract-component.d.ts +35 -0
- package/dist/_schemas/extract-component.js +146 -0
- package/dist/_schemas/graph.d.ts +25 -3
- package/dist/_schemas/graph.js +100 -14
- package/dist/_schemas/node.d.ts +1 -0
- package/dist/_schemas/node.js +3 -0
- package/dist/_schemas/port-spec.d.ts +10 -2
- package/dist/_schemas/port-spec.js +136 -54
- package/dist/_schemas/program-manifest.js +6 -8
- package/dist/cli.js +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +3 -0
- package/dist/typegen.js +8 -1
- package/docs/development.md +9 -2
- package/index.ts +1 -0
- package/package.json +11 -17
- package/templates/gitignore +4 -0
package/_schemas/port-spec.ts
CHANGED
|
@@ -5,17 +5,21 @@ import type { GraphPort, JsonSchema, PortIo } from "./node.js"
|
|
|
5
5
|
// 型の決まり方は3分類:宣言(program / AI の in・out)/固定(human reject の
|
|
6
6
|
// content 型)/導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// 「まだ型が決まっていない」=未宣言を {} で表す(未接続・上流が未宣言・fan-in の型割れ)。
|
|
9
|
+
// 「何でも受ける」型は存在しない:AI の in は接続元の具体型を鏡映し、差し戻しの指示は
|
|
10
|
+
// instruction ポートの宣言型を持つ(docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
11
|
+
export const UNDECLARED_SCHEMA: JsonSchema = {}
|
|
11
12
|
|
|
12
|
-
export const
|
|
13
|
+
export const isUndeclaredSchema = (schema: JsonSchema): boolean =>
|
|
13
14
|
Object.keys(schema).length === 0 || schema.type === undefined
|
|
14
15
|
|
|
15
|
-
// ---- file 型(
|
|
16
|
+
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
16
17
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
17
|
-
// ではなく storage path
|
|
18
|
-
//
|
|
18
|
+
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
|
19
|
+
// **file の位置は名前ではなくこのマーカーで決まる**:runner が宣言スキーマと値を並行走査して
|
|
20
|
+
// 入力はワークスペースへ materialize(パスに置換)、出力はアップロード(参照に置換)する
|
|
21
|
+
// (docs/tasks/wip/ポートスキーマ編集でファイル型を選べるようにする.md)。
|
|
22
|
+
// ajv は strict:false で未知キーワードを無視するので検証は素の JSON Schema のまま。
|
|
19
23
|
export const FILE_SCHEMA_MARKER = "x-mawaru-type"
|
|
20
24
|
export const FILE_SCHEMA_TYPE = "file"
|
|
21
25
|
|
|
@@ -34,6 +38,27 @@ export const fileSchema = (): JsonSchema => ({
|
|
|
34
38
|
export const isFileSchema = (schema: JsonSchema): boolean =>
|
|
35
39
|
schema[FILE_SCHEMA_MARKER] === FILE_SCHEMA_TYPE
|
|
36
40
|
|
|
41
|
+
// ---- 差し戻しの指示(AI の instruction ポート / human の reject ポート) ----
|
|
42
|
+
// AI 入力の既存エンベロープ { prompt, files } と同じ形にする。配管が効く根拠は
|
|
43
|
+
// files が file 型として宣言されていることであって、キー名ではない
|
|
44
|
+
// (runner がマーカーで位置を見つけて materialize する)。
|
|
45
|
+
// スクリーンショットを添えて差し戻すケースがあるので文章だけにしない
|
|
46
|
+
// (docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
47
|
+
export const AI_INSTRUCTION_PORT_KEY = "instruction"
|
|
48
|
+
|
|
49
|
+
export const instructionSchema = (): JsonSchema => ({
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
prompt: { type: "string", description: "差し戻しの理由(AI への指示)" },
|
|
53
|
+
files: {
|
|
54
|
+
type: "array",
|
|
55
|
+
items: fileSchema(),
|
|
56
|
+
description: "スクリーンショットなどの参考資料",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
required: ["prompt"],
|
|
60
|
+
})
|
|
61
|
+
|
|
37
62
|
// ---- データフォーマットカタログ ----
|
|
38
63
|
// human 承認 UI の表示・編集フォームの出し分けに使う(テキスト=string(markdown)/
|
|
39
64
|
// リスト=Array<Row>(列定義つき))。バリデーションには使わない(data は任意の型)
|
|
@@ -154,10 +179,42 @@ const FIXED_PORT_KEYS: Record<string, { in: string[]; out: string[] }> = {
|
|
|
154
179
|
wait: { in: ["in"], out: ["main"] },
|
|
155
180
|
// start は in 1つ(run 入力契約)+ out main 1つの恒等パススルー
|
|
156
181
|
start: { in: ["in"], out: ["main"] },
|
|
182
|
+
// component は in(子 loop の start.in 契約)+ out main(end.out の戻り値)。
|
|
183
|
+
// スキーマは保存時にサーバが子 loop の契約から焼き込む宣言型
|
|
184
|
+
// (DERIVED/MIRROR には入れない=接続で潰さない)
|
|
185
|
+
component: { in: ["in"], out: ["main"] },
|
|
157
186
|
}
|
|
158
187
|
|
|
159
188
|
const FREE_OUT_KINDS = new Set(["program", "ai"])
|
|
160
189
|
|
|
190
|
+
// out は自由(複数出口=分岐)だが in の構成は固定。ai は上流データを受ける in と、
|
|
191
|
+
// 差し戻しの指示を受ける instruction の2口(意味の違う入力を1つのポートに
|
|
192
|
+
// fan-in させない。docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
193
|
+
const FIXED_IN_KEYS: Record<string, string[]> = {
|
|
194
|
+
program: ["in"],
|
|
195
|
+
ai: ["in", AI_INSTRUCTION_PORT_KEY],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const fixedInKeysOf = (kind: string): string[] | null =>
|
|
199
|
+
FIXED_IN_KEYS[kind] ?? null
|
|
200
|
+
|
|
201
|
+
// config.json(manifest)の inputs 検査(適合なら null)。同じルールを二重に持たないため
|
|
202
|
+
// manifest 側はここを参照する(docs/tasks/wip/manifestのin複数対応(instructionポート).md)。
|
|
203
|
+
// **必須は "in" だけ**で、kind が固定する他の in(ai の instruction)は任意:スキーマは
|
|
204
|
+
// constSchemaOf が焼き込む固定型なのでエディタ側で補え、in を2口に分ける前に同期された
|
|
205
|
+
// config.json もそのまま読める
|
|
206
|
+
export const manifestInKeysViolation = (
|
|
207
|
+
kind: string,
|
|
208
|
+
keys: string[],
|
|
209
|
+
): string | null => {
|
|
210
|
+
const allowed = fixedInKeysOf(kind) ?? ["in"]
|
|
211
|
+
if (!keys.includes("in")) return 'inputs は "in" を宣言してください'
|
|
212
|
+
const unknown = keys.filter((key) => !allowed.includes(key))
|
|
213
|
+
return unknown.length > 0
|
|
214
|
+
? `inputs に宣言できない key があります: ${unknown.join(" / ")}(${allowed.join(" / ")} のみ)`
|
|
215
|
+
: null
|
|
216
|
+
}
|
|
217
|
+
|
|
161
218
|
// kind 別のポート構成違反を返す(適合なら null)。in=1/out>=1 の構造ルールは
|
|
162
219
|
// graph.ts 側の既存チェックが担い、ここは key の固定だけ見る
|
|
163
220
|
export const kindPortsViolation = (
|
|
@@ -178,12 +235,17 @@ export const kindPortsViolation = (
|
|
|
178
235
|
}
|
|
179
236
|
const fixed = FIXED_PORT_KEYS[kind]
|
|
180
237
|
if (!fixed) {
|
|
181
|
-
// program / ai(自由 out):in
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
238
|
+
// program / ai(自由 out):in の構成だけ固定
|
|
239
|
+
const expectIn = FIXED_IN_KEYS[kind]
|
|
240
|
+
if (FREE_OUT_KINDS.has(kind) && expectIn) {
|
|
241
|
+
const inKeys = ports
|
|
242
|
+
.filter((p) => p.io === "in")
|
|
243
|
+
.map((p) => p.key)
|
|
244
|
+
.sort()
|
|
245
|
+
if (inKeys.join(",") !== [...expectIn].sort().join(",")) {
|
|
246
|
+
return `${kind} の in ポートは ${expectIn.join(" / ")} です`
|
|
247
|
+
}
|
|
248
|
+
}
|
|
187
249
|
return null
|
|
188
250
|
}
|
|
189
251
|
const keysOf = (io: PortIo) =>
|
|
@@ -205,11 +267,34 @@ export const kindPortsViolation = (
|
|
|
205
267
|
// 接続の両端は同じスキーマになる(値入力フォームのSchemaForm統一.md)。
|
|
206
268
|
// wait / human は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
|
|
207
269
|
// 例外は human の approve:接続元が AI ノードのときだけ封筒を剥がして data 型になる。
|
|
208
|
-
// human の reject
|
|
270
|
+
// human の reject は未宣言のまま(段階2 で instruction エンベロープを宣言する)。
|
|
271
|
+
// 宣言 out(program / ai)は触らない。
|
|
209
272
|
// program / ai の in は接続鏡映(sticky):接続元 out が具体型のときだけ上書きし、
|
|
210
|
-
//
|
|
273
|
+
// 未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の config.json
|
|
211
274
|
// へも書き戻される。docs/tasks/wip/ポート編集のrepo書き戻し同期.md)
|
|
212
275
|
|
|
276
|
+
// 複数の流入(fan-in)から型を1つに決める:未宣言を除いて具体型が1種類ならその型、
|
|
277
|
+
// 0種類なら未宣言。2種類以上は型が割れているので未宣言に落とす
|
|
278
|
+
// (保存時に graph.ts が 422 で弾く。伝播は止めずに固定点へ収束させる)
|
|
279
|
+
export const mergeInboundSchemas = (schemas: JsonSchema[]): JsonSchema => {
|
|
280
|
+
const uniq = [
|
|
281
|
+
...new Set(
|
|
282
|
+
schemas
|
|
283
|
+
.filter((s) => !isUndeclaredSchema(s))
|
|
284
|
+
.map((s) => JSON.stringify(s)),
|
|
285
|
+
),
|
|
286
|
+
]
|
|
287
|
+
return uniq.length === 1 && uniq[0] !== undefined
|
|
288
|
+
? (JSON.parse(uniq[0]) as JsonSchema)
|
|
289
|
+
: UNDECLARED_SCHEMA
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 流入の具体型が2種類以上あるか(=同じポートに違う型を流し込んでいる)
|
|
293
|
+
export const hasInboundConflict = (schemas: JsonSchema[]): boolean =>
|
|
294
|
+
new Set(
|
|
295
|
+
schemas.filter((s) => !isUndeclaredSchema(s)).map((s) => JSON.stringify(s)),
|
|
296
|
+
).size > 1
|
|
297
|
+
|
|
213
298
|
type PropagationNode = {
|
|
214
299
|
id: string
|
|
215
300
|
kind: string
|
|
@@ -217,8 +302,8 @@ type PropagationNode = {
|
|
|
217
302
|
}
|
|
218
303
|
type PortLink = { from_port_id: string; to_port_id: string }
|
|
219
304
|
|
|
220
|
-
// end も導出ノード(各 in は上流から、out main
|
|
221
|
-
// resolvePortSchemas
|
|
305
|
+
// end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
|
|
306
|
+
// resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
|
|
222
307
|
// start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
|
|
223
308
|
// (run 入力契約=接続先の入力宣言。開始ノードの入力契約を接続先から導出.md)
|
|
224
309
|
const DERIVED_KINDS = new Set(["wait", "human", "end", "start"])
|
|
@@ -233,11 +318,27 @@ const isDerivedPort = (kind: string, port: Pick<GraphPort, "io" | "key">) =>
|
|
|
233
318
|
// AI の in は nodes/ai/<dir>/config.json の inputs.in 由来の宣言型が初期値で、
|
|
234
319
|
// 接続時は program と同じ鏡映で接続元と同値に同期される(const 扱い廃止。
|
|
235
320
|
// 接続バリデーションの「ai の in は常に許可」は維持)
|
|
236
|
-
|
|
237
|
-
|
|
321
|
+
// 宣言型で固定されるポート(導出も鏡映もしない)。差し戻しの指示エンベロープを
|
|
322
|
+
// human の出口と AI の入口の両方に焼き込むことで、両端が同じ具体型で一致する
|
|
323
|
+
const constSchemaOf = (
|
|
324
|
+
kind: string,
|
|
325
|
+
port: Pick<GraphPort, "io" | "key">,
|
|
326
|
+
): JsonSchema | null => {
|
|
327
|
+
if (kind === "human" && port.io === "out" && port.key === "reject") {
|
|
328
|
+
return instructionSchema()
|
|
329
|
+
}
|
|
330
|
+
if (
|
|
331
|
+
kind === "ai" &&
|
|
332
|
+
port.io === "in" &&
|
|
333
|
+
port.key === AI_INSTRUCTION_PORT_KEY
|
|
334
|
+
) {
|
|
335
|
+
return instructionSchema()
|
|
336
|
+
}
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
238
339
|
|
|
239
340
|
// port_id → 解決済みスキーマ。導出は接続を遡って固定点まで反復し、
|
|
240
|
-
//
|
|
341
|
+
// 未接続・純パラメトリック閉路・fan-in の型割れは未宣言に落とす
|
|
241
342
|
export const resolvePortSchemas = <N extends PropagationNode>(
|
|
242
343
|
nodes: N[],
|
|
243
344
|
connections: PortLink[],
|
|
@@ -247,11 +348,11 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
247
348
|
for (const node of nodes) {
|
|
248
349
|
for (const port of node.ports) {
|
|
249
350
|
kindByPortId.set(port.id, node.kind)
|
|
351
|
+
const constSchema = constSchemaOf(node.kind, port)
|
|
250
352
|
resolved.set(
|
|
251
353
|
port.id,
|
|
252
|
-
|
|
253
|
-
?
|
|
254
|
-
: port.schema,
|
|
354
|
+
constSchema ??
|
|
355
|
+
(isDerivedPort(node.kind, port) ? UNDECLARED_SCHEMA : port.schema),
|
|
255
356
|
)
|
|
256
357
|
}
|
|
257
358
|
}
|
|
@@ -278,7 +379,7 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
278
379
|
}
|
|
279
380
|
}
|
|
280
381
|
// start:out main は接続先 in の解決値を転写(逆向き導出)。fan-out は
|
|
281
|
-
//
|
|
382
|
+
// 非未宣言が1種類(JSON 等値)のときだけ採用し、未接続・型割れは未宣言。
|
|
282
383
|
// in は out main と同型(恒等の向きを out→in に逆転。run 入力契約は
|
|
283
384
|
// 接続先の入力宣言から自動で決まる)
|
|
284
385
|
if (node.kind === "start") {
|
|
@@ -287,47 +388,43 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
287
388
|
(p) => p.io === "out" && p.key === "main",
|
|
288
389
|
)
|
|
289
390
|
if (inPort && outMain) {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const schema =
|
|
296
|
-
uniq.length === 1 && uniq[0] !== undefined
|
|
297
|
-
? (JSON.parse(uniq[0]) as JsonSchema)
|
|
298
|
-
: ANY_SCHEMA
|
|
391
|
+
const schema = mergeInboundSchemas(
|
|
392
|
+
(outbound.get(outMain.id) ?? []).map(
|
|
393
|
+
(id) => resolved.get(id) ?? UNDECLARED_SCHEMA,
|
|
394
|
+
),
|
|
395
|
+
)
|
|
299
396
|
assign(outMain.id, schema)
|
|
300
397
|
assign(inPort.id, schema)
|
|
301
398
|
}
|
|
302
399
|
continue
|
|
303
400
|
}
|
|
304
401
|
// end:各 in を自分の単一上流から導出(複数ルートを別 in で集約)。
|
|
305
|
-
// out main
|
|
402
|
+
// out main は未宣言固定(ルートごとに型が違いうる。Loop-in-Loop で詰める)
|
|
306
403
|
if (node.kind === "end") {
|
|
307
404
|
for (const port of node.ports) {
|
|
308
405
|
if (port.io !== "in") continue
|
|
309
406
|
const srcs = inbound.get(port.id) ?? []
|
|
310
407
|
assign(
|
|
311
408
|
port.id,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
409
|
+
mergeInboundSchemas(
|
|
410
|
+
srcs.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
|
|
411
|
+
),
|
|
315
412
|
)
|
|
316
413
|
}
|
|
317
414
|
continue
|
|
318
415
|
}
|
|
319
416
|
const isMirror = MIRROR_KINDS.has(node.kind)
|
|
320
417
|
if (!DERIVED_KINDS.has(node.kind) && !isMirror) continue
|
|
321
|
-
|
|
418
|
+
// ai は in が2口あるので、上流データの口(key="in")だけを鏡映・導出の対象にする
|
|
419
|
+
const inPort = node.ports.find((p) => p.io === "in" && p.key === "in")
|
|
322
420
|
if (!inPort) continue
|
|
323
421
|
const sources = inbound.get(inPort.id) ?? []
|
|
324
|
-
const inSchema =
|
|
325
|
-
sources.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// 鏡映(program)は具体型が流れてきたときだけ上書き(宣言を any で潰さない)
|
|
422
|
+
const inSchema = mergeInboundSchemas(
|
|
423
|
+
sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
|
|
424
|
+
)
|
|
425
|
+
// 鏡映(program / ai)は具体型が流れてきたときだけ上書き(宣言を未宣言で潰さない)
|
|
329
426
|
if (isMirror) {
|
|
330
|
-
if (!
|
|
427
|
+
if (!isUndeclaredSchema(inSchema)) assign(inPort.id, inSchema)
|
|
331
428
|
continue
|
|
332
429
|
}
|
|
333
430
|
assign(inPort.id, inSchema)
|
|
@@ -342,7 +439,7 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
342
439
|
assign(
|
|
343
440
|
port.id,
|
|
344
441
|
node.kind === "human" && port.key === "approve" && fromAiNode
|
|
345
|
-
? (envelopeDataSchemaOf(inSchema) ??
|
|
442
|
+
? (envelopeDataSchemaOf(inSchema) ?? UNDECLARED_SCHEMA)
|
|
346
443
|
: inSchema,
|
|
347
444
|
)
|
|
348
445
|
}
|
|
@@ -373,33 +470,43 @@ export const propagateDerivedPorts = <N extends PropagationNode>(
|
|
|
373
470
|
}
|
|
374
471
|
|
|
375
472
|
// ---- 接続バリデーション(伝播型 ⊆ 宣言スキーマ) ----
|
|
376
|
-
// 完全な JSON Schema 包含ではなく実用的な構造チェック:
|
|
377
|
-
// object は宣言側 required の存在と型互換、array は items
|
|
473
|
+
// 完全な JSON Schema 包含ではなく実用的な構造チェック:type 一致、
|
|
474
|
+
// object は宣言側 required の存在と型互換、array は items 互換。
|
|
475
|
+
// どちらかが未宣言なら「適合」でも「不適合」でもなく unknown(判断できない)を返す。
|
|
476
|
+
// 保存時の接続チェックは unknown を通し、承認ビューの候補絞り込みは unknown を弾く
|
|
477
|
+
|
|
478
|
+
export type SchemaFit = "ok" | "mismatch" | "unknown"
|
|
378
479
|
|
|
379
|
-
export const
|
|
380
|
-
if (
|
|
381
|
-
// file は nominal:マーカー一致の file
|
|
480
|
+
export const schemaFit = (sub: JsonSchema, sup: JsonSchema): SchemaFit => {
|
|
481
|
+
if (isUndeclaredSchema(sup) || isUndeclaredSchema(sub)) return "unknown"
|
|
482
|
+
// file は nominal:マーカー一致の file 同士だけ可。
|
|
382
483
|
// 構造が同じだけの普通の object は file に繋がない/file は普通の object に繋がない
|
|
383
484
|
if (isFileSchema(sub) || isFileSchema(sup))
|
|
384
|
-
return isFileSchema(sub) && isFileSchema(sup)
|
|
385
|
-
if (sub.type !== sup.type) return
|
|
485
|
+
return isFileSchema(sub) && isFileSchema(sup) ? "ok" : "mismatch"
|
|
486
|
+
if (sub.type !== sup.type) return "mismatch"
|
|
386
487
|
if (sup.type === "object") {
|
|
387
488
|
const subProps = (sub.properties ?? {}) as Record<string, JsonSchema>
|
|
388
489
|
const subRequired = (sub.required ?? []) as string[]
|
|
389
490
|
const supProps = (sup.properties ?? {}) as Record<string, JsonSchema>
|
|
390
491
|
for (const key of (sup.required ?? []) as string[]) {
|
|
391
492
|
const subProp = subProps[key]
|
|
392
|
-
if (!subProp || !subRequired.includes(key)) return
|
|
493
|
+
if (!subProp || !subRequired.includes(key)) return "mismatch"
|
|
393
494
|
const supProp = supProps[key]
|
|
394
|
-
|
|
495
|
+
// ネストは「不適合でなければよい」(未宣言のプロパティで親を落とさない)
|
|
496
|
+
if (supProp && schemaFit(subProp, supProp) === "mismatch")
|
|
497
|
+
return "mismatch"
|
|
395
498
|
}
|
|
396
|
-
return
|
|
499
|
+
return "ok"
|
|
397
500
|
}
|
|
398
501
|
if (sup.type === "array") {
|
|
399
502
|
const supItems = sup.items as JsonSchema | undefined
|
|
400
503
|
const subItems = sub.items as JsonSchema | undefined
|
|
401
|
-
if (!supItems || !subItems) return
|
|
402
|
-
return
|
|
504
|
+
if (!supItems || !subItems) return "ok"
|
|
505
|
+
return schemaFit(subItems, supItems) === "mismatch" ? "mismatch" : "ok"
|
|
403
506
|
}
|
|
404
|
-
return
|
|
507
|
+
return "ok"
|
|
405
508
|
}
|
|
509
|
+
|
|
510
|
+
// 保存時の接続チェック用:判断できない(未宣言)ものは通す
|
|
511
|
+
export const isSchemaSubset = (sub: JsonSchema, sup: JsonSchema): boolean =>
|
|
512
|
+
schemaFit(sub, sup) !== "mismatch"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
2
|
import { jsonSchemaSchema, portKeySchema } from "./node.js"
|
|
3
|
+
import { manifestInKeysViolation } from "./port-spec.js"
|
|
3
4
|
|
|
4
5
|
// nodes/program/<dir>/config.json(リポジトリ規約 v2)。プログラム自身が入出力スキーマを宣言し、
|
|
5
6
|
// エディタが取り込んで ports を自動生成する(実行時の正は ports テーブルのまま。
|
|
@@ -23,14 +24,14 @@ export const programManifestSchema = z
|
|
|
23
24
|
refs: z.record(portKeySchema, jsonSchemaSchema).default({}),
|
|
24
25
|
})
|
|
25
26
|
.superRefine((manifest, ctx) => {
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
27
|
+
// in の構成は kind が固定する(program は "in" の1口)。ルールの正は
|
|
28
|
+
// port-spec 側で、ここは参照するだけ
|
|
29
|
+
const inViolation = manifestInKeysViolation(
|
|
30
|
+
"program",
|
|
31
|
+
Object.keys(manifest.inputs),
|
|
32
|
+
)
|
|
33
|
+
if (inViolation) {
|
|
34
|
+
ctx.addIssue({ code: "custom", path: ["inputs"], message: inViolation })
|
|
34
35
|
}
|
|
35
36
|
if (Object.keys(manifest.outputs).length < 1) {
|
|
36
37
|
ctx.addIssue({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { jsonSchemaSchema } from "./node.js";
|
|
3
|
-
import { isAiEnvelopeSchema } from "./port-spec.js";
|
|
3
|
+
import { isAiEnvelopeSchema, manifestInKeysViolation } from "./port-spec.js";
|
|
4
4
|
// AI ノードのモデル選択肢(既定は Opus:下書き品質が価値の中心。費用はユーザー持ち)
|
|
5
5
|
export const AI_MODELS = [
|
|
6
6
|
"claude-opus-4-8",
|
|
@@ -35,14 +35,11 @@ export const aiManifestSchema = z
|
|
|
35
35
|
outputs: z.record(z.string(), jsonSchemaSchema),
|
|
36
36
|
})
|
|
37
37
|
.superRefine((manifest, ctx) => {
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
path: ["inputs"],
|
|
44
|
-
message: 'inputs は "in" の1件だけ宣言してください(join 導入まで)',
|
|
45
|
-
});
|
|
38
|
+
// in の構成は kind が固定する(ai は上流データの in + 差し戻しの instruction の2口)。
|
|
39
|
+
// ルールの正は port-spec 側で、ここは参照するだけ
|
|
40
|
+
const inViolation = manifestInKeysViolation("ai", Object.keys(manifest.inputs));
|
|
41
|
+
if (inViolation) {
|
|
42
|
+
ctx.addIssue({ code: "custom", path: ["inputs"], message: inViolation });
|
|
46
43
|
}
|
|
47
44
|
// outputs は1件以上・key 自由(複数出口=分岐。AIノードの複数出口(分岐).md)
|
|
48
45
|
if (Object.keys(manifest.outputs).length < 1) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GraphConnection, GraphNode } from "./graph.js";
|
|
2
|
+
export type ExtractAnalysis = {
|
|
3
|
+
ok: false;
|
|
4
|
+
reason: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: true;
|
|
7
|
+
entryNodeId: string | null;
|
|
8
|
+
exitPortId: string | null;
|
|
9
|
+
internal: GraphConnection[];
|
|
10
|
+
inbound: GraphConnection[];
|
|
11
|
+
outbound: GraphConnection[];
|
|
12
|
+
};
|
|
13
|
+
export declare const analyzeExtractSelection: (nodes: GraphNode[], connections: GraphConnection[], selectedIds: readonly string[]) => ExtractAnalysis;
|
|
14
|
+
export type ExtractPlan = {
|
|
15
|
+
childNodes: GraphNode[];
|
|
16
|
+
childConnections: GraphConnection[];
|
|
17
|
+
groupNode: GraphNode;
|
|
18
|
+
parentNodes: GraphNode[];
|
|
19
|
+
parentConnections: GraphConnection[];
|
|
20
|
+
};
|
|
21
|
+
export declare const buildExtractPlan: (params: {
|
|
22
|
+
nodes: GraphNode[];
|
|
23
|
+
connections: GraphConnection[];
|
|
24
|
+
selectedIds: string[];
|
|
25
|
+
name: string;
|
|
26
|
+
componentLoopId: string;
|
|
27
|
+
childStartOutPortId: string;
|
|
28
|
+
childEndInPortId: string;
|
|
29
|
+
newId: () => string;
|
|
30
|
+
}) => {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
33
|
+
} | ({
|
|
34
|
+
ok: true;
|
|
35
|
+
} & ExtractPlan);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const analyzeExtractSelection = (nodes, connections, selectedIds) => {
|
|
2
|
+
const selected = new Set(selectedIds);
|
|
3
|
+
if (selected.size < 2) {
|
|
4
|
+
return { ok: false, reason: "2つ以上のノードを選択してください" };
|
|
5
|
+
}
|
|
6
|
+
const selectedNodes = nodes.filter((n) => selected.has(n.id));
|
|
7
|
+
const flow = selectedNodes.find((n) => n.kind === "start" || n.kind === "end");
|
|
8
|
+
if (flow) {
|
|
9
|
+
return {
|
|
10
|
+
ok: false,
|
|
11
|
+
reason: "開始・終了ノードはグループ化できません",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const nodeIdByPortId = new Map(nodes.flatMap((n) => n.ports.map((p) => [p.id, n.id])));
|
|
15
|
+
const internal = [];
|
|
16
|
+
const inbound = [];
|
|
17
|
+
const outbound = [];
|
|
18
|
+
for (const conn of connections) {
|
|
19
|
+
const fromIn = selected.has(nodeIdByPortId.get(conn.from_port_id) ?? "");
|
|
20
|
+
const toIn = selected.has(nodeIdByPortId.get(conn.to_port_id) ?? "");
|
|
21
|
+
if (fromIn && toIn)
|
|
22
|
+
internal.push(conn);
|
|
23
|
+
else if (!fromIn && toIn)
|
|
24
|
+
inbound.push(conn);
|
|
25
|
+
else if (fromIn && !toIn)
|
|
26
|
+
outbound.push(conn);
|
|
27
|
+
}
|
|
28
|
+
const entryNodeIds = new Set(inbound.map((c) => nodeIdByPortId.get(c.to_port_id) ?? ""));
|
|
29
|
+
if (entryNodeIds.size > 1) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: "外部からの入力が複数のノードに入っています(入口は1つにしてください)",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const exitPortIds = new Set(outbound.map((c) => c.from_port_id));
|
|
36
|
+
if (exitPortIds.size > 1) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: "外部への出力が複数のポートから出ています(出口は1つにしてください)",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
entryNodeId: [...entryNodeIds][0] ?? null,
|
|
45
|
+
exitPortId: [...exitPortIds][0] ?? null,
|
|
46
|
+
internal,
|
|
47
|
+
inbound,
|
|
48
|
+
outbound,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export const buildExtractPlan = (params) => {
|
|
52
|
+
const { nodes, connections, selectedIds, name, componentLoopId, childStartOutPortId, childEndInPortId, newId, } = params;
|
|
53
|
+
const analysis = analyzeExtractSelection(nodes, connections, selectedIds);
|
|
54
|
+
if (!analysis.ok)
|
|
55
|
+
return analysis;
|
|
56
|
+
const selected = new Set(selectedIds);
|
|
57
|
+
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
58
|
+
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)
|
|
59
|
+
const nodeIdMap = new Map();
|
|
60
|
+
const portIdMap = new Map();
|
|
61
|
+
const childNodes = nodes
|
|
62
|
+
.filter((n) => selected.has(n.id))
|
|
63
|
+
.map((n) => {
|
|
64
|
+
const id = newId();
|
|
65
|
+
nodeIdMap.set(n.id, id);
|
|
66
|
+
return {
|
|
67
|
+
...n,
|
|
68
|
+
id,
|
|
69
|
+
ports: n.ports.map((p) => {
|
|
70
|
+
const portId = newId();
|
|
71
|
+
portIdMap.set(p.id, portId);
|
|
72
|
+
return { ...p, id: portId };
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const mapConn = (conn) => ({
|
|
77
|
+
from_port_id: portIdMap.get(conn.from_port_id) ?? conn.from_port_id,
|
|
78
|
+
to_port_id: portIdMap.get(conn.to_port_id) ?? conn.to_port_id,
|
|
79
|
+
});
|
|
80
|
+
const childConnections = analysis.internal.map(mapConn);
|
|
81
|
+
// 境界の配線:start.main → 入口ノードの in/出口ポート → end.in
|
|
82
|
+
if (analysis.entryNodeId !== null) {
|
|
83
|
+
const entry = childNodes.find((n) => n.id === nodeIdMap.get(analysis.entryNodeId ?? ""));
|
|
84
|
+
const entryIn = entry?.ports.find((p) => p.io === "in");
|
|
85
|
+
if (entryIn) {
|
|
86
|
+
childConnections.push({
|
|
87
|
+
from_port_id: childStartOutPortId,
|
|
88
|
+
to_port_id: entryIn.id,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (analysis.exitPortId !== null) {
|
|
93
|
+
childConnections.push({
|
|
94
|
+
from_port_id: portIdMap.get(analysis.exitPortId) ?? analysis.exitPortId,
|
|
95
|
+
to_port_id: childEndInPortId,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// 親に置くノード(位置は選択の重心。ポートスキーマは保存時にサーバが契約から焼き込む)
|
|
99
|
+
const selectedNodes = nodes.filter((n) => selected.has(n.id));
|
|
100
|
+
const centroid = {
|
|
101
|
+
x: selectedNodes.reduce((sum, n) => sum + n.position_x, 0) /
|
|
102
|
+
selectedNodes.length,
|
|
103
|
+
y: selectedNodes.reduce((sum, n) => sum + n.position_y, 0) /
|
|
104
|
+
selectedNodes.length,
|
|
105
|
+
};
|
|
106
|
+
const groupNode = {
|
|
107
|
+
id: newId(),
|
|
108
|
+
kind: "component",
|
|
109
|
+
name,
|
|
110
|
+
ports: [
|
|
111
|
+
{ id: newId(), io: "in", key: "in", name: "入力", schema: {} },
|
|
112
|
+
{ id: newId(), io: "out", key: "main", name: "出力", schema: {} },
|
|
113
|
+
],
|
|
114
|
+
hooks: [],
|
|
115
|
+
position_x: centroid.x,
|
|
116
|
+
position_y: centroid.y,
|
|
117
|
+
component: { component_loop_id: componentLoopId },
|
|
118
|
+
};
|
|
119
|
+
const groupIn = groupNode.ports.find((p) => p.io === "in");
|
|
120
|
+
const groupMain = groupNode.ports.find((p) => p.io === "out");
|
|
121
|
+
if (!groupIn || !groupMain) {
|
|
122
|
+
return { ok: false, reason: "groupNode のポート生成に失敗しました" };
|
|
123
|
+
}
|
|
124
|
+
// 親の置換:選択関連の線(内部・境界)を除き、境界を group の in/main へ繋ぎ直す
|
|
125
|
+
const removed = new Set([...analysis.internal, ...analysis.inbound, ...analysis.outbound].map((c) => `${c.from_port_id}→${c.to_port_id}`));
|
|
126
|
+
const parentConnections = [
|
|
127
|
+
...connections.filter((c) => !removed.has(`${c.from_port_id}→${c.to_port_id}`)),
|
|
128
|
+
...analysis.inbound.map((c) => ({
|
|
129
|
+
from_port_id: c.from_port_id,
|
|
130
|
+
to_port_id: groupIn.id,
|
|
131
|
+
})),
|
|
132
|
+
...analysis.outbound.map((c) => ({
|
|
133
|
+
from_port_id: groupMain.id,
|
|
134
|
+
to_port_id: c.to_port_id,
|
|
135
|
+
})),
|
|
136
|
+
];
|
|
137
|
+
const parentNodes = [...nodes.filter((n) => !selected.has(n.id)), groupNode];
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
childNodes,
|
|
141
|
+
childConnections,
|
|
142
|
+
groupNode,
|
|
143
|
+
parentNodes,
|
|
144
|
+
parentConnections,
|
|
145
|
+
};
|
|
146
|
+
};
|