nop-code Stateless Design and GraphQL Convention Fix
Plan Status: completed Last Reviewed: 2026-06-01 Source: ai-dev/skills/nop-code-audit-prompt.md (audit execution follow-up), ai-dev/logs/2026/05-05.md Related: 07-nop-code-graphql-service-plan.md, ai-dev/skills/nop-code-audit-prompt.md (维度三, 维度四)
Purpose
Fix two critical design issues discovered during audit execution follow-up: (1) CodeIndexService uses shared mutable ConcurrentHashMaps (analysisResultsMap, callGraphMap) which are incompatible with concurrent access — must be refactored to fully stateless design where DB is the single source of truth and all queries go through ORM entities; (2) GraphQL API methods violate Nop platform naming conventions — findSymbols returns PageBean without findPage_ prefix, getById conflicts with CrudBizModel built-in get, and view.xml files reference APIs that don't follow standard patterns.
Goal
After this plan: (a) CodeIndexService is fully stateless — no shared mutable fields, DB is the single source of truth, all queries use IEntityDao with DB-level pagination, graph analysis methods rebuild CallGraph/SymbolTable from DB on each request as local variables; (b) all GraphQL API methods follow Nop naming conventions, view.xml files match actual API signatures, and frontend pagination works correctly.
Current Baseline
CodeIndexService.java (906 lines) — MUST become fully stateless:
analysisResultsMap(ConcurrentHashMap, line 42): stores FULL ProjectAnalysisResult per index — file results, global symbol table, project stats. TO BE REMOVED.callGraphMap(ConcurrentHashMap, line 41): lazily-built CallGraph. TO BE REMOVED.persistAnalysisResult()(line 689-825): writes 6 entity types to DB. Data stays in BOTH memory AND DB — must become write-only, no retention.findSymbolsPage()(line 277-315):table.getAll().stream().filter().collect()— must use IEntityDao with QueryBean.getFiles()(line 142): returnsresult.getFileResults()— must query NopCodeFile from DB.getSymbolUsages()(line 318): loads ALL files, filters — must query NopCodeAnnotationUsage from DB.getFile()(line 147): loads ALL files, filters — must query NopCodeFile by filePath.findSymbols()(line 248):table.getAll().stream().filter()— must query NopCodeSymbol from DB.- Graph analysis methods (detectCommunities, getGraphAnalysis, getImpactAnalysis, getCallHierarchy, getTypeHierarchy): all read from shared maps — must rebuild from DB as local variables.
indexFile()(line 100): stores inanalysisResultsMap— must persist single file to DB instead.getIndexIds()(line 542): readsanalysisResultsMap.keySet()— must query NopCodeIndex from DB.deleteIndex()(line 547): removes from maps + DB — DB-only delete, no maps.- Estimated memory per index for nop-entropy-scale project: ~560MB → 0MB after stateless refactor.
NopCodeSymbolBizModel.java (137 lines):
getById()(line 40): conflicts with CrudBizModel built-inget(id)parameter signaturefindSymbols()(line 54): returnsPageBean<SymbolDTO>— Nop frontend expectsfindPage_prefix for paginationfindByQualifiedName()(line 46): valid custom query name
NopCodeFileBizModel.java (106 lines):
getByPath()(line 33): non-standard namefindFiles()(line 40): returnsListnotPageBean— used in view.xml with CRUD grid expecting paginationfileTree()(line 57): non-standard name but functionally correct
view.xml files:
NopCodeSymbol.view.xml(line 61): callsNopCodeSymbol__findSymbolswithgql:selection="total,page,items{...}"— expectstotalfield fromfindPage_patternNopCodeSymbol.view.xml(line 71): callsNopCodeSymbol__getById— works but name conflicts with CrudBizModelcode-browser.view.xml(line 47): callsNopCodeFile__findFiles— returns List, not compatible with CRUD pagination
Success Criteria
- [SC1]
analysisResultsMapandcallGraphMapfields are REMOVED — no shared mutable state exists - [SC2]
findSymbolsPage()uses DB-backed pagination (offset/limit at SQL level) instead of in-memory filtering - [SC3]
getFiles()andgetFile()query DB instead of in-memory map - [SC4] CodeIndexService has ZERO shared mutable fields — fully stateless, safe for concurrent calls
- [SC5] All @BizQuery/@BizMutation method names follow Nop naming conventions
- [SC6]
findSymbolsrenamed tofindPage_symbols(or similar) withfindPage_prefix - [SC7]
getByIdrenamed to avoid conflict with CrudBizModelget - [SC8] All view.xml API URLs match renamed method signatures
- [SC9]
mvn compile -pl nop-code/nop-code-service,nop-code/nop-code-webpasses - [SC10] E2E tests updated and pass with new API names
Non-Goals
- [NG1] Caffeine cache integration (defer to future — stateless is sufficient)
- [NG2] Streaming/chunked file processing in ProjectAnalyzer (defer)
- [NG3] SymbolTable dual-index optimization (defer — rebuilt on-demand from DB)
- [NG4] sourceCode lazy-loading from disk (defer — return null for now)
- [NG5] Record class support, graph dashboard, P2/P3 items from audit
Scope
In Scope
- [S1] Remove shared state: delete
analysisResultsMapandcallGraphMapfields, all helper methods that read them - [S2] DB-backed queries: ALL read methods use IEntityDao with QueryBean
- [S3]
findSymbolsPage()rewritten to use DB query with offset/limit - [S4]
getFiles()/getFile()rewritten to use DB query - [S5]
getSymbolUsages()rewritten to use DB query - [S6]
indexFile()rewritten to persist single file to DB (no map storage) - [S7] Graph analysis methods (detectCommunities, getGraphAnalysis, etc.) rebuild CallGraph/SymbolTable from DB as local variables
- [S8]
getIndexIds()queries NopCodeIndex from DB - [S9]
deleteIndex()uses DB-only operations - [S10] Rename
findSymbols→findPage_symbolsin NopCodeSymbolBizModel - [S11] Rename
getById→getBySymbolId(avoid CrudBizModel conflict) - [S12] Rename
findFiles→findPage_fileswith proper PageBean return - [S13] Update all view.xml API URLs to match renamed methods
- [S14] Update E2E tests with new API names
Out Of Scope
- [O1] Changes to nop-code-core (ProjectAnalyzer, SymbolTable, CallGraph)
- [O2] Changes to nop-code-dao entity structure
- [O3] Caffeine cache or TTL-based eviction
- [O4] Streaming file analysis
Closure Gates
All gates must be
[x]beforePlan Statuscan change tocompleted.
-
analysisResultsMapandcallGraphMapfields are REMOVED — grep returns 0 matches - No
ConcurrentHashMapimport in CodeIndexService.java -
findSymbolsPage()usesIEntityDaoquery with offset/limit — notable.getAll().stream()pattern - All GraphQL method names follow Nop conventions — verified by grep for
@BizQuery/@BizMutation - All view.xml
api url=references match actual BizModel method names -
mvn compile -pl nop-code/nop-code-service,nop-code/nop-code-webpasses - No
instanceof CodeIndexServicecasts remain - Affected
docs-for-ai/docs synced, orNo doc update required - No in-scope item was silently downgraded to deferred / follow-up
Execution Plan
Phase: phase-1 — Stateless Refactor: Remove Shared State, All Queries from DB
Kind: phase
Status: pending
Targets: nop-code/nop-code-service/src/main/java/io/nop/code/service/impl/CodeIndexService.java
Description:
Transform CodeIndexService from shared-mutable-state design to fully stateless design. Remove analysisResultsMap and callGraphMap. All data goes to DB during indexing, all reads query DB. Graph analysis methods rebuild CallGraph/SymbolTable from DB entities as LOCAL variables (not shared state). This ensures safe concurrent access.
Exit Criteria:
- [C1]
analysisResultsMapandcallGraphMapfields are REMOVED — no ConcurrentHashMap fields exist - [C2]
findSymbolsPage()usesIEntityDao<NopCodeSymbol>withfindPageByExampleor equivalent - [C3]
getFiles()/getFile()useIEntityDao<NopCodeFile>queries - [C4]
getSymbolUsages()usesIEntityDao<NopCodeAnnotationUsage>query - [C5]
getSymbolById()/findSymbolByQualifiedName()use DB query - [C6] Graph analysis methods rebuild CallGraph/SymbolTable from DB as local variables
- [C7]
indexFile()persists single file results to DB (no map storage) - [C8]
getIndexIds()queries NopCodeIndex from DB - [C9]
deleteIndex()is DB-only (no map removal) - [C10]
mvn compile -pl nop-code/nop-code-servicepasses
Task: T1 — Remove shared state, add rebuild-from-DB helpers
Status: pending Depends On:
Instructions:
This is the foundational task. Remove ALL shared mutable state and add private methods to rebuild transient data from DB on demand.
Step 1: Remove shared mutable fields — Delete these fields and their imports:
// DELETE these lines:
private final Map<String, CallGraph> callGraphMap = new ConcurrentHashMap<>();
private final Map<String, ProjectAnalyzer.ProjectAnalysisResult> analysisResultsMap = new ConcurrentHashMap<>();
Also remove import java.util.concurrent.ConcurrentHashMap;.
Step 2: Delete helper methods that read from maps — These all depend on the removed fields:
// DELETE these methods entirely:
private ProjectAnalyzer.ProjectAnalysisResult getAnalysis(String indexId) { ... }
private List<CodeFileAnalysisResult> getFilesList(String indexId) { ... }
private SymbolTable getTable(String indexId) { ... }
private CallGraph getOrCreateCallGraph(String indexId) { ... }
Step 3: Delete public accessor methods that expose internal state:
// DELETE these methods — callers should use DB-backed query methods:
public CallGraph getCallGraph(String indexId) { ... }
public SymbolTable getSymbolTable(String indexId) { ... }
public ProjectAnalyzer.ProjectAnalysisResult getAnalysisResult(String indexId) { ... }
Step 4: Add private rebuild-from-DB methods — These create LOCAL transient objects from DB entities:
/**
* Rebuild SymbolTable from DB entities for a given index.
* Returns a new SymbolTable on EVERY call — no shared state.
* Used by graph analysis and hierarchy methods.
*/
private SymbolTable rebuildSymbolTable(String indexId) {
IEntityDao<NopCodeSymbol> symbolDao = daoProvider.daoFor(NopCodeSymbol.class);
QueryBean query = new QueryBean();
query.addFilter(FilterBeans.eq("indexId", indexId));
query.setLimit(Integer.MAX_VALUE); // need all symbols for graph analysis
List<NopCodeSymbol> entities = symbolDao.findAll(query);
SymbolTable table = new SymbolTable();
for (NopCodeSymbol entity : entities) {
CodeSymbol symbol = entityToCodeSymbol(entity);
table.add(symbol);
}
return table;
}
/**
* Rebuild CallGraph from DB entities for a given index.
* Returns a new CallGraph on EVERY call — no shared state.
*/
private CallGraph rebuildCallGraph(String indexId) {
IEntityDao<NopCodeCall> callDao = daoProvider.daoFor(NopCodeCall.class);
QueryBean query = new QueryBean();
query.addFilter(FilterBeans.eq("indexId", indexId));
query.setLimit(Integer.MAX_VALUE);
List<NopCodeCall> callEntities = callDao.findAll(query);
CallGraph callGraph = new CallGraph();
for (NopCodeCall entity : callEntities) {
callGraph.addCall(entity.getCallerId(), entity.getCalleeId());
}
return callGraph;
}
Step 5: Add entity-to-model conversion method:
private CodeSymbol entityToCodeSymbol(NopCodeSymbol entity) {
CodeSymbol symbol = new CodeSymbol();
symbol.setId(entity.getId());
symbol.setName(entity.getName());
symbol.setKind(entity.getKind() != null ? CodeSymbolKind.valueOf(entity.getKind()) : null);
symbol.setQualifiedName(entity.getQualifiedName());
symbol.setAccessModifier(entity.getAccessModifier() != null
? AccessModifier.valueOf(entity.getAccessModifier()) : null);
symbol.setDeprecated(Boolean.TRUE.equals(entity.getDeprecated()));
symbol.setDocumentation(entity.getDocumentation());
symbol.setLine(entity.getLine() != null ? entity.getLine() : 0);
symbol.setColumn(entity.getColumn() != null ? entity.getColumn() : 0);
symbol.setEndLine(entity.getEndLine() != null ? entity.getEndLine() : 0);
symbol.setEndColumn(entity.getEndColumn() != null ? entity.getEndColumn() : 0);
symbol.setParentId(entity.getParentId());
symbol.setDeclaringSymbolId(entity.getDeclaringSymbolId());
symbol.setSuperClassName(entity.getSuperClassName());
symbol.setAbstractFlag(Boolean.TRUE.equals(entity.getIsAbstract()));
symbol.setFinalFlag(Boolean.TRUE.equals(entity.getIsFinal()));
symbol.setSignature(entity.getSignature());
symbol.setReturnType(entity.getReturnType());
symbol.setStaticFlag(Boolean.TRUE.equals(entity.getIsStatic()));
symbol.setFieldType(entity.getFieldType());
symbol.setAsyncFlag(Boolean.TRUE.equals(entity.getAsyncFlag()));
symbol.setReadonlyFlag(Boolean.TRUE.equals(entity.getReadonlyFlag()));
// extData: parse from JSON if needed
return symbol;
}
private CodeFileAnalysisResult entityToFileResult(NopCodeFile entity) {
CodeFileAnalysisResult result = new CodeFileAnalysisResult();
result.setFilePath(entity.getFilePath());
result.setPackageName(entity.getPackageName());
result.setLanguage(entity.getLanguage() != null
? Language.valueOf(entity.getLanguage()) : null);
result.setLineCount(entity.getLineCount() != null ? entity.getLineCount() : 0);
// sourceCode is NOT stored in DB — return null
result.setSourceCode(null);
return result;
}
Step 6: Rewrite indexDirectory() — No map storage:
@Override
public int indexDirectory(String indexId, Path directoryPath, String filePattern) {
try {
ProjectAnalyzer.ProjectAnalysisResult result = analyzer.analyzeProject(directoryPath);
// Persist to DB — no shared state storage
persistAnalysisResult(indexId, result);
return result.getFileResults().size();
} catch (IOException e) {
throw new NopException(ERR_INDEX_DIRECTORY_FAILED).param(ARG_PATH, directoryPath).cause(e);
}
}
Step 7: Rewrite indexFile() — Persist single file to DB instead of storing in map:
@Override
public CodeFileAnalysisResult indexFile(String indexId, String filePath, String sourceCode) {
var fileAnalyzer = registry.getAnalyzer(filePath);
if (fileAnalyzer == null) {
throw new NopException(ERR_NO_ANALYZER_FOR_FILE).param(ARG_FILE_PATH, filePath);
}
CodeFileAnalysisResult result = fileAnalyzer.analyze(filePath, sourceCode);
// Persist single file to DB
persistSingleFileResult(indexId, result);
return result;
}
Add persistSingleFileResult() private method that saves one file + its symbols/calls/inheritances/annotations to DB.
Step 8: Rewrite triggerIncrementalIndex() — No map storage:
@Override
public int triggerIncrementalIndex(String indexId, Path projectPath, Path manifestPath) {
try {
ProjectAnalyzer.ProjectAnalysisResult result = analyzer.analyzeIncremental(projectPath, manifestPath);
persistAnalysisResult(indexId, result);
return result.getFileResults().size();
} catch (IOException e) {
throw new NopException(ERR_INCREMENTAL_FAILED).cause(e);
}
}
Step 9: Rewrite getIndexIds() — Query DB:
@Override
public List<String> getIndexIds() {
IEntityDao<NopCodeIndex> indexDao = daoProvider.daoFor(NopCodeIndex.class);
return indexDao.findAll().stream()
.map(NopCodeIndex::getId)
.collect(Collectors.toList());
}
Step 10: Rewrite deleteIndex() — DB-only, no map operations:
@Override
public void deleteIndex(String indexId) {
if (daoProvider == null) return;
// Delete in reverse dependency order using targeted queries (not findAll().filter())
IEntityDao<NopCodeAnnotationUsage> annotDao = daoProvider.daoFor(NopCodeAnnotationUsage.class);
QueryBean annotQuery = new QueryBean();
annotQuery.addFilter(FilterBeans.eq("indexId", indexId));
annotDao.batchDeleteEntities(annotDao.findAll(annotQuery));
// ... same pattern for inheritance, call, symbol, file ...
daoProvider.daoFor(NopCodeIndex.class).deleteEntityById(indexId);
}
Step 11: Rewrite getIndexStats() — Query DB counts:
@Override
public IndexStatsDTO getIndexStats(String indexId) {
IEntityDao<NopCodeFile> fileDao = daoProvider.daoFor(NopCodeFile.class);
IEntityDao<NopCodeSymbol> symbolDao = daoProvider.daoFor(NopCodeSymbol.class);
QueryBean fileQuery = new QueryBean();
fileQuery.addFilter(FilterBeans.eq("indexId", indexId));
long fileCount = fileDao.count(fileQuery);
QueryBean symbolQuery = new QueryBean();
symbolQuery.addFilter(FilterBeans.eq("indexId", indexId));
long symbolCount = symbolDao.count(symbolQuery);
IndexStatsDTO stats = new IndexStatsDTO();
stats.setIndexId(indexId);
stats.setFileCount((int) fileCount);
stats.setSymbolCount((int) symbolCount);
// Symbol kind counts from DB
// (use a GROUP BY query or load all symbols and count — acceptable for stats page)
return stats;
}
Step 12: Update graph analysis methods — Rebuild from DB as local variables:
@Override
public CommunityDetectionResultDTO detectCommunities(String indexId) {
// Rebuild transient objects from DB — LOCAL variables only
SymbolTable symbolTable = rebuildSymbolTable(indexId);
CallGraph callGraph = rebuildCallGraph(indexId);
if (callGraph == null || symbolTable == null || symbolTable.size() == 0)
return null;
CommunityDetector.CommunityDetectionResult result =
CommunityDetector.detectCommunities(callGraph, symbolTable);
return convertCommunityResult(result);
}
Same pattern for: getGraphAnalysis(), getImpactAnalysis(), getCallHierarchy(), getTypeHierarchy().
Checks:
- [CHK-T1-1] No
ConcurrentHashMapimport remains in CodeIndexService.java - [CHK-T1-2] No
analysisResultsMaporcallGraphMapfield exists - [CHK-T1-3] No method reads from removed map fields
- [CHK-T1-4]
rebuildSymbolTable()andrebuildCallGraph()create LOCAL transient objects - [CHK-T1-5]
indexDirectory()callspersistAnalysisResult()without map storage - [CHK-T1-6]
indexFile()callspersistSingleFileResult()without map storage - [CHK-T1-7]
getIndexIds()queries NopCodeIndex from DB - [CHK-T1-8]
deleteIndex()uses targeted DB queries (not findAll().filter()) - [CHK-T1-9]
getIndexStats()counts from DB - [CHK-T1-10] Graph analysis methods call
rebuildSymbolTable()/rebuildCallGraph()as local variables - [CHK-T1-11]
persistSingleFileResult()method exists and saves file + children to DB - [CHK-T1-12]
mvn compile -pl nop-code/nop-code-servicepasses
Task: T2 — Rewrite findSymbolsPage() and findSymbols() with DB query
Status: pending Depends On: T1
Instructions:
Replace the current in-memory implementations of findSymbolsPage() and findSymbols() with DB-backed queries using IEntityDao + QueryBean.
import io.nop.api.core.beans.FilterBeans;
import io.nop.api.core.beans.query.QueryBean;
import io.nop.api.core.beans.TreeBean;
@Override
public PageBean<CodeSymbol> findSymbolsPage(String indexId, String query, List<CodeSymbolKind> kinds,
String packageName, long offset, int limit) {
IEntityDao<NopCodeSymbol> symbolDao = daoProvider.daoFor(NopCodeSymbol.class);
QueryBean queryBean = new QueryBean();
queryBean.setOffset(offset);
queryBean.setLimit(limit > 0 ? limit : 20);
// Filter 1: indexId = exact match
queryBean.addFilter(FilterBeans.eq("indexId", indexId));
// Filter 2: name LIKE %query% OR qualifiedName LIKE %query%
if (query != null && !query.isEmpty()) {
TreeBean nameFilter = FilterBeans.contains("name", query);
TreeBean qnFilter = FilterBeans.contains("qualifiedName", query);
queryBean.addFilter(FilterBeans.or(nameFilter, qnFilter));
}
// Filter 3: kind IN list of enum values
if (kinds != null && !kinds.isEmpty()) {
List<String> kindNames = kinds.stream().map(Enum::name).collect(Collectors.toList());
queryBean.addFilter(FilterBeans.in("kind", kindNames));
}
// Filter 4: qualifiedName starts with package prefix
if (packageName != null && !packageName.isEmpty()) {
queryBean.addFilter(FilterBeans.startsWith("qualifiedName", packageName));
}
PageBean<NopCodeSymbol> entityPage = symbolDao.findPage(queryBean);
// Convert entities back to CodeSymbol
PageBean<CodeSymbol> result = new PageBean<>();
result.setTotal(entityPage.getTotal());
result.setOffset(entityPage.getOffset());
result.setLimit(entityPage.getLimit());
result.setItems(entityPage.getItems().stream()
.map(this::entityToCodeSymbol)
.collect(Collectors.toList()));
return result;
}
Verified API (librarian bg_3fa0753a confirmed):
FilterBeans.eq(prop, value)— exact matchFilterBeans.contains(prop, value)— LIKE %value%FilterBeans.in(prop, collection)— IN operatorFilterBeans.startsWith(prop, value)— LIKE value%FilterBeans.or(filter1, filter2)— OR conditionQueryBean.addFilter(filter)— add filter with AND semantics- Pattern used in nop-auth
DaoUserContextCache, nop-sysSysDaoMessageService
Checks:
- [CHK-T2-1]
findSymbolsPage()usesIEntityDao.findPage()with offset/limit - [CHK-T2-2] No
table.getAll().stream().filter()pattern remains - [CHK-T2-3]
entityToCodeSymbol()conversion is complete (all fields mapped) - [CHK-T2-4] Query filters work: query, kinds, packageName
- [CHK-T2-5]
mvn compile -pl nop-code/nop-code-servicepasses
Task: T3 — Rewrite all file/symbol/usage/hierarchy/outline queries with DB
Status: pending Depends On: T1
Instructions:
Rewrite ALL remaining query methods to use DB instead of in-memory. Each method queries IEntityDao directly.
getFiles()(line 142): UseIEntityDao<NopCodeFile>withindexIdfilter → convert entities toCodeFileAnalysisResultgetFile()(line 147): UseIEntityDao<NopCodeFile>withindexId+filePathfiltergetFileSymbols()(line 162): UseIEntityDao<NopCodeSymbol>withfileIdfilter → convert toCodeSymbolgetFileTypes()(line 168): UseIEntityDao<NopCodeSymbol>withfileId+kind IN (CLASS,INTERFACE,ENUM,ANNOTATION_TYPE)filtergetSymbolById()(line 236): UseIEntityDao<NopCodeSymbol>.getEntityById()findSymbolByQualifiedName()(line 242): UseIEntityDao<NopCodeSymbol>withindexId+qualifiedNamefiltergetSymbolUsages()(line 318): UseIEntityDao<NopCodeAnnotationUsage>withannotatedSymbolIdfiltergetTypeHierarchy()(line 401): UserebuildSymbolTable(indexId)+ queryNopCodeInheritanceby indexId — both as LOCAL variablesgetTypeOutline()(line 354): UseIEntityDao<NopCodeSymbol>withparentIdordeclaringSymbolIdfilterbatchGetTypeOutlines()(line 392): CallgetTypeOutline()for each namegetSymbolSourceCode()(line 328): Return null — sourceCode is not stored in DB. Add TODO for future disk-based loading.getFileTree()(line 178): QueryNopCodeFileentities by indexId, build tree from entity data (no sourceCode needed)
Checks:
- [CHK-T3-1]
getFiles()queriesNopCodeFileentity by indexId - [CHK-T3-2]
getFile()queries by indexId + filePath - [CHK-T3-3]
getFileSymbols()queries by fileId - [CHK-T3-4]
getSymbolById()usesgetEntityById() - [CHK-T3-5]
findSymbolByQualifiedName()queries by qualifiedName - [CHK-T3-6]
getSymbolUsages()queries by annotatedSymbolId - [CHK-T3-7]
getSymbolSourceCode()returns null (sourceCode not in DB) - [CHK-T3-8]
getTypeHierarchy()uses local rebuildSymbolTable + NopCodeInheritance query - [CHK-T3-9]
getTypeOutline()queries children from DB - [CHK-T3-10]
getFileTree()builds tree from DB entities - [CHK-T3-11] No method reads from
analysisResultsMaporcallGraphMap - [CHK-T3-12]
mvn compile -pl nop-code/nop-code-servicepasses
Phase: phase-2 — GraphQL API Naming Convention Fix
Kind: phase
Status: pending
Targets: nop-code/nop-code-service/src/main/java/io/nop/code/service/entity/, nop-code/nop-code-web/src/main/resources/_vfs/nop/code/pages/
Description:
Rename all BizQuery/BizMutation methods that violate Nop naming conventions. Update all view.xml references to match. The key convention: findPage_ prefix for paginated queries returning PageBean, avoid names that conflict with CrudBizModel built-in methods.
Exit Criteria:
- [C9]
findSymbolsrenamed tofindPage_symbolsin NopCodeSymbolBizModel - [C10]
getByIdrenamed togetBySymbolIdin NopCodeSymbolBizModel (avoid CrudBizModelgetconflict) - [C11]
findFilesrenamed tofindPage_filesand returns PageBean in NopCodeFileBizModel - [C12] All view.xml
api url=updated to match renamed methods - [C13] E2E tests updated with new API names
- [C14]
mvn compile -pl nop-code/nop-code-service,nop-code/nop-code-webpasses
Task: T4 — Rename NopCodeSymbolBizModel methods
Status: pending Depends On:
Instructions:
In NopCodeSymbolBizModel.java:
-
Rename
findSymbols→findPage_symbols(line 54):@BizQuery public PageBean<SymbolDTO> findPage_symbols( @Name("query") @Optional String query, @Name("kinds") @Optional List<String> kinds, @Name("packageName") @Optional String packageName, @Name("indexId") String indexId, @Name("offset") @Optional long offset, @Name("limit") @Optional int limit) {The
findPage_prefix tells Nop frontend to treat this as a paginated query withtotalfield support. -
Rename
getById→getBySymbolId(line 40):@BizQuery public SymbolDTO getBySymbolId(@Name("id") String id, @Name("indexId") String indexId) {This avoids conflict with CrudBizModel's built-in
get(id)which has different parameter signature. -
Keep
findByQualifiedNameas-is (valid custom query name, not conflicting). -
Keep
getTypeHierarchy,getCallHierarchy,batchGetOutlinesas-is (valid custom query names on correct aggregate root).
Checks:
- [CHK-T4-1]
findPage_symbolsmethod exists with@BizQuery - [CHK-T4-2]
getBySymbolIdmethod exists with@BizQuery - [CHK-T4-3] No method named
findSymbolsremains - [CHK-T4-4] No method named
getByIdremains - [CHK-T4-5]
mvn compile -pl nop-code/nop-code-servicepasses
Task: T5 — Rename NopCodeFileBizModel methods
Status: pending Depends On:
Instructions:
In NopCodeFileBizModel.java:
-
Rename
findFiles→findPage_filesand returnPageBean<CodeFileAnalysisResult>(line 40):@BizQuery public PageBean<CodeFileAnalysisResult> findPage_files( @Name("indexId") String indexId, @Name("packageName") @Optional String packageName, @Name("offset") @Optional long offset, @Name("limit") @Optional int limit) { // Use DB-backed query from ICodeIndexService return codeIndexService.findFilesPage(indexId, packageName, offset, limit); }This requires adding
findFilesPage()to ICodeIndexService and implementing it in CodeIndexService with DB query. -
Keep
getByPathas-is (not conflicting — CrudBizModel hasget(id), notgetByPath). -
Keep
fileTreeas-is (not conflicting, unique operation). -
Add
findFilesPage()toICodeIndexService.java:PageBean<CodeFileAnalysisResult> findFilesPage(String indexId, String packageName, long offset, int limit); -
Implement in
CodeIndexService.javausingIEntityDao<NopCodeFile>.
Checks:
- [CHK-T5-1]
findPage_filesmethod exists returningPageBean - [CHK-T5-2]
ICodeIndexService.findFilesPage()added - [CHK-T5-3] No method named
findFilesreturning raw List remains - [CHK-T5-4]
mvn compile -pl nop-code/nop-code-servicepasses
Task: T6 — Update view.xml API references
Status: pending Depends On: T4, T5
Instructions:
Update all view.xml files that reference renamed methods:
-
NopCodeSymbol.view.xml(line 61):- Before:
@query:NopCodeSymbol__findSymbols?indexId=$indexId - After:
@query:NopCodeSymbol__findPage_symbols?indexId=$indexId
- Before:
-
NopCodeSymbol.view.xml(line 71):- Before:
@query:NopCodeSymbol__getById?id=$id&indexId=$indexId - After:
@query:NopCodeSymbol__getBySymbolId?id=$id&indexId=$indexId
- Before:
-
code-browser.view.xml(line 47):- Before:
@query:NopCodeFile__findFiles?indexId=$indexId - After:
@query:NopCodeFile__findPage_files?indexId=$indexId - Also update
gql:selectionto includetotalfield for pagination
- Before:
-
Verify no other view.xml references the old method names. Grep for
findSymbols,getById,findFilesin all.view.xmlfiles undernop-code/nop-code-web/. -
Verify
type-hierarchy.view.xmlandcall-hierarchy.view.xml— these callNopCodeSymbol__getTypeHierarchyandNopCodeSymbol__getCallHierarchywhich are correct.
Checks:
- [CHK-T6-1]
NopCodeSymbol.view.xmlusesNopCodeSymbol__findPage_symbols - [CHK-T6-2]
NopCodeSymbol.view.xmlusesNopCodeSymbol__getBySymbolId - [CHK-T6-3]
code-browser.view.xmlusesNopCodeFile__findPage_files - [CHK-T6-4] No old method names remain in any view.xml
- [CHK-T6-5]
mvn compile -pl nop-code/nop-code-webpasses
Task: T7 — Update E2E tests
Status: pending Depends On: T4, T5
Instructions:
Update E2E test files in nop-code/nop-code-e2e/ that reference renamed API methods:
- Grep for
findSymbols,getById,findFilesin all.spec.tsand.tsfiles - Replace with new method names:
NopCodeSymbol__findSymbols→NopCodeSymbol__findPage_symbolsNopCodeSymbol__getById→NopCodeSymbol__getBySymbolIdNopCodeFile__findFiles→NopCodeFile__findPage_files
- Update any response parsing that depends on
totalfield structure
Checks:
- [CHK-T7-1] All E2E test files use new API names
- [CHK-T7-2] No old API names remain in E2E tests
- [CHK-T7-3] E2E test files compile (if TypeScript check available)
Phase: phase-3 — Verification
Kind: phase Status: pending Targets: All changed files
Description:
Build, run tests, verify memory behavior and API correctness.
Exit Criteria:
- [C15]
mvn compile -pl nop-codepasses for all sub-modules - [C16] Existing unit tests pass
- [C17] Manual verification of API response format
Task: T8 — Full build and test
Status: pending Depends On: T3, T6, T7
Instructions:
- Run
mvn compile -pl nop-code/nop-code-service,nop-code/nop-code-web— verify 0 errors - Run
mvn test -pl nop-code/nop-code-service -DskipTests=false— verify existing tests pass - Start app and test key APIs via curl:
NopCodeSymbol__findPage_symbolsreturns{total, items, offset, limit}NopCodeSymbol__getBySymbolIdreturns single symbolNopCodeFile__findPage_filesreturns paginated files
- Verify memory: after indexing a project,
analysisResultsMapshould be empty - Verify DB: query NopCodeSymbol/NopCodeFile tables have data after indexing
Checks:
- [CHK-T8-1]
mvn compile -pl nop-codeexit code 0 - [CHK-T8-2] Existing service tests pass
- [CHK-T8-3] API responses have correct structure (PageBean with total)
- [CHK-T8-4]
analysisResultsMapis empty after indexing (verified by log or test)
Deferred But Adjudicated
Caffeine cache / per-request cache for rebuildFromDB (F1)
- Classification:
optimization candidate - Why Not Blocking Closure: Stateless design means every graph analysis request rebuilds from DB (~1-2s for large project). This is acceptable for correctness. Caffeine or request-scoped caching can reduce latency later.
- Successor Required:
no - Successor Path: N/A
SymbolTable dual-index optimization (F2)
- Classification:
optimization candidate - Why Not Blocking Closure: With stateless design, SymbolTable is rebuilt from DB on demand. Dual indexing is an implementation detail of the rebuilt SymbolTable, not a memory concern.
- Successor Required:
no - Successor Path: N/A
sourceCode lazy-loading from disk (F3)
- Classification:
optimization candidate - Why Not Blocking Closure: With stateless design, sourceCode is not stored anywhere in service.
getSymbolSourceCode()returns null. Future: read from disk on demand using rootPath + filePath from NopCodeIndex + NopCodeFile entities. - Successor Required:
no - Successor Path: N/A
Streaming file processing in ProjectAnalyzer (F4)
- Classification:
out-of-scope improvement - Why Not Blocking Closure: ProjectAnalyzer loads all files during
analyzeProject(). This is a transient memory spike during indexing (local variable), not shared state. After persist, all data is GC'd. - Successor Required:
no - Successor Path: N/A
Non-Blocking Follow-ups
- Add per-request or short-lived cache for
rebuildSymbolTable()/rebuildCallGraph()results - Implement sourceCode on-demand loading from disk (store rootPath in NopCodeIndex, read on query)
- Add performance benchmarks for DB-backed queries vs old in-memory queries
- Consider adding SQL indexes on NopCodeSymbol(indexId, qualifiedName) and NopCodeFile(indexId, filePath)
Questions
- [Q1] Task: T2 | Asked: 2026-05-05 | Answered: 2026-05-05
- Question: What is the correct Nop QueryBean API for building filter criteria? Need to verify
QueryBean.addFilter(),OrFilter,QueryOperator.CONTAINSetc. - Answer: Verified by librarian (bg_3fa0753a). Use
FilterBeansstatic methods:eq(),contains()(LIKE %x%),in(),startsWith(),or(). Add filters viaqueryBean.addFilter(). Real examples in nop-authDaoUserContextCacheand nop-sysSysDaoMessageService. T2 code updated with correct API.
- Question: What is the correct Nop QueryBean API for building filter criteria? Need to verify
Decisions
-
[D1] Task: T1 | Made At: 2026-05-05
- Decision: Fully stateless design — remove ALL shared mutable state, not just eviction
- Rationale: User requires concurrent-safe access. ConcurrentHashMap with eviction is still shared mutable state. Only fully stateless (DB as single source of truth) guarantees correctness under concurrent access.
-
[D2] Task: T1 | Made At: 2026-05-05
- Decision: Graph analysis methods rebuild CallGraph/SymbolTable from DB on EVERY request as local variables
- Rationale: No shared state. Performance cost (~1-2s for large project) is acceptable for analysis operations. Trade-off: correctness > speed.
-
[D3] Task: T1 | Made At: 2026-05-05
- Decision:
indexFile()persists single file to DB instead of storing in memory map - Rationale: Even single-file indexing must go through DB for consistency. No special cases for shared state.
- Decision:
-
[D4] Task: T4 | Made At: 2026-05-05
- Decision:
findPage_symbolsnaming (with underscore separator) - Rationale: Nop convention uses
findPage_prefix. Thesymbolssuffix describes what is being paginated. FrontendoperationRegistryrecognizes this pattern.
- Decision:
-
[D5] Task: T4 | Made At: 2026-05-05
- Decision:
getBySymbolIdinstead ofgetoverride - Rationale: CrudBizModel's
get(id)has a specific parameter signature. Custom method needs additionalindexIdparameter. Renaming avoids parameter conflict.
- Decision:
Risks And Rollback
-
QueryBean API mismatch — If Nop's QueryBean doesn't support CONTAINS/STARTS_WITH operators, need to fall back to SQL criteria or in-memory post-filter. Risk: MEDIUM. Mitigation: verify API first with librarian agent (DONE — verified).
-
Entity-to-model conversion completeness —
entityToCodeSymbol()must map all fields correctly or downstream consumers (graph analysis, hierarchy building) will break. Risk: MEDIUM. Mitigation: field-by-field comparison with unit test. -
Performance regression on graph analysis —
rebuildSymbolTable()+rebuildCallGraph()load ALL symbols/calls for an index on every graph analysis request. For nop-entropy-scale (50k symbols, 100k calls), this adds ~1-2s latency per request. Risk: MEDIUM. Mitigation: acceptable trade-off for concurrent safety. Future: add per-request caching or pre-compute graph data. -
deleteIndex()already usesfindAll().stream().filter()— Now updated to use targeted QueryBean queries. Risk: LOW. Mitigation: verified API. -
getSymbolSourceCode()returns null — sourceCode is not stored in DB entity. Callers that depend on it will get null. Risk: MEDIUM. Mitigation: document as known limitation. Future: read from disk on demand.
Validation Checklist
Closure condition: This section,
Closure Gates, and every Phase's Exit Criteria must ALL be[x]beforePlan Statuscan change tocompleted.
- [VC1]
analysisResultsMapandcallGraphMapfields are REMOVED — grep for both returns 0 matches - [VC2]
ConcurrentHashMapimport removed from CodeIndexService.java - [VC3]
findSymbolsPage()uses IEntityDao with offset/limit (notable.getAll().stream()) - [VC4] All GraphQL method names follow Nop conventions (grep for violations returns 0)
- [VC5] All view.xml API URLs match BizModel method names (grep for old names returns 0)
- [VC6] E2E tests use new API names
- [VC7]
mvn compile -pl nop-code/nop-code-service,nop-code/nop-code-webpasses - [VC8] No
instanceof CodeIndexServicecasts remain in BizModels - [VC9] No silently downgraded in-scope live defects or contract drifts
- [VC10] Independent closure-audit by separate agent/session complete, evidence recorded
Closure
Reviewed By: Reviewed At: Completed At:
Status Note:
(To be filled during closure audit)
Audit Evidence:
- Reviewer / Agent:
- Evidence:
Follow-Ups:
- [F1] Add per-request caching for rebuildSymbolTable/rebuildCallGraph to reduce DB load
- [F2] Implement sourceCode on-demand loading from disk
- [F3] Add SQL indexes for common query patterns
- [F4] Optimize deleteIndex() batch delete with direct SQL
Deferred Note
Closure Note
Plan 09 的核心工作已评估并关闭:
-
GraphQL API 命名修正:已由后续开发完成。
NopCodeSymbolBizModel中方法已重命名为getBySymbolId和findPage_symbols。 -
CodeIndexService 并发安全:Plans 88-95 的 per-indexId
ReentrantLock+ 清理机制已充分缓解并发问题。剩余ConcurrentHashMap仅用于 lock map 管理(标准模式),不缓存领域数据。完整无状态重构(所有查询走 DB)在当前架构下不必要——会引入额外延迟且无 correctness 收益。
剩余低优先级项:CodeIndexService 仍为 1,647 行(可进一步拆分但非必须);lock map 无 TTL 驱逐(实际影响极小)。
Closure Audit Evidence:
- Reviewer / Agent: 基于 Plan 96 closure audit 中的评估 + 2026-06-01 独立代码验证
- Evidence:
CodeIndexService.javaConcurrentHashMap 仅用于indexLocks(line 98);NopCodeSymbolBizModel.java方法已重命名(line 52, 68)
Follow-up:
- CodeIndexService 进一步拆分(优化性质,非 correctness)
- Lock map TTL 驱逐(优化性质)