clay-server 4.0.0-beta.8 → 4.0.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.
Files changed (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -0,0 +1,463 @@
1
+ // Authorization and binding layer for Project Logs.
2
+ //
3
+ // Modeled on workspace-query-service.js: a caller never states who it is.
4
+ // Every binder re-derives the principal from authoritative server state and
5
+ // returns null when anything fails to line up, so an unbound or stale caller
6
+ // gets no capability rather than a degraded one.
7
+ //
8
+ // Product rules enforced here:
9
+ // - Project Logs are project records. Mate projects have none.
10
+ // - Ordinary Mates cannot reach project Logs at all.
11
+ // - Authoritative builtin Clay may read Logs, and only for projects the
12
+ // owning user is authorized to see. It may never write.
13
+ // - A project session is bound to exactly its own project's Logs.
14
+ // - Canonical authorship belongs to Project Driver sessions alone. A human,
15
+ // however privileged, may read and comment but may never create or revise
16
+ // a canonical entry. The project owner may delete an entry.
17
+ // - Members of a shared project read each other's entries and comments with
18
+ // attribution.
19
+ // - A worktree resolves to its parent project for both authorization and
20
+ // storage, so worktree work never forks project knowledge.
21
+
22
+ var builtinMates = require("./builtin-mates");
23
+ var logsStore = require("./project-logs-store");
24
+ var LOG_REF_PATTERN = /^log:[A-Za-z0-9_-]{24}$/;
25
+
26
+ function readOnly() {
27
+ throw new Error("Project Logs are read-only for builtin Clay.");
28
+ }
29
+
30
+ // Canonical entries are written by the project's own agent sessions. A human
31
+ // participates by commenting, which is a separate, append-only surface.
32
+ function agentOnly() {
33
+ throw new Error("Project Logs are written by this project's agent sessions. Add a comment instead.");
34
+ }
35
+
36
+ // Reviewing feedback and moving the record between revisions are the Driver's
37
+ // judgement calls. A human cannot review their own comment into the record,
38
+ // and Clay cannot review another project's.
39
+ function driverOnly() {
40
+ throw new Error("Only this project's agent sessions can review feedback or change revisions.");
41
+ }
42
+
43
+ function ownerOnly() {
44
+ throw new Error("Only the project owner can delete Project Logs.");
45
+ }
46
+
47
+ // Bounded discovery of comments still waiting on the Driver. The store owns the
48
+ // one-pass scan over every live entry, so nothing here imposes a ceiling that
49
+ // could hide a pending comment on an older record.
50
+ function attachProjectLogsService(ctx) {
51
+ var context = ctx || {};
52
+ var getProjects = context.getProjects;
53
+ var isMultiUser = context.isMultiUser;
54
+ var resolveMate = context.resolveMate;
55
+ var canAccessProject = context.canAccessProject;
56
+ var findUserById = context.findUserById || function () { return null; };
57
+ var storeCache = new Map();
58
+
59
+ var openStore = context.openStore || function (cwd, storeOptions) {
60
+ var options = storeOptions || {};
61
+ var key = options.projectKnowledgeId || cwd;
62
+ if (!storeCache.has(key)) {
63
+ storeCache.set(key, logsStore.createProjectLogsStore({
64
+ cwd: cwd,
65
+ baseDir: context.baseDir,
66
+ projectKnowledgeId: options.projectKnowledgeId || null,
67
+ }));
68
+ }
69
+ return storeCache.get(key);
70
+ };
71
+
72
+ function projectFor(slug) {
73
+ if (!slug || typeof slug !== "string") return null;
74
+ var projects = getProjects();
75
+ return (projects && projects.get(slug)) || null;
76
+ }
77
+
78
+ // A worktree inherits its parent's visibility, so authorization is decided
79
+ // against the parent whenever the parent is registered.
80
+ function effectiveStatus(status) {
81
+ if (!status || status.isWorktree !== true || !status.parentSlug) return status;
82
+ var parent = projectFor(status.parentSlug);
83
+ if (!parent) return status;
84
+ var parentStatus = parent.getStatus();
85
+ return parentStatus || status;
86
+ }
87
+
88
+ function ownsProject(principal, status) {
89
+ if (!status) return false;
90
+ if (isMultiUser()) return !!principal.userId && status.projectOwnerId === principal.userId;
91
+ return !status.projectOwnerId || (!!principal.singleUserOwnerId && status.projectOwnerId === principal.singleUserOwnerId);
92
+ }
93
+
94
+ function authorizedProject(principal, status) {
95
+ if (!status) return false;
96
+ if (status.isMate === true) return false;
97
+ var governing = effectiveStatus(status);
98
+ if (ownsProject(principal, governing)) return true;
99
+ if (isMultiUser() && principal.userId && typeof canAccessProject === "function") {
100
+ return canAccessProject(principal.userId, governing) === true;
101
+ }
102
+ return false;
103
+ }
104
+
105
+ // Open the store for the governing project. When a worktree's parent is
106
+ // registered, its path is authoritative, so worktree Logs are shared without
107
+ // depending on a Git subprocess. The store's own root resolution remains the
108
+ // fallback for unregistered parents and for subdirectories of a checkout.
109
+ function storeForStatus(status) {
110
+ var governing = effectiveStatus(status);
111
+ if (!governing || !governing.path) throw new Error("Project Logs are unavailable for this project.");
112
+ return openStore(governing.path, { projectKnowledgeId: governing.projectKnowledgeId || null });
113
+ }
114
+
115
+ function recordContext(status) {
116
+ if (!status || status.isWorktree !== true || !status.changeSetId) return null;
117
+ return {
118
+ kind: "worktree",
119
+ changeSetId: status.changeSetId,
120
+ branch: status.branch || null,
121
+ baseCommit: status.baseCommit || null,
122
+ headCommit: status.headCommit || null,
123
+ status: "active",
124
+ };
125
+ }
126
+
127
+ function scopedArgs(args, status) {
128
+ var out = Object.assign({}, args || {});
129
+ if (!out.contextMode) out.contextMode = status && status.isWorktree ? "current" : "project";
130
+ out.currentChangeSetId = status && status.changeSetId || null;
131
+ return out;
132
+ }
133
+
134
+ function statusFor(principal, slug) {
135
+ var project = projectFor(slug);
136
+ if (!project) throw new Error("Project not found.");
137
+ var status = project.getStatus();
138
+ if (!authorizedProject(principal, status)) throw new Error("Project Logs are not available for this project.");
139
+ return status;
140
+ }
141
+
142
+ function displayNameFor(userId) {
143
+ if (!userId) return null;
144
+ var user = findUserById(userId);
145
+ if (!user) return null;
146
+ return user.displayName || user.username || null;
147
+ }
148
+
149
+ function sessionAuthor(session) {
150
+ return {
151
+ type: "session",
152
+ userId: session.ownerId || null,
153
+ displayName: displayNameFor(session.ownerId),
154
+ sessionKey: session.cliSessionId || "local:" + session.localId,
155
+ vendor: session.vendor || null,
156
+ };
157
+ }
158
+
159
+ function userAuthor(user) {
160
+ return {
161
+ type: "user",
162
+ userId: (user && user.id) || null,
163
+ displayName: (user && (user.displayName || user.username)) || null,
164
+ sessionKey: null,
165
+ vendor: null,
166
+ };
167
+ }
168
+
169
+ // --- Binders ---------------------------------------------------------
170
+
171
+ // Exact session object identity, not a caller-supplied session id. A session
172
+ // handed in from a stale or forged reference is rejected even when its
173
+ // localId matches a live session.
174
+ function resolveProjectSession(source) {
175
+ if (!source || !source.projectSlug || !source.session) return null;
176
+ var project = projectFor(source.projectSlug);
177
+ if (!project) return null;
178
+ var status = project.getStatus();
179
+ if (!status || status.isMate === true) return null;
180
+ var manager = project.getSessionManager ? project.getSessionManager() : project.sm;
181
+ if (!manager || !manager.sessions || manager.sessions.get(source.session.localId) !== source.session) return null;
182
+
183
+ var principal;
184
+ if (isMultiUser()) {
185
+ // Fail closed: an unattributed session in multi-user mode gets nothing.
186
+ if (!source.session.ownerId) return null;
187
+ principal = { userId: source.session.ownerId, singleUserOwnerId: null };
188
+ } else {
189
+ if (source.session.ownerId && status.projectOwnerId && source.session.ownerId !== status.projectOwnerId) return null;
190
+ principal = { userId: null, singleUserOwnerId: source.session.ownerId || status.projectOwnerId || null };
191
+ }
192
+ if (!authorizedProject(principal, status)) return null;
193
+
194
+ principal.kind = "session";
195
+ principal.projectSlug = source.projectSlug;
196
+ principal.isClay = false;
197
+ return { principal: principal, status: status, author: sessionAuthor(source.session) };
198
+ }
199
+
200
+ // A tool handler captured earlier stays valid only while its exact source
201
+ // still resolves. Session identity and project authorization are both
202
+ // re-derived on every call, so a session dropped from the manager, or
203
+ // replaced by a different object with the same id, loses the capability
204
+ // immediately rather than continuing to work through a captured handler.
205
+ function bindProjectSession(source) {
206
+ var initial = resolveProjectSession(source);
207
+ if (!initial) return null;
208
+ function revalidate() {
209
+ var current = resolveProjectSession(source);
210
+ if (!current || current.principal.projectSlug !== initial.principal.projectSlug) {
211
+ throw new Error("This Project Logs binding is no longer valid for the current session.");
212
+ }
213
+ return current;
214
+ }
215
+ return createProjectBound(initial, revalidate);
216
+ }
217
+
218
+ function bindUser(source) {
219
+ if (!source || !source.projectSlug) return null;
220
+ var project = projectFor(source.projectSlug);
221
+ if (!project) return null;
222
+ var status = project.getStatus();
223
+ if (!status || status.isMate === true) return null;
224
+ var user = source.user || null;
225
+
226
+ var principal;
227
+ if (isMultiUser()) {
228
+ if (!user || !user.id) return null;
229
+ principal = { userId: user.id, singleUserOwnerId: null };
230
+ } else {
231
+ principal = { userId: null, singleUserOwnerId: (user && user.id) || status.projectOwnerId || null };
232
+ }
233
+ if (!authorizedProject(principal, status)) return null;
234
+
235
+ principal.kind = "user";
236
+ principal.projectSlug = source.projectSlug;
237
+ principal.isClay = false;
238
+ principal.isProjectOwner = ownsProject(principal, status);
239
+ var envelope = { principal: principal, status: status, author: userAuthor(user) };
240
+ return createProjectBound(envelope, function () { return envelope; });
241
+ }
242
+
243
+ // Ordinary Mates get null. Only the authoritative builtin Clay host agent,
244
+ // confirmed against the server Mate registry rather than a project flag,
245
+ // receives a read-only cross-project binding.
246
+ function resolveMateSource(source) {
247
+ if (!source || source.isMate !== true || !source.mateId || typeof resolveMate !== "function") return null;
248
+ var sourceProject = projectFor(source.projectSlug);
249
+ if (!sourceProject) return null;
250
+ var sourceStatus = sourceProject.getStatus();
251
+ if (!sourceStatus || sourceStatus.isMate !== true) return null;
252
+ if (sourceStatus.mateId !== source.mateId || sourceStatus.projectOwnerId !== source.projectOwnerId) return null;
253
+ if (source.session) {
254
+ var manager = sourceProject.getSessionManager ? sourceProject.getSessionManager() : sourceProject.sm;
255
+ if (!manager || !manager.sessions || manager.sessions.get(source.session.localId) !== source.session) return null;
256
+ }
257
+ var mate = resolveMate(source.projectOwnerId || null, source.mateId);
258
+ if (!mate || mate.id !== source.mateId) return null;
259
+ if (isMultiUser() && (!source.projectOwnerId || mate.createdBy !== source.projectOwnerId)) return null;
260
+
261
+ var def = mate.builtinKey ? builtinMates.getBuiltinByKey(mate.builtinKey) : null;
262
+ var isClay = !!(mate.builtinKey === "clay" && def && def.hostAgent === true);
263
+ if (!isClay) return null;
264
+ if (isMultiUser() && !source.projectOwnerId) return null;
265
+
266
+ return {
267
+ kind: "clay",
268
+ userId: isMultiUser() ? source.projectOwnerId : null,
269
+ singleUserOwnerId: isMultiUser() ? null : (source.projectOwnerId || null),
270
+ projectSlug: null,
271
+ mateId: mate.id,
272
+ isClay: true,
273
+ };
274
+ }
275
+
276
+ // Clay keeps its authoritative registry and user authorization checks on
277
+ // every call, so a Mate that is deleted, demoted out of builtin Clay, or
278
+ // reassigned to another user loses the read view immediately.
279
+ function bindMate(source) {
280
+ var initial = resolveMateSource(source);
281
+ if (!initial) return null;
282
+ function revalidate() {
283
+ var current = resolveMateSource(source);
284
+ if (!current || current.mateId !== initial.mateId || current.userId !== initial.userId) {
285
+ throw new Error("This Project Logs binding is no longer valid for the current session.");
286
+ }
287
+ return current;
288
+ }
289
+ return createClayBound(revalidate);
290
+ }
291
+
292
+ // --- Bound capability surfaces ---------------------------------------
293
+
294
+ function createProjectBound(initial, revalidate) {
295
+ var store = storeForStatus(initial.status);
296
+
297
+ // Identity, authorization, and the target store are all re-derived per
298
+ // call. revalidate() re-checks exact session identity where there is one;
299
+ // the project authorization check then catches a visibility change.
300
+ function live() {
301
+ var current = revalidate();
302
+ var project = projectFor(current.principal.projectSlug);
303
+ if (!project) throw new Error("Project not found.");
304
+ var currentStatus = project.getStatus();
305
+ if (!authorizedProject(current.principal, currentStatus)) throw new Error("Project Logs are not available for this project.");
306
+ return storeForStatus(currentStatus);
307
+ }
308
+
309
+ function author() {
310
+ return revalidate().author;
311
+ }
312
+
313
+ function currentStatus() {
314
+ var current = revalidate();
315
+ var project = projectFor(current.principal.projectSlug);
316
+ if (!project) throw new Error("Project not found.");
317
+ return project.getStatus();
318
+ }
319
+
320
+ function mutationContext() {
321
+ return recordContext(currentStatus());
322
+ }
323
+
324
+ // A session may write canonically; a user may only comment. The capability
325
+ // is decided by the binder that produced this envelope, never by a flag on
326
+ // the request.
327
+ var canonical = initial.principal.kind === "session";
328
+ var canDelete = canonical || initial.principal.isProjectOwner === true;
329
+ return {
330
+ isClay: false,
331
+ canWrite: canonical,
332
+ canComment: true,
333
+ canDelete: canDelete,
334
+ projectSlug: initial.principal.projectSlug,
335
+ scopeId: store.scopeId,
336
+ root: store.root,
337
+ author: initial.author,
338
+ listLogs: function (args) {
339
+ var status = currentStatus();
340
+ var options = scopedArgs(args, status);
341
+ var result = live().list(options);
342
+ result.contextMode = options.contextMode;
343
+ result.currentChangeSetId = status.changeSetId || null;
344
+ result.isWorktree = status.isWorktree === true;
345
+ return result;
346
+ },
347
+ searchLogs: function (args) {
348
+ var status = currentStatus();
349
+ var options = scopedArgs(args, status);
350
+ var result = live().search(options);
351
+ result.contextMode = options.contextMode;
352
+ result.currentChangeSetId = status.changeSetId || null;
353
+ result.isWorktree = status.isWorktree === true;
354
+ return result;
355
+ },
356
+ readLog: function (args) {
357
+ var entry = live().read(args && args.ref, false);
358
+ if (!entry) throw new Error("Log entry not found.");
359
+ return entry;
360
+ },
361
+ logHistory: function (args) { return live().history(args && args.ref, args || {}); },
362
+ commentLog: function (args) { return live().comment(args && args.ref, args || {}, author(), mutationContext()); },
363
+ readLogRevision: function (args) { return live().readRevision(args && args.ref, args && args.revision); },
364
+ listLogFeedback: canonical ? function (args) { return live().feedback(args || {}); } : driverOnly,
365
+ reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author(), mutationContext()); } : driverOnly,
366
+ revertLog: canonical
367
+ ? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author(), mutationContext()); }
368
+ : driverOnly,
369
+ createLog: canonical ? function (args) { return live().create(args || {}, author(), mutationContext()); } : agentOnly,
370
+ updateLog: canonical ? function (args) { return live().update(args && args.ref, args || {}, author(), mutationContext()); } : agentOnly,
371
+ linkLog: canonical ? function (args) { return live().link(args && args.ref, args && args.links, author(), mutationContext()); } : agentOnly,
372
+ removeLog: canDelete ? function (args) {
373
+ var current = revalidate();
374
+ var project = projectFor(current.principal.projectSlug);
375
+ var currentStatus = project && project.getStatus();
376
+ if (!project || !authorizedProject(current.principal, currentStatus)) {
377
+ throw new Error("Project Logs are not available for this project.");
378
+ }
379
+ if (current.principal.kind !== "session" && !ownsProject(current.principal, currentStatus)) ownerOnly();
380
+ return storeForStatus(currentStatus).remove(args && args.ref, current.author, recordContext(currentStatus));
381
+ } : ownerOnly,
382
+ };
383
+ }
384
+
385
+ function setContextState(projectSlug, status) {
386
+ var project = projectFor(projectSlug);
387
+ var projectStatus = project && project.getStatus();
388
+ if (!projectStatus || !projectStatus.isWorktree || !projectStatus.changeSetId) return false;
389
+ return storeForStatus(projectStatus).setContextState(recordContext(projectStatus), status);
390
+ }
391
+
392
+ function createClayBound(revalidate) {
393
+ function target(args) {
394
+ var principal = revalidate();
395
+ var slug = args && args.projectSlug;
396
+ if (!slug) throw new Error("An exact project slug is required.");
397
+ return storeForStatus(statusFor(principal, slug));
398
+ }
399
+
400
+ function resolveLogNavigation(args) {
401
+ var principal = revalidate();
402
+ var ref = args && args.ref;
403
+ if (!LOG_REF_PATTERN.test(ref || "")) throw new Error("Invalid log reference.");
404
+ var projects = getProjects();
405
+ var seenScopes = new Set();
406
+ var match = null;
407
+ if (!projects || typeof projects.forEach !== "function") throw new Error("Project Logs are unavailable.");
408
+ projects.forEach(function (project, slug) {
409
+ if (!project || typeof project.getStatus !== "function") return;
410
+ var status = project.getStatus();
411
+ if (!authorizedProject(principal, status)) return;
412
+ var governing = effectiveStatus(status);
413
+ var scope = governing && (governing.projectKnowledgeId || governing.path);
414
+ if (!scope || seenScopes.has(scope)) return;
415
+ seenScopes.add(scope);
416
+ var entry = null;
417
+ try { entry = storeForStatus(status).read(ref, false); } catch (e) { return; }
418
+ if (!entry) return;
419
+ var targetSlug = status.isWorktree && status.parentSlug && projectFor(status.parentSlug) ? status.parentSlug : slug;
420
+ if (match && match.projectSlug !== targetSlug) throw new Error("Log reference is ambiguous.");
421
+ match = { projectSlug: targetSlug, ref: ref };
422
+ });
423
+ if (!match) throw new Error("Log entry not found.");
424
+ return match;
425
+ }
426
+
427
+ return {
428
+ isClay: true,
429
+ canWrite: false,
430
+ canComment: false,
431
+ canDelete: false,
432
+ projectSlug: null,
433
+ author: null,
434
+ listLogs: function (args) { return target(args).list(args || {}); },
435
+ searchLogs: function (args) { return target(args).search(args || {}); },
436
+ readLog: function (args) {
437
+ var entry = target(args).read(args && args.ref, false);
438
+ if (!entry) throw new Error("Log entry not found.");
439
+ return entry;
440
+ },
441
+ logHistory: function (args) { return target(args).history(args && args.ref, args || {}); },
442
+ commentLog: readOnly,
443
+ readLogRevision: function (args) { return target(args).readRevision(args && args.ref, args && args.revision); },
444
+ resolveLogNavigation: resolveLogNavigation,
445
+ listLogFeedback: readOnly,
446
+ reviewLogComment: readOnly,
447
+ revertLog: readOnly,
448
+ createLog: readOnly,
449
+ updateLog: readOnly,
450
+ linkLog: readOnly,
451
+ removeLog: readOnly,
452
+ };
453
+ }
454
+
455
+ return {
456
+ bindProjectSession: bindProjectSession,
457
+ bindUser: bindUser,
458
+ bindMate: bindMate,
459
+ setContextState: setContextState,
460
+ };
461
+ }
462
+
463
+ module.exports = { attachProjectLogsService: attachProjectLogsService };
@@ -0,0 +1,205 @@
1
+ // Canonical snapshots and revision reconstruction for Project Logs.
2
+ //
3
+ // Split from project-logs-store.js so the store stays inside the module size
4
+ // limit and so version control has one place to live.
5
+ //
6
+ // Every NEW canonical edit stores a complete immutable snapshot of the record:
7
+ // category, priority, title, summary, body, tags, links. That makes a revision
8
+ // self-contained, so reading revision N never depends on replaying everything
9
+ // before it.
10
+ //
11
+ // Records written before snapshots existed carry only the fields that changed.
12
+ // They are never rewritten. Instead, reconstruction folds forward: a record
13
+ // with a `snapshot` replaces the state outright, and a legacy partial record
14
+ // applies its deltas onto the state built so far. Both paths produce the same
15
+ // shape, so history is deterministic across the boundary.
16
+
17
+ var logsSchema = require("./project-logs-schema");
18
+ var logContext = require("./project-log-context");
19
+
20
+ // Ops that advance the canonical record. `comment` and a non-incorporating
21
+ // `review` are participation, not revisions, and never appear here.
22
+ var CANONICAL_OPS = ["create", "update", "link", "revert"];
23
+
24
+ var SNAPSHOT_FIELDS = ["category", "priority", "title", "summary", "body", "tags", "links"];
25
+
26
+ function emptySnapshot() {
27
+ return {
28
+ category: "",
29
+ priority: logsSchema.DEFAULT_PRIORITY,
30
+ title: "",
31
+ summary: "",
32
+ body: "",
33
+ tags: [],
34
+ links: [],
35
+ };
36
+ }
37
+
38
+ function cloneSnapshot(snapshot) {
39
+ var source = snapshot || {};
40
+ return {
41
+ category: source.category || "",
42
+ priority: logsSchema.normalizePriority(source.priority),
43
+ title: typeof source.title === "string" ? source.title : "",
44
+ summary: typeof source.summary === "string" ? source.summary : "",
45
+ body: typeof source.body === "string" ? source.body : "",
46
+ tags: Array.isArray(source.tags) ? source.tags.slice() : [],
47
+ links: Array.isArray(source.links) ? source.links.slice() : [],
48
+ };
49
+ }
50
+
51
+ // True when this record carries a complete canonical snapshot.
52
+ function hasSnapshot(record) {
53
+ return !!(record && record.snapshot && typeof record.snapshot === "object");
54
+ }
55
+
56
+ // A canonical op, including an incorporating review. A review that clarifies or
57
+ // declines resolves a comment without changing the record.
58
+ function isCanonical(record) {
59
+ if (!record) return false;
60
+ if (record.op === "review") return record.action === "incorporate";
61
+ return CANONICAL_OPS.indexOf(record.op) !== -1;
62
+ }
63
+
64
+ // Apply a legacy partial record onto the running state. Only used for records
65
+ // written before snapshots; a snapshot-bearing record never takes this path.
66
+ function applyPartial(state, record, maxLinks) {
67
+ var next = cloneSnapshot(state);
68
+ if (record.op === "create") {
69
+ next.category = record.kind || "";
70
+ next.priority = logsSchema.normalizePriority(record.priority);
71
+ next.title = typeof record.title === "string" ? record.title : "";
72
+ next.summary = typeof record.summary === "string" ? record.summary : "";
73
+ next.body = typeof record.body === "string" ? record.body : "";
74
+ next.tags = Array.isArray(record.tags) ? record.tags.slice() : [];
75
+ next.links = Array.isArray(record.links) ? record.links.slice() : [];
76
+ return next;
77
+ }
78
+ if (record.op === "link") {
79
+ var added = Array.isArray(record.links) ? record.links : [];
80
+ for (var i = 0; i < added.length && next.links.length < maxLinks; i++) {
81
+ var exists = false;
82
+ for (var j = 0; j < next.links.length; j++) {
83
+ if (next.links[j].ref === added[i].ref) { exists = true; break; }
84
+ }
85
+ if (!exists) next.links.push(added[i]);
86
+ }
87
+ return next;
88
+ }
89
+ if (record.kind) next.category = record.kind;
90
+ if (record.priority) next.priority = logsSchema.normalizePriority(record.priority);
91
+ if (typeof record.title === "string") next.title = record.title;
92
+ if (typeof record.summary === "string") next.summary = record.summary;
93
+ if (typeof record.body === "string") next.body = record.body;
94
+ if (Array.isArray(record.tags)) next.tags = record.tags.slice();
95
+ if (Array.isArray(record.links)) next.links = record.links.slice();
96
+ return next;
97
+ }
98
+
99
+ function sameSnapshot(a, b) {
100
+ var left = cloneSnapshot(a);
101
+ var right = cloneSnapshot(b);
102
+ for (var i = 0; i < SNAPSHOT_FIELDS.length; i++) {
103
+ var field = SNAPSHOT_FIELDS[i];
104
+ if (JSON.stringify(left[field]) !== JSON.stringify(right[field])) return false;
105
+ }
106
+ return true;
107
+ }
108
+
109
+ function changedFields(previous, next) {
110
+ var changed = [];
111
+ if (!previous) return SNAPSHOT_FIELDS.slice();
112
+ for (var i = 0; i < SNAPSHOT_FIELDS.length; i++) {
113
+ var field = SNAPSHOT_FIELDS[i];
114
+ if (JSON.stringify(previous[field]) !== JSON.stringify(next[field])) changed.push(field);
115
+ }
116
+ return changed;
117
+ }
118
+
119
+ // Every canonical revision of one entry, in causal order, each with the exact
120
+ // snapshot as of that revision. Deleting is a tombstone rather than a
121
+ // revision, so it is reported separately.
122
+ function revisions(chain, maxLinks) {
123
+ var out = [];
124
+ var state = emptySnapshot();
125
+ var deleted = null;
126
+ var started = false;
127
+
128
+ for (var i = 0; i < chain.length; i++) {
129
+ var record = chain[i];
130
+ if (record.op === "delete") {
131
+ if (started && !deleted) deleted = { at: record.at || 0, author: record.author || null };
132
+ continue;
133
+ }
134
+ if (deleted) continue;
135
+ if (!isCanonical(record)) continue;
136
+ if (record.op === "create") {
137
+ if (started) continue;
138
+ started = true;
139
+ } else if (!started) {
140
+ continue;
141
+ }
142
+
143
+ var previous = out.length ? out[out.length - 1].snapshot : null;
144
+ state = hasSnapshot(record) ? cloneSnapshot(record.snapshot) : applyPartial(state, record, maxLinks);
145
+ out.push({
146
+ revision: out.length + 1,
147
+ op: record.op === "review" ? "incorporate" : record.op,
148
+ at: record.at || 0,
149
+ author: record.author || null,
150
+ context: record.context || null,
151
+ snapshot: state,
152
+ changed: record.op === "create" ? [] : changedFields(previous, state),
153
+ // Present only on a revert, so a reader can see where it came from.
154
+ revertedFrom: record.revertedFrom || null,
155
+ reason: typeof record.reason === "string" ? record.reason : null,
156
+ // Present only on an incorporation, linking the revision to the comment
157
+ // that prompted it.
158
+ commentId: record.op === "review" ? (record.commentId || null) : null,
159
+ // Whether this revision was reconstructed from a legacy partial record.
160
+ reconstructed: !hasSnapshot(record),
161
+ // The record that produced this revision. Internal: it exists so a
162
+ // review can be linked to its exact revision by identity rather than by
163
+ // matching timestamps, and describeRevision deliberately drops it so no
164
+ // internal record id reaches a public response.
165
+ sourceRecordId: record.id,
166
+ });
167
+ }
168
+ return { revisions: out, deleted: deleted, started: started };
169
+ }
170
+
171
+ // Revision metadata without any body, for history listings and for the bounded
172
+ // timeline the detail view shows. `sourceRecordId` is intentionally not copied:
173
+ // the only identifiers a caller ever sees are the opaque entry ref and comment
174
+ // id.
175
+ function describeRevision(entry) {
176
+ return {
177
+ revision: entry.revision,
178
+ op: entry.op,
179
+ at: entry.at,
180
+ author: entry.author,
181
+ context: logContext.normalizeRecordContext(entry.context),
182
+ changed: entry.changed,
183
+ revertedFrom: entry.revertedFrom,
184
+ reason: entry.reason,
185
+ commentId: entry.commentId,
186
+ reconstructed: entry.reconstructed,
187
+ title: entry.snapshot.title,
188
+ category: entry.snapshot.category,
189
+ priority: entry.snapshot.priority,
190
+ };
191
+ }
192
+
193
+ module.exports = {
194
+ CANONICAL_OPS: CANONICAL_OPS,
195
+ SNAPSHOT_FIELDS: SNAPSHOT_FIELDS,
196
+ emptySnapshot: emptySnapshot,
197
+ cloneSnapshot: cloneSnapshot,
198
+ hasSnapshot: hasSnapshot,
199
+ isCanonical: isCanonical,
200
+ applyPartial: applyPartial,
201
+ sameSnapshot: sameSnapshot,
202
+ changedFields: changedFields,
203
+ revisions: revisions,
204
+ describeRevision: describeRevision,
205
+ };