@mawaru/sdk 0.5.0 → 0.8.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 +201 -12
- package/_schemas/graph.ts +99 -10
- 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 +19 -0
- package/dist/_schemas/graph.js +82 -11
- 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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +3 -0
- package/index.ts +1 -0
- package/package.json +1 -1
- package/templates/gitignore +4 -0
package/dist/_schemas/graph.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare const graphNodeKindSchema: z.ZodEnum<{
|
|
|
23
23
|
wait: "wait";
|
|
24
24
|
start: "start";
|
|
25
25
|
end: "end";
|
|
26
|
+
component: "component";
|
|
26
27
|
}>;
|
|
27
28
|
export type GraphNodeKind = z.infer<typeof graphNodeKindSchema>;
|
|
28
29
|
export declare const waitConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -47,6 +48,8 @@ export declare const aiConfigSchema: z.ZodObject<{
|
|
|
47
48
|
}, z.core.$strip>;
|
|
48
49
|
export type AiConfig = z.infer<typeof aiConfigSchema>;
|
|
49
50
|
export declare const humanConfigSchema: z.ZodObject<{
|
|
51
|
+
view: z.ZodOptional<z.ZodString>;
|
|
52
|
+
view_config: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
|
|
50
53
|
assignment: z.ZodObject<{
|
|
51
54
|
strategy: z.ZodDefault<z.ZodEnum<{
|
|
52
55
|
fixed: "fixed";
|
|
@@ -60,6 +63,10 @@ export declare const humanConfigSchema: z.ZodObject<{
|
|
|
60
63
|
}, z.core.$strip>;
|
|
61
64
|
}, z.core.$strip>;
|
|
62
65
|
export type HumanConfig = z.infer<typeof humanConfigSchema>;
|
|
66
|
+
export declare const componentConfigSchema: z.ZodObject<{
|
|
67
|
+
component_loop_id: z.ZodUUID;
|
|
68
|
+
}, z.core.$strip>;
|
|
69
|
+
export type ComponentConfig = z.infer<typeof componentConfigSchema>;
|
|
63
70
|
export declare const graphNodeHookSchema: z.ZodObject<{
|
|
64
71
|
hook_dir: z.ZodString;
|
|
65
72
|
ref: z.ZodOptional<z.ZodString>;
|
|
@@ -77,6 +84,7 @@ export declare const graphNodeSchema: z.ZodObject<{
|
|
|
77
84
|
wait: "wait";
|
|
78
85
|
start: "start";
|
|
79
86
|
end: "end";
|
|
87
|
+
component: "component";
|
|
80
88
|
}>;
|
|
81
89
|
name: z.ZodString;
|
|
82
90
|
ports: z.ZodArray<z.ZodObject<{
|
|
@@ -124,6 +132,8 @@ export declare const graphNodeSchema: z.ZodObject<{
|
|
|
124
132
|
ref: z.ZodOptional<z.ZodString>;
|
|
125
133
|
}, z.core.$strip>>;
|
|
126
134
|
human: z.ZodOptional<z.ZodObject<{
|
|
135
|
+
view: z.ZodOptional<z.ZodString>;
|
|
136
|
+
view_config: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
|
|
127
137
|
assignment: z.ZodObject<{
|
|
128
138
|
strategy: z.ZodDefault<z.ZodEnum<{
|
|
129
139
|
fixed: "fixed";
|
|
@@ -136,6 +146,9 @@ export declare const graphNodeSchema: z.ZodObject<{
|
|
|
136
146
|
units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUUID>>>;
|
|
137
147
|
}, z.core.$strip>;
|
|
138
148
|
}, z.core.$strip>>;
|
|
149
|
+
component: z.ZodOptional<z.ZodObject<{
|
|
150
|
+
component_loop_id: z.ZodUUID;
|
|
151
|
+
}, z.core.$strip>>;
|
|
139
152
|
}, z.core.$strip>;
|
|
140
153
|
export type GraphNode = z.infer<typeof graphNodeSchema>;
|
|
141
154
|
export declare const graphConnectionSchema: z.ZodObject<{
|
|
@@ -153,6 +166,7 @@ export declare const saveGraphBodySchema: z.ZodObject<{
|
|
|
153
166
|
wait: "wait";
|
|
154
167
|
start: "start";
|
|
155
168
|
end: "end";
|
|
169
|
+
component: "component";
|
|
156
170
|
}>;
|
|
157
171
|
name: z.ZodString;
|
|
158
172
|
ports: z.ZodArray<z.ZodObject<{
|
|
@@ -200,6 +214,8 @@ export declare const saveGraphBodySchema: z.ZodObject<{
|
|
|
200
214
|
ref: z.ZodOptional<z.ZodString>;
|
|
201
215
|
}, z.core.$strip>>;
|
|
202
216
|
human: z.ZodOptional<z.ZodObject<{
|
|
217
|
+
view: z.ZodOptional<z.ZodString>;
|
|
218
|
+
view_config: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
|
|
203
219
|
assignment: z.ZodObject<{
|
|
204
220
|
strategy: z.ZodDefault<z.ZodEnum<{
|
|
205
221
|
fixed: "fixed";
|
|
@@ -212,6 +228,9 @@ export declare const saveGraphBodySchema: z.ZodObject<{
|
|
|
212
228
|
units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUUID>>>;
|
|
213
229
|
}, z.core.$strip>;
|
|
214
230
|
}, z.core.$strip>>;
|
|
231
|
+
component: z.ZodOptional<z.ZodObject<{
|
|
232
|
+
component_loop_id: z.ZodUUID;
|
|
233
|
+
}, z.core.$strip>>;
|
|
215
234
|
}, z.core.$strip>>;
|
|
216
235
|
connections: z.ZodArray<z.ZodObject<{
|
|
217
236
|
from_port_id: z.ZodUUID;
|
package/dist/_schemas/graph.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { graphPortSchema, jsonSchemaSchema, nodeKindSchema, portKeySchema, } from "./node.js";
|
|
3
|
-
import { isAiEnvelopeSchema, isSchemaSubset, kindPortsViolation, resolvePortSchemas, } from "./port-spec.js";
|
|
3
|
+
import { AI_INSTRUCTION_PORT_KEY, hasInboundConflict, isAiEnvelopeSchema, isSchemaSubset, kindPortsViolation, resolvePortSchemas, UNDECLARED_SCHEMA, } from "./port-spec.js";
|
|
4
4
|
// refs=ループ変数の袋(key→任意の JSON 値。上流出力の参照(refs).md)。
|
|
5
5
|
// run 開始時に定義から複製され、全 step に全量差し込まれる。
|
|
6
6
|
// key はポート key と同じ規則(^[a-z][a-z0-9_]*$)。実行系ボディ
|
|
@@ -50,6 +50,7 @@ export const graphNodeKindSchema = nodeKindSchema.extract([
|
|
|
50
50
|
"wait",
|
|
51
51
|
"start",
|
|
52
52
|
"end",
|
|
53
|
+
"component",
|
|
53
54
|
]);
|
|
54
55
|
// Wait ノードの kind 別設定(node_waits の wait_type に対応する判別ユニオン)。
|
|
55
56
|
// duration=経過秒、until=絶対時刻(ISO)、cron=cron 式(次回発火まで1回待つ)。
|
|
@@ -97,6 +98,12 @@ export const aiConfigSchema = z.object({
|
|
|
97
98
|
// メンバーが tenant の membership であることの検証は保存 API 側で行う
|
|
98
99
|
// (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
|
|
99
100
|
export const humanConfigSchema = z.object({
|
|
101
|
+
// 承認 UI(承認ビュー)の識別子。program_dir / ai_dir と同じ「不透明なキー」で、
|
|
102
|
+
// どのキーが実在するか・そのテナントが選べるかは保存時に mawaru 側が検証する
|
|
103
|
+
// (カタログは非公開。@mawaru/common の approval-view.ts)。省略時は標準ビュー
|
|
104
|
+
view: z.string().min(1).optional(),
|
|
105
|
+
// ビューに渡すパラメータ(列・ラベル・閾値など)。形はビューごとに違うので任意の JSON
|
|
106
|
+
view_config: z.unknown().nullish(),
|
|
100
107
|
assignment: z
|
|
101
108
|
.object({
|
|
102
109
|
strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
|
|
@@ -131,6 +138,12 @@ export const humanConfigSchema = z.object({
|
|
|
131
138
|
});
|
|
132
139
|
}),
|
|
133
140
|
});
|
|
141
|
+
// コンポーネントノードの kind 別設定(node_components に対応)。呼ぶ先は
|
|
142
|
+
// kind='component' の loop。同 tenant・kind の検証と自 loop 参照の禁止は保存 API 側で行う
|
|
143
|
+
// (docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
|
|
144
|
+
export const componentConfigSchema = z.object({
|
|
145
|
+
component_loop_id: z.uuid(),
|
|
146
|
+
});
|
|
134
147
|
// ノードにアタッチされた IOHook(node_hooks に対応。アタッチできるのは program ノードのみ)。
|
|
135
148
|
// on_input / on_output / on_signal はアタッチ時に repo の config.json(hookManifestSchema)から
|
|
136
149
|
// エディタが焼き込むスナップショット。ref 省略時はデフォルトブランチ
|
|
@@ -161,6 +174,7 @@ export const graphNodeSchema = z.object({
|
|
|
161
174
|
program: programConfigSchema.optional(),
|
|
162
175
|
ai: aiConfigSchema.optional(),
|
|
163
176
|
human: humanConfigSchema.optional(),
|
|
177
|
+
component: componentConfigSchema.optional(),
|
|
164
178
|
});
|
|
165
179
|
export const graphConnectionSchema = z.object({
|
|
166
180
|
from_port_id: z.uuid(),
|
|
@@ -228,15 +242,18 @@ export const saveGraphBodySchema = z
|
|
|
228
242
|
else
|
|
229
243
|
outCount++;
|
|
230
244
|
});
|
|
231
|
-
// end
|
|
232
|
-
//
|
|
233
|
-
|
|
245
|
+
// end は「in は1つ以上」(複数ルートを別々の in で集約する)。
|
|
246
|
+
// ai は2つ(上流データの in + 差し戻しの instruction)。他 kind は1つ固定。
|
|
247
|
+
// key の妥当性は kindPortsViolation が別途見る
|
|
248
|
+
const expectIn = node.kind === "ai" ? 2 : 1;
|
|
249
|
+
const inCountOk = node.kind === "end" ? inCount >= 1 : inCount === expectIn;
|
|
250
|
+
if (!inCountOk) {
|
|
234
251
|
ctx.addIssue({
|
|
235
252
|
code: "custom",
|
|
236
253
|
path: ["nodes", i, "ports"],
|
|
237
254
|
message: node.kind === "end"
|
|
238
255
|
? "in ポートが1つ以上必要です"
|
|
239
|
-
:
|
|
256
|
+
: `in ポートはちょうど${expectIn}つ必要です`,
|
|
240
257
|
});
|
|
241
258
|
}
|
|
242
259
|
if (outCount < 1) {
|
|
@@ -266,6 +283,15 @@ export const saveGraphBodySchema = z
|
|
|
266
283
|
}
|
|
267
284
|
hookDirs.add(hook.hook_dir);
|
|
268
285
|
});
|
|
286
|
+
// component は参照先が無いと成立しない(他 kind の設定はデフォルトで補えるが
|
|
287
|
+
// component_loop_id は補えない)
|
|
288
|
+
if (node.kind === "component" && node.component === undefined) {
|
|
289
|
+
ctx.addIssue({
|
|
290
|
+
code: "custom",
|
|
291
|
+
path: ["nodes", i, "component"],
|
|
292
|
+
message: "component ノードには component_loop_id が必要です",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
269
295
|
// kind 別のポート構成(program 以外は数・key・io 固定。ポートのkind別仕様.md)
|
|
270
296
|
const violation = kindPortsViolation(node.kind, node.ports);
|
|
271
297
|
if (violation) {
|
|
@@ -295,7 +321,7 @@ export const saveGraphBodySchema = z
|
|
|
295
321
|
const usedPairs = new Set();
|
|
296
322
|
graph.connections.forEach((conn, i) => {
|
|
297
323
|
const from = portById.get(conn.from_port_id);
|
|
298
|
-
if (
|
|
324
|
+
if (from?.io !== "out") {
|
|
299
325
|
ctx.addIssue({
|
|
300
326
|
code: "custom",
|
|
301
327
|
path: ["connections", i, "from_port_id"],
|
|
@@ -303,7 +329,7 @@ export const saveGraphBodySchema = z
|
|
|
303
329
|
});
|
|
304
330
|
}
|
|
305
331
|
const to = portById.get(conn.to_port_id);
|
|
306
|
-
if (
|
|
332
|
+
if (to?.io !== "in") {
|
|
307
333
|
ctx.addIssue({
|
|
308
334
|
code: "custom",
|
|
309
335
|
path: ["connections", i, "to_port_id"],
|
|
@@ -323,6 +349,29 @@ export const saveGraphBodySchema = z
|
|
|
323
349
|
// 型の接続バリデーション(ポートのkind別仕様.md §接続バリデーション)。
|
|
324
350
|
// 導出ポートは接続から型を解決した上で判定する。ai の in・wait の in は常に許可
|
|
325
351
|
const resolved = resolvePortSchemas(graph.nodes, graph.connections);
|
|
352
|
+
// fan-in(同じ in ポートへの流入が2本以上)は同じ型でなければならない。
|
|
353
|
+
// 型が割れていると伝播が未宣言に落ちて下流の型チェックが効かなくなる
|
|
354
|
+
// (docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
355
|
+
const inboundByPort = new Map();
|
|
356
|
+
for (const conn of graph.connections) {
|
|
357
|
+
const list = inboundByPort.get(conn.to_port_id) ?? [];
|
|
358
|
+
list.push(conn.from_port_id);
|
|
359
|
+
inboundByPort.set(conn.to_port_id, list);
|
|
360
|
+
}
|
|
361
|
+
for (const [toPortId, fromPortIds] of inboundByPort) {
|
|
362
|
+
if (fromPortIds.length < 2)
|
|
363
|
+
continue;
|
|
364
|
+
const schemas = fromPortIds.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA);
|
|
365
|
+
if (!hasInboundConflict(schemas))
|
|
366
|
+
continue;
|
|
367
|
+
const to = portById.get(toPortId);
|
|
368
|
+
const toNode = nodeByPortId.get(toPortId);
|
|
369
|
+
ctx.addIssue({
|
|
370
|
+
code: "custom",
|
|
371
|
+
path: ["connections"],
|
|
372
|
+
message: `同じ入力ポート(${toNode?.name ?? "?"}.${to?.key ?? "?"})に異なる型を接続できません`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
326
375
|
graph.connections.forEach((conn, i) => {
|
|
327
376
|
const from = portById.get(conn.from_port_id);
|
|
328
377
|
const to = portById.get(conn.to_port_id);
|
|
@@ -345,21 +394,34 @@ export const saveGraphBodySchema = z
|
|
|
345
394
|
message: "終了ノードからは接続できません",
|
|
346
395
|
});
|
|
347
396
|
}
|
|
348
|
-
// human の reject の接続先は
|
|
397
|
+
// human の reject の接続先は AI の instruction ポートのみ。
|
|
398
|
+
// 差し戻しは「元のセッションに指示を与える」操作で、上流データの口(in)とは
|
|
399
|
+
// 意味が違う(docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
349
400
|
if (fromNode.kind === "human" &&
|
|
350
401
|
from.key === "reject" &&
|
|
351
|
-
toNode.kind
|
|
402
|
+
!(toNode.kind === "ai" && to.key === AI_INSTRUCTION_PORT_KEY)) {
|
|
352
403
|
ctx.addIssue({
|
|
353
404
|
code: "custom",
|
|
354
405
|
path: ["connections", i, "to_port_id"],
|
|
355
|
-
message: "差し戻し(reject)の接続先は AI
|
|
406
|
+
message: "差し戻し(reject)の接続先は AI ノードの instruction ポートのみです",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
// instruction ポートに繋げるのは human の reject だけ(データを流し込ませない)
|
|
410
|
+
if (toNode.kind === "ai" &&
|
|
411
|
+
to.key === AI_INSTRUCTION_PORT_KEY &&
|
|
412
|
+
!(fromNode.kind === "human" && from.key === "reject")) {
|
|
413
|
+
ctx.addIssue({
|
|
414
|
+
code: "custom",
|
|
415
|
+
path: ["connections", i, "to_port_id"],
|
|
416
|
+
message: "instruction ポートに接続できるのは human の差し戻し(reject)だけです",
|
|
356
417
|
});
|
|
357
418
|
}
|
|
358
419
|
const fromSchema = resolved.get(conn.from_port_id) ?? from.schema;
|
|
359
420
|
// human の in はどの型でも接続できる(上流が AI なら封筒 { data, description }、
|
|
360
421
|
// それ以外は input 全体を data として表示する。型制約は課さない)
|
|
361
422
|
// program の in への接続は 伝播型 ⊆ 宣言スキーマ。in は接続鏡映(sticky)なので
|
|
362
|
-
//
|
|
423
|
+
// 具体型の接続では自明に通り、接続元が未宣言 × 宣言維持のときだけ従来の意味を持つ
|
|
424
|
+
// (未宣言は unknown=判断できないので通す。作りかけのグラフを保存できなくしない)
|
|
363
425
|
const toSchema = resolved.get(conn.to_port_id) ?? to.schema;
|
|
364
426
|
if (toNode.kind === "program" && !isSchemaSubset(fromSchema, toSchema)) {
|
|
365
427
|
ctx.addIssue({
|
|
@@ -368,5 +430,14 @@ export const saveGraphBodySchema = z
|
|
|
368
430
|
message: "接続元の型が program の入力スキーマに適合しません(伝播型 ⊆ 宣言スキーマ)",
|
|
369
431
|
});
|
|
370
432
|
}
|
|
433
|
+
// component の in は子 loop の契約(焼き込み済み宣言)。program と同じ包含チェック
|
|
434
|
+
if (toNode.kind === "component" &&
|
|
435
|
+
!isSchemaSubset(fromSchema, toSchema)) {
|
|
436
|
+
ctx.addIssue({
|
|
437
|
+
code: "custom",
|
|
438
|
+
path: ["connections", i, "to_port_id"],
|
|
439
|
+
message: "接続元の型がコンポーネントの入力契約に適合しません(伝播型 ⊆ 契約)",
|
|
440
|
+
});
|
|
441
|
+
}
|
|
371
442
|
});
|
|
372
443
|
});
|
package/dist/_schemas/node.d.ts
CHANGED
package/dist/_schemas/node.js
CHANGED
|
@@ -9,6 +9,9 @@ export const nodeKindSchema = z.enum([
|
|
|
9
9
|
// docs/tasks/wip/開始・終了ノードの定義.md)
|
|
10
10
|
"start",
|
|
11
11
|
"end",
|
|
12
|
+
// コンポーネント(kind='component' の loop を配置して呼ぶ。子 run・戻り値あり。
|
|
13
|
+
// docs/tasks/wip/コンポーネント(ループの部品化と再利用).md)
|
|
14
|
+
"component",
|
|
12
15
|
]);
|
|
13
16
|
// ポート key(input/output 用の参照キー)の規則(db.md §2)。
|
|
14
17
|
// name(ユーザーが見る表示名)は自由テキスト
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js";
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
3
|
+
export declare const UNDECLARED_SCHEMA: JsonSchema;
|
|
4
|
+
export declare const isUndeclaredSchema: (schema: JsonSchema) => boolean;
|
|
5
5
|
export declare const FILE_SCHEMA_MARKER = "x-mawaru-type";
|
|
6
6
|
export declare const FILE_SCHEMA_TYPE = "file";
|
|
7
7
|
export declare const fileSchema: () => JsonSchema;
|
|
8
8
|
export declare const isFileSchema: (schema: JsonSchema) => boolean;
|
|
9
|
+
export declare const AI_INSTRUCTION_PORT_KEY = "instruction";
|
|
10
|
+
export declare const instructionSchema: () => JsonSchema;
|
|
9
11
|
export declare const LIST_COLUMN_TYPES: readonly ["string", "number", "boolean", "date"];
|
|
10
12
|
export declare const listColumnTypeSchema: z.ZodEnum<{
|
|
11
13
|
string: "string";
|
|
@@ -46,7 +48,11 @@ export declare const dataFormatOf: (schema: JsonSchema) => DataFormat | null;
|
|
|
46
48
|
export declare const envelopeSchemaOf: (dataSchema: JsonSchema) => JsonSchema;
|
|
47
49
|
export declare const envelopeDataSchemaOf: (schema: JsonSchema) => JsonSchema | null;
|
|
48
50
|
export declare const isAiEnvelopeSchema: (schema: JsonSchema) => boolean;
|
|
51
|
+
export declare const fixedInKeysOf: (kind: string) => string[] | null;
|
|
52
|
+
export declare const manifestInKeysViolation: (kind: string, keys: string[]) => string | null;
|
|
49
53
|
export declare const kindPortsViolation: (kind: string, ports: Pick<GraphPort, "io" | "key">[]) => string | null;
|
|
54
|
+
export declare const mergeInboundSchemas: (schemas: JsonSchema[]) => JsonSchema;
|
|
55
|
+
export declare const hasInboundConflict: (schemas: JsonSchema[]) => boolean;
|
|
50
56
|
type PropagationNode = {
|
|
51
57
|
id: string;
|
|
52
58
|
kind: string;
|
|
@@ -58,5 +64,7 @@ type PortLink = {
|
|
|
58
64
|
};
|
|
59
65
|
export declare const resolvePortSchemas: <N extends PropagationNode>(nodes: N[], connections: PortLink[]) => Map<string, JsonSchema>;
|
|
60
66
|
export declare const propagateDerivedPorts: <N extends PropagationNode>(nodes: N[], connections: PortLink[]) => N[];
|
|
67
|
+
export type SchemaFit = "ok" | "mismatch" | "unknown";
|
|
68
|
+
export declare const schemaFit: (sub: JsonSchema, sup: JsonSchema) => SchemaFit;
|
|
61
69
|
export declare const isSchemaSubset: (sub: JsonSchema, sup: JsonSchema) => boolean;
|
|
62
70
|
export {};
|
|
@@ -2,14 +2,18 @@ import { z } from "zod";
|
|
|
2
2
|
// ポートの kind 別型システム(docs/tasks/wip/ポートのkind別仕様.md)。
|
|
3
3
|
// 型の決まり方は3分類:宣言(program / AI の in・out)/固定(human reject の
|
|
4
4
|
// content 型)/導出(それ以外。接続元から伝播し、結果は ports.schema にキャッシュする)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
export const
|
|
9
|
-
|
|
5
|
+
// 「まだ型が決まっていない」=未宣言を {} で表す(未接続・上流が未宣言・fan-in の型割れ)。
|
|
6
|
+
// 「何でも受ける」型は存在しない:AI の in は接続元の具体型を鏡映し、差し戻しの指示は
|
|
7
|
+
// instruction ポートの宣言型を持つ(docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
8
|
+
export const UNDECLARED_SCHEMA = {};
|
|
9
|
+
export const isUndeclaredSchema = (schema) => Object.keys(schema).length === 0 || schema.type === undefined;
|
|
10
|
+
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
10
11
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
11
|
-
// ではなく storage path
|
|
12
|
-
//
|
|
12
|
+
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
|
13
|
+
// **file の位置は名前ではなくこのマーカーで決まる**:runner が宣言スキーマと値を並行走査して
|
|
14
|
+
// 入力はワークスペースへ materialize(パスに置換)、出力はアップロード(参照に置換)する
|
|
15
|
+
// (docs/tasks/wip/ポートスキーマ編集でファイル型を選べるようにする.md)。
|
|
16
|
+
// ajv は strict:false で未知キーワードを無視するので検証は素の JSON Schema のまま。
|
|
13
17
|
export const FILE_SCHEMA_MARKER = "x-mawaru-type";
|
|
14
18
|
export const FILE_SCHEMA_TYPE = "file";
|
|
15
19
|
export const fileSchema = () => ({
|
|
@@ -24,6 +28,25 @@ export const fileSchema = () => ({
|
|
|
24
28
|
required: ["path", "name"],
|
|
25
29
|
});
|
|
26
30
|
export const isFileSchema = (schema) => schema[FILE_SCHEMA_MARKER] === FILE_SCHEMA_TYPE;
|
|
31
|
+
// ---- 差し戻しの指示(AI の instruction ポート / human の reject ポート) ----
|
|
32
|
+
// AI 入力の既存エンベロープ { prompt, files } と同じ形にする。配管が効く根拠は
|
|
33
|
+
// files が file 型として宣言されていることであって、キー名ではない
|
|
34
|
+
// (runner がマーカーで位置を見つけて materialize する)。
|
|
35
|
+
// スクリーンショットを添えて差し戻すケースがあるので文章だけにしない
|
|
36
|
+
// (docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
37
|
+
export const AI_INSTRUCTION_PORT_KEY = "instruction";
|
|
38
|
+
export const instructionSchema = () => ({
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
prompt: { type: "string", description: "差し戻しの理由(AI への指示)" },
|
|
42
|
+
files: {
|
|
43
|
+
type: "array",
|
|
44
|
+
items: fileSchema(),
|
|
45
|
+
description: "スクリーンショットなどの参考資料",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["prompt"],
|
|
49
|
+
});
|
|
27
50
|
// ---- データフォーマットカタログ ----
|
|
28
51
|
// human 承認 UI の表示・編集フォームの出し分けに使う(テキスト=string(markdown)/
|
|
29
52
|
// リスト=Array<Row>(列定義つき))。バリデーションには使わない(data は任意の型)
|
|
@@ -131,8 +154,34 @@ const FIXED_PORT_KEYS = {
|
|
|
131
154
|
wait: { in: ["in"], out: ["main"] },
|
|
132
155
|
// start は in 1つ(run 入力契約)+ out main 1つの恒等パススルー
|
|
133
156
|
start: { in: ["in"], out: ["main"] },
|
|
157
|
+
// component は in(子 loop の start.in 契約)+ out main(end.out の戻り値)。
|
|
158
|
+
// スキーマは保存時にサーバが子 loop の契約から焼き込む宣言型
|
|
159
|
+
// (DERIVED/MIRROR には入れない=接続で潰さない)
|
|
160
|
+
component: { in: ["in"], out: ["main"] },
|
|
134
161
|
};
|
|
135
162
|
const FREE_OUT_KINDS = new Set(["program", "ai"]);
|
|
163
|
+
// out は自由(複数出口=分岐)だが in の構成は固定。ai は上流データを受ける in と、
|
|
164
|
+
// 差し戻しの指示を受ける instruction の2口(意味の違う入力を1つのポートに
|
|
165
|
+
// fan-in させない。docs/tasks/wip/ポートスキーマのany撲滅.md)
|
|
166
|
+
const FIXED_IN_KEYS = {
|
|
167
|
+
program: ["in"],
|
|
168
|
+
ai: ["in", AI_INSTRUCTION_PORT_KEY],
|
|
169
|
+
};
|
|
170
|
+
export const fixedInKeysOf = (kind) => FIXED_IN_KEYS[kind] ?? null;
|
|
171
|
+
// config.json(manifest)の inputs 検査(適合なら null)。同じルールを二重に持たないため
|
|
172
|
+
// manifest 側はここを参照する(docs/tasks/wip/manifestのin複数対応(instructionポート).md)。
|
|
173
|
+
// **必須は "in" だけ**で、kind が固定する他の in(ai の instruction)は任意:スキーマは
|
|
174
|
+
// constSchemaOf が焼き込む固定型なのでエディタ側で補え、in を2口に分ける前に同期された
|
|
175
|
+
// config.json もそのまま読める
|
|
176
|
+
export const manifestInKeysViolation = (kind, keys) => {
|
|
177
|
+
const allowed = fixedInKeysOf(kind) ?? ["in"];
|
|
178
|
+
if (!keys.includes("in"))
|
|
179
|
+
return 'inputs は "in" を宣言してください';
|
|
180
|
+
const unknown = keys.filter((key) => !allowed.includes(key));
|
|
181
|
+
return unknown.length > 0
|
|
182
|
+
? `inputs に宣言できない key があります: ${unknown.join(" / ")}(${allowed.join(" / ")} のみ)`
|
|
183
|
+
: null;
|
|
184
|
+
};
|
|
136
185
|
// kind 別のポート構成違反を返す(適合なら null)。in=1/out>=1 の構造ルールは
|
|
137
186
|
// graph.ts 側の既存チェックが担い、ここは key の固定だけ見る
|
|
138
187
|
export const kindPortsViolation = (kind, ports) => {
|
|
@@ -150,10 +199,17 @@ export const kindPortsViolation = (kind, ports) => {
|
|
|
150
199
|
}
|
|
151
200
|
const fixed = FIXED_PORT_KEYS[kind];
|
|
152
201
|
if (!fixed) {
|
|
153
|
-
// program / ai(自由 out):in
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
202
|
+
// program / ai(自由 out):in の構成だけ固定
|
|
203
|
+
const expectIn = FIXED_IN_KEYS[kind];
|
|
204
|
+
if (FREE_OUT_KINDS.has(kind) && expectIn) {
|
|
205
|
+
const inKeys = ports
|
|
206
|
+
.filter((p) => p.io === "in")
|
|
207
|
+
.map((p) => p.key)
|
|
208
|
+
.sort();
|
|
209
|
+
if (inKeys.join(",") !== [...expectIn].sort().join(",")) {
|
|
210
|
+
return `${kind} の in ポートは ${expectIn.join(" / ")} です`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
157
213
|
return null;
|
|
158
214
|
}
|
|
159
215
|
const keysOf = (io) => ports
|
|
@@ -167,8 +223,32 @@ export const kindPortsViolation = (kind, ports) => {
|
|
|
167
223
|
}
|
|
168
224
|
return null;
|
|
169
225
|
};
|
|
170
|
-
//
|
|
171
|
-
//
|
|
226
|
+
// ---- 型伝播(導出ポートの解決) ----
|
|
227
|
+
// 接続の両端は同じスキーマになる(値入力フォームのSchemaForm統一.md)。
|
|
228
|
+
// wait / human は素通し(パラメトリック)ノード:in は接続元の out、out は in と同型。
|
|
229
|
+
// 例外は human の approve:接続元が AI ノードのときだけ封筒を剥がして data 型になる。
|
|
230
|
+
// human の reject は未宣言のまま(段階2 で instruction エンベロープを宣言する)。
|
|
231
|
+
// 宣言 out(program / ai)は触らない。
|
|
232
|
+
// program / ai の in は接続鏡映(sticky):接続元 out が具体型のときだけ上書きし、
|
|
233
|
+
// 未接続・型割れ・未宣言は現スキーマ(宣言)を維持する(鏡映結果は repo の config.json
|
|
234
|
+
// へも書き戻される。docs/tasks/wip/ポート編集のrepo書き戻し同期.md)
|
|
235
|
+
// 複数の流入(fan-in)から型を1つに決める:未宣言を除いて具体型が1種類ならその型、
|
|
236
|
+
// 0種類なら未宣言。2種類以上は型が割れているので未宣言に落とす
|
|
237
|
+
// (保存時に graph.ts が 422 で弾く。伝播は止めずに固定点へ収束させる)
|
|
238
|
+
export const mergeInboundSchemas = (schemas) => {
|
|
239
|
+
const uniq = [
|
|
240
|
+
...new Set(schemas
|
|
241
|
+
.filter((s) => !isUndeclaredSchema(s))
|
|
242
|
+
.map((s) => JSON.stringify(s))),
|
|
243
|
+
];
|
|
244
|
+
return uniq.length === 1 && uniq[0] !== undefined
|
|
245
|
+
? JSON.parse(uniq[0])
|
|
246
|
+
: UNDECLARED_SCHEMA;
|
|
247
|
+
};
|
|
248
|
+
// 流入の具体型が2種類以上あるか(=同じポートに違う型を流し込んでいる)
|
|
249
|
+
export const hasInboundConflict = (schemas) => new Set(schemas.filter((s) => !isUndeclaredSchema(s)).map((s) => JSON.stringify(s))).size > 1;
|
|
250
|
+
// end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
|
|
251
|
+
// resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
|
|
172
252
|
// start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
|
|
173
253
|
// (run 入力契約=接続先の入力宣言。開始ノードの入力契約を接続先から導出.md)
|
|
174
254
|
const DERIVED_KINDS = new Set(["wait", "human", "end", "start"]);
|
|
@@ -179,18 +259,30 @@ const isDerivedPort = (kind, port) => DERIVED_KINDS.has(kind) &&
|
|
|
179
259
|
// AI の in は nodes/ai/<dir>/config.json の inputs.in 由来の宣言型が初期値で、
|
|
180
260
|
// 接続時は program と同じ鏡映で接続元と同値に同期される(const 扱い廃止。
|
|
181
261
|
// 接続バリデーションの「ai の in は常に許可」は維持)
|
|
182
|
-
|
|
262
|
+
// 宣言型で固定されるポート(導出も鏡映もしない)。差し戻しの指示エンベロープを
|
|
263
|
+
// human の出口と AI の入口の両方に焼き込むことで、両端が同じ具体型で一致する
|
|
264
|
+
const constSchemaOf = (kind, port) => {
|
|
265
|
+
if (kind === "human" && port.io === "out" && port.key === "reject") {
|
|
266
|
+
return instructionSchema();
|
|
267
|
+
}
|
|
268
|
+
if (kind === "ai" &&
|
|
269
|
+
port.io === "in" &&
|
|
270
|
+
port.key === AI_INSTRUCTION_PORT_KEY) {
|
|
271
|
+
return instructionSchema();
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
183
275
|
// port_id → 解決済みスキーマ。導出は接続を遡って固定点まで反復し、
|
|
184
|
-
//
|
|
276
|
+
// 未接続・純パラメトリック閉路・fan-in の型割れは未宣言に落とす
|
|
185
277
|
export const resolvePortSchemas = (nodes, connections) => {
|
|
186
278
|
const resolved = new Map();
|
|
187
279
|
const kindByPortId = new Map();
|
|
188
280
|
for (const node of nodes) {
|
|
189
281
|
for (const port of node.ports) {
|
|
190
282
|
kindByPortId.set(port.id, node.kind);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
: port.schema);
|
|
283
|
+
const constSchema = constSchemaOf(node.kind, port);
|
|
284
|
+
resolved.set(port.id, constSchema ??
|
|
285
|
+
(isDerivedPort(node.kind, port) ? UNDECLARED_SCHEMA : port.schema));
|
|
194
286
|
}
|
|
195
287
|
}
|
|
196
288
|
// in ポートへの流入(差し戻しの fan-in があり得る。1本のときだけ型を採用)
|
|
@@ -215,52 +307,42 @@ export const resolvePortSchemas = (nodes, connections) => {
|
|
|
215
307
|
}
|
|
216
308
|
};
|
|
217
309
|
// start:out main は接続先 in の解決値を転写(逆向き導出)。fan-out は
|
|
218
|
-
//
|
|
310
|
+
// 非未宣言が1種類(JSON 等値)のときだけ採用し、未接続・型割れは未宣言。
|
|
219
311
|
// in は out main と同型(恒等の向きを out→in に逆転。run 入力契約は
|
|
220
312
|
// 接続先の入力宣言から自動で決まる)
|
|
221
313
|
if (node.kind === "start") {
|
|
222
314
|
const inPort = node.ports.find((p) => p.io === "in");
|
|
223
315
|
const outMain = node.ports.find((p) => p.io === "out" && p.key === "main");
|
|
224
316
|
if (inPort && outMain) {
|
|
225
|
-
const
|
|
226
|
-
.map((id) => resolved.get(id) ?? ANY_SCHEMA)
|
|
227
|
-
.filter((s) => !isAnySchema(s))
|
|
228
|
-
.map((s) => JSON.stringify(s));
|
|
229
|
-
const uniq = [...new Set(targets)];
|
|
230
|
-
const schema = uniq.length === 1 && uniq[0] !== undefined
|
|
231
|
-
? JSON.parse(uniq[0])
|
|
232
|
-
: ANY_SCHEMA;
|
|
317
|
+
const schema = mergeInboundSchemas((outbound.get(outMain.id) ?? []).map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA));
|
|
233
318
|
assign(outMain.id, schema);
|
|
234
319
|
assign(inPort.id, schema);
|
|
235
320
|
}
|
|
236
321
|
continue;
|
|
237
322
|
}
|
|
238
323
|
// end:各 in を自分の単一上流から導出(複数ルートを別 in で集約)。
|
|
239
|
-
// out main
|
|
324
|
+
// out main は未宣言固定(ルートごとに型が違いうる。Loop-in-Loop で詰める)
|
|
240
325
|
if (node.kind === "end") {
|
|
241
326
|
for (const port of node.ports) {
|
|
242
327
|
if (port.io !== "in")
|
|
243
328
|
continue;
|
|
244
329
|
const srcs = inbound.get(port.id) ?? [];
|
|
245
|
-
assign(port.id, srcs.
|
|
246
|
-
? (resolved.get(srcs[0]) ?? ANY_SCHEMA)
|
|
247
|
-
: ANY_SCHEMA);
|
|
330
|
+
assign(port.id, mergeInboundSchemas(srcs.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA)));
|
|
248
331
|
}
|
|
249
332
|
continue;
|
|
250
333
|
}
|
|
251
334
|
const isMirror = MIRROR_KINDS.has(node.kind);
|
|
252
335
|
if (!DERIVED_KINDS.has(node.kind) && !isMirror)
|
|
253
336
|
continue;
|
|
254
|
-
|
|
337
|
+
// ai は in が2口あるので、上流データの口(key="in")だけを鏡映・導出の対象にする
|
|
338
|
+
const inPort = node.ports.find((p) => p.io === "in" && p.key === "in");
|
|
255
339
|
if (!inPort)
|
|
256
340
|
continue;
|
|
257
341
|
const sources = inbound.get(inPort.id) ?? [];
|
|
258
|
-
const inSchema = sources.
|
|
259
|
-
|
|
260
|
-
: ANY_SCHEMA;
|
|
261
|
-
// 鏡映(program)は具体型が流れてきたときだけ上書き(宣言を any で潰さない)
|
|
342
|
+
const inSchema = mergeInboundSchemas(sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA));
|
|
343
|
+
// 鏡映(program / ai)は具体型が流れてきたときだけ上書き(宣言を未宣言で潰さない)
|
|
262
344
|
if (isMirror) {
|
|
263
|
-
if (!
|
|
345
|
+
if (!isUndeclaredSchema(inSchema))
|
|
264
346
|
assign(inPort.id, inSchema);
|
|
265
347
|
continue;
|
|
266
348
|
}
|
|
@@ -273,7 +355,7 @@ export const resolvePortSchemas = (nodes, connections) => {
|
|
|
273
355
|
for (const port of node.ports) {
|
|
274
356
|
if (port.io === "out" && isDerivedPort(node.kind, port)) {
|
|
275
357
|
assign(port.id, node.kind === "human" && port.key === "approve" && fromAiNode
|
|
276
|
-
? (envelopeDataSchemaOf(inSchema) ??
|
|
358
|
+
? (envelopeDataSchemaOf(inSchema) ?? UNDECLARED_SCHEMA)
|
|
277
359
|
: inSchema);
|
|
278
360
|
}
|
|
279
361
|
}
|
|
@@ -299,18 +381,15 @@ export const propagateDerivedPorts = (nodes, connections) => {
|
|
|
299
381
|
return touched ? { ...node, ports } : node;
|
|
300
382
|
});
|
|
301
383
|
};
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (isAnySchema(sup) || isAnySchema(sub))
|
|
307
|
-
return true;
|
|
308
|
-
// file は nominal:マーカー一致の file 同士だけ可(file → any は上の any 判定で許可)。
|
|
384
|
+
export const schemaFit = (sub, sup) => {
|
|
385
|
+
if (isUndeclaredSchema(sup) || isUndeclaredSchema(sub))
|
|
386
|
+
return "unknown";
|
|
387
|
+
// file は nominal:マーカー一致の file 同士だけ可。
|
|
309
388
|
// 構造が同じだけの普通の object は file に繋がない/file は普通の object に繋がない
|
|
310
389
|
if (isFileSchema(sub) || isFileSchema(sup))
|
|
311
|
-
return isFileSchema(sub) && isFileSchema(sup);
|
|
390
|
+
return isFileSchema(sub) && isFileSchema(sup) ? "ok" : "mismatch";
|
|
312
391
|
if (sub.type !== sup.type)
|
|
313
|
-
return
|
|
392
|
+
return "mismatch";
|
|
314
393
|
if (sup.type === "object") {
|
|
315
394
|
const subProps = (sub.properties ?? {});
|
|
316
395
|
const subRequired = (sub.required ?? []);
|
|
@@ -318,19 +397,22 @@ export const isSchemaSubset = (sub, sup) => {
|
|
|
318
397
|
for (const key of (sup.required ?? [])) {
|
|
319
398
|
const subProp = subProps[key];
|
|
320
399
|
if (!subProp || !subRequired.includes(key))
|
|
321
|
-
return
|
|
400
|
+
return "mismatch";
|
|
322
401
|
const supProp = supProps[key];
|
|
323
|
-
|
|
324
|
-
|
|
402
|
+
// ネストは「不適合でなければよい」(未宣言のプロパティで親を落とさない)
|
|
403
|
+
if (supProp && schemaFit(subProp, supProp) === "mismatch")
|
|
404
|
+
return "mismatch";
|
|
325
405
|
}
|
|
326
|
-
return
|
|
406
|
+
return "ok";
|
|
327
407
|
}
|
|
328
408
|
if (sup.type === "array") {
|
|
329
409
|
const supItems = sup.items;
|
|
330
410
|
const subItems = sub.items;
|
|
331
411
|
if (!supItems || !subItems)
|
|
332
|
-
return
|
|
333
|
-
return
|
|
412
|
+
return "ok";
|
|
413
|
+
return schemaFit(subItems, supItems) === "mismatch" ? "mismatch" : "ok";
|
|
334
414
|
}
|
|
335
|
-
return
|
|
415
|
+
return "ok";
|
|
336
416
|
};
|
|
417
|
+
// 保存時の接続チェック用:判断できない(未宣言)ものは通す
|
|
418
|
+
export const isSchemaSubset = (sub, sup) => schemaFit(sub, sup) !== "mismatch";
|