clay-server 4.0.0-beta.16 → 4.0.0-beta.18

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 (50) hide show
  1. package/lib/daemon-projects.js +10 -5
  2. package/lib/daemon.js +61 -10
  3. package/lib/project-log-context.js +171 -0
  4. package/lib/project-log-feedback-delivery.js +47 -5
  5. package/lib/project-logs-context-state.js +42 -0
  6. package/lib/project-logs-mcp-server.js +15 -4
  7. package/lib/project-logs-query.js +18 -0
  8. package/lib/project-logs-root.js +8 -0
  9. package/lib/project-logs-service.js +121 -15
  10. package/lib/project-logs-snapshot.js +3 -0
  11. package/lib/project-logs-store.js +29 -32
  12. package/lib/project-logs-versioning.js +6 -2
  13. package/lib/project-logs.js +61 -13
  14. package/lib/project-message-delivery.js +70 -0
  15. package/lib/project-user-message.js +11 -0
  16. package/lib/project.js +6 -1
  17. package/lib/public/app.js +5 -0
  18. package/lib/public/css/command-palette.css +19 -9
  19. package/lib/public/css/project-logs.css +20 -2
  20. package/lib/public/css/sticky-notes.css +7 -0
  21. package/lib/public/index.html +2 -2
  22. package/lib/public/modules/admin.js +8 -8
  23. package/lib/public/modules/app-connection.js +22 -3
  24. package/lib/public/modules/app-messages.js +29 -1
  25. package/lib/public/modules/app-rendering.js +1 -0
  26. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  27. package/lib/public/modules/clay-log-links.js +70 -0
  28. package/lib/public/modules/command-palette.js +52 -15
  29. package/lib/public/modules/input.js +6 -1
  30. package/lib/public/modules/markdown.js +2 -0
  31. package/lib/public/modules/message-delivery.js +97 -0
  32. package/lib/public/modules/project-logs-render.js +48 -2
  33. package/lib/public/modules/project-logs.js +140 -11
  34. package/lib/public/modules/sidebar-session-hierarchy.js +16 -7
  35. package/lib/public/modules/sidebar-sessions.js +3 -1
  36. package/lib/public/modules/sticky-note-markdown.js +5 -0
  37. package/lib/public/modules/sticky-notes-browser.js +2 -0
  38. package/lib/public/modules/sticky-notes-card.js +2 -0
  39. package/lib/public/modules/sticky-notes-editor.js +3 -0
  40. package/lib/public/modules/sticky-notes.js +2 -0
  41. package/lib/public/style.css +1 -1
  42. package/lib/sdk-bridge.js +20 -10
  43. package/lib/server-global-ws.js +4 -0
  44. package/lib/server-home-chat-events.js +3 -0
  45. package/lib/server-home-chat.js +9 -2
  46. package/lib/server-home-clay-entry.js +1 -1
  47. package/lib/server-home-clay-session-links.js +34 -5
  48. package/lib/server.js +14 -3
  49. package/lib/ws-schema.js +8 -2
  50. package/package.json +1 -1
@@ -62,6 +62,19 @@ function page(items, args) {
62
62
  };
63
63
  }
64
64
 
65
+ function contextMode(options) {
66
+ var value = options && options.contextMode;
67
+ return value === "all" || value === "current" || value === "project" ? value : "all";
68
+ }
69
+
70
+ function matchesContext(entry, options) {
71
+ var mode = contextMode(options);
72
+ var changeSetId = entry && entry.context && entry.context.changeSetId;
73
+ if (mode === "all") return true;
74
+ if (mode === "project") return !changeSetId;
75
+ return !changeSetId || changeSetId === options.currentChangeSetId;
76
+ }
77
+
65
78
  function listEntries(entries, args, cleanLine, validateKind) {
66
79
  var options = args || {};
67
80
  var kind = options.kind ? validateKind(options.kind, null) : null;
@@ -73,6 +86,7 @@ function listEntries(entries, args, cleanLine, validateKind) {
73
86
  if (kind && entry.category !== kind) return false;
74
87
  if (priority && entry.priority !== priority) return false;
75
88
  if (tag && entry.tags.indexOf(tag) === -1) return false;
89
+ if (!matchesContext(entry, options)) return false;
76
90
  return true;
77
91
  });
