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,40 @@
1
+ import {
2
+ executeAgentOperation,
3
+ createProjectSession,
4
+ getAgentToolDefinitions,
5
+ loadPromptPrefixBundle,
6
+ } from "llmnav";
7
+
8
+ export async function createLlmnavHost(root) {
9
+ let bundle = await loadPromptPrefixBundle(root);
10
+ let session = await createProjectSession(root);
11
+ const host = {
12
+ toolDefinitions: getAgentToolDefinitions(),
13
+ basePromptPartitions: selectPromptPartitions(bundle),
14
+ selectPromptPartitions(moduleIds = []) {
15
+ return selectPromptPartitions(bundle, moduleIds);
16
+ },
17
+ execute(call) {
18
+ return executeAgentOperation(root, call.name, call.input ?? {}, { session });
19
+ },
20
+ async refresh() {
21
+ [bundle, session] = await Promise.all([
22
+ loadPromptPrefixBundle(root),
23
+ createProjectSession(root),
24
+ ]);
25
+ host.basePromptPartitions = selectPromptPartitions(bundle);
26
+ return host;
27
+ },
28
+ };
29
+ return host;
30
+ }
31
+
32
+ export function selectPromptPartitions(bundle, moduleIds = []) {
33
+ const requested = new Set(moduleIds.map((id) => id.startsWith("module:") ? id : `module:${id}`));
34
+ const available = new Set(bundle.assembly.modulePartitionIds);
35
+ const missing = [...requested].filter((id) => !available.has(id)).sort();
36
+ if (missing.length > 0) throw new Error(`Unknown prompt module partition(s): ${missing.join(", ")}.`);
37
+ return bundle.partitions.filter((partition) =>
38
+ bundle.assembly.basePartitionIds.includes(partition.id) || requested.has(partition.id),
39
+ );
40
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "llmnav",
3
+ "version": "0.5.1",
4
+ "description": "A deterministic semantic navigation layer for LLM coding agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "llmnav": "bin/llmnav.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "import": "./src/index.js"
13
+ },
14
+ "./spec": {
15
+ "types": "./src/spec.d.ts",
16
+ "import": "./src/spec.js"
17
+ },
18
+ "./examples/provider-neutral-host.mjs": {
19
+ "types": "./examples/provider-neutral-host.d.mts",
20
+ "import": "./examples/provider-neutral-host.mjs"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "templates",
28
+ "schema",
29
+ "docs",
30
+ "examples/provider-neutral-host.mjs",
31
+ "examples/provider-neutral-host.d.mts",
32
+ "README.md",
33
+ "CHANGELOG.md",
34
+ "ROADMAP.md",
35
+ "LICENSE"
36
+ ],
37
+ "engines": {
38
+ "node": ">=22.0.0"
39
+ },
40
+ "scripts": {
41
+ "test": "node --test",
42
+ "test:coverage": "node --test --experimental-test-coverage",
43
+ "lint": "node ./scripts/lint-repo.js",
44
+ "check": "npm run lint && npm test && node ./bin/llmnav.js check --root . && node ./bin/llmnav.js generate --root . --full --check",
45
+ "pack:check": "npm pack --dry-run",
46
+ "release:check": "node ./scripts/release-check.js",
47
+ "prepublishOnly": "npm run check && npm run release:check && npm run pack:check",
48
+ "release:tag": "node ./scripts/verify-tag.js",
49
+ "test:performance": "node --test tests/performance.test.js",
50
+ "benchmark:v0.2": "node ./benchmarks/run-v0.2.js",
51
+ "smoke:pack": "node ./scripts/pack-smoke.js"
52
+ },
53
+ "keywords": [
54
+ "llm",
55
+ "coding-agent",
56
+ "code-navigation",
57
+ "semantic-search",
58
+ "repository-map",
59
+ "ai",
60
+ "cli",
61
+ "lint"
62
+ ],
63
+ "author": "제로디",
64
+ "license": "MIT",
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "git+https://github.com/0disoft/llmnav.git"
68
+ },
69
+ "bugs": {
70
+ "url": "https://github.com/0disoft/llmnav/issues"
71
+ },
72
+ "homepage": "https://github.com/0disoft/llmnav#readme",
73
+ "sideEffects": false,
74
+ "types": "./src/index.d.ts",
75
+ "publishConfig": {
76
+ "access": "public",
77
+ "provenance": true
78
+ }
79
+ }
@@ -0,0 +1,296 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://llmnav.dev/schema/config.schema.json",
4
+ "title": "LLMNav configuration",
5
+ "type": "object",
6
+ "required": [
7
+ "version",
8
+ "repositoryId",
9
+ "sourceRoots",
10
+ "includeExtensions"
11
+ ],
12
+ "additionalProperties": false,
13
+ "properties": {
14
+ "$schema": {
15
+ "type": "string"
16
+ },
17
+ "version": {
18
+ "const": 1
19
+ },
20
+ "repositoryId": {
21
+ "type": "string",
22
+ "pattern": "^[a-z][a-z0-9-]{0,63}$"
23
+ },
24
+ "sourceRoots": {
25
+ "type": "array",
26
+ "minItems": 1,
27
+ "items": {
28
+ "type": "string",
29
+ "pattern": "^(?!/|[A-Za-z]:|.*(?:^|/)\\.\\.(?:/|$))(?:\\.|[^\\\\]+)$"
30
+ },
31
+ "uniqueItems": true
32
+ },
33
+ "includeExtensions": {
34
+ "type": "array",
35
+ "minItems": 1,
36
+ "items": {
37
+ "type": "string",
38
+ "pattern": "^\\."
39
+ },
40
+ "uniqueItems": true
41
+ },
42
+ "excludeDirectories": {
43
+ "type": "array",
44
+ "items": {
45
+ "type": "string"
46
+ },
47
+ "uniqueItems": true
48
+ },
49
+ "excludeFiles": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string"
53
+ },
54
+ "uniqueItems": true
55
+ },
56
+ "coverageRules": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "object",
60
+ "required": [
61
+ "match"
62
+ ],
63
+ "additionalProperties": false,
64
+ "properties": {
65
+ "name": {
66
+ "type": "string"
67
+ },
68
+ "match": {
69
+ "type": "array",
70
+ "minItems": 1,
71
+ "items": {
72
+ "type": "string"
73
+ },
74
+ "uniqueItems": true
75
+ },
76
+ "scope": {
77
+ "enum": [
78
+ "file",
79
+ "module",
80
+ "symbol"
81
+ ]
82
+ },
83
+ "requiredFields": {
84
+ "type": "array",
85
+ "items": {
86
+ "enum": [
87
+ "id",
88
+ "role",
89
+ "owns",
90
+ "excludes",
91
+ "search",
92
+ "invariant",
93
+ "effect",
94
+ "risk",
95
+ "rel",
96
+ "stability"
97
+ ]
98
+ },
99
+ "uniqueItems": true
100
+ }
101
+ }
102
+ }
103
+ },
104
+ "graph": {
105
+ "type": "object",
106
+ "additionalProperties": false,
107
+ "properties": {
108
+ "indexFiles": {
109
+ "type": "array",
110
+ "items": {
111
+ "type": "string",
112
+ "pattern": "^(?!/|[A-Za-z]:|.*(?:^|/)\\.\\.(?:/|$))[^\\\\]+$"
113
+ },
114
+ "uniqueItems": true
115
+ }
116
+ }
117
+ },
118
+ "lint": {
119
+ "type": "object",
120
+ "additionalProperties": false,
121
+ "properties": {
122
+ "maxRoleLength": {
123
+ "type": "integer",
124
+ "minimum": 40
125
+ },
126
+ "maxSearchTerms": {
127
+ "type": "integer",
128
+ "minimum": 1
129
+ },
130
+ "minSearchTerms": {
131
+ "type": "integer",
132
+ "minimum": 0
133
+ },
134
+ "maxInvariants": {
135
+ "type": "integer",
136
+ "minimum": 0
137
+ },
138
+ "maxEffects": {
139
+ "type": "integer",
140
+ "minimum": 0
141
+ },
142
+ "maxRelations": {
143
+ "type": "integer",
144
+ "minimum": 0
145
+ },
146
+ "maxBlockBytes": {
147
+ "type": "object",
148
+ "properties": {
149
+ "file": {
150
+ "type": "integer",
151
+ "minimum": 100
152
+ },
153
+ "module": {
154
+ "type": "integer",
155
+ "minimum": 100
156
+ },
157
+ "symbol": {
158
+ "type": "integer",
159
+ "minimum": 100
160
+ }
161
+ }
162
+ },
163
+ "maxSemanticRatio": {
164
+ "type": "number",
165
+ "minimum": 0,
166
+ "maximum": 1
167
+ },
168
+ "minimumSourceBytesForRatio": {
169
+ "type": "integer",
170
+ "minimum": 0
171
+ },
172
+ "searchTermSaturation": {
173
+ "type": "number",
174
+ "minimum": 0,
175
+ "maximum": 1
176
+ },
177
+ "minimumCardsForSaturation": {
178
+ "type": "integer",
179
+ "minimum": 1
180
+ },
181
+ "genericSearchTerms": {
182
+ "type": "array",
183
+ "items": {
184
+ "type": "string"
185
+ },
186
+ "uniqueItems": true
187
+ },
188
+ "vagueRoleWords": {
189
+ "type": "array",
190
+ "items": {
191
+ "type": "string"
192
+ },
193
+ "uniqueItems": true
194
+ },
195
+ "strictRisks": {
196
+ "type": "array",
197
+ "items": {
198
+ "type": "string"
199
+ },
200
+ "uniqueItems": true
201
+ },
202
+ "additionalEffects": {
203
+ "type": "array",
204
+ "items": {
205
+ "type": "string",
206
+ "pattern": "^[a-z][a-z0-9.-]*$"
207
+ },
208
+ "uniqueItems": true
209
+ },
210
+ "additionalRisks": {
211
+ "type": "array",
212
+ "items": {
213
+ "type": "string",
214
+ "pattern": "^[a-z][a-z0-9.-]*$"
215
+ },
216
+ "uniqueItems": true
217
+ },
218
+ "additionalRelations": {
219
+ "type": "array",
220
+ "items": {
221
+ "type": "string",
222
+ "pattern": "^[a-z][a-z0-9-]*$"
223
+ },
224
+ "uniqueItems": true
225
+ },
226
+ "requireCanonicalOrder": {
227
+ "type": "boolean"
228
+ },
229
+ "requireCanonicalFormatting": {
230
+ "type": "boolean"
231
+ }
232
+ }
233
+ },
234
+ "generation": {
235
+ "type": "object",
236
+ "additionalProperties": false,
237
+ "properties": {
238
+ "cacheDirectory": {
239
+ "type": "string",
240
+ "pattern": "^\\.llmnav/(?!.*(?:^|/)\\.\\.(?:/|$))[^\\\\]+$"
241
+ },
242
+ "moduleDepth": {
243
+ "type": "integer",
244
+ "minimum": 1,
245
+ "maximum": 6
246
+ },
247
+ "searchShardSize": {
248
+ "type": "integer",
249
+ "minimum": 0
250
+ },
251
+ "repositoryCatalogStabilities": {
252
+ "type": "array",
253
+ "items": {
254
+ "enum": [
255
+ "architecture",
256
+ "contract",
257
+ "implementation"
258
+ ]
259
+ },
260
+ "uniqueItems": true
261
+ },
262
+ "moduleCatalogStabilities": {
263
+ "type": "array",
264
+ "items": {
265
+ "enum": [
266
+ "architecture",
267
+ "contract",
268
+ "implementation"
269
+ ]
270
+ },
271
+ "uniqueItems": true
272
+ }
273
+ }
274
+ },
275
+ "evaluation": {
276
+ "type": "object",
277
+ "additionalProperties": false,
278
+ "properties": {
279
+ "queryFile": {
280
+ "type": "string",
281
+ "pattern": "^(?!/|[A-Za-z]:|.*(?:^|/)\\.\\.(?:/|$))[^\\\\]+$"
282
+ },
283
+ "minimumRecallAt1": {
284
+ "type": "number",
285
+ "minimum": 0,
286
+ "maximum": 1
287
+ },
288
+ "minimumRecallAt5": {
289
+ "type": "number",
290
+ "minimum": 0,
291
+ "maximum": 1
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
@@ -0,0 +1,117 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.agent.protocol
3
+ role=Execute bounded repository navigation operations through one provider-neutral result envelope.
4
+ owns=operation validation|operation dispatch|provider-neutral result envelope
5
+ excludes=provider SDK transport|repository discovery|source mutation
6
+ search=agent tool schema|provider neutral tools|tool dispatcher|agent operation protocol
7
+ rel=workflow>llmnav.search.query
8
+ rel=workflow>llmnav.rules.validate
9
+ stability=contract
10
+ */
11
+
12
+ import { loadGraphInputs } from "./graph-input.js";
13
+ import { scanProject } from "./project.js";
14
+ import { buildContext, queryProject, showProjectCard } from "./search.js";
15
+ import { countDiagnostics, validateProject } from "./validator.js";
16
+ import { getAgentToolDefinitions } from "./agent-tools.js";
17
+
18
+ export { AGENT_TOOL_SCHEMA_VERSION, getAgentToolDefinitions } from "./agent-tools.js";
19
+ export const AGENT_OPERATION_SCHEMA_VERSION = 1;
20
+
21
+ const DEFINITIONS = getAgentToolDefinitions();
22
+
23
+ const OPERATIONS = new Map([
24
+ ["llmnav_query", "query"],
25
+ ["llmnav_show", "show"],
26
+ ["llmnav_context", "context"],
27
+ ["llmnav_check", "check"],
28
+ ]);
29
+
30
+ export async function executeAgentOperation(root, name, input = {}, options = {}) {
31
+ const canonicalName = String(name);
32
+ const operation = OPERATIONS.get(canonicalName);
33
+ if (!operation) return failure("unknown", "LNVAP001", `Unknown agent operation ${JSON.stringify(name)}.`);
34
+ const definition = DEFINITIONS.find((item) => item.name === canonicalName);
35
+ const validationError = validateInput(definition.inputSchema, input);
36
+ if (validationError) return failure(operation, "LNVAP002", validationError);
37
+
38
+ try {
39
+ if (operation === "query") {
40
+ return success(operation, options.session
41
+ ? options.session.query(input.task.trim(), { top: input.top ?? 5 })
42
+ : await queryProject(root, input.task.trim(), { top: input.top ?? 5 }));
43
+ }
44
+ if (operation === "show") {
45
+ const result = options.session
46
+ ? options.session.show(input.id.trim())
47
+ : await showProjectCard(root, input.id.trim());
48
+ if (!result.card && !result.node) return failure(operation, "LNVAP404", `Unknown or inactive semantic ID ${input.id}.`, result);
49
+ return success(operation, result);
50
+ }
51
+ if (operation === "context") {
52
+ const contextOptions = {
53
+ depth: input.depth ?? 1,
54
+ budget: input.budget ?? 2500,
55
+ maxEdges: input.maxEdges ?? 24,
56
+ };
57
+ return success(operation, options.session
58
+ ? options.session.context(input.id.trim(), contextOptions)
59
+ : await buildContext(root, input.id.trim(), contextOptions));
60
+ }
61
+ const project = await scanProject(root, { paths: input.paths ?? [] });
62
+ const graphInputs = await loadGraphInputs(root, project.config);
63
+ const diagnostics = [...validateProject(project), ...graphInputs.diagnostics].sort(compareDiagnostics);
64
+ const counts = countDiagnostics(diagnostics);
65
+ return {
66
+ schemaVersion: AGENT_OPERATION_SCHEMA_VERSION,
67
+ operation,
68
+ ok: counts.error === 0,
69
+ data: { counts, diagnostics, cardCount: project.records.length },
70
+ error: null,
71
+ };
72
+ } catch (error) {
73
+ return failure(operation, "LNVAP500", error instanceof Error ? error.message : String(error));
74
+ }
75
+ }
76
+
77
+ function validateInput(schema, input) {
78
+ if (!input || typeof input !== "object" || Array.isArray(input)) return "Operation input must be an object.";
79
+ const keys = Object.keys(input).sort();
80
+ const unknown = keys.filter((key) => !Object.hasOwn(schema.properties, key));
81
+ if (unknown.length > 0) return `Unknown input field(s): ${unknown.join(", ")}.`;
82
+ const missing = schema.required.filter((key) => input[key] === undefined);
83
+ if (missing.length > 0) return `Missing required input field(s): ${missing.join(", ")}.`;
84
+ for (const key of keys) {
85
+ const error = validateProperty(key, input[key], schema.properties[key]);
86
+ if (error) return error;
87
+ }
88
+ return null;
89
+ }
90
+
91
+ function validateProperty(key, value, schema) {
92
+ if (schema.type === "string") {
93
+ if (typeof value !== "string" || value.trim().length < (schema.minLength ?? 0)) return `Input field ${key} must be a non-empty string.`;
94
+ } else if (schema.type === "integer") {
95
+ if (!Number.isInteger(value) || value < schema.minimum || value > schema.maximum) {
96
+ return `Input field ${key} must be an integer from ${schema.minimum} to ${schema.maximum}.`;
97
+ }
98
+ } else if (schema.type === "array") {
99
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length < schema.items.minLength)) {
100
+ return `Input field ${key} must be an array of non-empty strings.`;
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+
106
+ function success(operation, data) {
107
+ return { schemaVersion: AGENT_OPERATION_SCHEMA_VERSION, operation, ok: true, data, error: null };
108
+ }
109
+
110
+ function failure(operation, code, message, data = null) {
111
+ return { schemaVersion: AGENT_OPERATION_SCHEMA_VERSION, operation, ok: false, data, error: { code, message } };
112
+ }
113
+
114
+ function compareDiagnostics(left, right) {
115
+ return left.file.localeCompare(right.file, "en") || left.line - right.line || left.column - right.column ||
116
+ left.code.localeCompare(right.code, "en") || left.message.localeCompare(right.message, "en");
117
+ }
@@ -0,0 +1,61 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.agent.tool-schema
3
+ role=Publish stable provider-neutral tool definitions without loading repository execution code.
4
+ owns=agent tool names|tool input schemas|tool order
5
+ excludes=operation execution|provider SDK transport|repository scope
6
+ search=agent tool definitions|tool JSON schema|stable tool prefix|provider neutral schema
7
+ rel=workflow>llmnav.agent.protocol
8
+ stability=contract
9
+ */
10
+
11
+ export const AGENT_TOOL_SCHEMA_VERSION = 1;
12
+
13
+ const DEFINITIONS = [
14
+ tool("llmnav_query", "Find the most relevant semantic cards for a coding task.", {
15
+ task: stringProperty("Task language to search for."),
16
+ top: integerProperty("Maximum results.", 1, 100, 5),
17
+ }, ["task"]),
18
+ tool("llmnav_show", "Resolve one local or qualified workspace semantic ID.", {
19
+ id: stringProperty("Semantic ID or repository-qualified semantic ID."),
20
+ }, ["id"]),
21
+ tool("llmnav_context", "Build bounded semantic and graph context around one ID.", {
22
+ id: stringProperty("Semantic ID or repository-qualified semantic ID."),
23
+ depth: integerProperty("Maximum graph traversal depth.", 0, 8, 1),
24
+ budget: integerProperty("Approximate token budget.", 128, 100000, 2500),
25
+ maxEdges: integerProperty("Maximum graph edges to inspect and pack.", 0, 1000, 24),
26
+ }, ["id"]),
27
+ tool("llmnav_check", "Validate semantic cards and configured graph inputs without mutation.", {
28
+ paths: {
29
+ type: "array",
30
+ description: "Optional repository-relative paths to validate.",
31
+ items: { type: "string", minLength: 1 },
32
+ default: [],
33
+ },
34
+ }),
35
+ ];
36
+
37
+ export function getAgentToolDefinitions() {
38
+ return structuredClone(DEFINITIONS);
39
+ }
40
+
41
+ function tool(name, description, properties, required = []) {
42
+ return {
43
+ schemaVersion: AGENT_TOOL_SCHEMA_VERSION,
44
+ name,
45
+ description,
46
+ inputSchema: {
47
+ type: "object",
48
+ additionalProperties: false,
49
+ properties,
50
+ required,
51
+ },
52
+ };
53
+ }
54
+
55
+ function stringProperty(description) {
56
+ return { type: "string", minLength: 1, description };
57
+ }
58
+
59
+ function integerProperty(description, minimum, maximum, defaultValue) {
60
+ return { type: "integer", minimum, maximum, default: defaultValue, description };
61
+ }