lat.md 0.7.1 → 0.7.2
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/dist/src/cli/check.js +3 -14
- package/dist/src/cli/hook.js +86 -6
- package/dist/src/cli/section.js +8 -4
- package/dist/src/source-parser.d.ts +2 -0
- package/dist/src/source-parser.js +15 -11
- package/package.json +1 -1
package/dist/src/cli/check.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { basename, dirname, extname, join, relative } from 'node:path';
|
|
4
4
|
import { listLatticeFiles, loadAllSections, extractRefs, flattenSections, parseFrontmatter, parseSections, buildFileIndex, resolveRef, } from '../lattice.js';
|
|
5
5
|
import { scanCodeRefs } from '../code-refs.js';
|
|
6
|
+
import { SOURCE_EXTENSIONS } from '../source-parser.js';
|
|
6
7
|
import { walkEntries } from '../walk.js';
|
|
7
8
|
import { INIT_VERSION, readInitVersion } from '../init-version.js';
|
|
8
9
|
function filePart(id) {
|
|
@@ -32,23 +33,11 @@ function countByExt(paths) {
|
|
|
32
33
|
}
|
|
33
34
|
return stats;
|
|
34
35
|
}
|
|
35
|
-
/** Source file extensions recognized for code wiki links. */
|
|
36
|
-
const SOURCE_EXTS = new Set([
|
|
37
|
-
'.ts',
|
|
38
|
-
'.tsx',
|
|
39
|
-
'.js',
|
|
40
|
-
'.jsx',
|
|
41
|
-
'.py',
|
|
42
|
-
'.rs',
|
|
43
|
-
'.go',
|
|
44
|
-
'.c',
|
|
45
|
-
'.h',
|
|
46
|
-
]);
|
|
47
36
|
function isSourcePath(target) {
|
|
48
37
|
const hashIdx = target.indexOf('#');
|
|
49
38
|
const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
|
|
50
39
|
const ext = extname(filePart);
|
|
51
|
-
return
|
|
40
|
+
return SOURCE_EXTENSIONS.has(ext);
|
|
52
41
|
}
|
|
53
42
|
/**
|
|
54
43
|
* Try resolving a wiki link target as a source code reference.
|
|
@@ -61,7 +50,7 @@ async function tryResolveSourceRef(target, projectRoot) {
|
|
|
61
50
|
const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
|
|
62
51
|
const ext = extname(filePart);
|
|
63
52
|
if (ext && hashIdx !== -1) {
|
|
64
|
-
const supported = [...
|
|
53
|
+
const supported = [...SOURCE_EXTENSIONS].sort().join(', ');
|
|
65
54
|
return `broken link [[${target}]] — unsupported file extension "${ext}". Supported: ${supported}`;
|
|
66
55
|
}
|
|
67
56
|
return `broken link [[${target}]] — no matching section found`;
|
package/dist/src/cli/hook.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { dirname, extname } from 'node:path';
|
|
2
3
|
import { findLatticeDir } from '../lattice.js';
|
|
3
4
|
import { plainStyler } from '../context.js';
|
|
4
5
|
import { expandPrompt } from './expand.js';
|
|
5
6
|
import { runSearch } from './search.js';
|
|
6
7
|
import { getSection, formatSectionOutput } from './section.js';
|
|
7
8
|
import { getLlmKey } from '../config.js';
|
|
9
|
+
import { checkMd, checkCodeRefs, checkIndex, checkSections } from './check.js';
|
|
10
|
+
import { SOURCE_EXTENSIONS } from '../source-parser.js';
|
|
8
11
|
function outputPromptSubmit(context) {
|
|
9
12
|
process.stdout.write(JSON.stringify({
|
|
10
13
|
hookSpecificOutput: {
|
|
@@ -74,7 +77,7 @@ async function handleUserPromptSubmit() {
|
|
|
74
77
|
// If we can't parse stdin, still emit the reminder
|
|
75
78
|
}
|
|
76
79
|
const parts = [];
|
|
77
|
-
parts.push("Before starting work, run `lat search` with one or more queries describing the user's intent.", 'ALWAYS do this, even when the task seems straightforward — search results may reveal critical design details, protocols, or constraints.', 'Use `lat section` to read the full content of relevant matches.', 'Do not read files, write code, or run commands until you have searched.');
|
|
80
|
+
parts.push("Before starting work, run `lat search` with one or more queries describing the user's intent.", 'ALWAYS do this, even when the task seems straightforward — search results may reveal critical design details, protocols, or constraints.', 'Use `lat section` to read the full content of relevant matches.', 'Do not read files, write code, or run commands until you have searched.', '', 'Remember: `lat.md/` must stay in sync with the codebase. If you change code, update the relevant sections in `lat.md/` and run `lat check` before finishing.');
|
|
78
81
|
const latDir = findLatticeDir();
|
|
79
82
|
if (latDir && userPrompt) {
|
|
80
83
|
const ctx = makeHookCtx(latDir);
|
|
@@ -106,8 +109,44 @@ async function handleUserPromptSubmit() {
|
|
|
106
109
|
}
|
|
107
110
|
outputPromptSubmit(parts.join('\n'));
|
|
108
111
|
}
|
|
112
|
+
/** Minimum diff size (in lines) to consider "significant" code change. */
|
|
113
|
+
/** Minimum code change size (lines) before we consider flagging lat.md/ sync. */
|
|
114
|
+
const DIFF_THRESHOLD = 5;
|
|
115
|
+
/** lat.md/ changes below this ratio of code changes trigger a sync reminder. */
|
|
116
|
+
const LATMD_RATIO = 0.05;
|
|
117
|
+
/** Run `git diff --numstat` and return { codeLines, latMdLines }. */
|
|
118
|
+
function analyzeDiff(projectRoot) {
|
|
119
|
+
let output;
|
|
120
|
+
try {
|
|
121
|
+
output = execSync('git diff HEAD --numstat', {
|
|
122
|
+
cwd: projectRoot,
|
|
123
|
+
encoding: 'utf-8',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { codeLines: 0, latMdLines: 0 };
|
|
128
|
+
}
|
|
129
|
+
let codeLines = 0;
|
|
130
|
+
let latMdLines = 0;
|
|
131
|
+
// Each line: "added\tremoved\tfile" (e.g. "42\t11\tsrc/cli/hook.ts")
|
|
132
|
+
for (const line of output.split('\n')) {
|
|
133
|
+
const parts = line.split('\t');
|
|
134
|
+
if (parts.length < 3)
|
|
135
|
+
continue;
|
|
136
|
+
const added = parseInt(parts[0], 10) || 0;
|
|
137
|
+
const removed = parseInt(parts[1], 10) || 0;
|
|
138
|
+
const file = parts[2];
|
|
139
|
+
const changed = added + removed;
|
|
140
|
+
if (file.startsWith('lat.md/')) {
|
|
141
|
+
latMdLines += changed;
|
|
142
|
+
}
|
|
143
|
+
else if (SOURCE_EXTENSIONS.has(extname(file))) {
|
|
144
|
+
codeLines += changed;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { codeLines, latMdLines };
|
|
148
|
+
}
|
|
109
149
|
async function handleStop() {
|
|
110
|
-
// Only emit the reminder if we're in a project with lat.md
|
|
111
150
|
const latDir = findLatticeDir();
|
|
112
151
|
if (!latDir)
|
|
113
152
|
return;
|
|
@@ -121,11 +160,52 @@ async function handleStop() {
|
|
|
121
160
|
catch {
|
|
122
161
|
// If we can't parse stdin, treat as first attempt
|
|
123
162
|
}
|
|
124
|
-
//
|
|
125
|
-
|
|
163
|
+
// Always run lat check — even on second pass
|
|
164
|
+
const md = await checkMd(latDir);
|
|
165
|
+
const code = await checkCodeRefs(latDir);
|
|
166
|
+
const indexErrors = await checkIndex(latDir);
|
|
167
|
+
const sectionErrors = await checkSections(latDir);
|
|
168
|
+
const totalErrors = md.errors.length +
|
|
169
|
+
code.errors.length +
|
|
170
|
+
indexErrors.length +
|
|
171
|
+
sectionErrors.length;
|
|
172
|
+
const checkFailed = totalErrors > 0;
|
|
173
|
+
// Second pass — warn the user but don't block again
|
|
174
|
+
if (stopHookActive) {
|
|
175
|
+
if (checkFailed) {
|
|
176
|
+
console.error(`lat check is still failing (${totalErrors} error(s)). Run \`lat check\` to see details.`);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const projectRoot = dirname(latDir);
|
|
181
|
+
// Analyze git diff — flag when lat.md/ changes are < 5% of code changes
|
|
182
|
+
const { codeLines, latMdLines } = analyzeDiff(projectRoot);
|
|
183
|
+
let needsSync = false;
|
|
184
|
+
if (codeLines >= DIFF_THRESHOLD) {
|
|
185
|
+
// Round up lat.md lines to 1 if there are more than 5 code lines changed
|
|
186
|
+
// (a tiny lat.md touch still counts as effort)
|
|
187
|
+
const effectiveLatMd = latMdLines === 0 ? 0 : Math.max(latMdLines, 1);
|
|
188
|
+
needsSync = effectiveLatMd < codeLines * LATMD_RATIO;
|
|
189
|
+
}
|
|
190
|
+
// Nothing to do — let the agent stop cleanly
|
|
191
|
+
if (!checkFailed && !needsSync)
|
|
126
192
|
return;
|
|
127
193
|
const parts = [];
|
|
128
|
-
|
|
194
|
+
if (checkFailed && needsSync) {
|
|
195
|
+
parts.push('`lat check` found errors AND the codebase has changes (' +
|
|
196
|
+
codeLines +
|
|
197
|
+
' lines) with no updates to `lat.md/`. Before finishing:', '', '1. Update `lat.md/` to reflect your code changes — run `lat search` to find relevant sections.', '2. Run `lat check` until it passes.');
|
|
198
|
+
}
|
|
199
|
+
else if (checkFailed) {
|
|
200
|
+
parts.push('`lat check` found ' +
|
|
201
|
+
totalErrors +
|
|
202
|
+
' error(s). Run `lat check`, fix the errors, and repeat until it passes.');
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
parts.push('The codebase has changes (' +
|
|
206
|
+
codeLines +
|
|
207
|
+
' lines) but `lat.md/` was not updated. Update `lat.md/` to be in sync with the changes — run `lat search` to find relevant sections. Run `lat check` at the end.');
|
|
208
|
+
}
|
|
129
209
|
outputStop(parts.join('\n'));
|
|
130
210
|
}
|
|
131
211
|
export async function hookCmd(agent, event) {
|
package/dist/src/cli/section.js
CHANGED
|
@@ -22,13 +22,12 @@ export async function getSection(ctx, query) {
|
|
|
22
22
|
return { kind: 'no-match', suggestions: matches };
|
|
23
23
|
}
|
|
24
24
|
const section = top.section;
|
|
25
|
-
// Read raw content between startLine and
|
|
25
|
+
// Read raw content between startLine and the end of the last descendant
|
|
26
26
|
const absPath = join(ctx.projectRoot, section.filePath);
|
|
27
27
|
const fileContent = await readFile(absPath, 'utf-8');
|
|
28
28
|
const lines = fileContent.split('\n');
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
.join('\n');
|
|
29
|
+
const end = fullEndLine(section);
|
|
30
|
+
const content = lines.slice(section.startLine - 1, end).join('\n');
|
|
32
31
|
// Find outgoing wiki link targets within this section's content
|
|
33
32
|
const flat = flattenSections(allSections);
|
|
34
33
|
const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
|
|
@@ -73,6 +72,11 @@ export async function getSection(ctx, query) {
|
|
|
73
72
|
}
|
|
74
73
|
return { kind: 'found', section, content, outgoingRefs, incomingRefs };
|
|
75
74
|
}
|
|
75
|
+
function fullEndLine(section) {
|
|
76
|
+
if (section.children.length === 0)
|
|
77
|
+
return section.endLine;
|
|
78
|
+
return fullEndLine(section.children[section.children.length - 1]);
|
|
79
|
+
}
|
|
76
80
|
function truncate(s, max) {
|
|
77
81
|
return s.length > max ? s.slice(0, max) + '...' : s;
|
|
78
82
|
}
|
|
@@ -7,6 +7,8 @@ export type SourceSymbol = {
|
|
|
7
7
|
endLine: number;
|
|
8
8
|
signature: string;
|
|
9
9
|
};
|
|
10
|
+
/** All source file extensions that lat can parse (derived from grammarMap). */
|
|
11
|
+
export declare const SOURCE_EXTENSIONS: ReadonlySet<string>;
|
|
10
12
|
export declare function parseSourceSymbols(filePath: string, content: string): Promise<SourceSymbol[]>;
|
|
11
13
|
/**
|
|
12
14
|
* Check whether a source file path (relative to projectRoot) has a given symbol.
|
|
@@ -21,18 +21,22 @@ async function ensureParser() {
|
|
|
21
21
|
}
|
|
22
22
|
return parserInstance;
|
|
23
23
|
}
|
|
24
|
+
/** Extension → tree-sitter WASM grammar mapping. This is the single source of
|
|
25
|
+
* truth for which source file extensions lat supports. */
|
|
26
|
+
const grammarMap = {
|
|
27
|
+
'.ts': 'tree-sitter-typescript.wasm',
|
|
28
|
+
'.tsx': 'tree-sitter-tsx.wasm',
|
|
29
|
+
'.js': 'tree-sitter-javascript.wasm',
|
|
30
|
+
'.jsx': 'tree-sitter-javascript.wasm',
|
|
31
|
+
'.py': 'tree-sitter-python.wasm',
|
|
32
|
+
'.rs': 'tree-sitter-rust.wasm',
|
|
33
|
+
'.go': 'tree-sitter-go.wasm',
|
|
34
|
+
'.c': 'tree-sitter-c.wasm',
|
|
35
|
+
'.h': 'tree-sitter-c.wasm',
|
|
36
|
+
};
|
|
37
|
+
/** All source file extensions that lat can parse (derived from grammarMap). */
|
|
38
|
+
export const SOURCE_EXTENSIONS = new Set(Object.keys(grammarMap));
|
|
24
39
|
async function getLanguage(ext) {
|
|
25
|
-
const grammarMap = {
|
|
26
|
-
'.ts': 'tree-sitter-typescript.wasm',
|
|
27
|
-
'.tsx': 'tree-sitter-tsx.wasm',
|
|
28
|
-
'.js': 'tree-sitter-javascript.wasm',
|
|
29
|
-
'.jsx': 'tree-sitter-javascript.wasm',
|
|
30
|
-
'.py': 'tree-sitter-python.wasm',
|
|
31
|
-
'.rs': 'tree-sitter-rust.wasm',
|
|
32
|
-
'.go': 'tree-sitter-go.wasm',
|
|
33
|
-
'.c': 'tree-sitter-c.wasm',
|
|
34
|
-
'.h': 'tree-sitter-c.wasm',
|
|
35
|
-
};
|
|
36
40
|
const wasmFile = grammarMap[ext];
|
|
37
41
|
if (!wasmFile)
|
|
38
42
|
return null;
|