78
92
  var result = page(filtered, options);
@@ -116,6 +130,7 @@ function summarize(entry) {
116
130
  // How many comments still await Driver review. A count only: the ledger
117
131
  // never carries comment bodies.
118
132
  pendingFeedbackCount: entry.pendingFeedbackCount || 0,
133
+ context: entry.context || null,
119
134
  };
120
135
  }
121
136
 
@@ -128,6 +143,7 @@ function searchEntries(entries, args, cleanLine, validateKind) {
128
143
  var candidates = entries.filter(function (entry) {
129
144
  if (kind && entry.category !== kind) return false;
130
145
  if (priority && entry.priority !== priority) return false;
146
+ if (!matchesContext(entry, options)) return false;
131
147
  return true;
132
148
  });
133
149
 
@@ -161,6 +177,8 @@ module.exports = {
161
177
  encodeCursor: encodeCursor,
162
178
  decodeCursor: decodeCursor,
163
179
  page: page,
180
+ contextMode: contextMode,
181
+ matchesContext: matchesContext,
164
182
  summarize: summarize,
165
183
  listEntries: listEntries,
166
184
  searchEntries: searchEntries,
@@ -60,8 +60,16 @@ function scopeIdForRoot(root) {
60
60
  return "project/" + utils.encodeCwd(root);
61
61
  }
62
62
 
63
+ function scopeIdForKnowledgeId(knowledgeId) {
64
+ if (typeof knowledgeId !== "string" || !/^[A-Za-z0-9_-]{1,120}$/.test(knowledgeId)) {
65
+ throw new Error("Invalid project knowledge id.");
66
+ }
67
+ return "project/" + knowledgeId;
68
+ }
69
+
63
70
  module.exports = {
64
71
  resolveProjectRoot: resolveProjectRoot,
65
72
  scopeIdForRoot: scopeIdForRoot,
73
+ scopeIdForKnowledgeId: scopeIdForKnowledgeId,
66
74
  canonicalPath: canonicalPath,
67
75
  };
@@ -13,7 +13,7 @@
13
13
  // - A project session is bound to exactly its own project's Logs.
14
14
  // - Canonical authorship belongs to Project Driver sessions alone. A human,
15
15
  // however privileged, may read and comment but may never create or revise
16
- // a canonical entry: the log is the agent's record of the work.
16
+ // a canonical entry. The project owner may delete an entry.
17
17
  // - Members of a shared project read each other's entries and comments with
18
18
  // attribution.
19
19
  // - A worktree resolves to its parent project for both authorization and
@@ -21,6 +21,7 @@
21
21
 
22
22
  var builtinMates = require("./builtin-mates");
23
23
  var logsStore = require("./project-logs-store");
24
+ var LOG_REF_PATTERN = /^log:[A-Za-z0-9_-]{24}$/;
24
25
 
25
26
  function readOnly() {
26
27
  throw new Error("Project Logs are read-only for builtin Clay.");
@@ -39,6 +40,10 @@ function driverOnly() {
39
40
  throw new Error("Only this project's agent sessions can review feedback or change revisions.");
40
41
  }
41
42
 
43
+ function ownerOnly() {
44
+ throw new Error("Only the project owner can delete Project Logs.");
45
+ }
46
+
42
47
  // Bounded discovery of comments still waiting on the Driver. The store owns the
43
48
  // one-pass scan over every live entry, so nothing here imposes a ceiling that
44
49
  // could hide a pending comment on an older record.
@@ -51,11 +56,17 @@ function attachProjectLogsService(ctx) {
51
56
  var findUserById = context.findUserById || function () { return null; };
52
57
  var storeCache = new Map();
53
58
 
54
- var openStore = context.openStore || function (cwd) {
55
- if (!storeCache.has(cwd)) {
56
- storeCache.set(cwd, logsStore.createProjectLogsStore({ cwd: cwd, baseDir: context.baseDir }));
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
+ }));
57
68
  }
58
- return storeCache.get(cwd);
69
+ return storeCache.get(key);
59
70
  };
60
71
 
61
72
  function projectFor(slug) {
@@ -98,7 +109,26 @@ function attachProjectLogsService(ctx) {
98
109
  function storeForStatus(status) {
99
110
  var governing = effectiveStatus(status);
100
111
  if (!governing || !governing.path) throw new Error("Project Logs are unavailable for this project.");
101
- return openStore(governing.path);
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;
102
132
  }
103
133
 
104
134
  function statusFor(principal, slug) {
@@ -205,6 +235,7 @@ function attachProjectLogsService(ctx) {
205
235
  principal.kind = "user";
206
236
  principal.projectSlug = source.projectSlug;
207
237
  principal.isClay = false;
238
+ principal.isProjectOwner = ownsProject(principal, status);
208
239
  var envelope = { principal: principal, status: status, author: userAuthor(user) };
209
240
  return createProjectBound(envelope, function () { return envelope; });
210
241
  }
@@ -279,40 +310,85 @@ function attachProjectLogsService(ctx) {
279
310
  return revalidate().author;
280
311
  }
281
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
+
282
324
  // A session may write canonically; a user may only comment. The capability
283
325
  // is decided by the binder that produced this envelope, never by a flag on
284
326
  // the request.
285
327
  var canonical = initial.principal.kind === "session";
328
+ var canDelete = canonical || initial.principal.isProjectOwner === true;
286
329
  return {
287
330
  isClay: false,
288
331
  canWrite: canonical,
289
332
  canComment: true,
333
+ canDelete: canDelete,
290
334
  projectSlug: initial.principal.projectSlug,
291
335
  scopeId: store.scopeId,
292
336
  root: store.root,
293
337
  author: initial.author,
294
- listLogs: function (args) { return live().list(args || {}); },
295
- searchLogs: function (args) { return live().search(args || {}); },
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
+ },
296
356
  readLog: function (args) {
297
357
  var entry = live().read(args && args.ref, false);
298
358
  if (!entry) throw new Error("Log entry not found.");
299
359
  return entry;
300
360
  },
301
361
  logHistory: function (args) { return live().history(args && args.ref, args || {}); },
302
- commentLog: function (args) { return live().comment(args && args.ref, args || {}, author()); },
362
+ commentLog: function (args) { return live().comment(args && args.ref, args || {}, author(), mutationContext()); },
303
363
  readLogRevision: function (args) { return live().readRevision(args && args.ref, args && args.revision); },
304
364
  listLogFeedback: canonical ? function (args) { return live().feedback(args || {}); } : driverOnly,
305
- reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author()); } : driverOnly,
365
+ reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author(), mutationContext()); } : driverOnly,
306
366
  revertLog: canonical
