@zabaca/lattice 1.1.1 → 1.2.0

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",
@@ -465,9 +489,13 @@ OBSIDIAN_VAULT_DIR=docs
465
489
  console.log();
466
490
  console.log("Claude Code slash commands:");
467
491
  console.log(" /research <topic> - AI-assisted research workflow");
468
- console.log(" /graph-sync - Extract entities and sync to graph");
469
492
  console.log(" /entity-extract - Extract entities from a document");
470
493
  console.log();
494
+ console.log("Question tracking:");
495
+ console.log(" lattice question:add <question> - Add a question");
496
+ console.log(" lattice question:link <q> --doc <p> - Link question to answer");
497
+ console.log(" lattice question:unanswered - List unanswered questions");
498
+ console.log();
471
499
  if (!(await fs.readFile(envPath, "utf-8")).includes("pa-")) {
472
500
  console.log(`\u26A0\uFE0F Add your Voyage API key to: ${envPath}`);
473
501
  console.log();
@@ -513,7 +541,7 @@ OBSIDIAN_VAULT_DIR=docs
513
541
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
514
542
  await fs.copyFile(sourcePath, targetPath);
515
543
  copied++;
516
- } catch (err) {}
544
+ } catch (_err) {}
517
545
  }
518
546
  if (copied > 0) {
519
547
  console.log(`\u2705 Site template: ${copied} file(s) installed`);
@@ -557,7 +585,7 @@ OBSIDIAN_VAULT_DIR=docs
557
585
  await fs.copyFile(sourcePath, targetPath);
558
586
  installed.push(file);
559
587
  copied++;
560
- } catch (err) {}
588
+ } catch (_err) {}
561
589
  }
