@danypops/papyrus 0.44.9 → 0.44.10

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.
@@ -13,17 +13,23 @@
13
13
  * (service.ts's moduleRegistry) stays registered unchanged for pi-papyrus's
14
14
  * own /discuss TUI, which never went through the retired mega-tool.
15
15
  */
16
- import { bindVehicleOperation, defineVehicleOperation, type VehicleContentBlock } from "@danypops/vehicle-core";
16
+ import type { VehicleContentBlock } from "@danypops/vehicle-core";
17
17
  import type { VehicleRegistry } from "@danypops/vehicle-server";
18
18
  import type { Artifact } from "../artifact/artifact.ts";
19
19
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
20
20
  import type { DiscussionAndRounds, Discussions } from "../discussion/discussion-service.ts";
21
21
  import { DISCUSSION_SUBTYPE, type DiscussionRound } from "../domain/discussion.ts";
22
22
  import { discussOperations } from "../modules/discuss.ts";
23
- import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
23
+ import {
24
+ createOperationDefiner,
25
+ numberProp,
26
+ type OperationSchemaProperties,
27
+ resolveArtifactIdWidened,
28
+ stringProp,
29
+ validationError,
30
+ } from "./shared.ts";
24
31
 
25
32
  const OWNER = "discuss";
26
- const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
27
33
  const arrayProp = { type: "array" } as const;
28
34
  /** Purely a client-side hint (see vehicle-client-pi's interactiveFollowUps) -- never read server-side, but must still be declared or the schema's additionalProperties:false rejects it outright. */
29
35
  const boolProp = { type: "boolean" } as const;
@@ -115,34 +121,30 @@ const optionsUnionSchema = { type: "array" } as const;
115
121
  export function registerDiscussVehicleOperations(registry: VehicleRegistry, discussions: Discussions, artifacts: ArtifactStore): void {
116
122
  const moduleOperations = new Map(discussOperations(discussions).map((op) => [op.name, op]));
117
123
  const call = <Output>(name: string, input: Record<string, unknown>): Output => moduleOperations.get(name)!.execute(input) as Output;
124
+ const baseDefine = createOperationDefiner(registry, OWNER, "discuss", ["discuss:read", "discuss:write"], call);
118
125
 
126
+ /**
127
+ * Defensively shallow-copies context.input before handing it to resolve() -- unlike every
128
+ * other domain's resolve, discuss's own (normalizeOptions, defaultActorToAgent) mutate their
129
+ * argument in place, and context.input must never be the object that mutation lands on.
130
+ */
119
131
  const define = (
120
132
  action: string,
121
133
  description: string,
122
134
  effect: "read" | "local-write",
123
- properties: Record<string, { type: string; enum?: readonly string[] }>,
135
+ properties: OperationSchemaProperties,
124
136
  required: readonly string[],
125
137
  resolve: (input: Record<string, unknown>) => Record<string, unknown>,
126
138
  wrap: (raw: unknown, resolvedInput: Record<string, unknown>) => unknown = (raw) => raw,
127
139
  ): void => {
128
- const operation = defineVehicleOperation({
129
- name: `discuss.${action}`,
130
- version: 1,
140
+ baseDefine(
141
+ action,
131
142
  description,
132
- input: looseObjectSchema(properties, required),
133
- output: passthroughOutput,
134
- permissions: ["discuss:read", "discuss:write"],
135
143
  effect,
136
- idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
137
- limits: LIMITS,
138
- });
139
- registry.register(
140
- OWNER,
141
- bindVehicleOperation(operation, () => async (context) => {
142
- const resolvedInput = resolve({ ...(context.input as Record<string, unknown>) });
143
- const raw = call(`discuss.${action}`, resolvedInput);
144
- return wrap(raw, resolvedInput);
145
- }),
144
+ properties,
145
+ required,
146
+ (input) => resolve({ ...input }),
147
+ (resolvedInput) => wrap(call(`discuss.${action}`, resolvedInput), resolvedInput),
146
148
  );
147
149
  };
148
150
 
@@ -3,17 +3,15 @@
3
3
  * Wraps modules/docs.ts's operation definitions. remove/restore/remove_subtree
4
4
  * are not duplicated here -- see ./artifact-trash-vehicle.ts.
5
5
  */
6
- import { bindVehicleOperation, defineVehicleOperation } from "@danypops/vehicle-core";
7
6
  import type { VehicleRegistry } from "@danypops/vehicle-server";
8
7
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
9
8
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
10
9
  import type { AuthorityRegistry } from "../authority-registry.ts";
11
10
  import { listDocuments } from "../domain-services.ts";
12
11
  import { docsOperations } from "../modules/docs.ts";
13
- import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
12
+ import { createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
14
13
 
15
14
  const OWNER = "docs";
16
- const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
17
15
 
18
16
  function resolveDocId(
19
17
  artifacts: ArtifactStore,
@@ -47,31 +45,7 @@ export function registerDocsVehicleOperations(
47
45
  ): void {
48
46
  const moduleOperations = new Map(docsOperations(artifacts, scopes, authority).map((op) => [op.name, op]));
49
47
  const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
50
-
51
- const define = (
52
- action: string,
53
- description: string,
54
- effect: "read" | "local-write",
55
- properties: Record<string, { type: string; enum?: readonly string[] }>,
56
- required: readonly string[],
57
- resolve: (input: Record<string, unknown>) => Record<string, unknown>,
58
- ): void => {
59
- const operation = defineVehicleOperation({
60
- name: `docs.${action}`,
61
- version: 1,
62
- description,
63
- input: looseObjectSchema(properties, required),
64
- output: passthroughOutput,
65
- permissions: ["docs:read", "docs:write"],
66
- effect,
67
- idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
68
- limits: LIMITS,
69
- });
70
- registry.register(
71
- OWNER,
72
- bindVehicleOperation(operation, () => async (context) => call(`docs.${action}`, resolve(context.input))),
73
- );
74
- };
48
+ const define = createOperationDefiner(registry, OWNER, "docs", ["docs:read", "docs:write"], call);
75
49
 
76
50
  define(
77
51
  "create",
@@ -7,17 +7,14 @@
7
7
  * input parsing. Resolves `name`/`target_name` to `id`/`target_id` server-side, in
8
8
  * the same call -- avoids a separate round trip per name before the real call.
9
9
  */
10
- import { bindVehicleOperation, defineVehicleOperation } from "@danypops/vehicle-core";
11
10
  import type { VehicleRegistry } from "@danypops/vehicle-server";
12
11
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
13
12
  import { notesOperations } from "../modules/notes.ts";
14
13
  import { NOTE_DISPOSITIONS, type Notes } from "../note/note-service.ts";
15
- import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
14
+ import { createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
16
15
 
17
16
  const OWNER = "notes";
18
17
 
19
- const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
20
-
21
18
  /** Resolves a note's id from either an explicit id or its title within projectRoot. */
22
19
  function resolveNoteId(artifacts: ArtifactStore, notes: Notes, projectRoot: string, id: unknown, name: unknown): string {
23
20
  if (typeof id === "string" && id.length > 0) return id;
@@ -43,31 +40,7 @@ function resolveArtifactId(artifacts: ArtifactStore, id: unknown, name: unknown)
43
40
  export function registerNotesVehicleOperations(registry: VehicleRegistry, notes: Notes, artifacts: ArtifactStore): void {
44
41
  const moduleOperations = new Map(notesOperations(notes).map((op) => [op.name, op]));
45
42
  const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
46
-
47
- const define = (
48
- action: string,
49
- description: string,
50
- effect: "read" | "local-write",
51
- properties: Record<string, { type: string; enum?: readonly string[] }>,
52
- required: readonly string[],
53
- resolve: (input: Record<string, unknown>) => Record<string, unknown>,
54
- ): void => {
55
- const operation = defineVehicleOperation({
56
- name: `notes.${action}`,
57
- version: 1,
58
- description,
59
- input: looseObjectSchema(properties, required),
60
- output: passthroughOutput,
61
- permissions: ["notes:read", "notes:write"],
62
- effect,
63
- idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
64
- limits: LIMITS,
65
- });
66
- registry.register(
67
- OWNER,
68
- bindVehicleOperation(operation, () => async (context) => call(`notes.${action}`, resolve(context.input))),
69
- );
70
- };
43
+ const define = createOperationDefiner(registry, OWNER, "notes", ["notes:read", "notes:write"], call);
71
44
 
72
45
  define(
73
46
  "capture",
@@ -18,7 +18,6 @@
18
18
  * WithVehicleContent) built from the same execution-DAG summary pi-papyrus's hand-rolled
19
19
  * tool used to build client-side.
20
20
  */
21
- import { bindVehicleOperation, defineVehicleOperation, type VehicleOperationContext } from "@danypops/vehicle-core";
22
21
  import type { VehicleRegistry } from "@danypops/vehicle-server";
23
22
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
24
23
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
@@ -34,17 +33,16 @@ import {
34
33
  classifyPlaybookComposition,
35
34
  classifySessionAuthorization,
36
35
  classifyTaskExecutionBounds,
37
- looseObjectSchema,
36
+ createOperationDefiner,
37
+ definePairedMutation,
38
38
  normalizeJsonEncodedField,
39
39
  numberProp,
40
- passthroughOutput,
41
40
  resolveArtifactIdWidened,
42
41
  stringProp,
43
42
  validationError,
44
43
  } from "./shared.ts";
45
44
 
46
45
  const OWNER = "playbooks";
47
- const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
48
46
 
49
47
  export interface PlaybooksVehicleDeps {
50
48
  artifacts: ArtifactStore;
@@ -80,36 +78,7 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
80
78
  classifySessionAuthorization(() =>
81
79
  classifyTaskExecutionBounds(() => classifyPlaybookComposition(() => moduleOperations.get(name)!.execute(input))),
82
80
  );
83
-
84
- const define = (
85
- action: string,
86
- description: string,
87
- effect: "read" | "local-write",
88
- properties: Record<string, { type: string; enum?: readonly string[] }>,
89
- required: readonly string[],
90
- resolve: (input: Record<string, unknown>) => Record<string, unknown>,
91
- execute?: (input: Record<string, unknown>, context: VehicleOperationContext<Record<string, unknown>>) => unknown,
92
- ): void => {
93
- const operation = defineVehicleOperation({
94
- name: `playbooks.${action}`,
95
- version: 1,
96
- description,
97
- input: looseObjectSchema(properties, required),
98
- output: passthroughOutput,
99
- permissions: ["playbooks:read", "playbooks:write"],
100
- effect,
101
- idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
102
- limits: LIMITS,
103
- });
104
- registry.register(
105
- OWNER,
106
- bindVehicleOperation(
107
- operation,
108
- () => async (context) =>
109
- (execute ?? ((input: Record<string, unknown>) => call(`playbooks.${action}`, input)))(resolve(context.input), context),
110
- ),
111
- );
112
- };
81
+ const define = createOperationDefiner(registry, OWNER, "playbooks", ["playbooks:read", "playbooks:write"], call);
113
82
 
114
83
  define(
115
84
  "create",
@@ -242,10 +211,13 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
242
211
  (input) => ({ ...input, id: resolvePlaybookId(artifacts, artifactScopes, input.id, input.name) }),
243
212
  );
244
213
 
245
- define(
246
- "contain",
247
- "Nests a child Playbook inside a parent -- the child's steps run AFTER the parent's own. Prefer parent_name/child_name over parent_id/child_id -- resolved server-side.",
248
- "local-write",
214
+ const resolvePlaybookIdField = (input: Record<string, unknown>, idProp: string, nameProp: string): string =>
215
+ resolvePlaybookId(artifacts, artifactScopes, input[idProp], input[nameProp]);
216
+
217
+ definePairedMutation(
218
+ define,
219
+ { idProp: "parent_id", nameProp: "parent_name" },
220
+ { idProp: "child_id", nameProp: "child_name" },
249
221
  {
250
222
  parent_id: stringProp,
251
223
  parent_name: stringProp,
@@ -256,38 +228,19 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
256
228
  session_id: stringProp,
257
229
  },
258
230
  [],
259
- (input) => ({
260
- ...input,
261
- parent_id: resolvePlaybookId(artifacts, artifactScopes, input.parent_id, input.parent_name),
262
- child_id: resolvePlaybookId(artifacts, artifactScopes, input.child_id, input.child_name),
263
- }),
264
- );
265
-
266
- define(
267
- "uncontain",
268
- "Removes a parent/child Playbook nesting. Idempotent -- a no-op if the edge is already absent.",
269
- "local-write",
231
+ resolvePlaybookIdField,
270
232
  {
271
- parent_id: stringProp,
272
- parent_name: stringProp,
273
- child_id: stringProp,
274
- child_name: stringProp,
275
- actor: stringProp,
276
- source: stringProp,
277
- session_id: stringProp,
233
+ action: "contain",
234
+ description:
235
+ "Nests a child Playbook inside a parent -- the child's steps run AFTER the parent's own. Prefer parent_name/child_name over parent_id/child_id -- resolved server-side.",
278
236
  },
279
- [],
280
- (input) => ({
281
- ...input,
282
- parent_id: resolvePlaybookId(artifacts, artifactScopes, input.parent_id, input.parent_name),
283
- child_id: resolvePlaybookId(artifacts, artifactScopes, input.child_id, input.child_name),
284
- }),
237
+ { action: "uncontain", description: "Removes a parent/child Playbook nesting. Idempotent -- a no-op if the edge is already absent." },
285
238
  );
286
239
 
287
- define(
288
- "depend",
289
- "Chains a prerequisite Playbook before another -- it must fully complete FIRST. Prefer dependency_name over dependency_id.",
290
- "local-write",
240
+ definePairedMutation(
241
+ define,
242
+ { idProp: "id", nameProp: "name" },
243
+ { idProp: "dependency_id", nameProp: "dependency_name" },
291
244
  {
292
245
  id: stringProp,
293
246
  name: stringProp,
@@ -298,31 +251,12 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
298
251
  session_id: stringProp,
299
252
  },
300
253
  [],
301
- (input) => ({
302
- ...input,
303
- id: resolvePlaybookId(artifacts, artifactScopes, input.id, input.name),
304
- dependency_id: resolvePlaybookId(artifacts, artifactScopes, input.dependency_id, input.dependency_name),
305
- }),
306
- );
307
-
308
- define(
309
- "undepend",
310
- "Removes a Playbook dependency. Idempotent -- a no-op if the edge is already absent.",
311
- "local-write",
254
+ resolvePlaybookIdField,
312
255
  {
313
- id: stringProp,
314
- name: stringProp,
315
- dependency_id: stringProp,
316
- dependency_name: stringProp,
317
- actor: stringProp,
318
- source: stringProp,
319
- session_id: stringProp,
256
+ action: "depend",
257
+ description:
258
+ "Chains a prerequisite Playbook before another -- it must fully complete FIRST. Prefer dependency_name over dependency_id.",
320
259
  },
321
- [],
322
- (input) => ({
323
- ...input,
324
- id: resolvePlaybookId(artifacts, artifactScopes, input.id, input.name),
325
- dependency_id: resolvePlaybookId(artifacts, artifactScopes, input.dependency_id, input.dependency_name),
326
- }),
260
+ { action: "undepend", description: "Removes a Playbook dependency. Idempotent -- a no-op if the edge is already absent." },
327
261
  );
328
262
  }
@@ -4,16 +4,14 @@
4
4
  * composition-root-only concern, absent here too). remove/restore/remove_subtree
5
5
  * are not duplicated here -- see ./artifact-trash-vehicle.ts.
6
6
  */
7
- import { bindVehicleOperation, defineVehicleOperation } from "@danypops/vehicle-core";
8
7
  import type { VehicleRegistry } from "@danypops/vehicle-server";
9
8
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
10
9
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
11
10
  import { listRules } from "../domain-services.ts";
12
11
  import { rulesOperations } from "../modules/rules.ts";
13
- import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
12
+ import { createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
14
13
 
15
14
  const OWNER = "rules";
16
- const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
17
15
 
18
16
  /** Resolves a rule's id from either an explicit id or its title, widened past project_root when unscoped-vs-scoped search finds nothing. */
19
17
  function resolveRuleId(
@@ -47,31 +45,7 @@ function resolveTaskId(artifacts: ArtifactStore, _projectRoot: string | undefine
47
45
  export function registerRulesVehicleOperations(registry: VehicleRegistry, artifacts: ArtifactStore, scopes: ArtifactScopeStore): void {
48
46
  const moduleOperations = new Map(rulesOperations(artifacts, scopes).map((op) => [op.name, op]));
49
47
  const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
50
-
51
- const define = (
52
- action: string,
53
- description: string,
54
- effect: "read" | "local-write",
55
- properties: Record<string, { type: string; enum?: readonly string[] }>,
56
- required: readonly string[],
57
- resolve: (input: Record<string, unknown>) => Record<string, unknown>,
58
- ): void => {
59
- const operation = defineVehicleOperation({
60
- name: `rules.${action}`,
61
- version: 1,
62
- description,
63
- input: looseObjectSchema(properties, required),
64
- output: passthroughOutput,
65
- permissions: ["rules:read", "rules:write"],
66
- effect,
67
- idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
68
- limits: LIMITS,
69
- });
70
- registry.register(
71
- OWNER,
72
- bindVehicleOperation(operation, () => async (context) => call(`rules.${action}`, resolve(context.input))),
73
- );
74
- };
48
+ const define = createOperationDefiner(registry, OWNER, "rules", ["rules:read", "rules:write"], call);
75
49
 
76
50
  define(
77
51
  "create",
@@ -3,7 +3,16 @@
3
3
  * VehicleRegistry projection (notes-vehicle.ts, rules-vehicle.ts, docs-vehicle.ts,
4
4
  * artifact-trash-vehicle.ts).
5
5
  */
6
- import { defineVehicleSchema, type VehicleContentBlock, VehicleError, type VehicleSchemaCodec } from "@danypops/vehicle-core";
6
+ import {
7
+ bindVehicleOperation,
8
+ defineVehicleOperation,
9
+ defineVehicleSchema,
10
+ type VehicleContentBlock,
11
+ VehicleError,
12
+ type VehicleOperationContext,
13
+ type VehicleSchemaCodec,
14
+ } from "@danypops/vehicle-core";
15
+ import type { VehicleRegistry } from "@danypops/vehicle-server";
7
16
  import type { Artifact } from "../artifact/artifact.ts";
8
17
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
9
18
  import { PlaybookCompositionError } from "../playbook/playbook-definition.ts";
@@ -223,3 +232,84 @@ export function buildWorkflowRunContent(
223
232
  ].join("\n");
224
233
  return { type: "text", text };
225
234
  }
235
+
236
+ export type OperationSchemaProperties = Record<string, { type: string; enum?: readonly string[] }>;
237
+
238
+ export type DefineOperation = (
239
+ action: string,
240
+ description: string,
241
+ effect: "read" | "local-write",
242
+ properties: OperationSchemaProperties,
243
+ required: readonly string[],
244
+ resolve: (input: Record<string, unknown>) => Record<string, unknown>,
245
+ execute?: (input: Record<string, unknown>, context: VehicleOperationContext<Record<string, unknown>>) => unknown,
246
+ ) => void;
247
+
248
+ const STANDARD_OPERATION_LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
249
+
250
+ /**
251
+ * Every *-vehicle.ts handler wires up the identical defineVehicleOperation +
252
+ * bindVehicleOperation + registry.register triple per action, differing only in
253
+ * owner/domain-prefix/permissions and (for tasks/playbooks) a real execute() override
254
+ * in place of the default "call the wrapped module operation" behavior. One factory,
255
+ * called once per domain, replaces that repetition.
256
+ */
257
+ export function createOperationDefiner(
258
+ registry: VehicleRegistry,
259
+ owner: string,
260
+ domain: string,
261
+ permissions: readonly [string, string],
262
+ defaultCall: (name: string, input: Record<string, unknown>) => unknown,
263
+ ): DefineOperation {
264
+ return (action, description, effect, properties, required, resolve, execute) => {
265
+ const operation = defineVehicleOperation({
266
+ name: `${domain}.${action}`,
267
+ version: 1,
268
+ description,
269
+ input: looseObjectSchema(properties, required),
270
+ output: passthroughOutput,
271
+ permissions: [...permissions],
272
+ effect,
273
+ idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
274
+ limits: STANDARD_OPERATION_LIMITS,
275
+ });
276
+ registry.register(
277
+ owner,
278
+ bindVehicleOperation(
279
+ operation,
280
+ () => async (context) =>
281
+ (execute ?? ((input: Record<string, unknown>) => defaultCall(`${domain}.${action}`, input)))(resolve(context.input), context),
282
+ ),
283
+ );
284
+ };
285
+ }
286
+
287
+ export interface PairedMutationFieldSpec {
288
+ idProp: string;
289
+ nameProp: string;
290
+ }
291
+
292
+ /**
293
+ * depend/undepend and contain/uncontain (tasks-vehicle.ts, playbooks-vehicle.ts) share
294
+ * one shape: two id-or-name fields resolved the same way for both the add and the
295
+ * remove action, differing only in action name/description. One call replaces two
296
+ * near-identical define() invocations.
297
+ */
298
+ export function definePairedMutation(
299
+ define: DefineOperation,
300
+ first: PairedMutationFieldSpec,
301
+ second: PairedMutationFieldSpec,
302
+ properties: OperationSchemaProperties,
303
+ required: readonly string[],
304
+ resolveId: (input: Record<string, unknown>, idProp: string, nameProp: string) => string,
305
+ add: { action: string; description: string },
306
+ remove: { action: string; description: string },
307
+ ): void {
308
+ const resolve = (input: Record<string, unknown>): Record<string, unknown> => ({
309
+ ...input,
310
+ [first.idProp]: resolveId(input, first.idProp, first.nameProp),
311
+ [second.idProp]: resolveId(input, second.idProp, second.nameProp),
312
+ });
313
+ define(add.action, add.description, "local-write", properties, required, resolve);
314
+ define(remove.action, remove.description, "local-write", properties, required, resolve);
315
+ }