@danypops/papyrus 0.44.8 → 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.
Files changed (93) hide show
  1. package/package.json +3 -2
  2. package/src/{ports → artifact}/artifact-event-reader.ts +1 -1
  3. package/src/{artifact-relationship-view.ts → artifact/artifact-relationship-view.ts} +3 -3
  4. package/src/{ports → artifact}/artifact-store.ts +2 -2
  5. package/src/{artifact-subtree.ts → artifact/artifact-subtree.ts} +4 -4
  6. package/src/{ports → artifact}/artifact-trash-store.ts +2 -2
  7. package/src/{adapters → artifact}/sqlite-artifact-scope-store.ts +1 -1
  8. package/src/{adapters → artifact}/sqlite-artifact-store.ts +15 -15
  9. package/src/authority-registry.ts +3 -3
  10. package/src/cli/artifact-command.ts +210 -0
  11. package/src/cli/discuss-command.ts +256 -0
  12. package/src/cli/docs-command.ts +192 -0
  13. package/src/cli/gates-command.ts +46 -0
  14. package/src/cli/graph-command.ts +171 -0
  15. package/src/cli/graph-projection-command.ts +79 -0
  16. package/src/cli/log-command.ts +101 -0
  17. package/src/cli/migration-command.ts +40 -0
  18. package/src/cli/note-command.ts +186 -0
  19. package/src/cli/playbooks-command.ts +313 -0
  20. package/src/cli/rules-command.ts +220 -0
  21. package/src/cli/session-identity-command.ts +58 -0
  22. package/src/cli/shared.ts +5 -0
  23. package/src/cli/stricli-run.ts +30 -0
  24. package/src/cli/task-command.ts +892 -0
  25. package/src/cli.ts +32 -1914
  26. package/src/client.ts +2 -2
  27. package/src/{daemon-state.ts → daemon/daemon-state.ts} +1 -1
  28. package/src/{daemon.ts → daemon/daemon.ts} +3 -3
  29. package/src/db.ts +1 -1
  30. package/src/{discussion-service.ts → discussion/discussion-service.ts} +7 -6
  31. package/src/domain-services.ts +5 -5
  32. package/src/{graph-projection-service.ts → graph-projection/graph-projection-service.ts} +6 -6
  33. package/src/{vehicle/artifact-trash-vehicle.ts → handlers/artifact-trash.ts} +4 -4
  34. package/src/{vehicle/discuss-vehicle.ts → handlers/discuss.ts} +20 -25
  35. package/src/{vehicle/docs-vehicle.ts → handlers/docs.ts} +4 -37
  36. package/src/{vehicle/notes-vehicle.ts → handlers/notes.ts} +4 -38
  37. package/src/{vehicle/playbooks-vehicle.ts → handlers/playbooks.ts} +34 -100
  38. package/src/{vehicle/papyrus-vehicle.ts → handlers/registry.ts} +16 -16
  39. package/src/{vehicle/rules-vehicle.ts → handlers/rules.ts} +4 -37
  40. package/src/{vehicle/artifact-vehicle-shared.ts → handlers/shared.ts} +96 -6
  41. package/src/{vehicle/tasks-vehicle.ts → handlers/tasks.ts} +37 -124
  42. package/src/index.ts +13 -13
  43. package/src/{log-service.ts → log/log-service.ts} +2 -2
  44. package/src/modules/discuss.ts +7 -28
  45. package/src/modules/docs.ts +4 -24
  46. package/src/modules/graph-projection.ts +5 -11
  47. package/src/modules/logs.ts +2 -23
  48. package/src/modules/notes.ts +2 -23
  49. package/src/modules/operation-input.ts +35 -0
  50. package/src/modules/playbooks.ts +13 -33
  51. package/src/modules/rules.ts +4 -24
  52. package/src/modules/session-identity.ts +2 -16
  53. package/src/modules/tasks.ts +11 -38
  54. package/src/{note-service.ts → note/note-service.ts} +6 -6
  55. package/src/ops.ts +6 -6
  56. package/src/{playbook-definition.ts → playbook/playbook-definition.ts} +7 -6
  57. package/src/{playbook-execution.ts → playbook/playbook-execution.ts} +5 -4
  58. package/src/{workflow-execution.ts → playbook/workflow-execution.ts} +12 -12
  59. package/src/service.ts +30 -57
  60. package/src/{session-identity-service.ts → session-identity/session-identity-service.ts} +2 -2
  61. package/src/{ports → stores}/session-identity-store.ts +1 -1
  62. package/src/{adapters → stores}/sqlite-discussion-round-store.ts +1 -1
  63. package/src/{adapters → stores}/sqlite-gate-runner.ts +1 -1
  64. package/src/{adapters → stores}/sqlite-graph-projection-store.ts +1 -1
  65. package/src/{adapters → stores}/sqlite-log-store.ts +1 -1
  66. package/src/{adapters → stores}/sqlite-note-event-store.ts +1 -1
  67. package/src/{adapters → stores}/sqlite-session-identity-store.ts +1 -1
  68. package/src/{adapters → stores}/sqlite-task-event-store.ts +1 -1
  69. package/src/{adapters → stores}/sqlite-task-focus-store.ts +1 -1
  70. package/src/{adapters → stores}/sqlite-task-lease-store.ts +1 -1
  71. package/src/{adapters → stores}/sqlite-task-scope-store.ts +1 -1
  72. package/src/{task-context.ts → task/task-context.ts} +4 -4
  73. package/src/{task-execution.ts → task/task-execution.ts} +1 -1
  74. package/src/{task-graph-view.ts → task/task-graph-view.ts} +1 -1
  75. package/src/{task-relationship-view.ts → task/task-relationship-view.ts} +3 -3
  76. package/src/{task-service.ts → task/task-service.ts} +14 -14
  77. /package/src/{domain → artifact}/artifact-alias.ts +0 -0
  78. /package/src/{domain → artifact}/artifact-event.ts +0 -0
  79. /package/src/{ports → artifact}/artifact-scope-store.ts +0 -0
  80. /package/src/{domain → artifact}/artifact-trash.ts +0 -0
  81. /package/src/{domain → artifact}/artifact.ts +0 -0
  82. /package/src/{ports → artifact}/atomic-artifact-store.ts +0 -0
  83. /package/src/{log.ts → log/log.ts} +0 -0
  84. /package/src/{ports → stores}/discussion-round-store.ts +0 -0
  85. /package/src/{ports → stores}/gate-runner.ts +0 -0
  86. /package/src/{ports → stores}/graph-projection-store.ts +0 -0
  87. /package/src/{ports → stores}/graph-renderer.ts +0 -0
  88. /package/src/{ports → stores}/log-store.ts +0 -0
  89. /package/src/{ports → stores}/note-event-store.ts +0 -0
  90. /package/src/{ports → stores}/task-event-store.ts +0 -0
  91. /package/src/{ports → stores}/task-focus-store.ts +0 -0
  92. /package/src/{ports → stores}/task-lease-store.ts +0 -0
  93. /package/src/{ports → stores}/task-scope-store.ts +0 -0
