@danypops/papyrus 0.44.9 → 0.44.11

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
 
@@ -7,6 +7,7 @@
7
7
  * ArtifactStore-based with no other module's concrete class dependency.
8
8
  */
9
9
 
10
+ import { summarizeArtifact } from "../artifact/artifact.ts";
10
11
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
11
12
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
12
13
  import type { AuthorityRegistry } from "../authority-registry.ts";
@@ -21,31 +22,10 @@ import {
21
22
  updateDocument,
22
23
  } from "../domain-services.ts";
23
24
  import type { OperationDefinition } from "../module-registry.ts";
25
+ import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
24
26
 
25
27
  const MODULE_ID = "docs";
26
28
 
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
29
  const eventContext = (input: OperationInput) => ({
50
30
  actor: optionalString(input, "actor"),
51
31
  source: optionalString(input, "source"),
@@ -97,7 +77,10 @@ export function docsOperations(artifacts: ArtifactStore, scopes: ArtifactScopeSt
97
77
  eventContext(input),
98
78
  ),
99
79
  ),
100
- define("docs.list", (input: OperationInput) => listDocuments(artifacts, scopes, artifactFilter(input))),
80
+ define("docs.list", (input: OperationInput) => {
81
+ const docs = listDocuments(artifacts, scopes, artifactFilter(input));
82
+ return optionalBoolean(input, "full") === true ? docs : docs.map(summarizeArtifact);
83
+ }),
101
84
  define("docs.show", (input: OperationInput) => showDocument(artifacts, string(input, "id"))),
102
85
  define("docs.activate", (input: OperationInput) =>
103
86
  transitionDocument(artifacts, string(input, "id"), "activate", authority, eventContext(input)),
@@ -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,42 @@
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
+ }
36
+
37
+ export function optionalBoolean(input: OperationInput, key: string): boolean | undefined {
38
+ const value = input[key];
39
+ if (value === undefined) return undefined;
40
+ if (typeof value !== "boolean") throw new Error(`${key} must be a boolean`);
41
+ return value;
42
+ }
@@ -10,6 +10,7 @@
10
10
  * playbook/playbook-definition.ts for the full rationale.
11
11
  */
12
12
 
13
+ import { summarizeArtifact } from "../artifact/artifact.ts";
13
14
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
14
15
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
15
16
  import {
@@ -31,31 +32,10 @@ import type { SessionIdentity } from "../session-identity/session-identity-servi
31
32
  import type { TaskEventStore } from "../stores/task-event-store.ts";
32
33
  import type { TaskScopeStore } from "../stores/task-scope-store.ts";
33
34
  import type { Tasks } from "../task/task-service.ts";
35
+ import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
34
36
 
35
37
  const MODULE_ID = "playbooks";
36
38
 
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
39
  const eventContext = (input: OperationInput) => ({
60
40
  actor: optionalString(input, "actor"),
61
41
  source: optionalString(input, "source"),
@@ -135,7 +115,10 @@ export function playbooksOperations({
135
115
  eventContext(input),
136
116
  ),
137
117
  ),
138
- define("playbooks.list", (input: OperationInput) => listPlaybooks(artifacts, artifactScopes, artifactFilter(input))),
118
+ define("playbooks.list", (input: OperationInput) => {
119
+ const playbooks = listPlaybooks(artifacts, artifactScopes, artifactFilter(input));
120
+ return optionalBoolean(input, "full") === true ? playbooks : playbooks.map(summarizeArtifact);
121
+ }),
139
122
  define("playbooks.show", (input: OperationInput) => showPlaybook(artifacts, string(input, "id"))),
140
123
  define("playbooks.preview", (input: OperationInput) =>
141
124
  playbookInvocation(artifacts, string(input, "id"), input.arguments as Record<string, unknown> | undefined),
@@ -11,6 +11,7 @@
11
11
  * registry" convention.
12
12
  */
13
13
 
14
+ import { summarizeArtifact } from "../artifact/artifact.ts";
14
15
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
15
16
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
16
17
  import {
@@ -24,31 +25,10 @@ import {
24
25
  updateRule,
25
26
  } from "../domain-services.ts";
26
27
  import type { OperationDefinition } from "../module-registry.ts";
28
+ import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
27
29
 
28
30
  const MODULE_ID = "rules";
29
31
 
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
32
  const eventContext = (input: OperationInput) => ({
53
33
  actor: optionalString(input, "actor"),
54
34
  source: optionalString(input, "source"),
@@ -100,7 +80,10 @@ export function rulesOperations(artifacts: ArtifactStore, scopes: ArtifactScopeS
100
80
  eventContext(input),
101
81
  ),
102
82
  ),
103
- define("rules.list", (input: OperationInput) => listRules(artifacts, scopes, artifactFilter(input))),
83
+ define("rules.list", (input: OperationInput) => {
84
+ const rules = listRules(artifacts, scopes, artifactFilter(input));
85
+ return optionalBoolean(input, "full") === true ? rules : rules.map(summarizeArtifact);
86
+ }),
104
87
  define("rules.show", (input: OperationInput) => showRule(artifacts, string(input, "id"))),
105
88
  define("rules.preview", (input: OperationInput) => previewRule(artifacts, string(input, "id"))),
106
89
  define("rules.enable", (input: OperationInput) => transitionRule(artifacts, string(input, "id"), "enable", eventContext(input))),
@@ -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"),