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,107 @@
1
+ import path from "node:path";
2
+ import { ID_PATTERN } from "./spec.js";
3
+ import { atomicWrite, parseJsonLines, readText } from "./util.js";
4
+
5
+ const REGISTRY_STATES = new Set(["active", "redirect", "replaced", "retired"]);
6
+ const REGISTRY_KEYS = new Set(["id", "state", "to", "by"]);
7
+
8
+ export async function loadRegistry(root) {
9
+ const registryPath = path.join(root, ".llmnav", "ids.jsonl");
10
+ const text = await readText(registryPath, "");
11
+ const parsed = parseJsonLines(text, registryPath);
12
+ const byId = new Map();
13
+ const errors = [...parsed.errors];
14
+
15
+ for (const record of parsed.records) {
16
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
17
+ errors.push(`${registryPath}: registry records must be JSON objects`);
18
+ continue;
19
+ }
20
+ for (const key of Object.keys(record)) {
21
+ if (!REGISTRY_KEYS.has(key)) errors.push(`${registryPath}: registry record contains unknown property ${JSON.stringify(key)}`);
22
+ }
23
+ if (typeof record.id !== "string" || !ID_PATTERN.test(record.id)) {
24
+ errors.push(`${registryPath}: registry records require a valid local semantic id`);
25
+ continue;
26
+ }
27
+ if (byId.has(record.id)) errors.push(`${registryPath}: duplicate registry id ${record.id}`);
28
+ byId.set(record.id, record);
29
+
30
+ if (!REGISTRY_STATES.has(record.state)) {
31
+ errors.push(`${registryPath}: registry id ${record.id} has invalid state ${JSON.stringify(record.state)}`);
32
+ continue;
33
+ }
34
+ if (record.state === "active" || record.state === "retired") {
35
+ if (record.to !== undefined || record.by !== undefined) {
36
+ errors.push(`${registryPath}: ${record.state} registry id ${record.id} must not declare to or by`);
37
+ }
38
+ } else if (record.state === "redirect") {
39
+ if (typeof record.to !== "string" || !ID_PATTERN.test(record.to)) {
40
+ errors.push(`${registryPath}: redirect registry id ${record.id} requires a valid to id`);
41
+ }
42
+ if (record.by !== undefined) errors.push(`${registryPath}: redirect registry id ${record.id} must not declare by`);
43
+ } else if (record.state === "replaced") {
44
+ if (!Array.isArray(record.by) || record.by.length === 0) {
45
+ errors.push(`${registryPath}: replaced registry id ${record.id} requires a non-empty by array`);
46
+ } else {
47
+ const seen = new Set();
48
+ for (const target of record.by) {
49
+ if (typeof target !== "string" || !ID_PATTERN.test(target)) {
50
+ errors.push(`${registryPath}: replaced registry id ${record.id} contains an invalid replacement id`);
51
+ continue;
52
+ }
53
+ if (seen.has(target)) errors.push(`${registryPath}: replaced registry id ${record.id} repeats replacement ${target}`);
54
+ seen.add(target);
55
+ }
56
+ }
57
+ if (record.to !== undefined) errors.push(`${registryPath}: replaced registry id ${record.id} must not declare to`);
58
+ }
59
+ }
60
+ return { registryPath, records: [...byId.values()], byId, errors };
61
+ }
62
+
63
+ export async function ensureActiveIds(root, registry, ids) {
64
+ const { records, changed } = mergeActiveIds(registry, ids);
65
+ if (changed) await atomicWrite(registry.registryPath, renderRegistryRecords(records));
66
+ return { records, changed };
67
+ }
68
+
69
+ export function mergeActiveIds(registry, ids) {
70
+ const records = [...registry.records];
71
+ const known = new Set(records.map((record) => record.id));
72
+ let changed = false;
73
+ for (const id of ids) {
74
+ if (known.has(id)) continue;
75
+ records.push({ id, state: "active" });
76
+ known.add(id);
77
+ changed = true;
78
+ }
79
+ return { records, changed };
80
+ }
81
+
82
+ export function renderRegistryRecords(records) {
83
+ const content = records.map((record) => JSON.stringify(record)).join("\n");
84
+ return content ? `${content}\n` : "";
85
+ }
86
+
87
+ export function resolveRegistryId(registry, id) {
88
+ return resolveRegistryPath(registry, id, []);
89
+ }
90
+
91
+ function resolveRegistryPath(registry, current, path) {
92
+ const cycleIndex = path.indexOf(current);
93
+ if (cycleIndex >= 0) return { id: current, state: "cycle", chain: [...path.slice(cycleIndex), current] };
94
+ const chain = [...path, current];
95
+ const record = registry.byId.get(current);
96
+ if (!record) return { id: current, state: "unknown", chain };
97
+ if (record.state === "active") return { id: current, state: "active", chain };
98
+ if (record.state === "redirect" && record.to) return resolveRegistryPath(registry, record.to, chain);
99
+ if (record.state === "replaced" && Array.isArray(record.by)) {
100
+ if (record.by.length === 1) return resolveRegistryPath(registry, record.by[0], chain);
101
+ const outcomes = record.by.map((target) => resolveRegistryPath(registry, target, chain));
102
+ const cycle = outcomes.find((outcome) => outcome.state === "cycle");
103
+ if (cycle) return cycle;
104
+ return { id: current, state: "ambiguous", chain, candidates: [...record.by] };
105
+ }
106
+ return { id: current, state: record.state ?? "unknown", chain };
107
+ }
package/src/sarif.js ADDED
@@ -0,0 +1,70 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.diagnostics.sarif
3
+ role=Serialize deterministic LLMNav diagnostics as a SARIF 2.1.0 result log.
4
+ owns=SARIF schema mapping|diagnostic rule table|artifact locations
5
+ excludes=diagnostic discovery|file writes
6
+ search=SARIF output|code scanning diagnostics|static analysis report
7
+ rel=workflow>llmnav.rules.validate
8
+ stability=contract
9
+ */
10
+
11
+ import { compareText, toPosix } from "./util.js";
12
+ import { PACKAGE_VERSION } from "./spec.js";
13
+
14
+ export const SARIF_VERSION = "2.1.0";
15
+ export const SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
16
+
17
+ export function diagnosticsToSarif(diagnostics) {
18
+ const sorted = [...diagnostics].sort(compareDiagnostics);
19
+ const rules = [...new Map(sorted.map((item) => [item.code, item])).values()]
20
+ .sort((left, right) => compareText(left.code, right.code))
21
+ .map((item) => ({
22
+ id: item.code,
23
+ name: item.code,
24
+ shortDescription: { text: `LLMNav diagnostic ${item.code}` },
25
+ defaultConfiguration: { level: sarifLevel(item.severity) },
26
+ }));
27
+
28
+ return {
29
+ $schema: SARIF_SCHEMA,
30
+ version: SARIF_VERSION,
31
+ runs: [{
32
+ tool: {
33
+ driver: {
34
+ name: "LLMNav",
35
+ semanticVersion: PACKAGE_VERSION,
36
+ informationUri: "https://github.com/0disoft/llmnav",
37
+ rules,
38
+ },
39
+ },
40
+ results: sorted.map((item) => ({
41
+ ruleId: item.code,
42
+ level: sarifLevel(item.severity),
43
+ message: { text: item.message },
44
+ locations: [{
45
+ physicalLocation: {
46
+ artifactLocation: { uri: toPosix(item.file).split("/").map(encodeURIComponent).join("/") },
47
+ region: {
48
+ startLine: Math.max(1, item.line),
49
+ startColumn: Math.max(1, item.column),
50
+ },
51
+ },
52
+ }],
53
+ })),
54
+ }],
55
+ };
56
+ }
57
+
58
+ function sarifLevel(severity) {
59
+ if (severity === "error") return "error";
60
+ if (severity === "warning") return "warning";
61
+ return "note";
62
+ }
63
+
64
+ function compareDiagnostics(left, right) {
65
+ return compareText(left.file, right.file) ||
66
+ left.line - right.line ||
67
+ left.column - right.column ||
68
+ compareText(left.code, right.code) ||
69
+ compareText(left.message, right.message);
70
+ }
@@ -0,0 +1,75 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.index.search-shards
3
+ role=Split a compact search index into deterministic card-range artifacts without retokenizing documents.
4
+ owns=search shard schema|card range partitioning|shard manifest
5
+ excludes=query ranking|source parsing
6
+ search=search index sharding|monorepo search artifacts|card range shard
7
+ rel=workflow>llmnav.index.inverted
8
+ rel=workflow>llmnav.index.generate
9
+ stability=architecture
10
+ */
11
+
12
+ import { searchCardSetHash } from "./inverted-index.js";
13
+ import { sha256, stableStringify } from "./util.js";
14
+
15
+ export const SEARCH_SHARD_SCHEMA_VERSION = 1;
16
+ export const SEARCH_SHARD_ENCODING = "card-range-v1";
17
+
18
+ export function buildSearchShards(index, searchIndex, shardSize) {
19
+ if (!Number.isInteger(shardSize) || shardSize < 0) throw new Error("search shard size must be a non-negative integer");
20
+ if (shardSize === 0 || searchIndex.cardIds.length <= shardSize) {
21
+ return { manifest: null, shards: new Map() };
22
+ }
23
+
24
+ const cardsById = new Map(index.cards.map((card) => [card.id, card]));
25
+ const shards = new Map();
26
+ const records = [];
27
+ for (let start = 0, ordinal = 0; start < searchIndex.cardIds.length; start += shardSize, ordinal += 1) {
28
+ const end = Math.min(start + shardSize, searchIndex.cardIds.length);
29
+ const cardIds = searchIndex.cardIds.slice(start, end);
30
+ const cards = cardIds.map((id) => cardsById.get(id)).filter(Boolean);
31
+ const tokens = [];
32
+ const postings = [];
33
+ for (const [tokenIndex, token] of searchIndex.tokens.entries()) {
34
+ const selected = (searchIndex.postings[tokenIndex] ?? [])
35
+ .filter(([cardIndex]) => cardIndex >= start && cardIndex < end)
36
+ .map(([cardIndex, vector]) => [cardIndex - start, vector]);
37
+ if (selected.length === 0) continue;
38
+ tokens.push(token);
39
+ postings.push(selected);
40
+ }
41
+ const shard = {
42
+ ...searchIndex,
43
+ cardSetHash: searchCardSetHash(cards),
44
+ documentCount: cardIds.length,
45
+ cardIds,
46
+ tokens,
47
+ documents: searchIndex.documents.slice(start, end),
48
+ postings,
49
+ };
50
+ const file = `search-shards/${String(ordinal).padStart(4, "0")}.json`;
51
+ const content = stableStringify(shard);
52
+ shards.set(file, content);
53
+ records.push({
54
+ file,
55
+ firstId: cardIds[0],
56
+ lastId: cardIds.at(-1),
57
+ cardCount: cardIds.length,
58
+ cardSetHash: shard.cardSetHash,
59
+ sha256: sha256(content),
60
+ });
61
+ }
62
+
63
+ return {
64
+ manifest: {
65
+ schemaVersion: SEARCH_SHARD_SCHEMA_VERSION,
66
+ encoding: SEARCH_SHARD_ENCODING,
67
+ repositoryId: index.repositoryId,
68
+ sourceCardSetHash: searchIndex.cardSetHash,
69
+ shardSize,
70
+ shardCount: records.length,
71
+ shards: records,
72
+ },
73
+ shards,
74
+ };
75
+ }