@plebeiantech/lat.md 0.12.3-fork.1

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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/src/cli/check-frontmatter.d.ts +12 -0
  4. package/dist/src/cli/check-frontmatter.js +44 -0
  5. package/dist/src/cli/check-mode.d.ts +6 -0
  6. package/dist/src/cli/check-mode.js +303 -0
  7. package/dist/src/cli/check-status.d.ts +74 -0
  8. package/dist/src/cli/check-status.js +217 -0
  9. package/dist/src/cli/check.d.ts +45 -0
  10. package/dist/src/cli/check.js +777 -0
  11. package/dist/src/cli/checklist-menu.d.ts +12 -0
  12. package/dist/src/cli/checklist-menu.js +111 -0
  13. package/dist/src/cli/comment-guard.d.ts +16 -0
  14. package/dist/src/cli/comment-guard.js +111 -0
  15. package/dist/src/cli/comment-reminder.d.ts +62 -0
  16. package/dist/src/cli/comment-reminder.js +210 -0
  17. package/dist/src/cli/context.d.ts +11 -0
  18. package/dist/src/cli/context.js +58 -0
  19. package/dist/src/cli/expand.d.ts +8 -0
  20. package/dist/src/cli/expand.js +94 -0
  21. package/dist/src/cli/gen-index.d.ts +45 -0
  22. package/dist/src/cli/gen-index.js +235 -0
  23. package/dist/src/cli/gen.d.ts +6 -0
  24. package/dist/src/cli/gen.js +42 -0
  25. package/dist/src/cli/graph.d.ts +7 -0
  26. package/dist/src/cli/graph.js +47 -0
  27. package/dist/src/cli/hook.d.ts +1 -0
  28. package/dist/src/cli/hook.js +432 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +307 -0
  31. package/dist/src/cli/init.d.ts +9 -0
  32. package/dist/src/cli/init.js +1031 -0
  33. package/dist/src/cli/link-scheme.d.ts +54 -0
  34. package/dist/src/cli/link-scheme.js +101 -0
  35. package/dist/src/cli/locate.d.ts +2 -0
  36. package/dist/src/cli/locate.js +17 -0
  37. package/dist/src/cli/refs.d.ts +22 -0
  38. package/dist/src/cli/refs.js +229 -0
  39. package/dist/src/cli/reindex.d.ts +13 -0
  40. package/dist/src/cli/reindex.js +136 -0
  41. package/dist/src/cli/search.d.ts +36 -0
  42. package/dist/src/cli/search.js +217 -0
  43. package/dist/src/cli/section.d.ts +43 -0
  44. package/dist/src/cli/section.js +252 -0
  45. package/dist/src/cli/select-menu.d.ts +13 -0
  46. package/dist/src/cli/select-menu.js +100 -0
  47. package/dist/src/cli/templates.d.ts +1 -0
  48. package/dist/src/cli/templates.js +15 -0
  49. package/dist/src/cli/ui.d.ts +11 -0
  50. package/dist/src/cli/ui.js +34 -0
  51. package/dist/src/code-refs.d.ts +17 -0
  52. package/dist/src/code-refs.js +277 -0
  53. package/dist/src/config.d.ts +29 -0
  54. package/dist/src/config.js +83 -0
  55. package/dist/src/context.d.ts +23 -0
  56. package/dist/src/context.js +11 -0
  57. package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
  58. package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
  59. package/dist/src/extensions/wiki-link/index.d.ts +4 -0
  60. package/dist/src/extensions/wiki-link/index.js +3 -0
  61. package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
  62. package/dist/src/extensions/wiki-link/syntax.js +121 -0
  63. package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
  64. package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
  65. package/dist/src/extensions/wiki-link/types.d.ts +34 -0
  66. package/dist/src/extensions/wiki-link/types.js +1 -0
  67. package/dist/src/format.d.ts +8 -0
  68. package/dist/src/format.js +43 -0
  69. package/dist/src/graph-export.d.ts +60 -0
  70. package/dist/src/graph-export.js +309 -0
  71. package/dist/src/init-version.d.ts +10 -0
  72. package/dist/src/init-version.js +49 -0
  73. package/dist/src/knowledge/bd.d.ts +2 -0
  74. package/dist/src/knowledge/bd.js +107 -0
  75. package/dist/src/knowledge/claude-memory.d.ts +13 -0
  76. package/dist/src/knowledge/claude-memory.js +204 -0
  77. package/dist/src/knowledge/cq.d.ts +2 -0
  78. package/dist/src/knowledge/cq.js +74 -0
  79. package/dist/src/knowledge/index.d.ts +49 -0
  80. package/dist/src/knowledge/index.js +271 -0
  81. package/dist/src/knowledge/ranking.d.ts +17 -0
  82. package/dist/src/knowledge/ranking.js +62 -0
  83. package/dist/src/knowledge/session.d.ts +46 -0
  84. package/dist/src/knowledge/session.js +203 -0
  85. package/dist/src/knowledge/types.d.ts +47 -0
  86. package/dist/src/knowledge/types.js +10 -0
  87. package/dist/src/lattice.d.ts +108 -0
  88. package/dist/src/lattice.js +816 -0
  89. package/dist/src/mcp/server.d.ts +1 -0
  90. package/dist/src/mcp/server.js +58 -0
  91. package/dist/src/parser.d.ts +3 -0
  92. package/dist/src/parser.js +18 -0
  93. package/dist/src/search/db.d.ts +16 -0
  94. package/dist/src/search/db.js +54 -0
  95. package/dist/src/search/embedder.d.ts +25 -0
  96. package/dist/src/search/embedder.js +58 -0
  97. package/dist/src/search/index.d.ts +9 -0
  98. package/dist/src/search/index.js +66 -0
  99. package/dist/src/search/search.d.ts +9 -0
  100. package/dist/src/search/search.js +16 -0
  101. package/dist/src/source-parser.d.ts +27 -0
  102. package/dist/src/source-parser.js +916 -0
  103. package/dist/src/untrusted.d.ts +24 -0
  104. package/dist/src/untrusted.js +97 -0
  105. package/dist/src/version.d.ts +7 -0
  106. package/dist/src/version.js +39 -0
  107. package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
  108. package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
  109. package/dist/src/view/client/index.html +14 -0
  110. package/dist/src/view/diagnostics.d.ts +5 -0
  111. package/dist/src/view/diagnostics.js +171 -0
  112. package/dist/src/view/git-diff.d.ts +3 -0
  113. package/dist/src/view/git-diff.js +255 -0
  114. package/dist/src/view/git.d.ts +22 -0
  115. package/dist/src/view/git.js +193 -0
  116. package/dist/src/view/highlight.d.ts +2 -0
  117. package/dist/src/view/highlight.js +3767 -0
  118. package/dist/src/view/markdown.d.ts +22 -0
  119. package/dist/src/view/markdown.js +322 -0
  120. package/dist/src/view/protocol.d.ts +89 -0
  121. package/dist/src/view/protocol.js +1 -0
  122. package/dist/src/view/references.d.ts +62 -0
  123. package/dist/src/view/references.js +345 -0
  124. package/dist/src/view/repository.d.ts +12 -0
  125. package/dist/src/view/repository.js +167 -0
  126. package/dist/src/view/search.d.ts +14 -0
  127. package/dist/src/view/search.js +64 -0
  128. package/dist/src/view/server.d.ts +20 -0
  129. package/dist/src/view/server.js +233 -0
  130. package/dist/src/view/store.d.ts +57 -0
  131. package/dist/src/view/store.js +404 -0
  132. package/dist/src/walk.d.ts +18 -0
  133. package/dist/src/walk.js +27 -0
  134. package/package.json +85 -0
  135. package/templates/AGENTS.md +118 -0
  136. package/templates/README +1 -0
  137. package/templates/cursor-rules.md +77 -0
  138. package/templates/init/lat.md +1 -0
  139. package/templates/logo-dark.svg +182 -0
  140. package/templates/logo-light.svg +182 -0
  141. package/templates/logo.txt +7 -0
  142. package/templates/opencode-plugin.ts +180 -0
  143. package/templates/pi-extension.ts +364 -0
  144. package/templates/skill/SKILL.md +189 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * mdast-util extension to serialize wiki-link nodes back to markdown.
