@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.
@@ -19,7 +19,6 @@
19
19
  *
20
20
  * remove/remove_subtree/restore are not duplicated here -- see ./artifact-trash-vehicle.ts.
21
21
  */
22
- import { bindVehicleOperation, defineVehicleOperation, type VehicleOperationContext } from "@danypops/vehicle-core";
23
22
  import type { VehicleRegistry } from "@danypops/vehicle-server";
24
23
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
25
24
  import type { TaskViewMode } from "../domain/task-scope.ts";
@@ -31,10 +30,10 @@ import {
31
30
  classifySessionAuthorization,
32
31
  classifyTaskDependencyCycles,
33
32
  classifyTaskExecutionBounds,
33
+ createOperationDefiner,
34
+ definePairedMutation,
34
35
  labelsById,
35
- looseObjectSchema,
36
36
  numberProp,
37
- passthroughOutput,
38
37
  resolveArtifactIdWidened,
39
38
  stringProp,
40
39
  validationError,
@@ -167,36 +166,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
167
166
  classifySessionAuthorization(() =>
168
167
  classifyTaskExecutionBounds(() => classifyTaskDependencyCycles(() => moduleOperations.get(name)!.execute(input))),
169
168
  );
170
-
171
- const define = (
172
- action: string,
173
- description: string,
174
- effect: "read" | "local-write",
175
- properties: Record<string, { type: string; enum?: readonly string[] }>,
176
- required: readonly string[],
177
- resolve: (input: Record<string, unknown>) => Record<string, unknown>,
178
- execute?: (input: Record<string, unknown>, context: VehicleOperationContext<Record<string, unknown>>) => unknown,
179
- ): void => {
180
- const operation = defineVehicleOperation({
181
- name: `tasks.${action}`,
182
- version: 1,
183
- description,
184
- input: looseObjectSchema(properties, required),
185
- output: passthroughOutput,
186
- permissions: ["tasks:read", "tasks:write"],
187
- effect,
188
- idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
189
- limits: LIMITS,
190
- });
191
- registry.register(
192
- OWNER,
193
- bindVehicleOperation(
194
- operation,
195
- () => async (context) =>
196
- (execute ?? ((input: Record<string, unknown>) => call(`tasks.${action}`, input)))(resolve(context.input), context),
197
- ),
198
- );
199
- };
169
+ const define = createOperationDefiner(registry, OWNER, "tasks", ["tasks:read", "tasks:write"], call);
200
170
 
201
171
  /** Shared by every action taking a single id/name: resolves root_task_name first, then name -> id against the final scope. */
202
172
  const resolveIdAndScope = (input: Record<string, unknown>): Record<string, unknown> => {
@@ -395,6 +365,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
395
365
  (input) => input,
396
366
  );
397
367
 
368
+ /** Injects the caller's own session claims (never a model-visible input field) onto every focus/pause/unpause/clear_focus call -- see this file's own doc comment. */
398
369
  const focusOperation = (
399
370
  action: "focus" | "pause" | "unpause" | "clear_focus",
400
371
  description: string,
@@ -402,24 +373,10 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
402
373
  required: readonly string[],
403
374
  resolve: (input: Record<string, unknown>) => Record<string, unknown>,
404
375
  ): void => {
405
- const operation = defineVehicleOperation({
406
- name: `tasks.${action}`,
407
- version: 1,
408
- description,
409
- input: looseObjectSchema(properties, required),
410
- output: passthroughOutput,
411
- permissions: ["tasks:read", "tasks:write"],
412
- effect: "local-write",
413
- idempotency: { mode: "unsafe" },
414
- limits: LIMITS,
376
+ define(action, description, "local-write", properties, required, resolve, (resolvedInput, context) => {
377
+ const claims = context.principal?.claims as { sessionId?: string; sessionSecret?: string } | undefined;
378
+ return call(`tasks.${action}`, { ...resolvedInput, session_id: claims?.sessionId, session_secret: claims?.sessionSecret });
415
379
  });
416
- registry.register(
417
- OWNER,
418
- bindVehicleOperation(operation, () => async (context) => {
419
- const claims = context.principal?.claims as { sessionId?: string; sessionSecret?: string } | undefined;
420
- return call(`tasks.${action}`, { ...resolve(context.input), session_id: claims?.sessionId, session_secret: claims?.sessionSecret });
421
- }),
422
- );
423
380
  };
424
381
 
425
382
  focusOperation(
@@ -585,34 +542,16 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
585
542
 
586
543
  const scopeProp = { type: "string", enum: ["project", "graph", "all"] } as const;
587
544
 
588
- define(
589
- "depend",
590
- "Adds a dependency edge (this task waits for dependency_id/dependency_name). Dependency edges form an executable DAG -- self-dependencies and cycles are rejected. A name resolved outside project_root's own scope is retried once against every project before failing, unless scope is pinned explicitly.",
591
- "local-write",
592
- {
593
- id: stringProp,
594
- name: stringProp,
595
- dependency_id: stringProp,
596
- dependency_name: stringProp,
597
- project_root: stringProp,
598
- scope: scopeProp,
599
- session_id: stringProp,
600
- },
601
- [],
602
- (input) => {
603
- const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
604
- return {
605
- ...input,
606
- id: resolveTaskId(artifacts, tasks, filter, input.id, input.name),
607
- dependency_id: resolveTaskId(artifacts, tasks, filter, input.dependency_id, input.dependency_name),
608
- };
609
- },
610
- );
545
+ /** Resolves a task id/name pair against the filter derived from this same input's own project_root/scope -- shared by depend/undepend and contain/uncontain below. */
546
+ const resolveScopedTaskId = (input: Record<string, unknown>, idProp: string, nameProp: string): string => {
547
+ const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
548
+ return resolveTaskId(artifacts, tasks, filter, input[idProp], input[nameProp]);
549
+ };
611
550
 
612
- define(
613
- "undepend",
614
- "Removes a dependency edge. Idempotent -- a no-op if the edge is already absent.",
615
- "local-write",
551
+ definePairedMutation(
552
+ define,
553
+ { idProp: "id", nameProp: "name" },
554
+ { idProp: "dependency_id", nameProp: "dependency_name" },
616
555
  {
617
556
  id: stringProp,
618
557
  name: stringProp,
@@ -623,44 +562,19 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
623
562
  session_id: stringProp,
624
563
  },
625
564
  [],
626
- (input) => {
627
- const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
628
- return {
629
- ...input,
630
- id: resolveTaskId(artifacts, tasks, filter, input.id, input.name),
631
- dependency_id: resolveTaskId(artifacts, tasks, filter, input.dependency_id, input.dependency_name),
632
- };
633
- },
634
- );
635
-
636
- define(
637
- "contain",
638
- "Nests a child Task inside a parent (parent_id/parent_name contains child_id/child_name) -- explicit hierarchy, distinct from depends_on execution ordering. A name resolved outside project_root's own scope is retried once against every project before failing, unless scope is pinned explicitly.",
639
- "local-write",
565
+ resolveScopedTaskId,
640
566
  {
641
- parent_id: stringProp,
642
- parent_name: stringProp,
643
- child_id: stringProp,
644
- child_name: stringProp,
645
- project_root: stringProp,
646
- scope: scopeProp,
647
- session_id: stringProp,
648
- },
649
- [],
650
- (input) => {
651
- const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
652
- return {
653
- ...input,
654
- parent_id: resolveTaskId(artifacts, tasks, filter, input.parent_id, input.parent_name),
655
- child_id: resolveTaskId(artifacts, tasks, filter, input.child_id, input.child_name),
656
- };
567
+ action: "depend",
568
+ description:
569
+ "Adds a dependency edge (this task waits for dependency_id/dependency_name). Dependency edges form an executable DAG -- self-dependencies and cycles are rejected. A name resolved outside project_root's own scope is retried once against every project before failing, unless scope is pinned explicitly.",
657
570
  },
571
+ { action: "undepend", description: "Removes a dependency edge. Idempotent -- a no-op if the edge is already absent." },
658
572
  );
659
573
 
660
- define(
661
- "uncontain",
662
- "Removes a parent/child nesting. Idempotent -- a no-op if the edge is already absent.",
663
- "local-write",
574
+ definePairedMutation(
575
+ define,
576
+ { idProp: "parent_id", nameProp: "parent_name" },
577
+ { idProp: "child_id", nameProp: "child_name" },
664
578
  {
665
579
  parent_id: stringProp,
666
580
  parent_name: stringProp,
@@ -671,14 +585,13 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
671
585
  session_id: stringProp,
672
586
  },
673
587
  [],
674
- (input) => {
675
- const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
676
- return {
677
- ...input,
678
- parent_id: resolveTaskId(artifacts, tasks, filter, input.parent_id, input.parent_name),
679
- child_id: resolveTaskId(artifacts, tasks, filter, input.child_id, input.child_name),
680
- };
588
+ resolveScopedTaskId,
589
+ {
590
+ action: "contain",
591
+ description:
592
+ "Nests a child Task inside a parent (parent_id/parent_name contains child_id/child_name) -- explicit hierarchy, distinct from depends_on execution ordering. A name resolved outside project_root's own scope is retried once against every project before failing, unless scope is pinned explicitly.",
681
593
  },
594
+ { action: "uncontain", description: "Removes a parent/child nesting. Idempotent -- a no-op if the edge is already absent." },
682
595
  );
683
596
 
684
597
  define(
@@ -6,41 +6,20 @@
6
6
  import type { ArtifactEventContext } from "../artifact/artifact-event.ts";
7
7
  import type { Discussions } from "../discussion/discussion-service.ts";
8
8
  import type { OperationDefinition } from "../module-registry.ts";
9
+ import { type OperationInput, optionalNumber, optionalString, optionalStringArray } from "./operation-input.ts";
9
10
 
10
11
  const MODULE_ID = "discuss";
11
12
 
12
- type OperationInput = Record<string, unknown>;
13
-
14
- function string(input: OperationInput, key: string): string {
15
- const value = input[key];
16
- if (typeof value !== "string" || value.length === 0)
17
- throw new Error(`${key} is required${REQUIRED_FIELD_HINTS[key] ? ` (${REQUIRED_FIELD_HINTS[key]})` : ""}`);
18
- return value;
19
- }
20
-
21
13
  /** Fields whose name alone doesn't say what a valid value looks like -- everything else (title, content, id, settlement) is self-explanatory. */
22
14
  const REQUIRED_FIELD_HINTS: Record<string, string> = {
23
15
  actor: 'a display name for who is posting, e.g. "alice" or "agent"',
24
16
  };
25
17
 
26
- function optionalString(input: OperationInput, key: string): string | undefined {
27
- const value = input[key];
28
- if (value === undefined) return undefined;
29
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
30
- return value;
31
- }
32
-
33
- function optionalStringArray(input: OperationInput, key: string): string[] | undefined {
34
- const value = input[key];
35
- if (value === undefined) return undefined;
36
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
37
- return value as string[];
38
- }
39
-
40
- function optionalNumber(input: OperationInput, key: string): number | undefined {
18
+ /** Same contract as the shared string() in ./operation-input.ts, plus a field-specific hint on failure -- discuss's own operations are the one surface where a bare "actor is required" isn't self-explanatory. */
19
+ function string(input: OperationInput, key: string): string {
41
20
  const value = input[key];
42
- if (value === undefined) return undefined;
43
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
21
+ if (typeof value !== "string" || value.length === 0)
22
+ throw new Error(`${key} is required${REQUIRED_FIELD_HINTS[key] ? ` (${REQUIRED_FIELD_HINTS[key]})` : ""}`);
44
23
  return value;
45
24
  }
46
25
 
@@ -21,31 +21,10 @@ import {
21
21
  updateDocument,
22
22
  } from "../domain-services.ts";
23
23
  import type { OperationDefinition } from "../module-registry.ts";
24
+ import { type OperationInput, optionalNumber, optionalString, string } from "./operation-input.ts";
24
25
 
25
26
  const MODULE_ID = "docs";
26
27
 
27
- type OperationInput = Record<string, unknown>;
28
-
29
- function string(input: OperationInput, key: string): string {
30
- const value = input[key];
31
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
32
- return value;
33
- }
34
-
35
- function optionalString(input: OperationInput, key: string): string | undefined {
36
- const value = input[key];
37
- if (value === undefined) return undefined;
38
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
39
- return value;
40
- }
41
-
42
- function optionalNumber(input: OperationInput, key: string): number | undefined {
43
- const value = input[key];
44
- if (value === undefined) return undefined;
45
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
46
- return value;
47
- }
48
-
49
28
  const eventContext = (input: OperationInput) => ({
50
29
  actor: optionalString(input, "actor"),
51
30
  source: optionalString(input, "source"),
@@ -17,17 +17,10 @@ import type {
17
17
  import { GraphProjection } from "../graph-projection/graph-projection-service.ts";
18
18
  import type { OperationDefinition } from "../module-registry.ts";
19
19
  import type { GraphProjectionStore } from "../stores/graph-projection-store.ts";
20
+ import { type OperationInput, string } from "./operation-input.ts";
20
21
 
21
22
  const MODULE_ID = "graph_projection";
22
23
 
23
- type OperationInput = Record<string, unknown>;
24
-
25
- function string(input: OperationInput, key: string): string {
26
- const value = input[key];
27
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
28
- return value;
29
- }
30
-
31
24
  function number(input: OperationInput, key: string): number {
32
25
  const value = input[key];
33
26
  if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} is required and must be a number`);
@@ -8,31 +8,10 @@
8
8
  import type { JsonValue, LogLevel } from "../domain/log-entry.ts";
9
9
  import type { Logs } from "../log/log-service.ts";
10
10
  import type { OperationDefinition } from "../module-registry.ts";
11
+ import { type OperationInput, optionalNumber, optionalString, string } from "./operation-input.ts";
11
12
 
12
13
  const MODULE_ID = "logs";
13
14
 
14
- type OperationInput = Record<string, unknown>;
15
-
16
- function string(input: OperationInput, key: string): string {
17
- const value = input[key];
18
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
19
- return value;
20
- }
21
-
22
- function optionalString(input: OperationInput, key: string): string | undefined {
23
- const value = input[key];
24
- if (value === undefined) return undefined;
25
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
26
- return value;
27
- }
28
-
29
- function optionalNumber(input: OperationInput, key: string): number | undefined {
30
- const value = input[key];
31
- if (value === undefined) return undefined;
32
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
33
- return value;
34
- }
35
-
36
15
  function isJsonValue(value: unknown): value is JsonValue {
37
16
  return (
38
17
  value === null ||
@@ -16,31 +16,10 @@
16
16
  import type { NoteEventDirection } from "../domain/note-event.ts";
17
17
  import type { OperationDefinition } from "../module-registry.ts";
18
18
  import type { NoteDisposition, Notes } from "../note/note-service.ts";
19
+ import { type OperationInput, optionalNumber, optionalString, string } from "./operation-input.ts";
19
20
 
20
21
  const MODULE_ID = "notes";
21
22
 
22
- type OperationInput = Record<string, unknown>;
23
-
24
- function string(input: OperationInput, key: string): string {
25
- const value = input[key];
26
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
27
- return value;
28
- }
29
-
30
- function optionalString(input: OperationInput, key: string): string | undefined {
31
- const value = input[key];
32
- if (value === undefined) return undefined;
33
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
34
- return value;
35
- }
36
-
37
- function optionalNumber(input: OperationInput, key: string): number | undefined {
38
- const value = input[key];
39
- if (value === undefined) return undefined;
40
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
41
- return value;
42
- }
43
-
44
23
  /** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. */
45
24
  export const NOTES_OPERATION_NAMES = [
46
25
  "notes.capture",
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared runtime validators for a Vehicle operation's raw `input` object.
3
+ * A Vehicle operation's own JSON-schema `input` field is descriptive metadata
4
+ * only (see handlers/shared.ts's looseObjectSchema) -- never itself enforced
5
+ * at runtime -- so every module's own operation body narrows `unknown` to
6
+ * the real shape it needs through these.
7
+ */
8
+ export type OperationInput = Record<string, unknown>;
9
+
10
+ export function string(input: OperationInput, key: string): string {
11
+ const value = input[key];
12
+ if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
13
+ return value;
14
+ }
15
+
16
+ export function optionalString(input: OperationInput, key: string): string | undefined {
17
+ const value = input[key];
18
+ if (value === undefined) return undefined;
19
+ if (typeof value !== "string") throw new Error(`${key} must be a string`);
20
+ return value;
21
+ }
22
+
23
+ export function optionalStringArray(input: OperationInput, key: string): string[] | undefined {
24
+ const value = input[key];
25
+ if (value === undefined) return undefined;
26
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
27
+ return value as string[];
28
+ }
29
+
30
+ export function optionalNumber(input: OperationInput, key: string): number | undefined {
31
+ const value = input[key];
32
+ if (value === undefined) return undefined;
33
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
34
+ return value;
35
+ }
@@ -31,31 +31,10 @@ import type { SessionIdentity } from "../session-identity/session-identity-servi
31
31
  import type { TaskEventStore } from "../stores/task-event-store.ts";
32
32
  import type { TaskScopeStore } from "../stores/task-scope-store.ts";
33
33
  import type { Tasks } from "../task/task-service.ts";
34
+ import { type OperationInput, optionalNumber, optionalString, string } from "./operation-input.ts";
34
35
 
35
36
  const MODULE_ID = "playbooks";
36
37
 
37
- type OperationInput = Record<string, unknown>;
38
-
39
- function string(input: OperationInput, key: string): string {
40
- const value = input[key];
41
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
42
- return value;
43
- }
44
-
45
- function optionalString(input: OperationInput, key: string): string | undefined {
46
- const value = input[key];
47
- if (value === undefined) return undefined;
48
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
49
- return value;
50
- }
51
-
52
- function optionalNumber(input: OperationInput, key: string): number | undefined {
53
- const value = input[key];
54
- if (value === undefined) return undefined;
55
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
56
- return value;
57
- }
58
-
59
38
  const eventContext = (input: OperationInput) => ({
60
39
  actor: optionalString(input, "actor"),
61
40
  source: optionalString(input, "source"),
@@ -24,31 +24,10 @@ import {
24
24
  updateRule,
25
25
  } from "../domain-services.ts";
26
26
  import type { OperationDefinition } from "../module-registry.ts";
27
+ import { type OperationInput, optionalNumber, optionalString, string } from "./operation-input.ts";
27
28
 
28
29
  const MODULE_ID = "rules";
29
30
 
30
- type OperationInput = Record<string, unknown>;
31
-
32
- function string(input: OperationInput, key: string): string {
33
- const value = input[key];
34
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
35
- return value;
36
- }
37
-
38
- function optionalString(input: OperationInput, key: string): string | undefined {
39
- const value = input[key];
40
- if (value === undefined) return undefined;
41
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
42
- return value;
43
- }
44
-
45
- function optionalNumber(input: OperationInput, key: string): number | undefined {
46
- const value = input[key];
47
- if (value === undefined) return undefined;
48
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
49
- return value;
50
- }
51
-
52
31
  const eventContext = (input: OperationInput) => ({
53
32
  actor: optionalString(input, "actor"),
54
33
  source: optionalString(input, "source"),
@@ -5,24 +5,10 @@
5
5
  */
6
6
  import type { OperationDefinition } from "../module-registry.ts";
7
7
  import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
8
+ import { type OperationInput, optionalString, string } from "./operation-input.ts";
8
9
 
9
10
  const MODULE_ID = "session-identity";
10
11
 
11
- type OperationInput = Record<string, unknown>;
12
-
13
- function string(input: OperationInput, key: string): string {
14
- const value = input[key];
15
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
16
- return value;
17
- }
18
-
19
- function optionalString(input: OperationInput, key: string): string | undefined {
20
- const value = input[key];
21
- if (value === undefined) return undefined;
22
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
23
- return value;
24
- }
25
-
26
12
  /** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. */
27
13
  export const SESSION_IDENTITY_OPERATION_NAMES = ["session.register", "session.release"] as const;
28
14
 
@@ -13,10 +13,10 @@
13
13
  *
14
14
  * Task-domain-internal files (task-context.ts, task-execution.ts, domain/task-event.ts,
15
15
  * domain/task-scope.ts) are imported directly — they belong to this bounded context,
16
- * unlike a different module's infrastructure. Generic input-parsing helpers are
17
- * duplicated locally rather than imported from src/service.ts, matching the precedent
18
- * set by modules/notes.ts: a module does not import another module's infrastructure,
19
- * including the composition root's own helpers.
16
+ * unlike a different module's infrastructure. Generic input-parsing helpers come from
17
+ * ./operation-input.ts, a peer utility at this same modules/ layer with no domain logic
18
+ * of its own -- not "another module's infrastructure" or the composition root's helpers,
19
+ * the actual thing this module avoids importing.
20
20
  */
21
21
 
22
22
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
@@ -28,38 +28,10 @@ import type { SessionIdentity } from "../session-identity/session-identity-servi
28
28
  import { taskContext } from "../task/task-context.ts";
29
29
  import { projectTaskExecution } from "../task/task-execution.ts";
30
30
  import type { TaskStatus, Tasks } from "../task/task-service.ts";
31
+ import { type OperationInput, optionalNumber, optionalString, optionalStringArray, string } from "./operation-input.ts";
31
32
 
32
33
  const MODULE_ID = "tasks";
33
34
 
34
- type OperationInput = Record<string, unknown>;
35
-
36
- function string(input: OperationInput, key: string): string {
37
- const value = input[key];
38
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
39
- return value;
40
- }
41
-
42
- function optionalString(input: OperationInput, key: string): string | undefined {
43
- const value = input[key];
44
- if (value === undefined) return undefined;
45
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
46
- return value;
47
- }
48
-
49
- function optionalStringArray(input: OperationInput, key: string): string[] | undefined {
50
- const value = input[key];
51
- if (value === undefined) return undefined;
52
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
53
- return value as string[];
54
- }
55
-
56
- function optionalNumber(input: OperationInput, key: string): number | undefined {
57
- const value = input[key];
58
- if (value === undefined) return undefined;
59
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
60
- return value;
61
- }
62
-
63
35
  const eventContext = (input: OperationInput): TaskEventContext => ({
64
36
  actor: optionalString(input, "actor"),
65
37
  source: optionalString(input, "source"),
package/src/service.ts CHANGED
@@ -25,6 +25,7 @@ import { DOCS_OPERATION_NAMES, docsOperations } from "./modules/docs.ts";
25
25
  import { GRAPH_PROJECTION_OPERATION_NAMES, graphProjectionOperations } from "./modules/graph-projection.ts";
26
26
  import { LOGS_OPERATION_NAMES, logsOperations } from "./modules/logs.ts";
27
27
  import { NOTES_OPERATION_NAMES, notesOperations } from "./modules/notes.ts";
28
+ import { type OperationInput, optionalNumber, optionalString, string } from "./modules/operation-input.ts";
28
29
  import { PLAYBOOKS_OPERATION_NAMES, playbooksOperations } from "./modules/playbooks.ts";
29
30
  import { RULES_OPERATION_NAMES, rulesOperations } from "./modules/rules.ts";
30
31
  import { SESSION_IDENTITY_OPERATION_NAMES, sessionIdentityOperations } from "./modules/session-identity.ts";
@@ -99,7 +100,6 @@ export const EXPECTED_OPERATION_NAMES = [
99
100
  ] as const;
100
101
 
101
102
  export type OperationName = (typeof EXPECTED_OPERATION_NAMES)[number];
102
- type OperationInput = Record<string, unknown>;
103
103
  type OperationHandler = (input: OperationInput) => unknown;
104
104
 
105
105
  export class UnknownOperationError extends Error {}
@@ -107,33 +107,6 @@ export class MigrationRequiredError extends Error {}
107
107
  export class PayloadTooLargeError extends Error {}
108
108
  export { InvalidSessionSecretError };
109
109
 
110
- function string(input: OperationInput, key: string): string {
111
- const value = input[key];
112
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
113
- return value;
114
- }
115
-
116
- function optionalString(input: OperationInput, key: string): string | undefined {
117
- const value = input[key];
118
- if (value === undefined) return undefined;
119
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
120
- return value;
121
- }
122
-
123
- function _optionalStringArray(input: OperationInput, key: string): string[] | undefined {
124
- const value = input[key];
125
- if (value === undefined) return undefined;
126
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
127
- return value as string[];
128
- }
129
-
130
- function optionalNumber(input: OperationInput, key: string): number | undefined {
131
- const value = input[key];
132
- if (value === undefined) return undefined;
133
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
134
- return value;
135
- }
136
-
137
110
  function normalizeCreateInput(input: OperationInput): CreateArtifactInput {
138
111
  const { template_id, ...rest } = input;
139
112
  return { ...rest, templateId: typeof template_id === "string" ? template_id : undefined } as CreateArtifactInput;