@danypops/papyrus 0.60.0 → 0.60.2
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 +3 -2
- package/src/binder/binder-service.ts +507 -0
- package/src/binder/binder.ts +34 -0
- package/src/binder/index.ts +2 -0
- package/src/cli/binders-command.ts +340 -0
- package/src/cli.ts +14 -0
- package/src/constants.ts +6 -1
- package/src/db.ts +23 -0
- package/src/handlers/artifact-trash.ts +12 -3
- package/src/handlers/binders.ts +231 -0
- package/src/handlers/registry.ts +2 -0
- package/src/handlers/tasks.ts +9 -3
- package/src/index.ts +8 -0
- package/src/modules/binders.ts +171 -0
- package/src/project-registry/scope-source.ts +1 -1
- package/src/service.ts +44 -4
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
2
|
+
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
3
|
+
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
4
|
+
import type { ArtifactTrashStore } from "../artifact/artifact-trash-store.ts";
|
|
5
|
+
import { binderTree } from "../binder/binder-service.ts";
|
|
6
|
+
import { bindersOperations } from "../modules/binders.ts";
|
|
7
|
+
import type { ProjectRegistryStore } from "../project-registry/project-registry-store.ts";
|
|
8
|
+
import { normalizeProjectRoot } from "../project-registry/scope-source.ts";
|
|
9
|
+
import type { ScopeGroupStore } from "../scope-group/scope-group-store.ts";
|
|
10
|
+
import { booleanProp, createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
|
|
11
|
+
|
|
12
|
+
const OWNER = "binders";
|
|
13
|
+
|
|
14
|
+
function resolveBinderId(
|
|
15
|
+
artifacts: ArtifactStore,
|
|
16
|
+
scopes: ArtifactScopeStore,
|
|
17
|
+
projectRoot: string | undefined,
|
|
18
|
+
id: unknown,
|
|
19
|
+
name: unknown,
|
|
20
|
+
): string {
|
|
21
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
22
|
+
if (typeof name !== "string" || name.trim().length === 0) throw validationError("id or name is required");
|
|
23
|
+
const alias = artifacts.getByAlias(name.trim());
|
|
24
|
+
if (alias?.kind === "binder" && (projectRoot === undefined || scopes.appliesToProjectRoot(alias.id, normalizeProjectRoot(projectRoot)))) {
|
|
25
|
+
return alias.id;
|
|
26
|
+
}
|
|
27
|
+
const tree = binderTree(artifacts, scopes, { projectRoot });
|
|
28
|
+
const pathNeedle = name.trim().startsWith("/") ? name.trim() : `/${name.trim()}`;
|
|
29
|
+
const pathMatches = tree.nodes.filter((node) => node.path.toLowerCase() === pathNeedle.toLowerCase());
|
|
30
|
+
if (pathMatches.length === 1) return pathMatches[0]!.binder.id;
|
|
31
|
+
const titleMatches = tree.nodes.filter((node) => node.binder.title.trim().toLowerCase() === name.trim().toLowerCase());
|
|
32
|
+
if (titleMatches.length === 1) return titleMatches[0]!.binder.id;
|
|
33
|
+
if (titleMatches.length > 1 || pathMatches.length > 1) {
|
|
34
|
+
const matches = pathMatches.length > 0 ? pathMatches : titleMatches;
|
|
35
|
+
throw validationError(`binder name "${name}" is ambiguous: ${matches.map((node) => node.path).join(", ")} -- use a path or id`);
|
|
36
|
+
}
|
|
37
|
+
throw validationError(`no binder named "${name}" found in this project context`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveAnyArtifactId(artifacts: ArtifactStore, id: unknown, name: unknown): string {
|
|
41
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
42
|
+
if (typeof name !== "string" || name.trim().length === 0) throw validationError("artifact_id or artifact_name is required");
|
|
43
|
+
return resolveArtifactIdWidened(artifacts, name, () => artifacts.query({ text: name }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function registerBindersVehicleOperations(
|
|
47
|
+
registry: VehicleRegistry,
|
|
48
|
+
artifacts: ArtifactStore & ArtifactTrashStore,
|
|
49
|
+
scopes: ArtifactScopeStore,
|
|
50
|
+
projectRegistry: ProjectRegistryStore,
|
|
51
|
+
scopeGroups: ScopeGroupStore,
|
|
52
|
+
): void {
|
|
53
|
+
const operations = new Map(
|
|
54
|
+
bindersOperations(artifacts, scopes, projectRegistry, scopeGroups).map((operation) => [operation.name, operation]),
|
|
55
|
+
);
|
|
56
|
+
const call = (name: string, input: Record<string, unknown>): unknown => operations.get(name)!.execute(input);
|
|
57
|
+
const define = createOperationDefiner(registry, OWNER, "binders", ["binders:read", "binders:write"], call);
|
|
58
|
+
const arrayProp = { type: "array" } as unknown as { type: string };
|
|
59
|
+
const withBinderId = (input: Record<string, unknown>, idKey = "id", nameKey = "name") => ({
|
|
60
|
+
...input,
|
|
61
|
+
[idKey]: resolveBinderId(artifacts, scopes, input.project_root as string | undefined, input[idKey], input[nameKey]),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
define(
|
|
65
|
+
"create",
|
|
66
|
+
"Creates a filesystem-style Binder. Binders organize artifacts without changing Task containment or Playbook execution. Direct labels on a Binder are inherited additively by descendants at read time. Prefer parent_name (a title, alias, or /path) over parent_id.",
|
|
67
|
+
"local-write",
|
|
68
|
+
{ title: stringProp, labels: arrayProp, parent_id: stringProp, parent_name: stringProp, project_root: stringProp, projects: arrayProp },
|
|
69
|
+
["title"],
|
|
70
|
+
(input) => ({
|
|
71
|
+
...input,
|
|
72
|
+
...(input.parent_id || input.parent_name
|
|
73
|
+
? { parent_id: resolveBinderId(artifacts, scopes, input.project_root as string | undefined, input.parent_id, input.parent_name) }
|
|
74
|
+
: {}),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
define(
|
|
78
|
+
"list",
|
|
79
|
+
"Lists Binders. project_root alone is exact-membership audit scope; project_root plus applicable:true includes global Binders and Binders applicable to the project. Returns lean summaries unless full:true.",
|
|
80
|
+
"read",
|
|
81
|
+
{ text: stringProp, limit: numberProp, project_root: stringProp, applicable: booleanProp, full: booleanProp },
|
|
82
|
+
[],
|
|
83
|
+
(input) => input,
|
|
84
|
+
);
|
|
85
|
+
define(
|
|
86
|
+
"tree",
|
|
87
|
+
"Returns the project-context Binder tree plus placements and direct/inherited/effective labels for the bounded artifact_ids supplied. Label inheritance is computed, not copied into artifact labels.",
|
|
88
|
+
"read",
|
|
89
|
+
{ project_root: stringProp, artifact_ids: arrayProp },
|
|
90
|
+
[],
|
|
91
|
+
(input) => input,
|
|
92
|
+
);
|
|
93
|
+
define(
|
|
94
|
+
"show",
|
|
95
|
+
"Shows one Binder node by id, alias, title, or /path, including its path and inherited/effective labels.",
|
|
96
|
+
"read",
|
|
97
|
+
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
98
|
+
[],
|
|
99
|
+
(input) => withBinderId(input),
|
|
100
|
+
);
|
|
101
|
+
define(
|
|
102
|
+
"update",
|
|
103
|
+
"Renames a Binder and/or replaces its direct labels. Inherited labels on descendants update immediately because they are computed dynamically.",
|
|
104
|
+
"local-write",
|
|
105
|
+
{ id: stringProp, name: stringProp, title: stringProp, labels: arrayProp, project_root: stringProp },
|
|
106
|
+
[],
|
|
107
|
+
(input) => withBinderId(input),
|
|
108
|
+
);
|
|
109
|
+
define(
|
|
110
|
+
"move",
|
|
111
|
+
"Moves a Binder under parent_id/parent_name, or to the project-context root when neither is supplied. Rejects cycles and duplicate sibling names.",
|
|
112
|
+
"local-write",
|
|
113
|
+
{ id: stringProp, name: stringProp, parent_id: stringProp, parent_name: stringProp, project_root: stringProp },
|
|
114
|
+
[],
|
|
115
|
+
(input) => {
|
|
116
|
+
const resolved = withBinderId(input);
|
|
117
|
+
return {
|
|
118
|
+
...resolved,
|
|
119
|
+
...(input.parent_id || input.parent_name
|
|
120
|
+
? { parent_id: resolveBinderId(artifacts, scopes, input.project_root as string | undefined, input.parent_id, input.parent_name) }
|
|
121
|
+
: {}),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
define(
|
|
126
|
+
"file",
|
|
127
|
+
"Files one non-Binder artifact in a Binder for this project context, replacing its previous visible placement. Prefer binder_name/artifact_name over ids when unambiguous.",
|
|
128
|
+
"local-write",
|
|
129
|
+
{
|
|
130
|
+
binder_id: stringProp,
|
|
131
|
+
binder_name: stringProp,
|
|
132
|
+
artifact_id: stringProp,
|
|
133
|
+
artifact_name: stringProp,
|
|
134
|
+
project_root: stringProp,
|
|
135
|
+
},
|
|
136
|
+
[],
|
|
137
|
+
(input) => ({
|
|
138
|
+
...input,
|
|
139
|
+
binder_id: resolveBinderId(artifacts, scopes, input.project_root as string | undefined, input.binder_id, input.binder_name),
|
|
140
|
+
artifact_id: resolveAnyArtifactId(artifacts, input.artifact_id, input.artifact_name),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
define(
|
|
144
|
+
"unfile",
|
|
145
|
+
"Moves one artifact to this project context's Binder root. This changes organization only, never Task containment or dependencies.",
|
|
146
|
+
"local-write",
|
|
147
|
+
{ artifact_id: stringProp, artifact_name: stringProp, project_root: stringProp },
|
|
148
|
+
[],
|
|
149
|
+
(input) => ({ ...input, artifact_id: resolveAnyArtifactId(artifacts, input.artifact_id, input.artifact_name) }),
|
|
150
|
+
);
|
|
151
|
+
define(
|
|
152
|
+
"remove",
|
|
153
|
+
"Trashes an empty Binder. A non-empty Binder is rejected until its contents are moved or unfiled.",
|
|
154
|
+
"local-write",
|
|
155
|
+
{ id: stringProp, name: stringProp, project_root: stringProp, reason: stringProp },
|
|
156
|
+
[],
|
|
157
|
+
(input) => withBinderId(input),
|
|
158
|
+
);
|
|
159
|
+
define(
|
|
160
|
+
"scope",
|
|
161
|
+
"Shows a Binder's project/scope-group scope.",
|
|
162
|
+
"read",
|
|
163
|
+
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
164
|
+
[],
|
|
165
|
+
(input) => withBinderId(input),
|
|
166
|
+
);
|
|
167
|
+
define(
|
|
168
|
+
"set_global",
|
|
169
|
+
"Makes a Binder apply in every project.",
|
|
170
|
+
"local-write",
|
|
171
|
+
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
172
|
+
[],
|
|
173
|
+
(input) => withBinderId(input),
|
|
174
|
+
);
|
|
175
|
+
define(
|
|
176
|
+
"set_none",
|
|
177
|
+
"Hides a Binder from every project context.",
|
|
178
|
+
"local-write",
|
|
179
|
+
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
180
|
+
[],
|
|
181
|
+
(input) => withBinderId(input),
|
|
182
|
+
);
|
|
183
|
+
define(
|
|
184
|
+
"add_project",
|
|
185
|
+
"Adds one registered project to a Binder's explicit scope.",
|
|
186
|
+
"local-write",
|
|
187
|
+
{ id: stringProp, name: stringProp, project: stringProp, project_root: stringProp },
|
|
188
|
+
["project"],
|
|
189
|
+
(input) => withBinderId(input),
|
|
190
|
+
);
|
|
191
|
+
define(
|
|
192
|
+
"remove_project",
|
|
193
|
+
"Removes one project from a Binder's explicit scope; removing the final member is rejected.",
|
|
194
|
+
"local-write",
|
|
195
|
+
{ id: stringProp, name: stringProp, project: stringProp, project_root: stringProp },
|
|
196
|
+
["project"],
|
|
197
|
+
(input) => withBinderId(input),
|
|
198
|
+
);
|
|
199
|
+
define(
|
|
200
|
+
"replace_projects",
|
|
201
|
+
"Replaces a Binder's project membership with a bounded non-empty list.",
|
|
202
|
+
"local-write",
|
|
203
|
+
{ id: stringProp, name: stringProp, projects: arrayProp, project_root: stringProp },
|
|
204
|
+
["projects"],
|
|
205
|
+
(input) => withBinderId(input),
|
|
206
|
+
);
|
|
207
|
+
define(
|
|
208
|
+
"add_group",
|
|
209
|
+
"Adds one nested scope group to a Binder's explicit scope.",
|
|
210
|
+
"local-write",
|
|
211
|
+
{ id: stringProp, name: stringProp, group: stringProp, project_root: stringProp },
|
|
212
|
+
["group"],
|
|
213
|
+
(input) => withBinderId(input),
|
|
214
|
+
);
|
|
215
|
+
define(
|
|
216
|
+
"remove_group",
|
|
217
|
+
"Removes one scope group from a Binder's explicit scope; removing the final member is rejected.",
|
|
218
|
+
"local-write",
|
|
219
|
+
{ id: stringProp, name: stringProp, group: stringProp, project_root: stringProp },
|
|
220
|
+
["group"],
|
|
221
|
+
(input) => withBinderId(input),
|
|
222
|
+
);
|
|
223
|
+
define(
|
|
224
|
+
"replace_groups",
|
|
225
|
+
"Replaces a Binder's scope-group membership with a bounded non-empty list.",
|
|
226
|
+
"local-write",
|
|
227
|
+
{ id: stringProp, name: stringProp, groups: arrayProp, project_root: stringProp },
|
|
228
|
+
["groups"],
|
|
229
|
+
(input) => withBinderId(input),
|
|
230
|
+
);
|
|
231
|
+
}
|
package/src/handlers/registry.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { TaskScopeStore } from "../task/scope/task-scope-store.ts";
|
|
|
21
21
|
import type { Tasks } from "../task/task-service.ts";
|
|
22
22
|
import { registerArtifactTrashOperations } from "./artifact-trash.ts";
|
|
23
23
|
import { registerBatchVehicleOperation } from "./batch.ts";
|
|
24
|
+
import { registerBindersVehicleOperations } from "./binders.ts";
|
|
24
25
|
import { registerDiscussVehicleOperations } from "./discuss.ts";
|
|
25
26
|
import { registerDocsVehicleOperations } from "./docs.ts";
|
|
26
27
|
import { registerNotesVehicleOperations } from "./notes.ts";
|
|
@@ -56,6 +57,7 @@ export function createPapyrusVehicleRegistry(deps: PapyrusVehicleDeps): VehicleR
|
|
|
56
57
|
// session_id, a correlation id, not a secret -- see session-identity-service.ts).
|
|
57
58
|
registry.setExposeHandlerFailureDetails(true);
|
|
58
59
|
registerNotesVehicleOperations(registry, deps.notes, deps.artifacts);
|
|
60
|
+
registerBindersVehicleOperations(registry, deps.artifacts, deps.scopes, deps.projectRegistry, deps.scopeGroups);
|
|
59
61
|
registerRulesVehicleOperations(registry, deps.artifacts, deps.scopes, deps.projectRegistry, deps.scopeGroups);
|
|
60
62
|
registerDocsVehicleOperations(registry, deps.artifacts, deps.scopes, deps.authority, deps.projectRegistry, deps.scopeGroups);
|
|
61
63
|
registerPlaybooksVehicleOperations(registry, {
|
package/src/handlers/tasks.ts
CHANGED
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
*
|
|
20
20
|
* remove/remove_subtree/restore are not duplicated here -- see ./artifact-trash-vehicle.ts.
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
VEHICLE_SCHEMA_PRESENTATION_EXTENSION,
|
|
24
|
+
VehicleError,
|
|
25
|
+
type VehicleLimits,
|
|
26
|
+
type VehicleOperationContext,
|
|
27
|
+
} from "@danypops/vehicle-core";
|
|
23
28
|
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
24
29
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
25
30
|
import { GATE_TIMEOUT_MAX_MS, TASK_CREATE_IDEMPOTENCY_KEY_MAX_LENGTH, TASK_MUTATION_IDEMPOTENCY_KEY_MAX_LENGTH } from "../constants.ts";
|
|
@@ -83,6 +88,7 @@ const GATE_OPERATION_LIMITS: VehicleLimits = {
|
|
|
83
88
|
const objectProp = { type: "object" } as const;
|
|
84
89
|
const arrayProp = { type: "array" } as const;
|
|
85
90
|
const _boolProp = { type: "boolean" } as const;
|
|
91
|
+
const streamingStringProp = { ...stringProp, [VEHICLE_SCHEMA_PRESENTATION_EXTENSION]: "stream" } as const;
|
|
86
92
|
const mutationIdempotencyProp = {
|
|
87
93
|
type: "string",
|
|
88
94
|
minLength: 1,
|
|
@@ -368,7 +374,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
368
374
|
"local-write",
|
|
369
375
|
{
|
|
370
376
|
title: stringProp,
|
|
371
|
-
body:
|
|
377
|
+
body: streamingStringProp,
|
|
372
378
|
status: stringProp,
|
|
373
379
|
labels: arrayProp,
|
|
374
380
|
extra: objectProp,
|
|
@@ -418,7 +424,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
418
424
|
id: stringProp,
|
|
419
425
|
name: stringProp,
|
|
420
426
|
title: stringProp,
|
|
421
|
-
body:
|
|
427
|
+
body: streamingStringProp,
|
|
422
428
|
labels: arrayProp,
|
|
423
429
|
status: stringProp,
|
|
424
430
|
reason: stringProp,
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
export type { Artifact, ArtifactEdge } from "./artifact/artifact.ts";
|
|
9
9
|
export { projectArtifactRelationships } from "./artifact/artifact-relationship-view.ts";
|
|
10
10
|
export type { ArtifactStore } from "./artifact/artifact-store.ts";
|
|
11
|
+
export {
|
|
12
|
+
BINDER_FILED_IN_RELATION,
|
|
13
|
+
BINDER_KIND,
|
|
14
|
+
BINDER_ORGANIZES_RELATION,
|
|
15
|
+
type BinderArtifactPlacement,
|
|
16
|
+
type BinderNode,
|
|
17
|
+
type BinderTree,
|
|
18
|
+
} from "./binder/binder.ts";
|
|
11
19
|
export {
|
|
12
20
|
connectPapyrusClient,
|
|
13
21
|
type PapyrusClient,
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { summarizeArtifact } from "../artifact/artifact.ts";
|
|
2
|
+
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
3
|
+
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
4
|
+
import type { ArtifactTrashStore } from "../artifact/artifact-trash-store.ts";
|
|
5
|
+
import {
|
|
6
|
+
addBinderGroup,
|
|
7
|
+
addBinderProject,
|
|
8
|
+
binderScope,
|
|
9
|
+
binderTree,
|
|
10
|
+
createBinder,
|
|
11
|
+
fileArtifact,
|
|
12
|
+
listBinders,
|
|
13
|
+
moveBinder,
|
|
14
|
+
removeBinder,
|
|
15
|
+
removeBinderGroup,
|
|
16
|
+
removeBinderProject,
|
|
17
|
+
replaceBinderGroups,
|
|
18
|
+
replaceBinderProjects,
|
|
19
|
+
setBinderGlobal,
|
|
20
|
+
setBinderNone,
|
|
21
|
+
unfileArtifact,
|
|
22
|
+
updateBinder,
|
|
23
|
+
} from "../binder/binder-service.ts";
|
|
24
|
+
import type { OperationDefinition } from "../module-registry.ts";
|
|
25
|
+
import type { ProjectRegistryStore } from "../project-registry/project-registry-store.ts";
|
|
26
|
+
import type { ScopeGroupStore } from "../scope-group/scope-group-store.ts";
|
|
27
|
+
import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
|
|
28
|
+
|
|
29
|
+
const MODULE_ID = "binders";
|
|
30
|
+
|
|
31
|
+
const eventContext = (input: OperationInput) => ({
|
|
32
|
+
actor: optionalString(input, "actor"),
|
|
33
|
+
source: optionalString(input, "source"),
|
|
34
|
+
sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const listFilter = (input: OperationInput) => {
|
|
38
|
+
const projectRoot = optionalString(input, "project_root");
|
|
39
|
+
const applicable = optionalBoolean(input, "applicable") === true;
|
|
40
|
+
if (applicable && projectRoot === undefined) throw new Error("applicable requires project_root");
|
|
41
|
+
return {
|
|
42
|
+
text: optionalString(input, "text"),
|
|
43
|
+
limit: optionalNumber(input, "limit"),
|
|
44
|
+
...(applicable ? { applicableToProjectRoot: projectRoot } : { projectRoot }),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const BINDERS_OPERATION_NAMES = [
|
|
49
|
+
"binders.create",
|
|
50
|
+
"binders.list",
|
|
51
|
+
"binders.tree",
|
|
52
|
+
"binders.show",
|
|
53
|
+
"binders.update",
|
|
54
|
+
"binders.move",
|
|
55
|
+
"binders.file",
|
|
56
|
+
"binders.unfile",
|
|
57
|
+
"binders.remove",
|
|
58
|
+
"binders.scope",
|
|
59
|
+
"binders.set_global",
|
|
60
|
+
"binders.set_none",
|
|
61
|
+
"binders.add_project",
|
|
62
|
+
"binders.remove_project",
|
|
63
|
+
"binders.replace_projects",
|
|
64
|
+
"binders.add_group",
|
|
65
|
+
"binders.remove_group",
|
|
66
|
+
"binders.replace_groups",
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
export function bindersOperations(
|
|
70
|
+
artifacts: ArtifactStore & ArtifactTrashStore,
|
|
71
|
+
scopes: ArtifactScopeStore,
|
|
72
|
+
registry: ProjectRegistryStore,
|
|
73
|
+
scopeGroups: ScopeGroupStore,
|
|
74
|
+
): OperationDefinition[] {
|
|
75
|
+
const define = <Input, Output>(name: string, execute: (input: Input) => Output): OperationDefinition<Input, Output> => ({
|
|
76
|
+
name,
|
|
77
|
+
moduleId: MODULE_ID,
|
|
78
|
+
execute,
|
|
79
|
+
});
|
|
80
|
+
return [
|
|
81
|
+
define("binders.create", (input: OperationInput) =>
|
|
82
|
+
createBinder(
|
|
83
|
+
artifacts,
|
|
84
|
+
scopes,
|
|
85
|
+
{
|
|
86
|
+
title: string(input, "title"),
|
|
87
|
+
labels: input.labels as string[] | undefined,
|
|
88
|
+
parentId: optionalString(input, "parent_id"),
|
|
89
|
+
projectRoot: optionalString(input, "project_root"),
|
|
90
|
+
projectReferences: input.projects as string[] | undefined,
|
|
91
|
+
},
|
|
92
|
+
eventContext(input),
|
|
93
|
+
registry,
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
define("binders.list", (input: OperationInput) => {
|
|
97
|
+
const binders = listBinders(artifacts, scopes, listFilter(input));
|
|
98
|
+
return optionalBoolean(input, "full") === true ? binders : binders.map(summarizeArtifact);
|
|
99
|
+
}),
|
|
100
|
+
define("binders.tree", (input: OperationInput) =>
|
|
101
|
+
binderTree(artifacts, scopes, {
|
|
102
|
+
projectRoot: optionalString(input, "project_root"),
|
|
103
|
+
artifactIds: input.artifact_ids as string[] | undefined,
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
define("binders.show", (input: OperationInput) => {
|
|
107
|
+
const id = string(input, "id");
|
|
108
|
+
const tree = binderTree(artifacts, scopes, { projectRoot: optionalString(input, "project_root") });
|
|
109
|
+
const node = tree.nodes.find((candidate) => candidate.binder.id === id);
|
|
110
|
+
if (!node) throw new Error(`binder artifact "${id}" not found in this project context`);
|
|
111
|
+
return node;
|
|
112
|
+
}),
|
|
113
|
+
define("binders.update", (input: OperationInput) =>
|
|
114
|
+
updateBinder(
|
|
115
|
+
artifacts,
|
|
116
|
+
scopes,
|
|
117
|
+
string(input, "id"),
|
|
118
|
+
{ title: optionalString(input, "title"), labels: input.labels as string[] | undefined },
|
|
119
|
+
optionalString(input, "project_root"),
|
|
120
|
+
eventContext(input),
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
define("binders.move", (input: OperationInput) =>
|
|
124
|
+
moveBinder(
|
|
125
|
+
artifacts,
|
|
126
|
+
scopes,
|
|
127
|
+
string(input, "id"),
|
|
128
|
+
optionalString(input, "parent_id"),
|
|
129
|
+
optionalString(input, "project_root"),
|
|
130
|
+
eventContext(input),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
define("binders.file", (input: OperationInput) =>
|
|
134
|
+
fileArtifact(
|
|
135
|
+
artifacts,
|
|
136
|
+
scopes,
|
|
137
|
+
string(input, "artifact_id"),
|
|
138
|
+
string(input, "binder_id"),
|
|
139
|
+
optionalString(input, "project_root"),
|
|
140
|
+
eventContext(input),
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
define("binders.unfile", (input: OperationInput) =>
|
|
144
|
+
unfileArtifact(artifacts, scopes, string(input, "artifact_id"), optionalString(input, "project_root"), eventContext(input)),
|
|
145
|
+
),
|
|
146
|
+
define("binders.remove", (input: OperationInput) =>
|
|
147
|
+
removeBinder(artifacts, string(input, "id"), eventContext(input), optionalString(input, "reason")),
|
|
148
|
+
),
|
|
149
|
+
define("binders.scope", (input: OperationInput) => binderScope(artifacts, scopes, string(input, "id"))),
|
|
150
|
+
define("binders.set_global", (input: OperationInput) => setBinderGlobal(artifacts, scopes, string(input, "id"))),
|
|
151
|
+
define("binders.set_none", (input: OperationInput) => setBinderNone(artifacts, scopes, string(input, "id"))),
|
|
152
|
+
define("binders.add_project", (input: OperationInput) =>
|
|
153
|
+
addBinderProject(artifacts, scopes, registry, string(input, "id"), string(input, "project")),
|
|
154
|
+
),
|
|
155
|
+
define("binders.remove_project", (input: OperationInput) =>
|
|
156
|
+
removeBinderProject(artifacts, scopes, registry, string(input, "id"), string(input, "project")),
|
|
157
|
+
),
|
|
158
|
+
define("binders.replace_projects", (input: OperationInput) =>
|
|
159
|
+
replaceBinderProjects(artifacts, scopes, registry, string(input, "id"), (input.projects as string[] | undefined) ?? []),
|
|
160
|
+
),
|
|
161
|
+
define("binders.add_group", (input: OperationInput) =>
|
|
162
|
+
addBinderGroup(artifacts, scopes, scopeGroups, string(input, "id"), string(input, "group")),
|
|
163
|
+
),
|
|
164
|
+
define("binders.remove_group", (input: OperationInput) =>
|
|
165
|
+
removeBinderGroup(artifacts, scopes, scopeGroups, string(input, "id"), string(input, "group")),
|
|
166
|
+
),
|
|
167
|
+
define("binders.replace_groups", (input: OperationInput) =>
|
|
168
|
+
replaceBinderGroups(artifacts, scopes, scopeGroups, string(input, "id"), (input.groups as string[] | undefined) ?? []),
|
|
169
|
+
),
|
|
170
|
+
];
|
|
171
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isAbsolute, normalize } from "node:path";
|
|
2
2
|
import { TASK_PROJECT_ROOT_MAX_LENGTH } from "../constants.ts";
|
|
3
3
|
|
|
4
4
|
/** How a project root got attached to a scoped artifact -- shared across Tasks/Docs/Rules/Playbooks, same category as Project in project-registry.ts. */
|
package/src/service.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ArtifactTrashStore } from "./artifact/artifact-trash-store.ts";
|
|
|
12
12
|
import { SQLiteArtifactScopeStore } from "./artifact/sqlite-artifact-scope-store.ts";
|
|
13
13
|
import { SQLiteArtifactStore } from "./artifact/sqlite-artifact-store.ts";
|
|
14
14
|
import { type AuthorityClaim, AuthorityRegistry, AuthorizedArtifactWriter } from "./authority-registry.ts";
|
|
15
|
+
import { BINDER_FILED_IN_RELATION, BINDER_KIND, BINDER_ORGANIZES_RELATION } from "./binder/binder.ts";
|
|
15
16
|
import { SERVICE_MAX_BODY_BYTES, SQLITE_SCHEMA_VERSION } from "./constants.ts";
|
|
16
17
|
import { migrateDb, openDb, schemaVersion } from "./db.ts";
|
|
17
18
|
import { Discussions } from "./discussion/discussion-service.ts";
|
|
@@ -25,6 +26,7 @@ import { logEvent } from "./log/log.ts";
|
|
|
25
26
|
import { Logs } from "./log/log-service.ts";
|
|
26
27
|
import { SQLiteLogStore } from "./log/sqlite-log-store.ts";
|
|
27
28
|
import { OperationRegistry } from "./module-registry.ts";
|
|
29
|
+
import { BINDERS_OPERATION_NAMES, bindersOperations } from "./modules/binders.ts";
|
|
28
30
|
import { DISCUSS_OPERATION_NAMES, discussOperations } from "./modules/discuss.ts";
|
|
29
31
|
import { DOCS_OPERATION_NAMES, docsOperations } from "./modules/docs.ts";
|
|
30
32
|
import { GRAPH_PROJECTION_OPERATION_NAMES, graphProjectionOperations } from "./modules/graph-projection.ts";
|
|
@@ -99,6 +101,7 @@ const COMPOSITION_ROOT_OPERATION_NAMES = [
|
|
|
99
101
|
export const EXPECTED_OPERATION_NAMES = [
|
|
100
102
|
...COMPOSITION_ROOT_OPERATION_NAMES,
|
|
101
103
|
...TASKS_OPERATION_NAMES,
|
|
104
|
+
...BINDERS_OPERATION_NAMES,
|
|
102
105
|
...DOCS_OPERATION_NAMES,
|
|
103
106
|
...NOTES_OPERATION_NAMES,
|
|
104
107
|
...RULES_OPERATION_NAMES,
|
|
@@ -163,6 +166,13 @@ const tasksAuthorityClaim: AuthorityClaim = {
|
|
|
163
166
|
denyMessage: () => "task lifecycle changes require a tasks.* operation so history and review invariants are preserved",
|
|
164
167
|
};
|
|
165
168
|
|
|
169
|
+
const bindersAuthorityClaim: AuthorityClaim = {
|
|
170
|
+
owner: "binders",
|
|
171
|
+
matchesArtifact: (kind) => kind === BINDER_KIND,
|
|
172
|
+
matchesRelation: (relation) => relation === BINDER_ORGANIZES_RELATION || relation === BINDER_FILED_IN_RELATION,
|
|
173
|
+
denyMessage: () => "Binder creation, hierarchy, and filing require a binders.* operation so path and cycle invariants are preserved",
|
|
174
|
+
};
|
|
175
|
+
|
|
166
176
|
/**
|
|
167
177
|
* The same status-bypass protection Tasks and Notes already have, extended to every other kind
|
|
168
178
|
* with its own validated transition set (Doc's draft/active/archived, Rule/Playbook's
|
|
@@ -183,6 +193,7 @@ export function createAuthorityRegistry(): AuthorityRegistry {
|
|
|
183
193
|
authority.claimAll([
|
|
184
194
|
notesAuthorityClaim,
|
|
185
195
|
tasksAuthorityClaim,
|
|
196
|
+
bindersAuthorityClaim,
|
|
186
197
|
lifecycleAuthorityClaim("docs", "doc"),
|
|
187
198
|
lifecycleAuthorityClaim("rules", "rule"),
|
|
188
199
|
lifecycleAuthorityClaim("playbooks", "playbook"),
|
|
@@ -320,10 +331,20 @@ function handlers(
|
|
|
320
331
|
depth: optionalNumber(input, "depth"),
|
|
321
332
|
maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
|
|
322
333
|
}),
|
|
323
|
-
"artifact.remove": (input) =>
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
"artifact.remove": (input) => {
|
|
335
|
+
const id = string(input, "id");
|
|
336
|
+
if (artifacts.get(id)?.kind === BINDER_KIND) {
|
|
337
|
+
throw new Error("Binder removal requires binders.remove so a non-empty directory cannot be orphaned");
|
|
338
|
+
}
|
|
339
|
+
return artifacts.trash(id, { reason: optionalString(input, "reason"), context: eventContext(input) });
|
|
340
|
+
},
|
|
341
|
+
"artifact.remove_subtree": (input) => {
|
|
342
|
+
const id = string(input, "id");
|
|
343
|
+
if (artifacts.get(id)?.kind === BINDER_KIND) {
|
|
344
|
+
throw new Error("Binder removal requires binders.remove so a non-empty directory cannot be orphaned");
|
|
345
|
+
}
|
|
346
|
+
return removeArtifactSubtree(artifacts, id, { reason: optionalString(input, "reason"), context: eventContext(input) });
|
|
347
|
+
},
|
|
327
348
|
"artifact.restore": (input) => artifacts.restore(string(input, "id"), eventContext(input)),
|
|
328
349
|
"artifact.trash_status": (input) => artifacts.trashStatus(string(input, "id")),
|
|
329
350
|
"artifact.trash_list": () => artifacts.listTrash(),
|
|
@@ -428,6 +449,24 @@ function handlers(
|
|
|
428
449
|
"tasks.reap_stale_leases": forwardToModule("tasks.reap_stale_leases"),
|
|
429
450
|
"tasks.event_feed": forwardToModule("tasks.event_feed"),
|
|
430
451
|
"tasks.reap_stale_focus": forwardToModule("tasks.reap_stale_focus"),
|
|
452
|
+
"binders.create": forwardToModule("binders.create"),
|
|
453
|
+
"binders.list": forwardToModule("binders.list"),
|
|
454
|
+
"binders.tree": forwardToModule("binders.tree"),
|
|
455
|
+
"binders.show": forwardToModule("binders.show"),
|
|
456
|
+
"binders.update": forwardToModule("binders.update"),
|
|
457
|
+
"binders.move": forwardToModule("binders.move"),
|
|
458
|
+
"binders.file": forwardToModule("binders.file"),
|
|
459
|
+
"binders.unfile": forwardToModule("binders.unfile"),
|
|
460
|
+
"binders.remove": forwardToModule("binders.remove"),
|
|
461
|
+
"binders.scope": forwardToModule("binders.scope"),
|
|
462
|
+
"binders.set_global": forwardToModule("binders.set_global"),
|
|
463
|
+
"binders.set_none": forwardToModule("binders.set_none"),
|
|
464
|
+
"binders.add_project": forwardToModule("binders.add_project"),
|
|
465
|
+
"binders.remove_project": forwardToModule("binders.remove_project"),
|
|
466
|
+
"binders.replace_projects": forwardToModule("binders.replace_projects"),
|
|
467
|
+
"binders.add_group": forwardToModule("binders.add_group"),
|
|
468
|
+
"binders.remove_group": forwardToModule("binders.remove_group"),
|
|
469
|
+
"binders.replace_groups": forwardToModule("binders.replace_groups"),
|
|
431
470
|
"docs.create": forwardToModule("docs.create"),
|
|
432
471
|
"docs.list": forwardToModule("docs.list"),
|
|
433
472
|
"docs.show": forwardToModule("docs.show"),
|
|
@@ -563,6 +602,7 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
563
602
|
moduleRegistry.registerAll(sessionIdentityOperations(sessionIdentity));
|
|
564
603
|
moduleRegistry.registerAll(discussOperations(discussions));
|
|
565
604
|
moduleRegistry.registerAll(tasksOperations(tasks, artifacts, sessionIdentity));
|
|
605
|
+
moduleRegistry.registerAll(bindersOperations(artifacts, artifactScopes, projectRegistry, scopeGroups));
|
|
566
606
|
moduleRegistry.registerAll(docsOperations(artifacts, artifactScopes, authority, projectRegistry, scopeGroups));
|
|
567
607
|
moduleRegistry.registerAll(rulesOperations(artifacts, artifactScopes, projectRegistry, scopeGroups));
|
|
568
608
|
moduleRegistry.registerAll(
|