llmnav 0.5.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 (68) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +294 -0
  4. package/ROADMAP.md +71 -0
  5. package/bin/llmnav.js +16 -0
  6. package/docs/agent-integration.md +114 -0
  7. package/docs/api.md +290 -0
  8. package/docs/architecture.md +286 -0
  9. package/docs/benchmarking.md +164 -0
  10. package/docs/ci.md +196 -0
  11. package/docs/cli.md +233 -0
  12. package/docs/configuration.md +117 -0
  13. package/docs/editor-integration.md +29 -0
  14. package/docs/faq.md +59 -0
  15. package/docs/graph.md +92 -0
  16. package/docs/language-examples.md +130 -0
  17. package/docs/migration.md +130 -0
  18. package/docs/performance-v0.2.md +42 -0
  19. package/docs/provider-neutral-integration.md +66 -0
  20. package/docs/publishing.md +86 -0
  21. package/docs/quickstart.md +139 -0
  22. package/docs/research.md +31 -0
  23. package/docs/spec.md +424 -0
  24. package/examples/provider-neutral-host.d.mts +17 -0
  25. package/examples/provider-neutral-host.mjs +40 -0
  26. package/package.json +79 -0
  27. package/schema/config.schema.json +296 -0
  28. package/src/agent-protocol.js +117 -0
  29. package/src/agent-tools.js +61 -0
  30. package/src/agents.js +127 -0
  31. package/src/boundaries.js +50 -0
  32. package/src/changes.js +168 -0
  33. package/src/cli.js +459 -0
  34. package/src/config.js +305 -0
  35. package/src/contracts.js +70 -0
  36. package/src/declaration.js +334 -0
  37. package/src/doctor.js +124 -0
  38. package/src/editor.js +107 -0
  39. package/src/evaluation.js +67 -0
  40. package/src/files.js +81 -0
  41. package/src/formatter.js +23 -0
  42. package/src/generator.js +528 -0
  43. package/src/graph-input.js +157 -0
  44. package/src/graph.js +403 -0
  45. package/src/incremental.js +262 -0
  46. package/src/index.d.ts +673 -0
  47. package/src/index.js +115 -0
  48. package/src/initializer.js +137 -0
  49. package/src/inverted-index.js +350 -0
  50. package/src/parser.js +449 -0
  51. package/src/project.js +65 -0
  52. package/src/prompt-bundle.js +108 -0
  53. package/src/registry.js +107 -0
  54. package/src/sarif.js +70 -0
  55. package/src/search-shards.js +75 -0
  56. package/src/search.js +636 -0
  57. package/src/spec.d.ts +27 -0
  58. package/src/spec.js +237 -0
  59. package/src/tokenizer.js +37 -0
  60. package/src/transaction.js +557 -0
  61. package/src/util.js +256 -0
  62. package/src/validator.js +635 -0
  63. package/templates/file-card.txt +8 -0
  64. package/templates/lexicon.json +7 -0
  65. package/templates/line-card.txt +9 -0
  66. package/templates/module-card.txt +9 -0
  67. package/templates/queries.jsonl +1 -0
  68. package/templates/symbol-card.txt +10 -0
