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.
- package/lib/daemon-projects.js +10 -5
- package/lib/daemon.js +61 -10
- package/lib/project-log-context.js +171 -0
- package/lib/project-log-feedback-delivery.js +47 -5
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +15 -4
- package/lib/project-logs-query.js +18 -0
- package/lib/project-logs-root.js +8 -0
- package/lib/project-logs-service.js +121 -15
- package/lib/project-logs-snapshot.js +3 -0
- package/lib/project-logs-store.js +29 -32
- package/lib/project-logs-versioning.js +6 -2
- package/lib/project-logs.js +61 -13
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-user-message.js +11 -0
- package/lib/project.js +6 -1
- package/lib/public/app.js +5 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/project-logs.css +20 -2
- package/lib/public/css/sticky-notes.css +7 -0
- package/lib/public/index.html +2 -2
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +22 -3
- package/lib/public/modules/app-messages.js +29 -1
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/input.js +6 -1
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/project-logs-render.js +48 -2
- package/lib/public/modules/project-logs.js +140 -11
- package/lib/public/modules/sidebar-session-hierarchy.js +16 -7
- package/lib/public/modules/sidebar-sessions.js +3 -1
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +2 -0
- package/lib/public/modules/sticky-notes-card.js +2 -0
- package/lib/public/modules/sticky-notes-editor.js +3 -0
- package/lib/public/modules/sticky-notes.js +2 -0
- package/lib/public/style.css +1 -1
- package/lib/sdk-bridge.js +20 -10
- package/lib/server-global-ws.js +4 -0
- package/lib/server-home-chat-events.js +3 -0
- package/lib/server-home-chat.js +9 -2
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server.js +14 -3
- package/lib/ws-schema.js +8 -2
- 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,
|
package/lib/project-logs-root.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
56
|
-
|
|
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(
|
|
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) {
|
|
295
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|