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.
- package/CHANGELOG.md +15 -0
- package/dist/cli/index.js +301 -103
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +276 -92
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +15 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +276 -92
- package/dist/sdk.js.map +1 -1
- package/docs/API_REFERENCE.md +2 -0
- package/docs/CONFIGURATION.md +18 -0
- package/docs/dev-log/progress.txt +21 -34
- package/package.json +1 -1
- package/src/cli/commands/codegraph.ts +4 -0
- package/src/cli/commands/config-get.ts +9 -2
- package/src/cli/commands/doctor.ts +3 -0
- package/src/codegraph/auto-context.ts +6 -6
- package/src/codegraph/binder.ts +109 -16
- package/src/codegraph/context-pack.ts +7 -10
- package/src/codegraph/exclude.ts +47 -0
- package/src/codegraph/lite-provider.ts +5 -28
- package/src/codegraph/project-context.ts +46 -29
- package/src/codegraph/store.ts +59 -0
- package/src/config/resolved-config.ts +6 -0
- package/src/config/toml-loader.ts +4 -0
- package/src/config/yaml-loader.ts +7 -0
- package/src/server.ts +3 -0
- package/src/store/orama-store.ts +15 -8
package/docs/API_REFERENCE.md
CHANGED
|
@@ -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.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
-
- Branch:
|
|
9
|
-
-
|
|
10
|
-
- Last updated: 2026-07-
|
|
11
|
-
- Goal:
|
|
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
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- `
|
|
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
|
-
-
|
|
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.
|
|
50
|
-
-
|
|
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
|
@@ -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'
|
|
33
|
-
|
|
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,
|
package/src/codegraph/binder.ts
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 (
|
|
98
|
-
|
|
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 => !
|
|
251
|
-
const visibleSuggestedReads = pack.suggestedReads.filter(path => !
|
|
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
|
|
144
|
-
{ kind: 'function', re: /^def\s+([A-Za-z_][\w!?=]
|
|
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 (
|
|
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);
|