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.
- package/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/ROADMAP.md +71 -0
- package/bin/llmnav.js +16 -0
- package/docs/agent-integration.md +114 -0
- package/docs/api.md +290 -0
- package/docs/architecture.md +286 -0
- package/docs/benchmarking.md +164 -0
- package/docs/ci.md +196 -0
- package/docs/cli.md +233 -0
- package/docs/configuration.md +117 -0
- package/docs/editor-integration.md +29 -0
- package/docs/faq.md +59 -0
- package/docs/graph.md +92 -0
- package/docs/language-examples.md +130 -0
- package/docs/migration.md +130 -0
- package/docs/performance-v0.2.md +42 -0
- package/docs/provider-neutral-integration.md +66 -0
- package/docs/publishing.md +86 -0
- package/docs/quickstart.md +139 -0
- package/docs/research.md +31 -0
- package/docs/spec.md +424 -0
- package/examples/provider-neutral-host.d.mts +17 -0
- package/examples/provider-neutral-host.mjs +40 -0
- package/package.json +79 -0
- package/schema/config.schema.json +296 -0
- package/src/agent-protocol.js +117 -0
- package/src/agent-tools.js +61 -0
- package/src/agents.js +127 -0
- package/src/boundaries.js +50 -0
- package/src/changes.js +168 -0
- package/src/cli.js +459 -0
- package/src/config.js +305 -0
- package/src/contracts.js +70 -0
- package/src/declaration.js +334 -0
- package/src/doctor.js +124 -0
- package/src/editor.js +107 -0
- package/src/evaluation.js +67 -0
- package/src/files.js +81 -0
- package/src/formatter.js +23 -0
- package/src/generator.js +528 -0
- package/src/graph-input.js +157 -0
- package/src/graph.js +403 -0
- package/src/incremental.js +262 -0
- package/src/index.d.ts +673 -0
- package/src/index.js +115 -0
- package/src/initializer.js +137 -0
- package/src/inverted-index.js +350 -0
- package/src/parser.js +449 -0
- package/src/project.js +65 -0
- package/src/prompt-bundle.js +108 -0
- package/src/registry.js +107 -0
- package/src/sarif.js +70 -0
- package/src/search-shards.js +75 -0
- package/src/search.js +636 -0
- package/src/spec.d.ts +27 -0
- package/src/spec.js +237 -0
- package/src/tokenizer.js +37 -0
- package/src/transaction.js +557 -0
- package/src/util.js +256 -0
- package/src/validator.js +635 -0
- package/templates/file-card.txt +8 -0
- package/templates/lexicon.json +7 -0
- package/templates/line-card.txt +9 -0
- package/templates/module-card.txt +9 -0
- package/templates/queries.jsonl +1 -0
- package/templates/symbol-card.txt +10 -0
package/src/files.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { lstat, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compareText, matchesAnyGlob, relativePosix, toPosix } from "./util.js";
|
|
4
|
+
|
|
5
|
+
export async function findProjectRoot(start = process.cwd()) {
|
|
6
|
+
let current = path.resolve(start);
|
|
7
|
+
let fallback = null;
|
|
8
|
+
for (;;) {
|
|
9
|
+
if (await pathExists(path.join(current, ".llmnav"))) return current;
|
|
10
|
+
if (fallback === null) {
|
|
11
|
+
if ((await pathExists(path.join(current, "package.json"))) || (await pathExists(path.join(current, ".git")))) {
|
|
12
|
+
fallback = current;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const parent = path.dirname(current);
|
|
16
|
+
if (parent === current) return fallback ?? path.resolve(start);
|
|
17
|
+
current = parent;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function pathExists(candidate) {
|
|
22
|
+
try {
|
|
23
|
+
await stat(candidate);
|
|
24
|
+
return true;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error && typeof error === "object" && error.code === "ENOENT") return false;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function collectSourceFiles(root, config, requestedPaths = []) {
|
|
32
|
+
const roots = requestedPaths.length > 0 ? requestedPaths : config.sourceRoots;
|
|
33
|
+
const files = new Map();
|
|
34
|
+
for (const sourceRoot of roots) {
|
|
35
|
+
const absolute = path.resolve(root, sourceRoot);
|
|
36
|
+
assertInsideRoot(root, absolute, sourceRoot);
|
|
37
|
+
try {
|
|
38
|
+
const linkDetails = await lstat(absolute);
|
|
39
|
+
if (linkDetails.isSymbolicLink()) throw new Error(`Source path ${JSON.stringify(sourceRoot)} is a symbolic link.`);
|
|
40
|
+
const details = await stat(absolute);
|
|
41
|
+
if (details.isFile()) {
|
|
42
|
+
if (shouldIncludeFile(root, absolute, config)) files.set(absolute, true);
|
|
43
|
+
} else if (details.isDirectory()) {
|
|
44
|
+
await walkDirectory(root, absolute, config, files);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...files.keys()].sort((left, right) => compareText(relativePosix(root, left), relativePosix(root, right)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertInsideRoot(root, absolutePath, sourcePath) {
|
|
54
|
+
const relative = path.relative(path.resolve(root), absolutePath);
|
|
55
|
+
if (relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))) return;
|
|
56
|
+
throw new Error(`Source path ${JSON.stringify(sourcePath)} escapes the repository root.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function walkDirectory(root, directory, config, files) {
|
|
60
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
61
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const absolute = path.join(directory, entry.name);
|
|
64
|
+
const relative = relativePosix(root, absolute);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
if (config.excludeDirectories.includes(entry.name)) continue;
|
|
67
|
+
if (relative.startsWith(`${toPosix(config.generation.cacheDirectory)}/`)) continue;
|
|
68
|
+
await walkDirectory(root, absolute, config, files);
|
|
69
|
+
} else if (entry.isFile() && shouldIncludeFile(root, absolute, config)) {
|
|
70
|
+
files.set(absolute, true);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shouldIncludeFile(root, absolutePath, config) {
|
|
76
|
+
const extension = path.extname(absolutePath).toLowerCase();
|
|
77
|
+
if (!config.includeExtensions.includes(extension)) return false;
|
|
78
|
+
const relative = relativePosix(root, absolutePath);
|
|
79
|
+
if (matchesAnyGlob(relative, config.excludeFiles)) return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
package/src/formatter.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { collectSourceFiles } from "./files.js";
|
|
4
|
+
import { canonicalizeSource } from "./parser.js";
|
|
5
|
+
import { atomicWrite, relativePosix } from "./util.js";
|
|
6
|
+
|
|
7
|
+
export async function formatProject(root, options = {}) {
|
|
8
|
+
const { config } = await loadConfig(root);
|
|
9
|
+
const files = await collectSourceFiles(root, config, options.paths ?? []);
|
|
10
|
+
const changedFiles = [];
|
|
11
|
+
const errors = [];
|
|
12
|
+
for (const filePath of files) {
|
|
13
|
+
const source = await readFile(filePath, "utf8");
|
|
14
|
+
const relativePath = relativePosix(root, filePath);
|
|
15
|
+
const result = canonicalizeSource(source, relativePath);
|
|
16
|
+
errors.push(...result.errors.map((error) => ({ file: relativePath, ...error })));
|
|
17
|
+
if (!result.changed) continue;
|
|
18
|
+
changedFiles.push(relativePath);
|
|
19
|
+
if (!options.check) await atomicWrite(filePath, result.source);
|
|
20
|
+
}
|
|
21
|
+
const ok = errors.length === 0 && (options.check ? changedFiles.length === 0 : true);
|
|
22
|
+
return { ok, changedFiles, errors };
|
|
23
|
+
}
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.index.generate
|
|
3
|
+
role=Compile validated semantic cards into deterministic repository and module catalogs for coding agents.
|
|
4
|
+
owns=stable card order|generated index|cache catalogs
|
|
5
|
+
excludes=source semantics|agent search decisions
|
|
6
|
+
search=repository map|semantic index|cache catalog
|
|
7
|
+
rel=workflow>llmnav.rules.validate
|
|
8
|
+
rel=workflow>llmnav.index.incremental
|
|
9
|
+
rel=workflow>llmnav.index.inverted
|
|
10
|
+
rel=workflow>llmnav.index.transaction
|
|
11
|
+
stability=architecture
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdir } from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { cardToCanonicalObject } from "./parser.js";
|
|
17
|
+
import { scanProject } from "./project.js";
|
|
18
|
+
import { mergeActiveIds, renderRegistryRecords } from "./registry.js";
|
|
19
|
+
import { countDiagnostics, validateProject } from "./validator.js";
|
|
20
|
+
import {
|
|
21
|
+
assertNoSymlinkTraversal,
|
|
22
|
+
compareText,
|
|
23
|
+
readJsonSafe,
|
|
24
|
+
readText,
|
|
25
|
+
relativePosix,
|
|
26
|
+
sha256,
|
|
27
|
+
stableJson,
|
|
28
|
+
stableStringify,
|
|
29
|
+
toPosix,
|
|
30
|
+
} from "./util.js";
|
|
31
|
+
import { PACKAGE_VERSION, SPEC_VERSION } from "./spec.js";
|
|
32
|
+
import {
|
|
33
|
+
buildFileStateFromProject,
|
|
34
|
+
persistStatHints,
|
|
35
|
+
renderFileState,
|
|
36
|
+
scanProjectIncremental,
|
|
37
|
+
} from "./incremental.js";
|
|
38
|
+
import { buildInvertedIndex, renderSearchIndex } from "./inverted-index.js";
|
|
39
|
+
import {
|
|
40
|
+
buildModuleCatalogMetadata,
|
|
41
|
+
compareCardIndexes,
|
|
42
|
+
describeAffectedBoundaries,
|
|
43
|
+
describeAffectedCatalogs,
|
|
44
|
+
moduleIdForCard,
|
|
45
|
+
safeModuleName,
|
|
46
|
+
} from "./changes.js";
|
|
47
|
+
import { commitGeneratedCache, recoverGenerationTransaction, withGenerationLock } from "./transaction.js";
|
|
48
|
+
import { loadConfig } from "./config.js";
|
|
49
|
+
import { buildContractFingerprints, compareContractFingerprints } from "./contracts.js";
|
|
50
|
+
import { detectBoundaries } from "./boundaries.js";
|
|
51
|
+
import { buildSearchShards, SEARCH_SHARD_SCHEMA_VERSION } from "./search-shards.js";
|
|
52
|
+
import { loadGraphInputs } from "./graph-input.js";
|
|
53
|
+
import {
|
|
54
|
+
buildRepositoryGraphIncremental,
|
|
55
|
+
GRAPH_SCHEMA_VERSION,
|
|
56
|
+
GRAPH_STATE_SCHEMA_VERSION,
|
|
57
|
+
renderGraphState,
|
|
58
|
+
renderRepositoryGraph,
|
|
59
|
+
} from "./graph.js";
|
|
60
|
+
import { AGENT_PROTOCOL } from "./agents.js";
|
|
61
|
+
import { getAgentToolDefinitions } from "./agent-tools.js";
|
|
62
|
+
import {
|
|
63
|
+
buildPromptPrefixBundle,
|
|
64
|
+
PROMPT_BUNDLE_SCHEMA_VERSION,
|
|
65
|
+
renderPromptPrefixBundle,
|
|
66
|
+
} from "./prompt-bundle.js";
|
|
67
|
+
|
|
68
|
+
export async function generateProject(root, options = {}) {
|
|
69
|
+
return withGenerationLock(
|
|
70
|
+
root,
|
|
71
|
+
(lock) => generateProjectLocked(root, { ...options, lockOwnerId: lock.ownerId }),
|
|
72
|
+
options.lockOptions,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function generateProjectLocked(root, options) {
|
|
77
|
+
const { config: recoveryConfig } = await loadConfig(root);
|
|
78
|
+
const recovery = await recoverGenerationTransaction(root, {
|
|
79
|
+
cacheDirectory: recoveryConfig.generation.cacheDirectory,
|
|
80
|
+
renameOptions: options.renameOptions,
|
|
81
|
+
lockOwnerId: options.lockOwnerId,
|
|
82
|
+
});
|
|
83
|
+
const cacheDirectory = path.join(root, recoveryConfig.generation.cacheDirectory);
|
|
84
|
+
const previousIndex = await readJsonSafe(path.join(cacheDirectory, "index.json"), null);
|
|
85
|
+
const previousSearchIndex = await readJsonSafe(path.join(cacheDirectory, "search-index.json"), null);
|
|
86
|
+
const previousGraphState = await readJsonSafe(path.join(cacheDirectory, "graph-state.json"), null);
|
|
87
|
+
|
|
88
|
+
let project;
|
|
89
|
+
let fileState;
|
|
90
|
+
let statHints = null;
|
|
91
|
+
let hintsPath = null;
|
|
92
|
+
let scanStats;
|
|
93
|
+
if (options.incremental === false) {
|
|
94
|
+
project = await scanProject(root, options);
|
|
95
|
+
fileState = buildFileStateFromProject(project);
|
|
96
|
+
scanStats = {
|
|
97
|
+
totalFiles: project.fileRecords.length,
|
|
98
|
+
parsedFiles: project.fileRecords.length,
|
|
99
|
+
reusedFiles: 0,
|
|
100
|
+
reusedFilesByStat: 0,
|
|
101
|
+
reusedFilesByHash: 0,
|
|
102
|
+
deletedFiles: 0,
|
|
103
|
+
bytesRead: project.sourceBytes,
|
|
104
|
+
cardsParsed: project.records.length,
|
|
105
|
+
cardsReused: 0,
|
|
106
|
+
};
|
|
107
|
+
} else {
|
|
108
|
+
const incremental = await scanProjectIncremental(root, {
|
|
109
|
+
...options,
|
|
110
|
+
useStatHints: options.useStatHints,
|
|
111
|
+
});
|
|
112
|
+
({ project, fileState, statHints, hintsPath, stats: scanStats } = incremental);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const graphInputs = await loadGraphInputs(root, project.config);
|
|
116
|
+
project.graphInputs = graphInputs.indexes;
|
|
117
|
+
const diagnostics = [...validateProject(project), ...graphInputs.diagnostics].sort(compareGeneratedDiagnostics);
|
|
118
|
+
let counts = countDiagnostics(diagnostics);
|
|
119
|
+
if (counts.error > 0) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
diagnostics,
|
|
123
|
+
changedFiles: [],
|
|
124
|
+
changedCards: [],
|
|
125
|
+
affectedBoundaries: [],
|
|
126
|
+
affectedCatalogs: [],
|
|
127
|
+
project,
|
|
128
|
+
counts,
|
|
129
|
+
incremental: { enabled: options.incremental !== false, files: scanStats, cards: null, graph: null },
|
|
130
|
+
transaction: { committed: false, skipped: true, recovered: recovery.recovered, recoveryAction: recovery.action },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ids = project.records.map((record) => record.card.id).filter(Boolean);
|
|
135
|
+
const order = await buildStableOrder(root, ids);
|
|
136
|
+
const built = buildArtifactSet(project, order, {
|
|
137
|
+
previousSearchIndex: options.incremental === false ? null : previousSearchIndex,
|
|
138
|
+
previousGraphState: options.incremental === false ? null : previousGraphState,
|
|
139
|
+
fileState,
|
|
140
|
+
});
|
|
141
|
+
const contractChanges = compareContractFingerprints(
|
|
142
|
+
previousIndex?.contractFingerprints,
|
|
143
|
+
built.index.contractFingerprints,
|
|
144
|
+
);
|
|
145
|
+
for (const change of contractChanges) {
|
|
146
|
+
diagnostics.push(
|
|
147
|
+
diagnosticForContractChange(change),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
diagnostics.sort(compareGeneratedDiagnostics);
|
|
151
|
+
counts = countDiagnostics(diagnostics);
|
|
152
|
+
const artifacts = built.artifacts;
|
|
153
|
+
const changedFiles = await compareArtifacts(root, project.config.generation.cacheDirectory, artifacts);
|
|
154
|
+
const orderContent = renderOrder(order);
|
|
155
|
+
const orderChanged = await compareOne(path.join(root, ".llmnav", "order.lock"), orderContent);
|
|
156
|
+
if (orderChanged) changedFiles.push(".llmnav/order.lock");
|
|
157
|
+
|
|
158
|
+
const missingRegistryIds = ids.filter((id) => !project.registry.byId.has(id));
|
|
159
|
+
if (missingRegistryIds.length > 0) changedFiles.push(".llmnav/ids.jsonl");
|
|
160
|
+
|
|
161
|
+
const uniqueChangedFiles = [...new Set(changedFiles.map(toPosix))].sort(compareText);
|
|
162
|
+
const changedCards = compareCardIndexes(previousIndex, built.index);
|
|
163
|
+
const affectedBoundaries = describeAffectedBoundaries(
|
|
164
|
+
changedCards,
|
|
165
|
+
project.config,
|
|
166
|
+
previousIndex,
|
|
167
|
+
built.index,
|
|
168
|
+
);
|
|
169
|
+
const affectedCatalogs = describeAffectedCatalogs(
|
|
170
|
+
uniqueChangedFiles,
|
|
171
|
+
project.config.generation.cacheDirectory,
|
|
172
|
+
project.config,
|
|
173
|
+
previousIndex,
|
|
174
|
+
built.index,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
let transaction = {
|
|
178
|
+
committed: false,
|
|
179
|
+
skipped: true,
|
|
180
|
+
recovered: recovery.recovered,
|
|
181
|
+
recoveryAction: recovery.action,
|
|
182
|
+
};
|
|
183
|
+
let statHintsPersisted = false;
|
|
184
|
+
let statHintsError = null;
|
|
185
|
+
|
|
186
|
+
if (!options.check) {
|
|
187
|
+
await assertNoSymlinkTraversal(root, path.join(root, ".llmnav"), ".llmnav");
|
|
188
|
+
await assertNoSymlinkTraversal(
|
|
189
|
+
root,
|
|
190
|
+
path.join(root, project.config.generation.cacheDirectory),
|
|
191
|
+
project.config.generation.cacheDirectory,
|
|
192
|
+
);
|
|
193
|
+
const cachePrefix = `${toPosix(project.config.generation.cacheDirectory).replace(/\/+$/u, "")}/`;
|
|
194
|
+
const cacheChanges = uniqueChangedFiles.filter((file) => file.startsWith(cachePrefix));
|
|
195
|
+
const controlArtifacts = new Map();
|
|
196
|
+
if (missingRegistryIds.length > 0) {
|
|
197
|
+
controlArtifacts.set(
|
|
198
|
+
".llmnav/ids.jsonl",
|
|
199
|
+
renderRegistryRecords(mergeActiveIds(project.registry, ids).records),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (orderChanged) controlArtifacts.set(".llmnav/order.lock", orderContent);
|
|
203
|
+
if (cacheChanges.length > 0 || controlArtifacts.size > 0) {
|
|
204
|
+
transaction = await commitGeneratedCache(
|
|
205
|
+
root,
|
|
206
|
+
project.config.generation.cacheDirectory,
|
|
207
|
+
artifacts,
|
|
208
|
+
{
|
|
209
|
+
failpoint: options.failpoint,
|
|
210
|
+
onPhase: options.onTransactionPhase,
|
|
211
|
+
renameOptions: options.renameOptions,
|
|
212
|
+
lockOwnerId: options.lockOwnerId,
|
|
213
|
+
controlArtifacts,
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (statHints && hintsPath) {
|
|
219
|
+
try {
|
|
220
|
+
await persistStatHints(hintsPath, statHints);
|
|
221
|
+
statHintsPersisted = true;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
statHintsError = error instanceof Error ? error.message : String(error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
ok: options.check ? uniqueChangedFiles.length === 0 : true,
|
|
230
|
+
diagnostics,
|
|
231
|
+
changedFiles: uniqueChangedFiles,
|
|
232
|
+
changedCards,
|
|
233
|
+
affectedBoundaries,
|
|
234
|
+
affectedCatalogs,
|
|
235
|
+
project,
|
|
236
|
+
counts,
|
|
237
|
+
artifacts,
|
|
238
|
+
index: built.index,
|
|
239
|
+
searchIndex: built.searchIndex,
|
|
240
|
+
graph: built.graph,
|
|
241
|
+
incremental: {
|
|
242
|
+
enabled: options.incremental !== false,
|
|
243
|
+
files: scanStats,
|
|
244
|
+
cards: built.searchStats,
|
|
245
|
+
graph: built.graphStats,
|
|
246
|
+
statHintsPersisted,
|
|
247
|
+
statHintsError,
|
|
248
|
+
},
|
|
249
|
+
transaction,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildArtifacts(project, order, options = {}) {
|
|
254
|
+
return buildArtifactSet(project, order, options).artifacts;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function buildArtifactSet(project, order, options = {}) {
|
|
258
|
+
const orderMap = new Map(order.map((id, index) => [id, index]));
|
|
259
|
+
const cards = project.records
|
|
260
|
+
.filter((record) => record.card.id)
|
|
261
|
+
.map((record) => indexedCard(record))
|
|
262
|
+
.sort((left, right) => {
|
|
263
|
+
const orderDifference = (orderMap.get(left.id) ?? Number.MAX_SAFE_INTEGER) -
|
|
264
|
+
(orderMap.get(right.id) ?? Number.MAX_SAFE_INTEGER);
|
|
265
|
+
return orderDifference || compareText(left.id, right.id);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const index = {
|
|
269
|
+
schemaVersion: 1,
|
|
270
|
+
specVersion: SPEC_VERSION,
|
|
271
|
+
generatedBy: `llmnav@${PACKAGE_VERSION}`,
|
|
272
|
+
repositoryId: project.config.repositoryId,
|
|
273
|
+
contractFingerprints: buildContractFingerprints(project, cards),
|
|
274
|
+
sourceHash: sha256(cards.map((card) => `${card.hashes.semantic}:${card.hashes.structure}:${card.hashes.body}`).join("\n")),
|
|
275
|
+
cards,
|
|
276
|
+
};
|
|
277
|
+
const { searchIndex, stats: searchStats } = buildInvertedIndex(index, options.previousSearchIndex);
|
|
278
|
+
const fileState = options.fileState ?? buildFileStateFromProject(project);
|
|
279
|
+
const graphBuild = buildRepositoryGraphIncremental(project, index, options.previousGraphState);
|
|
280
|
+
const { graph, state: graphState, stats: graphStats } = graphBuild;
|
|
281
|
+
|
|
282
|
+
const artifacts = new Map();
|
|
283
|
+
const cacheRoot = toPosix(project.config.generation.cacheDirectory).replace(/\/+$/u, "");
|
|
284
|
+
artifacts.set(`${cacheRoot}/index.json`, stableStringify(index));
|
|
285
|
+
artifacts.set(
|
|
286
|
+
`${cacheRoot}/cards.jsonl`,
|
|
287
|
+
cards.length > 0 ? `${cards.map((card) => stableJson(card)).join("\n")}\n` : "",
|
|
288
|
+
);
|
|
289
|
+
artifacts.set(`${cacheRoot}/search-index.json`, renderSearchIndex(searchIndex));
|
|
290
|
+
artifacts.set(`${cacheRoot}/file-state.json`, renderFileState(fileState));
|
|
291
|
+
artifacts.set(`${cacheRoot}/graph.json`, renderRepositoryGraph(graph));
|
|
292
|
+
artifacts.set(`${cacheRoot}/graph-state.json`, renderGraphState(graphState));
|
|
293
|
+
const searchShards = buildSearchShards(index, searchIndex, project.config.generation.searchShardSize);
|
|
294
|
+
if (searchShards.manifest) {
|
|
295
|
+
artifacts.set(`${cacheRoot}/search-shards.json`, stableStringify(searchShards.manifest));
|
|
296
|
+
for (const [relativePath, content] of searchShards.shards) {
|
|
297
|
+
artifacts.set(`${cacheRoot}/${relativePath}`, content);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const repositoryCards = cards.filter((card) =>
|
|
302
|
+
project.config.generation.repositoryCatalogStabilities.includes(card.stability),
|
|
303
|
+
);
|
|
304
|
+
artifacts.set(`${cacheRoot}/repo-core.txt`, renderCatalog(project.config.repositoryId, "repository", repositoryCards));
|
|
305
|
+
|
|
306
|
+
const modules = groupByModule(cards, project.config.generation.moduleDepth);
|
|
307
|
+
const moduleManifest = [];
|
|
308
|
+
for (const [moduleId, moduleCards] of modules) {
|
|
309
|
+
const filtered = moduleCards.filter((card) =>
|
|
310
|
+
project.config.generation.moduleCatalogStabilities.includes(card.stability),
|
|
311
|
+
);
|
|
312
|
+
if (filtered.length === 0) continue;
|
|
313
|
+
const relativePath = `${cacheRoot}/modules/${safeModuleName(moduleId)}.txt`;
|
|
314
|
+
artifacts.set(relativePath, renderCatalog(project.config.repositoryId, moduleId, filtered));
|
|
315
|
+
moduleManifest.push({ id: moduleId, file: relativePath, cards: filtered.length });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
artifacts.set(`${cacheRoot}/agent-context.md`, renderAgentContext(project.config.repositoryId, moduleManifest));
|
|
319
|
+
const promptBundle = buildPromptPrefixBundle({
|
|
320
|
+
repositoryId: project.config.repositoryId,
|
|
321
|
+
toolDefinitions: getAgentToolDefinitions(),
|
|
322
|
+
agentProtocol: AGENT_PROTOCOL,
|
|
323
|
+
repositoryCore: artifacts.get(`${cacheRoot}/repo-core.txt`),
|
|
324
|
+
modules: moduleManifest.map((item) => ({ id: item.id, content: artifacts.get(item.file) })),
|
|
325
|
+
});
|
|
326
|
+
artifacts.set(`${cacheRoot}/prompt-prefix.json`, renderPromptPrefixBundle(promptBundle));
|
|
327
|
+
|
|
328
|
+
const contentHashes = Object.fromEntries(
|
|
329
|
+
[...artifacts.entries()]
|
|
330
|
+
.sort(([left], [right]) => compareText(left, right))
|
|
331
|
+
.map(([relativePath, content]) => [relativePath, sha256(content)]),
|
|
332
|
+
);
|
|
333
|
+
const manifest = {
|
|
334
|
+
schemaVersion: 1,
|
|
335
|
+
specVersion: SPEC_VERSION,
|
|
336
|
+
repositoryId: project.config.repositoryId,
|
|
337
|
+
cardCount: cards.length,
|
|
338
|
+
moduleCount: moduleManifest.length,
|
|
339
|
+
sourceHash: index.sourceHash,
|
|
340
|
+
searchCardSetHash: searchIndex.cardSetHash,
|
|
341
|
+
searchIndexSchemaVersion: searchIndex.schemaVersion,
|
|
342
|
+
fileStateSchemaVersion: fileState.schemaVersion,
|
|
343
|
+
graphSchemaVersion: GRAPH_SCHEMA_VERSION,
|
|
344
|
+
graphStateSchemaVersion: GRAPH_STATE_SCHEMA_VERSION,
|
|
345
|
+
promptBundleSchemaVersion: PROMPT_BUNDLE_SCHEMA_VERSION,
|
|
346
|
+
searchShardSchemaVersion: searchShards.manifest ? SEARCH_SHARD_SCHEMA_VERSION : null,
|
|
347
|
+
files: contentHashes,
|
|
348
|
+
};
|
|
349
|
+
artifacts.set(`${cacheRoot}/manifest.json`, stableStringify(manifest));
|
|
350
|
+
return { artifacts, index, searchIndex, searchStats, fileState, graph, graphState, graphStats, promptBundle, moduleManifest };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function diagnosticForContractChange(change) {
|
|
354
|
+
const file = change.kind === "configuration" ? ".llmnav/config.json" : ".llmnav/cache/index.json";
|
|
355
|
+
const label = change.kind === "configuration" ? "Effective configuration" : "Exported API";
|
|
356
|
+
return {
|
|
357
|
+
severity: "warning",
|
|
358
|
+
code: "LNV009",
|
|
359
|
+
message: `${label} contract fingerprint changed; review the affected contract before committing generated artifacts.`,
|
|
360
|
+
file,
|
|
361
|
+
line: 1,
|
|
362
|
+
column: 1,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function compareGeneratedDiagnostics(left, right) {
|
|
367
|
+
return compareText(left.file, right.file) ||
|
|
368
|
+
left.line - right.line ||
|
|
369
|
+
left.column - right.column ||
|
|
370
|
+
compareText(left.code, right.code) ||
|
|
371
|
+
compareText(left.message, right.message);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function indexedCard(record) {
|
|
375
|
+
const canonical = cardToCanonicalObject(record.card);
|
|
376
|
+
const boundaries = detectBoundaries(record);
|
|
377
|
+
const semanticPayload = stableJson(canonical);
|
|
378
|
+
const structurePayload = stableJson({
|
|
379
|
+
path: record.relativePath,
|
|
380
|
+
scope: record.block.scope,
|
|
381
|
+
symbol: record.declaration?.symbol ?? null,
|
|
382
|
+
kind: record.declaration?.kind ?? null,
|
|
383
|
+
signature: record.declaration?.signature ?? null,
|
|
384
|
+
language: record.declaration?.language ?? null,
|
|
385
|
+
exported: record.declaration?.exported ?? null,
|
|
386
|
+
visibility: record.declaration?.visibility ?? null,
|
|
387
|
+
receiver: record.declaration?.receiver ?? null,
|
|
388
|
+
imports: record.imports,
|
|
389
|
+
boundaries,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
...canonical,
|
|
393
|
+
location: {
|
|
394
|
+
path: record.relativePath,
|
|
395
|
+
startLine: record.block.startLine,
|
|
396
|
+
endLine: record.block.endLine,
|
|
397
|
+
symbol: record.declaration?.symbol ?? null,
|
|
398
|
+
kind: record.declaration?.kind ?? null,
|
|
399
|
+
declarationLine: record.declaration?.line ?? null,
|
|
400
|
+
signature: record.declaration?.signature ?? null,
|
|
401
|
+
language: record.declaration?.language ?? null,
|
|
402
|
+
exported: record.declaration?.exported ?? null,
|
|
403
|
+
visibility: record.declaration?.visibility ?? null,
|
|
404
|
+
receiver: record.declaration?.receiver ?? null,
|
|
405
|
+
},
|
|
406
|
+
imports: record.imports,
|
|
407
|
+
boundaries,
|
|
408
|
+
hashes: {
|
|
409
|
+
semantic: sha256(semanticPayload),
|
|
410
|
+
structure: sha256(structurePayload),
|
|
411
|
+
body: record.bodyHash ?? sha256(record.source ?? ""),
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function groupByModule(cards, depth) {
|
|
417
|
+
const groups = new Map();
|
|
418
|
+
for (const card of cards) {
|
|
419
|
+
const moduleId = moduleIdForCard(card.id, depth);
|
|
420
|
+
const existing = groups.get(moduleId) ?? [];
|
|
421
|
+
existing.push(card);
|
|
422
|
+
groups.set(moduleId, existing);
|
|
423
|
+
}
|
|
424
|
+
return [...groups.entries()].sort(([left], [right]) => compareText(left, right));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderCatalog(repositoryId, catalogId, cards) {
|
|
428
|
+
const lines = [
|
|
429
|
+
`llmnav-catalog/1 repository=${repositoryId} catalog=${catalogId}`,
|
|
430
|
+
"Read semantic cards before opening source. Resolve current paths and signatures with llmnav query or show.",
|
|
431
|
+
"",
|
|
432
|
+
];
|
|
433
|
+
for (const card of cards) lines.push(renderSemanticCard(card), "");
|
|
434
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function renderSemanticCard(card) {
|
|
438
|
+
const lines = [`@${card.id}`, `role ${card.role}`];
|
|
439
|
+
if (card.owns?.length) lines.push(`owns ${card.owns.join(", ")}`);
|
|
440
|
+
if (card.excludes?.length) lines.push(`excludes ${card.excludes.join(", ")}`);
|
|
441
|
+
if (card.invariant?.length) lines.push(`invariant ${card.invariant.join(" ")}`);
|
|
442
|
+
if (card.effect?.length) lines.push(`effect ${card.effect.join(", ")}`);
|
|
443
|
+
if (card.risk?.length) lines.push(`risk ${card.risk.join(", ")}`);
|
|
444
|
+
if (card.rel?.length) lines.push(`rel ${card.rel.join(", ")}`);
|
|
445
|
+
return lines.join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function renderCompactCard(card) {
|
|
449
|
+
const lines = renderSemanticCard(card).split("\n");
|
|
450
|
+
const location = card.location;
|
|
451
|
+
if (location) {
|
|
452
|
+
const symbol = location.symbol ? `#${location.symbol}` : "";
|
|
453
|
+
lines.push(`loc ${location.path}${symbol}:${location.declarationLine ?? location.startLine}`);
|
|
454
|
+
if (location.signature) lines.push(`sig ${location.signature}`);
|
|
455
|
+
}
|
|
456
|
+
return lines.join("\n");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function renderAgentContext(repositoryId, modules) {
|
|
460
|
+
const lines = [
|
|
461
|
+
"# LLMNav generated context",
|
|
462
|
+
"",
|
|
463
|
+
`Repository: ${repositoryId}`,
|
|
464
|
+
"",
|
|
465
|
+
"Use `npm exec -- llmnav query \"<task>\" --top 5` before broad directory scans or grep.",
|
|
466
|
+
"Use `npm exec -- llmnav show <id>` to resolve one semantic card and `npm exec -- llmnav context <id>` for related cards.",
|
|
467
|
+
"Treat paths, line numbers, signatures, imports, and hashes as generated data.",
|
|
468
|
+
"Keep semantic IDs stable across moves and renames.",
|
|
469
|
+
"",
|
|
470
|
+
"## Module catalogs",
|
|
471
|
+
"",
|
|
472
|
+
];
|
|
473
|
+
if (modules.length === 0) lines.push("No module catalogs have been generated yet.");
|
|
474
|
+
for (const module of modules) lines.push(`* ${module.id}: ${module.file} (${module.cards} cards)`);
|
|
475
|
+
return `${lines.join("\n")}\n`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function buildStableOrder(root, ids) {
|
|
479
|
+
const existing = (await readText(path.join(root, ".llmnav", "order.lock"), ""))
|
|
480
|
+
.split(/\r?\n/u)
|
|
481
|
+
.map((line) => line.trim())
|
|
482
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
483
|
+
const seen = new Set(existing);
|
|
484
|
+
const additions = [...new Set(ids)].filter((id) => !seen.has(id)).sort(compareText);
|
|
485
|
+
return [...existing, ...additions];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function renderOrder(order) {
|
|
489
|
+
return order.length > 0 ? `${order.join("\n")}\n` : "";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function compareArtifacts(root, cacheDirectory, artifacts) {
|
|
493
|
+
const changed = [];
|
|
494
|
+
for (const [relativePath, content] of artifacts) {
|
|
495
|
+
if (await compareOne(path.join(root, relativePath), content)) changed.push(toPosix(relativePath));
|
|
496
|
+
}
|
|
497
|
+
const actualFiles = await listFiles(path.join(root, cacheDirectory));
|
|
498
|
+
const expected = new Set([...artifacts.keys()].map((item) => toPosix(item)));
|
|
499
|
+
for (const absolutePath of actualFiles) {
|
|
500
|
+
const relativePath = relativePosix(root, absolutePath);
|
|
501
|
+
if (!expected.has(relativePath)) changed.push(relativePath);
|
|
502
|
+
}
|
|
503
|
+
return changed.sort(compareText);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function compareOne(filePath, expected) {
|
|
507
|
+
const actual = await readText(filePath, null);
|
|
508
|
+
return actual !== expected;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function listFiles(directory) {
|
|
512
|
+
try {
|
|
513
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
514
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
515
|
+
const files = [];
|
|
516
|
+
for (const entry of entries) {
|
|
517
|
+
const absolute = path.join(directory, entry.name);
|
|
518
|
+
if (entry.isDirectory()) files.push(...(await listFiles(absolute)));
|
|
519
|
+
else if (entry.isFile()) files.push(absolute);
|
|
520
|
+
}
|
|
521
|
+
return files;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error && typeof error === "object" && error.code === "ENOENT") return [];
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export { buildModuleCatalogMetadata };
|