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
JavaScript
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
};