@danypops/papyrus 0.11.4 → 0.12.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/README.md +16 -2
- package/extension/src/active-task-continuation.ts +6 -0
- package/extension/src/domain-tools.ts +108 -52
- package/extension/src/index.ts +90 -37
- package/extension/src/notes.ts +14 -1
- package/extension/src/task-focus-events.ts +57 -0
- package/extension/src/tasks.ts +51 -15
- package/extension/src/tool-rendering/artifact-card.ts +117 -0
- package/extension/src/tool-rendering/artifact-list.ts +179 -0
- package/extension/src/tool-rendering/index.ts +107 -0
- package/extension/src/tool-rendering/render-model.ts +406 -0
- package/package.json +4 -2
- package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
- package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
- package/src/adapters/sqlite-artifact-store.ts +20 -11
- package/src/adapters/sqlite-discourse-store.ts +325 -0
- package/src/adapters/sqlite-graph-projection-store.ts +41 -0
- package/src/adapters/sqlite-task-focus-store.ts +34 -15
- package/src/authority-registry.ts +115 -0
- package/src/cli.ts +904 -124
- package/src/constants.ts +38 -5
- package/src/conversation-journal-service.ts +87 -0
- package/src/db.ts +285 -33
- package/src/domain/artifact-event.ts +99 -0
- package/src/domain/conversation-journal.ts +168 -0
- package/src/domain/discourse-store.ts +142 -0
- package/src/domain/graph-projection.ts +74 -0
- package/src/domain/task-event.ts +4 -0
- package/src/domain-services.ts +133 -38
- package/src/graph-projection-service.ts +103 -0
- package/src/id-migration.ts +200 -0
- package/src/module-registry.ts +53 -0
- package/src/modules/docs.ts +77 -0
- package/src/modules/graph-projection.ts +82 -0
- package/src/modules/notes.ts +76 -0
- package/src/modules/rules.ts +81 -0
- package/src/modules/skills.ts +113 -0
- package/src/modules/tasks.ts +164 -0
- package/src/ops.ts +142 -15
- package/src/ports/artifact-scope-store.ts +20 -0
- package/src/ports/artifact-store.ts +10 -5
- package/src/ports/conversation-journal-store.ts +17 -0
- package/src/ports/graph-projection-store.ts +15 -0
- package/src/ports/task-focus-store.ts +62 -20
- package/src/service.ts +218 -223
- package/src/task-service.ts +70 -38
package/src/service.ts
CHANGED
|
@@ -3,111 +3,68 @@ import { VERSION } from "./version.ts";
|
|
|
3
3
|
import { migrateDb, openDb, schemaVersion } from "./db.ts";
|
|
4
4
|
import { SQLiteArtifactStore } from "./adapters/sqlite-artifact-store.ts";
|
|
5
5
|
import { SQLiteGateRunner } from "./adapters/sqlite-gate-runner.ts";
|
|
6
|
+
import { SQLiteDiscourseStore } from "./adapters/sqlite-discourse-store.ts";
|
|
7
|
+
import { SQLiteArtifactScopeStore } from "./adapters/sqlite-artifact-scope-store.ts";
|
|
8
|
+
import { SQLiteGraphProjectionStore } from "./adapters/sqlite-graph-projection-store.ts";
|
|
6
9
|
import { SQLiteTaskFocusStore } from "./adapters/sqlite-task-focus-store.ts";
|
|
7
10
|
import { SQLiteTaskEventStore } from "./adapters/sqlite-task-event-store.ts";
|
|
8
11
|
import { SQLiteTaskScopeStore } from "./adapters/sqlite-task-scope-store.ts";
|
|
9
12
|
import type { CreateArtifactInput } from "./domain/artifact.ts";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
13
|
+
import { DISCOURSE_RELATIONS, isDiscourseSubtype } from "./domain/discourse-store.ts";
|
|
14
|
+
import { AuthorityRegistry, AuthorizedArtifactWriter, type AuthorityClaim } from "./authority-registry.ts";
|
|
15
|
+
import type { TaskEventContext } from "./domain/task-event.ts";
|
|
12
16
|
import type { TaskViewMode } from "./domain/task-scope.ts";
|
|
13
17
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
14
18
|
import type { GateRunner } from "./ports/gate-runner.ts";
|
|
15
19
|
import type { TaskEventStore } from "./ports/task-event-store.ts";
|
|
16
20
|
import type { TaskScopeStore } from "./ports/task-scope-store.ts";
|
|
17
|
-
import { projectTaskExecution } from "./task-execution.ts";
|
|
18
21
|
import { Tasks, type TaskStatus } from "./task-service.ts";
|
|
19
22
|
import {
|
|
20
|
-
createArtifactTemplate,
|
|
21
|
-
createDocument,
|
|
22
|
-
createRule,
|
|
23
|
-
createSkill,
|
|
24
|
-
linkDocument,
|
|
25
|
-
gateTaskWithRule,
|
|
26
23
|
instantiateTemplate,
|
|
27
|
-
listDocuments,
|
|
28
|
-
listRules,
|
|
29
24
|
listInjectableRules,
|
|
30
|
-
listSkills,
|
|
31
|
-
previewRule,
|
|
32
|
-
showDocument,
|
|
33
|
-
showRule,
|
|
34
|
-
showSkill,
|
|
35
|
-
skillInvocation,
|
|
36
|
-
transitionDocument,
|
|
37
|
-
transitionRule,
|
|
38
|
-
transitionSkill,
|
|
39
|
-
type DocumentRelation,
|
|
40
25
|
} from "./domain-services.ts";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
26
|
+
import { Notes, NOTE_SUBTYPE } from "./note-service.ts";
|
|
27
|
+
import { OperationRegistry } from "./module-registry.ts";
|
|
28
|
+
import { docsOperations, DOCS_OPERATION_NAMES } from "./modules/docs.ts";
|
|
29
|
+
import { graphProjectionOperations, GRAPH_PROJECTION_OPERATION_NAMES } from "./modules/graph-projection.ts";
|
|
30
|
+
import { notesOperations, NOTES_OPERATION_NAMES } from "./modules/notes.ts";
|
|
31
|
+
import { rulesOperations, RULES_OPERATION_NAMES } from "./modules/rules.ts";
|
|
32
|
+
import { skillsOperations, SKILLS_OPERATION_NAMES } from "./modules/skills.ts";
|
|
33
|
+
import { tasksOperations, TASKS_OPERATION_NAMES } from "./modules/tasks.ts";
|
|
44
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Operations with no registered module: the generic, cross-cutting kernel surface
|
|
37
|
+
* (artifact create/query/show, graph link/unlink/tree/status/history, gates run --
|
|
38
|
+
* no domain owns creation/linking/traversal for every kind, the same way system.migrate
|
|
39
|
+
* has no owning module) and two permanent composition-root exceptions (rules.injectable
|
|
40
|
+
* needs tasks.active(); skills.instantiate branches into tasks.create()) -- see
|
|
41
|
+
* src/modules/rules.ts and src/modules/skills.ts's module comments. discourse.store's
|
|
42
|
+
* eventual home depends on the still-open Discourse projection-target decision, not on
|
|
43
|
+
* module extraction.
|
|
44
|
+
*/
|
|
45
|
+
const COMPOSITION_ROOT_OPERATION_NAMES = [
|
|
46
|
+
"system.migrate", "discourse.store", "artifact.create", "artifact.query", "artifact.show",
|
|
47
|
+
"graph.link", "graph.unlink", "graph.tree", "graph.status", "graph.history", "gates.run",
|
|
48
|
+
"rules.injectable", "skills.instantiate",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Each registered module owns its own operation-name list (src/modules/*.ts); this is a
|
|
53
|
+
* spread of those plus the composition-root exceptions above, not a second hand-
|
|
54
|
+
* maintained copy. TypeScript needs this to stay a compile-time-known array (it derives
|
|
55
|
+
* OperationName, which powers Record<OperationName, OperationHandler>'s exhaustiveness
|
|
56
|
+
* check below) — it cannot be generated from moduleRegistry.list() (a runtime value)
|
|
57
|
+
* without losing that guarantee, so this composition of `as const` arrays is the
|
|
58
|
+
* furthest this can go while keeping that safety net.
|
|
59
|
+
*/
|
|
45
60
|
export const EXPECTED_OPERATION_NAMES = [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"gates.run",
|
|
54
|
-
"rules.injectable",
|
|
55
|
-
"tasks.create",
|
|
56
|
-
"tasks.update",
|
|
57
|
-
"tasks.list",
|
|
58
|
-
"tasks.graph",
|
|
59
|
-
"tasks.plan",
|
|
60
|
-
"tasks.show",
|
|
61
|
-
"tasks.history",
|
|
62
|
-
"tasks.scope",
|
|
63
|
-
"tasks.set_scope",
|
|
64
|
-
"tasks.assign_project",
|
|
65
|
-
"tasks.active",
|
|
66
|
-
"tasks.focused",
|
|
67
|
-
"tasks.focus",
|
|
68
|
-
"tasks.pause",
|
|
69
|
-
"tasks.unpause",
|
|
70
|
-
"tasks.clear_focus",
|
|
71
|
-
"tasks.start",
|
|
72
|
-
"tasks.submit",
|
|
73
|
-
"tasks.complete",
|
|
74
|
-
"tasks.run_gates",
|
|
75
|
-
"tasks.set_checklist",
|
|
76
|
-
"tasks.context",
|
|
77
|
-
"tasks.reject",
|
|
78
|
-
"tasks.retry",
|
|
79
|
-
"tasks.cancel",
|
|
80
|
-
"tasks.depend",
|
|
81
|
-
"tasks.contain",
|
|
82
|
-
"docs.create",
|
|
83
|
-
"docs.list",
|
|
84
|
-
"docs.show",
|
|
85
|
-
"docs.activate",
|
|
86
|
-
"docs.archive",
|
|
87
|
-
"docs.reopen",
|
|
88
|
-
"docs.link",
|
|
89
|
-
"notes.capture",
|
|
90
|
-
"notes.list",
|
|
91
|
-
"notes.show",
|
|
92
|
-
"notes.consume",
|
|
93
|
-
"notes.promote",
|
|
94
|
-
"notes.archive",
|
|
95
|
-
"rules.create",
|
|
96
|
-
"rules.list",
|
|
97
|
-
"rules.show",
|
|
98
|
-
"rules.preview",
|
|
99
|
-
"rules.enable",
|
|
100
|
-
"rules.disable",
|
|
101
|
-
"rules.gate",
|
|
102
|
-
"skills.create",
|
|
103
|
-
"skills.create_template",
|
|
104
|
-
"skills.list",
|
|
105
|
-
"skills.show",
|
|
106
|
-
"skills.invoke",
|
|
107
|
-
"skills.run",
|
|
108
|
-
"skills.enable",
|
|
109
|
-
"skills.disable",
|
|
110
|
-
"skills.instantiate",
|
|
61
|
+
...COMPOSITION_ROOT_OPERATION_NAMES,
|
|
62
|
+
...TASKS_OPERATION_NAMES,
|
|
63
|
+
...DOCS_OPERATION_NAMES,
|
|
64
|
+
...NOTES_OPERATION_NAMES,
|
|
65
|
+
...RULES_OPERATION_NAMES,
|
|
66
|
+
...SKILLS_OPERATION_NAMES,
|
|
67
|
+
...GRAPH_PROJECTION_OPERATION_NAMES,
|
|
111
68
|
] as const;
|
|
112
69
|
|
|
113
70
|
export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
|
|
@@ -150,6 +107,57 @@ function normalizeCreateInput(input: OperationInput): CreateArtifactInput {
|
|
|
150
107
|
return { ...rest, templateId: typeof template_id === "string" ? template_id : undefined } as CreateArtifactInput;
|
|
151
108
|
}
|
|
152
109
|
|
|
110
|
+
function templateSubtype(artifacts: ArtifactStore, templateId: string | undefined): string | undefined {
|
|
111
|
+
if (!templateId) return undefined;
|
|
112
|
+
const defaults = artifacts.get(templateId)?.extra["defaults"];
|
|
113
|
+
if (typeof defaults !== "object" || defaults === null || Array.isArray(defaults)) return undefined;
|
|
114
|
+
const subtype = (defaults as Record<string, unknown>)["subtype"];
|
|
115
|
+
return typeof subtype === "string" ? subtype : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The one deep enforcement point (step 4 of reducing-papyrus-consumer-change-amplification-with-modules--pvdo)
|
|
120
|
+
* replacing the previously scattered isDiscourseSubtype/NOTE_SUBTYPE/task-kind checks that used
|
|
121
|
+
* to be re-implemented at every write call site. "generic" is the caller identity for the
|
|
122
|
+
* low-level artifact.create / graph.link / graph.unlink / graph.status surface, which owns
|
|
123
|
+
* nothing itself, so any claimed kind, subtype, or relation is rejected for it — exactly
|
|
124
|
+
* matching the historical behavior these checks replace.
|
|
125
|
+
*/
|
|
126
|
+
const GENERIC_CALLER = "generic";
|
|
127
|
+
|
|
128
|
+
const discourseAuthorityClaim: AuthorityClaim = {
|
|
129
|
+
owner: "discourse",
|
|
130
|
+
matchesArtifact: (_kind, subtype) => isDiscourseSubtype(subtype),
|
|
131
|
+
matchesRelation: (relation) => DISCOURSE_RELATIONS.has(relation),
|
|
132
|
+
denyMessage: (action) => action === "link" ? "forum-owned Context Mesh links require discourse.store" : "forum-owned Context Mesh Docs require discourse.store",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const notesAuthorityClaim: AuthorityClaim = {
|
|
136
|
+
owner: "notes",
|
|
137
|
+
matchesArtifact: (kind, subtype) => kind === "doc" && subtype === NOTE_SUBTYPE,
|
|
138
|
+
denyMessage: (action) => {
|
|
139
|
+
if (action === "link") return "note relationships require a notes.* operation so disposition provenance is preserved";
|
|
140
|
+
if (action === "status") return "note lifecycle changes require a notes.* operation so disposition provenance is preserved";
|
|
141
|
+
return "note creation requires notes.capture";
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Only ever checked for the "status" action today (graph.status): artifact.create redirects
|
|
146
|
+
// kind="task" to tasks.create rather than rejecting, and graph.link/unlink never checked task
|
|
147
|
+
// ownership historically. appliesToAction scopes the claim so it cannot leak into those paths.
|
|
148
|
+
const tasksAuthorityClaim: AuthorityClaim = {
|
|
149
|
+
owner: "tasks",
|
|
150
|
+
matchesArtifact: (kind) => kind === "task",
|
|
151
|
+
appliesToAction: (action) => action === "status",
|
|
152
|
+
denyMessage: () => "task lifecycle changes require a tasks.* operation so history and review invariants are preserved",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function createAuthorityRegistry(): AuthorityRegistry {
|
|
156
|
+
const authority = new AuthorityRegistry();
|
|
157
|
+
authority.claimAll([discourseAuthorityClaim, notesAuthorityClaim, tasksAuthorityClaim]);
|
|
158
|
+
return authority;
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
export interface SchemaState {
|
|
154
162
|
current: number;
|
|
155
163
|
required: number;
|
|
@@ -170,10 +178,19 @@ function handlers(
|
|
|
170
178
|
gates: GateRunner,
|
|
171
179
|
tasks: Tasks,
|
|
172
180
|
notes: Notes,
|
|
181
|
+
discourse: SQLiteDiscourseStore,
|
|
173
182
|
events: TaskEventStore,
|
|
174
183
|
scopes: TaskScopeStore,
|
|
175
184
|
migrate: () => unknown,
|
|
185
|
+
moduleRegistry: OperationRegistry,
|
|
186
|
+
authority: AuthorityRegistry,
|
|
176
187
|
): Record<OperationName, OperationHandler> {
|
|
188
|
+
const genericWriter = new AuthorizedArtifactWriter(artifacts, authority, GENERIC_CALLER);
|
|
189
|
+
// Notes is the first module extracted behind the OperationRegistry (src/modules/notes.ts);
|
|
190
|
+
// these six entries stay in this completeness-checked table only as a thin forward so
|
|
191
|
+
// `Record<OperationName, OperationHandler>` still guarantees every operation has an entry
|
|
192
|
+
// at compile time. The actual notes.* logic now lives in the module, not here.
|
|
193
|
+
const forwardToModule = (name: OperationName): OperationHandler => (input) => moduleRegistry.get(name)!.execute(input);
|
|
177
194
|
const eventContext = (input: OperationInput): TaskEventContext => ({
|
|
178
195
|
actor: optionalString(input, "actor"),
|
|
179
196
|
source: optionalString(input, "source"),
|
|
@@ -194,12 +211,15 @@ function handlers(
|
|
|
194
211
|
projectRoot: string(input, "project_root"),
|
|
195
212
|
scope: optionalString(input, "scope") as TaskViewMode | undefined,
|
|
196
213
|
rootTaskId: optionalString(input, "root_task_id"),
|
|
214
|
+
sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
|
|
197
215
|
});
|
|
198
216
|
return {
|
|
199
217
|
"system.migrate": () => migrate(),
|
|
218
|
+
"discourse.store": (input) => discourse.execute(input),
|
|
200
219
|
"artifact.create": (input) => {
|
|
201
220
|
const normalized = normalizeCreateInput(input);
|
|
202
|
-
|
|
221
|
+
authority.requireArtifactAllowed(normalized.kind, normalized.subtype ?? templateSubtype(artifacts, normalized.templateId), "create", GENERIC_CALLER);
|
|
222
|
+
authority.requireArtifactAllowed(normalized.kind, normalized.subtype, "create", GENERIC_CALLER);
|
|
203
223
|
if (normalized.kind !== "task") return artifacts.create(normalized);
|
|
204
224
|
return tasks.create({
|
|
205
225
|
id: normalized.id,
|
|
@@ -224,25 +244,44 @@ function handlers(
|
|
|
224
244
|
const from = string(input, "from");
|
|
225
245
|
const relation = string(input, "relation");
|
|
226
246
|
const to = string(input, "to");
|
|
247
|
+
genericWriter.checkLink({ from, relation, to });
|
|
227
248
|
if (relation === "depends_on" && artifacts.get(from)?.kind === "task" && artifacts.get(to)?.kind === "task") {
|
|
228
|
-
tasks.depend(from, to);
|
|
249
|
+
tasks.depend(from, to, eventContext(input));
|
|
229
250
|
} else {
|
|
230
|
-
artifacts.link({ from, relation, to });
|
|
251
|
+
artifacts.link({ from, relation, to }, eventContext(input));
|
|
231
252
|
}
|
|
232
253
|
return { ok: true };
|
|
233
254
|
},
|
|
255
|
+
"graph.unlink": (input) => {
|
|
256
|
+
const from = string(input, "from");
|
|
257
|
+
const relation = string(input, "relation");
|
|
258
|
+
const to = string(input, "to");
|
|
259
|
+
genericWriter.checkLink({ from, relation, to });
|
|
260
|
+
let removed: boolean;
|
|
261
|
+
if (relation === "depends_on" && artifacts.get(from)?.kind === "task" && artifacts.get(to)?.kind === "task") {
|
|
262
|
+
const before = tasks.graph().nodes.find((node) => node.task.id === from)?.dependencyIds.includes(to) ?? false;
|
|
263
|
+
tasks.undepend(from, to, eventContext(input));
|
|
264
|
+
removed = before;
|
|
265
|
+
} else {
|
|
266
|
+
removed = artifacts.unlink({ from, relation, to }, eventContext(input));
|
|
267
|
+
}
|
|
268
|
+
return { removed };
|
|
269
|
+
},
|
|
234
270
|
"graph.tree": (input) => artifacts.get(string(input, "id"), {
|
|
235
271
|
tree: true,
|
|
236
272
|
depth: optionalNumber(input, "depth"),
|
|
237
273
|
maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
|
|
238
274
|
}),
|
|
239
|
-
"graph.status": (input) =>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
"graph.status": (input) => genericWriter.setStatus(string(input, "id"), string(input, "status"), eventContext(input)),
|
|
276
|
+
"graph.history": (input) => artifacts.events({
|
|
277
|
+
artifactId: optionalString(input, "id"),
|
|
278
|
+
actor: optionalString(input, "actor"),
|
|
279
|
+
sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
|
|
280
|
+
since: optionalString(input, "since"),
|
|
281
|
+
limit: optionalNumber(input, "limit"),
|
|
282
|
+
cursor: optionalNumber(input, "cursor"),
|
|
283
|
+
direction: optionalString(input, "direction") as "asc" | "desc" | undefined,
|
|
284
|
+
}),
|
|
246
285
|
"gates.run": (input) => {
|
|
247
286
|
const id = string(input, "id");
|
|
248
287
|
return artifacts.get(id)?.kind === "task"
|
|
@@ -251,131 +290,74 @@ function handlers(
|
|
|
251
290
|
},
|
|
252
291
|
"rules.injectable": (input) => listInjectableRules(artifacts, tasks.active(taskFilter(input))?.id)
|
|
253
292
|
.map(({ id, title, body, extra }) => ({ id, title, body, extra })),
|
|
254
|
-
"tasks.create": (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
"tasks.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"tasks.
|
|
275
|
-
"tasks.
|
|
276
|
-
"tasks.
|
|
277
|
-
"tasks.
|
|
278
|
-
"tasks.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
),
|
|
289
|
-
"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
),
|
|
294
|
-
"
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
315
|
-
}),
|
|
316
|
-
"docs.list": (input) => listDocuments(artifacts, artifactFilter(input)),
|
|
317
|
-
"docs.show": (input) => showDocument(artifacts, string(input, "id")),
|
|
318
|
-
"docs.activate": (input) => transitionDocument(artifacts, string(input, "id"), "activate"),
|
|
319
|
-
"docs.archive": (input) => transitionDocument(artifacts, string(input, "id"), "archive"),
|
|
320
|
-
"docs.reopen": (input) => transitionDocument(artifacts, string(input, "id"), "reopen"),
|
|
321
|
-
"docs.link": (input) => linkDocument(artifacts, string(input, "id"), string(input, "relation") as DocumentRelation, string(input, "target_id")),
|
|
322
|
-
"notes.capture": (input) => notes.capture({
|
|
323
|
-
body: string(input, "body"), title: optionalString(input, "title"), projectRoot: string(input, "project_root"),
|
|
324
|
-
actor: optionalString(input, "actor"), source: optionalString(input, "source"), sessionId: optionalString(input, "session_id"),
|
|
325
|
-
}),
|
|
326
|
-
"notes.list": (input) => notes.list({
|
|
327
|
-
projectRoot: string(input, "project_root"), status: optionalString(input, "status") as "draft" | "active" | "archived" | undefined,
|
|
328
|
-
text: optionalString(input, "text"), limit: optionalNumber(input, "limit"),
|
|
329
|
-
}),
|
|
330
|
-
"notes.show": (input) => notes.show(string(input, "id"), string(input, "project_root")),
|
|
331
|
-
"notes.consume": (input) => notes.consume(string(input, "id"), {
|
|
332
|
-
projectRoot: string(input, "project_root"), actor: optionalString(input, "actor"), source: optionalString(input, "source"),
|
|
333
|
-
sessionId: optionalString(input, "session_id"), reason: optionalString(input, "reason"),
|
|
334
|
-
}),
|
|
335
|
-
"notes.promote": (input) => notes.promote(string(input, "id"), string(input, "target_id"), {
|
|
336
|
-
projectRoot: string(input, "project_root"), actor: optionalString(input, "actor"), source: optionalString(input, "source"),
|
|
337
|
-
sessionId: optionalString(input, "session_id"), reason: optionalString(input, "reason"),
|
|
338
|
-
}),
|
|
339
|
-
"notes.archive": (input) => notes.archive(string(input, "id"), {
|
|
340
|
-
projectRoot: string(input, "project_root"), disposition: string(input, "disposition") as NoteDisposition,
|
|
341
|
-
actor: optionalString(input, "actor"), source: optionalString(input, "source"), sessionId: optionalString(input, "session_id"),
|
|
342
|
-
reason: optionalString(input, "reason"),
|
|
343
|
-
}),
|
|
344
|
-
"rules.create": (input) => createRule(artifacts, {
|
|
345
|
-
title: string(input, "title"), body: optionalString(input, "body"), condition: optionalString(input, "condition"),
|
|
346
|
-
action: optionalString(input, "rule_action") ?? optionalString(input, "governance_action"),
|
|
347
|
-
severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
|
|
348
|
-
labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
|
|
349
|
-
}),
|
|
350
|
-
"rules.list": (input) => listRules(artifacts, artifactFilter(input)),
|
|
351
|
-
"rules.show": (input) => showRule(artifacts, string(input, "id")),
|
|
352
|
-
"rules.preview": (input) => previewRule(artifacts, string(input, "id")),
|
|
353
|
-
"rules.enable": (input) => transitionRule(artifacts, string(input, "id"), "enable"),
|
|
354
|
-
"rules.disable": (input) => transitionRule(artifacts, string(input, "id"), "disable"),
|
|
355
|
-
"rules.gate": (input) => gateTaskWithRule(artifacts, string(input, "id"), string(input, "task_id")),
|
|
356
|
-
"skills.create": (input) => createSkill(artifacts, {
|
|
357
|
-
title: string(input, "title"), body: optionalString(input, "body"), trigger: optionalString(input, "trigger"),
|
|
358
|
-
steps: input["steps"] as string[] | undefined, tools: input["tools"] as string[] | undefined,
|
|
359
|
-
definition: input["definition"],
|
|
360
|
-
labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
|
|
361
|
-
}),
|
|
362
|
-
"skills.create_template": (input) => createArtifactTemplate(artifacts, {
|
|
363
|
-
title: string(input, "title"), targetKind: string(input, "target_kind"), defaults: input["defaults"] as Record<string, unknown> | undefined,
|
|
364
|
-
required: input["required"] as string[] | undefined, body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
365
|
-
}),
|
|
366
|
-
"skills.list": (input) => listSkills(artifacts, artifactFilter(input)),
|
|
367
|
-
"skills.show": (input) => showSkill(artifacts, string(input, "id")),
|
|
368
|
-
"skills.invoke": (input) => skillInvocation(artifacts, string(input, "id")),
|
|
369
|
-
"skills.run": (input) => instantiateSkillWorkflow(artifacts, string(input, "id"), {
|
|
370
|
-
runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
|
|
371
|
-
arguments: input["arguments"] as Record<string, unknown> | undefined,
|
|
372
|
-
}, { events, scopes, projectRoot: string(input, "project_root"), context: eventContextFor(input, "skill-run") }),
|
|
373
|
-
"skills.enable": (input) => transitionSkill(artifacts, string(input, "id"), "enable"),
|
|
374
|
-
"skills.disable": (input) => transitionSkill(artifacts, string(input, "id"), "disable"),
|
|
293
|
+
"tasks.create": forwardToModule("tasks.create"),
|
|
294
|
+
"tasks.update": forwardToModule("tasks.update"),
|
|
295
|
+
"tasks.list": forwardToModule("tasks.list"),
|
|
296
|
+
"tasks.graph": forwardToModule("tasks.graph"),
|
|
297
|
+
"tasks.plan": forwardToModule("tasks.plan"),
|
|
298
|
+
"tasks.show": forwardToModule("tasks.show"),
|
|
299
|
+
"tasks.history": forwardToModule("tasks.history"),
|
|
300
|
+
"tasks.scope": forwardToModule("tasks.scope"),
|
|
301
|
+
"tasks.set_scope": forwardToModule("tasks.set_scope"),
|
|
302
|
+
"tasks.assign_project": forwardToModule("tasks.assign_project"),
|
|
303
|
+
"tasks.active": forwardToModule("tasks.active"),
|
|
304
|
+
"tasks.focused": forwardToModule("tasks.focused"),
|
|
305
|
+
"tasks.focus": forwardToModule("tasks.focus"),
|
|
306
|
+
"tasks.pause": forwardToModule("tasks.pause"),
|
|
307
|
+
"tasks.unpause": forwardToModule("tasks.unpause"),
|
|
308
|
+
"tasks.clear_focus": forwardToModule("tasks.clear_focus"),
|
|
309
|
+
"tasks.start": forwardToModule("tasks.start"),
|
|
310
|
+
"tasks.submit": forwardToModule("tasks.submit"),
|
|
311
|
+
"tasks.complete": forwardToModule("tasks.complete"),
|
|
312
|
+
"tasks.run_gates": forwardToModule("tasks.run_gates"),
|
|
313
|
+
"tasks.set_checklist": forwardToModule("tasks.set_checklist"),
|
|
314
|
+
"tasks.context": forwardToModule("tasks.context"),
|
|
315
|
+
"tasks.reject": forwardToModule("tasks.reject"),
|
|
316
|
+
"tasks.retry": forwardToModule("tasks.retry"),
|
|
317
|
+
"tasks.cancel": forwardToModule("tasks.cancel"),
|
|
318
|
+
"tasks.depend": forwardToModule("tasks.depend"),
|
|
319
|
+
"tasks.undepend": forwardToModule("tasks.undepend"),
|
|
320
|
+
"tasks.contain": forwardToModule("tasks.contain"),
|
|
321
|
+
"tasks.uncontain": forwardToModule("tasks.uncontain"),
|
|
322
|
+
"docs.create": forwardToModule("docs.create"),
|
|
323
|
+
"docs.list": forwardToModule("docs.list"),
|
|
324
|
+
"docs.show": forwardToModule("docs.show"),
|
|
325
|
+
"docs.activate": forwardToModule("docs.activate"),
|
|
326
|
+
"docs.archive": forwardToModule("docs.archive"),
|
|
327
|
+
"docs.reopen": forwardToModule("docs.reopen"),
|
|
328
|
+
"docs.link": forwardToModule("docs.link"),
|
|
329
|
+
"docs.assign_project": forwardToModule("docs.assign_project"),
|
|
330
|
+
"notes.capture": forwardToModule("notes.capture"),
|
|
331
|
+
"notes.list": forwardToModule("notes.list"),
|
|
332
|
+
"notes.show": forwardToModule("notes.show"),
|
|
333
|
+
"notes.consume": forwardToModule("notes.consume"),
|
|
334
|
+
"notes.promote": forwardToModule("notes.promote"),
|
|
335
|
+
"notes.archive": forwardToModule("notes.archive"),
|
|
336
|
+
"rules.create": forwardToModule("rules.create"),
|
|
337
|
+
"rules.list": forwardToModule("rules.list"),
|
|
338
|
+
"rules.show": forwardToModule("rules.show"),
|
|
339
|
+
"rules.preview": forwardToModule("rules.preview"),
|
|
340
|
+
"rules.enable": forwardToModule("rules.enable"),
|
|
341
|
+
"rules.disable": forwardToModule("rules.disable"),
|
|
342
|
+
"rules.gate": forwardToModule("rules.gate"),
|
|
343
|
+
"rules.assign_project": forwardToModule("rules.assign_project"),
|
|
344
|
+
"skills.create": forwardToModule("skills.create"),
|
|
345
|
+
"skills.create_template": forwardToModule("skills.create_template"),
|
|
346
|
+
"skills.list": forwardToModule("skills.list"),
|
|
347
|
+
"skills.show": forwardToModule("skills.show"),
|
|
348
|
+
"skills.invoke": forwardToModule("skills.invoke"),
|
|
349
|
+
"skills.run": forwardToModule("skills.run"),
|
|
350
|
+
"skills.enable": forwardToModule("skills.enable"),
|
|
351
|
+
"skills.disable": forwardToModule("skills.disable"),
|
|
352
|
+
"skills.assign_project": forwardToModule("skills.assign_project"),
|
|
375
353
|
"skills.instantiate": (input) => {
|
|
376
354
|
const templateId = string(input, "template_id");
|
|
377
355
|
const template = artifacts.get(templateId);
|
|
378
|
-
|
|
356
|
+
// Deliberately pass an unresolved kind so only the discourse claim (kind-agnostic) can match here —
|
|
357
|
+
// the historical requireDiscourseStoreForSubtype never checked notes at this call site; that check
|
|
358
|
+
// already happens inside instantiateTemplate's own rejectsNoteTemplate for the non-task branch below.
|
|
359
|
+
authority.requireArtifactAllowed(undefined, templateSubtype(artifacts, templateId), "create", GENERIC_CALLER);
|
|
360
|
+
if (template?.extra["targetKind"] !== "task") return instantiateTemplate(artifacts, templateId, normalizeCreateInput(input), authority, eventContext(input));
|
|
379
361
|
return tasks.create({
|
|
380
362
|
title: optionalString(input, "title") as string,
|
|
381
363
|
body: optionalString(input, "body"),
|
|
@@ -387,6 +369,8 @@ function handlers(
|
|
|
387
369
|
projectSource: "cwd",
|
|
388
370
|
}, eventContextFor(input, "template-instantiation"));
|
|
389
371
|
},
|
|
372
|
+
"graph_projection.apply": forwardToModule("graph_projection.apply"),
|
|
373
|
+
"graph_projection.checkpoint": forwardToModule("graph_projection.checkpoint"),
|
|
390
374
|
};
|
|
391
375
|
}
|
|
392
376
|
|
|
@@ -399,7 +383,18 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
399
383
|
const scopes = new SQLiteTaskScopeStore(db);
|
|
400
384
|
const tasks = new Tasks(artifacts, gates, focus, events, scopes);
|
|
401
385
|
const notes = new Notes(artifacts);
|
|
402
|
-
const
|
|
386
|
+
const discourse = new SQLiteDiscourseStore(db, artifacts);
|
|
387
|
+
const projections = new SQLiteGraphProjectionStore(db);
|
|
388
|
+
const artifactScopes = new SQLiteArtifactScopeStore(db);
|
|
389
|
+
const authority = createAuthorityRegistry();
|
|
390
|
+
const moduleRegistry = new OperationRegistry();
|
|
391
|
+
moduleRegistry.registerAll(notesOperations(notes));
|
|
392
|
+
moduleRegistry.registerAll(tasksOperations(tasks, artifacts));
|
|
393
|
+
moduleRegistry.registerAll(docsOperations(artifacts, artifactScopes, authority));
|
|
394
|
+
moduleRegistry.registerAll(rulesOperations(artifacts, artifactScopes));
|
|
395
|
+
moduleRegistry.registerAll(skillsOperations({ artifacts, events, scopes, artifactScopes, authority }));
|
|
396
|
+
moduleRegistry.registerAll(graphProjectionOperations(artifacts, projections, authority));
|
|
397
|
+
const registry = handlers(artifacts, gates, tasks, notes, discourse, events, scopes, () => migrateDb(db), moduleRegistry, authority);
|
|
403
398
|
const state = (): SchemaState => {
|
|
404
399
|
const current = schemaVersion(db);
|
|
405
400
|
return { current, required: SQLITE_SCHEMA_VERSION, migrationRequired: current !== SQLITE_SCHEMA_VERSION };
|
|
@@ -411,7 +406,7 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
411
406
|
const handler = registry[operation as OperationName];
|
|
412
407
|
if (!handler) throw new UnknownOperationError(`unknown operation "${operation}"`);
|
|
413
408
|
if (operation !== "system.migrate" && state().migrationRequired) {
|
|
414
|
-
throw new MigrationRequiredError("database migration required; run `papyrus migrate
|
|
409
|
+
throw new MigrationRequiredError("database migration required; run `papyrus migrate schema`");
|
|
415
410
|
}
|
|
416
411
|
return handler(input);
|
|
417
412
|
},
|