memorix 1.1.8 → 1.1.10

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.
@@ -103,6 +103,8 @@ MCP:
103
103
 
104
104
  `memorix context` defaults to `--refresh auto`, so first use can seed CodeGraph Memory without a separate manual `memorix codegraph refresh`. Its brief puts live package/changelog/Git facts before memory hints and flags old `progress.txt` / dev-log notes as historical when they predate the latest changelog, so agents should treat current facts as the source of truth when files disagree. Task lenses keep the packet shaped to the work: bugfix briefs prefer failing tests and repros, release briefs prefer metadata/changelog/package checks, and onboarding briefs prefer docs and entry points while hiding unrelated suspect details. Use `--refresh never` for read-only inspection and `--refresh always` when you want to force a fresh scan.
105
105
 
106
+ Project-specific generated, vendored, or cache paths can be excluded from CodeGraph Memory with `[codegraph].exclude_patterns` in `memorix.toml` or `~/.memorix/config.toml` (`codegraph.excludePatterns` in legacy YAML). User patterns extend the built-in excludes and are applied to indexing, Project Context suggested reads, and Context Pack suggested reads.
107
+
106
108
  SessionStart hooks keep the default minimal hint lightweight. When memory behavior is configured with `sessionInject=full`, Memorix injects the compact Memory Autopilot brief at session start instead of only listing recent text memories.
107
109
 
108
110
  The intended loop for agents is: get the project brief when it helps, inspect the suggested current files, use stale or unbound memory only as a lead, store durable outcomes after the work changes the project, and resolve obsolete memories.
@@ -60,6 +60,9 @@ skip_merge_commits = true
60
60
  exclude_patterns = ["*.lock", "dist/**"]
61
61
  noise_keywords = ["format", "typo"]
62
62
 
63
+ [codegraph]
64
+ exclude_patterns = ["vendor/**", "third_party/**", "generated/**"]
65
+
63
66
  [server]
64
67
  transport = "stdio"
65
68
  dashboard = true
@@ -197,6 +200,21 @@ Project identity is still resolved from the real `.git` root. A project
197
200
  `memorix.toml` is an override file under that root; it does not create or rename
198
201
  the Memorix project ID.
199
202
 
203
+ ### `[codegraph]`
204
+
205
+ CodeGraph Memory and Project Context path filtering.
206
+
207
+ Common keys:
208
+
209
+ - `exclude_patterns = ["vendor/**", "third_party/**", "generated/**"]`
210
+
211
+ Legacy YAML uses `codegraph.excludePatterns` for the same setting.
212
+
213
+ These patterns extend Memorix's built-in CodeGraph excludes (`node_modules`,
214
+ build outputs, worktrees, and similar generated directories). Matching paths are
215
+ skipped during CodeGraph indexing and hidden from Project Context / Context Pack
216
+ suggested reads.
217
+
200
218
  ### `[server]`
201
219
 
202
220
  Server and dashboard behavior.
@@ -4,48 +4,35 @@
4
4
  > exist under subdirectories; prefer this file for repository-wide state.
5
5
 
6
6
  ## Current State
7
- - Phase: 1.1.8 release finalization
8
- - Branch: main
9
- - PR: #118 merged
10
- - Last updated: 2026-07-08
11
- - Goal: publish 1.1.8 after committing the final Claude local MCP repair and CodeGraph noise-control patch.
7
+ - Phase: 1.1.10 emergency patch release
8
+ - Branch: codex/issue-124-project-context-timeout
9
+ - Issue: #124
10
+ - Last updated: 2026-07-13
11
+ - Goal: release the large-project `memorix_project_context` timeout fix and follow up with the reporter.
12
12
 
13
13
  ## Completed Recently
