@danypops/papyrus 0.42.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.42.2",
3
+ "version": "0.43.0",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -34,8 +34,8 @@
34
34
  },
35
35
  "files": ["src", "README.md"],
36
36
  "dependencies": {
37
- "@danypops/vehicle-core": "^0.2.0",
38
- "@danypops/vehicle-client": "^0.1.1",
39
- "@danypops/vehicle-server": "^0.4.1"
37
+ "@danypops/vehicle-core": "^0.10.0",
38
+ "@danypops/vehicle-client": "^0.5.0",
39
+ "@danypops/vehicle-server": "^0.11.0"
40
40
  }
41
41
  }
@@ -8,7 +8,7 @@ import type { VehicleRegistry } from "@danypops/vehicle-server";
8
8
  import { removeArtifactSubtree } from "../artifact-subtree.ts";
9
9
  import type { ArtifactStore } from "../ports/artifact-store.ts";
10
10
  import type { ArtifactTrashStore } from "../ports/artifact-trash-store.ts";
11
- import { looseObjectSchema, numberProp, passthroughOutput, stringProp } from "./artifact-vehicle-shared.ts";
11
+ import { looseObjectSchema, numberProp, passthroughOutput, stringProp, validationError } from "./artifact-vehicle-shared.ts";
12
12
 
13
13
  const OWNER = "artifact";
14
14
  const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
@@ -26,7 +26,7 @@ function eventContext(input: Record<string, unknown>): { actor?: string; source?
26
26
 
27
27
  function requireId(input: Record<string, unknown>): string {
28
28
  const id = input.id;
29
- if (typeof id !== "string" || id.length === 0) throw new Error("id is required");
29
+ if (typeof id !== "string" || id.length === 0) throw validationError("id is required");
30
30
  return id;
31
31
  }
32
32
 
@@ -3,7 +3,7 @@
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, type VehicleSchemaCodec } from "@danypops/vehicle-core";
6
+ import { defineVehicleSchema, VehicleError, type VehicleContentBlock, type VehicleSchemaCodec } from "@danypops/vehicle-core";
7
7
  import type { Artifact } from "../domain/artifact.ts";
8
8
  import type { ArtifactStore } from "../ports/artifact-store.ts";
9
9
  import type { TaskExecutionPlan } from "../task-execution.ts";
@@ -47,6 +47,19 @@ export const passthroughOutput: VehicleSchemaCodec<unknown> = defineVehicleSchem
47
47
  export const stringProp = { type: "string" } as const;
48
48
  export const numberProp = { type: "number" } as const;
49
49
 
50
+ /**
51
+ * A plain `throw new Error(...)` inside any resolve()/execute() step here is caught by
52
+ * vehicle-registry.ts's generic dispatch and re-wrapped as VehicleError("handler-failed",
53
+ * `${key} handler failed`, {category: "internal"}) -- built to catch a genuine crash, but
54
+ * it can't distinguish that from an ordinary, expected validation/lookup failure, so it
55
+ * discards the original message and category either way. Every guard clause and name
56
+ * resolution below must throw a VehicleError directly so it passes through that dispatch
57
+ * unchanged (vehicle-registry.ts only rewraps errors that are NOT already a VehicleError).
58
+ */
59
+ export function validationError(message: string): VehicleError {
60
+ return new VehicleError("validation-failed", message, { category: "validation" });
61
+ }
62
+
50
63
  /** A known LLM tool-calling quirk: a nested-object field arrives JSON-stringified rather than as a real object. Mutates input[key] in place when it's a string, leaves it untouched otherwise. */