562
590
  if (copied > 0) {
563
591
  console.log(`\u2705 Claude commands: ${copied} installed to ${targetDir}`);
@@ -1081,18 +1109,18 @@ import { readFile as readFile3, writeFile } from "fs/promises";
1081
1109
  import { Injectable as Injectable5 } from "@nestjs/common";
1082
1110
 
1083
1111
  // 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()
1112
+ import { z as z3 } from "zod";
1113
+ var ManifestEntrySchema = z3.object({
1114
+ contentHash: z3.string(),
1115
+ frontmatterHash: z3.string(),
1116
+ lastSynced: z3.string(),
1117
+ entityCount: z3.number().int().nonnegative(),
1118
+ relationshipCount: z3.number().int().nonnegative()
1091
1119
  });
1092
- var SyncManifestSchema = z2.object({
1093
- version: z2.string(),
1094
- lastSync: z2.string(),
1095
- documents: z2.record(z2.string(), ManifestEntrySchema)
1120
+ var SyncManifestSchema = z3.object({
1121
+ version: z3.string(),
1122
+ lastSync: z3.string(),
1123
+ documents: z3.record(z3.string(), ManifestEntrySchema)
1096
1124
  });
1097
1125
 
1098
1126
  // src/sync/manifest.service.ts
@@ -1184,15 +1212,15 @@ import { Injectable as Injectable6, Logger as Logger3 } from "@nestjs/common";
1184
1212
  import { ConfigService as ConfigService2 } from "@nestjs/config";
1185
1213
 
1186
1214
  // 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)
1215
+ import { z as z4 } from "zod";
1216
+ var DuckDBConfigSchema = z4.object({
1217
+ embeddingDimensions: z4.coerce.number().int().positive().default(512)
1190
1218
  });
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)
1219
+ var EmbeddingConfigSchema = z4.object({
1220
+ provider: z4.enum(["openai", "voyage", "nomic", "mock"]).default("voyage"),
1221
+ apiKey: z4.string().optional(),
1222
+ model: z4.string().min(1).default("voyage-3.5-lite"),
1223
+ dimensions: z4.coerce.number().int().positive().default(512)
1196
1224
  });
1197
1225
 
1198
1226
  // src/embedding/embedding.types.ts
@@ -1285,17 +1313,17 @@ class OpenAIEmbeddingProvider {
1285
1313
  }
1286
1314
 
1287
1315
  // 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()
1316
+ import { z as z5 } from "zod";
1317
+ var VoyageEmbeddingResponseSchema = z5.object({
1318
+ object: z5.string(),
1319
+ data: z5.array(z5.object({
1320
+ object: z5.string(),
1321
+ embedding: z5.array(z5.number()),
1322
+ index: z5.number().int().nonnegative()
1295
1323
  })),
1296
- model: z4.string(),
1297
- usage: z4.object({
1298
- total_tokens: z4.number().int().nonnegative()
1324
+ model: z5.string(),
1325
+ usage: z5.object({
1326
+ total_tokens: z5.number().int().nonnegative()
1299
1327
  })
1300
1328
  });
1301
1329
 
@@ -1606,8 +1634,8 @@ import { glob } from "glob";
1606
1634
 
1607
1635
  // src/utils/frontmatter.ts
1608
1636
  import matter from "gray-matter";
1609
- import { z as z5 } from "zod";
1610
- var EntityTypeSchema = z5.enum([
1637
+ import { z as z6 } from "zod";
1638
+ var EntityTypeSchema2 = z6.enum([
1611
1639
  "Topic",
1612
1640
  "Technology",
1613
1641
  "Concept",
@@ -1617,20 +1645,20 @@ var EntityTypeSchema = z5.enum([
1617
1645
  "Organization",
1618
1646
  "Document"
1619
1647
  ]);
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)
1648
+ var RelationTypeSchema2 = z6.enum(["REFERENCES"]);
1649
+ var EntitySchema2 = z6.object({
1650
+ name: z6.string().min(1),
1651
+ type: EntityTypeSchema2,
1652
+ description: z6.string().min(1)
1625
1653
  });
1626
- var RelationshipSchema = z5.object({
1627
- source: z5.string().min(1),
1628
- relation: RelationTypeSchema,
1629
- target: z5.string().min(1)
1654
+ var RelationshipSchema2 = z6.object({
1655
+ source: z6.string().min(1),
1656
+ relation: RelationTypeSchema2,
1657
+ target: z6.string().min(1)
1630
1658
  });
1631
- var GraphMetadataSchema = z5.object({
1632
- importance: z5.enum(["high", "medium", "low"]).optional(),
1633
- domain: z5.string().optional()
1659
+ var GraphMetadataSchema2 = z6.object({
1660
+ importance: z6.enum(["high", "medium", "low"]).optional(),
1661
+ domain: z6.string().optional()
1634
1662
  });
1635
1663
  var validateDateFormat = (dateStr) => {
1636
1664
  const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
@@ -1650,16 +1678,16 @@ var validateDateFormat = (dateStr) => {
1650
1678
  }
1651
1679
  return day <= daysInMonth[month - 1];
1652
1680
  };
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()
1681
+ var FrontmatterSchema = z6.object({
1682
+ created: z6.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
1683
+ updated: z6.string().refine(validateDateFormat, "Date must be in YYYY-MM-DD format"),
1684
+ status: z6.enum(["draft", "ongoing", "complete"]).optional(),
1685
+ topic: z6.string().optional(),
1686
+ tags: z6.array(z6.string()).optional(),
1687
+ summary: z6.string().optional(),
1688
+ entities: z6.array(EntitySchema2).optional(),
1689
+ relationships: z6.array(RelationshipSchema2).optional(),
1690
+ graph: GraphMetadataSchema2.optional()
1663
1691
  }).passthrough();
1664
1692
  function parseFrontmatter(content) {
1665
1693
  try {
@@ -1775,7 +1803,7 @@ class DocumentParserService {
1775
1803
  if (!fm?.graph) {
1776
1804
  return;
1777
1805
  }
1778
- const result = GraphMetadataSchema.safeParse(fm.graph);
1806
+ const result = GraphMetadataSchema2.safeParse(fm.graph);
1779
1807
  return result.success ? result.data : undefined;
1780
1808
  }
1781
1809
  computeHash(content) {
@@ -3197,13 +3225,244 @@ SqlCommand = __legacyDecorateClassTS([
3197
3225
  typeof GraphService === "undefined" ? Object : GraphService
3198
3226
  ])
3199
3227
  ], SqlCommand);
3228
+ // src/commands/question.command.ts
3229
+ import { Injectable as Injectable16 } from "@nestjs/common";
3230
+ import { Command as Command6, CommandRunner as CommandRunner6, Option as Option4 } from "nest-commander";
3231
+ function escapeSql(value) {
3232
+ return value.replace(/'/g, "''");
3233
+ }
3234
+
3235
+ class QuestionAddCommand extends CommandRunner6 {
3236
+ graphService;
3237
+ embeddingService;
3238
+ constructor(graphService, embeddingService) {
3239
+ super();
3240
+ this.graphService = graphService;
3241
+ this.embeddingService = embeddingService;
3242
+ }
3243
+ async run(inputs, options) {
3244
+ const questionText = inputs[0];
3245
+ if (!questionText) {
3246
+ console.error("Error: Question text is required");
3247
+ console.error('Usage: lattice question:add "your question"');
3248
+ process.exit(1);
3249
+ }
3250
+ try {
3251
+ await this.graphService.upsertNode("Question", {
3252
+ name: questionText,
3253
+ text: questionText,
3254
+ createdAt: new Date().toISOString()
3255
+ });
3256
+ const embedding = await this.embeddingService.generateEmbedding(`Question: ${questionText}`);
3257
+ await this.graphService.updateNodeEmbedding("Question", questionText, embedding);
3258
+ console.log(`
3259
+ \u2705 Added question: "${questionText}"`);
3260
+ if (options.answeredBy) {
3261
+ const docResult = await this.graphService.query(`
3262
+ SELECT name FROM nodes
3263
+ WHERE label = 'Document' AND name = '${escapeSql(options.answeredBy)}'
3264
+ `);
3265
+ if (docResult.resultSet.length === 0) {
3266
+ console.log(`
3267
+ \u26A0\uFE0F Document not found: ${options.answeredBy}`);
3268
+ console.log(" Run 'lattice sync' first to add documents to the graph.");
3269
+ console.log(` Question was created but not linked.
3270
+ `);
3271
+ process.exit(0);
3272
+ }
3273
+ await this.graphService.upsertRelationship("Question", questionText, "ANSWERED_BY", "Document", options.answeredBy, {});
3274
+ console.log(` Linked to: ${options.answeredBy}`);
3275
+ }
3276
+ console.log();
3277
+ process.exit(0);
3278
+ } catch (error) {
3279
+ console.error("Error:", error instanceof Error ? error.message : String(error));
3280
+ process.exit(1);
3281
+ }
3282
+ }
3283
+ parseAnsweredBy(value) {
3284
+ return value;
3285
+ }
3286
+ }
3287
+ __legacyDecorateClassTS([
3288
+ Option4({
3289
+ flags: "--answered-by <path>",
3290
+ description: "Immediately link to a document that answers this question"
3291
+ }),
3292
+ __legacyMetadataTS("design:type", Function),
3293
+ __legacyMetadataTS("design:paramtypes", [
3294
+ String
3295
+ ]),
3296
+ __legacyMetadataTS("design:returntype", String)
3297
+ ], QuestionAddCommand.prototype, "parseAnsweredBy", null);
3298
+ QuestionAddCommand = __legacyDecorateClassTS([
3299
+ Injectable16(),
3300
+ Command6({
3301
+ name: "question:add",
3302
+ arguments: "<question>",
3303
+ description: "Add a new question to the knowledge graph"
3304
+ }),
3305
+ __legacyMetadataTS("design:paramtypes", [
3306
+ typeof GraphService === "undefined" ? Object : GraphService,
3307
+ typeof EmbeddingService === "undefined" ? Object : EmbeddingService
3308
+ ])
3309
+ ], QuestionAddCommand);
3310
+
3311
+ class QuestionLinkCommand extends CommandRunner6 {
3312
+ graphService;
3313
+ embeddingService;
3314
+ constructor(graphService, embeddingService) {
3315
+ super();
3316
+ this.graphService = graphService;
3317
+ this.embeddingService = embeddingService;
3318
+ }
3319
+ async run(inputs, options) {
3320
+ const questionText = inputs[0];
3321
+ if (!questionText) {
3322
+ console.error("Error: Question text is required");
3323
+ console.error('Usage: lattice question:link "your question" --doc path/to/doc.md');
3324
+ process.exit(1);
3325
+ }
3326
+ if (!options.doc) {
3327
+ console.error("Error: --doc flag is required");
3328
+ console.error('Usage: lattice question:link "your question" --doc path/to/doc.md');
3329
+ process.exit(1);
3330
+ }
3331
+ try {
3332
+ const existingQuestions = await this.graphService.query(`
3333
+ SELECT name FROM nodes
3334
+ WHERE label = 'Question' AND name = '${escapeSql(questionText)}'
3335
+ `);
3336
+ if (existingQuestions.resultSet.length === 0) {
3337
+ await this.graphService.upsertNode("Question", {
3338
+ name: questionText,
3339
+ text: questionText,
3340
+ createdAt: new Date().toISOString()
3341
+ });
3342
+ const embedding = await this.embeddingService.generateEmbedding(`Question: ${questionText}`);
3343
+ await this.graphService.updateNodeEmbedding("Question", questionText, embedding);
3344
+ console.log(`
3345
+ \u2705 Created question: "${questionText}"`);
3346
+ }
3347
+ const existingDocs = await this.graphService.query(`
3348
+ SELECT name FROM nodes
3349
+ WHERE label = 'Document' AND name = '${escapeSql(options.doc)}'
3350
+ `);
3351
+ if (existingDocs.resultSet.length === 0) {
3352
+ console.error(`
3353
+ \u274C Document not found in graph: ${options.doc}`);
3354
+ console.error(` Run 'lattice sync' first to add documents to the graph.
3355
+ `);
3356
+ process.exit(1);
3357
+ }
3358
+ await this.graphService.upsertRelationship("Question", questionText, "ANSWERED_BY", "Document", options.doc, {});
3359
+ console.log(`
3360
+ \u2705 Linked: "${questionText}"`);
3361
+ console.log(` \u2192 ${options.doc}
3362
+ `);
3363
+ process.exit(0);
3364
+ } catch (error) {
3365
+ console.error("Error:", error instanceof Error ? error.message : String(error));
3366
+ process.exit(1);
3367
+ }
3368
+ }
3369
+ parseDoc(value) {
3370
+ return value;
3371
+ }
3372
+ }
3373
+ __legacyDecorateClassTS([
3374
+ Option4({
3375
+ flags: "-d, --doc <path>",
3376
+ description: "Path to the document that answers this question"
3377
+ }),
3378
+ __legacyMetadataTS("design:type", Function),
3379
+ __legacyMetadataTS("design:paramtypes", [
3380
+ String
3381
+ ]),
3382
+ __legacyMetadataTS("design:returntype", String)
3383
+ ], QuestionLinkCommand.prototype, "parseDoc", null);
3384
+ QuestionLinkCommand = __legacyDecorateClassTS([
3385
+ Injectable16(),
3386
+ Command6({
3387
+ name: "question:link",
3388
+ arguments: "<question>",
3389
+ description: "Link a question to a document via ANSWERED_BY relationship"
3390
+ }),
3391
+ __legacyMetadataTS("design:paramtypes", [
3392
+ typeof GraphService === "undefined" ? Object : GraphService,
3393
+ typeof EmbeddingService === "undefined" ? Object : EmbeddingService
3394
+ ])
3395
+ ], QuestionLinkCommand);
3396
+
3397
+ class QuestionUnansweredCommand extends CommandRunner6 {
3398
+ graphService;
3399
+ constructor(graphService) {
3400
+ super();
3401
+ this.graphService = graphService;
3402
+ }
3403
+ async run() {
3404
+ try {
3405
+ const result = await this.graphService.query(`
3406
+ SELECT
3407
+ q.name as question,
3408
+ q.properties->>'createdAt' as created_at,
3409
+ q.created_at as db_created_at
3410
+ FROM nodes q
3411
+ WHERE q.label = 'Question'
3412
+ AND NOT EXISTS (
3413
+ SELECT 1 FROM relationships r
3414
+ WHERE r.source_label = 'Question'
3415
+ AND r.source_name = q.name
3416
+ AND r.relation_type = 'ANSWERED_BY'
3417
+ )
3418
+ ORDER BY COALESCE(q.properties->>'createdAt', q.created_at::VARCHAR) DESC
3419
+ `);
3420
+ console.log(`
3421
+ === Unanswered Questions ===
3422
+ `);
3423
+ if (result.resultSet.length === 0) {
3424
+ console.log(`No unanswered questions found.
3425
+ `);
3426
+ console.log(`Add questions with: lattice question:add "your question"
3427
+ `);
3428
+ process.exit(0);
3429
+ }
3430
+ result.resultSet.forEach((row, idx) => {
3431
+ const [question, createdAt, dbCreatedAt] = row;
3432
+ const displayDate = createdAt || dbCreatedAt || "unknown";
3433
+ console.log(`${idx + 1}. ${question}`);
3434
+ console.log(` Created: ${displayDate}`);
3435
+ });
3436
+ console.log(`
3437
+ Total: ${result.resultSet.length} unanswered question(s)
3438
+ `);
3439
+ console.log("To link a question to an answer:");
3440
+ console.log(` lattice question:link "question text" --doc path/to/doc.md
3441
+ `);
3442
+ process.exit(0);
3443
+ } catch (error) {
3444
+ console.error("Error:", error instanceof Error ? error.message : String(error));
3445
+ process.exit(1);
3446
+ }
3447
+ }
3448
+ }
3449
+ QuestionUnansweredCommand = __legacyDecorateClassTS([
3450
+ Injectable16(),
3451
+ Command6({
3452
+ name: "question:unanswered",
3453
+ description: "List all questions without ANSWERED_BY relationships"
3454
+ }),
3455
+ __legacyMetadataTS("design:paramtypes", [
3456
+ typeof GraphService === "undefined" ? Object : GraphService
3457
+ ])
3458
+ ], QuestionUnansweredCommand);
3200
3459
  // src/commands/site.command.ts
3201
3460
  import { spawn } from "child_process";
3202
3461
  import { existsSync as existsSync6, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
3203
3462
  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 {
3463
+ import { Injectable as Injectable17 } from "@nestjs/common";
3464
+ import { Command as Command7, CommandRunner as CommandRunner7, Option as Option5 } from "nest-commander";
3465
+ class SiteCommand extends CommandRunner7 {
3207
3466
  getPidFile() {
3208
3467
  return path2.join(getLatticeHome(), "site.pid");
3209
3468
  }
@@ -3336,7 +3595,7 @@ class SiteCommand extends CommandRunner6 {
3336
3595
  }
3337
3596
  unlinkSync(pidFile);
3338
3597
  console.log(`\u2705 Killed Lattice site process (PID: ${pid})`);
3339
- } catch (err) {
3598
+ } catch (_err) {
3340
3599
  try {
3341
3600
  unlinkSync(pidFile);
3342
3601
  } catch {}
@@ -3357,7 +3616,7 @@ class SiteCommand extends CommandRunner6 {
3357
3616
  }
3358
3617
  }
3359
3618
  __legacyDecorateClassTS([
3360
- Option4({
3619
+ Option5({
3361
3620
  flags: "-b, --build",
3362
3621
  description: "Build the site without starting the server"
3363
3622
  }),
@@ -3366,7 +3625,7 @@ __legacyDecorateClassTS([
3366
3625
  __legacyMetadataTS("design:returntype", Boolean)
3367
3626
  ], SiteCommand.prototype, "parseBuild", null);
3368
3627
  __legacyDecorateClassTS([
3369
- Option4({
3628
+ Option5({
3370
3629
  flags: "-d, --dev",
3371
3630
  description: "Run in development mode (hot reload, no search)"
3372
3631
  }),
@@ -3375,7 +3634,7 @@ __legacyDecorateClassTS([
3375
3634
  __legacyMetadataTS("design:returntype", Boolean)
3376
3635
  ], SiteCommand.prototype, "parseDev", null);
3377
3636
  __legacyDecorateClassTS([
3378
- Option4({
3637
+ Option5({
3379
3638
  flags: "-p, --port <port>",
3380
3639
  description: "Port to run the server on (default: 4321)"
3381
3640
  }),
@@ -3386,7 +3645,7 @@ __legacyDecorateClassTS([
3386
3645
  __legacyMetadataTS("design:returntype", String)
3387
3646
  ], SiteCommand.prototype, "parsePort", null);
3388
3647
  __legacyDecorateClassTS([
3389
- Option4({
3648
+ Option5({
3390
3649
  flags: "-k, --kill",
3391
3650
  description: "Kill the running Lattice site process"
3392
3651
  }),
@@ -3395,16 +3654,16 @@ __legacyDecorateClassTS([
3395
3654
  __legacyMetadataTS("design:returntype", Boolean)
3396
3655
  ], SiteCommand.prototype, "parseKill", null);
3397
3656
  SiteCommand = __legacyDecorateClassTS([
3398
- Injectable16(),
3399
- Command6({
3657
+ Injectable17(),
3658
+ Command7({
3400
3659
  name: "site",
3401
3660
  description: "Build and run the Lattice documentation site"
3402
3661
  })
3403
3662
  ], SiteCommand);
3404
3663
  // 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 {
3664
+ import { Injectable as Injectable18 } from "@nestjs/common";
3665
+ import { Command as Command8, CommandRunner as CommandRunner8, Option as Option6 } from "nest-commander";
3666
+ class StatusCommand extends CommandRunner8 {
3408
3667
  syncService;
3409
3668
  dbChangeDetector;
3410
3669
  constructor(syncService, dbChangeDetector) {
@@ -3470,7 +3729,7 @@ class StatusCommand extends CommandRunner7 {
3470
3729
  }
3471
3730
  }
3472
3731
  __legacyDecorateClassTS([
3473
- Option5({
3732
+ Option6({
3474
3733
  flags: "-v, --verbose",
3475
3734
  description: "Show all documents including unchanged"
3476
3735
  }),
@@ -3479,8 +3738,8 @@ __legacyDecorateClassTS([
3479
3738
  __legacyMetadataTS("design:returntype", Boolean)
3480
3739
  ], StatusCommand.prototype, "parseVerbose", null);
3481
3740
  StatusCommand = __legacyDecorateClassTS([
3482
- Injectable17(),
3483
- Command7({
3741
+ Injectable18(),
3742
+ Command8({
3484
3743
  name: "status",
3485
3744
  description: "Show documents that need syncing (new or updated)"
3486
3745
  }),
@@ -3492,11 +3751,11 @@ StatusCommand = __legacyDecorateClassTS([
3492
3751
  // src/commands/sync.command.ts
3493
3752
  import { watch } from "fs";
3494
3753
  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";
3754
+ import { Injectable as Injectable20 } from "@nestjs/common";
3755
+ import { Command as Command9, CommandRunner as CommandRunner9, Option as Option7 } from "nest-commander";
3497
3756
 
3498
3757
  // src/sync/graph-validator.service.ts
3499
- import { Injectable as Injectable18, Logger as Logger9 } from "@nestjs/common";
3758
+ import { Injectable as Injectable19, Logger as Logger9 } from "@nestjs/common";
3500
3759
  class GraphValidatorService {
3501
3760
  graph;
3502
3761
  logger = new Logger9(GraphValidatorService.name);
@@ -3645,14 +3904,14 @@ class GraphValidatorService {
3645
3904
  }
3646
3905
  }
3647
3906
  GraphValidatorService = __legacyDecorateClassTS([
3648
- Injectable18(),
3907
+ Injectable19(),
3649
3908
  __legacyMetadataTS("design:paramtypes", [
3650
3909
  typeof GraphService === "undefined" ? Object : GraphService
3651
3910
  ])
3652
3911
  ], GraphValidatorService);
3653
3912
 
3654
3913
  // src/commands/sync.command.ts
3655
- class SyncCommand extends CommandRunner8 {
3914
+ class SyncCommand extends CommandRunner9 {
3656
3915
  syncService;
3657
3916
  graphService;
3658
3917
  _graphValidator;
@@ -3667,7 +3926,7 @@ class SyncCommand extends CommandRunner8 {
3667
3926
  async safeExit(code) {
3668
3927
  try {
3669
3928
  await this.graphService.checkpoint();
3670
- } catch (error) {
3929
+ } catch (_error) {
3671
3930
  console.error("Warning: checkpoint failed during exit");
3672
3931
  }
3673
3932
  process.exit(code);
@@ -3925,7 +4184,7 @@ class SyncCommand extends CommandRunner8 {
3925
4184
  }
3926
4185
  }
3927
4186
  __legacyDecorateClassTS([
3928
- Option6({
4187
+ Option7({
3929
4188
  flags: "-f, --force",
3930
4189
  description: "Force re-sync specified documents (requires paths to be specified)"
3931
4190
  }),
@@ -3934,7 +4193,7 @@ __legacyDecorateClassTS([
3934
4193
  __legacyMetadataTS("design:returntype", Boolean)
3935
4194
  ], SyncCommand.prototype, "parseForce", null);
3936
4195
  __legacyDecorateClassTS([
3937
- Option6({
4196
+ Option7({
3938
4197
  flags: "-d, --dry-run",
3939
4198
  description: "Show what would change without applying"
3940
4199
  }),
@@ -3943,7 +4202,7 @@ __legacyDecorateClassTS([
3943
4202
  __legacyMetadataTS("design:returntype", Boolean)
3944
4203
  ], SyncCommand.prototype, "parseDryRun", null);
3945
4204
  __legacyDecorateClassTS([
3946
- Option6({
4205
+ Option7({
3947
4206
  flags: "-v, --verbose",
3948
4207
  description: "Show detailed output"
3949
4208
  }),
@@ -3952,7 +4211,7 @@ __legacyDecorateClassTS([
3952
4211
  __legacyMetadataTS("design:returntype", Boolean)
3953
4212
  ], SyncCommand.prototype, "parseVerbose", null);
3954
4213
  __legacyDecorateClassTS([
3955
- Option6({
4214
+ Option7({
3956
4215
  flags: "-w, --watch",
3957
4216
  description: "Watch for file changes and sync automatically"
3958
4217
  }),
@@ -3961,7 +4220,7 @@ __legacyDecorateClassTS([
3961
4220
  __legacyMetadataTS("design:returntype", Boolean)
3962
4221
  ], SyncCommand.prototype, "parseWatch", null);
3963
4222
  __legacyDecorateClassTS([
3964
- Option6({
4223
+ Option7({
3965
4224
  flags: "--diff",
3966
4225
  description: "Show only changed documents (alias for --dry-run)"
3967
4226
  }),
@@ -3970,7 +4229,7 @@ __legacyDecorateClassTS([
3970
4229
  __legacyMetadataTS("design:returntype", Boolean)
3971
4230
  ], SyncCommand.prototype, "parseDiff", null);
3972
4231
  __legacyDecorateClassTS([
3973
- Option6({
4232
+ Option7({
3974
4233
  flags: "--skip-cascade",
3975
4234
  description: "Skip cascade analysis (faster for large repos)"
3976
4235
  }),
@@ -3979,7 +4238,7 @@ __legacyDecorateClassTS([
3979
4238
  __legacyMetadataTS("design:returntype", Boolean)
3980
4239
  ], SyncCommand.prototype, "parseSkipCascade", null);
3981
4240
  __legacyDecorateClassTS([
3982
- Option6({
4241
+ Option7({
3983
4242
  flags: "--no-embeddings",
3984
4243
  description: "Disable embedding generation during sync"
3985
4244
  }),
@@ -3988,7 +4247,7 @@ __legacyDecorateClassTS([
3988
4247
  __legacyMetadataTS("design:returntype", Boolean)
3989
4248
  ], SyncCommand.prototype, "parseNoEmbeddings", null);
3990
4249
  __legacyDecorateClassTS([
3991
- Option6({
4250
+ Option7({
3992
4251
  flags: "--skip-extraction",
3993
4252
  description: "Skip AI entity extraction (sync without re-extracting entities)"
3994
4253
  }),
@@ -3997,8 +4256,8 @@ __legacyDecorateClassTS([
3997
4256
  __legacyMetadataTS("design:returntype", Boolean)
3998
4257
  ], SyncCommand.prototype, "parseSkipExtraction", null);
3999
4258
  SyncCommand = __legacyDecorateClassTS([
4000
- Injectable19(),
4001
- Command8({
4259
+ Injectable20(),
4260
+ Command9({
4002
4261
  name: "sync",
4003
4262
  arguments: "[paths...]",
4004
4263
  description: "Synchronize documents to the knowledge graph"
@@ -4037,7 +4296,7 @@ GraphModule = __legacyDecorateClassTS([
4037
4296
  import { Module as Module3 } from "@nestjs/common";
4038
4297
 
4039
4298
  // src/query/query.service.ts
4040
- import { Injectable as Injectable20, Logger as Logger10 } from "@nestjs/common";
4299
+ import { Injectable as Injectable21, Logger as Logger10 } from "@nestjs/common";
4041
4300
  class QueryService {
4042
4301
  graphService;
4043
4302
  logger = new Logger10(QueryService.name);
@@ -4050,7 +4309,7 @@ class QueryService {
4050
4309
  }
4051
4310
  }
4052
4311
  QueryService = __legacyDecorateClassTS([
4053
- Injectable20(),
4312
+ Injectable21(),
4054
4313
  __legacyMetadataTS("design:paramtypes", [
4055
4314
  typeof GraphService === "undefined" ? Object : GraphService
4056
4315
  ])
@@ -4124,7 +4383,10 @@ AppModule = __legacyDecorateClassTS([
4124
4383
  OntologyCommand,
4125
4384
  InitCommand,
4126
4385
  MigrateCommand,
4127
- SiteCommand
4386
+ SiteCommand,
4387
+ QuestionAddCommand,
4388
+ QuestionLinkCommand,
4389
+ QuestionUnansweredCommand
4128
4390
  ]
4129
4391
  })
4130
4392
  ], 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.0",
4
4
  "description": "Human-initiated, AI-powered knowledge graph for markdown documentation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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