@monoes/monograph 1.5.0 → 1.5.2

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 (67) hide show
  1. package/.monodesign/hook.cache.json +1 -0
  2. package/__tests__/storage/stores.test.ts +66 -1
  3. package/dist/src/analysis/cycles.d.ts.map +1 -1
  4. package/dist/src/analysis/cycles.js +5 -3
  5. package/dist/src/analysis/cycles.js.map +1 -1
  6. package/dist/src/analysis/trace.d.ts.map +1 -1
  7. package/dist/src/analysis/trace.js +1 -19
  8. package/dist/src/analysis/trace.js.map +1 -1
  9. package/dist/src/analysis/worker-pool.d.ts.map +1 -1
  10. package/dist/src/analysis/worker-pool.js +9 -7
  11. package/dist/src/analysis/worker-pool.js.map +1 -1
  12. package/dist/src/cli/skill-gen.d.ts.map +1 -1
  13. package/dist/src/cli/skill-gen.js +16 -25
  14. package/dist/src/cli/skill-gen.js.map +1 -1
  15. package/dist/src/export/html.d.ts.map +1 -1
  16. package/dist/src/export/html.js +3 -2
  17. package/dist/src/export/html.js.map +1 -1
  18. package/dist/src/graph/regex-search.d.ts.map +1 -1
  19. package/dist/src/graph/regex-search.js +9 -3
  20. package/dist/src/graph/regex-search.js.map +1 -1
  21. package/dist/src/pipeline/phases/call-site-extractors.d.ts +17 -0
  22. package/dist/src/pipeline/phases/call-site-extractors.d.ts.map +1 -0
  23. package/dist/src/pipeline/phases/call-site-extractors.js +132 -0
  24. package/dist/src/pipeline/phases/call-site-extractors.js.map +1 -0
  25. package/dist/src/pipeline/phases/module-resolution.d.ts +10 -0
  26. package/dist/src/pipeline/phases/module-resolution.d.ts.map +1 -0
  27. package/dist/src/pipeline/phases/module-resolution.js +301 -0
  28. package/dist/src/pipeline/phases/module-resolution.js.map +1 -0
  29. package/dist/src/pipeline/phases/orm.js +1 -2
  30. package/dist/src/pipeline/phases/orm.js.map +1 -1
  31. package/dist/src/pipeline/phases/scope-resolution.d.ts +2 -25
  32. package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
  33. package/dist/src/pipeline/phases/scope-resolution.js +18 -580
  34. package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
  35. package/dist/src/pipeline/phases/wildcard-phase.d.ts.map +1 -1
  36. package/dist/src/pipeline/phases/wildcard-phase.js +6 -1
  37. package/dist/src/pipeline/phases/wildcard-phase.js.map +1 -1
  38. package/dist/src/search/hybrid-query.js +1 -1
  39. package/dist/src/search/hybrid-query.js.map +1 -1
  40. package/dist/src/storage/fts-store.d.ts.map +1 -1
  41. package/dist/src/storage/fts-store.js +7 -4
  42. package/dist/src/storage/fts-store.js.map +1 -1
  43. package/dist/src/watch/watcher.d.ts +2 -0
  44. package/dist/src/watch/watcher.d.ts.map +1 -1
  45. package/dist/src/watch/watcher.js +23 -1
  46. package/dist/src/watch/watcher.js.map +1 -1
  47. package/dist/src/web/api.d.ts.map +1 -1
  48. package/dist/src/web/api.js +17 -14
  49. package/dist/src/web/api.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/src/__tests__/pipeline/phases/wildcard-phase.test.ts +39 -4
  53. package/src/analysis/cycles.ts +7 -3
  54. package/src/analysis/trace.ts +1 -21
  55. package/src/analysis/worker-pool.ts +8 -7
  56. package/src/cli/skill-gen.ts +6 -14
  57. package/src/export/html.ts +3 -2
  58. package/src/graph/regex-search.ts +9 -5
  59. package/src/pipeline/phases/call-site-extractors.ts +169 -0
  60. package/src/pipeline/phases/module-resolution.ts +294 -0
  61. package/src/pipeline/phases/orm.ts +1 -2
  62. package/src/pipeline/phases/scope-resolution.ts +25 -618
  63. package/src/pipeline/phases/wildcard-phase.ts +5 -1
  64. package/src/search/hybrid-query.ts +1 -1
  65. package/src/storage/fts-store.ts +7 -3
  66. package/src/watch/watcher.ts +23 -1
  67. package/src/web/api.ts +17 -13