51
64
  export function normalizeJsonEncodedField(input: Record<string, unknown>, key: string): void {
52
65
  const value = input[key];
@@ -54,7 +67,7 @@ export function normalizeJsonEncodedField(input: Record<string, unknown>, key: s
54
67
  try {
55
68
  input[key] = JSON.parse(value);
56
69
  } catch {
57
- throw new Error(`${key} must be valid JSON`);
70
+ throw validationError(`${key} must be valid JSON`);
58
71
  }
59
72
  }
60
73
 
@@ -62,10 +75,14 @@ export function normalizeJsonEncodedField(input: Record<string, unknown>, key: s
62
75
  export function matchArtifactByName(candidates: readonly Artifact[], name: string): string {
63
76
  const needle = name.trim().toLowerCase();
64
77
  const matches = candidates.filter((artifact) => artifact.title.trim().toLowerCase() === needle);
65
- if (matches.length === 0) throw new Error(`no artifact named "${name}" found in this scope`);
78
+ if (matches.length === 0) {
79
+ throw new VehicleError("artifact-not-found", `no artifact named "${name}" found in this scope`, { category: "not_found" });
80
+ }
66
81
  if (matches.length > 1) {
67
- throw new Error(
82
+ throw new VehicleError(
83
+ "artifact-name-ambiguous",
68
84
  `${matches.length} artifacts are named "${name}": ${matches.map((a) => `${a.title} (${a.id})`).join(", ")} -- use id to disambiguate`,
85
+ { category: "conflict" },
69
86
  );
70
87
  }
71
88
  return matches[0]!.id;
@@ -85,7 +102,7 @@ export function resolveArtifactIdWidened(
85
102
  try {
86
103
  return matchArtifactByName(fetchCandidates(), name);
87
104
  } catch (error) {
88
- if (!(error instanceof Error) || !error.message.startsWith("no artifact named") || !fetchWidened) throw error;
105
+ if (!(error instanceof VehicleError) || error.code !== "artifact-not-found" || !fetchWidened) throw error;
89
106
  return matchArtifactByName(fetchWidened(), name);
90
107
  }
91
108
  }
@@ -10,7 +10,7 @@ import { listDocuments } from "../domain-services.ts";
10
10
  import { docsOperations } from "../modules/docs.ts";
11
11
  import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
12
12
  import type { ArtifactStore } from "../ports/artifact-store.ts";
13
- import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp } from "./artifact-vehicle-shared.ts";
13
+ import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp, validationError } from "./artifact-vehicle-shared.ts";
14
14
 
15
15
  const OWNER = "docs";
16
16
  const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
@@ -23,7 +23,7 @@ function resolveDocId(
23
23
  name: unknown,
24
24
  ): string {
25
25
  if (typeof id === "string" && id.length > 0) return id;
26
- if (typeof name !== "string" || name.length === 0) throw new Error("id or name is required");
26
+ if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
27
27
  return resolveArtifactIdWidened(
28
28
  name,
29
29
  () => listDocuments(artifacts, scopes, { text: name, projectRoot }),
@@ -34,7 +34,7 @@ function resolveDocId(
34
34
  /** Cross-kind resolution for a link target -- can be a doc, task, rule, or playbook. Unscoped, matching the exact behavior of the artifact.query-backed resolution it replaces. */
35
35
  function resolveTargetId(artifacts: ArtifactStore, id: unknown, name: unknown): string {
36
36
  if (typeof id === "string" && id.length > 0) return id;
37
- if (typeof name !== "string" || name.length === 0) throw new Error("target_id or target_name is required");
37
+ if (typeof name !== "string" || name.length === 0) throw validationError("target_id or target_name is required");
38
38
  return resolveArtifactIdWidened(name, () => artifacts.query({ text: name }));
39
39
  }
40
40
 
@@ -12,7 +12,7 @@ import type { VehicleRegistry } from "@danypops/vehicle-server";
12
12
  import { notesOperations } from "../modules/notes.ts";
13
13
  import { NOTE_DISPOSITIONS, type Notes } from "../note-service.ts";
14
14
  import type { ArtifactStore } from "../ports/artifact-store.ts";
15
- import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp } from "./artifact-vehicle-shared.ts";
15
+ import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp, validationError } from "./artifact-vehicle-shared.ts";
16
16
 
17
17
  const OWNER = "notes";
18
18
 
@@ -21,14 +21,14 @@ const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes:
21
21
  /** Resolves a note's id from either an explicit id or its title within projectRoot. */
22
22
  function resolveNoteId(notes: Notes, projectRoot: string, id: unknown, name: unknown): string {
23
23
  if (typeof id === "string" && id.length > 0) return id;
24
- if (typeof name !== "string" || name.length === 0) throw new Error("id or name is required");
24
+ if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
25
25
  return resolveArtifactIdWidened(name, () => notes.list({ projectRoot, text: name }));
26
26
  }
27
27
 
28
28
  /** Cross-kind equivalent for a promotion target -- a target can be a task, doc, rule, or playbook, not just a note. Unscoped by project, matching the exact behavior of the artifact.query-backed resolution it replaces. */
29
29
  function resolveArtifactId(artifacts: ArtifactStore, id: unknown, name: unknown): string {
30
30
  if (typeof id === "string" && id.length > 0) return id;
31
- if (typeof name !== "string" || name.length === 0) throw new Error("target_id or target_name is required");
31
+ if (typeof name !== "string" || name.length === 0) throw validationError("target_id or target_name is required");
32
32
  return resolveArtifactIdWidened(name, () => artifacts.query({ text: name }));
33
33
  }
34
34
 
@@ -37,6 +37,7 @@ import {
37
37
  passthroughOutput,
38
38
  resolveArtifactIdWidened,
39
39
  stringProp,
40
+ validationError,
40
41
  } from "./artifact-vehicle-shared.ts";
41
42
 
42
43
  const OWNER = "playbooks";
@@ -54,7 +55,7 @@ export interface PlaybooksVehicleDeps {
54
55
  /** Unscoped resolution -- a Playbook is commonly cross-project (e.g. a lab-deploy playbook), matching the hand-rolled tool's own resolutionRequest choice. */
55
56
  function resolvePlaybookId(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: unknown, name: unknown): string {
56
57
  if (typeof id === "string" && id.length > 0) return id;
57
- if (typeof name !== "string" || name.length === 0) throw new Error("id or name is required");
58
+ if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
58
59
  return resolveArtifactIdWidened(name, () => listPlaybooks(artifacts, scopes, { text: name }));
59
60
  }
60
61
 
@@ -10,7 +10,7 @@ import { listRules } from "../domain-services.ts";
10
10
  import { rulesOperations } from "../modules/rules.ts";
11
11
  import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
12
12
  import type { ArtifactStore } from "../ports/artifact-store.ts";
13
- import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp } from "./artifact-vehicle-shared.ts";
13
+ import { looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp, validationError } from "./artifact-vehicle-shared.ts";
14
14
 
15
15
  const OWNER = "rules";
16
16
  const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
@@ -24,7 +24,7 @@ function resolveRuleId(
24
24
  name: unknown,
25
25
  ): string {
26
26
  if (typeof id === "string" && id.length > 0) return id;
27
- if (typeof name !== "string" || name.length === 0) throw new Error("id or name is required");
27
+ if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
28
28
  return resolveArtifactIdWidened(
29
29
  name,
30
30
  () => listRules(artifacts, scopes, { text: name, projectRoot }),
@@ -151,7 +151,7 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
151
151
  [],
152
152
  (input) => {
153
153
  const taskId = resolveTaskId(artifacts, input.project_root as string | undefined, input.task_id, input.task_name);
154
- if (!taskId) throw new Error("task_id or task_name is required");
154
+ if (!taskId) throw validationError("task_id or task_name is required");
155
155
  return {
156
156
  ...input,
157
157
  id: resolveRuleId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name),
@@ -34,6 +34,7 @@ import {
34
34
  passthroughOutput,
35
35
  resolveArtifactIdWidened,
36
36
  stringProp,
37
+ validationError,
37
38
  } from "./artifact-vehicle-shared.ts";
38
39
 
39
40
  const OWNER = "tasks";
@@ -70,8 +71,8 @@ function resolveTaskId(
70
71
  name: unknown,
71
72
  ): string {
72
73
  if (typeof id === "string" && id.length > 0) return id;
73
- if (typeof name !== "string" || name.length === 0) throw new Error("id or name is required");
74
- if (!filter.projectRoot) throw new Error("project_root is required when resolving a task by name");
74
+ if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
75
+ if (!filter.projectRoot) throw validationError("project_root is required when resolving a task by name");
75
76
  return resolveArtifactIdWidened(
76
77
  name,
77
78
  () => tasks.list({ ...filter, text: name }),