@kentwynn/kgraph 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -74,6 +74,8 @@ That shows matched files/symbols, files importing the target, known callers/call
74
74
 
75
75
  ## Install
76
76
 
77
+ The official npm package is `@kentwynn/kgraph`; the official repository is `github.com/kentwynn/KGraph`.
78
+
77
79
  Use the published CLI:
78
80
 
79
81
  ```bash
@@ -90,6 +92,8 @@ npx @kentwynn/kgraph@latest "auth token refresh"
90
92
 
91
93
  KGraph requires Node.js 20 or newer.
92
94
 
95
+ KGraph's core functionality is free and local-first. It does not require accounts, telemetry, cloud services, API keys, or source-code upload.
96
+
93
97
  ## Quick Start
94
98
 
95
99
  From the root of a repository:
@@ -215,6 +219,8 @@ kgraph scan
215
219
 
216
220
  Refresh only the structural maps in `.kgraph/map/`.
217
221
 
222
+ If the repository is a git repo, KGraph stores the HEAD commit hash with the scan result. On the next scan it computes which files changed since that commit using `git diff --name-only` and skips unchanged files without any filesystem `stat()` calls. In large repos this is measurably faster than the mtime+size fallback, which still runs automatically in non-git directories.
223
+
218
224
  ```bash
219
225
  kgraph context "auth token refresh"
220
226
  kgraph context "auth token refresh" --json
@@ -223,6 +229,8 @@ kgraph context "auth token refresh" --json
223
229
  Return context from existing maps and cognition without scanning or updating first.
224
230
  Markdown output includes the reason each file, symbol, cognition note, nearby symbol, or relationship was selected. Use `--json` when an agent or script needs the same explanation data programmatically.
225
231
 