3
+ */
4
+ function handler(node, _parent, state, _info) {
5
+ const exit = state.enter('wikiLink');
6
+ const target = state.safe(node.value, { before: '[', after: ']' });
7
+ let value;
8
+ if (node.data.alias) {
9
+ const alias = state.safe(node.data.alias, { before: '[', after: ']' });
10
+ value = `[[${target}|${alias}]]`;
11
+ }
12
+ else {
13
+ value = `[[${target}]]`;
14
+ }
15
+ exit();
16
+ return value;
17
+ }
18
+ export function wikiLinkToMarkdown() {
19
+ return {
20
+ unsafe: [
21
+ { character: '[', inConstruct: ['phrasing', 'label', 'reference'] },
22
+ { character: ']', inConstruct: ['label', 'reference'] },
23
+ ],
24
+ handlers: {
25
+ wikiLink: handler,
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,34 @@
1
+ import type { Data, Literal } from 'mdast';
2
+ /**
3
+ * mdast node for a wiki-style link like [[target]] or [[target|alias]].
4
+ */
5
+ export interface WikiLink extends Literal {
6
+ type: 'wikiLink';
7
+ value: string;
8
+ data: Data & {
9
+ alias: string | null;
10
+ };
11
+ }
12
+ declare module 'mdast' {
13
+ interface RootContentMap {
14
+ wikiLink: WikiLink;
15
+ }
16
+ interface PhrasingContentMap {
17
+ wikiLink: WikiLink;
18
+ }
19
+ }
20
+ declare module 'micromark-util-types' {
21
+ interface TokenTypeMap {
22
+ wikiLink: 'wikiLink';
23
+ wikiLinkMarker: 'wikiLinkMarker';
24
+ wikiLinkData: 'wikiLinkData';
25
+ wikiLinkTarget: 'wikiLinkTarget';
26
+ wikiLinkAliasMarker: 'wikiLinkAliasMarker';
27
+ wikiLinkAlias: 'wikiLinkAlias';
28
+ }
29
+ }
30
+ declare module 'mdast-util-to-markdown' {
31
+ interface ConstructNameMap {
32
+ wikiLink: 'wikiLink';
33
+ }
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { Section, SectionMatch } from './lattice.js';
2
+ import type { CmdContext, Styler } from './context.js';
3
+ export declare function formatSectionId(id: string, s: Styler): string;
4
+ export declare function formatSectionPreview(ctx: CmdContext, section: Section, opts?: {
5
+ reason?: string;
6
+ }): string;
7
+ export declare function formatResultList(ctx: CmdContext, header: string, matches: SectionMatch[]): string;
8
+ export declare function formatNavHints(ctx: CmdContext): string;
@@ -0,0 +1,43 @@
1
+ import { join, relative } from 'node:path';
2
+ export function formatSectionId(id, s) {
3
+ const parts = id.split('#');
4
+ return parts.length === 1
5
+ ? s.boldWhite(parts[0])
6
+ : s.dim(parts.slice(0, -1).join('#') + '#') +
7
+ s.boldWhite(parts[parts.length - 1]);
8
+ }
9
+ export function formatSectionPreview(ctx, section, opts) {
10
+ const s = ctx.styler;
11
+ const relPath = relative(process.cwd(), join(ctx.projectRoot, section.filePath));
12
+ const kind = section.id.includes('#') ? 'Section' : 'File';
13
+ const reasonSuffix = opts?.reason ? ' ' + s.dim(`(${opts.reason})`) : '';
14
+ const lines = [
15
+ `${s.dim('*')} ${s.dim(kind + ':')} [[${formatSectionId(section.id, s)}]]${reasonSuffix}`,
16
+ ` ${s.dim('Defined in')} ${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`,
17
+ ];
18
+ if (section.firstParagraph) {
19
+ lines.push('', ` ${s.dim('>')} ${section.firstParagraph}`);
20
+ }
21
+ return lines.join('\n');
22
+ }
23
+ export function formatResultList(ctx, header, matches) {
24
+ const lines = ['', `## ${header}`, ''];
25
+ for (let i = 0; i < matches.length; i++) {
26
+ if (i > 0)
27
+ lines.push('');
28
+ lines.push(formatSectionPreview(ctx, matches[i].section, {
29
+ reason: matches[i].reason,
30
+ }));
31
+ }
32
+ lines.push('');
33
+ return lines.join('\n');
34
+ }
35
+ export function formatNavHints(ctx) {
36
+ const s = ctx.styler;
37
+ const hints = ctx.mode === 'cli'
38
+ ? `${s.dim('*')} \`lat section "section#id"\` \u2014 show full content with outgoing/incoming refs\n` +
39
+ `${s.dim('*')} \`lat search "new query"\` \u2014 search for something else`
40
+ : `${s.dim('*')} \`lat_section\` \u2014 show full content with outgoing/incoming refs\n` +
41
+ `${s.dim('*')} \`lat_search\` \u2014 search for something else`;
42
+ return `\n## To navigate further:\n\n${hints}`;
43
+ }
@@ -0,0 +1,60 @@
1
+ export type GraphNodeType = 'document' | 'section' | 'code' | 'tag';
2
+ export type GraphEdgeType = 'contains' | 'wikilink' | 'code-ref' | 'tag';
3
+ export type GraphNode = {
4
+ id: string;
5
+ type: GraphNodeType;
6
+ label: string;
7
+ file?: string;
8
+ mode?: string;
9
+ status?: string;
10
+ reviewedHash?: string;
11
+ };
12
+ export type GraphEdge = {
13
+ from: string;
14
+ to: string;
15
+ type: GraphEdgeType;
16
+ };
17
+ export type Graph = {
18
+ nodes: GraphNode[];
19
+ edges: GraphEdge[];
20
+ };
21
+ /** A lat.md markdown file's project-root-relative path and content. */
22
+ export type GraphSourceFile = {
23
+ /** Project-root-relative, posix-separated path (e.g. "lat.md/tests.md"). */
24
+ relPath: string;
25
+ content: string;
26
+ };
27
+ /**
28
+ * Build the knowledge graph from a set of already-loaded lat.md file
29
+ * contents. Used for both the working tree and a reconstructed git
30
+ * revision -- the only difference between the two call sites is where the
31
+ * file contents came from.
32
+ *
33
+ * `includeCode` controls whether the code-ref and source-wikilink halves of
34
+ * the graph are built by scanning the live project tree on disk. This is
35
+ * only meaningful for the working tree: a git revision's source tree is not
36
+ * reconstructed, only its lat.md/ documents are, so `includeCode` is false
37
+ * for `--at` / `--since`.
38
+ */
39
+ export declare function buildGraph(files: GraphSourceFile[], projectRoot: string, includeCode: boolean): Promise<Graph>;
40
+ /** Read the working tree's lat.md files straight off disk. */
41
+ export declare function loadWorkingTreeFiles(latticeDir: string, projectRoot: string): Promise<GraphSourceFile[]>;
42
+ /**
43
+ * Reconstruct the lat.md/ markdown tree as it existed at a git revision.
44
+ * Only the documents are reconstructed, not the surrounding source tree —
45
+ * `buildGraph` is called with `includeCode: false` for these.
46
+ */
47
+ export declare function loadRevisionFiles(projectRoot: string, latticeDir: string, rev: string): Promise<GraphSourceFile[]>;
48
+ export type GraphDiff = {
49
+ added: string[];
50
+ removed: string[];
51
+ changed: string[];
52
+ };
53
+ /** Diff two graphs' section nodes by id, reporting added/removed/changed. */
54
+ export declare function diffGraphs(before: Graph, after: Graph): GraphDiff;
55
+ export declare function formatGraphJson(graph: Graph): string;
56
+ export declare function formatGraphMermaid(graph: Graph): string;
57
+ export declare function formatGraphDot(graph: Graph): string;
58
+ export type GraphFormat = 'json' | 'mermaid' | 'dot';
59
+ export declare function formatGraph(graph: Graph, format: GraphFormat): string;
60
+ export declare function formatGraphDiff(diff: GraphDiff): string;
@@ -0,0 +1,309 @@
1
+ // Export the whole lat.md knowledge graph: sections, documents, code refs,
2
+ // wiki links, and test-spec coverage, at the working tree or at a git
3
+ // revision. See lat-t1y.10.
4
+ //
5
+ // Every label placed into a mermaid or dot rendering is repository text, so
6
+ // it is passed through `cleanUntrustedId` before it becomes a node id or
7
+ // label -- never a hand-rolled sanitizer. `cleanUntrustedId` (not
8
+ // `quoteUntrusted`) is correct here: node ids and labels must not be
9
+ // truncated, and quoting them would corrupt them as identifiers.
10
+ import { execFile } from 'node:child_process';
11
+ import { join, relative } from 'node:path';
12
+ import { promisify } from 'node:util';
13
+ import { extractRefs, buildFileIndex, buildSectionSlugIndex, parseSections, parseFrontmatter, resolveRef, } from './lattice.js';
14
+ import { scanCodeRefs } from './code-refs.js';
15
+ import { DIATAXIS_MODES, MODE_DIRS } from './cli/check-mode.js';
16
+ import { readProvenance } from './cli/check-status.js';
17
+ import { cleanUntrustedId } from './untrusted.js';
18
+ import { toPosix } from './walk.js';
19
+ const execFileAsync = promisify(execFile);
20
+ function docId(relPath) {
21
+ return `doc:${relPath}`;
22
+ }
23
+ function sectionId(id) {
24
+ return `section:${id}`;
25
+ }
26
+ function codeId(target) {
27
+ return `code:${target}`;
28
+ }
29
+ function tagId(tag) {
30
+ return `tag:${tag}`;
31
+ }
32
+ function modeForFile(relPath, fm) {
33
+ const declared = fm['mode'];
34
+ if (typeof declared === 'string')
35
+ return declared;
36
+ for (const mode of DIATAXIS_MODES) {
37
+ const dir = MODE_DIRS[mode];
38
+ if (relPath.includes(`/${dir}/`) || relPath.startsWith(`${dir}/`)) {
39
+ return mode;
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+ function tagsForFile(fm) {
45
+ const raw = fm['tags'];
46
+ if (Array.isArray(raw))
47
+ return raw.filter((t) => typeof t === 'string');
48
+ if (typeof raw === 'string')
49
+ return [raw];
50
+ return [];
51
+ }
52
+ /**
53
+ * Build the knowledge graph from a set of already-loaded lat.md file
54
+ * contents. Used for both the working tree and a reconstructed git
55
+ * revision -- the only difference between the two call sites is where the
56
+ * file contents came from.
57
+ *
58
+ * `includeCode` controls whether the code-ref and source-wikilink halves of
59
+ * the graph are built by scanning the live project tree on disk. This is
60
+ * only meaningful for the working tree: a git revision's source tree is not
61
+ * reconstructed, only its lat.md/ documents are, so `includeCode` is false
62
+ * for `--at` / `--since`.
63
+ */
64
+ export async function buildGraph(files, projectRoot, includeCode) {
65
+ const nodes = new Map();
66
+ const edges = [];
67
+ const addNode = (node) => {
68
+ if (!nodes.has(node.id))
69
+ nodes.set(node.id, node);
70
+ };
71
+ const addEdge = (edge) => {
72
+ edges.push(edge);
73
+ };
74
+ const ensureSectionNode = (id, label) => {
75
+ const sid = sectionId(id);
76
+ if (!nodes.has(sid)) {
77
+ addNode({
78
+ id: sid,
79
+ type: 'section',
80
+ label: cleanUntrustedId(label ?? id),
81
+ });
82
+ }
83
+ return sid;
84
+ };
85
+ const allSections = [];
86
+ const sortedFiles = [...files].sort((a, b) => a.relPath.localeCompare(b.relPath));
87
+ for (const f of sortedFiles) {
88
+ const absPath = join(projectRoot, f.relPath);
89
+ const sections = parseSections(absPath, f.content, projectRoot);
90
+ allSections.push(...sections);
91
+ const fm = parseFrontmatter(f.content).raw;
92
+ const provenance = readProvenance(f.content);
93
+ const mode = modeForFile(toPosix(f.relPath), fm);
94
+ const tags = tagsForFile(fm);
95
+ const dId = docId(f.relPath);
96
+ addNode({
97
+ id: dId,
98
+ type: 'document',
99
+ label: cleanUntrustedId(f.relPath),
100
+ file: f.relPath,
101
+ mode,
102
+ status: provenance.status,
103
+ reviewedHash: provenance.reviewedHash,
104
+ });
105
+ for (const tag of tags) {
106
+ const tId = tagId(tag);
107
+ addNode({ id: tId, type: 'tag', label: cleanUntrustedId(tag) });
108
+ addEdge({ from: dId, to: tId, type: 'tag' });
109
+ }
110
+ const walkSection = (section, parentId) => {
111
+ const sId = ensureSectionNode(section.id, section.heading);
112
+ const node = nodes.get(sId);
113
+ node.file = f.relPath;
114
+ addEdge({ from: parentId, to: sId, type: 'contains' });
115
+ for (const child of section.children) {
116
+ walkSection(child, sId);
117
+ }
118
+ };
119
+ for (const root of sections) {
120
+ walkSection(root, dId);
121
+ }
122
+ }
123
+ const flat = [];
124
+ const flatten = (secs) => {
125
+ for (const s of secs) {
126
+ flat.push(s);
127
+ flatten(s.children);
128
+ }
129
+ };
130
+ flatten(allSections);
131
+ const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
132
+ const fileIndex = buildFileIndex(allSections);
133
+ const slugIndex = buildSectionSlugIndex(allSections);
134
+ // Wiki links: section -> section (or section -> code target).
135
+ for (const f of sortedFiles) {
136
+ const absPath = join(projectRoot, f.relPath);
137
+ const refs = extractRefs(absPath, f.content, projectRoot);
138
+ for (const ref of refs) {
139
+ const { resolved, ambiguous } = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
140
+ const fromId = ensureSectionNode(ref.fromSection);
141
+ if (!ambiguous && sectionIds.has(resolved.toLowerCase())) {
142
+ const toId = ensureSectionNode(resolved);
143
+ addEdge({ from: fromId, to: toId, type: 'wikilink' });
144
+ }
145
+ else {
146
+ const cId = codeId(ref.target);
147
+ addNode({
148
+ id: cId,
149
+ type: 'code',
150
+ label: cleanUntrustedId(ref.target),
151
+ });
152
+ addEdge({ from: fromId, to: cId, type: 'wikilink' });
153
+ }
154
+ }
155
+ }
156
+ // Code refs (the `// @lat: [[section-id]]` comment marker) and test-spec coverage: code -> section.
157
+ if (includeCode) {
158
+ const scan = await scanCodeRefs(projectRoot);
159
+ for (const ref of scan.refs) {
160
+ const { resolved, ambiguous } = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
161
+ const label = `${ref.file}:${ref.line}`;
162
+ const cId = codeId(label);
163
+ addNode({ id: cId, type: 'code', label: cleanUntrustedId(label) });
164
+ const toId = !ambiguous && sectionIds.has(resolved.toLowerCase())
165
+ ? ensureSectionNode(resolved)
166
+ : ensureSectionNode(ref.target);
167
+ addEdge({ from: cId, to: toId, type: 'code-ref' });
168
+ }
169
+ }
170
+ return { nodes: [...nodes.values()], edges };
171
+ }
172
+ /** Read the working tree's lat.md files straight off disk. */
173
+ export async function loadWorkingTreeFiles(latticeDir, projectRoot) {
174
+ const { listLatticeFiles } = await import('./lattice.js');
175
+ const { readFile } = await import('node:fs/promises');
176
+ const paths = await listLatticeFiles(latticeDir);
177
+ const files = [];
178
+ for (const p of paths) {
179
+ const content = await readFile(p, 'utf-8');
180
+ files.push({ relPath: toPosix(relative(projectRoot, p)), content });
181
+ }
182
+ return files;
183
+ }
184
+ /**
185
+ * Reconstruct the lat.md/ markdown tree as it existed at a git revision.
186
+ * Only the documents are reconstructed, not the surrounding source tree —
187
+ * `buildGraph` is called with `includeCode: false` for these.
188
+ */
189
+ export async function loadRevisionFiles(projectRoot, latticeDir, rev) {
190
+ const latRelDir = toPosix(relative(projectRoot, latticeDir));
191
+ let lsOutput;
192
+ try {
193
+ const { stdout } = await execFileAsync('git', ['ls-tree', '-r', '--name-only', rev, '--', latRelDir], { cwd: projectRoot });
194
+ lsOutput = stdout;
195
+ }
196
+ catch (err) {
197
+ throw new Error(`failed to list "${latRelDir}" at revision "${rev}": ${err instanceof Error ? err.message : String(err)}`);
198
+ }
199
+ const paths = lsOutput
200
+ .split('\n')
201
+ .map((l) => l.trim())
202
+ .filter((l) => l.endsWith('.md'));
203
+ const files = [];
204
+ for (const p of paths) {
205
+ const { stdout } = await execFileAsync('git', ['show', `${rev}:${p}`], {
206
+ cwd: projectRoot,
207
+ });
208
+ files.push({ relPath: p, content: stdout });
209
+ }
210
+ return files;
211
+ }
212
+ /** Diff two graphs' section nodes by id, reporting added/removed/changed. */
213
+ export function diffGraphs(before, after) {
214
+ const beforeSections = new Map(before.nodes.filter((n) => n.type === 'section').map((n) => [n.id, n]));
215
+ const afterSections = new Map(after.nodes.filter((n) => n.type === 'section').map((n) => [n.id, n]));
216
+ const added = [];
217
+ const removed = [];
218
+ const changed = [];
219
+ for (const [id, node] of afterSections) {
220
+ const prior = beforeSections.get(id);
221
+ if (!prior) {
222
+ added.push(id);
223
+ }
224
+ else if (prior.label !== node.label) {
225
+ changed.push(id);
226
+ }
227
+ }
228
+ for (const id of beforeSections.keys()) {
229
+ if (!afterSections.has(id))
230
+ removed.push(id);
231
+ }
232
+ return { added, removed, changed };
233
+ }
234
+ // --- Formatters -----------------------------------------------------------
235
+ export function formatGraphJson(graph) {
236
+ return JSON.stringify(graph, null, 2);
237
+ }
238
+ const NODE_SHAPE = {
239
+ document: ['([', '])'],
240
+ section: ['[', ']'],
241
+ code: ['[[', ']]'],
242
+ tag: ['((', '))'],
243
+ };
244
+ function mermaidEscape(label) {
245
+ return label.replace(/"/g, '#quot;');
246
+ }
247
+ export function formatGraphMermaid(graph) {
248
+ const idMap = new Map();
249
+ graph.nodes.forEach((n, i) => idMap.set(n.id, `n${i}`));
250
+ const lines = ['flowchart TD'];
251
+ for (const node of graph.nodes) {
252
+ const [open, close] = NODE_SHAPE[node.type];
253
+ const label = mermaidEscape(`${node.type}: ${node.label}`);
254
+ lines.push(` ${idMap.get(node.id)}${open}"${label}"${close}`);
255
+ }
256
+ for (const edge of graph.edges) {
257
+ const from = idMap.get(edge.from);
258
+ const to = idMap.get(edge.to);
259
+ if (!from || !to)
260
+ continue;
261
+ lines.push(` ${from} -->|${edge.type}| ${to}`);
262
+ }
263
+ return lines.join('\n');
264
+ }
265
+ function dotEscape(label) {
266
+ return label.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
267
+ }
268
+ export function formatGraphDot(graph) {
269
+ const idMap = new Map();
270
+ graph.nodes.forEach((n, i) => idMap.set(n.id, `n${i}`));
271
+ const lines = ['digraph lat {'];
272
+ for (const node of graph.nodes) {
273
+ const label = dotEscape(`${node.type}: ${node.label}`);
274
+ lines.push(` ${idMap.get(node.id)} [label="${label}"];`);
275
+ }
276
+ for (const edge of graph.edges) {
277
+ const from = idMap.get(edge.from);
278
+ const to = idMap.get(edge.to);
279
+ if (!from || !to)
280
+ continue;
281
+ lines.push(` ${from} -> ${to} [label="${dotEscape(edge.type)}"];`);
282
+ }
283
+ lines.push('}');
284
+ return lines.join('\n');
285
+ }
286
+ export function formatGraph(graph, format) {
287
+ switch (format) {
288
+ case 'mermaid':
289
+ return formatGraphMermaid(graph);
290
+ case 'dot':
291
+ return formatGraphDot(graph);
292
+ case 'json':
293
+ default:
294
+ return formatGraphJson(graph);
295
+ }
296
+ }
297
+ export function formatGraphDiff(diff) {
298
+ const lines = [];
299
+ lines.push(`added (${diff.added.length}):`);
300
+ for (const id of diff.added)
301
+ lines.push(` + ${cleanUntrustedId(id)}`);
302
+ lines.push(`removed (${diff.removed.length}):`);
303
+ for (const id of diff.removed)
304
+ lines.push(` - ${cleanUntrustedId(id)}`);
305
+ lines.push(`changed (${diff.changed.length}):`);
306
+ for (const id of diff.changed)
307
+ lines.push(` ~ ${cleanUntrustedId(id)}`);
308
+ return lines.join('\n');
309
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Bump this number whenever `lat init` setup or defaults change in a way that
3
+ * requires users to re-run it (e.g. new hooks, generated files, MCP config, or
4
+ * a configuration migration).
5
+ */
6
+ export declare const INIT_VERSION = 5;
7
+ export declare function readInitVersion(latDir: string): number | null;
8
+ export declare function readFileHash(latDir: string, relPath: string): string | null;
9
+ export declare function contentHash(content: string): string;
10
+ export declare function writeInitMeta(latDir: string, fileHashes: Record<string, string>): void;
@@ -0,0 +1,49 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Bump this number whenever `lat init` setup or defaults change in a way that
6
+ * requires users to re-run it (e.g. new hooks, generated files, MCP config, or
7
+ * a configuration migration).
8
+ */
9
+ export const INIT_VERSION = 5;
10
+ function cachePath(latDir) {
11
+ return join(latDir, '.cache', 'lat_init.json');
12
+ }
13
+ function readMeta(latDir) {
14
+ const p = cachePath(latDir);
15
+ if (!existsSync(p))
16
+ return null;
17
+ try {
18
+ return JSON.parse(readFileSync(p, 'utf-8'));
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function readInitVersion(latDir) {
25
+ const meta = readMeta(latDir);
26
+ if (!meta)
27
+ return null;
28
+ return typeof meta.init_version === 'number' ? meta.init_version : null;
29
+ }
30
+ export function readFileHash(latDir, relPath) {
31
+ const meta = readMeta(latDir);
32
+ return meta?.file_hashes?.[relPath] ?? null;
33
+ }
34
+ export function contentHash(content) {
35
+ return createHash('sha256').update(content).digest('hex');
36
+ }
37
+ export function writeInitMeta(latDir, fileHashes) {
38
+ const cacheDir = join(latDir, '.cache');
39
+ mkdirSync(cacheDir, { recursive: true });
40
+ // Merge with existing hashes so we don't lose entries from agents
41
+ // that weren't selected this run
42
+ const existing = readMeta(latDir);
43
+ const mergedHashes = { ...existing?.file_hashes, ...fileHashes };
44
+ const data = {
45
+ init_version: INIT_VERSION,
46
+ file_hashes: mergedHashes,
47
+ };
48
+ writeFileSync(cachePath(latDir), JSON.stringify(data, null, 2) + '\n');
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { Store } from './types.js';
2
+ export declare const bdStore: Store;
@@ -0,0 +1,107 @@
1
+ import { execFile, } from 'node:child_process';
2
+ const CALL_TIMEOUT_MS = 2000;
3
+ /**
4
+ * Run `bd memories --json <term>` for a single term. Never throws: a missing
5
+ * binary (ENOENT), non-zero exit, timeout, or unparseable JSON all collapse
6
+ * to "no matches for this term" so one bad term can't sink the whole query.
7
+ *
8
+ * Uses the async `execFile` (not `execFileSync`) so this call never blocks
9
+ * the event loop: `federateTags` fans out to all three stores with
10
+ * `Promise.allSettled`, and that fan-out only buys real concurrency if each
11
+ * store's I/O actually yields control while its subprocess runs.
12
+ */
13
+ function queryTerm(term) {
14
+ return new Promise((resolvePromise) => {
15
+ // argv form, no shell: terms come from repository frontmatter and are
16
+ // attacker-controlled when `lat` runs in a repo we don't own, so a term
17
+ // must never be interpretable as shell syntax.
18
+ // stdin: 'ignore' is load-bearing, not cosmetic. The shell version this
19
+ // replaced ran its per-term loop inside a `while read` on stdin; an
20
+ // external tool in that loop that itself read stdin would silently
21
+ // consume the remaining terms, and the symptom was some documents just
22
+ // never showing up — indistinguishable from "no match". Detaching stdin
23
+ // here removes that whole class of bug. `stdio` isn't in the string-
24
+ // encoding overload's declared option type even though Node accepts it
25
+ // at runtime, hence the explicit cast.
26
+ const options = {
27
+ stdio: ['ignore', 'pipe', 'ignore'],
28
+ timeout: CALL_TIMEOUT_MS,
29
+ encoding: 'utf-8',
30
+ };
31
+ execFile('bd', ['memories', '--json', term], options, (error, stdout) => {
32
+ // execFile's async form reports a non-zero exit and a timeout kill
33
+ // both through `error`, and never rejects — so every failure mode
34
+ // is handled right here rather than via a catch on the returned
35
+ // promise. Match `queryTerm`'s old spawnSync contract of "empty
36
+ // object" for anything that isn't a clean, parseable JSON object.
37
+ if (error || !stdout) {
38
+ resolvePromise({});
39
+ return;
40
+ }
41
+ try {
42
+ const parsed = JSON.parse(stdout);
43
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
44
+ resolvePromise(parsed);
45
+ }
46
+ else {
47
+ resolvePromise({});
48
+ }
49
+ }
50
+ catch {
51
+ resolvePromise({});
52
+ }
53
+ });
54
+ });
55
+ }
56
+ async function query(q) {
57
+ try {
58
+ return await rank(q);
59
+ }
60
+ catch {
61
+ // The store contract is that a lookup never fails the caller's prompt.
62
+ // queryTerm already absorbs a failing `bd`, but the surrounding logic
63
+ // could still throw on an unexpected input, so the guarantee is made
64
+ // unconditional here.
65
+ return [];
66
+ }
67
+ }
68
+ async function rank(q) {
69
+ // key -> { content, count }, built in first-seen order so a stable sort
70
+ // below keeps ties in the author's original term order rather than an
71
+ // alphabetical accident.
72
+ const seen = new Map();
73
+ // Terms belong to the same document, so their subprocess calls are
74
+ // independent of each other — run them concurrently rather than one at a
75
+ // time. Each individual call still enforces CALL_TIMEOUT_MS; running them
76
+ // together bounds the whole document's bd lookup by that same timeout
77
+ // instead of `terms.length * CALL_TIMEOUT_MS`.
78
+ const perTerm = await Promise.all(q.terms.map((term) => queryTerm(term)));
79
+ for (const memories of perTerm) {
80
+ for (const [key, content] of Object.entries(memories)) {
81
+ if (!key || typeof content !== 'string' || content === '')
82
+ continue;
83
+ const existing = seen.get(key);
84
+ if (existing) {
85
+ existing.count += 1;
86
+ }
87
+ else {
88
+ seen.set(key, { content, count: 1 });
89
+ }
90
+ }
91
+ }
92
+ const hits = [...seen.entries()].map(([key, v]) => ({
93
+ store: 'bd',
94
+ key,
95
+ title: key,
96
+ detail: v.content.split('\n')[0] ?? '',
97
+ score: v.count,
98
+ }));
99
+ // Array.prototype.sort is stable, so entries with equal score retain the
100
+ // first-seen (author's term) order from the map iteration above.
101
+ hits.sort((a, b) => b.score - a.score);
102
+ return hits.slice(0, q.limit);
103
+ }
104
+ export const bdStore = {
105
+ name: 'bd',
106
+ query,
107
+ };
@@ -0,0 +1,13 @@
1
+ import type { Store } from './types.js';
2
+ /**
3
+ * Match Claude Code's own project-directory slug: every character that
4
+ * isn't a letter or digit becomes `-`, including `.` — a project root like
5
+ * `/Users/dave/projects/claude-code/lat.md` slugifies to
6
+ * `-Users-dave-projects-claude-code-lat-md`, verified against a real
7
+ * `~/.claude/projects/` listing. Replacing only `/` (the previous
8
+ * implementation) leaves dots in the slug and never matches the real
9
+ * directory, so the store silently finds nothing for any project root
10
+ * containing a dot.
11
+ */
12
+ export declare function slugify(absPath: string): string;
13
+ export declare const claudeMemoryStore: Store;