14
- - Merged PR #118: `fix(setup): make Memory Autopilot self-checking`.
15
- - Added `memorix doctor agents` and `memorix repair agents` so users can detect and repair stale agent MCP/rules setup.
16
- - Strengthened generated agent guidance so `memorix_project_context` is the default first step for non-trivial coding work.
17
- - Verified main at `8567f5e` after merge; GitHub CI passed before merge.
18
- - Confirmed local OpenRouter embedding lane resolves to `qwen/qwen3-embedding-8b` without printing secrets.
19
- - Fixed Claude Code 2.x local MCP repair: doctor/repair now inspect `~/.claude.json` project-private entries and rewrite stale worktree commands to `memorix serve` with `alwaysLoad: true`.
20
- - Fixed all-scope doctor noise: one healthy local, project, or global scope is enough in all-scope mode unless a stale actionable config is present.
21
- - Fixed CodeGraph Memory noise: `.tmp`, `.worktrees`, and `.claude/worktrees` are ignored by the lite scan and Autopilot suggested reads.
22
- - Bumped root and workspace package versions to `1.1.8` and updated lockfile/internal workspace dependencies.
14
+ - Reproduced #124 with 3308 active memories and confirmed the old refresh path exceeded the MCP 60-second timeout.
15
+ - Replaced broad per-observation symbol scans with indexed exact-name lookup and bulk existing-ref reads.
16
+ - Changed first-use backfill to load one project graph snapshot, match all missing observations in memory, and persist refs in one transaction.
17
+ - Reused one graph snapshot per project-context request and batched project refs/referenced symbols.
18
+ - Prevented prose-only and ambiguous symbol mentions from creating large volumes of noisy code refs.
19
+ - Preserved Ruby namespace and punctuated method names through Lite indexing and both binding paths.
20
+ - Bumped root and workspace metadata to `1.1.10`.
23
21
 
24
22
  ## Current Follow-Up
25
- - The release patch is implemented and verified locally, but not committed or pushed yet.
26
- - Two unrelated local changes were temporarily stashed before final verification so they do not enter the release package: `packages/ai/src/models.generated.ts` and `packages/memcode/docs/images/exy.png`.
23
+ - Commit, publish `memorix@1.1.10`, create the GitHub release, and reply to #124 with the upgrade path.
27
24
 
28
25
  ## Verification Snapshot
29
- - `git diff --check`
30
- - `npm test -- tests/cli/agent-doctor.test.ts tests/codegraph/lite-provider.test.ts tests/codegraph/project-context.test.ts tests/codegraph/context-pack.test.ts`
31
- - `npm run lint`
32
- - `npm run build`
33
- - `npm test`
34
- - `npm pack --dry-run --json` -> `memorix@1.1.8`, about 14.1 MB packed / 60.3 MB unpacked
35
- - `node dist/cli/index.js doctor agents --agent claude --scope local`
36
- - `node dist/cli/index.js doctor agents --agent claude --scope all`
37
- - `node dist/cli/index.js codegraph refresh --json`
38
- - `node dist/cli/index.js context --refresh never --task "准备发布 1.1.8:判断当前进度、风险和下一步"`
39
- - `claude mcp get memorix`
40
- - `claude mcp list`
41
- - Claude Code user-style smoke: a short print-mode handoff correctly blocked release because local changes were not committed yet; longer print-mode smoke can still time out.
26
+ - TDD regression coverage passes for CodeGraph store, binder, project context, and auto context.
27
+ - A copied real database with all refs removed improved from 57.9 seconds for 173 active memories to 12.4 seconds.
28
+ - A scaled 3308-active first-use refresh completed in 13.3 seconds, including a forced code scan and full code-ref backfill.
29
+ - Built stdio MCP smoke returned `memorix_project_context` successfully in 5.79 seconds.
30
+ - Final release verification: `git diff --check`, `npm run lint`, `npm run build`, `npm test`, package dry-run, and clean-install npm smoke.
42
31
 
43
32
  ## Known UX Notes
44
- - Claude Code print-mode can be slow or fail to finish under budget during full user-style smoke. Use tool traces plus deterministic CLI/MCP smoke to distinguish product failures from model/runtime drift.
45
- - `memorix context --refresh never --task "<task>"` is fast and stable for first-contact context. `--refresh auto` can take longer on this repository.
33
+ - Before 1.1.10, `refresh: "never"` is a temporary workaround for very large active-memory corpora.
46
34
  - Current code and release facts outrank stale handoff notes or older dev logs when they conflict.
47
35
 
48
36
  ## Next Steps