232
+ Context output includes a **Recent Git Changes** section that surfaces files with staged edits, unstaged edits, or changes in recent commits. This lets AI agents know which files are actively in flux without running a separate `git status` or `git log`.
233
+
226
234
  ```bash
227
235
  kgraph update
228
236
  kgraph update --dry-run
@@ -261,22 +269,22 @@ kgraph integrate remove cursor
261
269
 
262
270
  New integrations default to `always` mode because coding agents often under-classify small UI, route, button, and link changes as not needing repo context.
263
271
 
264
- | Mode | Behavior |
265
- | --- | --- |
266
- | `always` | Every chat in the repository starts with `kgraph "<topic>"`, even simple or conversational requests. |
267
- | `smart` | Runs KGraph automatically for repo-specific coding, debugging, architecture, refactor, review, or file-exploration requests. Skips simple conversational requests that do not depend on repo knowledge. |
268
- | `manual` | Exposes KGraph commands and instructions, but the agent runs KGraph only when the user explicitly asks. |
269
- | `off` | Disables that integration and removes generated KGraph instruction blocks/command files. |
272
+ | Mode | Behavior |
273
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
274
+ | `always` | Every chat in the repository starts with `kgraph "<topic>"`, even simple or conversational requests. |
275
+ | `smart` | Runs KGraph automatically for repo-specific coding, debugging, architecture, refactor, review, or file-exploration requests. Skips simple conversational requests that do not depend on repo knowledge. |
276
+ | `manual` | Exposes KGraph commands and instructions, but the agent runs KGraph only when the user explicitly asks. |
277
+ | `off` | Disables that integration and removes generated KGraph instruction blocks/command files. |
270
278
 
271
- | Tool | Files KGraph manages |
272
- | --- | --- |
273
- | Codex | `AGENTS.md`, `.agents/skills/kgraph/SKILL.md` |
279
+ | Tool | Files KGraph manages |
280
+ | -------------- | ------------------------------------------------------ |
281
+ | Codex | `AGENTS.md`, `.agents/skills/kgraph/SKILL.md` |
274
282
  | GitHub Copilot | `.github/copilot-instructions.md`, `.github/prompts/*` |
275
- | Cursor | `.cursor/rules/kgraph.mdc` |
276
- | Claude Code | `CLAUDE.md`, `.claude/commands/*` |
277
- | Gemini CLI | `GEMINI.md` |
278
- | Windsurf | `.windsurf/rules/kgraph.md` |
279
- | Cline | `.clinerules/kgraph.md` |
283
+ | Cursor | `.cursor/rules/kgraph.mdc` |
284
+ | Claude Code | `CLAUDE.md`, `.claude/commands/*` |
285
+ | Gemini CLI | `GEMINI.md` |
286
+ | Windsurf | `.windsurf/rules/kgraph.md` |
287
+ | Cline | `.clinerules/kgraph.md` |
280
288
 
281
289
  Antigravity is supported through the existing agent instruction surfaces it can read, especially `AGENTS.md` and `GEMINI.md`; it does not need a separate KGraph adapter yet.
282
290
 
@@ -302,7 +310,7 @@ All runtime data lives under `.kgraph/`:
302
310
  └── context/
303
311
  ```
304
312
 
305
- The files are local, inspectable, and human-readable. There is no database, telemetry, cloud service, account, API key, embedding service, or model provider.
313
+ The files are local, inspectable, and human-readable. Core KGraph functionality is free. There is no database, telemetry, cloud service, account, API key, embedding service, model provider, or source-code upload.
306
314
 
307
315
  ## Language Support
308
316
 
@@ -371,14 +379,25 @@ npm run release:pack
371
379
 
372
380
  ## Release
373
381
 
374
- Releases are tag-driven:
382
+ Releases are PR-first because `main` is protected. Use the Makefile helper to bump the version on a release branch, push it, and open a pull request when the GitHub CLI is available:
383
+
384
+ ```bash
385
+ make release
386
+ ```
387
+
388
+ Use `RELEASE=minor` or `RELEASE=major` when needed:
389
+
390
+ ```bash
391
+ make release RELEASE=minor
392
+ ```
393
+
394
+ After the PR is merged, tag the merged commit from an up-to-date `main`:
375
395
 
376
396
  ```bash
377
- npm version patch
378
- git push origin main --follow-tags
397
+ make release-tag VERSION=v0.2.2
379
398
  ```
380
399
 
381
- The release workflow builds, tests, packs, publishes the npm package on version tags, creates a GitHub Release, and uploads the tarball artifact.
400
+ The release workflow builds, tests, packs, publishes the npm package on version tags, creates a GitHub Release, and uploads the tarball artifact. Do not push directly to `main` for releases.
382
401
 
383
402
  ## Design Principles
384
403
 
@@ -394,5 +413,5 @@ The release workflow builds, tests, packs, publishes the npm package on version
394
413
  - Smarter cross-file symbol and call relationship inference.
395
414
  - Stronger TypeScript path alias and package export resolution.
396
415
  - Richer graph filtering for large repositories.
397
- - Optional MCP server for editor tool-call access.
398
- - Team workflows for shared committed cognition.
416
+ - Optional MCP and editor integration.
417
+ - Team-friendly shared cognition workflows that stay local-first.
@@ -62,6 +62,30 @@ export function renderContextMarkdown(response) {
62
62
  })));
63
63
  lines.push('', '## Stale References', '');
64
64
  lines.push(...formatList(response.staleReferences.map((ref) => `- ${ref}`)));
65
+ lines.push('', '## Recent Git Changes', '');
66
+ if (response.gitChanges && response.gitChanges.length > 0) {
67
+ const staged = response.gitChanges.filter((c) => c.status === 'staged');
68
+ const unstaged = response.gitChanges.filter((c) => c.status === 'unstaged');
69
+ const recent = response.gitChanges.filter((c) => c.status === 'recent-commit');
70
+ if (staged.length > 0) {
71
+ lines.push('Staged:');
72
+ for (const c of staged)
73
+ lines.push(` ${c.path} (${c.reason})`);
74
+ }
75
+ if (unstaged.length > 0) {
76
+ lines.push('Unstaged:');
77
+ for (const c of unstaged)
78
+ lines.push(` ${c.path} (${c.reason})`);
79
+ }
80
+ if (recent.length > 0) {
81
+ lines.push('Recent commits:');
82
+ for (const c of recent)
83
+ lines.push(` ${c.path} (${c.reason})`);
84
+ }
85
+ }
86
+ else {
87
+ lines.push('- None');
88
+ }
65
89
  return lines.join('\n');
66
90
  }
67
91
  function formatGroupedRelationships(relationships, explanations) {
@@ -38,6 +38,7 @@ export function registerInitCommand(program) {
38
38
  dependencies: previousMaps.dependencyMap.dependencies,
39
39
  relationships: previousMaps.relationshipMap.relationships,
40
40
  warnings: [],
41
+ scannedAtCommit: previousMaps.fileMap.scannedAtCommit,
41
42
  });
42
43
  await writeMaps(workspace, result);
43
44
  console.log(`Scanned ${result.files.length} files and ${result.symbols.length} symbols.`);
@@ -1,2 +1,2 @@
1
- import type { Command } from "commander";
1
+ import type { Command } from 'commander';
2
2
  export declare function registerScanCommand(program: Command): void;
@@ -1,14 +1,14 @@
1
- import { refreshCognitionReferenceStatuses } from "../../cognition/cognition-updater.js";
2
- import { loadConfig } from "../../config/config.js";
3
- import { scanRepository } from "../../scanner/repo-scanner.js";
4
- import { assertWorkspace } from "../../storage/kgraph-paths.js";
5
- import { readMaps, writeMaps } from "../../storage/map-store.js";
6
- import { runCommand } from "../errors.js";
1
+ import { refreshCognitionReferenceStatuses } from '../../cognition/cognition-updater.js';
2
+ import { loadConfig } from '../../config/config.js';
3
+ import { scanRepository } from '../../scanner/repo-scanner.js';
4
+ import { assertWorkspace } from '../../storage/kgraph-paths.js';
5
+ import { readMaps, writeMaps } from '../../storage/map-store.js';
6
+ import { runCommand } from '../errors.js';
7
7
  export function registerScanCommand(program) {
8
8
  program
9
- .command("scan")
10
- .description("Scan the repository into deterministic KGraph maps")
11
- .option("--verbose", "Print scan warnings")
9
+ .command('scan')
10
+ .description('Scan the repository into deterministic KGraph maps')
11
+ .option('--verbose', 'Print scan warnings')
12
12
  .action((options) => runCommand(async () => {
13
13
  const workspace = await assertWorkspace(process.cwd());
14
14
  const config = await loadConfig(workspace);
@@ -18,10 +18,14 @@ export function registerScanCommand(program) {
18
18
  symbols: previousMaps.symbolMap.symbols,
19
19
  dependencies: previousMaps.dependencyMap.dependencies,
20
20
  relationships: previousMaps.relationshipMap.relationships,
21
- warnings: []
21
+ warnings: [],
22
+ scannedAtCommit: previousMaps.fileMap.scannedAtCommit,
22
23
  });
23
24
  await writeMaps(workspace, result);
24
- await refreshCognitionReferenceStatuses(workspace, { files: result.files, symbols: result.symbols });
25
+ await refreshCognitionReferenceStatuses(workspace, {
26
+ files: result.files,
27
+ symbols: result.symbols,
28
+ });
25
29
  console.log(`Scanned ${result.files.length} files and ${result.symbols.length} symbols.`);
26
30
  if (options.verbose && result.warnings.length > 0) {
27
31
  for (const warning of result.warnings) {
@@ -24,6 +24,7 @@ export async function runDefaultWorkflow(query) {
24
24
  dependencies: previousMaps.dependencyMap.dependencies,
25
25
  relationships: previousMaps.relationshipMap.relationships,
26
26
  warnings: [],
27
+ scannedAtCommit: previousMaps.fileMap.scannedAtCommit,
27
28
  });
28
29
  await writeMaps(workspace, scan);
29
30
  await refreshCognitionReferenceStatuses(workspace, {
@@ -1,3 +1,4 @@
1
+ import { getRecentlyCommittedFiles, getWorkingTreeChangesDetailed, isGitRepo, } from '../scanner/git-utils.js';
1
2
  import { readCognitionNotes, readDomainRecords, } from '../storage/cognition-store.js';
2
3
  import { rankByFields } from './ranking.js';
3
4
  export async function queryContext(workspace, config, maps, query) {
@@ -109,6 +110,41 @@ export async function queryContext(workspace, config, maps, query) {
109
110
  ...dependenciesForImportedSymbol(symbol, maps.dependencyMap.dependencies),
110
111
  ],
111
112
  }));
113
+ // Collect git changes: working-tree and recently committed files known to KGraph
114
+ const knownFilePaths = new Set(maps.fileMap.files.map((f) => f.path));
115
+ const gitChanges = [];
116
+ if (await isGitRepo(workspace.rootPath)) {
117
+ const workingTreeChanges = await getWorkingTreeChangesDetailed(workspace.rootPath);
118
+ for (const change of workingTreeChanges) {
119
+ if (!knownFilePaths.has(change.path))
120
+ continue;
121
+ const status = change.staged && !change.unstaged
122
+ ? 'staged'
123
+ : change.unstaged && !change.staged
124
+ ? 'unstaged'
125
+ : 'staged'; // both staged and unstaged → report as staged
126
+ gitChanges.push({
127
+ path: change.path,
128
+ status,
129
+ reason: change.staged && change.unstaged
130
+ ? 'partially staged'
131
+ : status === 'staged'
132
+ ? 'staged change'
133
+ : 'unstaged change',
134
+ });
135
+ }
136
+ const committedPaths = new Set(gitChanges.map((c) => c.path));
137
+ const recentCommitted = await getRecentlyCommittedFiles(workspace.rootPath);
138
+ for (const filePath of recentCommitted) {
139
+ if (!knownFilePaths.has(filePath) || committedPaths.has(filePath))
140
+ continue;
141
+ gitChanges.push({
142
+ path: filePath,
143
+ status: 'recent-commit',
144
+ reason: 'changed in recent commits',
145
+ });
146
+ }
147
+ }
112
148
  return {
113
149
  query,
114
150
  matchedDomains,
@@ -119,6 +155,7 @@ export async function queryContext(workspace, config, maps, query) {
119
155
  relationshipExplanations: relationshipExplanations.slice(0, max),
120
156
  nearbySymbols,
121
157
  nearbySymbolExplanations,
158
+ gitChanges,
122
159
  staleReferences,
123
160
  warnings: [],
124
161
  };
@@ -130,7 +167,8 @@ function explainRelationships(relationships, context) {
130
167
  ]));
131
168
  return relationships.map((relationship) => {
132
169
  const reasons = new Set();
133
- for (const reason of rankedReasons.get(relationshipKey(relationship)) ?? []) {
170
+ for (const reason of rankedReasons.get(relationshipKey(relationship)) ??
171
+ []) {
134
172
  reasons.add(reason);
135
173
  }
136
174
  for (const file of context.relevantFiles) {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Returns true if the given directory is inside a git repository.
3
+ * Uses a fast filesystem check rather than spawning a process.
4
+ */
5
+ export declare function isGitRepo(rootPath: string): Promise<boolean>;
6
+ /**
7
+ * Returns the current HEAD commit hash, or null if unavailable.
8
+ */
9
+ export declare function getCurrentCommit(rootPath: string): Promise<string | null>;
10
+ /**
11
+ * Returns paths of files changed between the given ref and HEAD.
12
+ * Returns an empty array if git is unavailable or the ref is unknown.
13
+ */
14
+ export declare function getChangedFilesSince(rootPath: string, ref: string): Promise<string[]>;
15
+ /**
16
+ * Returns paths of files with uncommitted changes (staged or unstaged)
17
+ * relative to HEAD. Returns an empty array if git is unavailable.
18
+ */
19
+ export declare function getWorkingTreeChanges(rootPath: string): Promise<string[]>;
20
+ export type WorkingTreeChange = {
21
+ path: string;
22
+ staged: boolean;
23
+ unstaged: boolean;
24
+ };
25
+ /**
26
+ * Returns working-tree changes with staged/unstaged flags.
27
+ * Returns an empty array if git is unavailable.
28
+ */
29
+ export declare function getWorkingTreeChangesDetailed(rootPath: string): Promise<WorkingTreeChange[]>;
30
+ /**
31
+ * Returns up to `limit` files changed in the most recent commits
32
+ * (excluding uncommitted changes). Returns an empty array if git is unavailable.
33
+ */
34
+ export declare function getRecentlyCommittedFiles(rootPath: string, limit?: number): Promise<string[]>;
@@ -0,0 +1,130 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { access } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ /**
7
+ * Returns true if the given directory is inside a git repository.
8
+ * Uses a fast filesystem check rather than spawning a process.
9
+ */
10
+ export async function isGitRepo(rootPath) {
11
+ try {
12
+ await access(path.join(rootPath, '.git'));
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /**
20
+ * Returns the current HEAD commit hash, or null if unavailable.
21
+ */
22
+ export async function getCurrentCommit(rootPath) {
23
+ try {
24
+ const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
25
+ cwd: rootPath,
26
+ });
27
+ return stdout.trim() || null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Returns paths of files changed between the given ref and HEAD.
35
+ * Returns an empty array if git is unavailable or the ref is unknown.
36
+ */
37
+ export async function getChangedFilesSince(rootPath, ref) {
38
+ try {
39
+ const { stdout } = await execFileAsync('git', ['diff', '--name-only', ref, 'HEAD'], { cwd: rootPath });
40
+ return stdout
41
+ .trim()
42
+ .split('\n')
43
+ .map((line) => line.trim())
44
+ .filter(Boolean);
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ }
50
+ /**
51
+ * Returns paths of files with uncommitted changes (staged or unstaged)
52
+ * relative to HEAD. Returns an empty array if git is unavailable.
53
+ */
54
+ export async function getWorkingTreeChanges(rootPath) {
55
+ try {
56
+ const [stagedResult, unstagedResult] = await Promise.all([
57
+ execFileAsync('git', ['diff', '--name-only', '--cached'], {
58
+ cwd: rootPath,
59
+ }),
60
+ execFileAsync('git', ['diff', '--name-only'], { cwd: rootPath }),
61
+ ]);
62
+ const staged = stagedResult.stdout
63
+ .trim()
64
+ .split('\n')
65
+ .map((line) => line.trim())
66
+ .filter(Boolean);
67
+ const unstaged = unstagedResult.stdout
68
+ .trim()
69
+ .split('\n')
70
+ .map((line) => line.trim())
71
+ .filter(Boolean);
72
+ // Deduplicate: a file can appear in both if partially staged
73
+ return [...new Set([...staged, ...unstaged])];
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ }
79
+ /**
80
+ * Returns working-tree changes with staged/unstaged flags.
81
+ * Returns an empty array if git is unavailable.
82
+ */
83
+ export async function getWorkingTreeChangesDetailed(rootPath) {
84
+ try {
85
+ const [stagedResult, unstagedResult] = await Promise.all([
86
+ execFileAsync('git', ['diff', '--name-only', '--cached'], {
87
+ cwd: rootPath,
88
+ }),
89
+ execFileAsync('git', ['diff', '--name-only'], { cwd: rootPath }),
90
+ ]);
91
+ const staged = new Set(stagedResult.stdout
92
+ .trim()
93
+ .split('\n')
94
+ .map((line) => line.trim())
95
+ .filter(Boolean));
96
+ const unstaged = new Set(unstagedResult.stdout
97
+ .trim()
98
+ .split('\n')
99
+ .map((line) => line.trim())
100
+ .filter(Boolean));
101
+ const all = new Set([...staged, ...unstaged]);
102
+ return [...all].map((filePath) => ({
103
+ path: filePath,
104
+ staged: staged.has(filePath),
105
+ unstaged: unstaged.has(filePath),
106
+ }));
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ }
112
+ /**
113
+ * Returns up to `limit` files changed in the most recent commits
114
+ * (excluding uncommitted changes). Returns an empty array if git is unavailable.
115
+ */
116
+ export async function getRecentlyCommittedFiles(rootPath, limit = 5) {
117
+ try {
118
+ // git log --name-only gives files touched in each of the last N commits
119
+ const { stdout } = await execFileAsync('git', ['log', '--name-only', '--pretty=format:', `-n`, String(limit), 'HEAD'], { cwd: rootPath });
120
+ return [
121
+ ...new Set(stdout
122
+ .split('\n')
123
+ .map((line) => line.trim())
124
+ .filter(Boolean)),
125
+ ];
126
+ }
127
+ catch {
128
+ return [];
129
+ }
130
+ }
@@ -1,3 +1,5 @@
1
1
  import type { KGraphConfig } from '../types/config.js';
2
2
  import type { ScanResult } from '../types/maps.js';
3
- export declare function scanRepository(rootPath: string, config: KGraphConfig, previous?: ScanResult): Promise<ScanResult>;
3
+ export declare function scanRepository(rootPath: string, config: KGraphConfig, previous?: ScanResult & {
4
+ scannedAtCommit?: string;
5
+ }): Promise<ScanResult>;
@@ -6,6 +6,7 @@ import { estimateTokens } from '../session/token-estimator.js';
6
6
  import { extractCSymbols } from './c-symbol-extractor.js';
7
7
  import { extractCSharpSymbols } from './csharp-symbol-extractor.js';
8
8
  import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
9
+ import { getChangedFilesSince, isGitRepo } from './git-utils.js';
9
10
  import { extractGoSymbols } from './go-symbol-extractor.js';
10
11
  import { extractJvmSymbols } from './jvm-symbol-extractor.js';
11
12
  import { extractPythonSymbols } from './python-symbol-extractor.js';
@@ -71,6 +72,16 @@ export async function scanRepository(rootPath, config, previous) {
71
72
  }
72
73
  }
73
74
  }
75
+ // Build git-changed-file set for fast incremental skip bypassing stat() calls.
76
+ // Only used when the previous scan stored a commit hash and we are in a git repo.
77
+ let gitChangedFiles = null;
78
+ if (previous?.scannedAtCommit && (await isGitRepo(rootPath))) {
79
+ const changed = await getChangedFilesSince(rootPath, previous.scannedAtCommit);
80
+ // Also include working-tree dirty files so uncommitted edits are always re-scanned.
81
+ // getChangedFilesSince returns committed changes; we complement with a stat check
82
+ // fallback below for uncommitted edits to keep correctness.
83
+ gitChangedFiles = new Set(changed);
84
+ }
74
85
  const files = [];
75
86
  const symbols = [];
76
87
  const dependencies = [];
@@ -83,9 +94,34 @@ export async function scanRepository(rootPath, config, previous) {
83
94
  }
84
95
  const absolutePath = path.join(rootPath, repoPath);
85
96
  try {
86
- const info = await stat(absolutePath);
87
- // Incremental skip: if mtime and size match previous, carry forward
88
97
  const prevFile = prevFileByPath.get(repoPath);
98
+ // Fast git-based skip: if we have a git-changed set and this file is NOT in it,
99
+ // carry forward directly without touching the filesystem.
100
+ if (prevFile &&
101
+ gitChangedFiles !== null &&
102
+ !gitChangedFiles.has(repoPath)) {
103
+ files.push(prevFile);
104
+ const prevSyms = prevSymbolsByFile.get(repoPath);
105
+ if (prevSyms)
106
+ symbols.push(...prevSyms);
107
+ const prevDeps = prevDepsByFile.get(repoPath);
108
+ if (prevDeps)
109
+ dependencies.push(...prevDeps);
110
+ const prevRels = prevRelsBySource.get(repoPath);
111
+ if (prevRels)
112
+ relationships.push(...prevRels);
113
+ if (prevSyms) {
114
+ for (const sym of prevSyms) {
115
+ const symRels = prevRelsBySource.get(sym.id);
116
+ if (symRels)
117
+ relationships.push(...symRels);
118
+ }
119
+ }
120
+ skippedFiles++;
121
+ continue;
122
+ }
123
+ const info = await stat(absolutePath);
124
+ // Fallback incremental skip: if mtime and size match previous, carry forward
89
125
  if (prevFile &&
90
126
  prevFile.sizeBytes === info.size &&
91
127
  prevFile.modifiedAt === info.mtime.toISOString()) {
@@ -1,5 +1,5 @@
1
- import type { KGraphWorkspace } from "../types/config.js";
2
- import type { DependencyMap, FileMap, RelationshipMap, ScanResult, SymbolMap } from "../types/maps.js";
1
+ import type { KGraphWorkspace } from '../types/config.js';
2
+ import type { DependencyMap, FileMap, RelationshipMap, ScanResult, SymbolMap } from '../types/maps.js';
3
3
  export declare function mapPaths(workspace: KGraphWorkspace): Record<string, string>;
4
4
  export declare function readMaps(workspace: KGraphWorkspace): Promise<{
5
5
  fileMap: FileMap;
@@ -1,13 +1,14 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { pathExists } from "./kgraph-paths.js";
4
- import { KGraphError } from "../cli/errors.js";
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { KGraphError } from '../cli/errors.js';
4
+ import { getCurrentCommit } from '../scanner/git-utils.js';
5
+ import { pathExists } from './kgraph-paths.js';
5
6
  async function readJson(filePath, fallback) {
6
7
  if (!(await pathExists(filePath))) {
7
8
  return fallback;
8
9
  }
9
10
  try {
10
- return JSON.parse(await readFile(filePath, "utf8"));
11
+ return JSON.parse(await readFile(filePath, 'utf8'));
11
12
  }
12
13
  catch (error) {
13
14
  const message = error instanceof Error ? error.message : String(error);
@@ -15,40 +16,57 @@ async function readJson(filePath, fallback) {
15
16
  }
16
17
  }
17
18
  async function writeJson(filePath, value) {
18
- await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
19
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
19
20
  }
20
21
  export function mapPaths(workspace) {
21
22
  return {
22
- files: path.join(workspace.mapPath, "files.json"),
23
- symbols: path.join(workspace.mapPath, "symbols.json"),
24
- dependencies: path.join(workspace.mapPath, "dependencies.json"),
25
- relationships: path.join(workspace.mapPath, "relationships.json")
23
+ files: path.join(workspace.mapPath, 'files.json'),
24
+ symbols: path.join(workspace.mapPath, 'symbols.json'),
25
+ dependencies: path.join(workspace.mapPath, 'dependencies.json'),
26
+ relationships: path.join(workspace.mapPath, 'relationships.json'),
26
27
  };
27
28
  }
28
29
  export async function readMaps(workspace) {
29
30
  const paths = mapPaths(workspace);
30
31
  return {
31
- fileMap: await readJson(paths.files, { generatedAt: "", files: [] }),
32
- symbolMap: await readJson(paths.symbols, { generatedAt: "", symbols: [] }),
33
- dependencyMap: await readJson(paths.dependencies, { generatedAt: "", dependencies: [] }),
32
+ fileMap: await readJson(paths.files, {
33
+ generatedAt: '',
34
+ files: [],
35
+ }),
36
+ symbolMap: await readJson(paths.symbols, {
37
+ generatedAt: '',
38
+ symbols: [],
39
+ }),
40
+ dependencyMap: await readJson(paths.dependencies, {
41
+ generatedAt: '',
42
+ dependencies: [],
43
+ }),
34
44
  relationshipMap: await readJson(paths.relationships, {
35
- generatedAt: "",
36
- relationships: []
37
- })
45
+ generatedAt: '',
46
+ relationships: [],
47
+ }),
38
48
  };
39
49
  }
40
50
  export async function writeMaps(workspace, result) {
41
51
  const generatedAt = new Date().toISOString();
52
+ const scannedAtCommit = (await getCurrentCommit(workspace.rootPath)) ?? undefined;
42
53
  const paths = mapPaths(workspace);
43
- await writeJson(paths.files, { generatedAt, files: result.files });
44
- await writeJson(paths.symbols, { generatedAt, symbols: result.symbols });
54
+ await writeJson(paths.files, {
55
+ generatedAt,
56
+ scannedAtCommit,
57
+ files: result.files,
58
+ });
59
+ await writeJson(paths.symbols, {
60
+ generatedAt,
61
+ symbols: result.symbols,
62
+ });
45
63
  await writeJson(paths.dependencies, {
46
64
  generatedAt,
47
- dependencies: result.dependencies
65
+ dependencies: result.dependencies,
48
66
  });
49
67
  await writeJson(paths.relationships, {
50
68
  generatedAt,
51
- relationships: result.relationships
69
+ relationships: result.relationships,
52
70
  });
53
71
  }
54
72
  export async function mapsExist(workspace) {
@@ -31,6 +31,12 @@ export interface RankedItem<T> {
31
31
  score: number;
32
32
  reasons: string[];
33
33
  }
34
+ export type GitChangeStatus = 'staged' | 'unstaged' | 'recent-commit';
35
+ export interface GitContextChange {
36
+ path: string;
37
+ status: GitChangeStatus;
38
+ reason: string;
39
+ }
34
40
  export interface ContextResponse {
35
41
  query: string;
36
42
  matchedDomains: RankedItem<DomainRecord>[];
@@ -47,6 +53,7 @@ export interface ContextResponse {
47
53
  symbol: CodeSymbol;
48
54
  reasons: string[];
49
55
  }>;
56
+ gitChanges?: GitContextChange[];
50
57
  staleReferences: string[];
51
58
  warnings: string[];
52
59
  }
@@ -40,6 +40,7 @@ export interface Relationship {
40
40
  }
41
41
  export interface FileMap {
42
42
  generatedAt: string;
43
+ scannedAtCommit?: string;
43
44
  files: RepositoryFile[];
44
45
  }
45
46
  export interface SymbolMap {
@@ -61,4 +62,5 @@ export interface ScanResult {
61
62
  relationships: Relationship[];
62
63
  warnings: string[];
63
64
  skippedFiles?: number;
65
+ scannedAtCommit?: string;
64
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {