clay-server 4.0.0-beta.2 → 4.0.0-beta.21

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 (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -0,0 +1,495 @@
1
+ // Project Logs projection over the Clay-wide knowledge record backend.
2
+
3
+ var fs = require("fs");
4
+ var path = require("path");
5
+ var crypto = require("crypto");
6
+ var recordStore = require("./knowledge-record-store");
7
+ var logsQuery = require("./project-logs-query");
8
+ var logsSchema = require("./project-logs-schema");
9
+ var logsSnapshot = require("./project-logs-snapshot");
10
+ var logsComments = require("./project-logs-comments");
11
+ var logsVersioning = require("./project-logs-versioning");
12
+ var logsRoot = require("./project-logs-root");
13
+ var logContext = require("./project-log-context");
14
+ var contextState = require("./project-logs-context-state");
15
+
16
+ var MAX_SUMMARY_CHARS = logsSchema.MAX_SUMMARY_CHARS;
17
+ var MAX_COMMENT_CHARS = 4000;
18
+ var MAX_COMMENTS = 200;
19
+ var MAX_FEEDBACK_ITEMS = 25;
20
+ var MAX_FEEDBACK_BODY_CHARS = 600;
21
+ var MAX_TITLE_CHARS = 200;
22
+ var MAX_BODY_CHARS = 20000;
23
+ var MAX_TAGS = 12;
24
+ var MAX_TAG_CHARS = 40;
25
+ var MAX_LINKS = 20;
26
+ var MAX_LINK_CHARS = 200;
27
+ var MAX_PAGE = logsQuery.MAX_PAGE;
28
+ var REF_PATTERN = /^log:[A-Za-z0-9_-]{24}$/;
29
+
30
+ var cleanText = logsSchema.cleanText;
31
+ var cleanLine = logsSchema.cleanLine;
32
+ var normalizeAuthor = logsSchema.normalizeAuthor;
33
+ var normalizeTags = logsSchema.normalizeTags;
34
+ var normalizeLinks = logsSchema.normalizeLinks;
35
+
36
+ var page = logsQuery.page;
37
+
38
+ // Filters accept any well-formed category. An unknown but valid one matches
39
+ // nothing, which is a real answer. A supplied malformed one is an error:
40
+ // silently dropping it would widen the result set to everything the caller is
41
+ // authorized to see, which is the opposite of failing closed, and it would give
42
+ // an agent no signal that its filter was wrong.
43
+ function validateKind(kind, fallback) {
44
+ if (kind === undefined || kind === null || kind === "") return fallback || null;
45
+ return logsSchema.normalizeCategory(kind);
46
+ }
47
+
48
+
49
+ function createProjectLogsStore(opts) {
50
+ var options = opts || {};
51
+ var root = options.root ? path.resolve(options.root) : logsRoot.resolveProjectRoot(options.cwd, options.runGit);
52
+ var scopeId = options.projectKnowledgeId
53
+ ? logsRoot.scopeIdForKnowledgeId(options.projectKnowledgeId)
54
+ : logsRoot.scopeIdForRoot(root);
55
+ var store = recordStore.createRecordStore({ scopeId: scopeId, baseDir: options.baseDir });
56
+ var contexts = contextState.attachContextState(store, scopeId);
57
+
58
+ function refFor(rootId) {
59
+ var digest = crypto.createHash("sha256").update(scopeId + "\u0000" + String(rootId)).digest("base64url");
60
+ return "log:" + digest.substring(0, 24);
61
+ }
62
+
63
+ function chains() {
64
+ var records = store.all();
65
+ var byRoot = new Map();
66
+ for (var i = 0; i < records.length; i++) {
67
+ var record = records[i];
68
+ var rootId = record.rootId || record.id;
69
+ if (!byRoot.has(rootId)) byRoot.set(rootId, []);
70
+ byRoot.get(rootId).push(record);
71
+ }
72
+ return byRoot;
73
+ }
74
+
75
+ function fold(rootId, chain, knownContextStates) {
76
+ var history = logsSnapshot.revisions(chain, MAX_LINKS);
77
+ if (!history.started || history.revisions.length === 0) return null;
78
+ var last = history.revisions[history.revisions.length - 1];
79
+ var first = history.revisions[0];
80
+ var originRecord = null;
81
+ for (var ci = 0; ci < chain.length; ci++) {
82
+ if (chain[ci].id === first.sourceRecordId) { originRecord = chain[ci]; break; }
83
+ }
84
+
85
+ if (!logsSchema.safeCategory(first.snapshot.category)) return null;
86
+ if (!first.snapshot.title || !first.snapshot.title.trim()) return null;
87
+
88
+ var revisionForRecordId = {};
89
+ for (var v = 0; v < history.revisions.length; v++) {
90
+ var source = history.revisions[v].sourceRecordId;
91
+ if (source) revisionForRecordId[source] = history.revisions[v].revision;
92
+ }
93
+
94
+ var thread = logsComments.comments(chain, {
95
+ maxComments: MAX_COMMENTS,
96
+ revisionForRecordId: revisionForRecordId,
97
+ });
98
+
99
+ var entry = {
100
+ ref: refFor(rootId),
101
+ kind: last.snapshot.category,
102
+ category: last.snapshot.category,
103
+ priority: last.snapshot.priority,
104
+ title: last.snapshot.title,
105
+ summary: last.snapshot.summary,
106
+ body: last.snapshot.body,
107
+ tags: last.snapshot.tags.slice(),
108
+ links: last.snapshot.links.slice(),
109
+ comments: thread,
110
+ createdAt: first.at,
111
+ createdBy: first.author,
112
+ updatedAt: last.at,
113
+ updatedBy: last.author,
114
+ revisions: history.revisions.length,
115
+ deleted: !!history.deleted,
116
+ deletedAt: history.deleted ? history.deleted.at : null,
117
+ deletedBy: history.deleted ? history.deleted.author : null,
118
+ context: logContext.normalizeRecordContext(originRecord && originRecord.context),
119
+ };
120
+ if (entry.context.changeSetId) {
121
+ var states = knownContextStates || contexts.states();
122
+ entry.context.status = states[entry.context.changeSetId] || entry.context.status;
123
+ }
124
+ // Entries written before summaries existed get a deterministic one derived
125
+ // from their own body. Nothing on disk is rewritten.
126
+ entry.summary = logsSchema.fallbackSummary(entry);
127
+ entry.commentCount = thread.length;
128
+ entry.pendingFeedbackCount = logsComments.pendingReviewCount(thread);
129
+ // Bounded revision metadata, never bodies, so the detail view can show a
130
+ // history timeline without a second round trip.
131
+ entry.history = history.revisions.map(logsSnapshot.describeRevision);
132
+ return entry;
133
+ }
134
+
135
+ function entries(includeDeleted) {
136
+ var out = [];
137
+ var states = contexts.states();
138
+ chains().forEach(function (chain, rootId) {
139
+ var entry = fold(rootId, chain, states);
140
+ if (!entry) return;
141
+ if (entry.deleted && !includeDeleted) return;
142
+ out.push(entry);
143
+ });
144
+ // Latest canonical update first, ref as a deterministic tie-break. Only a
145
+ // revision moves updatedAt, so comments and clarify/decline never promote.
146
+ out.sort(function (a, b) { return b.updatedAt - a.updatedAt || logsQuery.compareRefs(a.ref, b.ref); });
147
+ return out;
148
+ }
149
+
150
+ function findChain(ref) {
151
+ if (typeof ref !== "string" || !REF_PATTERN.test(ref)) return null;
152
+ var found = null;
153
+ chains().forEach(function (chain, rootId) {
154
+ if (found) return;
155
+ if (refFor(rootId) === ref) found = { rootId: rootId, chain: chain };
156
+ });
157
+ return found;
158
+ }
159
+
160
+ function read(ref, includeDeleted) {
161
+ var located = findChain(ref);
162
+ if (!located) return null;
163
+ var entry = fold(located.rootId, located.chain);
164
+ if (!entry) return null;
165
+ if (entry.deleted && !includeDeleted) return null;
166
+ return entry;
167
+ }
168
+
169
+ function requireLive(ref) {
170
+ var located = findChain(ref);
171
+ if (!located) throw new Error("Log entry not found.");
172
+ var entry = fold(located.rootId, located.chain);
173
+ if (!entry || entry.deleted) throw new Error("Log entry not found.");
174
+ return located;
175
+ }
176
+
177
+ function create(input, author, context) {
178
+ var data = input || {};
179
+ var validated = logsSchema.validateNewRecord(data);
180
+ var title = cleanLine(data.title || "", MAX_TITLE_CHARS);
181
+ if (!title) throw new Error("A log title is required.");
182
+ var summary = cleanLine(data.summary || "", MAX_SUMMARY_CHARS);
183
+ if (!summary) throw new Error("A one or two sentence summary is required so the entry is understandable from a list.");
184
+ var id = recordStore.newRecordId();
185
+ var snapshot = logsSnapshot.cloneSnapshot({
186
+ category: validated.category,
187
+ priority: validated.priority,
188
+ title: title,
189
+ summary: summary,
190
+ body: cleanText(data.body || "", MAX_BODY_CHARS),
191
+ tags: normalizeTags(data.tags),
192
+ links: normalizeLinks(data.links),
193
+ });
194
+ store.append({
195
+ id: id,
196
+ rootId: id,
197
+ op: "create",
198
+ scope: scopeId,
199
+ // Legacy field names are kept so an older build still reads this record.
200
+ kind: snapshot.category,
201
+ priority: snapshot.priority,
202
+ title: snapshot.title,
203
+ summary: snapshot.summary,
204
+ body: snapshot.body,
205
+ tags: snapshot.tags,
206
+ links: snapshot.links,
207
+ snapshot: snapshot,
208
+ author: normalizeAuthor(author),
209
+ context: logContext.normalizeRecordContext(context),
210
+ at: Date.now(),
211
+ });
212
+ return read(refFor(id), false);
213
+ }
214
+
215
+ // Build the complete snapshot a revision will store, from the current state
216
+ // plus the requested changes. Every new canonical edit is self-contained.
217
+ function nextSnapshot(current, validated, data) {
218
+ var snapshot = logsSnapshot.cloneSnapshot(current);
219
+ if (validated.category !== undefined) snapshot.category = validated.category;
220
+ if (validated.priority !== undefined) snapshot.priority = validated.priority;
221
+ if (validated.title !== undefined) {
222
+ snapshot.title = cleanLine(validated.title, MAX_TITLE_CHARS);
223
+ if (!snapshot.title) throw new Error("A log title cannot be emptied.");
224
+ }
225
+ if (validated.summary !== undefined) {
226
+ snapshot.summary = cleanLine(validated.summary, MAX_SUMMARY_CHARS);
227
+ if (!snapshot.summary) throw new Error("A log summary cannot be emptied.");
228
+ }
229
+ if (data.body !== undefined) snapshot.body = cleanText(data.body || "", MAX_BODY_CHARS);
230
+ if (data.tags !== undefined) snapshot.tags = normalizeTags(data.tags);
231
+ return snapshot;
232
+ }
233
+
234
+ function currentSnapshot(ref) {
235
+ var entry = read(ref, false);
236
+ if (!entry) throw new Error("Log entry not found.");
237
+ return logsSnapshot.cloneSnapshot(entry);
238
+ }
239
+
240
+ function update(ref, changes, author, context) {
241
+ var located = requireLive(ref);
242
+ var data = changes || {};
243
+ var validated = logsSchema.validateRevision(data);
244
+ var current = currentSnapshot(ref);
245
+ var snapshot = nextSnapshot(current, validated, data);
246
+ if (logsSnapshot.sameSnapshot(current, snapshot)) {
247
+ throw new Error("An update requires at least one changed field.");
248
+ }
249
+ appendRevision(located.rootId, "update", snapshot, author, { context: logContext.normalizeRecordContext(context) });
250
+ return read(ref, false);
251
+ }
252
+
253
+ // One append per canonical revision, always carrying a complete snapshot.
254
+ // Legacy field names ride along so an older build can still read the record.
255
+ function appendRevision(rootId, op, snapshot, author, extra) {
256
+ var record = {
257
+ id: recordStore.newRecordId(),
258
+ rootId: rootId,
259
+ op: op,
260
+ scope: scopeId,
261
+ kind: snapshot.category,
262
+ priority: snapshot.priority,
263
+ title: snapshot.title,
264
+ summary: snapshot.summary,
265
+ body: snapshot.body,
266
+ tags: snapshot.tags,
267
+ links: snapshot.links,
268
+ snapshot: snapshot,
269
+ author: normalizeAuthor(author),
270
+ at: Date.now(),
271
+ };
272
+ var keys = Object.keys(extra || {});
273
+ for (var i = 0; i < keys.length; i++) record[keys[i]] = extra[keys[i]];
274
+ store.append(record);
275
+ return record;
276
+ }
277
+
278
+ // Human participation. Append-only for this slice: no edit, no delete, and
279
+ // never a canonical revision. The author is supplied by the authorization
280
+ // layer, never by the caller's payload.
281
+ function comment(ref, input, author, context) {
282
+ var located = requireLive(ref);
283
+ var data = input || {};
284
+ var body = cleanText(data.body || "", MAX_COMMENT_CHARS);
285
+ if (!body) throw new Error("A comment body is required.");
286
+ var existing = read(ref, false);
287
+ if (existing && existing.comments.length >= MAX_COMMENTS) {
288
+ throw new Error("This entry already has the maximum of " + MAX_COMMENTS + " comments.");
289
+ }
290
+ store.append({
291
+ id: recordStore.newRecordId(),
292
+ rootId: located.rootId,
293
+ op: "comment",
294
+ scope: scopeId,
295
+ body: body,
296
+ author: normalizeAuthor(author),
297
+ context: logContext.normalizeRecordContext(context),
298
+ at: Date.now(),
299
+ });
300
+ return read(ref, false);
301
+ }
302
+
303
+ function link(ref, links, author, context) {
304
+ var located = requireLive(ref);
305
+ var normalized = normalizeLinks(links);
306
+ if (normalized.length === 0) throw new Error("At least one link reference is required.");
307
+ var current = currentSnapshot(ref);
308
+ var snapshot = logsSnapshot.cloneSnapshot(current);
309
+ for (var i = 0; i < normalized.length && snapshot.links.length < MAX_LINKS; i++) {
310
+ var exists = false;
311
+ for (var j = 0; j < snapshot.links.length; j++) {
312
+ if (snapshot.links[j].ref === normalized[i].ref) { exists = true; break; }
313
+ }
314
+ if (!exists) snapshot.links.push(normalized[i]);
315
+ }
316
+ if (logsSnapshot.sameSnapshot(current, snapshot)) {
317
+ throw new Error("Those links are already attached to this entry.");
318
+ }
319
+ appendRevision(located.rootId, "link", snapshot, author, { context: logContext.normalizeRecordContext(context) });
320
+ return read(ref, false);
321
+ }
322
+
323
+ function remove(ref, author, context) {
324
+ var located = requireLive(ref);
325
+ store.append({
326
+ id: recordStore.newRecordId(),
327
+ rootId: located.rootId,
328
+ op: "delete",
329
+ scope: scopeId,
330
+ author: normalizeAuthor(author),
331
+ context: logContext.normalizeRecordContext(context),
332
+ at: Date.now(),
333
+ });
334
+ return read(ref, true);
335
+ }
336
+
337
+ // Filtering, ranking, and pagination live in project-logs-query.js so this
338
+ // module stays focused on records. Ranking is the shared BM25 engine.
339
+ // The project's category vocabulary, derived from its own live non-deleted
340
+ // entries. A category that only exists on deleted entries disappears; a
341
+ // revision that changes category updates the vocabulary without rewriting a
342
+ // single historical record. A shared project shares this list because it
343
+ // shares this store.
344
+ function categories() {
345
+ var seen = {};
346
+ var out = [];
347
+ var live = entries(false);
348
+ for (var i = 0; i < live.length; i++) {
349
+ var value = live[i].category;
350
+ if (!value || seen[value]) continue;
351
+ seen[value] = true;
352
+ out.push(value);
353
+ }
354
+ out.sort();
355
+ return out;
356
+ }
357
+
358
+ var versioning = logsVersioning.attachVersioning({
359
+ read: read,
360
+ requireLive: requireLive,
361
+ findChain: findChain,
362
+ appendRevision: appendRevision,
363
+ nextSnapshot: nextSnapshot,
364
+ cleanText: cleanText,
365
+ maxLinks: MAX_LINKS,
366
+ maxResponseChars: MAX_COMMENT_CHARS,
367
+ scopeId: scopeId,
368
+ store: store,
369
+ normalizeAuthor: normalizeAuthor,
370
+ });
371
+
372
+ // Every comment still awaiting Driver review, across every live entry, in
373
+ // one pass over the projection. The exact total is always reported; only the
374
+ // returned summaries are clamped, so a pending comment on an old entry can
375
+ // never be invisible.
376
+ function feedback(args) {
377
+ var options2 = args || {};
378
+ var limit = Number(options2.limit);
379
+ if (!Number.isFinite(limit)) limit = MAX_FEEDBACK_ITEMS;
380
+ limit = Math.max(1, Math.min(MAX_FEEDBACK_ITEMS, Math.floor(limit)));
381
+ var live = entries(false);
382
+ var out = [];
383
+ var total = 0;
384
+ for (var i = 0; i < live.length; i++) {
385
+ var awaiting = logsComments.pendingReviewComments(live[i].comments);
386
+ for (var j = 0; j < awaiting.length; j++) {
387
+ total++;
388
+ if (out.length >= limit) continue;
389
+ var body = awaiting[j].body || "";
390
+ out.push({
391
+ ref: live[i].ref,
392
+ title: live[i].title,
393
+ category: live[i].category,
394
+ priority: live[i].priority,
395
+ commentId: awaiting[j].id,
396
+ status: awaiting[j].status,
397
+ author: awaiting[j].author,
398
+ at: awaiting[j].at,
399
+ body: body.length > MAX_FEEDBACK_BODY_CHARS
400
+ ? body.substring(0, MAX_FEEDBACK_BODY_CHARS) + "..."
401
+ : body,
402
+ });
403
+ }
404
+ }
405
+ return { feedback: out, total: total, truncated: total > out.length };
406
+ }
407
+
408
+ function list(args) {
409
+ var options2 = args || {};
410
+ var result = logsQuery.listEntries(entries(options2.includeDeleted === true), options2, cleanLine, validateKind);
411
+ result.categories = categories();
412
+ return result;
413
+ }
414
+
415
+ function search(args) {
416
+ var result = logsQuery.searchEntries(entries(false), args || {}, cleanLine, validateKind);
417
+ result.categories = categories();
418
+ return result;
419
+ }
420
+
421
+ // The blame view: every canonical revision in causal order with its author
422
+ // and the fields it changed, reconstructed from the append-only chain.
423
+ // Metadata only: no bodies, so history never dumps the record.
424
+ function history(ref, args) {
425
+ var located = findChain(ref);
426
+ if (!located) throw new Error("Log entry not found.");
427
+ var reconstructed = logsSnapshot.revisions(located.chain, MAX_LINKS);
428
+ var revisions = reconstructed.revisions.map(logsSnapshot.describeRevision);
429
+ if (reconstructed.deleted) {
430
+ revisions.push({
431
+ revision: revisions.length + 1,
432
+ op: "delete",
433
+ at: reconstructed.deleted.at,
434
+ author: reconstructed.deleted.author,
435
+ changed: [],
436
+ revertedFrom: null,
437
+ reason: null,
438
+ commentId: null,
439
+ reconstructed: false,
440
+ });
441
+ }
442
+ var result = page(revisions, args || {});
443
+ return { ref: ref, revisions: result.items, nextCursor: result.nextCursor, total: result.total };
444
+ }
445
+
446
+ function stats() {
447
+ var base = store.stats();
448
+ return {
449
+ root: root,
450
+ scopeId: scopeId,
451
+ filePath: base.filePath,
452
+ records: base.records,
453
+ skippedRecords: base.skipped,
454
+ entries: entries(false).length,
455
+ };
456
+ }
457
+
458
+ return {
459
+ root: root,
460
+ scopeId: scopeId,
461
+ filePath: store.filePath,
462
+ categories: categories,
463
+ feedback: feedback,
464
+ create: create,
465
+ update: update,
466
+ comment: comment,
467
+ review: versioning.review,
468
+ revert: versioning.revert,
469
+ readRevision: versioning.readRevision,
470
+ link: link,
471
+ remove: remove,
472
+ setContextState: contexts.set,
473
+ read: read,
474
+ list: list,
475
+ search: search,
476
+ history: history,
477
+ stats: stats,
478
+ };
479
+ }
480
+
481
+ module.exports = {
482
+ SEED_CATEGORIES: logsSchema.SEED_CATEGORIES,
483
+ MAX_CATEGORY_CHARS: logsSchema.MAX_CATEGORY_CHARS,
484
+ PRIORITIES: logsSchema.PRIORITIES,
485
+ MAX_SUMMARY_CHARS: MAX_SUMMARY_CHARS,
486
+ MAX_COMMENT_CHARS: MAX_COMMENT_CHARS,
487
+ MAX_COMMENTS: MAX_COMMENTS,
488
+ MAX_TITLE_CHARS: MAX_TITLE_CHARS,
489
+ MAX_BODY_CHARS: MAX_BODY_CHARS,
490
+ MAX_PAGE: MAX_PAGE,
491
+ REF_PATTERN: REF_PATTERN,
492
+ resolveProjectRoot: logsRoot.resolveProjectRoot,
493
+ scopeIdForRoot: logsRoot.scopeIdForRoot,
494
+ createProjectLogsStore: createProjectLogsStore,
495
+ };
@@ -0,0 +1,132 @@
1
+ // Comment review and revision control for Project Logs.
2
+ //
3
+ // Split from project-logs-store.js so the store stays inside the module size
4
+ // limit. Everything here is Project Driver territory: reviewing a user comment
5
+ // and moving the canonical record between revisions. The authorization layer
6
+ // decides who may call these; this module only guarantees the record shape.
7
+
8
+ var logsSchema = require("./project-logs-schema");
9
+ var logsSnapshot = require("./project-logs-snapshot");
10
+ var logsComments = require("./project-logs-comments");
11
+ var recordStore = require("./knowledge-record-store");
12
+ var logContext = require("./project-log-context");
13
+
14
+ function attachVersioning(ctx) {
15
+ var read = ctx.read;
16
+ var requireLive = ctx.requireLive;
17
+ var findChain = ctx.findChain;
18
+ var appendRevision = ctx.appendRevision;
19
+ var nextSnapshot = ctx.nextSnapshot;
20
+ var cleanText = ctx.cleanText;
21
+ var MAX_LINKS = ctx.maxLinks;
22
+ var MAX_RESPONSE_CHARS = ctx.maxResponseChars;
23
+ var scopeId = ctx.scopeId;
24
+ var store = ctx.store;
25
+ var normalizeAuthor = ctx.normalizeAuthor;
26
+
27
+ // Review a user comment. `clarify` and `decline` resolve the comment and
28
+ // create no canonical revision. `incorporate` appends exactly one record that
29
+ // both resolves the comment and carries the new canonical snapshot, so the
30
+ // two can never drift apart.
31
+ function review(ref, input, author, context) {
32
+ var located = requireLive(ref);
33
+ var data = input || {};
34
+ if (!logsComments.isAction(data.action)) {
35
+ throw new Error("A review action is required: " + logsComments.ACTIONS.join(", "));
36
+ }
37
+ var entry = read(ref, false);
38
+ var target = null;
39
+ for (var i = 0; i < entry.comments.length; i++) {
40
+ if (entry.comments[i].id === data.commentId) { target = entry.comments[i]; break; }
41
+ }
42
+ if (!target) throw new Error("Comment not found on this entry.");
43
+ if (target.review) throw new Error("That comment has already been reviewed.");
44
+
45
+ var response = cleanText(data.response || "", MAX_RESPONSE_CHARS);
46
+ if (data.action !== "incorporate" && !response) {
47
+ throw new Error("A " + data.action + " review requires a response explaining the decision.");
48
+ }
49
+
50
+ if (data.action !== "incorporate") {
51
+ store.append({
52
+ id: recordStore.newRecordId(),
53
+ rootId: located.rootId,
54
+ op: "review",
55
+ scope: scopeId,
56
+ action: data.action,
57
+ commentId: data.commentId,
58
+ response: response,
59
+ author: normalizeAuthor(author),
60
+ context: logContext.normalizeRecordContext(context),
61
+ at: Date.now(),
62
+ });
63
+ return read(ref, false);
64
+ }
65
+
66
+ var validated = logsSchema.validateRevision(data);
67
+ var current = logsSnapshot.cloneSnapshot(entry);
68
+ var snapshot = nextSnapshot(current, validated, data);
69
+ if (logsSnapshot.sameSnapshot(current, snapshot)) {
70
+ throw new Error("Incorporating a comment requires an actual canonical change.");
71
+ }
72
+ appendRevision(located.rootId, "review", snapshot, author, {
73
+ action: "incorporate",
74
+ commentId: data.commentId,
75
+ response: response,
76
+ context: logContext.normalizeRecordContext(context),
77
+ });
78
+ return read(ref, false);
79
+ }
80
+
81
+ // Restore an earlier revision by appending a new one. Later history is never
82
+ // erased: a revert is itself a revision, with the source recorded.
83
+ function revert(ref, revision, reason, author, context) {
84
+ var located = requireLive(ref);
85
+ var target = Number(revision);
86
+ if (!Number.isInteger(target) || target < 1) throw new Error("A revision number is required.");
87
+ var trimmedReason = cleanText(reason || "", MAX_RESPONSE_CHARS);
88
+ if (!trimmedReason) throw new Error("A revert requires a reason.");
89
+
90
+ var history = logsSnapshot.revisions(located.chain, MAX_LINKS);
91
+ if (target > history.revisions.length) {
92
+ throw new Error("This entry has only " + history.revisions.length + " revisions.");
93
+ }
94
+ var source = history.revisions[target - 1];
95
+ var current = history.revisions[history.revisions.length - 1];
96
+ if (logsSnapshot.sameSnapshot(current.snapshot, source.snapshot)) {
97
+ throw new Error("Revision " + target + " is identical to the current one.");
98
+ }
99
+ appendRevision(located.rootId, "revert", logsSnapshot.cloneSnapshot(source.snapshot), author, {
100
+ revertedFrom: target,
101
+ reason: trimmedReason,
102
+ context: logContext.normalizeRecordContext(context),
103
+ });
104
+ return read(ref, false);
105
+ }
106
+
107
+ // The exact reconstructed snapshot as of one revision.
108
+ function readRevision(ref, revision) {
109
+ var located = findChain(ref);
110
+ if (!located) throw new Error("Log entry not found.");
111
+ var target = Number(revision);
112
+ if (!Number.isInteger(target) || target < 1) throw new Error("A revision number is required.");
113
+ var history = logsSnapshot.revisions(located.chain, MAX_LINKS);
114
+ if (target > history.revisions.length) {
115
+ throw new Error("This entry has only " + history.revisions.length + " revisions.");
116
+ }
117
+ var found = history.revisions[target - 1];
118
+ var described = logsSnapshot.describeRevision(found);
119
+ described.ref = ref;
120
+ described.snapshot = found.snapshot;
121
+ return described;
122
+ }
123
+
124
+
125
+ return {
126
+ review: review,
127
+ revert: revert,
128
+ readRevision: readRevision,
129
+ };
130
+ }
131
+
132
+ module.exports = { attachVersioning: attachVersioning };