---
title: "Graph-Assisted Refactoring: Turning Relational Clues into Checks"
description: "Turn note and code graph relations into verifiable fault checks: trace candidate edges, check dependencies, and avoid treating links as proof."
image: "https://zengliangyi.com/og/blog/en/graph-assisted-refactoring.png"
---

Knowledge relations can supply investigation clues before a refactor: nearby notes suggest checking callers, state, and persistence boundaries, then returning to source and fault tests. An edge is neither a call graph nor proof of a cause. The post-merge incident described in the original, its search scores, and colleague feedback lack reviewable evidence, so this article turns them into an explicit synthetic exercise.

## Separate three graphs and scores

[server/src/services/relations.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/relations.ts) takes semantic candidates or recent notes and asks a model for bounded relation types, descriptions, and confidence. It retains candidate targets with confidence at least 0.5, up to five edges. Manual relations store confidence 1. These values are suggestions, not calibrated causal probabilities; a manual 1 does not verify a dependency.

[server/src/routes/relations.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/routes/relations.ts) exposes both note-relation and tag-co-occurrence projections. Note projections select a visible subgraph by project, relation type, confidence, and a node limit, and may be truncated. Tag scores divide shared-note count by the square root of the product of the two tag note counts. The current [client/src/pages/RelationGraph.tsx](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/pages/RelationGraph.tsx) requests a level=tag graph with limit=120, then lists notes when a tag is clicked. The original centered Embedding-note view is not this page’s current default.

| Clue | Current meaning | What it does not establish |

| --- | --- | --- |

| Search score | Vector candidates ranked with lexical matching, thresholds, and other logic | Failure probability or comparable confidence across queries |

| Relation confidence | Model suggestion or manually set relation field | Verified calls, state writes, or a causal chain |

| Tag co-occurrence score | Normalized tag co-occurrence in notes | Code modules or dependency direction |

## Expansion is an investigation entry point

[server/src/services/embedding.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/embedding.ts) expands direct hits through one-hop note relations with confidence>=0.5. Linked notes must also have embedding_status=done. Their score multiplies the direct score, 0.7, and confidence. A note already present is not updated through a later path, and the final set may exceed topK. The 0.5 boundary is inclusive; ordinary expansion does not necessarily exclude that edge. See [vector-query internals](/en/blog/vectra-search-internals/) for the same implementation.

The current CLI search has no –expand option. After confirming the connection and authentication, use expand=true through the REST contract. This only illustrates the interface and was not run against real models or data:



```bash
curl   --fail-with-body   --get   "http://localhost:3721/api/search"     --data-urlencode   "q=embedding search regression"     --data-urlencode   "expand=true"     --data-urlencode   "limit=5"
```



For cloud use, choose the reviewed HTTPS target and its authentication; the local tokenless form is not a cloud deployment plan. The [REST search mapping](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/routes/notes.ts) returns note_id, conversation_id, and via_relation; the internal service field is named viaRelation. Follow note IDs to source content, relation descriptions, and created_by before selecting the next source check. via_relation is not a root-cause conclusion.

## Audit the original explanation through control flow

The original sketch awaits Promise.all before beginUpdate. If that promise rejects, execution never opens an index update; an assignment to done after a throw also does not execute automatically. The same fragment cannot establish a model-request failure, a committed half-batch, and a done state together. Promise.all rejection does not cancel requests already started either.

Current [server/src/services/embedding.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/embedding.ts) obtains vectors sequentially before opening an index update. It inserts new items, swaps mappings and sets syncing in a SQL transaction, saves the database after the transaction, deletes old index items, calls endUpdate, then marks done and saves. The catch has two independent conditions: updateOpen first triggers an attempt to cancelUpdate; dbSwapped then triggers failed status and a save. Both can run during the same failure. Retries also check committed syncing items. This is not the original proposed partial-state scheme or a fully atomic transaction spanning SQLite and index files.

cancelQueue in [server/src/queue/index.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/queue/index.ts) clears pending jobs and marks queued tasks failed. It does not interrupt the active job. Mid-run cancellation, process termination, and index-commit failure require separate real fault entry points; clearing pending jobs is not their substitute.

## A synthetic investigation and ordering exercise

Save this as refactor.mjs under Node 24 and run `node refactor.mjs`. It collects questions through two synthetic note hops, then simulates request-stage and insertion-stage failures through a call trace. It does not run ChatCrystal search, the graph page, generateEmbeddings, a Provider, Vectra, or SQL. Its simulated cancel does not prove rollback of real disk side effects.



