@sdsrs/code-graph 0.162.0 → 0.163.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.162.0",
7
+ "version": "0.163.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -9,7 +9,9 @@
9
9
  //
10
10
  // .cg-edits-<cwdHash>-<sid>.jsonl one line per Edit call: {ts, file, symbol[, sigs]}
11
11
  // .cg-stop-<cwdHash>-<sid>.json written by the Stop hook only:
12
- // {lastStopAt, reported: ["file#symbol", …]}
12
+ // {lastStopAt, reported: ["file#symbol", …],
13
+ // pending?: {at, files: […]}} — the last
14
+ // report's caller files, until its follow-up
13
15
  //
14
16
  // The edit log is APPEND-only: parallel Edit calls each run their own hook
15
17
  // process, and a read-modify-write JSON file would drop one of two concurrent
@@ -108,10 +110,15 @@ function readStopState(root, sessionId) {
108
110
  if (!p) return empty;
109
111
  try {
110
112
  const s = JSON.parse(fs.readFileSync(p, 'utf8'));
111
- return {
113
+ const out = {
112
114
  lastStopAt: typeof s.lastStopAt === 'number' ? s.lastStopAt : null,
113
115
  reported: Array.isArray(s.reported) ? s.reported.filter((x) => typeof x === 'string') : [],
114
116
  };
117
+ const p2 = s.pending;
118
+ if (p2 && typeof p2.at === 'number' && Array.isArray(p2.files)) {
119
+ out.pending = { at: p2.at, files: p2.files.filter((x) => typeof x === 'string') };
120
+ }
121
+ return out;
115
122
  } catch { return empty; }
116
123
  }
117
124
 
@@ -291,6 +291,7 @@ function computeStopReport({ edits, state, now, workText, callers, mtimeMs }) {
291
291
  }
292
292
 
293
293
  const lines = [];
294
+ const listed = new Set();
294
295
  for (const c of changed.slice(0, MAX_SYMBOLS_CHECKED)) {
295
296
  const refs = callers(c.symbol, c.file);
296
297
  if (!refs || refs.length === 0) continue;
@@ -318,6 +319,7 @@ function computeStopReport({ edits, state, now, workText, callers, mtimeMs }) {
318
319
  }
319
320
  lines.push(line);
320
321
  next.reported.push(c.key);
322
+ for (const r of untouched) listed.add(r.file);
321
323
  }
322
324
  // Changed beyond the query cap: named, not dropped in silence (review M1).
323
325
  // Not marked reported, and a later turn does not see them as changed again
@@ -327,7 +329,54 @@ function computeStopReport({ edits, state, now, workText, callers, mtimeMs }) {
327
329
  lines.push(' ' + unchecked.length + ' more changed, callers not checked: ' +
328
330
  unchecked.map((c) => shellQuoteArg(c.symbol) + '() in ' + shellQuoteArg(c.file)).join(', '));
329
331
  }
330
- return { lines, state: next };
332
+ // `symbols` / `files`: what the report names, for its telemetry and its
333
+ // follow-up (D#163). Not part of `state`.
334
+ return { lines, state: next, symbols: next.reported.length - state.reported.length, files: [...listed].sort() };
335
+ }
336
+
337
+ // The follow-up remembers at most this many of a report's caller files.
338
+ const MAX_FOLLOWUP_FILES = 64;
339
+
340
+ /**
341
+ * What a report leaves behind (D#163): the `stop_check` record — for every
342
+ * report the model is shown, the one made only of the "more changed, callers
343
+ * not checked" line included — and the follow-up to judge at the next Stop,
344
+ * only when the report named caller files. `callers` counts every caller file
345
+ * the report found, the ones past the text's per-symbol cap included.
346
+ * @param {{lines:string[], state:{lastStopAt:number}, symbols?:number, files?:string[]}} report
347
+ * @returns {{check: object|null, pending: {at:number, files:string[]}|null}}
348
+ */
349
+ function reportRecords(report) {
350
+ if (!report.lines.length) return { check: null, pending: null };
351
+ const files = report.files || [];
352
+ return {
353
+ check: { hook: 'stop', action: 'stop_check', symbols: report.symbols || 0, callers: files.length },
354
+ pending: files.length ? { at: report.state.lastStopAt, files: files.slice(0, MAX_FOLLOWUP_FILES) } : null,
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Whether a report was acted on: a caller file it listed was edited after it —
360
+ * an Edit logged since `pending.at`, or an mtime at or after it (Write, `sed
361
+ * -i`, formatters; the same reading as "touched this turn"). Judged at the
362
+ * next Stop, which is the end of the continuation the feedback starts.
363
+ * @param {{at:number, files:string[]}} pending left in the Stop state by the report
364
+ * @param {{ts:number, file:string}[]} edits the session's edit log
365
+ * @param {(file:string)=>number|null} mtimeMs
366
+ * @returns {{adopted:boolean, listed:number, edited:number}}
367
+ */
368
+ function followUpOf(pending, edits, mtimeMs) {
369
+ const listed = new Set(pending.files);
370
+ const edited = new Set();
371
+ for (const e of edits) {
372
+ if (e.ts >= pending.at && listed.has(e.file)) edited.add(e.file);
373
+ }
374
+ for (const f of listed) {
375
+ if (edited.has(f)) continue;
376
+ const m = mtimeMs(f);
377
+ if (m !== null && m >= pending.at) edited.add(f);
378
+ }
379
+ return { adopted: edited.size > 0, listed: listed.size, edited: edited.size };
331
380
  }
332
381
 
333
382
  function formatStopContext(lines) {
@@ -350,17 +399,32 @@ function readStdinJson() {
350
399
  function runMain() {
351
400
  if (process.env.CODE_GRAPH_QUIET_HOOKS === '1') return;
352
401
  const input = readStdinJson();
353
- if (!input || input.stop_hook_active === true) return;
354
- if (!input.session_id) return;
402
+ if (!input || !input.session_id) return;
355
403
 
356
404
  const { resolveProjectRoot } = require('./project-root');
357
405
  const root = resolveProjectRoot(typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd());
358
406
  if (root === null) return;
359
407
 
360
408
  const sessionEdits = require('./session-edits');
409
+ const { recordRecommendation } = require('./recommendation-log');
410
+ const mtimeMs = (file) => {
411
+ try { return fs.statSync(path.join(root, file)).mtimeMs; } catch { return null; }
412
+ };
413
+ const state = sessionEdits.readStopState(root, input.session_id);
414
+ // The previous report's follow-up (D#163), at every Stop — the one ending
415
+ // the continuation that report started included. It only records: nothing
416
+ // is emitted, so a continuation cannot loop through it.
417
+ if (state.pending) {
418
+ const f = followUpOf(state.pending, sessionEdits.readEdits(root, input.session_id), mtimeMs);
419
+ recordRecommendation(root, { hook: 'stop', action: 'stop_followup', ...f });
420
+ delete state.pending;
421
+ sessionEdits.writeStopState(root, input.session_id, state);
422
+ }
423
+ // A continuation caused by a Stop hook is never re-checked.
424
+ if (input.stop_hook_active === true) return;
425
+
361
426
  const edits = sessionEdits.readEdits(root, input.session_id);
362
427
  if (edits.length === 0) return; // no log → nothing to check, and nothing written
363
- const state = sessionEdits.readStopState(root, input.session_id);
364
428
 
365
429
  const { remainingMs } = require('./hook-fail-open');
366
430
  const run = (cmd, args, defaultMs) => {
@@ -410,11 +474,12 @@ function runMain() {
410
474
  return { file: r.file_path, name: r.name, line: findCallSiteLine(text, symbol, r.start_line) };
411
475
  });
412
476
  },
413
- mtimeMs: (file) => {
414
- try { return fs.statSync(path.join(root, file)).mtimeMs; } catch { return null; }
415
- },
477
+ mtimeMs,
416
478
  });
417
479
  }
480
+ const rec = reportRecords(report);
481
+ if (rec.check) recordRecommendation(root, rec.check);
482
+ if (rec.pending) report.state.pending = rec.pending;
418
483
  sessionEdits.writeStopState(root, input.session_id, report.state);
419
484
 
420
485
  const text = formatStopContext(report.lines);
@@ -427,5 +492,5 @@ if (require.main === module) runMain();
427
492
 
428
493
  module.exports = {
429
494
  extractSignatures, signatureChanged, findCallSiteLine, computeStopReport, formatStopContext,
430
- MAX_SYMBOLS_CHECKED, MAX_CALLERS_LISTED,
495
+ followUpOf, reportRecords, MAX_SYMBOLS_CHECKED, MAX_CALLERS_LISTED, MAX_FOLLOWUP_FILES,
431
496
  };
@@ -88,6 +88,14 @@ function runMain() {
88
88
  if (!text) return;
89
89
  const { emitEventContext } = require('./hook-emit');
90
90
  process.stdout.write(emitEventContext('SubagentStart', text) + '\n');
91
+ // D#163: how many subagents were handed the facts. Whether they then used
92
+ // the index is in their transcripts, not here (scripts/subagent_share.py).
93
+ // The agent type is harness input; only a plain name is kept.
94
+ const agent = typeof input.agent_type === 'string' && /^[\w:.-]{1,64}$/.test(input.agent_type)
95
+ ? input.agent_type : null;
96
+ require('./recommendation-log').recordRecommendation(root, {
97
+ hook: 'subagent', action: 'subagent_context', ...(agent ? { agent } : {}),
98
+ });
91
99
  }
92
100
 
93
101
  if (require.main === module) runMain();
@@ -35,7 +35,7 @@ jobs:
35
35
  node-version: '20'
36
36
  - name: Build snapshot
37
37
  run: |
38
- npx -y -p @sdsrs/code-graph@0.162.0 code-graph-mcp snapshot create --out snapshot.db
38
+ npx -y -p @sdsrs/code-graph@0.163.0 code-graph-mcp snapshot create --out snapshot.db
39
39
  zstd -9 snapshot.db -o snapshot.db.zst
40
40
  mv snapshot.db.zst "code-graph-snapshot-${GITHUB_SHA:0:7}.db.zst"
41
41
  - name: Upload to release
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.162.0",
3
+ "version": "0.163.0",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "node": ">=16"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@sdsrs/code-graph-linux-x64": "0.162.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.162.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.162.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.162.0",
42
- "@sdsrs/code-graph-win32-x64": "0.162.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.163.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.163.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.163.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.163.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.163.0"
43
43
  }
44
44
  }