@danypops/papyrus 0.37.0 → 0.38.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 +1 -1
- package/src/artifact-subtree.ts +54 -0
- package/src/cli.ts +9 -1
- package/src/constants.ts +2 -0
- package/src/service.ts +3 -1
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk trash of a `contains` subtree, any artifact kind -- a whole materialized Task run
|
|
3
|
+
* (root container + steps + nested playbook children) or a Playbook's own nested-playbook
|
|
4
|
+
* tree can be moved to trash in one call instead of enumerating every id by hand. Mirrors
|
|
5
|
+
* Tasks.cancelSubtree's traversal shape but performs trash(), not a lifecycle transition,
|
|
6
|
+
* so it applies to any kind that participates in `contains` (task, playbook), not just Task.
|
|
7
|
+
*/
|
|
8
|
+
import { ARTIFACT_REMOVE_SUBTREE_MAX_NODES } from "./constants.ts";
|
|
9
|
+
import type { ArtifactEventContext } from "./domain/artifact-event.ts";
|
|
10
|
+
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
11
|
+
import type { ArtifactTrashStore } from "./ports/artifact-trash-store.ts";
|
|
12
|
+
|
|
13
|
+
export interface RemoveSubtreeResult {
|
|
14
|
+
removed: string[];
|
|
15
|
+
/** Already trashed -- a real no-op, not an error, matching trash()/restore()'s own idempotence elsewhere. */
|
|
16
|
+
skipped: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Trashes `id` and every artifact reachable by following `contains` edges outward from it,
|
|
21
|
+
* transitively. An already-trashed node is skipped rather than re-trashed (trash() itself is
|
|
22
|
+
* idempotent, but skipping keeps the result's `removed` list meaningful -- only nodes newly
|
|
23
|
+
* moved to trash by this call). A node that is the live Task Focus in some scope still throws
|
|
24
|
+
* (the same guard trash() always enforces for a single artifact) rather than being silently
|
|
25
|
+
* skipped -- an active Focus is a real conflict to surface, not routine already-done state.
|
|
26
|
+
*/
|
|
27
|
+
export function removeArtifactSubtree(
|
|
28
|
+
store: ArtifactStore & ArtifactTrashStore,
|
|
29
|
+
id: string,
|
|
30
|
+
options: { reason?: string; context?: ArtifactEventContext } = {},
|
|
31
|
+
): RemoveSubtreeResult {
|
|
32
|
+
if (!store.get(id)) throw new Error(`artifact "${id}" not found`);
|
|
33
|
+
const visited = new Set<string>();
|
|
34
|
+
const queue = [id];
|
|
35
|
+
const removed: string[] = [];
|
|
36
|
+
const skipped: string[] = [];
|
|
37
|
+
while (queue.length > 0) {
|
|
38
|
+
const current = queue.shift()!;
|
|
39
|
+
if (visited.has(current)) continue;
|
|
40
|
+
visited.add(current);
|
|
41
|
+
if (visited.size > ARTIFACT_REMOVE_SUBTREE_MAX_NODES) throw new Error(`remove_subtree exceeds ${ARTIFACT_REMOVE_SUBTREE_MAX_NODES} artifacts`);
|
|
42
|
+
const childIds = store.relationships({ artifactIds: [current] })
|
|
43
|
+
.filter((edge) => edge.from === current && edge.relation === "contains")
|
|
44
|
+
.map((edge) => edge.to);
|
|
45
|
+
queue.push(...childIds);
|
|
46
|
+
if (store.trashStatus(current)) {
|
|
47
|
+
skipped.push(current);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
store.trash(current, { reason: options.reason, context: options.context });
|
|
51
|
+
removed.push(current);
|
|
52
|
+
}
|
|
53
|
+
return { removed, skipped };
|
|
54
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -83,6 +83,7 @@ const USAGE = `Usage:
|
|
|
83
83
|
papyrus artifact query [--kind <kind>] [--status <status>] [--text <query>] [--limit <count>] [--json]
|
|
84
84
|
papyrus artifact show <id> [--depth <n>] [--max-nodes <n>] [--json]
|
|
85
85
|
papyrus artifact remove <id> [--reason <text>] [--json]
|
|
86
|
+
papyrus artifact remove-subtree <id> [--reason <text>] [--json]
|
|
86
87
|
papyrus artifact restore <id> [--json]
|
|
87
88
|
papyrus artifact trash-status <id> [--json]
|
|
88
89
|
papyrus artifact trash-list [--json]
|
|
@@ -1023,6 +1024,13 @@ export async function runArtifactCli(args: string[], client: TaskCliClient, proj
|
|
|
1023
1024
|
human = `Trashed ${record.artifactId}: eligible for purge at ${record.purgeAfter}`;
|
|
1024
1025
|
break;
|
|
1025
1026
|
}
|
|
1027
|
+
case "remove-subtree": {
|
|
1028
|
+
if (!id) throw new Error("artifact remove-subtree requires exactly one artifact id");
|
|
1029
|
+
const outcome = await client.call<Record<string, unknown>, { removed: string[]; skipped: string[] }>("artifact.remove_subtree", { id, reason });
|
|
1030
|
+
result = outcome;
|
|
1031
|
+
human = `Trashed ${outcome.removed.length} artifact(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-trashed` : ""}.`;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1026
1034
|
case "restore": {
|
|
1027
1035
|
if (!id) throw new Error("artifact restore requires exactly one artifact id");
|
|
1028
1036
|
const outcome = await client.call<Record<string, unknown>, { restored: boolean }>("artifact.restore", { id });
|
|
@@ -1045,7 +1053,7 @@ export async function runArtifactCli(args: string[], client: TaskCliClient, proj
|
|
|
1045
1053
|
break;
|
|
1046
1054
|
}
|
|
1047
1055
|
default:
|
|
1048
|
-
throw new Error("artifact action must be create, query, show, remove, restore, trash-status, or trash-list");
|
|
1056
|
+
throw new Error("artifact action must be create, query, show, remove, remove-subtree, restore, trash-status, or trash-list");
|
|
1049
1057
|
}
|
|
1050
1058
|
return json ? JSON.stringify(result) : human;
|
|
1051
1059
|
}
|
package/src/constants.ts
CHANGED
|
@@ -123,6 +123,8 @@ export const PLAYBOOK_INVOCATION_MAX_CREATED_TASKS = 200;
|
|
|
123
123
|
|
|
124
124
|
/** Tasks.cancelSubtree walks `contains` edges transitively (a whole materialized playbook run can be torn down in one call instead of enumerating every task id by hand) -- bounded the same way PLAYBOOK_INVOCATION_MAX_CREATED_TASKS bounds the forward direction. */
|
|
125
125
|
export const TASK_CANCEL_SUBTREE_MAX_NODES = 500;
|
|
126
|
+
/** artifact.remove_subtree walks `contains` transitively across any artifact kind (a task tree, or a playbook's own nested-playbook children) -- same bound rationale as TASK_CANCEL_SUBTREE_MAX_NODES, kept separate since the two traversals serve different operations. */
|
|
127
|
+
export const ARTIFACT_REMOVE_SUBTREE_MAX_NODES = 500;
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* At the core, a workflow Skill creates Tasks and begins a pipeline -- an Ansible playbook or
|
package/src/service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SERVICE_MAX_BODY_BYTES, SQLITE_SCHEMA_VERSION } from "./constants.ts";
|
|
2
2
|
import { VERSION } from "./version.ts";
|
|
3
3
|
import { migrateDb, openDb, schemaVersion } from "./db.ts";
|
|
4
|
+
import { removeArtifactSubtree } from "./artifact-subtree.ts";
|
|
4
5
|
import { SQLiteArtifactStore } from "./adapters/sqlite-artifact-store.ts";
|
|
5
6
|
import { SQLiteGateRunner } from "./adapters/sqlite-gate-runner.ts";
|
|
6
7
|
import { SQLiteArtifactScopeStore } from "./adapters/sqlite-artifact-scope-store.ts";
|
|
@@ -57,7 +58,7 @@ import { SQLiteDiscussionRoundStore } from "./adapters/sqlite-discussion-round-s
|
|
|
57
58
|
*/
|
|
58
59
|
const COMPOSITION_ROOT_OPERATION_NAMES = [
|
|
59
60
|
"system.migrate", "artifact.create", "artifact.query", "artifact.show",
|
|
60
|
-
"artifact.remove", "artifact.restore", "artifact.trash_status", "artifact.trash_list",
|
|
61
|
+
"artifact.remove", "artifact.remove_subtree", "artifact.restore", "artifact.trash_status", "artifact.trash_list",
|
|
61
62
|
"graph.link", "graph.unlink", "graph.tree", "graph.status", "graph.history", "gates.run",
|
|
62
63
|
"rules.injectable", "skills.instantiate",
|
|
63
64
|
] as const;
|
|
@@ -280,6 +281,7 @@ function handlers(
|
|
|
280
281
|
maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
|
|
281
282
|
}),
|
|
282
283
|
"artifact.remove": (input) => artifacts.trash(string(input, "id"), { reason: optionalString(input, "reason"), context: eventContext(input) }),
|
|
284
|
+
"artifact.remove_subtree": (input) => removeArtifactSubtree(artifacts, string(input, "id"), { reason: optionalString(input, "reason"), context: eventContext(input) }),
|
|
283
285
|
"artifact.restore": (input) => artifacts.restore(string(input, "id"), eventContext(input)),
|
|
284
286
|
"artifact.trash_status": (input) => artifacts.trashStatus(string(input, "id")),
|
|
285
287
|
"artifact.trash_list": () => artifacts.listTrash(),
|