@@ -0,0 +1,262 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.index.incremental
3
+ role=Reuse unchanged file parses and card records while rebuilding deterministic generated artifacts.
4
+ owns=file fingerprints|parsed-file state|incremental scan metrics
5
+ excludes=source comment mutation|cache directory commit
6
+ search=incremental indexing|file cache|card reuse
7
+ rel=workflow>llmnav.index.generate
8
+ stability=architecture
9
+ */
10
+
11
+ import { readFile, stat } from "node:fs/promises";
12
+ import path from "node:path";
13
+ import { loadConfig } from "./config.js";
14
+ import { findAttachedDeclaration, extractImports } from "./declaration.js";
15
+ import { collectSourceFiles } from "./files.js";
16
+ import { parseLlmnavBlocks } from "./parser.js";
17
+ import { loadRegistry } from "./registry.js";
18
+ import {
19
+ atomicWrite,
20
+ compareText,
21
+ readJsonSafe,
22
+ relativePosix,
23
+ sha256,
24
+ stableStringify,
25
+ } from "./util.js";
26
+
27
+ export const FILE_STATE_SCHEMA_VERSION = 1;
28
+ export const SOURCE_INDEXER_VERSION = 4;
29
+ const STAT_HINTS_SCHEMA_VERSION = 1;
30
+
31
+ export async function scanProjectIncremental(root, options = {}) {
32
+ const { config, configPath } = await loadConfig(root);
33
+ const files = await collectSourceFiles(root, config, options.paths ?? []);
34
+ const cacheDirectory = path.join(root, config.generation.cacheDirectory);
35
+ const previousState = options.previousState ?? await readJsonSafe(path.join(cacheDirectory, "file-state.json"), null);
36
+ const hintsPath = path.join(root, ".llmnav", "state", "stat-hints.json");
37
+ const previousHints = options.useStatHints === false
38
+ ? null
39
+ : await readJsonSafe(hintsPath, null);
40
+ const previousFiles = usableFileState(previousState) ? mapStateFiles(previousState.files) : new Map();
41
+ const hintFiles = usableStatHints(previousHints) ? previousHints.files ?? {} : {};
42
+
43
+ const fileRecords = [];
44
+ const records = [];
45
+ const nextStateFiles = [];
46
+ const nextHintFiles = {};
47
+ const seenPaths = new Set();
48
+ const stats = {
49
+ totalFiles: files.length,
50
+ parsedFiles: 0,
51
+ reusedFiles: 0,
52
+ reusedFilesByStat: 0,
53
+ reusedFilesByHash: 0,
54
+ deletedFiles: 0,
55
+ bytesRead: 0,
56
+ cardsParsed: 0,
57
+ cardsReused: 0,
58
+ };
59
+ let sourceBytes = 0;
60
+ let semanticBytes = 0;
61
+
62
+ for (const absolutePath of files) {
63
+ const relativePath = relativePosix(root, absolutePath);
64
+ const details = await stat(absolutePath, { bigint: true });
65
+ const fingerprint = statFingerprint(details);
66
+ nextHintFiles[relativePath] = fingerprint;
67
+ seenPaths.add(relativePath);
68
+ const previousFile = previousFiles.get(relativePath);
69
+ const previousHint = hintFiles[relativePath];
70
+ let stateFile;
71
+
72
+ if (previousFile && fingerprintsEqual(previousHint, fingerprint)) {
73
+ stateFile = previousFile;
74
+ stats.reusedFiles += 1;
75
+ stats.reusedFilesByStat += 1;
76
+ stats.cardsReused += previousFile.blocks.length;
77
+ } else {
78
+ const source = await readFile(absolutePath, "utf8");
79
+ stats.bytesRead += Buffer.byteLength(source);
80
+ const contentHash = sha256(source);
81
+ if (previousFile?.contentHash === contentHash) {
82
+ stateFile = previousFile;
83
+ stats.reusedFiles += 1;
84
+ stats.reusedFilesByHash += 1;
85
+ stats.cardsReused += previousFile.blocks.length;
86
+ } else {
87
+ stateFile = analyzeFile(relativePath, source);
88
+ stats.parsedFiles += 1;
89
+ stats.cardsParsed += stateFile.blocks.length;
90
+ }
91
+ }
92
+
93
+ const normalizedStateFile = normalizeStateFile(stateFile, relativePath);
94
+ nextStateFiles.push(normalizedStateFile);
95
+ sourceBytes += normalizedStateFile.sourceBytes;
96
+ semanticBytes += normalizedStateFile.semanticBytes;
97
+ const fileRecord = hydrateFileRecord(root, absolutePath, normalizedStateFile);
98
+ fileRecords.push(fileRecord);
99
+ records.push(...hydrateRecords(root, absolutePath, normalizedStateFile));
100
+ }
101
+
102
+ for (const previousPath of previousFiles.keys()) {
103
+ if (!seenPaths.has(previousPath)) stats.deletedFiles += 1;
104
+ }
105
+
106
+ nextStateFiles.sort((left, right) => compareText(left.path, right.path));
107
+ const fileState = {
108
+ schemaVersion: FILE_STATE_SCHEMA_VERSION,
109
+ indexerVersion: SOURCE_INDEXER_VERSION,
110
+ files: nextStateFiles,
111
+ };
112
+ const statHints = {
113
+ schemaVersion: STAT_HINTS_SCHEMA_VERSION,
114
+ files: Object.fromEntries(Object.entries(nextHintFiles).sort(([left], [right]) => compareText(left, right))),
115
+ };
116
+ const registry = await loadRegistry(root);
117
+ const project = {
118
+ root,
119
+ config,
120
+ configPath,
121
+ files,
122
+ fileRecords,
123
+ records,
124
+ registry,
125
+ sourceBytes,
126
+ semanticBytes,
127
+ incremental: stats,
128
+ };
129
+
130
+ return { project, fileState, statHints, hintsPath, stats };
131
+ }
132
+
133
+ export function buildFileStateFromProject(project) {
134
+ const files = project.fileRecords.map((fileRecord) => {
135
+ const source = fileRecord.source ?? "";
136
+ const declarations = fileRecord.blocks.map((block) =>
137
+ findAttachedDeclaration(source, block, fileRecord.relativePath),
138
+ );
139
+ return normalizeStateFile(
140
+ {
141
+ path: fileRecord.relativePath,
142
+ contentHash: fileRecord.contentHash ?? sha256(source),
143
+ sourceBytes: Buffer.byteLength(source),
144
+ semanticBytes: fileRecord.blocks.reduce((sum, block) => sum + Buffer.byteLength(block.raw), 0),
145
+ imports: fileRecord.imports,
146
+ blocks: fileRecord.blocks,
147
+ declarations,
148
+ },
149
+ fileRecord.relativePath,
150
+ );
151
+ });
152
+ files.sort((left, right) => compareText(left.path, right.path));
153
+ return {
154
+ schemaVersion: FILE_STATE_SCHEMA_VERSION,
155
+ indexerVersion: SOURCE_INDEXER_VERSION,
156
+ files,
157
+ };
158
+ }
159
+
160
+ export async function persistStatHints(hintsPath, statHints) {
161
+ await atomicWrite(hintsPath, stableStringify(statHints));
162
+ }
163
+
164
+ export function renderFileState(fileState) {
165
+ return stableStringify(fileState);
166
+ }
167
+
168
+ export function usableFileState(value) {
169
+ return Boolean(
170
+ value &&
171
+ value.schemaVersion === FILE_STATE_SCHEMA_VERSION &&
172
+ value.indexerVersion === SOURCE_INDEXER_VERSION &&
173
+ Array.isArray(value.files),
174
+ );
175
+ }
176
+
177
+ function analyzeFile(relativePath, source) {
178
+ const blocks = parseLlmnavBlocks(source, relativePath);
179
+ return {
180
+ path: relativePath,
181
+ contentHash: sha256(source),
182
+ sourceBytes: Buffer.byteLength(source),
183
+ semanticBytes: blocks.reduce((sum, block) => sum + Buffer.byteLength(block.raw), 0),
184
+ imports: extractImports(source, relativePath),
185
+ blocks,
186
+ declarations: blocks.map((block) => findAttachedDeclaration(source, block, relativePath)),
187
+ };
188
+ }
189
+
190
+ function normalizeStateFile(file, relativePath) {
191
+ return {
192
+ path: relativePath,
193
+ contentHash: String(file.contentHash),
194
+ sourceBytes: Number(file.sourceBytes),
195
+ semanticBytes: Number(file.semanticBytes),
196
+ imports: [...(file.imports ?? [])],
197
+ blocks: structuredClone(file.blocks ?? []),
198
+ declarations: structuredClone(file.declarations ?? []),
199
+ };
200
+ }
201
+
202
+ function hydrateFileRecord(root, absolutePath, stateFile) {
203
+ return {
204
+ absolutePath,
205
+ relativePath: stateFile.path,
206
+ source: null,
207
+ contentHash: stateFile.contentHash,
208
+ bodyHash: stateFile.contentHash,
209
+ sourceBytes: stateFile.sourceBytes,
210
+ semanticBytes: stateFile.semanticBytes,
211
+ blocks: stateFile.blocks,
212
+ imports: stateFile.imports,
213
+ root,
214
+ };
215
+ }
216
+
217
+ function hydrateRecords(root, absolutePath, stateFile) {
218
+ return stateFile.blocks.map((block, index) => {
219
+ const declaration = stateFile.declarations[index] ?? null;
220
+ return {
221
+ root,
222
+ absolutePath,
223
+ relativePath: stateFile.path,
224
+ source: null,
225
+ bodyHash: declaration?.bodyHash ?? stateFile.contentHash,
226
+ imports: stateFile.imports,
227
+ block,
228
+ card: block.card,
229
+ declaration,
230
+ };
231
+ });
232
+ }
233
+
234
+ function mapStateFiles(files) {
235
+ const output = new Map();
236
+ for (const file of files ?? []) {
237
+ if (!file || typeof file.path !== "string" || !Array.isArray(file.blocks)) continue;
238
+ output.set(file.path, file);
239
+ }
240
+ return output;
241
+ }
242
+
243
+ function statFingerprint(details) {
244
+ return {
245
+ size: details.size.toString(),
246
+ mtimeNs: details.mtimeNs.toString(),
247
+ ctimeNs: details.ctimeNs.toString(),
248
+ };
249
+ }
250
+
251
+ function fingerprintsEqual(left, right) {
252
+ return Boolean(
253
+ left &&
254
+ left.size === right.size &&
255
+ left.mtimeNs === right.mtimeNs &&
256
+ left.ctimeNs === right.ctimeNs,
257
+ );
258
+ }
259
+
260
+ function usableStatHints(value) {
261
+ return Boolean(value && value.schemaVersion === STAT_HINTS_SCHEMA_VERSION && value.files && typeof value.files === "object");
262
+ }