clay-server 3.8.1 → 4.0.0-beta.10

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 (222) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/background-task-timing.js +105 -0
  3. package/lib/builtin-mates.js +6 -6
  4. package/lib/claude-hook-installer.js +15 -0
  5. package/lib/clay-history-mcp-server.js +62 -297
  6. package/lib/daemon.js +23 -1
  7. package/lib/debate-model-selection.js +119 -0
  8. package/lib/home-debate-tool-policy.js +164 -0
  9. package/lib/home-response-notifications.js +26 -0
  10. package/lib/knowledge-import.js +436 -0
  11. package/lib/knowledge-record-store.js +188 -0
  12. package/lib/knowledge-search.js +224 -0
  13. package/lib/mate-creation-mcp-server.js +55 -0
  14. package/lib/mate-knowledge-mcp-server.js +151 -0
  15. package/lib/mate-knowledge-migration.js +283 -0
  16. package/lib/mate-knowledge-service.js +488 -0
  17. package/lib/mate-knowledge-sync.js +482 -0
  18. package/lib/mate-ready-creation.js +95 -0
  19. package/lib/mates.js +2 -0
  20. package/lib/notes.js +2 -1
  21. package/lib/project-ask-user.js +64 -0
  22. package/lib/project-capsule-catalog.js +101 -0
  23. package/lib/project-connection.js +17 -0
  24. package/lib/project-debate-proposal.js +133 -17
  25. package/lib/project-debate.js +214 -110
  26. package/lib/project-delegated-follow-up.js +84 -0
  27. package/lib/project-delegated-session.js +53 -0
  28. package/lib/project-filesystem.js +7 -9
  29. package/lib/project-knowledge.js +23 -0
  30. package/lib/project-logs-comments.js +137 -0
  31. package/lib/project-logs-mcp-server.js +290 -0
  32. package/lib/project-logs-query.js +167 -0
  33. package/lib/project-logs-root.js +67 -0
  34. package/lib/project-logs-schema.js +259 -0
  35. package/lib/project-logs-service.js +357 -0
  36. package/lib/project-logs-snapshot.js +202 -0
  37. package/lib/project-logs-store.js +498 -0
  38. package/lib/project-logs-versioning.js +128 -0
  39. package/lib/project-logs.js +347 -0
  40. package/lib/project-mate-creation-proposal.js +120 -0
  41. package/lib/project-mate-interaction.js +14 -0
  42. package/lib/project-mate-knowledge.js +110 -0
  43. package/lib/project-memory.js +36 -7
  44. package/lib/project-models.js +157 -32
  45. package/lib/project-session-notes.js +2 -2
  46. package/lib/project-session-pair.js +8 -8
  47. package/lib/project-sessions.js +66 -51
  48. package/lib/project-vendor-login.js +414 -0
  49. package/lib/project-worker-proposal.js +25 -25
  50. package/lib/project-workspace-query.js +144 -0
  51. package/lib/project.js +433 -102
  52. package/lib/public/app.js +109 -48
  53. package/lib/public/css/avatar-imprints.css +0 -1
  54. package/lib/public/css/base.css +4 -0
  55. package/lib/public/css/capsule-ui.css +389 -0
  56. package/lib/public/css/command-palette.css +306 -6
  57. package/lib/public/css/home-capsule-library.css +243 -0
  58. package/lib/public/css/home-capsule-source.css +192 -0
  59. package/lib/public/css/home-debate-live.css +146 -0
  60. package/lib/public/css/home-debate-planning.css +125 -0
  61. package/lib/public/css/home-debates-archive.css +281 -0
  62. package/lib/public/css/home-hub.css +896 -405
  63. package/lib/public/css/home-mate-creation.css +34 -0
  64. package/lib/public/css/home-mate-model-picker.css +131 -0
  65. package/lib/public/css/home-mate-settings.css +344 -0
  66. package/lib/public/css/home-session-actions.css +265 -0
  67. package/lib/public/css/home-sidebar.css +667 -0
  68. package/lib/public/css/icon-strip.css +1 -2
  69. package/lib/public/css/input.css +79 -25
  70. package/lib/public/css/mates.css +7 -34
  71. package/lib/public/css/pane.css +1 -1
  72. package/lib/public/css/playbook.css +0 -80
  73. package/lib/public/css/project-logs.css +234 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/sticky-notes.css +53 -117
  76. package/lib/public/css/tui-attention.css +0 -44
  77. package/lib/public/css/workspace-assignment.css +45 -0
  78. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  79. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  80. package/lib/public/index.html +96 -74
  81. package/lib/public/modules/app-connection.js +100 -5
  82. package/lib/public/modules/app-dm.js +43 -160
  83. package/lib/public/modules/app-home-hub.js +328 -585
  84. package/lib/public/modules/app-message-cards.js +242 -0
  85. package/lib/public/modules/app-message-router.js +84 -0
  86. package/lib/public/modules/app-messages.js +124 -38
  87. package/lib/public/modules/app-notifications.js +8 -119
  88. package/lib/public/modules/app-panels.js +43 -2
  89. package/lib/public/modules/app-projects.js +9 -1
  90. package/lib/public/modules/app-rendering.js +21 -602
  91. package/lib/public/modules/avatar-imprint.js +11 -8
  92. package/lib/public/modules/background-tasks-ui.js +139 -16
  93. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  94. package/lib/public/modules/chat-render-runtime.js +198 -0
  95. package/lib/public/modules/command-palette.js +225 -436
  96. package/lib/public/modules/dm-render.js +82 -0
  97. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  98. package/lib/public/modules/home-capsule-library.js +146 -0
  99. package/lib/public/modules/home-capsule-source.js +383 -0
  100. package/lib/public/modules/home-chat-identity.js +35 -0
  101. package/lib/public/modules/home-chat-scroll.js +77 -0
  102. package/lib/public/modules/home-chat-stream-state.js +34 -0
  103. package/lib/public/modules/home-composer-focus.js +25 -0
  104. package/lib/public/modules/home-conversations-sheet.js +179 -0
  105. package/lib/public/modules/home-debate-activity.js +48 -0
  106. package/lib/public/modules/home-debate-controls.js +295 -0
  107. package/lib/public/modules/home-debate-launch.js +41 -0
  108. package/lib/public/modules/home-debate-live.js +284 -0
  109. package/lib/public/modules/home-debate-models.js +73 -0
  110. package/lib/public/modules/home-debate-planning.js +335 -0
  111. package/lib/public/modules/home-debates-archive.js +279 -0
  112. package/lib/public/modules/home-dock-resize.js +74 -0
  113. package/lib/public/modules/home-dock.js +446 -0
  114. package/lib/public/modules/home-mate-chat.js +496 -0
  115. package/lib/public/modules/home-mate-creation.js +109 -0
  116. package/lib/public/modules/home-mate-model-picker.js +281 -0
  117. package/lib/public/modules/home-mate-selection.js +38 -0
  118. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  119. package/lib/public/modules/home-mate-settings.js +383 -0
  120. package/lib/public/modules/home-session-actions.js +373 -0
  121. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  122. package/lib/public/modules/home-shell.js +9 -0
  123. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  124. package/lib/public/modules/home-sidebar.js +165 -0
  125. package/lib/public/modules/home-sub-surface.js +19 -0
  126. package/lib/public/modules/home-surface-boot.js +68 -0
  127. package/lib/public/modules/home-surface.js +141 -0
  128. package/lib/public/modules/home-tools.js +271 -0
  129. package/lib/public/modules/markdown.js +50 -2
  130. package/lib/public/modules/mate-management.js +86 -0
  131. package/lib/public/modules/mate-sidebar.js +0 -8
  132. package/lib/public/modules/mate-wizard.js +0 -7
  133. package/lib/public/modules/mention.js +17 -19
  134. package/lib/public/modules/pane-bridge.js +9 -0
  135. package/lib/public/modules/profile.js +4 -5
  136. package/lib/public/modules/project-logs-ambient.js +238 -0
  137. package/lib/public/modules/project-logs-render.js +359 -0
  138. package/lib/public/modules/project-logs.js +328 -0
  139. package/lib/public/modules/project-settings.js +34 -28
  140. package/lib/public/modules/project-switcher.js +11 -5
  141. package/lib/public/modules/project-workspace-assignment.js +23 -0
  142. package/lib/public/modules/scheduler.js +17 -3
  143. package/lib/public/modules/search-clay-chat.js +402 -0
  144. package/lib/public/modules/server-settings.js +14 -12
  145. package/lib/public/modules/session-actions.js +1 -1
  146. package/lib/public/modules/sidebar-mates.js +56 -552
  147. package/lib/public/modules/sidebar-mobile.js +12 -73
  148. package/lib/public/modules/sidebar-presence.js +37 -0
  149. package/lib/public/modules/sidebar-sessions.js +25 -1
  150. package/lib/public/modules/split-pair-ui.js +13 -13
  151. package/lib/public/modules/split-view.js +7 -0
  152. package/lib/public/modules/sticky-notes.js +106 -53
  153. package/lib/public/modules/tool-input-composition.js +48 -0
  154. package/lib/public/modules/tool-llm-status.js +167 -0
  155. package/lib/public/modules/tool-palette.js +76 -4
  156. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  157. package/lib/public/modules/tool-renderer-chart.js +158 -0
  158. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  159. package/lib/public/modules/tool-renderer.js +464 -0
  160. package/lib/public/modules/tool-runtime.js +309 -0
  161. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  162. package/lib/public/modules/tool-ui-tree.js +21 -0
  163. package/lib/public/modules/tui-attention.js +10 -0
  164. package/lib/public/modules/user-settings.js +16 -23
  165. package/lib/public/modules/vendor-login.js +287 -0
  166. package/lib/public/modules/worker-proposal.js +2 -2
  167. package/lib/public/modules/workspace-assignment-card.js +149 -0
  168. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  169. package/lib/public/style.css +14 -2
  170. package/lib/runtime-env.js +65 -0
  171. package/lib/sdk-bridge.js +120 -32
  172. package/lib/sdk-message-processor.js +34 -13
  173. package/lib/server-home-capsule-creation.js +36 -0
  174. package/lib/server-home-chat-events.js +287 -0
  175. package/lib/server-home-chat.js +518 -0
  176. package/lib/server-home-clay-entry.js +65 -0
  177. package/lib/server-home-clay-session-links.js +26 -0
  178. package/lib/server-home-debate-planning.js +214 -0
  179. package/lib/server-home-debates.js +137 -0
  180. package/lib/server-home-mate-creation.js +261 -0
  181. package/lib/server-home-models.js +241 -0
  182. package/lib/server-home-preferences.js +61 -0
  183. package/lib/server-mates.js +37 -0
  184. package/lib/server-tools.js +377 -0
  185. package/lib/server.js +166 -59
  186. package/lib/session-pair-mcp-server.js +6 -6
  187. package/lib/sessions.js +22 -4
  188. package/lib/tool-capsule-source.js +142 -0
  189. package/lib/tool-control-mcp-server.js +126 -0
  190. package/lib/tool-llm.js +48 -0
  191. package/lib/tool-storage.js +77 -0
  192. package/lib/tool-ui-spec-advanced.js +82 -0
  193. package/lib/tool-ui-spec.js +261 -0
  194. package/lib/tools-registry.js +286 -0
  195. package/lib/users-home-dock-preferences.js +77 -0
  196. package/lib/users-home-surface-preferences.js +99 -0
  197. package/lib/users-preferences.js +4 -7
  198. package/lib/users.js +12 -0
  199. package/lib/whats-new-content.js +3 -3
  200. package/lib/workspace-assignment-service.js +420 -0
  201. package/lib/workspace-query-mcp-server.js +154 -0
  202. package/lib/workspace-query-service.js +492 -0
  203. package/lib/ws-schema.js +107 -12
  204. package/lib/yoke/adapters/acp.js +2 -1
  205. package/lib/yoke/adapters/antigravity.js +2 -1
  206. package/lib/yoke/adapters/claude-worker.js +11 -4
  207. package/lib/yoke/adapters/claude.js +141 -46
  208. package/lib/yoke/adapters/codex.js +79 -11
  209. package/lib/yoke/adapters/kiro.js +2 -1
  210. package/lib/yoke/claude-user-input.js +38 -0
  211. package/lib/yoke/codex-app-server.js +20 -2
  212. package/lib/yoke/codex-background-tasks.js +8 -2
  213. package/lib/yoke/codex-user-input.js +64 -0
  214. package/lib/yoke/complete-once.js +164 -0
  215. package/lib/yoke/index.js +5 -1
  216. package/lib/yoke/interface.js +19 -0
  217. package/lib/yoke/mcp-bridge-server.js +2 -6
  218. package/lib/yoke/user-input.js +254 -0
  219. package/package.json +4 -3
  220. package/lib/public/css/home-chat.css +0 -380
  221. package/lib/public/modules/home-chat.js +0 -438
  222. package/lib/server-clay-home.js +0 -245
