@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 +4 -4
- package/src/vehicle/artifact-trash-vehicle.ts +2 -2
- package/src/vehicle/artifact-vehicle-shared.ts +22 -5
- package/src/vehicle/docs-vehicle.ts +3 -3
- package/src/vehicle/notes-vehicle.ts +3 -3
- package/src/vehicle/playbooks-vehicle.ts +2 -1
- package/src/vehicle/rules-vehicle.ts +3 -3
- package/src/vehicle/tasks-vehicle.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/papyrus",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
38
|
-
"@danypops/vehicle-client": "^0.
|
|
39
|
-
"@danypops/vehicle-server": "^0.
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
74
|
-
if (!filter.projectRoot) throw
|
|
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 }),
|