// Stable project knowledge and Git worktree change-set identities. // // Existing projects freeze their current path-derived scope so every opaque // log ref remains valid. New projects receive a path-independent id. A linked // worktree stores its generated change-set marker in Git's administrative // directory, which survives daemon restarts and branch renames but disappears // with the worktree itself; the durable log records retain the id afterward. var crypto = require("crypto"); var fs = require("fs"); var path = require("path"); var { runGitSync } = require("./git-cli"); var { fsAsUser } = require("./os-users"); var logsRoot = require("./project-logs-root"); var utils = require("./utils"); var ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/; function newId(prefix) { return prefix + "_" + crypto.randomBytes(16).toString("base64url"); } function validId(value, prefix) { return typeof value === "string" && ID_PATTERN.test(value) && (!prefix || value.indexOf(prefix + "_") === 0); } function legacyKnowledgeId(cwd) { return utils.encodeCwd(logsRoot.resolveProjectRoot(cwd)); } function ensureProjectKnowledgeId(project) { if (!project || typeof project !== "object") return false; if (validId(project.projectKnowledgeId)) return false; project.projectKnowledgeId = project.path ? legacyKnowledgeId(project.path) : newId("pk"); return true; } function initializeProjectKnowledge(config) { var changed = false; var projects = config && Array.isArray(config.projects) ? config.projects : []; for (var i = 0; i < projects.length; i++) { if (ensureProjectKnowledgeId(projects[i])) changed = true; } return changed; } function createProjectKnowledgeId() { return newId("pk"); } function git(cwd, args, osUserInfo) { return runGitSync(cwd, args, null, osUserInfo).trim(); } function commit(cwd, ref, osUserInfo) { try { return git(cwd, ["rev-parse", ref || "HEAD"], osUserInfo); } catch (e) { return null; } } function markerPath(cwd, osUserInfo) { try { var gitDir = git(cwd, ["rev-parse", "--git-dir"], osUserInfo); return path.join(path.resolve(cwd, gitDir), "clay-change-set.json"); } catch (e) { return null; } } function readMarker(filePath, knowledgeId) { if (!filePath) return null; try { var parsed = JSON.parse(fs.readFileSync(filePath, "utf8")); if (!parsed || parsed.projectKnowledgeId !== knowledgeId || !validId(parsed.changeSetId, "cs")) return null; return parsed; } catch (e) { return null; } } function deterministicFallback(knowledgeId, cwd) { var digest = crypto.createHash("sha256").update(knowledgeId + "\u0000" + logsRoot.canonicalPath(cwd)).digest("base64url"); return "cs_" + digest.substring(0, 22); } function writeMarker(filePath, marker, osUserInfo) { var content = JSON.stringify(marker, null, 2) + "\n"; if (osUserInfo) { fsAsUser("write", { file: filePath, content: content, flag: "wx", mode: 0o600 }, osUserInfo); return; } fs.writeFileSync(filePath, content, { flag: "wx", mode: 0o600 }); } function resolveWorktreeContext(cwd, knowledgeId, details, osUserInfo) { var input = details || {}; var filePath = markerPath(cwd, osUserInfo); var marker = readMarker(filePath, knowledgeId); if (!marker) { marker = { projectKnowledgeId: knowledgeId, changeSetId: newId("cs"), baseCommit: input.baseCommit || null, createdAt: Date.now(), }; if (filePath) { try { writeMarker(filePath, marker, osUserInfo); } catch (e) { marker = readMarker(filePath, knowledgeId) || marker; } } } if (!filePath || !readMarker(filePath, knowledgeId)) { marker.changeSetId = deterministicFallback(knowledgeId, cwd); } return { kind: "worktree", changeSetId: marker.changeSetId, branch: input.branch || null, baseCommit: marker.baseCommit || input.baseCommit || null, headCommit: commit(cwd, "HEAD", osUserInfo), status: "active", }; } function isMerged(parentCwd, worktreeCwd, osUserInfo) { var head = commit(worktreeCwd, "HEAD", osUserInfo); if (!head) return false; try { runGitSync(parentCwd, ["merge-base", "--is-ancestor", head, "HEAD"], null, osUserInfo); return true; } catch (e) { return false; } } function cleanCommit(value) { return typeof value === "string" && /^[0-9a-f]{7,64}$/i.test(value) ? value : null; } function cleanBranch(value) { if (typeof value !== "string") return null; var cleaned = value.trim().substring(0, 200); return cleaned && cleaned.indexOf("\n") === -1 && cleaned.indexOf("\r") === -1 ? cleaned : null; } function normalizeRecordContext(value) { if (!value || value.kind !== "worktree" || !validId(value.changeSetId, "cs")) { return { kind: "project", changeSetId: null, branch: null, baseCommit: null, headCommit: null, status: "active" }; } return { kind: "worktree", changeSetId: value.changeSetId, branch: cleanBranch(value.branch), baseCommit: cleanCommit(value.baseCommit), headCommit: cleanCommit(value.headCommit), status: value.status === "merged" || value.status === "archived" ? value.status : "active", }; } module.exports = { ID_PATTERN: ID_PATTERN, validId: validId, legacyKnowledgeId: legacyKnowledgeId, ensureProjectKnowledgeId: ensureProjectKnowledgeId, initializeProjectKnowledge: initializeProjectKnowledge, createProjectKnowledgeId: createProjectKnowledgeId, resolveWorktreeContext: resolveWorktreeContext, commit: commit, isMerged: isMerged, normalizeRecordContext: normalizeRecordContext, };