clay-server 4.0.0-beta.2 → 4.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/background-task-timing.js +105 -0
- package/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +14 -1
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-vendor-login.js +20 -3
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +178 -6
- package/lib/public/app.js +58 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +163 -28
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/index.html +45 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +109 -4
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +84 -7
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +46 -4
- package/lib/public/modules/app-projects.js +13 -8
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/capsule-preference.js +44 -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/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +21 -1
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +132 -46
- package/lib/sdk-message-processor.js +60 -14
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +124 -18
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +39 -0
- package/lib/sessions.js +61 -11
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +2 -2
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +81 -4
- package/lib/yoke/adapters/codex.js +63 -15
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Query, ranking, and pagination for Project Logs.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from project-logs-store.js so the store stays focused on records
|
|
4
|
+
// and stays inside the module size limit. Ranking uses the shared BM25 adapter,
|
|
5
|
+
// which wraps Clay's single ranking engine; the response shapes, filters,
|
|
6
|
+
// pagination, and tie-breaks are exactly what the store returned before.
|
|
7
|
+
|
|
8
|
+
var knowledgeSearch = require("./knowledge-search");
|
|
9
|
+
|
|
10
|
+
var MAX_PAGE = 50;
|
|
11
|
+
var DEFAULT_PAGE = 20;
|
|
12
|
+
var MAX_SNIPPET_CHARS = 320;
|
|
13
|
+
|
|
14
|
+
// Title and tags outrank body text, expressed as repetition for BM25.
|
|
15
|
+
var TITLE_WEIGHT = 3;
|
|
16
|
+
// A summary is a human-written distillation of the whole record, so it ranks
|
|
17
|
+
// between the title and the raw body.
|
|
18
|
+
var SUMMARY_WEIGHT = 2;
|
|
19
|
+
var TAG_WEIGHT = 2;
|
|
20
|
+
var BODY_WEIGHT = 1;
|
|
21
|
+
|
|
22
|
+
// Locale-independent lexical order over UTF-16 code units.
|
|
23
|
+
//
|
|
24
|
+
// An opaque ref is base64url, so it contains both "-" (0x2D) and "_" (0x5F).
|
|
25
|
+
// String.prototype.localeCompare applies collation rules that order those two
|
|
26
|
+
// the other way round from code-unit order, and the result varies by ICU data,
|
|
27
|
+
// so it cannot be the basis of a documented deterministic tie-break. This
|
|
28
|
+
// matches the default Array.prototype.sort ordering exactly.
|
|
29
|
+
function compareRefs(a, b) {
|
|
30
|
+
var left = String(a);
|
|
31
|
+
var right = String(b);
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseLimit(value) {
|
|
36
|
+
var limit = Number(value);
|
|
37
|
+
if (!Number.isFinite(limit)) limit = DEFAULT_PAGE;
|
|
38
|
+
return Math.max(1, Math.min(MAX_PAGE, Math.floor(limit)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function encodeCursor(offset) {
|
|
42
|
+
return offset > 0 ? Buffer.from(String(offset), "utf8").toString("base64url") : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function decodeCursor(value) {
|
|
46
|
+
if (!value) return 0;
|
|
47
|
+
try {
|
|
48
|
+
var parsed = Number(Buffer.from(String(value), "base64url").toString("utf8"));
|
|
49
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function page(items, args) {
|
|
56
|
+
var offset = decodeCursor(args && args.cursor);
|
|
57
|
+
var limit = parseLimit(args && args.limit);
|
|
58
|
+
return {
|
|
59
|
+
items: items.slice(offset, offset + limit),
|
|
60
|
+
nextCursor: offset + limit < items.length ? encodeCursor(offset + limit) : null,
|
|
61
|
+
total: items.length,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
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
|
+
|
|
78
|
+
function listEntries(entries, args, cleanLine, validateKind) {
|
|
79
|
+
var options = args || {};
|
|
80
|
+
var kind = options.kind ? validateKind(options.kind, null) : null;
|
|
81
|
+
var tag = options.tag ? cleanLine(String(options.tag), 40).toLowerCase() : null;
|
|
82
|
+
var priority = options.priority ? String(options.priority) : null;
|
|
83
|
+
var filtered = entries.filter(function (entry) {
|
|
84
|
+
// Exact match on the project's own vocabulary. A well-formed category this
|
|
85
|
+
// project has never used simply matches nothing.
|
|
86
|
+
if (kind && entry.category !== kind) return false;
|
|
87
|
+
if (priority && entry.priority !== priority) return false;
|
|
88
|
+
if (tag && entry.tags.indexOf(tag) === -1) return false;
|
|
89
|
+
if (!matchesContext(entry, options)) return false;
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
var result = page(filtered, options);
|
|
93
|
+
return {
|
|
94
|
+
entries: result.items.map(summarize),
|
|
95
|
+
nextCursor: result.nextCursor,
|
|
96
|
+
total: result.total,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function projectEntry(entry) {
|
|
101
|
+
return {
|
|
102
|
+
id: entry.ref,
|
|
103
|
+
fields: [
|
|
104
|
+
{ text: entry.title, weight: TITLE_WEIGHT },
|
|
105
|
+
{ text: entry.summary || "", weight: SUMMARY_WEIGHT },
|
|
106
|
+
{ text: (entry.tags || []).join(" "), weight: TAG_WEIGHT },
|
|
107
|
+
{ text: entry.body, weight: BODY_WEIGHT },
|
|
108
|
+
],
|
|
109
|
+
meta: entry,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The ledger row. Everything needed to understand an entry at a glance, and
|
|
114
|
+
// never the body: a list must not dump record contents.
|
|
115
|
+
function summarize(entry) {
|
|
116
|
+
return {
|
|
117
|
+
ref: entry.ref,
|
|
118
|
+
kind: entry.kind,
|
|
119
|
+
category: entry.category,
|
|
120
|
+
priority: entry.priority,
|
|
121
|
+
title: entry.title,
|
|
122
|
+
summary: entry.summary || "",
|
|
123
|
+
tags: entry.tags,
|
|
124
|
+
createdAt: entry.createdAt,
|
|
125
|
+
createdBy: entry.createdBy,
|
|
126
|
+
updatedAt: entry.updatedAt,
|
|
127
|
+
updatedBy: entry.updatedBy,
|
|
128
|
+
revisions: entry.revisions,
|
|
129
|
+
commentCount: entry.commentCount || 0,
|
|
130
|
+
// How many comments still await Driver review. A count only: the ledger
|
|
131
|
+
// never carries comment bodies.
|
|
132
|
+
pendingFeedbackCount: entry.pendingFeedbackCount || 0,
|
|
133
|
+
context: entry.context || null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function searchEntries(entries, args, cleanLine, validateKind) {
|
|
138
|
+
var options = args || {};
|
|
139
|
+
var query = cleanLine(options.query || "", 200);
|
|
140
|
+
if (!query) throw new Error("A search query is required.");
|
|
141
|
+
var kind = options.kind ? validateKind(options.kind, null) : null;
|
|
142
|
+
var priority = options.priority ? String(options.priority) : null;
|
|
143
|
+
var candidates = entries.filter(function (entry) {
|
|
144
|
+
if (kind && entry.category !== kind) return false;
|
|
145
|
+
if (priority && entry.priority !== priority) return false;
|
|
146
|
+
if (!matchesContext(entry, options)) return false;
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
var ranked = knowledgeSearch.rank(candidates, query, projectEntry, candidates.length);
|
|
151
|
+
var hits = [];
|
|
152
|
+
for (var i = 0; i < ranked.length; i++) {
|
|
153
|
+
var entry = ranked[i].meta;
|
|
154
|
+
var hit = summarize(entry);
|
|
155
|
+
hit.score = ranked[i].score;
|
|
156
|
+
hit.snippet = cleanLine(knowledgeSearch.snippet(entry.body, query, MAX_SNIPPET_CHARS), MAX_SNIPPET_CHARS);
|
|
157
|
+
hits.push(hit);
|
|
158
|
+
}
|
|
159
|
+
// Deterministic ordering: score, then most recent, then a stable ref.
|
|
160
|
+
hits.sort(function (a, b) {
|
|
161
|
+
return b.score - a.score || b.updatedAt - a.updatedAt || compareRefs(a.ref, b.ref);
|
|
162
|
+
});
|
|
163
|
+
var result = page(hits, options);
|
|
164
|
+
return { results: result.items, nextCursor: result.nextCursor, total: result.total };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
compareRefs: compareRefs,
|
|
169
|
+
MAX_PAGE: MAX_PAGE,
|
|
170
|
+
DEFAULT_PAGE: DEFAULT_PAGE,
|
|
171
|
+
MAX_SNIPPET_CHARS: MAX_SNIPPET_CHARS,
|
|
172
|
+
TITLE_WEIGHT: TITLE_WEIGHT,
|
|
173
|
+
SUMMARY_WEIGHT: SUMMARY_WEIGHT,
|
|
174
|
+
TAG_WEIGHT: TAG_WEIGHT,
|
|
175
|
+
BODY_WEIGHT: BODY_WEIGHT,
|
|
176
|
+
parseLimit: parseLimit,
|
|
177
|
+
encodeCursor: encodeCursor,
|
|
178
|
+
decodeCursor: decodeCursor,
|
|
179
|
+
page: page,
|
|
180
|
+
contextMode: contextMode,
|
|
181
|
+
matchesContext: matchesContext,
|
|
182
|
+
summarize: summarize,
|
|
183
|
+
listEntries: listEntries,
|
|
184
|
+
searchEntries: searchEntries,
|
|
185
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Project root resolution for Project Logs storage.
|
|
2
|
+
//
|
|
3
|
+
// Split from project-logs-store.js so the store stays inside the module size
|
|
4
|
+
// limit. A worktree shares its parent project's Logs: the store root is
|
|
5
|
+
// resolved through Git's common directory, so a worktree session and a parent
|
|
6
|
+
// session read and write the same record file instead of forking knowledge.
|
|
7
|
+
|
|
8
|
+
var fs = require("fs");
|
|
9
|
+
var path = require("path");
|
|
10
|
+
var execFileSync = require("child_process").execFileSync;
|
|
11
|
+
var utils = require("./utils");
|
|
12
|
+
|
|
13
|
+
var _rootCache = new Map();
|
|
14
|
+
|
|
15
|
+
function defaultGit(cwd, args) {
|
|
16
|
+
return execFileSync("git", args, {
|
|
17
|
+
cwd: cwd,
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
timeout: 5000,
|
|
20
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Symlinked paths must collapse to one identity. Git reports a worktree's
|
|
25
|
+
// common directory in real terms while a project path may arrive symlinked
|
|
26
|
+
// (/var vs /private/var on macOS), and two spellings of one root would fork
|
|
27
|
+
// the Logs, which is precisely what worktree sharing exists to prevent.
|
|
28
|
+
function canonicalPath(value) {
|
|
29
|
+
var resolved = path.resolve(value);
|
|
30
|
+
try {
|
|
31
|
+
return fs.realpathSync.native(resolved);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A worktree's common directory points at the parent checkout's .git, so its
|
|
38
|
+
// parent working tree is that directory's parent. Anything that is not an
|
|
39
|
+
// ordinary .git directory (bare-backed worktrees, non-Git folders) falls back
|
|
40
|
+
// to the path itself rather than guessing.
|
|
41
|
+
function resolveProjectRoot(cwd, runGit) {
|
|
42
|
+
var resolved = canonicalPath(cwd);
|
|
43
|
+
if (!runGit && _rootCache.has(resolved)) return _rootCache.get(resolved);
|
|
44
|
+
var root = resolved;
|
|
45
|
+
try {
|
|
46
|
+
var commonDir = (runGit || defaultGit)(resolved, ["rev-parse", "--git-common-dir"]);
|
|
47
|
+
var absoluteCommon = path.resolve(resolved, String(commonDir || "").trim());
|
|
48
|
+
if (path.basename(absoluteCommon) === ".git") {
|
|
49
|
+
var parent = path.dirname(absoluteCommon);
|
|
50
|
+
if (fs.statSync(parent).isDirectory()) root = canonicalPath(parent);
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
root = resolved;
|
|
54
|
+
}
|
|
55
|
+
if (!runGit) _rootCache.set(resolved, root);
|
|
56
|
+
return root;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scopeIdForRoot(root) {
|
|
60
|
+
return "project/" + utils.encodeCwd(root);
|
|
61
|
+
}
|
|
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
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
resolveProjectRoot: resolveProjectRoot,
|
|
72
|
+
scopeIdForRoot: scopeIdForRoot,
|
|
73
|
+
scopeIdForKnowledgeId: scopeIdForKnowledgeId,
|
|
74
|
+
canonicalPath: canonicalPath,
|
|
75
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Vocabulary and validation for Project Logs.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from project-logs-store.js so the store stays focused on records
|
|
4
|
+
// and stays inside the module size limit.
|
|
5
|
+
//
|
|
6
|
+
// Two independent axes, deliberately:
|
|
7
|
+
// category - what kind of record this is, a project-local vocabulary
|
|
8
|
+
// priority - how much it matters, a stable global enum
|
|
9
|
+
// Conflating them is what made "an urgent decision" unrepresentable before.
|
|
10
|
+
//
|
|
11
|
+
// Category is NOT a global enum. Every project evolves its own vocabulary: an
|
|
12
|
+
// agent reuses an established category when one fits and coins a new one when
|
|
13
|
+
// the project needs a durable distinction it does not yet have. What is fixed
|
|
14
|
+
// is only the shape of a category label, not the set of labels. The seeds
|
|
15
|
+
// below are guidance for a project with no history yet, never a whitelist.
|
|
16
|
+
|
|
17
|
+
// Suggested starting points, surfaced in tool guidance only.
|
|
18
|
+
var SEED_CATEGORIES = [
|
|
19
|
+
"decision",
|
|
20
|
+
"security",
|
|
21
|
+
"idea",
|
|
22
|
+
"operations",
|
|
23
|
+
"incident",
|
|
24
|
+
"investigation",
|
|
25
|
+
"progress",
|
|
26
|
+
"reference",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
var MAX_CATEGORY_CHARS = 32;
|
|
30
|
+
// Letters and digits in any script, so a project can keep its vocabulary in the
|
|
31
|
+
// language it actually works in. Normalization runs server-side only, so this
|
|
32
|
+
// depends on Node's Unicode property escapes and never on a browser baseline.
|
|
33
|
+
var CATEGORY_PATTERN = /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u;
|
|
34
|
+
var CATEGORY_STRIP = /[^\p{L}\p{N}-]/gu;
|
|
35
|
+
|
|
36
|
+
// Length is counted in code points, not UTF-16 units, so a CJK or Hangul label
|
|
37
|
+
// is bounded the same way a Latin one is.
|
|
38
|
+
function codePointLength(value) {
|
|
39
|
+
return Array.from(value).length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deterministic normalization to a lowercase, hyphen-separated label. Anything
|
|
43
|
+
// that cannot be reduced to a safe label is rejected rather than silently
|
|
44
|
+
// coerced, so a caller is told when its input was unusable instead of getting a
|
|
45
|
+
// surprising slug.
|
|
46
|
+
//
|
|
47
|
+
// Path and identity shapes are refused outright: a category is dry metadata and
|
|
48
|
+
// must never be able to carry a slug, a path segment, or a user id.
|
|
49
|
+
function normalizeCategory(value) {
|
|
50
|
+
if (typeof value !== "string") throw new Error("A log category must be a string.");
|
|
51
|
+
var raw = value.normalize("NFC").trim();
|
|
52
|
+
if (!raw) throw new Error("A log category is required.");
|
|
53
|
+
if (codePointLength(raw) > MAX_CATEGORY_CHARS * 2) {
|
|
54
|
+
throw new Error("A log category must be " + MAX_CATEGORY_CHARS + " characters or fewer.");
|
|
55
|
+
}
|
|
56
|
+
if (/[\/\\]/.test(raw) || raw.indexOf("..") !== -1) {
|
|
57
|
+
throw new Error("A log category cannot contain path characters.");
|
|
58
|
+
}
|
|
59
|
+
var slug = raw
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[_\s]+/g, "-")
|
|
62
|
+
.replace(CATEGORY_STRIP, "")
|
|
63
|
+
.replace(/-+/g, "-")
|
|
64
|
+
.replace(/^-|-$/g, "");
|
|
65
|
+
if (!slug) throw new Error("A log category must contain letters or digits: " + value);
|
|
66
|
+
if (codePointLength(slug) > MAX_CATEGORY_CHARS) {
|
|
67
|
+
throw new Error("A log category must be " + MAX_CATEGORY_CHARS + " characters or fewer.");
|
|
68
|
+
}
|
|
69
|
+
if (!CATEGORY_PATTERN.test(slug)) throw new Error("Invalid log category: " + value);
|
|
70
|
+
return slug;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Non-throwing form for reading stored records, where a corrupt on-disk value
|
|
74
|
+
// should make that record unusable rather than break the whole projection.
|
|
75
|
+
// Filters deliberately do NOT use this: a caller that supplies a malformed
|
|
76
|
+
// category is told so.
|
|
77
|
+
function safeCategory(value) {
|
|
78
|
+
try {
|
|
79
|
+
return normalizeCategory(value);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var PRIORITIES = ["normal", "important", "urgent"];
|
|
86
|
+
var DEFAULT_PRIORITY = "normal";
|
|
87
|
+
|
|
88
|
+
var MAX_SUMMARY_CHARS = 400;
|
|
89
|
+
|
|
90
|
+
function isPriority(value) {
|
|
91
|
+
return PRIORITIES.indexOf(value) !== -1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizePriority(value) {
|
|
95
|
+
return isPriority(value) ? value : DEFAULT_PRIORITY;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A summary for an entry written before summaries existed. Deterministic and
|
|
99
|
+
// derived from stored content only, so the same record always yields the same
|
|
100
|
+
// text and nothing is invented.
|
|
101
|
+
function fallbackSummary(entry) {
|
|
102
|
+
if (!entry) return "";
|
|
103
|
+
if (typeof entry.summary === "string" && entry.summary.trim()) {
|
|
104
|
+
return entry.summary.trim().substring(0, MAX_SUMMARY_CHARS);
|
|
105
|
+
}
|
|
106
|
+
// Prefer the first line of prose. A leading Markdown heading usually just
|
|
107
|
+
// restates the title, so it is only used when there is nothing else.
|
|
108
|
+
var body = typeof entry.body === "string" ? entry.body : "";
|
|
109
|
+
var lines = body.split("\n");
|
|
110
|
+
var heading = "";
|
|
111
|
+
var firstLine = "";
|
|
112
|
+
for (var i = 0; i < lines.length; i++) {
|
|
113
|
+
var raw = lines[i].trim();
|
|
114
|
+
if (!raw) continue;
|
|
115
|
+
var isHeading = raw.charAt(0) === "#";
|
|
116
|
+
var line = raw.replace(/^#+\s*/, "").replace(/^[-*>]\s*/, "").trim();
|
|
117
|
+
if (!line) continue;
|
|
118
|
+
if (isHeading) {
|
|
119
|
+
if (!heading) heading = line;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
firstLine = line;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if (!firstLine) firstLine = heading;
|
|
126
|
+
if (!firstLine) return "";
|
|
127
|
+
return firstLine.length > MAX_SUMMARY_CHARS
|
|
128
|
+
? firstLine.substring(0, MAX_SUMMARY_CHARS)
|
|
129
|
+
: firstLine;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validation for a new agent-written record. Category, title, and summary are
|
|
133
|
+
// all required: a log without a summary cannot be understood from a list, and
|
|
134
|
+
// that is the whole point of the surface.
|
|
135
|
+
function validateNewRecord(input) {
|
|
136
|
+
var data = input || {};
|
|
137
|
+
var category = normalizeCategory(data.category || data.kind);
|
|
138
|
+
if (typeof data.title !== "string" || !data.title.trim()) throw new Error("A log title is required.");
|
|
139
|
+
if (typeof data.summary !== "string" || !data.summary.trim()) {
|
|
140
|
+
throw new Error("A one or two sentence summary is required so the entry is understandable from a list.");
|
|
141
|
+
}
|
|
142
|
+
// A mistyped priority is refused rather than silently downgraded to normal:
|
|
143
|
+
// an agent that meant "urgent" must not get "normal" without being told.
|
|
144
|
+
if (data.priority !== undefined && data.priority !== null && !isPriority(data.priority)) {
|
|
145
|
+
throw new Error("Unknown log priority: " + data.priority);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
category: category,
|
|
149
|
+
priority: normalizePriority(data.priority),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validation for a revision. The category may move to another writable value
|
|
154
|
+
// or stay on its legacy one; nothing is required, but nothing may be emptied.
|
|
155
|
+
function validateRevision(input) {
|
|
156
|
+
var data = input || {};
|
|
157
|
+
var out = {};
|
|
158
|
+
if (data.category !== undefined || data.kind !== undefined) {
|
|
159
|
+
out.category = normalizeCategory(data.category || data.kind);
|
|
160
|
+
}
|
|
161
|
+
if (data.priority !== undefined) {
|
|
162
|
+
if (!isPriority(data.priority)) throw new Error("Unknown log priority: " + data.priority);
|
|
163
|
+
out.priority = data.priority;
|
|
164
|
+
}
|
|
165
|
+
if (data.title !== undefined) {
|
|
166
|
+
if (typeof data.title !== "string" || !data.title.trim()) throw new Error("A log title cannot be emptied.");
|
|
167
|
+
out.title = data.title;
|
|
168
|
+
}
|
|
169
|
+
if (data.summary !== undefined) {
|
|
170
|
+
if (typeof data.summary !== "string" || !data.summary.trim()) throw new Error("A log summary cannot be emptied.");
|
|
171
|
+
out.summary = data.summary;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
// --- Normalization -------------------------------------------------------
|
|
178
|
+
//
|
|
179
|
+
// Moved here from project-logs-store.js so the store stays inside the module
|
|
180
|
+
// size limit. These are pure text and shape helpers with no record semantics.
|
|
181
|
+
|
|
182
|
+
var MAX_TAGS = 12;
|
|
183
|
+
var MAX_TAG_CHARS = 40;
|
|
184
|
+
var MAX_LINKS = 20;
|
|
185
|
+
var MAX_LINK_CHARS = 200;
|
|
186
|
+
|
|
187
|
+
function cleanText(value, max) {
|
|
188
|
+
if (typeof value !== "string") return "";
|
|
189
|
+
var text = value.replace(/\u0000/g, "").trim();
|
|
190
|
+
return text.length > max ? text.substring(0, max) : text;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cleanLine(value, max) {
|
|
194
|
+
return cleanText(value, max).replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeAuthor(author) {
|
|
198
|
+
var source = author || {};
|
|
199
|
+
var type = source.type === "user" ? "user" : "session";
|
|
200
|
+
return {
|
|
201
|
+
type: type,
|
|
202
|
+
userId: source.userId ? cleanLine(String(source.userId), 120) : null,
|
|
203
|
+
displayName: cleanLine(source.displayName || "", 120) || null,
|
|
204
|
+
sessionKey: source.sessionKey ? cleanLine(String(source.sessionKey), 200) : null,
|
|
205
|
+
vendor: cleanLine(source.vendor || "", 40) || null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeTags(tags) {
|
|
210
|
+
if (!Array.isArray(tags)) return [];
|
|
211
|
+
var out = [];
|
|
212
|
+
for (var i = 0; i < tags.length && out.length < MAX_TAGS; i++) {
|
|
213
|
+
var tag = cleanLine(String(tags[i] == null ? "" : tags[i]), MAX_TAG_CHARS).toLowerCase();
|
|
214
|
+
if (tag && out.indexOf(tag) === -1) out.push(tag);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeLinks(links) {
|
|
220
|
+
if (!Array.isArray(links)) return [];
|
|
221
|
+
var out = [];
|
|
222
|
+
for (var i = 0; i < links.length && out.length < MAX_LINKS; i++) {
|
|
223
|
+
var raw = links[i];
|
|
224
|
+
var value = typeof raw === "string" ? { ref: raw } : (raw || {});
|
|
225
|
+
var ref = cleanLine(String(value.ref || value.sessionRef || ""), MAX_LINK_CHARS);
|
|
226
|
+
if (!ref) continue;
|
|
227
|
+
var label = cleanLine(value.label || "", MAX_LINK_CHARS) || null;
|
|
228
|
+
var duplicate = false;
|
|
229
|
+
for (var j = 0; j < out.length; j++) {
|
|
230
|
+
if (out[j].ref === ref) { duplicate = true; break; }
|
|
231
|
+
}
|
|
232
|
+
if (!duplicate) out.push({ ref: ref, label: label });
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
SEED_CATEGORIES: SEED_CATEGORIES,
|
|
239
|
+
MAX_CATEGORY_CHARS: MAX_CATEGORY_CHARS,
|
|
240
|
+
CATEGORY_PATTERN: CATEGORY_PATTERN,
|
|
241
|
+
codePointLength: codePointLength,
|
|
242
|
+
normalizeCategory: normalizeCategory,
|
|
243
|
+
safeCategory: safeCategory,
|
|
244
|
+
PRIORITIES: PRIORITIES,
|
|
245
|
+
DEFAULT_PRIORITY: DEFAULT_PRIORITY,
|
|
246
|
+
MAX_SUMMARY_CHARS: MAX_SUMMARY_CHARS,
|
|
247
|
+
isPriority: isPriority,
|
|
248
|
+
normalizePriority: normalizePriority,
|
|
249
|
+
fallbackSummary: fallbackSummary,
|
|
250
|
+
MAX_TAGS: MAX_TAGS,
|
|
251
|
+
MAX_LINKS: MAX_LINKS,
|
|
252
|
+
cleanText: cleanText,
|
|
253
|
+
cleanLine: cleanLine,
|
|
254
|
+
normalizeAuthor: normalizeAuthor,
|
|
255
|
+
normalizeTags: normalizeTags,
|
|
256
|
+
normalizeLinks: normalizeLinks,
|
|
257
|
+
validateNewRecord: validateNewRecord,
|
|
258
|
+
validateRevision: validateRevision,
|
|
259
|
+
};
|