@mawaru/sdk 0.24.0 → 0.26.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/duplicate-selection.test.ts +8 -0
- package/_schemas/duplicate-selection.ts +4 -2
- package/_schemas/extract-component.test.ts +22 -0
- package/_schemas/extract-component.ts +4 -2
- package/_schemas/graph.test.ts +22 -5
- package/_schemas/graph.ts +15 -2
- package/dist/_schemas/duplicate-selection.js +4 -2
- package/dist/_schemas/extract-component.js +4 -2
- package/dist/_schemas/graph.d.ts +3 -3
- package/dist/_schemas/graph.js +14 -2
- package/package.json +1 -1
|
@@ -93,6 +93,14 @@ describe("duplicateSelection(選択ノードの複製)", () => {
|
|
|
93
93
|
expect(new Set(newPortIds).size).toBe(newPortIds.length)
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
+
it("node_key は引き継がない(同じ版に同じ node_key が2つあると保存 API が 422 になる)", () => {
|
|
97
|
+
const { nodes, connections, b } = chain()
|
|
98
|
+
const saved = nodes.map((n) => ({ ...n, node_key: uuid() }))
|
|
99
|
+
const result = dup(saved, connections, [b.id])
|
|
100
|
+
expect(result.nodes).toHaveLength(1)
|
|
101
|
+
expect(result.nodes[0]?.node_key).toBeUndefined()
|
|
102
|
+
})
|
|
103
|
+
|
|
96
104
|
it("元のノード・接続を変更しない", () => {
|
|
97
105
|
const { b, c, nodes, connections } = chain()
|
|
98
106
|
const snapshot = structuredClone({ nodes, connections })
|
|
@@ -20,11 +20,13 @@ export const duplicateSelection = (params: {
|
|
|
20
20
|
const selected = new Set(selectedIds)
|
|
21
21
|
|
|
22
22
|
// start / end は回路の構造ノードなので複製しない(選択に混ざっていても落とす)。
|
|
23
|
-
//
|
|
23
|
+
// 並び順は元グラフのまま(選択の指定順に依存させない)。
|
|
24
|
+
// node_key は既存行との突き合わせ用なので複製には引き継がない(同じ版に同じ key が
|
|
25
|
+
// 2つあると保存 API が 422 にする)
|
|
24
26
|
const portIdMap = new Map<string, string>()
|
|
25
27
|
const copied = nodes
|
|
26
28
|
.filter((n) => selected.has(n.id) && n.kind !== "start" && n.kind !== "end")
|
|
27
|
-
.map((n) => ({
|
|
29
|
+
.map(({ node_key: _node_key, ...n }) => ({
|
|
28
30
|
...n,
|
|
29
31
|
id: newId(),
|
|
30
32
|
ports: n.ports.map((p) => {
|
|
@@ -216,6 +216,28 @@ describe("buildExtractPlan(子グラフと親置換の生成)", () => {
|
|
|
216
216
|
expect(internal).toBeDefined()
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
+
it("子:node_key は持ち込まない(子 loop の draft に無い key で保存 API が 422 になる)", () => {
|
|
220
|
+
const { nodes, connections, b, c } = chain()
|
|
221
|
+
// 保存済みの親を読んだ状態(各ノードが node_key を持つ)から抽出する
|
|
222
|
+
const saved = nodes.map((n) => ({ ...n, node_key: uuid() }))
|
|
223
|
+
const plan = buildExtractPlan({
|
|
224
|
+
nodes: saved,
|
|
225
|
+
connections,
|
|
226
|
+
selectedIds: [b.id, c.id],
|
|
227
|
+
name: "PRレビュー",
|
|
228
|
+
componentLoopId: uuid(),
|
|
229
|
+
childStartOutPortId: uuid(),
|
|
230
|
+
childEnd: childEndNode(),
|
|
231
|
+
newId: uuid,
|
|
232
|
+
})
|
|
233
|
+
if (!plan.ok) throw new Error(plan.reason)
|
|
234
|
+
for (const n of plan.childNodes) expect(n.node_key).toBeUndefined()
|
|
235
|
+
expect(plan.groupNode.node_key).toBeUndefined()
|
|
236
|
+
// 親に残るノードの node_key はそのまま(既存行との突き合わせに使う)
|
|
237
|
+
const remaining = plan.parentNodes.filter((n) => n.kind !== "component")
|
|
238
|
+
for (const n of remaining) expect(n.node_key).toBeDefined()
|
|
239
|
+
})
|
|
240
|
+
|
|
219
241
|
it("子:start.main → 入口、出口ポート → end.in が配線される(出口1つは seed の in を使う)", () => {
|
|
220
242
|
const { plan, childStartOutPortId, childEnd } = build()
|
|
221
243
|
const [nb, nc] = plan.childNodes
|
|
@@ -134,12 +134,14 @@ export const buildExtractPlan = (params: {
|
|
|
134
134
|
|
|
135
135
|
const selected = new Set(selectedIds)
|
|
136
136
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
137
|
-
// =この後 JSON 化して保存 API
|
|
137
|
+
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
138
|
+
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
139
|
+
// 無い key を送ると保存 API が「この版に存在しないノード」で 422 にする)
|
|
138
140
|
const nodeIdMap = new Map<string, string>()
|
|
139
141
|
const portIdMap = new Map<string, string>()
|
|
140
142
|
const childNodes = nodes
|
|
141
143
|
.filter((n) => selected.has(n.id))
|
|
142
|
-
.map((n) => {
|
|
144
|
+
.map(({ node_key: _node_key, ...n }) => {
|
|
143
145
|
const id = newId()
|
|
144
146
|
nodeIdMap.set(n.id, id)
|
|
145
147
|
return {
|
package/_schemas/graph.test.ts
CHANGED
|
@@ -160,17 +160,25 @@ describe("aiConfigSchema", () => {
|
|
|
160
160
|
})
|
|
161
161
|
|
|
162
162
|
describe("humanConfigSchema", () => {
|
|
163
|
-
it("
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
it("strategy / approval_mode の既定値を補う(fixed・any)", () => {
|
|
164
|
+
const u1 = uuid()
|
|
165
|
+
expect(humanConfigSchema.parse({ assignment: { units: [[u1]] } })).toEqual({
|
|
166
|
+
assignment: { strategy: "fixed", approval_mode: "any", units: [[u1]] },
|
|
166
167
|
})
|
|
167
168
|
})
|
|
168
169
|
|
|
170
|
+
it("担当なし(units 空・省略)を拒否する(「誰でも」は無い)", () => {
|
|
171
|
+
expect(() => humanConfigSchema.parse({ assignment: {} })).toThrow()
|
|
172
|
+
expect(() =>
|
|
173
|
+
humanConfigSchema.parse({ assignment: { units: [] } }),
|
|
174
|
+
).toThrow(/担当/)
|
|
175
|
+
})
|
|
176
|
+
|
|
169
177
|
// 承認ビューは不透明なキー(program_dir / ai_dir と同じ)。実在するか・そのテナントが
|
|
170
178
|
// 選べるかは mawaru 側のカタログ(非公開)と保存 API が見るので、ここは形だけ
|
|
171
179
|
it("承認ビューを受け付ける", () => {
|
|
172
180
|
const parsed = humanConfigSchema.parse({
|
|
173
|
-
assignment: {},
|
|
181
|
+
assignment: { units: [[uuid()]] },
|
|
174
182
|
view: "acme/invoice",
|
|
175
183
|
})
|
|
176
184
|
expect(parsed.view).toBe("acme/invoice")
|
|
@@ -178,7 +186,7 @@ describe("humanConfigSchema", () => {
|
|
|
178
186
|
|
|
179
187
|
it("空のビューキーは拒否する", () => {
|
|
180
188
|
expect(() =>
|
|
181
|
-
humanConfigSchema.parse({ assignment: {}, view: "" }),
|
|
189
|
+
humanConfigSchema.parse({ assignment: { units: [[uuid()]] }, view: "" }),
|
|
182
190
|
).toThrow()
|
|
183
191
|
})
|
|
184
192
|
|
|
@@ -513,6 +521,8 @@ const humanNode = () => ({
|
|
|
513
521
|
port("out", "reject"),
|
|
514
522
|
]),
|
|
515
523
|
kind: "human" as const,
|
|
524
|
+
// 担当は必須(「担当なし=誰でも」は無い)
|
|
525
|
+
human: { assignment: { units: [[uuid()]] } },
|
|
516
526
|
})
|
|
517
527
|
const aiNode = () => ({
|
|
518
528
|
...makeNode([
|
|
@@ -538,6 +548,13 @@ const outByKey = (node: TestNode, key: string): GraphPort => {
|
|
|
538
548
|
}
|
|
539
549
|
|
|
540
550
|
describe("saveGraphBodySchema(kind 別ポート構成)", () => {
|
|
551
|
+
it("human の担当なし(human 設定の省略)を拒否する", () => {
|
|
552
|
+
const { human: _human, ...noAssign } = humanNode()
|
|
553
|
+
expect(() =>
|
|
554
|
+
saveGraphBodySchema.parse({ nodes: [noAssign], connections: [] }),
|
|
555
|
+
).toThrow(/担当/)
|
|
556
|
+
})
|
|
557
|
+
|
|
541
558
|
it("human の旧 ok/ng 構成を拒否し、in/approve/reject を受け付ける", () => {
|
|
542
559
|
expect(() =>
|
|
543
560
|
saveGraphBodySchema.parse({ nodes: [humanNode()], connections: [] }),
|
package/_schemas/graph.ts
CHANGED
|
@@ -135,7 +135,8 @@ export type AssignMember = z.infer<typeof assignMemberSchema>
|
|
|
135
135
|
|
|
136
136
|
// Human ノードの kind 別設定(node_humans + node_human_assignees に対応)。
|
|
137
137
|
// 担当 = ユニット(1人以上のメンバー)の順序付きリスト + 承認モード + 選出戦略。
|
|
138
|
-
// units[i] はメンバーの配列(1
|
|
138
|
+
// units[i] はメンバーの配列(1人=個人・複数=グループ)。**担当は必ず明示する**
|
|
139
|
+
// (「担当なし=tenant の誰でも」は無い。Human担当の必須化(誰でもの廃止).md)。
|
|
139
140
|
// approval_mode はノード単位(any=誰か1人 / all=全員。差し戻しは1人で成立=veto)。
|
|
140
141
|
// メンバーが tenant の membership であることの検証は保存 API 側で行う
|
|
141
142
|
// (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
|
|
@@ -148,7 +149,9 @@ export const humanConfigSchema = z.object({
|
|
|
148
149
|
.object({
|
|
149
150
|
strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
|
|
150
151
|
approval_mode: z.enum(["any", "all"]).default("any"),
|
|
151
|
-
units: z
|
|
152
|
+
units: z
|
|
153
|
+
.array(z.array(assignMemberSchema).min(1))
|
|
154
|
+
.min(1, "担当を1人以上指定してください"),
|
|
152
155
|
})
|
|
153
156
|
.superRefine((assignment, ctx) => {
|
|
154
157
|
if (assignment.strategy === "fixed" && assignment.units.length > 1) {
|
|
@@ -403,6 +406,16 @@ export const saveGraphBodySchema = z
|
|
|
403
406
|
hookDirs.add(hook.hook_dir)
|
|
404
407
|
})
|
|
405
408
|
|
|
409
|
+
// human は担当を必ず明示する(「担当なし=tenant の誰でも」は無い。
|
|
410
|
+
// Human担当の必須化(誰でもの廃止).md)
|
|
411
|
+
if (node.kind === "human" && node.human === undefined) {
|
|
412
|
+
ctx.addIssue({
|
|
413
|
+
code: "custom",
|
|
414
|
+
path: ["nodes", i, "human"],
|
|
415
|
+
message: "human ノードには担当(assignment.units)が必要です",
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
406
419
|
// component は参照先が無いと成立しない(他 kind の設定はデフォルトで補えるが
|
|
407
420
|
// component_loop_id は補えない)
|
|
408
421
|
if (node.kind === "component" && node.component === undefined) {
|
|
@@ -8,11 +8,13 @@ export const duplicateSelection = (params) => {
|
|
|
8
8
|
const { nodes, connections, selectedIds, dx, dy, newId } = params;
|
|
9
9
|
const selected = new Set(selectedIds);
|
|
10
10
|
// start / end は回路の構造ノードなので複製しない(選択に混ざっていても落とす)。
|
|
11
|
-
//
|
|
11
|
+
// 並び順は元グラフのまま(選択の指定順に依存させない)。
|
|
12
|
+
// node_key は既存行との突き合わせ用なので複製には引き継がない(同じ版に同じ key が
|
|
13
|
+
// 2つあると保存 API が 422 にする)
|
|
12
14
|
const portIdMap = new Map();
|
|
13
15
|
const copied = nodes
|
|
14
16
|
.filter((n) => selected.has(n.id) && n.kind !== "start" && n.kind !== "end")
|
|
15
|
-
.map((n) => ({
|
|
17
|
+
.map(({ node_key: _node_key, ...n }) => ({
|
|
16
18
|
...n,
|
|
17
19
|
id: newId(),
|
|
18
20
|
ports: n.ports.map((p) => {
|
|
@@ -61,12 +61,14 @@ export const buildExtractPlan = (params) => {
|
|
|
61
61
|
return analysis;
|
|
62
62
|
const selected = new Set(selectedIds);
|
|
63
63
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
64
|
-
// =この後 JSON 化して保存 API
|
|
64
|
+
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
65
|
+
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
66
|
+
// 無い key を送ると保存 API が「この版に存在しないノード」で 422 にする)
|
|
65
67
|
const nodeIdMap = new Map();
|
|
66
68
|
const portIdMap = new Map();
|
|
67
69
|
const childNodes = nodes
|
|
68
70
|
.filter((n) => selected.has(n.id))
|
|
69
|
-
.map((n) => {
|
|
71
|
+
.map(({ node_key: _node_key, ...n }) => {
|
|
70
72
|
const id = newId();
|
|
71
73
|
nodeIdMap.set(n.id, id);
|
|
72
74
|
return {
|
package/dist/_schemas/graph.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export declare const humanConfigSchema: z.ZodObject<{
|
|
|
58
58
|
any: "any";
|
|
59
59
|
all: "all";
|
|
60
60
|
}>>;
|
|
61
|
-
units: z.
|
|
61
|
+
units: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodUUID, z.ZodLiteral<"run_starter">]>>>;
|
|
62
62
|
}, z.core.$strip>;
|
|
63
63
|
}, z.core.$strip>;
|
|
64
64
|
export type HumanConfig = z.infer<typeof humanConfigSchema>;
|
|
@@ -199,7 +199,7 @@ export declare const graphNodeSchema: z.ZodObject<{
|
|
|
199
199
|
any: "any";
|
|
200
200
|
all: "all";
|
|
201
201
|
}>>;
|
|
202
|
-
units: z.
|
|
202
|
+
units: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodUUID, z.ZodLiteral<"run_starter">]>>>;
|
|
203
203
|
}, z.core.$strip>;
|
|
204
204
|
}, z.core.$strip>>;
|
|
205
205
|
component: z.ZodOptional<z.ZodObject<{
|
|
@@ -298,7 +298,7 @@ export declare const saveGraphBodySchema: z.ZodObject<{
|
|
|
298
298
|
any: "any";
|
|
299
299
|
all: "all";
|
|
300
300
|
}>>;
|
|
301
|
-
units: z.
|
|
301
|
+
units: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodUUID, z.ZodLiteral<"run_starter">]>>>;
|
|
302
302
|
}, z.core.$strip>;
|
|
303
303
|
}, z.core.$strip>>;
|
|
304
304
|
component: z.ZodOptional<z.ZodObject<{
|
package/dist/_schemas/graph.js
CHANGED
|
@@ -96,7 +96,8 @@ export const RUN_STARTER = "run_starter";
|
|
|
96
96
|
export const assignMemberSchema = z.union([z.uuid(), z.literal(RUN_STARTER)]);
|
|
97
97
|
// Human ノードの kind 別設定(node_humans + node_human_assignees に対応)。
|
|
98
98
|
// 担当 = ユニット(1人以上のメンバー)の順序付きリスト + 承認モード + 選出戦略。
|
|
99
|
-
// units[i] はメンバーの配列(1
|
|
99
|
+
// units[i] はメンバーの配列(1人=個人・複数=グループ)。**担当は必ず明示する**
|
|
100
|
+
// (「担当なし=tenant の誰でも」は無い。Human担当の必須化(誰でもの廃止).md)。
|
|
100
101
|
// approval_mode はノード単位(any=誰か1人 / all=全員。差し戻しは1人で成立=veto)。
|
|
101
102
|
// メンバーが tenant の membership であることの検証は保存 API 側で行う
|
|
102
103
|
// (docs/tasks/backlog/Human担当者の高度化(複数人承認とラウンドロビン).md)
|
|
@@ -109,7 +110,9 @@ export const humanConfigSchema = z.object({
|
|
|
109
110
|
.object({
|
|
110
111
|
strategy: z.enum(["fixed", "round_robin"]).default("fixed"),
|
|
111
112
|
approval_mode: z.enum(["any", "all"]).default("any"),
|
|
112
|
-
units: z
|
|
113
|
+
units: z
|
|
114
|
+
.array(z.array(assignMemberSchema).min(1))
|
|
115
|
+
.min(1, "担当を1人以上指定してください"),
|
|
113
116
|
})
|
|
114
117
|
.superRefine((assignment, ctx) => {
|
|
115
118
|
if (assignment.strategy === "fixed" && assignment.units.length > 1) {
|
|
@@ -335,6 +338,15 @@ export const saveGraphBodySchema = z
|
|
|
335
338
|
}
|
|
336
339
|
hookDirs.add(hook.hook_dir);
|
|
337
340
|
});
|
|
341
|
+
// human は担当を必ず明示する(「担当なし=tenant の誰でも」は無い。
|
|
342
|
+
// Human担当の必須化(誰でもの廃止).md)
|
|
343
|
+
if (node.kind === "human" && node.human === undefined) {
|
|
344
|
+
ctx.addIssue({
|
|
345
|
+
code: "custom",
|
|
346
|
+
path: ["nodes", i, "human"],
|
|
347
|
+
message: "human ノードには担当(assignment.units)が必要です",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
338
350
|
// component は参照先が無いと成立しない(他 kind の設定はデフォルトで補えるが
|
|
339
351
|
// component_loop_id は補えない)
|
|
340
352
|
if (node.kind === "component" && node.component === undefined) {
|