@@ -4,23 +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";
8
+ import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
9
+ import type { ArtifactStore } from "../artifact/artifact-store.ts";
9
10
  import { listRules } from "../domain-services.ts";
10
11
  import { rulesOperations } from "../modules/rules.ts";
11
- import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
12
- import type { ArtifactStore } from "../ports/artifact-store.ts";
13
- import {
14
- looseObjectSchema,
15
- numberProp,
16
- passthroughOutput,
17
- resolveArtifactIdWidened,
18
- stringProp,
19
- validationError,
20
- } from "./artifact-vehicle-shared.ts";
12
+ import { createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
21
13
 
22
14
  const OWNER = "rules";
23
- const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
24
15
 
25
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. */
26
17
  function resolveRuleId(
@@ -54,31 +45,7 @@ function resolveTaskId(artifacts: ArtifactStore, _projectRoot: string | undefine
54
45
  export function registerRulesVehicleOperations(registry: VehicleRegistry, artifacts: ArtifactStore, scopes: ArtifactScopeStore): void {
55
46
  const moduleOperations = new Map(rulesOperations(artifacts, scopes).map((op) => [op.name, op]));
56
47
  const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
57
-
58
- const define = (
59
- action: string,
60
- description: string,
61
- effect: "read" | "local-write",
62
- properties: Record<string, { type: string; enum?: readonly string[] }>,
63
- required: readonly string[],
64
- resolve: (input: Record<string, unknown>) => Record<string, unknown>,
65
- ): void => {
66
- const operation = defineVehicleOperation({
67
- name: `rules.${action}`,
68
- version: 1,
69
- description,
70
- input: looseObjectSchema(properties, required),
71
- output: passthroughOutput,
72
- permissions: ["rules:read", "rules:write"],
73
- effect,
74
- idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
75
- limits: LIMITS,
76
- });
77
- registry.register(
78
- OWNER,
79
- bindVehicleOperation(operation, () => async (context) => call(`rules.${action}`, resolve(context.input))),
80
- );
81
- };
48
+ const define = createOperationDefiner(registry, OWNER, "rules", ["rules:read", "rules:write"], call);
82
49
 
83
50
  define(
84
51
  "create",
@@ -3,12 +3,21 @@
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";
7
- import type { Artifact } from "../domain/artifact.ts";
8
- import { PlaybookCompositionError } from "../playbook-definition.ts";
9
- import type { ArtifactStore } from "../ports/artifact-store.ts";
10
- import { InvalidSessionSecretError } from "../session-identity-service.ts";
11
- import { TaskDependencyCycleError, TaskExecutionBoundExceededError, type TaskExecutionPlan } from "../task-execution.ts";
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";
16
+ import type { Artifact } from "../artifact/artifact.ts";
17
+ import type { ArtifactStore } from "../artifact/artifact-store.ts";
18
+ import { PlaybookCompositionError } from "../playbook/playbook-definition.ts";
19
+ import { InvalidSessionSecretError } from "../session-identity/session-identity-service.ts";
20
+ import { TaskDependencyCycleError, TaskExecutionBoundExceededError, type TaskExecutionPlan } from "../task/task-execution.ts";
12
21
 
13
22
  /**
14
23
  * VehicleRegistry only ever calls a schema's own safeParse -- jsonSchema is
@@ -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
+ }
@@ -10,7 +10,7 @@
10
10
  * - session_secret authorizes which session's Task Focus row gets mutated
11
11
  * (modules/tasks.ts's own guardFocusMutation). It must never be a model-visible
12
12
  * input field -- it travels through VehicleInvocationOptions.principal.claims,
13
- * the same mechanism playbooks.invoke uses (see vehicle-notes-client.ts).
13
+ * the same mechanism playbooks.invoke uses (see pi-papyrus's tools/vehicle-notes-client.ts).
14
14
  * - papyrus.task-focus.v1 is a same-process Pi extension event bus broadcast (a
15
15
  * token-cost router or similar can correlate its own telemetry with the
16
16
  * currently focused task) with no Vehicle-transport equivalent -- fired from
@@ -19,26 +19,25 @@
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";
23
+ import type { ArtifactStore } from "../artifact/artifact-store.ts";
24
24
  import type { TaskViewMode } from "../domain/task-scope.ts";
25
25
  import { tasksOperations } from "../modules/tasks.ts";
26
- import type { ArtifactStore } from "../ports/artifact-store.ts";
27
- import type { SessionIdentity } from "../session-identity-service.ts";
28
- import type { TaskExecutionPlan } from "../task-execution.ts";
29
- import type { TaskCompletion, Tasks } from "../task-service.ts";
26
+ import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
27
+ import type { TaskExecutionPlan } from "../task/task-execution.ts";
28
+ import type { TaskCompletion, Tasks } from "../task/task-service.ts";
30
29
  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,
41
- } from "./artifact-vehicle-shared.ts";
40
+ } from "./shared.ts";
42
41
 
43
42
  const OWNER = "tasks";
44
43
  const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
@@ -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(
package/src/index.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  * purposeful API, not "everything happens to be reachable."
6
6
  */
7
7
 
8
- export { projectArtifactRelationships } from "./artifact-relationship-view.ts";
8
+ export type { Artifact, ArtifactEdge } from "./artifact/artifact.ts";
9
+ export { projectArtifactRelationships } from "./artifact/artifact-relationship-view.ts";
10
+ export type { ArtifactStore } from "./artifact/artifact-store.ts";
9
11
  export {
10
12
  connectPapyrusClient,
11
13
  type PapyrusClient,
@@ -15,8 +17,7 @@ export {
15
17
  type VehicleClientTarget,
16
18
  } from "./client.ts";
17
19
  export * from "./constants.ts";
18
- export type { DiscussionAndRounds } from "./discussion-service.ts";
19
- export type { Artifact, ArtifactEdge } from "./domain/artifact.ts";
20
+ export type { DiscussionAndRounds } from "./discussion/discussion-service.ts";
20
21
  export { checklistEntries, PROOF_TYPES, type ProofReference } from "./domain/checklist.ts";
21
22
  export { DISCUSSION_SUBTYPE, type DiscussionRound, readDiscussionExtra } from "./domain/discussion.ts";
22
23
  export type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode, RenderedGraph } from "./domain/display-graph.ts";
@@ -25,14 +26,13 @@ export type { NoteHistoryPage } from "./domain/note-event.ts";
25
26
  export type { TaskEvent, TaskHistoryPage } from "./domain/task-event.ts";
26
27
  export type { TaskLease } from "./domain/task-lease.ts";
27
28
  export type { TaskViewSelection } from "./domain/task-scope.ts";
28
- export { NOTE_DISPOSITIONS } from "./note-service.ts";
29
- export type { PlaybookInvocationResult, PlaybookMissingArguments } from "./playbook-execution.ts";
30
- export type { ArtifactStore } from "./ports/artifact-store.ts";
31
- export type { GraphRenderer } from "./ports/graph-renderer.ts";
29
+ export { NOTE_DISPOSITIONS } from "./note/note-service.ts";
30
+ export type { PlaybookInvocationResult, PlaybookMissingArguments } from "./playbook/playbook-execution.ts";
31
+ export type { WorkflowRunResult } from "./playbook/workflow-execution.ts";
32
32
  export type { OperationName, SchemaState } from "./service.ts";
33
- export { taskContext } from "./task-context.ts";
34
- export { projectTaskExecution, type TaskExecutionPlan, type TaskExecutionState } from "./task-execution.ts";
35
- export { projectTaskGraph, type TaskGraphView } from "./task-graph-view.ts";
36
- export { fallbackLabel, projectTaskRelationships } from "./task-relationship-view.ts";
37
- export type { TaskCompletion, TaskGraph, TaskNode, TaskStatus } from "./task-service.ts";
38
- export type { WorkflowRunResult } from "./workflow-execution.ts";
33
+ export type { GraphRenderer } from "./stores/graph-renderer.ts";
34
+ export { taskContext } from "./task/task-context.ts";
35
+ export { projectTaskExecution, type TaskExecutionPlan, type TaskExecutionState } from "./task/task-execution.ts";
36
+ export { projectTaskGraph, type TaskGraphView } from "./task/task-graph-view.ts";
37
+ export { fallbackLabel, projectTaskRelationships } from "./task/task-relationship-view.ts";
38
+ export type { TaskCompletion, TaskGraph, TaskNode, TaskStatus } from "./task/task-service.ts";
@@ -9,8 +9,8 @@ import {
9
9
  type LogQuery,
10
10
  meetsLevel,
11
11
  validateAppendLogEntryCommand,
12
- } from "./domain/log-entry.ts";
13
- import type { LogStore } from "./ports/log-store.ts";
12
+ } from "../domain/log-entry.ts";
13
+ import type { LogStore } from "../stores/log-store.ts";
14
14
 
15
15
  export class Logs {
16
16
  constructor(private readonly store: LogStore) {}
@@ -3,44 +3,23 @@
3
3
  * domain/discussion.ts and discussion-service.ts for the full design.
4
4
  */
5
5
 
6
- import type { Discussions } from "../discussion-service.ts";
7
- import type { ArtifactEventContext } from "../domain/artifact-event.ts";
6
+ import type { ArtifactEventContext } from "../artifact/artifact-event.ts";
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
 
@@ -6,6 +6,9 @@
6
6
  * Imports only src/domain-services.ts's Doc functions, which are already generic
7
7
  * ArtifactStore-based with no other module's concrete class dependency.
8
8
  */
9
+
10
+ import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
11
+ import type { ArtifactStore } from "../artifact/artifact-store.ts";
9
12
  import type { AuthorityRegistry } from "../authority-registry.ts";
10
13
  import {
11
14
  assignDocumentProject,
@@ -18,33 +21,10 @@ import {
18
21
  updateDocument,
19
22
  } from "../domain-services.ts";
20
23
  import type { OperationDefinition } from "../module-registry.ts";
21
- import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
22
- import type { ArtifactStore } from "../ports/artifact-store.ts";
24
+ import { type OperationInput, optionalNumber, optionalString, string } from "./operation-input.ts";
23
25
 
24
26
  const MODULE_ID = "docs";
25
27
 
26
- type OperationInput = Record<string, unknown>;
27
-
28
- function string(input: OperationInput, key: string): string {
29
- const value = input[key];
30
- if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
31
- return value;
32
- }
33
-
34
- function optionalString(input: OperationInput, key: string): string | undefined {
35
- const value = input[key];
36
- if (value === undefined) return undefined;
37
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
38
- return value;
39
- }
40
-
41
- function optionalNumber(input: OperationInput, key: string): number | undefined {
42
- const value = input[key];
43
- if (value === undefined) return undefined;
44
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
45
- return value;
46
- }
47
-
48
28
  const eventContext = (input: OperationInput) => ({
49
29
  actor: optionalString(input, "actor"),
50
30
  source: optionalString(input, "source"),
@@ -5,6 +5,8 @@
5
5
  * src/domain/graph-projection.ts and src/graph-projection-service.ts for the protocol
6
6
  * itself; this file only parses/validates raw operation input into that typed shape.
7
7
  */
8
+
9
+ import type { ArtifactStore } from "../artifact/artifact-store.ts";
8
10
  import type { AuthorityRegistry } from "../authority-registry.ts";
9
11
  import type {
10
12
  GRAPH_PROJECTION_SCHEMA_VERSION,
@@ -12,21 +14,13 @@ import type {
12
14
  ProjectedArtifact,
13
15
  ProjectedEdge,
14
16
  } from "../domain/graph-projection.ts";
15
- import { GraphProjection } from "../graph-projection-service.ts";
17
+ import { GraphProjection } from "../graph-projection/graph-projection-service.ts";
16
18
  import type { OperationDefinition } from "../module-registry.ts";
17
- import type { ArtifactStore } from "../ports/artifact-store.ts";
18
- import type { GraphProjectionStore } from "../ports/graph-projection-store.ts";
19
+ import type { GraphProjectionStore } from "../stores/graph-projection-store.ts";
20
+ import { type OperationInput, string } from "./operation-input.ts";
19
21
 
20
22
  const MODULE_ID = "graph_projection";
21
23
 
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
24
  function number(input: OperationInput, key: string): number {
31
25
  const value = input[key];
32
26
  if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} is required and must be a number`);
@@ -6,33 +6,12 @@
6
6
  * comment on why `log` is not an Artifact kind).
7
7
  */
8
8
  import type { JsonValue, LogLevel } from "../domain/log-entry.ts";
9
- import type { Logs } from "../log-service.ts";
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 ||