notex-companion 0.3.3 → 0.4.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.
package/dist/cli.js CHANGED
@@ -6790,6 +6790,36 @@ function readHeadSha(checkoutPath) {
6790
6790
  return null;
6791
6791
  }
6792
6792
  }
6793
+ var SUGGESTED_QUESTIONS_HEADING = /^## Suggested Questions\s*$/;
6794
+ var SECTION_HEADING = /^## /;
6795
+ var QUESTION_BULLET = /^- \*\*(.+)\*\*$/;
6796
+ var RATIONALE_LINE = /^\s*_(.+)_\s*$/;
6797
+ function parseSuggestedQuestions(markdown) {
6798
+ const lines = markdown.split(`
6799
+ `);
6800
+ const headingIndex = lines.findIndex((line) => SUGGESTED_QUESTIONS_HEADING.test(line));
6801
+ if (headingIndex === -1)
6802
+ return [];
6803
+ const sectionEnd = lines.findIndex((line, i) => i > headingIndex && SECTION_HEADING.test(line));
6804
+ const section = lines.slice(headingIndex + 1, sectionEnd === -1 ? undefined : sectionEnd);
6805
+ const result = [];
6806
+ for (let i = 0;i < section.length; i++) {
6807
+ const questionMatch = section[i].match(QUESTION_BULLET);
6808
+ if (!questionMatch)
6809
+ continue;
6810
+ const rationaleMatch = section[i + 1]?.match(RATIONALE_LINE);
6811
+ result.push({ question: questionMatch[1], rationale: rationaleMatch?.[1] ?? "" });
6812
+ }
6813
+ return result;
6814
+ }
6815
+ function loadSuggestedQuestions(checkoutPath) {
6816
+ try {
6817
+ const raw = readFileSync2(resolve(checkoutPath, "graphify-out/GRAPH_REPORT.md"), "utf8");
6818
+ return parseSuggestedQuestions(raw);
6819
+ } catch {
6820
+ return [];
6821
+ }
6822
+ }
6793
6823
  function loadGraph(checkoutPath) {
6794
6824
  const graphPath = resolve(checkoutPath, "graphify-out/graph.json");
6795
6825
  let raw;
@@ -6851,7 +6881,16 @@ function loadGraph(checkoutPath) {
6851
6881
  sourceFile: checkoutRelative(e.source_file),
6852
6882
  sourceLocation: e.source_location
6853
6883
  });
6854
- return { stamp, nodesById, edges: doc.links, adjacency, scoreIndex, project, projectEdge };
6884
+ return {
6885
+ stamp,
6886
+ nodesById,
6887
+ edges: doc.links,
6888
+ adjacency,
6889
+ scoreIndex,
6890
+ suggestedQuestions: loadSuggestedQuestions(checkoutPath),
6891
+ project,
6892
+ projectEdge
6893
+ };
6855
6894
  }
6856
6895
 
6857
6896
  // src/http.ts
@@ -7014,7 +7053,7 @@ function traverse(adjacency, allEdges, seedIds, depth, maxNodes) {
7014
7053
 
7015
7054
  // src/ops.ts
7016
7055
  var API_VERSION = "0.2.1";
7017
- var CAPABILITIES = ["search", "query", "path", "node", "browse"];
7056
+ var CAPABILITIES = ["search", "query", "path", "node", "browse", "suggestedQuestions"];
7018
7057
  var MAX_NODES_CEILING = 1000;
7019
7058
  var MAX_DEPTH_CEILING = 3;
7020
7059
  var DEFAULT_DEPTH = 1;
@@ -7032,6 +7071,9 @@ function status(index) {
7032
7071
  limits: { maxNodes: MAX_NODES_CEILING, maxDepth: MAX_DEPTH_CEILING }
7033
7072
  };
7034
7073
  }