```js
import   assert   from   'node:assert/strict'  ;  
  
  // Synthetic note links identify questions, never prove code dependencies.  
  const   edges   =   [  
    { from:   'embedding'  , to:   'queue'  , type:   'SIMILAR_TO'  , confidence:   0.8   },  
    { from:   'queue'  , to:   'index'  , type:   'REFERENCES'  , confidence:   0.5   },  
    { from:   'queue'  , to:   'unrelated'  , type:   'SIMILAR_TO'  , confidence:   0.49   },  
  ];  
  function   candidates  (  start  ,   maxDepth   =   2  ) {  
    const   seen   =   new   Map  ([[start,   0  ]]);  
    let   frontier   =   [start];  
    for   (  let   depth   =   1  ; depth   <=   maxDepth; depth  ++  ) {  
      const   next   =   [];  
      for   (  const   node   of   frontier) {  
        for   (  const   edge   of   edges) {  
          if   (edge.confidence   <   0.5  )   continue  ;  
          const   neighbor   =   edge.from   ===   node   ?   edge.to   :   edge.to   ===   node   ?   edge.from   :   null  ;  
          if   (  !  neighbor   ||   seen.  has  (neighbor))   continue  ;  
          seen.  set  (neighbor, depth);  
          next.  push  (neighbor);  
        }  
      }  
      frontier   =   next;  
    }  
    return   seen;  
  }  
  const   clues   =   candidates  (  'embedding'  );  
  assert.  equal  (clues.  get  (  'index'  ),   2  );  
  assert.  equal  (clues.  has  (  'unrelated'  ),   false  );  
  
  async   function   exercise  (  fault  ) {  
    const   calls   =   [];  
    let   done   =   false  ;  
    const   embed   =   async   (  i  )   =>   {  
      calls.  push  (  `embed:${  i  }`  );  
      if   (fault   ===   'embed'   &&   i   ===   1  )   throw   new   Error  (  'embed failed'  );  
      return   [i,   1  ];  
    };  
    try   {  
      const   vectors   =   await   Promise  .  all  ([  0  ,   1  ].  map  (embed));  
      calls.  push  (  'beginUpdate'  );  
      try   {  
        for   (  let   i   =   0  ; i   <   vectors.  length  ; i  ++  ) {  
          calls.  push  (  `insert:${  i  }`  );  
          if   (fault   ===   'insert'   &&   i   ===   1  )   throw   new   Error  (  'insert failed'  );  
        }  
        calls.  push  (  'endUpdate'  );  
        done   =   true  ;  
      }   catch   (error) {  
        calls.  push  (  'cancelUpdate'  );  
        throw   error;  
      }  
    }   catch   (error) {  
      return   { calls, done, error: error.message };  
    }  
    return   { calls, done, error:   null   };  
  }  
  const   beforeBegin   =   await   exercise  (  'embed'  );  
  assert.  equal  (beforeBegin.done,   false  );  
  assert.  equal  (beforeBegin.calls.  includes  (  'beginUpdate'  ),   false  );  
  assert.  equal  (beforeBegin.calls.  includes  (  'endUpdate'  ),   false  );  
  const   duringInsert   =   await   exercise  (  'insert'  );  
  assert.  equal  (duringInsert.done,   false  );  
  assert.  equal  (duringInsert.calls.  includes  (  'cancelUpdate'  ),   true  );  
  assert.  equal  (duringInsert.calls.  includes  (  'endUpdate'  ),   false  );  
  const   healthy   =   await   exercise  (  'none'  );  
  assert.  equal  (healthy.done,   true  );  
  assert.  equal  (healthy.calls.  filter  ((  x  )   =>   x   ===   'endUpdate'  ).  length  ,   1  );  
  console.  log  (  
    'two-hop clues and simulated failure ordering pass; no product graph or index executes'  
  );
```



## Turn clues into a reviewable refactor checklist

Change the `confidence<0.5` exclusion to `<=0.5`: the second-hop assertion must fail. Replace cancelUpdate with endUpdate in the insertion-failure catch: the failure trace assertion must fail. Restore and rerun. These check this exercise’s edge selection and ordering, not a real call chain.

For the actual refactor, list whether callers await work, when state is saved, when old mappings are invalidated, what index-commit failure leaves behind, how reopening establishes searchability, how repeated requests behave, and which layer cancellation affects. Verify each with fixed fixtures and fault injection, then record results and the source version in notes. Relations propose questions; counterexamples, control flow, and recovery evidence establish causes. Continue with [summary queues](/en/blog/pqueue-task-queues/) and [index recovery](/en/blog/vectra-index-recovery/).

Share:

##  Related Posts

[##  Note Relations and Graph Views: Discover, Project, and Verify

Trace ChatCrystal relation APIs, candidate discovery, and graph projections, keeping generated edges and tag co-occurrence within verified boundaries.          曾亮懿       October 3, 2026      3 min read

#ChatCrystal

#Knowledge graph

#Relations](/en/blog/note-relations-and-graph)    [##  Semantic Search and Context: Finding Hidden Code Dependencies

Combine vector search with one-hop graph relation expansion in code refactoring, keeping similarity scores, candidates, and sources verifiable on disk.          曾亮懿       October 3, 2026      3 min read

#ChatCrystal

#Semantic search

#Knowledge graph](/en/blog/semantic-search-relation-context)    [##  AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-format-divergence)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Graph-Assisted Refactoring: Turning Relational Clues into Checks",
  "description": "Turn note and code graph relations into verifiable fault checks: trace candidate edges, check dependencies, and avoid treating links as proof.",
  "url": "https://zengliangyi.com/en/blog/graph-assisted-refactoring/",
  "image": "https://zengliangyi.com/images/blog/graph-assisted-refactoring.svg",
  "datePublished": "2026-10-07T00:00:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/graph-assisted-refactoring/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Graph-Assisted Refactoring: Turning Relational Clues into Checks",
      "item": "https://zengliangyi.com/en/blog/graph-assisted-refactoring/"
    }
  ]
}
```
