UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

859 lines (852 loc) 23.6 kB
import { __esm, __export, __require } from "./chunk-2ESYSVXG.js"; // src/links/parser.ts function parseFileRefs(content, filePath) { const ext = filePath.split(".").pop()?.toLowerCase() ?? ""; if (ext === "md") { return parseMarkdownRefs(content, filePath); } return parseDocCommentRefs(content, filePath, ext); } function parseMarkdownRefs(content, filePath) { return extractRefs(content, filePath, "markdown"); } function parseDocCommentRefs(content, filePath, ext) { const fileExt = ext ?? (filePath.split(".").pop()?.toLowerCase() ?? ""); const commentBlocks = extractDocComments(content, fileExt); const refs = []; for (const block of commentBlocks) { const blockRefs = extractRefs(block.text, filePath, block.context, block.startLine); refs.push(...blockRefs); } return refs; } function extractRefs(text, filePath, context, lineOffset = 0) { const refs = []; const lines = text.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; let match; const re = new RegExp(WIKI_LINK_RE.source, WIKI_LINK_RE.flags); while ((match = re.exec(line)) !== null) { const raw = match[1]; const col = match.index; const lineNum = i + 1 + lineOffset; const parsed = parseRefContent(raw); if (!parsed) continue; refs.push({ ...parsed, raw, source: { filePath, line: lineNum, col, context } }); } } return refs; } function parseRefContent(raw) { if (!raw || !raw.trim()) return null; let content; let alias; const pipeIdx = raw.indexOf("|"); if (pipeIdx !== -1) { content = raw.substring(0, pipeIdx).trim(); alias = raw.substring(pipeIdx + 1).trim(); if (!alias) alias = void 0; } else { content = raw.trim(); } if (!content) return null; const colonIdx = content.indexOf(":"); if (colonIdx !== -1) { const possibleNs = content.substring(0, colonIdx); if (isValidNamespace(possibleNs)) { const rest = content.substring(colonIdx + 1); const { target: target2, anchor: anchor2 } = splitAnchor(rest); const ns = possibleNs; return { namespace: ns, target: target2, anchor: anchor2, alias }; } } const { target, anchor } = splitAnchor(content); const inferred = inferNamespace(target, anchor); if (!inferred) return null; return { namespace: inferred, target, anchor, alias }; } function splitAnchor(content) { const hashIdx = content.indexOf("#"); if (hashIdx === -1) return { target: content }; return { target: content.substring(0, hashIdx), anchor: content.substring(hashIdx + 1) || void 0 }; } function isValidNamespace(s) { return VALID_NAMESPACES.has(s); } function inferNamespace(target, anchor) { if (/^TRL-\d+$/i.test(target)) return "issue"; if (/^DEC-\d+$/i.test(target)) return "decision"; if (anchor) return "symbol"; if (target.includes("/")) return "file"; const ext = target.split(".").pop()?.toLowerCase(); if (ext && CODE_EXTENSIONS.has(ext)) return "file"; return null; } function extractDocComments(content, ext) { switch (ext) { case "ts": case "tsx": case "js": case "jsx": case "mjs": case "cjs": case "java": case "cs": return extractJSDocComments(content); case "py": case "pyi": return extractPythonDocstrings(content); case "rs": return extractRustDocComments(content); case "go": return extractGoDocComments(content); case "rb": return extractRubyDocComments(content); default: return []; } } function extractJSDocComments(content) { const blocks = []; const lines = content.split("\n"); let inBlock = false; let blockLines = []; let blockStart = 0; for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (!inBlock && (trimmed.startsWith("/**") || trimmed.startsWith("/*"))) { inBlock = true; blockStart = i; blockLines = [trimmed]; if (trimmed.endsWith("*/") && trimmed.length > 4) { blocks.push({ text: stripBlockCommentMarkers(blockLines.join("\n")), startLine: blockStart, context: "jsdoc" }); inBlock = false; blockLines = []; } continue; } if (inBlock) { blockLines.push(trimmed); if (trimmed.includes("*/")) { blocks.push({ text: stripBlockCommentMarkers(blockLines.join("\n")), startLine: blockStart, context: "jsdoc" }); inBlock = false; blockLines = []; } continue; } if (trimmed.startsWith("//")) { blocks.push({ text: trimmed.replace(/^\/\/\s?/, ""), startLine: i, context: "comment" }); } } return blocks; } function stripBlockCommentMarkers(text) { return text.replace(/^\/\*\*?\s?/, "").replace(/\*\/\s*$/, "").split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim(); } function extractPythonDocstrings(content) { const blocks = []; const lines = content.split("\n"); let inDocstring = false; let delimiter = ""; let blockLines = []; let blockStart = 0; for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (!inDocstring) { for (const delim of ['"""', "'''"]) { if (trimmed.startsWith(delim)) { if (trimmed.endsWith(delim) && trimmed.length > delim.length * 2) { blocks.push({ text: trimmed.slice(delim.length, -delim.length).trim(), startLine: i, context: "pydoc" }); break; } inDocstring = true; delimiter = delim; blockStart = i; blockLines = [trimmed.slice(delim.length)]; break; } } if (!inDocstring && trimmed.startsWith("#")) { blocks.push({ text: trimmed.replace(/^#\s?/, ""), startLine: i, context: "comment" }); } } else { if (trimmed.endsWith(delimiter)) { blockLines.push(trimmed.slice(0, -delimiter.length)); blocks.push({ text: blockLines.join("\n").trim(), startLine: blockStart, context: "pydoc" }); inDocstring = false; blockLines = []; } else { blockLines.push(trimmed); } } } return blocks; } function extractRustDocComments(content) { const blocks = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (trimmed.startsWith("///") || trimmed.startsWith("//!")) { blocks.push({ text: trimmed.replace(/^\/\/[\/!]\s?/, ""), startLine: i, context: "rustdoc" }); } } return blocks; } function extractGoDocComments(content) { const blocks = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (trimmed.startsWith("//")) { blocks.push({ text: trimmed.replace(/^\/\/\s?/, ""), startLine: i, context: "godoc" }); } } return blocks; } function extractRubyDocComments(content) { const blocks = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (trimmed.startsWith("#")) { blocks.push({ text: trimmed.replace(/^#\s?/, ""), startLine: i, context: "comment" }); } } return blocks; } var WIKI_LINK_RE, CODE_EXTENSIONS, VALID_NAMESPACES; var init_parser = __esm({ "src/links/parser.ts"() { "use strict"; WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g; CODE_EXTENSIONS = /* @__PURE__ */ new Set([ "ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "pyi", "go", "rs", "rb", "java", "cs", "md", "json", "yaml", "yml", "toml", "css", "scss", "less", "html", "vue", "svelte" ]); VALID_NAMESPACES = /* @__PURE__ */ new Set([ "issue", "file", "symbol", "identity", "milestone", "decision" ]); } }); // src/links/resolver.ts function resolveRef(ref, ctx) { switch (ref.namespace) { case "issue": return resolveIssue(ref, ctx); case "file": return resolveFile(ref, ctx); case "symbol": return resolveSymbol(ref, ctx); case "identity": return resolveIdentity(ref, ctx); case "milestone": return resolveMilestone(ref, ctx); case "decision": return resolveDecision(ref, ctx); default: return { ...ref, state: "broken" }; } } function resolveRefs(refs, ctx) { return refs.map((ref) => resolveRef(ref, ctx)); } function resolveIssue(ref, ctx) { const title = ctx.getIssueTitle(ref.target); if (title !== void 0) { return { ...ref, state: "resolved", entityId: `issue:${ref.target}`, title }; } return { ...ref, state: "broken" }; } function resolveFile(ref, ctx) { if (ctx.hasTrackedFile(ref.target)) { return { ...ref, state: "resolved", entityId: `file:${ref.target}`, title: ref.target }; } return { ...ref, state: "broken" }; } function resolveSymbol(ref, ctx) { if (!ctx.hasTrackedFile(ref.target)) { return { ...ref, state: "broken" }; } if (ref.anchor && ctx.hasSymbol(ref.target, ref.anchor)) { return { ...ref, state: "resolved", entityId: `symbol:${ref.target}#${ref.anchor}`, title: `${ref.anchor} in ${ref.target}` }; } if (ref.anchor) { return { ...ref, state: "broken" }; } return { ...ref, state: "resolved", entityId: `file:${ref.target}`, title: ref.target }; } function resolveIdentity(ref, ctx) { if (ctx.hasIdentity(ref.target)) { return { ...ref, state: "resolved", entityId: `identity:${ref.target}`, title: ref.target }; } return { ...ref, state: "broken" }; } function resolveMilestone(ref, ctx) { const title = ctx.getMilestoneTitle(ref.target); if (title !== void 0) { return { ...ref, state: "resolved", entityId: `milestone:${ref.target}`, title }; } return { ...ref, state: "broken" }; } function resolveDecision(ref, ctx) { if (ctx.hasDecision(ref.target)) { return { ...ref, state: "resolved", entityId: `decision:${ref.target}`, title: ctx.getDecisionTitle(ref.target) ?? ref.target }; } return { ...ref, state: "broken" }; } function createResolverContext(engine) { const trackedSet = new Set(engine.trackedFiles().map((f) => f.path)); const agentIds = new Set(engine.getOps().map((op) => op.agentId)); const issues = engine.listIssues(); const milestones = engine.listMilestones(); return { hasTrackedFile(path) { return trackedSet.has(path); }, getIssueTitle(id) { const issue = engine.getIssue(id); return issue?.title; }, getMilestoneTitle(idOrMessage) { const byId = milestones.find((m) => m.id === idOrMessage); if (byId) return byId.message; const byMsg = milestones.find( (m) => m.message && m.message.toLowerCase().includes(idOrMessage.toLowerCase()) ); return byMsg?.message; }, hasSymbol(filePath, symbolName) { try { const { readFileSync: readFileSync2 } = __require("fs"); const { join: join2 } = __require("path"); const absPath = join2(engine.getRootPath(), filePath); const content = readFileSync2(absPath, "utf-8"); const result = engine.parseFile(content, filePath); if (!result) return false; return result.declarations.some((d) => d.name === symbolName); } catch { return false; } }, hasIdentity(id) { return agentIds.has(id) || agentIds.has(`agent:${id}`); }, getKnownAgentIds() { return [...agentIds]; }, getTrackedFilePaths() { return [...trackedSet]; }, getIssueIds() { return issues.map((i) => i.id); }, getMilestoneIds() { return milestones.map((m) => m.id); }, hasDecision(id) { if (!engine.getDecision) return false; return engine.getDecision(id) !== null; }, getDecisionTitle(id) { if (!engine.getDecision) return void 0; const d = engine.getDecision(id); return d?.toolName; }, getSymbolNames(filePath) { try { const { readFileSync: readFileSync2 } = __require("fs"); const { join: join2 } = __require("path"); const absPath = join2(engine.getRootPath(), filePath); const content = readFileSync2(absPath, "utf-8"); const result = engine.parseFile(content, filePath); if (!result) return []; return result.declarations.map((d) => d.name); } catch { return []; } } }; } var init_resolver = __esm({ "src/links/resolver.ts"() { "use strict"; } }); // src/links/ref-index.ts function buildRefIndex(files, ctx) { const index = { outgoing: /* @__PURE__ */ new Map(), incoming: /* @__PURE__ */ new Map() }; for (const file of files) { const refs = parseFileRefs(file.content, file.path); addFileToIndex(index, file.path, refs, ctx); } return index; } function updateFileInIndex(index, filePath, content, ctx) { removeFileFromIndex(index, filePath); const refs = parseFileRefs(content, filePath); addFileToIndex(index, filePath, refs, ctx); } function removeFileFromIndex(index, filePath) { const oldRefs = index.outgoing.get(filePath); if (!oldRefs) return; for (const ref of oldRefs) { const resolved = resolveRefToEntityId(ref); if (resolved) { const sources = index.incoming.get(resolved); if (sources) { const filtered = sources.filter((s) => s.filePath !== filePath); if (filtered.length > 0) { index.incoming.set(resolved, filtered); } else { index.incoming.delete(resolved); } } } } index.outgoing.delete(filePath); } function getOutgoingRefs(index, filePath) { return index.outgoing.get(filePath) ?? []; } function getBacklinks(index, entityId) { return index.incoming.get(entityId) ?? []; } function getReferencedEntities(index) { return [...index.incoming.keys()]; } function getFilesWithRefs(index) { return [...index.outgoing.keys()]; } function getIndexStats(index) { let totalRefs = 0; for (const refs of index.outgoing.values()) { totalRefs += refs.length; } return { totalFiles: index.outgoing.size, totalRefs, totalEntities: index.incoming.size }; } function addFileToIndex(index, filePath, refs, ctx) { if (refs.length === 0) return; index.outgoing.set(filePath, refs); for (const ref of refs) { const resolved = resolveRef(ref, ctx); const entityId = resolved.entityId ?? buildFallbackEntityId(ref); const sources = index.incoming.get(entityId) ?? []; sources.push(ref.source); index.incoming.set(entityId, sources); } } function buildFallbackEntityId(ref) { if (ref.anchor) { return `${ref.namespace}:${ref.target}#${ref.anchor}`; } return `${ref.namespace}:${ref.target}`; } function resolveRefToEntityId(ref) { if (ref.anchor) { return `${ref.namespace}:${ref.target}#${ref.anchor}`; } return `${ref.namespace}:${ref.target}`; } var init_ref_index = __esm({ "src/links/ref-index.ts"() { "use strict"; init_parser(); init_resolver(); } }); // src/links/lifecycle.ts import { readFileSync } from "fs"; import { join } from "path"; function buildRenameProposal(index, filePath, oldName, newName) { const oldEntityId = `symbol:${filePath}#${oldName}`; const newEntityId = `symbol:${filePath}#${newName}`; const sources = getBacklinks(index, oldEntityId); const altEntityId = `symbol:${filePath}#${oldName}`; const altSources = getBacklinks(index, altEntityId); const allSources = deduplicateSources([...sources, ...altSources]); const rewrites = []; const affectedFiles = /* @__PURE__ */ new Set(); for (const source of allSources) { affectedFiles.add(source.filePath); const oldText = buildRefText(filePath, oldName); const newText = buildRefText(filePath, newName); rewrites.push({ filePath: source.filePath, line: source.line, col: source.col, oldText, newText }); } return { oldTarget: oldEntityId, newTarget: newEntityId, affectedFiles: [...affectedFiles], rewrites }; } function applyRenameProposal(proposal, rootPath) { const modifiedFiles = []; const byFile = /* @__PURE__ */ new Map(); for (const rw of proposal.rewrites) { const existing = byFile.get(rw.filePath) ?? []; existing.push(rw); byFile.set(rw.filePath, existing); } for (const [filePath, rewrites] of byFile) { try { const absPath = join(rootPath, filePath); let content = readFileSync(absPath, "utf-8"); let modified = false; for (const rw of rewrites) { if (content.includes(rw.oldText)) { content = content.replaceAll(rw.oldText, rw.newText); modified = true; } } if (modified) { const { writeFileSync } = __require("fs"); writeFileSync(absPath, content); modifiedFiles.push(filePath); } } catch { } } return modifiedFiles; } function handleSymbolDeletion(index, registry, filePath, symbolName, causeOpHash) { const entityId = `symbol:${filePath}#${symbolName}`; const sources = getBacklinks(index, entityId); if (sources.length === 0) return null; return registry.markStale(entityId, "deleted", sources, { causeOpHash }); } function handleFileDeletion(index, registry, filePath, causeOpHash) { const entityId = `file:${filePath}`; const sources = getBacklinks(index, entityId); if (sources.length === 0) return null; return registry.markStale(entityId, "deleted", sources, { causeOpHash }); } function getDiagnostics(index, registry, resolvedEntityIds) { const diagnostics = []; for (const [filePath, refs] of index.outgoing) { for (const ref of refs) { const entityId = buildEntityIdFromRef(ref); const staleInfo = registry.getStale(entityId); if (staleInfo) { const reason = staleInfo.reason === "renamed" ? `renamed to ${staleInfo.newTarget}` : "removed"; diagnostics.push({ entityId, state: "stale", source: ref.source, message: `Reference to '${ref.target}${ref.anchor ? "#" + ref.anchor : ""}' is stale: target was ${reason}` }); continue; } if (!resolvedEntityIds.has(entityId)) { diagnostics.push({ entityId, state: "broken", source: ref.source, message: `Cannot resolve reference: '${ref.target}${ref.anchor ? "#" + ref.anchor : ""}' does not exist` }); } } } return diagnostics; } function processSemanticPatches(patches, filePath, index, registry, causeOpHash) { const events = []; for (const patch of patches) { if (patch.kind === "symbolRename") { const proposal = buildRenameProposal( index, filePath, patch.oldName, patch.newName ); if (proposal.rewrites.length > 0) { events.push({ type: "rename-proposal", filePath, proposal }); } } if (patch.kind === "symbolRemove") { const staleRef = handleSymbolDeletion( index, registry, filePath, patch.entityName, causeOpHash ); if (staleRef) { events.push({ type: "stale-detected", filePath, staleRef }); } } } return events; } function buildRefText(filePath, symbolName) { return `[[${filePath}#${symbolName}]]`; } function buildEntityIdFromRef(ref) { if (ref.anchor) { return `${ref.namespace}:${ref.target}#${ref.anchor}`; } return `${ref.namespace}:${ref.target}`; } function deduplicateSources(sources) { const seen = /* @__PURE__ */ new Set(); return sources.filter((s) => { const key = `${s.filePath}:${s.line}:${s.col}`; if (seen.has(key)) return false; seen.add(key); return true; }); } var StaleRefRegistry; var init_lifecycle = __esm({ "src/links/lifecycle.ts"() { "use strict"; init_ref_index(); StaleRefRegistry = class { staleRefs = /* @__PURE__ */ new Map(); /** * Mark an entity as stale due to rename or deletion. */ markStale(entityId, reason, sources, opts) { const entry = { entityId, reason, causeOpHash: opts?.causeOpHash, newTarget: opts?.newTarget, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sources }; this.staleRefs.set(entityId, entry); return entry; } /** * Remove stale status (e.g. after user accepts rename update). */ clearStale(entityId) { this.staleRefs.delete(entityId); } /** * Check if an entity is stale. */ isStale(entityId) { return this.staleRefs.has(entityId); } /** * Get stale info for an entity. */ getStale(entityId) { return this.staleRefs.get(entityId); } /** * Get all stale refs. */ getAllStale() { return [...this.staleRefs.values()]; } /** * Get stale refs filtered by reason. */ getByReason(reason) { return this.getAllStale().filter((s) => s.reason === reason); } }; } }); // src/links/index.ts var links_exports = {}; __export(links_exports, { StaleRefRegistry: () => StaleRefRegistry, applyRenameProposal: () => applyRenameProposal, buildRefIndex: () => buildRefIndex, buildRenameProposal: () => buildRenameProposal, createResolverContext: () => createResolverContext, getBacklinks: () => getBacklinks, getDiagnostics: () => getDiagnostics, getFilesWithRefs: () => getFilesWithRefs, getIndexStats: () => getIndexStats, getOutgoingRefs: () => getOutgoingRefs, getReferencedEntities: () => getReferencedEntities, handleFileDeletion: () => handleFileDeletion, handleSymbolDeletion: () => handleSymbolDeletion, inferNamespace: () => inferNamespace, parseDocCommentRefs: () => parseDocCommentRefs, parseFileRefs: () => parseFileRefs, parseMarkdownRefs: () => parseMarkdownRefs, parseRefContent: () => parseRefContent, processSemanticPatches: () => processSemanticPatches, removeFileFromIndex: () => removeFileFromIndex, resolveRef: () => resolveRef, resolveRefs: () => resolveRefs, updateFileInIndex: () => updateFileInIndex }); var init_links = __esm({ "src/links/index.ts"() { init_parser(); init_resolver(); init_ref_index(); init_lifecycle(); } }); export { parseFileRefs, parseMarkdownRefs, parseDocCommentRefs, parseRefContent, inferNamespace, init_parser, resolveRef, resolveRefs, createResolverContext, buildRefIndex, updateFileInIndex, removeFileFromIndex, getOutgoingRefs, getBacklinks, getReferencedEntities, getFilesWithRefs, getIndexStats, StaleRefRegistry, buildRenameProposal, applyRenameProposal, handleSymbolDeletion, handleFileDeletion, getDiagnostics, processSemanticPatches, links_exports, init_links };