7074
+ function suggestedQuestions(index) {
7075
+ return { graph: index.stamp, questions: index.suggestedQuestions };
7076
+ }
7035
7077
  function search(index, req) {
7036
7078
  const limit = Math.max(0, Math.min(req.limit ?? DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT));
7037
7079
  const scored = scoreNodes(index.scoreIndex, terms(req.q)).slice(0, limit);
@@ -7227,6 +7269,8 @@ async function dispatch(req, url, opts) {
7227
7269
  }
7228
7270
  if (method === "GET" && pathname === "/v1/browse")
7229
7271
  return browse(index, parseBrowseRequest(url.searchParams));
7272
+ if (method === "GET" && pathname === "/v1/suggested-questions")
7273
+ return suggestedQuestions(index);
7230
7274
  throw new OpError("not_found", `No such route: ${method} ${pathname}`);
7231
7275
  }
7232
7276
  function decodeNodeId(raw) {
@@ -33888,6 +33932,18 @@ function createGraphTools(ctx) {
33888
33932
  return toResult(outcome, text);
33889
33933
  }
33890
33934
  },
33935
+ graph_suggested_questions: {
33936
+ description: "Questions graphify's own analysis (GRAPH_REPORT.md) flagged as ones this graph is uniquely positioned to answer, each with a one-line rationale (e.g. high betweenness centrality, a weakly-connected community). Empty when the checkout has no GRAPH_REPORT.md.",
33937
+ inputSchema: {},
33938
+ handler: () => {
33939
+ const outcome = runOp(ctx, (index) => suggestedQuestions(index));
33940
+ if (!outcome.ok)
33941
+ return outcome.error;
33942
+ const { result } = outcome;
33943
+ const text = `${result.questions.length} suggested question(s)`;
33944
+ return toResult(outcome, text);
33945
+ }
33946
+ },
33891
33947
  graph_search: {
33892
33948
  description: "Literal label/path search over the loaded graph. Returns scored nodes.",
33893
33949
  inputSchema: { q: exports_external.string().min(1), limit: exports_external.number().int().min(0).optional() },
package/dist/client.js CHANGED
@@ -196,7 +196,7 @@ function traverse(adjacency, allEdges, seedIds, depth, maxNodes) {
196
196
 
197
197
  // src/ops.ts
198
198
  var API_VERSION = "0.2.1";
199
- var CAPABILITIES = ["search", "query", "path", "node", "browse"];
199
+ var CAPABILITIES = ["search", "query", "path", "node", "browse", "suggestedQuestions"];
200
200
  var MAX_NODES_CEILING = 1000;
201
201
  var MAX_DEPTH_CEILING = 3;
202
202
  var DEFAULT_DEPTH = 1;
@@ -214,6 +214,9 @@ function status(index) {
214
214
  limits: { maxNodes: MAX_NODES_CEILING, maxDepth: MAX_DEPTH_CEILING }
215
215
  };
216
216
  }
217
+ function suggestedQuestions(index) {
218
+ return { graph: index.stamp, questions: index.suggestedQuestions };
219
+ }
217
220
  function search(index, req) {
218
221
  const limit = Math.max(0, Math.min(req.limit ?? DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT));
219
222
  const scored = scoreNodes(index.scoreIndex, terms(req.q)).slice(0, limit);
@@ -361,6 +364,7 @@ function node(index, req) {
361
364
  return { graph: index.stamp, node: index.project(raw), neighbours };
362
365
  }
363
366
  export {
367
+ suggestedQuestions,
364
368
  status,
365
369
  search,
366
370
  query,
package/dist/graph.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ScoreIndexEntry } from "./scoring.js";
2
- import type { GraphEdge, GraphNode, GraphStamp } from "./types.js";
2
+ import type { GraphEdge, GraphNode, GraphStamp, SuggestedQuestion } from "./types.js";
3
3
  type RawNode = {
4
4
  id: string;
5
5
  label: string;
@@ -30,6 +30,7 @@ export type GraphIndex = {
30
30
  /** Undirected — the graph is `"directed": false` (companion-api.md §2.2). */
31
31
  adjacency: Map<string, Array<AdjacencyEntry>>;
32
32
  scoreIndex: Array<ScoreIndexEntry>;
33
+ suggestedQuestions: Array<SuggestedQuestion>;
33
34
  project: (n: RawNode) => GraphNode;
34
35
  projectEdge: (e: RawEdge) => GraphEdge;
35
36
  };
@@ -38,5 +39,16 @@ export declare function rootPrefixFor(checkoutPath: string, graphRoot: string):
38
39
  /** Also used by the MCP binding (mcpTools.ts) to detect staleness at call time — re-reads HEAD,
39
40
  * doesn't cache it, so a commit made mid-session is picked up on the next tool call. */
40
41
  export declare function readHeadSha(checkoutPath: string): string | null;
42
+ /**
43
+ * Parses graphify's own `GRAPH_REPORT.md` "## Suggested Questions" section (a bullet list of
44
+ * bold question / italic one-line rationale pairs) into structured pairs. Pure and best-effort:
45
+ * an absent heading yields `[]`, a bullet that isn't a bold question line is skipped, and a
46
+ * question with no following italic line still gets a `""` rationale rather than being dropped.
47
+ */
48
+ export declare function parseSuggestedQuestions(markdown: string): Array<SuggestedQuestion>;
49
+ /** Best-effort: a missing/unreadable `GRAPH_REPORT.md` (e.g. graphify was never run for
50
+ * suggestions, or only `graph.json` is present) degrades to `[]` rather than failing the
51
+ * whole graph load — suggested questions are a nice-to-have, never load-bearing. */
52
+ export declare function loadSuggestedQuestions(checkoutPath: string): Array<SuggestedQuestion>;
41
53
  export declare function loadGraph(checkoutPath: string): GraphIndex;
42
54
  export {};
package/dist/index.js CHANGED
@@ -196,7 +196,7 @@ function traverse(adjacency, allEdges, seedIds, depth, maxNodes) {
196
196
 
197
197
  // src/ops.ts
198
198
  var API_VERSION = "0.2.1";
199
- var CAPABILITIES = ["search", "query", "path", "node", "browse"];
199
+ var CAPABILITIES = ["search", "query", "path", "node", "browse", "suggestedQuestions"];
200
200
  var MAX_NODES_CEILING = 1000;
201
201
  var MAX_DEPTH_CEILING = 3;
202
202
  var DEFAULT_DEPTH = 1;
@@ -214,6 +214,9 @@ function status(index) {
214
214
  limits: { maxNodes: MAX_NODES_CEILING, maxDepth: MAX_DEPTH_CEILING }
215
215
  };
216
216
  }
217
+ function suggestedQuestions(index) {
218
+ return { graph: index.stamp, questions: index.suggestedQuestions };
219
+ }
217
220
  function search(index, req) {
218
221
  const limit = Math.max(0, Math.min(req.limit ?? DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT));
219
222
  const scored = scoreNodes(index.scoreIndex, terms(req.q)).slice(0, limit);
@@ -387,6 +390,36 @@ function readHeadSha(checkoutPath) {
387
390
  return null;
388
391
  }
389
392
  }
393
+ var SUGGESTED_QUESTIONS_HEADING = /^## Suggested Questions\s*$/;
394
+ var SECTION_HEADING = /^## /;
395
+ var QUESTION_BULLET = /^- \*\*(.+)\*\*$/;
396
+ var RATIONALE_LINE = /^\s*_(.+)_\s*$/;
397
+ function parseSuggestedQuestions(markdown) {
398
+ const lines = markdown.split(`
399
+ `);
400
+ const headingIndex = lines.findIndex((line) => SUGGESTED_QUESTIONS_HEADING.test(line));
401
+ if (headingIndex === -1)
402
+ return [];
403
+ const sectionEnd = lines.findIndex((line, i) => i > headingIndex && SECTION_HEADING.test(line));
404
+ const section = lines.slice(headingIndex + 1, sectionEnd === -1 ? undefined : sectionEnd);
405
+ const result = [];
406
+ for (let i = 0;i < section.length; i++) {
407
+ const questionMatch = section[i].match(QUESTION_BULLET);
408
+ if (!questionMatch)
409
+ continue;
410
+ const rationaleMatch = section[i + 1]?.match(RATIONALE_LINE);
411
+ result.push({ question: questionMatch[1], rationale: rationaleMatch?.[1] ?? "" });
412
+ }
413
+ return result;
414
+ }
415
+ function loadSuggestedQuestions(checkoutPath) {
416
+ try {
417
+ const raw = readFileSync(resolve(checkoutPath, "graphify-out/GRAPH_REPORT.md"), "utf8");
418
+ return parseSuggestedQuestions(raw);
419
+ } catch {
420
+ return [];
421
+ }
422
+ }
390
423
  function loadGraph(checkoutPath) {
391
424
  const graphPath = resolve(checkoutPath, "graphify-out/graph.json");
392
425
  let raw;
@@ -448,7 +481,16 @@ function loadGraph(checkoutPath) {
448
481
  sourceFile: checkoutRelative(e.source_file),
449
482
  sourceLocation: e.source_location
450
483
  });
451
- return { stamp, nodesById, edges: doc.links, adjacency, scoreIndex, project, projectEdge };
484
+ return {
485
+ stamp,
486
+ nodesById,
487
+ edges: doc.links,
488
+ adjacency,
489
+ scoreIndex,
490
+ suggestedQuestions: loadSuggestedQuestions(checkoutPath),
491
+ project,
492
+ projectEdge
493
+ };
452
494
  }
453
495
  // src/http.ts
454
496
  import { timingSafeEqual } from "node:crypto";
@@ -529,6 +571,8 @@ async function dispatch(req, url, opts) {
529
571
  }
530
572
  if (method === "GET" && pathname === "/v1/browse")
531
573
  return browse(index, parseBrowseRequest(url.searchParams));
574
+ if (method === "GET" && pathname === "/v1/suggested-questions")
575
+ return suggestedQuestions(index);
532
576
  throw new OpError("not_found", `No such route: ${method} ${pathname}`);
533
577
  }
534
578
  function decodeNodeId(raw) {
@@ -795,6 +839,7 @@ function serve(opts) {
795
839
  return { server, token, baseUrl, pairingLine: pairingLine(baseUrl, token) };
796
840
  }
797
841
  export {
842
+ suggestedQuestions,
798
843
  status,
799
844
  serve,
800
845
  search,
package/dist/ops.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GraphEdge, GraphNode, OpResponse } from "./types.js";
1
+ import type { GraphEdge, GraphNode, OpResponse, SuggestedQuestion } from "./types.js";
2
2
  import type { GraphIndex } from "./graph.js";
3
3
  /** Also the version /v1/ping reports (companion-api.md §4.1) — the two must never drift apart. */
4
4
  export declare const API_VERSION = "0.2.1";
@@ -14,6 +14,13 @@ export type StatusResult = {
14
14
  };
15
15
  };
16
16
  export declare function status(index: GraphIndex): OpResponse<StatusResult>;
17
+ export type SuggestedQuestionsResult = {
18
+ questions: Array<SuggestedQuestion>;
19
+ };
20
+ /** graphify's own `GRAPH_REPORT.md`-derived suggestions (graph.ts's loadSuggestedQuestions),
21
+ * already loaded onto the index at graph-load time — this op just echoes them alongside the
22
+ * stamp, same shape as every other op. */
23
+ export declare function suggestedQuestions(index: GraphIndex): OpResponse<SuggestedQuestionsResult>;
17
24
  export type SearchRequest = {
18
25
  q: string;
19
26
  limit?: number;
package/dist/types.d.ts CHANGED
@@ -21,6 +21,12 @@ export type GraphNode = {
21
21
  name: string;
22
22
  } | null;
23
23
  };
24
+ /** Parsed from `graphify-out/GRAPH_REPORT.md`'s "## Suggested Questions" section — free-text,
25
+ * not derived from graph.json, so there is no stable id to key it by. */
26
+ export type SuggestedQuestion = {
27
+ question: string;
28
+ rationale: string;
29
+ };
24
30
  export type GraphEdge = {
25
31
  source: string;
26
32
  target: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notex-companion",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Local retrieval companion for Notex — reads a checkout's graphify-out/graph.json and serves deterministic search/query/path/node lookups over loopback HTTP and MCP stdio. No LLM, no graph building, no network beyond 127.0.0.1.",
5
5
  "keywords": ["notex", "graphify", "mcp", "code-graph"],
6
6
  "license": "MIT",