49
- - Commit and push the 1.1.8 release patch.
50
- - Publish `memorix@1.1.8` to npm, then tag and push `v1.1.8`.
51
- - Restore the temporary local stash after publishing if those local model/image changes are still needed.
37
+ - Commit and push the 1.1.10 release patch.
38
+ - Tag, publish the root package, create the GitHub release, and reply to #124.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -3,6 +3,7 @@ import { CodeGraphStore } from '../../codegraph/store.js';
3
3
  import { indexProjectLite } from '../../codegraph/lite-provider.js';
4
4
  import { assembleContextPackForTask, buildContextPackPrompt } from '../../codegraph/context-pack.js';
5
5
  import { backfillMissingObservationCodeRefs } from '../../codegraph/binder.js';
6
+ import { getResolvedConfig } from '../../config/resolved-config.js';
6
7
  import { getAllObservations } from '../../memory/observations.js';
7
8
  import { emitError, emitResult, getCliProjectContext, parsePositiveInt } from './operator-shared.js';
8
9
 
@@ -49,6 +50,7 @@ export default defineCommand({
49
50
  const store = new CodeGraphStore();
50
51
  await store.init(dataDir);
51
52
  const explicitAction = Boolean(positional[0] || (args.action as string | undefined));
53
+ const exclude = getResolvedConfig({ projectRoot: project.rootPath }).codegraph.excludePatterns;
52
54
 
53
55
  switch (action) {
54
56
  case 'status': {
@@ -64,6 +66,7 @@ export default defineCommand({
64
66
  const indexed = await indexProjectLite({
65
67
  projectId: project.id,
66
68
  projectRoot: project.rootPath,
69
+ exclude,
67
70
  });
68
71
  store.replaceProjectIndex(project.id, indexed);
69
72
  const activeObservations = getAllObservations()
@@ -99,6 +102,7 @@ export default defineCommand({
99
102
  task,
100
103
  observations,
101
104
  limit,
105
+ exclude,
102
106
  });
103
107
  emitResult({ project, pack }, buildContextPackPrompt(pack), asJson);
104
108
  return;
@@ -29,12 +29,19 @@ export default defineCommand({
29
29
  function readDotted(source: Record<string, unknown>, key: string): unknown {
30
30
  let cursor: unknown = source;
31
31
  for (const part of key.split('.')) {
32
- if (!cursor || typeof cursor !== 'object' || !(part in cursor)) return undefined;
33
- cursor = (cursor as Record<string, unknown>)[part];
32
+ if (!cursor || typeof cursor !== 'object') return undefined;
33
+ const record = cursor as Record<string, unknown>;
34
+ const normalizedPart = part in record ? part : snakeToCamel(part);
35
+ if (!(normalizedPart in record)) return undefined;
36
+ cursor = record[normalizedPart];
34
37
  }
35
38
  return cursor;
36
39
  }
37
40
 
41
+ function snakeToCamel(value: string): string {
42
+ return value.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase());
43
+ }
44
+
38
45
  function formatValue(value: unknown, key: string): string {
39
46
  if (typeof value === 'string') return shouldRedactKey(key) ? '<redacted>' : value;
40
47
  if (typeof value === 'number' || typeof value === 'boolean') return String(value);
@@ -318,8 +318,10 @@ export default defineCommand({
318
318
  try {
319
319
  const { CodeGraphStore } = await import('../../codegraph/store.js');
320
320
  const { buildProjectContextOverview } = await import('../../codegraph/project-context.js');
321
+ const { getResolvedConfig } = await import('../../config/resolved-config.js');
321
322
  const codeStore = new CodeGraphStore();
322
323
  await codeStore.init(dataDir);
324
+ const exclude = getResolvedConfig({ projectRoot: projectRoot || process.cwd() }).codegraph.excludePatterns;
323
325
  const overview = buildProjectContextOverview({
324
326
  project: {
325
327
  id: projectId,
@@ -328,6 +330,7 @@ export default defineCommand({
328
330
  },
329
331
  store: codeStore,
330
332
  observations: projectObservations,
333
+ exclude,
331
334
  });
332
335
  const languageText = overview.code.languages.length > 0
333
336
  ? overview.code.languages.map((item: any) => `${item.language} ${item.files}`).join(', ')
@@ -1,12 +1,12 @@
1
1
  import { existsSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { getResolvedConfig } from '../config/resolved-config.js';
3
4
  import type { ProjectInfo } from '../types.js';
4
5
  import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
5
6
  import { collectCurrentProjectFacts, type CurrentProjectFacts } from './current-facts.js';
6
7
  import { indexProjectLite } from './lite-provider.js';
7
8
  import {
8
9
  buildProjectContextExplain,
9
- buildProjectContextOverview,
10
10
  type ProjectContextExplain,
11
11
  type ProjectContextObservation,
12
12
  type ProjectContextOverview,
@@ -100,11 +100,13 @@ export async function buildAutoProjectContext(input: {
100
100
  refresh?: AutoContextRefreshMode;
101
101
  maxAgeMs?: number;
102
102
  now?: Date;
103
+ exclude?: string[];
103
104
  }): Promise<AutoProjectContext> {
104
105
  const refreshMode = input.refresh ?? 'auto';
105
106
  const now = input.now ?? new Date();
106
107
  const task = input.task?.trim();
107
108
  const lens = resolveTaskLens(task);
109
+ const exclude = input.exclude ?? getResolvedConfig({ projectRoot: input.project.rootPath }).codegraph.excludePatterns;
108
110
  const store = new CodeGraphStore();
109
111
  await store.init(input.dataDir);
110
112
 
@@ -126,6 +128,7 @@ export async function buildAutoProjectContext(input: {
126
128
  const indexed = await indexProjectLite({
127
129
  projectId: input.project.id,
128
130
  projectRoot: input.project.rootPath,
131
+ exclude,
129
132
  });
130
133
  store.replaceProjectIndex(input.project.id, indexed);
131
134
  const backfill = await backfillMissingObservationCodeRefs(
@@ -143,16 +146,13 @@ export async function buildAutoProjectContext(input: {
143
146
  }
144
147
  }
145
148
 
146
- const overview = buildProjectContextOverview({
147
- project: input.project,
148
- store,
149
- observations: input.observations,
150
- });
151
149
  const explain = buildProjectContextExplain({
152
150
  project: input.project,
153
151
  store,
154
152
  observations: input.observations,
153
+ exclude,
155
154
  });
155
+ const overview = explain.overview;
156
156
 
157
157
  return {
158
158
  project: input.project,
@@ -27,6 +27,24 @@ function mentionsSymbol(text: string, symbol: CodeSymbol): boolean {
27
27
  return new RegExp(`(^|[^\\w$])${name}([^\\w$]|$)`).test(text);
28
28
  }
29
29
 
30
+ function identifierCandidates(text: string): string[] {
31
+ return [...new Set(text.match(/[A-Za-z_$][\w$]*(?:::[A-Za-z_$][\w$]*)*[!?=]?/g) ?? [])];
32
+ }
33
+
34
+ function codeIdentifierCandidates(text: string): string[] {
35
+ const explicit = new Set<string>();
36
+ for (const match of text.matchAll(/`([A-Za-z_$][\w$]*(?:::[A-Za-z_$][\w$]*)*[!?=]?)`/g)) explicit.add(match[1]);
37
+ for (const match of text.matchAll(/\b([A-Za-z_$][\w$]*(?:::[A-Za-z_$][\w$]*)*[!?=]?)\s*\(/g)) explicit.add(match[1]);
38
+ return identifierCandidates(text).filter(candidate =>
39
+ explicit.has(candidate) ||
40
+ candidate.includes('::') ||
41
+ /[!?=]$/.test(candidate) ||
42
+ /^[A-Z]/.test(candidate) ||
43
+ /[_$]/.test(candidate) ||
44
+ /[a-z0-9][A-Z]/.test(candidate) ||
45
+ /[A-Z]{2}/.test(candidate));
46
+ }
47
+
30
48
  function fileRef(projectId: string, obs: BindableObservation, file: CodeFile): ObservationCodeRef {
31
49
  return {
32
50
  id: makeObservationCodeRefId(projectId, obs.id, file.id),
@@ -55,35 +73,89 @@ function symbolRef(projectId: string, obs: BindableObservation, file: CodeFile,
55
73
  };
56
74
  }
57
75
 
58
- export async function bindObservationToCode(
59
- store: CodeGraphStore,
76
+ interface CodeBindingLookup {
77
+ findFile(path: string): CodeFile | undefined;
78
+ findSymbols(names: string[], hintedFileIds: string[]): CodeSymbol[];
79
+ }
80
+
81
+ function resolveObservationCodeRefs(
60
82
  obs: BindableObservation,
61
- ): Promise<ObservationCodeRef[]> {
83
+ lookup: CodeBindingLookup,
84
+ ): ObservationCodeRef[] {
62
85
  const refs = new Map<string, ObservationCodeRef>();
63
86
  const text = observationText(obs);
64
87
  const candidateFiles = new Map<string, CodeFile>();
65
88
 
66
89
  for (const rawPath of obs.filesModified ?? []) {
67
- const file = store.getFile(obs.projectId, normalizeCodePath(rawPath));
90
+ const file = lookup.findFile(normalizeCodePath(rawPath));
68
91
  if (!file) continue;
69
92
  candidateFiles.set(file.id, file);
70
93
  const ref = fileRef(obs.projectId, obs, file);
71
94
  refs.set(ref.id, ref);
72
95
  }
73
96
 
74
- const symbols = store.findSymbols(obs.projectId, '', 500);
97
+ const hintedFileIds = new Set(candidateFiles.keys());
98
+ const symbolNames = hintedFileIds.size > 0 ? identifierCandidates(text) : codeIdentifierCandidates(text);
99
+ const symbols = lookup.findSymbols(symbolNames, [...hintedFileIds]);
100
+ const symbolsByName = new Map<string, CodeSymbol[]>();
75
101
  for (const symbol of symbols) {
76
- if (!mentionsSymbol(text, symbol)) continue;
77
- const file = candidateFiles.get(symbol.fileId) ?? store.getFile(obs.projectId, symbol.path);
78
- if (!file) continue;
79
- candidateFiles.set(file.id, file);
80
- const ref = symbolRef(obs.projectId, obs, file, symbol);
81
- refs.set(ref.id, ref);
102
+ const group = symbolsByName.get(symbol.name) ?? [];
103
+ group.push(symbol);
104
+ symbolsByName.set(symbol.name, group);
105
+ }
106
+
107
+ for (const group of symbolsByName.values()) {
108
+ const candidates = group.length === 1 ? group : [];
109
+ for (const symbol of candidates) {
110
+ if (!mentionsSymbol(text, symbol)) continue;
111
+ const file = candidateFiles.get(symbol.fileId) ?? lookup.findFile(symbol.path);
112
+ if (!file) continue;
113
+ candidateFiles.set(file.id, file);
114
+ const ref = symbolRef(obs.projectId, obs, file, symbol);
115
+ refs.set(ref.id, ref);
116
+ }
82
117
  }
83
118
 
84
- const result = [...refs.values()];
85
- store.replaceObservationRefs(obs.projectId, obs.id, result);
86
- return result;
119
+ return [...refs.values()];
120
+ }
121
+
122
+ function createStoreLookup(store: CodeGraphStore, projectId: string): CodeBindingLookup {
123
+ return {
124
+ findFile: path => store.getFile(projectId, path) ?? undefined,
125
+ findSymbols: (names, hintedFileIds) => store.findSymbolsByNames(projectId, names, hintedFileIds),
126
+ };
127
+ }
128
+
129
+ function createSnapshotLookup(files: CodeFile[], symbols: CodeSymbol[]): CodeBindingLookup {
130
+ const filesByPath = new Map(files.map(file => [normalizeCodePath(file.path), file]));
131
+ const symbolsByName = new Map<string, CodeSymbol[]>();
132
+ for (const symbol of symbols) {
133
+ const group = symbolsByName.get(symbol.name) ?? [];
134
+ group.push(symbol);
135
+ symbolsByName.set(symbol.name, group);
136
+ }
137
+
138
+ return {
139
+ findFile: path => filesByPath.get(normalizeCodePath(path)),
140
+ findSymbols: (names, hintedFileIds) => {
141
+ const hinted = new Set(hintedFileIds);
142
+ const found: CodeSymbol[] = [];
143
+ for (const name of names) {
144
+ const candidates = symbolsByName.get(name) ?? [];
145
+ found.push(...(hinted.size > 0 ? candidates.filter(symbol => hinted.has(symbol.fileId)) : candidates));
146
+ }
147
+ return found;
148
+ },
149
+ };
150
+ }
151
+
152
+ export async function bindObservationToCode(
153
+ store: CodeGraphStore,
154
+ obs: BindableObservation,
155
+ ): Promise<ObservationCodeRef[]> {
156
+ const refs = resolveObservationCodeRefs(obs, createStoreLookup(store, obs.projectId));
157
+ store.replaceObservationRefs(obs.projectId, obs.id, refs);
158
+ return refs;
87
159
  }
88
160
 
89
161
  export async function backfillMissingObservationCodeRefs(
@@ -92,14 +164,35 @@ export async function backfillMissingObservationCodeRefs(
92
164
  ): Promise<CodeRefBackfillResult> {
93
165
  let observationsBackfilled = 0;
94
166
  let refsBackfilled = 0;
167
+ const boundObservationIdsByProject = new Map<string, Set<number>>();
168
+ const lookupByProject = new Map<string, CodeBindingLookup>();
169
+
170
+ for (const projectId of new Set(observations.map(observation => observation.projectId))) {
171
+ boundObservationIdsByProject.set(
172
+ projectId,
173
+ new Set(store.listProjectObservationRefs(projectId).map(ref => ref.observationId)),
174
+ );
175
+ lookupByProject.set(
176
+ projectId,
177
+ createSnapshotLookup(store.listFiles(projectId), store.listSymbols(projectId)),
178
+ );
179
+ }
95
180
 
181
+ const refsToInsert: ObservationCodeRef[] = [];
96
182
  for (const observation of observations) {
97
- if (store.listObservationRefs(observation.projectId, observation.id).length > 0) continue;
98
- const refs = await bindObservationToCode(store, observation);
183
+ if (boundObservationIdsByProject.get(observation.projectId)?.has(observation.id)) continue;
184
+ if ((observation.filesModified?.length ?? 0) === 0 && codeIdentifierCandidates(observationText(observation)).length === 0) {
185
+ continue;
186
+ }
187
+ const lookup = lookupByProject.get(observation.projectId);
188
+ if (!lookup) continue;
189
+ const refs = resolveObservationCodeRefs(observation, lookup);
99
190
  if (refs.length === 0) continue;
100
191
  observationsBackfilled += 1;
101
192
  refsBackfilled += refs.length;
193
+ refsToInsert.push(...refs);
102
194
  }
195
+ store.upsertObservationRefs(refsToInsert);
103
196
 
104
197
  return {
105
198
  observationsScanned: observations.length,
@@ -1,6 +1,7 @@
1
1
  import { evaluateCodeRefFreshness } from './freshness.js';
2
2
  import type { CodeFile, CodeRefStatus, CodeSymbol, ObservationCodeRef } from './types.js';
3
3
  import type { CodeGraphStore } from './store.js';
4
+ import { isCodeGraphExcludedPath } from './exclude.js';
4
5
 
5
6
  export interface ContextPackMemory {
6
7
  id: number;
@@ -52,6 +53,7 @@ export interface AssembleContextPackInput {
52
53
  files: CodeFile[];
53
54
  symbols: CodeSymbol[];
54
55
  suggestedVerification?: string[];
56
+ exclude?: string[];
55
57
  }
56
58
 
57
59
  function uniq<T>(items: T[]): T[] {
@@ -68,14 +70,6 @@ function timestampOf(observation: ContextPackObservation): number {
68
70
  return Date.parse(observation.updatedAt ?? observation.createdAt ?? '') || 0;
69
71
  }
70
72
 
71
- function isGeneratedPath(path: string): boolean {
72
- const normalized = path.replace(/\\/g, '/');
73
- return (
74
- /(^|\/)(dist|build|coverage|\.next|\.turbo|node_modules|\.git|\.tmp|\.worktrees)(\/|$)/i.test(normalized) ||
75
- /(^|\/)\.claude\/worktrees(\/|$)/i.test(normalized)
76
- );
77
- }
78
-
79
73
  function relevanceScore(observation: ContextPackObservation, taskTokens: string[]): number {
80
74
  if (taskTokens.length === 0) return 0;
81
75
  const title = observation.title.toLowerCase();
@@ -137,6 +131,7 @@ export function assembleContextPackForTask(input: {
137
131
  observations: ContextPackObservation[];
138
132
  limit?: number;
139
133
  suggestedVerification?: string[];
134
+ exclude?: string[];
140
135
  }): ContextPack {
141
136
  const selected = selectRelevantObservations(input.observations, input.task, input.limit ?? 20);
142
137
  const refs = selected.flatMap(obs => input.store.listObservationRefs(input.projectId, obs.id));
@@ -151,6 +146,7 @@ export function assembleContextPackForTask(input: {
151
146
  files,
152
147
  symbols,
153
148
  suggestedVerification: input.suggestedVerification,
149
+ exclude: input.exclude,
154
150
  });
155
151
  }
156
152
 
@@ -172,6 +168,7 @@ export function assembleContextPack(input: AssembleContextPackInput): ContextPac
172
168
 
173
169
  const file = ref.fileId ? files.get(ref.fileId) : undefined;
174
170
  const symbol = ref.symbolId ? symbols.get(ref.symbolId) : undefined;
171
+ if (file && isCodeGraphExcludedPath(file.path, input.exclude)) continue;
175
172
  const freshness = evaluateCodeRefFreshness(ref, file, symbol);
176
173
 
177
174
  if (freshness.status === 'current') {
@@ -247,8 +244,8 @@ export function buildContextPackPrompt(pack: ContextPack): string {
247
244
  const reliableMemories = pack.memories.filter(memory => memory.status === 'current');
248
245
  const unboundMemories = pack.memories.filter(memory => memory.status === 'unbound');
249
246
  const lines: string[] = ['## Task', pack.task, '', '## Reliable Memories'];
250
- const visibleCodeFacts = pack.codeFacts.filter(fact => !isGeneratedPath(fact.path)).slice(0, 5);
251
- const visibleSuggestedReads = pack.suggestedReads.filter(path => !isGeneratedPath(path)).slice(0, 5);
247
+ const visibleCodeFacts = pack.codeFacts.filter(fact => !isCodeGraphExcludedPath(fact.path)).slice(0, 5);
248
+ const visibleSuggestedReads = pack.suggestedReads.filter(path => !isCodeGraphExcludedPath(path)).slice(0, 5);
252
249
 
253
250
  if (reliableMemories.length === 0) lines.push('- none');
254
251
  for (const memory of reliableMemories) {
@@ -0,0 +1,47 @@
1
+ export const DEFAULT_CODEGRAPH_EXCLUDES = [
2
+ 'node_modules/**',
3
+ 'dist/**',
4
+ 'build/**',
5
+ 'coverage/**',
6
+ '.next/**',
7
+ '.turbo/**',
8
+ '.git/**',
9
+ '.tmp/**',
10
+ '.worktrees/**',
11
+ '.claude/worktrees/**',
12
+ ];
13
+
14
+ export function normalizeCodeGraphExcludePatterns(exclude?: string[]): string[] {
15
+ return [...new Set([
16
+ ...DEFAULT_CODEGRAPH_EXCLUDES,
17
+ ...(exclude ?? []).map(pattern => pattern.trim()).filter(Boolean),
18
+ ])];
19
+ }
20
+
21
+ export function isCodeGraphExcludedPath(path: string, exclude?: string[]): boolean {
22
+ const normalized = normalizeCodePath(path);
23
+ return normalizeCodeGraphExcludePatterns(exclude).some((pattern) => matchesPattern(normalized, normalizeCodePath(pattern)));
24
+ }
25
+
26
+ function normalizeCodePath(path: string): string {
27
+ return path.replace(/\\/g, '/').replace(/^\.\/+/, '');
28
+ }
29
+
30
+ function matchesPattern(path: string, pattern: string): boolean {
31
+ if (pattern.endsWith('/**')) {
32
+ const base = pattern.slice(0, -3);
33
+ if (base.startsWith('**/')) {
34
+ const suffix = base.slice(3);
35
+ return path === suffix || path.endsWith(`/${suffix}`) || path.includes(`/${suffix}/`);
36
+ }
37
+ if (!base.includes('/')) {
38
+ return path === base || path.startsWith(`${base}/`) || path.includes(`/${base}/`);
39
+ }
40
+ return path === base || path.startsWith(`${base}/`);
41
+ }
42
+ if (pattern.startsWith('**/')) {
43
+ const suffix = pattern.slice(3);
44
+ return path === suffix || path.endsWith(`/${suffix}`);
45
+ }
46
+ return path === pattern || path.startsWith(`${pattern}/`);
47
+ }
@@ -3,6 +3,7 @@ import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
3
3
  import { join, relative } from 'node:path';
4
4
  import type { CodeEdge, CodeFile, CodeSymbol } from './types.js';
5
5
  import { makeCodeEdgeId, makeCodeFileId, makeCodeSymbolId, normalizeCodePath } from './ids.js';
6
+ import { isCodeGraphExcludedPath, normalizeCodeGraphExcludePatterns } from './exclude.js';
6
7
 
7
8
  export interface LiteIndexOptions {
8
9
  projectId: string;
@@ -140,8 +141,8 @@ const languageProfiles: Record<string, LanguageProfile> = {
140
141
  },
141
142
  ruby: {
142
143
  symbols: [
143
- { kind: 'class', re: /^class\s+([A-Za-z_][\w:]*?)\b/gm },
144
- { kind: 'function', re: /^def\s+([A-Za-z_][\w!?=]*)\b/gm },
144
+ { kind: 'class', re: /^class\s+([A-Za-z_][\w]*(?:::[A-Za-z_][\w]*)*)/gm },
145
+ { kind: 'function', re: /^def\s+([A-Za-z_][\w]*[!?=]?)/gm },
145
146
  ],
146
147
  imports: [/require\s+['"]([^'"]+)['"]/g],
147
148
  },
@@ -169,19 +170,6 @@ function languageForPath(path: string): string {
169
170
  return LANGUAGE_BY_EXTENSION.get(ext) ?? 'unknown';
170
171
  }
171
172
 
172
- function isExcluded(path: string, exclude: string[]): boolean {
173
- const normalized = normalizeCodePath(path);
174
- return exclude.some((pattern) => {
175
- const p = normalizeCodePath(pattern);
176
- if (p.endsWith('/**')) {
177
- const base = p.slice(0, -3);
178
- return normalized === base || normalized.startsWith(`${base}/`);
179
- }
180
- if (p.startsWith('**/')) return normalized.endsWith(p.slice(3));
181
- return normalized === p || normalized.startsWith(`${p}/`);
182
- });
183
- }
184
-
185
173
  function walk(root: string, exclude: string[], maxFiles: number): string[] {
186
174
  const out: string[] = [];
187
175
  const visit = (dir: string) => {
@@ -196,7 +184,7 @@ function walk(root: string, exclude: string[], maxFiles: number): string[] {
196
184
  for (const entry of entries) {
197
185
  const abs = join(dir, entry.name);
198
186
  const rel = normalizeCodePath(relative(root, abs));
199
- if (isExcluded(rel, exclude)) continue;
187
+ if (isCodeGraphExcludedPath(rel, exclude)) continue;
200
188
  if (entry.isDirectory()) {
201
189
  if (entry.name === '.git' || entry.name === '.worktrees' || entry.name === '.tmp' || entry.name === 'node_modules') continue;
202
190
  if (rel === '.claude/worktrees' || rel.startsWith('.claude/worktrees/')) continue;
@@ -290,18 +278,7 @@ function extractImportEdges(projectId: string, file: CodeFile, text: string, ind
290
278
  }
291
279
 
292
280
  export async function indexProjectLite(options: LiteIndexOptions): Promise<LiteIndexResult> {
293
- const exclude = options.exclude ?? [
294
- 'node_modules/**',
295
- 'dist/**',
296
- 'build/**',
297
- 'coverage/**',
298
- '.next/**',
299
- '.turbo/**',
300
- '.git/**',
301
- '.tmp/**',
302
- '.worktrees/**',
303
- '.claude/worktrees/**',
304
- ];
281
+ const exclude = normalizeCodeGraphExcludePatterns(options.exclude);
305
282
  const maxFiles = options.maxFiles ?? 5000;
306
283
  const indexedAt = new Date().toISOString();
307
284
  const paths = walk(options.projectRoot, exclude, maxFiles);