@@ -32,7 +32,11 @@ export const wildcardSynthesisPhase: PipelinePhase<WildcardSynthesisOutput> = {
32
32
  `);
33
33
 
34
34
  for (const [filePath, source] of fileContents) {
35
- const fileNodeId = fileNodeIndex.get(filePath) ?? `file:${filePath}`;
35
+ const fileNodeId = fileNodeIndex.get(filePath);
36
+ // No real node for this file (e.g. no parsed symbols) — there's no
37
+ // valid row to attach a source_id to, and fabricating one violates the
38
+ // edges table's FOREIGN KEY constraint on source_id (issue #40).
39
+ if (!fileNodeId) continue;
36
40
  const { synthesizedEdges } = synthesizeWildcardImports(fileNodeId, source, allKnownNodes, allKnownEdges);
37
41
 
38
42
  for (const edge of synthesizedEdges) {
@@ -48,7 +48,7 @@ export async function hybridQuery(
48
48
  const normalizedQuery = normalizeSearchTerm(query);
49
49
 
50
50
  // ── BM25 via FTS5 ──────────────────────────────────────────────────────────
51
- const bm25Limit = 50;
51
+ const bm25Limit = Math.max(limit, 50);
52
52
  const bm25Raw = ftsSearch(db, normalizedQuery, bm25Limit, label);
53
53
  const bm25Results: RankedResult[] = bm25Raw.map((r) => ({
54
54
  id: r.id,
@@ -24,10 +24,14 @@ const STOPWORDS = new Set([
24
24
  */
25
25
  export function extractSearchTerms(query: string): string[] {
26
26
  const words = query.split(/[\s,;:!?()\[\]{}'"]+/).filter(Boolean);
27
- // If query is a single token or all tokens are code-like, it's already an identifier query
27
+ // If query is a single token, it's already a bare identifier query — nothing to extract.
28
+ // (We used to also bail out for short, stopword-free multi-token queries under the
29
+ // assumption they were "already an identifier query", but that misfires for queries
30
+ // like "ExtensionBridge keepalive reconnect" — three distinct identifier-ish tokens
31
+ // that FTS5 MATCH ANDs together and fails to find as a single row. Since this function
32
+ // only runs as a fallback after the raw MATCH already returned zero rows, there's no
33
+ // extra cost to always extracting terms for multi-token queries.)
28
34
  if (words.length <= 1) return [];
29
- const hasStopword = words.some(w => STOPWORDS.has(w.toLowerCase()));
30
- if (!hasStopword && words.length <= 3) return [];
31
35
 
32
36
  const terms = new Set<string>();
33
37
  for (const word of words) {
@@ -13,6 +13,8 @@ export interface WatchAsyncOptions extends WatcherOptions {
13
13
  force?: boolean;
14
14
  codeOnly?: boolean;
15
15
  llmMaxSections?: number;
16
+ /** Auto-stop after this many ms of no file changes. Default 30min. 0 = never. */
17
+ idleTimeoutMs?: number;
16
18
  }
17
19
 
18
20
  /** Convenience: start a watcher and trigger buildAsync on every change. Returns stop() fn. */
@@ -23,11 +25,25 @@ export async function watchAsync(
23
25
  const { buildAsync } = await import('../pipeline/orchestrator.js');
24
26
  const watcher = new MonographWatcher(repoPath, { debounceMs: opts.debounceMs ?? 3000 });
25
27
 
28
+ // Idle timeout: auto-stop after prolonged inactivity to reclaim resources.
29
+ const idleMs = opts.idleTimeoutMs ?? 30 * 60_000; // default 30min
30
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
31
+ const resetIdle = (): void => {
32
+ if (idleMs <= 0) return;
33
+ if (idleTimer) clearTimeout(idleTimer);
34
+ idleTimer = setTimeout(() => {
35
+ opts.onProgress?.({ phase: 'watch', message: `No changes for ${Math.round(idleMs / 60_000)}min — auto-stopping watcher.` });
36
+ watcher.stop().catch(() => {});
37
+ }, idleMs);
38
+ (idleTimer as { unref?: () => void }).unref?.();
39
+ };
40
+
26
41
  // monolean: full rebuild per change-batch, serialized — true incremental rebuild
27
42
  // (re-parse only changed files) requires restructuring the phase pipeline.
28
43
  let building = false;
29
44
  let rerun = false;
30
45
  watcher.on('monograph:updated', async (files: string[]) => {
46
+ resetIdle();
31
47
  if (building) { rerun = true; return; } // coalesce saves that land mid-build
32
48
  building = true;
33
49
  try {
@@ -52,7 +68,13 @@ export async function watchAsync(
52
68
  });
53
69
 
54
70
  await watcher.start();
55
- return { stop: () => watcher.stop() };
71
+ resetIdle(); // start the idle clock
72
+ return {
73
+ stop: async () => {
74
+ if (idleTimer) clearTimeout(idleTimer);
75
+ await watcher.stop();
76
+ },
77
+ };
56
78
  }
57
79
 
58
80
  export class MonographWatcher extends EventEmitter {
package/src/web/api.ts CHANGED
@@ -95,23 +95,27 @@ export function queryGraphData(db: Database.Database): GraphData {
95
95
 
96
96
  const edges: ApiEdge[] = [];
97
97
  if (nodeIds.size > 0) {
98
- // Scan edges and keep those where at least one endpoint is a visible node
98
+ // Use a temp table so SQLite filters edges instead of scanning full table in JS
99
+ db.exec('CREATE TEMP TABLE IF NOT EXISTS _vis_nodes (id TEXT PRIMARY KEY)');
100
+ db.exec('DELETE FROM _vis_nodes');
101
+ const insertVis = db.prepare('INSERT OR IGNORE INTO _vis_nodes (id) VALUES (?)');
102
+ const insertAll = db.transaction((ids: string[]) => { for (const id of ids) insertVis.run(id); });
103
+ insertAll([...nodeIds]);
104
+
99
105
  const edgeRows = db
100
- .prepare('SELECT source_id, target_id, relation, confidence_score FROM edges')
106
+ .prepare(`SELECT e.source_id, e.target_id, e.relation, e.confidence_score FROM edges e
107
+ JOIN _vis_nodes s ON e.source_id = s.id
108
+ JOIN _vis_nodes t ON e.target_id = t.id
109
+ LIMIT 10000`)
101
110
  .all() as Record<string, unknown>[];
102
111
 
103
112
  for (const r of edgeRows) {
104
- const src = r['source_id'] as string;
105
- const tgt = r['target_id'] as string;
106
- if (nodeIds.has(src) && nodeIds.has(tgt)) {
107
- edges.push({
108
- sourceId: src,
109
- targetId: tgt,
110
- relation: r['relation'] as string,
111
- confidenceScore: r['confidence_score'] as number,
112
- });
113
- if (edges.length >= 10000) break;
114
- }
113
+ edges.push({
114
+ sourceId: r['source_id'] as string,
115
+ targetId: r['target_id'] as string,
116
+ relation: r['relation'] as string,
117
+ confidenceScore: r['confidence_score'] as number,
118
+ });
115
119
  }
116
120
  }
117
121