307
- ? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author()); }
367
+ ? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author(), mutationContext()); }
308
368
  : driverOnly,
309
- createLog: canonical ? function (args) { return live().create(args || {}, author()); } : agentOnly,
310
- updateLog: canonical ? function (args) { return live().update(args && args.ref, args || {}, author()); } : agentOnly,
311
- linkLog: canonical ? function (args) { return live().link(args && args.ref, args && args.links, author()); } : agentOnly,
312
- removeLog: canonical ? function (args) { return live().remove(args && args.ref, author()); } : agentOnly,
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,
313
382
  };
314
383
  }
315
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
+
316
392
  function createClayBound(revalidate) {
317
393
  function target(args) {
318
394
  var principal = revalidate();
@@ -321,10 +397,38 @@ function attachProjectLogsService(ctx) {
321
397
  return storeForStatus(statusFor(principal, slug));
322
398
  }
323
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
+
324
427
  return {
325
428
  isClay: true,
326
429
  canWrite: false,
327
430
  canComment: false,
431
+ canDelete: false,
328
432
  projectSlug: null,
329
433
  author: null,
330
434
  listLogs: function (args) { return target(args).list(args || {}); },
@@ -337,6 +441,7 @@ function attachProjectLogsService(ctx) {
337
441
  logHistory: function (args) { return target(args).history(args && args.ref, args || {}); },
338
442
  commentLog: readOnly,
339
443
  readLogRevision: function (args) { return target(args).readRevision(args && args.ref, args && args.revision); },
444
+ resolveLogNavigation: resolveLogNavigation,
340
445
  listLogFeedback: readOnly,
341
446
  reviewLogComment: readOnly,
342
447
  revertLog: readOnly,
@@ -351,6 +456,7 @@ function attachProjectLogsService(ctx) {
351
456
  bindProjectSession: bindProjectSession,
352
457
  bindUser: bindUser,
353
458
  bindMate: bindMate,
459
+ setContextState: setContextState,
354
460
  };
355
461
  }
356
462
 
@@ -15,6 +15,7 @@
15
15
  // shape, so history is deterministic across the boundary.
16
16
 
17
17
  var logsSchema = require("./project-logs-schema");
18
+ var logContext = require("./project-log-context");
18
19
 
19
20
  // Ops that advance the canonical record. `comment` and a non-incorporating
20
21
  // `review` are participation, not revisions, and never appear here.
@@ -146,6 +147,7 @@ function revisions(chain, maxLinks) {
146
147
  op: record.op === "review" ? "incorporate" : record.op,
147
148
  at: record.at || 0,
148
149
  author: record.author || null,
150
+ context: record.context || null,
149
151
  snapshot: state,
150
152
  changed: record.op === "create" ? [] : changedFields(previous, state),
151
153
  // Present only on a revert, so a reader can see where it came from.
@@ -176,6 +178,7 @@ function describeRevision(entry) {
176
178
  op: entry.op,
177
179
  at: entry.at,
178
180
  author: entry.author,
181
+ context: logContext.normalizeRecordContext(entry.context),
179
182
  changed: entry.changed,
180
183
  revertedFrom: entry.revertedFrom,
181
184
  reason: entry.reason,
@@ -1,13 +1,4 @@
1
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
2
 
12
3
  var fs = require("fs");
13
4
  var path = require("path");
@@ -19,6 +10,8 @@ var logsSnapshot = require("./project-logs-snapshot");
19
10
  var logsComments = require("./project-logs-comments");
20
11
  var logsVersioning = require("./project-logs-versioning");
21
12
  var logsRoot = require("./project-logs-root");
13
+ var logContext = require("./project-log-context");
14
+ var contextState = require("./project-logs-context-state");
22
15
 
23
16
  var MAX_SUMMARY_CHARS = logsSchema.MAX_SUMMARY_CHARS;
24
17
  var MAX_COMMENT_CHARS = 4000;
@@ -56,18 +49,17 @@ function validateKind(kind, fallback) {
56
49
  function createProjectLogsStore(opts) {
57
50
  var options = opts || {};
58
51
  var root = options.root ? path.resolve(options.root) : logsRoot.resolveProjectRoot(options.cwd, options.runGit);
59
- var scopeId = logsRoot.scopeIdForRoot(root);
52
+ var scopeId = options.projectKnowledgeId
53
+ ? logsRoot.scopeIdForKnowledgeId(options.projectKnowledgeId)
54
+ : logsRoot.scopeIdForRoot(root);
60
55
  var store = recordStore.createRecordStore({ scopeId: scopeId, baseDir: options.baseDir });
56
+ var contexts = contextState.attachContextState(store, scopeId);
61
57
 
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
58
  function refFor(rootId) {
65
59
  var digest = crypto.createHash("sha256").update(scopeId + "\u0000" + String(rootId)).digest("base64url");
66
60
  return "log:" + digest.substring(0, 24);
67
61
  }
68
62
 
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
63
  function chains() {
72
64
  var records = store.all();
73
65
  var byRoot = new Map();
@@ -80,24 +72,19 @@ function createProjectLogsStore(opts) {
80
72
  return byRoot;
81
73
  }
82
74
 
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) {
75
+ function fold(rootId, chain, knownContextStates) {
87
76
  var history = logsSnapshot.revisions(chain, MAX_LINKS);
88
77
  if (!history.started || history.revisions.length === 0) return null;
89
78
  var last = history.revisions[history.revisions.length - 1];
90
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
+ }
91
84
 
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
85
  if (!logsSchema.safeCategory(first.snapshot.category)) return null;
96
86
  if (!first.snapshot.title || !first.snapshot.title.trim()) return null;
97
87
 
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
88
  var revisionForRecordId = {};
102
89
  for (var v = 0; v < history.revisions.length; v++) {
103
90
  var source = history.revisions[v].sourceRecordId;
@@ -128,7 +115,12 @@ function createProjectLogsStore(opts) {
128
115
  deleted: !!history.deleted,
129
116
  deletedAt: history.deleted ? history.deleted.at : null,
130
117
  deletedBy: history.deleted ? history.deleted.author : null,
118
+ context: logContext.normalizeRecordContext(originRecord && originRecord.context),
131
119
  };
120
+ if (entry.context.changeSetId) {
121
+ var states = knownContextStates || contexts.states();
122
+ entry.context.status = states[entry.context.changeSetId] || entry.context.status;
123
+ }
132
124
  // Entries written before summaries existed get a deterministic one derived
133
125
  // from their own body. Nothing on disk is rewritten.
134
126
  entry.summary = logsSchema.fallbackSummary(entry);
@@ -142,8 +134,9 @@ function createProjectLogsStore(opts) {
142
134
 
143
135
  function entries(includeDeleted) {
144
136
  var out = [];
137
+ var states = contexts.states();
145
138
  chains().forEach(function (chain, rootId) {
146
- var entry = fold(rootId, chain);
139
+ var entry = fold(rootId, chain, states);
147
140
  if (!entry) return;
148
141
  if (entry.deleted && !includeDeleted) return;
149
142
  out.push(entry);
@@ -181,7 +174,7 @@ function createProjectLogsStore(opts) {
181
174
  return located;
182
175
  }
183
176
 
184
- function create(input, author) {
177
+ function create(input, author, context) {
185
178
  var data = input || {};
186
179
  var validated = logsSchema.validateNewRecord(data);
187
180
  var title = cleanLine(data.title || "", MAX_TITLE_CHARS);
@@ -213,6 +206,7 @@ function createProjectLogsStore(opts) {
213
206
  links: snapshot.links,
214
207
  snapshot: snapshot,
215
208
  author: normalizeAuthor(author),
209
+ context: logContext.normalizeRecordContext(context),
216
210
  at: Date.now(),
217
211
  });
218
212
  return read(refFor(id), false);
@@ -243,7 +237,7 @@ function createProjectLogsStore(opts) {
243
237
  return logsSnapshot.cloneSnapshot(entry);
244
238
  }
245
239
 
246
- function update(ref, changes, author) {
240
+ function update(ref, changes, author, context) {
247
241
  var located = requireLive(ref);
248
242
  var data = changes || {};
249
243
  var validated = logsSchema.validateRevision(data);
@@ -252,7 +246,7 @@ function createProjectLogsStore(opts) {
252
246
  if (logsSnapshot.sameSnapshot(current, snapshot)) {
253
247
  throw new Error("An update requires at least one changed field.");
254
248
  }
255
- appendRevision(located.rootId, "update", snapshot, author, {});
249
+ appendRevision(located.rootId, "update", snapshot, author, { context: logContext.normalizeRecordContext(context) });
256
250
  return read(ref, false);
257
251
  }
258
252
 
@@ -284,7 +278,7 @@ function createProjectLogsStore(opts) {
284
278
  // Human participation. Append-only for this slice: no edit, no delete, and
285
279
  // never a canonical revision. The author is supplied by the authorization
286
280
  // layer, never by the caller's payload.
287
- function comment(ref, input, author) {
281
+ function comment(ref, input, author, context) {
288
282
  var located = requireLive(ref);
289
283
  var data = input || {};
290
284
  var body = cleanText(data.body || "", MAX_COMMENT_CHARS);
@@ -300,12 +294,13 @@ function createProjectLogsStore(opts) {
300
294
  scope: scopeId,
301
295
  body: body,
302
296
  author: normalizeAuthor(author),
297
+ context: logContext.normalizeRecordContext(context),
303
298
  at: Date.now(),
304
299
  });
305
300
  return read(ref, false);
306
301
  }
307
302
 
308
- function link(ref, links, author) {
303
+ function link(ref, links, author, context) {
309
304
  var located = requireLive(ref);
310
305
  var normalized = normalizeLinks(links);
311
306
  if (normalized.length === 0) throw new Error("At least one link reference is required.");
@@ -321,11 +316,11 @@ function createProjectLogsStore(opts) {
321
316
  if (logsSnapshot.sameSnapshot(current, snapshot)) {
322
317
  throw new Error("Those links are already attached to this entry.");
323
318
  }
324
- appendRevision(located.rootId, "link", snapshot, author, {});
319
+ appendRevision(located.rootId, "link", snapshot, author, { context: logContext.normalizeRecordContext(context) });
325
320
  return read(ref, false);
326
321
  }
327
322
 
328
- function remove(ref, author) {
323
+ function remove(ref, author, context) {
329
324
  var located = requireLive(ref);
330
325
  store.append({
331
326
  id: recordStore.newRecordId(),
@@ -333,6 +328,7 @@ function createProjectLogsStore(opts) {
333
328
  op: "delete",
334
329
  scope: scopeId,
335
330
  author: normalizeAuthor(author),
331
+ context: logContext.normalizeRecordContext(context),
336
332
  at: Date.now(),
337
333
  });
338
334
  return read(ref, true);
@@ -473,6 +469,7 @@ function createProjectLogsStore(opts) {
473
469
  readRevision: versioning.readRevision,
474
470
  link: link,
475
471
  remove: remove,
472
+ setContextState: contexts.set,
476
473
  read: read,
477
474
  list: list,
478
475
  search: search,
@@ -9,6 +9,7 @@ var logsSchema = require("./project-logs-schema");
9
9
  var logsSnapshot = require("./project-logs-snapshot");
10
10
  var logsComments = require("./project-logs-comments");
11
11
  var recordStore = require("./knowledge-record-store");
12
+ var logContext = require("./project-log-context");
12
13
 
13
14
  function attachVersioning(ctx) {
14
15
  var read = ctx.read;
@@ -27,7 +28,7 @@ function attachVersioning(ctx) {
27
28
  // create no canonical revision. `incorporate` appends exactly one record that
28
29
  // both resolves the comment and carries the new canonical snapshot, so the
29
30
  // two can never drift apart.
30
- function review(ref, input, author) {
31
+ function review(ref, input, author, context) {
31
32
  var located = requireLive(ref);
32
33
  var data = input || {};
33
34
  if (!logsComments.isAction(data.action)) {
@@ -56,6 +57,7 @@ function attachVersioning(ctx) {
56
57
  commentId: data.commentId,
57
58
  response: response,
58
59
  author: normalizeAuthor(author),
60
+ context: logContext.normalizeRecordContext(context),
59
61
  at: Date.now(),
60
62
  });
61
63
  return read(ref, false);
@@ -71,13 +73,14 @@ function attachVersioning(ctx) {
71
73
  action: "incorporate",
72
74
  commentId: data.commentId,
73
75
  response: response,
76
+ context: logContext.normalizeRecordContext(context),
74
77
  });
75
78
  return read(ref, false);
76
79
  }
77
80
 
78
81
  // Restore an earlier revision by appending a new one. Later history is never
79
82
  // erased: a revert is itself a revision, with the source recorded.
80
- function revert(ref, revision, reason, author) {
83
+ function revert(ref, revision, reason, author, context) {
81
84
  var located = requireLive(ref);
82
85
  var target = Number(revision);
83
86
  if (!Number.isInteger(target) || target < 1) throw new Error("A revision number is required.");
@@ -96,6 +99,7 @@ function attachVersioning(ctx) {
96
99
  appendRevision(located.rootId, "revert", logsSnapshot.cloneSnapshot(source.snapshot), author, {
97
100
  revertedFrom: target,
98
101
  reason: trimmedReason,
102
+ context: logContext.normalizeRecordContext(context),
99
103
  });
100
104
  return read(ref, false);
101
105
  }