@@ -0,0 +1,202 @@
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
+
19
+ // Ops that advance the canonical record. `comment` and a non-incorporating
20
+ // `review` are participation, not revisions, and never appear here.
21
+ var CANONICAL_OPS = ["create", "update", "link", "revert"];
22
+
23
+ var SNAPSHOT_FIELDS = ["category", "priority", "title", "summary", "body", "tags", "links"];
24
+
25
+ function emptySnapshot() {
26
+ return {
27
+ category: "",
28
+ priority: logsSchema.DEFAULT_PRIORITY,
29
+ title: "",
30
+ summary: "",
31
+ body: "",
32
+ tags: [],
33
+ links: [],
34
+ };
35
+ }
36
+
37
+ function cloneSnapshot(snapshot) {
38
+ var source = snapshot || {};
39
+ return {
40
+ category: source.category || "",
41
+ priority: logsSchema.normalizePriority(source.priority),
42
+ title: typeof source.title === "string" ? source.title : "",
43
+ summary: typeof source.summary === "string" ? source.summary : "",
44
+ body: typeof source.body === "string" ? source.body : "",
45
+ tags: Array.isArray(source.tags) ? source.tags.slice() : [],
46
+ links: Array.isArray(source.links) ? source.links.slice() : [],
47
+ };
48
+ }
49
+
50
+ // True when this record carries a complete canonical snapshot.
51
+ function hasSnapshot(record) {
52
+ return !!(record && record.snapshot && typeof record.snapshot === "object");
53
+ }
54
+
55
+ // A canonical op, including an incorporating review. A review that clarifies or
56
+ // declines resolves a comment without changing the record.
57
+ function isCanonical(record) {
58
+ if (!record) return false;
59
+ if (record.op === "review") return record.action === "incorporate";
60
+ return CANONICAL_OPS.indexOf(record.op) !== -1;
61
+ }
62
+
63
+ // Apply a legacy partial record onto the running state. Only used for records
64
+ // written before snapshots; a snapshot-bearing record never takes this path.
65
+ function applyPartial(state, record, maxLinks) {
66
+ var next = cloneSnapshot(state);
67
+ if (record.op === "create") {
68
+ next.category = record.kind || "";
69
+ next.priority = logsSchema.normalizePriority(record.priority);
70
+ next.title = typeof record.title === "string" ? record.title : "";
71
+ next.summary = typeof record.summary === "string" ? record.summary : "";
72
+ next.body = typeof record.body === "string" ? record.body : "";
73
+ next.tags = Array.isArray(record.tags) ? record.tags.slice() : [];
74
+ next.links = Array.isArray(record.links) ? record.links.slice() : [];
75
+ return next;
76
+ }
77
+ if (record.op === "link") {
78
+ var added = Array.isArray(record.links) ? record.links : [];
79
+ for (var i = 0; i < added.length && next.links.length < maxLinks; i++) {
80
+ var exists = false;
81
+ for (var j = 0; j < next.links.length; j++) {
82
+ if (next.links[j].ref === added[i].ref) { exists = true; break; }
83
+ }
84
+ if (!exists) next.links.push(added[i]);
85
+ }
86
+ return next;
87
+ }
88
+ if (record.kind) next.category = record.kind;
89
+ if (record.priority) next.priority = logsSchema.normalizePriority(record.priority);
90
+ if (typeof record.title === "string") next.title = record.title;
91
+ if (typeof record.summary === "string") next.summary = record.summary;
92
+ if (typeof record.body === "string") next.body = record.body;
93
+ if (Array.isArray(record.tags)) next.tags = record.tags.slice();
94
+ if (Array.isArray(record.links)) next.links = record.links.slice();
95
+ return next;
96
+ }
97
+
98
+ function sameSnapshot(a, b) {
99
+ var left = cloneSnapshot(a);
100
+ var right = cloneSnapshot(b);
101
+ for (var i = 0; i < SNAPSHOT_FIELDS.length; i++) {
102
+ var field = SNAPSHOT_FIELDS[i];
103
+ if (JSON.stringify(left[field]) !== JSON.stringify(right[field])) return false;
104
+ }
105
+ return true;
106
+ }
107
+
108
+ function changedFields(previous, next) {
109
+ var changed = [];
110
+ if (!previous) return SNAPSHOT_FIELDS.slice();
111
+ for (var i = 0; i < SNAPSHOT_FIELDS.length; i++) {
112
+ var field = SNAPSHOT_FIELDS[i];
113
+ if (JSON.stringify(previous[field]) !== JSON.stringify(next[field])) changed.push(field);
114
+ }
115
+ return changed;
116
+ }
117
+
118
+ // Every canonical revision of one entry, in causal order, each with the exact
119
+ // snapshot as of that revision. Deleting is a tombstone rather than a
120
+ // revision, so it is reported separately.
121
+ function revisions(chain, maxLinks) {
122
+ var out = [];
123
+ var state = emptySnapshot();
124
+ var deleted = null;
125
+ var started = false;
126
+
127
+ for (var i = 0; i < chain.length; i++) {
128
+ var record = chain[i];
129
+ if (record.op === "delete") {
130
+ if (started && !deleted) deleted = { at: record.at || 0, author: record.author || null };
131
+ continue;
132
+ }
133
+ if (deleted) continue;
134
+ if (!isCanonical(record)) continue;
135
+ if (record.op === "create") {
136
+ if (started) continue;
137
+ started = true;
138
+ } else if (!started) {
139
+ continue;
140
+ }
141
+
142
+ var previous = out.length ? out[out.length - 1].snapshot : null;
143
+ state = hasSnapshot(record) ? cloneSnapshot(record.snapshot) : applyPartial(state, record, maxLinks);
144
+ out.push({
145
+ revision: out.length + 1,
146
+ op: record.op === "review" ? "incorporate" : record.op,
147
+ at: record.at || 0,
148
+ author: record.author || null,
149
+ snapshot: state,
150
+ changed: record.op === "create" ? [] : changedFields(previous, state),
151
+ // Present only on a revert, so a reader can see where it came from.
152
+ revertedFrom: record.revertedFrom || null,
153
+ reason: typeof record.reason === "string" ? record.reason : null,
154
+ // Present only on an incorporation, linking the revision to the comment
155
+ // that prompted it.
156
+ commentId: record.op === "review" ? (record.commentId || null) : null,
157
+ // Whether this revision was reconstructed from a legacy partial record.
158
+ reconstructed: !hasSnapshot(record),
159
+ // The record that produced this revision. Internal: it exists so a
160
+ // review can be linked to its exact revision by identity rather than by
161
+ // matching timestamps, and describeRevision deliberately drops it so no
162
+ // internal record id reaches a public response.
163
+ sourceRecordId: record.id,
164
+ });
165
+ }
166
+ return { revisions: out, deleted: deleted, started: started };
167
+ }
168
+
169
+ // Revision metadata without any body, for history listings and for the bounded
170
+ // timeline the detail view shows. `sourceRecordId` is intentionally not copied:
171
+ // the only identifiers a caller ever sees are the opaque entry ref and comment
172
+ // id.
173
+ function describeRevision(entry) {
174
+ return {
175
+ revision: entry.revision,
176
+ op: entry.op,
177
+ at: entry.at,
178
+ author: entry.author,
179
+ changed: entry.changed,
180
+ revertedFrom: entry.revertedFrom,
181
+ reason: entry.reason,
182
+ commentId: entry.commentId,
183
+ reconstructed: entry.reconstructed,
184
+ title: entry.snapshot.title,
185
+ category: entry.snapshot.category,
186
+ priority: entry.snapshot.priority,
187
+ };
188
+ }
189
+
190
+ module.exports = {
191
+ CANONICAL_OPS: CANONICAL_OPS,
192
+ SNAPSHOT_FIELDS: SNAPSHOT_FIELDS,
193
+ emptySnapshot: emptySnapshot,
194
+ cloneSnapshot: cloneSnapshot,
195
+ hasSnapshot: hasSnapshot,
196
+ isCanonical: isCanonical,
197
+ applyPartial: applyPartial,
198
+ sameSnapshot: sameSnapshot,
199
+ changedFields: changedFields,
200
+ revisions: revisions,
201
+ describeRevision: describeRevision,
202
+ };
@@ -0,0 +1,498 @@
1
+ // Project Logs projection over the Clay-wide knowledge record backend.
2
+ //
3
+ // Project Logs are dry, factual project records: decisions, investigations,
4
+ // session notes, runbooks, references, incidents, and progress. They carry no
5
+ // persona and no autobiographical memory; that separation is a product rule,
6
+ // not a storage detail, so nothing here writes identity-shaped content.
7
+ //
8
+ // A worktree shares its parent project's Logs. The store root is resolved
9
+ // through Git's common directory, so a worktree session and a parent session
10
+ // read and write the same record file instead of forking project knowledge.
11
+
12
+ var fs = require("fs");
13
+ var path = require("path");
14
+ var crypto = require("crypto");
15
+ var recordStore = require("./knowledge-record-store");
16
+ var logsQuery = require("./project-logs-query");
17
+ var logsSchema = require("./project-logs-schema");
18
+ var logsSnapshot = require("./project-logs-snapshot");
19
+ var logsComments = require("./project-logs-comments");
20
+ var logsVersioning = require("./project-logs-versioning");
21
+ var logsRoot = require("./project-logs-root");
22
+
23
+ var MAX_SUMMARY_CHARS = logsSchema.MAX_SUMMARY_CHARS;
24
+ var MAX_COMMENT_CHARS = 4000;
25
+ var MAX_COMMENTS = 200;
26
+ var MAX_FEEDBACK_ITEMS = 25;
27
+ var MAX_FEEDBACK_BODY_CHARS = 600;
28
+ var MAX_TITLE_CHARS = 200;
29
+ var MAX_BODY_CHARS = 20000;
30
+ var MAX_TAGS = 12;
31
+ var MAX_TAG_CHARS = 40;
32
+ var MAX_LINKS = 20;
33
+ var MAX_LINK_CHARS = 200;
34
+ var MAX_PAGE = logsQuery.MAX_PAGE;
35
+ var REF_PATTERN = /^log:[A-Za-z0-9_-]{24}$/;
36
+
37
+ var cleanText = logsSchema.cleanText;
38
+ var cleanLine = logsSchema.cleanLine;
39
+ var normalizeAuthor = logsSchema.normalizeAuthor;
40
+ var normalizeTags = logsSchema.normalizeTags;
41
+ var normalizeLinks = logsSchema.normalizeLinks;
42
+
43
+ var page = logsQuery.page;
44
+
45
+ // Filters accept any well-formed category. An unknown but valid one matches
46
+ // nothing, which is a real answer. A supplied malformed one is an error:
47
+ // silently dropping it would widen the result set to everything the caller is
48
+ // authorized to see, which is the opposite of failing closed, and it would give
49
+ // an agent no signal that its filter was wrong.
50
+ function validateKind(kind, fallback) {
51
+ if (kind === undefined || kind === null || kind === "") return fallback || null;
52
+ return logsSchema.normalizeCategory(kind);
53
+ }
54
+
55
+
56
+ function createProjectLogsStore(opts) {
57
+ var options = opts || {};
58
+ var root = options.root ? path.resolve(options.root) : logsRoot.resolveProjectRoot(options.cwd, options.runGit);
59
+ var scopeId = logsRoot.scopeIdForRoot(root);
60
+ var store = recordStore.createRecordStore({ scopeId: scopeId, baseDir: options.baseDir });
61
+
62
+ // Stable across every revision of an entry: derived from the scope and the
63
+ // chain root, never from a path or an internal record id.
64
+ function refFor(rootId) {
65
+ var digest = crypto.createHash("sha256").update(scopeId + "\u0000" + String(rootId)).digest("base64url");
66
+ return "log:" + digest.substring(0, 24);
67
+ }
68
+
69
+ // File order is causal order. Wall-clock timestamps are recorded but never
70
+ // used to order a chain, so a skewed clock cannot reorder a revision.
71
+ function chains() {
72
+ var records = store.all();
73
+ var byRoot = new Map();
74
+ for (var i = 0; i < records.length; i++) {
75
+ var record = records[i];
76
+ var rootId = record.rootId || record.id;
77
+ if (!byRoot.has(rootId)) byRoot.set(rootId, []);
78
+ byRoot.get(rootId).push(record);
79
+ }
80
+ return byRoot;
81
+ }
82
+
83
+ // The canonical record is reconstructed from its revision chain, and the
84
+ // discussion is folded separately. Neither comments nor a clarify/decline
85
+ // review touch the canonical state or the revision count.
86
+ function fold(rootId, chain) {
87
+ var history = logsSnapshot.revisions(chain, MAX_LINKS);
88
+ if (!history.started || history.revisions.length === 0) return null;
89
+ var last = history.revisions[history.revisions.length - 1];
90
+ var first = history.revisions[0];
91
+
92
+ // A create record that does not carry the minimum a log entry needs is not
93
+ // a log entry. Hand-edited files and torn writes that were later completed
94
+ // can look structurally valid without being usable.
95
+ if (!logsSchema.safeCategory(first.snapshot.category)) return null;
96
+ if (!first.snapshot.title || !first.snapshot.title.trim()) return null;
97
+
98
+ // Each reconstructed revision knows exactly which record produced it, so
99
+ // an incorporating review maps to its revision by identity. Two reviews
100
+ // sharing a millisecond cannot be confused for one another.
101
+ var revisionForRecordId = {};
102
+ for (var v = 0; v < history.revisions.length; v++) {
103
+ var source = history.revisions[v].sourceRecordId;
104
+ if (source) revisionForRecordId[source] = history.revisions[v].revision;
105
+ }
106
+
107
+ var thread = logsComments.comments(chain, {
108
+ maxComments: MAX_COMMENTS,
109
+ revisionForRecordId: revisionForRecordId,
110
+ });
111
+
112
+ var entry = {
113
+ ref: refFor(rootId),
114
+ kind: last.snapshot.category,
115
+ category: last.snapshot.category,
116
+ priority: last.snapshot.priority,
117
+ title: last.snapshot.title,
118
+ summary: last.snapshot.summary,
119
+ body: last.snapshot.body,
120
+ tags: last.snapshot.tags.slice(),
121
+ links: last.snapshot.links.slice(),
122
+ comments: thread,
123
+ createdAt: first.at,
124
+ createdBy: first.author,
125
+ updatedAt: last.at,
126
+ updatedBy: last.author,
127
+ revisions: history.revisions.length,
128
+ deleted: !!history.deleted,
129
+ deletedAt: history.deleted ? history.deleted.at : null,
130
+ deletedBy: history.deleted ? history.deleted.author : null,
131
+ };
132
+ // Entries written before summaries existed get a deterministic one derived
133
+ // from their own body. Nothing on disk is rewritten.
134
+ entry.summary = logsSchema.fallbackSummary(entry);
135
+ entry.commentCount = thread.length;
136
+ entry.pendingFeedbackCount = logsComments.pendingReviewCount(thread);
137
+ // Bounded revision metadata, never bodies, so the detail view can show a
138
+ // history timeline without a second round trip.
139
+ entry.history = history.revisions.map(logsSnapshot.describeRevision);
140
+ return entry;
141
+ }
142
+
143
+ function entries(includeDeleted) {
144
+ var out = [];
145
+ chains().forEach(function (chain, rootId) {
146
+ var entry = fold(rootId, chain);
147
+ if (!entry) return;
148
+ if (entry.deleted && !includeDeleted) return;
149
+ out.push(entry);
150
+ });
151
+ // Latest canonical update first, ref as a deterministic tie-break. Only a
152
+ // revision moves updatedAt, so comments and clarify/decline never promote.
153
+ out.sort(function (a, b) { return b.updatedAt - a.updatedAt || logsQuery.compareRefs(a.ref, b.ref); });
154
+ return out;
155
+ }
156
+
157
+ function findChain(ref) {
158
+ if (typeof ref !== "string" || !REF_PATTERN.test(ref)) return null;
159
+ var found = null;
160
+ chains().forEach(function (chain, rootId) {
161
+ if (found) return;
162
+ if (refFor(rootId) === ref) found = { rootId: rootId, chain: chain };
163
+ });
164
+ return found;
165
+ }
166
+
167
+ function read(ref, includeDeleted) {
168
+ var located = findChain(ref);
169
+ if (!located) return null;
170
+ var entry = fold(located.rootId, located.chain);
171
+ if (!entry) return null;
172
+ if (entry.deleted && !includeDeleted) return null;
173
+ return entry;
174
+ }
175
+
176
+ function requireLive(ref) {
177
+ var located = findChain(ref);
178
+ if (!located) throw new Error("Log entry not found.");
179
+ var entry = fold(located.rootId, located.chain);
180
+ if (!entry || entry.deleted) throw new Error("Log entry not found.");
181
+ return located;
182
+ }
183
+
184
+ function create(input, author) {
185
+ var data = input || {};
186
+ var validated = logsSchema.validateNewRecord(data);
187
+ var title = cleanLine(data.title || "", MAX_TITLE_CHARS);
188
+ if (!title) throw new Error("A log title is required.");
189
+ var summary = cleanLine(data.summary || "", MAX_SUMMARY_CHARS);
190
+ if (!summary) throw new Error("A one or two sentence summary is required so the entry is understandable from a list.");
191
+ var id = recordStore.newRecordId();
192
+ var snapshot = logsSnapshot.cloneSnapshot({
193
+ category: validated.category,
194
+ priority: validated.priority,
195
+ title: title,
196
+ summary: summary,
197
+ body: cleanText(data.body || "", MAX_BODY_CHARS),
198
+ tags: normalizeTags(data.tags),
199
+ links: normalizeLinks(data.links),
200
+ });
201
+ store.append({
202
+ id: id,
203
+ rootId: id,
204
+ op: "create",
205
+ scope: scopeId,
206
+ // Legacy field names are kept so an older build still reads this record.
207
+ kind: snapshot.category,
208
+ priority: snapshot.priority,
209
+ title: snapshot.title,
210
+ summary: snapshot.summary,
211
+ body: snapshot.body,
212
+ tags: snapshot.tags,
213
+ links: snapshot.links,
214
+ snapshot: snapshot,
215
+ author: normalizeAuthor(author),
216
+ at: Date.now(),
217
+ });
218
+ return read(refFor(id), false);
219
+ }
220
+
221
+ // Build the complete snapshot a revision will store, from the current state
222
+ // plus the requested changes. Every new canonical edit is self-contained.
223
+ function nextSnapshot(current, validated, data) {
224
+ var snapshot = logsSnapshot.cloneSnapshot(current);
225
+ if (validated.category !== undefined) snapshot.category = validated.category;
226
+ if (validated.priority !== undefined) snapshot.priority = validated.priority;
227
+ if (validated.title !== undefined) {
228
+ snapshot.title = cleanLine(validated.title, MAX_TITLE_CHARS);
229
+ if (!snapshot.title) throw new Error("A log title cannot be emptied.");
230
+ }
231
+ if (validated.summary !== undefined) {
232
+ snapshot.summary = cleanLine(validated.summary, MAX_SUMMARY_CHARS);
233
+ if (!snapshot.summary) throw new Error("A log summary cannot be emptied.");
234
+ }
235
+ if (data.body !== undefined) snapshot.body = cleanText(data.body || "", MAX_BODY_CHARS);
236
+ if (data.tags !== undefined) snapshot.tags = normalizeTags(data.tags);
237
+ return snapshot;
238
+ }
239
+
240
+ function currentSnapshot(ref) {
241
+ var entry = read(ref, false);
242
+ if (!entry) throw new Error("Log entry not found.");
243
+ return logsSnapshot.cloneSnapshot(entry);
244
+ }
245
+
246
+ function update(ref, changes, author) {
247
+ var located = requireLive(ref);
248
+ var data = changes || {};
249
+ var validated = logsSchema.validateRevision(data);
250
+ var current = currentSnapshot(ref);
251
+ var snapshot = nextSnapshot(current, validated, data);
252
+ if (logsSnapshot.sameSnapshot(current, snapshot)) {
253
+ throw new Error("An update requires at least one changed field.");
254
+ }
255
+ appendRevision(located.rootId, "update", snapshot, author, {});
256
+ return read(ref, false);
257
+ }
258
+
259
+ // One append per canonical revision, always carrying a complete snapshot.
260
+ // Legacy field names ride along so an older build can still read the record.
261
+ function appendRevision(rootId, op, snapshot, author, extra) {
262
+ var record = {
263
+ id: recordStore.newRecordId(),
264
+ rootId: rootId,
265
+ op: op,
266
+ scope: scopeId,
267
+ kind: snapshot.category,
268
+ priority: snapshot.priority,
269
+ title: snapshot.title,
270
+ summary: snapshot.summary,
271
+ body: snapshot.body,
272
+ tags: snapshot.tags,
273
+ links: snapshot.links,
274
+ snapshot: snapshot,
275
+ author: normalizeAuthor(author),
276
+ at: Date.now(),
277
+ };
278
+ var keys = Object.keys(extra || {});
279
+ for (var i = 0; i < keys.length; i++) record[keys[i]] = extra[keys[i]];
280
+ store.append(record);
281
+ return record;
282
+ }
283
+
284
+ // Human participation. Append-only for this slice: no edit, no delete, and
285
+ // never a canonical revision. The author is supplied by the authorization
286
+ // layer, never by the caller's payload.
287
+ function comment(ref, input, author) {
288
+ var located = requireLive(ref);
289
+ var data = input || {};
290
+ var body = cleanText(data.body || "", MAX_COMMENT_CHARS);
291
+ if (!body) throw new Error("A comment body is required.");
292
+ var existing = read(ref, false);
293
+ if (existing && existing.comments.length >= MAX_COMMENTS) {
294
+ throw new Error("This entry already has the maximum of " + MAX_COMMENTS + " comments.");
295
+ }
296
+ store.append({
297
+ id: recordStore.newRecordId(),
298
+ rootId: located.rootId,
299
+ op: "comment",
300
+ scope: scopeId,
301
+ body: body,
302
+ author: normalizeAuthor(author),
303
+ at: Date.now(),
304
+ });
305
+ return read(ref, false);
306
+ }
307
+
308
+ function link(ref, links, author) {
309
+ var located = requireLive(ref);
310
+ var normalized = normalizeLinks(links);
311
+ if (normalized.length === 0) throw new Error("At least one link reference is required.");
312
+ var current = currentSnapshot(ref);
313
+ var snapshot = logsSnapshot.cloneSnapshot(current);
314
+ for (var i = 0; i < normalized.length && snapshot.links.length < MAX_LINKS; i++) {
315
+ var exists = false;
316
+ for (var j = 0; j < snapshot.links.length; j++) {
317
+ if (snapshot.links[j].ref === normalized[i].ref) { exists = true; break; }
318
+ }
319
+ if (!exists) snapshot.links.push(normalized[i]);
320
+ }
321
+ if (logsSnapshot.sameSnapshot(current, snapshot)) {
322
+ throw new Error("Those links are already attached to this entry.");
323
+ }
324
+ appendRevision(located.rootId, "link", snapshot, author, {});
325
+ return read(ref, false);
326
+ }
327
+
328
+ function remove(ref, author) {
329
+ var located = requireLive(ref);
330
+ store.append({
331
+ id: recordStore.newRecordId(),
332
+ rootId: located.rootId,
333
+ op: "delete",
334
+ scope: scopeId,
335
+ author: normalizeAuthor(author),
336
+ at: Date.now(),
337
+ });
338
+ return read(ref, true);
339
+ }
340
+
341
+ // Filtering, ranking, and pagination live in project-logs-query.js so this
342
+ // module stays focused on records. Ranking is the shared BM25 engine.
343
+ // The project's category vocabulary, derived from its own live non-deleted
344
+ // entries. A category that only exists on deleted entries disappears; a
345
+ // revision that changes category updates the vocabulary without rewriting a
346
+ // single historical record. A shared project shares this list because it
347
+ // shares this store.
348
+ function categories() {
349
+ var seen = {};
350
+ var out = [];
351
+ var live = entries(false);
352
+ for (var i = 0; i < live.length; i++) {
353
+ var value = live[i].category;
354
+ if (!value || seen[value]) continue;
355
+ seen[value] = true;
356
+ out.push(value);
357
+ }
358
+ out.sort();
359
+ return out;
360
+ }
361
+
362
+ var versioning = logsVersioning.attachVersioning({
363
+ read: read,
364
+ requireLive: requireLive,
365
+ findChain: findChain,
366
+ appendRevision: appendRevision,
367
+ nextSnapshot: nextSnapshot,
368
+ cleanText: cleanText,
369
+ maxLinks: MAX_LINKS,
370
+ maxResponseChars: MAX_COMMENT_CHARS,
371
+ scopeId: scopeId,
372
+ store: store,
373
+ normalizeAuthor: normalizeAuthor,
374
+ });
375
+
376
+ // Every comment still awaiting Driver review, across every live entry, in
377
+ // one pass over the projection. The exact total is always reported; only the
378
+ // returned summaries are clamped, so a pending comment on an old entry can
379
+ // never be invisible.
380
+ function feedback(args) {
381
+ var options2 = args || {};
382
+ var limit = Number(options2.limit);
383
+ if (!Number.isFinite(limit)) limit = MAX_FEEDBACK_ITEMS;
384
+ limit = Math.max(1, Math.min(MAX_FEEDBACK_ITEMS, Math.floor(limit)));
385
+ var live = entries(false);
386
+ var out = [];
387
+ var total = 0;
388
+ for (var i = 0; i < live.length; i++) {
389
+ var awaiting = logsComments.pendingReviewComments(live[i].comments);
390
+ for (var j = 0; j < awaiting.length; j++) {
391
+ total++;
392
+ if (out.length >= limit) continue;
393
+ var body = awaiting[j].body || "";
394
+ out.push({
395
+ ref: live[i].ref,
396
+ title: live[i].title,
397
+ category: live[i].category,
398
+ priority: live[i].priority,
399
+ commentId: awaiting[j].id,
400
+ status: awaiting[j].status,
401
+ author: awaiting[j].author,
402
+ at: awaiting[j].at,
403
+ body: body.length > MAX_FEEDBACK_BODY_CHARS
404
+ ? body.substring(0, MAX_FEEDBACK_BODY_CHARS) + "..."
405
+ : body,
406
+ });
407
+ }
408
+ }
409
+ return { feedback: out, total: total, truncated: total > out.length };
410
+ }
411
+
412
+ function list(args) {
413
+ var options2 = args || {};
414
+ var result = logsQuery.listEntries(entries(options2.includeDeleted === true), options2, cleanLine, validateKind);
415
+ result.categories = categories();
416
+ return result;
417
+ }
418
+
419
+ function search(args) {
420
+ var result = logsQuery.searchEntries(entries(false), args || {}, cleanLine, validateKind);
421
+ result.categories = categories();
422
+ return result;
423
+ }
424
+
425
+ // The blame view: every canonical revision in causal order with its author
426
+ // and the fields it changed, reconstructed from the append-only chain.
427
+ // Metadata only: no bodies, so history never dumps the record.
428
+ function history(ref, args) {
429
+ var located = findChain(ref);
430
+ if (!located) throw new Error("Log entry not found.");
431
+ var reconstructed = logsSnapshot.revisions(located.chain, MAX_LINKS);
432
+ var revisions = reconstructed.revisions.map(logsSnapshot.describeRevision);
433
+ if (reconstructed.deleted) {
434
+ revisions.push({
435
+ revision: revisions.length + 1,
436
+ op: "delete",
437
+ at: reconstructed.deleted.at,
438
+ author: reconstructed.deleted.author,
439
+ changed: [],
440
+ revertedFrom: null,
441
+ reason: null,
442
+ commentId: null,
443
+ reconstructed: false,
444
+ });
445
+ }
446
+ var result = page(revisions, args || {});
447
+ return { ref: ref, revisions: result.items, nextCursor: result.nextCursor, total: result.total };
448
+ }
449
+
450
+ function stats() {
451
+ var base = store.stats();
452
+ return {
453
+ root: root,
454
+ scopeId: scopeId,
455
+ filePath: base.filePath,
456
+ records: base.records,
457
+ skippedRecords: base.skipped,
458
+ entries: entries(false).length,
459
+ };
460
+ }
461
+
462
+ return {
463
+ root: root,
464
+ scopeId: scopeId,
465
+ filePath: store.filePath,
466
+ categories: categories,
467
+ feedback: feedback,
468
+ create: create,
469
+ update: update,
470
+ comment: comment,
471
+ review: versioning.review,
472
+ revert: versioning.revert,
473
+ readRevision: versioning.readRevision,
474
+ link: link,
475
+ remove: remove,
476
+ read: read,
477
+ list: list,
478
+ search: search,
479
+ history: history,
480
+ stats: stats,
481
+ };
482
+ }
483
+
484
+ module.exports = {
485
+ SEED_CATEGORIES: logsSchema.SEED_CATEGORIES,
486
+ MAX_CATEGORY_CHARS: logsSchema.MAX_CATEGORY_CHARS,
487
+ PRIORITIES: logsSchema.PRIORITIES,
488
+ MAX_SUMMARY_CHARS: MAX_SUMMARY_CHARS,
489
+ MAX_COMMENT_CHARS: MAX_COMMENT_CHARS,
490
+ MAX_COMMENTS: MAX_COMMENTS,
491
+ MAX_TITLE_CHARS: MAX_TITLE_CHARS,
492
+ MAX_BODY_CHARS: MAX_BODY_CHARS,
493
+ MAX_PAGE: MAX_PAGE,
494
+ REF_PATTERN: REF_PATTERN,
495
+ resolveProjectRoot: logsRoot.resolveProjectRoot,
496
+ scopeIdForRoot: logsRoot.scopeIdForRoot,
497
+ createProjectLogsStore: createProjectLogsStore,
498
+ };