@danypops/papyrus 0.60.1 → 0.60.3
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 +2 -1
- package/src/artifact/artifact.ts +9 -0
- 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/note-command.ts +29 -0
- package/src/cli/task-command.ts +51 -0
- package/src/cli.ts +16 -0
- package/src/constants.ts +9 -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/notes.ts +15 -0
- package/src/handlers/registry.ts +2 -0
- package/src/handlers/tasks.ts +13 -0
- package/src/index.ts +8 -0
- package/src/modules/binders.ts +171 -0
- package/src/modules/notes.ts +10 -0
- package/src/modules/tasks.ts +9 -0
- package/src/note/note-service.ts +72 -0
- package/src/ops.ts +7 -1
- package/src/service.ts +46 -4
- package/src/task/task-service.ts +96 -0
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(),
|
|
@@ -388,6 +409,7 @@ function handlers(
|
|
|
388
409
|
"tasks.create": forwardToModule("tasks.create"),
|
|
389
410
|
"tasks.update": forwardToModule("tasks.update"),
|
|
390
411
|
"tasks.list": forwardToModule("tasks.list"),
|
|
412
|
+
"tasks.list_page": forwardToModule("tasks.list_page"),
|
|
391
413
|
"tasks.graph": forwardToModule("tasks.graph"),
|
|
392
414
|
"tasks.plan": forwardToModule("tasks.plan"),
|
|
393
415
|
"tasks.show": forwardToModule("tasks.show"),
|
|
@@ -428,6 +450,24 @@ function handlers(
|
|
|
428
450
|
"tasks.reap_stale_leases": forwardToModule("tasks.reap_stale_leases"),
|
|
429
451
|
"tasks.event_feed": forwardToModule("tasks.event_feed"),
|
|
430
452
|
"tasks.reap_stale_focus": forwardToModule("tasks.reap_stale_focus"),
|
|
453
|
+
"binders.create": forwardToModule("binders.create"),
|
|
454
|
+
"binders.list": forwardToModule("binders.list"),
|
|
455
|
+
"binders.tree": forwardToModule("binders.tree"),
|
|
456
|
+
"binders.show": forwardToModule("binders.show"),
|
|
457
|
+
"binders.update": forwardToModule("binders.update"),
|
|
458
|
+
"binders.move": forwardToModule("binders.move"),
|
|
459
|
+
"binders.file": forwardToModule("binders.file"),
|
|
460
|
+
"binders.unfile": forwardToModule("binders.unfile"),
|
|
461
|
+
"binders.remove": forwardToModule("binders.remove"),
|
|
462
|
+
"binders.scope": forwardToModule("binders.scope"),
|
|
463
|
+
"binders.set_global": forwardToModule("binders.set_global"),
|
|
464
|
+
"binders.set_none": forwardToModule("binders.set_none"),
|
|
465
|
+
"binders.add_project": forwardToModule("binders.add_project"),
|
|
466
|
+
"binders.remove_project": forwardToModule("binders.remove_project"),
|
|
467
|
+
"binders.replace_projects": forwardToModule("binders.replace_projects"),
|
|
468
|
+
"binders.add_group": forwardToModule("binders.add_group"),
|
|
469
|
+
"binders.remove_group": forwardToModule("binders.remove_group"),
|
|
470
|
+
"binders.replace_groups": forwardToModule("binders.replace_groups"),
|
|
431
471
|
"docs.create": forwardToModule("docs.create"),
|
|
432
472
|
"docs.list": forwardToModule("docs.list"),
|
|
433
473
|
"docs.show": forwardToModule("docs.show"),
|
|
@@ -448,6 +488,7 @@ function handlers(
|
|
|
448
488
|
"docs.update": forwardToModule("docs.update"),
|
|
449
489
|
"notes.capture": forwardToModule("notes.capture"),
|
|
450
490
|
"notes.list": forwardToModule("notes.list"),
|
|
491
|
+
"notes.list_page": forwardToModule("notes.list_page"),
|
|
451
492
|
"notes.show": forwardToModule("notes.show"),
|
|
452
493
|
"notes.history": forwardToModule("notes.history"),
|
|
453
494
|
"notes.consume": forwardToModule("notes.consume"),
|
|
@@ -563,6 +604,7 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
563
604
|
moduleRegistry.registerAll(sessionIdentityOperations(sessionIdentity));
|
|
564
605
|
moduleRegistry.registerAll(discussOperations(discussions));
|
|
565
606
|
moduleRegistry.registerAll(tasksOperations(tasks, artifacts, sessionIdentity));
|
|
607
|
+
moduleRegistry.registerAll(bindersOperations(artifacts, artifactScopes, projectRegistry, scopeGroups));
|
|
566
608
|
moduleRegistry.registerAll(docsOperations(artifacts, artifactScopes, authority, projectRegistry, scopeGroups));
|
|
567
609
|
moduleRegistry.registerAll(rulesOperations(artifacts, artifactScopes, projectRegistry, scopeGroups));
|
|
568
610
|
moduleRegistry.registerAll(
|
package/src/task/task-service.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
TASK_EXECUTION_MAX_NODES,
|
|
12
12
|
TASK_LABEL_MAX_COUNT,
|
|
13
13
|
TASK_LABEL_MAX_LENGTH,
|
|
14
|
+
TASK_LIST_PAGE_DEFAULT_LIMIT,
|
|
15
|
+
TASK_LIST_PAGE_MAX_LIMIT,
|
|
14
16
|
TASK_SCOPE_MAX_TASKS,
|
|
15
17
|
TASK_TITLE_MAX_LENGTH,
|
|
16
18
|
} from "../constants.ts";
|
|
@@ -98,6 +100,15 @@ export interface TaskFilter {
|
|
|
98
100
|
labels?: string[];
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
export interface TaskPageFilter extends TaskFilter {
|
|
104
|
+
cursor?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface TaskPage {
|
|
108
|
+
items: Artifact[];
|
|
109
|
+
nextCursor?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
export type TaskStatus = TaskLifecycleStatus;
|
|
102
113
|
|
|
103
114
|
export interface TaskMutationMetadata {
|
|
@@ -207,6 +218,45 @@ function canonicalJson(value: unknown): string {
|
|
|
207
218
|
return JSON.stringify(value) ?? "null";
|
|
208
219
|
}
|
|
209
220
|
|
|
221
|
+
interface TaskPageCursor {
|
|
222
|
+
v: 1;
|
|
223
|
+
createdAt: string;
|
|
224
|
+
id: string;
|
|
225
|
+
filterHash: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function taskPageFilterHash(filter: TaskFilter, selection: TaskViewSelection): string {
|
|
229
|
+
return createHash("sha256")
|
|
230
|
+
.update(
|
|
231
|
+
canonicalJson({
|
|
232
|
+
status: filter.status,
|
|
233
|
+
text: filter.text,
|
|
234
|
+
labels: [...(filter.labels ?? [])].sort(),
|
|
235
|
+
mode: selection.mode,
|
|
236
|
+
projectRoot: selection.projectRoot,
|
|
237
|
+
rootTaskId: selection.rootTaskId,
|
|
238
|
+
}),
|
|
239
|
+
)
|
|
240
|
+
.digest("base64url");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function encodeTaskPageCursor(task: Artifact, filterHash: string): string {
|
|
244
|
+
return Buffer.from(JSON.stringify({ v: 1, createdAt: task.created_at, id: task.id, filterHash } satisfies TaskPageCursor)).toString(
|
|
245
|
+
"base64url",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function decodeTaskPageCursor(cursor: string | undefined, filterHash: string): TaskPageCursor | undefined {
|
|
250
|
+
if (cursor === undefined) return undefined;
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")) as Partial<TaskPageCursor>;
|
|
253
|
+
if (parsed.v !== 1 || !parsed.createdAt || !parsed.id || parsed.filterHash !== filterHash) throw new Error("invalid cursor");
|
|
254
|
+
return parsed as TaskPageCursor;
|
|
255
|
+
} catch {
|
|
256
|
+
throw new Error("task page cursor is invalid or does not match the requested filters");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
210
260
|
export class Tasks {
|
|
211
261
|
constructor(
|
|
212
262
|
private readonly artifacts: ArtifactStore,
|
|
@@ -428,6 +478,52 @@ export class Tasks {
|
|
|
428
478
|
.slice(0, limit);
|
|
429
479
|
}
|
|
430
480
|
|
|
481
|
+
/** Stable, cursor-paged Task inventory. Creation order is immutable, so content updates cannot move an item between pages. */
|
|
482
|
+
listPage(filter: TaskPageFilter = {}): TaskPage {
|
|
483
|
+
const selection = this.projectScope.scopeSelection(filter.projectRoot, filter.scope, filter.rootTaskId);
|
|
484
|
+
const limit = filter.limit ?? TASK_LIST_PAGE_DEFAULT_LIMIT;
|
|
485
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > TASK_LIST_PAGE_MAX_LIMIT) {
|
|
486
|
+
throw new Error(`task page limit must be between 1 and ${TASK_LIST_PAGE_MAX_LIMIT}`);
|
|
487
|
+
}
|
|
488
|
+
const filterHash = taskPageFilterHash(filter, selection);
|
|
489
|
+
const cursor = decodeTaskPageCursor(filter.cursor, filterHash);
|
|
490
|
+
let candidates: Artifact[];
|
|
491
|
+
if (selection.mode === "all") {
|
|
492
|
+
candidates = this.artifacts.query({
|
|
493
|
+
kind: "task",
|
|
494
|
+
excludeSubtype: DISCUSSION_SUBTYPE,
|
|
495
|
+
status: filter.status,
|
|
496
|
+
text: filter.text,
|
|
497
|
+
labels: filter.labels,
|
|
498
|
+
order: "created_desc",
|
|
499
|
+
...(cursor ? { after: { createdAt: cursor.createdAt, id: cursor.id } } : {}),
|
|
500
|
+
limit: limit + 1,
|
|
501
|
+
});
|
|
502
|
+
} else {
|
|
503
|
+
const ids = this.scopes.taskIds(selection.projectRoot, TASK_SCOPE_MAX_TASKS + 1);
|
|
504
|
+
if (ids.length > TASK_SCOPE_MAX_TASKS) throw new Error(`task project scope exceeds ${TASK_SCOPE_MAX_TASKS} tasks`);
|
|
505
|
+
const selectedIds = selection.mode === "graph" ? this.descendantIds(selection.rootTaskId!, ids) : new Set(ids);
|
|
506
|
+
const text = filter.text?.toLowerCase();
|
|
507
|
+
const labels = filter.labels ?? [];
|
|
508
|
+
candidates = this.artifacts
|
|
509
|
+
.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE, ids: [...selectedIds] })
|
|
510
|
+
.filter((task) => filter.status === undefined || task.status === filter.status)
|
|
511
|
+
.filter((task) => text === undefined || task.title.toLowerCase().includes(text) || task.body.toLowerCase().includes(text))
|
|
512
|
+
.filter((task) => labels.every((label) => task.labels.includes(label)))
|
|
513
|
+
.sort((left, right) => right.created_at.localeCompare(left.created_at) || left.id.localeCompare(right.id))
|
|
514
|
+
.filter(
|
|
515
|
+
(task) =>
|
|
516
|
+
cursor === undefined || task.created_at < cursor.createdAt || (task.created_at === cursor.createdAt && task.id > cursor.id),
|
|
517
|
+
)
|
|
518
|
+
.slice(0, limit + 1);
|
|
519
|
+
}
|
|
520
|
+
const items = candidates.slice(0, limit);
|
|
521
|
+
return {
|
|
522
|
+
items,
|
|
523
|
+
...(candidates.length > limit && items.length > 0 ? { nextCursor: encodeTaskPageCursor(items.at(-1)!, filterHash) } : {}),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
431
527
|
scopeSelection(projectRoot?: string, mode?: TaskViewMode, rootTaskId?: string): TaskViewSelection {
|
|
432
528
|
return this.projectScope.scopeSelection(projectRoot, mode, rootTaskId);
|
|
433
529
|
}
|