@zabaca/lattice 1.1.1 → 1.2.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.
@@ -8,7 +8,7 @@ Research the topic "$ARGUMENTS" by first checking existing documentation, then p
8
8
 
9
9
  ## Configuration
10
10
 
11
- **⚠️ CRITICAL: All documentation lives in `~/.lattice/docs/`**
11
+ **CRITICAL: All documentation lives in `~/.lattice/docs/`**
12
12
 
13
13
  | Path | Purpose |
14
14
  |------|---------|
@@ -21,22 +21,37 @@ Research the topic "$ARGUMENTS" by first checking existing documentation, then p
21
21
 
22
22
  ## Process
23
23
 
24
- ### Step 1: Search Existing Research
24
+ ### Step 1: Create or Find Question
25
25
 
26
- Run semantic search to find related documents:
26
+ First, search to see if this question (or similar) already exists:
27
27
 
28
28
  ```bash
29
- lattice search "$ARGUMENTS" --limit 10
29
+ lattice search "$ARGUMENTS" --limit 5
30
+ ```
31
+
32
+ Look for results with `[Question]` label and high similarity (>70%).
33
+
34
+ **If similar question exists:** Use that existing question (don't duplicate).
35
+
36
+ **If no similar question:** Create the question entity:
37
+
38
+ ```bash
39
+ lattice question:add "$ARGUMENTS"
30
40
  ```
31
41
 
32
- ### Step 2: Review Search Results
42
+ This ensures the question is tracked regardless of whether we find an answer.
33
43
 
34
- Review the top results from the semantic search:
44
+ ### Step 2: Search for Answers
35
45
 
36
- 1. **Read top results** regardless of path - high similarity may indicate related content
37
- 2. **Path/title matching** is a bonus signal, not a filter
38
- 3. **Don't dismiss** high-similarity docs just because path doesn't match query
39
- 4. Use judgment after reading - the doc content determines relevance, not the filename
46
+ Search for documents that might answer this question:
47
+
48
+ ```bash
49
+ lattice search "$ARGUMENTS" --limit 10
50
+ ```
51
+
52
+ Review results focusing on:
53
+ - Documents (`[Document]` label) with relevant content
54
+ - High similarity scores (>40% often indicates relevance)
40
55
 
41
56
  **Calibration notes:**
42
57
  - Exact topic matches often show 30-40% similarity
@@ -48,13 +63,21 @@ For each promising result:
48
63
  - Check if it answers the user's question
49
64
  - Note relevant sections
50
65
 
51
- ### Step 3: Present Findings to User
66
+ ### Step 3: Present Findings and Link Answer
52
67
 
53
68
  Summarize what you found in existing docs:
54
69
  - What topics are covered
55
70
  - Quote relevant sections if helpful
56
71
  - Identify gaps in existing research
57
72
 
73
+ **If existing documentation answers the question:**
74
+
75
+ Link the question to the answering document:
76
+
77
+ ```bash
78
+ lattice question:link "$ARGUMENTS" --doc {path-to-doc}
79
+ ```
80
+
58
81
  Ask the user: **"Does this existing research cover your question?"**
59
82
 
60
83
  ### Step 4: Ask About New Research
@@ -148,23 +171,43 @@ What this research addresses.
148
171
  **2. Update** `~/.lattice/docs/{topic-name}/README.md`:
149
172
  - Add new row to the Documents table
150
173
 
151
- ### Step 8: Confirmation
174
+ ### Step 8: Sync and Link Question
175
+
176
+ After creating files, sync to the knowledge graph:
177
+
178
+ ```bash
179
+ lattice sync
180
+ ```
181
+
182
+ This will:
183
+ - Add documents to the graph
184
+ - Extract entities automatically via AI
185
+ - Generate embeddings for semantic search
152
186
 
153
- After creating files, confirm:
187
+ Then link the question to the new document:
188
+
189
+ ```bash
190
+ lattice question:link "$ARGUMENTS" --doc ~/.lattice/docs/{topic-name}/{research-filename}.md
191
+ ```
192
+
193
+ ### Step 9: Confirmation
194
+
195
+ Confirm to the user:
196
+ - Question entity created/found
154
197
  - Topic directory path
155
- - README.md created/updated
156
- - Research file created with name
157
- - Remind user to run `/graph-sync` to sync to knowledge graph
198
+ - Research file created
199
+ - Question linked to document via ANSWERED_BY
200
+ - Sync completed
158
201
 
159
202
  ## Important Notes
160
203
 
161
- - **Do NOT** auto-run graph sync - use `/graph-sync` separately
162
204
  - **Always create README.md** for new topics (lightweight index)
163
205
  - **Always create separate research file** (never put research content in README)
164
206
  - Use kebab-case for all directory and file names
165
207
  - Always cite sources with URLs
166
208
  - Cross-link to related research topics when relevant
167
209
  - **No frontmatter needed** - AI extracts entities automatically during sync
210
+ - **Questions track user intent** - even if a doc exists, the question helps future discovery
168
211
 
169
212
  ## File Structure Standard
170
213
 
@@ -177,3 +220,12 @@ After creating files, confirm:
177
220
  ```
178
221
 
179
222
  This structure allows topics to grow organically while keeping README as a clean navigation index.
223
+
224
+ ## Question Commands Reference
225
+
226
+ | Command | Purpose |
227
+ |---------|---------|
228
+ | `lattice question:add "question"` | Create a question entity |
229
+ | `lattice question:add "question" --answered-by path` | Create and link in one step |
230
+ | `lattice question:link "question" --doc path` | Link question to answering doc |
231
+ | `lattice question:unanswered` | List questions without answers |
package/dist/main.js CHANGED
@@ -38,7 +38,38 @@ import {
38
38
  tool
39
39
  } from "@anthropic-ai/claude-agent-sdk";
40
40
  import { Injectable, Logger } from "@nestjs/common";
41
+ import { z as z2 } from "zod";
42
+
43
+ // src/graph/graph.types.ts
41
44
  import { z } from "zod";
45
+ var EntityTypeSchema = z.enum([
46
+ "Topic",
47
+ "Technology",
48
+ "Concept",
49
+ "Tool",
50
+ "Process",
51
+ "Person",
52
+ "Organization",
53
+ "Document",
54
+ "Question"
55
+ ]);
56
+ var RelationTypeSchema = z.enum(["REFERENCES", "ANSWERED_BY"]);
57
+ var EntitySchema = z.object({
58
+ name: z.string().min(1),
59
+ type: EntityTypeSchema,
60
+ description: z.string().optional()
61
+ });
62
+ var RelationshipSchema = z.object({
63
+ source: z.string().min(1),
64
+ relation: RelationTypeSchema,
65
+ target: z.string().min(1)
66
+ });
67
+ var GraphMetadataSchema = z.object({
68
+ importance: z.enum(["high", "medium", "low"]).optional(),
69
+ domain: z.string().optional()
70
+ });
71
+
72
+ // src/sync/entity-extractor.service.ts
42
73
  function validateExtraction(input, _filePath) {
43
74
  const errors = [];
44
75
  const { entities, relationships } = input ?? {};
@@ -65,26 +96,17 @@ function createValidationServer(filePath) {
65
96
  version: "1.0.0",
66
97
  tools: [
67
98
  tool("validate_extraction", "Validate your extracted entities and relationships. Call this to check your work before finishing.", {
68
- entities: z.array(z.object({
69
- name: z.string().min(1),
70
- type: z.enum([
71
- "Topic",
72
- "Technology",
73
- "Concept",
74
- "Tool",
75
- "Process",
76
- "Person",
77
- "Organization",
78
- "Document"
79
- ]),
80
- description: z.string().min(1)
99
+ entities: z2.array(z2.object({
100
+ name: z2.string().min(1),
101
+ type: EntityTypeSchema,
102
+ description: z2.string().min(1)
81
103
  })),
82
- relationships: z.array(z.object({
83
- source: z.string().min(1),
84
- relation: z.enum(["REFERENCES"]),
85
- target: z.string().min(1)
104
+ relationships: z2.array(z2.object({
105
+ source: z2.string().min(1),
106
+ relation: RelationTypeSchema,
107
+ target: z2.string().min(1)
86
108
  })),
87
- summary: z.string().min(10)
109
+ summary: z2.string().min(10)
88
110
  }, async (args) => {
89
111
  const errors = validateExtraction(args, filePath);
90
112
  if (errors.length === 0) {
@@ -210,15 +232,17 @@ Extract the following and call the validation tool with EXACTLY this schema:
210
232
  ### 1. Entities (array of 3-10 objects)
211
233
  Each entity must have:
212
234
  - "name": string (entity name)
213
- - "type": one of "Topic", "Technology", "Concept", "Tool", "Process", "Person", "Organization", "Document"
235
+ - "type": one of "Topic", "Technology", "Concept", "Tool", "Process", "Person", "Organization", "Document", "Question"
214
236
  - "description": string (brief description)
215
237
 
216
238
  ### 2. Relationships (array of objects)
217
239
  Each relationship must have:
218
240
  - "source": "this" (for document-to-entity) or an entity name
219
- - "relation": "REFERENCES" (IMPORTANT: use "relation", not "type")
241
+ - "relation": "REFERENCES" or "ANSWERED_BY" (IMPORTANT: use "relation", not "type")
220
242
  - "target": an entity name from your entities list
221
243
 
244
+ Use ANSWERED_BY when a Question entity is answered by this document (source: Question name, target: "this").
245
+
222
246
  ### 3. Summary
223
247
  A 50-100 word summary of the document's main purpose and key concepts.
224
248
 
@@ -405,7 +429,7 @@ function ensureLatticeHome() {
405
429
  // src/commands/init.command.ts
406
430
  var __filename2 = fileURLToPath(import.meta.url);
407
431
  var __dirname2 = path.dirname(__filename2);
408
- var COMMANDS = ["research.md", "graph-sync.md", "entity-extract.md"];
432
+ var COMMANDS = ["research.md", "entity-extract.md"];
409
433
  var SITE_TEMPLATE_FILES = [
410
434
  "astro.config.ts",
411
435
  "package.json",
@@ -413,7 +437,8 @@ var SITE_TEMPLATE_FILES = [
413
437
  "src/content.config.ts",
414
438
  "src/collections/authors.ts",
415
439
  "src/collections/documents.ts",
416
- "src/collections/tags.ts"
440
+ "src/collections/tags.ts",
441
+ "src/plugins/rehype-strip-md-extension.ts"
417
442
  ];
418
443
 
419
444
  class InitCommand extends CommandRunner2 {
@@ -465,9 +490,13 @@ OBSIDIAN_VAULT_DIR=docs
465
490
  console.log();
466
491
  console.log("Claude Code slash commands:");
467
492
  console.log(" /research <topic> - AI-assisted research workflow");
468
- console.log(" /graph-sync - Extract entities and sync to graph");
469
493
  console.log(" /entity-extract - Extract entities from a document");
470
494
  console.log();
495
+ console.log("Question tracking:");
496
+ console.log(" lattice question:add <question> - Add a question");
497
+ console.log(" lattice question:link <q> --doc <p> - Link question to answer");
498
+ console.log(" lattice question:unanswered - List unanswered questions");
499
+ console.log();
471
500
  if (!(await fs.readFile(envPath, "utf-8")).includes("pa-")) {
472
501
  console.log(`\u26A0\uFE0F Add your Voyage API key to: ${envPath}`);
473
502
  console.log();
@@ -494,6 +523,9 @@ OBSIDIAN_VAULT_DIR=docs
494
523
  await fs.mkdir(path.join(latticeHome, "src", "collections"), {
495
524
  recursive: true
496
525
  });
526
+ await fs.mkdir(path.join(latticeHome, "src", "plugins"), {
527
+ recursive: true
528
+ });
497
529
  let copied = 0;
498
530
  let skipped = 0;
499
531
  for (const file of SITE_TEMPLATE_FILES) {
@@ -513,7 +545,7 @@ OBSIDIAN_VAULT_DIR=docs
513
545
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
514
546
  await fs.copyFile(sourcePath, targetPath);
515
547
  copied++;
516
- } catch (err) {}
548
+ } catch (_err) {}
517
549
  }
518
550
  if (copied > 0) {
519
551
  console.log(`\u2705 Site template: ${copied} file(s) installed`);
@@ -557,7 +589,7 @@ OBSIDIAN_VAULT_DIR=docs
557
589
  await fs.copyFile(sourcePath, targetPath);
558
590
  installed.push(file);
559
591
  copied++;
560
- } catch (err) {}
592
+ } catch (_err) {}
561
593
  }
562
594
  if (copied > 0) {
563
595
  console.log(`\u2705 Claude commands: ${copied} installed to ${targetDir}`);
@@ -1081,18 +1113,18 @@ import { readFile as readFile3, writeFile } from "fs/promises";
1081
1113
  import { Injectable as Injectable5 } from "@nestjs/common";
1082
1114
 
1083
1115
  // src/schemas/manifest.schemas.ts
1084
- import { z as z2 } from "zod";
1085
- var ManifestEntrySchema = z2.object({
1086
- contentHash: z2.string(),
1087
- frontmatterHash: z2.string(),
1088
- lastSynced: z2.string(),
1089
- entityCount: z2.number().int().nonnegative(),
1090
- relationshipCount: z2.number().int().nonnegative()
1116
+ import { z as z3 } from "zod";
1117
+ var ManifestEntrySchema = z3.object({
1118
+ contentHash: z3.string(),
1119
+ frontmatterHash: z3.string(),
1120
+ lastSynced: z3.string(),
1121
+ entityCount: z3.number().int().nonnegative(),
1122
+ relationshipCount: z3.number().int().nonnegative()
1091
1123
  });
1092
- var SyncManifestSchema = z2.object({
1093
- version: z2.string(),
1094
- lastSync: z2.string(),
1095
- documents: z2.record(z2.string(), ManifestEntrySchema)
1124
+ var SyncManifestSchema = z3.object({
1125
+ version: z3.string(),
1126
+ lastSync: z3.string(),
1127
+ documents: z3.record(z3.string(), ManifestEntrySchema)
1096
1128
  });
1097
1129
 
1098
1130
  // src/sync/manifest.service.ts
@@ -1184,15 +1216,15 @@ import { Injectable as Injectable6, Logger as Logger3 } from "@nestjs/common";
1184
1216
  import { ConfigService as ConfigService2 } from "@nestjs/config";
1185
1217
 
1186
1218
  // src/schemas/config.schemas.ts
1187
- import { z as z3 } from "zod";
1188
- var DuckDBConfigSchema = z3.object({
1189
- embeddingDimensions: z3.coerce.number().int().positive().default(512)
1219
+ import { z as z4 } from "zod";
1220
+ var DuckDBConfigSchema = z4.object({
1221
+ embeddingDimensions: z4.coerce.number().int().positive().default(512)
1190
1222
  });
1191
- var EmbeddingConfigSchema = z3.object({
1192
- provider: z3.enum(["openai", "voyage", "nomic", "mock"]).default("voyage"),
1193
- apiKey: z3.string().optional(),
1194
- model: z3.string().min(1).default("voyage-3.5-lite"),
1195
- dimensions: z3.coerce.number().int().positive().default(512)
1223
+ var EmbeddingConfigSchema = z4.object({
1224
+ provider: z4.enum(["openai", "voyage", "nomic", "mock"]).default("voyage"),
1225
+ apiKey: z4.string().optional(),
1226
+ model: z4.string().min(1).default("voyage-3.5-lite"),
1227
+ dimensions: z4.coerce.number().int().positive().default(512)
1196
1228
  });
1197
1229
 
1198
1230
  // src/embedding/embedding.types.ts
@@ -1285,17 +1317,17 @@ class OpenAIEmbeddingProvider {
1285
1317
  }
1286
1318
 
1287
1319
  // src/schemas/embedding.schemas.ts
1288
- import { z as z4 } from "zod";
1289
- var VoyageEmbeddingResponseSchema = z4.object({
1290
- object: z4.string(),
1291
- data: z4.array(z4.object({
1292
- object: z4.string(),
1293
- embedding: z4.array(z4.number()),
1294
- index: z4.number().int().nonnegative()
1320
+ import { z as z5 } from "zod";
1321
+ var VoyageEmbeddingResponseSchema = z5.object({
1322
+ object: z5.string(),
1323
+ data: z5.array(z5.object({
1324
+ object: z5.string(),
1325
+ embedding: z5.array(z5.number()),
1326
+ index: z5.number().int().nonnegative()
1295
1327
  })),
1296
- model: z4.string(),
1297
- usage: z4.object({
1298
- total_tokens: z4.number().int().nonnegative()
1328
+ model: z5.string(),
1329
+ usage: z5.object({
1330
+ total_tokens: z5.number().int().nonnegative()
1299
1331
  })
1300
1332
  });
1301
1333
 
@@ -1606,8 +1638,8 @@ import { glob } from "glob";
1606
1638
 
1607
1639
  // src/utils/frontmatter.ts
1608
1640
  import matter from "gray-matter";
1609
- import { z as z5 } from "zod";
1610
- var EntityTypeSchema = z5.enum([
1641
+ import { z as z6 } from "zod";
1642
+ var EntityTypeSchema2 = z6.enum([
1611
1643
  "Topic",
1612
1644
  "Technology",
1613
1645
  "Concept",
@@ -1617,20 +1649,20 @@ var EntityTypeSchema = z5.enum([
1617
1649
  "Organization",
1618
1650
  "Document"
1619
1651
  ]);
1620
- var RelationTypeSchema = z5.enum(["REFERENCES"]);
1621
- var EntitySchema = z5.object({
1622
- name: z5.string().min(1),
1623
- type: EntityTypeSchema,
1624
- description: z5.string().min(1)
1652
+ var RelationTypeSchema2 = z6.enum(["REFERENCES"]);
1653
+ var EntitySchema2 = z6.object({
1654
+ name: z6.string().min(1),
1655
+ type: EntityTypeSchema2,
1656
+ description: z6.string().min(1)
1625
1657
  });
1626
- var RelationshipSchema = z5.object({
1627
- source: z5.string().min(1),
1628
- relation: RelationTypeSchema,
1629
- target: z5.string().min(1)
1658
+ var RelationshipSchema2 = z6.object({
1659
+ source: z6.string().min(1),
1660
+ relation: RelationTypeSchema2,
1661
+ target: z6.string().min(1)
1630
1662
  });
1631
- var GraphMetadataSchema = z5.object({
1632
- importance: z5.enum(["high", "medium", "low"]).optional(),
1633
- domain: z5.string().optional()
1663
+ var GraphMetadataSchema2 = z6.object({
1664
+ importance: z6.enum(["high", "medium", "low"]).optional(),
1665
+ domain: z6.string().optional()
1634
1666
  });
1635
1667
  var validateDateFormat = (dateStr) => {
1636
1668
  const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
@@ -1650,16 +1682,16 @@ var validateDateFormat = (dateStr) => {
1650
1682
  }
1651
1683
  return day <= daysInMonth[month - 1];
1652
1684
  };
1653
- var FrontmatterSchema = z5.object({
1654
- created: z5.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
1655
- updated: z5.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
1656
- status: z5.enum(["draft", "ongoing", "complete"]).optional(),
1657
- topic: z5.string().optional(),
1658
- tags: z5.array(z5.string()).optional(),
1659
- summary: z5.string().optional(),
1660
- entities: z5.array(EntitySchema).optional(),
1661
- relationships: z5.array(RelationshipSchema).optional(),
1662
- graph: GraphMetadataSchema.optional()
1685
+ var FrontmatterSchema = z6.object({
1686
+ created: z6.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
1687
+ updated: z6.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
1688
+ status: z6.enum(["draft", "ongoing", "complete"]).optional(),
1689
+ topic: z6.string().optional(),
1690
+ tags: z6.array(z6.string()).optional(),
1691
+ summary: z6.string().optional(),
1692
+ entities: z6.array(EntitySchema2).optional(),
1693
+ relationships: z6.array(RelationshipSchema2).optional(),
1694
+ graph: GraphMetadataSchema2.optional()
1663
1695
  }).passthrough();
1664
1696
  function parseFrontmatter(content) {
1665
1697
  try {
@@ -1775,7 +1807,7 @@ class DocumentParserService {
1775
1807
  if (!fm?.graph) {
1776
1808
  return;
1777
1809
  }
1778
- const result = GraphMetadataSchema.safeParse(fm.graph);
1810
+ const result = GraphMetadataSchema2.safeParse(fm.graph);
1779
1811
  return result.success ? result.data : undefined;
1780
1812
  }
1781
1813
  computeHash(content) {
@@ -3197,13 +3229,244 @@ SqlCommand = __legacyDecorateClassTS([
3197
3229
  typeof GraphService === "undefined" ? Object : GraphService
3198
3230
  ])
3199
3231
  ], SqlCommand);
3232
+ // src/commands/question.command.ts
3233
+ import { Injectable as Injectable16 } from "@nestjs/common";
3234
+ import { Command as Command6, CommandRunner as CommandRunner6, Option as Option4 } from "nest-commander";
3235
+ function escapeSql(value) {
3236
+ return value.replace(/'/g, "''");
3237
+ }
3238
+
3239
+ class QuestionAddCommand extends CommandRunner6 {
3240
+ graphService;
3241
+ embeddingService;
3242
+ constructor(graphService, embeddingService) {
3243
+ super();
3244
+ this.graphService = graphService;
3245
+ this.embeddingService = embeddingService;
3246
+ }
3247
+ async run(inputs, options) {
3248
+ const questionText = inputs[0];
3249
+ if (!questionText) {
3250
+ console.error("Error: Question text is required");
3251
+ console.error('Usage: lattice question:add "your question"');
3252
+ process.exit(1);
3253
+ }
3254
+ try {
3255
+ await this.graphService.upsertNode("Question", {
3256
+ name: questionText,
3257
+ text: questionText,
3258
+ createdAt: new Date().toISOString()
3259
+ });
3260
+ const embedding = await this.embeddingService.generateEmbedding(`Question: ${questionText}`);
3261
+ await this.graphService.updateNodeEmbedding("Question", questionText, embedding);
3262
+ console.log(`
3263
+ \u2705 Added question: "${questionText}"`);
3264
+ if (options.answeredBy) {
3265
+ const docResult = await this.graphService.query(`
3266
+ SELECT name FROM nodes
3267
+ WHERE label = 'Document' AND name = '${escapeSql(options.answeredBy)}'
3268
+ `);
3269
+ if (docResult.resultSet.length === 0) {
3270
+ console.log(`
3271
+ \u26A0\uFE0F Document not found: ${options.answeredBy}`);
3272
+ console.log(" Run 'lattice sync' first to add documents to the graph.");
3273
+ console.log(` Question was created but not linked.
3274
+ `);
3275
+ process.exit(0);
3276
+ }
3277
+ await this.graphService.upsertRelationship("Question", questionText, "ANSWERED_BY", "Document", options.answeredBy, {});
3278
+ console.log(` Linked to: ${options.answeredBy}`);
3279
+ }
3280
+ console.log();
3281
+ process.exit(0);
3282
+ } catch (error) {
3283
+ console.error("Error:", error instanceof Error ? error.message : String(error));
3284
+ process.exit(1);
3285
+ }
3286
+ }
3287
+ parseAnsweredBy(value) {
3288
+ return value;
3289
+ }
3290
+ }
3291
+ __legacyDecorateClassTS([
3292
+ Option4({
3293
+ flags: "--answered-by <path>",
3294
+ description: "Immediately link to a document that answers this question"
3295
+ }),
3296
+ __legacyMetadataTS("design:type", Function),
3297
+ __legacyMetadataTS("design:paramtypes", [
3298
+ String
3299
+ ]),
3300
+ __legacyMetadataTS("design:returntype", String)
3301
+ ], QuestionAddCommand.prototype, "parseAnsweredBy", null);
3302
+ QuestionAddCommand = __legacyDecorateClassTS([
3303
+ Injectable16(),
3304
+ Command6({
3305
+ name: "question:add",
3306
+ arguments: "<question>",
3307
+ description: "Add a new question to the knowledge graph"
3308
+ }),
3309
+ __legacyMetadataTS("design:paramtypes", [
3310
+ typeof GraphService === "undefined" ? Object : GraphService,
3311
+ typeof EmbeddingService === "undefined" ? Object : EmbeddingService
3312
+ ])
3313
+ ], QuestionAddCommand);
3314
+
3315
+ class QuestionLinkCommand extends CommandRunner6 {
3316
+ graphService;
3317
+ embeddingService;
3318
+ constructor(graphService, embeddingService) {
3319
+ super();
3320
+ this.graphService = graphService;
3321
+ this.embeddingService = embeddingService;
3322
+ }
3323
+ async run(inputs, options) {
3324
+ const questionText = inputs[0];
3325
+ if (!questionText) {
3326
+ console.error("Error: Question text is required");
3327
+ console.error('Usage: lattice question:link "your question" --doc path/to/doc.md');
3328
+ process.exit(1);
3329
+ }
3330
+ if (!options.doc) {
3331
+ console.error("Error: --doc flag is required");
3332
+ console.error('Usage: lattice question:link "your question" --doc path/to/doc.md');
3333
+ process.exit(1);
3334
+ }
3335
+ try {
3336
+ const existingQuestions = await this.graphService.query(`
3337
+ SELECT name FROM nodes
3338
+ WHERE label = 'Question' AND name = '${escapeSql(questionText)}'
3339
+ `);
3340
+ if (existingQuestions.resultSet.length === 0) {
3341
+ await this.graphService.upsertNode("Question", {
3342
+ name: questionText,
3343
+ text: questionText,
3344
+ createdAt: new Date().toISOString()
3345
+ });
3346
+ const embedding = await this.embeddingService.generateEmbedding(`Question: ${questionText}`);
3347
+ await this.graphService.updateNodeEmbedding("Question", questionText, embedding);
3348
+ console.log(`
3349
+ \u2705 Created question: "${questionText}"`);
3350
+ }
3351
+ const existingDocs = await this.graphService.query(`
3352
+ SELECT name FROM nodes
3353
+ WHERE label = 'Document' AND name = '${escapeSql(options.doc)}'
3354
+ `);
3355
+ if (existingDocs.resultSet.length === 0) {
3356
+ console.error(`
3357
+ \u274C Document not found in graph: ${options.doc}`);
3358
+ console.error(` Run 'lattice sync' first to add documents to the graph.
3359
+ `);
3360
+ process.exit(1);
3361
+ }
3362
+ await this.graphService.upsertRelationship("Question", questionText, "ANSWERED_BY", "Document", options.doc, {});
3363
+ console.log(`
3364
+ \u2705 Linked: "${questionText}"`);
3365
+ console.log(` \u2192 ${options.doc}
3366
+ `);
3367
+ process.exit(0);
3368
+ } catch (error) {
3369
+ console.error("Error:", error instanceof Error ? error.message : String(error));
3370
+ process.exit(1);
3371
+ }
3372
+ }
3373
+ parseDoc(value) {
3374
+ return value;
3375
+ }
3376
+ }
3377
+ __legacyDecorateClassTS([
3378
+ Option4({
3379
+ flags: "-d, --doc <path>",
3380
+ description: "Path to the document that answers this question"
3381
+ }),
3382
+ __legacyMetadataTS("design:type", Function),
3383
+ __legacyMetadataTS("design:paramtypes", [
3384
+ String
3385
+ ]),
3386
+ __legacyMetadataTS("design:returntype", String)
3387
+ ], QuestionLinkCommand.prototype, "parseDoc", null);
3388
+ QuestionLinkCommand = __legacyDecorateClassTS([
3389
+ Injectable16(),
3390
+ Command6({
3391
+ name: "question:link",
3392
+ arguments: "<question>",
3393
+ description: "Link a question to a document via ANSWERED_BY relationship"
3394
+ }),
3395
+ __legacyMetadataTS("design:paramtypes", [
3396
+ typeof GraphService === "undefined" ? Object : GraphService,
3397
+ typeof EmbeddingService === "undefined" ? Object : EmbeddingService
3398
+ ])
3399
+ ], QuestionLinkCommand);
3400
+
3401
+ class QuestionUnansweredCommand extends CommandRunner6 {
3402
+ graphService;
3403
+ constructor(graphService) {
3404
+ super();
3405
+ this.graphService = graphService;
3406
+ }
3407
+ async run() {
3408
+ try {
3409
+ const result = await this.graphService.query(`
3410
+ SELECT
3411
+ q.name as question,
3412
+ q.properties->>'createdAt' as created_at,
3413
+ q.created_at as db_created_at
3414
+ FROM nodes q
3415
+ WHERE q.label = 'Question'
3416
+ AND NOT EXISTS (
3417
+ SELECT 1 FROM relationships r
3418
+ WHERE r.source_label = 'Question'
3419
+ AND r.source_name = q.name
3420
+ AND r.relation_type = 'ANSWERED_BY'
3421
+ )
3422
+ ORDER BY COALESCE(q.properties->>'createdAt', q.created_at::VARCHAR) DESC
3423
+ `);
3424
+ console.log(`
3425
+ === Unanswered Questions ===
3426
+ `);
3427
+ if (result.resultSet.length === 0) {
3428
+ console.log(`No unanswered questions found.
3429
+ `);
3430
+ console.log(`Add questions with: lattice question:add "your question"
3431
+ `);
3432
+ process.exit(0);
3433
+ }
3434
+ result.resultSet.forEach((row, idx) => {
3435
+ const [question, createdAt, dbCreatedAt] = row;
3436
+ const displayDate = createdAt || dbCreatedAt || "unknown";
3437
+ console.log(`${idx + 1}. ${question}`);
3438
+ console.log(` Created: ${displayDate}`);
3439
+ });
3440
+ console.log(`
3441
+ Total: ${result.resultSet.length} unanswered question(s)
3442
+ `);
3443
+ console.log("To link a question to an answer:");
3444
+ console.log(` lattice question:link "question text" --doc path/to/doc.md
3445
+ `);
3446
+ process.exit(0);
3447
+ } catch (error) {
3448
+ console.error("Error:", error instanceof Error ? error.message : String(error));
3449
+ process.exit(1);
3450
+ }
3451
+ }
3452
+ }
3453
+ QuestionUnansweredCommand = __legacyDecorateClassTS([
3454
+ Injectable16(),
3455
+ Command6({
3456
+ name: "question:unanswered",
3457
+ description: "List all questions without ANSWERED_BY relationships"
3458
+ }),
3459
+ __legacyMetadataTS("design:paramtypes", [
3460
+ typeof GraphService === "undefined" ? Object : GraphService
3461
+ ])
3462
+ ], QuestionUnansweredCommand);
3200
3463
  // src/commands/site.command.ts
3201
3464
  import { spawn } from "child_process";
3202
3465
  import { existsSync as existsSync6, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
3203
3466
  import * as path2 from "path";
3204
- import { Injectable as Injectable16 } from "@nestjs/common";
3205
- import { Command as Command6, CommandRunner as CommandRunner6, Option as Option4 } from "nest-commander";
3206
- class SiteCommand extends CommandRunner6 {
3467
+ import { Injectable as Injectable17 } from "@nestjs/common";
3468
+ import { Command as Command7, CommandRunner as CommandRunner7, Option as Option5 } from "nest-commander";
3469
+ class SiteCommand extends CommandRunner7 {
3207
3470
  getPidFile() {
3208
3471
  return path2.join(getLatticeHome(), "site.pid");
3209
3472
  }
@@ -3336,7 +3599,7 @@ class SiteCommand extends CommandRunner6 {
3336
3599
  }
3337
3600
  unlinkSync(pidFile);
3338
3601
  console.log(`\u2705 Killed Lattice site process (PID: ${pid})`);
3339
- } catch (err) {
3602
+ } catch (_err) {
3340
3603
  try {
3341
3604
  unlinkSync(pidFile);
3342
3605
  } catch {}
@@ -3357,7 +3620,7 @@ class SiteCommand extends CommandRunner6 {
3357
3620
  }
3358
3621
  }
3359
3622
  __legacyDecorateClassTS([
3360
- Option4({
3623
+ Option5({
3361
3624
  flags: "-b, --build",
3362
3625
  description: "Build the site without starting the server"
3363
3626
  }),
@@ -3366,7 +3629,7 @@ __legacyDecorateClassTS([
3366
3629
  __legacyMetadataTS("design:returntype", Boolean)
3367
3630
  ], SiteCommand.prototype, "parseBuild", null);
3368
3631
  __legacyDecorateClassTS([
3369
- Option4({
3632
+ Option5({
3370
3633
  flags: "-d, --dev",
3371
3634
  description: "Run in development mode (hot reload, no search)"
3372
3635
  }),
@@ -3375,7 +3638,7 @@ __legacyDecorateClassTS([
3375
3638
  __legacyMetadataTS("design:returntype", Boolean)
3376
3639
  ], SiteCommand.prototype, "parseDev", null);
3377
3640
  __legacyDecorateClassTS([
3378
- Option4({
3641
+ Option5({
3379
3642
  flags: "-p, --port <port>",
3380
3643
  description: "Port to run the server on (default: 4321)"
3381
3644
  }),
@@ -3386,7 +3649,7 @@ __legacyDecorateClassTS([
3386
3649
  __legacyMetadataTS("design:returntype", String)
3387
3650
  ], SiteCommand.prototype, "parsePort", null);
3388
3651
  __legacyDecorateClassTS([
3389
- Option4({
3652
+ Option5({
3390
3653
  flags: "-k, --kill",
3391
3654
  description: "Kill the running Lattice site process"
3392
3655
  }),
@@ -3395,16 +3658,16 @@ __legacyDecorateClassTS([
3395
3658
  __legacyMetadataTS("design:returntype", Boolean)
3396
3659
  ], SiteCommand.prototype, "parseKill", null);
3397
3660
  SiteCommand = __legacyDecorateClassTS([
3398
- Injectable16(),
3399
- Command6({
3661
+ Injectable17(),
3662
+ Command7({
3400
3663
  name: "site",
3401
3664
  description: "Build and run the Lattice documentation site"
3402
3665
  })
3403
3666
  ], SiteCommand);
3404
3667
  // src/commands/status.command.ts
3405
- import { Injectable as Injectable17 } from "@nestjs/common";
3406
- import { Command as Command7, CommandRunner as CommandRunner7, Option as Option5 } from "nest-commander";
3407
- class StatusCommand extends CommandRunner7 {
3668
+ import { Injectable as Injectable18 } from "@nestjs/common";
3669
+ import { Command as Command8, CommandRunner as CommandRunner8, Option as Option6 } from "nest-commander";
3670
+ class StatusCommand extends CommandRunner8 {
3408
3671
  syncService;
3409
3672
  dbChangeDetector;
3410
3673
  constructor(syncService, dbChangeDetector) {
@@ -3470,7 +3733,7 @@ class StatusCommand extends CommandRunner7 {
3470
3733
  }
3471
3734
  }
3472
3735
  __legacyDecorateClassTS([
3473
- Option5({
3736
+ Option6({
3474
3737
  flags: "-v, --verbose",
3475
3738
  description: "Show all documents including unchanged"
3476
3739
  }),
@@ -3479,8 +3742,8 @@ __legacyDecorateClassTS([
3479
3742
  __legacyMetadataTS("design:returntype", Boolean)
3480
3743
  ], StatusCommand.prototype, "parseVerbose", null);
3481
3744
  StatusCommand = __legacyDecorateClassTS([
3482
- Injectable17(),
3483
- Command7({
3745
+ Injectable18(),
3746
+ Command8({
3484
3747
  name: "status",
3485
3748
  description: "Show documents that need syncing (new or updated)"
3486
3749
  }),
@@ -3492,11 +3755,11 @@ StatusCommand = __legacyDecorateClassTS([
3492
3755
  // src/commands/sync.command.ts
3493
3756
  import { watch } from "fs";
3494
3757
  import { join as join4 } from "path";
3495
- import { Injectable as Injectable19 } from "@nestjs/common";
3496
- import { Command as Command8, CommandRunner as CommandRunner8, Option as Option6 } from "nest-commander";
3758
+ import { Injectable as Injectable20 } from "@nestjs/common";
3759
+ import { Command as Command9, CommandRunner as CommandRunner9, Option as Option7 } from "nest-commander";
3497
3760
 
3498
3761
  // src/sync/graph-validator.service.ts
3499
- import { Injectable as Injectable18, Logger as Logger9 } from "@nestjs/common";
3762
+ import { Injectable as Injectable19, Logger as Logger9 } from "@nestjs/common";
3500
3763
  class GraphValidatorService {
3501
3764
  graph;
3502
3765
  logger = new Logger9(GraphValidatorService.name);
@@ -3645,14 +3908,14 @@ class GraphValidatorService {
3645
3908
  }
3646
3909
  }
3647
3910
  GraphValidatorService = __legacyDecorateClassTS([
3648
- Injectable18(),
3911
+ Injectable19(),
3649
3912
  __legacyMetadataTS("design:paramtypes", [
3650
3913
  typeof GraphService === "undefined" ? Object : GraphService
3651
3914
  ])
3652
3915
  ], GraphValidatorService);
3653
3916
 
3654
3917
  // src/commands/sync.command.ts
3655
- class SyncCommand extends CommandRunner8 {
3918
+ class SyncCommand extends CommandRunner9 {
3656
3919
  syncService;
3657
3920
  graphService;
3658
3921
  _graphValidator;
@@ -3667,7 +3930,7 @@ class SyncCommand extends CommandRunner8 {
3667
3930
  async safeExit(code) {
3668
3931
  try {
3669
3932
  await this.graphService.checkpoint();
3670
- } catch (error) {
3933
+ } catch (_error) {
3671
3934
  console.error("Warning: checkpoint failed during exit");
3672
3935
  }
3673
3936
  process.exit(code);
@@ -3925,7 +4188,7 @@ class SyncCommand extends CommandRunner8 {
3925
4188
  }
3926
4189
  }
3927
4190
  __legacyDecorateClassTS([
3928
- Option6({
4191
+ Option7({
3929
4192
  flags: "-f, --force",
3930
4193
  description: "Force re-sync specified documents (requires paths to be specified)"
3931
4194
  }),
@@ -3934,7 +4197,7 @@ __legacyDecorateClassTS([
3934
4197
  __legacyMetadataTS("design:returntype", Boolean)
3935
4198
  ], SyncCommand.prototype, "parseForce", null);
3936
4199
  __legacyDecorateClassTS([
3937
- Option6({
4200
+ Option7({
3938
4201
  flags: "-d, --dry-run",
3939
4202
  description: "Show what would change without applying"
3940
4203
  }),
@@ -3943,7 +4206,7 @@ __legacyDecorateClassTS([
3943
4206
  __legacyMetadataTS("design:returntype", Boolean)
3944
4207
  ], SyncCommand.prototype, "parseDryRun", null);
3945
4208
  __legacyDecorateClassTS([
3946
- Option6({
4209
+ Option7({
3947
4210
  flags: "-v, --verbose",
3948
4211
  description: "Show detailed output"
3949
4212
  }),
@@ -3952,7 +4215,7 @@ __legacyDecorateClassTS([
3952
4215
  __legacyMetadataTS("design:returntype", Boolean)
3953
4216
  ], SyncCommand.prototype, "parseVerbose", null);
3954
4217
  __legacyDecorateClassTS([
3955
- Option6({
4218
+ Option7({
3956
4219
  flags: "-w, --watch",
3957
4220
  description: "Watch for file changes and sync automatically"
3958
4221
  }),
@@ -3961,7 +4224,7 @@ __legacyDecorateClassTS([
3961
4224
  __legacyMetadataTS("design:returntype", Boolean)
3962
4225
  ], SyncCommand.prototype, "parseWatch", null);
3963
4226
  __legacyDecorateClassTS([
3964
- Option6({
4227
+ Option7({
3965
4228
  flags: "--diff",
3966
4229
  description: "Show only changed documents (alias for --dry-run)"
3967
4230
  }),
@@ -3970,7 +4233,7 @@ __legacyDecorateClassTS([
3970
4233
  __legacyMetadataTS("design:returntype", Boolean)
3971
4234
  ], SyncCommand.prototype, "parseDiff", null);
3972
4235
  __legacyDecorateClassTS([
3973
- Option6({
4236
+ Option7({
3974
4237
  flags: "--skip-cascade",
3975
4238
  description: "Skip cascade analysis (faster for large repos)"
3976
4239
  }),
@@ -3979,7 +4242,7 @@ __legacyDecorateClassTS([
3979
4242
  __legacyMetadataTS("design:returntype", Boolean)
3980
4243
  ], SyncCommand.prototype, "parseSkipCascade", null);
3981
4244
  __legacyDecorateClassTS([
3982
- Option6({
4245
+ Option7({
3983
4246
  flags: "--no-embeddings",
3984
4247
  description: "Disable embedding generation during sync"
3985
4248
  }),
@@ -3988,7 +4251,7 @@ __legacyDecorateClassTS([
3988
4251
  __legacyMetadataTS("design:returntype", Boolean)
3989
4252
  ], SyncCommand.prototype, "parseNoEmbeddings", null);
3990
4253
  __legacyDecorateClassTS([
3991
- Option6({
4254
+ Option7({
3992
4255
  flags: "--skip-extraction",
3993
4256
  description: "Skip AI entity extraction (sync without re-extracting entities)"
3994
4257
  }),
@@ -3997,8 +4260,8 @@ __legacyDecorateClassTS([
3997
4260
  __legacyMetadataTS("design:returntype", Boolean)
3998
4261
  ], SyncCommand.prototype, "parseSkipExtraction", null);
3999
4262
  SyncCommand = __legacyDecorateClassTS([
4000
- Injectable19(),
4001
- Command8({
4263
+ Injectable20(),
4264
+ Command9({
4002
4265
  name: "sync",
4003
4266
  arguments: "[paths...]",
4004
4267
  description: "Synchronize documents to the knowledge graph"
@@ -4037,7 +4300,7 @@ GraphModule = __legacyDecorateClassTS([
4037
4300
  import { Module as Module3 } from "@nestjs/common";
4038
4301
 
4039
4302
  // src/query/query.service.ts
4040
- import { Injectable as Injectable20, Logger as Logger10 } from "@nestjs/common";
4303
+ import { Injectable as Injectable21, Logger as Logger10 } from "@nestjs/common";
4041
4304
  class QueryService {
4042
4305
  graphService;
4043
4306
  logger = new Logger10(QueryService.name);
@@ -4050,7 +4313,7 @@ class QueryService {
4050
4313
  }
4051
4314
  }
4052
4315
  QueryService = __legacyDecorateClassTS([
4053
- Injectable20(),
4316
+ Injectable21(),
4054
4317
  __legacyMetadataTS("design:paramtypes", [
4055
4318
  typeof GraphService === "undefined" ? Object : GraphService
4056
4319
  ])
@@ -4124,7 +4387,10 @@ AppModule = __legacyDecorateClassTS([
4124
4387
  OntologyCommand,
4125
4388
  InitCommand,
4126
4389
  MigrateCommand,
4127
- SiteCommand
4390
+ SiteCommand,
4391
+ QuestionAddCommand,
4392
+ QuestionLinkCommand,
4393
+ QuestionUnansweredCommand
4128
4394
  ]
4129
4395
  })
4130
4396
  ], AppModule);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zabaca/lattice",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Human-initiated, AI-powered knowledge graph for markdown documentation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,13 @@
1
1
  import { defineConfig } from 'astro/config';
2
2
  import { astroSpaceship } from 'astro-spaceship';
3
+ import rehypeStripMdExtension from './src/plugins/rehype-strip-md-extension';
3
4
 
4
5
  import websiteConfig from 'astro-spaceship/config';
5
6
 
6
7
  export default defineConfig({
8
+ markdown: {
9
+ rehypePlugins: [rehypeStripMdExtension],
10
+ },
7
11
  integrations: [
8
12
  astroSpaceship(websiteConfig)
9
13
  ]
@@ -0,0 +1,29 @@
1
+ import { visit } from 'unist-util-visit';
2
+ import type { Root, Element } from 'hast';
3
+
4
+ /**
5
+ * Rehype plugin that strips .md extensions from relative links.
6
+ * This allows standard markdown links like [text](./file.md) to work
7
+ * correctly in Astro where routes don't include the .md extension.
8
+ */
9
+ export default function rehypeStripMdExtension() {
10
+ return (tree: Root) => {
11
+ visit(tree, 'element', (node: Element) => {
12
+ if (
13
+ node.tagName === 'a' &&
14
+ typeof node.properties?.href === 'string'
15
+ ) {
16
+ const href = node.properties.href;
17
+ // Only process relative links ending in .md
18
+ if (
19
+ !href.startsWith('http://') &&
20
+ !href.startsWith('https://') &&
21
+ !href.startsWith('//') &&
22
+ href.endsWith('.md')
23
+ ) {
24
+ node.properties.href = href.slice(0, -3);
25
+ }
26
+ }
27
+ });
28
+ };
29
+ }
@@ -1,87 +0,0 @@
1
- ---
2
- description: Sync modified docs to knowledge graph
3
- model: sonnet
4
- ---
5
-
6
- Sync modified documents in `~/.lattice/docs/` to the knowledge graph.
7
-
8
- ## Configuration
9
-
10
- **⚠️ CRITICAL: All documentation lives in `~/.lattice/docs/`**
11
-
12
- | Path | Purpose |
13
- |------|---------|
14
- | `~/.lattice/docs/` | Root documentation directory (ALWAYS use this) |
15
- | `~/.lattice/docs/{topic}/` | Topic directories |
16
- | `~/.lattice/docs/{topic}/*.md` | Research documents |
17
-
18
- **NEVER use project-local `docs/` directories. ALWAYS use absolute path `~/.lattice/docs/`.**
19
-
20
- ## Process
21
-
22
- ### Step 1: Check What Needs Syncing
23
-
24
- Run the status command to identify modified documents:
25
-
26
- ```bash
27
- lattice status
28
- ```
29
-
30
- This will show:
31
- - **New** documents not yet in the graph
32
- - **Updated** documents that have changed since last sync
33
-
34
- If no documents need syncing, report that and exit.
35
-
36
- ### Step 2: Sync to Graph
37
-
38
- Run sync to process all changed documents:
39
-
40
- ```bash
41
- lattice sync
42
- ```
43
-
44
- This will automatically:
45
- - **Extract entities** using AI (Claude Haiku) for each new/updated document
46
- - **Generate embeddings** for semantic search
47
- - **Create entity relationships** in the graph
48
- - **Update the sync manifest** with new hashes
49
-
50
- The sync command includes built-in rate limiting (500ms between extractions) to avoid API throttling.
51
-
52
- ### Step 3: Report Results
53
-
54
- Summarize what was processed:
55
- - Number of documents synced
56
- - Entities extracted per document
57
- - Graph sync statistics (added, updated, unchanged)
58
- - Any errors encountered
59
-
60
- ## Example Output
61
-
62
- ```
63
- ## Graph Sync
64
-
65
- lattice status:
66
- - 3 documents need syncing (2 new, 1 updated)
67
-
68
- lattice sync:
69
- - ~/.lattice/docs/american-holidays/README.md → 4 entities extracted
70
- - ~/.lattice/docs/american-holidays/thanksgiving-vs-christmas.md → 8 entities extracted
71
- - ~/.lattice/docs/bun-nestjs/notes.md → 5 entities extracted
72
-
73
- Summary:
74
- - Added: 2
75
- - Updated: 1
76
- - Unchanged: 126
77
- - Duration: 3.2s
78
- ```
79
-
80
- ## Important Notes
81
-
82
- - **AI extraction is automatic** - no need for manual `/entity-extract` calls
83
- - **Incremental sync** - only processes changed documents
84
- - **Self-correcting** - Claude validates extractions and fixes errors automatically
85
- - **Safe to run frequently** - won't duplicate or corrupt data
86
- - **No frontmatter required** - documents are plain markdown
87
- - **Batch syncing** - run once after multiple research sessions for efficiency